@zone-eu/mailsplit 5.4.21 → 5.4.22
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/headers.js +15 -4
- package/lib/message-splitter.js +34 -23
- package/lib/mime-node.js +61 -5
- package/package.json +1 -1
package/lib/headers.js
CHANGED
|
@@ -392,18 +392,29 @@ class Headers {
|
|
|
392
392
|
lines.push({ key: this._normalizeHeader(first.substr(0, first.indexOf(':'))), line: first });
|
|
393
393
|
}
|
|
394
394
|
|
|
395
|
+
// Set while the last entry has no colon yet, so that its key may still come from a
|
|
396
|
+
// continuation line. Only the new line is searched then: searching the whole
|
|
397
|
+
// accumulated line again for every continuation was quadratic on a keyless block
|
|
398
|
+
let keyPending = false;
|
|
399
|
+
|
|
395
400
|
for (; i < rawLines.length; i++) {
|
|
396
401
|
let currentLine = rawLines[i];
|
|
397
402
|
if (this._isFoldingChar(currentLine.charAt(0))) {
|
|
398
403
|
// the loop starts on a non-folded line, so there is always a last entry here
|
|
399
404
|
let entry = lines[lines.length - 1];
|
|
400
|
-
|
|
401
|
-
if (!entry.key) {
|
|
405
|
+
if (keyPending) {
|
|
402
406
|
// the key comes from the first colon, which may only appear in a continuation
|
|
403
|
-
|
|
407
|
+
let colon = currentLine.indexOf(':');
|
|
408
|
+
if (colon >= 0) {
|
|
409
|
+
entry.key = this._normalizeHeader(entry.line + '\r\n' + currentLine.substr(0, colon));
|
|
410
|
+
keyPending = false;
|
|
411
|
+
}
|
|
404
412
|
}
|
|
413
|
+
entry.line += '\r\n' + currentLine;
|
|
405
414
|
} else {
|
|
406
|
-
|
|
415
|
+
let colon = currentLine.indexOf(':');
|
|
416
|
+
lines.push({ key: this._normalizeHeader(currentLine.substr(0, colon)), line: currentLine });
|
|
417
|
+
keyPending = colon < 0;
|
|
407
418
|
}
|
|
408
419
|
}
|
|
409
420
|
|
package/lib/message-splitter.js
CHANGED
|
@@ -94,6 +94,9 @@ class MessageSplitter extends Transform {
|
|
|
94
94
|
// set once the closing delimiter of the current node's multipart has been seen, so
|
|
95
95
|
// that any later boundary line of that node counts as epilogue. Reset per node.
|
|
96
96
|
this.inEpilogue = false;
|
|
97
|
+
// the open ancestor multipart whose boundary the last delimiter line matched, set
|
|
98
|
+
// by checkBoundary() for a sibling (3) or closing (4) delimiter
|
|
99
|
+
this.delimiterOwner = /** @type {MimeNodeType | false} */ (false);
|
|
97
100
|
this.newNode();
|
|
98
101
|
// incomplete trailing line of the previous chunk, kept as a list of chunks so
|
|
99
102
|
// that a long line without a line break is not copied over for every write
|
|
@@ -452,15 +455,38 @@ class MessageSplitter extends Transform {
|
|
|
452
455
|
return boundary;
|
|
453
456
|
}
|
|
454
457
|
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
458
|
+
// A delimiter of any open ancestor multipart, not only the parent, ends everything
|
|
459
|
+
// below it. An inner multipart that is never closed would otherwise swallow the
|
|
460
|
+
// rest of the message as body text of its last part, while other parsers and mail
|
|
461
|
+
// clients show the parts that follow the outer delimiter
|
|
462
|
+
for (let owner = this.node._parentBoundaryOwner; owner; owner = owner._parentBoundaryOwner) {
|
|
463
|
+
if (owner._boundary && (boundary = this.compareBoundary(line, startpos, owner._boundary))) {
|
|
464
|
+
// 3: next sibling under the owner
|
|
465
|
+
// 4: the owner's multipart ends
|
|
466
|
+
this.delimiterOwner = owner;
|
|
467
|
+
return /** @type {3 | 4} */ (boundary + 2);
|
|
468
|
+
}
|
|
459
469
|
}
|
|
460
470
|
|
|
461
471
|
return false;
|
|
462
472
|
}
|
|
463
473
|
|
|
474
|
+
/**
|
|
475
|
+
* Longest boundary a delimiter line on the current node could carry: the node's own
|
|
476
|
+
* boundary or that of any open ancestor multipart.
|
|
477
|
+
*
|
|
478
|
+
* @returns {number} Boundary length in bytes, 0 when no boundary applies.
|
|
479
|
+
*/
|
|
480
|
+
maxBoundaryLength() {
|
|
481
|
+
let max = this.node._boundary ? this.node._boundary.length : 0;
|
|
482
|
+
for (let owner = this.node._parentBoundaryOwner; owner; owner = owner._parentBoundaryOwner) {
|
|
483
|
+
if (owner._boundary && owner._boundary.length > max) {
|
|
484
|
+
max = owner._boundary.length;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
return max;
|
|
488
|
+
}
|
|
489
|
+
|
|
464
490
|
/**
|
|
465
491
|
* Checks the header bytes collected for the current node against maxHeadSize.
|
|
466
492
|
*
|
|
@@ -488,12 +514,7 @@ class MessageSplitter extends Transform {
|
|
|
488
514
|
return null;
|
|
489
515
|
}
|
|
490
516
|
|
|
491
|
-
|
|
492
|
-
this.node._boundary ? this.node._boundary.length : 0,
|
|
493
|
-
this.node._parentBoundary ? this.node._parentBoundary.length : 0
|
|
494
|
-
);
|
|
495
|
-
|
|
496
|
-
if (this.lineLength <= maxBoundaryLength + BOUNDARY_LINE_OVERHEAD) {
|
|
517
|
+
if (this.lineLength <= this.maxBoundaryLength() + BOUNDARY_LINE_OVERHEAD) {
|
|
497
518
|
// might still turn out to be a boundary delimiter line
|
|
498
519
|
return null;
|
|
499
520
|
}
|
|
@@ -571,8 +592,8 @@ class MessageSplitter extends Transform {
|
|
|
571
592
|
// reached end of children, keep current node
|
|
572
593
|
break;
|
|
573
594
|
case 3: {
|
|
574
|
-
// next sibling
|
|
575
|
-
this.newNode(this.
|
|
595
|
+
// next sibling under the multipart that owns the matched boundary
|
|
596
|
+
this.newNode(this.delimiterOwner);
|
|
576
597
|
flush = true;
|
|
577
598
|
break;
|
|
578
599
|
}
|
|
@@ -583,7 +604,7 @@ class MessageSplitter extends Transform {
|
|
|
583
604
|
this.push(this.node);
|
|
584
605
|
}
|
|
585
606
|
// move up to the multipart node this closing delimiter belongs to
|
|
586
|
-
let parentNode = this.
|
|
607
|
+
let parentNode = this.delimiterOwner;
|
|
587
608
|
if (parentNode) {
|
|
588
609
|
this.node = parentNode;
|
|
589
610
|
// the closing delimiter of this multipart was just processed, so any
|
|
@@ -662,16 +683,6 @@ class MessageSplitter extends Transform {
|
|
|
662
683
|
next(null, false);
|
|
663
684
|
}
|
|
664
685
|
|
|
665
|
-
/**
|
|
666
|
-
* Resolves the multipart node that owns the boundary of the current node, ie. the
|
|
667
|
-
* node a sibling delimiter or a closing delimiter of _parentBoundary refers to.
|
|
668
|
-
*
|
|
669
|
-
* @returns {MimeNodeType | false} Owner of _parentBoundary or false for the root node.
|
|
670
|
-
*/
|
|
671
|
-
parentMultipartNode() {
|
|
672
|
-
return this.node._parentBoundaryOwner || false;
|
|
673
|
-
}
|
|
674
|
-
|
|
675
686
|
/**
|
|
676
687
|
* @param {MimeNodeType | false} [parent]
|
|
677
688
|
* @returns {void}
|
package/lib/mime-node.js
CHANGED
|
@@ -44,6 +44,55 @@ function stripComments(value) {
|
|
|
44
44
|
.join('');
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Lists the parameters a header value gives in RFC 2231 form (`name*=`, `name*0=` or
|
|
49
|
+
* `name*0*=`). libmime decodes those while parsing the header, so the result must not
|
|
50
|
+
* go through `decodeWords` as well: a value like `filename*=utf-8''a%3D%3Futf-8%3FQ%3F%3D2Eexe%3F%3D`
|
|
51
|
+
* would otherwise come out as `a.exe` while a parser that decodes once sees the encoded word.
|
|
52
|
+
*
|
|
53
|
+
* @param {string} value Raw header value.
|
|
54
|
+
* @returns {Set<string>} Lowercased parameter names.
|
|
55
|
+
*/
|
|
56
|
+
function rfc2231ParamNames(value) {
|
|
57
|
+
let names = new Set();
|
|
58
|
+
let quote = false;
|
|
59
|
+
let escaped = false;
|
|
60
|
+
let start = 0;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* @param {number} end End offset of the segment starting at `start`.
|
|
64
|
+
* @returns {void}
|
|
65
|
+
*/
|
|
66
|
+
let addSegment = end => {
|
|
67
|
+
let segment = value.substring(start, end);
|
|
68
|
+
let eq = segment.indexOf('=');
|
|
69
|
+
if (eq < 0) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
let match = segment.substring(0, eq).trim().toLowerCase().match(/^(.+?)\*(\d+\*?)?$/);
|
|
73
|
+
if (match) {
|
|
74
|
+
names.add(match[1]);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
for (let i = 0; i < value.length; i++) {
|
|
79
|
+
let chr = value.charAt(i);
|
|
80
|
+
if (escaped) {
|
|
81
|
+
escaped = false;
|
|
82
|
+
} else if (chr === '\\') {
|
|
83
|
+
escaped = true;
|
|
84
|
+
} else if (chr === '"') {
|
|
85
|
+
quote = !quote;
|
|
86
|
+
} else if (chr === ';' && !quote) {
|
|
87
|
+
addSegment(i);
|
|
88
|
+
start = i + 1;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
addSegment(value.length);
|
|
92
|
+
|
|
93
|
+
return names;
|
|
94
|
+
}
|
|
95
|
+
|
|
47
96
|
/**
|
|
48
97
|
* Parsed MIME node with mutable headers and transfer-encoding helpers.
|
|
49
98
|
*/
|
|
@@ -142,7 +191,8 @@ class MimeNode {
|
|
|
142
191
|
this.headers = new Headers(Buffer.concat(this._headersLines, this._headerlen), this.config);
|
|
143
192
|
let headers = this.headers;
|
|
144
193
|
|
|
145
|
-
|
|
194
|
+
let dispositionHeader = headers.getFirst('Content-Disposition');
|
|
195
|
+
this._parsedContentDisposition = this.libmime.parseHeaderValue(dispositionHeader);
|
|
146
196
|
let parsedContentDisposition = /** @type {StructuredHeader} */ (this._parsedContentDisposition);
|
|
147
197
|
|
|
148
198
|
// if content-type is missing default to plaintext
|
|
@@ -192,10 +242,16 @@ class MimeNode {
|
|
|
192
242
|
}
|
|
193
243
|
|
|
194
244
|
if (this.filename) {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
245
|
+
// a parameter given in RFC 2231 form was decoded by libmime already
|
|
246
|
+
let decoded = parsedContentDisposition.params.filename
|
|
247
|
+
? rfc2231ParamNames(dispositionHeader).has('filename')
|
|
248
|
+
: rfc2231ParamNames(contentHeader).has('name');
|
|
249
|
+
if (!decoded) {
|
|
250
|
+
try {
|
|
251
|
+
this.filename = this.libmime.decodeWords(this.filename);
|
|
252
|
+
} catch (E) {
|
|
253
|
+
// failed to parse filename, keep as is (most probably an unknown charset is used)
|
|
254
|
+
}
|
|
199
255
|
}
|
|
200
256
|
}
|
|
201
257
|
|