@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
|
@@ -11,7 +11,7 @@ import { detectExtension, detectMimeType, isPlainText, encodeWord } from '../mim
|
|
|
11
11
|
* 通常由 Mailer 实例内部创建和调用,不建议直接实例化;
|
|
12
12
|
*/
|
|
13
13
|
class MailMessage {
|
|
14
|
-
//
|
|
14
|
+
// 构造函数,初始化邮件消息对象
|
|
15
15
|
constructor(mailer, data = {}) {
|
|
16
16
|
this.mailer = mailer, this.message = null;
|
|
17
17
|
const { options = {}, _defaults = {} } = mailer;
|
|
@@ -22,37 +22,39 @@ class MailMessage {
|
|
|
22
22
|
const headers = { ...defaults.headers, ...data.headers }, mailData = { ...defaults, ...data, headers };
|
|
23
23
|
// 强制应用 transporter 选项中的特定键
|
|
24
24
|
['disableFileAccess', 'disableUrlAccess', 'normalizeHeaderKey'].forEach(key => {
|
|
25
|
-
if (key in options) mailData[key] = options[key]; //
|
|
25
|
+
if (key in options) mailData[key] = options[key]; // 如果在选项中设置,则使用选项值
|
|
26
26
|
});
|
|
27
27
|
|
|
28
28
|
return mailData;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
//
|
|
31
|
+
// 解析内容方法(代理给 resolveContent)
|
|
32
32
|
resolveContent(...args) {
|
|
33
33
|
return resolveContent(...args);
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
//
|
|
36
|
+
// 解析所有内容(文本、HTML、附件等)
|
|
37
37
|
resolveAll(callback) {
|
|
38
38
|
// 定义需要解析的内容键
|
|
39
39
|
const keys = [
|
|
40
|
-
[this.data, 'html'], [this.data, 'text'], [this.data, 'watchHtml'], [this.data, 'amp'],
|
|
40
|
+
[this.data, 'html'], [this.data, 'text'], [this.data, 'watchHtml'], [this.data, 'amp'],
|
|
41
|
+
[this.data, 'icalEvent']
|
|
41
42
|
], { alternatives, attachments } = this.data;
|
|
42
|
-
alternatives?.forEach((_, i) => keys.push([alternatives, i])); //
|
|
43
|
+
alternatives?.forEach((_, i) => keys.push([alternatives, i])); // 如果有替代内容,则添加到键中
|
|
43
44
|
|
|
44
45
|
// 处理附件
|
|
45
46
|
attachments?.forEach((attachment, i) => {
|
|
46
47
|
const { filename, path, href = '', contentType } = attachment;
|
|
47
48
|
let newFname = filename;
|
|
48
|
-
//
|
|
49
|
+
// 如果没有文件名,则从路径或href中提取,或使用默认名称
|
|
49
50
|
if (!newFname) {
|
|
50
51
|
newFname = (path || href).split('/').pop().split('?').shift() || `attachment-${(i + 1)}`;
|
|
51
|
-
|
|
52
|
+
// 如果没有扩展名,则根据内容类型添加
|
|
53
|
+
if (!newFname.includes('.')) newFname += `.${detectExtension(contentType)}`;
|
|
52
54
|
attachment.filename = newFname; // 设置回attachment对象
|
|
53
55
|
}
|
|
54
56
|
|
|
55
|
-
//
|
|
57
|
+
// 如果没有内容类型,则根据文件名、路径或href检测
|
|
56
58
|
if (!contentType) attachment.contentType = detectMimeType(newFname || path || href || 'bin');
|
|
57
59
|
keys.push([attachments, i]);
|
|
58
60
|
});
|
|
@@ -63,16 +65,16 @@ class MailMessage {
|
|
|
63
65
|
addressKeys.forEach(address => {
|
|
64
66
|
let value;
|
|
65
67
|
const rAddress = this.message.getHeader(address === 'replyTo' ? 'reply-to' : address);
|
|
66
|
-
//
|
|
68
|
+
// 如果已有消息,从消息头解析地址
|
|
67
69
|
if (this.message) value = [].concat(mimeNode.parseAddresses(rAddress) || []);
|
|
68
|
-
//
|
|
70
|
+
// 否则如果数据中有,则直接从数据中解析
|
|
69
71
|
else if (this.data[address]) value = [].concat(mimeNode.parseAddresses(this.data[address]) || []);
|
|
70
72
|
|
|
71
|
-
if (value?.length) this.data[address] = value; //
|
|
72
|
-
else if (address in this.data) this.data[address] = null; //
|
|
73
|
+
if (value?.length) this.data[address] = value; // 如果有值,则保存到数据中
|
|
74
|
+
else if (address in this.data) this.data[address] = null; // 否则如果数据中有地址字段,则删除地址字段
|
|
73
75
|
});
|
|
74
76
|
|
|
75
|
-
//
|
|
77
|
+
// 处理单值地址字段(from, sender);如果数据中有地址,则保存为地址对象
|
|
76
78
|
['from', 'sender'].forEach(address => {
|
|
77
79
|
if (this.data[address]) this.data[address] = this.data[address].shift();
|
|
78
80
|
});
|
|
@@ -87,11 +89,12 @@ class MailMessage {
|
|
|
87
89
|
if (!parent || !currentItem) return resolveNext(); // 跳过空值
|
|
88
90
|
resolveContent(...args, (err, value) => {
|
|
89
91
|
if (err) return callback(err);
|
|
90
|
-
const node = { content: value }; //
|
|
92
|
+
const node = { content: value }; // 创建内容节点,保留原始数据的其他属性
|
|
91
93
|
if (currentItem && typeof currentItem === 'object' && !Buffer.isBuffer(currentItem))
|
|
92
94
|
Object.keys(currentItem).forEach(key => {
|
|
93
|
-
//
|
|
94
|
-
if (!(key in node) &&
|
|
95
|
+
// 如果没有在节点中,则从原始数据中复制属性
|
|
96
|
+
if (!(key in node) &&
|
|
97
|
+
!['content', 'path', 'href', 'raw'].includes(key)) node[key] = currentItem[key];
|
|
95
98
|
});
|
|
96
99
|
|
|
97
100
|
parent[key] = node, resolveNext(); // 更新数据为节点对象并继续解析下一个
|
|
@@ -103,15 +106,16 @@ class MailMessage {
|
|
|
103
106
|
|
|
104
107
|
// 规范化邮件数据
|
|
105
108
|
normalize(callback) {
|
|
106
|
-
|
|
109
|
+
// 获取信封信息
|
|
110
|
+
const envelope = this.data.envelope || this.message.getEnvelope(), messageId = this.message.messageId();
|
|
107
111
|
// 首先解析所有内容
|
|
108
112
|
this.resolveAll((err, data) => {
|
|
109
|
-
if (err) return callback(err); //
|
|
113
|
+
if (err) return callback(err); // 如果解析失败,返回错误
|
|
110
114
|
|
|
111
115
|
data.envelope = envelope, data.messageId = messageId; // 设置信封和消息ID
|
|
112
116
|
['html', 'text', 'watchHtml', 'amp'].forEach(key => {
|
|
113
117
|
const content = data[key]?.content;
|
|
114
|
-
//
|
|
118
|
+
// 如果内容不是是字符串,则转换为字符串,否则直接使用,
|
|
115
119
|
if (content) data[key] = Buffer.isBuffer(content) ? content.toString() : content;
|
|
116
120
|
});
|
|
117
121
|
|
|
@@ -158,7 +162,7 @@ class MailMessage {
|
|
|
158
162
|
|
|
159
163
|
// 设置邮件发送器头部
|
|
160
164
|
setMailerHeader() {
|
|
161
|
-
if (!this.message || !this.data.xMailer) return; // 如果没有消息或没有xMailer
|
|
165
|
+
if (!this.message || !this.data.xMailer) return; // 如果没有消息或没有xMailer,则直接返回
|
|
162
166
|
this.message.setHeader('X-Mailer', this.data.xMailer);
|
|
163
167
|
}
|
|
164
168
|
|
|
@@ -177,7 +181,7 @@ class MailMessage {
|
|
|
177
181
|
// 设置列表头部
|
|
178
182
|
setListHeaders() {
|
|
179
183
|
const list = this.data.list;
|
|
180
|
-
if (!this.message || !list || typeof list !== 'object') return; //
|
|
184
|
+
if (!this.message || !list || typeof list !== 'object') return; // 如果没有消息或没有列表头部,则直接返回
|
|
181
185
|
|
|
182
186
|
// 添加可选的 List-* 头部
|
|
183
187
|
if (list && typeof list === 'object')
|
|
@@ -206,7 +210,7 @@ class MailMessage {
|
|
|
206
210
|
url = formattedUrl.replace(regexs.LIST_ID_PROTOCOL_REMOVE, '');
|
|
207
211
|
return quotedComment + url; // List-ID: "comment" <domain>
|
|
208
212
|
}
|
|
209
|
-
// 返回格式化后的URL
|
|
213
|
+
// 返回格式化后的URL和注释(List-*: <http://domain> (comment))
|
|
210
214
|
return comment ? `${formattedUrl} (${comment})` : formattedUrl;
|
|
211
215
|
}).join(', ')
|
|
212
216
|
}]
|
|
@@ -218,7 +222,7 @@ class MailMessage {
|
|
|
218
222
|
const rUC = resetRegex(regexs.URL_CLEAN);
|
|
219
223
|
url = url.replace(rUC, ''); // 去除空白和尖括号
|
|
220
224
|
if (regexs.PROTOCOL_CHECK.test(url)) return `<${url}>`; // 如果包含协议,则使用尖括号包裹
|
|
221
|
-
//
|
|
225
|
+
// 如果包含@,则使用mailto协议,否则使用HTTP协议
|
|
222
226
|
return regexs.EMAIL_CHECK.test(url) ? `<mailto:${url}>` : `<http://${url}>`;
|
|
223
227
|
}
|
|
224
228
|
}
|
package/lib/mime-funcs/index.js
CHANGED
|
@@ -45,8 +45,8 @@ const defaultMimeType = 'application/octet-stream', defaultExtension = 'bin',
|
|
|
45
45
|
};
|
|
46
46
|
|
|
47
47
|
/**
|
|
48
|
-
*
|
|
49
|
-
*
|
|
48
|
+
* 检查值是否为纯文本字符串(仅使用可打印的7位字符)
|
|
49
|
+
* 允许的控制字符:\x09(制表符)、\x0a(换行符)、\x0d(回车符)、可打印ASCII(\x20-\x7e)
|
|
50
50
|
* 排除: 其他控制字符和DEL,非ASCII字符
|
|
51
51
|
* >查看定义:@see {@link isPlainText}
|
|
52
52
|
* @param {String} value 要测试的字符串
|
|
@@ -54,16 +54,16 @@ const defaultMimeType = 'application/octet-stream', defaultExtension = 'bin',
|
|
|
54
54
|
* @returns {Boolean} 如果是纯文本字符串则返回true
|
|
55
55
|
*/
|
|
56
56
|
const isPlainText = (value, isParam) => {
|
|
57
|
-
//
|
|
57
|
+
// 如果是参数,则允许双引号,否则不允许
|
|
58
58
|
const re = isParam ? regexs.PARAM_PLAIN_TEXT : regexs.PLAIN_TEXT;
|
|
59
59
|
return typeof value === 'string' && !re.test(value);
|
|
60
60
|
},
|
|
61
61
|
|
|
62
62
|
/**
|
|
63
|
-
* 拆分MIME
|
|
63
|
+
* 拆分MIME编码的字符串;用于将MIME字分成更小的块
|
|
64
64
|
*
|
|
65
65
|
* @param {String} str 要拆分的MIME编码字符串
|
|
66
|
-
* @param {Number} maxlen
|
|
66
|
+
* @param {Number} maxlen 一个部分的最大字符长度(最小12)
|
|
67
67
|
* @return {Array} 拆分后的字符串
|
|
68
68
|
*/
|
|
69
69
|
_splitMimeEncodedString = (str = '', maxlen) => {
|
|
@@ -82,7 +82,7 @@ const isPlainText = (value, isParam) => {
|
|
|
82
82
|
// 检查是否不在Unicode字符序列的中间
|
|
83
83
|
if ((match = str.substring(curLine.length).match(regexs.ENCODED_SEQUENCE))) {
|
|
84
84
|
chr = parseInt(match[1], 16);
|
|
85
|
-
//
|
|
85
|
+
// 无效序列,向后移动一个字符并重新检查
|
|
86
86
|
if (chr < 0xc2 && chr > 0x7f) curLine = curLine.substring(0, curLine.length - 3), done = false;
|
|
87
87
|
}
|
|
88
88
|
}
|
|
@@ -95,8 +95,8 @@ const isPlainText = (value, isParam) => {
|
|
|
95
95
|
},
|
|
96
96
|
|
|
97
97
|
/**
|
|
98
|
-
* 将字符串或Buffer编码为UTF-8
|
|
99
|
-
*
|
|
98
|
+
* 将字符串或Buffer编码为UTF-8参数值连续编码(rfc2231)
|
|
99
|
+
* 用于拆分长参数值;
|
|
100
100
|
*
|
|
101
101
|
* 例如
|
|
102
102
|
* title="unicode string"
|
|
@@ -121,19 +121,20 @@ const isPlainText = (value, isParam) => {
|
|
|
121
121
|
return '';
|
|
122
122
|
});
|
|
123
123
|
|
|
124
|
-
if (encodedStr) list.push({ line: encodedStr }); //
|
|
124
|
+
if (encodedStr) list.push({ line: encodedStr }); // 如果最后一个字符串不为空,则将其推送到数组中
|
|
125
125
|
}
|
|
126
|
-
//
|
|
126
|
+
// 否则,如果字符串包含Unicode或特殊字符,则将其拆分为多个块
|
|
127
127
|
else {
|
|
128
128
|
let chr, i, len, line = "utf-8''", encoded = true, startPos = 0;
|
|
129
|
-
//
|
|
129
|
+
// 如果字符串包含代理对,则将其规范化为字符数组
|
|
130
130
|
if (regexs.UTF16_HIGH_SURROGATE.test(encodedStr)) {
|
|
131
131
|
const encodedStrArr = [];
|
|
132
132
|
for (i = 0, len = encodedStr.length; i < len; i++) {
|
|
133
133
|
chr = encodedStr.charAt(i);
|
|
134
134
|
const ord = chr.charCodeAt(0);
|
|
135
|
-
//
|
|
136
|
-
if (ord >= 0xd800 && ord <= 0xdbff && i < len - 1) chr += encodedStr.charAt(i + 1),
|
|
135
|
+
// 如果字符是代理对的第一个字符,则将其与第二个字符一起添加到数组中
|
|
136
|
+
if (ord >= 0xd800 && ord <= 0xdbff && i < len - 1) chr += encodedStr.charAt(i + 1),
|
|
137
|
+
encodedStrArr.push(chr), i++;
|
|
137
138
|
else encodedStrArr.push(chr); // 否则,将其添加到数组中
|
|
138
139
|
}
|
|
139
140
|
encodedStr = encodedStrArr
|
|
@@ -143,13 +144,13 @@ const isPlainText = (value, isParam) => {
|
|
|
143
144
|
for (i = 0; i < encodedStr.length; i++) {
|
|
144
145
|
chr = encodedStr[i];
|
|
145
146
|
const encodedChr = _safeEncodeURIComponent(chr);
|
|
146
|
-
if (encoded) chr = encodedChr; //
|
|
147
|
+
if (encoded) chr = encodedChr; // 如果需要编码,则对当前字符进行URL编码
|
|
147
148
|
else {
|
|
148
149
|
chr = chr === ' ' ? chr : encodedChr; // 尝试对当前字符进行URL编码
|
|
149
|
-
//
|
|
150
|
+
// 默认情况下不需要编码行,只有当字符串包含Unicode或特殊字符时才需要
|
|
150
151
|
if (chr !== encodedStr[i]) {
|
|
151
152
|
// 检查是否可以将编码字符添加到行中
|
|
152
|
-
//
|
|
153
|
+
// 如果不能,则没有理由使用此行,只需将其推送到列表,并开始新行处理需要编码的字符
|
|
153
154
|
if ((_safeEncodeURIComponent(line) + chr).length >= maxLength)
|
|
154
155
|
list.push({ line, encoded }), line = '', startPos = i - 1;
|
|
155
156
|
else {
|
|
@@ -159,25 +160,26 @@ const isPlainText = (value, isParam) => {
|
|
|
159
160
|
}
|
|
160
161
|
}
|
|
161
162
|
|
|
162
|
-
//
|
|
163
|
+
// 如果行已经太长,将其推送到列表并开始新行
|
|
163
164
|
if ((line + chr).length >= maxLength) {
|
|
164
|
-
list.push({ line, encoded }),
|
|
165
|
-
|
|
165
|
+
list.push({ line, encoded }),
|
|
166
|
+
line = chr = encodedStr[i] === ' ' ? ' ' : _safeEncodeURIComponent(encodedStr[i]);
|
|
167
|
+
// 如果添加的字符不需要编码,则将编码设置为false,并继续处理下一个字符;否则设为需要编码
|
|
166
168
|
chr === encodedStr[i] ? (encoded = false, startPos = i - 1) : encoded = true;
|
|
167
169
|
}
|
|
168
|
-
else line += chr; //
|
|
170
|
+
else line += chr; // 否则,将字符添加到行中
|
|
169
171
|
}
|
|
170
172
|
|
|
171
|
-
if (line) list.push({ line, encoded }); //
|
|
173
|
+
if (line) list.push({ line, encoded }); // 如果最后一行不为空,则将其推送到列表中
|
|
172
174
|
}
|
|
173
175
|
|
|
174
|
-
//
|
|
176
|
+
// 如果任何行需要编码,则第一行(part==0)总是编码的
|
|
175
177
|
return list.map((item, i) => ({ key: `${key}*${i}${(item.encoded ? '*' : '')}`, value: item.line }));
|
|
176
178
|
},
|
|
177
179
|
|
|
178
180
|
/**
|
|
179
181
|
* 处理 RFC2231 编码的参数
|
|
180
|
-
* 将拆分的rfc2231
|
|
182
|
+
* 将拆分的rfc2231字符串合并到单个键中,并进行解码
|
|
181
183
|
* @param {Object} params 参数对象
|
|
182
184
|
*/
|
|
183
185
|
_processRFC2231Params = params => {
|
|
@@ -186,7 +188,8 @@ const isPlainText = (value, isParam) => {
|
|
|
186
188
|
let match, value;
|
|
187
189
|
if ((match = key.match(regexs.RFC2231_KEY))) {
|
|
188
190
|
const actualKey = key.substring(0, match.index), nr = Number(match[2] || match[3]) || 0;
|
|
189
|
-
if (!params[actualKey] || typeof params[actualKey] !== 'object')
|
|
191
|
+
if (!params[actualKey] || typeof params[actualKey] !== 'object')
|
|
192
|
+
params[actualKey] = { charset: false, values: [] };
|
|
190
193
|
|
|
191
194
|
value = params[key];
|
|
192
195
|
if (nr === 0 && match[0].endsWith('*') && (match = value.match(regexs.CHARSET_LANGUAGE_VALUE)))
|
|
@@ -217,7 +220,7 @@ const isPlainText = (value, isParam) => {
|
|
|
217
220
|
|
|
218
221
|
/**
|
|
219
222
|
* 检查多行字符串是否包含超过指定长度的行
|
|
220
|
-
*
|
|
223
|
+
* 如果值是纯文本但有超过允许长度的行,则使用format=flowed
|
|
221
224
|
* >查看定义:@see {@link hasLongerLines}
|
|
222
225
|
* @param {String} str 要检查的字符串(不检查超过128kB的字符串)
|
|
223
226
|
* @param {Number} lineLength 要检查的最大行长度
|
|
@@ -231,8 +234,8 @@ const hasLongerLines = (str, lineLength) => {
|
|
|
231
234
|
* 将字符串或Buffer编码为UTF-8 MIME字(rfc2047)
|
|
232
235
|
* >查看定义:@see {@link encodeWord}
|
|
233
236
|
* @param {String|Buffer} data 要编码的字符串
|
|
234
|
-
* @param {String} mimeWordEncoding='Q' MIME
|
|
235
|
-
* @param {Number} [maxLength=0]
|
|
237
|
+
* @param {String} mimeWordEncoding='Q' MIME字的编码,Q或B
|
|
238
|
+
* @param {Number} [maxLength=0] 如果设置,根据需要将MIME字拆分为多个块
|
|
236
239
|
* @return {String} 单个或多个连接在一起的MIME字
|
|
237
240
|
*/
|
|
238
241
|
const encodeWord = (data, mimeWordEncoding = "Q", maxLength = 0) => {
|
|
@@ -242,15 +245,15 @@ const encodeWord = (data, mimeWordEncoding = "Q", maxLength = 0) => {
|
|
|
242
245
|
const toCharset = 'UTF-8', offset = 7 + toCharset.length;
|
|
243
246
|
maxLength && maxLength > offset && (maxLength -= offset);
|
|
244
247
|
|
|
245
|
-
// 如果mimeWordEncoding为Q
|
|
248
|
+
// 如果mimeWordEncoding为Q,则使用qp编码,否则使用base64编码
|
|
246
249
|
if (newMWE === 'Q') {
|
|
247
250
|
const rNSC = resetRegex(regexs.NON_SAFE_CHAR);
|
|
248
251
|
encodedStr = qpEncode(data).replace(rNSC, chr => {
|
|
249
252
|
const ord = chr.charCodeAt(0).toString(16).toUpperCase(), XX = ord.length === 1 ? `0${ord}` : ord
|
|
250
|
-
return chr === ' ' ? '_' : `=${XX}`;//
|
|
253
|
+
return chr === ' ' ? '_' : `=${XX}`;// 如果字符是空格,则使用_;否则使用=XX
|
|
251
254
|
});
|
|
252
255
|
}
|
|
253
|
-
// 否则如果mimeWordEncoding为B
|
|
256
|
+
// 否则如果mimeWordEncoding为B,则使用base64编码
|
|
254
257
|
else if (newMWE === 'B') {
|
|
255
258
|
encodedStr = typeof data === 'string' ? data : base64Encode(data);
|
|
256
259
|
maxLength = maxLength ? Math.max(3, ((maxLength - (maxLength % 4)) / 4) * 3) : 0;
|
|
@@ -264,16 +267,16 @@ const encodeWord = (data, mimeWordEncoding = "Q", maxLength = 0) => {
|
|
|
264
267
|
let lpart = '';
|
|
265
268
|
for (let i = 0, len = encodedStr.length; i < len; i++) {
|
|
266
269
|
let chr = encodedStr.charAt(i);
|
|
267
|
-
// 如果字符是emoji
|
|
270
|
+
// 如果字符是emoji,则将下一个字符也添加到字符串中
|
|
268
271
|
if (regexs.EMOJI_OR_SURROGATE.test(chr) && i < len - 1) chr += encodedStr.charAt(++i);
|
|
269
272
|
|
|
270
|
-
//
|
|
273
|
+
// 如果字符串加上当前字符的长度超过最大长度,则将字符串推送到数组中,并开始一个新的字符串
|
|
271
274
|
if (Buffer.byteLength(lpart + chr) <= maxLength || i === 0) lpart += chr;
|
|
272
|
-
else parts.push(base64Encode(lpart)), lpart = chr; //
|
|
275
|
+
else parts.push(base64Encode(lpart)), lpart = chr; // 达到长度限制,推送现有字符串并重新开始
|
|
273
276
|
}
|
|
274
|
-
if (lpart) parts.push(base64Encode(lpart)); //
|
|
277
|
+
if (lpart) parts.push(base64Encode(lpart)); // 如果最后一个字符串不为空,则将其推送到数组中
|
|
275
278
|
|
|
276
|
-
//
|
|
279
|
+
// 如果有多个字符串,则将它们连接到一起,并用分隔符分隔;否则,如果只有一个字符串,则将其返回
|
|
277
280
|
encodedStr = parts.length > 1 ? parts.join(delimiter) : parts.join('');
|
|
278
281
|
}
|
|
279
282
|
}
|
|
@@ -285,20 +288,21 @@ const encodeWord = (data, mimeWordEncoding = "Q", maxLength = 0) => {
|
|
|
285
288
|
* 查找包含非ASCII文本的单词序列并将其转换为MIME字
|
|
286
289
|
* >查看定义:@see {@link encodeWords}
|
|
287
290
|
* @param {String} value 要编码的字符串
|
|
288
|
-
* @param {String} mimeWordEncoding='Q' MIME
|
|
289
|
-
* @param {Number} [maxLength=0]
|
|
290
|
-
* @param {Boolean} [encodeAll=false] 如果为true
|
|
291
|
+
* @param {String} mimeWordEncoding='Q' MIME字的编码,Q或B
|
|
292
|
+
* @param {Number} [maxLength=0] 如果设置,根据需要将MIME字拆分为多个块
|
|
293
|
+
* @param {Boolean} [encodeAll=false] 如果为true且值需要编码,则编码整个字符串,而不仅仅是最小匹配
|
|
291
294
|
* @return {String} 可能包含MIME字的字符串
|
|
292
295
|
*/
|
|
293
296
|
const encodeWords = (value, mimeWordEncoding = "Q", maxLength = 0, encodeAll) => {
|
|
294
|
-
// 查找第一个和最后一个是否包含不可打印ASCII
|
|
297
|
+
// 查找第一个和最后一个是否包含不可打印ASCII或特殊符号的单词,如果找不到,则返回原始值
|
|
295
298
|
const firstMatch = value.match(regexs.FIRST_NON_ASCII_WORD), lastMatch = value.match(regexs.LAST_NON_ASCII_WORD);
|
|
296
299
|
if (!firstMatch || !lastMatch) return value;
|
|
297
|
-
//
|
|
300
|
+
// 如果请求编码所有内容或字符串包含类似编码字的内容,则返回编码后的所有内容
|
|
298
301
|
if (encodeAll) return encodeWord(value, mimeWordEncoding, maxLength);
|
|
299
302
|
|
|
300
303
|
const startIndex = firstMatch.index + firstMatch[0].length - firstMatch[0].trimStart().length,
|
|
301
|
-
endIndex = lastMatch.index + (lastMatch[1] || '').length,
|
|
304
|
+
endIndex = lastMatch.index + (lastMatch[1] || '').length,
|
|
305
|
+
startStr = startIndex ? value.substring(0, startIndex) : '',
|
|
302
306
|
endStr = endIndex < value.length ? value.substring(endIndex) : '',
|
|
303
307
|
eWord = encodeWords(value.substring(startIndex, endIndex), mimeWordEncoding, maxLength);
|
|
304
308
|
return `${startStr}${eWord}${endStr}`;
|
|
@@ -307,8 +311,8 @@ const encodeWords = (value, mimeWordEncoding = "Q", maxLength = 0, encodeAll) =>
|
|
|
307
311
|
/**
|
|
308
312
|
* 将解析后的头值连接为 'value; param1=value1; param2=value2';
|
|
309
313
|
* >查看定义:@see {@link buildHeaderValue}
|
|
310
|
-
* -
|
|
311
|
-
*
|
|
314
|
+
* - 注意:我们遵循RFC 822来维护需要放在引号中的特殊字符列表;
|
|
315
|
+
* 参考:https://www.w3.org/Protocols/rfc1341/4_Content-Type.html
|
|
312
316
|
* @param {Object} structured 解析后的头值
|
|
313
317
|
* @return {String} 连接后的头值
|
|
314
318
|
*/
|
|
@@ -316,7 +320,7 @@ const buildHeaderValue = structured => {
|
|
|
316
320
|
const paramsArray = [];
|
|
317
321
|
Object.keys(structured.params || {}).forEach(param => {
|
|
318
322
|
const pValue = structured.params[param];
|
|
319
|
-
//
|
|
323
|
+
// 如果值是纯文本字符串,则将其编码为MIME字
|
|
320
324
|
if (!isPlainText(pValue, true) || pValue.length >= 75) {
|
|
321
325
|
_buildHeaderParam(param, pValue, 50).forEach(encodedParam => {
|
|
322
326
|
const { key, value } = encodedParam;
|
|
@@ -325,7 +329,7 @@ const buildHeaderValue = structured => {
|
|
|
325
329
|
else paramsArray.push(`${key}=${JSON.stringify(value)}`); // 否则将参数编码为JSON字符串
|
|
326
330
|
});
|
|
327
331
|
}
|
|
328
|
-
//
|
|
332
|
+
// 否则如果值是纯文本字符串,则将其编码为JSON字符串
|
|
329
333
|
else if (regexs.UNSAFE_PARAM_VALUE.test(pValue)) paramsArray.push(`${param}=${JSON.stringify(pValue)}`);
|
|
330
334
|
else paramsArray.push(`${param}=${pValue}`); // 否则,如果值是安全的,则将其编码为MIME字
|
|
331
335
|
});
|
|
@@ -346,7 +350,7 @@ const buildHeaderValue = structured => {
|
|
|
346
350
|
*/
|
|
347
351
|
const parseHeaderValue = str => {
|
|
348
352
|
const result = { value: '', params: {} };
|
|
349
|
-
if (!str || typeof str !== 'string') return result; //
|
|
353
|
+
if (!str || typeof str !== 'string') return result; // 输入验证:如果输入为空或不是字符串,直接返回空结果
|
|
350
354
|
|
|
351
355
|
// 当前解析状态,正在解析的参数键名,正在解析的值,引号字符,是否处于转义状态
|
|
352
356
|
let state = 'value', currentKey = '', currentValue = '', quoteChar = '', escaped = false;
|
|
@@ -354,7 +358,7 @@ const parseHeaderValue = str => {
|
|
|
354
358
|
for (let i = 0; i < str.length; i++) {
|
|
355
359
|
const char = str[i];
|
|
356
360
|
|
|
357
|
-
//
|
|
361
|
+
// 处理转义字符:如果前一个字符是\,当前字符直接加入值,不进行特殊处理
|
|
358
362
|
if (escaped) {
|
|
359
363
|
currentValue += char, escaped = false;
|
|
360
364
|
continue;
|
|
@@ -362,16 +366,16 @@ const parseHeaderValue = str => {
|
|
|
362
366
|
|
|
363
367
|
// 根据当前状态处理字符
|
|
364
368
|
switch (state) {
|
|
365
|
-
case 'value': //
|
|
366
|
-
//
|
|
369
|
+
case 'value': // 解析主值部分(如:text/plain)
|
|
370
|
+
// 如果遇到分号,保存主值(去除前后空格),重置当前值,切换到键解析状态
|
|
367
371
|
if (char === ';') result.value = currentValue.trim(), currentValue = '', state = 'key';
|
|
368
372
|
else if (char === '\\') escaped = true; // 遇到反斜杠设置转义为真
|
|
369
|
-
else currentValue += char; //
|
|
373
|
+
else currentValue += char; // 普通字符:直接添加到当前值
|
|
370
374
|
break;
|
|
371
|
-
case 'key': //
|
|
375
|
+
case 'key': // 解析参数键名(如:charset)
|
|
372
376
|
if (char === '=') currentKey = currentValue.trim().toLowerCase(), currentValue = '', state = 'value';
|
|
373
377
|
else if (char === ';') {
|
|
374
|
-
//
|
|
378
|
+
// 遇到分号但没有等号如:;flag
|
|
375
379
|
if (currentValue.trim()) result.params[currentValue.trim().toLowerCase()] = ''; // 保存空值参数
|
|
376
380
|
currentValue = '';
|
|
377
381
|
}
|
|
@@ -384,11 +388,11 @@ const parseHeaderValue = str => {
|
|
|
384
388
|
break;
|
|
385
389
|
}
|
|
386
390
|
|
|
387
|
-
//
|
|
391
|
+
// 检查是否需要进入引号状态(不在转义状态下)
|
|
388
392
|
if (!escaped && (char === '"' || char === "'") && state !== 'quoted') quoteChar = char, state = 'quoted';
|
|
389
393
|
}
|
|
390
394
|
|
|
391
|
-
const finalValue = currentValue.trim(); //
|
|
395
|
+
const finalValue = currentValue.trim(); // 循环结束后,处理最后剩余的值
|
|
392
396
|
// 如果最后为值状态且有值
|
|
393
397
|
if (state === 'value' && finalValue) {
|
|
394
398
|
if (!result.value && !currentKey) result.value = finalValue; // 如果主值还未设置且没有当前键,设置为主值
|
|
@@ -397,7 +401,7 @@ const parseHeaderValue = str => {
|
|
|
397
401
|
// 否则,如果最后在为键状态且有值,设置为空参数
|
|
398
402
|
else if (state === 'key' && finalValue) result.params[finalValue.toLowerCase()] = '';
|
|
399
403
|
|
|
400
|
-
_processRFC2231Params(result.params); // 处理 RFC2231
|
|
404
|
+
_processRFC2231Params(result.params); // 处理 RFC2231 编码的参数(用于邮件头编码)
|
|
401
405
|
return result;
|
|
402
406
|
};
|
|
403
407
|
|
|
@@ -408,15 +412,16 @@ const parseHeaderValue = str => {
|
|
|
408
412
|
* @returns {string} 检测到的文件扩展名
|
|
409
413
|
*/
|
|
410
414
|
const detectExtension = (mimeType = '') => {
|
|
411
|
-
// 如果MIME
|
|
415
|
+
// 如果MIME类型为空,返回默认扩展名
|
|
412
416
|
if (!mimeType) return defaultExtension;
|
|
413
417
|
|
|
414
|
-
// 处理MIME类型字符串:转换为小写,去除空格,按斜杠分割;获取主类型(
|
|
415
|
-
const parts = mimeType.toLowerCase().trim().split('/'), rootType = parts.shift().trim(),
|
|
418
|
+
// 处理MIME类型字符串:转换为小写,去除空格,按斜杠分割;获取主类型(如:application、text等)和子类型(处理包含斜杠的情况)
|
|
419
|
+
const parts = mimeType.toLowerCase().trim().split('/'), rootType = parts.shift().trim(),
|
|
420
|
+
subType = parts.join('/').trim(),
|
|
416
421
|
fullMimeType = `${rootType}/${subType}`; // 重新构建标准化的MIME类型字符串并查找对应的扩展名
|
|
417
422
|
if (MIME_TYPES.has(fullMimeType)) {
|
|
418
423
|
const value = MIME_TYPES.get(fullMimeType);
|
|
419
|
-
return Array.isArray(value) ? value[0] : value; //
|
|
424
|
+
return Array.isArray(value) ? value[0] : value; // 如果值是数组,返回第一个元素
|
|
420
425
|
}
|
|
421
426
|
|
|
422
427
|
// 根据主类型返回默认扩展名
|
|
@@ -433,15 +438,15 @@ const detectExtension = (mimeType = '') => {
|
|
|
433
438
|
* @returns {string} 检测到的MIME类型
|
|
434
439
|
*/
|
|
435
440
|
const detectMimeType = filename => {
|
|
436
|
-
if (!filename) return defaultMimeType; //
|
|
441
|
+
if (!filename) return defaultMimeType; // 如果文件名为空,返回默认MIME类型
|
|
437
442
|
|
|
438
443
|
const { ext, name = '' } = parse(filename), // 解析文件路径获取扩展名
|
|
439
|
-
//
|
|
444
|
+
// 提取扩展名(去掉点号),处理查询参数,转换为小写
|
|
440
445
|
extension = (ext.substring(1) || name).split('?').shift().trim().toLowerCase(),
|
|
441
446
|
|
|
442
|
-
//
|
|
447
|
+
// 如果在扩展名映射表中找到对应项,则使用对应的MIME类型
|
|
443
448
|
value = EXTENSIONS.has(extension) ? EXTENSIONS.get(extension) : defaultMimeType;
|
|
444
|
-
return Array.isArray(value) ? value[0] : value; //
|
|
449
|
+
return Array.isArray(value) ? value[0] : value; // 如果值是数组,返回第一个元素;否则返回值
|
|
445
450
|
};
|
|
446
451
|
|
|
447
452
|
/**
|
|
@@ -449,7 +454,7 @@ const detectMimeType = filename => {
|
|
|
449
454
|
* >查看定义:@see {@link foldLines}
|
|
450
455
|
• @param {String} str 要折叠的字符串
|
|
451
456
|
• @param {Number} [lineLength=76] 一行的最大长度
|
|
452
|
-
• @param {Boolean} afterSpace 如果为true
|
|
457
|
+
• @param {Boolean} afterSpace 如果为true,在行尾留一个空格
|
|
453
458
|
• @return {String} 带有折叠行的字符串
|
|
454
459
|
*/
|
|
455
460
|
const foldLines = (str = '', lineLength = 76, afterSpace) => {
|
|
@@ -1102,7 +1102,8 @@ const EXTENSIONS = new Map([
|
|
|
1102
1102
|
['bdm', 'application/vnd.syncml.dm+wbxml'],
|
|
1103
1103
|
['bed', 'application/vnd.realvnc.bed'],
|
|
1104
1104
|
['bh2', 'application/vnd.fujitsu.oasysprs'],
|
|
1105
|
-
['bin', ['application/octet-stream', 'application/mac-binary', 'application/macbinary', 'application/x-macbinary',
|
|
1105
|
+
['bin', ['application/octet-stream', 'application/mac-binary', 'application/macbinary', 'application/x-macbinary',
|
|
1106
|
+
'application/x-binary']],
|
|
1106
1107
|
['bm', 'image/bmp'],
|
|
1107
1108
|
['bmi', 'application/vnd.bmi'],
|
|
1108
1109
|
['bmp', ['image/bmp', 'image/x-windows-bmp']],
|
|
@@ -1147,7 +1148,8 @@ const EXTENSIONS = new Map([
|
|
|
1147
1148
|
['cii', 'application/vnd.anser-web-certificate-issue-initiation'],
|
|
1148
1149
|
['cil', 'application/vnd.ms-artgalry'],
|
|
1149
1150
|
['cla', 'application/vnd.claymore'],
|
|
1150
|
-
['class', ['application/octet-stream', 'application/java', 'application/java-byte-code', 'application/java-vm',
|
|
1151
|
+
['class', ['application/octet-stream', 'application/java', 'application/java-byte-code', 'application/java-vm',
|
|
1152
|
+
'application/x-java-class']],
|
|
1151
1153
|
['clkk', 'application/vnd.crick.clicker.keyboard'],
|
|
1152
1154
|
['clkp', 'application/vnd.crick.clicker.palette'],
|
|
1153
1155
|
['clkt', 'application/vnd.crick.clicker.template'],
|
|
@@ -1407,7 +1409,8 @@ const EXTENSIONS = new Map([
|
|
|
1407
1409
|
['jpgv', 'video/jpeg'],
|
|
1408
1410
|
['jpm', 'video/jpm'],
|
|
1409
1411
|
['jps', 'image/x-jps'],
|
|
1410
|
-
['js', ['application/javascript', 'application/ecmascript', 'text/javascript', 'text/ecmascript',
|
|
1412
|
+
['js', ['application/javascript', 'application/ecmascript', 'text/javascript', 'text/ecmascript',
|
|
1413
|
+
'application/x-javascript']],
|
|
1411
1414
|
['json', 'application/json'],
|
|
1412
1415
|
['jut', 'image/jutvision'],
|
|
1413
1416
|
['kar', ['audio/midi', 'music/x-karaoke']],
|
|
@@ -1484,7 +1487,8 @@ const EXTENSIONS = new Map([
|
|
|
1484
1487
|
['mgz', 'application/vnd.proteus.magazine'],
|
|
1485
1488
|
['mht', 'message/rfc822'],
|
|
1486
1489
|
['mhtml', 'message/rfc822'],
|
|
1487
|
-
['mid', ['audio/mid', 'audio/midi', 'music/crescendo', 'x-music/x-midi', 'audio/x-midi', 'application/x-midi',
|
|
1490
|
+
['mid', ['audio/mid', 'audio/midi', 'music/crescendo', 'x-music/x-midi', 'audio/x-midi', 'application/x-midi',
|
|
1491
|
+
'audio/x-mid']],
|
|
1488
1492
|
['midi', ['audio/midi', 'music/crescendo', 'x-music/x-midi', 'audio/x-midi', 'application/x-midi', 'audio/x-mid']],
|
|
1489
1493
|
['mif', ['application/vnd.mif', 'application/x-mif', 'application/x-frame']],
|
|
1490
1494
|
['mime', ['message/rfc822', 'www/mime']],
|
|
@@ -1669,7 +1673,8 @@ const EXTENSIONS = new Map([
|
|
|
1669
1673
|
['pps', ['application/vnd.ms-powerpoint', 'application/mspowerpoint']],
|
|
1670
1674
|
['ppsm', 'application/vnd.ms-powerpoint.slideshow.macroenabled.12'],
|
|
1671
1675
|
['ppsx', 'application/vnd.openxmlformats-officedocument.presentationml.slideshow'],
|
|
1672
|
-
['ppt', ['application/vnd.ms-powerpoint', 'application/mspowerpoint', 'application/powerpoint',
|
|
1676
|
+
['ppt', ['application/vnd.ms-powerpoint', 'application/mspowerpoint', 'application/powerpoint',
|
|
1677
|
+
'application/x-mspowerpoint']],
|
|
1673
1678
|
['pptm', 'application/vnd.ms-powerpoint.presentation.macroenabled.12'],
|
|
1674
1679
|
['pptx', 'application/vnd.openxmlformats-officedocument.presentationml.presentation'],
|
|
1675
1680
|
['ppz', 'application/mspowerpoint'],
|
|
@@ -1752,7 +1757,8 @@ const EXTENSIONS = new Map([
|
|
|
1752
1757
|
['sbml', 'application/sbml+xml'],
|
|
1753
1758
|
['sc', 'application/vnd.ibm.secure-container'],
|
|
1754
1759
|
['scd', 'application/x-msschedule'],
|
|
1755
|
-
['scm', ['application/vnd.lotus-screencam', 'video/x-scm', 'text/x-script.guile', 'application/x-lotusscreencam',
|
|
1760
|
+
['scm', ['application/vnd.lotus-screencam', 'video/x-scm', 'text/x-script.guile', 'application/x-lotusscreencam',
|
|
1761
|
+
'text/x-script.scheme']],
|
|
1756
1762
|
['scq', 'application/scvp-cv-request'],
|
|
1757
1763
|
['scs', 'application/scvp-cv-response'],
|
|
1758
1764
|
['sct', 'text/scriptlet'],
|