作为 Node 程序员,如何收发邮件更显专业?

除了微信外,邮件也是我们常用的通讯方式。

那你平时都是怎么收发邮件的呢?

大多数人会回答,就用邮箱客户端啊,比如 qq 邮箱的:

但是这样体验并不好,比如写邮件的时候:

我有个漂亮的 html 页面,想直接把它作为邮件内容。

或者我想用 markdown 来写邮件。

但是它只支持富文本编辑器:

再比如收邮件的时候,我想把一些重要邮件的内容保存下来,附件啥的都下载到本地。

但是邮件多了的话,一个个手动搞太麻烦了。

有没有什么更好的方式呢?

当然是有的,作为一个专业的 Node 程序员,自然要用代码的方式来收发邮件了!

邮件有专门的协议:

发邮件用 SMTP 协议。

收邮件用 POP3 协议、或者 IMAP 协议。

并且在 node 里也有对应的包,发邮件用 nodemailer 包,收邮件用 imap 包。

我们来试试:

首先,要开启 smtp、imap 等服务,这里以 qq 邮箱举例(其他邮箱也类似):

在邮箱帮助中心 https://service.mail.qq.com/ 可以搜到如何开启 smtp、imap 等服务:

开启后可以在设置里看到:

然后在帮助中心页面搜索授权码:

按照指引生成一个授权码:

这个是 qq 邮箱特有的一个第三方登录密码:

然后就可以开始写代码了:

代码语言:javascript
复制
const nodemailer = require("nodemailer");

const transporter = nodemailer.createTransport({
host: "smtp.qq.com",
port: 587,
secure: false,
auth: {
user: 'xxxxx@qq.com',
pass: '你的授权码'
},
});

async function main() {
const info = await transporter.sendMail({
from: '"guang" <xxxx@qq.com>',
to: "xxxx@xx.com",
subject: "Hello 111",
text: "xxxxx"
});

console.log("邮件发送成功:", info.messageId);
}

main().catch(console.error);

安装 nodemailer 包,然后执行上面的代码:

可以看到邮件发送成功了。

我们在邮箱里看看:

确实收到了这个邮件:

这样我们就用 node 发送了第一个邮件!

而且邮件是支持 html + css 的,比如我们把我之前那个 3 只小鸟的 button 的 html 拿过来:

放到一个文件里,然后发邮件的时候读取这个文件:

然后再跑下:

收到的邮件也渲染出了这个 html,并且 css 动画也是正常的:

那是不是可以加一些 js 呢?

想多了,邮件里可以包含任何 html+ css,但是不支持 js。

不过基于 html + css,我们就已经可以实现各种炫酷的邮件了。

就像前面说的 markdown 格式来写邮件,这个加一个 markdown 转 html 的包,然后作为邮件的 html 内容发送就好了。

也就是说,通过代码的方式,我们可以做出更炫酷的邮件来。

发邮件我们会了,那如何通过 node 来收邮件呢?

收邮件是用 pop3 或者 imap 协议,需要换一个包。

代码语言:javascript
复制
const Imap = require('imap');

const imap = new Imap({
user: 'xxx@qq.com',
password: '你的授权码',
host: 'imap.qq.com',
port: 993,
tls: true
});

imap.once('ready', () => {
imap.openBox('INBOX', true, (err) => {
imap.search([['SEEN'], ['SINCE', new Date('2023-07-10 19:00:00').toLocaleString()]], (err, results) => {
if (!err) {
console.log(results);
} else {
throw err;
}
});
});
});

imap.connect();

安装 imap 的包,然后填入 qq 邮箱的 imap 服务器的域名、端口,填入用户名和授权码,就可以连接了。

这里的 imap 服务器的信息也是在帮助中心里搜索:

search 的参数我们写了两个:

['SEEN'] 是查询已读的邮件。

['SINCE', '某个日期'] 是查询从这个日期以来的邮件。

当然,还有更多的搜索条件,可以看 imap 包的文档。

我们跑下试试:

可以看到打印了搜索出的符合条件的邮件的 id,然后我们来处理下这些 id:

代码语言:javascript
复制
const { MailParser } =require('mailparser');
const fs = require('fs');
const path = require('path');

function handleResults(results) {
imap.fetch(results, {
bodies: '',
}).on('message', (msg) => {
const mailparser = new MailParser();

    msg.on(&#39;body&#39;, (stream) =&gt; {

        

    });
});

}

这里用 imap.fetch 来请求这些 id 的内容,bodies 为 '' 是查询 header + body 的意思:

然后处理下 body 的内容,把结果保存到 info 对象里。

这里解析邮件内容要使用 mailparser 这个包:

代码语言:javascript
复制
const { MailParser } =require('mailparser');
const fs = require('fs');
const path = require('path');
const Imap = require('imap');

