@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.
@@ -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
- // 准备重定向请求(GET方法,无请求体)
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] - 用于写入响应数据的可写流,默认创建一个 PassThrough 流;
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] - 允许的最大重定向次数,默认为 MAX_REDIRECTS 常量;
102
- * @param {string} [options.method=''] - HTTP 请求方法(如 'GET'、'POST' 等),不提供时若存在请求体则默认为 'POST',否则为 'GET';
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 相关选项,直接传递给底层请求的 tls 配置;
106
- * @param {number} [options.timeout] - 请求超时时间(毫秒),超时后触发错误并销毁请求;
107
- * @param {*} [options.body] - 请求体内容,可以是字符串、Buffer 或可读流;若存在且 method 未指定,则方法自动设为 'POST';
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} 返回用于接收响应数据的可写流(通常为 PassThrough 实例),可通过监听 'finish' 或 'error' 事件获取结果;
111
+ * @returns {stream.Writable} 返回用于接收响应数据的可写流(通常为 PassThrough 实例),
112
+ *可通过监听 'finish' 或 'error' 事件获取结果;
111
113
  */
112
114
  const nmfetch = (url, options = {}) => {
113
- const { fetchRes: fetchRes_, cookies, redirects = 0, maxRedirects, method = '', headers: headers_, userAgent, } = options,
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方法,默认为GET
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中包含认证信息,则设置Authorization头
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头为真,则存储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) {
@@ -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内容