@zone-eu/mailsplit 5.4.21 → 5.4.23

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 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
- entry.line += '\r\n' + currentLine;
401
- if (!entry.key) {
405
+ if (keyPending) {
402
406
  // the key comes from the first colon, which may only appear in a continuation
403
- entry.key = this._normalizeHeader(entry.line.substr(0, entry.line.indexOf(':')));
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
- lines.push({ key: this._normalizeHeader(currentLine.substr(0, currentLine.indexOf(':'))), line: currentLine });
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
 
@@ -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
- if (this.node._parentBoundary && (boundary = this.compareBoundary(line, startpos, this.node._parentBoundary))) {
456
- // 3: next sibling
457
- // 4: parent end
458
- return /** @type {3 | 4} */ (boundary + 2);
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
- let maxBoundaryLength = Math.max(
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.parentMultipartNode());
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.parentMultipartNode();
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
- this._parsedContentDisposition = this.libmime.parseHeaderValue(headers.getFirst('Content-Disposition'));
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
- try {
196
- this.filename = this.libmime.decodeWords(this.filename);
197
- } catch (E) {
198
- // failed to parse filename, keep as is (most probably an unknown charset is used)
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zone-eu/mailsplit",
3
- "version": "5.4.21",
3
+ "version": "5.4.23",
4
4
  "description": "Split email messages into an object stream",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -16,8 +16,8 @@
16
16
  "license": "(MIT OR EUPL-1.1+)",
17
17
  "dependencies": {
18
18
  "libbase64": "1.3.3",
19
- "libmime": "5.4.8",
20
- "libqp": "2.1.2"
19
+ "libmime": "5.4.9",
20
+ "libqp": "2.1.3"
21
21
  },
22
22
  "devDependencies": {
23
23
  "@types/eslint": "9.6.1",