@flun/mailer 2.1.24 → 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 +7 -5
- 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 +88 -74
- 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 +4 -4
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'],
|