@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/punycode.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
Punycode.js
|
|
3
|
-
|
|
4
|
-
1.将Unicode字符串转换为Punycode
|
|
5
|
-
2.将Punycode字符串转换回Unicode
|
|
6
|
-
3
|
|
7
|
-
4.将Unicode字符串转换为Punycode
|
|
3
|
+
主要功能包括:
|
|
4
|
+
1.将Unicode字符串转换为Punycode字符串(仅包含ASCII字符);
|
|
5
|
+
2.将Punycode字符串转换回Unicode字符串;
|
|
6
|
+
3.处理域名或电子邮件地址的转换,其中只有非ASCII部分会被转换;
|
|
7
|
+
4.将Unicode字符串转换为Punycode字符串,并将结果附加到域名或电子邮件地址的非ASCII部分之前;
|
|
8
8
|
*/
|
|
9
9
|
import { regexs, resetRegex, } from './regexs.js';
|
|
10
10
|
|
|
@@ -14,7 +14,8 @@ const maxInt = 2147483647, // 即 0x7FFFFFFF 或 2^31-1
|
|
|
14
14
|
base = 36, tMin = 1, tMax = 26, skew = 38, damp = 700, initialBias = 72, initialN = 128, delimiter = '-',
|
|
15
15
|
/** 错误消息 */
|
|
16
16
|
errors = {
|
|
17
|
-
overflow: '
|
|
17
|
+
overflow: '溢出:输入需要更宽的整数来处理', 'not-basic': '非法输入 >= 0x80(不是基本码点)',
|
|
18
|
+
'invalid-input': '无效输入'
|
|
18
19
|
},
|
|
19
20
|
/** 便捷缩写 */
|
|
20
21
|
baseMinusTMin = base - tMin, floor = Math.floor, strFromCharCode = String.fromCharCode, // 字符代码转字符串函数
|
|
@@ -46,7 +47,7 @@ const maxInt = 2147483647, // 即 0x7FFFFFFF 或 2^31-1
|
|
|
46
47
|
},
|
|
47
48
|
|
|
48
49
|
/**
|
|
49
|
-
* 一个简单的类似 `Array#map`
|
|
50
|
+
* 一个简单的类似 `Array#map` 的包装器,用于处理域名字符串或电子邮件地址
|
|
50
51
|
* @private
|
|
51
52
|
* @param {String} domain 域名或电子邮件地址
|
|
52
53
|
* @param {Function} callback 为每个字符调用的函数
|
|
@@ -55,9 +56,9 @@ const maxInt = 2147483647, // 即 0x7FFFFFFF 或 2^31-1
|
|
|
55
56
|
mapDomain = (domain, callback) => {
|
|
56
57
|
const parts = domain.split('@');
|
|
57
58
|
let result = '';
|
|
58
|
-
//
|
|
59
|
+
// 在电子邮件地址中,只有域名部分应进行punycode编码;保持本地部分(即`@`之前的所有内容)不变
|
|
59
60
|
if (parts.length > 1) result = `${parts[0]}@`, domain = parts[1];
|
|
60
|
-
// 为了IE8
|
|
61
|
+
// 为了IE8兼容性,避免使用`split(regex)`;参见 #17
|
|
61
62
|
const rS = resetRegex(regexs.SEPARATORS);
|
|
62
63
|
domain = domain.replace(rS, '\x2E');
|
|
63
64
|
const encoded = map(domain.split('.'), callback).join('.');
|
|
@@ -65,13 +66,13 @@ const maxInt = 2147483647, // 即 0x7FFFFFFF 或 2^31-1
|
|
|
65
66
|
},
|
|
66
67
|
|
|
67
68
|
/**
|
|
68
|
-
* 创建一个包含字符串中每个Unicode
|
|
69
|
-
* 虽然JavaScript内部使用UCS-2
|
|
70
|
-
*
|
|
69
|
+
* 创建一个包含字符串中每个Unicode字符的数字码点的数组;
|
|
70
|
+
* 虽然JavaScript内部使用UCS-2,但此函数会将一对代理半部(UCS-2将每个半部公开为单独的字符)
|
|
71
|
+
* 转换为单个码点,以匹配UTF-16;
|
|
71
72
|
* @see `punycode.ucs2.encode`
|
|
72
73
|
* @memberOf punycode.ucs2
|
|
73
74
|
* @name decode
|
|
74
|
-
* @param {String} string Unicode
|
|
75
|
+
* @param {String} string Unicode输入字符串(UCS-2)
|
|
75
76
|
* @returns {Array} 新的码点数组
|
|
76
77
|
*/
|
|
77
78
|
ucs2decode = string => {
|
|
@@ -79,7 +80,7 @@ const maxInt = 2147483647, // 即 0x7FFFFFFF 或 2^31-1
|
|
|
79
80
|
let counter = 0;
|
|
80
81
|
while (counter < length) {
|
|
81
82
|
const value = string.charCodeAt(counter++); // 获取当前字符的UTF-16代码单元
|
|
82
|
-
//
|
|
83
|
+
// 如果是高代理项(范围:0xD800-0xDBFF)并且不是最后一个字符
|
|
83
84
|
if (value >= 0xd800 && value <= 0xdbff && counter < length) {
|
|
84
85
|
const extra = string.charCodeAt(counter++); // 获取下一个字符
|
|
85
86
|
// 如果下一个字符是低代理项,则添加代理项对;否则,仅添加高代理项并回退计数器
|
|
@@ -92,53 +93,56 @@ const maxInt = 2147483647, // 即 0x7FFFFFFF 或 2^31-1
|
|
|
92
93
|
},
|
|
93
94
|
|
|
94
95
|
/**
|
|
95
|
-
*
|
|
96
|
+
* 基于数字码点数组创建字符串;
|
|
96
97
|
* @see `punycode.ucs2.decode`
|
|
97
98
|
* @memberOf punycode.ucs2
|
|
98
99
|
* @name encode
|
|
99
100
|
* @param {Array} codePoints 数字码点数组
|
|
100
|
-
* @returns {String} 新的Unicode
|
|
101
|
+
* @returns {String} 新的Unicode字符串(UCS-2)
|
|
101
102
|
*/
|
|
102
103
|
ucs2encode = codePoints => {
|
|
103
104
|
return String.fromCodePoint(...codePoints);
|
|
104
105
|
},
|
|
105
106
|
|
|
106
107
|
/**
|
|
107
|
-
*
|
|
108
|
+
* 将基本码点转换为数字/整数;
|
|
108
109
|
* @see `digitToBasic()`
|
|
109
110
|
* @private
|
|
110
111
|
* @param {Number} codePoint 基本数字码点值
|
|
111
|
-
* @returns {Number}
|
|
112
|
+
* @returns {Number} 基本码点的数值(用于表示整数),范围从`0`到`base - 1`,如果码点不表示值,则返回`base`
|
|
112
113
|
*/
|
|
113
114
|
basicToDigit = codePoint => {
|
|
114
115
|
|
|
115
|
-
|
|
116
|
-
if (codePoint >=
|
|
117
|
-
|
|
116
|
+
//如码点在`0x30`到`0x39`范围内(`0'-`9`),则返回其数值;
|
|
117
|
+
if (codePoint >= 0x30 && codePoint < 0x3a) return 26 + (codePoint - 0x30);
|
|
118
|
+
// 如码点在`0x41`到`0x5a`范围内(`A`-`Z`),则返回其数值;
|
|
119
|
+
if (codePoint >= 0x41 && codePoint < 0x5b) return codePoint - 0x41;
|
|
120
|
+
// 如码点在`0x61`到`0x7a`范围内(`a`-`z`),则返回其数值;
|
|
121
|
+
if (codePoint >= 0x61 && codePoint < 0x7b) return codePoint - 0x61;
|
|
118
122
|
return base; // 不在基本字符范围内
|
|
119
123
|
},
|
|
120
124
|
|
|
121
125
|
/**
|
|
122
|
-
*
|
|
126
|
+
* 将数字/整数转换为基本码点;
|
|
123
127
|
* @see `basicToDigit()`
|
|
124
128
|
* @private
|
|
125
129
|
* @param {Number} digit 基本码点的数值
|
|
126
|
-
* @returns {Number}
|
|
127
|
-
* 如果`flag
|
|
128
|
-
* 如果`flag`非零且`digit
|
|
130
|
+
* @returns {Number} 其值(用于表示整数)为`digit`的基本码点,需要在`0`到`base - 1`范围内;
|
|
131
|
+
* 如果`flag`非零,则使用大写形式;否则使用小写形式;
|
|
132
|
+
* 如果`flag`非零且`digit`没有大写形式,则行为未定义;
|
|
129
133
|
*/
|
|
130
134
|
digitToBasic = (digit, flag) => {
|
|
131
135
|
return digit + 22 + 75 * (digit < 26) - ((flag != 0) << 5);
|
|
132
136
|
},
|
|
133
137
|
|
|
134
138
|
/**
|
|
135
|
-
* 根据 RFC 3492 第 3.4
|
|
139
|
+
* 根据 RFC 3492 第 3.4 节的偏差适应函数;
|
|
136
140
|
* https://tools.ietf.org/html/rfc3492#section-3.4
|
|
137
141
|
* @private
|
|
138
142
|
*/
|
|
139
143
|
adapt = (delta, numPoints, firstTime) => {
|
|
140
144
|
let k = 0;
|
|
141
|
-
delta = firstTime ? floor(delta / damp) : delta >> 1; //
|
|
145
|
+
delta = firstTime ? floor(delta / damp) : delta >> 1; // 首次处理时除以阻尼因子,否则右移一位
|
|
142
146
|
delta += floor(delta / numPoints);
|
|
143
147
|
for (; /* 无初始化 */ delta > (baseMinusTMin * tMax) >> 1; k += base) delta = floor(delta / baseMinusTMin);
|
|
144
148
|
|
|
@@ -146,14 +150,14 @@ const maxInt = 2147483647, // 即 0x7FFFFFFF 或 2^31-1
|
|
|
146
150
|
},
|
|
147
151
|
|
|
148
152
|
/**
|
|
149
|
-
* 将仅包含ASCII符号的Punycode字符串转换为Unicode
|
|
153
|
+
* 将仅包含ASCII符号的Punycode字符串转换为Unicode符号字符串;
|
|
150
154
|
* @memberOf punycode
|
|
151
155
|
* @param {String} input 仅包含ASCII符号的Punycode字符串
|
|
152
156
|
* @returns {String} 生成的Unicode符号字符串
|
|
153
157
|
*/
|
|
154
158
|
decode = input => {
|
|
155
159
|
const output = [], inputLen = input.length;
|
|
156
|
-
// 当前码点值,初始码点,初始偏差,处理基本ASCII
|
|
160
|
+
// 当前码点值,初始码点,初始偏差,处理基本ASCII字符(分隔符前的部分)
|
|
157
161
|
let i = 0, n = initialN, bias = initialBias, basic = input.lastIndexOf(delimiter);
|
|
158
162
|
if (basic < 0) basic = 0;
|
|
159
163
|
|
|
@@ -190,24 +194,25 @@ const maxInt = 2147483647, // 即 0x7FFFFFFF 或 2^31-1
|
|
|
190
194
|
const out = output.length + 1;
|
|
191
195
|
bias = adapt(i - oldi, out, oldi === 0);
|
|
192
196
|
if (Math.floor(i / out) > maxInt - n) throw new Error(overflow); // 更新码点值并插入到输出
|
|
193
|
-
|
|
197
|
+
// 在指定位置插入解码后的字符,并移动到下一个插入位置
|
|
198
|
+
n += Math.floor(i / out), i %= out, output.splice(i, 0, n), i++;
|
|
194
199
|
}
|
|
195
200
|
|
|
196
201
|
return String.fromCodePoint(...output);
|
|
197
202
|
},
|
|
198
203
|
|
|
199
204
|
/**
|
|
200
|
-
* 将Unicode
|
|
205
|
+
* 将Unicode符号字符串(例如域名标签)转换为仅包含ASCII符号的Punycode字符串;
|
|
201
206
|
* @memberOf punycode
|
|
202
207
|
* @param {String} input Unicode符号字符串
|
|
203
208
|
* @returns {String} 生成的仅包含ASCII符号的Punycode字符串
|
|
204
209
|
*/
|
|
205
210
|
encode = input => {
|
|
206
|
-
input = ucs2decode(input); // 将UCS-2输入转换为Unicode
|
|
211
|
+
input = ucs2decode(input); // 将UCS-2输入转换为Unicode码点数组;
|
|
207
212
|
const output = [], inputLen = input.length; // 缓存长度
|
|
208
213
|
let n = initialN, delta = 0, bias = initialBias; // 初始化状态
|
|
209
214
|
|
|
210
|
-
//
|
|
215
|
+
// 处理基本码点;
|
|
211
216
|
for (const currentValue of input)
|
|
212
217
|
if (currentValue < 0x80) output.push(strFromCharCode(currentValue));
|
|
213
218
|
|
|
@@ -215,14 +220,14 @@ const maxInt = 2147483647, // 即 0x7FFFFFFF 或 2^31-1
|
|
|
215
220
|
let handledCPCount = basicLen; // `handledCPCount`是已处理的码点数量;`basicLength`是基本码点的数量;
|
|
216
221
|
if (basicLen) output.push(delimiter); // 用分隔符结束基本字符串,除非它是空的;
|
|
217
222
|
|
|
218
|
-
//
|
|
223
|
+
// 主编码循环:
|
|
219
224
|
while (handledCPCount < inputLen) {
|
|
220
|
-
// 所有小于 n
|
|
225
|
+
// 所有小于 n 的非基本码点都已处理;找到下一个更大的:
|
|
221
226
|
let m = maxInt;
|
|
222
227
|
for (const currentValue of input)
|
|
223
228
|
if (currentValue >= n && currentValue < m) m = currentValue;
|
|
224
229
|
|
|
225
|
-
// 增加 `delta`
|
|
230
|
+
// 增加 `delta` 足够多,以将解码器的 <n,i> 状态推进到 <m,0>,但要防止溢出;
|
|
226
231
|
const handledCPCountPlusOne = handledCPCount + 1;
|
|
227
232
|
if (m - n > floor((maxInt - delta) / handledCPCountPlusOne)) error('overflow');
|
|
228
233
|
|
|
@@ -230,14 +235,15 @@ const maxInt = 2147483647, // 即 0x7FFFFFFF 或 2^31-1
|
|
|
230
235
|
for (const currentValue of input) {
|
|
231
236
|
if (currentValue < n && ++delta > maxInt) error('overflow');
|
|
232
237
|
if (currentValue === n) {
|
|
233
|
-
// 将 delta
|
|
238
|
+
// 将 delta 表示为广义可变长度整数;
|
|
234
239
|
let q = delta;
|
|
235
240
|
for (let k = base /* 无条件 */; ; k += base) {
|
|
236
241
|
const t = k <= bias ? tMin : k >= bias + tMax ? tMax : k - bias;
|
|
237
242
|
if (q < t) break;
|
|
238
243
|
|
|
239
244
|
const qMinusT = q - t, baseMinusT = base - t;
|
|
240
|
-
output.push(strFromCharCode(digitToBasic(t + (qMinusT % baseMinusT), 0))),
|
|
245
|
+
output.push(strFromCharCode(digitToBasic(t + (qMinusT % baseMinusT), 0))),
|
|
246
|
+
q = floor(qMinusT / baseMinusT);
|
|
241
247
|
}
|
|
242
248
|
|
|
243
249
|
output.push(strFromCharCode(digitToBasic(q, 0)));
|
|
@@ -250,8 +256,8 @@ const maxInt = 2147483647, // 即 0x7FFFFFFF 或 2^31-1
|
|
|
250
256
|
},
|
|
251
257
|
|
|
252
258
|
/**
|
|
253
|
-
* 将表示域名或电子邮件地址的Punycode字符串转换为Unicode
|
|
254
|
-
* 只有输入的Punycode
|
|
259
|
+
* 将表示域名或电子邮件地址的Punycode字符串转换为Unicode;
|
|
260
|
+
* 只有输入的Punycode部分会被转换,即如果你在已经转换为Unicode的字符串上调用它也没有关系;
|
|
255
261
|
* @memberOf punycode
|
|
256
262
|
* @param {String} input 要转换为Unicode的Punycode编码的域名或电子邮件地址
|
|
257
263
|
* @returns {String} 给定Punycode字符串的Unicode表示
|
package/lib/qp.js
CHANGED
|
@@ -10,7 +10,7 @@ import { regexs, resetRegex } from './regexs.js';
|
|
|
10
10
|
*/
|
|
11
11
|
const qpWrap = (str = '', lineLength = 76) => {
|
|
12
12
|
str = str.toString();
|
|
13
|
-
if (str.length <= lineLength) return str; //
|
|
13
|
+
if (str.length <= lineLength) return str; // 如果字符串长度不超过行限制,直接返回
|
|
14
14
|
let pos = 0, match, code, line, result = '';
|
|
15
15
|
const len = str.length, lineMargin = Math.floor(lineLength / 3),
|
|
16
16
|
// 将行添加到结果中并更新位置
|
|
@@ -25,7 +25,7 @@ const qpWrap = (str = '', lineLength = 76) => {
|
|
|
25
25
|
continue;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
//
|
|
28
|
+
// 如果以换行符结尾,保持不变
|
|
29
29
|
if (line.endsWith('\n')) {
|
|
30
30
|
addLineAndUpdatePos(line);
|
|
31
31
|
continue;
|
|
@@ -34,15 +34,18 @@ const qpWrap = (str = '', lineLength = 76) => {
|
|
|
34
34
|
line = line.slice(0, line.length - (match[0].length - 1)), addLineAndUpdatePos(line); // 截断到最近的换行符
|
|
35
35
|
continue;
|
|
36
36
|
}
|
|
37
|
-
else if (line.length > lineLength - lineMargin &&
|
|
37
|
+
else if (line.length > lineLength - lineMargin &&
|
|
38
|
+
(match = line.slice(-lineMargin).match(regexs.TRAILING_SPACE_OR_PUNCTUATION)))
|
|
38
39
|
line = line.slice(0, line.length - (match[0].length - 1)); // 截断到最近的空格
|
|
39
40
|
else if (line.match(regexs.INCOMPLETE_ENCODING)) {
|
|
40
41
|
// 将不完整的编码序列推到下一行
|
|
41
|
-
if ((match = line.match(regexs.INCOMPLETE_ENCODING_SHORT)))
|
|
42
|
+
if ((match = line.match(regexs.INCOMPLETE_ENCODING_SHORT)))
|
|
43
|
+
line = line.slice(0, line.length - match[0].length);
|
|
42
44
|
|
|
43
45
|
const rCE = resetRegex(regexs.COMPLETE_ENCODING);
|
|
44
46
|
// 确保UTF-8序列不被分割
|
|
45
|
-
while (line.length > 3 && line.length < len - pos &&
|
|
47
|
+
while (line.length > 3 && line.length < len - pos &&
|
|
48
|
+
!line.match(regexs.UTF8_ENCODING) && (match = line.match(rCE))) {
|
|
46
49
|
code = parseInt(match[0].slice(1, 3), 16);
|
|
47
50
|
if (code < 128) break;
|
|
48
51
|
|
|
@@ -68,17 +71,18 @@ const qpWrap = (str = '', lineLength = 76) => {
|
|
|
68
71
|
},
|
|
69
72
|
|
|
70
73
|
/**
|
|
71
|
-
*
|
|
74
|
+
* 辅助函数:检查数字是否在指定范围内
|
|
72
75
|
*
|
|
73
76
|
* @param {Number} nr 要检查的数字
|
|
74
77
|
* @param {Array} ranges 允许的值范围数组
|
|
75
|
-
* @returns {Boolean} 如果在允许范围内返回true
|
|
78
|
+
* @returns {Boolean} 如果在允许范围内返回true,否则返回false
|
|
76
79
|
*/
|
|
77
80
|
checkRanges = (nr, ranges) => {
|
|
78
81
|
for (let i = ranges.length - 1; i >= 0; i--) {
|
|
79
|
-
if (!ranges[i].length) continue; //
|
|
80
|
-
if (ranges[i].length === 1 && nr === ranges[i][0]) return true; //
|
|
81
|
-
|
|
82
|
+
if (!ranges[i].length) continue; // 如果范围为空,则跳过
|
|
83
|
+
if (ranges[i].length === 1 && nr === ranges[i][0]) return true; // 如果范围只有一个值,则直接比较
|
|
84
|
+
// 如果范围有两个值,则比较是否在范围内
|
|
85
|
+
if (ranges[i].length === 2 && nr >= ranges[i][0] && nr <= ranges[i][1]) return true;
|
|
82
86
|
}
|
|
83
87
|
return false;
|
|
84
88
|
};
|
|
@@ -90,7 +94,7 @@ const qpWrap = (str = '', lineLength = 76) => {
|
|
|
90
94
|
* @returns {String} Quoted-Printable编码的字符串
|
|
91
95
|
*/
|
|
92
96
|
const qpEncode = buffer => {
|
|
93
|
-
if (typeof buffer === 'string') buffer = Buffer.from(buffer, 'utf-8'); //
|
|
97
|
+
if (typeof buffer === 'string') buffer = Buffer.from(buffer, 'utf-8'); // 如果输入是字符串,先转换为Buffer
|
|
94
98
|
|
|
95
99
|
// 不需要编码的可用字符范围
|
|
96
100
|
const ranges = [
|
|
@@ -105,13 +109,13 @@ const qpEncode = buffer => {
|
|
|
105
109
|
for (let i = 0, len = buffer.length; i < len; i++) {
|
|
106
110
|
const ord = buffer[i], nextByte = buffer[i + 1];
|
|
107
111
|
|
|
108
|
-
//
|
|
112
|
+
// 如果字符在允许范围内,并且不是行尾的空格或制表符,则直接使用原字符
|
|
109
113
|
if (checkRanges(ord, ranges) && !((ord === 0x20 || ord === 0x09) && (i === len - 1 || nextByte === 0x0a
|
|
110
114
|
|| nextByte === 0x0d))) {
|
|
111
115
|
result += String.fromCharCode(ord);
|
|
112
116
|
continue;
|
|
113
117
|
}
|
|
114
|
-
result += `=${(ord < 0x10 ? '0' : '')}${ord.toString(16).toUpperCase()}`; //
|
|
118
|
+
result += `=${(ord < 0x10 ? '0' : '')}${ord.toString(16).toUpperCase()}`; // 否则进行编码:=后跟两位十六进制数
|
|
115
119
|
}
|
|
116
120
|
|
|
117
121
|
return result;
|
|
@@ -154,13 +158,13 @@ class QpEncoder extends Transform {
|
|
|
154
158
|
|
|
155
159
|
if (qp) this.outputBytes += qp.length, this.push(qp);
|
|
156
160
|
}
|
|
157
|
-
//
|
|
161
|
+
// 否则没有启用行长度限制,直接编码并输出
|
|
158
162
|
else qp = qpEncode(chunk), this.outputBytes += qp.length, this.push(qp, 'ascii');
|
|
159
163
|
done();
|
|
160
164
|
}
|
|
161
165
|
|
|
162
166
|
/**
|
|
163
|
-
*
|
|
167
|
+
* 刷新流,输出剩余数据
|
|
164
168
|
*/
|
|
165
169
|
_flush(done) {
|
|
166
170
|
if (this._curLine) this.outputBytes += this._curLine.length, this.push(this._curLine, 'ascii');
|
package/lib/regexs.js
CHANGED
|
@@ -6,28 +6,28 @@ const regexs = {
|
|
|
6
6
|
LINE_SEPARATOR: /\r?\n/, // 按行分割丢弃换行符
|
|
7
7
|
SPLIT_KEEP_NEWLINE: /(\r?\n)/, // 按行分割并保留行结束符
|
|
8
8
|
LINE_BREAK: /^[^\n\r]*(\r?\n|\r)/, // 匹配行结束符
|
|
9
|
-
INVALID_ADDRESS_CHARS: /[\r\n<>]/, //
|
|
9
|
+
INVALID_ADDRESS_CHARS: /[\r\n<>]/, // 匹配地址中的非法字符(回车、换行、<>)
|
|
10
10
|
WHITESPACE: /\s+/, // 匹配空白字符
|
|
11
|
-
FOLDED_HEADER_LINE: /^\s/, //
|
|
11
|
+
FOLDED_HEADER_LINE: /^\s/, // 匹配折叠的头信息行(以空白字符开头)
|
|
12
12
|
RESPONSE_CODE: /^(\d+)(?:\s(\d+\.\d+\.\d+))?\s/, // 匹配SMTP响应代码和可选状态码
|
|
13
13
|
NEXT_WORD: /^[^\s]+(\s*)/, // 匹配下一个单词及其后的空白
|
|
14
|
-
ADDRESS_CLEAN: /^[^<]*<\s*/, //
|
|
15
|
-
EMAIL_LOOSE: /\s*\b[^@\s]+@[^\s]+\b\s*/, //
|
|
14
|
+
ADDRESS_CLEAN: /^[^<]*<\s*/, // 地址令牌清理:清理地址令牌中的多余字符
|
|
15
|
+
EMAIL_LOOSE: /\s*\b[^@\s]+@[^\s]+\b\s*/, // 邮箱地址宽松匹配:匹配文本中可能包含特殊字符的邮箱地址
|
|
16
16
|
UNSAFE_PARAM_VALUE: /[\s'"\\;:/=(),<>@[\]?]|^-/, // 匹配参数值中的不安全字符或以连字符开头
|
|
17
17
|
|
|
18
|
-
TRAILING_WSP: /[ \t]+$/, //
|
|
18
|
+
TRAILING_WSP: /[ \t]+$/, // 匹配行尾空白字符(WSP)
|
|
19
19
|
OPTIONAL_CRLF: /\r?\n$/, // 匹配可选的CRLF行结束符
|
|
20
20
|
TRAILING_NEWLINE: /\n.*?$/, // 匹配行尾的换行符及其后的内容
|
|
21
|
-
LAST_LINE: /(^|\n)([^\n]*)$/, //
|
|
21
|
+
LAST_LINE: /(^|\n)([^\n]*)$/, // 匹配最后一行(用于换行处理)
|
|
22
22
|
EMAIL_CHECK: /^[^@]+@[^@]+$/, // 检查电子邮件格式
|
|
23
23
|
TRAILING_SPACES: /(\s+)[^\s]*$/, // 匹配行尾的空白字符及其后的非空白字符
|
|
24
|
-
EMAIL_EXACT: /^[^@\s]+@[^@\s]+$/, //
|
|
24
|
+
EMAIL_EXACT: /^[^@\s]+@[^@\s]+$/, // 邮箱地址精确匹配:匹配简单的邮箱格式(名称@域名)
|
|
25
25
|
PRINTABLE_ASCII: /^[\x20-\x7e]*$/, // 匹配可打印ASCII字符
|
|
26
26
|
CRAM_MD5_CHALLENGE: /^334\s+(.+)$/, // 匹配CRAM-MD5挑战字符串
|
|
27
27
|
TRAILING_LINE_BREAK: /(?:\r\n|\r|\n)$/, // 检测字符串是否以行结束符结尾
|
|
28
|
-
RFC2231_KEY: /(\*(\d+)|\*(\d+)\*|\*)$/, // 匹配RFC2231
|
|
28
|
+
RFC2231_KEY: /(\*(\d+)|\*(\d+)\*|\*)$/, // 匹配RFC2231格式的参数键名(带星号和编号)
|
|
29
29
|
RALPHANUMERIC_UNDERSCORE_SPACE: /^[\w ]*$/, // 匹配仅包含字母数字、下划线和空格的字符串
|
|
30
|
-
CHARSET_LANGUAGE_VALUE: /^([^']*)'[^']*'(.*)$/, //
|
|
30
|
+
CHARSET_LANGUAGE_VALUE: /^([^']*)'[^']*'(.*)$/, // 匹配字符集、语言和值的格式(RFC2231)
|
|
31
31
|
IPV4_ADDRESS: /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/, // 匹配IPv4地址格式
|
|
32
32
|
UNSAFE_HEADER_PARAM: /[\s"\\;:/=(),<>@[\]?]|^[-']|'$/, // 匹配头参数值中的不安全字符
|
|
33
33
|
TRAILING_SPACE_OR_PUNCTUATION: /[ \t.,!?][^ \t.,!?]*$/, // 匹配空格、制表符或标点符号以及其后的非空格、非标点符号字符
|
|
@@ -38,32 +38,32 @@ const regexs = {
|
|
|
38
38
|
TEXT_TYPE: /^text\//i, // 匹配所有text/*内容类型
|
|
39
39
|
IMAGE_TYPE: /^image\//i, // 检测图像内容类型
|
|
40
40
|
HTTP_URL: /^https?:\/\//i, // 检测HTTP/HTTPS协议
|
|
41
|
-
ERROR_SUFFIX: /Error\]$/i, // 匹配以"Error]"
|
|
41
|
+
ERROR_SUFFIX: /Error\]$/i, // 匹配以"Error]"结尾的字符串(不区分大小写)
|
|
42
42
|
MULTIPART: /^multipart\//i, // 匹配multipart/*内容类型
|
|
43
43
|
HTML_TYPE: /^text\/html\b/i, // 检测HTML内容类型
|
|
44
44
|
MESSAGE_TYPE: /^message\//i, // 检测消息内容类型
|
|
45
|
-
ENCODED_SEQUENCE: /^[=]([0-9A-F]{2})/i, //
|
|
45
|
+
ENCODED_SEQUENCE: /^[=]([0-9A-F]{2})/i, // 匹配编码序列(等号后跟2个十六进制数字)
|
|
46
46
|
CONTENT_FEATURES: /^Content-Features$/i, // 匹配Content-Features头部
|
|
47
|
-
UTF8_ENCODING: /^(?:=[\da-f]{2}){1,4}$/i, // 匹配UTF-8
|
|
48
|
-
INCOMPLETE_ENCODING: /[=][\da-f]{0,2}$/i, //
|
|
47
|
+
UTF8_ENCODING: /^(?:=[\da-f]{2}){1,4}$/i, // 匹配UTF-8编码序列(1-4个连续的编码字节)
|
|
48
|
+
INCOMPLETE_ENCODING: /[=][\da-f]{0,2}$/i, // 匹配不完整的编码序列(等号后跟0-2个十六进制数字)
|
|
49
49
|
INCOMPLETE_ENCODING_END: /[=][0-9A-F]?$/i, // 匹配字符串末尾的不完整编码序列
|
|
50
|
-
TRANSPORTER_PROTOCOL: /^(smtps?|direct):/i, //
|
|
51
|
-
INCOMPLETE_ENCODING_SHORT: /[=][\da-f]{0,1}$/i, //
|
|
50
|
+
TRANSPORTER_PROTOCOL: /^(smtps?|direct):/i, // 匹配传输器协议(smtp://, smtps://, direct://)
|
|
51
|
+
INCOMPLETE_ENCODING_SHORT: /[=][\da-f]{0,1}$/i, // 匹配更短的不完整编码序列(等号后跟0-1个十六进制数字)
|
|
52
52
|
MULTIPART_OR_MESSAGE: /^(multipart|message)\//i, // 匹配multipart/*或message/*内容类型
|
|
53
53
|
HTTP_RESPONSE_STATUS_LINE: /^HTTP\/\d+\.\d+ (\d+)/i, // 匹配 HTTP 响应状态行
|
|
54
54
|
AUTH_MECHANISM: /[ -]AUTH(?:(\s+|=)[^\n]*\s+|\s+|=)/i, // 匹配AUTH机制
|
|
55
55
|
IPV6_ADDRESS: /^(?:[A-F0-9]{1,4}:){7}[A-F0-9]{1,4}$/i, // 匹配IPv6地址格式
|
|
56
56
|
|
|
57
|
-
NON_ASCII: /\P{ASCII}/u, // 匹配非ASCII
|
|
58
|
-
NON_BASIC_ASCII: /[^\x00-\x7F]/, // 检测非基本ASCII
|
|
57
|
+
NON_ASCII: /\P{ASCII}/u, // 匹配非ASCII字符(Unicode字符)
|
|
58
|
+
NON_BASIC_ASCII: /[^\x00-\x7F]/, // 检测非基本ASCII字符(编码>127)
|
|
59
59
|
PROTOCOL_CHECK: /^(https?|mailto|ftp):/, // 检查URL协议
|
|
60
60
|
LIST_ID_PROTOCOL_REMOVE: /^<[^:]+\/{,2}/, // 移除List-ID中的协议部分
|
|
61
|
-
UTF16_HIGH_SURROGATE: /[\uD800-\uDBFF]/, // 匹配UTF-16
|
|
61
|
+
UTF16_HIGH_SURROGATE: /[\uD800-\uDBFF]/, // 匹配UTF-16高代理项(代理对的第一部分)
|
|
62
62
|
EMOJI_OR_SURROGATE: /[\ud83c\ud83d\ud83e]/, // 匹配emoji或UTF-16代理对起始字符
|
|
63
|
-
PLAIN_TEXT: /[^\x09\x0a\x0d\x20-\x7e]/, // 匹配非可打印7位ASCII
|
|
64
|
-
PARAM_PLAIN_TEXT: /[^\x09\x0a\x0d\x20-\x7e]|"/, //
|
|
63
|
+
PLAIN_TEXT: /[^\x09\x0a\x0d\x20-\x7e]/, // 匹配非可打印7位ASCII字符(允许\t\n\r和\x20-\x7e)
|
|
64
|
+
PARAM_PLAIN_TEXT: /[^\x09\x0a\x0d\x20-\x7e]|"/, // 匹配非可打印字符或双引号(参数校验用)
|
|
65
65
|
FIRST_NON_ASCII_WORD: /(?:^|\s)([^\s]*[^\x00-\x7F])/, // 匹配第一个包含非ASCII字符的单词
|
|
66
|
-
NEED_ENCODING: /[\x00-\x20\x2B\x3D\x7F]|[^\x21-\x7E]/, // 不可打印ASCII
|
|
66
|
+
NEED_ENCODING: /[\x00-\x20\x2B\x3D\x7F]|[^\x21-\x7E]/, // 不可打印ASCII字符(包括非ASCII字符)
|
|
67
67
|
|
|
68
68
|
DSN: /[ -]DSN\b/im, // 匹配DSN扩展支持
|
|
69
69
|
STARTTLS: /[ -]STARTTLS\b/im, // 匹配STARTTLS扩展支持
|
|
@@ -84,28 +84,28 @@ const regexs = {
|
|
|
84
84
|
NON_DIGIT: /\D/g, // 匹配非数字字符
|
|
85
85
|
CID_CLEAN: /[<>]/g, // 清理Content-ID中的尖括号
|
|
86
86
|
BLANK_REGEX: /\s/g, // 匹配所有空白字符
|
|
87
|
-
URL_SAFE_BASE64: /\W/g, //
|
|
87
|
+
URL_SAFE_BASE64: /\W/g, // 匹配非单词字符(非字母、数字、下划线)
|
|
88
88
|
MULTIPLE_WSP: /[ \t]+/g, // 匹配行内多个空白字符
|
|
89
|
-
LINEBREAKS: /\r?\n|\r/g, //
|
|
90
|
-
QUOTED_PAIR: /([\\"])/g, //
|
|
89
|
+
LINEBREAKS: /\r?\n|\r/g, // 匹配各种换行符(CRLF、LF、CR)
|
|
90
|
+
QUOTED_PAIR: /([\\"])/g, // 匹配需要转义的字符(反斜杠和引号)
|
|
91
91
|
HYPHEN_REPLACE: /-(\w)/g, // 连字符替换为驼峰命名
|
|
92
92
|
ANGLE_BRACKET: /<[^>]*>/g, // 匹配尖括号内的内容
|
|
93
93
|
GLOBAL_WHITESPACE: /\s+/g, // 匹配多个连续空白字符为单个空格
|
|
94
94
|
ENCODING_FORMAT: /[-_\s]/g, // 格式化编码字符串
|
|
95
|
-
CLEAN_MESSAGE_ID: /[<>\s]/g, // 清理message-id
|
|
95
|
+
CLEAN_MESSAGE_ID: /[<>\s]/g, // 清理message-id头,移除尖括号和空白字符
|
|
96
96
|
GLOBAL_CRLF_OR_LF: /\r?\n/g, // 匹配CRLF或LF换行符
|
|
97
97
|
URL_CLEAN: /[\s<]+|[\s>]+/g, // 清理URL中的空白和尖括号
|
|
98
|
-
RFC2231_SPECIAL_CHARS: /[=?_\s]/g, // RFC2231
|
|
99
|
-
FORBIDDEN_CHARS: /[\x00-\x1F<>]+/g, //
|
|
98
|
+
RFC2231_SPECIAL_CHARS: /[=?_\s]/g, // RFC2231编码中需要转义的特殊字符:等号、问号、下划线和空白字符
|
|
99
|
+
FORBIDDEN_CHARS: /[\x00-\x1F<>]+/g, // 匹配控制字符和尖括号(不允许的字符)
|
|
100
100
|
SEPARATORS: /[\x2E\u3002\uFF0E\uFF61]/g, // RFC 3490 分隔符
|
|
101
101
|
NON_ALPHANUMERIC_DOT_HYPHEN: /[^a-zA-Z0-9.-]/g, // 匹配非字母数字、点号和连字符的字符
|
|
102
102
|
SIGNATURE_FOLDING: /(^.{73}|.{75}(?!\r?\n|\r))/g, // 签名折行处理正则
|
|
103
|
-
NON_LATIN: /[\x00-\x08\x0B\x0C\x0E-\x1F\P{ASCII}]/gu, //
|
|
103
|
+
NON_LATIN: /[\x00-\x08\x0B\x0C\x0E-\x1F\P{ASCII}]/gu, // 匹配非拉丁字符(控制字符和Unicode)
|
|
104
104
|
NON_SAFE_URI: /[^\x00-\x1F *'()<>@,;:\\"[\]?=\u007F-\uFFFF]+/g, // 匹配URI中的不安全字符范围
|
|
105
105
|
NON_SAFE_URI_CHAR: /[\x00-\x1F *'()<>@,;:\\"[\]?=\u007F-\uFFFF]/g, // 匹配单个URI不安全字符
|
|
106
106
|
LATIN: /[a-z]/gi, // 匹配拉丁字母
|
|
107
|
-
NON_SAFE_CHAR: /[^a-z0-9!*+\-/=]/gi, //
|
|
108
|
-
COMPLETE_ENCODING: /[=][\da-f]{2}$/gi, //
|
|
107
|
+
NON_SAFE_CHAR: /[^a-z0-9!*+\-/=]/gi, // 匹配非安全字符(用于QP编码)
|
|
108
|
+
COMPLETE_ENCODING: /[=][\da-f]{2}$/gi, // 匹配完整的编码序列(等号后跟2个十六进制数字)
|
|
109
109
|
DATA_URL_IMAGE: /(]{0,1024} src\s{0,20}=[\s"']{0,20})(data:([^;]+);[^"'>\s]+)/gi, // 匹配HTML中的data URL图片
|
|
110
110
|
NORMALIZE_HEADER_KEY: /^X-SMTPAPI$|^(MIME|DKIM|ARC|BIMI)\b|^[a-z]|-(SPF|FBL|ID|MD5)$|-[a-z]/gi, // 头部键规范化规则
|
|
111
111
|
};
|
|
@@ -4,7 +4,7 @@ import { PK, getLogger, prepareMessageForSending, cleanup } from './shared.js';
|
|
|
4
4
|
/**
|
|
5
5
|
* 生成Sendmail传输对象
|
|
6
6
|
*
|
|
7
|
-
*
|
|
7
|
+
* 可用的选项如下:
|
|
8
8
|
* **path** sendmail二进制文件的可选路径
|
|
9
9
|
* **newline** 换行符类型,可以是'windows'或'unix'
|
|
10
10
|
* **args** sendmail二进制文件的参数数组
|
|
@@ -24,8 +24,8 @@ class SendmailTransport {
|
|
|
24
24
|
|
|
25
25
|
// 根据选项配置参数
|
|
26
26
|
if (options) {
|
|
27
|
-
if (typeof options === 'string') this.path = options; // 如果options
|
|
28
|
-
// 否则,如果options
|
|
27
|
+
if (typeof options === 'string') this.path = options; // 如果options是字符串,直接作为sendmail路径
|
|
28
|
+
// 否则,如果options是对象,检查path和args属性
|
|
29
29
|
else if (typeof options === 'object') {
|
|
30
30
|
const { path, args, newline = '' } = options;
|
|
31
31
|
if (path) this.path = path;
|
package/lib/ses-transport.js
CHANGED
|
@@ -23,7 +23,7 @@ class SesTransport extends EventEmitter {
|
|
|
23
23
|
*/
|
|
24
24
|
getRegion(cb) {
|
|
25
25
|
const regionFunc = this.ses.sesClient.config?.region;
|
|
26
|
-
//
|
|
26
|
+
// 如果是函数,则调用该函数并返回结果,否则返回结果
|
|
27
27
|
return typeof regionFunc === 'function' ? regionFunc().then(r => cb(null, r)).catch(cb) : cb(null, false);
|
|
28
28
|
}
|
|
29
29
|
|
|
@@ -50,14 +50,16 @@ class SesTransport extends EventEmitter {
|
|
|
50
50
|
*/
|
|
51
51
|
getRawMessage = next => {
|
|
52
52
|
// 在 DKIM 签名中不使用 Message-ID 和 Date; 将流数据读取到Buffer中
|
|
53
|
-
|
|
54
|
-
|
|
53
|
+
// 创建消息流
|
|
54
|
+
const sF = _dkim.skipFields, dMsgId = 'date:message-id', stream = readStream.pipe(new LeWindows());
|
|
55
|
+
_dkim.skipFields = (sF && typeof sF === 'string') ? `${sF}:${dMsgId}` : dMsgId,
|
|
56
|
+
resolveStream(stream, next);
|
|
55
57
|
};
|
|
56
58
|
|
|
57
59
|
// 异步执行发送过程
|
|
58
60
|
setImmediate(() =>
|
|
59
61
|
getRawMessage((err, raw) => {
|
|
60
|
-
if (err) return handleError(err, '为 %s
|
|
62
|
+
if (err) return handleError(err, '为 %s 创建消息失败;%s');
|
|
61
63
|
|
|
62
64
|
// 构建 SES 消息对象(添加自定义参数: ses)
|
|
63
65
|
const sesMessage = {
|
|
@@ -86,19 +88,21 @@ class SesTransport extends EventEmitter {
|
|
|
86
88
|
* 验证 SES 配置
|
|
87
89
|
*
|
|
88
90
|
* @param {Function} callback 回调函数
|
|
89
|
-
* @return {Promise}
|
|
91
|
+
* @return {Promise} 如果没有提供回调函数,则返回 Promise
|
|
90
92
|
*/
|
|
91
93
|
verify(callback) {
|
|
92
94
|
const promise = !callback ? new Promise((resolve, reject) => callback = callbackPromise(resolve, reject)) : null,
|
|
93
|
-
//
|
|
95
|
+
// 忽略某些特定错误,仍然认为验证成功
|
|
94
96
|
cb = err => callback(
|
|
95
|
-
err && !['InvalidParameterValue', 'MessageRejected'].includes(err?.code || err?.Code || err?.name)
|
|
97
|
+
err && !['InvalidParameterValue', 'MessageRejected'].includes(err?.code || err?.Code || err?.name)
|
|
98
|
+
? err : null, true
|
|
96
99
|
),
|
|
97
100
|
|
|
98
101
|
iEmail = 'invalid@invalid',
|
|
99
102
|
// 创建测试用的无效消息
|
|
100
103
|
sesMessage = {
|
|
101
|
-
Content: { Raw: { Data: Buffer.from(
|
|
104
|
+
Content: { Raw: { Data: Buffer.from(
|
|
105
|
+
`From: <${iEmail}>\r\nTo: <${iEmail}>\r\nSubject: Invalid\r\n\r\nInvalid`) } },
|
|
102
106
|
FromEmailAddress: iEmail, Destination: { ToAddresses: [iEmail] }
|
|
103
107
|
};
|
|
104
108
|
|