@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.
@@ -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 代理配置,例如 "http://proxy.host:3128/"
16
+ * @param {String} proxyUrl 代理配置,例如 "http://proxy.host:3128/"
17
17
  * @param {Number} destinationPort 目标主机端口
18
18
  * @param {String} destinationHost 目标主机名
19
- * @param {Function} callback 连接建立后的回调函数,接收socket对象
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
- options = { host: hostname, port: Number(port) || (isHttps ? 443 : 80), rejectUnauthorized: !isHttps }, // 配置连接选项
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'), headersEndIndex = headers.indexOf('\r\n\r\n');
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), headerPart = headers.substring(0, headersEndIndex),
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
- return callback(new Error(`代理服务器返回无效响应${((statusMatch && `: ${statusMatch[1]}`) || '')}`));
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
- socket.setTimeout(httpProxyClient.timeout || 30 * 1000).on('timeout', timeoutErr).once('error', tempSocketErr);//监听连接错误
99
+ // 监听连接错误
100
+ socket.setTimeout(httpProxyClient.timeout || 30 * 1000).on('timeout', timeoutErr).once('error', tempSocketErr);
96
101
  }
97
102
 
98
103
  export { httpProxyClient };
@@ -8,8 +8,10 @@ import { PK, getLogger, prepareMessageForSending } from './shared.js';
8
8
  */
9
9
  class JsonTransport {
10
10
  constructor(options = {}) {
11
- this.options = options, this.name = 'JsonTransport', this.version = PK.version; // 设置传输对象名称和版本
12
- this.logger = getLogger(this.options, { component: this.options.component || 'jsonTransport' }); // 初始化日志记录器
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 }, '为 %s 构建JSON结构失败: %s', messageId, err.message);
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, { envelope, messageId, message: this.options.skipEncoding ? data : JSON.stringify(data) });
35
+ return done(null,
36
+ { envelope, messageId, message: this.options.skipEncoding ? data : JSON.stringify(data) });
33
37
  });
34
38
  });
35
39
  }
@@ -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
- * 负责处理邮件的多部分结构(纯文本、HTML、附件等)
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
- * 编译邮件内容,生成MimeNode实例
15
- * 根据邮件内容类型(纯文本、HTML、附件等)构建相应的MIME结构
14
+ * 编译邮件内容,生成MimeNode实例
15
+ * 根据邮件内容类型(纯文本、HTML、附件等)构建相应的MIME结构
16
16
  * @returns {MimeNode} 编译后的MimeNode实例
17
17
  */
18
18
  compile() {
19
- const aTives = this._alternatives = this.getAlternatives(); // 获取所有替代内容(纯文本、HTML、日历事件等)
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
- // 获取附件,并根据是否存在HTML内容区分相关附件
22
+ // 获取附件,并根据是否存在HTML内容区分相关附件
23
23
  const { attached, related } = this._attachments = this.getAttachments(!!this._htmlNode);
24
24
 
25
25
  // 确定需要使用的MIME类型结构
26
- this._useRelated = !!(this._htmlNode && related.length); // 是否需要相关部分(HTML + 内联图片)
27
- this._useAlternative = aTives.length > 1; // 是否需要替代部分(纯文本 + HTML)
28
- this._useMixed = attached.length > 1 || (aTives.length && attached.length === 1); // 是否需要混合部分(内容 + 附件)
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字段,则直接使用raw字段
33
- else if (this._useMixed) this.message = this.#createMixed(); // 混合类型:内容 + 多个附件
34
- else if (this._useAlternative) this.message = this.#createAlternative(); // 替代类型:纯文本 + HTML
35
- else if (this._useRelated) this.message = this.#createRelated(); // 相关类型:HTML + 内联资源
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 - 是否查找相关附件(用于HTML内联)
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 || icalEvent.raw)
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 = '', httpHeaders } = pAtt,
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 && cid) ? 'inline' : 'attachment'),
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
- // 设置内容源(优先级:raw > path > href > content)
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
- * 获取邮件的替代内容(纯文本、HTML、日历事件等)
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; method=${(iEvent.method || 'PUBLISH').toString().trim().toUpperCase()}`,
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, _resolve: true } })
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, headers } = oAtive,
151
+ { contentType, filename, path, href, raw, content = '', contentTransferEncoding, encoding,
152
+ headers } = oAtive,
145
153
  data = ({
146
- contentType: contentType || detectMimeType(filename || path || href || 'txt'), contentTransferEncoding,
147
- ...(raw && { raw }), ...(filename && { filename }), ...(encoding && { encoding }), ...(headers && { headers })
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
- if (!raw) data.content = path ? { path } : href ? { href } : content;// 设置内容源(优先级:raw>path>href>content)
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
- * 预处理附件对象(处理dataURL和标准化路径,将HTTP / HTTPS路径转换为href属性)
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, ...this.#processDataUrl(obj) } : { ...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 - 父节点,如果为null则创建根节点
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, this.#getChildNodeOptions());
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节点(纯文本 + HTML)
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, ative);
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节点(HTML + 内联资源)
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, newline
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), contentType };
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, newline } = this.mail;
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
  });
@@ -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方法,设置getSocket
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 插件阶段('compile'或'stream')
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
- // 如果没有提供回调,则创建Promise
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 }, '在 %s 阶段使用 %s 个插件', step, count);
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,则使用userId和password字段,否则使用authentication字段
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代理,则使用userid字段
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
- // 解析内容方法(代理给 resolveContent)
31
+ // 解析内容方法(代理给 resolveContent)
32
32
  resolveContent(...args) {
33
33
  return resolveContent(...args);
34
34
  }
35
35
 
36
- // 解析所有内容(文本、HTML、附件等)
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'], [this.data, 'icalEvent']
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
- // 如果没有文件名,则从路径或href中提取,或使用默认名称
49
+ // 如果没有文件名,则从路径或href中提取,或使用默认名称
49
50
  if (!newFname) {
50
51
  newFname = (path || href).split('/').pop().split('?').shift() || `attachment-${(i + 1)}`;
51
- if (!newFname.includes('.')) newFname += `.${detectExtension(contentType)}`; // 如果没有扩展名,则根据内容类型添加
52
+ // 如果没有扩展名,则根据内容类型添加
53
+ if (!newFname.includes('.')) newFname += `.${detectExtension(contentType)}`;
52
54
  attachment.filename = newFname; // 设置回attachment对象
53
55
  }
54
56
 
55
- // 如果没有内容类型,则根据文件名、路径或href检测
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
- // 处理单值地址字段(from, sender);如果数据中有地址,则保存为地址对象
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) && !['content', 'path', 'href', 'raw'].includes(key)) node[key] = currentItem[key];
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
- const envelope = this.data.envelope || this.message.getEnvelope(), messageId = this.message.messageId(); // 获取信封信息
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和注释(List-*: <http://domain> (comment))
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
- // 如果包含@,则使用mailto协议,否则使用HTTP协议
225
+ // 如果包含@,则使用mailto协议,否则使用HTTP协议
222
226
  return regexs.EMAIL_CHECK.test(url) ? `<mailto:${url}>` : `<http://${url}>`;
223
227
  }
224
228
  }