@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.
Files changed (42) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/LICENSE +15 -0
  3. package/Mail.js +47 -0
  4. package/README.md +475 -0
  5. package/copy-files.js +36 -0
  6. package/index.d.ts +816 -0
  7. package/index.js +109 -0
  8. package/lib/addressparser.js +230 -0
  9. package/lib/base64.js +103 -0
  10. package/lib/dkim/index.js +211 -0
  11. package/lib/dkim/message-parser.js +114 -0
  12. package/lib/dkim/relaxed-body.js +133 -0
  13. package/lib/dkim/sign.js +121 -0
  14. package/lib/fetch/cookies.js +224 -0
  15. package/lib/fetch/index.js +233 -0
  16. package/lib/http-proxy-client.js +98 -0
  17. package/lib/json-transport.js +38 -0
  18. package/lib/mail-composer.js +316 -0
  19. package/lib/mailer/index.js +301 -0
  20. package/lib/mailer/mail-message.js +226 -0
  21. package/lib/mime-funcs/index.js +493 -0
  22. package/lib/mime-funcs/mime-types.js +2061 -0
  23. package/lib/mime-node/index.js +811 -0
  24. package/lib/mime-node/last-newline.js +44 -0
  25. package/lib/mime-node/le-unix.js +37 -0
  26. package/lib/mime-node/le-windows.js +46 -0
  27. package/lib/punycode.js +283 -0
  28. package/lib/qp.js +171 -0
  29. package/lib/regexs.js +124 -0
  30. package/lib/sendmail-transport.js +91 -0
  31. package/lib/ses-transport.js +115 -0
  32. package/lib/shared.js +725 -0
  33. package/lib/smtp-connection/data-stream.js +68 -0
  34. package/lib/smtp-connection/index.js +1062 -0
  35. package/lib/smtp-pool/index.js +294 -0
  36. package/lib/smtp-pool/pool-resource.js +93 -0
  37. package/lib/smtp-transport.js +81 -0
  38. package/lib/stream-transport.js +59 -0
  39. package/lib/well-known/index.js +54 -0
  40. package/lib/well-known/services.json +802 -0
  41. package/lib/xoauth2.js +249 -0
  42. package/package.json +78 -0
