@flun/mailer 2.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/LICENSE +15 -0
  3. package/Mail.js +47 -0
  4. package/README.md +475 -0
  5. package/copy-files.js +36 -0
  6. package/index.d.ts +816 -0
  7. package/index.js +109 -0
  8. package/lib/addressparser.js +230 -0
  9. package/lib/base64.js +103 -0
  10. package/lib/dkim/index.js +211 -0
  11. package/lib/dkim/message-parser.js +114 -0
  12. package/lib/dkim/relaxed-body.js +133 -0
  13. package/lib/dkim/sign.js +121 -0
  14. package/lib/fetch/cookies.js +224 -0
  15. package/lib/fetch/index.js +233 -0
  16. package/lib/http-proxy-client.js +98 -0
  17. package/lib/json-transport.js +38 -0
  18. package/lib/mail-composer.js +316 -0
  19. package/lib/mailer/index.js +301 -0
  20. package/lib/mailer/mail-message.js +226 -0
  21. package/lib/mime-funcs/index.js +493 -0
  22. package/lib/mime-funcs/mime-types.js +2061 -0
  23. package/lib/mime-node/index.js +811 -0
  24. package/lib/mime-node/last-newline.js +44 -0
  25. package/lib/mime-node/le-unix.js +37 -0
  26. package/lib/mime-node/le-windows.js +46 -0
  27. package/lib/punycode.js +283 -0
  28. package/lib/qp.js +171 -0
  29. package/lib/regexs.js +124 -0
  30. package/lib/sendmail-transport.js +91 -0
  31. package/lib/ses-transport.js +115 -0
  32. package/lib/shared.js +725 -0
  33. package/lib/smtp-connection/data-stream.js +68 -0
  34. package/lib/smtp-connection/index.js +1062 -0
  35. package/lib/smtp-pool/index.js +294 -0
  36. package/lib/smtp-pool/pool-resource.js +93 -0
  37. package/lib/smtp-transport.js +81 -0
  38. package/lib/stream-transport.js +59 -0
  39. package/lib/well-known/index.js +54 -0
  40. package/lib/well-known/services.json +802 -0
  41. package/lib/xoauth2.js +249 -0
  42. package/package.json +78 -0
