Skip to content

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

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

大多数人会回答,就用邮箱客户端啊,比如 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('body', (stream) => {

            

        });
    });
}

这里用 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('body', (stream) => {

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

            mailparser.on("headers", (headers) => {
                info.theme = headers.get('subject');
                info.form = headers.get('from').value[0].address;
                info.mailName = headers.get('from').value[0].name;
                info.to = headers.get('to').value[0].address;
                info.datatime = headers.get('date').toLocaleString();
            });

            mailparser.on("data", (data) => {
                if (data.type === 'text') {
                    info.html = data.html;
                    info.text = data.text;
                    console.log(info);
                }
                if (data.type === 'attachment') {
                    const filePath = path.join(__dirname, 'files', 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:

npx http-server .

和在邮箱里看一模一样。

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

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

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

收邮件部分的代码如下:

javascript
const { MailParser } =require('mailparser');
const fs = require('fs');
const path = require('path');
const Imap = require('imap');

const 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('body', (stream) => {

            const info = {};
            stream.pipe(mailparser);
            mailparser.on("headers", (headers) => {
                info.theme = headers.get('subject');
                info.form = headers.get('from').value[0].address;
                info.mailName = headers.get('from').value[0].name;
                info.to = headers.get('to').value[0].address;
                info.datatime = headers.get('date').toLocaleString();
            });

            mailparser.on("data", (data) => {
                if (data.type === 'text') {
                    info.html = data.html;
                    info.text = data.text;

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

                    console.log(info);
                }
                if (data.type === 'attachment') {
                    const filePath = path.join(__dirname, 'files', 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 里收发邮件的方式。