@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/lib/base64.js CHANGED
@@ -25,9 +25,10 @@ const base64Encode = buffer => {
25
25
  let pos = 0;
26
26
  const result = [], chunkLength = lineLength * 1024;
27
27
 
28
- // 每行最多允许lineLength个字符,每1024行进行一次处理
28
+ // 每行最多允许lineLength个字符,每1024行进行一次处理
29
29
  while (pos < str.length) {
30
- const end = pos + chunkLength, wLines = str.substring(pos, end).replace(new RegExp(`.{${lineLength}}`, 'g'), '$&\r\n');
30
+ const end = pos + chunkLength,
31
+ wLines = str.substring(pos, end).replace(new RegExp(`.{${lineLength}}`, 'g'), '$&\r\n');
31
32
  result.push(wLines), pos = end;
32
33
  }
33
34
 
@@ -39,7 +40,7 @@ const base64Encode = buffer => {
39
40
  * >查看定义:@see {@link Base64Encoder}
40
41
  * @constructor
41
42
  * @param {Object} options 流选项
42
- * @param {Number} [options.lineLength=76] 行最大长度,设置为false可禁用换行
43
+ * @param {Number} [options.lineLength=76] 行最大长度,设置为false可禁用换行
43
44
  */
44
45
  class Base64Encoder extends Transform {
45
46
  constructor(options = {}) {
@@ -54,16 +55,16 @@ class Base64Encoder extends Transform {
54
55
  }
55
56
 
56
57
  _transform(chunk, encoding, done) {
57
- if (encoding !== 'buffer') chunk = Buffer.from(chunk, encoding); // 如果不是Buffer,则按指定编码转换为Buffer
58
- if (!chunk?.length) return setImmediate(done); // 如果没有数据,直接返回
58
+ if (encoding !== 'buffer') chunk = Buffer.from(chunk, encoding); // 如果不是Buffer,则按指定编码转换为Buffer
59
+ if (!chunk?.length) return setImmediate(done); // 如果没有数据,直接返回
59
60
 
60
61
  this.inputBytes += chunk.length;
61
- // 如果上次有剩余的字节,先补齐
62
+ // 如果上次有剩余的字节,先补齐
62
63
  const rLen = this._remainingBytes?.length ?? 0;
63
64
  if (rLen)
64
65
  chunk = Buffer.concat([this._remainingBytes, chunk], rLen + chunk.length), this._remainingBytes = false;
65
66
 
66
- // base64每3个字节编码为4个字符,如果不是3的倍数,则剩余的字节留到下次处理
67
+ // base64每3个字节编码为4个字符,如果不是3的倍数,则剩余的字节留到下次处理
67
68
  const remain = chunk.length % 3;
68
69
  if (remain) {
69
70
  const chunkEnd = chunk.length - remain;
package/lib/dkim/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * 用于邮件服务器或邮件发送系统,为外发邮件添加DKIM签名,
2
+ * 用于邮件服务器或邮件发送系统,为外发邮件添加DKIM签名,
3
3
  * 提高邮件的可信度和防伪造能力,帮助通过SPF、DKIM、DMARC等反垃圾邮件验证
4
4
  */
5
5
  import { unlink, createReadStream, createWriteStream } from 'fs';
@@ -64,7 +64,7 @@ class DKIMSigner {
64
64
 
65
65
  // 如果没有缓存文件则结束输出,否则创建读取缓存流
66
66
  if (this.readPos >= this.chunks.length) return !this.cache ? this.output.end() : this.createReadCache();
67
- // 写入数据,如果返回false则等待'drain'事件
67
+ // 写入数据,如果返回false则等待'drain'事件
68
68
  const chunk = this.chunks[this.readPos++];
69
69
  if (this.output.write(chunk) === false) return this.output.once('drain', () => this.sendNextChunk());
70
70
  setImmediate(() => this.sendNextChunk());
@@ -74,12 +74,13 @@ class DKIMSigner {
74
74
  sendSignedOutput() {
75
75
  let keyPos = 0;
76
76
  const signNextKey = () => {
77
- // 所有密钥都已处理,发送原始头部并继续发送数据块
77
+ // 所有密钥都已处理,发送原始头部并继续发送数据块
78
78
  if (keyPos >= this.keys.length) {
79
79
  this.output.write(this.parser.rawHeaders);
80
80
  return setImmediate(() => this.sendNextChunk());
81
81
  }
82
- const { domainName, keySelector, privateKey } = this.keys[keyPos++], { headerFieldNames, skipFields } = this.options,
82
+ const { domainName, keySelector, privateKey } = this.keys[keyPos++],
83
+ { headerFieldNames, skipFields } = this.options,
83
84
  // 定义 DKIM 签名字段
84
85
  dkimField = signer(this.headers, this.hashAlgo, this.bodyHash,
85
86
  { domainName, keySelector, privateKey, headerFieldNames, skipFields });
@@ -145,22 +146,22 @@ class DKIMSigner {
145
146
  * const dkim = new DKIM({
146
147
  * domainName: 'example.com', // 域名,用于标识签名来源
147
148
  * keySelector: 'key-selector', // 密钥选择器,用于在DNS中查找公钥
148
- * privateKey, // RSA 私钥,PEM 格式字符串(包含 -----BEGIN RSA PRIVATE KEY----- 等),用于生成 DKIM 签名
149
+ * privateKey, // RSA 私钥,PEM 格式字符串(包含 -----BEGIN RSA PRIVATE KEY----- 等),用于生成 DKIM 签名
149
150
  * cacheDir: '/tmp' // 缓存目录,用于处理大邮件时的临时存储
150
151
  * });
151
152
  *
152
153
  * // 2. 对邮件进行签名
153
154
  * dkim.sign(input).pipe(process.stdout);
154
155
  * ```
155
- * 参数说明:
156
- * input: 输入邮件内容,可以是以下类型:
157
- * Stream (流):可读流,包含RFC822格式的原始邮件
158
- * String (字符串):邮件内容的字符串形式
159
- * Buffer (缓冲区):邮件内容的二进制缓冲区
156
+ * 参数说明:
157
+ * input: 输入邮件内容,可以是以下类型:
158
+ * Stream (流):可读流,包含RFC822格式的原始邮件
159
+ * String (字符串):邮件内容的字符串形式
160
+ * Buffer (缓冲区):邮件内容的二进制缓冲区
160
161
  *
161
- * - 返回值:一个可读流,包含已添加DKIM签名的完整邮件
162
+ * - 返回值:一个可读流,包含已添加DKIM签名的完整邮件
162
163
  *
163
- * 工作流程:
164
+ * 工作流程:
164
165
  * 1. 解析输入邮件的头部和正文
165
166
  * 2. 对邮件正文进行规范化处理并计算哈希值
166
167
  * 3. 使用私钥对指定邮件头字段和正文哈希进行签名
@@ -180,14 +181,14 @@ class DKIM {
180
181
  const output = new PassThrough();
181
182
  let inputStream = input, writeValue = false;
182
183
 
183
- // 如果输入是字符串或Buffer,则转换为流
184
+ // 如果输入是字符串或Buffer,则转换为流
184
185
  if (Buffer.isBuffer(input)) writeValue = input, inputStream = new PassThrough();
185
- // 否则如果输入是字符串,则转换为Buffer
186
+ // 否则如果输入是字符串,则转换为Buffer
186
187
  else if (typeof input === 'string') writeValue = Buffer.from(input), inputStream = new PassThrough();
187
188
 
188
189
  const options = this.options;
189
190
  let newOptions = {};
190
- // 如果有额外选项,则合并选项
191
+ // 如果有额外选项,则合并选项
191
192
  if (extraOptions && Object.keys(extraOptions).length) {
192
193
  newOptions = { ...options };
193
194
  // 仅添加不存在的选项
@@ -197,11 +198,13 @@ class DKIM {
197
198
  }
198
199
  else newOptions = options;
199
200
 
200
- const signer = new DKIMSigner(newOptions, this.keys, inputStream, output); // 创建 DKIM 签名器实例,使用最终的合并选项
201
+ // 创建 DKIM 签名器实例,使用最终的合并选项
202
+ const signer = new DKIMSigner(newOptions, this.keys, inputStream, output);
201
203
  // 异步启动签名流处理
202
204
  setImmediate(() => {
203
205
  signer.signStream();
204
- if (writeValue) setImmediate(() => inputStream.end(writeValue)); // 如果有写入值,则在下一个事件循环中结束输入流
206
+ // 如果有写入值,则在下一个事件循环中结束输入流
207
+ if (writeValue) setImmediate(() => inputStream.end(writeValue));
205
208
  });
206
209
 
207
210
  return output;
@@ -29,7 +29,7 @@ class MessageParser extends Transform {
29
29
 
30
30
  // 在流结束时处理任何剩余的头信息
31
31
  _flush(callback) {
32
- // 如果还有未处理的头信息块,则推送它们
32
+ // 如果还有未处理的头信息块,则推送它们
33
33
  if (this.headerChunks) {
34
34
  const chunk = Buffer.concat(this.headerChunks, this.headerBytes);
35
35
  this.bodySize += chunk.length, this.push(chunk), this.headerChunks = null;
@@ -38,7 +38,7 @@ class MessageParser extends Transform {
38
38
  }
39
39
 
40
40
  /**
41
- * 更新记录最后4个字节,以便在块边界检测换行符
41
+ * 更新记录最后4个字节,以便在块边界检测换行符
42
42
  *
43
43
  * @param {Buffer} data 来自流的下一个数据块
44
44
  */
@@ -52,27 +52,29 @@ class MessageParser extends Transform {
52
52
  }
53
53
 
54
54
  /**
55
- * 从剩余的消息体中查找并移除消息头。将头信息保持分离,直到最终投递,以便修改
55
+ * 从剩余的消息体中查找并移除消息头;将头信息保持分离,直到最终投递,以便修改
56
56
  *
57
57
  * @param {Buffer} data 下一个数据块
58
- * @return {Boolean} 如果已经找到头信息则返回true,否则返回false
58
+ * @return {Boolean} 如果已经找到头信息则返回true,否则返回false
59
59
  */
60
60
  checkHeaders(data) {
61
- if (this.headersParsed) return true; // 如果已经找到头信息,则不需要检查
61
+ if (this.headersParsed) return true; // 如果已经找到头信息,则不需要检查
62
62
 
63
63
  const lblen = this.lastBytes.length;
64
64
  let headerPos = 0;
65
65
  this.curLinePos = 0;
66
- // 检查是否有两个连续的换行符,表示头信息的结尾
66
+ // 检查是否有两个连续的换行符,表示头信息的结尾
67
67
  for (let i = 0; i < lblen + data.length; i++) {
68
- const chr = i < lblen ? this.lastBytes[i] : data[i - lblen]; // 如果小于长度,使用lastBytes中的字节,否则使用data中的字节
68
+ // 如果小于长度,使用lastBytes中的字节,否则使用data中的字节
69
+ const chr = i < lblen ? this.lastBytes[i] : data[i - lblen];
69
70
  // 检查是否有两个连续的换行符
70
71
  if (chr === 0x0a && i) {
71
- const pr1 = i - 1 < lblen ? this.lastBytes[i - 1] : data[i - 1 - lblen], // 定义pr1为当前字节的前一个字节的值
72
+ // 定义pr1为当前字节的前一个字节的值
73
+ const pr1 = i - 1 < lblen ? this.lastBytes[i - 1] : data[i - 1 - lblen],
72
74
  // 定义pr2为当前字节的前两个字节的值(如果存在)
73
75
  pr2 = i > 1 ? (i - 2 < lblen ? this.lastBytes[i - 2] : data[i - 2 - lblen]) : false;
74
76
 
75
- // 如果前一个字符是0x0a,或者前两个字符是0x0d 0x0a,则表示头信息结束
77
+ // 如果前一个字符是0x0a,或者前两个字符是0x0d 0x0a,则表示头信息结束
76
78
  if (pr1 === 0x0a || (pr1 === 0x0d && pr2 === 0x0a)) {
77
79
  this.headersParsed = true, headerPos = i - lblen + 1, this.headerBytes += headerPos;
78
80
  break;
@@ -82,9 +84,10 @@ class MessageParser extends Transform {
82
84
 
83
85
  // 如果找到了头信息
84
86
  if (this.headersParsed) {
85
- this.headerChunks.push(data.subarray(0, headerPos)), this.rawHeaders = Buffer.concat(this.headerChunks, this.headerBytes);
87
+ this.headerChunks.push(data.subarray(0, headerPos)),
88
+ this.rawHeaders = Buffer.concat(this.headerChunks, this.headerBytes);
86
89
  this.headerChunks = null, this.emit('headers', this.parseHeaders());
87
- // 如果数据块中还有剩余的消息体,则推送剩余的消息体
90
+ // 如果数据块中还有剩余的消息体,则推送剩余的消息体
88
91
  if (data.length - 1 > headerPos) {
89
92
  const chunk = data.subarray(headerPos);
90
93
  this.bodySize += chunk.length, setImmediate(() => this.push(chunk));
@@ -103,7 +106,7 @@ class MessageParser extends Transform {
103
106
  const lines = (this.rawHeaders || '').toString().split(regexs.LINE_SEPARATOR); // 按行拆分(丢弃换行符)
104
107
  // 处理折叠的头信息行
105
108
  for (let i = lines.length - 1; i > 0; i--)
106
- // 如果当前行是空白行,则将其与前一行合并
109
+ // 如果当前行是空白行,则将其与前一行合并
107
110
  if (regexs.FOLDED_HEADER_LINE.test(lines[i])) lines[i - 1] += `\n${lines[i]}`, lines.splice(i, 1);
108
111
 
109
112
  return lines.filter(line => line.trim())
@@ -4,7 +4,7 @@ import { regexs, resetRegex } from '../regexs.js';
4
4
 
5
5
  /**
6
6
  * 宽松规范化邮件正文的转换流,实现DKIM签名中的relaxed body canonicalization
7
- * - 主要功能:
7
+ * - 主要功能:
8
8
  * 1. 将所有的行结束符统一为CRLF
9
9
  * 2. 移除每行末尾的空白字符(WSP)
10
10
  * 3. 将行内的多个空白字符替换为单个空格
@@ -15,7 +15,7 @@ class RelaxedBody extends Transform {
15
15
  /**
16
16
  * 构造函数
17
17
  * @param {Object} options - 配置选项
18
- * @param {string} options.hashAlgo - 哈希算法,默认为'sha1'
18
+ * @param {string} options.hashAlgo - 哈希算法,默认为'sha1'
19
19
  * @param {boolean} options.debug - 是否启用调试模式
20
20
  */
21
21
  constructor(options = {}) {
@@ -28,7 +28,7 @@ class RelaxedBody extends Transform {
28
28
 
29
29
  /**
30
30
  * 处理数据块并更新哈希值
31
- * 应用宽松规范化规则:
31
+ * 应用宽松规范化规则:
32
32
  * 1. 将所有行结束符统一为CRLF
33
33
  * 2. 移除行尾空白字符(WSP)
34
34
  * 3. 将行内多个空白字符替换为单个空格
@@ -37,15 +37,16 @@ class RelaxedBody extends Transform {
37
37
  updateHash(chunk) {
38
38
  // 将当前数据块转换为字符串并与之前的余数合并
39
39
  const fullStr = this.remainder + chunk.toString('latin1');
40
- if (fullStr.length === 0) return; // 如果合并后字符串为空,无需处理
41
- this.remainder = ''; // 清空余数,将在处理过程中重新收集
42
- const lines = fullStr.split(regexs.SPLIT_KEEP_NEWLINE); // 使用正则表达式按行分割,保留行结束符信息
43
- if (!regexs.TRAILING_LINE_BREAK.test(fullStr)) this.remainder = lines.pop() || ''; // 如最后不是完整的行结束符,保留作为余数
40
+ if (fullStr.length === 0) return; // 如果合并后字符串为空,无需处理
41
+ this.remainder = ''; // 清空余数,将在处理过程中重新收集
42
+ const lines = fullStr.split(regexs.SPLIT_KEEP_NEWLINE); // 使用正则表达式按行分割,保留行结束符信息
43
+ // 如最后不是完整的行结束符,保留作为余数
44
+ if (!regexs.TRAILING_LINE_BREAK.test(fullStr)) this.remainder = lines.pop() || '';
44
45
 
45
- // 处理每一行内容(跳过行结束符部分)
46
+ // 处理每一行内容(跳过行结束符部分)
46
47
  const processedLines = [];
47
48
  for (let i = 0; i < lines.length; i++) {
48
- // 如果是行结束符,直接添加
49
+ // 如果是行结束符,直接添加
49
50
  if (i % 2 === 1) {
50
51
  processedLines.push(lines[i]);
51
52
  continue;
@@ -61,9 +62,9 @@ class RelaxedBody extends Transform {
61
62
 
62
63
  // 重新组装处理后的内容
63
64
  let processedStr = processedLines.join('');
64
- // 如果有余数,需要特殊处理(可能是不完整的行)
65
+ // 如果有余数,需要特殊处理(可能是不完整的行)
65
66
  if (this.remainder) {
66
- // 余数中的空白字符处理(但不移除行尾空白,因为可能不是完整行)
67
+ // 余数中的空白字符处理(但不移除行尾空白,因为可能不是完整行)
67
68
  const rMW = resetRegex(regexs.MULTIPLE_WSP), pRemainder = this.remainder.replace(rMW, ' ');
68
69
  processedStr += pRemainder;
69
70
  }
@@ -75,14 +76,14 @@ class RelaxedBody extends Transform {
75
76
  }
76
77
 
77
78
  /**
78
- * 转换流的核心方法,处理输入数据
79
+ * 转换流的核心方法,处理输入数据
79
80
  * @param {Buffer|string} chunk - 输入数据块
80
81
  * @param {string} encoding - 编码方式
81
82
  * @param {function} callback - 完成回调
82
83
  */
83
84
  _transform(chunk, encoding, callback) {
84
85
  try {
85
- // 如果是空块,更新哈希并完成
86
+ // 如果是空块,更新哈希并完成
86
87
  if (!chunk?.length) {
87
88
  this.updateHash(Buffer.alloc(0));
88
89
  return callback();
@@ -110,12 +111,12 @@ class RelaxedBody extends Transform {
110
111
  pRemainder = this.remainder.replace(regexs.TRAILING_WSP, '').replace(rMW, ' '),
111
112
  remainderChunk = Buffer.from(pRemainder, 'latin1');
112
113
 
113
- if (this.debug) this._debugBody.push(remainderChunk); // 调试模式:记录处理后的余数
114
+ if (this.debug) this._debugBody.push(remainderChunk); // 调试模式:记录处理后的余数
114
115
  // 更新哈希值,将处理后的余数推送到输出流,然后更新已处理的字节长
115
116
  this.bodyHash.update(remainderChunk); this.push(remainderChunk); this.byteLength += remainderChunk.length;
116
117
  }
117
118
 
118
- // 确保以CRLF结束(如果消息体为空,或者余数不以CRLF结束)
119
+ // 确保以CRLF结束(如果消息体为空,或者余数不以CRLF结束)
119
120
  if (this.byteLength === 0 || !this.remainder?.endsWith('\r\n')) {
120
121
  const crlf = Buffer.from('\r\n');
121
122
  this.bodyHash.update(crlf), this.push(crlf);
package/lib/dkim/sign.js CHANGED
@@ -1,11 +1,11 @@
1
1
  // 引入必要的模块
2
- import { createSign } from 'crypto'; // Node.js加密模块,用于生成签名
2
+ import { createSign } from 'crypto'; // Node.js加密模块,用于生成签名
3
3
  import { toASCII } from '../punycode.js'; // 用于处理Unicode域名的punycode转换
4
4
  import { foldLines } from '../mime-funcs/index.js'; // 提供MIME相关工具函数
5
5
  import { regexs, resetRegex } from '../regexs.js';
6
6
 
7
7
  /**
8
- * 生成DKIM签名头(不含签名值部分)
8
+ * 生成DKIM签名头(不含签名值部分)
9
9
  *
10
10
  * @param {String} domainName 域名
11
11
  * @param {String} keySelector 密钥选择器
@@ -19,8 +19,8 @@ const _generateDKIMHeader = (domainName, keySelector, fieldNames, hashAlgo, body
19
19
  let dkim = [
20
20
  'v=1', // 版本
21
21
  `a=rsa-${hashAlgo}`, // 算法
22
- 'c=relaxed/relaxed', // 规范化方法(头/正文)
23
- `d=${toASCII(domainName)}`, // 域名(转换为ASCII)
22
+ 'c=relaxed/relaxed', // 规范化方法(头/正文)
23
+ `d=${toASCII(domainName)}`, // 域名(转换为ASCII)
24
24
  'q=dns/txt', // 查询方式
25
25
  `s=${keySelector}`, // 密钥选择器
26
26
  `bh=${bodyHash}`, // 正文哈希
@@ -34,8 +34,8 @@ const _generateDKIMHeader = (domainName, keySelector, fieldNames, hashAlgo, body
34
34
  * 从邮件头中提取并规范化指定字段的数据
35
35
  *
36
36
  * @param {Array} headers 邮件头数组
37
- * @param {String} fieldNames 需要处理的字段列表(冒号分隔)
38
- * @param {String} skipFields 需要跳过的字段列表(冒号分隔)
37
+ * @param {String} fieldNames 需要处理的字段列表(冒号分隔)
38
+ * @param {String} skipFields 需要跳过的字段列表(冒号分隔)
39
39
  * @return {Object} 包含规范化后的头数据和字段列表的对象
40
40
  */
41
41
  _relaxedHeaders = (headers, fieldNames = '', skipFields = '') => {
@@ -45,21 +45,21 @@ const _generateDKIMHeader = (domainName, keySelector, fieldNames, hashAlgo, body
45
45
  // 处理需要跳过的字段列表(去除空白并添加到跳过集合)
46
46
  skipFields.toLowerCase().split(':').forEach(field => skip.add(field.trim()));
47
47
 
48
- // 处理需要包含的字段,过滤掉跳过的字段(添加到包含字段集合)
48
+ // 处理需要包含的字段,过滤掉跳过的字段(添加到包含字段集合)
49
49
  fieldNames.toLowerCase().split(':').filter(field => !skip.has(field.trim()))
50
50
  .forEach(field => includedFields.add(field.trim()));
51
51
 
52
- // 从后向前遍历头(确保获取每个字段的最后出现值)
52
+ // 从后向前遍历头(确保获取每个字段的最后出现值)
53
53
  for (let i = headers.length - 1; i >= 0; i--) {
54
54
  const { key, line } = headers[i];
55
- // 只包含第一个遇到的值(从底部到顶部)
55
+ // 只包含第一个遇到的值(从底部到顶部)
56
56
  if (includedFields.has(key) && !headerFields.has(key)) headerFields.set(key, _relaxedHeaderLine(line));
57
57
  }
58
58
 
59
59
  // 构建规范化后的头数据行和字段列表
60
60
  const headersList = [], fields = [];
61
61
  includedFields.forEach(field => {
62
- // 如果字段在头中存在,则将其添加到规范化数据中
62
+ // 如果字段在头中存在,则将其添加到规范化数据中
63
63
  if (headerFields.has(field)) fields.push(field), headersList.push(`${field}:${headerFields.get(field)}`);
64
64
  });
65
65
 
@@ -103,12 +103,13 @@ const generateDKIMSignature = (headers, hashAlgo, bodyHash, options = {}) => {
103
103
  fieldNames = headerFieldNames || defaultFieldNames, // 使用选项中的头字段列表或默认列表
104
104
  result = _relaxedHeaders(headers, fieldNames, skipFields), // 获取经过"relaxed"规范化的头数据及字段列表
105
105
 
106
- // 生成DKIM签名头(不包含签名值部分)
106
+ // 生成DKIM签名头(不包含签名值部分)
107
107
  dkimHeader = _generateDKIMHeader(domainName, keySelector, result.fieldNames, hashAlgo, bodyHash);
108
108
 
109
109
  let signer, signature;
110
- result.headers += `dkim-signature:${_relaxedHeaderLine(dkimHeader)}`; // 将DKIM签名头自身添加到规范化数据中(用于生成签名)
111
- // 创建签名对象,使用指定的哈希算法;更新要签名的数据(规范化后的头数据)
110
+ // 将DKIM签名头自身添加到规范化数据中(用于生成签名)
111
+ result.headers += `dkim-signature:${_relaxedHeaderLine(dkimHeader)}`;
112
+ // 创建签名对象,使用指定的哈希算法;更新要签名的数据(规范化后的头数据)
112
113
  signer = createSign((`rsa-${hashAlgo}`).toUpperCase()), signer.update(result.headers);
113
114
  try {
114
115
  signature = signer.sign(privateKey, 'base64'); // 使用私钥进行签名,输出Base64格式
@@ -1,5 +1,5 @@
1
1
  // Cookie处理模块
2
- const SESSION_TIMEOUT = 1800; // 会话默认超时时间:30分钟(1800秒)
2
+ const SESSION_TIMEOUT = 1800; // 会话默认超时时间:30分钟(1800秒)
3
3
 
4
4
  /**
5
5
  * 创建一个Biskviit Cookie存储容器,用于内存中的Cookie管理
@@ -15,14 +15,14 @@ class Cookies {
15
15
  // 检查主机名是否匹配Cookie域名
16
16
  #isDomainMatch(hostname, domain) {
17
17
  if (hostname === domain) return true; // 精确匹配
18
- // 如果域名以点开头,则匹配所有子域名,否则返回false
18
+ // 如果域名以点开头,则匹配所有子域名,否则返回false
19
19
  return domain.startsWith('.') ? (`.${hostname}`).endsWith(domain) : false;
20
20
  }
21
21
 
22
22
  /**
23
23
  * 尝试将给定的字符串解析为URL对象
24
- * @param {string} str - 需要解析的URL字符串,默认为空字符串
25
- * @returns {URL|boolean} 如果解析成功返回URL对象,否则返回false
24
+ * @param {string} str - 需要解析的URL字符串,默认为空字符串
25
+ * @returns {URL|boolean} 如果解析成功返回URL对象,否则返回false
26
26
  */
27
27
  #newURL(str = '') {
28
28
  try {
@@ -45,21 +45,22 @@ class Cookies {
45
45
  const cookie = this.parse(cookieStr), // 解析Cookie字符串
46
46
  { hostname, pathname } = urlObj, { domain, path, expires } = cookie;
47
47
 
48
- // 当域名不存在时或域名存在但不匹配时,直接使用主机名;否则当域名存在且匹配时,保留原始值
48
+ // 当域名不存在时或域名存在但不匹配时,直接使用主机名;否则当域名存在且匹配时,保留原始值
49
49
  cookie.domain = !domain || !this.#isDomainMatch(hostname, domain) ? hostname : domain;
50
50
 
51
- if (!path) cookie.path = this.getPath(pathname); // 如果没有设置路径,使用URL的路径
52
- // 如果没有过期时间,使用会话超时时间
53
- if (!expires) cookie.expires = new Date(Date.now() + Number(this.options.sessionTimeout ?? SESSION_TIMEOUT) * 1000);
51
+ if (!path) cookie.path = this.getPath(pathname); // 如果没有设置路径,使用URL的路径
52
+ // 如果没有过期时间,使用会话超时时间
53
+ if (!expires) cookie.expires =
54
+ new Date(Date.now() + Number(this.options.sessionTimeout ?? SESSION_TIMEOUT) * 1000);
54
55
 
55
56
  return this.add(cookie); // 添加Cookie到存储
56
57
  }
57
58
 
58
59
  /**
59
- * 获取适用于指定URL的Cookie字符串,用于'Cookie:'请求头
60
+ * 获取适用于指定URL的Cookie字符串,用于'Cookie:'请求头
60
61
  *
61
62
  * @param {String} url 要检查的URL
62
- * @returns {String} Cookie头字符串,如果没有匹配则返回空字符串
63
+ * @returns {String} Cookie头字符串,如果没有匹配则返回空字符串
63
64
  */
64
65
  get(url) {
65
66
  // 将Cookie对象转换为name=value格式,用分号和空格连接所有Cookie
@@ -76,7 +77,7 @@ class Cookies {
76
77
  const result = [];
77
78
  let cookie;
78
79
 
79
- // 倒序遍历Cookie数组(便于删除过期Cookie)
80
+ // 倒序遍历Cookie数组(便于删除过期Cookie)
80
81
  for (let i = this.cookies.length - 1; i >= 0; i--) {
81
82
  cookie = this.cookies[i];
82
83
 
@@ -118,11 +119,11 @@ class Cookies {
118
119
  break;
119
120
  case 'domain': // 域名
120
121
  domain = value.toLowerCase();
121
- // 如果域名存在且没有前导点,则添加前导点
122
+ // 如果域名存在且没有前导点,则添加前导点
122
123
  if (domain.length && !domain.startsWith('.')) domain = `.${domain}`;
123
124
  cookie.domain = domain;
124
125
  break;
125
- case 'max-age': // 最大存活时间(秒)
126
+ case 'max-age': // 最大存活时间(秒)
126
127
  cookie.expires = new Date(Date.now() + (Number(value) || 0) * 1000);
127
128
  break;
128
129
  case 'secure': // 安全标志
@@ -132,7 +133,7 @@ class Cookies {
132
133
  cookie.httponly = true;
133
134
  break;
134
135
  default:
135
- // 如果没有匹配的属性,则默认处理Cookie的名称和值
136
+ // 如果没有匹配的属性,则默认处理Cookie的名称和值
136
137
  if (!cookie.name) cookie.name = key, cookie.value = value;
137
138
  }
138
139
  });
@@ -153,22 +154,23 @@ class Cookies {
153
154
  if (!this.#isDomainMatch(urlObj.hostname, cookie.domain)) return false; // 检查主机名是否不匹配Cookie域名
154
155
 
155
156
  const path = this.getPath(urlObj.pathname); // 检查路径是否匹配
156
- if (!path.startsWith(cookie.path)) return false; // 如果路径不以Cookie路径开头,则不匹配
157
- return cookie.secure && urlObj.protocol !== 'https:' ? false : true; // 如果Cookie是安全的但URL不是https,则不匹配,否则匹配
157
+ if (!path.startsWith(cookie.path)) return false; // 如果路径不以Cookie路径开头,则不匹配
158
+ // 如果Cookie是安全的但URL不是https,则不匹配,否则匹配
159
+ return cookie.secure && urlObj.protocol !== 'https:' ? false : true;
158
160
  }
159
161
 
160
162
  /**
161
- * 添加(或更新/删除)Cookie对象到Cookie存储
163
+ * 添加(或更新/删除)Cookie对象到Cookie存储
162
164
  *
163
165
  * @param {Object} cookie 要存储的Cookie值
164
166
  */
165
167
  add(cookie) {
166
168
  if (!cookie || !cookie.name) return false; // 如果Cookie无效则直接返回
167
169
 
168
- // 检查是否有相同参数的Cookie(用于更新)
170
+ // 检查是否有相同参数的Cookie(用于更新)
169
171
  for (let i = 0; i < this.cookies.length; i++) {
170
172
  if (this.compare(this.cookies[i], cookie)) {
171
- // 检查是否需要删除Cookie(如果已过期)
173
+ // 检查是否需要删除Cookie(如果已过期)
172
174
  if (this.isExpired(cookie)) {
173
175
  this.cookies.splice(i, 1);
174
176
  return false;
@@ -179,7 +181,7 @@ class Cookies {
179
181
  }
180
182
  }
181
183
 
182
- if (!this.isExpired(cookie)) this.cookies.push(cookie); // 如果没有过期,则添加为新Cookie
184
+ if (!this.isExpired(cookie)) this.cookies.push(cookie); // 如果没有过期,则添加为新Cookie
183
185
  return true;
184
186
  }
185
187
 
@@ -211,9 +213,9 @@ class Cookies {
211
213
  * @returns {String} 规范化后的路径
212
214
  */
213
215
  getPath(pathname) {
214
- let path = pathname || '/'; // 处理空值,默认为根路径
216
+ let path = pathname || '/'; // 处理空值,默认为根路径
215
217
 
216
- // 直接找到最后一个斜杠的位置,保留到该位置(包含斜杠)
218
+ // 直接找到最后一个斜杠的位置,保留到该位置(包含斜杠)
217
219
  const lastIndex = path.lastIndexOf('/');
218
220
  path = path.substring(0, lastIndex + 1);
219
221
 
@@ -35,7 +35,7 @@ const MAX_REDIRECTS = 5, // 最大重定向次数常量
35
35
  let processedBody = null;
36
36
  const { contentType, body, headers } = options;
37
37
 
38
- // 设置内容类型(除非明确禁止)
38
+ // 设置内容类型(除非明确禁止)
39
39
  if (contentType !== false) headers['Content-Type'] = contentType || 'application/x-www-form-urlencoded';
40
40
 
41
41
  // 流式数据处理
@@ -76,7 +76,7 @@ const MAX_REDIRECTS = 5, // 最大重定向次数常量
76
76
  return true;
77
77
  }
78
78
 
79
- // 准备重定向请求(GET方法,无请求体)
79
+ // 准备重定向请求(GET方法,无请求体)
80
80
  options.method = 'GET', options.body = false;
81
81
 
82
82
  try {
@@ -94,23 +94,26 @@ const MAX_REDIRECTS = 5, // 最大重定向次数常量
94
94
  * >查看定义:@see {@link nmfetch}
95
95
  * @param {string} url - 请求的目标 URL;
96
96
  * @param {Object} [options={}] - 可选配置对象;
97
- * @param {stream.Writable} [options.fetchRes] - 用于写入响应数据的可写流,默认创建一个 PassThrough 流;
97
+ * @param {stream.Writable} [options.fetchRes] - 用于写入响应数据的可写流,默认创建一个 PassThrough 流;
98
98
  * @param {Cookies|Array|string} [options.cookies] - Cookie 管理器实例、Cookie 字符串数组或单个 Cookie 字符串,
99
99
  * 用于自动管理请求和响应的 Cookie;
100
- * @param {number} [options.redirects=0] - 已发生的重定向次数(内部使用);
101
- * @param {number} [options.maxRedirects] - 允许的最大重定向次数,默认为 MAX_REDIRECTS 常量;
102
- * @param {string} [options.method=''] - HTTP 请求方法(如 'GET'、'POST' 等),不提供时若存在请求体则默认为 'POST',否则为 'GET';
100
+ * @param {number} [options.redirects=0] - 已发生的重定向次数(内部使用);
101
+ * @param {number} [options.maxRedirects] - 允许的最大重定向次数,默认为 MAX_REDIRECTS 常量;
102
+ * @param {string} [options.method=''] - HTTP 请求方法(如 'GET'、'POST' 等),不提供时若存在请求体则默认为 'POST',
103
+ *否则为 'GET';
103
104
  * @param {Object} [options.headers] - 自定义请求头对象,键名会转为小写;
104
105
  * @param {string} [options.userAgent] - 自定义 User-Agent,会覆盖默认值;
105
- * @param {Object} [options.tls] - TLS/SSL 相关选项,直接传递给底层请求的 tls 配置;
106
- * @param {number} [options.timeout] - 请求超时时间(毫秒),超时后触发错误并销毁请求;
107
- * @param {*} [options.body] - 请求体内容,可以是字符串、Buffer 或可读流;若存在且 method 未指定,则方法自动设为 'POST';
106
+ * @param {Object} [options.tls] - TLS/SSL 相关选项,直接传递给底层请求的 tls 配置;
107
+ * @param {number} [options.timeout] - 请求超时时间(毫秒),超时后触发错误并销毁请求;
108
+ * @param {*} [options.body] - 请求体内容,可以是字符串、Buffer 或可读流;若存在且 method 未指定,则方法自动设为 'POST';
108
109
  * @param {boolean} [options.allowErrorResponse=false] - 是否允许状态码 ≥300 的响应被视为成功,若为 false 则触发错误;
109
110
  *
110
- * @returns {stream.Writable} 返回用于接收响应数据的可写流(通常为 PassThrough 实例),可通过监听 'finish' 或 'error' 事件获取结果;
111
+ * @returns {stream.Writable} 返回用于接收响应数据的可写流(通常为 PassThrough 实例),
112
+ *可通过监听 'finish' 或 'error' 事件获取结果;
111
113
  */
112
114
  const nmfetch = (url, options = {}) => {
113
- const { fetchRes: fetchRes_, cookies, redirects = 0, maxRedirects, method = '', headers: headers_, userAgent, } = options,
115
+ const { fetchRes: fetchRes_, cookies, redirects = 0, maxRedirects,
116
+ method = '', headers: headers_, userAgent, } = options,
114
117
  // 设置默认响应流、Cookie管理器和重定向计数器
115
118
  fetchRes = fetchRes_ || new PassThrough(), newCookies = cookies || new Cookies();
116
119
  options.redirects = redirects, options.maxRedirects = isNaN(maxRedirects) ? MAX_REDIRECTS : maxRedirects;
@@ -123,7 +126,7 @@ const nmfetch = (url, options = {}) => {
123
126
  if (!parsedResult.success) return fetchRes;
124
127
  const { protocol, hostname: host, pathname, search = '', port, username, password } = parsedResult.result;
125
128
 
126
- // 确定HTTP方法,默认为GET
129
+ // 确定HTTP方法,默认为GET
127
130
  let newMethod = method.toString().trim().toUpperCase() || 'GET';
128
131
  const handler = protocol === 'https:' ? https : http,
129
132
  // 设置默认请求头
@@ -132,8 +135,8 @@ const nmfetch = (url, options = {}) => {
132
135
  // 合并用户自定义头部
133
136
  Object.keys(headers_ || {}).forEach(key => headers[key.toLowerCase().trim()] = headers_[key]);
134
137
 
135
- if (userAgent) headers['user-agent'] = userAgent; // 设置自定义User-Agent(如果提供)
136
- // 如果URL中包含认证信息,则设置Authorization头
138
+ if (userAgent) headers['user-agent'] = userAgent; // 设置自定义User-Agent(如果提供)
139
+ // 如果URL中包含认证信息,则设置Authorization头
137
140
  if (username || password) {
138
141
  const auth = `${username}:${password}`;
139
142
  headers.Authorization = `Basic ${Buffer.from(auth).toString('base64')}`;
@@ -187,7 +190,7 @@ const nmfetch = (url, options = {}) => {
187
190
  break;
188
191
  }
189
192
 
190
- // 如果响应中的Set-Cookie头为真,则存储Cookie
193
+ // 如果响应中的Set-Cookie头为真,则存储Cookie
191
194
  const setCookie = res.headers['set-cookie'];
192
195
  if (setCookie) [].concat(setCookie).forEach(cookie => newCookies.set(cookie, url));
193
196
  if (_handleRedirect(res, url, options, fetchRes, finishedFlag, req)) return; // 处理重定向
@@ -204,7 +207,7 @@ const nmfetch = (url, options = {}) => {
204
207
  _markFinishedWithError(finishedFlag, fetchRes, err, url), req.destroy();
205
208
  });
206
209
 
207
- // 将响应流pipe到输出流,支持解压
210
+ // 将响应流pipe到输出流,支持解压
208
211
  if (inflate) {
209
212
  res.pipe(inflate).pipe(fetchRes);
210
213
  inflate.on('error', err => {
@@ -214,7 +217,7 @@ const nmfetch = (url, options = {}) => {
214
217
  else res.pipe(fetchRes);
215
218
  });
216
219
 
217
- // 发送请求体(如果有)
220
+ // 发送请求体(如果有)
218
221
  setImmediate(() => {
219
222
  const body = bodyResult.body;
220
223
  if (body) {