@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/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
|
*/
|
|
@@ -265,9 +272,8 @@ class MimeNode {
|
|
|
265
272
|
}
|
|
266
273
|
|
|
267
274
|
this._headers.forEach(header => {
|
|
268
|
-
let structured, param, value = header.value;
|
|
269
|
-
const
|
|
270
|
-
formattedHeaders = ['From', 'Sender', 'To', 'Cc', 'Bcc', 'Reply-To', 'Date', 'References'];
|
|
275
|
+
let structured, param, value = header.value, key = header.key;
|
|
276
|
+
const options = {}, formattedHeaders = ['From', 'Sender', 'To', 'Cc', 'Bcc', 'Reply-To', 'Date', 'References'];
|
|
271
277
|
|
|
272
278
|
if (value && typeof value === 'object' && !formattedHeaders.includes(key)) {
|
|
273
279
|
Object.keys(value).forEach(key => {
|
|
@@ -291,27 +297,29 @@ class MimeNode {
|
|
|
291
297
|
break;
|
|
292
298
|
case C_TYPE:
|
|
293
299
|
structured = parseHeaderValue(value), this.#handleContentType(structured);
|
|
294
|
-
if (regexs.TEXT_PLAIN.test(structured.value)
|
|
300
|
+
if (regexs.TEXT_PLAIN.test(structured.value)
|
|
301
|
+
&& typeof content === 'string' && regexs.NON_ASCII.test(content))
|
|
295
302
|
structured.params.charset = 'utf-8';
|
|
296
303
|
|
|
297
304
|
value = buildHeaderValue(structured);
|
|
298
305
|
if (filename) {
|
|
299
|
-
// 为非兼容客户端(如QQ网页邮件)添加支持,不能使用buildHeaderValue
|
|
306
|
+
// 为非兼容客户端(如QQ网页邮件)添加支持,不能使用buildHeaderValue构建值,
|
|
307
|
+
// 因为该值是非标准的,并且不支持参数;
|
|
300
308
|
param = this.#encodeWords(filename);
|
|
301
|
-
//
|
|
309
|
+
// 如果参数与文件名匹配,则不添加参数;如果参数包含特殊字符,则添加引号;
|
|
302
310
|
if (param !== filename || regexs.UNSAFE_PARAM_VALUE.test(param)) param = `"${param}"`;
|
|
303
311
|
value += `;name=${param}`;
|
|
304
312
|
}
|
|
305
313
|
break;
|
|
306
314
|
|
|
307
315
|
case 'Bcc':
|
|
308
|
-
if (!this.keepBcc) return; // 如果不保留Bcc
|
|
316
|
+
if (!this.keepBcc) return; // 如果不保留Bcc,则不添加头部
|
|
309
317
|
break;
|
|
310
318
|
}
|
|
311
319
|
|
|
312
320
|
value = this.encodeHeaderValue(key, value);
|
|
313
321
|
if (!value.toString().trim()) return; // 跳过空行
|
|
314
|
-
// 如果存在自定义的normalizeHeaderKey
|
|
322
|
+
// 如果存在自定义的normalizeHeaderKey函数,则使用它来规范化头部键
|
|
315
323
|
if (typeof this.normalizeHeaderKey === 'function') {
|
|
316
324
|
let normalized = this.normalizeHeaderKey(key, value);
|
|
317
325
|
if (normalized && typeof normalized === 'string' && normalized.length) key = normalized;
|
|
@@ -324,8 +332,8 @@ class MimeNode {
|
|
|
324
332
|
}
|
|
325
333
|
|
|
326
334
|
/**
|
|
327
|
-
* 从当前节点流式传输rfc2822
|
|
328
|
-
*
|
|
335
|
+
* 从当前节点流式传输rfc2822消息;如果这是根节点,
|
|
336
|
+
* 则设置缺失的必需头部字段(Date, Message-Id, MIME-Version)
|
|
329
337
|
*
|
|
330
338
|
* @return {String} 编译后的消息 */
|
|
331
339
|
createReadStream(options = {}) {
|
|
@@ -352,7 +360,7 @@ class MimeNode {
|
|
|
352
360
|
// 在可能的用户转换之后确保终止换行符
|
|
353
361
|
transform = new LastNewline(), outputStream = pipeStream(outputStream, transform);
|
|
354
362
|
|
|
355
|
-
//
|
|
363
|
+
// 循环遍历所有后处理函数,并将输出流传递给每个函数;
|
|
356
364
|
for (let i = 0; i < this._processFuncs.length; i++) {
|
|
357
365
|
transform = this._processFuncs[i], outputStream = transform(outputStream);
|
|
358
366
|
}
|
|
@@ -368,7 +376,7 @@ class MimeNode {
|
|
|
368
376
|
}
|
|
369
377
|
|
|
370
378
|
/**
|
|
371
|
-
*
|
|
379
|
+
* 将转换流对象附加到转换列表;最终输出在暴露之前通过此流传递
|
|
372
380
|
*
|
|
373
381
|
* @param {Object} transform 读写流
|
|
374
382
|
*/
|
|
@@ -377,7 +385,7 @@ class MimeNode {
|
|
|
377
385
|
}
|
|
378
386
|
|
|
379
387
|
/**
|
|
380
|
-
*
|
|
388
|
+
* 附加后处理函数;该函数在转换之后运行,并使用以下语法
|
|
381
389
|
*
|
|
382
390
|
* processFunc(input) -> outputStream
|
|
383
391
|
*
|
|
@@ -394,7 +402,7 @@ class MimeNode {
|
|
|
394
402
|
const callback = args => {
|
|
395
403
|
cleanup(state), done(args);
|
|
396
404
|
},
|
|
397
|
-
//
|
|
405
|
+
// 对于多部分节点,推送子节点;对于内容节点,结束流
|
|
398
406
|
finalize = () => {
|
|
399
407
|
let childId = 0;
|
|
400
408
|
const processChildNode = () => {
|
|
@@ -417,7 +425,7 @@ class MimeNode {
|
|
|
417
425
|
sendContent = () => {
|
|
418
426
|
const content = this.content;
|
|
419
427
|
if (content) {
|
|
420
|
-
//
|
|
428
|
+
// 如果内容是错误,则直接调用回调
|
|
421
429
|
if (Object.prototype.toString.call(content) === '[object Error]') return callback(content);
|
|
422
430
|
|
|
423
431
|
if (typeof content.pipe === 'function') {
|
|
@@ -456,12 +464,13 @@ class MimeNode {
|
|
|
456
464
|
|
|
457
465
|
if (this._raw) {
|
|
458
466
|
setImmediate(() => {
|
|
459
|
-
//
|
|
467
|
+
// 如果内容是错误,则直接调用回调;如果设置错误处理程序,则将其移除
|
|
460
468
|
if (Object.prototype.toString.call(this._raw) === '[object Error]') return callback(this._raw);
|
|
461
469
|
if (typeof this._raw.pipe === 'function') this._raw.removeListener('error', this.contentErrorHandler);
|
|
462
470
|
|
|
463
471
|
const raw = this.#getStream(this._raw);
|
|
464
|
-
raw.pipe(outputStream, { end: false })
|
|
472
|
+
raw.pipe(outputStream, { end: false })
|
|
473
|
+
, raw.on('end', finalize).on('error', err => outputStream.emit('error', err));
|
|
465
474
|
});
|
|
466
475
|
}
|
|
467
476
|
else outputStream.write(`${this.buildHeaders()}\r\n\r\n`), setImmediate(sendContent);
|
|
@@ -470,7 +479,7 @@ class MimeNode {
|
|
|
470
479
|
/**
|
|
471
480
|
* 设置要使用的信封而不是生成的信封
|
|
472
481
|
*
|
|
473
|
-
* @return {Object} SMTP
|
|
482
|
+
* @return {Object} SMTP信封,格式为 {from: 'from@example.com', to: ['to@example.com']}
|
|
474
483
|
*/
|
|
475
484
|
setEnvelope(envelope) {
|
|
476
485
|
this._envelope = { from: false, to: [] };
|
|
@@ -483,11 +492,13 @@ class MimeNode {
|
|
|
483
492
|
|
|
484
493
|
const { to } = this._envelope, standardFields = ['to', 'cc', 'bcc', 'from'];
|
|
485
494
|
['to', 'cc', 'bcc'].forEach(key => {
|
|
486
|
-
|
|
495
|
+
// 如果存在,则将地址添加到信封中
|
|
496
|
+
if (envelope[key]) this.#convertAddresses(this.parseAddresses(envelope[key]), to);
|
|
487
497
|
});
|
|
488
498
|
this._envelope.to = to.map(to => to.address).filter(address => address);
|
|
489
499
|
Object.keys(envelope).forEach(key => {
|
|
490
|
-
|
|
500
|
+
// 如果字段不是标准字段,则将其添加到信封中
|
|
501
|
+
if (!standardFields.includes(key)) this._envelope[key] = envelope[key];
|
|
491
502
|
});
|
|
492
503
|
|
|
493
504
|
return this;
|
|
@@ -514,7 +525,7 @@ class MimeNode {
|
|
|
514
525
|
/**
|
|
515
526
|
* 生成并返回包含发件人地址和收件人地址列表的SMTP信封
|
|
516
527
|
*
|
|
517
|
-
* @return {Object} SMTP
|
|
528
|
+
* @return {Object} SMTP信封,格式为 {from: 'from@example.com', to: ['to@example.com']}
|
|
518
529
|
*/
|
|
519
530
|
getEnvelope() {
|
|
520
531
|
if (this._envelope) return this._envelope;
|
|
@@ -533,7 +544,7 @@ class MimeNode {
|
|
|
533
544
|
}
|
|
534
545
|
|
|
535
546
|
/**
|
|
536
|
-
* 返回Message-Id
|
|
547
|
+
* 返回Message-Id值;如果不存在,则创建一个
|
|
537
548
|
*
|
|
538
549
|
* @return {String} Message-Id值
|
|
539
550
|
*/
|
|
@@ -542,14 +553,14 @@ class MimeNode {
|
|
|
542
553
|
}
|
|
543
554
|
|
|
544
555
|
/**
|
|
545
|
-
*
|
|
556
|
+
* 设置预生成的内容,该内容将用作此节点的输出
|
|
546
557
|
*
|
|
547
558
|
* @param {String|Buffer|Stream} 原始MIME内容
|
|
548
559
|
*/
|
|
549
560
|
setRaw(raw) {
|
|
550
561
|
this._raw = raw;
|
|
551
562
|
if (this._raw && typeof this._raw.pipe === 'function') {
|
|
552
|
-
//
|
|
563
|
+
// 预流处理程序;如果流被设置为内容,并且在此流执行任何操作之前触发'error',则可能被触发
|
|
553
564
|
this.contentErrorHandler = err => {
|
|
554
565
|
this._raw.removeListener('error', this.contentErrorHandler), this._raw = err;
|
|
555
566
|
};
|
|
@@ -560,7 +571,7 @@ class MimeNode {
|
|
|
560
571
|
}
|
|
561
572
|
|
|
562
573
|
/**
|
|
563
|
-
*
|
|
574
|
+
* 检测并返回与内容相关的流的句柄;
|
|
564
575
|
*
|
|
565
576
|
* @param {Mixed} content 节点内容
|
|
566
577
|
* @returns {Object} 流对象
|
|
@@ -586,25 +597,26 @@ class MimeNode {
|
|
|
586
597
|
return stream;
|
|
587
598
|
}, { _resolvedValue, pipe, path, href, httpHeaders } = content;
|
|
588
599
|
if (_resolvedValue) return createContentStream(_resolvedValue); // 处理已解析的字符串或Buffer类型内容
|
|
589
|
-
else if (typeof pipe === 'function') return content; //
|
|
590
|
-
//
|
|
600
|
+
else if (typeof pipe === 'function') return content; // 如果是流本身,则直接返回该流
|
|
601
|
+
// 如果是文件路径:根据访问权限返回文件流或错误流
|
|
591
602
|
else if (content && typeof path === 'string' && !href)
|
|
592
603
|
return this.disableFileAccess ? createErrorStream(`文件访问被拒绝:${path}`) : fsReadStream(path);
|
|
593
|
-
// 如果是URL
|
|
604
|
+
// 如果是URL:根据访问权限返回网络流或错误流
|
|
594
605
|
else if (content && typeof href === 'string')
|
|
595
|
-
return this.disableUrlAccess ? createErrorStream(`URL访问被拒绝:${href}`)
|
|
596
|
-
|
|
606
|
+
return this.disableUrlAccess ? createErrorStream(`URL访问被拒绝:${href}`)
|
|
607
|
+
: nmfetch(href, { headers: httpHeaders });
|
|
608
|
+
return createContentStream(content || ''); // 兜底处理:将其他类型内容转换为流
|
|
597
609
|
}
|
|
598
610
|
|
|
599
611
|
/**
|
|
600
|
-
*
|
|
612
|
+
* 解析地址;接受单个地址或数组或地址数组的数组(例如To: [[第一组], [第二组],...])
|
|
601
613
|
*
|
|
602
614
|
* @param {Mixed} addresses 要解析的地址
|
|
603
615
|
* @return {Array} 地址对象数组
|
|
604
616
|
*/
|
|
605
617
|
parseAddresses(addresses) {
|
|
606
618
|
return [].concat.apply([], [].concat(addresses).map(mAddress => {
|
|
607
|
-
//
|
|
619
|
+
// 如果地址和名称都存在,则将其转换为数组
|
|
608
620
|
if (mAddress?.address) {
|
|
609
621
|
const { address, name = '' } = mAddress;
|
|
610
622
|
mAddress.address = this.#normalizeAddress(address), mAddress.name = name;
|
|
@@ -615,7 +627,7 @@ class MimeNode {
|
|
|
615
627
|
}
|
|
616
628
|
|
|
617
629
|
/**
|
|
618
|
-
*
|
|
630
|
+
* 规范化头部键,使用驼峰式,除了大写的MIME-
|
|
619
631
|
*
|
|
620
632
|
* @param {String} key 要规范化的键
|
|
621
633
|
* @return {String} 驼峰式形式的键
|
|
@@ -630,8 +642,8 @@ class MimeNode {
|
|
|
630
642
|
}
|
|
631
643
|
|
|
632
644
|
/**
|
|
633
|
-
*
|
|
634
|
-
*
|
|
645
|
+
* 检查内容类型是否为多部分,并在需要时定义边界;
|
|
646
|
+
* 不返回任何内容,而是修改对象参数;
|
|
635
647
|
*
|
|
636
648
|
* @param {Object} structured 为'Content-Type'键解析的头部值
|
|
637
649
|
*/
|
|
@@ -651,14 +663,15 @@ class MimeNode {
|
|
|
651
663
|
}
|
|
652
664
|
|
|
653
665
|
/**
|
|
654
|
-
* 对生成的rfc2822
|
|
666
|
+
* 对生成的rfc2822电子邮件中使用的头部值进行编码;
|
|
655
667
|
*
|
|
656
668
|
* @param {String} key 头部键
|
|
657
669
|
* @param {String} value 头部值
|
|
658
670
|
*/
|
|
659
671
|
encodeHeaderValue(key, value = '') {
|
|
660
672
|
key = this.#normalizeHeaderKey(key);
|
|
661
|
-
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),
|
|
662
675
|
cleanValue = (val = '') => {
|
|
663
676
|
return val.toString().replace(rL, ' '); // 将值转换为字符串,并替换掉换行符
|
|
664
677
|
},
|
|
@@ -681,7 +694,7 @@ class MimeNode {
|
|
|
681
694
|
case 'In-Reply-To':
|
|
682
695
|
case 'Content-Id':
|
|
683
696
|
return ensureAngleBrackets(this.#encodeWords(cleanValue(value)));
|
|
684
|
-
//
|
|
697
|
+
// 用<>括起来的值,以空格分隔的列表
|
|
685
698
|
case 'References':
|
|
686
699
|
value = [].concat(value)
|
|
687
700
|
// 将值转换为字符串,删除尖括号中的空白字符,然后按空白字符分割
|
|
@@ -692,8 +705,8 @@ class MimeNode {
|
|
|
692
705
|
if (value instanceof Date) return value.toUTCString().replace('GMT', '+0000');
|
|
693
706
|
return this.#encodeWords(cleanValue(value));
|
|
694
707
|
case 'Content-Type':
|
|
695
|
-
case 'Content-Disposition': return cleanValue(value); //
|
|
696
|
-
default: return this.#encodeWords(cleanValue(value)); // 默认encodeWords
|
|
708
|
+
case 'Content-Disposition': return cleanValue(value); // 如果包含文件名,则已编码
|
|
709
|
+
default: return this.#encodeWords(cleanValue(value)); // 默认encodeWords仅在需要时编码,否则返回原始字符串
|
|
697
710
|
}
|
|
698
711
|
}
|
|
699
712
|
|
|
@@ -735,7 +748,7 @@ class MimeNode {
|
|
|
735
748
|
address = address.toString().replace(rFC, ' ').trim();
|
|
736
749
|
|
|
737
750
|
const lIndex = address.lastIndexOf('@');
|
|
738
|
-
if (lIndex < 0) return address; //
|
|
751
|
+
if (lIndex < 0) return address; // 没有@符号,保持原样
|
|
739
752
|
|
|
740
753
|
let user = address.substring(0, lIndex), encodedDomain;
|
|
741
754
|
const domain = address.substring(lIndex + 1);
|
|
@@ -755,9 +768,9 @@ class MimeNode {
|
|
|
755
768
|
|
|
756
769
|
/**
|
|
757
770
|
* 对邮件地址中的名称部分进行编码处理
|
|
758
|
-
* 如果名称包含非ASCII
|
|
771
|
+
* 如果名称包含非ASCII字符或特殊字符,则进行MIME编码或引号包裹
|
|
759
772
|
*
|
|
760
|
-
* @param {String} name
|
|
773
|
+
* @param {String} name 邮件地址中的名称部分(如 "张三" in "张三<zhangsan@example.com>")
|
|
761
774
|
* @returns {String} 编码后的名称字符串
|
|
762
775
|
*/
|
|
763
776
|
#encodeAddressName(name) {
|
|
@@ -789,7 +802,7 @@ class MimeNode {
|
|
|
789
802
|
*/
|
|
790
803
|
#getTextEncoding(value = '') {
|
|
791
804
|
value = value.toString();
|
|
792
|
-
if (this.textEncoding) return this.textEncoding; //
|
|
805
|
+
if (this.textEncoding) return this.textEncoding; // 如果有预设编码,直接返回
|
|
793
806
|
const rNL = resetRegex(regexs.NON_LATIN), rL = resetRegex(regexs.LATIN),
|
|
794
807
|
nonLatinLen = (value.match(rNL) || []).length, latinLen = (value.match(rL) || []).length;
|
|
795
808
|
return nonLatinLen < latinLen ? 'Q' : 'B';
|
|
@@ -801,7 +814,7 @@ class MimeNode {
|
|
|
801
814
|
* @return {String} 随机Message-ID值
|
|
802
815
|
*/
|
|
803
816
|
#generateMessageId() {
|
|
804
|
-
// 使用不同长度的随机字节生成类似UUID
|
|
817
|
+
// 使用不同长度的随机字节生成类似UUID的格式,优先使用发件人地址中的域名,服务器主机名或localhost次之
|
|
805
818
|
const randomPart = [4, 2, 2, 2, 6].map(len => randomBytes(len).toString('hex')).join('-'),
|
|
806
819
|
domain = (this.getEnvelope().from || this.hostname || 'localhost').split('@').pop();
|
|
807
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();
|