function handleResults(results) {
imap.fetch(results, {
bodies: '',
}).on('message', (msg) => {
const mailparser = new MailParser();

    msg.on(&#39;body&#39;, (stream) =&gt; {

        const info = {};
        stream.pipe(mailparser);

        mailparser.on(&#34;headers&#34;, (headers) =&gt; {
            info.theme = headers.get(&#39;subject&#39;);
            info.form = headers.get(&#39;from&#39;).value[0].address;
            info.mailName = headers.get(&#39;from&#39;).value[0].name;
            info.to = headers.get(&#39;to&#39;).value[0].address;
            info.datatime = headers.get(&#39;date&#39;).toLocaleString();
        });

        mailparser.on(&#34;data&#34;, (data) =&gt; {
            if (data.type === &#39;text&#39;) {
                info.html = data.html;
                info.text = data.text;
                console.log(info);
            }
            if (data.type === &#39;attachment&#39;) {
                const filePath = path.join(__dirname, &#39;files&#39;, data.filename);
                const ws = fs.createWriteStream(filePath);
                data.content.pipe(ws);
            }
        });
    });
});

}

这部分还是容易看懂的,就是把 headers 的信息提取出来,把邮件 body 的信息提取出来,放到 info 对象里,打印。

如果有附件,就写到 files 目录下。

我们在本地创建个 files 目录,然后跑一下(下面多了张图片)。

可以看到,我们前面发的那两个邮件都取到了。

日期也确实都是 7 月 10 日的。

我邮箱里有这样一个邮件:

可以看到,附件也下载到了 files 目录下:

我们把 html 的内容保存到本地文件里:

代码语言:javascript
复制
const filePath = path.join(__dirname, 'mails', info.theme + '.html');
fs.writeFileSync(filePath, info.html || info.text)

以邮件主题为文件名。

当然,要现在本地创建 mails 这个目录,然后跑一下:

邮件内容和附件内容都保存了下来:

在邮箱里可以看到也是这些邮件:

我们打开这些 html 看看,起一个 http-server:

代码语言:javascript
复制
npx http-server .

和在邮箱里看一模一样。

这样,我们就把邮件内容和附件都保存了下来。

你想保存一些重要邮件的时候,还需要手动一个个复制和下载附件么?

不需要,用 node 写代码保存不更方便么?

全部代码如下:

代码语言:javascript
复制
const { MailParser } =require('mailparser');
const fs = require('fs');
const path = require('path');
const Imap = require('imap');

const moment = require('moment');

var imap = new Imap({
user: 'xx@qq.com',
password: '你的授权码',
host: 'imap.qq.com',
port: 993,
tls: true
});

imap.once('ready', () => {
imap.openBox('INBOX', true, (err) => {
imap.search([['SEEN'], ['SINCE', new Date('2023-07-10 19:00:00').toLocaleString()]], (err, results) => {
if (!err) {
handleResults(results);
} else {
throw err;
}
});
});
});

function handleResults(results) {
imap.fetch(results, {
bodies: '',
}).on('message', (msg) => {
const mailparser = new MailParser();

    msg.on(&#39;body&#39;, (stream) =&gt; {

        const info = {};
        stream.pipe(mailparser);
        mailparser.on(&#34;headers&#34;, (headers) =&gt; {
            info.theme = headers.get(&#39;subject&#39;);
            info.form = headers.get(&#39;from&#39;).value[0].address;
            info.mailName = headers.get(&#39;from&#39;).value[0].name;
            info.to = headers.get(&#39;to&#39;).value[0].address;
            info.datatime = headers.get(&#39;date&#39;).toLocaleString();
        });

        mailparser.on(&#34;data&#34;, (data) =&gt; {
            if (data.type === &#39;text&#39;) {
                info.html = data.html;
                info.text = data.text;

                const filePath = path.join(__dirname, &#39;mails&#39;, info.theme + &#39;.html&#39;);
                fs.writeFileSync(filePath, info.html || info.text)

                console.log(info);
            }
            if (data.type === &#39;attachment&#39;) {
                const filePath = path.join(__dirname, &#39;files&#39;, data.filename);
                const ws = fs.createWriteStream(filePath);
                data.content.pipe(ws);
            }
        });
    });
});

}

imap.connect();

总结

邮件是常用的通讯方式,我们一般是通过邮箱客户端来收发邮件。

但是这样不够方便:

比如写邮件不能直接贴 html + css,不能写 markdown,收邮件不能按照规则自动下载附件、自动保存邮件内容。

这些需求我们都能通过代码来自己实现。

发邮件是基于 SMTP 协议,收邮件是基于 POP3 或 IMAP 协议。

node 分别有 nodemailer 包和 imap 包用来支持收发邮件的协议。

我们通过 nodemailer 发送了 html 的邮件,可以发送任何 html+css 的内容。

通过 imap 实现了邮件的搜索,然后用 mailparser来做了内容解析,然后把邮件内容和附件做了下载。

能够写代码来收发邮件之后,就可以做很多自动化的事情了:

比如定时自动发一些邮件,内容是从数据库查出来的,比如自动拉取邮件,根据一定的规则来保存邮件和附件内容等。

作为 Node 程序员,日常收发邮件通过代码来做,会显得更加专业。