@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.
@@ -9,7 +9,7 @@ class Tokenizer {
9
9
  constructor(str = '') {
10
10
  this.str = str.toString(), this.operatorExpecting = '', this.node = null, this.escaped = false, this.list = [];
11
11
 
12
- // 操作符映射:开始操作符到期望的结束操作符(引号,注释,邮件地址,地址分隔符,组定义开始,组定义结束)
12
+ // 操作符映射:开始操作符到期望的结束操作符(引号,注释,邮件地址,地址分隔符,组定义开始,组定义结束)
13
13
  this.operators = { '"': '"', '(': ')', '<': '>', ',': '', ':': ';', ';': '' };
14
14
  }
15
15
 
@@ -35,9 +35,9 @@ class Tokenizer {
35
35
  }
36
36
 
37
37
  /**
38
- * 处理单个字符,识别操作符或文本内容
38
+ * 处理单个字符,识别操作符或文本内容
39
39
  * @param {String} chr 当前字符
40
- * @param {String} nextChr 下一个字符(用于前瞻)
40
+ * @param {String} nextChr 下一个字符(用于前瞻)
41
41
  */
42
42
  #processChar(chr, nextChr) {
43
43
  const { escaped, operatorExpecting: oExpecting, operators } = this;
@@ -47,7 +47,7 @@ class Tokenizer {
47
47
  // 检查是否是新的开始操作符
48
48
  else if (!escaped && !oExpecting && chr in operators) return this.#addOperatorToken(chr, nextChr, operators[chr]);
49
49
 
50
- // 检查转义字符(在引号或注释中)
50
+ // 检查转义字符(在引号或注释中)
51
51
  else if (!escaped && ['"', "'"].includes(oExpecting) && chr === '\\') return this.escaped = true;
52
52
  this.#addTextChar(chr), this.escaped = false; // 处理文本字符并重置转义标志
53
53
  }
@@ -55,12 +55,12 @@ class Tokenizer {
55
55
  /**
56
56
  * 添加操作符令牌到列表
57
57
  * @param {String} chr 操作符字符
58
- * @param {String} nextChr 下一个字符(用于设置noBreak标志)
58
+ * @param {String} nextChr 下一个字符(用于设置noBreak标志)
59
59
  * @param {String} newExpecting 新的期望操作符
60
60
  */
61
61
  #addOperatorToken(chr, nextChr, newExpecting = '') {
62
62
  const node = { type: 'operator', value: chr };
63
- // 如果下一个字符不是空白或分隔符,设置noBreak标志
63
+ // 如果下一个字符不是空白或分隔符,设置noBreak标志
64
64
  if (nextChr && ![' ', '\t', '\r', '\n', ',', ';'].includes(nextChr)) node.noBreak = true;
65
65
  this.list.push(node), this.node = null, this.escaped = false, this.operatorExpecting = newExpecting;
66
66
  }
@@ -70,15 +70,17 @@ class Tokenizer {
70
70
  * @param {String} chr 文本字符
71
71
  */
72
72
  #addTextChar(chr) {
73
- if (!this.node) this.node = { type: 'text', value: '' }, this.list.push(this.node); // 如果没有当前文本节点,则创建一个新的
73
+ if (!this.node)
74
+ this.node = { type: 'text', value: '' }, this.list.push(this.node); // 如果没有当前文本节点,则创建一个新的
74
75
 
75
76
  const processedChr = chr === '\n' ? ' ' : chr; // 规范化空白字符
76
- if (chr.charCodeAt(0) >= 0x21 || [' ', '\t'].includes(chr)) this.node.value += processedChr;// 如果是可见字符或空格,则添加
77
+ if (chr.charCodeAt(0) >= 0x21 || [' ', '\t'].includes(chr))
78
+ this.node.value += processedChr; // 如果是可见字符或空格,则添加
77
79
  }
78
80
  }
79
81
 