@@ -0,0 +1,493 @@
1
+ import { parse } from 'path';
2
+ import { MIME_TYPES, EXTENSIONS } from './mime-types.js';
3
+ import { regexs, resetRegex } from '../regexs.js';
4
+ import { base64Encode } from '../base64.js';
5
+ import { qpEncode } from '../qp.js';
6
+
7
+ // 默认的MIME类型(通用的二进制数据流) 和 默认的文件扩展名
8
+ const defaultMimeType = 'application/octet-stream', defaultExtension = 'bin',
9
+
10
+ /**
11
+ * 对URI字符组件进行编码
12
+ *
13
+ * @param {String} chr 要编码的字符
14
+ * @return {String} 编码后的字符
15
+ */
16
+ _encodeURICharComponent = chr => {
17
+ let res = '', ord = chr.charCodeAt(0).toString(16).toUpperCase();
18
+
19
+ if (ord.length % 2) ord = `0${ord}`;
20
+ if (ord.length > 2)
21
+ for (let i = 0; i < ord.length / 2; i++) res += `%${ord.substring(i * 2, i * 2 + 2)}`;
22
+ else res += `%${ord}`;
23
+
24
+ return res;
25
+ },
26
+
27
+ /**
28
+ * 安全编码URI组件
29
+ *
30
+ * @param {String} str 要编码的字符串
31
+ * @return {String} 编码后的字符串
32
+ */
33
+ _safeEncodeURIComponent = (str = '') => {
34
+ str = str.toString();
35
+
36
+ try {
37
+ str = encodeURIComponent(str); // 尝试使用encodeURIComponent编码字符串
38
+ } catch (E) {
39
+ const rNSU = resetRegex(regexs.NON_SAFE_URI);
40
+ return str.replace(rNSU, ''); // 返回无效字符的替换字符串
41
+ }
42
+
43
+ const rNSUC = resetRegex(regexs.NON_SAFE_URI_CHAR);
44
+ return str.replace(rNSUC, chr => _encodeURICharComponent(chr)); // 确保encodeURIComponent未处理的字符也被转换
45
+ };
46
+
47
+ /**
48
+ * 检查值是否为纯文本字符串(仅使用可打印的7位字符)
49
+ * 允许的控制字符:\x09(制表符)、\x0a(换行符)、\x0d(回车符)、可打印ASCII(\x20-\x7e)
50
+ * 排除: 其他控制字符和DEL,非ASCII字符
51
+ * >查看定义:@see {@link isPlainText}
52
+ * @param {String} value 要测试的字符串
53
+ * @param {Boolean} isParam 是否为参数
54
+ * @returns {Boolean} 如果是纯文本字符串则返回true
55
+ */
56
+ const isPlainText = (value, isParam) => {
57
+ // 如果是参数,则允许双引号,否则不允许
58
+ const re = isParam ? regexs.PARAM_PLAIN_TEXT : regexs.PLAIN_TEXT;
59
+ return typeof value === 'string' && !re.test(value);
60
+ },
61
+
62
+ /**
63
+ * 拆分MIME编码的字符串。用于将MIME字分成更小的块
64
+ *
65
+ * @param {String} str 要拆分的MIME编码字符串
66
+ * @param {Number} maxlen 一个部分的最大字符长度(最小12)
67
+ * @return {Array} 拆分后的字符串
68
+ */
69
+ _splitMimeEncodedString = (str = '', maxlen) => {
70
+ let curLine, match, chr, done;
71
+ const lines = [];
72
+
73
+ maxlen = Math.max(maxlen || 0, 12); // 至少需要12个符号以适应可能的4字节UTF-8序列
74
+ while (str.length) {
75
+ curLine = str.substring(0, maxlen);
76
+ // 将不完整的转义字符移回主字符串
77
+ if (match = curLine.match(regexs.INCOMPLETE_ENCODING_END)) curLine = curLine.substring(0, match.index);
78
+
79
+ done = false;
80
+ while (!done) {
81
+ done = true;
82
+ // 检查是否不在Unicode字符序列的中间
83
+ if ((match = str.substring(curLine.length).match(regexs.ENCODED_SEQUENCE))) {
84
+ chr = parseInt(match[1], 16);
85
+ // 无效序列,向后移动一个字符并重新检查
86
+ if (chr < 0xc2 && chr > 0x7f) curLine = curLine.substring(0, curLine.length - 3), done = false;
87
+ }
88
+ }
89
+
90
+ if (curLine.length) lines.push(curLine);
91
+ str = str.substring(curLine.length);
92
+ }
93
+
94
+ return lines;
95
+ },
96
+
97
+ /**
98
+ * 将字符串或Buffer编码为UTF-8参数值连续编码(rfc2231)
99
+ * 用于拆分长参数值。
100
+ *
101
+ * 例如
102
+ * title="unicode string"
103
+ * 变为
104
+ * title*0*=utf-8''unicode
105
+ * title*1*=%20string
106
+ *
107
+ * @param {String} key 参数键
108
+ * @param {String|Buffer} data 要编码的字符串
109
+ * @param {Number} [maxLength=50] 生成块的最大长度
110
+ * @return {Array} 编码键和头的列表
111
+ */
112
+ _buildHeaderParam = (key, data = '', maxLength = 50) => {
113
+ const list = [];
114
+ let encodedStr = typeof data === 'string' ? data : data.toString();
115
+
116
+ // 处理仅ASCII文本
117
+ if (isPlainText(data, true)) {
118
+ if (encodedStr.length <= maxLength) return [{ key, value: encodedStr }]; // 检查是否需要转换
119
+ encodedStr = encodedStr.replace(new RegExp(`.{${maxLength}}`, 'g'), str => {
120
+ list.push({ line: str });
121
+ return '';
122
+ });
123
+
124
+ if (encodedStr) list.push({ line: encodedStr }); // 如果最后一个字符串不为空,则将其推送到数组中
125
+ }
126
+ // 否则,如果字符串包含Unicode或特殊字符,则将其拆分为多个块
127
+ else {
128
+ let chr, i, len, line = "utf-8''", encoded = true, startPos = 0;
129
+ // 如果字符串包含代理对,则将其规范化为字符数组
130
+ if (regexs.UTF16_HIGH_SURROGATE.test(encodedStr)) {
131
+ const encodedStrArr = [];
132
+ for (i = 0, len = encodedStr.length; i < len; i++) {
133
+ chr = encodedStr.charAt(i);
134
+ const ord = chr.charCodeAt(0);
135
+ // 如果字符是代理对的第一个字符,则将其与第二个字符一起添加到数组中
136
+ if (ord >= 0xd800 && ord <= 0xdbff && i < len - 1) chr += encodedStr.charAt(i + 1), encodedStrArr.push(chr), i++;
137
+ else encodedStrArr.push(chr); // 否则,将其添加到数组中
138
+ }
139
+ encodedStr = encodedStrArr
140
+ };
141
+
142
+ // 处理包含Unicode或特殊字符的文本,第一行包含字符集和语言信息,需要编码(即使不包含任何Unicode字符)
143
+ for (i = 0; i < encodedStr.length; i++) {
144
+ chr = encodedStr[i];
145
+ const encodedChr = _safeEncodeURIComponent(chr);
146
+ if (encoded) chr = encodedChr; // 如果需要编码,则对当前字符进行URL编码
147
+ else {
148
+ chr = chr === ' ' ? chr : encodedChr; // 尝试对当前字符进行URL编码
149
+ // 默认情况下不需要编码行,只有当字符串包含Unicode或特殊字符时才需要
150
+ if (chr !== encodedStr[i]) {
151
+ // 检查是否可以将编码字符添加到行中
152
+ // 如果不能,则没有理由使用此行,只需将其推送到列表,并开始新行处理需要编码的字符
153
+ if ((_safeEncodeURIComponent(line) + chr).length >= maxLength)
154
+ list.push({ line, encoded }), line = '', startPos = i - 1;
155
+ else {
156
+ encoded = true, i = startPos, line = '';
157
+ continue;
158
+ }
159
+ }
160
+ }
161
+
162
+ // 如果行已经太长,将其推送到列表并开始新行
163
+ if ((line + chr).length >= maxLength) {
164
+ list.push({ line, encoded }), line = chr = encodedStr[i] === ' ' ? ' ' : _safeEncodeURIComponent(encodedStr[i]);
165
+ // 如果添加的字符不需要编码,则将编码设置为false,并继续处理下一个字符;否则设为需要编码
166
+ chr === encodedStr[i] ? (encoded = false, startPos = i - 1) : encoded = true;
167
+ }
168
+ else line += chr; // 否则,将字符添加到行中
169
+ }
170
+
171
+ if (line) list.push({ line, encoded }); // 如果最后一行不为空,则将其推送到列表中
172
+ }
173
+
174
+ // 如果任何行需要编码,则第一行(part==0)总是编码的
175
+ return list.map((item, i) => ({ key: `${key}*${i}${(item.encoded ? '*' : '')}`, value: item.line }));
176
+ },
177
+
178
+ /**
179
+ * 处理 RFC2231 编码的参数
180
+ * 将拆分的rfc2231字符串合并到单个键中,并进行解码
181
+ * @param {Object} params 参数对象
182
+ */
183
+ _processRFC2231Params = params => {
184
+ // 将拆分的rfc2231字符串合并到单个键中
185
+ Object.keys(params).forEach(key => {
186
+ let match, value;
187
+ if ((match = key.match(regexs.RFC2231_KEY))) {
188
+ const actualKey = key.substring(0, match.index), nr = Number(match[2] || match[3]) || 0;
189
+ if (!params[actualKey] || typeof params[actualKey] !== 'object') params[actualKey] = { charset: false, values: [] };
190
+
191
+ value = params[key];
192
+ if (nr === 0 && match[0].endsWith('*') && (match = value.match(regexs.CHARSET_LANGUAGE_VALUE)))
193
+ params[actualKey].charset = match[1] || 'iso-8859-1', value = match[2];
194
+
195
+ params[actualKey].values[nr] = value, delete params[key]; // 删除旧引用
196
+ }
197
+ });
198
+
199
+ // 连接拆分的rfc2231字符串并将编码字符串转换为MIME编码字
200
+ Object.keys(params).forEach(key => {
201
+ let value;
202
+ if (params[key] && Array.isArray(params[key].values)) {
203
+ value = params[key].values.map(val => val || '').join('');
204
+
205
+ if (params[key].charset) {
206
+ const rRSC = resetRegex(regexs.RFC2231_SPECIAL_CHARS);
207
+ // 将"%AB"转换为"=?charset?Q?=AB?="
208
+ params[key] = `=?${params[key].charset}?Q?` + value.replace(rRSC, s => {
209
+ const c = s.charCodeAt(0).toString(16);
210
+ return s === ' ' ? '_' : `%${(c.length < 2 ? '0' : '')}${c}`; // 修复无效编码的字符
211
+ }).replace(/%/g, '=') + '?='; // 从URL编码更改为百分比编码
212
+ }
213
+ else params[key] = value;
214
+ }
215
+ });
216
+ };
217
+
218
+ /**
219
+ * 检查多行字符串是否包含超过指定长度的行
220
+ * 如果值是纯文本但有超过允许长度的行,则使用format=flowed
221
+ * >查看定义:@see {@link hasLongerLines}
222
+ * @param {String} str 要检查的字符串(不检查超过128kB的字符串)
223
+ * @param {Number} lineLength 要检查的最大行长度
224
+ * @returns {Boolean} 如果至少有一行超过lineLength个字符则返回true
225
+ */
226
+ const hasLongerLines = (str, lineLength) => {
227
+ return str.length > 128 * 1024 ? true : new RegExp(`^.{${lineLength + 1},}`, 'm').test(str);
228
+ };
229
+
230
+ /**
231
+ * 将字符串或Buffer编码为UTF-8 MIME字(rfc2047)
232
+ * >查看定义:@see {@link encodeWord}
233
+ * @param {String|Buffer} data 要编码的字符串
234
+ * @param {String} mimeWordEncoding='Q' MIME字的编码,Q或B
235
+ * @param {Number} [maxLength=0] 如果设置,根据需要将MIME字拆分为多个块
236
+ * @return {String} 单个或多个连接在一起的MIME字
237
+ */
238
+ const encodeWord = (data, mimeWordEncoding = "Q", maxLength = 0) => {
239
+ const newMWE = mimeWordEncoding.toString().toUpperCase().trim().charAt(0);
240
+
241
+ let encodedStr;
242
+ const toCharset = 'UTF-8', offset = 7 + toCharset.length;
243
+ maxLength && maxLength > offset && (maxLength -= offset);
244
+
245
+ // 如果mimeWordEncoding为Q,则使用qp编码,否则使用base64编码
246
+ if (newMWE === 'Q') {
247
+ const rNSC = resetRegex(regexs.NON_SAFE_CHAR);
248
+ encodedStr = qpEncode(data).replace(rNSC, chr => {
249
+ const ord = chr.charCodeAt(0).toString(16).toUpperCase(), XX = ord.length === 1 ? `0${ord}` : ord
250
+ return chr === ' ' ? '_' : `=${XX}`;// 如果字符是空格,则使用_;否则使用=XX
251
+ });
252
+ }
253
+ // 否则如果mimeWordEncoding为B,则使用base64编码
254
+ else if (newMWE === 'B') {
255
+ encodedStr = typeof data === 'string' ? data : base64Encode(data);
256
+ maxLength = maxLength ? Math.max(3, ((maxLength - (maxLength % 4)) / 4) * 3) : 0;
257
+ }
258
+
259
+ if (maxLength && encodedStr.length > maxLength) {
260
+ const delimiter = `?= =?${toCharset}?${newMWE}?`; // 将多个MIME编码字连接起来
261
+ if (newMWE === 'Q') encodedStr = _splitMimeEncodedString(encodedStr, maxLength).join(delimiter);
262
+ else {
263
+ const parts = [];
264
+ let lpart = '';
265
+ for (let i = 0, len = encodedStr.length; i < len; i++) {
266
+ let chr = encodedStr.charAt(i);
267
+ // 如果字符是emoji,则将下一个字符也添加到字符串中
268
+ if (regexs.EMOJI_OR_SURROGATE.test(chr) && i < len - 1) chr += encodedStr.charAt(++i);
269
+
270
+ // 如果字符串加上当前字符的长度超过最大长度,则将字符串推送到数组中,并开始一个新的字符串
271
+ if (Buffer.byteLength(lpart + chr) <= maxLength || i === 0) lpart += chr;
272
+ else parts.push(base64Encode(lpart)), lpart = chr; // 达到长度限制,推送现有字符串并重新开始
273
+ }
274
+ if (lpart) parts.push(base64Encode(lpart)); // 如果最后一个字符串不为空,则将其推送到数组中
275
+
276
+ // 如果有多个字符串,则将它们连接到一起,并用分隔符分隔;否则,如果只有一个字符串,则将其返回
277
+ encodedStr = parts.length > 1 ? parts.join(delimiter) : parts.join('');
278
+ }
279
+ }
280
+
281
+ return `=?${toCharset}?${newMWE}?${encodedStr}${encodedStr.slice(-2) === '?=' ? '' : '?='}`; // 返回编码后的字符串
282
+ };
283
+
284
+ /**
285
+ * 查找包含非ASCII文本的单词序列并将其转换为MIME字
286
+ * >查看定义:@see {@link encodeWords}
287
+ * @param {String} value 要编码的字符串
288
+ * @param {String} mimeWordEncoding='Q' MIME字的编码,Q或B
289
+ * @param {Number} [maxLength=0] 如果设置,根据需要将MIME字拆分为多个块
290
+ * @param {Boolean} [encodeAll=false] 如果为true且值需要编码,则编码整个字符串,而不仅仅是最小匹配
291
+ * @return {String} 可能包含MIME字的字符串
292
+ */
293
+ const encodeWords = (value, mimeWordEncoding = "Q", maxLength = 0, encodeAll) => {
294
+ // 查找第一个和最后一个是否包含不可打印ASCII或特殊符号的单词,如果找不到,则返回原始值
295
+ const firstMatch = value.match(regexs.FIRST_NON_ASCII_WORD), lastMatch = value.match(regexs.LAST_NON_ASCII_WORD);
296
+ if (!firstMatch || !lastMatch) return value;
297
+ // 如果请求编码所有内容或字符串包含类似编码字的内容,则返回编码后的所有内容
298
+ if (encodeAll) return encodeWord(value, mimeWordEncoding, maxLength);
299
+
300
+ const startIndex = firstMatch.index + firstMatch[0].length - firstMatch[0].trimStart().length,
301
+ endIndex = lastMatch.index + (lastMatch[1] || '').length, startStr = startIndex ? value.substring(0, startIndex) : '',
302
+ endStr = endIndex < value.length ? value.substring(endIndex) : '',
303
+ eWord = encodeWords(value.substring(startIndex, endIndex), mimeWordEncoding, maxLength);
304
+ return `${startStr}${eWord}${endStr}`;
305
+ };
306
+
307
+ /**
308
+ * 将解析后的头值连接为 'value; param1=value1; param2=value2';
309
+ * >查看定义:@see {@link buildHeaderValue}
310
+ * - 注意:我们遵循RFC 822来维护需要放在引号中的特殊字符列表;
311
+ * 参考:https://www.w3.org/Protocols/rfc1341/4_Content-Type.html
312
+ * @param {Object} structured 解析后的头值
313
+ * @return {String} 连接后的头值
314
+ */
315
+ const buildHeaderValue = structured => {
316
+ const paramsArray = [];
317
+ Object.keys(structured.params || {}).forEach(param => {
318
+ const pValue = structured.params[param];
319
+ // 如果值是纯文本字符串,则将其编码为MIME字
320
+ if (!isPlainText(pValue, true) || pValue.length >= 75) {
321
+ _buildHeaderParam(param, pValue, 50).forEach(encodedParam => {
322
+ const { key, value } = encodedParam;
323
+ // 如果参数值安全或参数是编码后的字符串,则不编码
324
+ if (!regexs.UNSAFE_HEADER_PARAM.test(value) || key.endsWith('*')) paramsArray.push(`${key}=${value}`);
325
+ else paramsArray.push(`${key}=${JSON.stringify(value)}`); // 否则将参数编码为JSON字符串
326
+ });
327
+ }
328
+ // 否则如果值是纯文本字符串,则将其编码为JSON字符串
329
+ else if (regexs.UNSAFE_PARAM_VALUE.test(pValue)) paramsArray.push(`${param}=${JSON.stringify(pValue)}`);
330
+ else paramsArray.push(`${param}=${pValue}`); // 否则,如果值是安全的,则将其编码为MIME字
331
+ });
332
+
333
+ return structured.value + (paramsArray.length ? `;${paramsArray.join('; ')}` : '');
334
+ };
335
+
336
+ /**
337
+ * 将带有key=value参数的头值解析为结构化对;
338
+ * >查看定义:@see {@link parseHeaderValue}
339
+ * - 示例:
340
+ * @example
341
+ * parseHeaderValue('content-type: text/plain; CHARSET='UTF-8'');
342
+ * // 解析结果:
343
+ * { 'value': 'text/plain', 'params': {'charset': 'UTF-8'} };
344
+ * @param {String} str 头值
345
+ * @return {Object} 头值作为解析后的结构
346
+ */
347
+ const parseHeaderValue = str => {
348
+ const result = { value: '', params: {} };
349
+ if (!str || typeof str !== 'string') return result; // 输入验证:如果输入为空或不是字符串,直接返回空结果
350
+
351
+ // 当前解析状态,正在解析的参数键名,正在解析的值,引号字符,是否处于转义状态
352
+ let state = 'value', currentKey = '', currentValue = '', quoteChar = '', escaped = false;
353
+ // 逐个字符遍历输入字符串
354
+ for (let i = 0; i < str.length; i++) {
355
+ const char = str[i];
356
+
357
+ // 处理转义字符:如果前一个字符是\,当前字符直接加入值,不进行特殊处理
358
+ if (escaped) {
359
+ currentValue += char, escaped = false;
360
+ continue;
361
+ }
362
+
363
+ // 根据当前状态处理字符
364
+ switch (state) {
365
+ case 'value': // 解析主值部分(如:text/plain)
366
+ // 如果遇到分号,保存主值(去除前后空格),重置当前值,切换到键解析状态
367
+ if (char === ';') result.value = currentValue.trim(), currentValue = '', state = 'key';
368
+ else if (char === '\\') escaped = true; // 遇到反斜杠设置转义为真
369
+ else currentValue += char; // 普通字符:直接添加到当前值
370
+ break;
371
+ case 'key': // 解析参数键名(如:charset)
372
+ if (char === '=') currentKey = currentValue.trim().toLowerCase(), currentValue = '', state = 'value';
373
+ else if (char === ';') {
374
+ // 遇到分号但没有等号如:;flag
375
+ if (currentValue.trim()) result.params[currentValue.trim().toLowerCase()] = ''; // 保存空值参数
376
+ currentValue = '';
377
+ }
378
+ else currentValue += char;
379
+ break;
380
+ case 'quoted': // 解析引号内的内容
381
+ if (char === quoteChar) state = 'value';
382
+ else if (char === '\\') escaped = true;
383
+ else currentValue += char;
384
+ break;
385
+ }
386
+
387
+ // 检查是否需要进入引号状态(不在转义状态下)
388
+ if (!escaped && (char === '"' || char === "'") && state !== 'quoted') quoteChar = char, state = 'quoted';
389
+ }
390
+
391
+ const finalValue = currentValue.trim(); // 循环结束后,处理最后剩余的值
392
+ // 如果最后为值状态且有值
393
+ if (state === 'value' && finalValue) {
394
+ if (!result.value && !currentKey) result.value = finalValue; // 如果主值还未设置且没有当前键,设置为主值
395
+ else if (currentKey) result.params[currentKey] = finalValue; // 如果有当前键,设置为参数值
396
+ }
397
+ // 否则,如果最后在为键状态且有值,设置为空参数
398
+ else if (state === 'key' && finalValue) result.params[finalValue.toLowerCase()] = '';
399
+
400
+ _processRFC2231Params(result.params); // 处理 RFC2231 编码的参数(用于邮件头编码)
401
+ return result;
402
+ };
403
+
404
+ /**
405
+ * 根据MIME类型检测文件扩展名;
406
+ * >查看定义:@see {@link detectExtension}
407
+ * @param {string} mimeType - 要检测的MIME类型
408
+ * @returns {string} 检测到的文件扩展名
409
+ */
410
+ const detectExtension = (mimeType = '') => {
411
+ // 如果MIME类型为空,返回默认扩展名
412
+ if (!mimeType) return defaultExtension;
413
+
414
+ // 处理MIME类型字符串:转换为小写,去除空格,按斜杠分割;获取主类型(如:application、text等)和子类型(处理包含斜杠的情况)
415
+ const parts = mimeType.toLowerCase().trim().split('/'), rootType = parts.shift().trim(), subType = parts.join('/').trim(),
416
+ fullMimeType = `${rootType}/${subType}`; // 重新构建标准化的MIME类型字符串并查找对应的扩展名
417
+ if (MIME_TYPES.has(fullMimeType)) {
418
+ const value = MIME_TYPES.get(fullMimeType);
419
+ return Array.isArray(value) ? value[0] : value; // 如果值是数组,返回第一个元素
420
+ }
421
+
422
+ // 根据主类型返回默认扩展名
423
+ switch (rootType) {
424
+ case 'text': return 'txt'; // 文本类型默认返回txt
425
+ default: return 'bin'; // 其他类型默认返回bin
426
+ }
427
+ };
428
+
429
+ /**
430
+ * 根据文件名检测MIME类型;
431
+ * >查看定义:@see {@link detectMimeType}
432
+ * @param {string} filename - 要检测的文件名
433
+ * @returns {string} 检测到的MIME类型
434
+ */
435
+ const detectMimeType = filename => {
436
+ if (!filename) return defaultMimeType; // 如果文件名为空,返回默认MIME类型
437
+
438
+ const { ext, name = '' } = parse(filename), // 解析文件路径获取扩展名
439
+ // 提取扩展名(去掉点号),处理查询参数,转换为小写
440
+ extension = (ext.substring(1) || name).split('?').shift().trim().toLowerCase(),
441
+
442
+ // 如果在扩展名映射表中找到对应项,则使用对应的MIME类型
443
+ value = EXTENSIONS.has(extension) ? EXTENSIONS.get(extension) : defaultMimeType;
444
+ return Array.isArray(value) ? value[0] : value; // 如果值是数组,返回第一个元素;否则返回值
445
+ };
446
+
447
+ /**
448
+ • 折叠长行,适用于折叠头行(afterSpace=false)和流文本(afterSpace=true);
449
+ * >查看定义:@see {@link foldLines}
450
+ • @param {String} str 要折叠的字符串
451
+ • @param {Number} [lineLength=76] 一行的最大长度
452
+ • @param {Boolean} afterSpace 如果为true,在行尾留一个空格
453
+ • @return {String} 带有折叠行的字符串
454
+ */
455
+ const foldLines = (str = '', lineLength = 76, afterSpace) => {
456
+ // 计算调整长度
457
+ const calculateAdjustment = (match, retainSpace) => {
458
+ return match[0].length - (retainSpace ? (match[1] || '').length : 0);
459
+ }
460
+
461
+ str = str.toString();
462
+ let pos = 0, result = '', line, match;
463
+ const len = str.length;
464
+ while (pos < len) {
465
+ line = str.substring(pos, pos + lineLength);
466
+ if (line.length < lineLength) {
467
+ result += line;
468
+ break;
469
+ }
470
+ if ((match = line.match(regexs.LINE_BREAK))) {
471
+ line = match[0], result += line, pos += line.length;
472
+ continue;
473
+ }
474
+ else if ((match = line.match(regexs.TRAILING_SPACES))) {
475
+ const adjustment = calculateAdjustment(match, afterSpace);
476
+ if (adjustment < line.length) line = line.substring(0, line.length - adjustment);
477
+ }
478
+ else if ((match = str.substring(pos + line.length).match(regexs.NEXT_WORD))) {
479
+ const adjustment = calculateAdjustment(match, !afterSpace);
480
+ line = line + match[0].substring(0, adjustment);
481
+ }
482
+
483
+ result += line, pos += line.length;
484
+ if (pos < len) result += '\r\n'
485
+ }
486
+
487
+ return result;
488
+ };
489
+
490
+ export {
491
+ isPlainText, hasLongerLines, encodeWord, encodeWords, buildHeaderValue, parseHeaderValue, detectExtension,
492
+ detectMimeType, foldLines
493
+ };