@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.
@@ -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
- // 解析内容方法(代理给 resolveContent)
31
+ // 解析内容方法(代理给 resolveContent)
32
32
  resolveContent(...args) {
33
33
  return resolveContent(...args);
34
34
  }
35
35
 
36
- // 解析所有内容(文本、HTML、附件等)
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'], [this.data, 'icalEvent']
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
- // 如果没有文件名,则从路径或href中提取,或使用默认名称
49
+ // 如果没有文件名,则从路径或href中提取,或使用默认名称
49
50
  if (!newFname) {
50
51
  newFname = (path || href).split('/').pop().split('?').shift() || `attachment-${(i + 1)}`;
51
- if (!newFname.includes('.')) newFname += `.${detectExtension(contentType)}`; // 如果没有扩展名,则根据内容类型添加
52
+ // 如果没有扩展名,则根据内容类型添加
53
+ if (!newFname.includes('.')) newFname += `.${detectExtension(contentType)}`;
52
54
  attachment.filename = newFname; // 设置回attachment对象
53
55
  }
54
56
 
55
- // 如果没有内容类型,则根据文件名、路径或href检测
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
- // 处理单值地址字段(from, sender);如果数据中有地址,则保存为地址对象
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) && !['content', 'path', 'href', 'raw'].includes(key)) node[key] = currentItem[key];
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
- const envelope = this.data.envelope || this.message.getEnvelope(), messageId = this.message.messageId(); // 获取信封信息
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和注释(List-*: <http://domain> (comment))
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
- // 如果包含@,则使用mailto协议,否则使用HTTP协议
225
+ // 如果包含@,则使用mailto协议,否则使用HTTP协议
222
226
  return regexs.EMAIL_CHECK.test(url) ? `<mailto:${url}>` : `<http://${url}>`;
223
227
  }
224
228
  }
@@ -45,8 +45,8 @@ const defaultMimeType = 'application/octet-stream', defaultExtension = 'bin',
45
45
  };
46
46
 
47
47
  /**
48
- * 检查值是否为纯文本字符串(仅使用可打印的7位字符)
49
- * 允许的控制字符:\x09(制表符)、\x0a(换行符)、\x0d(回车符)、可打印ASCII(\x20-\x7e)
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编码的字符串。用于将MIME字分成更小的块
63
+ * 拆分MIME编码的字符串;用于将MIME字分成更小的块
64
64
  *
65
65
  * @param {String} str 要拆分的MIME编码字符串
66
- * @param {Number} maxlen 一个部分的最大字符长度(最小12)
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参数值连续编码(rfc2231)
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
- // 否则,如果字符串包含Unicode或特殊字符,则将其拆分为多个块
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), encodedStrArr.push(chr), i++;
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; // 如果需要编码,则对当前字符进行URL编码
147
+ if (encoded) chr = encodedChr; // 如果需要编码,则对当前字符进行URL编码
147
148
  else {
148
149
  chr = chr === ' ' ? chr : encodedChr; // 尝试对当前字符进行URL编码
149
- // 默认情况下不需要编码行,只有当字符串包含Unicode或特殊字符时才需要
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 }), line = chr = encodedStr[i] === ' ' ? ' ' : _safeEncodeURIComponent(encodedStr[i]);
165
- // 如果添加的字符不需要编码,则将编码设置为false,并继续处理下一个字符;否则设为需要编码
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
- // 如果任何行需要编码,则第一行(part==0)总是编码的
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') params[actualKey] = { charset: false, values: [] };
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
- * 如果值是纯文本但有超过允许长度的行,则使用format=flowed
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字的编码,Q或B
235
- * @param {Number} [maxLength=0] 如果设置,根据需要将MIME字拆分为多个块
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,则使用qp编码,否则使用base64编码
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}`;// 如果字符是空格,则使用_;否则使用=XX
253
+ return chr === ' ' ? '_' : `=${XX}`;// 如果字符是空格,则使用_;否则使用=XX
251
254
  });
252
255
  }
253
- // 否则如果mimeWordEncoding为B,则使用base64编码
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字的编码,Q或B
289
- * @param {Number} [maxLength=0] 如果设置,根据需要将MIME字拆分为多个块
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, startStr = startIndex ? value.substring(0, startIndex) : '',
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
- * - 注意:我们遵循RFC 822来维护需要放在引号中的特殊字符列表;
311
- * 参考:https://www.w3.org/Protocols/rfc1341/4_Content-Type.html
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
- // 如果值是纯文本字符串,则将其编码为MIME字
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
- // 否则如果值是纯文本字符串,则将其编码为JSON字符串
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': // 解析主值部分(如:text/plain)
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': // 解析参数键名(如:charset)
375
+ case 'key': // 解析参数键名(如:charset)
372
376
  if (char === '=') currentKey = currentValue.trim().toLowerCase(), currentValue = '', state = 'value';
373
377
  else if (char === ';') {
374
- // 遇到分号但没有等号如:;flag
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类型字符串:转换为小写,去除空格,按斜杠分割;获取主类型(如:application、text等)和子类型(处理包含斜杠的情况)
415
- const parts = mimeType.toLowerCase().trim().split('/'), rootType = parts.shift().trim(), subType = parts.join('/').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; // 如果文件名为空,返回默认MIME类型
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
- // 如果在扩展名映射表中找到对应项,则使用对应的MIME类型
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', 'application/x-binary']],
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', 'application/x-java-class']],
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', 'application/x-javascript']],
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', 'audio/x-mid']],
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', 'application/x-mspowerpoint']],
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', 'text/x-script.scheme']],
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'],