@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
package/lib/mime-node/index.js
CHANGED
|
@@ -22,15 +22,15 @@ const B64 = 'base64', QP = 'quoted-printable';
|
|
|
22
22
|
* 创建一个新的MIME树节点;如果是分支节点,假设内容类型为'multipart/*',
|
|
23
23
|
* 其他任何内容类型都视为叶节点,如果选项中缺少rootNode,则假设此为根节点;
|
|
24
24
|
* >查看定义:@see {@link MimeNode}
|
|
25
|
-
* @param {String} contentType
|
|
25
|
+
* @param {String} contentType 定义节点的内容类型,对于附件可以留空(从文件名派生)
|
|
26
26
|
* @param {Object} [options] 可选选项
|
|
27
27
|
* @param {Object} [options.rootNode] 此树的根节点
|
|
28
28
|
* @param {Object} [options.parentNode] 此节点的直接父节点
|
|
29
29
|
* @param {Object} [options.filename] 附件节点的文件名
|
|
30
30
|
* @param {String} [options.baseBoundary] 唯一多部分边界值的共享部分
|
|
31
|
-
* @param {Boolean} [options.keepBcc] 如果为true
|
|
31
|
+
* @param {Boolean} [options.keepBcc] 如果为true,不在生成的头部中排除Bcc
|
|
32
32
|
* @param {Function} [options.normalizeHeaderKey] 用于自定义头部键大小写规范化的方法
|
|
33
|
-
* @param {String} [options.textEncoding]
|
|
33
|
+
* @param {String} [options.textEncoding] 编码类型:'Q'(默认)或'B'
|
|
34
34
|
*/
|
|
35
35
|
class MimeNode {
|
|
36
36
|
constructor(contentType, options = {}) {
|
|
@@ -50,25 +50,29 @@ class MimeNode {
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
this.newline = newline; // 如果设置为'win'则使用\r\n,如果为'linux'则使用\n;如果未设置(或使用`raw`),则换行符保持原样
|
|
53
|
-
this.textEncoding = textEncoding.toString().trim().charAt(0).toUpperCase(); //
|
|
54
|
-
|
|
53
|
+
this.textEncoding = textEncoding.toString().trim().charAt(0).toUpperCase(); // 指示头部字符串应使用的编码:"Q"或"B"
|
|
54
|
+
// 此节点的直接父节点(如果未设置则为undefined)
|
|
55
|
+
this.parentNode = parentNode;
|
|
55
56
|
this.hostname = hostname; // 默认message - id值使用的主机名
|
|
56
57
|
|
|
57
58
|
this.date = new Date(); // 当前日期
|
|
58
59
|
this.childNodes = []; // 可能的子节点数组
|
|
59
|
-
this._headers = []; //
|
|
60
|
-
|
|
61
|
-
this.
|
|
62
|
-
|
|
60
|
+
this._headers = []; // 此节点的头部值列表,格式为[{ key: '', value: '' }]
|
|
61
|
+
// 在通过createReadStream暴露之前,消息将被管道传输的额外转换流
|
|
62
|
+
this._transforms = [];
|
|
63
|
+
// 在通过createReadStream暴露之前,消息将被管道传输的额外处理函数
|
|
64
|
+
this._processFuncs = [];
|
|
65
|
+
this._nodeId = ++this.rootNode.nodeCounter; // 用于生成唯一边界值(前缀添加到共享基础值之前)
|
|
63
66
|
this._isPlainText = false; // 如果内容仅使用ASCII可打印字符则为true
|
|
64
67
|
this._hasLongLines = false; // 如果内容是纯文本但有超过允许长度的行则为true
|
|
65
|
-
this._envelope = false; //
|
|
66
|
-
this._raw = false; //
|
|
67
|
-
|
|
68
|
+
this._envelope = false; // 如果设置,则使用此值作为信封而不是生成一个
|
|
69
|
+
this._raw = false; // 如果设置,则使用此值作为流内容而不是构建它
|
|
70
|
+
// 如果设置了内容类型(或从文件名派生),则将其添加到头部
|
|
71
|
+
if (contentType) this.setHeader('Content-Type', contentType);
|
|
68
72
|
}
|
|
69
73
|
|
|
70
74
|
/**
|
|
71
|
-
*
|
|
75
|
+
* 创建并附加一个子节点;提供的参数传递给MimeNode构造函数
|
|
72
76
|
*
|
|
73
77
|
* @param {String} [contentType] 可选内容类型
|
|
74
78
|
* @param {Object} [options] 可选选项对象
|
|
@@ -127,11 +131,12 @@ class MimeNode {
|
|
|
127
131
|
}
|
|
128
132
|
}
|
|
129
133
|
|
|
130
|
-
//
|
|
134
|
+
// 统一处理头部操作,支持多种输入格式和操作类型
|
|
131
135
|
#processHeadersOp(key, value, singleOp, isSet) {
|
|
132
|
-
//
|
|
136
|
+
// 如果是批量操作(没有value且key是对象)
|
|
133
137
|
if (!value && key && typeof key === 'object') {
|
|
134
|
-
// 允许{key:'content-type',value:'text/plain'},[{key:'content-type',value:'text/plain'}],
|
|
138
|
+
// 允许{key:'content-type',value:'text/plain'},[{key:'content-type',value:'text/plain'}],
|
|
139
|
+
// {'content-type':'text/plain'}
|
|
135
140
|
if (key.key && 'value' in key) singleOp.call(this, key.key, key.value);
|
|
136
141
|
else if (Array.isArray(key)) key.forEach(i => singleOp.call(this, i.key, i.value));
|
|
137
142
|
else Object.keys(key).forEach(i => singleOp.call(this, i, key[i]));
|
|
@@ -148,29 +153,29 @@ class MimeNode {
|
|
|
148
153
|
}
|
|
149
154
|
|
|
150
155
|
/**
|
|
151
|
-
*
|
|
152
|
-
* 可以使用[{key:'', value:''}]或{key: 'value'}
|
|
156
|
+
* 设置头部值;如果选定键的值已存在,则会被覆盖;
|
|
157
|
+
* 可以使用[{key:'', value:''}]或{key: 'value'}作为第一个参数来设置多个值;
|
|
153
158
|
*
|
|
154
159
|
* @param {String|Array|Object} key 头部键或键值对列表
|
|
155
160
|
* @param {String} value 头部值
|
|
156
161
|
* @return {Object} 当前节点
|
|
157
162
|
*/
|
|
158
163
|
setHeader(key, value) {
|
|
159
|
-
//
|
|
164
|
+
// 如果未处理批量操作,说明是单个操作,已经在#processHeadersOp中处理完成
|
|
160
165
|
if (!this.#processHeadersOp(key, value, this.setHeader, true)); // true表示是set操作
|
|
161
166
|
return this;
|
|
162
167
|
}
|
|
163
168
|
|
|
164
169
|
/**
|
|
165
|
-
*
|
|
166
|
-
* 您也可以使用[{key:'', value:''}]或{key: 'value'}
|
|
170
|
+
* 添加头部值;如果选定键的值已存在,则值将作为新字段附加,旧字段不被触动;
|
|
171
|
+
* 您也可以使用[{key:'', value:''}]或{key: 'value'}作为第一个参数来设置多个值;
|
|
167
172
|
*
|
|
168
173
|
* @param {String|Array|Object} key 头部键或键值对列表
|
|
169
174
|
* @param {String} value 头部值
|
|
170
175
|
* @return {Object} 当前节点
|
|
171
176
|
*/
|
|
172
177
|
addHeader(key, value) {
|
|
173
|
-
//
|
|
178
|
+
// 如果未处理批量操作,说明是单个操作,已经在#processHeadersOp中处理完成
|
|
174
179
|
if (!this.#processHeadersOp(key, value, this.addHeader, false)); // false表示是add操作
|
|
175
180
|
return this;
|
|
176
181
|
}
|
|
@@ -188,8 +193,8 @@ class MimeNode {
|
|
|
188
193
|
}
|
|
189
194
|
|
|
190
195
|
/**
|
|
191
|
-
*
|
|
192
|
-
* 如果值是Buffer
|
|
196
|
+
* 设置当前节点的正文内容;如果值是字符串,则自动将字符集添加到Content-Type(如果是text/*);
|
|
197
|
+
* 如果值是Buffer,则需要自行指定字符集
|
|
193
198
|
*
|
|
194
199
|
* @param (String|Buffer) content 正文内容
|
|
195
200
|
* @return {Object} 当前节点
|
|
@@ -197,14 +202,14 @@ class MimeNode {
|
|
|
197
202
|
setContent(content) {
|
|
198
203
|
this.content = content;
|
|
199
204
|
if (typeof this.content.pipe === 'function') {
|
|
200
|
-
//
|
|
205
|
+
// 预流处理程序;如果流被设置为内容,并且在此流执行任何操作之前触发'error',则可能被触发
|
|
201
206
|
this.contentErrorHandler = err => {
|
|
202
207
|
this.content.removeListener('error', this.contentErrorHandler), this.content = err;
|
|
203
208
|
};
|
|
204
209
|
this.content.once('error', this.contentErrorHandler);
|
|
205
210
|
} else if (typeof this.content === 'string') {
|
|
206
211
|
this._isPlainText = isPlainText(this.content);
|
|
207
|
-
// 如果有超过76
|
|
212
|
+
// 如果有超过76个符号/字节的行,则不使用7bit编码,而是使用8位编码;
|
|
208
213
|
if (this._isPlainText && hasLongerLines(this.content, 76)) this._hasLongLines = true;
|
|
209
214
|
}
|
|
210
215
|
return this;
|
|
@@ -212,7 +217,7 @@ class MimeNode {
|
|
|
212
217
|
|
|
213
218
|
// 构建MIME树
|
|
214
219
|
build(callback) {
|
|
215
|
-
//
|
|
220
|
+
// 如果没有回调,则返回一个承诺,该承诺将在构建完成时解析
|
|
216
221
|
const promise = !callback ? new Promise((resolve, reject) => callback = callbackPromise(resolve, reject)) : null;
|
|
217
222
|
resolveStream(this.createReadStream(), callback); // 将流数据读取到Buffer中
|
|
218
223
|
return promise;
|
|
@@ -227,12 +232,14 @@ class MimeNode {
|
|
|
227
232
|
encoding = (this.getHeader('Content-Transfer-Encoding') || '').toString().toLowerCase().trim();
|
|
228
233
|
if (!encoding || ![B64, QP].includes(encoding)) {
|
|
229
234
|
if (regexs.TEXT_TYPE.test(contentType)) {
|
|
230
|
-
if (this._isPlainText && !this._hasLongLines) encoding = '7bit'; //
|
|
231
|
-
|
|
235
|
+
if (this._isPlainText && !this._hasLongLines) encoding = '7bit'; // 如果没有特殊符号,则无需修改文本
|
|
236
|
+
// 否则,内容是字符串或Buffer,则用QP或Base64
|
|
237
|
+
else if (typeof content === 'string' || content instanceof Buffer)
|
|
232
238
|
encoding = this.#getTextEncoding(content) === 'Q' ? QP : B64;
|
|
233
|
-
|
|
239
|
+
// 否则,如果对于流,因无法检查内容,则使用首选编码或回退到QP
|
|
240
|
+
else encoding = this.textEncoding === 'B' ? B64 : QP;
|
|
234
241
|
}
|
|
235
|
-
// 否则,如果不是 multipart 或 message
|
|
242
|
+
// 否则,如果不是 multipart 或 message 类型,默认使用 base64 编码
|
|
236
243
|
else if (!regexs.MULTIPART_OR_MESSAGE.test(contentType)) encoding = encoding || B64;
|
|
237
244
|
}
|
|
238
245
|
}
|
|
@@ -240,7 +247,7 @@ class MimeNode {
|
|
|
240
247
|
}
|
|
241
248
|
|
|
242
249
|
/**
|
|
243
|
-
* 构建MIME
|
|
250
|
+
* 构建MIME节点的头部块;在写入内容之前追加 \r\n\r\n
|
|
244
251
|
*
|
|
245
252
|
* @returns {String} 头部
|
|
246
253
|
*/
|
|
@@ -290,27 +297,29 @@ class MimeNode {
|
|
|
290
297
|
break;
|
|
291
298
|
case C_TYPE:
|
|
292
299
|
structured = parseHeaderValue(value), this.#handleContentType(structured);
|
|
293
|
-
if (regexs.TEXT_PLAIN.test(structured.value)
|
|
300
|
+
if (regexs.TEXT_PLAIN.test(structured.value)
|
|
301
|
+
&& typeof content === 'string' && regexs.NON_ASCII.test(content))
|
|
294
302
|
structured.params.charset = 'utf-8';
|
|
295
303
|
|
|
296
304
|
value = buildHeaderValue(structured);
|
|
297
305
|
if (filename) {
|
|
298
|
-
// 为非兼容客户端(如QQ网页邮件)添加支持,不能使用buildHeaderValue
|
|
306
|
+
// 为非兼容客户端(如QQ网页邮件)添加支持,不能使用buildHeaderValue构建值,
|
|
307
|
+
// 因为该值是非标准的,并且不支持参数;
|
|
299
308
|
param = this.#encodeWords(filename);
|
|
300
|
-
//
|
|
309
|
+
// 如果参数与文件名匹配,则不添加参数;如果参数包含特殊字符,则添加引号;
|
|
301
310
|
if (param !== filename || regexs.UNSAFE_PARAM_VALUE.test(param)) param = `"${param}"`;
|
|
302
311
|
value += `;name=${param}`;
|
|
303
312
|
}
|
|
304
313
|
break;
|
|
305
314
|
|
|
306
315
|
case 'Bcc':
|
|
307
|
-
if (!this.keepBcc) return; // 如果不保留Bcc
|
|
316
|
+
if (!this.keepBcc) return; // 如果不保留Bcc,则不添加头部
|
|
308
317
|
break;
|
|
309
318
|
}
|
|
310
319
|
|
|
311
320
|
value = this.encodeHeaderValue(key, value);
|
|
312
321
|
if (!value.toString().trim()) return; // 跳过空行
|
|
313
|
-
// 如果存在自定义的normalizeHeaderKey
|
|
322
|
+
// 如果存在自定义的normalizeHeaderKey函数,则使用它来规范化头部键
|
|
314
323
|
if (typeof this.normalizeHeaderKey === 'function') {
|
|
315
324
|
let normalized = this.normalizeHeaderKey(key, value);
|
|
316
325
|
if (normalized && typeof normalized === 'string' && normalized.length) key = normalized;
|
|
@@ -323,8 +332,8 @@ class MimeNode {
|
|
|
323
332
|
}
|
|
324
333
|
|
|
325
334
|
/**
|
|
326
|
-
* 从当前节点流式传输rfc2822
|
|
327
|
-
*
|
|
335
|
+
* 从当前节点流式传输rfc2822消息;如果这是根节点,
|
|
336
|
+
* 则设置缺失的必需头部字段(Date, Message-Id, MIME-Version)
|
|
328
337
|
*
|
|
329
338
|
* @return {String} 编译后的消息 */
|
|
330
339
|
createReadStream(options = {}) {
|
|
@@ -351,7 +360,7 @@ class MimeNode {
|
|
|
351
360
|
// 在可能的用户转换之后确保终止换行符
|
|
352
361
|
transform = new LastNewline(), outputStream = pipeStream(outputStream, transform);
|
|
353
362
|
|
|
354
|
-
//
|
|
363
|
+
// 循环遍历所有后处理函数,并将输出流传递给每个函数;
|
|
355
364
|
for (let i = 0; i < this._processFuncs.length; i++) {
|
|
356
365
|
transform = this._processFuncs[i], outputStream = transform(outputStream);
|
|
357
366
|
}
|
|
@@ -367,7 +376,7 @@ class MimeNode {
|
|
|
367
376
|
}
|
|
368
377
|
|
|
369
378
|
/**
|
|
370
|
-
*
|
|
379
|
+
* 将转换流对象附加到转换列表;最终输出在暴露之前通过此流传递
|
|
371
380
|
*
|
|
372
381
|
* @param {Object} transform 读写流
|
|
373
382
|
*/
|
|
@@ -376,7 +385,7 @@ class MimeNode {
|
|
|
376
385
|
}
|
|
377
386
|
|
|
378
387
|
/**
|
|
379
|
-
*
|
|
388
|
+
* 附加后处理函数;该函数在转换之后运行,并使用以下语法
|
|
380
389
|
*
|
|
381
390
|
* processFunc(input) -> outputStream
|
|
382
391
|
*
|
|
@@ -393,7 +402,7 @@ class MimeNode {
|
|
|
393
402
|
const callback = args => {
|
|
394
403
|
cleanup(state), done(args);
|
|
395
404
|
},
|
|
396
|
-
//
|
|
405
|
+
// 对于多部分节点,推送子节点;对于内容节点,结束流
|
|
397
406
|
finalize = () => {
|
|
398
407
|
let childId = 0;
|
|
399
408
|
const processChildNode = () => {
|
|
@@ -416,7 +425,7 @@ class MimeNode {
|
|
|
416
425
|
sendContent = () => {
|
|
417
426
|
const content = this.content;
|
|
418
427
|
if (content) {
|
|
419
|
-
//
|
|
428
|
+
// 如果内容是错误,则直接调用回调
|
|
420
429
|
if (Object.prototype.toString.call(content) === '[object Error]') return callback(content);
|
|
421
430
|
|
|
422
431
|
if (typeof content.pipe === 'function') {
|
|
@@ -455,12 +464,13 @@ class MimeNode {
|
|
|
455
464
|
|
|
456
465
|
if (this._raw) {
|
|
457
466
|
setImmediate(() => {
|
|
458
|
-
//
|
|
467
|
+
// 如果内容是错误,则直接调用回调;如果设置错误处理程序,则将其移除
|
|
459
468
|
if (Object.prototype.toString.call(this._raw) === '[object Error]') return callback(this._raw);
|
|
460
469
|
if (typeof this._raw.pipe === 'function') this._raw.removeListener('error', this.contentErrorHandler);
|
|
461
470
|
|
|
462
471
|
const raw = this.#getStream(this._raw);
|
|
463
|
-
raw.pipe(outputStream, { end: false })
|
|
472
|
+
raw.pipe(outputStream, { end: false })
|
|
473
|
+
, raw.on('end', finalize).on('error', err => outputStream.emit('error', err));
|
|
464
474
|
});
|
|
465
475
|
}
|
|
466
476
|
else outputStream.write(`${this.buildHeaders()}\r\n\r\n`), setImmediate(sendContent);
|
|
@@ -469,7 +479,7 @@ class MimeNode {
|
|
|
469
479
|
/**
|
|
470
480
|
* 设置要使用的信封而不是生成的信封
|
|
471
481
|
*
|
|
472
|
-
* @return {Object} SMTP
|
|
482
|
+
* @return {Object} SMTP信封,格式为 {from: 'from@example.com', to: ['to@example.com']}
|
|
473
483
|
*/
|
|
474
484
|
setEnvelope(envelope) {
|
|
475
485
|
this._envelope = { from: false, to: [] };
|
|
@@ -482,11 +492,13 @@ class MimeNode {
|
|
|
482
492
|
|
|
483
493
|
const { to } = this._envelope, standardFields = ['to', 'cc', 'bcc', 'from'];
|
|
484
494
|
['to', 'cc', 'bcc'].forEach(key => {
|
|
485
|
-
|
|
495
|
+
// 如果存在,则将地址添加到信封中
|
|
496
|
+
if (envelope[key]) this.#convertAddresses(this.parseAddresses(envelope[key]), to);
|
|
486
497
|
});
|
|
487
498
|
this._envelope.to = to.map(to => to.address).filter(address => address);
|
|
488
499
|
Object.keys(envelope).forEach(key => {
|
|
489
|
-
|
|
500
|
+
// 如果字段不是标准字段,则将其添加到信封中
|
|
501
|
+
if (!standardFields.includes(key)) this._envelope[key] = envelope[key];
|
|
490
502
|
});
|
|
491
503
|
|
|
492
504
|
return this;
|
|
@@ -513,7 +525,7 @@ class MimeNode {
|
|
|
513
525
|
/**
|
|
514
526
|
* 生成并返回包含发件人地址和收件人地址列表的SMTP信封
|
|
515
527
|
*
|
|
516
|
-
* @return {Object} SMTP
|
|
528
|
+
* @return {Object} SMTP信封,格式为 {from: 'from@example.com', to: ['to@example.com']}
|
|
517
529
|
*/
|
|
518
530
|
getEnvelope() {
|
|
519
531
|
if (this._envelope) return this._envelope;
|
|
@@ -532,7 +544,7 @@ class MimeNode {
|
|
|
532
544
|
}
|
|
533
545
|
|
|
534
546
|
/**
|
|
535
|
-
* 返回Message-Id
|
|
547
|
+
* 返回Message-Id值;如果不存在,则创建一个
|
|
536
548
|
*
|
|
537
549
|
* @return {String} Message-Id值
|
|
538
550
|
*/
|
|
@@ -541,14 +553,14 @@ class MimeNode {
|
|
|
541
553
|
}
|
|
542
554
|
|
|
543
555
|
/**
|
|
544
|
-
*
|
|
556
|
+
* 设置预生成的内容,该内容将用作此节点的输出
|
|
545
557
|
*
|
|
546
558
|
* @param {String|Buffer|Stream} 原始MIME内容
|
|
547
559
|
*/
|
|
548
560
|
setRaw(raw) {
|
|
549
561
|
this._raw = raw;
|
|
550
562
|
if (this._raw && typeof this._raw.pipe === 'function') {
|
|
551
|
-
//
|
|
563
|
+
// 预流处理程序;如果流被设置为内容,并且在此流执行任何操作之前触发'error',则可能被触发
|
|
552
564
|
this.contentErrorHandler = err => {
|
|
553
565
|
this._raw.removeListener('error', this.contentErrorHandler), this._raw = err;
|
|
554
566
|
};
|
|
@@ -559,7 +571,7 @@ class MimeNode {
|
|
|
559
571
|
}
|
|
560
572
|
|
|
561
573
|
/**
|
|
562
|
-
*
|
|
574
|
+
* 检测并返回与内容相关的流的句柄;
|
|
563
575
|
*
|
|
564
576
|
* @param {Mixed} content 节点内容
|
|
565
577
|
* @returns {Object} 流对象
|
|
@@ -585,25 +597,26 @@ class MimeNode {
|
|
|
585
597
|
return stream;
|
|
586
598
|
}, { _resolvedValue, pipe, path, href, httpHeaders } = content;
|
|
587
599
|
if (_resolvedValue) return createContentStream(_resolvedValue); // 处理已解析的字符串或Buffer类型内容
|
|
588
|
-
else if (typeof pipe === 'function') return content; //
|
|
589
|
-
//
|
|
600
|
+
else if (typeof pipe === 'function') return content; // 如果是流本身,则直接返回该流
|
|
601
|
+
// 如果是文件路径:根据访问权限返回文件流或错误流
|
|
590
602
|
else if (content && typeof path === 'string' && !href)
|
|
591
603
|
return this.disableFileAccess ? createErrorStream(`文件访问被拒绝:${path}`) : fsReadStream(path);
|
|
592
|
-
// 如果是URL
|
|
604
|
+
// 如果是URL:根据访问权限返回网络流或错误流
|
|
593
605
|
else if (content && typeof href === 'string')
|
|
594
|
-
return this.disableUrlAccess ? createErrorStream(`URL访问被拒绝:${href}`)
|
|
595
|
-
|
|
606
|
+
return this.disableUrlAccess ? createErrorStream(`URL访问被拒绝:${href}`)
|
|
607
|
+
: nmfetch(href, { headers: httpHeaders });
|
|
608
|
+
return createContentStream(content || ''); // 兜底处理:将其他类型内容转换为流
|
|
596
609
|
}
|
|
597
610
|
|
|
598
611
|
/**
|
|
599
|
-
*
|
|
612
|
+
* 解析地址;接受单个地址或数组或地址数组的数组(例如To: [[第一组], [第二组],...])
|
|
600
613
|
*
|
|
601
614
|
* @param {Mixed} addresses 要解析的地址
|
|
602
615
|
* @return {Array} 地址对象数组
|
|
603
616
|
*/
|
|
604
617
|
parseAddresses(addresses) {
|
|
605
618
|
return [].concat.apply([], [].concat(addresses).map(mAddress => {
|
|
606
|
-
//
|
|
619
|
+
// 如果地址和名称都存在,则将其转换为数组
|
|
607
620
|
if (mAddress?.address) {
|
|
608
621
|
const { address, name = '' } = mAddress;
|
|
609
622
|
mAddress.address = this.#normalizeAddress(address), mAddress.name = name;
|
|
@@ -614,7 +627,7 @@ class MimeNode {
|
|
|
614
627
|
}
|
|
615
628
|
|
|
616
629
|
/**
|
|
617
|
-
*
|
|
630
|
+
* 规范化头部键,使用驼峰式,除了大写的MIME-
|
|
618
631
|
*
|
|
619
632
|
* @param {String} key 要规范化的键
|
|
620
633
|
* @return {String} 驼峰式形式的键
|
|
@@ -629,8 +642,8 @@ class MimeNode {
|
|
|
629
642
|
}
|
|
630
643
|
|
|
631
644
|
/**
|
|
632
|
-
*
|
|
633
|
-
*
|
|
645
|
+
* 检查内容类型是否为多部分,并在需要时定义边界;
|
|
646
|
+
* 不返回任何内容,而是修改对象参数;
|
|
634
647
|
*
|
|
635
648
|
* @param {Object} structured 为'Content-Type'键解析的头部值
|
|
636
649
|
*/
|
|
@@ -650,14 +663,15 @@ class MimeNode {
|
|
|
650
663
|
}
|
|
651
664
|
|
|
652
665
|
/**
|
|
653
|
-
* 对生成的rfc2822
|
|
666
|
+
* 对生成的rfc2822电子邮件中使用的头部值进行编码;
|
|
654
667
|
*
|
|
655
668
|
* @param {String} key 头部键
|
|
656
669
|
* @param {String} value 头部值
|
|
657
670
|
*/
|
|
658
671
|
encodeHeaderValue(key, value = '') {
|
|
659
672
|
key = this.#normalizeHeaderKey(key);
|
|
660
|
-
const rL = resetRegex(regexs.LINEBREAKS), rAB = resetRegex(regexs.ANGLE_BRACKET),
|
|
673
|
+
const rL = resetRegex(regexs.LINEBREAKS), rAB = resetRegex(regexs.ANGLE_BRACKET),
|
|
674
|
+
rBR = resetRegex(regexs.BLANK_REGEX),
|
|
661
675
|
cleanValue = (val = '') => {
|
|
662
676
|
return val.toString().replace(rL, ' '); // 将值转换为字符串,并替换掉换行符
|
|
663
677
|
},
|
|
@@ -680,7 +694,7 @@ class MimeNode {
|
|
|
680
694
|
case 'In-Reply-To':
|
|
681
695
|
case 'Content-Id':
|
|
682
696
|
return ensureAngleBrackets(this.#encodeWords(cleanValue(value)));
|
|
683
|
-
//
|
|
697
|
+
// 用<>括起来的值,以空格分隔的列表
|
|
684
698
|
case 'References':
|
|
685
699
|
value = [].concat(value)
|
|
686
700
|
// 将值转换为字符串,删除尖括号中的空白字符,然后按空白字符分割
|
|
@@ -691,8 +705,8 @@ class MimeNode {
|
|
|
691
705
|
if (value instanceof Date) return value.toUTCString().replace('GMT', '+0000');
|
|
692
706
|
return this.#encodeWords(cleanValue(value));
|
|
693
707
|
case 'Content-Type':
|
|
694
|
-
case 'Content-Disposition': return cleanValue(value); //
|
|
695
|
-
default: return this.#encodeWords(cleanValue(value)); // 默认encodeWords
|
|
708
|
+
case 'Content-Disposition': return cleanValue(value); // 如果包含文件名,则已编码
|
|
709
|
+
default: return this.#encodeWords(cleanValue(value)); // 默认encodeWords仅在需要时编码,否则返回原始字符串
|
|
696
710
|
}
|
|
697
711
|
}
|
|
698
712
|
|
|
@@ -734,7 +748,7 @@ class MimeNode {
|
|
|
734
748
|
address = address.toString().replace(rFC, ' ').trim();
|
|
735
749
|
|
|
736
750
|
const lIndex = address.lastIndexOf('@');
|
|
737
|
-
if (lIndex < 0) return address; //
|
|
751
|
+
if (lIndex < 0) return address; // 没有@符号,保持原样
|
|
738
752
|
|
|
739
753
|
let user = address.substring(0, lIndex), encodedDomain;
|
|
740
754
|
const domain = address.substring(lIndex + 1);
|
|
@@ -754,9 +768,9 @@ class MimeNode {
|
|
|
754
768
|
|
|
755
769
|
/**
|
|
756
770
|
* 对邮件地址中的名称部分进行编码处理
|
|
757
|
-
* 如果名称包含非ASCII
|
|
771
|
+
* 如果名称包含非ASCII字符或特殊字符,则进行MIME编码或引号包裹
|
|
758
772
|
*
|
|
759
|
-
* @param {String} name
|
|
773
|
+
* @param {String} name 邮件地址中的名称部分(如 "张三" in "张三<zhangsan@example.com>")
|
|
760
774
|
* @returns {String} 编码后的名称字符串
|
|
761
775
|
*/
|
|
762
776
|
#encodeAddressName(name) {
|
|
@@ -788,7 +802,7 @@ class MimeNode {
|
|
|
788
802
|
*/
|
|
789
803
|
#getTextEncoding(value = '') {
|
|
790
804
|
value = value.toString();
|
|
791
|
-
if (this.textEncoding) return this.textEncoding; //
|
|
805
|
+
if (this.textEncoding) return this.textEncoding; // 如果有预设编码,直接返回
|
|
792
806
|
const rNL = resetRegex(regexs.NON_LATIN), rL = resetRegex(regexs.LATIN),
|
|
793
807
|
nonLatinLen = (value.match(rNL) || []).length, latinLen = (value.match(rL) || []).length;
|
|
794
808
|
return nonLatinLen < latinLen ? 'Q' : 'B';
|
|
@@ -800,7 +814,7 @@ class MimeNode {
|
|
|
800
814
|
* @return {String} 随机Message-ID值
|
|
801
815
|
*/
|
|
802
816
|
#generateMessageId() {
|
|
803
|
-
// 使用不同长度的随机字节生成类似UUID
|
|
817
|
+
// 使用不同长度的随机字节生成类似UUID的格式,优先使用发件人地址中的域名,服务器主机名或localhost次之
|
|
804
818
|
const randomPart = [4, 2, 2, 2, 6].map(len => randomBytes(len).toString('hex')).join('-'),
|
|
805
819
|
domain = (this.getEnvelope().from || this.hostname || 'localhost').split('@').pop();
|
|
806
820
|
return `<${randomPart}@${domain}>`; // 返回符合RFC标准的Message-ID格式
|
|
@@ -18,8 +18,8 @@ class LastNewline extends Transform {
|
|
|
18
18
|
* @param {function} done - 完成回调
|
|
19
19
|
*/
|
|
20
20
|
_transform(chunk, encoding, done) {
|
|
21
|
-
if (typeof chunk === 'string') chunk = Buffer.from(chunk, encoding); //
|
|
22
|
-
if (chunk.length) this.lastByte = chunk.at(-1); //
|
|
21
|
+
if (typeof chunk === 'string') chunk = Buffer.from(chunk, encoding); // 如果是字符串,转换为Buffer
|
|
22
|
+
if (chunk.length) this.lastByte = chunk.at(-1); // 如果数据块不为空,记录最后一个字节
|
|
23
23
|
this.push(chunk), done(); // 将数据块推送到下一个流;完成
|
|
24
24
|
}
|
|
25
25
|
|
|
@@ -28,9 +28,9 @@ class LastNewline extends Transform {
|
|
|
28
28
|
* @param {function} done - 完成回调
|
|
29
29
|
*/
|
|
30
30
|
_flush(done) {
|
|
31
|
-
if (this.lastByte === 0x0a) return done(); //
|
|
31
|
+
if (this.lastByte === 0x0a) return done(); // 如果最后一个字节是换行符,直接结束
|
|
32
32
|
|
|
33
|
-
//
|
|
33
|
+
// 如果最后一个字节是回车符,添加换行符
|
|
34
34
|
if (this.lastByte === 0x0d) {
|
|
35
35
|
this.push(Buffer.from('\n'));
|
|
36
36
|
return done();
|
package/lib/mime-node/le-unix.js
CHANGED
|
@@ -27,9 +27,9 @@ class LeUnix extends Transform {
|
|
|
27
27
|
if (chunk[i] === 0x0d) buf = chunk.subarray(lastPos, i), lastPos = i + 1, this.push(buf);
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
//
|
|
30
|
+
// 如果剩余数据为真,则推送最后一个回车符之后的数据
|
|
31
31
|
if (lastPos && lastPos < chunk.length) buf = chunk.subarray(lastPos), this.push(buf);
|
|
32
|
-
else if (!lastPos) this.push(chunk); // 否则,
|
|
32
|
+
else if (!lastPos) this.push(chunk); // 否则, 如果没有找到回车符,直接推送整个数据块
|
|
33
33
|
done();
|
|
34
34
|
}
|
|
35
35
|
}
|
|
@@ -8,17 +8,17 @@ import { Transform } from 'stream';
|
|
|
8
8
|
class LeWindows extends Transform {
|
|
9
9
|
constructor(options = {}) {
|
|
10
10
|
super(options);
|
|
11
|
-
this.options = options, this.lastByte = false; //
|
|
11
|
+
this.options = options, this.lastByte = false; // 记录上一个字节,用于跨 chunk 的换行符检测
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
|
-
*
|
|
15
|
+
* 转换函数:确保所有换行符都是 \r\n 格式
|
|
16
16
|
*/
|
|
17
17
|
_transform(chunk, encoding, done) {
|
|
18
18
|
if (chunk.length === 0) return done();
|
|
19
19
|
if (typeof chunk === 'string') chunk = Buffer.from(chunk, encoding); // 如果chunk是字符串,根据encoding转换为Buffer
|
|
20
20
|
|
|
21
|
-
//
|
|
21
|
+
// 快速路径:无换行符直接推送
|
|
22
22
|
if (!chunk.includes(0x0a)) {
|
|
23
23
|
this.push(chunk), this.lastByte = chunk.at(-1);
|
|
24
24
|
return done();
|