@zone-eu/mailsplit 5.4.27 → 5.4.29
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 +38 -61
- 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
|
@@ -12,7 +12,7 @@ const pathlib = require('path');
|
|
|
12
12
|
/** @typedef {import('..').PartNumberItem} PartNumberItem */
|
|
13
13
|
/** @typedef {import('..').PartNumber} PartNumber */
|
|
14
14
|
/** @typedef {import('..').SplitterOptions} SplitterOptions */
|
|
15
|
-
/** @typedef {import('libmime').
|
|
15
|
+
/** @typedef {import('libmime').ParsedHeaderValue} StructuredHeader */
|
|
16
16
|
/** @typedef {import('stream').Transform | import('stream').PassThrough} ContentStream */
|
|
17
17
|
/** @typedef {import('..').MimeNode} MimeNodeType */
|
|
18
18
|
|
|
@@ -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
|
*/
|
|
@@ -48,59 +51,6 @@ function stripComments(value) {
|
|
|
48
51
|
.join('');
|
|
49
52
|
}
|
|
50
53
|
|
|
51
|
-
/**
|
|
52
|
-
* Lists the parameters a header value gives in RFC 2231 form (`name*=`, `name*0=` or
|
|
53
|
-
* `name*0*=`). libmime decodes those while parsing the header, so the result must not
|
|
54
|
-
* go through `decodeWords` as well: a value like `filename*=utf-8''a%3D%3Futf-8%3FQ%3F%3D2Eexe%3F%3D`
|
|
55
|
-
* would otherwise come out as `a.exe` while a parser that decodes once sees the encoded word.
|
|
56
|
-
*
|
|
57
|
-
* @param {string} value Raw header value.
|
|
58
|
-
* @returns {Set<string>} Lowercased parameter names.
|
|
59
|
-
*/
|
|
60
|
-
function rfc2231ParamNames(value) {
|
|
61
|
-
let names = new Set();
|
|
62
|
-
let quote = false;
|
|
63
|
-
let escaped = false;
|
|
64
|
-
let start = 0;
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* @param {number} end End offset of the segment starting at `start`.
|
|
68
|
-
* @returns {void}
|
|
69
|
-
*/
|
|
70
|
-
let addSegment = end => {
|
|
71
|
-
let segment = value.substring(start, end);
|
|
72
|
-
let eq = segment.indexOf('=');
|
|
73
|
-
if (eq < 0) {
|
|
74
|
-
return;
|
|
75
|
-
}
|
|
76
|
-
let match = segment
|
|
77
|
-
.substring(0, eq)
|
|
78
|
-
.trim()
|
|
79
|
-
.toLowerCase()
|
|
80
|
-
.match(/^(.+?)\*(\d+\*?)?$/);
|
|
81
|
-
if (match) {
|
|
82
|
-
names.add(match[1]);
|
|
83
|
-
}
|
|
84
|
-
};
|
|
85
|
-
|
|
86
|
-
for (let i = 0; i < value.length; i++) {
|
|
87
|
-
let chr = value.charAt(i);
|
|
88
|
-
if (escaped) {
|
|
89
|
-
escaped = false;
|
|
90
|
-
} else if (chr === '\\') {
|
|
91
|
-
escaped = true;
|
|
92
|
-
} else if (chr === '"') {
|
|
93
|
-
quote = !quote;
|
|
94
|
-
} else if (chr === ';' && !quote) {
|
|
95
|
-
addSegment(i);
|
|
96
|
-
start = i + 1;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
addSegment(value.length);
|
|
100
|
-
|
|
101
|
-
return names;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
54
|
/**
|
|
105
55
|
* Parsed MIME node with mutable headers and transfer-encoding helpers.
|
|
106
56
|
*/
|
|
@@ -216,8 +166,14 @@ class MimeNode {
|
|
|
216
166
|
this._headersLines = [];
|
|
217
167
|
let headers = this.headers;
|
|
218
168
|
|
|
169
|
+
// The comments of the MIME headers are dropped, so that `boundary=b (comment)` names
|
|
170
|
+
// the boundary `b` that postal-mime and Python's email.policy.default split by. Read as
|
|
171
|
+
// content, it matched no delimiter line and the parts both of them find stayed hidden
|
|
172
|
+
// here. Python's legacy compat32 policy, the default of message_from_bytes() without a
|
|
173
|
+
// policy, keeps the comment and splits by `--b (comment)` lines instead: a split
|
|
174
|
+
// against it is the price of agreeing with the two readers a renderer is built on
|
|
219
175
|
let dispositionHeader = headers.getFirst('Content-Disposition');
|
|
220
|
-
this._parsedContentDisposition = this.libmime.parseHeaderValue(dispositionHeader);
|
|
176
|
+
this._parsedContentDisposition = this.libmime.parseHeaderValue(dispositionHeader, { stripComments: true });
|
|
221
177
|
let parsedContentDisposition = /** @type {StructuredHeader} */ (this._parsedContentDisposition);
|
|
222
178
|
|
|
223
179
|
// if content-type is missing default to plaintext
|
|
@@ -240,9 +196,12 @@ class MimeNode {
|
|
|
240
196
|
}
|
|
241
197
|
}
|
|
242
198
|
|
|
243
|
-
this._parsedContentType = this.libmime.parseHeaderValue(contentHeader);
|
|
199
|
+
this._parsedContentType = this.libmime.parseHeaderValue(contentHeader, { stripComments: true });
|
|
244
200
|
let parsedContentType = /** @type {StructuredHeader} */ (this._parsedContentType);
|
|
245
201
|
|
|
202
|
+
// The encoding is a bare token, and one written in quotes stays quoted, so that
|
|
203
|
+
// "base64" is not decoded, as Python and postal-mime do not decode it. parseHeaderValue
|
|
204
|
+
// would unquote it, so the transfer encoding keeps its own comment stripper
|
|
246
205
|
this.encoding = stripComments(headers.getFirst('Content-Transfer-Encoding')).toLowerCase().trim();
|
|
247
206
|
this.contentType = (parsedContentType.value || '').toLowerCase().trim() || false;
|
|
248
207
|
this.charset = parsedContentType.params.charset || false;
|
|
@@ -259,7 +218,11 @@ class MimeNode {
|
|
|
259
218
|
|
|
260
219
|
this.filename = parsedContentDisposition.params.filename || parsedContentType.params.name || false;
|
|
261
220
|
|
|
262
|
-
|
|
221
|
+
// format=flowed is defined for text/plain (RFC 3676), and a browser never unfolds an
|
|
222
|
+
// html part. Flagged on any text part, a text/html part with delsp=yes had its
|
|
223
|
+
// `<scri \r\npt>` joined into a script tag by the consumer that unfolded it, a tag
|
|
224
|
+
// neither Python nor a browser given the raw bytes sees
|
|
225
|
+
if (this.contentType === 'text/plain' && parsedContentType.params.format && parsedContentType.params.format.toLowerCase().trim() === 'flowed') {
|
|
263
226
|
this.flowed = true;
|
|
264
227
|
if (parsedContentType.params.delsp && parsedContentType.params.delsp.toLowerCase().trim() === 'yes') {
|
|
265
228
|
this.delSp = true;
|
|
@@ -267,10 +230,15 @@ class MimeNode {
|
|
|
267
230
|
}
|
|
268
231
|
|
|
269
232
|
if (this.filename) {
|
|
270
|
-
//
|
|
233
|
+
// A parameter given in RFC 2231 form was decoded by libmime already, which names
|
|
234
|
+
// it in decodedParams, and must not go through decodeWords as well:
|
|
235
|
+
// filename*=utf-8''a%3D%3Futf-8%3FQ%3F%3D2Eexe%3F%3D would otherwise come out as
|
|
236
|
+
// a.exe while a parser that decodes once sees the encoded word. A second scan of
|
|
237
|
+
// the raw header for the name used to decide this, and read `(c)filename*=` as
|
|
238
|
+
// a parameter named `(c)filename` once the comment was dropped from the real one
|
|
271
239
|
let decoded = parsedContentDisposition.params.filename
|
|
272
|
-
?
|
|
273
|
-
:
|
|
240
|
+
? parsedContentDisposition.decodedParams.includes('filename')
|
|
241
|
+
: parsedContentType.decodedParams.includes('name');
|
|
274
242
|
if (!decoded) {
|
|
275
243
|
try {
|
|
276
244
|
this.filename = this.libmime.decodeWords(this.filename);
|
|
@@ -285,7 +253,16 @@ class MimeNode {
|
|
|
285
253
|
this.contentType.substr(0, this.contentType.indexOf('/')) === 'multipart' &&
|
|
286
254
|
this.contentType.substr(this.contentType.indexOf('/') + 1)) ||
|
|
287
255
|
false;
|
|
288
|
-
|
|
256
|
+
// A boundary is ASCII and does not end in whitespace (RFC 2046 5.1.1, bchars), and
|
|
257
|
+
// Python matches no line against one that is not. One holding a non-ASCII character
|
|
258
|
+
// came out of a header read as latin1 or as UTF-8, and matched under `--ab\xC3\xA9cd`
|
|
259
|
+
// lines parts that a scanner built on Python never saw. Such a boundary counts as
|
|
260
|
+
// none, as a boundary parameter that is missing does. Trailing whitespace is dropped
|
|
261
|
+
// as Python drops it, a delimiter line may end in whitespace anyway
|
|
262
|
+
let boundary = (parsedContentType.params.boundary || '').trimEnd();
|
|
263
|
+
// a non-ASCII character takes more than one byte in UTF-8
|
|
264
|
+
let ascii = Buffer.byteLength(boundary) === boundary.length;
|
|
265
|
+
this._boundary = boundary && ascii ? Buffer.from(boundary) : false;
|
|
289
266
|
|
|
290
267
|
this.rfc822 = this.contentType === 'message/rfc822';
|
|
291
268
|
|
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.29",
|
|
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": {
|