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