@flun/mailer 2.1.24 → 2.1.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -5
- package/LICENSE +19 -0
- package/Mail.js +2 -2
- package/README.md +35 -31
- package/copy-files.js +2 -2
- package/index.d.ts +27 -24
- package/index.js +24 -19
- package/lib/addressparser.js +21 -17
- package/lib/base64.js +8 -7
- package/lib/dkim/index.js +20 -17
- package/lib/dkim/message-parser.js +15 -12
- package/lib/dkim/relaxed-body.js +16 -15
- package/lib/dkim/sign.js +14 -13
- package/lib/fetch/cookies.js +24 -22
- package/lib/fetch/index.js +20 -17
- package/lib/http-proxy-client.js +14 -9
- package/lib/json-transport.js +8 -4
- package/lib/mail-composer.js +56 -39
- package/lib/mailer/index.js +17 -16
- package/lib/mailer/mail-message.js +28 -24
- package/lib/mime-funcs/index.js +69 -64
- package/lib/mime-funcs/mime-types.js +12 -6
- package/lib/mime-node/index.js +88 -74
- package/lib/mime-node/last-newline.js +4 -4
- package/lib/mime-node/le-unix.js +2 -2
- package/lib/mime-node/le-windows.js +3 -3
- package/lib/punycode.js +46 -40
- package/lib/qp.js +19 -15
- package/lib/regexs.js +30 -30
- package/lib/sendmail-transport.js +3 -3
- package/lib/ses-transport.js +12 -8
- package/lib/shared.js +74 -59
- package/lib/smtp-connection/data-stream.js +10 -9
- package/lib/smtp-connection/index.js +104 -86
- package/lib/smtp-pool/index.js +25 -22
- package/lib/smtp-pool/pool-resource.js +1 -1
- package/lib/smtp-transport.js +3 -2
- package/lib/stream-transport.js +12 -10
- package/lib/well-known/index.js +5 -4
- package/lib/xoauth2.js +31 -26
- package/package.json +4 -4
|
@@ -7,25 +7,25 @@ import {
|
|
|
7
7
|
PK, osHostname, isIP, netConnect, regexs, resetRegex, getLogger, resolveHostname, callbackPromise, encodeXText, cleanup
|
|
8
8
|
} from '../shared.js';
|
|
9
9
|
|
|
10
|
-
// 默认超时(连接,套接字无活动,等待SMTP问候,DNS解析)
|
|
10
|
+
// 默认超时(连接,套接字无活动,等待SMTP问候,DNS解析)时间(毫秒)
|
|
11
11
|
const CONNECTION_TIMEOUT = 2 * 60 * 1000, SOCKET_TIMEOUT = 10 * 60 * 1000,
|
|
12
12
|
GREETING_TIMEOUT = 30 * 1000, DNS_TIMEOUT = 30 * 1000;
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* 生成SMTP连接对象;
|
|
16
16
|
* >查看定义:@see {@link SmtpConnection}
|
|
17
|
-
*
|
|
18
|
-
* - **port** -
|
|
19
|
-
* - **host** - 连接的主机名或IP
|
|
17
|
+
* 可选的选项对象可以包含以下属性:
|
|
18
|
+
* - **port** - 连接的端口(默认为587或465)
|
|
19
|
+
* - **host** - 连接的主机名或IP地址(默认为'localhost')
|
|
20
20
|
* - **secure** - 使用SSL
|
|
21
21
|
* - **ignoreTLS** - 忽略服务器对STARTTLS的支持
|
|
22
22
|
* - **requireTLS** - 强制客户端使用STARTTLS
|
|
23
23
|
* - **name** - 客户端服务器名称
|
|
24
24
|
* - **localAddress** - 绑定的出站地址
|
|
25
|
-
* - **greetingTimeout** -
|
|
26
|
-
* - **connectionTimeout** -
|
|
27
|
-
* - **socketTimeout** -
|
|
28
|
-
* - **dnsTimeout** - 等待DNS
|
|
25
|
+
* - **greetingTimeout** - 等待服务器问候消息的超时时间(毫秒)
|
|
26
|
+
* - **connectionTimeout** - 等待连接建立的超时时间(毫秒)
|
|
27
|
+
* - **socketTimeout** - 连接关闭前的无活动超时时间(默认为1小时)
|
|
28
|
+
* - **dnsTimeout** - 等待DNS请求解析的超时时间(默认为30秒)
|
|
29
29
|
* - **lmtp** - 如果为true,使用LMTP而不是SMTP协议
|
|
30
30
|
* - **logger** - bunyan兼容的日志接口
|
|
31
31
|
* - **debug** - 如果为true,将SMTP流量传递给日志记录器
|
|
@@ -51,7 +51,7 @@ class SmtpConnection extends EventEmitter {
|
|
|
51
51
|
this.servername = servername ? servername : !isIP(this.host) ? this.host : false;
|
|
52
52
|
this.allowInternalNetworkInterfaces = allowInternalNetworkInterfaces;
|
|
53
53
|
|
|
54
|
-
// 如果secure选项未设置但端口是465
|
|
54
|
+
// 如果secure选项未设置但端口是465,则默认为安全连接
|
|
55
55
|
if (typeof secure === 'undefined' && this.port === 465) this.secureConnection = true;
|
|
56
56
|
this.name = name || this.#getHostname();
|
|
57
57
|
this.logger = getLogger(this.options, { component, sid: this.id }); // 设置日志记录器
|
|
@@ -64,7 +64,7 @@ class SmtpConnection extends EventEmitter {
|
|
|
64
64
|
this.customAuth.set(mapKey, customAuth[key]);
|
|
65
65
|
});
|
|
66
66
|
|
|
67
|
-
this.secure = !!this.secureConnection; //
|
|
67
|
+
this.secure = !!this.secureConnection; // 定义当前连接是否安全;如果不安全,如果服务器支持,可以使用STARTTLS
|
|
68
68
|
this.version = PK.version; // 暴露版本号
|
|
69
69
|
this._remainder = ''; // 存储来自服务器的不完整消息
|
|
70
70
|
this._maxAllowedSize = 0; // 定义单个消息允许的最大大小
|
|
@@ -73,16 +73,16 @@ class SmtpConnection extends EventEmitter {
|
|
|
73
73
|
this._recipientQueue = []; // 这个队列包含要发送的收件人列表
|
|
74
74
|
this._responseQueue = []; // 来自服务器的未处理响应队列
|
|
75
75
|
this._supportedAuth = []; // 列出支持的认证机制列表
|
|
76
|
-
this._socket = false; //
|
|
77
|
-
this._closing = false; //
|
|
78
|
-
this._envelope = false; //
|
|
79
|
-
this._destroyed = false; //
|
|
80
|
-
this._greetingTimeout = false; // 等待问候消息的超时变量,如果为true
|
|
81
|
-
this._connectionTimeout = false; // 等待连接开始的超时变量,如果为true
|
|
82
|
-
this.destroyed = false; // 如果设置为true
|
|
83
|
-
this.allowsAuth = false; // 如果EHLO响应包含"AUTH"
|
|
84
|
-
this.authenticated = false; // 如果为true
|
|
85
|
-
this.lastServerResponse = false; //
|
|
76
|
+
this._socket = false; // 连接到服务器的套接字,如果没有套接字,则为false
|
|
77
|
+
this._closing = false; // 如果套接字正在关闭,则设置为true
|
|
78
|
+
this._envelope = false; // 包含当前信封(发件人,收件人)的对象,如果没有信封,则为false
|
|
79
|
+
this._destroyed = false; // 如果套接字被认为已关闭,则设置为true
|
|
80
|
+
this._greetingTimeout = false; // 等待问候消息的超时变量,如果为true,则表示已超时
|
|
81
|
+
this._connectionTimeout = false; // 等待连接开始的超时变量,如果为true,则表示已超时
|
|
82
|
+
this.destroyed = false; // 如果设置为true,此实例不再活跃
|
|
83
|
+
this.allowsAuth = false; // 如果EHLO响应包含"AUTH",则设置为true;如果为false,则不尝试认证
|
|
84
|
+
this.authenticated = false; // 如果为true,则表示用户已认证
|
|
85
|
+
this.lastServerResponse = false; // 最后的服务器响应,如果没有响应,则为false
|
|
86
86
|
|
|
87
87
|
/**
|
|
88
88
|
* 套接字监听器的回调函数
|
|
@@ -151,13 +151,14 @@ class SmtpConnection extends EventEmitter {
|
|
|
151
151
|
};
|
|
152
152
|
if (localAddress) opts.localAddress = localAddress;
|
|
153
153
|
|
|
154
|
-
//
|
|
154
|
+
// 如果已提供连接对象,则使用它
|
|
155
155
|
if (connection) {
|
|
156
156
|
this._socket = connection, this.#setupConnectionHandlers();
|
|
157
157
|
if (this.secureConnection && !this.alreadySecured)
|
|
158
158
|
setImmediate(() =>
|
|
159
159
|
this.#upgradeConnection(err => {
|
|
160
|
-
if (err) return this.#onError(new Error(`启动TLS错误-${(err.message || err)}`), 'ETLS', false,
|
|
160
|
+
if (err) return this.#onError(new Error(`启动TLS错误-${(err.message || err)}`), 'ETLS', false,
|
|
161
|
+
'CONN');
|
|
161
162
|
this.#onConnect();
|
|
162
163
|
})
|
|
163
164
|
);
|
|
@@ -173,13 +174,13 @@ class SmtpConnection extends EventEmitter {
|
|
|
173
174
|
this._socket.setKeepAlive(true), this._socket.once('connect', () => this.#onConnect());
|
|
174
175
|
});
|
|
175
176
|
};
|
|
176
|
-
//
|
|
177
|
+
// 如果套接字已设置,则使用它
|
|
177
178
|
if (socket)
|
|
178
179
|
return createConnection((resolvedOpts) => {
|
|
179
180
|
socket.connect(this.port, this.host, () => { });
|
|
180
181
|
return socket;
|
|
181
182
|
});
|
|
182
|
-
// 如果secureConnection为true
|
|
183
|
+
// 如果secureConnection为true,则使用TLS连接
|
|
183
184
|
else if (this.secureConnection) {
|
|
184
185
|
if (tls) Object.keys(tls).forEach(key => { opts[key] = tls[key]; })
|
|
185
186
|
if (this.servername && !opts.servername) opts.servername = this.servername; // 确保SNI的服务器名称
|
|
@@ -232,12 +233,12 @@ class SmtpConnection extends EventEmitter {
|
|
|
232
233
|
|
|
233
234
|
const { method = '', oauth2, credentials, user, pass, options } = this._auth = authData;
|
|
234
235
|
this._authMethod = method.toString().trim().toUpperCase(); // 选择SASL认证方法
|
|
235
|
-
//
|
|
236
|
+
// 如果没有指定认证方法,则使用XOAUTH2认证,如果没有OAuth2凭据,则使用PLAIN认证
|
|
236
237
|
if (!this._authMethod && oauth2 && !credentials) this._authMethod = 'XOAUTH2';
|
|
237
238
|
else if (!this._authMethod || (this._authMethod === 'XOAUTH2' && !oauth2))
|
|
238
239
|
this._authMethod = (this._supportedAuth[0] || 'PLAIN').toUpperCase().trim();
|
|
239
240
|
|
|
240
|
-
//
|
|
241
|
+
// 如果没有指定凭据,则使用用户名和密码
|
|
241
242
|
if (this._authMethod !== 'XOAUTH2' && (!credentials || !credentials.user || !credentials.pass)) {
|
|
242
243
|
if ((user && pass) || this.customAuth.has(this._authMethod)) this._auth.credentials = { user, pass, options };
|
|
243
244
|
else return callback(this.#formatError(`缺少 "${this._authMethod}" 的凭据`, 'EAUTH', false, 'API'));
|
|
@@ -262,7 +263,8 @@ class SmtpConnection extends EventEmitter {
|
|
|
262
263
|
extensions: [].concat(this._supportedExtensions), authMethods: [].concat(this._supportedAuth),
|
|
263
264
|
maxAllowedSize: this._maxAllowedSize || false,
|
|
264
265
|
sendCommand: (cmd, done) => {
|
|
265
|
-
const promise = !done ? new Promise((resolve, reject) => done = callbackPromise(resolve, reject)) :
|
|
266
|
+
const promise = !done ? new Promise((resolve, reject) => done = callbackPromise(resolve, reject)) :
|
|
267
|
+
null;
|
|
266
268
|
this._responseActions.push(str => {
|
|
267
269
|
lastResponse = str;
|
|
268
270
|
const codes = str.match(regexs.RESPONSE_CODE), data = { command: cmd, response: str };
|
|
@@ -279,8 +281,9 @@ class SmtpConnection extends EventEmitter {
|
|
|
279
281
|
}, resolve, reject
|
|
280
282
|
});
|
|
281
283
|
|
|
282
|
-
// 如果返回了Promise
|
|
283
|
-
if (handlerResponse &&
|
|
284
|
+
// 如果返回了Promise,则等待Promise完成
|
|
285
|
+
if (handlerResponse &&
|
|
286
|
+
typeof handlerResponse.catch === 'function') handlerResponse.then(resolve).catch(reject);
|
|
284
287
|
return;
|
|
285
288
|
}
|
|
286
289
|
|
|
@@ -288,7 +291,8 @@ class SmtpConnection extends EventEmitter {
|
|
|
288
291
|
case 'XOAUTH2':
|
|
289
292
|
return this.#actionAUTH_XOAUTH2_TOKEN(false, callback);
|
|
290
293
|
case 'LOGIN':
|
|
291
|
-
this._responseActions.push(str => this.#actionAUTH_LOGIN_USER(str, callback)),
|
|
294
|
+
this._responseActions.push(str => this.#actionAUTH_LOGIN_USER(str, callback)),
|
|
295
|
+
this.#sendCommand('AUTH LOGIN');
|
|
292
296
|
return;
|
|
293
297
|
case 'PLAIN':
|
|
294
298
|
this._responseActions.push(str => this.#actionAUTH_PLAIN_RESPONSE(str, callback));
|
|
@@ -307,7 +311,7 @@ class SmtpConnection extends EventEmitter {
|
|
|
307
311
|
/**
|
|
308
312
|
* 发送消息
|
|
309
313
|
*
|
|
310
|
-
* @param {Object} envelope
|
|
314
|
+
* @param {Object} envelope 信封对象,{from: addr, to: [addr]}
|
|
311
315
|
* @param {Object} message 字符串、缓冲区或流
|
|
312
316
|
* @param {Function} callback 发送完成后调用的回调函数
|
|
313
317
|
*/
|
|
@@ -324,7 +328,8 @@ class SmtpConnection extends EventEmitter {
|
|
|
324
328
|
});
|
|
325
329
|
|
|
326
330
|
const state = { returned: false }, callback = (...args) => { cleanup(state), done(...args) };
|
|
327
|
-
if (typeof message.on === 'function') message.on('error', err => callback(this.#formatError(err, 'ESTREAM', false,
|
|
331
|
+
if (typeof message.on === 'function') message.on('error', err => callback(this.#formatError(err, 'ESTREAM', false,
|
|
332
|
+
'API')));
|
|
328
333
|
|
|
329
334
|
// 将消息写入流
|
|
330
335
|
const writeMessageToStream = stream => {
|
|
@@ -355,7 +360,8 @@ class SmtpConnection extends EventEmitter {
|
|
|
355
360
|
reset(callback) {
|
|
356
361
|
this.#sendCommand('RSET');
|
|
357
362
|
this._responseActions.push(str => {
|
|
358
|
-
if (!str.startsWith('2')) return callback(this.#formatError(`无法重置会话状态;响应=${str}`, 'EPROTOCOL', str,
|
|
363
|
+
if (!str.startsWith('2')) return callback(this.#formatError(`无法重置会话状态;响应=${str}`, 'EPROTOCOL', str,
|
|
364
|
+
'RSET'));
|
|
359
365
|
this._envelope = false;
|
|
360
366
|
return callback(null, true);
|
|
361
367
|
});
|
|
@@ -372,7 +378,7 @@ class SmtpConnection extends EventEmitter {
|
|
|
372
378
|
this.logger.info({ tnx: 'network', localAddress, localPort, remoteAddress, remotePort },
|
|
373
379
|
'%s 已建立到 %s:%s', this.secure ? '安全连接' : '连接', remoteAddress, remotePort
|
|
374
380
|
);
|
|
375
|
-
if (this._destroyed) return this.close(); //
|
|
381
|
+
if (this._destroyed) return this.close(); // 如果连接已销毁,则关闭连接
|
|
376
382
|
|
|
377
383
|
this.stage = 'connected';
|
|
378
384
|
// 清除套接字的现有监听器
|
|
@@ -383,7 +389,7 @@ class SmtpConnection extends EventEmitter {
|
|
|
383
389
|
.setTimeout(this.options.socketTimeout || SOCKET_TIMEOUT).on('timeout', this.onSocketTimeout);
|
|
384
390
|
|
|
385
391
|
this._greetingTimeout = setTimeout(() => {
|
|
386
|
-
//
|
|
392
|
+
// 如果仍在等待问候消息,则放弃
|
|
387
393
|
if (this._socket && !this._destroyed && this._responseActions[0] === this.#actionGreeting)
|
|
388
394
|
this.#onError('从未收到问候消息', 'ETIMEDOUT', false, 'CONN');
|
|
389
395
|
|
|
@@ -406,13 +412,14 @@ class SmtpConnection extends EventEmitter {
|
|
|
406
412
|
let lastline = rQueue.at(-1) ?? null;
|
|
407
413
|
for (let i = 0; i < lines.length; i++) {
|
|
408
414
|
const line = lines[i];
|
|
409
|
-
//
|
|
415
|
+
// 检查是否需要延续(确保 lastline 存在)
|
|
410
416
|
if (lastline && regexs.CONTINUATION_RESPONSE.test(lastline.split('\n').pop()))
|
|
411
417
|
lastline += `\n${line}`, rQueue[rQueue.length - 1] = lastline;
|
|
412
|
-
else rQueue.push(line), lastline = line; //
|
|
418
|
+
else rQueue.push(line), lastline = line; // 新响应:推入队列并更新 lastline
|
|
413
419
|
}
|
|
414
420
|
|
|
415
|
-
if (lastline &&
|
|
421
|
+
if (lastline &&
|
|
422
|
+
regexs.CONTINUATION_RESPONSE.test(lastline.split('\n').pop())) return; // 如果响应未完成,等待后续数据
|
|
416
423
|
this.#processResponse(); // 处理完整响应
|
|
417
424
|
}
|
|
418
425
|
|
|
@@ -425,7 +432,7 @@ class SmtpConnection extends EventEmitter {
|
|
|
425
432
|
*/
|
|
426
433
|
#onError(err, type, data, command) {
|
|
427
434
|
clearTimeout(this._connectionTimeout), clearTimeout(this._greetingTimeout);
|
|
428
|
-
if (this._destroyed) return; //
|
|
435
|
+
if (this._destroyed) return; // 如果连接已被销毁,则忽略错误
|
|
429
436
|
|
|
430
437
|
err = this.#formatError(err, type, data, command);
|
|
431
438
|
this.logger.error(data, err.message), this.emit('error', err), this.close();
|
|
@@ -452,12 +459,14 @@ class SmtpConnection extends EventEmitter {
|
|
|
452
459
|
let serverResponse = false;
|
|
453
460
|
const _remainder = this._remainder, { debug, transactionLog } = this.options;
|
|
454
461
|
if (_remainder?.trim()) {
|
|
455
|
-
if (debug || transactionLog) this.logger.debug({ tnx: 'server' }, _remainder.replace(regexs.OPTIONAL_CRLF,
|
|
462
|
+
if (debug || transactionLog) this.logger.debug({ tnx: 'server' }, _remainder.replace(regexs.OPTIONAL_CRLF,
|
|
463
|
+
''));
|
|
456
464
|
this.lastServerResponse = serverResponse = _remainder.trim();
|
|
457
465
|
}
|
|
458
466
|
|
|
459
467
|
this.logger.info({ tnx: 'network' }, '连接已关闭');
|
|
460
|
-
if (this.upgrading && !this._destroyed) return this.#onError(new Error('连接意外关闭'), 'ETLS', serverResponse,
|
|
468
|
+
if (this.upgrading && !this._destroyed) return this.#onError(new Error('连接意外关闭'), 'ETLS', serverResponse,
|
|
469
|
+
'CONN');
|
|
461
470
|
else if ((![this.#actionGreeting, this.close].includes(this._responseActions[0]) && !this._destroyed) ||
|
|
462
471
|
regexs.RESPONSE_4XX_5XX.test(serverResponse))
|
|
463
472
|
return this.#onError(new Error('连接意外关闭'), 'ECONNECTION', serverResponse, 'CONN');
|
|
@@ -484,7 +493,7 @@ class SmtpConnection extends EventEmitter {
|
|
|
484
493
|
}
|
|
485
494
|
|
|
486
495
|
/**
|
|
487
|
-
*
|
|
496
|
+
* 销毁客户端,发出'end'事件
|
|
488
497
|
*/
|
|
489
498
|
#destroy() {
|
|
490
499
|
if (this._destroyed) return;
|
|
@@ -521,13 +530,13 @@ class SmtpConnection extends EventEmitter {
|
|
|
521
530
|
this.upgrading = false;
|
|
522
531
|
return callback(err);
|
|
523
532
|
}
|
|
524
|
-
socketPlain.resume(); //
|
|
533
|
+
socketPlain.resume(); // 如果套接字暂停,则恢复
|
|
525
534
|
}
|
|
526
535
|
|
|
527
536
|
/**
|
|
528
537
|
* 处理来自服务器的排队响应
|
|
529
538
|
*
|
|
530
|
-
* @param {Boolean} force 如果为true
|
|
539
|
+
* @param {Boolean} force 如果为true,忽略_processing标志
|
|
531
540
|
*/
|
|
532
541
|
#processResponse() {
|
|
533
542
|
if (!this._responseQueue.length) return false;
|
|
@@ -545,13 +554,13 @@ class SmtpConnection extends EventEmitter {
|
|
|
545
554
|
}
|
|
546
555
|
|
|
547
556
|
/**
|
|
548
|
-
*
|
|
557
|
+
* 向服务器发送命令,附加\r\n
|
|
549
558
|
*
|
|
550
559
|
* @param {String} str 要发送到服务器的字符串
|
|
551
|
-
* @param {String} logStr
|
|
560
|
+
* @param {String} logStr 用于记录的可选字符串,而不是实际字符串
|
|
552
561
|
*/
|
|
553
562
|
#sendCommand(str = '', logStr) {
|
|
554
|
-
if (this._destroyed) return; //
|
|
563
|
+
if (this._destroyed) return; // 连接已关闭,无法发送更多数据
|
|
555
564
|
if (this._socket.destroyed) return this.close();
|
|
556
565
|
if (this.options.debug || this.options.transactionLog)
|
|
557
566
|
this.logger.debug({ tnx: 'client' }, (logStr || str).toString().replace(regexs.OPTIONAL_CRLF, ''));
|
|
@@ -560,9 +569,9 @@ class SmtpConnection extends EventEmitter {
|
|
|
560
569
|
}
|
|
561
570
|
|
|
562
571
|
/**
|
|
563
|
-
*
|
|
572
|
+
* 通过提交信封数据来初始化新消息,以MAIL FROM:命令开始
|
|
564
573
|
*
|
|
565
|
-
* @param {Object} envelope
|
|
574
|
+
* @param {Object} envelope 信封对象,形式为 {from:'...', to:['...']} 或
|
|
566
575
|
* {from:{address:'...',name:'...'}, to:[address:'...',name:'...']}
|
|
567
576
|
*/
|
|
568
577
|
#setEnvelope(envelope = {}, callback) {
|
|
@@ -579,7 +588,8 @@ class SmtpConnection extends EventEmitter {
|
|
|
579
588
|
// 验证单个地址并检测非ASCII字符
|
|
580
589
|
const _validateAddress = (address, type) => {
|
|
581
590
|
if (regexs.INVALID_ADDRESS_CHARS.test(address)) errors.push(`无效的${type}: ${JSON.stringify(address)}`);
|
|
582
|
-
|
|
591
|
+
// 检查地址是否使用ASCII字符,否则需使用SMTPUTF8扩展
|
|
592
|
+
else if (regexs.NON_ASCII.test(address)) useSmtpUtf8 = true;
|
|
583
593
|
};
|
|
584
594
|
_validateAddress(from, '发件人');
|
|
585
595
|
to.forEach(addr => _validateAddress(addr, '收件人'));
|
|
@@ -599,11 +609,13 @@ class SmtpConnection extends EventEmitter {
|
|
|
599
609
|
|
|
600
610
|
this._responseActions.push(str => this.#actionMAIL(str, callback));
|
|
601
611
|
|
|
602
|
-
// 如果服务器支持SMTPUTF8
|
|
603
|
-
if (useSmtpUtf8 && this._supportedExtensions.includes('SMTPUTF8')) args.push('SMTPUTF8'),
|
|
612
|
+
// 如果服务器支持SMTPUTF8并且信封包含国际化电子邮件地址,则将SMTPUTF8关键字附加到MAIL FROM命令
|
|
613
|
+
if (useSmtpUtf8 && this._supportedExtensions.includes('SMTPUTF8')) args.push('SMTPUTF8'),
|
|
614
|
+
this._usingSmtpUtf8 = true;
|
|
604
615
|
|
|
605
616
|
// 如果服务器支持8BITMIME并且消息可能包含非ASCII字节,则将8BITMIME关键字附加到MAIL FROM命令
|
|
606
|
-
if (use8BitMime && this._supportedExtensions.includes('8BITMIME')) args.push('BODY=8BITMIME'),
|
|
617
|
+
if (use8BitMime && this._supportedExtensions.includes('8BITMIME')) args.push('BODY=8BITMIME'),
|
|
618
|
+
this._using8BitMime = true;
|
|
607
619
|
|
|
608
620
|
if (size && this._supportedExtensions.includes('SIZE')) args.push(`SIZE=${size}`);
|
|
609
621
|
|
|
@@ -701,8 +713,8 @@ class SmtpConnection extends EventEmitter {
|
|
|
701
713
|
}
|
|
702
714
|
|
|
703
715
|
/**
|
|
704
|
-
*
|
|
705
|
-
* 如果传入消息以220
|
|
716
|
+
* 在连接创建后且服务器发送问候消息后运行;
|
|
717
|
+
* 如果传入消息以220开头,则通过发送EHLO命令启动SMTP会话
|
|
706
718
|
*
|
|
707
719
|
* @param {String} str 来自服务器的消息
|
|
708
720
|
*/
|
|
@@ -713,8 +725,8 @@ class SmtpConnection extends EventEmitter {
|
|
|
713
725
|
}
|
|
714
726
|
|
|
715
727
|
/**
|
|
716
|
-
* 处理LHLO
|
|
717
|
-
* 发出'error'
|
|
728
|
+
* 处理LHLO命令的服务器响应;如果产生错误,
|
|
729
|
+
* 发出'error',否则将其视为EHLO响应
|
|
718
730
|
*
|
|
719
731
|
* @param {String} str 来自服务器的消息
|
|
720
732
|
*/
|
|
@@ -724,14 +736,15 @@ class SmtpConnection extends EventEmitter {
|
|
|
724
736
|
}
|
|
725
737
|
|
|
726
738
|
/**
|
|
727
|
-
* 处理EHLO
|
|
728
|
-
* 尝试改用HELO
|
|
729
|
-
* 则启动TLS
|
|
739
|
+
* 处理EHLO命令的服务器响应;如果产生错误,
|
|
740
|
+
* 尝试改用HELO,否则如果服务器支持STARTTLS
|
|
741
|
+
* 则启动TLS协商,或进入认证阶段;
|
|
730
742
|
*
|
|
731
743
|
* @param {String} str 来自服务器的消息
|
|
732
744
|
*/
|
|
733
745
|
#actionEHLO(str) {
|
|
734
|
-
if (str.startsWith('421')) return this.#onError(new Error(`服务器终止连接;响应=${str}`), 'ECONNECTION', str,
|
|
746
|
+
if (str.startsWith('421')) return this.#onError(new Error(`服务器终止连接;响应=${str}`), 'ECONNECTION', str,
|
|
747
|
+
'EHLO');
|
|
735
748
|
|
|
736
749
|
// 通用命令队列函数
|
|
737
750
|
const queueCommand = (action, command) => {
|
|
@@ -739,7 +752,8 @@ class SmtpConnection extends EventEmitter {
|
|
|
739
752
|
}, { requireTLS, ignoreTLS } = this.options;
|
|
740
753
|
if (!str.startsWith('2')) {
|
|
741
754
|
if (requireTLS)
|
|
742
|
-
return this.#onError(new Error(`EHLO失败但HELO不支持必需的STARTTLS;响应=${str}`), 'ECONNECTION', str,
|
|
755
|
+
return this.#onError(new Error(`EHLO失败但HELO不支持必需的STARTTLS;响应=${str}`), 'ECONNECTION', str,
|
|
756
|
+
'EHLO');
|
|
743
757
|
return queueCommand(this.#actionHELO, `HELO ${this.name}`); // 使用通用函数发送 HELO
|
|
744
758
|
}
|
|
745
759
|
|
|
@@ -762,14 +776,14 @@ class SmtpConnection extends EventEmitter {
|
|
|
762
776
|
if (new RegExp(`${authMechanismSource}CRAM-MD5`, 'i').test(str)) this._supportedAuth.push('CRAM-MD5');
|
|
763
777
|
if (new RegExp(`${authMechanismSource}XOAUTH2`, 'i').test(str)) this._supportedAuth.push('XOAUTH2');
|
|
764
778
|
|
|
765
|
-
// 检测服务器是否支持SIZE
|
|
779
|
+
// 检测服务器是否支持SIZE扩展(以及最大允许大小)
|
|
766
780
|
if (match) this._supportedExtensions.push('SIZE'), this._maxAllowedSize = Number(match[1]) || 0;
|
|
767
781
|
this.emit('connect');
|
|
768
782
|
}
|
|
769
783
|
|
|
770
784
|
/**
|
|
771
|
-
* 处理HELO
|
|
772
|
-
* 发出'error'
|
|
785
|
+
* 处理HELO命令的服务器响应;如果产生错误,
|
|
786
|
+
* 发出'error',否则进入认证阶段;
|
|
773
787
|
*
|
|
774
788
|
* @param {String} str 来自服务器的消息
|
|
775
789
|
*/
|
|
@@ -779,15 +793,15 @@ class SmtpConnection extends EventEmitter {
|
|
|
779
793
|
}
|
|
780
794
|
|
|
781
795
|
/**
|
|
782
|
-
* 处理STARTTLS
|
|
783
|
-
* 尝试改用HELO
|
|
796
|
+
* 处理STARTTLS命令的服务器响应;如果有错误
|
|
797
|
+
* 尝试改用HELO,否则启动TLS升级;如果升级成功,重新启动EHLO
|
|
784
798
|
*
|
|
785
799
|
* @param {String} str 来自服务器的消息
|
|
786
800
|
*/
|
|
787
801
|
#actionSTARTTLS(str) {
|
|
788
802
|
if (!str.startsWith('2')) {
|
|
789
803
|
if (this.options.opportunisticTLS) {
|
|
790
|
-
this.logger.info({ tnx: 'smtp' }, 'STARTTLS
|
|
804
|
+
this.logger.info({ tnx: 'smtp' }, 'STARTTLS升级失败,继续未加密');
|
|
791
805
|
return this.emit('connect');
|
|
792
806
|
}
|
|
793
807
|
return this.#onError(new Error('使用STARTTLS升级连接时出错'), 'ETLS', str, 'STARTTLS');
|
|
@@ -797,14 +811,14 @@ class SmtpConnection extends EventEmitter {
|
|
|
797
811
|
if (err) return this.#onError(new Error(`启动TLS错误-${err.message || err}`), 'ETLS', false, 'STARTTLS');
|
|
798
812
|
|
|
799
813
|
this.logger.info({ tnx: 'smtp' }, '连接已使用STARTTLS升级');
|
|
800
|
-
secured ? this.#sendHeloCommand() : this.emit('connect');//
|
|
814
|
+
secured ? this.#sendHeloCommand() : this.emit('connect');// 如果升级成功,重新启动会话,否则继续未加密
|
|
801
815
|
});
|
|
802
816
|
}
|
|
803
817
|
|
|
804
818
|
/**
|
|
805
819
|
* 处理AUTH LOGIN用户名认证阶段
|
|
806
|
-
* 预期收到服务器334
|
|
807
|
-
* 发送Base64
|
|
820
|
+
* 预期收到服务器334响应(Base64编码的"Username:"提示)
|
|
821
|
+
* 发送Base64编码的用户名,并准备处理密码认证
|
|
808
822
|
*
|
|
809
823
|
* @param {String} str 服务器响应消息
|
|
810
824
|
* @param {Function} callback 认证结果回调
|
|
@@ -820,16 +834,17 @@ class SmtpConnection extends EventEmitter {
|
|
|
820
834
|
/**
|
|
821
835
|
* 处理AUTH CRAM-MD5命令:
|
|
822
836
|
* 1. 服务器发送一个随机字符串
|
|
823
|
-
* 2.
|
|
837
|
+
* 2. 客户端用密码作为密钥,对其进行HMAC-MD5加密
|
|
824
838
|
* 3. 将"用户名 加密结果"进行base64编码后返回给服务器
|
|
825
839
|
*
|
|
826
|
-
* @param {String} str
|
|
840
|
+
* @param {String} str 服务器发送的消息,格式:334 <base64编码的随机字符串>
|
|
827
841
|
* @param {Function} callback 认证结果回调函数
|
|
828
842
|
*/
|
|
829
843
|
#actionAUTH_CRAM_MD5(str, callback) {
|
|
830
844
|
const challengeMatch = str.match(regexs.CRAM_MD5_CHALLENGE);
|
|
831
845
|
let challengeString = '';
|
|
832
|
-
if (!challengeMatch) return callback(this.#formatError('在等待服务器响应时登录序列无效', 'EAUTH', str,
|
|
846
|
+
if (!challengeMatch) return callback(this.#formatError('在等待服务器响应时登录序列无效', 'EAUTH', str,
|
|
847
|
+
'AUTH CRAM-MD5'));
|
|
833
848
|
else challengeString = challengeMatch[1];
|
|
834
849
|
|
|
835
850
|
// 从base64解码
|
|
@@ -840,12 +855,13 @@ class SmtpConnection extends EventEmitter {
|
|
|
840
855
|
const prepended = `${user} ${hmacMD5.digest('hex')}`;
|
|
841
856
|
|
|
842
857
|
this._responseActions.push(str => this.#actionAUTH_CRAM_MD5_PASS(str, callback));
|
|
843
|
-
this.#sendCommand(Buffer.from(prepended).toString('base64'),
|
|
858
|
+
this.#sendCommand(Buffer.from(prepended).toString('base64'),
|
|
859
|
+
Buffer.from(`${user} /* secret */`).toString('base64'));
|
|
844
860
|
}
|
|
845
861
|
|
|
846
862
|
/**
|
|
847
|
-
* 处理CRAM-MD5
|
|
848
|
-
*
|
|
863
|
+
* 处理CRAM-MD5认证的响应,如果没有错误,
|
|
864
|
+
* 用户可以视为已登录;开始等待要发送的消息
|
|
849
865
|
*
|
|
850
866
|
* @param {String} str 来自服务器的消息
|
|
851
867
|
*/
|
|
@@ -862,7 +878,7 @@ class SmtpConnection extends EventEmitter {
|
|
|
862
878
|
|
|
863
879
|
/**
|
|
864
880
|
* 处理AUTH LOGIN命令的响应
|
|
865
|
-
* 以'334 UGFzc3dvcmQ6'
|
|
881
|
+
* 以'334 UGFzc3dvcmQ6'('Password:'的base64编码)作为响应发送的数据
|
|
866
882
|
*
|
|
867
883
|
* @param {String} str 来自服务器的消息
|
|
868
884
|
*/
|
|
@@ -878,8 +894,8 @@ class SmtpConnection extends EventEmitter {
|
|
|
878
894
|
}
|
|
879
895
|
|
|
880
896
|
/**
|
|
881
|
-
*
|
|
882
|
-
*
|
|
897
|
+
* 处理认证的响应,如果没有错误,
|
|
898
|
+
* 用户可以视为已登录;开始等待要发送的消息
|
|
883
899
|
*
|
|
884
900
|
* @param {String} str 来自服务器的消息
|
|
885
901
|
*/
|
|
@@ -902,7 +918,8 @@ class SmtpConnection extends EventEmitter {
|
|
|
902
918
|
return callback(this.#formatError('无效的登录', 'EAUTH', str, `AUTH ${method}`));
|
|
903
919
|
}
|
|
904
920
|
|
|
905
|
-
this.logger.info({ tnx: 'smtp', username: user, action: 'authenticated', method }, '用户 %s 已认证',
|
|
921
|
+
this.logger.info({ tnx: 'smtp', username: user, action: 'authenticated', method }, '用户 %s 已认证',
|
|
922
|
+
JSON.stringify(user));
|
|
906
923
|
this.authenticated = true, callback(null, true);
|
|
907
924
|
}
|
|
908
925
|
|
|
@@ -927,9 +944,9 @@ class SmtpConnection extends EventEmitter {
|
|
|
927
944
|
this.#sendCommand(`RCPT TO:<${recipient}>${this.#getDsnRcptToArgs()}`);
|
|
928
945
|
};
|
|
929
946
|
|
|
930
|
-
//
|
|
947
|
+
// 流水线模式:一次性发送所有收件人
|
|
931
948
|
if (this._supportedExtensions.includes('PIPELINING')) while (rcptQueue.length) processRecipient(rcptQueue.shift());
|
|
932
|
-
else processRecipient(rcptQueue.shift()); //
|
|
949
|
+
else processRecipient(rcptQueue.shift()); // 非流水线模式:逐个发送收件
|
|
933
950
|
}
|
|
934
951
|
|
|
935
952
|
/**
|
|
@@ -971,7 +988,7 @@ class SmtpConnection extends EventEmitter {
|
|
|
971
988
|
* @param {String} str 来自服务器的消息
|
|
972
989
|
*/
|
|
973
990
|
#actionDATA(str, callback) {
|
|
974
|
-
// 如果不是23
|
|
991
|
+
// 如果不是23,则数据命令失败
|
|
975
992
|
if (!regexs.DATA_RESPONSE.test(str)) return callback(this.#formatError('数据命令失败', 'EENVELOPE', str, 'DATA'));
|
|
976
993
|
const { accepted, rejected, rejectedErrors } = this._envelope, response = { accepted, rejected };
|
|
977
994
|
|
|
@@ -986,7 +1003,8 @@ class SmtpConnection extends EventEmitter {
|
|
|
986
1003
|
* @param {String} str 来自服务器的消息
|
|
987
1004
|
*/
|
|
988
1005
|
#actionSMTPStream(str, callback) {
|
|
989
|
-
return str.startsWith('2') ? callback(null, str) : callback(this.#formatError('消息失败', 'EMESSAGE', str,
|
|
1006
|
+
return str.startsWith('2') ? callback(null, str) : callback(this.#formatError('消息失败', 'EMESSAGE', str,
|
|
1007
|
+
'DATA'));
|
|
990
1008
|
}
|
|
991
1009
|
|
|
992
1010
|
/**
|
|
@@ -1007,7 +1025,7 @@ class SmtpConnection extends EventEmitter {
|
|
|
1007
1025
|
return callback(err);
|
|
1008
1026
|
}
|
|
1009
1027
|
|
|
1010
|
-
return callback(null, final ? str : null); //
|
|
1028
|
+
return callback(null, final ? str : null); // 成功情况:总是调用回调
|
|
1011
1029
|
}
|
|
1012
1030
|
|
|
1013
1031
|
// 处理_XOAUTH2认证
|