@limetech/lime-elements 40.7.0 → 40.7.1
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/CHANGELOG.md +7 -0
- package/dist/cjs/limel-dock-button.cjs.entry.js +2 -4
- package/dist/cjs/limel-file-viewer.cjs.entry.js +420 -101
- package/dist/collection/components/dock/dock-button/dock-button.css +13 -0
- package/dist/collection/components/dock/dock-button/dock-button.js +1 -3
- package/dist/esm/limel-dock-button.entry.js +2 -4
- package/dist/esm/limel-file-viewer.entry.js +420 -101
- package/dist/lime-elements/lime-elements.esm.js +1 -1
- package/dist/lime-elements/p-6e3a4482.entry.js +1 -0
- package/dist/lime-elements/p-9088acea.entry.js +1 -0
- package/package.json +4 -4
- package/dist/lime-elements/p-1740cb87.entry.js +0 -1
- package/dist/lime-elements/p-f3cae1e7.entry.js +0 -1
|
@@ -125,6 +125,8 @@ class Fullscreen {
|
|
|
125
125
|
}
|
|
126
126
|
|
|
127
127
|
const textEncoder = new TextEncoder();
|
|
128
|
+
/** SP or HTAB, the only whitespace RFC 5322 and RFC 2045 give a meaning to in a line */
|
|
129
|
+
const isWsp = (c) => c === 0x20 || c === 0x09;
|
|
128
130
|
const base64Chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
|
129
131
|
// Use a lookup table to find the index.
|
|
130
132
|
const base64Lookup = new Uint8Array(256);
|
|
@@ -342,7 +344,10 @@ function tryDecoder(charset) {
|
|
|
342
344
|
// Every runtime has utf-8, so a decoder is always returned even where windows-1252 is
|
|
343
345
|
// missing, rather than letting the caller fail on a null decoder
|
|
344
346
|
const utf8Decoder = new TextDecoder();
|
|
345
|
-
|
|
347
|
+
/**
|
|
348
|
+
* Finds the decoder for a charset label, or null when the runtime has no encoding for it
|
|
349
|
+
*/
|
|
350
|
+
function findDecoder(charset) {
|
|
346
351
|
charset = (charset || 'utf8').trim().toLowerCase();
|
|
347
352
|
// Try the label as written first, so the alias table only ever adds to what the
|
|
348
353
|
// runtime already supports instead of shadowing it.
|
|
@@ -355,7 +360,136 @@ function getDecoder(charset) {
|
|
|
355
360
|
// The normalized label is itself the last candidate, which resolves everything that
|
|
356
361
|
// differed from a supported label only by a prefix or a separator, eg. x-big5.
|
|
357
362
|
const alias = (codePage && codePageAliases.get(codePage[1])) || charsetAliases.get(normalized) || normalized;
|
|
358
|
-
return tryDecoder(alias)
|
|
363
|
+
return tryDecoder(alias);
|
|
364
|
+
}
|
|
365
|
+
function getDecoder(charset) {
|
|
366
|
+
return findDecoder(charset) || tryDecoder('windows-1252') || utf8Decoder;
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Whether the bytes are well formed UTF-8. A scan rather than a fatal TextDecoder, since
|
|
370
|
+
* the exception one throws costs more than the decode itself and a message decides how
|
|
371
|
+
* many lines throw it.
|
|
372
|
+
*/
|
|
373
|
+
function isUtf8(bytes) {
|
|
374
|
+
for (let i = 0; i < bytes.length;) {
|
|
375
|
+
const lead = bytes[i];
|
|
376
|
+
if (lead < 0x80) {
|
|
377
|
+
i++;
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
// The number of continuation bytes, and the range the first of them may take,
|
|
381
|
+
// which is what rules out overlong forms, surrogates and code points past U+10FFFF
|
|
382
|
+
let count;
|
|
383
|
+
if (lead >= 0xc2 && lead <= 0xdf) {
|
|
384
|
+
count = 1;
|
|
385
|
+
}
|
|
386
|
+
else if (lead >= 0xe0 && lead <= 0xef) {
|
|
387
|
+
count = 2;
|
|
388
|
+
}
|
|
389
|
+
else if (lead >= 0xf0 && lead <= 0xf4) {
|
|
390
|
+
count = 3;
|
|
391
|
+
}
|
|
392
|
+
else {
|
|
393
|
+
return false;
|
|
394
|
+
}
|
|
395
|
+
if (i + count >= bytes.length) {
|
|
396
|
+
return false;
|
|
397
|
+
}
|
|
398
|
+
const second = bytes[i + 1];
|
|
399
|
+
const min = lead === 0xe0 ? 0xa0 : lead === 0xf0 ? 0x90 : 0x80;
|
|
400
|
+
const max = lead === 0xed ? 0x9f : lead === 0xf4 ? 0x8f : 0xbf;
|
|
401
|
+
if (second < min || second > max) {
|
|
402
|
+
return false;
|
|
403
|
+
}
|
|
404
|
+
for (let k = 2; k <= count; k++) {
|
|
405
|
+
if ((bytes[i + k] & 0xc0) !== 0x80) {
|
|
406
|
+
return false;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
i += count + 1;
|
|
410
|
+
}
|
|
411
|
+
return true;
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Decodes bytes whose encoding is not known: as UTF-8 when they are well formed UTF-8, as
|
|
415
|
+
* windows-1252 otherwise.
|
|
416
|
+
*
|
|
417
|
+
* A message that declares no charset, or one the runtime has never heard of, usually
|
|
418
|
+
* predates RFC 6532 or comes from a generator that never heard of it either, and its 8-bit
|
|
419
|
+
* bytes are a legacy single byte encoding far more often than broken UTF-8. windows-1252
|
|
420
|
+
* is the fallback the WHATWG Encoding Standard prescribes and what a mail client's default
|
|
421
|
+
* charset amounts to, and it reads such text rather than filling it with replacement
|
|
422
|
+
* characters.
|
|
423
|
+
*
|
|
424
|
+
* @param bytes Bytes to decode
|
|
425
|
+
* @param utf8 The UTF-8 decoder to use, header lines bring one that keeps a BOM
|
|
426
|
+
* @return Decoded text
|
|
427
|
+
*/
|
|
428
|
+
function decodeUtf8OrWindows1252(bytes, utf8 = utf8Decoder) {
|
|
429
|
+
const view = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
|
|
430
|
+
return isUtf8(view) ? utf8.decode(view) : getDecoder('windows-1252').decode(view);
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Decodes bytes in a named charset. A label the runtime does not know is no charset at
|
|
434
|
+
* all, see decodeUtf8OrWindows1252.
|
|
435
|
+
*
|
|
436
|
+
* @param bytes Bytes to decode
|
|
437
|
+
* @param charset Charset label
|
|
438
|
+
* @return Decoded text
|
|
439
|
+
*/
|
|
440
|
+
function decodeCharset(bytes, charset) {
|
|
441
|
+
const decoder = findDecoder(charset);
|
|
442
|
+
return decoder ? decoder.decode(bytes) : decodeUtf8OrWindows1252(bytes);
|
|
443
|
+
}
|
|
444
|
+
// The charset an html document names for itself, in a `<meta charset>` or in the
|
|
445
|
+
// Content-Type `<meta http-equiv>`, read from the first 1024 bytes like a browser's prescan.
|
|
446
|
+
// Tags are found with indexOf and the attribute is matched inside one tag at a time: a
|
|
447
|
+
// single pattern over the whole head rescans it once for every `<meta` in it, and a head of
|
|
448
|
+
// nothing but `<meta ` took it quadratic
|
|
449
|
+
const META_PRESCAN_BYTES = 1024;
|
|
450
|
+
const CHARSET_ATTRIBUTE = /charset\s*=\s*["']?\s*([^\s"';>]+)/;
|
|
451
|
+
const UTF16_ENCODINGS = new Set(['utf-16le', 'utf-16be']);
|
|
452
|
+
function sniffHtmlCharset(bytes) {
|
|
453
|
+
const head = utf8Decoder.decode(bytes.subarray(0, META_PRESCAN_BYTES)).toLowerCase();
|
|
454
|
+
let pos = 0;
|
|
455
|
+
while ((pos = head.indexOf('<meta', pos)) >= 0) {
|
|
456
|
+
let end = head.indexOf('>', pos);
|
|
457
|
+
if (end < 0) {
|
|
458
|
+
end = head.length;
|
|
459
|
+
}
|
|
460
|
+
const match = CHARSET_ATTRIBUTE.exec(head.slice(pos, end));
|
|
461
|
+
if (match) {
|
|
462
|
+
return match[1];
|
|
463
|
+
}
|
|
464
|
+
pos = end;
|
|
465
|
+
}
|
|
466
|
+
return null;
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* Decodes the bytes of a text part: in the declared charset, otherwise in the one an html
|
|
470
|
+
* part names for itself, otherwise as UTF-8 or windows-1252 depending on what the bytes
|
|
471
|
+
* turn out to be, see decodeUtf8OrWindows1252.
|
|
472
|
+
*
|
|
473
|
+
* @param bytes Decoded body of the part
|
|
474
|
+
* @param charset The charset parameter of the Content-Type, if any
|
|
475
|
+
* @param html Whether the part is html, which may name its own charset
|
|
476
|
+
* @return The text of the part
|
|
477
|
+
*/
|
|
478
|
+
function decodeText(bytes, charset, html) {
|
|
479
|
+
const view = new Uint8Array(bytes);
|
|
480
|
+
if (charset) {
|
|
481
|
+
return decodeCharset(view, charset);
|
|
482
|
+
}
|
|
483
|
+
// The HTML prescan has two overrides, and for a reason: a `<meta>` tag that could be
|
|
484
|
+
// read as ASCII proves the document is not UTF-16, so such a label is read as UTF-8,
|
|
485
|
+
// and x-user-defined is read as windows-1252. Without them a sender picks the encoding
|
|
486
|
+
// that turns bytes a scanner sees as inert into markup for this parser alone
|
|
487
|
+
const sniffed = html ? sniffHtmlCharset(view) : null;
|
|
488
|
+
const decoder = sniffed ? findDecoder(sniffed) : null;
|
|
489
|
+
if (!decoder || UTF16_ENCODINGS.has(decoder.encoding)) {
|
|
490
|
+
return decodeUtf8OrWindows1252(view);
|
|
491
|
+
}
|
|
492
|
+
return (decoder.encoding === 'x-user-defined' ? getDecoder('windows-1252') : decoder).decode(view);
|
|
359
493
|
}
|
|
360
494
|
/**
|
|
361
495
|
* Converts a Blob into an ArrayBuffer
|
|
@@ -395,6 +529,25 @@ function hexNibble(c) {
|
|
|
395
529
|
}
|
|
396
530
|
return -1;
|
|
397
531
|
}
|
|
532
|
+
/**
|
|
533
|
+
* Folds the line breaks of a header value into spaces.
|
|
534
|
+
*
|
|
535
|
+
* A header value holds no line breaks. The raw value is folded when the headers are
|
|
536
|
+
* processed, see processHeaders in mime-node.ts, an RFC 2047 encoded word is folded as it
|
|
537
|
+
* is decoded, and an RFC 2231 filename is folded where the attachment is built. Otherwise a
|
|
538
|
+
* decoded name, subject or filename written into a header of an outgoing message by the
|
|
539
|
+
* consumer ends the header early and starts another one with the rest of the value.
|
|
540
|
+
*
|
|
541
|
+
* The RFC 2231 percent decoder itself does not fold: it also produces the boundary, and a
|
|
542
|
+
* boundary holding a line break can never match a line. That has to stay so, or a message
|
|
543
|
+
* that has no parts for every other parser would have them here.
|
|
544
|
+
*
|
|
545
|
+
* @param str Header value
|
|
546
|
+
* @return The value with every run of CR and LF replaced by a space
|
|
547
|
+
*/
|
|
548
|
+
function foldLineBreaks(str) {
|
|
549
|
+
return str.replace(/[\r\n]+/g, ' ');
|
|
550
|
+
}
|
|
398
551
|
/**
|
|
399
552
|
* Decode a complete mime word encoded string
|
|
400
553
|
*
|
|
@@ -447,7 +600,7 @@ function decodeWord(charset, encoding, str) {
|
|
|
447
600
|
// keep as is, convert ArrayBuffer to unicode string, assume utf8
|
|
448
601
|
byteStr = textEncoder.encode(str);
|
|
449
602
|
}
|
|
450
|
-
return
|
|
603
|
+
return foldLineBreaks(decodeCharset(byteStr, charset));
|
|
451
604
|
}
|
|
452
605
|
// A charset label runs to the next '?' so that labels containing punctuation, eg.
|
|
453
606
|
// ISO_8859-1:1987, are recognised. Whitespace is excluded so a stray '=?' in running text
|
|
@@ -591,7 +744,7 @@ function decodeURIComponentWithCharset(encodedStr, charset) {
|
|
|
591
744
|
for (let i = 0, len = encodedBytes.length; i < len; i++) {
|
|
592
745
|
dataView.setUint8(i, encodedBytes[i]);
|
|
593
746
|
}
|
|
594
|
-
return
|
|
747
|
+
return decodeCharset(byteStr, charset);
|
|
595
748
|
}
|
|
596
749
|
function decodeParameterValueContinuations(header) {
|
|
597
750
|
// handle parameter value continuations
|
|
@@ -658,27 +811,72 @@ function decodeParameterValueContinuations(header) {
|
|
|
658
811
|
});
|
|
659
812
|
}
|
|
660
813
|
|
|
814
|
+
// Hands the body through as it is, the line endings included.
|
|
815
|
+
//
|
|
816
|
+
// The line break is written when the next line arrives, not after each line. The CRLF in
|
|
817
|
+
// front of a multipart boundary belongs to the boundary (RFC 2046 5.1.1), so the last line
|
|
818
|
+
// of a part ended by a boundary must not carry one.
|
|
819
|
+
//
|
|
820
|
+
// The break written is the one the line came with. Writing LF for every line rewrote the
|
|
821
|
+
// bytes of every 7bit, 8bit and binary part: an attached message lost each of its CR and a
|
|
822
|
+
// binary attachment came out corrupted. Text is normalized to LF where it is read as text
|
|
823
|
+
// instead, see MimeNode.getTextContent.
|
|
824
|
+
//
|
|
825
|
+
// The lines of a part, breaks included, are one contiguous range of the message, so the
|
|
826
|
+
// decoder keeps the bounds of that range rather than a chunk per line: a Blob of two parts
|
|
827
|
+
// per line took 60 times longer to assemble than one of a single part.
|
|
661
828
|
class PassThroughDecoder {
|
|
662
829
|
constructor() {
|
|
663
830
|
this.chunks = [];
|
|
664
|
-
this.
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
831
|
+
this.runBuffer = null;
|
|
832
|
+
this.runStart = 0;
|
|
833
|
+
this.runEnd = 0;
|
|
834
|
+
this.pendingLineBreak = null;
|
|
835
|
+
}
|
|
836
|
+
// Whether the bytes sit in the run's buffer right at the end of the run
|
|
837
|
+
continuesRun(bytes) {
|
|
838
|
+
return bytes.buffer === this.runBuffer && bytes.byteOffset === this.runEnd;
|
|
839
|
+
}
|
|
840
|
+
flushRun() {
|
|
841
|
+
if (this.runBuffer && this.runEnd > this.runStart) {
|
|
842
|
+
this.chunks.push(new Uint8Array(this.runBuffer, this.runStart, this.runEnd - this.runStart));
|
|
843
|
+
}
|
|
844
|
+
this.runBuffer = null;
|
|
845
|
+
}
|
|
846
|
+
update(line, lineBreak) {
|
|
847
|
+
const pending = this.pendingLineBreak;
|
|
848
|
+
if (pending &&
|
|
849
|
+
this.continuesRun(pending) &&
|
|
850
|
+
line.buffer === this.runBuffer &&
|
|
851
|
+
line.byteOffset === this.runEnd + pending.byteLength) {
|
|
852
|
+
// the break follows the run and the line follows the break, so the run grows
|
|
853
|
+
this.runEnd = line.byteOffset + line.byteLength;
|
|
854
|
+
}
|
|
855
|
+
else {
|
|
856
|
+
this.flushRun();
|
|
857
|
+
if (pending) {
|
|
858
|
+
this.chunks.push(pending);
|
|
859
|
+
}
|
|
860
|
+
this.runBuffer = line.buffer;
|
|
861
|
+
this.runStart = line.byteOffset;
|
|
862
|
+
this.runEnd = line.byteOffset + line.byteLength;
|
|
672
863
|
}
|
|
673
|
-
this.
|
|
674
|
-
this.pendingLineBreak = true;
|
|
864
|
+
this.pendingLineBreak = lineBreak;
|
|
675
865
|
}
|
|
676
866
|
// A part ended by the end of the message has no boundary to own its final line
|
|
677
867
|
// break, so there it stays content
|
|
678
868
|
finalize(boundaryEnded = false) {
|
|
679
|
-
|
|
680
|
-
|
|
869
|
+
const pending = this.pendingLineBreak;
|
|
870
|
+
if (!boundaryEnded && pending) {
|
|
871
|
+
if (this.continuesRun(pending)) {
|
|
872
|
+
this.runEnd += pending.byteLength;
|
|
873
|
+
}
|
|
874
|
+
else {
|
|
875
|
+
this.flushRun();
|
|
876
|
+
this.chunks.push(pending);
|
|
877
|
+
}
|
|
681
878
|
}
|
|
879
|
+
this.flushRun();
|
|
682
880
|
// convert an array of arraybuffers into a blob and then back into a single arraybuffer
|
|
683
881
|
return blobToArrayBuffer(new Blob(this.chunks, { type: 'application/octet-stream' }));
|
|
684
882
|
}
|
|
@@ -727,14 +925,18 @@ class Base64Decoder {
|
|
|
727
925
|
}
|
|
728
926
|
|
|
729
927
|
const CHR_EQUALS = 0x3d;
|
|
730
|
-
const CHR_LF = 0x0a;
|
|
731
928
|
class QPDecoder {
|
|
732
929
|
constructor() {
|
|
733
930
|
this.maxChunkSize = 100 * 1024;
|
|
734
931
|
this.buffer = new Uint8Array(this.maxChunkSize);
|
|
735
932
|
this.bufferPos = 0;
|
|
736
933
|
this.chunks = [];
|
|
737
|
-
this.pendingLineBreak =
|
|
934
|
+
this.pendingLineBreak = null;
|
|
935
|
+
}
|
|
936
|
+
writeLineBreak() {
|
|
937
|
+
if (this.pendingLineBreak) {
|
|
938
|
+
this.writeBytes(this.pendingLineBreak, 0, this.pendingLineBreak.length);
|
|
939
|
+
}
|
|
738
940
|
}
|
|
739
941
|
writeByte(byte) {
|
|
740
942
|
if (this.bufferPos >= this.buffer.length) {
|
|
@@ -767,12 +969,17 @@ class QPDecoder {
|
|
|
767
969
|
// content that decoded correctly in base64 came out as mojibake in quoted-printable.
|
|
768
970
|
//
|
|
769
971
|
// As in PassThroughDecoder, a hard line break is only written once the next line
|
|
770
|
-
// arrives, so the CRLF that belongs to a following boundary is not decoded as content
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
}
|
|
972
|
+
// arrives, so the CRLF that belongs to a following boundary is not decoded as content,
|
|
973
|
+
// and the break written is the one the line came with.
|
|
974
|
+
update(line, lineBreak) {
|
|
975
|
+
this.writeLineBreak();
|
|
775
976
|
let len = line.length;
|
|
977
|
+
// Trailing white space on an encoded line was added in transit and is deleted (RFC
|
|
978
|
+
// 2045 6.7 rule 3). Before the soft line break is looked for, since the `=` it
|
|
979
|
+
// consists of may have had white space appended the same way
|
|
980
|
+
while (len > 0 && isWsp(line[len - 1])) {
|
|
981
|
+
len--;
|
|
982
|
+
}
|
|
776
983
|
// a line ending in '=' is a soft line break, the newline is not part of the content
|
|
777
984
|
const softBreak = len > 0 && line[len - 1] === CHR_EQUALS;
|
|
778
985
|
if (softBreak) {
|
|
@@ -795,12 +1002,12 @@ class QPDecoder {
|
|
|
795
1002
|
literalStart = i + 1;
|
|
796
1003
|
}
|
|
797
1004
|
this.writeBytes(line, literalStart, len);
|
|
798
|
-
this.pendingLineBreak =
|
|
1005
|
+
this.pendingLineBreak = softBreak ? null : lineBreak;
|
|
799
1006
|
}
|
|
800
1007
|
// see PassThroughDecoder.finalize
|
|
801
1008
|
finalize(boundaryEnded = false) {
|
|
802
|
-
if (!boundaryEnded
|
|
803
|
-
this.
|
|
1009
|
+
if (!boundaryEnded) {
|
|
1010
|
+
this.writeLineBreak();
|
|
804
1011
|
}
|
|
805
1012
|
this.flushBuffer();
|
|
806
1013
|
// convert an array of arraybuffers into a blob and then back into a single arraybuffer
|
|
@@ -812,6 +1019,10 @@ class QPDecoder {
|
|
|
812
1019
|
// kept as a character instead of being swallowed. A stripped BOM turns a line a strict
|
|
813
1020
|
// parser skips into a genuine header, which is how a second `From:` gets smuggled past
|
|
814
1021
|
// anything that inspects the raw message.
|
|
1022
|
+
//
|
|
1023
|
+
// Raw 8-bit header text is UTF-8 (RFC 6532), and a line that is not is legacy mail in a
|
|
1024
|
+
// single byte charset, read as windows-1252 rather than as replacement characters, see
|
|
1025
|
+
// decodeUtf8OrWindows1252.
|
|
815
1026
|
const headerDecoder = new TextDecoder('utf-8', { ignoreBOM: true });
|
|
816
1027
|
// Trims only the whitespace RFC 5322 allows around a field name. String.prototype.trim
|
|
817
1028
|
// also strips U+00A0, U+FEFF, U+2028 and the rest of the Unicode spaces, which turns a
|
|
@@ -823,7 +1034,6 @@ const headerDecoder = new TextDecoder('utf-8', { ignoreBOM: true });
|
|
|
823
1034
|
// An index scan rather than `/^[ \t]+|[ \t]+$/g`, which retries the trailing branch at
|
|
824
1035
|
// every position of a blank run that is followed by other text, so a single header with
|
|
825
1036
|
// a long run of spaces in the middle took seconds to trim.
|
|
826
|
-
const isWsp = (c) => c === 0x20 || c === 0x09;
|
|
827
1037
|
const trimWsp = (str) => {
|
|
828
1038
|
let start = 0;
|
|
829
1039
|
let end = str.length;
|
|
@@ -835,6 +1045,12 @@ const trimWsp = (str) => {
|
|
|
835
1045
|
}
|
|
836
1046
|
return str.slice(start, end);
|
|
837
1047
|
};
|
|
1048
|
+
// A line of nothing but SP and HTAB, see feed
|
|
1049
|
+
const isBlankLine = (line) => line.every(isWsp);
|
|
1050
|
+
// The start of a header field: a field name of printable ASCII other than the colon
|
|
1051
|
+
// (RFC 5322 3.6.8 ftext) and the colon itself, with the whitespace in between that
|
|
1052
|
+
// processHeaders tolerates as well
|
|
1053
|
+
const FIELD_START = /^[!-9;-~]+[ \t]*:/;
|
|
838
1054
|
// Headers that decide how this part's body is read, see processHeaders
|
|
839
1055
|
const CONTENT_HEADERS = new Set([
|
|
840
1056
|
'content-type',
|
|
@@ -847,6 +1063,12 @@ class MimeNode {
|
|
|
847
1063
|
constructor(options) {
|
|
848
1064
|
this.options = options;
|
|
849
1065
|
this.postalMime = options.postalMime;
|
|
1066
|
+
// Counted across the whole message like the header size, so that a multipart can
|
|
1067
|
+
// not carry the budget again for every part it declares
|
|
1068
|
+
this.postalMime.partCount++;
|
|
1069
|
+
if (this.postalMime.partCount > options.maxPartCount) {
|
|
1070
|
+
throw new Error(`Maximum MIME part count of ${options.maxPartCount} parts exceeded`);
|
|
1071
|
+
}
|
|
850
1072
|
this.childNodes = [];
|
|
851
1073
|
// Cursor into childNodes for finalizeChildNodes. Every new part of a multipart
|
|
852
1074
|
// finalizes its parent's children, so re-walking the whole array each time is
|
|
@@ -865,6 +1087,7 @@ class MimeNode {
|
|
|
865
1087
|
}
|
|
866
1088
|
this.state = 'header';
|
|
867
1089
|
this.headerLines = [];
|
|
1090
|
+
this.lastLineBlank = false;
|
|
868
1091
|
// RFC 2046 Section 5.1.5: multipart/digest defaults to message/rfc822
|
|
869
1092
|
const parentMultipartType = options.parentMultipartType || null;
|
|
870
1093
|
const defaultContentType = parentMultipartType === 'digest' ? 'message/rfc822' : 'text/plain';
|
|
@@ -906,7 +1129,7 @@ class MimeNode {
|
|
|
906
1129
|
return;
|
|
907
1130
|
}
|
|
908
1131
|
if (this.state === 'header') {
|
|
909
|
-
this.
|
|
1132
|
+
this.endHeaders();
|
|
910
1133
|
}
|
|
911
1134
|
// remove self from boundary listing
|
|
912
1135
|
let boundaries = this.postalMime.boundaries;
|
|
@@ -1157,55 +1380,51 @@ class MimeNode {
|
|
|
1157
1380
|
decodeParameterValueContinuations(response);
|
|
1158
1381
|
return response;
|
|
1159
1382
|
}
|
|
1383
|
+
// Unfolds format=flowed text (RFC 3676 4.1). A line is read as its quote marks, one
|
|
1384
|
+
// space of stuffing and its content, in that order, and a line whose content ends in a
|
|
1385
|
+
// space is flowed: the next line of the same quote depth continues its paragraph. The
|
|
1386
|
+
// paragraph ends at a fixed line, at a change of quote depth and at the signature
|
|
1387
|
+
// separator, which is neither flowed nor fixed. Joining a quoted paragraph used to
|
|
1388
|
+
// leave the quote marks of the continuation lines inside the text, so a quoted reply
|
|
1389
|
+
// read `> one > two` instead of `> one two`.
|
|
1390
|
+
//
|
|
1391
|
+
// The quote marks and stuffing of the first line of a paragraph are kept as they were
|
|
1392
|
+
// written, so a fixed quoted line comes out exactly as it went in. An unquoted stuffed
|
|
1393
|
+
// line loses its stuffing, which is all the stuffing is for.
|
|
1160
1394
|
decodeFlowedText(str, delSp) {
|
|
1161
|
-
// Pieces of the result, joined once at the end. Growing a single string
|
|
1162
|
-
//
|
|
1163
|
-
//
|
|
1164
|
-
// Empty pieces are never stored, so the last piece always holds the last
|
|
1165
|
-
// character of the result.
|
|
1395
|
+
// Pieces of the result, joined once at the end. Growing a single string instead
|
|
1396
|
+
// flattens the whole paragraph again on each line, which is quadratic in the
|
|
1397
|
+
// length of a paragraph.
|
|
1166
1398
|
const parts = [];
|
|
1167
|
-
//
|
|
1168
|
-
|
|
1169
|
-
let
|
|
1170
|
-
|
|
1171
|
-
const lines = str.split(/\r?\n/);
|
|
1399
|
+
// whether the previous line was flowed, and the quote depth of its paragraph
|
|
1400
|
+
let flowed = false;
|
|
1401
|
+
let depth = 0;
|
|
1402
|
+
const lines = str.split('\n');
|
|
1172
1403
|
for (let i = 0; i < lines.length; i++) {
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
// sitting in the middle of the joined paragraph
|
|
1178
|
-
if (line.charAt(0) === ' ') {
|
|
1179
|
-
line = line.slice(1);
|
|
1404
|
+
const line = lines[i];
|
|
1405
|
+
let quoteDepth = 0;
|
|
1406
|
+
while (line.charCodeAt(quoteDepth) === 0x3e /* > */) {
|
|
1407
|
+
quoteDepth++;
|
|
1180
1408
|
}
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
// these spaces can be removed once the text is unfolded
|
|
1190
|
-
if (last.length > 1) {
|
|
1191
|
-
parts[parts.length - 1] = last.slice(0, -1);
|
|
1192
|
-
}
|
|
1193
|
-
else {
|
|
1194
|
-
parts.pop();
|
|
1195
|
-
}
|
|
1196
|
-
lineLength--;
|
|
1197
|
-
}
|
|
1409
|
+
const contentStart = line.charCodeAt(quoteDepth) === 0x20 ? quoteDepth + 1 : quoteDepth;
|
|
1410
|
+
const content = line.slice(contentStart);
|
|
1411
|
+
const isSignature = content === '-- ';
|
|
1412
|
+
if (flowed && quoteDepth === depth && !isSignature) {
|
|
1413
|
+
if (delSp) {
|
|
1414
|
+
// delsp adds the space to make the text foldable, so it is not content
|
|
1415
|
+
const last = parts[parts.length - 1];
|
|
1416
|
+
parts[parts.length - 1] = last.slice(0, -1);
|
|
1198
1417
|
}
|
|
1199
|
-
|
|
1418
|
+
parts.push(content);
|
|
1419
|
+
}
|
|
1420
|
+
else {
|
|
1421
|
+
if (i) {
|
|
1200
1422
|
parts.push('\n');
|
|
1201
|
-
lineStart = parts.length;
|
|
1202
|
-
lineLength = 0;
|
|
1203
1423
|
}
|
|
1424
|
+
parts.push(quoteDepth ? line : content);
|
|
1204
1425
|
}
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
lineLength += line.length;
|
|
1208
|
-
}
|
|
1426
|
+
flowed = !isSignature && content.endsWith(' ');
|
|
1427
|
+
depth = quoteDepth;
|
|
1209
1428
|
}
|
|
1210
1429
|
return parts.join('');
|
|
1211
1430
|
}
|
|
@@ -1213,7 +1432,10 @@ class MimeNode {
|
|
|
1213
1432
|
if (!this.content) {
|
|
1214
1433
|
return '';
|
|
1215
1434
|
}
|
|
1216
|
-
|
|
1435
|
+
// The bytes keep the line endings the part was sent with, see PassThroughDecoder,
|
|
1436
|
+
// which differ from one transfer encoding to the next. Text is normalized to LF so
|
|
1437
|
+
// that a consumer does not get CRLF from one message and LF from another
|
|
1438
|
+
let str = decodeText(this.content, this.contentType.parsed.params.charset, this.contentType.parsed.value === 'text/html').replace(/\r\n/g, '\n');
|
|
1217
1439
|
if (/^flowed$/i.test(this.contentType.parsed.params.format)) {
|
|
1218
1440
|
str = this.decodeFlowedText(str, /^yes$/i.test(this.contentType.parsed.params.delsp));
|
|
1219
1441
|
}
|
|
@@ -1259,10 +1481,8 @@ class MimeNode {
|
|
|
1259
1481
|
let unfoldedLine = parts.join('');
|
|
1260
1482
|
sep = unfoldedLine.indexOf(':');
|
|
1261
1483
|
let key = trimWsp(sep < 0 ? unfoldedLine : unfoldedLine.slice(0, sep));
|
|
1262
|
-
// A bare CR is not legal in a field body
|
|
1263
|
-
|
|
1264
|
-
// write the value back out a line of their own.
|
|
1265
|
-
let value = sep < 0 ? '' : trimWsp(unfoldedLine.slice(sep + 1).replace(/[\r\n]+/g, ' '));
|
|
1484
|
+
// A bare CR is not legal in a field body, see foldLineBreaks
|
|
1485
|
+
let value = sep < 0 ? '' : trimWsp(foldLineBreaks(unfoldedLine.slice(sep + 1)));
|
|
1266
1486
|
this.headers.push({ key: key.toLowerCase(), originalKey: key, value });
|
|
1267
1487
|
// A header that decides how the body is read must resolve the same way every
|
|
1268
1488
|
// time it is duplicated, otherwise a message can present one Content-Type to a
|
|
@@ -1294,7 +1514,15 @@ class MimeNode {
|
|
|
1294
1514
|
this.contentType.multipart = /^multipart\//i.test(this.contentType.parsed.value)
|
|
1295
1515
|
? this.contentType.parsed.value.slice(this.contentType.parsed.value.indexOf('/') + 1)
|
|
1296
1516
|
: false;
|
|
1297
|
-
if (this.contentType.multipart && this.contentType.parsed.params.boundary) {
|
|
1517
|
+
if (this.contentType.multipart && !this.contentType.parsed.params.boundary) {
|
|
1518
|
+
// A multipart without the boundary RFC 2046 5.1.1 requires has no parts to find,
|
|
1519
|
+
// and its body used to be dropped without a trace. RFC 2045 5.2 recommends
|
|
1520
|
+
// reading an invalid Content-Type as plain text, which is also what the body of
|
|
1521
|
+
// such a message usually is
|
|
1522
|
+
this.contentType.parsed.value = 'text/plain';
|
|
1523
|
+
this.contentType.multipart = false;
|
|
1524
|
+
}
|
|
1525
|
+
if (this.contentType.multipart) {
|
|
1298
1526
|
// add self to boundary terminator listing
|
|
1299
1527
|
this.postalMime.boundaries.push({
|
|
1300
1528
|
value: textEncoder.encode(this.contentType.parsed.params.boundary),
|
|
@@ -1311,13 +1539,20 @@ class MimeNode {
|
|
|
1311
1539
|
.match(/[\w-]+/) || [''])[0];
|
|
1312
1540
|
this.setupContentDecoder(this.contentTransferEncoding.encoding);
|
|
1313
1541
|
}
|
|
1314
|
-
|
|
1542
|
+
// Ends the header section and sets up the body
|
|
1543
|
+
endHeaders() {
|
|
1544
|
+
this.state = 'body';
|
|
1545
|
+
this.processHeaders();
|
|
1546
|
+
}
|
|
1547
|
+
// Returns true when the line was not consumed: the headers ended with the line before
|
|
1548
|
+
// it, and the parser has to read it again, since the boundary this part declares was
|
|
1549
|
+
// not known yet when the line was checked for one
|
|
1550
|
+
feed(line, lineBreak) {
|
|
1315
1551
|
switch (this.state) {
|
|
1316
|
-
case 'header':
|
|
1552
|
+
case 'header': {
|
|
1317
1553
|
if (!line.length) {
|
|
1318
|
-
this.
|
|
1319
|
-
|
|
1320
|
-
return;
|
|
1554
|
+
this.endHeaders();
|
|
1555
|
+
return false;
|
|
1321
1556
|
}
|
|
1322
1557
|
// Counted across the whole message, not per part. A per-node budget lets a
|
|
1323
1558
|
// multipart carry the limit again for every part it declares, so a message
|
|
@@ -1327,17 +1562,34 @@ class MimeNode {
|
|
|
1327
1562
|
let error = new Error(`Maximum header size of ${this.options.maxHeadersSize} bytes exceeded`);
|
|
1328
1563
|
throw error;
|
|
1329
1564
|
}
|
|
1330
|
-
|
|
1565
|
+
const text = decodeUtf8OrWindows1252(line, headerDecoder);
|
|
1566
|
+
// A line of nothing but whitespace is legal folding (RFC 5322 4.2, obs-FWS),
|
|
1567
|
+
// but broken generators also put one where the empty line that ends the
|
|
1568
|
+
// headers belongs, and reading it as a fold then turned the whole body into
|
|
1569
|
+
// one more header. So the next line decides what it was: a continuation or
|
|
1570
|
+
// another header keep it a fold, anything else means the headers ended with
|
|
1571
|
+
// it, and it is taken back out of the header lines. Python and Go read such
|
|
1572
|
+
// a line as the end of the headers outright, so the body is where the other
|
|
1573
|
+
// parsers find it.
|
|
1574
|
+
if (this.lastLineBlank && !isWsp(line[0]) && !FIELD_START.test(text)) {
|
|
1575
|
+
this.headerLines.pop();
|
|
1576
|
+
this.endHeaders();
|
|
1577
|
+
return true;
|
|
1578
|
+
}
|
|
1579
|
+
this.headerLines.push(text);
|
|
1580
|
+
this.lastLineBlank = isBlankLine(line);
|
|
1331
1581
|
break;
|
|
1582
|
+
}
|
|
1332
1583
|
case 'body':
|
|
1333
1584
|
// add line to body. processHeaders installs a decoder before the state
|
|
1334
1585
|
// becomes body, and the only time the decoder is released is finalize,
|
|
1335
1586
|
// which moves the state on to finished, so the guard only narrows the type
|
|
1336
1587
|
if (this.contentDecoder) {
|
|
1337
|
-
this.contentDecoder.update(line);
|
|
1588
|
+
this.contentDecoder.update(line, lineBreak);
|
|
1338
1589
|
}
|
|
1339
1590
|
break;
|
|
1340
1591
|
}
|
|
1592
|
+
return false;
|
|
1341
1593
|
}
|
|
1342
1594
|
}
|
|
1343
1595
|
|
|
@@ -3576,6 +3828,39 @@ const htmlEntities = {
|
|
|
3576
3828
|
'‌': '\u200C'
|
|
3577
3829
|
};
|
|
3578
3830
|
|
|
3831
|
+
// What a numeric character reference to the C1 control range stands for, per the HTML
|
|
3832
|
+
// numeric character reference end state: the windows-1252 character at that byte. This is
|
|
3833
|
+
// how Outlook and Word write their curly quotes and dashes (`’`, `–`), and the
|
|
3834
|
+
// raw control character they decoded to otherwise is invisible in the text.
|
|
3835
|
+
const C1_ENTITY_REPLACEMENTS = new Map([
|
|
3836
|
+
[0x80, 0x20ac],
|
|
3837
|
+
[0x82, 0x201a],
|
|
3838
|
+
[0x83, 0x0192],
|
|
3839
|
+
[0x84, 0x201e],
|
|
3840
|
+
[0x85, 0x2026],
|
|
3841
|
+
[0x86, 0x2020],
|
|
3842
|
+
[0x87, 0x2021],
|
|
3843
|
+
[0x88, 0x02c6],
|
|
3844
|
+
[0x89, 0x2030],
|
|
3845
|
+
[0x8a, 0x0160],
|
|
3846
|
+
[0x8b, 0x2039],
|
|
3847
|
+
[0x8c, 0x0152],
|
|
3848
|
+
[0x8e, 0x017d],
|
|
3849
|
+
[0x91, 0x2018],
|
|
3850
|
+
[0x92, 0x2019],
|
|
3851
|
+
[0x93, 0x201c],
|
|
3852
|
+
[0x94, 0x201d],
|
|
3853
|
+
[0x95, 0x2022],
|
|
3854
|
+
[0x96, 0x2013],
|
|
3855
|
+
[0x97, 0x2014],
|
|
3856
|
+
[0x98, 0x02dc],
|
|
3857
|
+
[0x99, 0x2122],
|
|
3858
|
+
[0x9a, 0x0161],
|
|
3859
|
+
[0x9b, 0x203a],
|
|
3860
|
+
[0x9c, 0x0153],
|
|
3861
|
+
[0x9e, 0x017e],
|
|
3862
|
+
[0x9f, 0x0178]
|
|
3863
|
+
]);
|
|
3579
3864
|
function decodeHTMLEntities(str) {
|
|
3580
3865
|
return str.replace(/&(#\d+|#x[a-f0-9]+|[a-z]+\d*);?/gi, (match, entity) => {
|
|
3581
3866
|
if (typeof htmlEntities[match] === 'string') {
|
|
@@ -3586,19 +3871,23 @@ function decodeHTMLEntities(str) {
|
|
|
3586
3871
|
return match;
|
|
3587
3872
|
}
|
|
3588
3873
|
let codePoint;
|
|
3589
|
-
|
|
3590
|
-
|
|
3874
|
+
// The pattern matches the marker in either case, so the test has to as well. Reading
|
|
3875
|
+
// `A` as decimal gave NaN, which passes every range check below and comes out
|
|
3876
|
+
// of String.fromCharCode as a NUL character
|
|
3877
|
+
if (entity.charAt(1).toLowerCase() === 'x') {
|
|
3591
3878
|
codePoint = parseInt(entity.slice(2), 16);
|
|
3592
3879
|
}
|
|
3593
3880
|
else {
|
|
3594
|
-
// dec
|
|
3595
3881
|
codePoint = parseInt(entity.slice(1), 10);
|
|
3596
3882
|
}
|
|
3597
3883
|
let output = '';
|
|
3598
|
-
|
|
3599
|
-
|
|
3884
|
+
// A NUL reference, a surrogate or a code point out of range is a replacement
|
|
3885
|
+
// character, like in a browser. A NUL in particular would truncate the text in
|
|
3886
|
+
// whatever reads it next
|
|
3887
|
+
if (codePoint === 0 || (codePoint >= 0xd800 && codePoint <= 0xdfff) || codePoint > 0x10ffff) {
|
|
3600
3888
|
return '\ufffd';
|
|
3601
3889
|
}
|
|
3890
|
+
codePoint = C1_ENTITY_REPLACEMENTS.get(codePoint) ?? codePoint;
|
|
3602
3891
|
if (codePoint > 0xffff) {
|
|
3603
3892
|
codePoint -= 0x10000;
|
|
3604
3893
|
output += String.fromCharCode(((codePoint >>> 10) & 0x3ff) | 0xd800);
|
|
@@ -3697,13 +3986,17 @@ function replaceTags(str, prefix, pattern, replacement) {
|
|
|
3697
3986
|
break;
|
|
3698
3987
|
}
|
|
3699
3988
|
const start = candidate.index;
|
|
3989
|
+
// Looked up before the pattern runs. The engine finds out on its own that there is
|
|
3990
|
+
// no `>` to close on, but only after it has retried the tail of the pattern at every
|
|
3991
|
+
// `href` in the rest of the input and rescanned to the end each time, which made an
|
|
3992
|
+
// html part of many unclosed `<a href` tags quadratic to convert
|
|
3993
|
+
const gt = str.indexOf('>', start);
|
|
3994
|
+
if (gt < 0) {
|
|
3995
|
+
break;
|
|
3996
|
+
}
|
|
3700
3997
|
pattern.lastIndex = start;
|
|
3701
3998
|
const match = pattern.exec(str);
|
|
3702
3999
|
if (!match) {
|
|
3703
|
-
const gt = str.indexOf('>', start);
|
|
3704
|
-
if (gt < 0) {
|
|
3705
|
-
break;
|
|
3706
|
-
}
|
|
3707
4000
|
searchFrom = gt + 1;
|
|
3708
4001
|
continue;
|
|
3709
4002
|
}
|
|
@@ -4729,7 +5022,11 @@ class Tokenizer {
|
|
|
4729
5022
|
this.escaped = false;
|
|
4730
5023
|
return;
|
|
4731
5024
|
}
|
|
4732
|
-
else if (this.operatorExpecting === '"' && chr === '\\') {
|
|
5025
|
+
else if ((this.operatorExpecting === '"' || this.operatorExpecting === ')') && chr === '\\') {
|
|
5026
|
+
// A backslash escapes the next character inside a quoted string and inside a
|
|
5027
|
+
// comment alike (RFC 5322 3.2.2, ccontent includes quoted-pair). Honouring it
|
|
5028
|
+
// only in quoted strings let an escaped parenthesis close the comment, which
|
|
5029
|
+
// released the rest of it, an address included, into the header
|
|
4733
5030
|
this.escaped = true;
|
|
4734
5031
|
return;
|
|
4735
5032
|
}
|
|
@@ -4914,8 +5211,14 @@ function renderEntry(textEntry, textType, convert) {
|
|
|
4914
5211
|
}
|
|
4915
5212
|
return textType === 'html' ? textToHtml(textEntry.value) : htmlToText(textEntry.value);
|
|
4916
5213
|
}
|
|
5214
|
+
// The line break given to the last line of a message that ends without one, see readLine
|
|
5215
|
+
const LINE_FEED = new Uint8Array([0x0a]);
|
|
4917
5216
|
const MAX_NESTING_DEPTH = 256;
|
|
4918
5217
|
const MAX_HEADERS_SIZE = 2 * 1024 * 1024;
|
|
5218
|
+
// The depth limit does not bound the size of the tree. An empty part costs six bytes of
|
|
5219
|
+
// input (`--b` and a blank line) and over a kilobyte of parser state at its peak, so a
|
|
5220
|
+
// flat multipart of a million empty parts took a 7 MB message to 1.4 GB of memory.
|
|
5221
|
+
const MAX_PART_COUNT = 10000;
|
|
4919
5222
|
// Inline message/rfc822 parts are parsed recursively. Without a dedicated limit
|
|
4920
5223
|
// each nesting level spawns a new parser that retains the full nested message,
|
|
4921
5224
|
// so a small crafted email can exhaust memory (OOM crash). Cap the recursion and
|
|
@@ -4953,7 +5256,8 @@ class PostalMime {
|
|
|
4953
5256
|
this.options = options || {};
|
|
4954
5257
|
this.mimeOptions = {
|
|
4955
5258
|
maxNestingDepth: parseLimitOption(this.options.maxNestingDepth, MAX_NESTING_DEPTH, 'maxNestingDepth'),
|
|
4956
|
-
maxHeadersSize: parseLimitOption(this.options.maxHeadersSize, MAX_HEADERS_SIZE, 'maxHeadersSize')
|
|
5259
|
+
maxHeadersSize: parseLimitOption(this.options.maxHeadersSize, MAX_HEADERS_SIZE, 'maxHeadersSize'),
|
|
5260
|
+
maxPartCount: parseLimitOption(this.options.maxPartCount, MAX_PART_COUNT, 'maxPartCount')
|
|
4957
5261
|
};
|
|
4958
5262
|
// A limit of 0 disables inline parsing entirely, so every message/rfc822 part
|
|
4959
5263
|
// becomes an attachment.
|
|
@@ -4962,13 +5266,15 @@ class PostalMime {
|
|
|
4962
5266
|
// It is deliberately not an option, so that forwarding a caller supplied options
|
|
4963
5267
|
// object can not seed it and switch the recursion limit off.
|
|
4964
5268
|
this.rfc822NestingDepth = 0;
|
|
5269
|
+
// Header bytes seen across every part of this message, see MimeNode.feed, and the
|
|
5270
|
+
// number of parts created for it, the root included, see the MimeNode constructor
|
|
5271
|
+
this.headerSize = 0;
|
|
5272
|
+
this.partCount = 0;
|
|
4965
5273
|
this.root = this.currentNode = new MimeNode({
|
|
4966
5274
|
postalMime: this,
|
|
4967
5275
|
...this.mimeOptions
|
|
4968
5276
|
});
|
|
4969
5277
|
this.boundaries = [];
|
|
4970
|
-
// Header bytes seen across every part of this message, see MimeNode.feed
|
|
4971
|
-
this.headerSize = 0;
|
|
4972
5278
|
this.textContent = {};
|
|
4973
5279
|
this.textMap = new Map();
|
|
4974
5280
|
this.textTypes = new Set();
|
|
@@ -4988,7 +5294,7 @@ class PostalMime {
|
|
|
4988
5294
|
await this.root.finalize();
|
|
4989
5295
|
}
|
|
4990
5296
|
/** @internal */
|
|
4991
|
-
async processLine(line, isFinal) {
|
|
5297
|
+
async processLine(line, lineBreak, isFinal) {
|
|
4992
5298
|
let boundaries = this.boundaries;
|
|
4993
5299
|
// check if this is a mime boundary
|
|
4994
5300
|
if (boundaries.length && line.length > 2 && line[0] === 0x2d && line[1] === 0x2d) {
|
|
@@ -5022,7 +5328,7 @@ class PostalMime {
|
|
|
5022
5328
|
// RFC 2046: boundary line may have trailing whitespace (space/tab) before CRLF
|
|
5023
5329
|
let hasValidTrailing = true;
|
|
5024
5330
|
for (let j = boundaryEnd; j < line.length; j++) {
|
|
5025
|
-
if (line[j]
|
|
5331
|
+
if (!isWsp(line[j])) {
|
|
5026
5332
|
hasValidTrailing = false;
|
|
5027
5333
|
break;
|
|
5028
5334
|
}
|
|
@@ -5050,11 +5356,18 @@ class PostalMime {
|
|
|
5050
5356
|
return;
|
|
5051
5357
|
}
|
|
5052
5358
|
}
|
|
5053
|
-
this.currentNode.feed(line)
|
|
5359
|
+
if (this.currentNode.feed(line, lineBreak)) {
|
|
5360
|
+
// The headers ended with the line before this one, so the boundary this part
|
|
5361
|
+
// declares was not registered when the line was checked above. Read it again
|
|
5362
|
+
return this.processLine(line, lineBreak, isFinal);
|
|
5363
|
+
}
|
|
5054
5364
|
if (isFinal) {
|
|
5055
5365
|
return this.finalize();
|
|
5056
5366
|
}
|
|
5057
5367
|
}
|
|
5368
|
+
// Reads the next line without its line break. The break itself, the exact bytes between
|
|
5369
|
+
// the line and the next one, is returned alongside, because the body decoders write it
|
|
5370
|
+
// back out: a part has to keep the line endings it was sent with, see PassThroughDecoder.
|
|
5058
5371
|
/** @internal */
|
|
5059
5372
|
readLine() {
|
|
5060
5373
|
let startPos = this.readPos;
|
|
@@ -5067,12 +5380,16 @@ class PostalMime {
|
|
|
5067
5380
|
if (c === 0x0a) {
|
|
5068
5381
|
return {
|
|
5069
5382
|
bytes: new Uint8Array(this.buf, startPos, endPos - startPos),
|
|
5383
|
+
lineBreak: this.av.subarray(endPos, this.readPos),
|
|
5070
5384
|
done: this.readPos >= this.av.length
|
|
5071
5385
|
};
|
|
5072
5386
|
}
|
|
5073
5387
|
}
|
|
5388
|
+
// A message that ends without a line break still gets one after its last line, the
|
|
5389
|
+
// decoders write it out unless a boundary ended the part
|
|
5074
5390
|
return {
|
|
5075
5391
|
bytes: new Uint8Array(this.buf, startPos, endPos - startPos),
|
|
5392
|
+
lineBreak: LINE_FEED,
|
|
5076
5393
|
done: this.readPos >= this.av.length
|
|
5077
5394
|
};
|
|
5078
5395
|
}
|
|
@@ -5153,7 +5470,9 @@ class PostalMime {
|
|
|
5153
5470
|
const filename = node.contentDisposition.parsed.params.filename || node.contentType.parsed.params.name || null;
|
|
5154
5471
|
// `content` is filled in below once the part type is known, hence the cast
|
|
5155
5472
|
const attachment = {
|
|
5156
|
-
|
|
5473
|
+
// An RFC 2231 filename is percent decoded without folding, since the same decoder
|
|
5474
|
+
// produces the boundary, so the fold happens here, see foldLineBreaks
|
|
5475
|
+
filename: filename ? decodeWords(foldLineBreaks(filename)) : null,
|
|
5157
5476
|
mimeType: node.contentType.parsed.value,
|
|
5158
5477
|
disposition: node.contentDisposition.parsed.value || null
|
|
5159
5478
|
};
|
|
@@ -5186,7 +5505,7 @@ class PostalMime {
|
|
|
5186
5505
|
// Enforce into unicode, ending in exactly one newline. The trailing
|
|
5187
5506
|
// newlines are counted rather than replaced with `/\n*$/`, which
|
|
5188
5507
|
// retries at every newline of a run that does not end the text.
|
|
5189
|
-
const decodedText = node.getTextContent()
|
|
5508
|
+
const decodedText = node.getTextContent();
|
|
5190
5509
|
let end = decodedText.length;
|
|
5191
5510
|
while (end > 0 && decodedText.charCodeAt(end - 1) === 0x0a) {
|
|
5192
5511
|
end--;
|
|
@@ -5435,7 +5754,7 @@ class PostalMime {
|
|
|
5435
5754
|
this.readPos = 0;
|
|
5436
5755
|
while (this.readPos < this.av.length) {
|
|
5437
5756
|
const line = this.readLine();
|
|
5438
|
-
await this.processLine(line.bytes, line.done);
|
|
5757
|
+
await this.processLine(line.bytes, line.lineBreak, line.done);
|
|
5439
5758
|
}
|
|
5440
5759
|
this.forceRfc822Attachments = Boolean(this.options.forceRfc822Attachments) || this.hasReportParts();
|
|
5441
5760
|
await this.collectNode(this.root, false, false);
|