mailauth 2.3.3 → 2.3.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.
@@ -1,9 +1,14 @@
1
- 'use strict';
1
+ /* eslint no-control-regex: 0 */
2
2
 
3
- // Calculates relaxed body hash for a message body stream
3
+ 'use strict';
4
4
 
5
5
  const crypto = require('crypto');
6
6
 
7
+ const CHAR_CR = 0x0d;
8
+ const CHAR_LF = 0x0a;
9
+ const CHAR_SPACE = 0x20;
10
+ const CHAR_TAB = 0x09;
11
+
7
12
  /**
8
13
  * Class for calculating body hash of an email message body stream
9
14
  * using the "relaxed" canonicalization
@@ -16,139 +21,244 @@ class RelaxedHash {
16
21
  * @param {Number} [maxBodyLength] Allowed body length count, the value from the l= parameter
17
22
  */
18
23
  constructor(algorithm, maxBodyLength) {
19
- algorithm = (algorithm || 'sha256').split('-').pop();
24
+ algorithm = (algorithm || 'sha256').split('-').pop().toLowerCase();
25
+
20
26
  this.bodyHash = crypto.createHash(algorithm);
21
27
 
22
- this.remainder = '';
28
+ this.remainder = false;
23
29
  this.byteLength = 0;
24
30
 
25
31
  this.bodyHashedBytes = 0;
26
32
  this.maxBodyLength = maxBodyLength;
33
+
34
+ this.maxSizeReached = false;
35
+
36
+ this.emptyLinesQueue = [];
27
37
  }
28
38
 
29
39
  _updateBodyHash(chunk) {
30
- // the following is needed for l= option
40
+ if (this.maxSizeReached) {
41
+ return;
42
+ }
43
+
44
+ // the following is needed for the l= option
31
45
  if (
32
46
  typeof this.maxBodyLength === 'number' &&
33
47
  !isNaN(this.maxBodyLength) &&
34
48
  this.maxBodyLength >= 0 &&
35
49
  this.bodyHashedBytes + chunk.length > this.maxBodyLength
36
50
  ) {
51
+ this.maxSizeReached = true;
37
52
  if (this.bodyHashedBytes >= this.maxBodyLength) {
38
53
  // nothing to do here, skip entire chunk
39
54
  return;
40
55
  }
56
+
41
57
  // only use allowed size of bytes
42
- chunk = chunk.slice(0, this.maxBodyLength - this.bodyHashedBytes);
58
+ chunk = chunk.subarray(0, this.maxBodyLength - this.bodyHashedBytes);
43
59
  }
44
60
 
45
61
  this.bodyHashedBytes += chunk.length;
46
62
  this.bodyHash.update(chunk);
63
+
64
+ //process.stdout.write(chunk);
47
65
  }
48
66
 
