@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.
- package/CHANGELOG.md +4 -0
- package/LICENSE +15 -0
- package/Mail.js +47 -0
- package/README.md +475 -0
- package/copy-files.js +36 -0
- package/index.d.ts +816 -0
- package/index.js +109 -0
- package/lib/addressparser.js +230 -0
- package/lib/base64.js +103 -0
- package/lib/dkim/index.js +211 -0
- package/lib/dkim/message-parser.js +114 -0
- package/lib/dkim/relaxed-body.js +133 -0
- package/lib/dkim/sign.js +121 -0
- package/lib/fetch/cookies.js +224 -0
- package/lib/fetch/index.js +233 -0
- package/lib/http-proxy-client.js +98 -0
- package/lib/json-transport.js +38 -0
- package/lib/mail-composer.js +316 -0
- package/lib/mailer/index.js +301 -0
- package/lib/mailer/mail-message.js +226 -0
- package/lib/mime-funcs/index.js +493 -0
- package/lib/mime-funcs/mime-types.js +2061 -0
- package/lib/mime-node/index.js +811 -0
- package/lib/mime-node/last-newline.js +44 -0
- package/lib/mime-node/le-unix.js +37 -0
- package/lib/mime-node/le-windows.js +46 -0
- package/lib/punycode.js +283 -0
- package/lib/qp.js +171 -0
- package/lib/regexs.js +124 -0
- package/lib/sendmail-transport.js +91 -0
- package/lib/ses-transport.js +115 -0
- package/lib/shared.js +725 -0
- package/lib/smtp-connection/data-stream.js +68 -0
- package/lib/smtp-connection/index.js +1062 -0
- package/lib/smtp-pool/index.js +294 -0
- package/lib/smtp-pool/pool-resource.js +93 -0
- package/lib/smtp-transport.js +81 -0
- package/lib/stream-transport.js +59 -0
- package/lib/well-known/index.js +54 -0
- package/lib/well-known/services.json +802 -0
- package/lib/xoauth2.js +249 -0
- package/package.json +78 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Transform } from 'stream';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 一个转换流,用于确保数据以正确的换行符结束;
|
|
5
|
+
* 继承自 stream.Transform
|
|
6
|
+
* >查看定义:@see {@link LastNewline}
|
|
7
|
+
*/
|
|
8
|
+
class LastNewline extends Transform {
|
|
9
|
+
constructor() {
|
|
10
|
+
super();
|
|
11
|
+
this.lastByte = false; // 保存最后一个字节用于判断换行状态
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 转换数据处理方法
|
|
16
|
+
* @param {Buffer} chunk - 输入的数据块
|
|
17
|
+
* @param {string} encoding - 编码方式
|
|
18
|
+
* @param {function} done - 完成回调
|
|
19
|
+
*/
|
|
20
|
+
_transform(chunk, encoding, done) {
|
|
21
|
+
if (typeof chunk === 'string') chunk = Buffer.from(chunk, encoding); // 如果是字符串,转换为Buffer
|
|
22
|
+
if (chunk.length) this.lastByte = chunk.at(-1); // 如果数据块不为空,记录最后一个字节
|
|
23
|
+
this.push(chunk), done(); // 将数据块推送到下一个流;完成
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 流结束时的处理逻辑
|
|
28
|
+
* @param {function} done - 完成回调
|
|
29
|
+
*/
|
|
30
|
+
_flush(done) {
|
|
31
|
+
if (this.lastByte === 0x0a) return done(); // 如果最后一个字节是换行符,直接结束
|
|
32
|
+
|
|
33
|
+
// 如果最后一个字节是回车符,添加换行符
|
|
34
|
+
if (this.lastByte === 0x0d) {
|
|
35
|
+
this.push(Buffer.from('\n'));
|
|
36
|
+
return done();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
this.push(Buffer.from('\r\n')); // 其他情况添加Windows风格的换行符\r\n
|
|
40
|
+
return done();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export { LastNewline }
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Transform } from 'stream';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 确保仅使用 `<LF>` 作为换行符(Windows风格处理);
|
|
5
|
+
* >查看定义:@see {@link LeUnix}
|
|
6
|
+
* @param {Object} options 流选项
|
|
7
|
+
*/
|
|
8
|
+
class LeUnix extends Transform {
|
|
9
|
+
constructor(options = {}) {
|
|
10
|
+
super(options);
|
|
11
|
+
this.options = options;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 转换函数 - 处理数据块中的换行符
|
|
16
|
+
* @param {Buffer} chunk 数据块
|
|
17
|
+
* @param {string} encoding 编码格式
|
|
18
|
+
* @param {Function} done 完成回调
|
|
19
|
+
*/
|
|
20
|
+
_transform(chunk, encoding, done) {
|
|
21
|
+
if (typeof chunk === 'string') chunk = Buffer.from(chunk, encoding); // 如果chunk是字符串,根据encoding转换为Buffer
|
|
22
|
+
let buf, lastPos = 0;
|
|
23
|
+
|
|
24
|
+
// 遍历数据块中的每个字节
|
|
25
|
+
for (let i = 0; i < chunk.length; i++) {
|
|
26
|
+
// 如果,遇到回车符, 则提取从上次位置到当前回车符之前的数据(跳过回车符,推送处理后的数据)
|
|
27
|
+
if (chunk[i] === 0x0d) buf = chunk.subarray(lastPos, i), lastPos = i + 1, this.push(buf);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// 如果剩余数据为真,则推送最后一个回车符之后的数据
|
|
31
|
+
if (lastPos && lastPos < chunk.length) buf = chunk.subarray(lastPos), this.push(buf);
|
|
32
|
+
else if (!lastPos) this.push(chunk); // 否则, 如果没有找到回车符,直接推送整个数据块
|
|
33
|
+
done();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export { LeUnix };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Transform } from 'stream';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 确保只使用 `<CR><LF>` 序列作为换行符(Windows风格处理)
|
|
5
|
+
* >查看定义:@see {@link LeWindows}
|
|
6
|
+
* @param {Object} options 流选项
|
|
7
|
+
*/
|
|
8
|
+
class LeWindows extends Transform {
|
|
9
|
+
constructor(options = {}) {
|
|
10
|
+
super(options);
|
|
11
|
+
this.options = options, this.lastByte = false; // 记录上一个字节,用于跨 chunk 的换行符检测
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 转换函数:确保所有换行符都是 \r\n 格式
|
|
16
|
+
*/
|
|
17
|
+
_transform(chunk, encoding, done) {
|
|
18
|
+
if (chunk.length === 0) return done();
|
|
19
|
+
if (typeof chunk === 'string') chunk = Buffer.from(chunk, encoding); // 如果chunk是字符串,根据encoding转换为Buffer
|
|
20
|
+
|
|
21
|
+
// 快速路径:无换行符直接推送
|
|
22
|
+
if (!chunk.includes(0x0a)) {
|
|
23
|
+
this.push(chunk), this.lastByte = chunk.at(-1);
|
|
24
|
+
return done();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const crlf = Buffer.from('\r\n'), output = [];
|
|
28
|
+
let start = 0;
|
|
29
|
+
|
|
30
|
+
for (let i = 0; i < chunk.length; i++) {
|
|
31
|
+
if (chunk[i] !== 0x0a) continue; // 跳过非换行符
|
|
32
|
+
|
|
33
|
+
const prevByte = i === 0 ? this.lastByte : chunk[i - 1];
|
|
34
|
+
if (prevByte === 0x0d) continue; // 已有 \r 前缀则跳过
|
|
35
|
+
if (i > start) output.push(chunk.slice(start, i)); // 发现需要转换的换行符
|
|
36
|
+
output.push(crlf), start = i + 1; // 跳过当前 \n
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (start < chunk.length) output.push(chunk.slice(start)); // 添加剩余数据
|
|
40
|
+
// 输出结果
|
|
41
|
+
const result = output.length ? Buffer.concat(output) : chunk;
|
|
42
|
+
this.push(result), this.lastByte = chunk.at(-1), done();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export { LeWindows };
|
package/lib/punycode.js
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
/**
|
|
2
|
+
Punycode.js
|
|
3
|
+
主要功能包括:
|
|
4
|
+
1.将Unicode字符串转换为Punycode字符串(仅包含ASCII字符)。
|
|
5
|
+
2.将Punycode字符串转换回Unicode字符串。
|
|
6
|
+
3.处理域名或电子邮件地址的转换,其中只有非ASCII部分会被转换。
|
|
7
|
+
4.将Unicode字符串转换为Punycode字符串,并将结果附加到域名或电子邮件地址的非ASCII部分之前。
|
|
8
|
+
*/
|
|
9
|
+
import { regexs, resetRegex, } from './regexs.js';
|
|
10
|
+
|
|
11
|
+
/** 32位最大正数浮点值 */
|
|
12
|
+
const maxInt = 2147483647, // 即 0x7FFFFFFF 或 2^31-1
|
|
13
|
+
// Bootstring 参数(基础数值,阈值最小值,阈值最大值,偏差值,阻尼因子,初始偏差,初始N值 0x80,分隔符 '\x2D')
|
|
14
|
+
base = 36, tMin = 1, tMax = 26, skew = 38, damp = 700, initialBias = 72, initialN = 128, delimiter = '-',
|
|
15
|
+
/** 错误消息 */
|
|
16
|
+
errors = {
|
|
17
|
+
overflow: '溢出:输入需要更宽的整数来处理', 'not-basic': '非法输入 >= 0x80(不是基本码点)', 'invalid-input': '无效输入'
|
|
18
|
+
},
|
|
19
|
+
/** 便捷缩写 */
|
|
20
|
+
baseMinusTMin = base - tMin, floor = Math.floor, strFromCharCode = String.fromCharCode, // 字符代码转字符串函数
|
|
21
|
+
|
|
22
|
+
/*--------------------------------------------------------------------------*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 通用错误工具函数
|
|
26
|
+
* @private
|
|
27
|
+
* @param {String} type 错误类型
|
|
28
|
+
* @returns {Error} 抛出带有相应错误消息的 `RangeError`
|
|
29
|
+
*/
|
|
30
|
+
error = type => {
|
|
31
|
+
throw new RangeError(errors[type]);
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 通用的 `Array#map` 工具函数
|
|
36
|
+
* @private
|
|
37
|
+
* @param {Array} array 要遍历的数组
|
|
38
|
+
* @param {Function} callback 为每个数组项调用的函数
|
|
39
|
+
* @returns {Array} 回调函数返回值的新数组
|
|
40
|
+
*/
|
|
41
|
+
map = (array, callback) => {
|
|
42
|
+
const result = [];
|
|
43
|
+
let length = array.length;
|
|
44
|
+
while (length--) result[length] = callback(array[length]);
|
|
45
|
+
return result;
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 一个简单的类似 `Array#map` 的包装器,用于处理域名字符串或电子邮件地址
|
|
50
|
+
* @private
|
|
51
|
+
* @param {String} domain 域名或电子邮件地址
|
|
52
|
+
* @param {Function} callback 为每个字符调用的函数
|
|
53
|
+
* @returns {String} 回调函数返回的字符组成的新字符串
|
|
54
|
+
*/
|
|
55
|
+
mapDomain = (domain, callback) => {
|
|
56
|
+
const parts = domain.split('@');
|
|
57
|
+
let result = '';
|
|
58
|
+
// 在电子邮件地址中,只有域名部分应进行punycode编码。保持本地部分(即`@`之前的所有内容)不变
|
|
59
|
+
if (parts.length > 1) result = `${parts[0]}@`, domain = parts[1];
|
|
60
|
+
// 为了IE8兼容性,避免使用`split(regex)`;参见 #17
|
|
61
|
+
const rS = resetRegex(regexs.SEPARATORS);
|
|
62
|
+
domain = domain.replace(rS, '\x2E');
|
|
63
|
+
const encoded = map(domain.split('.'), callback).join('.');
|
|
64
|
+
return result + encoded;
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* 创建一个包含字符串中每个Unicode字符的数字码点的数组。
|
|
69
|
+
* 虽然JavaScript内部使用UCS-2,但此函数会将一对代理半部(UCS-2将每个半部公开为单独的字符)
|
|
70
|
+
* 转换为单个码点,以匹配UTF-16。
|
|
71
|
+
* @see `punycode.ucs2.encode`
|
|
72
|
+
* @memberOf punycode.ucs2
|
|
73
|
+
* @name decode
|
|
74
|
+
* @param {String} string Unicode输入字符串(UCS-2)
|
|
75
|
+
* @returns {Array} 新的码点数组
|
|
76
|
+
*/
|
|
77
|
+
ucs2decode = string => {
|
|
78
|
+
const output = [], length = string.length;
|
|
79
|
+
let counter = 0;
|
|
80
|
+
while (counter < length) {
|
|
81
|
+
const value = string.charCodeAt(counter++); // 获取当前字符的UTF-16代码单元
|
|
82
|
+
// 如果是高代理项(范围:0xD800-0xDBFF)并且不是最后一个字符
|
|
83
|
+
if (value >= 0xd800 && value <= 0xdbff && counter < length) {
|
|
84
|
+
const extra = string.charCodeAt(counter++); // 获取下一个字符
|
|
85
|
+
// 如果下一个字符是低代理项,则添加代理项对;否则,仅添加高代理项并回退计数器
|
|
86
|
+
(extra & 0xfc00) == 0xdc00 ? output.push(((value & 0x3ff) << 10) + (extra & 0x3ff) + 0x10000) :
|
|
87
|
+
(output.push(value), counter--);
|
|
88
|
+
}
|
|
89
|
+
else output.push(value); // 否则,仅追加此代码单
|
|
90
|
+
}
|
|
91
|
+
return output;
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* 基于数字码点数组创建字符串。
|
|
96
|
+
* @see `punycode.ucs2.decode`
|
|
97
|
+
* @memberOf punycode.ucs2
|
|
98
|
+
* @name encode
|
|
99
|
+
* @param {Array} codePoints 数字码点数组
|
|
100
|
+
* @returns {String} 新的Unicode字符串(UCS-2)
|
|
101
|
+
*/
|
|
102
|
+
ucs2encode = codePoints => {
|
|
103
|
+
return String.fromCodePoint(...codePoints);
|
|
104
|
+
},
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* 将基本码点转换为数字/整数。
|
|
108
|
+
* @see `digitToBasic()`
|
|
109
|
+
* @private
|
|
110
|
+
* @param {Number} codePoint 基本数字码点值
|
|
111
|
+
* @returns {Number} 基本码点的数值(用于表示整数),范围从`0`到`base - 1`,如果码点不表示值,则返回`base`
|
|
112
|
+
*/
|
|
113
|
+
basicToDigit = codePoint => {
|
|
114
|
+
|
|
115
|
+
if (codePoint >= 0x30 && codePoint < 0x3a) return 26 + (codePoint - 0x30);//如码点在`0x30`到`0x39`范围内(`0'-`9`),则返回其数值;
|
|
116
|
+
if (codePoint >= 0x41 && codePoint < 0x5b) return codePoint - 0x41; // 如码点在`0x41`到`0x5a`范围内(`A`-`Z`),则返回其数值;
|
|
117
|
+
if (codePoint >= 0x61 && codePoint < 0x7b) return codePoint - 0x61; // 如码点在`0x61`到`0x7a`范围内(`a`-`z`),则返回其数值;
|
|
118
|
+
return base; // 不在基本字符范围内
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* 将数字/整数转换为基本码点。
|
|
123
|
+
* @see `basicToDigit()`
|
|
124
|
+
* @private
|
|
125
|
+
* @param {Number} digit 基本码点的数值
|
|
126
|
+
* @returns {Number} 其值(用于表示整数)为`digit`的基本码点,需要在`0`到`base - 1`范围内。
|
|
127
|
+
* 如果`flag`非零,则使用大写形式;否则使用小写形式。
|
|
128
|
+
* 如果`flag`非零且`digit`没有大写形式,则行为未定义。
|
|
129
|
+
*/
|
|
130
|
+
digitToBasic = (digit, flag) => {
|
|
131
|
+
return digit + 22 + 75 * (digit < 26) - ((flag != 0) << 5);
|
|
132
|
+
},
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* 根据 RFC 3492 第 3.4 节的偏差适应函数。
|
|
136
|
+
* https://tools.ietf.org/html/rfc3492#section-3.4
|
|
137
|
+
* @private
|
|
138
|
+
*/
|
|
139
|
+
adapt = (delta, numPoints, firstTime) => {
|
|
140
|
+
let k = 0;
|
|
141
|
+
delta = firstTime ? floor(delta / damp) : delta >> 1; // 首次处理时除以阻尼因子,否则右移一位
|
|
142
|
+
delta += floor(delta / numPoints);
|
|
143
|
+
for (; /* 无初始化 */ delta > (baseMinusTMin * tMax) >> 1; k += base) delta = floor(delta / baseMinusTMin);
|
|
144
|
+
|
|
145
|
+
return floor(k + ((baseMinusTMin + 1) * delta) / (delta + skew));
|
|
146
|
+
},
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* 将仅包含ASCII符号的Punycode字符串转换为Unicode符号字符串。
|
|
150
|
+
* @memberOf punycode
|
|
151
|
+
* @param {String} input 仅包含ASCII符号的Punycode字符串
|
|
152
|
+
* @returns {String} 生成的Unicode符号字符串
|
|
153
|
+
*/
|
|
154
|
+
decode = input => {
|
|
155
|
+
const output = [], inputLen = input.length;
|
|
156
|
+
// 当前码点值,初始码点,初始偏差,处理基本ASCII字符(分隔符前的部分)
|
|
157
|
+
let i = 0, n = initialN, bias = initialBias, basic = input.lastIndexOf(delimiter);
|
|
158
|
+
if (basic < 0) basic = 0;
|
|
159
|
+
|
|
160
|
+
// 复制分隔符前的ASCII字符到输出
|
|
161
|
+
for (let j = 0; j < basic; j++) {
|
|
162
|
+
if (input.charCodeAt(j) >= 0x80) throw new Error('包含非ASCII字符');
|
|
163
|
+
output.push(input.charCodeAt(j));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// 解码分隔符后的Punycode编码部分
|
|
167
|
+
let index = basic > 0 ? basic + 1 : 0; // 从分隔符后开始处理
|
|
168
|
+
while (index < inputLen) {
|
|
169
|
+
const oldi = i, overflow = '整数溢出';
|
|
170
|
+
let w = 1, k = base; // 权重, 基础值
|
|
171
|
+
|
|
172
|
+
// 解码可变长度整数
|
|
173
|
+
while (true) {
|
|
174
|
+
if (index >= inputLen) throw new Error('输入不完整');
|
|
175
|
+
|
|
176
|
+
const digit = basicToDigit(input.charCodeAt(index++));
|
|
177
|
+
if (digit >= base) throw new Error('无效数字');
|
|
178
|
+
if (digit > Math.floor((maxInt - i) / w)) throw new Error(overflow);
|
|
179
|
+
|
|
180
|
+
i += digit * w;
|
|
181
|
+
const threshold = k <= bias ? tMin : k >= bias + tMax ? tMax : k - bias;
|
|
182
|
+
|
|
183
|
+
if (digit < threshold) break;
|
|
184
|
+
const baseMinusT = base - threshold;
|
|
185
|
+
if (w > Math.floor(maxInt / baseMinusT)) throw new Error(overflow);
|
|
186
|
+
w *= baseMinusT, k += base;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// 计算新字符插入位置
|
|
190
|
+
const out = output.length + 1;
|
|
191
|
+
bias = adapt(i - oldi, out, oldi === 0);
|
|
192
|
+
if (Math.floor(i / out) > maxInt - n) throw new Error(overflow); // 更新码点值并插入到输出
|
|
193
|
+
n += Math.floor(i / out), i %= out, output.splice(i, 0, n), i++; // 在指定位置插入解码后的字符,并移动到下一个插入位置
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return String.fromCodePoint(...output);
|
|
197
|
+
},
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* 将Unicode符号字符串(例如域名标签)转换为仅包含ASCII符号的Punycode字符串。
|
|
201
|
+
* @memberOf punycode
|
|
202
|
+
* @param {String} input Unicode符号字符串
|
|
203
|
+
* @returns {String} 生成的仅包含ASCII符号的Punycode字符串
|
|
204
|
+
*/
|
|
205
|
+
encode = input => {
|
|
206
|
+
input = ucs2decode(input); // 将UCS-2输入转换为Unicode码点数组。
|
|
207
|
+
const output = [], inputLen = input.length; // 缓存长度
|
|
208
|
+
let n = initialN, delta = 0, bias = initialBias; // 初始化状态
|
|
209
|
+
|
|
210
|
+
// 处理基本码点。
|
|
211
|
+
for (const currentValue of input)
|
|
212
|
+
if (currentValue < 0x80) output.push(strFromCharCode(currentValue));
|
|
213
|
+
|
|
214
|
+
const basicLen = output.length;
|
|
215
|
+
let handledCPCount = basicLen; // `handledCPCount`是已处理的码点数量;`basicLength`是基本码点的数量;
|
|
216
|
+
if (basicLen) output.push(delimiter); // 用分隔符结束基本字符串,除非它是空的;
|
|
217
|
+
|
|
218
|
+
// 主编码循环:
|
|
219
|
+
while (handledCPCount < inputLen) {
|
|
220
|
+
// 所有小于 n 的非基本码点都已处理。找到下一个更大的:
|
|
221
|
+
let m = maxInt;
|
|
222
|
+
for (const currentValue of input)
|
|
223
|
+
if (currentValue >= n && currentValue < m) m = currentValue;
|
|
224
|
+
|
|
225
|
+
// 增加 `delta` 足够多,以将解码器的 <n,i> 状态推进到 <m,0>,但要防止溢出。
|
|
226
|
+
const handledCPCountPlusOne = handledCPCount + 1;
|
|
227
|
+
if (m - n > floor((maxInt - delta) / handledCPCountPlusOne)) error('overflow');
|
|
228
|
+
|
|
229
|
+
delta += (m - n) * handledCPCountPlusOne, n = m;
|
|
230
|
+
for (const currentValue of input) {
|
|
231
|
+
if (currentValue < n && ++delta > maxInt) error('overflow');
|
|
232
|
+
if (currentValue === n) {
|
|
233
|
+
// 将 delta 表示为广义可变长度整数。
|
|
234
|
+
let q = delta;
|
|
235
|
+
for (let k = base /* 无条件 */; ; k += base) {
|
|
236
|
+
const t = k <= bias ? tMin : k >= bias + tMax ? tMax : k - bias;
|
|
237
|
+
if (q < t) break;
|
|
238
|
+
|
|
239
|
+
const qMinusT = q - t, baseMinusT = base - t;
|
|
240
|
+
output.push(strFromCharCode(digitToBasic(t + (qMinusT % baseMinusT), 0))), q = floor(qMinusT / baseMinusT);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
output.push(strFromCharCode(digitToBasic(q, 0)));
|
|
244
|
+
bias = adapt(delta, handledCPCountPlusOne, handledCPCount === basicLen), delta = 0, ++handledCPCount;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
++delta, ++n;
|
|
248
|
+
}
|
|
249
|
+
return output.join('');
|
|
250
|
+
},
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* 将表示域名或电子邮件地址的Punycode字符串转换为Unicode。
|
|
254
|
+
* 只有输入的Punycode部分会被转换,即如果你在已经转换为Unicode的字符串上调用它也没有关系。
|
|
255
|
+
* @memberOf punycode
|
|
256
|
+
* @param {String} input 要转换为Unicode的Punycode编码的域名或电子邮件地址
|
|
257
|
+
* @returns {String} 给定Punycode字符串的Unicode表示
|
|
258
|
+
*/
|
|
259
|
+
toUnicode = input => {
|
|
260
|
+
return mapDomain(input, string => string.startsWith('xn--') ? decode(string.slice(4).toLowerCase()) : string);
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* 将表示域名或电子邮件地址的Unicode字符串转换为Punycode;
|
|
265
|
+
* 只有域名的非ASCII部分会被转换,即如果你使用已经是ASCII的域名调用它也没有关系;
|
|
266
|
+
* >查看定义:@see {@link toASCII}
|
|
267
|
+
* @memberOf punycode
|
|
268
|
+
* @param {String} input 要转换的域名或电子邮件地址,作为Unicode字符串
|
|
269
|
+
* @returns {String} 给定域名或电子邮件地址的Punycode表示
|
|
270
|
+
*/
|
|
271
|
+
const toASCII = input => {
|
|
272
|
+
return mapDomain(input, string => regexs.NON_BASIC_ASCII.test(string) ? `xn--${encode(string)}` : string);
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
/*--------------------------------------------------------------------------*/
|
|
276
|
+
// 导出punycode结构内容
|
|
277
|
+
// version: '2.3.1', // Punycode.js版本号 <https://mathiasbynens.be/notes/javascript-encoding>
|
|
278
|
+
// ucs2decode,
|
|
279
|
+
// ucs2encode,
|
|
280
|
+
// decode,
|
|
281
|
+
// encode,
|
|
282
|
+
// toUnicode,
|
|
283
|
+
export { toASCII };
|
package/lib/qp.js
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { Transform } from 'stream';
|
|
2
|
+
import { regexs, resetRegex } from './regexs.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 为Quoted-Printable字符串添加软换行
|
|
6
|
+
*
|
|
7
|
+
* @param {String} str 可能需要换行的Quoted-Printable编码字符串
|
|
8
|
+
* @param {Number} [lineLength=76] 每行的最大允许长度
|
|
9
|
+
* @returns {String} 经过软换行处理的Quoted-Printable编码字符串
|
|
10
|
+
*/
|
|
11
|
+
const qpWrap = (str = '', lineLength = 76) => {
|
|
12
|
+
str = str.toString();
|
|
13
|
+
if (str.length <= lineLength) return str; // 如果字符串长度不超过行限制,直接返回
|
|
14
|
+
let pos = 0, match, code, line, result = '';
|
|
15
|
+
const len = str.length, lineMargin = Math.floor(lineLength / 3),
|
|
16
|
+
// 将行添加到结果中并更新位置
|
|
17
|
+
addLineAndUpdatePos = currentLine => { result += currentLine, pos += currentLine.length };
|
|
18
|
+
|
|
19
|
+
// 在需要的位置插入软换行
|
|
20
|
+
while (pos < len) {
|
|
21
|
+
line = str.slice(pos, pos + lineLength);
|
|
22
|
+
// 处理已有的CRLF换行
|
|
23
|
+
if ((match = line.includes('\r\n'))) {
|
|
24
|
+
line = line.slice(0, match.index + match[0].length), addLineAndUpdatePos(line);
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// 如果以换行符结尾,保持不变
|
|
29
|
+
if (line.endsWith('\n')) {
|
|
30
|
+
addLineAndUpdatePos(line);
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
else if ((match = line.slice(-lineMargin).match(regexs.TRAILING_NEWLINE))) {
|
|
34
|
+
line = line.slice(0, line.length - (match[0].length - 1)), addLineAndUpdatePos(line); // 截断到最近的换行符
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
else if (line.length > lineLength - lineMargin && (match = line.slice(-lineMargin).match(regexs.TRAILING_SPACE_OR_PUNCTUATION)))
|
|
38
|
+
line = line.slice(0, line.length - (match[0].length - 1)); // 截断到最近的空格
|
|
39
|
+
else if (line.match(regexs.INCOMPLETE_ENCODING)) {
|
|
40
|
+
// 将不完整的编码序列推到下一行
|
|
41
|
+
if ((match = line.match(regexs.INCOMPLETE_ENCODING_SHORT))) line = line.slice(0, line.length - match[0].length);
|
|
42
|
+
|
|
43
|
+
const rCE = resetRegex(regexs.COMPLETE_ENCODING);
|
|
44
|
+
// 确保UTF-8序列不被分割
|
|
45
|
+
while (line.length > 3 && line.length < len - pos && !line.match(regexs.UTF8_ENCODING) && (match = line.match(rCE))) {
|
|
46
|
+
code = parseInt(match[0].slice(1, 3), 16);
|
|
47
|
+
if (code < 128) break;
|
|
48
|
+
|
|
49
|
+
line = line.slice(0, line.length - 3);
|
|
50
|
+
if (code >= 0xc0) break;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// 添加软换行
|
|
55
|
+
if (pos + line.length < len && line.endsWith('\n')) {
|
|
56
|
+
const rCE = resetRegex(regexs.COMPLETE_ENCODING);
|
|
57
|
+
if (line.length === lineLength && line.match(rCE)) line = line.slice(0, line.length - 3);
|
|
58
|
+
else if (line.length === lineLength) line = line.slice(0, line.length - 1);
|
|
59
|
+
|
|
60
|
+
pos += line.length, line += '=\r\n';
|
|
61
|
+
}
|
|
62
|
+
else pos += line.length;
|
|
63
|
+
|
|
64
|
+
result += line;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return result;
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* 辅助函数:检查数字是否在指定范围内
|
|
72
|
+
*
|
|
73
|
+
* @param {Number} nr 要检查的数字
|
|
74
|
+
* @param {Array} ranges 允许的值范围数组
|
|
75
|
+
* @returns {Boolean} 如果在允许范围内返回true,否则返回false
|
|
76
|
+
*/
|
|
77
|
+
checkRanges = (nr, ranges) => {
|
|
78
|
+
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
|
+
if (ranges[i].length === 2 && nr >= ranges[i][0] && nr <= ranges[i][1]) return true;// 如果范围有两个值,则比较是否在范围内
|
|
82
|
+
}
|
|
83
|
+
return false;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* 将Buffer编码为Quoted-Printable格式的字符串
|
|
88
|
+
* >查看定义:@see {@link qpEncode}
|
|
89
|
+
* @param {Buffer} buffer 要转换的Buffer
|
|
90
|
+
* @returns {String} Quoted-Printable编码的字符串
|
|
91
|
+
*/
|
|
92
|
+
const qpEncode = buffer => {
|
|
93
|
+
if (typeof buffer === 'string') buffer = Buffer.from(buffer, 'utf-8'); // 如果输入是字符串,先转换为Buffer
|
|
94
|
+
|
|
95
|
+
// 不需要编码的可用字符范围
|
|
96
|
+
const ranges = [
|
|
97
|
+
// https://tools.ietf.org/html/rfc2045#section-6.7
|
|
98
|
+
[0x09], [0x0a], [0x0d], // <TAB>,<LF>,<CR>
|
|
99
|
+
[0x20, 0x3c], // <SP>!"#$%&'()*+,-./0123456789:;
|
|
100
|
+
[0x3e, 0x7e] // >?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}
|
|
101
|
+
];
|
|
102
|
+
let result = '';
|
|
103
|
+
|
|
104
|
+
// 遍历buffer中的每个字节
|
|
105
|
+
for (let i = 0, len = buffer.length; i < len; i++) {
|
|
106
|
+
const ord = buffer[i], nextByte = buffer[i + 1];
|
|
107
|
+
|
|
108
|
+
// 如果字符在允许范围内,并且不是行尾的空格或制表符,则直接使用原字符
|
|
109
|
+
if (checkRanges(ord, ranges) && !((ord === 0x20 || ord === 0x09) && (i === len - 1 || nextByte === 0x0a
|
|
110
|
+
|| nextByte === 0x0d))) {
|
|
111
|
+
result += String.fromCharCode(ord);
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
result += `=${(ord < 0x10 ? '0' : '')}${ord.toString(16).toUpperCase()}`; // 否则进行编码:=后跟两位十六进制数
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return result;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* 创建用于将数据编码为Quoted-Printable格式的转换流
|
|
122
|
+
* >查看定义:@see {@link QpEncoder}
|
|
123
|
+
* @constructor
|
|
124
|
+
* @param {Object} options 流选项
|
|
125
|
+
* @param {Number} [options.lineLength=76] 行最大长度,设置为false可禁用换行
|
|
126
|
+
*/
|
|
127
|
+
class QpEncoder extends Transform {
|
|
128
|
+
constructor(options = {}) {
|
|
129
|
+
super();
|
|
130
|
+
this.options = options; // 初始化
|
|
131
|
+
const { lineLength = 76 } = this.options;
|
|
132
|
+
if (lineLength !== false) this.options.lineLength = lineLength; // 设置行长度,默认为76
|
|
133
|
+
this._curLine = '', this.inputBytes = 0, this.outputBytes = 0; // 当前行缓存和统计输入输出字节数
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* 转换数据块
|
|
138
|
+
*/
|
|
139
|
+
_transform(chunk, encoding, done) {
|
|
140
|
+
let qp;
|
|
141
|
+
if (encoding !== 'buffer') chunk = Buffer.from(chunk, encoding); // 确保chunk是Buffer类型
|
|
142
|
+
if (!chunk?.length) return done(); // 空数据块直接返回
|
|
143
|
+
this.inputBytes += chunk.length;
|
|
144
|
+
|
|
145
|
+
// 如果启用了行长度限制
|
|
146
|
+
const { lineLength } = this.options;
|
|
147
|
+
if (lineLength) {
|
|
148
|
+
// 编码并添加换行
|
|
149
|
+
qp = qpWrap(this._curLine + qpEncode(chunk), lineLength);
|
|
150
|
+
qp = qp.replace(regexs.LAST_LINE, (_, lineBreak, lastLine) => {
|
|
151
|
+
this._curLine = lastLine;
|
|
152
|
+
return lineBreak;
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
if (qp) this.outputBytes += qp.length, this.push(qp);
|
|
156
|
+
}
|
|
157
|
+
// 否则没有启用行长度限制,直接编码并输出
|
|
158
|
+
else qp = qpEncode(chunk), this.outputBytes += qp.length, this.push(qp, 'ascii');
|
|
159
|
+
done();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* 刷新流,输出剩余数据
|
|
164
|
+
*/
|
|
165
|
+
_flush(done) {
|
|
166
|
+
if (this._curLine) this.outputBytes += this._curLine.length, this.push(this._curLine, 'ascii');
|
|
167
|
+
done();
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export { qpEncode, QpEncoder };
|