@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/fetch/index.js
CHANGED
|
@@ -35,7 +35,7 @@ const MAX_REDIRECTS = 5, // 最大重定向次数常量
|
|
|
35
35
|
let processedBody = null;
|
|
36
36
|
const { contentType, body, headers } = options;
|
|
37
37
|
|
|
38
|
-
//
|
|
38
|
+
// 设置内容类型(除非明确禁止)
|
|
39
39
|
if (contentType !== false) headers['Content-Type'] = contentType || 'application/x-www-form-urlencoded';
|
|
40
40
|
|
|
41
41
|
// 流式数据处理
|
|
@@ -76,7 +76,7 @@ const MAX_REDIRECTS = 5, // 最大重定向次数常量
|
|
|
76
76
|
return true;
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
//
|
|
79
|
+
// 准备重定向请求(GET方法,无请求体)
|
|
80
80
|
options.method = 'GET', options.body = false;
|
|
81
81
|
|
|
82
82
|
try {
|
|
@@ -94,23 +94,26 @@ const MAX_REDIRECTS = 5, // 最大重定向次数常量
|
|
|
94
94
|
* >查看定义:@see {@link nmfetch}
|
|
95
95
|
* @param {string} url - 请求的目标 URL;
|
|
96
96
|
* @param {Object} [options={}] - 可选配置对象;
|
|
97
|
-
* @param {stream.Writable} [options.fetchRes] -
|
|
97
|
+
* @param {stream.Writable} [options.fetchRes] - 用于写入响应数据的可写流,默认创建一个 PassThrough 流;
|
|
98
98
|
* @param {Cookies|Array|string} [options.cookies] - Cookie 管理器实例、Cookie 字符串数组或单个 Cookie 字符串,
|
|
99
99
|
* 用于自动管理请求和响应的 Cookie;
|
|
100
|
-
* @param {number} [options.redirects=0] -
|
|
101
|
-
* @param {number} [options.maxRedirects] -
|
|
102
|
-
* @param {string} [options.method=''] - HTTP
|
|
100
|
+
* @param {number} [options.redirects=0] - 已发生的重定向次数(内部使用);
|
|
101
|
+
* @param {number} [options.maxRedirects] - 允许的最大重定向次数,默认为 MAX_REDIRECTS 常量;
|
|
102
|
+
* @param {string} [options.method=''] - HTTP 请求方法(如 'GET'、'POST' 等),不提供时若存在请求体则默认为 'POST',
|
|
103
|
+
*否则为 'GET';
|
|
103
104
|
* @param {Object} [options.headers] - 自定义请求头对象,键名会转为小写;
|
|
104
105
|
* @param {string} [options.userAgent] - 自定义 User-Agent,会覆盖默认值;
|
|
105
|
-
* @param {Object} [options.tls] - TLS/SSL
|
|
106
|
-
* @param {number} [options.timeout] -
|
|
107
|
-
* @param {*} [options.body] -
|
|
106
|
+
* @param {Object} [options.tls] - TLS/SSL 相关选项,直接传递给底层请求的 tls 配置;
|
|
107
|
+
* @param {number} [options.timeout] - 请求超时时间(毫秒),超时后触发错误并销毁请求;
|
|
108
|
+
* @param {*} [options.body] - 请求体内容,可以是字符串、Buffer 或可读流;若存在且 method 未指定,则方法自动设为 'POST';
|
|
108
109
|
* @param {boolean} [options.allowErrorResponse=false] - 是否允许状态码 ≥300 的响应被视为成功,若为 false 则触发错误;
|
|
109
110
|
*
|
|
110
|
-
* @returns {stream.Writable}
|
|
111
|
+
* @returns {stream.Writable} 返回用于接收响应数据的可写流(通常为 PassThrough 实例),
|
|
112
|
+
*可通过监听 'finish' 或 'error' 事件获取结果;
|
|
111
113
|
*/
|
|
112
114
|
const nmfetch = (url, options = {}) => {
|
|
113
|
-
const { fetchRes: fetchRes_, cookies, redirects = 0, maxRedirects,
|
|
115
|
+
const { fetchRes: fetchRes_, cookies, redirects = 0, maxRedirects,
|
|
116
|
+
method = '', headers: headers_, userAgent, } = options,
|
|
114
117
|
// 设置默认响应流、Cookie管理器和重定向计数器
|
|
115
118
|
fetchRes = fetchRes_ || new PassThrough(), newCookies = cookies || new Cookies();
|
|
116
119
|
options.redirects = redirects, options.maxRedirects = isNaN(maxRedirects) ? MAX_REDIRECTS : maxRedirects;
|
|
@@ -123,7 +126,7 @@ const nmfetch = (url, options = {}) => {
|
|
|
123
126
|
if (!parsedResult.success) return fetchRes;
|
|
124
127
|
const { protocol, hostname: host, pathname, search = '', port, username, password } = parsedResult.result;
|
|
125
128
|
|
|
126
|
-
// 确定HTTP
|
|
129
|
+
// 确定HTTP方法,默认为GET
|
|
127
130
|
let newMethod = method.toString().trim().toUpperCase() || 'GET';
|
|
128
131
|
const handler = protocol === 'https:' ? https : http,
|
|
129
132
|
// 设置默认请求头
|
|
@@ -132,8 +135,8 @@ const nmfetch = (url, options = {}) => {
|
|
|
132
135
|
// 合并用户自定义头部
|
|
133
136
|
Object.keys(headers_ || {}).forEach(key => headers[key.toLowerCase().trim()] = headers_[key]);
|
|
134
137
|
|
|
135
|
-
if (userAgent) headers['user-agent'] = userAgent; // 设置自定义User-Agent
|
|
136
|
-
// 如果URL
|
|
138
|
+
if (userAgent) headers['user-agent'] = userAgent; // 设置自定义User-Agent(如果提供)
|
|
139
|
+
// 如果URL中包含认证信息,则设置Authorization头
|
|
137
140
|
if (username || password) {
|
|
138
141
|
const auth = `${username}:${password}`;
|
|
139
142
|
headers.Authorization = `Basic ${Buffer.from(auth).toString('base64')}`;
|
|
@@ -187,7 +190,7 @@ const nmfetch = (url, options = {}) => {
|
|
|
187
190
|
break;
|
|
188
191
|
}
|
|
189
192
|
|
|
190
|
-
// 如果响应中的Set-Cookie
|
|
193
|
+
// 如果响应中的Set-Cookie头为真,则存储Cookie
|
|
191
194
|
const setCookie = res.headers['set-cookie'];
|
|
192
195
|
if (setCookie) [].concat(setCookie).forEach(cookie => newCookies.set(cookie, url));
|
|
193
196
|
if (_handleRedirect(res, url, options, fetchRes, finishedFlag, req)) return; // 处理重定向
|
|
@@ -204,7 +207,7 @@ const nmfetch = (url, options = {}) => {
|
|
|
204
207
|
_markFinishedWithError(finishedFlag, fetchRes, err, url), req.destroy();
|
|
205
208
|
});
|
|
206
209
|
|
|
207
|
-
// 将响应流pipe
|
|
210
|
+
// 将响应流pipe到输出流,支持解压
|
|
208
211
|
if (inflate) {
|
|
209
212
|
res.pipe(inflate).pipe(fetchRes);
|
|
210
213
|
inflate.on('error', err => {
|
|
@@ -214,7 +217,7 @@ const nmfetch = (url, options = {}) => {
|
|
|
214
217
|
else res.pipe(fetchRes);
|
|
215
218
|
});
|
|
216
219
|
|
|
217
|
-
//
|
|
220
|
+
// 发送请求体(如果有)
|
|
218
221
|
setImmediate(() => {
|
|
219
222
|
const body = bodyResult.body;
|
|
220
223
|
if (body) {
|
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内容
|