@@ -0,0 +1,294 @@
1
+ import { EventEmitter } from 'events';
2
+ import { SmtpConnection } from '../smtp-connection/index.js';
3
+ import { PoolResource } from './pool-resource.js';
4
+ import { regexs, resetRegex, initSmtpConstructor, verifySmtp } from '../shared.js';
5
+
6
+ /**
7
+ * 为 @flun/mailer 创建 SMTP 连接池传输对象
8
+ * >查看定义:@see {@link SmtpPool}
9
+ * @constructor
10
+ * @param {Object} options SMTP 连接选项
11
+ */
12
+ class SmtpPool extends EventEmitter {
13
+ constructor(options = {}) {
14
+ super();
15
+ this.name = 'SmtpPool';
16
+ initSmtpConstructor(this, options, SmtpConnection, 'smtpPool'); // 初始化SMTP连接
17
+ const { maxConnections, maxMessages, rateLimit, rateDelta } = this.options;
18
+ this.options.maxConnections = maxConnections || 5; // 最大连接数
19
+ this.options.maxMessages = maxMessages || 100; // 每个连接最大消息数
20
+
21
+
22
+ // 速率限制配置(计数器,超时定时器,等待队列,检查点,时间窗口->毫秒,限制数量)
23
+ this._rateLimit = {
24
+ counter: 0, timeout: null, waiting: [], checkpoint: false,
25
+ delta: Number(rateDelta) || 1000, limit: Number(rateLimit) || 0
26
+ };
27
+ // 消息队列,连接池,连接计数器,连接池是否已关闭,是否处于空闲状态
28
+ this._queue = [], this._connections = []; this._connectionCounter = 0; this._closed = false, this.idling = true;
29
+
30
+ // 立即触发空闲事件
31
+ setImmediate(() => {
32
+ if (this.idling) this.emit('idle');
33
+ });
34
+ }
35
+
36
+ /**
37
+ * 将电子邮件加入队列,使用选定设置发送
38
+ *
39
+ * @param {Object} mail 邮件对象
40
+ * @param {Function} callback 回调函数
41
+ */
42
+ send(mail, callback) {
43
+ if (this._closed) return false; // 如果连接池已关闭,则返回false
44
+ this._queue.push({ mail, requeueAttempts: 0, callback }); // 将邮件加入队列(包括重试尝试计数)
45
+
46
+ // 如果队列长度达到最大连接数且处于空闲状态,则标记为非空闲
47
+ if (this.idling && this._queue.length >= this.options.maxConnections) this.idling = false;
48
+ setImmediate(() => this.#processMessages()); // 立即开始处理消息
49
+
50
+ return true;
51
+ }
52
+
53
+ /**
54
+ * 关闭池中的所有连接。如果有消息正在发送,则稍后关闭连接
55
+ */
56
+ close() {
57
+ this._closed = true, clearTimeout(this._rateLimit.timeout); // 标记为已关闭并清除速率限制定时器(如果存在)
58
+ const conLen = this._connections.length;
59
+ let queLen = this._queue.length;
60
+ if (!conLen && !queLen) return; // 如果没有连接和队列,则直接返回
61
+
62
+ // 移除所有可用连接
63
+ for (let i = conLen - 1; i >= 0; i--) {
64
+ const connection = this._connections[i];
65
+ if (connection) {
66
+ const cid = connection.id;
67
+ connection.close(), this._connections.splice(i, 1); // 从数组中移除连接
68
+ this.logger.info({ tnx: 'connection', cid, action: 'removed' }, '连接 #%s 已移除', cid);
69
+ }
70
+ }
71
+
72
+ // 清理整个队列
73
+ const invokeCallbacks = () => {
74
+ while ((queLen = this._queue.length) > 0) {
75
+ const entry = this._queue.shift();
76
+ if (entry && typeof entry.callback === 'function') {
77
+ try {
78
+ entry.callback(new Error(`连接池已关闭,队列中还有 ${queLen} 个待处理请求`));
79
+ } catch (E) {
80
+ this.logger.error({ err: E, tnx: 'callback' }, '回调错误: %s', E.message);
81
+ }
82
+ }
83
+ }
84
+ this.logger.debug({ tnx: 'connection' }, '待处理队列条目已清除');
85
+ };
86
+
87
+ setImmediate(invokeCallbacks);
88
+ }
89
+
90
+ /**
91
+ * 检查队列和可用连接。如果有要发送的消息且有可用连接,则使用此连接发送邮件
92
+ */
93
+ #processMessages() {
94
+ let connection;
95
+ const queLen = this._queue.length, conLen = this._connections.length,
96
+ idlingEmit = () => {
97
+ this.idling = true, this.emit('idle'); // 标记为空闲
98
+ };
99
+
100
+ if (this._closed) return; // 如果已关闭,则不执行任何操作
101
+ // 如果队列为空,则不执行任何操作
102
+ if (!queLen) {
103
+ if (!this.idling) idlingEmit();
104
+ return;
105
+ }
106
+
107
+ // 查找第一个可用连接
108
+ for (let i = 0; i < conLen; i++)
109
+ if (this._connections[i].available) {
110
+ connection = this._connections[i];
111
+ break;
112
+ }
113
+
114
+ // 如果没有可用连接且未达到最大连接数,则创建新连接
115
+ const { maxConnections } = this.options;
116
+ if (!connection && conLen < maxConnections) connection = this.#createConnection();
117
+ if (!connection) return this.idling = false; // 如果没有可用连接,则标记为非空闲并返回
118
+ if (!this.idling && queLen < maxConnections) idlingEmit(); // 检查处理队列中是否有空闲空间
119
+
120
+ // 从队列中取出一个条目并分配给连接
121
+ const entry = connection.queueEntry = this._queue.shift(), rCMI = resetRegex(regexs.CLEAN_MESSAGE_ID),
122
+ messageId = entry.messageId = (entry.mail.message.getHeader('message-id') || '').replace(rCMI, ''),
123
+ cid = connection.id, _rateLimit = this._rateLimit;
124
+ connection.available = false; // 标记连接为忙碌
125
+ this.logger.debug({ tnx: 'pool', cid, messageId, action: 'assign' },
126
+ '将消息 <%s> 分配给 #%s (%s)', messageId, cid, connection.messages + 1
127
+ );
128
+ // 处理速率限制
129
+ if (_rateLimit.limit) {
130
+ _rateLimit.counter++;
131
+ if (!_rateLimit.checkpoint) _rateLimit.checkpoint = Date.now();
132
+ }
133
+
134
+ // 发送邮件
135
+ connection.send(entry.mail, (err, info) => {
136
+ // 仅当前处理程序未更改时处理回调
137
+ if (entry === connection.queueEntry) {
138
+ try {
139
+ entry.callback(err, info);
140
+ } catch (E) {
141
+ this.logger.error({ err: E, tnx: 'callback', cid }, '回调错误 #%s: %s', cid, E.message);
142
+ }
143
+ connection.queueEntry = false;
144
+ }
145
+ });
146
+ }
147
+
148
+ /**
149
+ * 创建新的池资源
150
+ */
151
+ #createConnection() {
152
+ const connection = new PoolResource(this), cid = connection.id = ++this._connectionCounter; // 分配连接ID
153
+
154
+ this.logger.info({ tnx: 'pool', cid, action: 'conection' }, '创建新的池资源 #%s', cid);
155
+ // 监听池资源事件
156
+ connection.on('available', () => {
157
+ this.logger.debug({ tnx: 'connection', cid, action: 'available' }, '连接 #%s 变为可用', cid);
158
+ this._closed ? this.close() : this.#processMessages(); // 如果已关闭,则从连接列表中移除此连接,否则检查是否还有其它发送
159
+ }).once('error', err => {
160
+ if (err.code !== 'EMAXLIMIT') this.logger.error({ err, tnx: 'pool', cid }, '池错误 #%s: %s', cid, err.message);
161
+ else this.logger.debug({ tnx: 'pool', cid, action: 'maxlimit' }, '连接 #%s 已达到最大消息限制', cid);
162
+
163
+ // 如果连接有队列条目,则调用回调并传递错误
164
+ if (connection.queueEntry) {
165
+ try {
166
+ connection.queueEntry.callback(err);
167
+ } catch (E) {
168
+ this.logger.error({ err: E, tnx: 'callback', cid }, '回调错误 #%s: %s', cid, E.message);
169
+ }
170
+ connection.queueEntry = false;
171
+ }
172
+
173
+ this.#removeConnection(connection), this.#continueProcessing(); // 从连接列表中移除错误的连接,然后继续处理队列
174
+ }).once('close', () => {
175
+ this.logger.info({ tnx: 'connection', cid, action: 'closed' }, '连接 #%s 已关闭', cid);
176
+ this.#removeConnection(connection);
177
+ const { queueEntry } = connection;
178
+ if (queueEntry)
179
+ // 如果连接在发送时关闭,将消息重新加入队列;如果未达到最大重试次数,则重新排队
180
+ setTimeout(() => {
181
+ this.#shouldRequeueOnClose(queueEntry) ? this.#requeueOnClose(connection) : this.#failOnClose(connection);
182
+ this.#continueProcessing();
183
+ }, 50); // 必须等待一下,因为"error"处理程序的回调可能在下一个事件循环中调用
184
+ else {
185
+ if (!this._closed && this.idling && !this._connections.length) this.emit('clear');
186
+ this.#continueProcessing();
187
+ }
188
+ });
189
+
190
+ this._connections.push(connection); // 将新连接添加到连接池
191
+ return connection;
192
+ }
193
+
194
+ /**
195
+ * 检查连接关闭时是否应该重新排队
196
+ */
197
+ #shouldRequeueOnClose(queueEntry) {
198
+ const { maxRequeues } = this.options;
199
+ return maxRequeues === undefined || maxRequeues < 0 || queueEntry.requeueAttempts < maxRequeues;
200
+ }
201
+
202
+ /**
203
+ * 连接关闭时失败传递
204
+ */
205
+ #failOnClose(connection) {
206
+ const { queueEntry, id } = connection;
207
+ if (queueEntry?.callback) {
208
+ try {
209
+ queueEntry.callback(new Error('连接关闭后达到最大重试次数'));
210
+ } catch (E) {
211
+ this.logger.error(
212
+ { err: E, tnx: 'callback', messageId: queueEntry.messageId, cid: id }, '回调错误 #%s: %s', id, E.message);
213
+ }
214
+ connection.queueEntry = false;
215
+ }
216
+ }
217
+
218
+ /**
219
+ * 连接关闭时重新排队条目
220
+ */
221
+ #requeueOnClose(connection) {
222
+ const { queueEntry, id } = connection;
223
+ queueEntry.requeueAttempts++;
224
+ const { messageId, requeueAttempts } = queueEntry;
225
+ this.logger.debug({ tnx: 'pool', cid: id, messageId, action: 'requeue' },
226
+ '为 #%s 重新排队消息 <%s>。尝试次数: #%s', messageId, id, requeueAttempts);
227
+ this._queue.unshift(queueEntry), connection.queueEntry = false;
228
+ }
229
+
230
+ /**
231
+ * 如果池未关闭,则继续处理消息
232
+ */
233
+ #continueProcessing() {
234
+ this._closed ? this.close() : setTimeout(() => this.#processMessages(), 100);
235
+ }
236
+
237
+ /**
238
+ * 从池中移除资源
239
+ *
240
+ * @param {Object} connection 要移除的 PoolResource
241
+ */
242
+ #removeConnection(connection) {
243
+ const index = this._connections.indexOf(connection);
244
+ if (index >= 0) this._connections.splice(index, 1);
245
+ }
246
+
247
+ /**
248
+ * 检查连接是否达到当前速率限制,如果是,则将可用性回调加入队列
249
+ *
250
+ * @param {Function} callback 速率限制清除后运行的回调函数
251
+ */
252
+ checkRateLimit(callback) {
253
+ const now = Date.now(), { limit, counter, waiting, checkpoint, delta, timeout } = this._rateLimit;
254
+ if (!limit) return callback();
255
+ if (counter < limit) return callback();
256
+
257
+ waiting.push(callback);
258
+ if (checkpoint <= now - delta) return this.#clearRateLimit();
259
+ else if (!timeout) {
260
+ this._rateLimit.timeout = setTimeout(() => this.#clearRateLimit(), delta - (now - checkpoint));
261
+ this._rateLimit.checkpoint = now;
262
+ }
263
+ }
264
+
265
+ /**
266
+ * 清除当前速率限制并运行暂停的回调
267
+ */
268
+ #clearRateLimit() {
269
+ clearTimeout(this._rateLimit.timeout);
270
+ this._rateLimit.timeout = null, this._rateLimit.counter = 0, this._rateLimit.checkpoint = false;
271
+
272
+ const { waiting } = this._rateLimit;
273
+ while (waiting.length) setImmediate(waiting.shift()); // 恢复所有暂停的连接
274
+ }
275
+
276
+ /**
277
+ * 如果队列中有空闲插槽,则返回true
278
+ */
279
+ isIdle() {
280
+ return this.idling;
281
+ }
282
+
283
+ /**
284
+ * 验证SMTP配置
285
+ *
286
+ * @param {Function} callback 回调函数
287
+ */
288
+ verify(callback) {
289
+ const auth = new PoolResource(this).auth; // 使用PoolResource的认证信息
290
+ return verifySmtp(this, callback, SmtpConnection, auth);
291
+ }
292
+ }
293
+
294
+ export { SmtpPool };
@@ -0,0 +1,93 @@
1
+ import { EventEmitter } from 'events';
2
+ import { XOAuth2 } from '../xoauth2.js';
3
+ import { SmtpConnection } from '../smtp-connection/index.js';
4
+ import {
5
+ createSmtpConnection, setupConnectionHandlers, performSmtpAuthentication, prepareMessageForSending,
6
+ handleSendResult, createAuthConfig
7
+ } from '../shared.js';
8
+
9
+ /**
10
+ * 为连接池创建资源元素
11
+ * >查看定义:@see {@link PoolResource}
12
+ * @constructor
13
+ * @param {Object} pool SmtpPool实例
14
+ */
15
+ class PoolResource extends EventEmitter {
16
+ constructor(pool) {
17
+ super();
18
+ this.pool = pool, this.options = pool.options, this.logger = this.pool.logger; // 所属连接池,连接选项,日志记录器
19
+ if (this.options.auth) this.auth = this.getAuth({}); // 如果提供了认证信息,则设置认证
20
+
21
+ this._connection = false, this._connected = false; // SMTP连接实例,连接状态标志
22
+ this.messages = 0, this.available = true; // 已发送消息计数,资源可用性标志
23
+ }
24
+
25
+ /**
26
+ * 获取认证信息
27
+ * @param {Object} authOpts 认证选项
28
+ * @returns {Object|boolean} 认证对象或false
29
+ */
30
+ getAuth(authOpts) {
31
+ if (!authOpts) return this.auth;
32
+ const authData = { ...this.options?.auth, ...authOpts, authMethod: this.options.authMethod || '' };
33
+ return createAuthConfig(authData, this.logger, this.mailer, XOAuth2); // 创建认证配置
34
+ }
35
+
36
+ /**
37
+ * 建立到SMTP服务器的连接
38
+ *
39
+ * @param {Function} callback 连接建立或失败后的回调函数
40
+ */
41
+ connect(callback) {
42
+ this.pool.getSocket(this.options, (err, socketOp) => {
43
+ if (err) return callback(err);
44
+ // 创建和配置SMTP连接并处理连接错误和结束事件
45
+ const { options: newOp, connection } = createSmtpConnection(this, socketOp, SmtpConnection),
46
+ state = { returned: false }, cleanC = setupConnectionHandlers(state, connection, callback, this);
47
+
48
+ // 连接建立后的处理(执行 SMTP 认证和清理监听)
49
+ connection.connect(() => {
50
+ if (state.returned) return;
51
+ performSmtpAuthentication(state, connection, this.auth, newOp, callback, this), cleanC();
52
+ });
53
+ });
54
+ }
55
+
56
+ /**
57
+ * 使用当前配置发送邮件
58
+ *
59
+ * @param {Object} mail 邮件对象
60
+ * @param {Function} callback 回调函数
61
+ */
62
+ send(mail, callback) {
63
+ if (!this._connected) return this.connect(err => err ? callback(err) : this.send(mail, callback));
64
+
65
+ const { envelope, readStream, from, to, messageId }
66
+ = prepareMessageForSending(mail, this.logger, this.id); // 邮件预处理
67
+ // 执行邮件发送
68
+ this.connection.send(envelope, readStream, (err, info) => {
69
+ this.messages++; // 增加消息计数
70
+
71
+ // 异步处理资源限制检查
72
+ setImmediate(() => {
73
+ if (this.messages >= this.options.maxMessages) {
74
+ const err = new Error('资源耗尽'); // 达到最大消息限制
75
+ err.code = 'EMAXLIMIT', this.connection.close(), this.emit('error', err);
76
+ }
77
+ // 检查速率限制并标记为可用
78
+ else this.pool.checkRateLimit(() => this.available = true, this.emit('available'));
79
+ });
80
+ const result = { err, info, from, to, messageId };
81
+ handleSendResult(result, callback, this.logger); // 处理发送结果
82
+ });
83
+ }
84
+
85
+ /**
86
+ * 关闭连接
87
+ */
88
+ close() {
89
+ this._connected = false, this.auth?.oauth2?.removeAllListeners(), this.connection?.close(), this.emit('close');
90
+ }
91
+ }
92
+
93
+ export { PoolResource };
@@ -0,0 +1,81 @@
1
+ import { EventEmitter } from 'events';
2
+ import { SmtpConnection } from './smtp-connection/index.js';
3
+ import { XOAuth2 } from './xoauth2.js';
4
+ import {
5
+ initSmtpConstructor, getSocket, createSmtpConnection, setupConnectionHandlers, createAuthConfig,
6
+ prepareMessageForSending, handleSendResult, performSmtpAuthentication, verifySmtp
7
+ } from './shared.js';
8
+
9
+ /**
10
+ * 为@flun/mailer创建SMTP传输对象
11
+ * >查看定义:@see {@link SmtpTransport}
12
+ * @constructor
13
+ * @param {Object} options 连接选项
14
+ */
15
+ class SmtpTransport extends EventEmitter {
16
+ constructor(options = {}) {
17
+ super();
18
+ this.name = 'SMTP';
19
+ initSmtpConstructor(this, options, SmtpConnection, 'smtpTransport'); // 初始化SMTP连接
20
+ if (this.options.auth) this.auth = this.getAuth({}); // 如果提供了认证信息,则设置认证
21
+ }
22
+
23
+ /**
24
+ * 获取认证信息
25
+ * @param {Object} authOpts 认证选项
26
+ * @returns {Object|boolean} 认证对象或false
27
+ */
28
+ getAuth(authOpts) {
29
+ if (!authOpts) return this.auth;
30
+ const authData = { ...this.options?.auth, ...authOpts };
31
+ return createAuthConfig(authData, this.logger, this.mailer, XOAuth2); // 创建认证配置
32
+ }
33
+
34
+ /**
35
+ * 使用选定的设置发送电子邮件
36
+ *
37
+ * @param {Object} mail 邮件对象
38
+ * @param {Function} callback 回调函数
39
+ */
40
+ send(mail, callback) {
41
+ getSocket(this.options, (err, socketOp) => {
42
+ if (err) return callback(err);
43
+ // 创建并配置SMTP连接,处理连接错误和结束事件
44
+ const { options: newOp, connection } = createSmtpConnection(this, socketOp, SmtpConnection),
45
+ state = { returned: false }, cleanC = setupConnectionHandlers(state, connection, callback, this),
46
+ // 发送消息
47
+ sendMessage = () => {
48
+ const { envelope, readStream, from, to, messageId } = prepareMessageForSending(mail, this.logger);//邮件预处理
49
+ connection.send(envelope, readStream, (err, info) => {
50
+ state.returned = true, connection.close();
51
+
52
+ const result = { err, info, from, to, messageId };
53
+ handleSendResult(result, callback, this.logger); // 处理发送结果
54
+ });
55
+ };
56
+ // 连接SMTP服务器
57
+ connection.connect(() => {
58
+ if (state.returned) return;
59
+ // 处理认证并发送消息
60
+ const auth = this.getAuth(mail.data?.auth);
61
+ performSmtpAuthentication(state, connection, auth, newOp, callback, this, sendMessage), cleanC();
62
+ });
63
+ });
64
+ }
65
+
66
+ /**
67
+ * 验证SMTP配置
68
+ *
69
+ * @param {Function} callback 回调函数
70
+ */
71
+ verify(callback) {
72
+ return verifySmtp(this, callback, SmtpConnection, this.auth);
73
+ }
74
+
75
+ // 释放资源
76
+ close() {
77
+ this.auth?.oauth2?.removeAllListeners(), this.emit('close');
78
+ }
79
+ }
80
+
81
+ export { SmtpTransport };
@@ -0,0 +1,59 @@
1
+ import { PK, getLogger, prepareMessageForSending, resolveStream } from './shared.js';
2
+
3
+ /**
4
+ * 创建一个输出内容是 流(Stream) 或 完整 Buffer的邮件内容的传输对象
5
+ * 适用于需要将邮件内容直接输出到控制台、文件或其他流接口的场景;
6
+ * 该传输对象不会实际发送邮件,而是将邮件内容以流或缓冲区的形式提供给调用者;
7
+ * >查看定义:@see {@link StreamTransport}
8
+ * @param {Object} [options] 配置选项
9
+ * @param {boolean} [options.buffer=false]
10
+ * - `false`(默认):**异步流式返回**,邮件内容以可读流 (Readable Stream) 的形式给出。
11
+ * 适合:需要实时推送、管道转发,或邮件体积很大不想一次全装进内存时。
12
+ * - `true`:**先收完整再返回**,邮件内容会被拼成一个 Buffer 对象一次性给你。
13
+ * 适合:需要加密、签名、保存文件等必须先拿到完整内容的场景。
14
+ * @param {string} [options.newline='']
15
+ * 生成的邮件正文使用哪种换行符:
16
+ * - `'windows'`, `'win'`, `'dos'` 或 `'\r\n'` → 使用 Windows 风格换行 `\r\n`
17
+ * - `'unix'`、空字符串或其他值 → 使用 Unix 风格换行 `\n`
18
+ * 通常保持默认即可;只有当你明确知道邮件客户端在 Windows 上有换行问题时才需设置。
19
+ *
20
+ * @constructor
21
+ */
22
+ class StreamTransport {
23
+ constructor(options = {}) {
24
+ this.options = options, this.name = 'StreamTransport', this.version = PK.version;
25
+
26
+ const { component = 'streamTransport', newline = '' } = this.options;
27
+ this.logger = getLogger(this.options, { component }); // 初始化日志记录器
28
+ this.winbreak = ['win', 'windows', 'dos', '\r\n'].includes(newline.toString().toLowerCase()); // 根据配置确定换行符类型
29
+ }
30
+
31
+ /**
32
+ * 编译 mailcomposer 消息并将其转发给发送处理器
33
+ *
34
+ * @param {Object} emailMessage MailComposer 对象
35
+ * @param {Function} callback 发送完成时运行的回调函数
36
+ */
37
+ send(mail, done) {
38
+ const { envelope, messageId, readStream } // 邮件预处理
39
+ = prepareMessageForSending(mail, this.logger, '', { transportType: 'stream', winbreak: this.winbreak });
40
+
41
+ // 使用 setImmediate 确保异步执行
42
+ setImmediate(() => {
43
+ const handleError = (err, context) => {
44
+ this.logger.error({ err, tnx: 'send', messageId }, context, messageId, err.message), done(err);
45
+ };
46
+
47
+ // 如果不使用缓冲区,直接返回流(流可读时才认为成功)
48
+ if (!this.options.buffer) {
49
+ readStream.once('readable', () => done(null, { envelope, messageId, message: readStream }))
50
+ .once('error', err => handleError(err, '为 %s 创建消息流失败。%s'))
51
+ }
52
+
53
+ // 将流数据读取到Buffer中
54
+ resolveStream(readStream, (err, value) => err ? handleError(err) : done(null, { envelope, messageId, message: value }));
55
+ });
56
+ }
57
+ }
58
+
59
+ export { StreamTransport };
@@ -0,0 +1,54 @@
1
+ import SERVICES from './services.json' with { type: 'json' }; // 导入服务配置
2
+ import { regexs, resetRegex } from '../regexs.js'; // 导入正则表达式及其重置函数
3
+
4
+ const normalized = {}, // 储存规范的服务对象
5
+
6
+ /**
7
+ * 规范化键名
8
+ * 移除非字母数字、点号和连字符的字符,并转换为小写
9
+ * @param {String} key - 需要规范化的键名
10
+ * @returns {String} 规范化后的键名
11
+ */
12
+ normalizeKey = key => {
13
+ const rNADH = resetRegex(regexs.NON_ALPHANUMERIC_DOT_HYPHEN);
14
+ return key.replace(rNADH, '').toLowerCase();
15
+ },
16
+
17
+ /**
18
+ * 规范化服务对象
19
+ * 过滤掉不需要的字段(domains 和 aliases)
20
+ * @param {Object} service - 原始服务对象
21
+ * @returns {Object} 规范化后的服务对象
22
+ */
23
+ normalizeService = service => {
24
+ const filter = ['domains', 'aliases']; // 需要过滤的字段
25
+ return Object.fromEntries(
26
+ Object.entries(service).filter(([key]) => !filter.includes(key)) // 只保留不在过滤列表中的属性
27
+ );
28
+ };
29
+
30
+ // 遍历所有服务键名
31
+ Object.keys(SERVICES).forEach(key => {
32
+ const service = SERVICES[key],
33
+
34
+ // 为指定键添加规范化服务条目
35
+ addServiceEntry = sourceKey => {
36
+ normalized[normalizeKey(sourceKey)] = normalizeService(service);
37
+ };
38
+ // 处理主键,别名,域名
39
+ addServiceEntry(key), (service.aliases ?? []).forEach(addServiceEntry), (service.domains ?? []).forEach(addServiceEntry);
40
+ });
41
+
42
+ /**
43
+ * 根据提供的键名解析 SMTP 配置;
44
+ * 键名可以是服务名称(如 'Gmail')、别名(如 'Google Mail')或邮箱地址(如 'test@googlemail.com')
45
+ * >查看定义:@see {@link wellKnown}
46
+ * @param {String} key - 服务名称、别名或邮箱地址
47
+ * @returns {Object|Boolean} SMTP 配置对象,如果未找到则返回 false
48
+ */
49
+ const wellKnown = key => {
50
+ key = normalizeKey(key.split('@').pop()); // 如果是邮箱地址,提取域名部分并规范化
51
+ return normalized[key] || false; // 返回对应的配置对象,未找到则返回 false
52
+ }
53
+
54
+ export { wellKnown }