@flun/mailer 2.1.4
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 +4 -0
- package/LICENSE +15 -0
- package/Mail.js +47 -0
- package/README.md +475 -0
- package/copy-files.js +36 -0
- package/index.d.ts +816 -0
- package/index.js +109 -0
- package/lib/addressparser.js +230 -0
- package/lib/base64.js +103 -0
- package/lib/dkim/index.js +211 -0
- package/lib/dkim/message-parser.js +114 -0
- package/lib/dkim/relaxed-body.js +133 -0
- package/lib/dkim/sign.js +121 -0
- package/lib/fetch/cookies.js +224 -0
- package/lib/fetch/index.js +233 -0
- package/lib/http-proxy-client.js +98 -0
- package/lib/json-transport.js +38 -0
- package/lib/mail-composer.js +316 -0
- package/lib/mailer/index.js +301 -0
- package/lib/mailer/mail-message.js +226 -0
- package/lib/mime-funcs/index.js +493 -0
- package/lib/mime-funcs/mime-types.js +2061 -0
- package/lib/mime-node/index.js +811 -0
- package/lib/mime-node/last-newline.js +44 -0
- package/lib/mime-node/le-unix.js +37 -0
- package/lib/mime-node/le-windows.js +46 -0
- package/lib/punycode.js +283 -0
- package/lib/qp.js +171 -0
- package/lib/regexs.js +124 -0
- package/lib/sendmail-transport.js +91 -0
- package/lib/ses-transport.js +115 -0
- package/lib/shared.js +725 -0
- package/lib/smtp-connection/data-stream.js +68 -0
- package/lib/smtp-connection/index.js +1062 -0
- package/lib/smtp-pool/index.js +294 -0
- package/lib/smtp-pool/pool-resource.js +93 -0
- package/lib/smtp-transport.js +81 -0
- package/lib/stream-transport.js +59 -0
- package/lib/well-known/index.js +54 -0
- package/lib/well-known/services.json +802 -0
- package/lib/xoauth2.js +249 -0
- package/package.json +78 -0
|
@@ -0,0 +1,811 @@
|
|
|
1
|
+
import { PassThrough } from 'stream';
|
|
2
|
+
import { randomBytes } from 'crypto';
|
|
3
|
+
|
|
4
|
+
// 引入自定义模块
|
|
5
|
+
import { QpEncoder } from '../qp.js';
|
|
6
|
+
import { Base64Encoder } from '../base64.js';
|
|
7
|
+
import { toASCII } from '../punycode.js';
|
|
8
|
+
import { addressparser } from '../addressparser.js';
|
|
9
|
+
import { fsReadStream, nmfetch, regexs, resetRegex, callbackPromise, cleanup, resolveStream } from '../shared.js';
|
|
10
|
+
import {
|
|
11
|
+
detectMimeType, isPlainText, hasLongerLines, foldLines, parseHeaderValue, buildHeaderValue, encodeWord, encodeWords
|
|
12
|
+
} from '../mime-funcs/index.js';
|
|
13
|
+
|
|
14
|
+
// 引入换行符处理模块
|
|
15
|
+
import { LastNewline } from './last-newline.js';
|
|
16
|
+
import { LeUnix } from './le-unix.js';
|
|
17
|
+
import { LeWindows } from './le-windows.js';
|
|
18
|
+
|
|
19
|
+
const B64 = 'base64', QP = 'quoted-printable';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 创建一个新的MIME树节点;如果是分支节点,假设内容类型为'multipart/*',
|
|
23
|
+
* 其他任何内容类型都视为叶节点,如果选项中缺少rootNode,则假设此为根节点;
|
|
24
|
+
* >查看定义:@see {@link MimeNode}
|
|
25
|
+
* @param {String} contentType 定义节点的内容类型,对于附件可以留空(从文件名派生)
|
|
26
|
+
* @param {Object} [options] 可选选项
|
|
27
|
+
* @param {Object} [options.rootNode] 此树的根节点
|
|
28
|
+
* @param {Object} [options.parentNode] 此节点的直接父节点
|
|
29
|
+
* @param {Object} [options.filename] 附件节点的文件名
|
|
30
|
+
* @param {String} [options.baseBoundary] 唯一多部分边界值的共享部分
|
|
31
|
+
* @param {Boolean} [options.keepBcc] 如果为true,不在生成的头部中排除Bcc
|
|
32
|
+
* @param {Function} [options.normalizeHeaderKey] 用于自定义头部键大小写规范化的方法
|
|
33
|
+
* @param {String} [options.textEncoding] 编码类型:'Q'(默认)或'B'
|
|
34
|
+
*/
|
|
35
|
+
class MimeNode {
|
|
36
|
+
constructor(contentType, options = {}) {
|
|
37
|
+
this.nodeCounter = 0;
|
|
38
|
+
const { baseBoundary, boundaryPrefix = '--_NmP', disableFileAccess, disableUrlAccess, normalizeHeaderKey, rootNode,
|
|
39
|
+
keepBcc, filename, textEncoding = '', parentNode, hostname, newline } = options;
|
|
40
|
+
// 唯一多部分边界值的共享部分(Bcc默认不保留)
|
|
41
|
+
this.baseBoundary = baseBoundary || randomBytes(8).toString('hex');
|
|
42
|
+
this.boundaryPrefix = boundaryPrefix, this.normalizeHeaderKey = normalizeHeaderKey;
|
|
43
|
+
this.disableFileAccess = !!disableFileAccess, this.disableUrlAccess = !!disableUrlAccess;
|
|
44
|
+
this.keepBcc = !!keepBcc, this.rootNode = rootNode || this;
|
|
45
|
+
|
|
46
|
+
// 如果指定了文件名但未指定内容类型(可能是附件, 则从文件扩展名检测内容类型)
|
|
47
|
+
if (filename) {
|
|
48
|
+
this.filename = filename;
|
|
49
|
+
if (!contentType) contentType = detectMimeType(this.filename.split('.').pop());
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
this.newline = newline; // 如果设置为'win'则使用\r\n,如果为'linux'则使用\n;如果未设置(或使用`raw`),则换行符保持原样
|
|
53
|
+
this.textEncoding = textEncoding.toString().trim().charAt(0).toUpperCase(); // 指示头部字符串应使用的编码:"Q"或"B"
|
|
54
|
+
this.parentNode = parentNode; // 此节点的直接父节点(如果未设置则为undefined)
|
|
55
|
+
this.hostname = hostname; // 默认message - id值使用的主机名
|
|
56
|
+
|
|
57
|
+
this.date = new Date(); // 当前日期
|
|
58
|
+
this.childNodes = []; // 可能的子节点数组
|
|
59
|
+
this._headers = []; // 此节点的头部值列表,格式为[{ key: '', value: '' }]
|
|
60
|
+
this._transforms = []; // 在通过createReadStream暴露之前,消息将被管道传输的额外转换流
|
|
61
|
+
this._processFuncs = []; // 在通过createReadStream暴露之前,消息将被管道传输的额外处理函数
|
|
62
|
+
this._nodeId = ++this.rootNode.nodeCounter; // 用于生成唯一边界值(前缀添加到共享基础值之前)
|
|
63
|
+
this._isPlainText = false; // 如果内容仅使用ASCII可打印字符则为true
|
|
64
|
+
this._hasLongLines = false; // 如果内容是纯文本但有超过允许长度的行则为true
|
|
65
|
+
this._envelope = false; // 如果设置,则使用此值作为信封而不是生成一个
|
|
66
|
+
this._raw = false; // 如果设置,则使用此值作为流内容而不是构建它
|
|
67
|
+
if (contentType) this.setHeader('Content-Type', contentType); // 如果设置了内容类型(或从文件名派生),则将其添加到头部
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* 创建并附加一个子节点。提供的参数传递给MimeNode构造函数
|
|
72
|
+
*
|
|
73
|
+
* @param {String} [contentType] 可选内容类型
|
|
74
|
+
* @param {Object} [options] 可选选项对象
|
|
75
|
+
* @return {Object} 创建的节点对象
|
|
76
|
+
*/
|
|
77
|
+
createChild(contentType, options) {
|
|
78
|
+
if (!options && typeof contentType === 'object') options = contentType, contentType = undefined;
|
|
79
|
+
const node = new MimeNode(contentType, options);
|
|
80
|
+
this.appendChild(node);
|
|
81
|
+
return node;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* 将现有节点附加到MIME树,如果需要,从现有树中移除该节点
|
|
86
|
+
*
|
|
87
|
+
* @param {Object} childNode 要附加的节点
|
|
88
|
+
* @return {Object} 附加的节点对象
|
|
89
|
+
*/
|
|
90
|
+
appendChild(childNode) {
|
|
91
|
+
const rootNode = this.rootNode;
|
|
92
|
+
if (childNode.rootNode !== rootNode) childNode.rootNode = rootNode, childNode._nodeId = ++rootNode.nodeCounter;
|
|
93
|
+
childNode.parentNode = this, this.childNodes.push(childNode);
|
|
94
|
+
return childNode;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* 用另一个节点替换当前节点
|
|
99
|
+
*
|
|
100
|
+
* @param {Object} node 替换节点
|
|
101
|
+
* @return {Object} 替换节点
|
|
102
|
+
*/
|
|
103
|
+
replace(node) {
|
|
104
|
+
if (node === this) return this;
|
|
105
|
+
this.parentNode.childNodes.forEach((childNode, i) => {
|
|
106
|
+
if (childNode === this) {
|
|
107
|
+
node.rootNode = this.rootNode, node.parentNode = this.parentNode, node._nodeId = this._nodeId;
|
|
108
|
+
this.rootNode = this, this.parentNode = undefined, node.parentNode.childNodes[i] = node;
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
return node;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* 从MIME树中移除当前节点
|
|
117
|
+
*
|
|
118
|
+
* @return {Object} 被移除的节点
|
|
119
|
+
*/
|
|
120
|
+
remove() {
|
|
121
|
+
if (!this.parentNode) return this;
|
|
122
|
+
const { childNodes } = this.parentNode;
|
|
123
|
+
for (let i = childNodes.length - 1; i >= 0; i--)
|
|
124
|
+
if (childNodes[i] === this) {
|
|
125
|
+
childNodes.splice(i, 1), this.parentNode = undefined, this.rootNode = this;
|
|
126
|
+
return this;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// 统一处理头部操作,支持多种输入格式和操作类型
|
|
131
|
+
#processHeadersOp(key, value, singleOp, isSet) {
|
|
132
|
+
// 如果是批量操作(没有value且key是对象)
|
|
133
|
+
if (!value && key && typeof key === 'object') {
|
|
134
|
+
// 允许{key:'content-type',value:'text/plain'},[{key:'content-type',value:'text/plain'}],{'content-type':'text/plain'}
|
|
135
|
+
if (key.key && 'value' in key) singleOp.call(this, key.key, key.value);
|
|
136
|
+
else if (Array.isArray(key)) key.forEach(i => singleOp.call(this, i.key, i.value));
|
|
137
|
+
else Object.keys(key).forEach(i => singleOp.call(this, i, key[i]));
|
|
138
|
+
return true; // 已处理批量操作
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
key = this.#normalizeHeaderKey(key); // 单个头部操作
|
|
142
|
+
if (isSet) this._headers = this._headers.filter(item => item.key !== key); // set操作:先移除已存在的同名头部
|
|
143
|
+
|
|
144
|
+
// add操作:添加新头部;否则直接添加
|
|
145
|
+
if (!isSet && Array.isArray(value)) value.forEach(val => this._headers.push({ key, value: val }));
|
|
146
|
+
else this._headers.push({ key, value });
|
|
147
|
+
return false; // 已处理单个操作
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* 设置头部值。如果选定键的值已存在,则会被覆盖。
|
|
152
|
+
* 可以使用[{key:'', value:''}]或{key: 'value'}作为第一个参数来设置多个值。
|
|
153
|
+
*
|
|
154
|
+
* @param {String|Array|Object} key 头部键或键值对列表
|
|
155
|
+
* @param {String} value 头部值
|
|
156
|
+
* @return {Object} 当前节点
|
|
157
|
+
*/
|
|
158
|
+
setHeader(key, value) {
|
|
159
|
+
// 如果未处理批量操作,说明是单个操作,已经在#processHeadersOp中处理完成
|
|
160
|
+
if (!this.#processHeadersOp(key, value, this.setHeader, true)); // true表示是set操作
|
|
161
|
+
return this;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* 添加头部值。如果选定键的值已存在,则值将作为新字段附加,旧字段不被触动。
|
|
166
|
+
* 您也可以使用[{key:'', value:''}]或{key: 'value'}作为第一个参数来设置多个值。
|
|
167
|
+
*
|
|
168
|
+
* @param {String|Array|Object} key 头部键或键值对列表
|
|
169
|
+
* @param {String} value 头部值
|
|
170
|
+
* @return {Object} 当前节点
|
|
171
|
+
*/
|
|
172
|
+
addHeader(key, value) {
|
|
173
|
+
// 如果未处理批量操作,说明是单个操作,已经在#processHeadersOp中处理完成
|
|
174
|
+
if (!this.#processHeadersOp(key, value, this.addHeader, false)); // false表示是add操作
|
|
175
|
+
return this;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* 检索选定键的第一个匹配值
|
|
180
|
+
*
|
|
181
|
+
* @param {String} key 要搜索的键
|
|
182
|
+
* @retun {String} 键的值
|
|
183
|
+
*/
|
|
184
|
+
getHeader(key) {
|
|
185
|
+
key = this.#normalizeHeaderKey(key);
|
|
186
|
+
for (let i = 0; i < this._headers.length; i++)
|
|
187
|
+
if (this._headers[i].key === key) return this._headers[i].value;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* 设置当前节点的正文内容。如果值是字符串,则自动将字符集添加到Content-Type(如果是text/*)。
|
|
192
|
+
* 如果值是Buffer,则需要自行指定字符集
|
|
193
|
+
*
|
|
194
|
+
* @param (String|Buffer) content 正文内容
|
|
195
|
+
* @return {Object} 当前节点
|
|
196
|
+
*/
|
|
197
|
+
setContent(content) {
|
|
198
|
+
this.content = content;
|
|
199
|
+
if (typeof this.content.pipe === 'function') {
|
|
200
|
+
// 预流处理程序。如果流被设置为内容,并且在此流执行任何操作之前触发'error',则可能被触发
|
|
201
|
+
this.contentErrorHandler = err => {
|
|
202
|
+
this.content.removeListener('error', this.contentErrorHandler), this.content = err;
|
|
203
|
+
};
|
|
204
|
+
this.content.once('error', this.contentErrorHandler);
|
|
205
|
+
} else if (typeof this.content === 'string') {
|
|
206
|
+
this._isPlainText = isPlainText(this.content);
|
|
207
|
+
// 如果有超过76个符号/字节的行,则不使用7bit编码,而是使用8位编码。
|
|
208
|
+
if (this._isPlainText && hasLongerLines(this.content, 76)) this._hasLongLines = true;
|
|
209
|
+
}
|
|
210
|
+
return this;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// 构建MIME树
|
|
214
|
+
build(callback) {
|
|
215
|
+
// 如果没有回调,则返回一个承诺,该承诺将在构建完成时解析
|
|
216
|
+
const promise = !callback ? new Promise((resolve, reject) => callback = callbackPromise(resolve, reject)) : null;
|
|
217
|
+
resolveStream(this.createReadStream(), callback); // 将流数据读取到Buffer中
|
|
218
|
+
return promise;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// 获取编码类型
|
|
222
|
+
getTransferEncoding() {
|
|
223
|
+
let encoding = false;
|
|
224
|
+
const contentType = (this.getHeader('Content-Type') || '').toString().toLowerCase().trim(), content = this.content;
|
|
225
|
+
|
|
226
|
+
if (content) {
|
|
227
|
+
encoding = (this.getHeader('Content-Transfer-Encoding') || '').toString().toLowerCase().trim();
|
|
228
|
+
if (!encoding || ![B64, QP].includes(encoding)) {
|
|
229
|
+
if (regexs.TEXT_TYPE.test(contentType)) {
|
|
230
|
+
if (this._isPlainText && !this._hasLongLines) encoding = '7bit'; // 如果没有特殊符号,则无需修改文本
|
|
231
|
+
else if (typeof content === 'string' || content instanceof Buffer) // 否则,内容是字符串或Buffer,则用QP或Base64
|
|
232
|
+
encoding = this.#getTextEncoding(content) === 'Q' ? QP : B64;
|
|
233
|
+
else encoding = this.textEncoding === 'B' ? B64 : QP; // 否则,如果对于流,因无法检查内容,则使用首选编码或回退到QP
|
|
234
|
+
}
|
|
235
|
+
// 否则,如果不是 multipart 或 message 类型,默认使用 base64 编码
|
|
236
|
+
else if (!regexs.MULTIPART_OR_MESSAGE.test(contentType)) encoding = encoding || B64;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return encoding;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* 构建MIME节点的头部块。在写入内容之前追加 \r\n\r\n
|
|
244
|
+
*
|
|
245
|
+
* @returns {String} 头部
|
|
246
|
+
*/
|
|
247
|
+
buildHeaders() {
|
|
248
|
+
const tEncoding = this.getTransferEncoding(), C_DISPOSITION = 'Content-Disposition', DATE = 'Date',
|
|
249
|
+
M_VERSION = 'MIME-Version', C_TYPE = 'Content-Type', headers = [], { filename, content } = this;
|
|
250
|
+
|
|
251
|
+
if (tEncoding) this.setHeader('Content-Transfer-Encoding', tEncoding);
|
|
252
|
+
if (filename && !this.getHeader(C_DISPOSITION)) this.setHeader(C_DISPOSITION, 'attachment');
|
|
253
|
+
|
|
254
|
+
// 确保必需的头部字段
|
|
255
|
+
if (this.rootNode === this) {
|
|
256
|
+
this.messageId();
|
|
257
|
+
if (!this.getHeader(DATE)) this.setHeader(DATE, this.date.toUTCString().replace('GMT', '+0000'));
|
|
258
|
+
if (!this.getHeader(M_VERSION)) this.setHeader(M_VERSION, '1.0');
|
|
259
|
+
|
|
260
|
+
// 确保根节点的Content-Type是最后一个头部
|
|
261
|
+
for (let i = this._headers.length - 2; i >= 0; i--) {
|
|
262
|
+
const header = this._headers[i];
|
|
263
|
+
if (header.key === C_TYPE) this._headers.splice(i, 1), this._headers.push(header);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
this._headers.forEach(header => {
|
|
268
|
+
let structured, param, value = header.value;
|
|
269
|
+
const key = header.key, options = {},
|
|
270
|
+
formattedHeaders = ['From', 'Sender', 'To', 'Cc', 'Bcc', 'Reply-To', 'Date', 'References'];
|
|
271
|
+
|
|
272
|
+
if (value && typeof value === 'object' && !formattedHeaders.includes(key)) {
|
|
273
|
+
Object.keys(value).forEach(key => {
|
|
274
|
+
if (key !== 'value') options[key] = value[key];
|
|
275
|
+
});
|
|
276
|
+
value = (value.value || '').toString();
|
|
277
|
+
if (!value.trim()) return;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (options.prepared) {
|
|
281
|
+
const pushKV = `${key}:${value}`;
|
|
282
|
+
options.foldLines ? headers.push(foldLines(pushKV)) : headers.push(pushKV); // 头部值已准备就绪
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
switch (key) {
|
|
287
|
+
case C_DISPOSITION:
|
|
288
|
+
structured = parseHeaderValue(value);
|
|
289
|
+
if (filename) structured.params.filename = filename;
|
|
290
|
+
value = buildHeaderValue(structured);
|
|
291
|
+
break;
|
|
292
|
+
case C_TYPE:
|
|
293
|
+
structured = parseHeaderValue(value), this.#handleContentType(structured);
|
|
294
|
+
if (regexs.TEXT_PLAIN.test(structured.value) && typeof content === 'string' && regexs.NON_ASCII.test(content))
|
|
295
|
+
structured.params.charset = 'utf-8';
|
|
296
|
+
|
|
297
|
+
value = buildHeaderValue(structured);
|
|
298
|
+
if (filename) {
|
|
299
|
+
// 为非兼容客户端(如QQ网页邮件)添加支持,不能使用buildHeaderValue构建值,因为该值是非标准的,并且不支持参数;
|
|
300
|
+
param = this.#encodeWords(filename);
|
|
301
|
+
// 如果参数与文件名匹配,则不添加参数。如果参数包含特殊字符,则添加引号。
|
|
302
|
+
if (param !== filename || regexs.UNSAFE_PARAM_VALUE.test(param)) param = `"${param}"`;
|
|
303
|
+
value += `;name=${param}`;
|
|
304
|
+
}
|
|
305
|
+
break;
|
|
306
|
+
|
|
307
|
+
case 'Bcc':
|
|
308
|
+
if (!this.keepBcc) return; // 如果不保留Bcc,则不添加头部
|
|
309
|
+
break;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
value = this.encodeHeaderValue(key, value);
|
|
313
|
+
if (!value.toString().trim()) return; // 跳过空行
|
|
314
|
+
// 如果存在自定义的normalizeHeaderKey函数,则使用它来规范化头部键
|
|
315
|
+
if (typeof this.normalizeHeaderKey === 'function') {
|
|
316
|
+
let normalized = this.normalizeHeaderKey(key, value);
|
|
317
|
+
if (normalized && typeof normalized === 'string' && normalized.length) key = normalized;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
headers.push(foldLines(`${key}:${value}`, 76));
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
return headers.join('\r\n');
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* 从当前节点流式传输rfc2822消息。如果这是根节点,
|
|
328
|
+
* 则设置缺失的必需头部字段(Date, Message-Id, MIME-Version)
|
|
329
|
+
*
|
|
330
|
+
* @return {String} 编译后的消息 */
|
|
331
|
+
createReadStream(options = {}) {
|
|
332
|
+
const stream = new PassThrough(options);
|
|
333
|
+
let outputStream = stream, transform;
|
|
334
|
+
|
|
335
|
+
// 处理流的错误传递和管道连接
|
|
336
|
+
const pipeStream = (source, transform) => {
|
|
337
|
+
source.once('error', err => transform.emit('error', err));
|
|
338
|
+
return source.pipe(transform);
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
this.stream(stream, options, err => {
|
|
342
|
+
if (err) return outputStream.emit('error', err);
|
|
343
|
+
stream.end();
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
// 处理所有转换流
|
|
347
|
+
for (let i = 0; i < this._transforms.length; i++) {
|
|
348
|
+
transform = typeof this._transforms[i] === 'function' ? this._transforms[i]() : this._transforms[i];
|
|
349
|
+
outputStream = pipeStream(outputStream, transform);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// 在可能的用户转换之后确保终止换行符
|
|
353
|
+
transform = new LastNewline(), outputStream = pipeStream(outputStream, transform);
|
|
354
|
+
|
|
355
|
+
// 循环遍历所有后处理函数,并将输出流传递给每个函数。
|
|
356
|
+
for (let i = 0; i < this._processFuncs.length; i++) {
|
|
357
|
+
transform = this._processFuncs[i], outputStream = transform(outputStream);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (this.newline) {
|
|
361
|
+
const winbreak = ['win', 'windows', 'dos', '\r\n'].includes(this.newline.toString().toLowerCase()),
|
|
362
|
+
nTransform = winbreak ? new LeWindows() : new LeUnix(), newStream = outputStream.pipe(nTransform);
|
|
363
|
+
outputStream.on('error', err => newStream.emit('error', err));
|
|
364
|
+
return newStream;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return outputStream;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* 将转换流对象附加到转换列表。最终输出在暴露之前通过此流传递
|
|
372
|
+
*
|
|
373
|
+
* @param {Object} transform 读写流
|
|
374
|
+
*/
|
|
375
|
+
transform(transform) {
|
|
376
|
+
this._transforms.push(transform);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* 附加后处理函数。该函数在转换之后运行,并使用以下语法
|
|
381
|
+
*
|
|
382
|
+
* processFunc(input) -> outputStream
|
|
383
|
+
*
|
|
384
|
+
* @param {Object} processFunc 读写流
|
|
385
|
+
*/
|
|
386
|
+
processFunc(processFunc) {
|
|
387
|
+
this._processFuncs.push(processFunc);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// 将当前节点流式传输到输出流
|
|
391
|
+
stream(outputStream, options, done) {
|
|
392
|
+
const transferEncoding = this.getTransferEncoding(), state = { returned: false };// 防止多次返回
|
|
393
|
+
let contentStream, localStream;
|
|
394
|
+
const callback = args => {
|
|
395
|
+
cleanup(state), done(args);
|
|
396
|
+
},
|
|
397
|
+
// 对于多部分节点,推送子节点;对于内容节点,结束流
|
|
398
|
+
finalize = () => {
|
|
399
|
+
let childId = 0;
|
|
400
|
+
const processChildNode = () => {
|
|
401
|
+
if (childId >= this.childNodes.length) {
|
|
402
|
+
outputStream.write(`\r\n--${this.boundary}--\r\n`);
|
|
403
|
+
return callback();
|
|
404
|
+
}
|
|
405
|
+
const child = this.childNodes[childId++];
|
|
406
|
+
outputStream.write(`${childId > 1 ? '\r\n' : ''}--${this.boundary}\r\n`);
|
|
407
|
+
child.stream(outputStream, options, err => {
|
|
408
|
+
if (err) return callback(err);
|
|
409
|
+
setImmediate(processChildNode);
|
|
410
|
+
});
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
if (this.multipart) setImmediate(processChildNode);
|
|
414
|
+
else return callback();
|
|
415
|
+
},
|
|
416
|
+
// 推送节点内容
|
|
417
|
+
sendContent = () => {
|
|
418
|
+
const content = this.content;
|
|
419
|
+
if (content) {
|
|
420
|
+
// 如果内容是错误,则直接调用回调
|
|
421
|
+
if (Object.prototype.toString.call(content) === '[object Error]') return callback(content);
|
|
422
|
+
|
|
423
|
+
if (typeof content.pipe === 'function') {
|
|
424
|
+
content.removeListener('error', this.contentErrorHandler);
|
|
425
|
+
this.contentErrorHandler = err => callback(err), content.once('error', this.contentErrorHandler);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const createStream = () => {
|
|
429
|
+
if ([QP, B64].includes(transferEncoding)) {
|
|
430
|
+
contentStream = new (transferEncoding === B64 ? Base64Encoder : QpEncoder)(options);
|
|
431
|
+
contentStream.pipe(outputStream, { end: false });
|
|
432
|
+
contentStream.once('end', finalize).once('error', err => callback(err));
|
|
433
|
+
localStream = this.#getStream(content), localStream.pipe(contentStream);
|
|
434
|
+
} else {
|
|
435
|
+
// 任何不是QP或Base54的内容都按原样传递
|
|
436
|
+
localStream = this.#getStream(content), localStream.pipe(outputStream, { end: false });
|
|
437
|
+
localStream.once('end', finalize);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
localStream.once('error', err => callback(err));
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
if (content._resolve) {
|
|
444
|
+
// 将流数据读取到Buffer中
|
|
445
|
+
resolveStream(this.#getStream(content), (err, resolvedValue) => {
|
|
446
|
+
if (err) return callback(err);
|
|
447
|
+
content._resolve = false, content._resolvedValue = resolvedValue, setImmediate(createStream);
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
else setImmediate(createStream);
|
|
451
|
+
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
return setImmediate(finalize);
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
if (this._raw) {
|
|
458
|
+
setImmediate(() => {
|
|
459
|
+
// 如果内容是错误,则直接调用回调;如果设置错误处理程序,则将其移除
|
|
460
|
+
if (Object.prototype.toString.call(this._raw) === '[object Error]') return callback(this._raw);
|
|
461
|
+
if (typeof this._raw.pipe === 'function') this._raw.removeListener('error', this.contentErrorHandler);
|
|
462
|
+
|
|
463
|
+
const raw = this.#getStream(this._raw);
|
|
464
|
+
raw.pipe(outputStream, { end: false }), raw.on('end', finalize).on('error', err => outputStream.emit('error', err));
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
else outputStream.write(`${this.buildHeaders()}\r\n\r\n`), setImmediate(sendContent);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* 设置要使用的信封而不是生成的信封
|
|
472
|
+
*
|
|
473
|
+
* @return {Object} SMTP信封,格式为 {from: 'from@example.com', to: ['to@example.com']}
|
|
474
|
+
*/
|
|
475
|
+
setEnvelope(envelope) {
|
|
476
|
+
this._envelope = { from: false, to: [] };
|
|
477
|
+
if (envelope.from) {
|
|
478
|
+
let list = [];
|
|
479
|
+
this.#convertAddresses(this.parseAddresses(envelope.from), list);
|
|
480
|
+
list = list.filter(address => address?.address);
|
|
481
|
+
if (list.length && list[0]) this._envelope.from = list[0].address;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const { to } = this._envelope, standardFields = ['to', 'cc', 'bcc', 'from'];
|
|
485
|
+
['to', 'cc', 'bcc'].forEach(key => {
|
|
486
|
+
if (envelope[key]) this.#convertAddresses(this.parseAddresses(envelope[key]), to); // 如果存在,则将地址添加到信封中
|
|
487
|
+
});
|
|
488
|
+
this._envelope.to = to.map(to => to.address).filter(address => address);
|
|
489
|
+
Object.keys(envelope).forEach(key => {
|
|
490
|
+
if (!standardFields.includes(key)) this._envelope[key] = envelope[key]; // 如果字段不是标准字段,则将其添加到信封中
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
return this;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* 生成并返回包含解析后的地址字段的对象
|
|
498
|
+
*
|
|
499
|
+
* @return {Object} 地址对象
|
|
500
|
+
*/
|
|
501
|
+
getAddresses() {
|
|
502
|
+
const addresses = {};
|
|
503
|
+
this._headers.forEach(header => {
|
|
504
|
+
const key = header.key.toLowerCase();
|
|
505
|
+
if (['from', 'sender', 'reply-to', 'to', 'cc', 'bcc'].includes(key)) {
|
|
506
|
+
if (!Array.isArray(addresses[key])) addresses[key] = [];
|
|
507
|
+
this.#convertAddresses(this.parseAddresses(header.value), addresses[key]);
|
|
508
|
+
}
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
return addresses;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* 生成并返回包含发件人地址和收件人地址列表的SMTP信封
|
|
516
|
+
*
|
|
517
|
+
* @return {Object} SMTP信封,格式为 {from: 'from@example.com', to: ['to@example.com']}
|
|
518
|
+
*/
|
|
519
|
+
getEnvelope() {
|
|
520
|
+
if (this._envelope) return this._envelope;
|
|
521
|
+
|
|
522
|
+
const envelope = { from: false, to: [] }, { from, to } = envelope;
|
|
523
|
+
this._headers.forEach(header => {
|
|
524
|
+
const list = [], { key, value } = header;
|
|
525
|
+
if (key === 'From' || (!from && ['Reply-To', 'Sender'].includes(key))) {
|
|
526
|
+
this.#convertAddresses(this.parseAddresses(value), list);
|
|
527
|
+
if (list.length && list[0]) envelope.from = list[0].address;
|
|
528
|
+
}
|
|
529
|
+
else if (['To', 'Cc', 'Bcc'].includes(key)) this.#convertAddresses(this.parseAddresses(value), to);
|
|
530
|
+
});
|
|
531
|
+
envelope.to = to.map(to => to.address);
|
|
532
|
+
return envelope;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* 返回Message-Id值。如果不存在,则创建一个
|
|
537
|
+
*
|
|
538
|
+
* @return {String} Message-Id值
|
|
539
|
+
*/
|
|
540
|
+
messageId() {
|
|
541
|
+
return this.getHeader('Message-ID') || this.setHeader('Message-ID', this.#generateMessageId());
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* 设置预生成的内容,该内容将用作此节点的输出
|
|
546
|
+
*
|
|
547
|
+
* @param {String|Buffer|Stream} 原始MIME内容
|
|
548
|
+
*/
|
|
549
|
+
setRaw(raw) {
|
|
550
|
+
this._raw = raw;
|
|
551
|
+
if (this._raw && typeof this._raw.pipe === 'function') {
|
|
552
|
+
// 预流处理程序。如果流被设置为内容,并且在此流执行任何操作之前触发'error',则可能被触发
|
|
553
|
+
this.contentErrorHandler = err => {
|
|
554
|
+
this._raw.removeListener('error', this.contentErrorHandler), this._raw = err;
|
|
555
|
+
};
|
|
556
|
+
this._raw.once('error', this.contentErrorHandler);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
return this;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* 检测并返回与内容相关的流的句柄。
|
|
564
|
+
*
|
|
565
|
+
* @param {Mixed} content 节点内容
|
|
566
|
+
* @returns {Object} 流对象
|
|
567
|
+
*/
|
|
568
|
+
#getStream(content = {}) {
|
|
569
|
+
// 创建并返回一个立即触发错误的流
|
|
570
|
+
const createErrorStream = message => {
|
|
571
|
+
const errorStream = new PassThrough();
|
|
572
|
+
setImmediate(() => errorStream.emit('error', new Error(message)));
|
|
573
|
+
return errorStream;
|
|
574
|
+
},
|
|
575
|
+
|
|
576
|
+
// 创建并返回包含指定数据的流
|
|
577
|
+
createContentStream = data => {
|
|
578
|
+
const stream = new PassThrough();
|
|
579
|
+
setImmediate(() => {
|
|
580
|
+
try {
|
|
581
|
+
stream.end(data);
|
|
582
|
+
} catch (err) {
|
|
583
|
+
stream.emit('error', err);
|
|
584
|
+
}
|
|
585
|
+
});
|
|
586
|
+
return stream;
|
|
587
|
+
}, { _resolvedValue, pipe, path, href, httpHeaders } = content;
|
|
588
|
+
if (_resolvedValue) return createContentStream(_resolvedValue); // 处理已解析的字符串或Buffer类型内容
|
|
589
|
+
else if (typeof pipe === 'function') return content; // 如果是流本身,则直接返回该流
|
|
590
|
+
// 如果是文件路径:根据访问权限返回文件流或错误流
|
|
591
|
+
else if (content && typeof path === 'string' && !href)
|
|
592
|
+
return this.disableFileAccess ? createErrorStream(`文件访问被拒绝:${path}`) : fsReadStream(path);
|
|
593
|
+
// 如果是URL:根据访问权限返回网络流或错误流
|
|
594
|
+
else if (content && typeof href === 'string')
|
|
595
|
+
return this.disableUrlAccess ? createErrorStream(`URL访问被拒绝:${href}`) : nmfetch(href, { headers: httpHeaders });
|
|
596
|
+
return createContentStream(content || ''); // 兜底处理:将其他类型内容转换为流
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* 解析地址。接受单个地址或数组或地址数组的数组(例如To: [[第一组], [第二组],...])
|
|
601
|
+
*
|
|
602
|
+
* @param {Mixed} addresses 要解析的地址
|
|
603
|
+
* @return {Array} 地址对象数组
|
|
604
|
+
*/
|
|
605
|
+
parseAddresses(addresses) {
|
|
606
|
+
return [].concat.apply([], [].concat(addresses).map(mAddress => {
|
|
607
|
+
// 如果地址和名称都存在,则将其转换为数组
|
|
608
|
+
if (mAddress?.address) {
|
|
609
|
+
const { address, name = '' } = mAddress;
|
|
610
|
+
mAddress.address = this.#normalizeAddress(address), mAddress.name = name;
|
|
611
|
+
return [mAddress];
|
|
612
|
+
}
|
|
613
|
+
return addressparser(mAddress);
|
|
614
|
+
}));
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* 规范化头部键,使用驼峰式,除了大写的MIME-
|
|
619
|
+
*
|
|
620
|
+
* @param {String} key 要规范化的键
|
|
621
|
+
* @return {String} 驼峰式形式的键
|
|
622
|
+
*/
|
|
623
|
+
#normalizeHeaderKey(key = '') {
|
|
624
|
+
const rL = resetRegex(regexs.LINEBREAKS), rNHK = resetRegex(regexs.NORMALIZE_HEADER_KEY);
|
|
625
|
+
// 转换为字符串,删除换行符,行首和行尾空格并转换为小写, 使用大写单词(除了MIME),特殊处理Content-features头部
|
|
626
|
+
key = key.toString().replace(rL, ' ').trim().toLowerCase().replace(rNHK, c => c.toUpperCase())
|
|
627
|
+
.replace(regexs.CONTENT_FEATURES, 'Content-features');
|
|
628
|
+
|
|
629
|
+
return key;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/**
|
|
633
|
+
* 检查内容类型是否为多部分,并在需要时定义边界。
|
|
634
|
+
* 不返回任何内容,而是修改对象参数。
|
|
635
|
+
*
|
|
636
|
+
* @param {Object} structured 为'Content-Type'键解析的头部值
|
|
637
|
+
*/
|
|
638
|
+
#handleContentType(structured) {
|
|
639
|
+
this.contentType = structured.value.trim().toLowerCase();
|
|
640
|
+
this.multipart = regexs.MULTIPART.test(this.contentType) && this.contentType.split('/')[1];
|
|
641
|
+
this.boundary = this.multipart ? structured.params.boundary = this.boundary || this.#generateBoundary() : false;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/**
|
|
645
|
+
* 生成多部分边界值
|
|
646
|
+
*
|
|
647
|
+
* @return {String} 边界值
|
|
648
|
+
*/
|
|
649
|
+
#generateBoundary() {
|
|
650
|
+
return `${this.rootNode.boundaryPrefix}-${this.rootNode.baseBoundary}-Part_${this._nodeId}`;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
/**
|
|
654
|
+
* 对生成的rfc2822电子邮件中使用的头部值进行编码。
|
|
655
|
+
*
|
|
656
|
+
* @param {String} key 头部键
|
|
657
|
+
* @param {String} value 头部值
|
|
658
|
+
*/
|
|
659
|
+
encodeHeaderValue(key, value = '') {
|
|
660
|
+
key = this.#normalizeHeaderKey(key);
|
|
661
|
+
const rL = resetRegex(regexs.LINEBREAKS), rAB = resetRegex(regexs.ANGLE_BRACKET), rBR = resetRegex(regexs.BLANK_REGEX),
|
|
662
|
+
cleanValue = (val = '') => {
|
|
663
|
+
return val.toString().replace(rL, ' '); // 将值转换为字符串,并替换掉换行符
|
|
664
|
+
},
|
|
665
|
+
|
|
666
|
+
ensureAngleBrackets = (val) => {
|
|
667
|
+
if (!val.startsWith('<')) val = `<${val}`;
|
|
668
|
+
if (!val.endsWith('>')) val += '>';
|
|
669
|
+
return val; // 确保字符串以'<'开头和以'>'结尾
|
|
670
|
+
};
|
|
671
|
+
switch (key) {
|
|
672
|
+
// 结构化头部
|
|
673
|
+
case 'From':
|
|
674
|
+
case 'Sender':
|
|
675
|
+
case 'To':
|
|
676
|
+
case 'Cc':
|
|
677
|
+
case 'Bcc':
|
|
678
|
+
case 'Reply-To': return this.#convertAddresses(this.parseAddresses(value));
|
|
679
|
+
// 用<>括起来的值
|
|
680
|
+
case 'Message-ID':
|
|
681
|
+
case 'In-Reply-To':
|
|
682
|
+
case 'Content-Id':
|
|
683
|
+
return ensureAngleBrackets(this.#encodeWords(cleanValue(value)));
|
|
684
|
+
// 用<>括起来的值,以空格分隔的列表
|
|
685
|
+
case 'References':
|
|
686
|
+
value = [].concat(value)
|
|
687
|
+
// 将值转换为字符串,删除尖括号中的空白字符,然后按空白字符分割
|
|
688
|
+
.flatMap(ref => cleanValue(ref).trim().replace(rAB, m => m.replace(rBR, '')).split(regexs.WHITESPACE))
|
|
689
|
+
.map(ensureAngleBrackets);
|
|
690
|
+
return this.#encodeWords(value.join(' ').trim());
|
|
691
|
+
case 'Date':
|
|
692
|
+
if (value instanceof Date) return value.toUTCString().replace('GMT', '+0000');
|
|
693
|
+
return this.#encodeWords(cleanValue(value));
|
|
694
|
+
case 'Content-Type':
|
|
695
|
+
case 'Content-Disposition': return cleanValue(value); // 如果包含文件名,则已编码
|
|
696
|
+
default: return this.#encodeWords(cleanValue(value)); // 默认encodeWords仅在需要时编码,否则返回原始字符串
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
/**
|
|
701
|
+
* 使用punycode和其他调整重建地址对象
|
|
702
|
+
*
|
|
703
|
+
* @param {Array} addresses 地址对象数组
|
|
704
|
+
* @param {Array} [uniqueList] 要填充地址的数组
|
|
705
|
+
* @return {String} 地址字符串
|
|
706
|
+
*/
|
|
707
|
+
#convertAddresses(addresses = [], uniqueList = []) {
|
|
708
|
+
const values = [];
|
|
709
|
+
[].concat(addresses).forEach(addrObj => {
|
|
710
|
+
const { address, name, group } = addrObj;
|
|
711
|
+
if (address) {
|
|
712
|
+
const normAddr = this.#normalizeAddress(address);
|
|
713
|
+
if (!name) values.push(normAddr.includes(' ') ? `<${normAddr}>` : normAddr);
|
|
714
|
+
else if (name) values.push(`${this.#encodeAddressName(name)} <${normAddr}>`);
|
|
715
|
+
|
|
716
|
+
if (!uniqueList.some(a => this.#normalizeAddress(a.address) === normAddr)) uniqueList.push(addrObj);
|
|
717
|
+
}
|
|
718
|
+
else if (group) {
|
|
719
|
+
const groupListAddresses = (group.length ? this.#convertAddresses(group, uniqueList) : '').trim();
|
|
720
|
+
values.push(`${this.#encodeAddressName(name)}:${groupListAddresses};`);
|
|
721
|
+
}
|
|
722
|
+
});
|
|
723
|
+
|
|
724
|
+
return values.join(',');
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
/**
|
|
728
|
+
* 规范化电子邮件地址
|
|
729
|
+
*
|
|
730
|
+
* @param {Array} address 地址对象数组
|
|
731
|
+
* @return {String} 地址字符串
|
|
732
|
+
*/
|
|
733
|
+
#normalizeAddress(address = '') {
|
|
734
|
+
const rFC = resetRegex(regexs.FORBIDDEN_CHARS);
|
|
735
|
+
address = address.toString().replace(rFC, ' ').trim();
|
|
736
|
+
|
|
737
|
+
const lIndex = address.lastIndexOf('@');
|
|
738
|
+
if (lIndex < 0) return address; // 没有@符号,保持原样
|
|
739
|
+
|
|
740
|
+
let user = address.substring(0, lIndex), encodedDomain;
|
|
741
|
+
const domain = address.substring(lIndex + 1);
|
|
742
|
+
|
|
743
|
+
// 域名默认使用punycode编码,非unicode域名保持原样(用户名),即使包含unicode;'jõgeva.ee'将被转换为'xn--jgeva-dua.ee'
|
|
744
|
+
try {
|
|
745
|
+
encodedDomain = toASCII(domain.toLowerCase());
|
|
746
|
+
} catch (err) { }
|
|
747
|
+
|
|
748
|
+
if (user.includes(' ')) {
|
|
749
|
+
if (!user.startsWith('"')) user = `"${user}`;
|
|
750
|
+
if (!user.endsWith('"')) user += '"';
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
return `${user}@${encodedDomain}`;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
/**
|
|
757
|
+
* 对邮件地址中的名称部分进行编码处理
|
|
758
|
+
* 如果名称包含非ASCII字符或特殊字符,则进行MIME编码或引号包裹
|
|
759
|
+
*
|
|
760
|
+
* @param {String} name 邮件地址中的名称部分(如 "张三" in "张三<zhangsan@example.com>")
|
|
761
|
+
* @returns {String} 编码后的名称字符串
|
|
762
|
+
*/
|
|
763
|
+
#encodeAddressName(name) {
|
|
764
|
+
const { QUOTED_PAIR: rQP, RALPHANUMERIC_UNDERSCORE_SPACE: rRUS, PRINTABLE_ASCII: rPA } = regexs;
|
|
765
|
+
|
|
766
|
+
if (!rRUS.test(name)) {
|
|
767
|
+
resetRegex(rQP);
|
|
768
|
+
return rPA.test(name) ? `"${name.replace(rQP, '\\$1')}"` : encodeWord(name, this.#getTextEncoding(name), 52);
|
|
769
|
+
}
|
|
770
|
+
return name;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
/**
|
|
774
|
+
* 对邮件头字段值进行MIME编码字编码
|
|
775
|
+
* 主要用于编码包含非ASCII字符的主题、注释等字段值
|
|
776
|
+
*
|
|
777
|
+
* @param {String} value 需要编码的邮件头字段值
|
|
778
|
+
* @returns {String} MIME编码字编码后的字符串
|
|
779
|
+
*/
|
|
780
|
+
#encodeWords(value) {
|
|
781
|
+
return encodeWords(value, this.#getTextEncoding(value), 52, true);
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
/**
|
|
785
|
+
* 检测文本值的最佳MIME编码
|
|
786
|
+
*
|
|
787
|
+
* @param {String} value 要检查的值
|
|
788
|
+
* @return {String} 'Q'或'B'
|
|
789
|
+
*/
|
|
790
|
+
#getTextEncoding(value = '') {
|
|
791
|
+
value = value.toString();
|
|
792
|
+
if (this.textEncoding) return this.textEncoding; // 如果有预设编码,直接返回
|
|
793
|
+
const rNL = resetRegex(regexs.NON_LATIN), rL = resetRegex(regexs.LATIN),
|
|
794
|
+
nonLatinLen = (value.match(rNL) || []).length, latinLen = (value.match(rL) || []).length;
|
|
795
|
+
return nonLatinLen < latinLen ? 'Q' : 'B';
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
/**
|
|
799
|
+
* 生成消息ID
|
|
800
|
+
*
|
|
801
|
+
* @return {String} 随机Message-ID值
|
|
802
|
+
*/
|
|
803
|
+
#generateMessageId() {
|
|
804
|
+
// 使用不同长度的随机字节生成类似UUID的格式,优先使用发件人地址中的域名,服务器主机名或localhost次之
|
|
805
|
+
const randomPart = [4, 2, 2, 2, 6].map(len => randomBytes(len).toString('hex')).join('-'),
|
|
806
|
+
domain = (this.getEnvelope().from || this.hostname || 'localhost').split('@').pop();
|
|
807
|
+
return `<${randomPart}@${domain}>`; // 返回符合RFC标准的Message-ID格式
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
export { MimeNode };
|