49
- update(chunk) {
50
- this.byteLength += chunk.length;
67
+ _drainPendingEmptyLines() {
68
+ if (this.emptyLinesQueue.length) {
69
+ for (let emptyLine of this.emptyLinesQueue) {
70
+ this._updateBodyHash(emptyLine);
71
+ }
72
+ this.emptyLinesQueue = [];
73
+ }
74
+ }
51
75
 
52
- let bodyStr;
76
+ _pushBodyHash(chunk) {
77
+ if (!chunk || !chunk.length) {
78
+ return;
79
+ }
53
80
 
54
- // find next remainder
55
- let nextRemainder = '';
81
+ // remove line endings
82
+ let foundNonLn = false;
56
83
 
57
- // This crux finds and removes the spaces from the last line and the newline characters after the last non-empty line
58
- // If we get another chunk that does not match this description then we can restore the previously processed data
59
- let state = 'file';
84
+ // buffer line endings and empty lines
60
85
  for (let i = chunk.length - 1; i >= 0; i--) {
61
- let c = chunk[i];
62
-
63
- if (state === 'file' && (c === 0x0a || c === 0x0d)) {
64
- // do nothing, found \n or \r at the end of chunk, stil end of file
65
- } else if (state === 'file' && (c === 0x09 || c === 0x20)) {
66
- // switch to line ending mode, this is the last non-empty line
67
- state = 'line';
68
- } else if (state === 'line' && (c === 0x09 || c === 0x20)) {
69
- // do nothing, found ' ' or \t at the end of line, keep processing the last non-empty line
70
- } else if (state === 'file' || state === 'line') {
71
- // non line/file ending character found, switch to body mode
72
- state = 'body';
73
- if (i === chunk.length - 1) {
74
- // final char is not part of line end or file end, so do nothing
75
- break;
86
+ if (chunk[i] !== CHAR_LF && chunk[i] !== CHAR_CR) {
87
+ this._drainPendingEmptyLines();
88
+ if (i < chunk.length - 1) {
89
+ this.emptyLinesQueue.push(chunk.subarray(i + 1));
90
+ chunk = chunk.subarray(0, i + 1);
76
91
  }
92
+ foundNonLn = true;
93
+ break;
77
94
  }
95
+ }
96
+
97
+ if (!foundNonLn) {
98
+ this.emptyLinesQueue.push(chunk);
99
+ return;
100
+ }
78
101
 
79
- if (i === 0) {
80
- // reached to the beginning of the chunk, check if it is still about the ending
81
- // and if the remainder also matches
82
- if (
83
- (state === 'file' && (!this.remainder || /[\r\n]$/.test(this.remainder))) ||
84
- (state === 'line' && (!this.remainder || /[ \t]$/.test(this.remainder)))
85
- ) {
86
- // keep everything
87
- this.remainder += chunk.toString('binary');
88
- return;
89
- } else if (state === 'line' || state === 'file') {
90
- // process existing remainder as normal line but store the current chunk
91
- nextRemainder = chunk.toString('binary');
92
- chunk = false;
93
- break;
102
+ this._updateBodyHash(chunk);
103
+ }
104
+
105
+ fixLineBuffer(line) {
106
+ let resultLine = [];
107
+
108
+ let nonWspFound = false;
109
+ let prevWsp = false;
110
+
111
+ for (let i = line.length - 1; i >= 0; i--) {
112
+ if (line[i] === CHAR_LF) {
113
+ resultLine.unshift(line[i]);
114
+ if (i === 0 || line[i - 1] !== CHAR_CR) {
115
+ // add missing carriage return
116
+ resultLine.unshift(CHAR_CR);
94
117
  }
118
+ continue;
95
119
  }
96
120
 
97
- if (state !== 'body') {
121
+ if (line[i] === CHAR_CR) {
122
+ resultLine.unshift(line[i]);
123
+ continue;
124
+ }
125
+
126
+ if (line[i] === CHAR_SPACE || line[i] === CHAR_TAB) {
127
+ if (nonWspFound) {
128
+ prevWsp = true;
129
+ }
98
130
  continue;
99
131
  }
100
132
 
101
- // reached first non ending byte
102
- nextRemainder = chunk.slice(i + 1).toString('binary');
103
- chunk = chunk.slice(0, i + 1);
104
- break;
133
+ if (prevWsp) {
134
+ resultLine.unshift(CHAR_SPACE);
135
+ prevWsp = false;
136
+ }
137
+
138
+ nonWspFound = true;
139
+ resultLine.unshift(line[i]);
140
+ }
141
+
142
+ if (prevWsp && nonWspFound) {
143
+ resultLine.unshift(CHAR_SPACE);
144
+ }
145
+
146
+ return Buffer.from(resultLine);
147
+ }
148
+
149
+ update(chunk, final) {
150
+ this.byteLength += (chunk && chunk.length) || 0;
151
+ if (this.maxSizeReached) {
152
+ return;
105
153
  }
106
154
 
107
- let needsFixing = !!this.remainder;
108
- if (chunk && !needsFixing) {
109
- // check if we even need to change anything
110
- for (let i = 0, len = chunk.length; i < len; i++) {
111
- if (i && chunk[i] === 0x0a && chunk[i - 1] !== 0x0d) {
112
- // missing \r before \n
113
- needsFixing = true;
114
- break;
115
- } else if (i && chunk[i] === 0x0d && chunk[i - 1] === 0x20) {
116
- // trailing WSP found
117
- needsFixing = true;
118
- break;
119
- } else if (i && chunk[i] === 0x20 && chunk[i - 1] === 0x20) {
120
- // multiple spaces found, needs to be replaced with just one
121
- needsFixing = true;
122
- break;
123
- } else if (chunk[i] === 0x09) {
124
- // TAB found, needs to be replaced with a space
125
- needsFixing = true;
126
- break;
155
+ // Canonicalize content by applying a and b in order:
156
+ // a.1. Ignore all whitespace at the end of lines.
157
+ // a.2. Reduce all sequences of WSP within a line to a single SP character.
158
+
159
+ // b.1. Ignore all empty lines at the end of the message body.
160
+ // b.2. If the body is non-empty but does not end with a CRLF, a CRLF is added.
161
+
162
+ let lineEndPos = -1;
163
+ let lineNeedsFixing = false;
164
+ let cursorPos = 0;
165
+
166
+ if (this.remainder && this.remainder.length) {
167
+ if (chunk) {
168
+ // concatting chunks might be bad for performance :S
169
+ chunk = Buffer.concat([this.remainder, chunk]);
170
+ } else {
171
+ chunk = this.remainder;
172
+ }
173
+ this.remainder = false;
174
+ }
175
+
176
+ if (chunk && chunk.length) {
177
+ for (let pos = 0; pos < chunk.length; pos++) {
178
+ switch (chunk[pos]) {
179
+ case CHAR_LF:
180
+ if (
181
+ !lineNeedsFixing &&
182
+ // previous character is not <CR>
183
+ ((pos >= 1 && chunk[pos - 1] !== CHAR_CR) ||
184
+ // LF is the first byte on the line
185
+ pos === 0 ||
186
+ // there's a space before line break
187
+ (pos >= 2 && chunk[pos - 1] === CHAR_CR && chunk[pos - 2] === CHAR_SPACE))
188
+ ) {
189
+ lineNeedsFixing = true;
190
+ }
191
+
192
+ // line break
193
+ if (lineNeedsFixing) {
194
+ // emit pending bytes up to the last line break before current line
195
+ if (lineEndPos >= 0 && lineEndPos >= cursorPos) {
196
+ let chunkPart = chunk.subarray(cursorPos, lineEndPos + 1);
197
+ this._pushBodyHash(chunkPart);
198
+ }
199
+
200
+ let line = chunk.subarray(lineEndPos + 1, pos + 1);
201
+ this._pushBodyHash(this.fixLineBuffer(line));
202
+
203
+ lineNeedsFixing = false;
204
+
205
+ // move cursor to the start of next line
206
+ cursorPos = pos + 1;
207
+ }
208
+
209
+ lineEndPos = pos;
210
+
211
+ break;
212
+
213
+ case CHAR_SPACE:
214
+ if (!lineNeedsFixing && pos && chunk[pos - 1] === CHAR_SPACE) {
215
+ lineNeedsFixing = true;
216
+ }
217
+ break;
218
+
219
+ case CHAR_TAB:
220
+ // non-space WSP always needs replacing
221
+ lineNeedsFixing = true;
222
+ break;
223
+
224
+ default:
127
225
  }
128
226
  }
129
227
  }
130
228
 
131
- if (needsFixing) {
132
- bodyStr = this.remainder + (chunk ? chunk.toString('binary') : '');
133
- this.remainder = nextRemainder;
134
- bodyStr = bodyStr
135
- .replace(/\r?\n/g, '\n') // use js line endings
136
- .replace(/[ \t]*$/gm, '') // remove line endings, rtrim
137
- .replace(/[ \t]+/gm, ' ') // single spaces
138
- .replace(/\n/g, '\r\n'); // restore rfc822 line endings
139
- chunk = Buffer.from(bodyStr, 'binary');
140
- } else if (nextRemainder) {
141
- this.remainder = nextRemainder;
229
+ if (chunk && cursorPos < chunk.length && cursorPos !== lineEndPos) {
230
+ // emit data from chunk
231
+
232
+ let chunkPart = chunk.subarray(cursorPos, lineEndPos + 1);
233
+
234
+ if (chunkPart.length) {
235
+ this._pushBodyHash(lineNeedsFixing ? this.fixLineBuffer(chunkPart) : chunkPart);
236
+ lineNeedsFixing = false;
237
+ }
238
+
239
+ cursorPos = lineEndPos + 1;
142
240
  }
143
241
 
144
- this._updateBodyHash(chunk);
242
+ if (chunk && !final && cursorPos < chunk.length) {
243
+ this.remainder = chunk.subarray(cursorPos);
244
+ }
245
+
246
+ if (final) {
247
+ let chunkPart = (cursorPos && chunk && chunk.subarray(cursorPos)) || chunk;
248
+ if (chunkPart && chunkPart.length) {
249
+ this._pushBodyHash(lineNeedsFixing ? this.fixLineBuffer(chunkPart) : chunkPart);
250
+ lineNeedsFixing = false;
251
+ }
252
+
253
+ if (this.bodyHashedBytes) {
254
+ // terminating line break for non-empty messages
255
+ this._updateBodyHash(Buffer.from([CHAR_CR, CHAR_LF]));
256
+ }
257
+ }
145
258
  }
146
259
 
147
260
  digest(encoding) {
148
- if (/[\r\n]$/.test(this.remainder) && this.bodyHashedBytes > 0) {
149
- // add terminating line end
150
- this._updateBodyHash(Buffer.from('\r\n'));
151
- }
261
+ this.update(null, true);
152
262
 
153
263
  // finalize
154
264
  return this.bodyHash.digest(encoding);
@@ -156,3 +266,27 @@ class RelaxedHash {
156
266
  }
157
267
 
158
268
  module.exports = { RelaxedHash };
269
+
270
+ /*
271
+ let fs = require('fs');
272
+
273
+ const getBody = message => {
274
+ message = message.toString('binary');
275
+ let match = message.match(/\r?\n\r?\n/);
276
+ if (match) {
277
+ message = message.substr(match.index + match[0].length);
278
+ }
279
+ return Buffer.from(message, 'binary');
280
+ };
281
+
282
+ let s = fs.readFileSync(process.argv[2]);
283
+
284
+ let k = new RelaxedHash('rsa-sha256', -1);
285
+
286
+ for (let byte of getBody(s)) {
287
+ k.update(Buffer.from([byte]));
288
+ }
289
+
290
+ console.error(k.digest('base64'));
291
+ console.error(k.byteLength, k.bodyHashedBytes);
292
+ */
@@ -22,6 +22,8 @@ class SimpleHash {
22
22
 
23
23
  this.bodyHashedBytes = 0;
24
24
  this.maxBodyLength = maxBodyLength;
25
+
26
+ this.lastNewline = false;
25
27
  }
26
28
 
27
29
  _updateBodyHash(chunk) {
@@ -42,6 +44,8 @@ class SimpleHash {
42
44
 
43
45
  this.bodyHashedBytes += chunk.length;
44
46
  this.bodyHash.update(chunk);
47
+
48
+ //process.stdout.write(chunk);
45
49
  }
46
50
 
47
51
  update(chunk) {
@@ -81,10 +85,11 @@ class SimpleHash {
81
85
  }
82
86
 
83
87
  this._updateBodyHash(chunk);
88
+ this.lastNewline = chunk[chunk.length - 1] === 0x0a;
84
89
  }
85
90
 
86
91
  digest(encoding) {
87
- if (this.remainder.length || !this.bodyHashedBytes) {
92
+ if (!this.lastNewline || !this.bodyHashedBytes) {
88
93
  // emit empty line buffer to keep the stream flowing
89
94
  this._updateBodyHash(Buffer.from('\r\n'));
90
95
  }
package/licenses.txt CHANGED
@@ -3,9 +3,9 @@ name license type link
3
3
  @fidm/x509 MIT git+ssh://git@github.com/fidm/x509.git 1.2.1
4
4
  ipaddr.js MIT git://github.com/whitequark/ipaddr.js.git 2.0.1 whitequark whitequark@whitequark.org
5
5
  joi BSD-3-Clause git://github.com/sideway/joi.git 17.6.0
6
- libmime MIT git://github.com/andris9/libmime.git 5.0.0 Andris Reinman andris@kreata.ee
6
+ libmime MIT git://github.com/andris9/libmime.git 5.1.0 Andris Reinman andris@kreata.ee
7
7
  node-forge (BSD-3-Clause OR GPL-2.0) git+https://github.com/digitalbazaar/forge.git 1.3.1 Digital Bazaar, Inc. support@digitalbazaar.com http://digitalbazaar.com/
8
- nodemailer MIT git+https://github.com/nodemailer/nodemailer.git 6.7.3 Andris Reinman
8
+ nodemailer MIT git+https://github.com/nodemailer/nodemailer.git 6.7.5 Andris Reinman
9
9
  psl MIT git+ssh://git@github.com/lupomontero/psl.git 1.8.0 Lupo Montero lupomontero@gmail.com https://lupomontero.com/
10
10
  punycode MIT git+https://github.com/bestiejs/punycode.js.git 2.1.1 Mathias Bynens https://mathiasbynens.be/
11
- yargs MIT git+https://github.com/yargs/yargs.git 17.4.1
11
+ yargs MIT git+https://github.com/yargs/yargs.git 17.5.0
package/man/mailauth.1 CHANGED
@@ -1,4 +1,4 @@
1
- .TH "MAILAUTH" "1" "May 2022" "v2.3.2" "Mailauth Help"
1
+ .TH "MAILAUTH" "1" "June 2022" "v2.3.3" "Mailauth Help"
2
2
  .SH "NAME"
3
3
  \fBmailauth\fR
4
4
  .QP
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mailauth",
3
- "version": "2.3.3",
3
+ "version": "2.3.4",
4
4
  "description": "Email authentication library for Node.js",
5
5
  "main": "lib/mailauth.js",
6
6
  "scripts": {
@@ -33,7 +33,7 @@
33
33
  "homepage": "https://github.com/postalsys/mailauth",
34
34
  "devDependencies": {
35
35
  "chai": "4.3.6",
36
- "eslint": "8.15.0",
36
+ "eslint": "8.17.0",
37
37
  "eslint-config-nodemailer": "1.2.0",
38
38
  "eslint-config-prettier": "8.5.0",
39
39
  "js-yaml": "4.1.0",
@@ -42,7 +42,7 @@
42
42
  "marked-man": "0.7.0",
43
43
  "mbox-reader": "1.1.5",
44
44
  "mocha": "10.0.0",
45
- "pkg": "5.6.0"
45
+ "pkg": "5.7.0"
46
46
  },
47
47
  "dependencies": {
48
48
  "@fidm/x509": "1.2.1",
@@ -53,7 +53,7 @@
53
53
  "nodemailer": "6.7.5",
54
54
  "psl": "1.8.0",
55
55
  "punycode": "2.1.1",
56
- "yargs": "17.5.0"
56
+ "yargs": "17.5.1"
57
57
  },
58
58
  "engines": {
59
59
  "node": ">=14.0.0"