@zone-eu/mailsplit 5.4.27 → 5.4.28
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/message-splitter.js +39 -27
- package/lib/mime-node.d.ts +1 -1
- package/lib/mime-node.js +29 -4
- package/lib/types.d.ts +1 -1
- package/package.json +2 -2
package/lib/message-splitter.js
CHANGED
|
@@ -20,9 +20,17 @@ const MAX_PENDING_LINE_SIZE = 64 * 1024;
|
|
|
20
20
|
// how many separate writes the pending line may be kept in before it is compacted
|
|
21
21
|
const MAX_PENDING_LINE_CHUNKS = 1024;
|
|
22
22
|
|
|
23
|
+
// The most SP and HTAB a delimiter line may carry between the boundary value, or its closing
|
|
24
|
+
// "--", and the line ending. RFC 2046 5.1.1 allows the whitespace, and Python and postal-mime
|
|
25
|
+
// accept any amount of it, so a line that ends in a few spaces delimits the same parts here
|
|
26
|
+
// as there. The line still has to fit a header line (RFC 5322 2.1.1), so the pending line
|
|
27
|
+
// kept as a possible delimiter stays bounded
|
|
28
|
+
const BOUNDARY_LINE_TRAILING_WSP = 998;
|
|
29
|
+
|
|
23
30
|
// what a delimiter line may carry after the boundary value: the "--" prefix, an optional
|
|
24
|
-
// "--" suffix and the line terminator. This is the bound
|
|
25
|
-
|
|
31
|
+
// "--" suffix, the trailing whitespace and the line terminator. This is the bound
|
|
32
|
+
// compareBoundary() accepts.
|
|
33
|
+
const BOUNDARY_LINE_SUFFIX = 2 /* "--" prefix */ + 2 /* "--" suffix */ + BOUNDARY_LINE_TRAILING_WSP + 2; /* trailing <CR><LF> */
|
|
26
34
|
|
|
27
35
|
// checkBoundary() additionally allows a line ending in front of the delimiter, so this is
|
|
28
36
|
// the longest a delimiter line can ever be once the boundary value is subtracted
|
|
@@ -389,7 +397,8 @@ class MessageSplitter extends Transform {
|
|
|
389
397
|
* @returns {1 | 2 | false}
|
|
390
398
|
*/
|
|
391
399
|
compareBoundary(line, startpos, boundary) {
|
|
392
|
-
// --{boundary}\r\n or --{boundary}--\r\n
|
|
400
|
+
// --{boundary}\r\n or --{boundary}--\r\n, with SP and HTAB allowed in front of the
|
|
401
|
+
// line ending (RFC 2046 5.1.1), where Python and postal-mime accept them as well
|
|
393
402
|
if (line.length < boundary.length + 3 + startpos || line.length > boundary.length + BOUNDARY_LINE_SUFFIX + startpos) {
|
|
394
403
|
return false;
|
|
395
404
|
}
|
|
@@ -399,34 +408,37 @@ class MessageSplitter extends Transform {
|
|
|
399
408
|
}
|
|
400
409
|
}
|
|
401
410
|
|
|
402
|
-
let
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
}
|
|
409
|
-
if (pos === 0 && c !== 0x2d) {
|
|
410
|
-
// expecting "-"
|
|
411
|
-
return false;
|
|
412
|
-
}
|
|
413
|
-
if (pos === 1 && c !== 0x2d) {
|
|
414
|
-
// expecting "-"
|
|
415
|
-
return false;
|
|
416
|
-
}
|
|
417
|
-
if (pos === 2 && c !== 0x0d && c !== 0x0a) {
|
|
418
|
-
// expecting line terminator, either <CR> or <LF>
|
|
411
|
+
let i = boundary.length + 2 + startpos;
|
|
412
|
+
// 1: next node, 2: multipart end
|
|
413
|
+
let result = /** @type {1 | 2} */ (1);
|
|
414
|
+
if (line[i] === 0x2d) {
|
|
415
|
+
if (line[i + 1] !== 0x2d) {
|
|
416
|
+
// expecting "--"
|
|
419
417
|
return false;
|
|
420
418
|
}
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
419
|
+
result = 2;
|
|
420
|
+
i += 2;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
while (i < line.length && (line[i] === 0x20 || line[i] === 0x09)) {
|
|
424
|
+
i++;
|
|
426
425
|
}
|
|
427
426
|
|
|
428
|
-
|
|
429
|
-
|
|
427
|
+
if (i === line.length) {
|
|
428
|
+
// only the closing delimiter may end the input without a line ending
|
|
429
|
+
return result === 2 ? 2 : false;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// expecting the line terminator and nothing after it: <CR><LF>, <CR> or <LF>, where
|
|
433
|
+
// the <CR> may be a run of them, the artifact of a second CR to CRLF conversion,
|
|
434
|
+
// which postal-mime reads as the line ending as well
|
|
435
|
+
while (i < line.length && line[i] === 0x0d) {
|
|
436
|
+
i++;
|
|
437
|
+
}
|
|
438
|
+
if (i < line.length && line[i] === 0x0a) {
|
|
439
|
+
i++;
|
|
440
|
+
}
|
|
441
|
+
return i === line.length ? result : false;
|
|
430
442
|
}
|
|
431
443
|
|
|
432
444
|
/**
|
package/lib/mime-node.d.ts
CHANGED
|
@@ -46,7 +46,7 @@ declare class MimeNode implements MimeNodeShape {
|
|
|
46
46
|
/** Decoded filename from Content-Disposition or Content-Type parameters, or `false` when absent. */
|
|
47
47
|
filename: string | false;
|
|
48
48
|
|
|
49
|
-
/** Whether this node is `text
|
|
49
|
+
/** Whether this node is `text/plain` with `format=flowed`. */
|
|
50
50
|
flowed: boolean;
|
|
51
51
|
|
|
52
52
|
/** Whether flowed text uses `delsp=yes`. */
|
package/lib/mime-node.js
CHANGED
|
@@ -27,6 +27,9 @@ const HEADER_LINE_MIN_COST = 64;
|
|
|
27
27
|
* `value.replace(/\(.*\)/g, '')`, but in linear time. The regex backtracks quadratically
|
|
28
28
|
* on a value with many `(` characters and no closing `)`.
|
|
29
29
|
*
|
|
30
|
+
* Used for the Content-Transfer-Encoding only. The MIME headers with parameters go through
|
|
31
|
+
* libmime's parseHeaderValue with stripComments, which knows where a comment may stand.
|
|
32
|
+
*
|
|
30
33
|
* @param {string} value
|
|
31
34
|
* @returns {string}
|
|
32
35
|
*/
|
|
@@ -216,8 +219,14 @@ class MimeNode {
|
|
|
216
219
|
this._headersLines = [];
|
|
217
220
|
let headers = this.headers;
|
|
218
221
|
|
|
222
|
+
// The comments of the MIME headers are dropped, so that `boundary=b (comment)` names
|
|
223
|
+
// the boundary `b` that postal-mime and Python's email.policy.default split by. Read as
|
|
224
|
+
// content, it matched no delimiter line and the parts both of them find stayed hidden
|
|
225
|
+
// here. Python's legacy compat32 policy, the default of message_from_bytes() without a
|
|
226
|
+
// policy, keeps the comment and splits by `--b (comment)` lines instead: a split
|
|
227
|
+
// against it is the price of agreeing with the two readers a renderer is built on
|
|
219
228
|
let dispositionHeader = headers.getFirst('Content-Disposition');
|
|
220
|
-
this._parsedContentDisposition = this.libmime.parseHeaderValue(dispositionHeader);
|
|
229
|
+
this._parsedContentDisposition = this.libmime.parseHeaderValue(dispositionHeader, { stripComments: true });
|
|
221
230
|
let parsedContentDisposition = /** @type {StructuredHeader} */ (this._parsedContentDisposition);
|
|
222
231
|
|
|
223
232
|
// if content-type is missing default to plaintext
|
|
@@ -240,9 +249,12 @@ class MimeNode {
|
|
|
240
249
|
}
|
|
241
250
|
}
|
|
242
251
|
|
|
243
|
-
this._parsedContentType = this.libmime.parseHeaderValue(contentHeader);
|
|
252
|
+
this._parsedContentType = this.libmime.parseHeaderValue(contentHeader, { stripComments: true });
|
|
244
253
|
let parsedContentType = /** @type {StructuredHeader} */ (this._parsedContentType);
|
|
245
254
|
|
|
255
|
+
// The encoding is a bare token, and one written in quotes stays quoted, so that
|
|
256
|
+
// "base64" is not decoded, as Python and postal-mime do not decode it. parseHeaderValue
|
|
257
|
+
// would unquote it, so the transfer encoding keeps its own comment stripper
|
|
246
258
|
this.encoding = stripComments(headers.getFirst('Content-Transfer-Encoding')).toLowerCase().trim();
|
|
247
259
|
this.contentType = (parsedContentType.value || '').toLowerCase().trim() || false;
|
|
248
260
|
this.charset = parsedContentType.params.charset || false;
|
|
@@ -259,7 +271,11 @@ class MimeNode {
|
|
|
259
271
|
|
|
260
272
|
this.filename = parsedContentDisposition.params.filename || parsedContentType.params.name || false;
|
|
261
273
|
|
|
262
|
-
|
|
274
|
+
// format=flowed is defined for text/plain (RFC 3676), and a browser never unfolds an
|
|
275
|
+
// html part. Flagged on any text part, a text/html part with delsp=yes had its
|
|
276
|
+
// `<scri \r\npt>` joined into a script tag by the consumer that unfolded it, a tag
|
|
277
|
+
// neither Python nor a browser given the raw bytes sees
|
|
278
|
+
if (this.contentType === 'text/plain' && parsedContentType.params.format && parsedContentType.params.format.toLowerCase().trim() === 'flowed') {
|
|
263
279
|
this.flowed = true;
|
|
264
280
|
if (parsedContentType.params.delsp && parsedContentType.params.delsp.toLowerCase().trim() === 'yes') {
|
|
265
281
|
this.delSp = true;
|
|
@@ -285,7 +301,16 @@ class MimeNode {
|
|
|
285
301
|
this.contentType.substr(0, this.contentType.indexOf('/')) === 'multipart' &&
|
|
286
302
|
this.contentType.substr(this.contentType.indexOf('/') + 1)) ||
|
|
287
303
|
false;
|
|
288
|
-
|
|
304
|
+
// A boundary is ASCII and does not end in whitespace (RFC 2046 5.1.1, bchars), and
|
|
305
|
+
// Python matches no line against one that is not. One holding a non-ASCII character
|
|
306
|
+
// came out of a header read as latin1 or as UTF-8, and matched under `--ab\xC3\xA9cd`
|
|
307
|
+
// lines parts that a scanner built on Python never saw. Such a boundary counts as
|
|
308
|
+
// none, as a boundary parameter that is missing does. Trailing whitespace is dropped
|
|
309
|
+
// as Python drops it, a delimiter line may end in whitespace anyway
|
|
310
|
+
let boundary = (parsedContentType.params.boundary || '').trimEnd();
|
|
311
|
+
// a non-ASCII character takes more than one byte in UTF-8
|
|
312
|
+
let ascii = Buffer.byteLength(boundary) === boundary.length;
|
|
313
|
+
this._boundary = boundary && ascii ? Buffer.from(boundary) : false;
|
|
289
314
|
|
|
290
315
|
this.rfc822 = this.contentType === 'message/rfc822';
|
|
291
316
|
|
package/lib/types.d.ts
CHANGED
|
@@ -107,7 +107,7 @@ export interface MimeNode {
|
|
|
107
107
|
/** Decoded filename from Content-Disposition or Content-Type parameters, or `false` when absent. */
|
|
108
108
|
filename: string | false;
|
|
109
109
|
|
|
110
|
-
/** Whether this node is `text
|
|
110
|
+
/** Whether this node is `text/plain` with `format=flowed`. */
|
|
111
111
|
flowed: boolean;
|
|
112
112
|
|
|
113
113
|
/** Whether flowed text uses `delsp=yes`. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zone-eu/mailsplit",
|
|
3
|
-
"version": "5.4.
|
|
3
|
+
"version": "5.4.28",
|
|
4
4
|
"description": "Split email messages into an object stream",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"license": "(MIT OR EUPL-1.1+)",
|
|
26
26
|
"dependencies": {
|
|
27
27
|
"libbase64": "1.4.1",
|
|
28
|
-
"libmime": "5.
|
|
28
|
+
"libmime": "5.6.3",
|
|
29
29
|
"libqp": "3.0.1"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|