@flun/mailer 2.1.23 → 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 +12 -9
- 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 +90 -77
- 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 +5 -5
package/lib/shared.js
CHANGED
|
@@ -31,7 +31,7 @@ const dnsLookup = dns.lookup;
|
|
|
31
31
|
*/
|
|
32
32
|
const dnsResolve = dns.resolve,
|
|
33
33
|
/**
|
|
34
|
-
* @param {number} family - IP
|
|
34
|
+
* @param {number} family - IP地址族(4=IPv4, 6=IPv6)
|
|
35
35
|
* @param {boolean} allowInternal - 是否允许内部网络接口
|
|
36
36
|
* @returns {boolean} 是否支持该地址族
|
|
37
37
|
*/
|
|
@@ -42,7 +42,7 @@ const dnsResolve = dns.resolve,
|
|
|
42
42
|
return !netInter ? true : Object.values(netInter).flat()
|
|
43
43
|
.some(i => (!i.internal || allowInternal) && (i.family === `IPv${family}` || i.family === family));
|
|
44
44
|
} catch (err) {
|
|
45
|
-
console.error('
|
|
45
|
+
console.error('获取网络接口信息失败:', err);
|
|
46
46
|
return true;
|
|
47
47
|
}
|
|
48
48
|
|
|
@@ -58,12 +58,15 @@ const dnsResolve = dns.resolve,
|
|
|
58
58
|
_resolver = (family, hostname, options = {}, callback) => {
|
|
59
59
|
// 检查地址族是否支持
|
|
60
60
|
if (!_isFamilySupported(family, options.allowInternalNetworkInterfaces)) return callback(null, []);
|
|
61
|
-
|
|
61
|
+
// 创建DNS解析器实例或使用默认的dns模块
|
|
62
|
+
const resolver = dns.Resolver ? new dns.Resolver(options) : dns,
|
|
62
63
|
|
|
63
64
|
// 可忽略的DNS错误代码列表
|
|
64
|
-
dnsErrs = new Set([dns.NODATA,
|
|
65
|
+
dnsErrs = new Set([dns.NODATA,
|
|
66
|
+
dns.NOTFOUND, dns.NOTIMP, dns.SERVFAIL, dns.CONNREFUSED, dns.REFUSED, 'EAI_AGAIN']);
|
|
65
67
|
resolver[`resolve${family}`](hostname, (err, addresses) => {
|
|
66
|
-
|
|
68
|
+
// 处理特定的DNS错误代码,返回空数组而不是错误
|
|
69
|
+
if (err) return callback(dnsErrs.has(err?.code) ? null : err, []);
|
|
67
70
|
const result = Array.isArray(addresses) ? addresses : [addresses].flat(); // 确保返回的是数组
|
|
68
71
|
return callback(null, result);
|
|
69
72
|
});
|
|
@@ -89,7 +92,7 @@ const dnsResolve = dns.resolve,
|
|
|
89
92
|
* 处理可读流的 readable 事件
|
|
90
93
|
* @param {stream.Readable} stream 可读流实例
|
|
91
94
|
* @param {Buffer[]} chunkArr 存储数据块的数组
|
|
92
|
-
* @param {{value: number}} lenRef 包含value
|
|
95
|
+
* @param {{value: number}} lenRef 包含value属性的对象,用于累加数据长度
|
|
93
96
|
*/
|
|
94
97
|
_handleReadableStream = (stream, chunkArr, lenRef) => {
|
|
95
98
|
let chunk;
|
|
@@ -100,7 +103,7 @@ const dnsResolve = dns.resolve,
|
|
|
100
103
|
|
|
101
104
|
/**
|
|
102
105
|
* 回调错误处理函数
|
|
103
|
-
* @param {Object} state
|
|
106
|
+
* @param {Object} state 状态对象,包含returned属性
|
|
104
107
|
* @param {Object} connection 连接对象
|
|
105
108
|
* @param {Function} callback 回调函数
|
|
106
109
|
* @param {Error} err 错误对象
|
|
@@ -112,7 +115,7 @@ const dnsResolve = dns.resolve,
|
|
|
112
115
|
},
|
|
113
116
|
|
|
114
117
|
/**
|
|
115
|
-
* 生成一个类似bunyan
|
|
118
|
+
* 生成一个类似bunyan的日志记录器,输出到控制台
|
|
116
119
|
* @param {array} levels - 日志级别数组
|
|
117
120
|
* @returns {object} Bunyan日志记录器实例
|
|
118
121
|
*/
|
|
@@ -137,7 +140,7 @@ const dnsResolve = dns.resolve,
|
|
|
137
140
|
const prefix = buildPrefix(entry), formattedMessage = format(message, ...args),// 构建前缀,格式化消息
|
|
138
141
|
timestamp = new Date().toISOString().substring(0, 19).replace('T', ' '); // 格式化时间戳
|
|
139
142
|
|
|
140
|
-
//
|
|
143
|
+
// 分行打印(处理多行消息)
|
|
141
144
|
formattedMessage.split(regexs.LINE_SEPARATOR).forEach(line => {
|
|
142
145
|
console.log('[%s] %s %s', timestamp, levelNames.get(level), prefix + line);
|
|
143
146
|
});
|
|
@@ -162,32 +165,34 @@ const dnsResolve = dns.resolve,
|
|
|
162
165
|
/**
|
|
163
166
|
* 返回bunyan兼容的日志记录器接口
|
|
164
167
|
* 查看定义:@see {@link getLogger}
|
|
165
|
-
* @param {object} options -
|
|
168
|
+
* @param {object} options - 配置选项,可能包含'logger'
|
|
166
169
|
* @return {object} bunyan兼容的日志记录器
|
|
167
170
|
*/
|
|
168
171
|
const getLogger = (options = {}, defaults) => {
|
|
169
172
|
const levels = ['trace', 'debug', 'info', 'warn', 'error', 'fatal'], { logger } = options;
|
|
170
|
-
if (!logger) return Object.fromEntries(levels.map(level => [level, () => false])); //
|
|
173
|
+
if (!logger) return Object.fromEntries(levels.map(level => [level, () => false])); // 如果没有提供日志记录器,创建空实现
|
|
171
174
|
|
|
172
|
-
|
|
175
|
+
// 如果logger为true,创建控制台日志记录器
|
|
176
|
+
const logger_ = logger === true ? _createDefaultLogger(levels) : logger;
|
|
173
177
|
// 包装日志方法
|
|
174
178
|
return Object.fromEntries(
|
|
175
|
-
levels.map(level => [level,
|
|
179
|
+
levels.map(level => [level,
|
|
180
|
+
(data, message, ...args) => _logFunc(logger_, level, defaults, data, message, ...args)])
|
|
176
181
|
);
|
|
177
182
|
};
|
|
178
183
|
|
|
179
184
|
/**
|
|
180
|
-
*
|
|
185
|
+
* 解析主机名,支持IPv4/IPv6和DNS缓存
|
|
181
186
|
* >查看定义:@see {@link resolveHostname}
|
|
182
187
|
* @param {object} options - 配置选项
|
|
183
188
|
* @param {function} callback - 回调函数
|
|
184
189
|
*/
|
|
185
190
|
const resolveHostname = (options = {}, callback) => {
|
|
186
191
|
const { host, servername: sName, dnsTtl } = options,
|
|
187
|
-
// 如果没有host但有servername
|
|
192
|
+
// 如果没有host但有servername,使用servername作为host;如果都没有,设置servername为false
|
|
188
193
|
newHost = !host && sName ? sName : host, servername = sName || newHost || false;
|
|
189
194
|
|
|
190
|
-
// 如果host是IP
|
|
195
|
+
// 如果host是IP地址或为空,直接返回
|
|
191
196
|
if (!newHost || isIP(newHost)) {
|
|
192
197
|
const value = { addresses: [newHost], servername };
|
|
193
198
|
return callback(null, _formatDNSValue(value, { cached: false }));
|
|
@@ -197,8 +202,9 @@ const resolveHostname = (options = {}, callback) => {
|
|
|
197
202
|
let cached;
|
|
198
203
|
if (DNS_MAP.has(newHost)) {
|
|
199
204
|
cached = DNS_MAP.get(newHost);
|
|
200
|
-
//
|
|
201
|
-
if (!cached.expires || cached.expires >= Date.now()) return callback(null,
|
|
205
|
+
// 如果缓存未过期,使用缓存值
|
|
206
|
+
if (!cached.expires || cached.expires >= Date.now()) return callback(null,
|
|
207
|
+
_formatDNSValue(cached.value, { cached: true }));
|
|
202
208
|
}
|
|
203
209
|
|
|
204
210
|
// 处理解析错误
|
|
@@ -214,10 +220,10 @@ const resolveHostname = (options = {}, callback) => {
|
|
|
214
220
|
|
|
215
221
|
// 首先尝试IPv4解析
|
|
216
222
|
_resolver(4, newHost, options, (err, addrV4) => {
|
|
217
|
-
if (err) return handleError(err); //
|
|
223
|
+
if (err) return handleError(err); // 如果出错但有缓存,使用过期的缓存值,否则返回错误
|
|
218
224
|
if (addrV4?.length) return handleSuccess(addrV4); // 如果IPv4解析成功,更新DNS缓存,返回成功
|
|
219
225
|
|
|
220
|
-
// IPv4
|
|
226
|
+
// IPv4失败,尝试IPv6解析
|
|
221
227
|
_resolver(6, newHost, options, (err, addrV6) => {
|
|
222
228
|
if (err) return handleError(err);
|
|
223
229
|
if (addrV6?.length) return handleSuccess(addrV6); // 如果IPv6解析成功,同理...
|
|
@@ -229,10 +235,10 @@ const resolveHostname = (options = {}, callback) => {
|
|
|
229
235
|
// 过滤出系统支持的地址族的地址(取第一个支持的地址)
|
|
230
236
|
const address = lookupAddr?.find(addr => _isFamilySupported(addr.family))?.address || false;
|
|
231
237
|
|
|
232
|
-
//
|
|
238
|
+
// 如果有地址但没有可用的,输出警告
|
|
233
239
|
if (lookupAddr?.length && !address) console.warn(`无法使用当前网络解析IPv${lookupAddr[0].family}地址`);
|
|
234
240
|
|
|
235
|
-
//
|
|
241
|
+
// 如果没有找到地址但有缓存,使用缓存;否则,使用找到的地址或原始主机名
|
|
236
242
|
return !address && cached ? callback(null, _formatDNSValue(cached.value, { cached: true })) :
|
|
237
243
|
handleSuccess(address ? [address] : [newHost]);
|
|
238
244
|
});
|
|
@@ -244,8 +250,8 @@ const resolveHostname = (options = {}, callback) => {
|
|
|
244
250
|
/**
|
|
245
251
|
* 尝试将给定的字符串解析为URL对象
|
|
246
252
|
* >查看定义:@see {@link newURL}
|
|
247
|
-
* @param {string} str - 需要解析的URL
|
|
248
|
-
* @returns {URL|boolean} 如果解析成功返回URL
|
|
253
|
+
* @param {string} str - 需要解析的URL字符串,默认为空字符串
|
|
254
|
+
* @returns {URL|boolean} 如果解析成功返回URL对象,否则返回false
|
|
249
255
|
*/
|
|
250
256
|
const newURL = (str = '') => {
|
|
251
257
|
try {
|
|
@@ -271,9 +277,11 @@ const parseConnectionUrl = str => {
|
|
|
271
277
|
return val;
|
|
272
278
|
},
|
|
273
279
|
options = {
|
|
274
|
-
...protocols[url.protocol], ...(url.port && { port: Number(url.port) }),
|
|
280
|
+
...protocols[url.protocol], ...(url.port && { port: Number(url.port) }),
|
|
281
|
+
...(url.hostname && { host: url.hostname }),
|
|
275
282
|
...((url.username || url.password) && {
|
|
276
|
-
auth: { user: decodeURIComponent(url.username),
|
|
283
|
+
auth: { user: decodeURIComponent(url.username),
|
|
284
|
+
pass: url.password ? decodeURIComponent(url.password) : '' }
|
|
277
285
|
})
|
|
278
286
|
};
|
|
279
287
|
|
|
@@ -301,11 +309,12 @@ const parseDataURI = uri => {
|
|
|
301
309
|
if (typeof uri !== 'string' || !uri.startsWith('data:')) return null; // 检查参数是否为字符串且以data:开头
|
|
302
310
|
|
|
303
311
|
const commaIndex = uri.indexOf(','); // 安全地查找第一个逗号的索引 -防止ReDoS攻击
|
|
304
|
-
if (commaIndex === -1) return null; //
|
|
312
|
+
if (commaIndex === -1) return null; // 如果没有找到逗号,返回
|
|
305
313
|
|
|
306
314
|
const data = uri.slice(commaIndex + 1), metaStr = uri.slice(5, commaIndex), metaEntries = metaStr.split(';'),
|
|
307
|
-
//
|
|
308
|
-
lastEntry = metaEntries.at(-1)?.toLowerCase().trim(),
|
|
315
|
+
// 提取编码类型(最后一个非键值对参数)
|
|
316
|
+
lastEntry = metaEntries.at(-1)?.toLowerCase().trim(),
|
|
317
|
+
contentType = metaEntries.shift() || 'application/octet-stream',
|
|
309
318
|
encoding = ['base64', 'utf8', 'utf-8'].includes(lastEntry) && !lastEntry.includes('=') ? metaEntries.pop() : null,
|
|
310
319
|
// 解析参数为键值对
|
|
311
320
|
params = Object.fromEntries(
|
|
@@ -375,7 +384,7 @@ const resolveStream = (stream, callback) => {
|
|
|
375
384
|
* @returns {Promise|null} 可能返回Promise
|
|
376
385
|
*/
|
|
377
386
|
const resolveContent = (data, key, callback) => {
|
|
378
|
-
//
|
|
387
|
+
// 如果没有提供回调,创建Promise
|
|
379
388
|
const promise = !callback ? new Promise((resolve, reject) => callback = callbackPromise(resolve, reject)) : null;
|
|
380
389
|
|
|
381
390
|
let content = data?.[key]?.content ?? data?.[key];
|
|
@@ -389,16 +398,17 @@ const resolveContent = (data, key, callback) => {
|
|
|
389
398
|
// 处理不同类型的content
|
|
390
399
|
if (typeof content === 'object') {
|
|
391
400
|
const { path, href, pipe } = content, source = path || href;
|
|
392
|
-
// 如果content
|
|
401
|
+
// 如果content是流,则解析
|
|
393
402
|
if (typeof pipe === 'function')
|
|
394
403
|
return resolveStream(content, (err, value) => {
|
|
395
404
|
if (err) return callback(err);
|
|
396
|
-
|
|
405
|
+
// 不能两次流式传输相同内容,所以需要替换流对象
|
|
406
|
+
data[key]?.content ? data[key].content = value : data[key] = value;
|
|
397
407
|
callback(null, value);
|
|
398
408
|
});
|
|
399
|
-
//
|
|
409
|
+
// 否则,如果content是URL,则解析URL
|
|
400
410
|
else if (regexs.HTTP_URL.test(source)) return resolveStream(nmfetch(source), callback);
|
|
401
|
-
// 否则,如果content是Data URI
|
|
411
|
+
// 否则,如果content是Data URI,则解析Data URI
|
|
402
412
|
else if (regexs.DATA_URL.test(source)) return callback(null, parseDataURI(source)?.data || Buffer.alloc(0));
|
|
403
413
|
// 处理文件路径
|
|
404
414
|
else if (path) return resolveStream(fsReadStream(path), callback);
|
|
@@ -406,8 +416,9 @@ const resolveContent = (data, key, callback) => {
|
|
|
406
416
|
|
|
407
417
|
// 处理特定编码的字符串
|
|
408
418
|
const keyC = data[key]?.content;
|
|
409
|
-
if (typeof keyC === 'string' && !['utf8', 'usascii', 'ascii'].includes(encoding)) content = Buffer.from(keyC,
|
|
410
|
-
|
|
419
|
+
if (typeof keyC === 'string' && !['utf8', 'usascii', 'ascii'].includes(encoding)) content = Buffer.from(keyC,
|
|
420
|
+
encoding);
|
|
421
|
+
setImmediate(() => callback(null, content)); // 默认操作,原样返回内容
|
|
411
422
|
return promise;
|
|
412
423
|
};
|
|
413
424
|
|
|
@@ -417,26 +428,26 @@ const resolveContent = (data, key, callback) => {
|
|
|
417
428
|
* @returns {object} 目标对象
|
|
418
429
|
*/
|
|
419
430
|
const assign = (target, ...sources) => {
|
|
420
|
-
target = target && typeof target === 'object' ? target : {}; // 确保 target
|
|
431
|
+
target = target && typeof target === 'object' ? target : {}; // 确保 target 是对象,如果是假值则重置为空对象
|
|
421
432
|
|
|
422
433
|
for (const source of sources) {
|
|
423
434
|
if (!source) continue;
|
|
424
435
|
for (const [k, v] of Object.entries(source))
|
|
425
|
-
// 如果key是tls或auth
|
|
436
|
+
// 如果key是tls或auth,并且value是对象,则递归合并对象
|
|
426
437
|
target[k] = ['tls', 'auth'].includes(k) && v && typeof v === 'object' ? { ...target[k], ...v } : v;
|
|
427
438
|
}
|
|
428
439
|
return target;
|
|
429
440
|
};
|
|
430
441
|
|
|
431
442
|
/**
|
|
432
|
-
* 对字符串进行XText
|
|
443
|
+
* 对字符串进行XText编码(SMTP扩展)
|
|
433
444
|
* >查看定义:@see {@link encodeXText}
|
|
434
445
|
* @param {string} str - 要编码的字符串
|
|
435
446
|
* @returns {string} 编码后的字符串
|
|
436
447
|
*/
|
|
437
448
|
const encodeXText = str => {
|
|
438
449
|
if (!regexs.NEED_ENCODING.test(str)) return str;
|
|
439
|
-
//
|
|
450
|
+
// 将字符串转换为字节数组,对需要编码的字符转换为"+十六进制"格式,否则保留原字符,最后拼接成字符串
|
|
440
451
|
return Array.from(Buffer.from(str)).map(c => c < 0x21 || c > 0x7E || c === 0x2B || c === 0x3D
|
|
441
452
|
? `+${c.toString(16).toUpperCase().padStart(2, '0')}` : String.fromCharCode(c)).join('');
|
|
442
453
|
};
|
|
@@ -457,7 +468,7 @@ const initSmtpConstructor = (instance, options, SmtpC, defaultComponent) => {
|
|
|
457
468
|
let urlData, service = s;
|
|
458
469
|
if (url) urlData = parseConnectionUrl(url), service = service || urlData.service; // 解析URL连接选项
|
|
459
470
|
|
|
460
|
-
//
|
|
471
|
+
// 合并选项:是否创建对象、常规选项、URL选项和知名服务选项
|
|
461
472
|
instance.options = assign(false, options, urlData, service && wellKnown(service));
|
|
462
473
|
const connection = new SmtpC(instance.options); // 创建临时连接对象用于获取版本信息
|
|
463
474
|
instance.version = `${PK.version}[client:${connection.version}]`;
|
|
@@ -470,7 +481,7 @@ const initSmtpConstructor = (instance, options, SmtpC, defaultComponent) => {
|
|
|
470
481
|
* 用于创建代理套接字的占位函数,此方法立即返回而不提供套接字
|
|
471
482
|
* >查看定义:@see {@link getSocket}
|
|
472
483
|
* @param {Object} options 连接选项
|
|
473
|
-
* @param {Function} callback
|
|
484
|
+
* @param {Function} callback 回调函数,用于处理套接字密钥
|
|
474
485
|
*/
|
|
475
486
|
const getSocket = (options, callback) => {
|
|
476
487
|
return setImmediate(() => callback(null, false)); // 立即返回空套接字
|
|
@@ -500,7 +511,8 @@ const createSmtpConnection = (transport, socketOptions, SmtpC) => {
|
|
|
500
511
|
// 处理代理socket情况
|
|
501
512
|
if (socketOptions?.connection) {
|
|
502
513
|
const { remoteAddress, remotePort } = socketOptions.connection, { host = '', port = '' } = newOptions;
|
|
503
|
-
transport.logger.info({ tnx: 'proxy', remoteAddress, remotePort, destHost: host, destPort: port,
|
|
514
|
+
transport.logger.info({ tnx: 'proxy', remoteAddress, remotePort, destHost: host, destPort: port,
|
|
515
|
+
action: 'connected' },
|
|
504
516
|
'使用来自 %s:%s 到 %s:%s 的代理socket', remoteAddress, remotePort, host, port
|
|
505
517
|
);
|
|
506
518
|
newOptions = { ...assign(false, newOptions), ...socketOptions };
|
|
@@ -569,7 +581,7 @@ const performSmtpAuthentication = (state, connection, auth, options, callback, t
|
|
|
569
581
|
const err = new Error('未提供认证信息');
|
|
570
582
|
err.code = 'NoAuth', _callbackErr(state, connection, callback, err, transport);
|
|
571
583
|
}
|
|
572
|
-
else onSuccess(); //
|
|
584
|
+
else onSuccess(); // 否则,直接执行成功回调
|
|
573
585
|
};
|
|
574
586
|
|
|
575
587
|
/**
|
|
@@ -577,7 +589,7 @@ const performSmtpAuthentication = (state, connection, auth, options, callback, t
|
|
|
577
589
|
* >查看定义:@see {@link createAuthConfig}
|
|
578
590
|
* @param {Object} authData 认证数据
|
|
579
591
|
* @param {Object} logger 日志记录器
|
|
580
|
-
* @param {Object} mailer
|
|
592
|
+
* @param {Object} mailer 邮件器实例(可选)
|
|
581
593
|
* @returns {Object|string} 认证配置对象或空字符串
|
|
582
594
|
*/
|
|
583
595
|
const createAuthConfig = (authData, logger, mailer = null, OAUTH2) => {
|
|
@@ -585,7 +597,8 @@ const createAuthConfig = (authData, logger, mailer = null, OAUTH2) => {
|
|
|
585
597
|
|
|
586
598
|
let hasAuth = false;
|
|
587
599
|
const authConfig = {}, mergeAuthData = source => {
|
|
588
|
-
if (source?.constructor === Object && Object.keys(source).length > 0) Object.assign(authConfig,
|
|
600
|
+
if (source?.constructor === Object && Object.keys(source).length > 0) Object.assign(authConfig,
|
|
601
|
+
source), hasAuth = true;
|
|
589
602
|
};
|
|
590
603
|
mergeAuthData(authData);
|
|
591
604
|
|
|
@@ -616,29 +629,29 @@ const createAuthConfig = (authData, logger, mailer = null, OAUTH2) => {
|
|
|
616
629
|
* >查看定义:@see {@link prepareMessageForSending}
|
|
617
630
|
* @param {Object} mail 邮件对象
|
|
618
631
|
* @param {Object} logger 日志记录器
|
|
619
|
-
* @param {String} connectionId 连接ID
|
|
620
|
-
* @param {Object} options
|
|
621
|
-
* @param {Function} done
|
|
632
|
+
* @param {String} connectionId 连接ID(可选)
|
|
633
|
+
* @param {Object} options 配置选项(可选)
|
|
634
|
+
* @param {Function} done 发送时执行的回调函数(可选)
|
|
622
635
|
* @returns {Object} 处理后的信封和消息信息
|
|
623
636
|
*/
|
|
624
637
|
const prepareMessageForSending = (mail, logger, connectionId = '', options = {}, done) => {
|
|
625
638
|
const { data, message, normalize } = mail, { envelope: En, dsn, _dkim, ses } = data, { _headers } = message;
|
|
626
|
-
message.keepBcc = true; // 保留BCC
|
|
639
|
+
message.keepBcc = true; // 保留BCC字段(适用于所有传输类型)
|
|
627
640
|
|
|
628
641
|
// 获取信封和消息ID
|
|
629
642
|
const envelope = En || message.getEnvelope(), messageId = message.messageId(), { from, to } = envelope,
|
|
630
643
|
rList = Array.from(to ?? []),
|
|
631
|
-
// 格式化收件人列表:仅显示前3
|
|
644
|
+
// 格式化收件人列表:仅显示前3个收件人,超过部分用省略号表示
|
|
632
645
|
recipients = rList.length > 3 ? [...rList.slice(0, 2), `...以及另外${rList.length - 2}个收件人`] : rList;
|
|
633
|
-
if (dsn) envelope.dsn = dsn; // 添加DSN
|
|
646
|
+
if (dsn) envelope.dsn = dsn; // 添加DSN信息(如果存在)
|
|
634
647
|
|
|
635
648
|
// 构建日志上下文
|
|
636
649
|
const logContext = { tnx: 'send', messageId, ...(connectionId && { cid: connectionId }) };
|
|
637
650
|
let logMessage, logParams, args, fromAddress = from; // 通用日志记录 - 适配不同传输类型的日志格式
|
|
638
651
|
if (options.transportType === 'json')
|
|
639
|
-
logMessage = '正在为 %s 构建JSON
|
|
652
|
+
logMessage = '正在为 %s 构建JSON结构,收件人: <%s>', logParams = [messageId, recipients.join(', ')];
|
|
640
653
|
else if (options.transportType === 'sendmail') {
|
|
641
|
-
//
|
|
654
|
+
// 检查无效地址格式(以连字符开头)
|
|
642
655
|
const hasInvalidAddresses = [...(from || []), ...(to || [])].some(addr => addr.startsWith('-'));
|
|
643
656
|
if (hasInvalidAddresses) return done(new Error('无法发送邮件,信封地址无效;'));
|
|
644
657
|
// 构建sendmail参数
|
|
@@ -651,13 +664,14 @@ const prepareMessageForSending = (mail, logger, connectionId = '', options = {},
|
|
|
651
664
|
const fromHeader = _headers?.find(header => header.key.toLowerCase() === 'from');
|
|
652
665
|
if (fromHeader?.value) fromAddress = fromHeader.value;
|
|
653
666
|
} catch (err) {
|
|
654
|
-
logger.warn({ err, messageId }, '解析 From
|
|
667
|
+
logger.warn({ err, messageId }, '解析 From 头失败,使用默认发件人地址: %s', from);
|
|
655
668
|
}
|
|
656
669
|
|
|
657
670
|
logMessage = '正在发送消息 %s 至 <%s>', logParams = [messageId, recipients.join(', ')];
|
|
658
671
|
} else if (options.transportType === 'stream') {
|
|
659
672
|
const lineBreakType = options.winbreak ? '<CR><LF>' : '<LF>';
|
|
660
|
-
logMessage = '正在发送消息 %s 到 <%s
|
|
673
|
+
logMessage = '正在发送消息 %s 到 <%s>,使用 %s 换行符',
|
|
674
|
+
logParams = [messageId, recipients.join(', '), lineBreakType];
|
|
661
675
|
} else {
|
|
662
676
|
// SMTP 和其他传输类型的默认日志格式
|
|
663
677
|
logMessage = connectionId ? '正在使用资源 #%s 发送消息 %s 至 <%s>' : '正在发送消息 %s 给 <%s>';
|
|
@@ -665,7 +679,8 @@ const prepareMessageForSending = (mail, logger, connectionId = '', options = {},
|
|
|
665
679
|
}
|
|
666
680
|
|
|
667
681
|
logger.info(logContext, logMessage, ...logParams);
|
|
668
|
-
return { normalize, envelope, messageId, from: fromAddress, to, _dkim, ses, args,
|
|
682
|
+
return { normalize, envelope, messageId, from: fromAddress, to, _dkim, ses, args,
|
|
683
|
+
readStream: message.createReadStream() };
|
|
669
684
|
};
|
|
670
685
|
|
|
671
686
|
/**
|
|
@@ -700,7 +715,7 @@ const handleSendResult = (result, callback, logger) => {
|
|
|
700
715
|
* @param {Object} auth 认证信息
|
|
701
716
|
*/
|
|
702
717
|
const verifySmtp = (transport, callback, SmtpC, auth = null) => {
|
|
703
|
-
//
|
|
718
|
+
// 如果没有提供回调,则创建Promise
|
|
704
719
|
const promise = !callback ? new Promise((resolve, reject) => callback = callbackPromise(resolve, reject)) : null;
|
|
705
720
|
|
|
706
721
|
getSocket(transport.options, (err, socketOp) => {
|
|
@@ -709,7 +724,7 @@ const verifySmtp = (transport, callback, SmtpC, auth = null) => {
|
|
|
709
724
|
state = { returned: false }, // 标记是否返回对象
|
|
710
725
|
|
|
711
726
|
finalize = (state, connection, callback) => {
|
|
712
|
-
cleanup(state), connection.quit(); //
|
|
727
|
+
cleanup(state), connection.quit(); // 对于验证操作,发送 QUIT 命令优雅关闭
|
|
713
728
|
return callback(null, true);
|
|
714
729
|
};
|
|
715
730
|
|
|
@@ -16,7 +16,7 @@ class DataStream extends Transform {
|
|
|
16
16
|
* 转义点号的处理函数
|
|
17
17
|
*/
|
|
18
18
|
_transform(chunk, encoding, done) {
|
|
19
|
-
if (typeof chunk === 'string') chunk = Buffer.from(chunk, encoding); // 如果chunk
|
|
19
|
+
if (typeof chunk === 'string') chunk = Buffer.from(chunk, encoding); // 如果chunk是字符串,则将其转换为Buffer
|
|
20
20
|
if (!chunk?.length) return done(); // 如果chunk为空则直接返回
|
|
21
21
|
const chunks = [];
|
|
22
22
|
let chunklen = 0, lastPos = 0, buf;
|
|
@@ -25,16 +25,17 @@ class DataStream extends Transform {
|
|
|
25
25
|
// 遍历chunk中的每个字节
|
|
26
26
|
for (let i = 0; i < chunk.length; i++) {
|
|
27
27
|
if (chunk[i] === 0x2e) {
|
|
28
|
-
// 遇到点号(.)
|
|
28
|
+
// 遇到点号(.),检查是否在行首,如果在,那么在行首的点号前额外添加一个点号进行转义
|
|
29
29
|
if ((i && chunk[i - 1] === 0x0a) || (!i && (!this.lastByte || this.lastByte === 0x0a))) {
|
|
30
30
|
buf = chunk.slice(lastPos, i + 1), chunks.push(buf), chunks.push(Buffer.from('.'));
|
|
31
31
|
chunklen += buf.length + 1, lastPos = i + 1;
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
else if (chunk[i] === 0x0a) {
|
|
35
|
-
// 遇到换行符(\n)
|
|
35
|
+
// 遇到换行符(\n),检查前面是否有回车符(\r)
|
|
36
36
|
if ((i && chunk[i - 1] !== 0x0d) || (!i && this.lastByte !== 0x0d)) {
|
|
37
|
-
i > lastPos ? (buf = chunk.slice(lastPos, i), chunks.push(buf), chunklen += buf.length + 2)
|
|
37
|
+
i > lastPos ? (buf = chunk.slice(lastPos, i), chunks.push(buf), chunklen += buf.length + 2)
|
|
38
|
+
: chunklen += 2;
|
|
38
39
|
chunks.push(Buffer.from('\r\n')), lastPos = i + 1;
|
|
39
40
|
}
|
|
40
41
|
}
|
|
@@ -46,20 +47,20 @@ class DataStream extends Transform {
|
|
|
46
47
|
if (lastPos < chunk.length) buf = chunk.slice(lastPos), chunks.push(buf), chunklen += buf.length;
|
|
47
48
|
this.outByteCount += chunklen, this.push(Buffer.concat(chunks, chunklen));
|
|
48
49
|
}
|
|
49
|
-
else this.outByteCount += chunk.length, this.push(chunk); //
|
|
50
|
+
else this.outByteCount += chunk.length, this.push(chunk); // 没有需要转义的情况,直接推送原数据
|
|
50
51
|
|
|
51
52
|
this.lastByte = chunk.at(-1); done(); // 记录最后一个字节后,完成处理
|
|
52
53
|
}
|
|
53
54
|
|
|
54
55
|
/**
|
|
55
|
-
*
|
|
56
|
+
* 结束流处理,添加终止序列
|
|
56
57
|
*/
|
|
57
58
|
_flush(done) {
|
|
58
59
|
let buf;
|
|
59
60
|
// 根据最后一个字节的类型添加适当的终止序列
|
|
60
|
-
if (this.lastByte === 0x0a) buf = Buffer.from('.\r\n'); // 如果最后是LF
|
|
61
|
-
else if (this.lastByte === 0x0d) buf = Buffer.from('\n.\r\n'); // 如果最后是CR
|
|
62
|
-
else buf = Buffer.from('\r\n.\r\n'); //
|
|
61
|
+
if (this.lastByte === 0x0a) buf = Buffer.from('.\r\n'); // 如果最后是LF,直接添加点号和CRLF
|
|
62
|
+
else if (this.lastByte === 0x0d) buf = Buffer.from('\n.\r\n'); // 如果最后是CR,添加LF、点号和CRLF
|
|
63
|
+
else buf = Buffer.from('\r\n.\r\n'); // 其他情况,添加完整的CRLF、点号和CRLF
|
|
63
64
|
|
|
64
65
|
this.outByteCount += buf.length, this.push(buf), done();
|
|
65
66
|
}
|