@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/smtp-pool/index.js
CHANGED
|
@@ -34,16 +34,16 @@ class SmtpPool extends EventEmitter {
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
/**
|
|
37
|
-
*
|
|
37
|
+
* 将电子邮件加入队列,使用选定设置发送
|
|
38
38
|
*
|
|
39
39
|
* @param {Object} mail 邮件对象
|
|
40
40
|
* @param {Function} callback 回调函数
|
|
41
41
|
*/
|
|
42
42
|
send(mail, callback) {
|
|
43
|
-
if (this._closed) return false; //
|
|
43
|
+
if (this._closed) return false; // 如果连接池已关闭,则返回false
|
|
44
44
|
this._queue.push({ mail, requeueAttempts: 0, callback }); // 将邮件加入队列(包括重试尝试计数)
|
|
45
45
|
|
|
46
|
-
//
|
|
46
|
+
// 如果队列长度达到最大连接数且处于空闲状态,则标记为非空闲
|
|
47
47
|
if (this.idling && this._queue.length >= this.options.maxConnections) this.idling = false;
|
|
48
48
|
setImmediate(() => this.#processMessages()); // 立即开始处理消息
|
|
49
49
|
|
|
@@ -51,13 +51,13 @@ class SmtpPool extends EventEmitter {
|
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
/**
|
|
54
|
-
*
|
|
54
|
+
* 关闭池中的所有连接;如果有消息正在发送,则稍后关闭连接
|
|
55
55
|
*/
|
|
56
56
|
close() {
|
|
57
|
-
this._closed = true, clearTimeout(this._rateLimit.timeout); //
|
|
57
|
+
this._closed = true, clearTimeout(this._rateLimit.timeout); // 标记为已关闭并清除速率限制定时器(如果存在)
|
|
58
58
|
const conLen = this._connections.length;
|
|
59
59
|
let queLen = this._queue.length;
|
|
60
|
-
if (!conLen && !queLen) return; //
|
|
60
|
+
if (!conLen && !queLen) return; // 如果没有连接和队列,则直接返回
|
|
61
61
|
|
|
62
62
|
// 移除所有可用连接
|
|
63
63
|
for (let i = conLen - 1; i >= 0; i--) {
|
|
@@ -75,7 +75,7 @@ class SmtpPool extends EventEmitter {
|
|
|
75
75
|
const entry = this._queue.shift();
|
|
76
76
|
if (entry && typeof entry.callback === 'function') {
|
|
77
77
|
try {
|
|
78
|
-
entry.callback(new Error(
|
|
78
|
+
entry.callback(new Error(`连接池已关闭,队列中还有 ${queLen} 个待处理请求`));
|
|
79
79
|
} catch (E) {
|
|
80
80
|
this.logger.error({ err: E, tnx: 'callback' }, '回调错误: %s', E.message);
|
|
81
81
|
}
|
|
@@ -88,7 +88,7 @@ class SmtpPool extends EventEmitter {
|
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
/**
|
|
91
|
-
*
|
|
91
|
+
* 检查队列和可用连接;如果有要发送的消息且有可用连接,则使用此连接发送邮件
|
|
92
92
|
*/
|
|
93
93
|
#processMessages() {
|
|
94
94
|
let connection;
|
|
@@ -97,8 +97,8 @@ class SmtpPool extends EventEmitter {
|
|
|
97
97
|
this.idling = true, this.emit('idle'); // 标记为空闲
|
|
98
98
|
};
|
|
99
99
|
|
|
100
|
-
if (this._closed) return; //
|
|
101
|
-
//
|
|
100
|
+
if (this._closed) return; // 如果已关闭,则不执行任何操作
|
|
101
|
+
// 如果队列为空,则不执行任何操作
|
|
102
102
|
if (!queLen) {
|
|
103
103
|
if (!this.idling) idlingEmit();
|
|
104
104
|
return;
|
|
@@ -111,10 +111,10 @@ class SmtpPool extends EventEmitter {
|
|
|
111
111
|
break;
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
-
//
|
|
114
|
+
// 如果没有可用连接且未达到最大连接数,则创建新连接
|
|
115
115
|
const { maxConnections } = this.options;
|
|
116
116
|
if (!connection && conLen < maxConnections) connection = this.#createConnection();
|
|
117
|
-
if (!connection) return this.idling = false; //
|
|
117
|
+
if (!connection) return this.idling = false; // 如果没有可用连接,则标记为非空闲并返回
|
|
118
118
|
if (!this.idling && queLen < maxConnections) idlingEmit(); // 检查处理队列中是否有空闲空间
|
|
119
119
|
|
|
120
120
|
// 从队列中取出一个条目并分配给连接
|
|
@@ -155,12 +155,13 @@ class SmtpPool extends EventEmitter {
|
|
|
155
155
|
// 监听池资源事件
|
|
156
156
|
connection.on('available', () => {
|
|
157
157
|
this.logger.debug({ tnx: 'connection', cid, action: 'available' }, '连接 #%s 变为可用', cid);
|
|
158
|
-
|
|
158
|
+
// 如果已关闭,则从连接列表中移除此连接,否则检查是否还有其它发送
|
|
159
|
+
this._closed ? this.close() : this.#processMessages();
|
|
159
160
|
}).once('error', err => {
|
|
160
161
|
if (err.code !== 'EMAXLIMIT') this.logger.error({ err, tnx: 'pool', cid }, '池错误 #%s: %s', cid, err.message);
|
|
161
162
|
else this.logger.debug({ tnx: 'pool', cid, action: 'maxlimit' }, '连接 #%s 已达到最大消息限制', cid);
|
|
162
163
|
|
|
163
|
-
//
|
|
164
|
+
// 如果连接有队列条目,则调用回调并传递错误
|
|
164
165
|
if (connection.queueEntry) {
|
|
165
166
|
try {
|
|
166
167
|
connection.queueEntry.callback(err);
|
|
@@ -176,11 +177,12 @@ class SmtpPool extends EventEmitter {
|
|
|
176
177
|
this.#removeConnection(connection);
|
|
177
178
|
const { queueEntry } = connection;
|
|
178
179
|
if (queueEntry)
|
|
179
|
-
//
|
|
180
|
+
// 如果连接在发送时关闭,将消息重新加入队列;如果未达到最大重试次数,则重新排队
|
|
180
181
|
setTimeout(() => {
|
|
181
|
-
this.#shouldRequeueOnClose(queueEntry) ? this.#requeueOnClose(connection)
|
|
182
|
+
this.#shouldRequeueOnClose(queueEntry) ? this.#requeueOnClose(connection)
|
|
183
|
+
: this.#failOnClose(connection);
|
|
182
184
|
this.#continueProcessing();
|
|
183
|
-
}, 50); //
|
|
185
|
+
}, 50); // 必须等待一下,因为"error"处理程序的回调可能在下一个事件循环中调用
|
|
184
186
|
else {
|
|
185
187
|
if (!this._closed && this.idling && !this._connections.length) this.emit('clear');
|
|
186
188
|
this.#continueProcessing();
|
|
@@ -209,7 +211,8 @@ class SmtpPool extends EventEmitter {
|
|
|
209
211
|
queueEntry.callback(new Error('连接关闭后达到最大重试次数'));
|
|
210
212
|
} catch (E) {
|
|
211
213
|
this.logger.error(
|
|
212
|
-
{ err: E, tnx: 'callback', messageId: queueEntry.messageId, cid: id },
|
|
214
|
+
{ err: E, tnx: 'callback', messageId: queueEntry.messageId, cid: id },
|
|
215
|
+
'回调错误 #%s: %s', id, E.message);
|
|
213
216
|
}
|
|
214
217
|
connection.queueEntry = false;
|
|
215
218
|
}
|
|
@@ -223,12 +226,12 @@ class SmtpPool extends EventEmitter {
|
|
|
223
226
|
queueEntry.requeueAttempts++;
|
|
224
227
|
const { messageId, requeueAttempts } = queueEntry;
|
|
225
228
|
this.logger.debug({ tnx: 'pool', cid: id, messageId, action: 'requeue' },
|
|
226
|
-
'为 #%s 重新排队消息 <%s
|
|
229
|
+
'为 #%s 重新排队消息 <%s>;尝试次数: #%s', messageId, id, requeueAttempts);
|
|
227
230
|
this._queue.unshift(queueEntry), connection.queueEntry = false;
|
|
228
231
|
}
|
|
229
232
|
|
|
230
233
|
/**
|
|
231
|
-
*
|
|
234
|
+
* 如果池未关闭,则继续处理消息
|
|
232
235
|
*/
|
|
233
236
|
#continueProcessing() {
|
|
234
237
|
this._closed ? this.close() : setTimeout(() => this.#processMessages(), 100);
|
|
@@ -245,7 +248,7 @@ class SmtpPool extends EventEmitter {
|
|
|
245
248
|
}
|
|
246
249
|
|
|
247
250
|
/**
|
|
248
|
-
*
|
|
251
|
+
* 检查连接是否达到当前速率限制,如果是,则将可用性回调加入队列
|
|
249
252
|
*
|
|
250
253
|
* @param {Function} callback 速率限制清除后运行的回调函数
|
|
251
254
|
*/
|
|
@@ -274,7 +277,7 @@ class SmtpPool extends EventEmitter {
|
|
|
274
277
|
}
|
|
275
278
|
|
|
276
279
|
/**
|
|
277
|
-
*
|
|
280
|
+
* 如果队列中有空闲插槽,则返回true
|
|
278
281
|
*/
|
|
279
282
|
isIdle() {
|
|
280
283
|
return this.idling;
|
|
@@ -16,7 +16,7 @@ class PoolResource extends EventEmitter {
|
|
|
16
16
|
constructor(pool) {
|
|
17
17
|
super();
|
|
18
18
|
this.pool = pool, this.options = pool.options, this.logger = this.pool.logger; // 所属连接池,连接选项,日志记录器
|
|
19
|
-
if (this.options.auth) this.auth = this.getAuth({}); //
|
|
19
|
+
if (this.options.auth) this.auth = this.getAuth({}); // 如果提供了认证信息,则设置认证
|
|
20
20
|
|
|
21
21
|
this._connection = false, this._connected = false; // SMTP连接实例,连接状态标志
|
|
22
22
|
this.messages = 0, this.available = true; // 已发送消息计数,资源可用性标志
|
package/lib/smtp-transport.js
CHANGED
|
@@ -17,7 +17,7 @@ class SmtpTransport extends EventEmitter {
|
|
|
17
17
|
super();
|
|
18
18
|
this.name = 'SMTP';
|
|
19
19
|
initSmtpConstructor(this, options, SmtpConnection, 'smtpTransport'); // 初始化SMTP连接
|
|
20
|
-
if (this.options.auth) this.auth = this.getAuth({}); //
|
|
20
|
+
if (this.options.auth) this.auth = this.getAuth({}); // 如果提供了认证信息,则设置认证
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
/**
|
|
@@ -45,7 +45,8 @@ class SmtpTransport extends EventEmitter {
|
|
|
45
45
|
state = { returned: false }, cleanC = setupConnectionHandlers(state, connection, callback, this),
|
|
46
46
|
// 发送消息
|
|
47
47
|
sendMessage = () => {
|
|
48
|
-
|
|
48
|
+
//邮件预处理
|
|
49
|
+
const { envelope, readStream, from, to, messageId } = prepareMessageForSending(mail, this.logger);
|
|
49
50
|
connection.send(envelope, readStream, (err, info) => {
|
|
50
51
|
state.returned = true, connection.close();
|
|
51
52
|
|
package/lib/stream-transport.js
CHANGED
|
@@ -7,15 +7,15 @@ import { PK, getLogger, prepareMessageForSending, resolveStream } from './shared
|
|
|
7
7
|
* >查看定义:@see {@link StreamTransport}
|
|
8
8
|
* @param {Object} [options] 配置选项
|
|
9
9
|
* @param {boolean} [options.buffer=false]
|
|
10
|
-
* - `false
|
|
11
|
-
*
|
|
12
|
-
* - `true
|
|
13
|
-
*
|
|
10
|
+
* - `false`(默认):**异步流式返回**,邮件内容以可读流 (Readable Stream) 的形式给出;
|
|
11
|
+
* 适合:需要实时推送、管道转发,或邮件体积很大不想一次全装进内存时;
|
|
12
|
+
* - `true`:**先收完整再返回**,邮件内容会被拼成一个 Buffer 对象一次性给你;
|
|
13
|
+
* 适合:需要加密、签名、保存文件等必须先拿到完整内容的场景;
|
|
14
14
|
* @param {string} [options.newline='']
|
|
15
|
-
*
|
|
15
|
+
* 生成的邮件正文使用哪种换行符:
|
|
16
16
|
* - `'windows'`, `'win'`, `'dos'` 或 `'\r\n'` → 使用 Windows 风格换行 `\r\n`
|
|
17
17
|
* - `'unix'`、空字符串或其他值 → 使用 Unix 风格换行 `\n`
|
|
18
|
-
*
|
|
18
|
+
* 通常保持默认即可;只有当你明确知道邮件客户端在 Windows 上有换行问题时才需设置;
|
|
19
19
|
*
|
|
20
20
|
* @constructor
|
|
21
21
|
*/
|
|
@@ -25,7 +25,8 @@ class StreamTransport {
|
|
|
25
25
|
|
|
26
26
|
const { component = 'streamTransport', newline = '' } = this.options;
|
|
27
27
|
this.logger = getLogger(this.options, { component }); // 初始化日志记录器
|
|
28
|
-
|
|
28
|
+
// 根据配置确定换行符类型
|
|
29
|
+
this.winbreak = ['win', 'windows', 'dos', '\r\n'].includes(newline.toString().toLowerCase());
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
/**
|
|
@@ -44,14 +45,15 @@ class StreamTransport {
|
|
|
44
45
|
this.logger.error({ err, tnx: 'send', messageId }, context, messageId, err.message), done(err);
|
|
45
46
|
};
|
|
46
47
|
|
|
47
|
-
//
|
|
48
|
+
// 如果不使用缓冲区,直接返回流(流可读时才认为成功)
|
|
48
49
|
if (!this.options.buffer) {
|
|
49
50
|
readStream.once('readable', () => done(null, { envelope, messageId, message: readStream }))
|
|
50
|
-
.once('error', err => handleError(err, '为 %s
|
|
51
|
+
.once('error', err => handleError(err, '为 %s 创建消息流失败;%s'))
|
|
51
52
|
}
|
|
52
53
|
|
|
53
54
|
// 将流数据读取到Buffer中
|
|
54
|
-
resolveStream(readStream,
|
|
55
|
+
resolveStream(readStream,
|
|
56
|
+
(err, value) => err ? handleError(err) : done(null, { envelope, messageId, message: value }));
|
|
55
57
|
});
|
|
56
58
|
}
|
|
57
59
|
}
|
package/lib/well-known/index.js
CHANGED
|
@@ -5,7 +5,7 @@ const normalized = {}, // 储存规范的服务对象
|
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* 规范化键名
|
|
8
|
-
*
|
|
8
|
+
* 移除非字母数字、点号和连字符的字符,并转换为小写
|
|
9
9
|
* @param {String} key - 需要规范化的键名
|
|
10
10
|
* @returns {String} 规范化后的键名
|
|
11
11
|
*/
|
|
@@ -16,7 +16,7 @@ const normalized = {}, // 储存规范的服务对象
|
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
18
|
* 规范化服务对象
|
|
19
|
-
*
|
|
19
|
+
* 过滤掉不需要的字段(domains 和 aliases)
|
|
20
20
|
* @param {Object} service - 原始服务对象
|
|
21
21
|
* @returns {Object} 规范化后的服务对象
|
|
22
22
|
*/
|
|
@@ -36,12 +36,13 @@ Object.keys(SERVICES).forEach(key => {
|
|
|
36
36
|
normalized[normalizeKey(sourceKey)] = normalizeService(service);
|
|
37
37
|
};
|
|
38
38
|
// 处理主键,别名,域名
|
|
39
|
-
addServiceEntry(key),
|
|
39
|
+
addServiceEntry(key),
|
|
40
|
+
(service.aliases ?? []).forEach(addServiceEntry), (service.domains ?? []).forEach(addServiceEntry);
|
|
40
41
|
});
|
|
41
42
|
|
|
42
43
|
/**
|
|
43
44
|
* 根据提供的键名解析 SMTP 配置;
|
|
44
|
-
*
|
|
45
|
+
* 键名可以是服务名称(如 'Gmail')、别名(如 'Google Mail')或邮箱地址(如 'test@googlemail.com')
|
|
45
46
|
* >查看定义:@see {@link wellKnown}
|
|
46
47
|
* @param {String} key - 服务名称、别名或邮箱地址
|
|
47
48
|
* @returns {Object|Boolean} SMTP 配置对象,如果未找到则返回 false
|
package/lib/xoauth2.js
CHANGED
|
@@ -8,13 +8,13 @@ import { nmfetch, getLogger, resolveStream } from './shared.js';
|
|
|
8
8
|
* 有关获取用户所需 refreshToken 的离线访问,请参阅离线访问
|
|
9
9
|
* https://developers.google.com/accounts/docs/OAuth2WebServer#offline
|
|
10
10
|
*
|
|
11
|
-
* 使用 provisionCallback
|
|
11
|
+
* 使用 provisionCallback 自定义方法生成访问令牌的用法:
|
|
12
12
|
* provisionCallback(user, renew, callback);
|
|
13
13
|
* * user 是要获取令牌的用户名
|
|
14
|
-
* * renew
|
|
14
|
+
* * renew 是一个布尔值,如果为 true 表示现有令牌失败需要更新
|
|
15
15
|
* * callback 是带有 (error, accessToken [, expires]) 的回调函数
|
|
16
16
|
* * accessToken 是一个字符串
|
|
17
|
-
* * expires
|
|
17
|
+
* * expires 是可选的过期时间(毫秒)
|
|
18
18
|
* 如果使用了 provisionCallback,则 @flun/mailer 不会尝试自行生成令牌
|
|
19
19
|
* >查看定义:@see {@link XOAuth2}
|
|
20
20
|
* @constructor
|
|
@@ -26,8 +26,8 @@ import { nmfetch, getLogger, resolveStream } from './shared.js';
|
|
|
26
26
|
* @param {String} options.accessUrl 令牌生成端点,默认为 'https://accounts.google.com/o/oauth2/token'
|
|
27
27
|
* @param {String} options.accessToken 现有的有效访问令牌
|
|
28
28
|
* @param {String} options.privateKey JWT 私钥
|
|
29
|
-
* @param {Number} options.expires
|
|
30
|
-
* @param {Number} options.timeout 可选的访问令牌 TTL
|
|
29
|
+
* @param {Number} options.expires 可选的访问令牌过期时间(毫秒)
|
|
30
|
+
* @param {Number} options.timeout 可选的访问令牌 TTL(秒)
|
|
31
31
|
* @param {Function} options.provisionCallback 需要新访问令牌时运行的函数
|
|
32
32
|
*/
|
|
33
33
|
class XOAuth2 extends Stream {
|
|
@@ -39,7 +39,8 @@ class XOAuth2 extends Stream {
|
|
|
39
39
|
expires, timeout, accessUrl, customHeaders = {}, customParams = {} } = this.options;
|
|
40
40
|
// 服务账户验证
|
|
41
41
|
if (this.options?.serviceClient) {
|
|
42
|
-
if (!privateKey || !user)
|
|
42
|
+
if (!privateKey || !user)
|
|
43
|
+
return setImmediate(() => this.emit('error', new Error('服务账户需要 "privateKey" 和 "user" 选项!')));
|
|
43
44
|
this.options.serviceRequestTimeout = Math.min(Math.max(Number(serviceRequestTimeout) || 0, 0), 3600) || 5 * 60;
|
|
44
45
|
}
|
|
45
46
|
|
|
@@ -55,35 +56,37 @@ class XOAuth2 extends Stream {
|
|
|
55
56
|
}
|
|
56
57
|
|
|
57
58
|
/**
|
|
58
|
-
*
|
|
59
|
+
* 返回或生成(如果之前的已过期)XOAuth2 令牌
|
|
59
60
|
*
|
|
60
|
-
* @param {Boolean} renew 如果为 false
|
|
61
|
+
* @param {Boolean} renew 如果为 false 则使用缓存的访问令牌(如果可用)
|
|
61
62
|
* @param {Function} callback 带有错误对象和令牌字符串的回调函数
|
|
62
63
|
*/
|
|
63
64
|
getToken(renew, callback) {
|
|
64
|
-
const { user, refreshToken, serviceClient } = this.options,
|
|
65
|
+
const { user, refreshToken, serviceClient } = this.options,
|
|
66
|
+
{ accessToken, expires, provisionCallback, renewing } = this,
|
|
65
67
|
// 重用令牌
|
|
66
68
|
reuseToken = message => {
|
|
67
69
|
this.logger.debug({ tnx: 'OAUTH2', user, action: 'reuse' }, message, user);
|
|
68
70
|
return callback(null, accessToken);
|
|
69
71
|
};
|
|
70
72
|
|
|
71
|
-
//
|
|
73
|
+
// 如果不需要续订且令牌有效,则直接重用
|
|
72
74
|
if (!renew && accessToken && (!expires || expires > Date.now())) return reuseToken('为 %s 重用现有访问令牌');
|
|
73
|
-
//
|
|
75
|
+
// 检查是否可以续订,如果不能,返回当前令牌或错误
|
|
74
76
|
if (!provisionCallback && !refreshToken && !serviceClient) {
|
|
75
|
-
if (accessToken) return reuseToken('为 %s
|
|
76
|
-
this.logger.error({ tnx: 'OAUTH2', user, action: 'renew' }, '无法为 %s
|
|
77
|
+
if (accessToken) return reuseToken('为 %s 重用现有访问令牌(无刷新能力)');
|
|
78
|
+
this.logger.error({ tnx: 'OAUTH2', user, action: 'renew' }, '无法为 %s 续订访问令牌:无可用刷新机制', user);
|
|
77
79
|
return callback(new Error("无法为用户创建新的访问令牌"));
|
|
78
80
|
}
|
|
79
81
|
|
|
80
|
-
//
|
|
82
|
+
// 如果已经在续订中,将此请求加入队列而不是开始另一个
|
|
81
83
|
if (renewing) return this.renewalQueue.push({ renew, callback });
|
|
82
84
|
this.renewing = true;
|
|
83
85
|
|
|
84
86
|
// 处理令牌续订完成 - 处理队列中的请求并清理
|
|
85
87
|
const generateCallback = (err, accessToken) => {
|
|
86
|
-
this.renewalQueue.forEach(item => item.callback(err, accessToken)),
|
|
88
|
+
this.renewalQueue.forEach(item => item.callback(err, accessToken)),
|
|
89
|
+
this.renewalQueue = [], this.renewing = false;
|
|
87
90
|
|
|
88
91
|
if (err) this.logger.error({ err, tnx: 'OAUTH2', user, action: 'renew' }, '为 %s 生成新访问令牌失败', user);
|
|
89
92
|
else this.logger.info({ tnx: 'OAUTH2', user, action: 'renew' }, '为 %s 生成新访问令牌', user);
|
|
@@ -102,9 +105,9 @@ class XOAuth2 extends Stream {
|
|
|
102
105
|
* 更新令牌值
|
|
103
106
|
*
|
|
104
107
|
* @param {String} accessToken 新的访问令牌
|
|
105
|
-
* @param {Number} timeout
|
|
108
|
+
* @param {Number} timeout 访问令牌生命周期(秒)
|
|
106
109
|
*
|
|
107
|
-
* 触发 'token'
|
|
110
|
+
* 触发 'token' 事件:{ user: 用户电子邮件地址, accessToken: 新的访问令牌, timeout: TTL(秒)}
|
|
108
111
|
*/
|
|
109
112
|
updateToken(accessToken = '', timeout) {
|
|
110
113
|
this.accessToken = accessToken, timeout = Math.max(Number(timeout) || 0, 0);
|
|
@@ -124,7 +127,8 @@ class XOAuth2 extends Stream {
|
|
|
124
127
|
if (serviceClient) {
|
|
125
128
|
// 服务账户 - https://developers.google.com/identity/protocols/OAuth2ServiceAccount
|
|
126
129
|
const iat = Math.floor(Date.now() / 1000), // unix 时间
|
|
127
|
-
tokenData = { iss: serviceClient, scope, sub: user, aud: accessUrl, iat,
|
|
130
|
+
tokenData = { iss: serviceClient, scope, sub: user, aud: accessUrl, iat,
|
|
131
|
+
exp: iat + serviceRequestTimeout };
|
|
128
132
|
let assertion;
|
|
129
133
|
try {
|
|
130
134
|
assertion = this.jwtSignRS256(tokenData);
|
|
@@ -165,7 +169,7 @@ class XOAuth2 extends Stream {
|
|
|
165
169
|
return callback(new Error('无效的身份验证响应'));
|
|
166
170
|
}
|
|
167
171
|
|
|
168
|
-
//
|
|
172
|
+
// 记录响应数据(隐藏访问令牌的完整内容)
|
|
169
173
|
const logData = Object.fromEntries(
|
|
170
174
|
Object.entries(data).map(([k, v = '']) => [k, k === 'access_token' ? `${v.toString().slice(0, 6)}...` : v])
|
|
171
175
|
);
|
|
@@ -173,7 +177,7 @@ class XOAuth2 extends Stream {
|
|
|
173
177
|
this.logger.debug({ tnx, user, action }, '响应: %s', JSON.stringify(logData));
|
|
174
178
|
|
|
175
179
|
if (data.error) {
|
|
176
|
-
let errorMessage = data.error; //
|
|
180
|
+
let errorMessage = data.error; // 错误响应:https://tools.ietf.org/html/rfc6749#section-5.2
|
|
177
181
|
|
|
178
182
|
if (data.error_description) errorMessage += `: ${data.error_description}`;
|
|
179
183
|
if (data.error_uri) errorMessage += ` (${data.error_uri})`;
|
|
@@ -202,19 +206,20 @@ class XOAuth2 extends Stream {
|
|
|
202
206
|
}
|
|
203
207
|
|
|
204
208
|
/**
|
|
205
|
-
* 自定义 POST
|
|
209
|
+
* 自定义 POST 请求处理程序;
|
|
206
210
|
* 这仅在需要保持 Windows 中路径短时才需要;通常此模块
|
|
207
|
-
*
|
|
208
|
-
* 比如 request
|
|
209
|
-
* 由于只进行简单的 POST
|
|
210
|
-
*
|
|
211
|
+
* 是依赖项的依赖项,如果它尝试 require 某些东西
|
|
212
|
+
* 比如 request 模块,路径对 Windows 来说会变得太长而无法处理;
|
|
213
|
+
* 由于只进行简单的 POST 请求,所以实际上不需要复杂的
|
|
214
|
+
* 逻辑支持(没有重定向,没有其他任何东西);
|
|
211
215
|
*
|
|
212
216
|
* @param {String} url 要 POST 的 URL
|
|
213
217
|
* @param {String|Buffer} payload 要 POST 的有效负载
|
|
214
218
|
* @param {Function} callback 带有 (err, buff) 的回调函数
|
|
215
219
|
*/
|
|
216
220
|
postRequest(url, payload, params, callback) {
|
|
217
|
-
const req = nmfetch(url, { method: 'post', headers: params.customHeaders, body: payload,
|
|
221
|
+
const req = nmfetch(url, { method: 'post', headers: params.customHeaders, body: payload,
|
|
222
|
+
allowErrorResponse: true });
|
|
218
223
|
resolveStream(req, callback); // 将流数据读取到Buffer中
|
|
219
224
|
}
|
|
220
225
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flun/mailer",
|
|
3
|
-
"version": "2.1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "2.1.25",
|
|
4
|
+
"description": "Node.js 邮件发送库;支持 SMTP、SMTP Pool、Sendmail、Stream、JSON、SES 多种传输;",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
7
7
|
".": "./index.js"
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"type": "git",
|
|
44
44
|
"url": "git+https://github.com/OpenFlun/mailer.git"
|
|
45
45
|
},
|
|
46
|
-
"homepage": "https://
|
|
46
|
+
"homepage": "https://github.com/OpenFlun/mailer#readme",
|
|
47
47
|
"bugs": {
|
|
48
48
|
"url": "https://github.com/OpenFlun/mailer/issues"
|
|
49
49
|
},
|
|
@@ -53,10 +53,10 @@
|
|
|
53
53
|
"npm": ">=10.0.0"
|
|
54
54
|
},
|
|
55
55
|
"dependencies": {
|
|
56
|
-
"@aws-sdk/client-sesv2": "^3.
|
|
56
|
+
"@aws-sdk/client-sesv2": "^3.1156.0"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
|
-
"eslint": "^10.
|
|
59
|
+
"eslint": "^10.13.0",
|
|
60
60
|
"jest": "^30.5.2"
|
|
61
61
|
},
|
|
62
62
|
"peerDependencies": {
|