80
82
  /**
81
- * 如果文本数据为空但存在注释数据,则用注释数据替换文本数据
83
+ * 如果文本数据为空但存在注释数据,则用注释数据替换文本数据
82
84
  * @param {Object} data 包含文本和注释数据的对象
83
85
  * @return {Object} 处理后的数据对象
84
86
  */
@@ -127,7 +129,7 @@ const _replaceTextWithComment = data => {
127
129
  let isGroup = false, state = 'text';
128
130
  const data = { address: [], comment: [], group: [], text: [] };
129
131
 
130
- // 解析令牌,分类到地址、注释、组或文本
132
+ // 解析令牌,分类到地址、注释、组或文本
131
133
  for (let i = 0; i < tokens.length; i++) {
132
134
  const token = tokens[i], prevToken = i ? tokens[i - 1] : null;
133
135
  if (token.type === 'operator')
@@ -150,24 +152,26 @@ const _replaceTextWithComment = data => {
150
152
  // 清理地址令牌中的多余字符(如未引用的名称中的"<")
151
153
  if (state === 'address') token.value = token.value.replace(regexs.ADDRESS_CLEAN, '');
152
154
 
153
- // 如果前一个令牌设置了noBreak,则附加到最后一个元素,否则创建新的元素
155
+ // 如果前一个令牌设置了noBreak,则附加到最后一个元素,否则创建新的元素
154
156
  const dLen = data[state].length;
155
157
  prevToken?.noBreak && dLen ? data[state][dLen - 1] += token.value : data[state].push(token.value);
156
158
  }
157
159
  }
158
- const { text, group, address } = _replaceTextWithComment(data); // 如果没有文本但有注释,用注释替换文本
159
- // 如果组存在,处理组地址:组名和组成员列表
160
+ const { text, group, address } = _replaceTextWithComment(data); // 如果没有文本但有注释,用注释替换文本
161
+ // 如果组存在,处理组地址:组名和组成员列表
160
162
  if (isGroup) return [{ name: text.join(' ') || '', group: group.length ? addressparser(group.join(',')) : [] }];
161
163
  else {
162
- if (!address.length && text.length) _extractEmailFromText(data); // 如果没有地址,从文本中提取电子邮件地址
164
+ if (!address.length && text.length)
165
+ _extractEmailFromText(data); // 如果没有地址,从文本中提取电子邮件地址
163
166
 
164
167
  const { text: newTxt, address: newAdd } = _replaceTextWithComment(data); // 再次检查是否需要将注释转换为文本
165
- if (newAdd.length > 1) newTxt.push(...newAdd.splice(1)); // 如果找到多个地址,只保留第一个,其余的转到文本数组
168
+ if (newAdd.length > 1)
169
+ newTxt.push(...newAdd.splice(1)); // 如果找到多个地址,只保留第一个,其余的转到文本数组
166
170
  const addressText = newTxt.join(' ').trim(), addressValue = newAdd.join(' ').trim(),// 合并文本和地址数据
167
171
  // 创建地址对象
168
172
  addressObj = { address: addressValue || addressText || '', name: addressText || addressValue || '' };
169
173
 
170
- // 如果地址和名称相同,根据内容决定清除哪个(如果是邮件地址,清除名称,否则,清除地址)
174
+ // 如果地址和名称相同,根据内容决定清除哪个(如果是邮件地址,清除名称,否则,清除地址)
171
175
  if (addressObj.address === addressObj.name)
172
176
  addressObj.address.includes('@') ? addressObj.name = '' : addressObj.address = '';
173
177
 
@@ -213,7 +217,7 @@ const addressparser = (str, options = {}) => {
213
217
  if (address.length) parsedAddresses = parsedAddresses.concat(address);
214
218
  });
215
219
 
216
- // 如果要求展平,将组地址转换为平面列表
220
+ // 如果要求展平,将组地址转换为平面列表
217
221
  if (options.flatten) {
218
222
  const flattenedAddresses = [],
219
223
  walkAddressList = list => {
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