@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/http-proxy-client.js
CHANGED
|
@@ -8,15 +8,15 @@ import { regexs } from './regexs.js';
|
|
|
8
8
|
/**
|
|
9
9
|
* 建立到目标端口的代理连接
|
|
10
10
|
* >查看定义:@see {@link httpProxyClient}
|
|
11
|
-
* -
|
|
11
|
+
* - 使用示例:
|
|
12
12
|
* @example
|
|
13
13
|
* httpProxyClient("http://localhost:3128/", 80, "google.com", (err, socket)=>{
|
|
14
14
|
* socket.write("GET / HTTP/1.0\r\n\r\n");
|
|
15
15
|
* });
|
|
16
|
-
* @param {String} proxyUrl
|
|
16
|
+
* @param {String} proxyUrl 代理配置,例如 "http://proxy.host:3128/"
|
|
17
17
|
* @param {Number} destinationPort 目标主机端口
|
|
18
18
|
* @param {String} destinationHost 目标主机名
|
|
19
|
-
* @param {Function} callback
|
|
19
|
+
* @param {Function} callback 连接建立后的回调函数,接收socket对象
|
|
20
20
|
*/
|
|
21
21
|
const httpProxyClient = (proxyUrl, destinationPort, destinationHost, callback) => {
|
|
22
22
|
let proxy;
|
|
@@ -27,7 +27,8 @@ const httpProxyClient = (proxyUrl, destinationPort, destinationHost, callback) =
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
const { hostname, port, protocol, username = '', password = '' } = proxy, isHttps = protocol === 'https:',
|
|
30
|
-
|
|
30
|
+
// 配置连接选项
|
|
31
|
+
options = { host: hostname, port: Number(port) || (isHttps ? 443 : 80), rejectUnauthorized: !isHttps },
|
|
31
32
|
connect = isHttps ? (options, callback) => tlsConnect(options, callback)
|
|
32
33
|
: (options, callback) => netConnect(options, callback), // 根据代理协议类型选择连接方式
|
|
33
34
|
state = { finished: false },
|
|
@@ -53,7 +54,7 @@ const httpProxyClient = (proxyUrl, destinationPort, destinationHost, callback) =
|
|
|
53
54
|
|
|
54
55
|
// 构造CONNECT请求头(目标主机和端口,连接类型)
|
|
55
56
|
const reqHeaders = { Host: `${destinationHost}:${destinationPort}`, Connection: 'close' };
|
|
56
|
-
//
|
|
57
|
+
// 如果代理需要认证,添加认证头
|
|
57
58
|
if (username || password) {
|
|
58
59
|
const auth = `${username}:${password}`;
|
|
59
60
|
reqHeaders['Proxy-Authorization'] = `Basic ${Buffer.from(auth).toString('base64')}`;
|
|
@@ -69,11 +70,13 @@ const httpProxyClient = (proxyUrl, destinationPort, destinationHost, callback) =
|
|
|
69
70
|
if (state.finished) return;
|
|
70
71
|
|
|
71
72
|
headerChunks.push(chunk); // 使用数组累积数据块
|
|
72
|
-
const headers = Buffer.concat(headerChunks).toString('latin1'),
|
|
73
|
+
const headers = Buffer.concat(headerChunks).toString('latin1'),
|
|
74
|
+
headersEndIndex = headers.indexOf('\r\n\r\n');
|
|
73
75
|
if (headersEndIndex !== -1) {
|
|
74
76
|
socket.removeListener('data', onSocketData);
|
|
75
77
|
|
|
76
|
-
const remainder = headers.substring(headersEndIndex + 4),
|
|
78
|
+
const remainder = headers.substring(headersEndIndex + 4),
|
|
79
|
+
headerPart = headers.substring(0, headersEndIndex),
|
|
77
80
|
statusMatch = headerPart.match(regexs.HTTP_RESPONSE_STATUS_LINE);
|
|
78
81
|
|
|
79
82
|
if (remainder) socket.unshift(Buffer.from(remainder, 'latin1'));
|
|
@@ -81,7 +84,8 @@ const httpProxyClient = (proxyUrl, destinationPort, destinationHost, callback) =
|
|
|
81
84
|
if (!statusMatch || (statusMatch[1]?.[0] ?? '') !== '2') {
|
|
82
85
|
try { socket.destroy(); } catch (E) { }
|
|
83
86
|
headerChunks = [];
|
|
84
|
-
|
|
87
|
+
const msg = `代理服务器返回无效响应${((statusMatch && `: ${statusMatch[1]}`) || '')}`;
|
|
88
|
+
return callback(new Error(msg));
|
|
85
89
|
}
|
|
86
90
|
|
|
87
91
|
socket.removeListener('error', tempSocketErr).removeListener('timeout', timeoutErr).setTimeout(0);
|
|
@@ -92,7 +96,8 @@ const httpProxyClient = (proxyUrl, destinationPort, destinationHost, callback) =
|
|
|
92
96
|
socket.on('data', onSocketData);
|
|
93
97
|
});
|
|
94
98
|
|
|
95
|
-
|
|
99
|
+
// 监听连接错误
|
|
100
|
+
socket.setTimeout(httpProxyClient.timeout || 30 * 1000).on('timeout', timeoutErr).once('error', tempSocketErr);
|
|
96
101
|
}
|
|
97
102
|
|
|
98
103
|
export { httpProxyClient };
|
package/lib/json-transport.js
CHANGED
|
@@ -8,8 +8,10 @@ import { PK, getLogger, prepareMessageForSending } from './shared.js';
|
|
|
8
8
|
*/
|
|
9
9
|
class JsonTransport {
|
|
10
10
|
constructor(options = {}) {
|
|
11
|
-
|
|
12
|
-
this.
|
|
11
|
+
// 设置传输对象名称和版本
|
|
12
|
+
this.options = options, this.name = 'JsonTransport', this.version = PK.version;
|
|
13
|
+
// 初始化日志记录器
|
|
14
|
+
this.logger = getLogger(this.options, { component: this.options.component || 'jsonTransport' });
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
/**
|
|
@@ -25,11 +27,13 @@ class JsonTransport {
|
|
|
25
27
|
setImmediate(() => {
|
|
26
28
|
normalize((err, data) => {
|
|
27
29
|
if (err) {
|
|
28
|
-
this.logger.error({ err, tnx: 'send', messageId },
|
|
30
|
+
this.logger.error({ err, tnx: 'send', messageId },
|
|
31
|
+
'为 %s 构建JSON结构失败: %s', messageId, err.message);
|
|
29
32
|
return done(err);
|
|
30
33
|
}
|
|
31
34
|
delete data.envelope, delete data.normalizedHeaders;
|
|
32
|
-
return done(null,
|
|
35
|
+
return done(null,
|
|
36
|
+
{ envelope, messageId, message: this.options.skipEncoding ? data : JSON.stringify(data) });
|
|
33
37
|
});
|
|
34
38
|
});
|
|
35
39
|
}
|
package/lib/mail-composer.js
CHANGED
|
@@ -3,7 +3,7 @@ import { regexs, resetRegex, parseDataURI } from './shared.js';
|
|
|
3
3
|
import { detectMimeType, detectExtension } from './mime-funcs/index.js';
|
|
4
4
|
/**
|
|
5
5
|
* 邮件编译器类,用于从邮件选项组合MimeNode实例;
|
|
6
|
-
*
|
|
6
|
+
* 负责处理邮件的多部分结构(纯文本、HTML、附件等)
|
|
7
7
|
* >查看定义:@see {@link MailComposer}
|
|
8
8
|
* @class MailComposer
|
|
9
9
|
*/
|
|
@@ -11,30 +11,32 @@ class MailComposer {
|
|
|
11
11
|
constructor(mail = {}) { this.mail = mail, this.message = false; }
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
|
-
*
|
|
15
|
-
*
|
|
14
|
+
* 编译邮件内容,生成MimeNode实例
|
|
15
|
+
* 根据邮件内容类型(纯文本、HTML、附件等)构建相应的MIME结构
|
|
16
16
|
* @returns {MimeNode} 编译后的MimeNode实例
|
|
17
17
|
*/
|
|
18
18
|
compile() {
|
|
19
|
-
const aTives = this._alternatives = this.getAlternatives(); //
|
|
19
|
+
const aTives = this._alternatives = this.getAlternatives(); // 获取所有替代内容(纯文本、HTML、日历事件等)
|
|
20
20
|
// 从替代内容中提取HTML节点
|
|
21
21
|
this._htmlNode = aTives.filter(alternative => regexs.HTML_TYPE.test(alternative.contentType)).pop();
|
|
22
|
-
//
|
|
22
|
+
// 获取附件,并根据是否存在HTML内容区分相关附件
|
|
23
23
|
const { attached, related } = this._attachments = this.getAttachments(!!this._htmlNode);
|
|
24
24
|
|
|
25
25
|
// 确定需要使用的MIME类型结构
|
|
26
|
-
this._useRelated = !!(this._htmlNode &&
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
this._useRelated = !!(this._htmlNode &&
|
|
27
|
+
related.length); // 是否需要相关部分(HTML + 内联图片)
|
|
28
|
+
// 是否需要替代部分(纯文本 + HTML)
|
|
29
|
+
this._useAlternative = aTives.length > 1;
|
|
30
|
+
this._useMixed = attached.length > 1 || (aTives.length && attached.length === 1); // 是否需要混合部分(内容 + 附件)
|
|
29
31
|
|
|
30
32
|
// 根据内容类型构建相应的MIME结构
|
|
31
33
|
const { raw, newline, headers, envelope } = this.mail;
|
|
32
|
-
if (raw) this.message = new MimeNode('message/rfc822', { newline }).setRaw(raw); // 如果有raw
|
|
33
|
-
else if (this._useMixed) this.message = this.#createMixed(); //
|
|
34
|
-
else if (this._useAlternative) this.message = this.#createAlternative(); //
|
|
35
|
-
else if (this._useRelated) this.message = this.#createRelated(); //
|
|
34
|
+
if (raw) this.message = new MimeNode('message/rfc822', { newline }).setRaw(raw); // 如果有raw字段,则直接使用raw字段
|
|
35
|
+
else if (this._useMixed) this.message = this.#createMixed(); // 混合类型:内容 + 多个附件
|
|
36
|
+
else if (this._useAlternative) this.message = this.#createAlternative(); // 替代类型:纯文本 + HTML
|
|
37
|
+
else if (this._useRelated) this.message = this.#createRelated(); // 相关类型:HTML + 内联资源
|
|
36
38
|
else this.message = this.#createContentNode(false, [aTives, attached].flatMap(arr => arr || [])
|
|
37
|
-
.find(Boolean) || { contentType: 'text/plain', content: '' }); //
|
|
39
|
+
.find(Boolean) || { contentType: 'text/plain', content: '' }); // 简单类型:单一内容
|
|
38
40
|
|
|
39
41
|
if (headers) this.message.addHeader(headers); // 添加自定义邮件头
|
|
40
42
|
// 设置标准邮件头
|
|
@@ -53,8 +55,8 @@ class MailComposer {
|
|
|
53
55
|
|
|
54
56
|
/**
|
|
55
57
|
* 获取邮件附件列表
|
|
56
|
-
*
|
|
57
|
-
* @param {boolean} findRelated -
|
|
58
|
+
* 处理普通附件和相关附件(如内联图片)
|
|
59
|
+
* @param {boolean} findRelated - 是否查找相关附件(用于HTML内联)
|
|
58
60
|
* @returns {Object} 包含attached和related附件的对象
|
|
59
61
|
*/
|
|
60
62
|
getAttachments(findRelated = false) {
|
|
@@ -63,7 +65,8 @@ class MailComposer {
|
|
|
63
65
|
|
|
64
66
|
// 处理日历事件附件
|
|
65
67
|
if (icalEvent) {
|
|
66
|
-
const iEvent = typeof icalEvent === 'object' && (icalEvent.content || icalEvent.path || icalEvent.href ||
|
|
68
|
+
const iEvent = typeof icalEvent === 'object' && (icalEvent.content || icalEvent.path || icalEvent.href ||
|
|
69
|
+
icalEvent.raw)
|
|
67
70
|
? { ...icalEvent } : { content: icalEvent };
|
|
68
71
|
|
|
69
72
|
eventObject = {
|
|
@@ -75,10 +78,12 @@ class MailComposer {
|
|
|
75
78
|
// 处理所有附件
|
|
76
79
|
const pAttachments = attachments.map((attachment, i) => {
|
|
77
80
|
const pAtt = this.#prepareAttachmentObject(attachment), // 预处理附件
|
|
78
|
-
{ filename, path, href, cid, raw, encoding, headers, contentType: cType, content = '',
|
|
81
|
+
{ filename, path, href, cid, raw, encoding, headers, contentType: cType, content = '',
|
|
82
|
+
httpHeaders } = pAtt,
|
|
79
83
|
source = path || href || 'bin', contentType = cType || detectMimeType(filename || source),
|
|
80
84
|
isImage = regexs.IMAGE_TYPE.test(contentType), isMessageNode = regexs.MESSAGE_TYPE.test(contentType),
|
|
81
|
-
contentDisposition = pAtt.contentDisposition || (isMessageNode || (isImage &&
|
|
85
|
+
contentDisposition = pAtt.contentDisposition || (isMessageNode || (isImage &&
|
|
86
|
+
cid) ? 'inline' : 'attachment'),
|
|
82
87
|
contentTransferEncoding = 'contentTransferEncoding' in pAtt ? pAtt.contentTransferEncoding
|
|
83
88
|
: isMessageNode ? '7bit' : 'base64',
|
|
84
89
|
// 构建附件数据
|
|
@@ -94,7 +99,7 @@ class MailComposer {
|
|
|
94
99
|
? `${baseName}.${detectExtension(contentType)}` : baseName || `attachment-${i + 1}`;
|
|
95
100
|
}
|
|
96
101
|
|
|
97
|
-
//
|
|
102
|
+
// 设置内容源(优先级:raw > path > href > content)
|
|
98
103
|
if (!raw) data.content = path ? { path } : href ? { href, httpHeaders } : content;
|
|
99
104
|
return data;
|
|
100
105
|
}), eventArray = eventObject ? [eventObject] : [];
|
|
@@ -104,7 +109,7 @@ class MailComposer {
|
|
|
104
109
|
}
|
|
105
110
|
|
|
106
111
|
/**
|
|
107
|
-
*
|
|
112
|
+
* 获取邮件的替代内容(纯文本、HTML、日历事件等)
|
|
108
113
|
* 用于构建多部分替代结构
|
|
109
114
|
* @returns {Array} 替代内容数组
|
|
110
115
|
*/
|
|
@@ -128,9 +133,11 @@ class MailComposer {
|
|
|
128
133
|
const iEvent = processAlt(iEvent, icalEvent, null);
|
|
129
134
|
eventObject = {
|
|
130
135
|
...iEvent, filename: false,
|
|
131
|
-
contentType: `text/calendar; charset=utf-8;
|
|
136
|
+
contentType: `text/calendar; charset=utf-8; ` +
|
|
137
|
+
`method=${(iEvent.method || 'PUBLISH').toString().trim().toUpperCase()}`,
|
|
132
138
|
headers: iEvent.headers || {},
|
|
133
|
-
...(iEvent.content && typeof iEvent.content === 'object' && { content: { ...iEvent.content,
|
|
139
|
+
...(iEvent.content && typeof iEvent.content === 'object' && { content: { ...iEvent.content,
|
|
140
|
+
_resolve: true } })
|
|
134
141
|
};
|
|
135
142
|
}
|
|
136
143
|
|
|
@@ -141,13 +148,17 @@ class MailComposer {
|
|
|
141
148
|
const gAlternatives = Array.prototype.concat.call([], text_, html_, watchHtml_, amp_, eventObject, alternatives)
|
|
142
149
|
.filter(Boolean).map(originalAlternative => {
|
|
143
150
|
const oAtive = this.#prepareAttachmentObject(originalAlternative), // 预处理附件
|
|
144
|
-
{ contentType, filename, path, href, raw, content = '', contentTransferEncoding, encoding,
|
|
151
|
+
{ contentType, filename, path, href, raw, content = '', contentTransferEncoding, encoding,
|
|
152
|
+
headers } = oAtive,
|
|
145
153
|
data = ({
|
|
146
|
-
contentType: contentType || detectMimeType(filename || path || href || 'txt'),
|
|
147
|
-
|
|
154
|
+
contentType: contentType || detectMimeType(filename || path || href || 'txt'),
|
|
155
|
+
contentTransferEncoding,
|
|
156
|
+
...(raw && { raw }), ...(filename && { filename }), ...(encoding && { encoding }), ...(headers &&
|
|
157
|
+
{ headers })
|
|
148
158
|
});
|
|
149
159
|
|
|
150
|
-
|
|
160
|
+
// 设置内容源(优先级:raw>path>href>content)
|
|
161
|
+
if (!raw) data.content = path ? { path } : href ? { href } : content;
|
|
151
162
|
return data;
|
|
152
163
|
});
|
|
153
164
|
|
|
@@ -155,30 +166,32 @@ class MailComposer {
|
|
|
155
166
|
}
|
|
156
167
|
|
|
157
168
|
/**
|
|
158
|
-
*
|
|
169
|
+
* 预处理附件对象(处理dataURL和标准化路径,将HTTP / HTTPS路径转换为href属性)
|
|
159
170
|
* @param {Object} obj - 要处理的附件对象
|
|
160
171
|
* @returns {Object} 处理后的对象
|
|
161
172
|
*/
|
|
162
173
|
#prepareAttachmentObject(obj) {
|
|
163
174
|
// 处理dataURL格式的内容和标准化路径
|
|
164
|
-
const prepared = regexs.DATA_URL.test(obj.path || obj.href) ? { ...obj,
|
|
175
|
+
const prepared = regexs.DATA_URL.test(obj.path || obj.href) ? { ...obj,
|
|
176
|
+
...this.#processDataUrl(obj) } : { ...obj },
|
|
165
177
|
{ path } = prepared;
|
|
166
178
|
if (regexs.HTTP_URL.test(path)) prepared.href = path, prepared.path = undefined;
|
|
167
179
|
return prepared;
|
|
168
180
|
}
|
|
169
181
|
|
|
170
182
|
/**
|
|
171
|
-
* 创建MIME
|
|
172
|
-
* @param {MimeNode|null} parentNode -
|
|
183
|
+
* 创建MIME节点(根节点或子节点)
|
|
184
|
+
* @param {MimeNode|null} parentNode - 父节点,如果为null则创建根节点
|
|
173
185
|
* @param {string} mimeType - MIME类型
|
|
174
186
|
* @returns {MimeNode} 创建的节点
|
|
175
187
|
*/
|
|
176
188
|
#createMimeNode(parentNode, mimeType) {
|
|
177
|
-
return !parentNode ? this.#createRootMimeNode(mimeType) : parentNode.createChild(mimeType,
|
|
189
|
+
return !parentNode ? this.#createRootMimeNode(mimeType) : parentNode.createChild(mimeType,
|
|
190
|
+
this.#getChildNodeOptions());
|
|
178
191
|
}
|
|
179
192
|
|
|
180
193
|
/**
|
|
181
|
-
* 创建混合类型的MIME
|
|
194
|
+
* 创建混合类型的MIME节点(内容 + 附件)
|
|
182
195
|
* @param {MimeNode} parentNode - 父节点
|
|
183
196
|
* @returns {MimeNode} 创建的混合节点
|
|
184
197
|
*/
|
|
@@ -197,7 +210,7 @@ class MailComposer {
|
|
|
197
210
|
}
|
|
198
211
|
|
|
199
212
|
/**
|
|
200
|
-
* 创建替代类型的MIME
|
|
213
|
+
* 创建替代类型的MIME节点(纯文本 + HTML)
|
|
201
214
|
* @param {MimeNode} parentNode - 父节点
|
|
202
215
|
* @returns {MimeNode} 创建的替代节点
|
|
203
216
|
*/
|
|
@@ -205,14 +218,15 @@ class MailComposer {
|
|
|
205
218
|
const node = this.#createMimeNode(parentNode, 'multipart/alternative');
|
|
206
219
|
// 添加所有替代内容
|
|
207
220
|
this._alternatives.forEach(ative => {
|
|
208
|
-
this._useRelated && this._htmlNode === ative ? this.#createRelated(node) : this.#createContentNode(node,
|
|
221
|
+
this._useRelated && this._htmlNode === ative ? this.#createRelated(node) : this.#createContentNode(node,
|
|
222
|
+
ative);
|
|
209
223
|
});
|
|
210
224
|
|
|
211
225
|
return node;
|
|
212
226
|
}
|
|
213
227
|
|
|
214
228
|
/**
|
|
215
|
-
* 创建相关类型的MIME
|
|
229
|
+
* 创建相关类型的MIME节点(HTML + 内联资源)
|
|
216
230
|
* @param {MimeNode} parentNode - 父节点
|
|
217
231
|
* @returns {MimeNode} 创建的相关节点
|
|
218
232
|
*/
|
|
@@ -237,9 +251,10 @@ class MailComposer {
|
|
|
237
251
|
{ baseBoundary, textEncoding, boundaryPrefix, disableUrlAccess, disableFileAccess, normalizeHeaderKey, newline,
|
|
238
252
|
encoding: mEncoding } = this.mail
|
|
239
253
|
|
|
240
|
-
//
|
|
254
|
+
// 创建节点(根节点或子节点)
|
|
241
255
|
const node = !parentNode ? new MimeNode(contentType, {
|
|
242
|
-
filename, baseBoundary, textEncoding, boundaryPrefix, disableUrlAccess, disableFileAccess, normalizeHeaderKey,
|
|
256
|
+
filename, baseBoundary, textEncoding, boundaryPrefix, disableUrlAccess, disableFileAccess, normalizeHeaderKey,
|
|
257
|
+
newline
|
|
243
258
|
}) : parentNode.createChild(contentType, {
|
|
244
259
|
filename, textEncoding, disableUrlAccess, disableFileAccess, normalizeHeaderKey, newline
|
|
245
260
|
});
|
|
@@ -274,7 +289,8 @@ class MailComposer {
|
|
|
274
289
|
const { path, href, contentType = 'application/octet-stream' } = element, dataUrl = path || href;
|
|
275
290
|
if (!dataUrl || typeof dataUrl !== 'string' || !dataUrl.startsWith('data:')) return element;
|
|
276
291
|
// 对过大的DataURL进行限制
|
|
277
|
-
if (dataUrl.length > 100000) return { ...element, path: false, href: false, content: Buffer.alloc(0),
|
|
292
|
+
if (dataUrl.length > 100000) return { ...element, path: false, href: false, content: Buffer.alloc(0),
|
|
293
|
+
contentType };
|
|
278
294
|
|
|
279
295
|
let parsedDataUri;
|
|
280
296
|
try {
|
|
@@ -297,7 +313,8 @@ class MailComposer {
|
|
|
297
313
|
* @returns {MimeNode} 创建的根节点
|
|
298
314
|
*/
|
|
299
315
|
#createRootMimeNode(contentType) {
|
|
300
|
-
const { baseBoundary, textEncoding, boundaryPrefix, disableUrlAccess, disableFileAccess, normalizeHeaderKey,
|
|
316
|
+
const { baseBoundary, textEncoding, boundaryPrefix, disableUrlAccess, disableFileAccess, normalizeHeaderKey,
|
|
317
|
+
newline } = this.mail;
|
|
301
318
|
return new MimeNode(contentType, {
|
|
302
319
|
baseBoundary, textEncoding, boundaryPrefix, disableUrlAccess, disableFileAccess, normalizeHeaderKey, newline
|
|
303
320
|
});
|
package/lib/mailer/index.js
CHANGED
|
@@ -18,13 +18,13 @@ class Mailer extends EventEmitter {
|
|
|
18
18
|
super();
|
|
19
19
|
this.options = options, this._defaults = defaults; // 初始化选项和默认值
|
|
20
20
|
|
|
21
|
-
//
|
|
21
|
+
// 默认插件配置(编译和流处理阶段)
|
|
22
22
|
this._defaultPlugins = { compile: [(...args) => this.#convertDataImages(...args)], stream: [] };
|
|
23
23
|
|
|
24
24
|
this._userPlugins = { compile: [], stream: [] }; // 用户自定义插件
|
|
25
25
|
this.meta = new Map(); // 元数据存储
|
|
26
26
|
const { dkim, proxy, component = 'mailer' } = this.options;
|
|
27
|
-
this.dkim = dkim ? new DKIM(dkim) : false; // DKIM
|
|
27
|
+
this.dkim = dkim ? new DKIM(dkim) : false; // DKIM配置(域名密钥识别邮件)
|
|
28
28
|
this.transporter = transporter, this.transporter.mailer = this; // 设置传输器并建立反向引用
|
|
29
29
|
this.logger = getLogger(this.options, { component }); // 初始化日志记录器
|
|
30
30
|
this.logger.debug({ tnx: 'create' }, '创建传输器: %s', this.getVersionString()); // 记录创建日志
|
|
@@ -44,7 +44,7 @@ class Mailer extends EventEmitter {
|
|
|
44
44
|
['close', 'isIdle', 'verify'].forEach(method => {
|
|
45
45
|
this[method] = (...args) => {
|
|
46
46
|
if (typeof this.transporter[method] === 'function') {
|
|
47
|
-
// 特殊处理verify
|
|
47
|
+
// 特殊处理verify方法,设置getSocket
|
|
48
48
|
if (method === 'verify' && typeof this.getSocket === 'function') this.#setupTransporterSocket();
|
|
49
49
|
return this.transporter[method](...args);
|
|
50
50
|
} else {
|
|
@@ -54,7 +54,7 @@ class Mailer extends EventEmitter {
|
|
|
54
54
|
};
|
|
55
55
|
});
|
|
56
56
|
|
|
57
|
-
if (proxy && typeof proxy === 'string') this.setupProxy(proxy); //
|
|
57
|
+
if (proxy && typeof proxy === 'string') this.setupProxy(proxy); // 如果有代理,则设置代理
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
/**
|
|
@@ -67,7 +67,7 @@ class Mailer extends EventEmitter {
|
|
|
67
67
|
|
|
68
68
|
/**
|
|
69
69
|
* 使用插件
|
|
70
|
-
* @param {string} step
|
|
70
|
+
* @param {string} step 插件阶段('compile'或'stream')
|
|
71
71
|
* @param {Function} plugin 插件函数
|
|
72
72
|
*/
|
|
73
73
|
use(step = '', plugin) {
|
|
@@ -84,7 +84,7 @@ class Mailer extends EventEmitter {
|
|
|
84
84
|
* @param {Function?} callback 发送成功或失败后运行的回调函数
|
|
85
85
|
*/
|
|
86
86
|
sendMail(data, callback = null) {
|
|
87
|
-
//
|
|
87
|
+
// 如果没有提供回调,则创建Promise
|
|
88
88
|
const promise = !callback ? new Promise((resolve, reject) => callback = callbackPromise(resolve, reject)) : null,
|
|
89
89
|
mail = new MailMessage(this, data); // 创建邮件消息对象
|
|
90
90
|
if (typeof this.getSocket === 'function') this.#setupTransporterSocket(); // 设置传输器的getSocket方法
|
|
@@ -150,26 +150,27 @@ class Mailer extends EventEmitter {
|
|
|
150
150
|
*/
|
|
151
151
|
#processPlugins(step = '', mail, callback) {
|
|
152
152
|
step = step.toString();
|
|
153
|
-
if (!this._userPlugins.hasOwnProperty(step)) return callback(); //
|
|
153
|
+
if (!this._userPlugins.hasOwnProperty(step)) return callback(); // 如果没有这个步骤,则直接返回
|
|
154
154
|
|
|
155
155
|
const userPlugins = this._userPlugins[step] || [], defaultPlugins = this._defaultPlugins[step] || [],
|
|
156
156
|
count = userPlugins.length;
|
|
157
|
-
// 记录插件使用情况(
|
|
158
|
-
if (count) this.logger.debug({ tnx: 'transaction', pluginCount: count, step },
|
|
157
|
+
// 记录插件使用情况(如果没有插件,则直接返回)
|
|
158
|
+
if (count) this.logger.debug({ tnx: 'transaction', pluginCount: count, step },
|
|
159
|
+
'在 %s 阶段使用 %s 个插件', step, count);
|
|
159
160
|
if (count + defaultPlugins.length === 0) return callback();
|
|
160
161
|
|
|
161
162
|
// 递归处理插件
|
|
162
163
|
let pos = 0, block = 'default';
|
|
163
164
|
const processPlugins = () => {
|
|
164
165
|
let curPlugins = block === 'default' ? defaultPlugins : userPlugins;
|
|
165
|
-
//
|
|
166
|
+
// 如果当前位置超过了当前插件列表的长度,则切换到下一个插件列表
|
|
166
167
|
if (pos >= curPlugins.length) {
|
|
167
168
|
if (block === 'default' && count) block = 'user', pos = 0, curPlugins = userPlugins;
|
|
168
169
|
else return callback();
|
|
169
170
|
}
|
|
170
171
|
let plugin = curPlugins[pos++];
|
|
171
172
|
plugin(mail, err => {
|
|
172
|
-
if (err) return callback(err); //
|
|
173
|
+
if (err) return callback(err); // 如果插件返回错误,则直接返回
|
|
173
174
|
processPlugins();
|
|
174
175
|
});
|
|
175
176
|
};
|
|
@@ -224,9 +225,9 @@ class Mailer extends EventEmitter {
|
|
|
224
225
|
if (auth) {
|
|
225
226
|
const username = decodeURIComponent(auth.split(':').shift()),
|
|
226
227
|
password = decodeURIComponent(auth.split(':').pop());
|
|
227
|
-
// 如果使用Socks5代理v2
|
|
228
|
+
// 如果使用Socks5代理v2,则使用userId和password字段,否则使用authentication字段
|
|
228
229
|
if (proxyV2) connectionOpts.proxy.userId = username, connectionOpts.proxy.password = password;
|
|
229
|
-
else if (type === 4) connectionOpts.userid = username; // 如果使用Socks4
|
|
230
|
+
else if (type === 4) connectionOpts.userid = username; // 如果使用Socks4代理,则使用userid字段
|
|
230
231
|
else connectionOpts.authentication = { username, password };
|
|
231
232
|
}
|
|
232
233
|
|
|
@@ -236,11 +237,11 @@ class Mailer extends EventEmitter {
|
|
|
236
237
|
});
|
|
237
238
|
};
|
|
238
239
|
|
|
239
|
-
// 如果代理主机是IP
|
|
240
|
+
// 如果代理主机是IP地址,则直接连接
|
|
240
241
|
if (isIP(hostname)) return connect(hostname);
|
|
241
242
|
|
|
242
243
|
return dnsResolve(hostname, (err, address) => {
|
|
243
|
-
if (err) return callback(err); //
|
|
244
|
+
if (err) return callback(err); // 如果出错,则直接返回
|
|
244
245
|
connect(Array.isArray(address) ? address[0] : address);
|
|
245
246
|
});
|
|
246
247
|
}
|
|
@@ -255,7 +256,7 @@ class Mailer extends EventEmitter {
|
|
|
255
256
|
* @param {Function} callback 回调函数
|
|
256
257
|
*/
|
|
257
258
|
#convertDataImages(mail, callback) {
|
|
258
|
-
// 如果没有附件数据URL或没有HTML
|
|
259
|
+
// 如果没有附件数据URL或没有HTML内容,则直接返回
|
|
259
260
|
if ((!this.options.attachDataUrls && !mail.data.attachDataUrls) || !mail.data.html) return callback();
|
|
260
261
|
|
|
261
262
|
// 解析HTML内容
|
|
@@ -11,7 +11,7 @@ import { detectExtension, detectMimeType, isPlainText, encodeWord } from '../mim
|
|
|
11
11
|
* 通常由 Mailer 实例内部创建和调用,不建议直接实例化;
|
|
12
12
|
*/
|
|
13
13
|
class MailMessage {
|
|
14
|
-
//
|
|
14
|
+
// 构造函数,初始化邮件消息对象
|
|
15
15
|
constructor(mailer, data = {}) {
|
|
16
16
|
this.mailer = mailer, this.message = null;
|
|
17
17
|
const { options = {}, _defaults = {} } = mailer;
|
|
@@ -22,37 +22,39 @@ class MailMessage {
|
|
|
22
22
|
const headers = { ...defaults.headers, ...data.headers }, mailData = { ...defaults, ...data, headers };
|
|
23
23
|
// 强制应用 transporter 选项中的特定键
|
|
24
24
|
['disableFileAccess', 'disableUrlAccess', 'normalizeHeaderKey'].forEach(key => {
|
|
25
|
-
if (key in options) mailData[key] = options[key]; //
|
|
25
|
+
if (key in options) mailData[key] = options[key]; // 如果在选项中设置,则使用选项值
|
|
26
26
|
});
|
|
27
27
|
|
|
28
28
|
return mailData;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
//
|
|
31
|
+
// 解析内容方法(代理给 resolveContent)
|
|
32
32
|
resolveContent(...args) {
|
|
33
33
|
return resolveContent(...args);
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
//
|
|
36
|
+
// 解析所有内容(文本、HTML、附件等)
|
|
37
37
|
resolveAll(callback) {
|
|
38
38
|
// 定义需要解析的内容键
|
|
39
39
|
const keys = [
|
|
40
|
-
[this.data, 'html'], [this.data, 'text'], [this.data, 'watchHtml'], [this.data, 'amp'],
|
|
40
|
+
[this.data, 'html'], [this.data, 'text'], [this.data, 'watchHtml'], [this.data, 'amp'],
|
|
41
|
+
[this.data, 'icalEvent']
|
|
41
42
|
], { alternatives, attachments } = this.data;
|
|
42
|
-
alternatives?.forEach((_, i) => keys.push([alternatives, i])); //
|
|
43
|
+
alternatives?.forEach((_, i) => keys.push([alternatives, i])); // 如果有替代内容,则添加到键中
|
|
43
44
|
|
|
44
45
|
// 处理附件
|
|
45
46
|
attachments?.forEach((attachment, i) => {
|
|
46
47
|
const { filename, path, href = '', contentType } = attachment;
|
|
47
48
|
let newFname = filename;
|
|
48
|
-
//
|
|
49
|
+
// 如果没有文件名,则从路径或href中提取,或使用默认名称
|
|
49
50
|
if (!newFname) {
|
|
50
51
|
newFname = (path || href).split('/').pop().split('?').shift() || `attachment-${(i + 1)}`;
|
|
51
|
-
|
|
52
|
+
// 如果没有扩展名,则根据内容类型添加
|
|
53
|
+
if (!newFname.includes('.')) newFname += `.${detectExtension(contentType)}`;
|
|
52
54
|
attachment.filename = newFname; // 设置回attachment对象
|
|
53
55
|
}
|
|
54
56
|
|
|
55
|
-
//
|
|
57
|
+
// 如果没有内容类型,则根据文件名、路径或href检测
|
|
56
58
|
if (!contentType) attachment.contentType = detectMimeType(newFname || path || href || 'bin');
|
|
57
59
|
keys.push([attachments, i]);
|
|
58
60
|
});
|
|
@@ -63,16 +65,16 @@ class MailMessage {
|
|
|
63
65
|
addressKeys.forEach(address => {
|
|
64
66
|
let value;
|
|
65
67
|
const rAddress = this.message.getHeader(address === 'replyTo' ? 'reply-to' : address);
|
|
66
|
-
//
|
|
68
|
+
// 如果已有消息,从消息头解析地址
|
|
67
69
|
if (this.message) value = [].concat(mimeNode.parseAddresses(rAddress) || []);
|
|
68
|
-
//
|
|
70
|
+
// 否则如果数据中有,则直接从数据中解析
|
|
69
71
|
else if (this.data[address]) value = [].concat(mimeNode.parseAddresses(this.data[address]) || []);
|
|
70
72
|
|
|
71
|
-
if (value?.length) this.data[address] = value; //
|
|
72
|
-
else if (address in this.data) this.data[address] = null; //
|
|
73
|
+
if (value?.length) this.data[address] = value; // 如果有值,则保存到数据中
|
|
74
|
+
else if (address in this.data) this.data[address] = null; // 否则如果数据中有地址字段,则删除地址字段
|
|
73
75
|
});
|
|
74
76
|
|
|
75
|
-
//
|
|
77
|
+
// 处理单值地址字段(from, sender);如果数据中有地址,则保存为地址对象
|
|
76
78
|
['from', 'sender'].forEach(address => {
|
|
77
79
|
if (this.data[address]) this.data[address] = this.data[address].shift();
|
|
78
80
|
});
|
|
@@ -87,11 +89,12 @@ class MailMessage {
|
|
|
87
89
|
if (!parent || !currentItem) return resolveNext(); // 跳过空值
|
|
88
90
|
resolveContent(...args, (err, value) => {
|
|
89
91
|
if (err) return callback(err);
|
|
90
|
-
const node = { content: value }; //
|
|
92
|
+
const node = { content: value }; // 创建内容节点,保留原始数据的其他属性
|
|
91
93
|
if (currentItem && typeof currentItem === 'object' && !Buffer.isBuffer(currentItem))
|
|
92
94
|
Object.keys(currentItem).forEach(key => {
|
|
93
|
-
//
|
|
94
|
-
if (!(key in node) &&
|
|
95
|
+
// 如果没有在节点中,则从原始数据中复制属性
|
|
96
|
+
if (!(key in node) &&
|
|
97
|
+
!['content', 'path', 'href', 'raw'].includes(key)) node[key] = currentItem[key];
|
|
95
98
|
});
|
|
96
99
|
|
|
97
100
|
parent[key] = node, resolveNext(); // 更新数据为节点对象并继续解析下一个
|
|
@@ -103,15 +106,16 @@ class MailMessage {
|
|
|
103
106
|
|
|
104
107
|
// 规范化邮件数据
|
|
105
108
|
normalize(callback) {
|
|
106
|
-
|
|
109
|
+
// 获取信封信息
|
|
110
|
+
const envelope = this.data.envelope || this.message.getEnvelope(), messageId = this.message.messageId();
|
|
107
111
|
// 首先解析所有内容
|
|
108
112
|
this.resolveAll((err, data) => {
|
|
109
|
-
if (err) return callback(err); //
|
|
113
|
+
if (err) return callback(err); // 如果解析失败,返回错误
|
|
110
114
|
|
|
111
115
|
data.envelope = envelope, data.messageId = messageId; // 设置信封和消息ID
|
|
112
116
|
['html', 'text', 'watchHtml', 'amp'].forEach(key => {
|
|
113
117
|
const content = data[key]?.content;
|
|
114
|
-
//
|
|
118
|
+
// 如果内容不是是字符串,则转换为字符串,否则直接使用,
|
|
115
119
|
if (content) data[key] = Buffer.isBuffer(content) ? content.toString() : content;
|
|
116
120
|
});
|
|
117
121
|
|
|
@@ -158,7 +162,7 @@ class MailMessage {
|
|
|
158
162
|
|
|
159
163
|
// 设置邮件发送器头部
|
|
160
164
|
setMailerHeader() {
|
|
161
|
-
if (!this.message || !this.data.xMailer) return; // 如果没有消息或没有xMailer
|
|
165
|
+
if (!this.message || !this.data.xMailer) return; // 如果没有消息或没有xMailer,则直接返回
|
|
162
166
|
this.message.setHeader('X-Mailer', this.data.xMailer);
|
|
163
167
|
}
|
|
164
168
|
|
|
@@ -177,7 +181,7 @@ class MailMessage {
|
|
|
177
181
|
// 设置列表头部
|
|
178
182
|
setListHeaders() {
|
|
179
183
|
const list = this.data.list;
|
|
180
|
-
if (!this.message || !list || typeof list !== 'object') return; //
|
|
184
|
+
if (!this.message || !list || typeof list !== 'object') return; // 如果没有消息或没有列表头部,则直接返回
|
|
181
185
|
|
|
182
186
|
// 添加可选的 List-* 头部
|
|
183
187
|
if (list && typeof list === 'object')
|
|
@@ -206,7 +210,7 @@ class MailMessage {
|
|
|
206
210
|
url = formattedUrl.replace(regexs.LIST_ID_PROTOCOL_REMOVE, '');
|
|
207
211
|
return quotedComment + url; // List-ID: "comment" <domain>
|
|
208
212
|
}
|
|
209
|
-
// 返回格式化后的URL
|
|
213
|
+
// 返回格式化后的URL和注释(List-*: <http://domain> (comment))
|
|
210
214
|
return comment ? `${formattedUrl} (${comment})` : formattedUrl;
|
|
211
215
|
}).join(', ')
|
|
212
216
|
}]
|
|
@@ -218,7 +222,7 @@ class MailMessage {
|
|
|
218
222
|
const rUC = resetRegex(regexs.URL_CLEAN);
|
|
219
223
|
url = url.replace(rUC, ''); // 去除空白和尖括号
|
|
220
224
|
if (regexs.PROTOCOL_CHECK.test(url)) return `<${url}>`; // 如果包含协议,则使用尖括号包裹
|
|
221
|
-
//
|
|
225
|
+
// 如果包含@,则使用mailto协议,否则使用HTTP协议
|
|
222
226
|
return regexs.EMAIL_CHECK.test(url) ? `<mailto:${url}>` : `<http://${url}>`;
|
|
223
227
|
}
|
|
224
228
|
}
|