nodemailer极简源码解析与实现原理
前言
① 本文只解析基于SMTP
协议发送邮件的情况
② 本文的解析基于删减学习版—simple-nodemailer
③ 关于处理email.content
的部分省略
一、使用
这段跟 官网example 一样:
//位置:index.js
const nodemailer=require('./nodemailer')
const config=require('./config')
async function sendEMail(option){
//根据用户名、密码、qq邮箱smtp地址、端口,新建 mailer 实例
//比较简单就是初始化属性
const transporter =nodemailer.createTransport({
host: "smtp.exmail.qq.com",
port: 465,
secure: true,
auth: {
user: config.user,
pass: config.pass,
},
});
const {email,title,date,content}=option
await transporter.sendMail({
from: email, // sender address
to: email, // list of receivers
subject: title, // 标题
html: `<div><div>日期:${date}</div><div>内容:${content}</div></div>`, // html body
});
}
sendEMail({
email:config.user,
title:"看下nodemailer原理",
date:new Date(),
content:'本作男主角,与三笠·阿克曼、爱尔敏·阿诺德是儿时玩伴,拥有强韧的精神力与非凡的行动力,对墙壁外的世界有者比人们都要高的憧憬,从小立志加入调查兵团。在目睹母亲遭巨人吞食后,立誓要驱逐所有巨人。他和儿时玩伴一起受训并认识不少人,以第五名毕业。n'
})
效果图:
二、nodemailer基于SMTP协议的流程
流程
1、创建基于smtp
协议的connection
① 使用DNS协议解析域名,获得ip
② 建立tls
连接
③ 发送greeting request
④ 发送login request
2、发送邮件
① 以rfc2822
标准创建stream
对象—message
② 发送MAIL FROM request
③ 发送RCPT TO request
④ 发送DATA request
,也就是邮件内容,此时就能收到邮件了
流程图
三、transporter.sendMail
nodemailer.createTransport
源码部分是初始化一些value,略过。
源码
//位置:nodemailer.js
sendMail(data, callback) {
let promise;
//初始化 promiseCallback
if (!callback) {
promise = new Promise((resolve, reject) => {
callback = shared.callbackPromise(resolve, reject);
});
}
//this 即 Mailer实例
//根据 发送邮件选项 新建 mailMessage 实例
//data:{ from:'xxx',to:'xxx',subject:'xxx',content:'xxx',headers:{}, },
//message:mimeNode 实例,
let mail = new MailMessage(this, data);
//过程处理
//1.compile
//2.stream
// this._processPlugins('compile', mail, err => {
//新建 mimeNode 实例
mail.message = new MailComposer(mail.data).compile();
// this._processPlugins('stream', mail, err => {
//SMPT 传输实例的send方法
this.transporter.send(mail);
// });
// });
return promise;
}
解析
1、mail
是一个object
,由两部分构成:
{
data:{ from:'xxx',to:'xxx',subject:'xxx',content:'xxx',headers:{}, },
message:mimeNode 实例,
}
2、sendMail()
核心是SMPT instance
的send
方法,流程图中的创建smtp的连接
就是从此方法开始
四、transporter. send
发送邮件的核心函数,按照流程图讲
1、DNS解析域名,获取ip
核心源码
//位置:shared.js
const dns = require('dns');
const resolver = (family, hostname, callback) => {
//使用DNS协议为hostname解析IPv4地址
//dns[resolve4]('smtp.exmail.qq.com',()=>{})
dns['resolve' + family](hostname, (err, addresses) => {
//smtp.exmail.qq.com通过DNS解析有三个ip地址
//addrresses=['113.96.208.92','113.96.232.106','113.96.200.115']
return callback(null, Array.isArray(addresses) ? addresses : [].concat(addresses || []));
});
};
直接调用dns-api
即可
2、建立tls连接
核心源码
//位置:smtp-connection.js
const tls = require('tls');
//tls.connect与https.connect的区别:默认情况下不启用SNI(服务器名称指示)扩展名,这可能导致某些服务器返回不正确的证书或完全拒绝连接
//http://nodejs.cn/api/tls.html#tls_tls_connect_options_callback
//建立tls连接
// opts={host:'113.96.232.106', port:465,servername:'smtp.exmail.qq.com'}
this._socket = tls.connect(opts, () => {
this._socket.setKeepAlive(true)
this._onConnect()
})
调用tls-api
后,执行的_onConnect()
核心源码:
//位置:smtp-connection.js
//当建立与服务器的连接时,运行监听器listener
_onConnect() {
//打开socket的 data listener
this._socket.on('data', this._onSocketData);
}
这个方法很重要,它的作用是用来监听server
发送过来的数据,也就是说,后面server
发送的response
,都能在该方法中获取到
_onSocketData
内部调用了_onData
,看下_onData
源码:
//位置:smtp-connection.js
_onData(chunk) {
if (this._destroyed || !chunk || !chunk.length) {
return;
}
//接收到server的response的情况
//1.建立tls连接成功时 220 smtp.qq.com Esmtp QQ Mail Server
//2.发送gretting问候请求时 250-smtp.qq.com 250-PIPELINING 250-SIZE 73400320 250-AUTH LOGIN PLAIN 250-AUTH=LOGIN 250-MAILCOMPRESS 250 8BITMIME
//3.发送auth登录验证时 235 Authentication successful
//4.发送邮件时 250 Ok: queued as
let data = (chunk || '').toString('binary');
//xxx
}
之后向server
发送请求时,会反复提到这段源码,我们下文均称它为data监听器
tls连接完成
当建立tls
连接成功时,data监听器
会收到如下greeting response
:
220 smtp.qq.com Esmtp QQ Mail Server
3、发送问候请求
在tls
连接成功,并且收到server
的greeting response
后,client
也会发送greeting request
,类似于三次握手的最后一次🤝
核心源码
//位置:smtp-connection.js
_actionGreeting(str) {
clearTimeout(this._greetingTimeout);
this._responseActions.push(this._actionEHLO);
this._sendCommand('EHLO ' + this.name);
}
socket发送request
_sendCommand(str) {
//str:EHLO 获取到的电脑信息
this._socket.write(Buffer.from(str + 'rn', 'utf-8'));
}
server response
data监听器
收到如下回复,也就是smtp.exmail.qq.com
支持的验证方式:
250-smtp.qq.com
250-PIPELINING
250-SIZE 73400320
250-AUTH LOGIN PLAIN
250-AUTH=LOGIN
250-MAILCOMPRESS
250 8BITMIME
nodemailer判断邮箱服务器支持哪些登录方式的函数为:
//位置:smtp-connection.js
//当socket.write发送了问候请求后
//判断server回复的内容里对登录方式的支持
_actionEHLO(str) {
// Detect if the server supports PLAIN auth
if (/[ -]AUTH(?:(s+|=)[^n]*s+|s+|=)PLAIN/i.test(str)) {
this._supportedAuth.push('PLAIN');
}
//xxx省略好多
//发送connect事件,也就是发送用户名、密码
this.emit('connect');
}
4、发送登录请求
核心源码
//位置:smtp-connection.js
//验证用户
login(authData, callback) {
this._auth = authData || {};
// Select SASL authentication method
//_responseActions的执行时机是等到server连接成功,并发送data后,再执行的
this._responseActions.push(str => {
this._actionAUTHComplete(str, callback);
});
//将用户名、密码转为base64并拼接
this._sendCommand(
'AUTH PLAIN ' +
Buffer.from(
//this._auth.user+'u0000'+
//u0000 表示空格也就是 空格+用户名+空格+密码
'u0000' + // skip authorization identity as it causes problems with some servers
this._auth.credentials.user +
'u0000' +
this._auth.credentials.pass,
'utf-8'
).toString('base64')
);
}
socket发送request
_sendCommand(str) {
//str:AUTH PLAIN base64编成的用户名密码
this._socket.write(Buffer.from(str + 'rn', 'utf-8'));
}
server response
data监听器
收到如下回复:
235 Authentication successful
登录成功后,就进入发送邮件阶段
5、以rfc2822标准创建stream对象
rfc2822
用来定义邮件信息的格式,具体的解释请参考 这里
源码
//位置:mime-node.js
//以 rfc2822 标准创建stream对象
//http://blog.chinaunix.net/uid-8532343-id-2029221.html
createReadStream(options) {
options = options || {};
let stream = new PassThrough(options);
let outputStream = stream;
let transform;
this.stream(stream, options, err => {
if (err) {
outputStream.emit('error', err);
return;
}
stream.end();
});
for (let i = 0, len = this._transforms.length; i < len; i++) {
transform = typeof this._transforms[i] === 'function' ? this._transforms[i]() : this._transforms[i];
outputStream.once('error', err => {
transform.emit('error', err);
});
outputStream = outputStream.pipe(transform);
}
// ensure terminating newline after possible user transforms
transform = new LastNewline();
outputStream.once('error', err => {
transform.emit('error', err);
});
outputStream = outputStream.pipe(transform);
// dkim and stuff
for (let i = 0, len = this._processFuncs.length; i < len; i++) {
transform = this._processFuncs[i];
outputStream = transform(outputStream);
}
return outputStream;
}
6、发送MAIL FROM请求
判断邮件的发起者是否可以正常发送
核心源码
//位置:smtp-transport.js
//mail.message即处理过的邮件内容
connection.send(envelope, mail.message.createReadStream(), callback)
//位置:smtp-connection.js
send(envelope, message, done) {
let startTime = Date.now();
this._setEnvelope(envelope, (err, info) => {
//这个callback是发送RCPT TO请求后,发送DATA请求时,执行的callback
if (err) {
return callback(err);
}
let envelopeTime = Date.now();
//创建发送流
let stream = this._createSendStream(callback);
//将发送流导入 可读流ReadStream中
message.pipe(stream);
});
}
socket发送MAIL FROM
//位置:smtp-connection.js
//创建新的message,从 MAIL FROM开始
_setEnvelope(envelope, callback) {
let args = [];
let useSmtpUtf8 = false;
this._envelope = envelope
// clone the recipients array for latter manipulation
// this._envelope.rcptQueue = JSON.parse(JSON.stringify(this._envelope.to || []));
this._envelope.rcptQueue = JSON.parse(JSON.stringify(this._envelope.to));
this._envelope.rejected = [];
this._envelope.rejectedErrors = [];
this._envelope.accepted = [];
this._responseActions.push(str => {
this._actionMAIL(str, callback);
});
this._sendCommand('MAIL FROM:<' + this._envelope.from + '>' + (args.length ? ' ' + args.join(' ') : ''));
}
//位置:smtp-connection.js
_sendCommand(str) {
//str:MAIL FROM:<发件人的邮箱地址>
this._socket.write(Buffer.from(str + 'rn', 'utf-8'));
}
server response
data监听器
收到如下回复:
250 Ok
7、发送RCPT TO请求
判断邮件的发起者是否可以正常发送
核心源码
//位置:smtp-connection.js
//发送MAIL FROM请求,判断邮件的发起者是否正常
_actionMAIL(str, callback) { //250 Ok
let message, curRecipient;
this._recipientQueue = [];
//有BFC那味儿了
while (this._envelope.rcptQueue.length) {
curRecipient = this._envelope.rcptQueue.shift();
this._recipientQueue.push(curRecipient);
this._responseActions.push(str => {
this._actionRCPT(str, callback);
});
this._sendCommand('RCPT TO:<' + curRecipient + '>' + this._getDsnRcptToArgs()); //'RCPT TO:<邮件接收者>'
}
}
socket发送RCPT TO
//位置:smtp-connection.js
_sendCommand(str) {
//str:RCPT TO:<接收方的邮箱>
this._socket.write(Buffer.from(str + 'rn', 'utf-8'));
}
server response
data监听器
收到如下回复:
250 Ok
发送方和接收方都没问题,接下来就发送邮件内容
8、发送DATA请求
发送RCPT TO
请求成功后,触发callback
,接着发送邮件content
核心源码
//位置:smtp-connection.js
//发送RCPT TO请求成功后,发起DATA请求
_actionRCPT(str, callback) {
let curRecipient = this._recipientQueue.shift(); //邮箱
this._envelope.accepted.push(curRecipient);
this._responseActions.push(str => {
this._actionDATA(str, callback);
});
this._sendCommand('DATA');
}
//位置:smtp-connection.js
_sendCommand(str) {
//str:DATA
this._socket.write(Buffer.from(str + 'rn', 'utf-8'));
}
server response
data监听器
收到如下回复:
354 End data with <CR><LF>.<CR><LF>
client
告诉server
,接下来我发送的是邮件内容,server
回复发送的邮件内容以<CR><LF>.<CR><LF>
结尾
这也表明server
是能收到数据的,接下来就正式发送邮件内容了
创建发送流
//位置:smtp-connection.js
//创建发送流
let stream = this._createSendStream(callback)
_createSendStream(callback) {
let dataStream = new DataStream();
let logStream;
this._responseActions.push(str => {
this._actionSMTPStream(str, callback);
});
//将TLSSocket写入流,以便边读边写
dataStream.pipe(this._socket, {
end: false
});
return dataStream;
}
将发送流导入ReadStream
//位置:smtp-connection.js
//将发送流导入 可读流ReadStream中
message.pipe(stream);
message.pipe(stream)
就是将邮件内容发送给server
端了,再具体一点的话是这样的
//位置:mime-node.js
stream(outputStream, options, done) {
let transferEncoding = this.getTransferEncoding();
let contentStream;
let localStream;
// protect actual callback against multiple triggering
let returned = false;
let callback = err => {
if (returned) {
return;
}
returned = true;
done(err);
};
// for multipart nodes, push child nodes
// for content nodes end the stream
let finalize = () => {
let childId = 0;
let processChildNode = () => {
if (childId >= this.childNodes.length) {
outputStream.write('rn--' + this.boundary + '--rn');
return callback();
}
let child = this.childNodes[childId++];
outputStream.write((childId > 1 ? 'rn' : '') + '--' + this.boundary + 'rn');
child.stream(outputStream, options, err => {
if (err) {
return callback(err);
}
setImmediate(processChildNode);
});
};
if (this.multipart) {
setImmediate(processChildNode);
} else {
return callback();
}
};
// pushes node content
let sendContent = () => {
let createStream = () => {
contentStream = new (transferEncoding === 'base64' ? base64 : qp).Encoder(options);
contentStream.pipe(outputStream, {
end: false
});
contentStream.once('end', finalize);
contentStream.once('error', err => callback(err));
localStream = this._getStream(this.content);
localStream.pipe(contentStream);
localStream.once('error', err => callback(err));
};
setImmediate(createStream);
};
outputStream.write(this.buildHeaders() + 'rnrn');
setImmediate(sendContent);
}
这边即5、以rfc2822标准创建stream对象
里的stream
方法,在建立数据流管道后,并发送DATA
字符串给server
,通知server
接下来发送邮件内容,然后通过message.pipe(stream)
,将邮件内容发送过去,邮件内容的处理这边就不讲了
至此,流程结束,你会收到邮件。
几点感受
① 有的函数的callback
要往上翻好几层才能找到
② if
条件判断巨多,在删减代码上花了很多时间
③ 发送-监听处理机制有点像BFS
,也就是将要处理response
的action
push
进array
中,待监听到后,再array.unshift
取出处理
④ nodemailer
库现在仍然处于活跃阶段,源码里无论是注释还是编码习惯都非常好
GitHub
nodemailer
:github.com/nodemailer/…
simple-nodemailer
:github.com/AttackXiaoJ…
要找完整源码的小伙伴,复制代码段,全局搜索即可
(完)
以上是 nodemailer极简源码解析与实现原理 的全部内容, 来源链接: utcz.com/a/31619.html