@limetech/lime-elements 40.6.0 → 40.6.2
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 +15 -0
- package/dist/cjs/{adapt-color-contrast-Beo1IEl_.js → adapt-color-contrast-h6iwOfZd.js} +1 -1
- package/dist/cjs/{checkbox.template-BPk8WkQF.js → checkbox.template-CoNVC4Nf.js} +2 -2
- package/dist/cjs/{component-CFk7Dfoi.js → component-Cz24PdGy.js} +3 -3
- package/dist/cjs/{component-BrNWuGdj.js → component-DNiVfYHV.js} +1 -1
- package/dist/cjs/{component-y8swlNNR.js → component-GW2n79GH.js} +1 -1
- package/dist/cjs/{component-UyqdiWGc.js → component-xiw46M1b.js} +1 -1
- package/dist/cjs/{files-DI4jItxi.js → files-DpGigQ8k.js} +1 -1
- package/dist/cjs/{format-bytes-CpUKKU86.js → format-bytes-WTHVI718.js} +1 -4
- package/dist/cjs/{hotkeys-BtR8uxvl.js → hotkeys-D761KWnZ.js} +1 -0
- package/dist/cjs/{image-resize-CG2fitJT.js → image-resize-JlRLB97I.js} +1 -0
- package/dist/cjs/{image.template-DY7qwbuy.js → image.template-DlF-f23b.js} +1 -1
- package/dist/cjs/{index--B6-r0uo.js → index-BlsqxTB1.js} +1 -7
- package/dist/cjs/index.cjs.js +1 -1
- package/dist/cjs/{keycodes-k-EdzcMX.js → keycodes-BQ_AERli.js} +4 -0
- package/dist/cjs/{language-CaqPKPyW.js → language-v-MMS6ym.js} +1 -1
- package/dist/cjs/lime-elements.cjs.js +2 -2
- package/dist/cjs/limel-3d-hover-effect-glow.cjs.entry.js +2 -2
- package/dist/cjs/limel-action-bar_3.cjs.entry.js +20 -18
- package/dist/cjs/limel-ai-avatar.cjs.entry.js +5 -5
- package/dist/cjs/limel-badge.cjs.entry.js +3 -3
- package/dist/cjs/limel-banner.cjs.entry.js +2 -2
- package/dist/cjs/limel-breadcrumbs_6.cjs.entry.js +86 -86
- package/dist/cjs/limel-button-group.cjs.entry.js +3 -3
- package/dist/cjs/limel-button.cjs.entry.js +4 -4
- package/dist/cjs/limel-callout.cjs.entry.js +6 -6
- package/dist/cjs/limel-card.cjs.entry.js +8 -7
- package/dist/cjs/limel-chart.cjs.entry.js +5 -5
- package/dist/cjs/limel-checkbox.cjs.entry.js +12 -10
- package/dist/cjs/limel-chip-set.cjs.entry.js +35 -44
- package/dist/cjs/limel-chip_2.cjs.entry.js +10 -10
- package/dist/cjs/limel-circular-progress.cjs.entry.js +2 -2
- package/dist/cjs/limel-code-diff.cjs.entry.js +25 -28
- package/dist/cjs/limel-code-editor.cjs.entry.js +17 -12
- package/dist/cjs/limel-collapsible-section.cjs.entry.js +8 -8
- package/dist/cjs/limel-color-picker-palette.cjs.entry.js +3 -3
- package/dist/cjs/limel-color-picker.cjs.entry.js +6 -5
- package/dist/cjs/limel-config.cjs.entry.js +3 -3
- package/dist/cjs/limel-date-picker.cjs.entry.js +12 -15
- package/dist/cjs/limel-dialog.cjs.entry.js +8 -11
- package/dist/cjs/limel-dock-button.cjs.entry.js +6 -11
- package/dist/cjs/limel-dock.cjs.entry.js +3 -3
- package/dist/cjs/limel-drag-handle.cjs.entry.js +6 -6
- package/dist/cjs/limel-dynamic-label.cjs.entry.js +1 -1
- package/dist/cjs/limel-email-viewer.cjs.entry.js +11 -17
- package/dist/cjs/limel-file-dropzone_2.cjs.entry.js +11 -12
- package/dist/cjs/limel-file-viewer.cjs.entry.js +1098 -1146
- package/dist/cjs/limel-file.cjs.entry.js +17 -16
- package/dist/cjs/limel-flatpickr-adapter.cjs.entry.js +100 -94
- package/dist/cjs/limel-flex-container.cjs.entry.js +2 -2
- package/dist/cjs/limel-form.cjs.entry.js +65 -50
- package/dist/cjs/limel-grid.cjs.entry.js +2 -2
- package/dist/cjs/limel-header.cjs.entry.js +2 -2
- package/dist/cjs/limel-help-content.cjs.entry.js +2 -2
- package/dist/cjs/limel-help.cjs.entry.js +3 -3
- package/dist/cjs/limel-helper-line_3.cjs.entry.js +14 -16
- package/dist/cjs/limel-hotkey_4.cjs.entry.js +23 -23
- package/dist/cjs/limel-icon-button.cjs.entry.js +4 -4
- package/dist/cjs/limel-icon.cjs.entry.js +15 -13
- package/dist/cjs/limel-info-tile.cjs.entry.js +10 -10
- package/dist/cjs/limel-list-item.cjs.entry.js +10 -10
- package/dist/cjs/limel-markdown.cjs.entry.js +25 -22
- package/dist/cjs/limel-masonry-layout.cjs.entry.js +6 -5
- package/dist/cjs/limel-menu-item-meta.cjs.entry.js +3 -3
- package/dist/cjs/limel-pagination-jump.cjs.entry.js +18 -16
- package/dist/cjs/limel-pagination.cjs.entry.js +10 -9
- package/dist/cjs/limel-picker.cjs.entry.js +32 -30
- package/dist/cjs/limel-popover_2.cjs.entry.js +26 -22
- package/dist/cjs/limel-profile-picture.cjs.entry.js +12 -11
- package/dist/cjs/limel-progress-flow-item.cjs.entry.js +1 -1
- package/dist/cjs/limel-progress-flow.cjs.entry.js +11 -10
- package/dist/cjs/limel-prosemirror-adapter.cjs.entry.js +475 -407
- package/dist/cjs/limel-radio-button-group.cjs.entry.js +2 -2
- package/dist/cjs/limel-radio-button.cjs.entry.js +3 -3
- package/dist/cjs/limel-scroller.cjs.entry.js +180 -0
- package/dist/cjs/limel-select.cjs.entry.js +20 -20
- package/dist/cjs/limel-shortcut.cjs.entry.js +1 -1
- package/dist/cjs/limel-slider.cjs.entry.js +18 -27
- package/dist/cjs/limel-snackbar.cjs.entry.js +5 -5
- package/dist/cjs/limel-split-button.cjs.entry.js +1 -1
- package/dist/cjs/limel-switch.cjs.entry.js +3 -3
- package/dist/cjs/limel-tab-bar.cjs.entry.js +235 -2036
- package/dist/cjs/limel-tab-panel.cjs.entry.js +12 -9
- package/dist/cjs/limel-table.cjs.entry.js +527 -106
- package/dist/cjs/limel-text-editor-link-menu.cjs.entry.js +16 -15
- package/dist/cjs/limel-text-editor.cjs.entry.js +3 -3
- package/dist/cjs/loader.cjs.js +2 -2
- package/dist/cjs/{make-enter-clickable-NuTMC9MU.js → make-enter-clickable-BZHEWIJD.js} +19 -12
- package/dist/cjs/{markdown-exporter-9p7d3Q76.js → markdown-exporter-C-UW43FF.js} +1 -1
- package/dist/cjs/{markdown-parser-Cgv5Y9Z4.js → markdown-parser-ssDij8_l.js} +24 -22
- package/dist/cjs/{moment-CqRdiK10.js → moment-Cfsz2WF1.js} +1062 -833
- package/dist/cjs/{ponyfill-BgIFyUG5.js → ponyfill-CHp7Zocr.js} +0 -1
- package/dist/cjs/{random-string-BTzDB2ee.js → random-string-CFCtFh5l.js} +1 -0
- package/dist/cjs/{render-list-component-5ZKWpCnM.js → render-list-component-BtAliHQp.js} +3 -5
- package/dist/cjs/{sv-BSR4W58y.js → sv-Co78RX70.js} +31 -3
- package/dist/cjs/{translations-DFFpR6AZ.js → translations-CNTWud2a.js} +5 -8
- package/dist/collection/collection-manifest.json +1 -0
- package/dist/collection/components/3d-hover-effect-glow/3d-hover-effect-glow.js +1 -1
- package/dist/collection/components/action-bar/action-bar-item/action-bar-item.js +8 -7
- package/dist/collection/components/action-bar/action-bar-item/action-bar-overflow-menu.js +1 -1
- package/dist/collection/components/action-bar/action-bar.js +6 -5
- package/dist/collection/components/ai-avatar/ai-avatar.js +1 -1
- package/dist/collection/components/badge/badge.js +2 -2
- package/dist/collection/components/banner/banner.js +1 -1
- package/dist/collection/components/breadcrumbs/breadcrumbs.js +2 -2
- package/dist/collection/components/button/button.js +2 -2
- package/dist/collection/components/button-group/button-group.js +1 -1
- package/dist/collection/components/callout/callout.js +1 -1
- package/dist/collection/components/card/card.js +6 -5
- package/dist/collection/components/checkbox/checkbox.js +9 -7
- package/dist/collection/components/checkbox/checkbox.template.js +1 -1
- package/dist/collection/components/chip/chip.js +1 -1
- package/dist/collection/components/chip-set/chip-set-input-helpers.js +18 -21
- package/dist/collection/components/chip-set/chip-set.js +7 -13
- package/dist/collection/components/circular-progress/circular-progress.js +1 -1
- package/dist/collection/components/code-diff/code-diff.js +19 -22
- package/dist/collection/components/code-diff/diff-engine.js +2 -2
- package/dist/collection/components/code-editor/code-editor.js +8 -3
- package/dist/collection/components/collapsible-section/collapsible-section.js +1 -1
- package/dist/collection/components/color-picker/color-picker-palette.js +2 -2
- package/dist/collection/components/color-picker/color-picker.js +5 -4
- package/dist/collection/components/config/config.js +2 -2
- package/dist/collection/components/date-picker/date-picker.js +3 -6
- package/dist/collection/components/date-picker/flatpickr-adapter/deferred-destroy.js +4 -3
- package/dist/collection/components/date-picker/flatpickr-adapter/flatpickr-adapter.js +1 -1
- package/dist/collection/components/date-picker/pickers/month-picker.js +40 -36
- package/dist/collection/components/date-picker/pickers/quarter-picker.js +35 -30
- package/dist/collection/components/date-picker/pickers/year-picker.js +16 -20
- package/dist/collection/components/dialog/dialog.js +4 -7
- package/dist/collection/components/dock/dock-button/dock-button.js +4 -9
- package/dist/collection/components/dock/dock.js +2 -2
- package/dist/collection/components/drag-handle/drag-handle.js +1 -1
- package/dist/collection/components/email-viewer/email-loader.js +14 -10
- package/dist/collection/components/email-viewer/email-viewer.js +1 -1
- package/dist/collection/components/email-viewer/sanitize-email-html.js +1 -4
- package/dist/collection/components/email-viewer/split-email-address-list.js +4 -10
- package/dist/collection/components/file/file.js +11 -10
- package/dist/collection/components/file-dropzone/file-dropzone.js +1 -1
- package/dist/collection/components/file-input/file-input.js +7 -8
- package/dist/collection/components/file-viewer/file-viewer.js +12 -9
- package/dist/collection/components/flex-container/flex-container.js +1 -1
- package/dist/collection/components/form/fields/schema-field.js +1 -1
- package/dist/collection/components/form/form.js +2 -5
- package/dist/collection/components/form/form.test-schemas.js +8 -10
- package/dist/collection/components/form/templates/array-field.js +7 -9
- package/dist/collection/components/form/templates/common.js +19 -19
- package/dist/collection/components/form/templates/object-field.js +1 -1
- package/dist/collection/components/form/validation-display.js +2 -2
- package/dist/collection/components/form/validators/format.js +1 -0
- package/dist/collection/components/form/widgets/code-editor.js +6 -9
- package/dist/collection/components/form/widgets/date-picker.js +1 -3
- package/dist/collection/components/grid/grid.js +1 -1
- package/dist/collection/components/header/header.js +1 -1
- package/dist/collection/components/help/help-content.js +1 -1
- package/dist/collection/components/help/help.js +2 -2
- package/dist/collection/components/helper-line/helper-line.js +3 -3
- package/dist/collection/components/hotkey/hotkey.js +1 -1
- package/dist/collection/components/icon/icon.js +11 -9
- package/dist/collection/components/icon-button/icon-button.js +1 -1
- package/dist/collection/components/info-tile/info-tile.js +9 -9
- package/dist/collection/components/input-field/input-field.js +11 -11
- package/dist/collection/components/list/list-renderer.js +5 -7
- package/dist/collection/components/list/list.js +2 -2
- package/dist/collection/components/list-item/list-item.js +2 -2
- package/dist/collection/components/list-item/menu-item-meta/menu-item-meta.js +1 -1
- package/dist/collection/components/markdown/hydrate-custom-elements.js +6 -5
- package/dist/collection/components/markdown/image-intersection-observer.js +9 -8
- package/dist/collection/components/markdown/image-markdown-plugin.js +9 -7
- package/dist/collection/components/markdown/link-markdown-plugin.js +11 -10
- package/dist/collection/components/markdown/markdown.js +6 -5
- package/dist/collection/components/markdown/remove-empty-paragraphs-plugin.js +4 -5
- package/dist/collection/components/masonry-layout/masonry-layout.js +5 -4
- package/dist/collection/components/menu/menu.js +48 -46
- package/dist/collection/components/menu-list/menu-list-renderer.js +8 -8
- package/dist/collection/components/menu-list/menu-list.js +1 -1
- package/dist/collection/components/menu-surface/menu-surface.js +6 -8
- package/dist/collection/components/notched-outline/notched-outline.js +1 -1
- package/dist/collection/components/pagination/jump/pagination-jump.js +13 -11
- package/dist/collection/components/pagination/pagination.js +5 -4
- package/dist/collection/components/picker/picker.js +25 -23
- package/dist/collection/components/popover/popover.js +13 -10
- package/dist/collection/components/popover-surface/popover-surface.js +1 -1
- package/dist/collection/components/portal/contains.js +9 -8
- package/dist/collection/components/portal/portal.js +16 -16
- package/dist/collection/components/profile-picture/profile-picture.js +4 -3
- package/dist/collection/components/progress-flow/progress-flow.js +10 -9
- package/dist/collection/components/radio-button-group/radio-button-group.js +1 -1
- package/dist/collection/components/radio-button-group/radio-button.js +2 -2
- package/dist/collection/components/scroller/scroller.css +317 -0
- package/dist/collection/components/scroller/scroller.js +284 -0
- package/dist/collection/components/select/select.js +13 -11
- package/dist/collection/components/select/select.template.js +1 -3
- package/dist/collection/components/slider/get-percentage-class.js +9 -18
- package/dist/collection/components/slider/slider.js +2 -2
- package/dist/collection/components/tab-bar/tab-bar.css +7 -289
- package/dist/collection/components/tab-bar/tab-bar.js +98 -128
- package/dist/collection/components/tab-panel/tab-panel.js +11 -8
- package/dist/collection/components/table/columns.js +1 -3
- package/dist/collection/components/table/element-pool.js +1 -1
- package/dist/collection/components/table/table-selection.js +6 -5
- package/dist/collection/components/table/table.css +15 -17
- package/dist/collection/components/table/table.js +44 -39
- package/dist/collection/components/text-editor/link-menu/editor-link-menu.js +11 -10
- package/dist/collection/components/text-editor/prosemirror-adapter/menu/menu-command-utils/list-utils.js +1 -1
- package/dist/collection/components/text-editor/prosemirror-adapter/menu/menu-commands.js +6 -10
- package/dist/collection/components/text-editor/prosemirror-adapter/plugins/image/inserter.js +37 -40
- package/dist/collection/components/text-editor/prosemirror-adapter/plugins/image/node.js +2 -1
- package/dist/collection/components/text-editor/prosemirror-adapter/plugins/link/link-plugin.js +21 -20
- package/dist/collection/components/text-editor/prosemirror-adapter/plugins/menu-state-tracking-plugin.js +6 -5
- package/dist/collection/components/text-editor/prosemirror-adapter/plugins/trigger/factory-helpers/detect-trigger-removal.js +15 -14
- package/dist/collection/components/text-editor/prosemirror-adapter/prosemirror-adapter.js +40 -34
- package/dist/collection/components/text-editor/text-editor.js +1 -1
- package/dist/collection/components/text-editor/utils/metadata-utils.js +1 -1
- package/dist/collection/components/text-editor/utils/plugin-factory.js +3 -3
- package/dist/collection/components/tooltip/tooltip-content.js +1 -1
- package/dist/collection/components/tooltip/tooltip.js +2 -2
- package/dist/collection/global/icon-cache/factory.js +2 -3
- package/dist/collection/global/icon-cache/in-memory-icon-cache.js +2 -0
- package/dist/collection/global/translations.js +1 -3
- package/dist/collection/translations/fr.js +3 -3
- package/dist/collection/util/adapt-color-contrast.js +1 -1
- package/dist/collection/util/files.js +1 -1
- package/dist/collection/util/format-bytes.js +1 -4
- package/dist/collection/util/hotkeys.js +1 -0
- package/dist/collection/util/image-resize.js +1 -0
- package/dist/collection/util/keyboard-navigation.js +144 -0
- package/dist/collection/util/keycodes.js +2 -0
- package/dist/collection/util/language.js +1 -1
- package/dist/collection/util/make-enter-clickable.js +19 -12
- package/dist/collection/util/random-string.js +1 -0
- package/dist/collection/util/render-list-component.js +2 -4
- package/dist/esm/{adapt-color-contrast-Dgcw_h7C.js → adapt-color-contrast-BQoih_t0.js} +1 -1
- package/dist/esm/{checkbox.template-Cx_3u92S.js → checkbox.template-IDyv2bgn.js} +2 -2
- package/dist/esm/{component-wGVqvUmL.js → component-CVihTrav.js} +3 -3
- package/dist/esm/{component-7QB0OUSF.js → component-CrjPGt1I.js} +1 -1
- package/dist/esm/{component-D4qgwaTf.js → component-CyFeUQ0l.js} +1 -1
- package/dist/esm/{component-B2Vy44Z3.js → component-kP1jCDLp.js} +1 -1
- package/dist/esm/{files-DnkFEKoB.js → files-CKnLqPDj.js} +1 -1
- package/dist/esm/{format-bytes-u_sd2HMb.js → format-bytes-24yUSW40.js} +1 -4
- package/dist/esm/{hotkeys-BxrRWYts.js → hotkeys-DjmlD0Mw.js} +1 -0
- package/dist/esm/{image-resize-BTSG--2-.js → image-resize-Dv2HOgWN.js} +1 -0
- package/dist/esm/{image.template-wqMwmOF-.js → image.template-BGjw97m_.js} +1 -1
- package/dist/esm/{index-Bnc9lzk2.js → index-BU9IJqli.js} +1 -7
- package/dist/esm/index.js +1 -1
- package/dist/esm/{keycodes-C4_9qUMP.js → keycodes-DQ5DmW_Y.js} +3 -1
- package/dist/esm/{language-C3hECvXE.js → language-ChagAaNf.js} +1 -1
- package/dist/esm/lime-elements.js +3 -3
- package/dist/esm/limel-3d-hover-effect-glow.entry.js +2 -2
- package/dist/esm/limel-action-bar_3.entry.js +20 -18
- package/dist/esm/limel-ai-avatar.entry.js +5 -5
- package/dist/esm/limel-badge.entry.js +3 -3
- package/dist/esm/limel-banner.entry.js +2 -2
- package/dist/esm/limel-breadcrumbs_6.entry.js +86 -86
- package/dist/esm/limel-button-group.entry.js +3 -3
- package/dist/esm/limel-button.entry.js +4 -4
- package/dist/esm/limel-callout.entry.js +6 -6
- package/dist/esm/limel-card.entry.js +8 -7
- package/dist/esm/limel-chart.entry.js +5 -5
- package/dist/esm/limel-checkbox.entry.js +12 -10
- package/dist/esm/limel-chip-set.entry.js +35 -44
- package/dist/esm/limel-chip_2.entry.js +10 -10
- package/dist/esm/limel-circular-progress.entry.js +2 -2
- package/dist/esm/limel-code-diff.entry.js +25 -28
- package/dist/esm/limel-code-editor.entry.js +17 -12
- package/dist/esm/limel-collapsible-section.entry.js +8 -8
- package/dist/esm/limel-color-picker-palette.entry.js +3 -3
- package/dist/esm/limel-color-picker.entry.js +6 -5
- package/dist/esm/limel-config.entry.js +3 -3
- package/dist/esm/limel-date-picker.entry.js +12 -15
- package/dist/esm/limel-dialog.entry.js +8 -11
- package/dist/esm/limel-dock-button.entry.js +6 -11
- package/dist/esm/limel-dock.entry.js +3 -3
- package/dist/esm/limel-drag-handle.entry.js +6 -6
- package/dist/esm/limel-dynamic-label.entry.js +1 -1
- package/dist/esm/limel-email-viewer.entry.js +11 -17
- package/dist/esm/limel-file-dropzone_2.entry.js +11 -12
- package/dist/esm/limel-file-viewer.entry.js +1098 -1146
- package/dist/esm/limel-file.entry.js +17 -16
- package/dist/esm/limel-flatpickr-adapter.entry.js +100 -94
- package/dist/esm/limel-flex-container.entry.js +2 -2
- package/dist/esm/limel-form.entry.js +65 -50
- package/dist/esm/limel-grid.entry.js +2 -2
- package/dist/esm/limel-header.entry.js +2 -2
- package/dist/esm/limel-help-content.entry.js +2 -2
- package/dist/esm/limel-help.entry.js +3 -3
- package/dist/esm/limel-helper-line_3.entry.js +14 -16
- package/dist/esm/limel-hotkey_4.entry.js +23 -23
- package/dist/esm/limel-icon-button.entry.js +4 -4
- package/dist/esm/limel-icon.entry.js +15 -13
- package/dist/esm/limel-info-tile.entry.js +10 -10
- package/dist/esm/limel-list-item.entry.js +10 -10
- package/dist/esm/limel-markdown.entry.js +25 -22
- package/dist/esm/limel-masonry-layout.entry.js +6 -5
- package/dist/esm/limel-menu-item-meta.entry.js +3 -3
- package/dist/esm/limel-pagination-jump.entry.js +18 -16
- package/dist/esm/limel-pagination.entry.js +10 -9
- package/dist/esm/limel-picker.entry.js +32 -30
- package/dist/esm/limel-popover_2.entry.js +26 -22
- package/dist/esm/limel-profile-picture.entry.js +12 -11
- package/dist/esm/limel-progress-flow-item.entry.js +1 -1
- package/dist/esm/limel-progress-flow.entry.js +11 -10
- package/dist/esm/limel-prosemirror-adapter.entry.js +475 -407
- package/dist/esm/limel-radio-button-group.entry.js +2 -2
- package/dist/esm/limel-radio-button.entry.js +3 -3
- package/dist/esm/limel-scroller.entry.js +178 -0
- package/dist/esm/limel-select.entry.js +20 -20
- package/dist/esm/limel-shortcut.entry.js +1 -1
- package/dist/esm/limel-slider.entry.js +18 -27
- package/dist/esm/limel-snackbar.entry.js +5 -5
- package/dist/esm/limel-split-button.entry.js +1 -1
- package/dist/esm/limel-switch.entry.js +3 -3
- package/dist/esm/limel-tab-bar.entry.js +235 -2036
- package/dist/esm/limel-tab-panel.entry.js +12 -9
- package/dist/esm/limel-table.entry.js +527 -106
- package/dist/esm/limel-text-editor-link-menu.entry.js +16 -15
- package/dist/esm/limel-text-editor.entry.js +3 -3
- package/dist/esm/loader.js +3 -3
- package/dist/esm/{make-enter-clickable-BgTwPGeH.js → make-enter-clickable-CIf8aGSd.js} +19 -12
- package/dist/esm/{markdown-exporter--3_MaqwD.js → markdown-exporter-DblYRMhY.js} +1 -1
- package/dist/esm/{markdown-parser-DCf-3dKZ.js → markdown-parser-CAPDx7i8.js} +24 -22
- package/dist/esm/{moment-DQRNe8qq.js → moment-_DadZLWp.js} +1062 -833
- package/dist/esm/{ponyfill-ChRGk668.js → ponyfill-6YuZiAPi.js} +1 -1
- package/dist/esm/{random-string-JbKhhoXs.js → random-string-BrjD-ITe.js} +1 -0
- package/dist/esm/{render-list-component-cicbMp0l.js → render-list-component-J1IzNpbv.js} +3 -5
- package/dist/esm/{sv-CY-6-yEp.js → sv-DpsDSi7k.js} +31 -3
- package/dist/esm/{translations-DMtX4tTX.js → translations-Y4i8PVc9.js} +5 -8
- package/dist/lime-elements/index.esm.js +1 -1
- package/dist/lime-elements/lime-elements.esm.js +1 -1
- package/dist/lime-elements/p-02a06cf7.entry.js +1 -0
- package/dist/lime-elements/{p-3916b316.entry.js → p-03cb609e.entry.js} +1 -1
- package/dist/lime-elements/p-05038bbb.entry.js +1 -0
- package/dist/lime-elements/{p-835149c4.entry.js → p-0d6925f4.entry.js} +1 -1
- package/dist/lime-elements/p-0e13ec91.entry.js +1 -0
- package/dist/lime-elements/p-0ece667e.entry.js +1 -0
- package/dist/lime-elements/{p-19344984.entry.js → p-107654bb.entry.js} +4 -4
- package/dist/lime-elements/p-120ca628.entry.js +1 -0
- package/dist/lime-elements/{p-BwJGg-WI.js → p-1CgnIGyd.js} +4 -4
- package/dist/lime-elements/p-22fd1c87.entry.js +1 -0
- package/dist/lime-elements/p-24yUSW40.js +1 -0
- package/dist/lime-elements/{p-f717cd7a.entry.js → p-271662d4.entry.js} +1 -1
- package/dist/lime-elements/{p-c2a60508.entry.js → p-29c73bdf.entry.js} +1 -1
- package/dist/lime-elements/{p-deceda00.entry.js → p-2a902e77.entry.js} +1 -1
- package/dist/lime-elements/{p-d0cec424.entry.js → p-2b033dfa.entry.js} +1 -1
- package/dist/lime-elements/{p-acba5700.entry.js → p-2b2253d4.entry.js} +1 -1
- package/dist/lime-elements/p-2e33d2a4.entry.js +1 -0
- package/dist/lime-elements/p-32a050a3.entry.js +1 -0
- package/dist/lime-elements/{p-3735111e.entry.js → p-3cfe0eaf.entry.js} +1 -1
- package/dist/lime-elements/{p-fbcd99a3.entry.js → p-3d3bdf45.entry.js} +1 -1
- package/dist/lime-elements/p-45461ec5.entry.js +1 -0
- package/dist/lime-elements/p-45c39fb4.entry.js +1 -0
- package/dist/lime-elements/p-45fde7a1.entry.js +1 -0
- package/dist/lime-elements/p-48afa6b7.entry.js +1 -0
- package/dist/lime-elements/{p-f04f7c8e.entry.js → p-4a88d365.entry.js} +1 -1
- package/dist/lime-elements/p-4b75e653.entry.js +1 -0
- package/dist/lime-elements/{p-5698e493.entry.js → p-4ecbc9f4.entry.js} +1 -1
- package/dist/lime-elements/{p-2e568c56.entry.js → p-54a9f48f.entry.js} +1 -1
- package/dist/lime-elements/{p-0fba64eb.entry.js → p-5df21b25.entry.js} +2 -2
- package/dist/lime-elements/p-5e448070.entry.js +1 -0
- package/dist/lime-elements/{p-408c4dcb.entry.js → p-5f5c6e7d.entry.js} +1 -1
- package/dist/lime-elements/{p-052c3bc6.entry.js → p-681cb6de.entry.js} +1 -1
- package/dist/lime-elements/{p-ChRGk668.js → p-6YuZiAPi.js} +3 -3
- package/dist/lime-elements/{p-90df3e1b.entry.js → p-6d9961f7.entry.js} +1 -1
- package/dist/lime-elements/p-74060535.entry.js +159 -0
- package/dist/lime-elements/{p-74bd0c1a.entry.js → p-7b4351f6.entry.js} +1 -1
- package/dist/lime-elements/{p-3fd7e8aa.entry.js → p-7eab1341.entry.js} +1 -1
- package/dist/lime-elements/p-7fa85785.entry.js +23 -0
- package/dist/lime-elements/{p-35fa1063.entry.js → p-85318636.entry.js} +1 -1
- package/dist/lime-elements/{p-4ffbfc79.entry.js → p-8ac79ef5.entry.js} +1 -1
- package/dist/lime-elements/{p-4902fb3f.entry.js → p-9218fa6a.entry.js} +1 -1
- package/dist/lime-elements/{p-38b09f87.entry.js → p-9415411e.entry.js} +1 -1
- package/dist/lime-elements/{p-be8610ff.entry.js → p-958396b5.entry.js} +1 -1
- package/dist/lime-elements/p-95ccd9e7.entry.js +1 -0
- package/dist/lime-elements/p-970e0810.entry.js +1 -0
- package/dist/lime-elements/{p-cb407bef.entry.js → p-9afa4721.entry.js} +1 -1
- package/dist/lime-elements/{p-54dcf46f.entry.js → p-9c8c2fb7.entry.js} +1 -1
- package/dist/lime-elements/p-9e084e7c.entry.js +1 -0
- package/dist/lime-elements/{p-B8hoCfhT.js → p-B3YP9GOq.js} +1 -1
- package/dist/lime-elements/{p-Dgcw_h7C.js → p-BQoih_t0.js} +1 -1
- package/dist/lime-elements/{p-Bnc9lzk2.js → p-BU9IJqli.js} +1 -1
- package/dist/lime-elements/{p-BN1-aIOw.js → p-BZXGwiAr.js} +1 -1
- package/dist/lime-elements/{p-CGVVDGcv.js → p-Bkvc4sus.js} +1 -1
- package/dist/lime-elements/{p-BPzhuHcl.js → p-Bvyt2bmV.js} +1 -1
- package/dist/lime-elements/{p-DZkKQUDM.js → p-C1FFnUvf.js} +3 -3
- package/dist/lime-elements/{p-BgTwPGeH.js → p-CIf8aGSd.js} +1 -1
- package/dist/lime-elements/{p-C3hECvXE.js → p-ChagAaNf.js} +1 -1
- package/dist/lime-elements/{p-C9yTLqR8.js → p-CmtleG1y.js} +1 -1
- package/dist/lime-elements/{p-C4_9qUMP.js → p-DQ5DmW_Y.js} +1 -1
- package/dist/lime-elements/p-DTcfed02.js +3 -0
- package/dist/lime-elements/{p-BY85K-4J.js → p-DqQOawxc.js} +1 -1
- package/dist/lime-elements/{p-CC4Usa-Q.js → p-I0PrvthC.js} +1 -1
- package/dist/lime-elements/p-N9vxZMhe.js +1 -0
- package/dist/lime-elements/{p-DsDkl4Sy.js → p-ZDFVISfu.js} +3 -3
- package/dist/lime-elements/p-a1c69e0c.entry.js +1 -0
- package/dist/lime-elements/{p-2daf0482.entry.js → p-a1d1470c.entry.js} +1 -1
- package/dist/lime-elements/{p-cef66e53.entry.js → p-a2e6f16e.entry.js} +1 -1
- package/dist/lime-elements/{p-36ce4fbc.entry.js → p-a42f8c2d.entry.js} +1 -1
- package/dist/lime-elements/p-a550ba18.entry.js +1 -0
- package/dist/lime-elements/{p-d146feda.entry.js → p-a65eb58e.entry.js} +1 -1
- package/dist/lime-elements/{p-03fb2603.entry.js → p-b22b52a1.entry.js} +1 -1
- package/dist/lime-elements/{p-c2a3ca7c.entry.js → p-b6d39581.entry.js} +1 -1
- package/dist/lime-elements/{p-8475a3b8.entry.js → p-c439b68b.entry.js} +1 -1
- package/dist/lime-elements/p-d19c7a3a.entry.js +1 -0
- package/dist/lime-elements/{p-d3bd4b80.entry.js → p-d1b7cc55.entry.js} +3 -3
- package/dist/lime-elements/{p-7cbf50fc.entry.js → p-d1e5dd12.entry.js} +1 -1
- package/dist/lime-elements/p-de4719a6.entry.js +1 -0
- package/dist/lime-elements/{p-b81a2728.entry.js → p-e02e2c5f.entry.js} +1 -1
- package/dist/lime-elements/{p-d351b9ae.entry.js → p-e6a34047.entry.js} +1 -1
- package/dist/lime-elements/p-eF09GxA_.js +1 -0
- package/dist/lime-elements/{p-8fa9c9ea.entry.js → p-ee719744.entry.js} +1 -1
- package/dist/lime-elements/p-f3cae1e7.entry.js +1 -0
- package/dist/lime-elements/{p-8115da75.entry.js → p-f4a0b1bc.entry.js} +1 -1
- package/dist/lime-elements/{p-4855dc9b.entry.js → p-f82f72e7.entry.js} +1 -1
- package/dist/lime-elements/{p-2d84ea04.entry.js → p-f97ec7a5.entry.js} +1 -1
- package/dist/lime-elements/{p-21109d9b.entry.js → p-fdd674fc.entry.js} +1 -1
- package/dist/lime-elements/{p-459eea13.entry.js → p-fdec09e9.entry.js} +1 -1
- package/dist/lime-elements/{p-2f57c3fa.entry.js → p-fe492ff3.entry.js} +1 -1
- package/dist/lime-elements/{p-65cded0e.entry.js → p-ff1a8b2e.entry.js} +1 -1
- package/dist/types/components/chip-set/chip-set-input-helpers.d.ts +1 -1
- package/dist/types/components/pagination/jump/pagination-jump.d.ts +1 -1
- package/dist/types/components/scroller/scroller.d.ts +67 -0
- package/dist/types/components/tab-bar/tab-bar.d.ts +23 -21
- package/dist/types/components.d.ts +132 -0
- package/dist/types/global/icon-cache/factory.d.ts +2 -2
- package/dist/types/global/translations.d.ts +2 -2
- package/dist/types/util/keyboard-navigation.d.ts +105 -0
- package/dist/types/util/keycodes.d.ts +2 -0
- package/package.json +12 -12
- package/dist/lime-elements/p-1e93473a.entry.js +0 -1
- package/dist/lime-elements/p-20658306.entry.js +0 -1
- package/dist/lime-elements/p-26c5c6b9.entry.js +0 -1
- package/dist/lime-elements/p-452b9b01.entry.js +0 -1
- package/dist/lime-elements/p-4aae63bd.entry.js +0 -1
- package/dist/lime-elements/p-507cc8a1.entry.js +0 -159
- package/dist/lime-elements/p-531b6a64.entry.js +0 -1
- package/dist/lime-elements/p-54793c06.entry.js +0 -1
- package/dist/lime-elements/p-5d974afa.entry.js +0 -1
- package/dist/lime-elements/p-66b125f2.entry.js +0 -1
- package/dist/lime-elements/p-7e212fa7.entry.js +0 -1
- package/dist/lime-elements/p-82696d24.entry.js +0 -1
- package/dist/lime-elements/p-9ac39039.entry.js +0 -1
- package/dist/lime-elements/p-9b631363.entry.js +0 -1
- package/dist/lime-elements/p-DQJU9gzh.js +0 -1
- package/dist/lime-elements/p-DpJgXIWf.js +0 -1
- package/dist/lime-elements/p-a1de9bbe.entry.js +0 -1
- package/dist/lime-elements/p-a71f6e87.entry.js +0 -67
- package/dist/lime-elements/p-bd7b3f8e.entry.js +0 -1
- package/dist/lime-elements/p-c72e5007.entry.js +0 -1
- package/dist/lime-elements/p-cafec335.entry.js +0 -1
- package/dist/lime-elements/p-d6dadc61.entry.js +0 -1
- package/dist/lime-elements/p-d93b7b9f.entry.js +0 -23
- package/dist/lime-elements/p-dc9cd793.entry.js +0 -1
- package/dist/lime-elements/p-dd62042a.entry.js +0 -1
- package/dist/lime-elements/p-u_sd2HMb.js +0 -1
- package/dist/lime-elements/p-zo6O9LC9.js +0 -3
- /package/dist/lime-elements/{p-JbKhhoXs.js → p-BrjD-ITe.js} +0 -0
- /package/dist/lime-elements/{p-BxrRWYts.js → p-DjmlD0Mw.js} +0 -0
- /package/dist/lime-elements/{p-BTSG--2-.js → p-Dv2HOgWN.js} +0 -0
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var index$1 = require('./index
|
|
4
|
-
var translations = require('./translations-
|
|
3
|
+
var index$1 = require('./index-BlsqxTB1.js');
|
|
4
|
+
var translations = require('./translations-CNTWud2a.js');
|
|
5
5
|
var index = require('./index-zZvs6xK-.js');
|
|
6
|
-
var image_template = require('./image.template-
|
|
7
|
-
require('./language-
|
|
6
|
+
var image_template = require('./image.template-DlF-f23b.js');
|
|
7
|
+
require('./language-v-MMS6ym.js');
|
|
8
8
|
require('./_commonjsHelpers-CFO10eej.js');
|
|
9
9
|
|
|
10
10
|
/**
|
|
@@ -125,15 +125,12 @@ class Fullscreen {
|
|
|
125
125
|
}
|
|
126
126
|
|
|
127
127
|
const textEncoder = new TextEncoder();
|
|
128
|
-
|
|
129
128
|
const base64Chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
|
130
|
-
|
|
131
129
|
// Use a lookup table to find the index.
|
|
132
130
|
const base64Lookup = new Uint8Array(256);
|
|
133
131
|
for (let i = 0; i < base64Chars.length; i++) {
|
|
134
132
|
base64Lookup[base64Chars.charCodeAt(i)] = i;
|
|
135
133
|
}
|
|
136
|
-
|
|
137
134
|
function decodeBase64(base64) {
|
|
138
135
|
// Padding carries no data, so the byte count comes from the payload alone. Sizing the
|
|
139
136
|
// buffer from the raw length instead treated '=' as a data character and left the
|
|
@@ -142,26 +139,20 @@ function decodeBase64(base64) {
|
|
|
142
139
|
while (len > 0 && base64.charAt(len - 1) === '=') {
|
|
143
140
|
len--;
|
|
144
141
|
}
|
|
145
|
-
|
|
146
142
|
// A remainder of one character cannot encode a byte, it is a truncated group
|
|
147
143
|
if (len % 4 === 1) {
|
|
148
144
|
len--;
|
|
149
145
|
}
|
|
150
|
-
|
|
151
146
|
const remainder = len % 4;
|
|
152
147
|
const bufferLength = Math.floor(len / 4) * 3 + (remainder ? remainder - 1 : 0);
|
|
153
|
-
|
|
154
148
|
const arrayBuffer = new ArrayBuffer(bufferLength);
|
|
155
149
|
const bytes = new Uint8Array(arrayBuffer);
|
|
156
|
-
|
|
157
150
|
let p = 0;
|
|
158
|
-
|
|
159
151
|
for (let i = 0; i < len; i += 4) {
|
|
160
152
|
let encoded1 = base64Lookup[base64.charCodeAt(i)];
|
|
161
153
|
let encoded2 = base64Lookup[base64.charCodeAt(i + 1)];
|
|
162
154
|
let encoded3 = base64Lookup[base64.charCodeAt(i + 2)];
|
|
163
155
|
let encoded4 = base64Lookup[base64.charCodeAt(i + 3)];
|
|
164
|
-
|
|
165
156
|
bytes[p++] = (encoded1 << 2) | (encoded2 >> 4);
|
|
166
157
|
if (p < bufferLength) {
|
|
167
158
|
bytes[p++] = ((encoded2 & 15) << 4) | (encoded3 >> 2);
|
|
@@ -170,10 +161,8 @@ function decodeBase64(base64) {
|
|
|
170
161
|
bytes[p++] = ((encoded3 & 3) << 6) | (encoded4 & 63);
|
|
171
162
|
}
|
|
172
163
|
}
|
|
173
|
-
|
|
174
164
|
return arrayBuffer;
|
|
175
165
|
}
|
|
176
|
-
|
|
177
166
|
// Charset labels the WHATWG Encoding Standard does not list, but that name an encoding
|
|
178
167
|
// TextDecoder can decode. Without an entry the TextDecoder constructor throws and
|
|
179
168
|
// getDecoder falls back to windows-1252, turning the body into mojibake with nothing to
|
|
@@ -187,14 +176,12 @@ const charsetAliases = new Map([
|
|
|
187
176
|
// Hebrew. The logical and explicit ordering variants share the iso-8859-8 index.
|
|
188
177
|
['iso88598i', 'iso-8859-8'],
|
|
189
178
|
['iso88598e', 'iso-8859-8'],
|
|
190
|
-
|
|
191
179
|
// Japanese. WHATWG shift_jis is the Windows-31J index, so cp932 text decodes
|
|
192
180
|
// identically, including the NEC and IBM extension rows.
|
|
193
181
|
['shiftjis', 'shift_jis'],
|
|
194
182
|
['windows31j', 'shift_jis'],
|
|
195
183
|
['mskanji', 'shift_jis'],
|
|
196
184
|
['eucjp', 'euc-jp'],
|
|
197
|
-
|
|
198
185
|
// ISO-2022-JP, including the -1 / -2 supersets. Escape sequences outside plain
|
|
199
186
|
// ISO-2022-JP (JIS X 0212, the non-Japanese G2 sets, and the SO/SI katakana shifts
|
|
200
187
|
// cp50222 uses) decode to replacement characters, but the Japanese text around them
|
|
@@ -203,15 +190,12 @@ const charsetAliases = new Map([
|
|
|
203
190
|
['iso2022jp1', 'iso-2022-jp'],
|
|
204
191
|
['iso2022jp2', 'iso-2022-jp'],
|
|
205
192
|
['junet', 'iso-2022-jp'],
|
|
206
|
-
|
|
207
193
|
// Korean. The WHATWG euc-kr index is the extended cp949 / UHC index.
|
|
208
194
|
['euckr', 'euc-kr'],
|
|
209
195
|
['uhc', 'euc-kr'],
|
|
210
|
-
|
|
211
196
|
// Thai.
|
|
212
197
|
['tis620', 'windows-874']
|
|
213
198
|
]);
|
|
214
|
-
|
|
215
199
|
// Windows and IBM code page numbers, as written in labels like cp932, windows-932, ms932
|
|
216
200
|
// or ibm932. An explicit allowlist rather than a derived one, because plenty of code
|
|
217
201
|
// pages that appear in mail (cp437, cp850, cp1361) have no equivalent to map onto and
|
|
@@ -229,114 +213,108 @@ const codePageAliases = new Map([
|
|
|
229
213
|
['50221', 'iso-2022-jp'],
|
|
230
214
|
['50222', 'iso-2022-jp']
|
|
231
215
|
]);
|
|
232
|
-
|
|
233
216
|
const codePagePattern = /^(?:cp|windows|ms|ibm)(\d+)$/;
|
|
234
|
-
|
|
235
217
|
// Strip the decorations mail clients add to an otherwise standard label: an x- vendor
|
|
236
218
|
// prefix, the IANA cs- prefix, and any separators.
|
|
237
219
|
function normalizeCharset(charset) {
|
|
238
220
|
return charset.replace(/^(?:x-ms-|x-|cs)/, '').replace(/[\s._-]+/g, '');
|
|
239
221
|
}
|
|
240
|
-
|
|
241
222
|
// Every label of the WHATWG Encoding Standard, https://encoding.spec.whatwg.org/encodings.json
|
|
242
223
|
//
|
|
243
224
|
// TextDecoder accepts these and nothing else, so a label outside the list is turned away
|
|
244
225
|
// without constructing a decoder for it. Letting the constructor throw instead costs
|
|
245
226
|
// several times the decode itself, and a message controls how often it happens: once
|
|
246
227
|
// per encoded word, twice when the label also fails after normalization.
|
|
247
|
-
const ENCODING_LABELS = new Set(
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
.split(' ')
|
|
338
|
-
);
|
|
339
|
-
|
|
228
|
+
const ENCODING_LABELS = new Set([
|
|
229
|
+
// UTF-8
|
|
230
|
+
'unicode-1-1-utf-8 unicode11utf8 unicode20utf8 utf-8 utf8 x-unicode20utf8',
|
|
231
|
+
// IBM866
|
|
232
|
+
'866 cp866 csibm866 ibm866',
|
|
233
|
+
// ISO-8859-2
|
|
234
|
+
'csisolatin2 iso-8859-2 iso-ir-101 iso8859-2 iso88592 iso_8859-2 iso_8859-2:1987 l2 latin2',
|
|
235
|
+
// ISO-8859-3
|
|
236
|
+
'csisolatin3 iso-8859-3 iso-ir-109 iso8859-3 iso88593 iso_8859-3 iso_8859-3:1988 l3 latin3',
|
|
237
|
+
// ISO-8859-4
|
|
238
|
+
'csisolatin4 iso-8859-4 iso-ir-110 iso8859-4 iso88594 iso_8859-4 iso_8859-4:1988 l4 latin4',
|
|
239
|
+
// ISO-8859-5
|
|
240
|
+
'csisolatincyrillic cyrillic iso-8859-5 iso-ir-144 iso8859-5 iso88595 iso_8859-5 iso_8859-5:1988',
|
|
241
|
+
// ISO-8859-6
|
|
242
|
+
'arabic asmo-708 csiso88596e csiso88596i csisolatinarabic ecma-114 iso-8859-6 iso-8859-6-e',
|
|
243
|
+
'iso-8859-6-i iso-ir-127 iso8859-6 iso88596 iso_8859-6 iso_8859-6:1987',
|
|
244
|
+
// ISO-8859-7
|
|
245
|
+
'csisolatingreek ecma-118 elot_928 greek greek8 iso-8859-7 iso-ir-126 iso8859-7 iso88597 iso_8859-7',
|
|
246
|
+
'iso_8859-7:1987 sun_eu_greek',
|
|
247
|
+
// ISO-8859-8
|
|
248
|
+
'csiso88598e csisolatinhebrew hebrew iso-8859-8 iso-8859-8-e iso-ir-138 iso8859-8 iso88598 iso_8859-8',
|
|
249
|
+
'iso_8859-8:1988 visual',
|
|
250
|
+
// ISO-8859-8-I
|
|
251
|
+
'csiso88598i iso-8859-8-i logical',
|
|
252
|
+
// ISO-8859-10
|
|
253
|
+
'csisolatin6 iso-8859-10 iso-ir-157 iso8859-10 iso885910 l6 latin6',
|
|
254
|
+
// ISO-8859-13
|
|
255
|
+
'iso-8859-13 iso8859-13 iso885913',
|
|
256
|
+
// ISO-8859-14
|
|
257
|
+
'iso-8859-14 iso8859-14 iso885914',
|
|
258
|
+
// ISO-8859-15
|
|
259
|
+
'csisolatin9 iso-8859-15 iso8859-15 iso885915 iso_8859-15 l9',
|
|
260
|
+
// ISO-8859-16
|
|
261
|
+
'iso-8859-16',
|
|
262
|
+
// KOI8-R
|
|
263
|
+
'cskoi8r koi koi8 koi8-r koi8_r',
|
|
264
|
+
// KOI8-U
|
|
265
|
+
'koi8-ru koi8-u',
|
|
266
|
+
// macintosh
|
|
267
|
+
'csmacintosh mac macintosh x-mac-roman',
|
|
268
|
+
// windows-874
|
|
269
|
+
'dos-874 iso-8859-11 iso8859-11 iso885911 tis-620 windows-874',
|
|
270
|
+
// windows-1250
|
|
271
|
+
'cp1250 windows-1250 x-cp1250',
|
|
272
|
+
// windows-1251
|
|
273
|
+
'cp1251 windows-1251 x-cp1251',
|
|
274
|
+
// windows-1252
|
|
275
|
+
'ansi_x3.4-1968 ascii cp1252 cp819 csisolatin1 ibm819 iso-8859-1 iso-ir-100 iso8859-1 iso88591',
|
|
276
|
+
'iso_8859-1 iso_8859-1:1987 l1 latin1 us-ascii windows-1252 x-cp1252',
|
|
277
|
+
// windows-1253
|
|
278
|
+
'cp1253 windows-1253 x-cp1253',
|
|
279
|
+
// windows-1254
|
|
280
|
+
'cp1254 csisolatin5 iso-8859-9 iso-ir-148 iso8859-9 iso88599 iso_8859-9 iso_8859-9:1989 l5 latin5',
|
|
281
|
+
'windows-1254 x-cp1254',
|
|
282
|
+
// windows-1255
|
|
283
|
+
'cp1255 windows-1255 x-cp1255',
|
|
284
|
+
// windows-1256
|
|
285
|
+
'cp1256 windows-1256 x-cp1256',
|
|
286
|
+
// windows-1257
|
|
287
|
+
'cp1257 windows-1257 x-cp1257',
|
|
288
|
+
// windows-1258
|
|
289
|
+
'cp1258 windows-1258 x-cp1258',
|
|
290
|
+
// x-mac-cyrillic
|
|
291
|
+
'x-mac-cyrillic x-mac-ukrainian',
|
|
292
|
+
// GBK
|
|
293
|
+
'chinese csgb2312 csiso58gb231280 gb2312 gb_2312 gb_2312-80 gbk iso-ir-58 x-gbk',
|
|
294
|
+
// gb18030
|
|
295
|
+
'gb18030',
|
|
296
|
+
// Big5
|
|
297
|
+
'big5 big5-hkscs cn-big5 csbig5 x-x-big5',
|
|
298
|
+
// EUC-JP
|
|
299
|
+
'cseucpkdfmtjapanese euc-jp x-euc-jp',
|
|
300
|
+
// ISO-2022-JP
|
|
301
|
+
'csiso2022jp iso-2022-jp',
|
|
302
|
+
// Shift_JIS
|
|
303
|
+
'csshiftjis ms932 ms_kanji shift-jis shift_jis sjis windows-31j x-sjis',
|
|
304
|
+
// EUC-KR
|
|
305
|
+
'cseuckr csksc56011987 euc-kr iso-ir-149 korean ks_c_5601-1987 ks_c_5601-1989 ksc5601 ksc_5601',
|
|
306
|
+
'windows-949',
|
|
307
|
+
// replacement
|
|
308
|
+
'csiso2022kr hz-gb-2312 iso-2022-cn iso-2022-cn-ext iso-2022-kr replacement',
|
|
309
|
+
// UTF-16BE
|
|
310
|
+
'unicodefffe utf-16be',
|
|
311
|
+
// UTF-16LE
|
|
312
|
+
'csunicode iso-10646-ucs-2 ucs-2 unicode unicodefeff utf-16 utf-16le',
|
|
313
|
+
// x-user-defined
|
|
314
|
+
'x-user-defined'
|
|
315
|
+
]
|
|
316
|
+
.join(' ')
|
|
317
|
+
.split(' '));
|
|
340
318
|
// Decoders by label, or null where the runtime refuses the label (the replacement
|
|
341
319
|
// encoding, or an encoding a runtime was built without). Only labels from the list above
|
|
342
320
|
// are stored, so the map can not grow past it however many labels a message makes up.
|
|
@@ -344,75 +322,66 @@ const ENCODING_LABELS = new Set(
|
|
|
344
322
|
// Sharing a decoder is safe because nothing decodes with `stream: true`, so every
|
|
345
323
|
// decode() call starts from a fresh state.
|
|
346
324
|
const decoders = new Map();
|
|
347
|
-
|
|
348
325
|
function tryDecoder(charset) {
|
|
349
326
|
if (!ENCODING_LABELS.has(charset)) {
|
|
350
327
|
return null;
|
|
351
328
|
}
|
|
352
|
-
|
|
353
|
-
if (
|
|
354
|
-
|
|
329
|
+
let decoder = decoders.get(charset);
|
|
330
|
+
if (decoder === undefined) {
|
|
331
|
+
decoder = null;
|
|
355
332
|
try {
|
|
356
333
|
decoder = new TextDecoder(charset);
|
|
357
|
-
}
|
|
334
|
+
}
|
|
335
|
+
catch {
|
|
358
336
|
// not supported by this runtime
|
|
359
337
|
}
|
|
360
338
|
decoders.set(charset, decoder);
|
|
361
339
|
}
|
|
362
|
-
|
|
363
|
-
return decoders.get(charset);
|
|
340
|
+
return decoder;
|
|
364
341
|
}
|
|
365
|
-
|
|
342
|
+
// Every runtime has utf-8, so a decoder is always returned even where windows-1252 is
|
|
343
|
+
// missing, rather than letting the caller fail on a null decoder
|
|
344
|
+
const utf8Decoder = new TextDecoder();
|
|
366
345
|
function getDecoder(charset) {
|
|
367
346
|
charset = (charset || 'utf8').trim().toLowerCase();
|
|
368
|
-
|
|
369
347
|
// Try the label as written first, so the alias table only ever adds to what the
|
|
370
348
|
// runtime already supports instead of shadowing it.
|
|
371
349
|
const decoder = tryDecoder(charset);
|
|
372
350
|
if (decoder) {
|
|
373
351
|
return decoder;
|
|
374
352
|
}
|
|
375
|
-
|
|
376
353
|
const normalized = normalizeCharset(charset);
|
|
377
354
|
const codePage = normalized.match(codePagePattern);
|
|
378
|
-
|
|
379
355
|
// The normalized label is itself the last candidate, which resolves everything that
|
|
380
356
|
// differed from a supported label only by a prefix or a separator, eg. x-big5.
|
|
381
357
|
const alias = (codePage && codePageAliases.get(codePage[1])) || charsetAliases.get(normalized) || normalized;
|
|
382
|
-
|
|
383
|
-
return tryDecoder(alias) || tryDecoder('windows-1252');
|
|
358
|
+
return tryDecoder(alias) || tryDecoder('windows-1252') || utf8Decoder;
|
|
384
359
|
}
|
|
385
|
-
|
|
386
360
|
/**
|
|
387
361
|
* Converts a Blob into an ArrayBuffer
|
|
388
|
-
* @param
|
|
389
|
-
* @returns
|
|
362
|
+
* @param blob Blob to convert
|
|
363
|
+
* @returns Converted value
|
|
390
364
|
*/
|
|
391
365
|
async function blobToArrayBuffer(blob) {
|
|
392
366
|
if ('arrayBuffer' in blob) {
|
|
393
367
|
return await blob.arrayBuffer();
|
|
394
368
|
}
|
|
395
|
-
|
|
396
369
|
const fr = new FileReader();
|
|
397
|
-
|
|
398
370
|
return new Promise((resolve, reject) => {
|
|
399
|
-
fr.onload =
|
|
400
|
-
resolve(
|
|
371
|
+
fr.onload = () => {
|
|
372
|
+
resolve(fr.result);
|
|
401
373
|
};
|
|
402
|
-
|
|
403
|
-
fr.onerror = function (e) {
|
|
374
|
+
fr.onerror = () => {
|
|
404
375
|
reject(fr.error);
|
|
405
376
|
};
|
|
406
|
-
|
|
407
377
|
fr.readAsArrayBuffer(blob);
|
|
408
378
|
});
|
|
409
379
|
}
|
|
410
|
-
|
|
411
380
|
/**
|
|
412
381
|
* Numeric value of an ASCII hex digit
|
|
413
382
|
*
|
|
414
|
-
* @param
|
|
415
|
-
* @return
|
|
383
|
+
* @param c Byte to read
|
|
384
|
+
* @return Value 0-15, or -1 if the byte is not a hex digit
|
|
416
385
|
*/
|
|
417
386
|
function hexNibble(c) {
|
|
418
387
|
if (c >= 0x30 /* 0 */ && c <= 0x39 /* 9 */) {
|
|
@@ -426,12 +395,13 @@ function hexNibble(c) {
|
|
|
426
395
|
}
|
|
427
396
|
return -1;
|
|
428
397
|
}
|
|
429
|
-
|
|
430
398
|
/**
|
|
431
399
|
* Decode a complete mime word encoded string
|
|
432
400
|
*
|
|
433
|
-
* @param
|
|
434
|
-
* @
|
|
401
|
+
* @param charset Character set of the encoded word
|
|
402
|
+
* @param encoding `Q` or `B`
|
|
403
|
+
* @param str Mime word encoded string
|
|
404
|
+
* @return Decoded unicode string
|
|
435
405
|
*/
|
|
436
406
|
function decodeWord(charset, encoding, str) {
|
|
437
407
|
// RFC2231 added language tag to the encoding
|
|
@@ -439,20 +409,16 @@ function decodeWord(charset, encoding, str) {
|
|
|
439
409
|
// this implementation silently ignores this tag
|
|
440
410
|
let splitPos = charset.indexOf('*');
|
|
441
411
|
if (splitPos >= 0) {
|
|
442
|
-
charset = charset.
|
|
412
|
+
charset = charset.slice(0, splitPos);
|
|
443
413
|
}
|
|
444
|
-
|
|
445
414
|
encoding = encoding.toUpperCase();
|
|
446
|
-
|
|
447
415
|
let byteStr;
|
|
448
|
-
|
|
449
416
|
if (encoding === 'Q') {
|
|
450
417
|
str = str
|
|
451
418
|
// remove spaces between = and hex char, this might indicate invalidly applied line splitting
|
|
452
419
|
.replace(/=\s+([0-9a-fA-F])/g, '=$1')
|
|
453
420
|
// convert all underscores to spaces
|
|
454
421
|
.replace(/[_\s]/g, ' ');
|
|
455
|
-
|
|
456
422
|
let buf = textEncoder.encode(str);
|
|
457
423
|
let encodedBytes = [];
|
|
458
424
|
for (let i = 0, len = buf.length; i < len; i++) {
|
|
@@ -473,38 +439,34 @@ function decodeWord(charset, encoding, str) {
|
|
|
473
439
|
for (let i = 0, len = encodedBytes.length; i < len; i++) {
|
|
474
440
|
dataView.setUint8(i, encodedBytes[i]);
|
|
475
441
|
}
|
|
476
|
-
}
|
|
442
|
+
}
|
|
443
|
+
else if (encoding === 'B') {
|
|
477
444
|
byteStr = decodeBase64(str.replace(/[^a-zA-Z0-9\+\/=]+/g, ''));
|
|
478
|
-
}
|
|
445
|
+
}
|
|
446
|
+
else {
|
|
479
447
|
// keep as is, convert ArrayBuffer to unicode string, assume utf8
|
|
480
448
|
byteStr = textEncoder.encode(str);
|
|
481
449
|
}
|
|
482
|
-
|
|
483
450
|
return getDecoder(charset).decode(byteStr);
|
|
484
451
|
}
|
|
485
|
-
|
|
486
452
|
// A charset label runs to the next '?' so that labels containing punctuation, eg.
|
|
487
453
|
// ISO_8859-1:1987, are recognised. Whitespace is excluded so a stray '=?' in running text
|
|
488
454
|
// can not swallow the rest of the line.
|
|
489
455
|
const ENCODED_WORD_PATTERN = '=\\?([^?\\s]+)\\?([QqBb])\\?([^?]*)\\?=';
|
|
490
456
|
const ENCODED_WORD_REGEX = new RegExp(ENCODED_WORD_PATTERN, 'g');
|
|
491
|
-
|
|
492
457
|
// Only linear whitespace separates encoded words, the rest is content
|
|
493
458
|
const WORD_SEPARATOR_REGEX = /^[ \t\r\n]+$/;
|
|
494
|
-
|
|
495
459
|
const ENCODED_WORDS_ONLY_REGEX = new RegExp(`^(?:${ENCODED_WORD_PATTERN}\\s*)+$`);
|
|
496
|
-
|
|
497
460
|
/**
|
|
498
461
|
* Checks whether a string is nothing but RFC 2047 encoded words. Kept next to the grammar
|
|
499
462
|
* it depends on, so the pattern has a single definition.
|
|
500
463
|
*
|
|
501
|
-
* @param
|
|
502
|
-
* @return
|
|
464
|
+
* @param str String to check
|
|
465
|
+
* @return true if the string holds encoded words and nothing else
|
|
503
466
|
*/
|
|
504
467
|
function isEncodedWordsOnly(str) {
|
|
505
468
|
return ENCODED_WORDS_ONLY_REGEX.test(str);
|
|
506
469
|
}
|
|
507
|
-
|
|
508
470
|
/**
|
|
509
471
|
* Splits a string into encoded words and the literal text around them.
|
|
510
472
|
*
|
|
@@ -512,17 +474,14 @@ function isEncodedWordsOnly(str) {
|
|
|
512
474
|
* means the input can not contain the marker, which previously let a sender delete text
|
|
513
475
|
* from a subject or a display name by writing the marker into the header themselves.
|
|
514
476
|
*
|
|
515
|
-
* @param
|
|
516
|
-
* @return
|
|
477
|
+
* @param str String to split
|
|
478
|
+
* @return Array of `{text}` and `{charset, encoding, encodedText}` tokens
|
|
517
479
|
*/
|
|
518
480
|
function splitEncodedWords(str) {
|
|
519
481
|
const tokens = [];
|
|
520
|
-
|
|
521
482
|
ENCODED_WORD_REGEX.lastIndex = 0;
|
|
522
|
-
|
|
523
483
|
let pos = 0;
|
|
524
484
|
let match;
|
|
525
|
-
|
|
526
485
|
while ((match = ENCODED_WORD_REGEX.exec(str))) {
|
|
527
486
|
if (match.index > pos) {
|
|
528
487
|
tokens.push({ text: str.substring(pos, match.index) });
|
|
@@ -530,14 +489,11 @@ function splitEncodedWords(str) {
|
|
|
530
489
|
tokens.push({ charset: match[1], encoding: match[2], encodedText: match[3] });
|
|
531
490
|
pos = match.index + match[0].length;
|
|
532
491
|
}
|
|
533
|
-
|
|
534
492
|
if (pos < str.length) {
|
|
535
493
|
tokens.push({ text: str.substring(pos) });
|
|
536
494
|
}
|
|
537
|
-
|
|
538
495
|
return tokens;
|
|
539
496
|
}
|
|
540
|
-
|
|
541
497
|
/**
|
|
542
498
|
* Checks if two adjacent encoded words may be decoded as a single unit. A multi byte
|
|
543
499
|
* character is often split across two words, so the bytes have to be concatenated before
|
|
@@ -546,32 +502,26 @@ function splitEncodedWords(str) {
|
|
|
546
502
|
*/
|
|
547
503
|
function canJoinWords(left, right) {
|
|
548
504
|
const encoding = left.encoding.toUpperCase();
|
|
549
|
-
|
|
550
505
|
if (left.charset !== right.charset || encoding !== right.encoding.toUpperCase()) {
|
|
551
506
|
return false;
|
|
552
507
|
}
|
|
553
|
-
|
|
554
508
|
if (encoding === 'B') {
|
|
555
509
|
return left.encodedText.length % 4 === 0 && !/=$/.test(left.encodedText);
|
|
556
510
|
}
|
|
557
|
-
|
|
558
511
|
return true;
|
|
559
512
|
}
|
|
560
|
-
|
|
561
513
|
/**
|
|
562
514
|
* Decodes a token list into a string, optionally merging adjacent encoded words.
|
|
563
515
|
*
|
|
564
|
-
* @param
|
|
565
|
-
* @param
|
|
566
|
-
* @return
|
|
516
|
+
* @param tokens Tokens from splitEncodedWords
|
|
517
|
+
* @param joinWords Whether adjacent encoded words may be decoded as one unit
|
|
518
|
+
* @return Decoded string
|
|
567
519
|
*/
|
|
568
520
|
function renderTokens(tokens, joinWords) {
|
|
569
521
|
let result = '';
|
|
570
522
|
let pending = null;
|
|
571
|
-
|
|
572
523
|
for (let i = 0; i < tokens.length; i++) {
|
|
573
524
|
const token = tokens[i];
|
|
574
|
-
|
|
575
525
|
if (token.text !== undefined) {
|
|
576
526
|
// whitespace between two encoded words is a folding artifact, not content
|
|
577
527
|
const nextToken = tokens[i + 1];
|
|
@@ -585,117 +535,103 @@ function renderTokens(tokens, joinWords) {
|
|
|
585
535
|
result += token.text;
|
|
586
536
|
continue;
|
|
587
537
|
}
|
|
588
|
-
|
|
589
538
|
if (pending && joinWords && canJoinWords(pending, token)) {
|
|
590
539
|
pending.encodedText += token.encodedText;
|
|
591
540
|
continue;
|
|
592
541
|
}
|
|
593
|
-
|
|
594
542
|
if (pending) {
|
|
595
543
|
result += decodeWord(pending.charset, pending.encoding, pending.encodedText);
|
|
596
544
|
}
|
|
597
545
|
pending = { charset: token.charset, encoding: token.encoding, encodedText: token.encodedText };
|
|
598
546
|
}
|
|
599
|
-
|
|
600
547
|
if (pending) {
|
|
601
548
|
result += decodeWord(pending.charset, pending.encoding, pending.encodedText);
|
|
602
549
|
}
|
|
603
|
-
|
|
604
550
|
return result;
|
|
605
551
|
}
|
|
606
|
-
|
|
552
|
+
/**
|
|
553
|
+
* Decodes RFC 2047 encoded words in a string
|
|
554
|
+
*
|
|
555
|
+
* @param str String that may contain encoded words
|
|
556
|
+
* @return Unicode string with every encoded word decoded
|
|
557
|
+
*/
|
|
607
558
|
function decodeWords(str) {
|
|
608
559
|
const tokens = splitEncodedWords((str || '').toString());
|
|
609
|
-
|
|
610
560
|
const result = renderTokens(tokens, true);
|
|
611
|
-
|
|
612
561
|
// A replacement character means the bytes did not decode, which happens when two
|
|
613
562
|
// words were joined that should have stayed apart. Retry keeping them separate.
|
|
614
563
|
return result.indexOf('\ufffd') < 0 ? result : renderTokens(tokens, false);
|
|
615
564
|
}
|
|
616
|
-
|
|
617
565
|
function decodeURIComponentWithCharset(encodedStr, charset) {
|
|
618
566
|
charset = charset || 'utf-8';
|
|
619
|
-
|
|
620
567
|
let encodedBytes = [];
|
|
621
568
|
for (let i = 0; i < encodedStr.length; i++) {
|
|
622
569
|
let c = encodedStr.charAt(i);
|
|
623
|
-
|
|
570
|
+
// -1 past the end of the string as well, since charCodeAt returns NaN there
|
|
571
|
+
const high = c === '%' ? hexNibble(encodedStr.charCodeAt(i + 1)) : -1;
|
|
572
|
+
const low = high >= 0 ? hexNibble(encodedStr.charCodeAt(i + 2)) : -1;
|
|
573
|
+
if (low >= 0) {
|
|
624
574
|
// encoded sequence
|
|
625
|
-
|
|
575
|
+
encodedBytes.push((high << 4) | low);
|
|
626
576
|
i += 2;
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
for (let j = 0; j <
|
|
631
|
-
encodedBytes.push(
|
|
577
|
+
}
|
|
578
|
+
else if (c.charCodeAt(0) > 126) {
|
|
579
|
+
const bytes = textEncoder.encode(c);
|
|
580
|
+
for (let j = 0; j < bytes.length; j++) {
|
|
581
|
+
encodedBytes.push(bytes[j]);
|
|
632
582
|
}
|
|
633
|
-
}
|
|
583
|
+
}
|
|
584
|
+
else {
|
|
634
585
|
// "normal" char
|
|
635
586
|
encodedBytes.push(c.charCodeAt(0));
|
|
636
587
|
}
|
|
637
588
|
}
|
|
638
|
-
|
|
639
589
|
const byteStr = new ArrayBuffer(encodedBytes.length);
|
|
640
590
|
const dataView = new DataView(byteStr);
|
|
641
591
|
for (let i = 0, len = encodedBytes.length; i < len; i++) {
|
|
642
592
|
dataView.setUint8(i, encodedBytes[i]);
|
|
643
593
|
}
|
|
644
|
-
|
|
645
594
|
return getDecoder(charset).decode(byteStr);
|
|
646
595
|
}
|
|
647
|
-
|
|
648
596
|
function decodeParameterValueContinuations(header) {
|
|
649
597
|
// handle parameter value continuations
|
|
650
598
|
// https://tools.ietf.org/html/rfc2231#section-3
|
|
651
|
-
|
|
652
599
|
// preprocess values
|
|
653
600
|
let paramKeys = new Map();
|
|
654
|
-
|
|
655
601
|
Object.keys(header.params).forEach(key => {
|
|
656
602
|
let match = key.match(/\*((\d+)\*?)?$/);
|
|
657
603
|
if (!match) {
|
|
658
604
|
// nothing to do here, does not seem like a continuation param
|
|
659
605
|
return;
|
|
660
606
|
}
|
|
661
|
-
|
|
662
|
-
let actualKey = key.substr(0, match.index).toLowerCase();
|
|
607
|
+
let actualKey = key.slice(0, match.index).toLowerCase();
|
|
663
608
|
let nr = Number(match[2]) || 0;
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
if (!paramKeys.has(actualKey)) {
|
|
609
|
+
let paramVal = paramKeys.get(actualKey);
|
|
610
|
+
if (!paramVal) {
|
|
667
611
|
paramVal = {
|
|
668
|
-
charset:
|
|
612
|
+
charset: null,
|
|
669
613
|
values: []
|
|
670
614
|
};
|
|
671
615
|
paramKeys.set(actualKey, paramVal);
|
|
672
|
-
} else {
|
|
673
|
-
paramVal = paramKeys.get(actualKey);
|
|
674
616
|
}
|
|
675
|
-
|
|
676
617
|
let value = header.params[key];
|
|
677
618
|
// RFC 2231 section 4.1: only a section whose name ends in '*' is percent encoded.
|
|
678
619
|
// A plain `name*0=` section is literal text, so decoding it invents characters
|
|
679
620
|
// that never appeared on the wire, turning `a%2F..%2Fetc` into a path traversal.
|
|
680
621
|
let encoded = match[0].charAt(match[0].length - 1) === '*';
|
|
681
|
-
|
|
682
622
|
if (nr === 0 && encoded && (match = value.match(/^([^']*)'[^']*'(.*)$/))) {
|
|
683
623
|
paramVal.charset = match[1] || 'utf-8';
|
|
684
624
|
value = match[2];
|
|
685
625
|
}
|
|
686
|
-
|
|
687
626
|
paramVal.values.push({ nr, value, encoded });
|
|
688
|
-
|
|
689
627
|
// remove the old reference
|
|
690
628
|
delete header.params[key];
|
|
691
629
|
});
|
|
692
|
-
|
|
693
630
|
paramKeys.forEach((paramVal, key) => {
|
|
694
631
|
let result = '';
|
|
695
632
|
// Adjacent encoded sections are decoded together, because a single multi byte
|
|
696
633
|
// character may be percent encoded across a section boundary.
|
|
697
634
|
let pending = '';
|
|
698
|
-
|
|
699
635
|
for (let part of paramVal.values.sort((a, b) => a.nr - b.nr)) {
|
|
700
636
|
if (part.encoded) {
|
|
701
637
|
pending += part.value;
|
|
@@ -707,11 +643,9 @@ function decodeParameterValueContinuations(header) {
|
|
|
707
643
|
}
|
|
708
644
|
result += part.value;
|
|
709
645
|
}
|
|
710
|
-
|
|
711
646
|
if (pending) {
|
|
712
647
|
result += decodeURIComponentWithCharset(pending, paramVal.charset);
|
|
713
648
|
}
|
|
714
|
-
|
|
715
649
|
header.params[key] = result;
|
|
716
650
|
});
|
|
717
651
|
}
|
|
@@ -720,12 +654,10 @@ class PassThroughDecoder {
|
|
|
720
654
|
constructor() {
|
|
721
655
|
this.chunks = [];
|
|
722
656
|
}
|
|
723
|
-
|
|
724
657
|
update(line) {
|
|
725
658
|
this.chunks.push(line);
|
|
726
659
|
this.chunks.push('\n');
|
|
727
660
|
}
|
|
728
|
-
|
|
729
661
|
finalize() {
|
|
730
662
|
// convert an array of arraybuffers into a blob and then back into a single arraybuffer
|
|
731
663
|
return blobToArrayBuffer(new Blob(this.chunks, { type: 'application/octet-stream' }));
|
|
@@ -735,30 +667,22 @@ class PassThroughDecoder {
|
|
|
735
667
|
class Base64Decoder {
|
|
736
668
|
constructor(opts) {
|
|
737
669
|
opts = opts || {};
|
|
738
|
-
|
|
739
670
|
this.decoder = opts.decoder || new TextDecoder();
|
|
740
|
-
|
|
741
671
|
this.maxChunkSize = 100 * 1024;
|
|
742
|
-
|
|
743
672
|
this.chunks = [];
|
|
744
|
-
|
|
745
673
|
this.remainder = '';
|
|
746
674
|
}
|
|
747
|
-
|
|
748
675
|
pushChunk(base64Str) {
|
|
749
676
|
if (base64Str.length) {
|
|
750
677
|
this.chunks.push(decodeBase64(base64Str));
|
|
751
678
|
}
|
|
752
679
|
}
|
|
753
|
-
|
|
754
680
|
flushRemainder() {
|
|
755
681
|
this.pushChunk(this.remainder);
|
|
756
682
|
this.remainder = '';
|
|
757
683
|
}
|
|
758
|
-
|
|
759
684
|
update(buffer) {
|
|
760
685
|
let str = this.decoder.decode(buffer).replace(/[^a-zA-Z0-9+/=]+/g, '');
|
|
761
|
-
|
|
762
686
|
// '=' terminates a base64 unit. Some mailers pad every line, and erasing the
|
|
763
687
|
// padding used to concatenate the units, which knocked everything after the first
|
|
764
688
|
// embedded pad out of 4 character alignment and decoded it to garbage.
|
|
@@ -770,41 +694,33 @@ class Base64Decoder {
|
|
|
770
694
|
this.flushRemainder();
|
|
771
695
|
}
|
|
772
696
|
}
|
|
773
|
-
|
|
774
697
|
if (this.remainder.length >= this.maxChunkSize) {
|
|
775
698
|
const alignedLength = Math.floor(this.remainder.length / 4) * 4;
|
|
776
699
|
this.pushChunk(this.remainder.substring(0, alignedLength));
|
|
777
700
|
this.remainder = this.remainder.substring(alignedLength);
|
|
778
701
|
}
|
|
779
702
|
}
|
|
780
|
-
|
|
781
703
|
finalize() {
|
|
782
704
|
this.flushRemainder();
|
|
783
|
-
|
|
784
705
|
return blobToArrayBuffer(new Blob(this.chunks, { type: 'application/octet-stream' }));
|
|
785
706
|
}
|
|
786
707
|
}
|
|
787
708
|
|
|
788
709
|
const CHR_EQUALS = 0x3d;
|
|
789
710
|
const CHR_LF = 0x0a;
|
|
790
|
-
|
|
791
711
|
class QPDecoder {
|
|
792
712
|
constructor() {
|
|
793
713
|
this.maxChunkSize = 100 * 1024;
|
|
794
|
-
|
|
795
714
|
this.buffer = new Uint8Array(this.maxChunkSize);
|
|
796
715
|
this.bufferPos = 0;
|
|
797
|
-
|
|
798
716
|
this.chunks = [];
|
|
799
717
|
}
|
|
800
|
-
|
|
801
718
|
writeByte(byte) {
|
|
802
719
|
if (this.bufferPos >= this.buffer.length) {
|
|
803
720
|
this.flushBuffer();
|
|
804
721
|
}
|
|
805
722
|
this.buffer[this.bufferPos++] = byte;
|
|
806
723
|
}
|
|
807
|
-
|
|
808
724
|
// Literal text is the bulk of a typical body, so it is copied in runs rather than a
|
|
809
725
|
// byte at a time
|
|
810
726
|
writeBytes(line, start, end) {
|
|
@@ -818,55 +734,46 @@ class QPDecoder {
|
|
|
818
734
|
start += count;
|
|
819
735
|
}
|
|
820
736
|
}
|
|
821
|
-
|
|
822
737
|
flushBuffer() {
|
|
823
738
|
if (this.bufferPos) {
|
|
824
739
|
this.chunks.push(this.buffer.slice(0, this.bufferPos));
|
|
825
740
|
this.bufferPos = 0;
|
|
826
741
|
}
|
|
827
742
|
}
|
|
828
|
-
|
|
829
743
|
// Quoted-printable source is 7 bit by definition, so it is decoded byte by byte and
|
|
830
744
|
// the result is handed on as bytes. Running the body charset over the encoded source
|
|
831
745
|
// instead corrupted every part whose charset was not ASCII compatible: the same
|
|
832
746
|
// content that decoded correctly in base64 came out as mojibake in quoted-printable.
|
|
833
747
|
update(line) {
|
|
834
748
|
let len = line.length;
|
|
835
|
-
|
|
836
749
|
// a line ending in '=' is a soft line break, the newline is not part of the content
|
|
837
750
|
const softBreak = len > 0 && line[len - 1] === CHR_EQUALS;
|
|
838
751
|
if (softBreak) {
|
|
839
752
|
len--;
|
|
840
753
|
}
|
|
841
|
-
|
|
842
754
|
let literalStart = 0;
|
|
843
755
|
for (let i = 0; i < len; i++) {
|
|
844
756
|
if (line[i] !== CHR_EQUALS || i + 2 >= len) {
|
|
845
757
|
continue;
|
|
846
758
|
}
|
|
847
|
-
|
|
848
759
|
const high = hexNibble(line[i + 1]);
|
|
849
760
|
const low = hexNibble(line[i + 2]);
|
|
850
761
|
if (high < 0 || low < 0) {
|
|
851
762
|
// not a valid escape sequence, keep it as literal text
|
|
852
763
|
continue;
|
|
853
764
|
}
|
|
854
|
-
|
|
855
765
|
this.writeBytes(line, literalStart, i);
|
|
856
766
|
this.writeByte((high << 4) | low);
|
|
857
767
|
i += 2;
|
|
858
768
|
literalStart = i + 1;
|
|
859
769
|
}
|
|
860
770
|
this.writeBytes(line, literalStart, len);
|
|
861
|
-
|
|
862
771
|
if (!softBreak) {
|
|
863
772
|
this.writeByte(CHR_LF);
|
|
864
773
|
}
|
|
865
774
|
}
|
|
866
|
-
|
|
867
775
|
finalize() {
|
|
868
776
|
this.flushBuffer();
|
|
869
|
-
|
|
870
777
|
// convert an array of arraybuffers into a blob and then back into a single arraybuffer
|
|
871
778
|
return blobToArrayBuffer(new Blob(this.chunks, { type: 'application/octet-stream' }));
|
|
872
779
|
}
|
|
@@ -877,19 +784,18 @@ class QPDecoder {
|
|
|
877
784
|
// parser skips into a genuine header, which is how a second `From:` gets smuggled past
|
|
878
785
|
// anything that inspects the raw message.
|
|
879
786
|
const headerDecoder = new TextDecoder('utf-8', { ignoreBOM: true });
|
|
880
|
-
|
|
881
787
|
// Trims only the whitespace RFC 5322 allows around a field name. String.prototype.trim
|
|
882
788
|
// also strips U+00A0, U+FEFF, U+2028 and the rest of the Unicode spaces, which turns a
|
|
883
|
-
// line that a strict parser rejects into a canonical field name: `
|
|
884
|
-
// `from` header, and since the first occurrence of a header wins
|
|
885
|
-
// sender. Leaving the character in the key keeps the line visible
|
|
886
|
-
// collide with a genuine header.
|
|
789
|
+
// line that a strict parser rejects into a canonical field name: a `From:` line with a
|
|
790
|
+
// leading U+00A0 became a `from` header, and since the first occurrence of a header wins
|
|
791
|
+
// it outranked the real sender. Leaving the character in the key keeps the line visible
|
|
792
|
+
// without letting it collide with a genuine header.
|
|
887
793
|
//
|
|
888
794
|
// An index scan rather than `/^[ \t]+|[ \t]+$/g`, which retries the trailing branch at
|
|
889
795
|
// every position of a blank run that is followed by other text, so a single header with
|
|
890
796
|
// a long run of spaces in the middle took seconds to trim.
|
|
891
|
-
const isWsp = c => c === 0x20 || c === 0x09;
|
|
892
|
-
const trimWsp = str => {
|
|
797
|
+
const isWsp = (c) => c === 0x20 || c === 0x09;
|
|
798
|
+
const trimWsp = (str) => {
|
|
893
799
|
let start = 0;
|
|
894
800
|
let end = str.length;
|
|
895
801
|
while (start < end && isWsp(str.charCodeAt(start))) {
|
|
@@ -900,7 +806,6 @@ const trimWsp = str => {
|
|
|
900
806
|
}
|
|
901
807
|
return str.slice(start, end);
|
|
902
808
|
};
|
|
903
|
-
|
|
904
809
|
// Headers that decide how this part's body is read, see processHeaders
|
|
905
810
|
const CONTENT_HEADERS = new Set([
|
|
906
811
|
'content-type',
|
|
@@ -909,78 +814,69 @@ const CONTENT_HEADERS = new Set([
|
|
|
909
814
|
'content-id',
|
|
910
815
|
'content-description'
|
|
911
816
|
]);
|
|
912
|
-
|
|
913
817
|
class MimeNode {
|
|
914
818
|
constructor(options) {
|
|
915
|
-
this.options = options
|
|
916
|
-
|
|
917
|
-
this.postalMime = this.options.postalMime;
|
|
918
|
-
|
|
819
|
+
this.options = options;
|
|
820
|
+
this.postalMime = options.postalMime;
|
|
919
821
|
this.childNodes = [];
|
|
920
822
|
// Cursor into childNodes for finalizeChildNodes. Every new part of a multipart
|
|
921
823
|
// finalizes its parent's children, so re-walking the whole array each time is
|
|
922
824
|
// quadratic in the number of parts.
|
|
923
825
|
this.finalizedChildCount = 0;
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
this.parentNode = this.options.parentNode;
|
|
927
|
-
|
|
826
|
+
if (options.parentNode) {
|
|
827
|
+
this.parentNode = options.parentNode;
|
|
928
828
|
this.depth = this.parentNode.depth + 1;
|
|
929
|
-
if (this.depth >
|
|
930
|
-
throw new Error(`Maximum MIME nesting depth of ${
|
|
829
|
+
if (this.depth > options.maxNestingDepth) {
|
|
830
|
+
throw new Error(`Maximum MIME nesting depth of ${options.maxNestingDepth} levels exceeded`);
|
|
931
831
|
}
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
832
|
+
options.parentNode.childNodes.push(this);
|
|
833
|
+
}
|
|
834
|
+
else {
|
|
935
835
|
this.depth = 0;
|
|
936
836
|
}
|
|
937
|
-
|
|
938
837
|
this.state = 'header';
|
|
939
|
-
|
|
940
838
|
this.headerLines = [];
|
|
941
|
-
|
|
942
839
|
// RFC 2046 Section 5.1.5: multipart/digest defaults to message/rfc822
|
|
943
|
-
const parentMultipartType =
|
|
840
|
+
const parentMultipartType = options.parentMultipartType || null;
|
|
944
841
|
const defaultContentType = parentMultipartType === 'digest' ? 'message/rfc822' : 'text/plain';
|
|
945
|
-
|
|
946
|
-
//
|
|
947
|
-
// processHeaders
|
|
842
|
+
// The value is replaced by the first matching header and the rest is derived
|
|
843
|
+
// from it, see the CONTENT_HEADERS pass in processHeaders
|
|
948
844
|
this.contentType = {
|
|
949
|
-
value: defaultContentType
|
|
845
|
+
value: defaultContentType,
|
|
846
|
+
parsed: { value: defaultContentType, params: {} },
|
|
847
|
+
multipart: false
|
|
950
848
|
};
|
|
951
|
-
|
|
952
849
|
this.contentTransferEncoding = {
|
|
953
|
-
value: '8bit'
|
|
850
|
+
value: '8bit',
|
|
851
|
+
encoding: ''
|
|
954
852
|
};
|
|
955
|
-
|
|
956
853
|
this.contentDisposition = {
|
|
957
|
-
value: ''
|
|
854
|
+
value: '',
|
|
855
|
+
parsed: { value: '', params: {} }
|
|
958
856
|
};
|
|
959
|
-
|
|
960
857
|
this.headers = [];
|
|
961
|
-
|
|
962
|
-
this.contentDecoder =
|
|
858
|
+
this.rawHeaderLines = [];
|
|
859
|
+
this.contentDecoder = null;
|
|
860
|
+
this.content = null;
|
|
963
861
|
}
|
|
964
|
-
|
|
965
862
|
setupContentDecoder(transferEncoding) {
|
|
966
863
|
if (/base64/i.test(transferEncoding)) {
|
|
967
864
|
this.contentDecoder = new Base64Decoder();
|
|
968
|
-
}
|
|
865
|
+
}
|
|
866
|
+
else if (/quoted-printable/i.test(transferEncoding)) {
|
|
969
867
|
this.contentDecoder = new QPDecoder();
|
|
970
|
-
}
|
|
868
|
+
}
|
|
869
|
+
else {
|
|
971
870
|
this.contentDecoder = new PassThroughDecoder();
|
|
972
871
|
}
|
|
973
872
|
}
|
|
974
|
-
|
|
975
873
|
async finalize() {
|
|
976
874
|
if (this.state === 'finished') {
|
|
977
875
|
return;
|
|
978
876
|
}
|
|
979
|
-
|
|
980
877
|
if (this.state === 'header') {
|
|
981
878
|
this.processHeaders();
|
|
982
879
|
}
|
|
983
|
-
|
|
984
880
|
// remove self from boundary listing
|
|
985
881
|
let boundaries = this.postalMime.boundaries;
|
|
986
882
|
for (let i = boundaries.length - 1; i >= 0; i--) {
|
|
@@ -990,19 +886,14 @@ class MimeNode {
|
|
|
990
886
|
break;
|
|
991
887
|
}
|
|
992
888
|
}
|
|
993
|
-
|
|
994
889
|
await this.finalizeChildNodes();
|
|
995
|
-
|
|
996
890
|
this.content = this.contentDecoder ? await this.contentDecoder.finalize() : null;
|
|
997
|
-
|
|
998
891
|
// The decoder buffers every body line it received, so keeping it around
|
|
999
892
|
// retains a second copy of the content for the lifetime of the node.
|
|
1000
893
|
// Nothing reads it once the node is finished, so release it here.
|
|
1001
|
-
this.contentDecoder =
|
|
1002
|
-
|
|
894
|
+
this.contentDecoder = null;
|
|
1003
895
|
this.state = 'finished';
|
|
1004
896
|
}
|
|
1005
|
-
|
|
1006
897
|
async finalizeChildNodes() {
|
|
1007
898
|
// Children are only ever appended, so everything before the cursor is already
|
|
1008
899
|
// finished and re-visiting it only costs time.
|
|
@@ -1010,7 +901,6 @@ class MimeNode {
|
|
|
1010
901
|
await this.childNodes[this.finalizedChildCount++].finalize();
|
|
1011
902
|
}
|
|
1012
903
|
}
|
|
1013
|
-
|
|
1014
904
|
// Strip RFC 822 comments (parenthesized text) from structured header values.
|
|
1015
905
|
//
|
|
1016
906
|
// Inside an unquoted parameter value a parenthesis that continues the current token is
|
|
@@ -1025,24 +915,20 @@ class MimeNode {
|
|
|
1025
915
|
let commentStart = -1;
|
|
1026
916
|
// A parameter value starts at `=` and ends at the `;` that begins the next one
|
|
1027
917
|
let inParameterValue = false;
|
|
1028
|
-
|
|
1029
918
|
// Whether result ends in SP or HTAB. Kept up to date on every append, because
|
|
1030
919
|
// testing `/[ \t]$/` against result flattens the whole string on each `(` and is
|
|
1031
920
|
// quadratic in the length of the header.
|
|
1032
921
|
let endsWithWsp = false;
|
|
1033
|
-
const append = c => {
|
|
922
|
+
const append = (c) => {
|
|
1034
923
|
result += c;
|
|
1035
924
|
endsWithWsp = c === ' ' || c === '\t';
|
|
1036
925
|
};
|
|
1037
|
-
|
|
1038
926
|
// A comment may only appear where linear whitespace is allowed, so inside a
|
|
1039
927
|
// parameter value the parenthesis has to follow whitespace to open one. Outside
|
|
1040
928
|
// one, eg. after the type itself, anything goes.
|
|
1041
929
|
const opensComment = () => !inParameterValue || !result.length || endsWithWsp;
|
|
1042
|
-
|
|
1043
930
|
for (let i = 0; i < str.length; i++) {
|
|
1044
931
|
const chr = str.charAt(i);
|
|
1045
|
-
|
|
1046
932
|
if (escaped) {
|
|
1047
933
|
if (depth === 0) {
|
|
1048
934
|
append(chr);
|
|
@@ -1050,7 +936,6 @@ class MimeNode {
|
|
|
1050
936
|
escaped = false;
|
|
1051
937
|
continue;
|
|
1052
938
|
}
|
|
1053
|
-
|
|
1054
939
|
if (chr === '\\') {
|
|
1055
940
|
escaped = true;
|
|
1056
941
|
if (depth === 0) {
|
|
@@ -1058,13 +943,11 @@ class MimeNode {
|
|
|
1058
943
|
}
|
|
1059
944
|
continue;
|
|
1060
945
|
}
|
|
1061
|
-
|
|
1062
946
|
if (chr === '"' && depth === 0) {
|
|
1063
947
|
inQuote = !inQuote;
|
|
1064
948
|
append(chr);
|
|
1065
949
|
continue;
|
|
1066
950
|
}
|
|
1067
|
-
|
|
1068
951
|
if (!inQuote) {
|
|
1069
952
|
if (chr === '(' && opensComment()) {
|
|
1070
953
|
if (depth === 0) {
|
|
@@ -1080,40 +963,34 @@ class MimeNode {
|
|
|
1080
963
|
if (depth === 0) {
|
|
1081
964
|
if (chr === '=') {
|
|
1082
965
|
inParameterValue = true;
|
|
1083
|
-
}
|
|
966
|
+
}
|
|
967
|
+
else if (chr === ';') {
|
|
1084
968
|
inParameterValue = false;
|
|
1085
969
|
}
|
|
1086
970
|
}
|
|
1087
971
|
}
|
|
1088
|
-
|
|
1089
972
|
if (depth === 0) {
|
|
1090
973
|
append(chr);
|
|
1091
974
|
}
|
|
1092
975
|
}
|
|
1093
|
-
|
|
1094
976
|
if (depth === 0) {
|
|
1095
977
|
return result;
|
|
1096
978
|
}
|
|
1097
|
-
|
|
1098
979
|
// An unbalanced `(` is not a comment. Dropping everything after it would take any
|
|
1099
980
|
// parameter that follows with it, including the boundary that holds the message
|
|
1100
981
|
// together, so the dangling text is only discarded when nothing follows it.
|
|
1101
982
|
return str.indexOf(';', commentStart) < 0 ? result : str;
|
|
1102
983
|
}
|
|
1103
|
-
|
|
1104
984
|
parseStructuredHeader(str) {
|
|
1105
985
|
// Strip RFC 822 comments before parsing
|
|
1106
986
|
str = this.stripComments(str);
|
|
1107
|
-
|
|
1108
987
|
let response = {
|
|
1109
|
-
value:
|
|
988
|
+
value: '',
|
|
1110
989
|
params: {}
|
|
1111
990
|
};
|
|
1112
|
-
|
|
1113
991
|
let key = false;
|
|
1114
992
|
let value = '';
|
|
1115
993
|
let stage = 'value';
|
|
1116
|
-
|
|
1117
994
|
// Whitespace seen outside a quoted string is held back until a significant
|
|
1118
995
|
// character follows it, so surrounding whitespace can be dropped without
|
|
1119
996
|
// trimming spaces the sender quoted on purpose. Trimming the stored value
|
|
@@ -1121,19 +998,16 @@ class MimeNode {
|
|
|
1121
998
|
// next continuation section is meant to be appended to.
|
|
1122
999
|
let pendingSpace = '';
|
|
1123
1000
|
let quoteClosed = false;
|
|
1124
|
-
|
|
1125
1001
|
let quote = false;
|
|
1126
1002
|
let escaped = false;
|
|
1127
1003
|
let chr;
|
|
1128
|
-
|
|
1129
|
-
const addChr = c => {
|
|
1004
|
+
const addChr = (c) => {
|
|
1130
1005
|
if (value.length) {
|
|
1131
1006
|
value += pendingSpace;
|
|
1132
1007
|
}
|
|
1133
1008
|
pendingSpace = '';
|
|
1134
1009
|
value += c;
|
|
1135
1010
|
};
|
|
1136
|
-
|
|
1137
1011
|
const takeValue = () => {
|
|
1138
1012
|
const result = value;
|
|
1139
1013
|
value = '';
|
|
@@ -1141,7 +1015,6 @@ class MimeNode {
|
|
|
1141
1015
|
quoteClosed = false;
|
|
1142
1016
|
return result;
|
|
1143
1017
|
};
|
|
1144
|
-
|
|
1145
1018
|
// A duplicated parameter resolves to its first occurrence, matching how duplicated
|
|
1146
1019
|
// headers are resolved. Letting the last one win means `boundary="b"; boundary="c"`
|
|
1147
1020
|
// registers a boundary that no delimiter in the message matches, which drops the
|
|
@@ -1152,16 +1025,15 @@ class MimeNode {
|
|
|
1152
1025
|
response.params[name] = result;
|
|
1153
1026
|
}
|
|
1154
1027
|
};
|
|
1155
|
-
|
|
1156
1028
|
const storeValue = () => {
|
|
1157
1029
|
const result = takeValue();
|
|
1158
1030
|
if (key === false) {
|
|
1159
1031
|
response.value = result;
|
|
1160
|
-
}
|
|
1032
|
+
}
|
|
1033
|
+
else {
|
|
1161
1034
|
storeParam(key, result);
|
|
1162
1035
|
}
|
|
1163
1036
|
};
|
|
1164
|
-
|
|
1165
1037
|
// A parameter name with no `=` is a valueless parameter, not the start of the
|
|
1166
1038
|
// next one. Without this the name would keep growing across the `;` and swallow
|
|
1167
1039
|
// whatever followed, which is how `x=1; flag; boundary="AAA"` loses its boundary.
|
|
@@ -1171,7 +1043,6 @@ class MimeNode {
|
|
|
1171
1043
|
storeParam(name.toLowerCase(), '');
|
|
1172
1044
|
}
|
|
1173
1045
|
};
|
|
1174
|
-
|
|
1175
1046
|
for (let i = 0, len = str.length; i < len; i++) {
|
|
1176
1047
|
chr = str.charAt(i);
|
|
1177
1048
|
switch (stage) {
|
|
@@ -1190,16 +1061,19 @@ class MimeNode {
|
|
|
1190
1061
|
case 'value':
|
|
1191
1062
|
if (escaped) {
|
|
1192
1063
|
addChr(chr);
|
|
1193
|
-
}
|
|
1064
|
+
}
|
|
1065
|
+
else if (quote && chr === '\\') {
|
|
1194
1066
|
// backslash only escapes inside a quoted string, everywhere else
|
|
1195
1067
|
// it is an ordinary character. Treating it as an escape turns
|
|
1196
1068
|
// `filename=C:\Users\me\a.txt` into `C:Usersmea.txt`.
|
|
1197
1069
|
escaped = true;
|
|
1198
1070
|
continue;
|
|
1199
|
-
}
|
|
1071
|
+
}
|
|
1072
|
+
else if (quote && chr === quote) {
|
|
1200
1073
|
quote = false;
|
|
1201
1074
|
quoteClosed = true;
|
|
1202
|
-
}
|
|
1075
|
+
}
|
|
1076
|
+
else if (!quote && chr === '"') {
|
|
1203
1077
|
quote = chr;
|
|
1204
1078
|
// whitespace before a quote that opens the value is padding, but
|
|
1205
1079
|
// between a token and a quoted string it is content
|
|
@@ -1207,12 +1081,15 @@ class MimeNode {
|
|
|
1207
1081
|
value += pendingSpace;
|
|
1208
1082
|
}
|
|
1209
1083
|
pendingSpace = '';
|
|
1210
|
-
}
|
|
1084
|
+
}
|
|
1085
|
+
else if (!quote && chr === ';') {
|
|
1211
1086
|
storeValue();
|
|
1212
1087
|
stage = 'key';
|
|
1213
|
-
}
|
|
1088
|
+
}
|
|
1089
|
+
else if (!quote && (chr === ' ' || chr === '\t')) {
|
|
1214
1090
|
pendingSpace += chr;
|
|
1215
|
-
}
|
|
1091
|
+
}
|
|
1092
|
+
else if (!quoteClosed) {
|
|
1216
1093
|
addChr(chr);
|
|
1217
1094
|
}
|
|
1218
1095
|
// Anything else is trailing junk after a closed quoted string. RFC 2045
|
|
@@ -1223,26 +1100,22 @@ class MimeNode {
|
|
|
1223
1100
|
break;
|
|
1224
1101
|
}
|
|
1225
1102
|
}
|
|
1226
|
-
|
|
1227
1103
|
// finalize remainder
|
|
1228
1104
|
if (stage === 'value') {
|
|
1229
1105
|
storeValue();
|
|
1230
|
-
}
|
|
1106
|
+
}
|
|
1107
|
+
else {
|
|
1231
1108
|
// treat as key without value, see emptykey:
|
|
1232
1109
|
// Header-Key: somevalue; key=value; emptykey
|
|
1233
1110
|
storeEmptyKey();
|
|
1234
1111
|
}
|
|
1235
|
-
|
|
1236
1112
|
if (response.value) {
|
|
1237
1113
|
response.value = response.value.toLowerCase();
|
|
1238
1114
|
}
|
|
1239
|
-
|
|
1240
1115
|
// convert Parameter Value Continuations into single strings
|
|
1241
1116
|
decodeParameterValueContinuations(response);
|
|
1242
|
-
|
|
1243
1117
|
return response;
|
|
1244
1118
|
}
|
|
1245
|
-
|
|
1246
1119
|
decodeFlowedText(str, delSp) {
|
|
1247
1120
|
// Pieces of the result, joined once at the end. Growing a single string and
|
|
1248
1121
|
// calling endsWith() on it for every line flattens the whole paragraph again on
|
|
@@ -1254,11 +1127,9 @@ class MimeNode {
|
|
|
1254
1127
|
// starts at this index of parts and is this many characters long
|
|
1255
1128
|
let lineStart = 0;
|
|
1256
1129
|
let lineLength = 0;
|
|
1257
|
-
|
|
1258
1130
|
const lines = str.split(/\r?\n/);
|
|
1259
1131
|
for (let i = 0; i < lines.length; i++) {
|
|
1260
1132
|
let line = lines[i];
|
|
1261
|
-
|
|
1262
1133
|
// remove whitespace stuffing before anything else
|
|
1263
1134
|
// http://tools.ietf.org/html/rfc3676#section-4.4
|
|
1264
1135
|
// doing it after the join leaves the stuffed space of a continuation line
|
|
@@ -1266,55 +1137,47 @@ class MimeNode {
|
|
|
1266
1137
|
if (line.charAt(0) === ' ') {
|
|
1267
1138
|
line = line.slice(1);
|
|
1268
1139
|
}
|
|
1269
|
-
|
|
1270
1140
|
if (i) {
|
|
1271
1141
|
const last = parts.length ? parts[parts.length - 1] : '';
|
|
1272
|
-
|
|
1273
1142
|
// soft linebreaks are added after space symbols, except for the signature
|
|
1274
1143
|
// separator which is a line of its own
|
|
1275
1144
|
const isSignature = lineLength === 3 && parts.slice(lineStart).join('') === '-- ';
|
|
1276
|
-
|
|
1277
1145
|
if (last.endsWith(' ') && !isSignature) {
|
|
1278
1146
|
if (delSp) {
|
|
1279
1147
|
// delsp adds space to text to be able to fold it
|
|
1280
1148
|
// these spaces can be removed once the text is unfolded
|
|
1281
1149
|
if (last.length > 1) {
|
|
1282
1150
|
parts[parts.length - 1] = last.slice(0, -1);
|
|
1283
|
-
}
|
|
1151
|
+
}
|
|
1152
|
+
else {
|
|
1284
1153
|
parts.pop();
|
|
1285
1154
|
}
|
|
1286
1155
|
lineLength--;
|
|
1287
1156
|
}
|
|
1288
|
-
}
|
|
1157
|
+
}
|
|
1158
|
+
else {
|
|
1289
1159
|
parts.push('\n');
|
|
1290
1160
|
lineStart = parts.length;
|
|
1291
1161
|
lineLength = 0;
|
|
1292
1162
|
}
|
|
1293
1163
|
}
|
|
1294
|
-
|
|
1295
1164
|
if (line) {
|
|
1296
1165
|
parts.push(line);
|
|
1297
1166
|
lineLength += line.length;
|
|
1298
1167
|
}
|
|
1299
1168
|
}
|
|
1300
|
-
|
|
1301
1169
|
return parts.join('');
|
|
1302
1170
|
}
|
|
1303
|
-
|
|
1304
1171
|
getTextContent() {
|
|
1305
1172
|
if (!this.content) {
|
|
1306
1173
|
return '';
|
|
1307
1174
|
}
|
|
1308
|
-
|
|
1309
1175
|
let str = getDecoder(this.contentType.parsed.params.charset).decode(this.content);
|
|
1310
|
-
|
|
1311
1176
|
if (/^flowed$/i.test(this.contentType.parsed.params.format)) {
|
|
1312
1177
|
str = this.decodeFlowedText(str, /^yes$/i.test(this.contentType.parsed.params.delsp));
|
|
1313
1178
|
}
|
|
1314
|
-
|
|
1315
1179
|
return str;
|
|
1316
1180
|
}
|
|
1317
|
-
|
|
1318
1181
|
processHeaders() {
|
|
1319
1182
|
// First pass: group folded continuation lines with the header they belong to.
|
|
1320
1183
|
//
|
|
@@ -1331,43 +1194,35 @@ class MimeNode {
|
|
|
1331
1194
|
for (let line of this.headerLines) {
|
|
1332
1195
|
if (foldedLines.length && /^[ \t]/.test(line)) {
|
|
1333
1196
|
foldedLines[foldedLines.length - 1].push(line);
|
|
1334
|
-
}
|
|
1197
|
+
}
|
|
1198
|
+
else {
|
|
1335
1199
|
foldedLines.push([line]);
|
|
1336
1200
|
}
|
|
1337
1201
|
}
|
|
1338
|
-
|
|
1339
|
-
// Initialize rawHeaderLines to store unmodified lines
|
|
1340
|
-
this.rawHeaderLines = [];
|
|
1341
|
-
|
|
1342
1202
|
let seenContentHeaders = new Set();
|
|
1343
|
-
|
|
1344
1203
|
// Second pass: process headers in document order
|
|
1345
1204
|
for (let parts of foldedLines) {
|
|
1346
1205
|
let rawLine = parts.join('\n');
|
|
1347
|
-
|
|
1348
1206
|
// Extract key from raw line for rawHeaderLines
|
|
1349
1207
|
let sep = rawLine.indexOf(':');
|
|
1350
|
-
let rawKey = trimWsp(sep < 0 ? rawLine : rawLine.
|
|
1351
|
-
|
|
1208
|
+
let rawKey = trimWsp(sep < 0 ? rawLine : rawLine.slice(0, sep));
|
|
1352
1209
|
// Store raw line with lowercase key
|
|
1353
1210
|
this.rawHeaderLines.push({
|
|
1354
1211
|
key: rawKey.toLowerCase(),
|
|
1355
1212
|
line: rawLine
|
|
1356
1213
|
});
|
|
1357
|
-
|
|
1358
1214
|
// Unfolding removes the line break and keeps the folding whitespace, so
|
|
1359
1215
|
// `Subject: Hello\r\n World` stays `Hello World`. Collapsing every
|
|
1360
1216
|
// whitespace run instead also rewrote boundary values and filenames, and it
|
|
1361
1217
|
// replaced the non-ASCII spaces that raw UTF-8 headers (RFC 6532) may carry.
|
|
1362
1218
|
let unfoldedLine = parts.join('');
|
|
1363
1219
|
sep = unfoldedLine.indexOf(':');
|
|
1364
|
-
let key = trimWsp(sep < 0 ? unfoldedLine : unfoldedLine.
|
|
1220
|
+
let key = trimWsp(sep < 0 ? unfoldedLine : unfoldedLine.slice(0, sep));
|
|
1365
1221
|
// A bare CR is not legal in a field body. It used to be folded into a space by
|
|
1366
1222
|
// the whitespace collapse, and passing it through would hand consumers that
|
|
1367
1223
|
// write the value back out a line of their own.
|
|
1368
|
-
let value = sep < 0 ? '' : trimWsp(unfoldedLine.
|
|
1224
|
+
let value = sep < 0 ? '' : trimWsp(unfoldedLine.slice(sep + 1).replace(/[\r\n]+/g, ' '));
|
|
1369
1225
|
this.headers.push({ key: key.toLowerCase(), originalKey: key, value });
|
|
1370
|
-
|
|
1371
1226
|
// A header that decides how the body is read must resolve the same way every
|
|
1372
1227
|
// time it is duplicated, otherwise a message can present one Content-Type to a
|
|
1373
1228
|
// scanner and a different one here. Every one of these takes the first
|
|
@@ -1375,16 +1230,15 @@ class MimeNode {
|
|
|
1375
1230
|
const lowerKey = key.toLowerCase();
|
|
1376
1231
|
if (CONTENT_HEADERS.has(lowerKey) && !seenContentHeaders.has(lowerKey)) {
|
|
1377
1232
|
seenContentHeaders.add(lowerKey);
|
|
1378
|
-
|
|
1379
1233
|
switch (lowerKey) {
|
|
1380
1234
|
case 'content-type':
|
|
1381
|
-
this.contentType =
|
|
1235
|
+
this.contentType.value = value;
|
|
1382
1236
|
break;
|
|
1383
1237
|
case 'content-transfer-encoding':
|
|
1384
|
-
this.contentTransferEncoding =
|
|
1238
|
+
this.contentTransferEncoding.value = value;
|
|
1385
1239
|
break;
|
|
1386
1240
|
case 'content-disposition':
|
|
1387
|
-
this.contentDisposition =
|
|
1241
|
+
this.contentDisposition.value = value;
|
|
1388
1242
|
break;
|
|
1389
1243
|
case 'content-id':
|
|
1390
1244
|
this.contentId = value;
|
|
@@ -1395,12 +1249,10 @@ class MimeNode {
|
|
|
1395
1249
|
}
|
|
1396
1250
|
}
|
|
1397
1251
|
}
|
|
1398
|
-
|
|
1399
1252
|
this.contentType.parsed = this.parseStructuredHeader(this.contentType.value);
|
|
1400
1253
|
this.contentType.multipart = /^multipart\//i.test(this.contentType.parsed.value)
|
|
1401
|
-
? this.contentType.parsed.value.
|
|
1254
|
+
? this.contentType.parsed.value.slice(this.contentType.parsed.value.indexOf('/') + 1)
|
|
1402
1255
|
: false;
|
|
1403
|
-
|
|
1404
1256
|
if (this.contentType.multipart && this.contentType.parsed.params.boundary) {
|
|
1405
1257
|
// add self to boundary terminator listing
|
|
1406
1258
|
this.postalMime.boundaries.push({
|
|
@@ -1408,9 +1260,7 @@ class MimeNode {
|
|
|
1408
1260
|
node: this
|
|
1409
1261
|
});
|
|
1410
1262
|
}
|
|
1411
|
-
|
|
1412
1263
|
this.contentDisposition.parsed = this.parseStructuredHeader(this.contentDisposition.value);
|
|
1413
|
-
|
|
1414
1264
|
// Take the first token rather than splitting on the first non-token character.
|
|
1415
1265
|
// `split()` returns an empty string for anything that does not start with a word
|
|
1416
1266
|
// character, so `(comment) base64` and `"base64"` used to fall through to the
|
|
@@ -1418,34 +1268,34 @@ class MimeNode {
|
|
|
1418
1268
|
this.contentTransferEncoding.encoding = (this.stripComments(this.contentTransferEncoding.value)
|
|
1419
1269
|
.toLowerCase()
|
|
1420
1270
|
.match(/[\w-]+/) || [''])[0];
|
|
1421
|
-
|
|
1422
1271
|
this.setupContentDecoder(this.contentTransferEncoding.encoding);
|
|
1423
1272
|
}
|
|
1424
|
-
|
|
1425
1273
|
feed(line) {
|
|
1426
1274
|
switch (this.state) {
|
|
1427
1275
|
case 'header':
|
|
1428
1276
|
if (!line.length) {
|
|
1429
1277
|
this.state = 'body';
|
|
1430
|
-
|
|
1278
|
+
this.processHeaders();
|
|
1279
|
+
return;
|
|
1431
1280
|
}
|
|
1432
|
-
|
|
1433
1281
|
// Counted across the whole message, not per part. A per-node budget lets a
|
|
1434
1282
|
// multipart carry the limit again for every part it declares, so a message
|
|
1435
1283
|
// many times over the limit still parses.
|
|
1436
1284
|
this.postalMime.headerSize += line.length;
|
|
1437
|
-
|
|
1438
1285
|
if (this.postalMime.headerSize > this.options.maxHeadersSize) {
|
|
1439
1286
|
let error = new Error(`Maximum header size of ${this.options.maxHeadersSize} bytes exceeded`);
|
|
1440
1287
|
throw error;
|
|
1441
1288
|
}
|
|
1442
|
-
|
|
1443
1289
|
this.headerLines.push(headerDecoder.decode(line));
|
|
1444
1290
|
break;
|
|
1445
|
-
case 'body':
|
|
1446
|
-
// add line to body
|
|
1447
|
-
|
|
1448
|
-
|
|
1291
|
+
case 'body':
|
|
1292
|
+
// add line to body. processHeaders installs a decoder before the state
|
|
1293
|
+
// becomes body, and the only time the decoder is released is finalize,
|
|
1294
|
+
// which moves the state on to finished, so the guard only narrows the type
|
|
1295
|
+
if (this.contentDecoder) {
|
|
1296
|
+
this.contentDecoder.update(line);
|
|
1297
|
+
}
|
|
1298
|
+
break;
|
|
1449
1299
|
}
|
|
1450
1300
|
}
|
|
1451
1301
|
}
|
|
@@ -3690,40 +3540,33 @@ function decodeHTMLEntities(str) {
|
|
|
3690
3540
|
if (typeof htmlEntities[match] === 'string') {
|
|
3691
3541
|
return htmlEntities[match];
|
|
3692
3542
|
}
|
|
3693
|
-
|
|
3694
3543
|
if (entity.charAt(0) !== '#' || match.charAt(match.length - 1) !== ';') {
|
|
3695
3544
|
// keep as is, invalid or unknown sequence
|
|
3696
3545
|
return match;
|
|
3697
3546
|
}
|
|
3698
|
-
|
|
3699
3547
|
let codePoint;
|
|
3700
3548
|
if (entity.charAt(1) === 'x') {
|
|
3701
3549
|
// hex
|
|
3702
|
-
codePoint = parseInt(entity.
|
|
3703
|
-
}
|
|
3550
|
+
codePoint = parseInt(entity.slice(2), 16);
|
|
3551
|
+
}
|
|
3552
|
+
else {
|
|
3704
3553
|
// dec
|
|
3705
|
-
codePoint = parseInt(entity.
|
|
3554
|
+
codePoint = parseInt(entity.slice(1), 10);
|
|
3706
3555
|
}
|
|
3707
|
-
|
|
3708
3556
|
let output = '';
|
|
3709
|
-
|
|
3710
3557
|
if ((codePoint >= 0xd800 && codePoint <= 0xdfff) || codePoint > 0x10ffff) {
|
|
3711
3558
|
// Invalid range, return a replacement character instead
|
|
3712
|
-
return '\
|
|
3559
|
+
return '\ufffd';
|
|
3713
3560
|
}
|
|
3714
|
-
|
|
3715
3561
|
if (codePoint > 0xffff) {
|
|
3716
3562
|
codePoint -= 0x10000;
|
|
3717
3563
|
output += String.fromCharCode(((codePoint >>> 10) & 0x3ff) | 0xd800);
|
|
3718
3564
|
codePoint = 0xdc00 | (codePoint & 0x3ff);
|
|
3719
3565
|
}
|
|
3720
|
-
|
|
3721
3566
|
output += String.fromCharCode(codePoint);
|
|
3722
|
-
|
|
3723
3567
|
return output;
|
|
3724
3568
|
});
|
|
3725
3569
|
}
|
|
3726
|
-
|
|
3727
3570
|
function escapeHtml(str) {
|
|
3728
3571
|
return str.trim().replace(/[<>"'?&]/g, c => {
|
|
3729
3572
|
let hex = c.charCodeAt(0).toString(16);
|
|
@@ -3733,26 +3576,21 @@ function escapeHtml(str) {
|
|
|
3733
3576
|
return '&#x' + hex.toUpperCase() + ';';
|
|
3734
3577
|
});
|
|
3735
3578
|
}
|
|
3736
|
-
|
|
3737
3579
|
function textToHtml(str) {
|
|
3738
3580
|
let html = escapeHtml(str).replace(/\n/g, '<br />');
|
|
3739
3581
|
return '<div>' + html + '</div>';
|
|
3740
3582
|
}
|
|
3741
|
-
|
|
3742
3583
|
// htmlToText strips tags with regexes like `<br\b[^>]*>` and `<!--.*?-->`. Handed to
|
|
3743
3584
|
// String#replace as they are, the engine tries every candidate in turn and a candidate
|
|
3744
3585
|
// that does not close scans on to the next `>`, the end of the line or the end of the
|
|
3745
3586
|
// input before giving up, so an html part holding many unclosed `<`, `<a ` or `<!--`
|
|
3746
3587
|
// took seconds to minutes to convert. The helpers below give the same results in linear
|
|
3747
3588
|
// time.
|
|
3748
|
-
|
|
3749
3589
|
// Characters that `.` does not match, so a `.*` can not run across them
|
|
3750
3590
|
const LINE_TERMINATOR = /[\n\r\u2028\u2029]/g;
|
|
3751
|
-
|
|
3752
3591
|
function lastLineTerminator(str) {
|
|
3753
3592
|
return Math.max(str.lastIndexOf('\n'), str.lastIndexOf('\r'), str.lastIndexOf('\u2028'), str.lastIndexOf('\u2029'));
|
|
3754
3593
|
}
|
|
3755
|
-
|
|
3756
3594
|
/**
|
|
3757
3595
|
* Returns a function that finds the next match of a global regex at or after a position.
|
|
3758
3596
|
*
|
|
@@ -3760,14 +3598,13 @@ function lastLineTerminator(str) {
|
|
|
3760
3598
|
* until a position passes it, and no match stays the answer for good. That way every part
|
|
3761
3599
|
* of the string is scanned once, however many times it is asked about.
|
|
3762
3600
|
*
|
|
3763
|
-
* @param
|
|
3764
|
-
* @param
|
|
3765
|
-
* @return
|
|
3601
|
+
* @param str String to search
|
|
3602
|
+
* @param regex Global regex to look for
|
|
3603
|
+
* @return `pos => match` of the next match, or null if there is none
|
|
3766
3604
|
*/
|
|
3767
3605
|
function createFinder(str, regex) {
|
|
3768
3606
|
let searchedFrom = Infinity;
|
|
3769
3607
|
let found = null;
|
|
3770
|
-
|
|
3771
3608
|
return pos => {
|
|
3772
3609
|
if (pos < searchedFrom || (found && pos > found.index)) {
|
|
3773
3610
|
regex.lastIndex = pos;
|
|
@@ -3777,7 +3614,6 @@ function createFinder(str, regex) {
|
|
|
3777
3614
|
return found;
|
|
3778
3615
|
};
|
|
3779
3616
|
}
|
|
3780
|
-
|
|
3781
3617
|
/**
|
|
3782
3618
|
* `str.replace(pattern, replacement)` for a pattern shaped like `<tag\b[^>]*>`.
|
|
3783
3619
|
*
|
|
@@ -3785,16 +3621,15 @@ function createFinder(str, regex) {
|
|
|
3785
3621
|
* the last `>` can never match and is where every rescan happened. Leaving it out keeps
|
|
3786
3622
|
* the native replace, and its speed, for everything else.
|
|
3787
3623
|
*
|
|
3788
|
-
* @param
|
|
3789
|
-
* @param
|
|
3790
|
-
* @param
|
|
3791
|
-
* @return
|
|
3624
|
+
* @param str String to process
|
|
3625
|
+
* @param pattern Global regex that ends on the first `>` after its start
|
|
3626
|
+
* @param replacement Replacement text
|
|
3627
|
+
* @return Processed string
|
|
3792
3628
|
*/
|
|
3793
3629
|
function replaceClosedTags(str, pattern, replacement) {
|
|
3794
3630
|
const end = str.lastIndexOf('>') + 1;
|
|
3795
3631
|
return str.slice(0, end).replace(pattern, replacement) + str.slice(end);
|
|
3796
3632
|
}
|
|
3797
|
-
|
|
3798
3633
|
/**
|
|
3799
3634
|
* `str.replace(pattern, replacement)` for a tag pattern that may fail even when a `>`
|
|
3800
3635
|
* follows, like `<a\b[^>]*href...>`, or that runs on past its `>`.
|
|
@@ -3803,25 +3638,23 @@ function replaceClosedTags(str, pattern, replacement) {
|
|
|
3803
3638
|
* either. A candidate that does not match skips every other candidate before the same
|
|
3804
3639
|
* `>`, since those see the same tag body and fail the same way.
|
|
3805
3640
|
*
|
|
3806
|
-
* @param
|
|
3807
|
-
* @param
|
|
3808
|
-
* @param
|
|
3809
|
-
* @param
|
|
3810
|
-
* @return
|
|
3641
|
+
* @param str String to process
|
|
3642
|
+
* @param prefix Global regex for the start of a candidate, which can not contain `>`
|
|
3643
|
+
* @param pattern The whole pattern as a sticky regex
|
|
3644
|
+
* @param replacement Function given the match and its groups
|
|
3645
|
+
* @return Processed string
|
|
3811
3646
|
*/
|
|
3812
3647
|
function replaceTags(str, prefix, pattern, replacement) {
|
|
3813
3648
|
const parts = [];
|
|
3814
3649
|
// everything before this index is already in parts
|
|
3815
3650
|
let copied = 0;
|
|
3816
3651
|
let searchFrom = 0;
|
|
3817
|
-
|
|
3818
3652
|
while (true) {
|
|
3819
3653
|
prefix.lastIndex = searchFrom;
|
|
3820
3654
|
const candidate = prefix.exec(str);
|
|
3821
3655
|
if (!candidate) {
|
|
3822
3656
|
break;
|
|
3823
3657
|
}
|
|
3824
|
-
|
|
3825
3658
|
const start = candidate.index;
|
|
3826
3659
|
pattern.lastIndex = start;
|
|
3827
3660
|
const match = pattern.exec(str);
|
|
@@ -3833,28 +3666,25 @@ function replaceTags(str, prefix, pattern, replacement) {
|
|
|
3833
3666
|
searchFrom = gt + 1;
|
|
3834
3667
|
continue;
|
|
3835
3668
|
}
|
|
3836
|
-
|
|
3837
3669
|
parts.push(str.slice(copied, start), replacement(...match));
|
|
3838
3670
|
copied = searchFrom = start + match[0].length;
|
|
3839
3671
|
}
|
|
3840
|
-
|
|
3841
3672
|
parts.push(str.slice(copied));
|
|
3842
3673
|
return parts.join('');
|
|
3843
3674
|
}
|
|
3844
|
-
|
|
3845
3675
|
/**
|
|
3846
3676
|
* `str.replace(pattern, replacement)` for a pattern like `<!--.*?-->` or
|
|
3847
3677
|
* `<script\b[^>]*>.*?<\/script\b[^>]*>`, ie. an opener and a closer, either of which may
|
|
3848
3678
|
* continue with `[^>]*>`, and a lazy `.*?` between them. The engine rescans to the end of
|
|
3849
3679
|
* the line for every opener that is not closed.
|
|
3850
3680
|
*
|
|
3851
|
-
* @param
|
|
3852
|
-
* @param
|
|
3853
|
-
* @param
|
|
3854
|
-
* @param
|
|
3855
|
-
* @param
|
|
3856
|
-
* @param
|
|
3857
|
-
* @return
|
|
3681
|
+
* @param str String to process
|
|
3682
|
+
* @param open Global regex for the opener
|
|
3683
|
+
* @param openToGt Whether the opener continues with `[^>]*>`
|
|
3684
|
+
* @param close Global regex for the closer
|
|
3685
|
+
* @param closeToGt Whether the closer continues with `[^>]*>`
|
|
3686
|
+
* @param replacement Replacement text
|
|
3687
|
+
* @return Processed string
|
|
3858
3688
|
*/
|
|
3859
3689
|
function replaceBlocks(str, open, openToGt, close, closeToGt, replacement) {
|
|
3860
3690
|
const nextGt = createFinder(str, />/g);
|
|
@@ -3863,17 +3693,14 @@ function replaceBlocks(str, open, openToGt, close, closeToGt, replacement) {
|
|
|
3863
3693
|
const parts = [];
|
|
3864
3694
|
let copied = 0;
|
|
3865
3695
|
let searchFrom = 0;
|
|
3866
|
-
|
|
3867
3696
|
while (true) {
|
|
3868
3697
|
open.lastIndex = searchFrom;
|
|
3869
3698
|
const opener = open.exec(str);
|
|
3870
3699
|
if (!opener) {
|
|
3871
3700
|
break;
|
|
3872
3701
|
}
|
|
3873
|
-
|
|
3874
3702
|
const start = opener.index;
|
|
3875
3703
|
searchFrom = start + 1;
|
|
3876
|
-
|
|
3877
3704
|
// where the lazy `.*?` starts
|
|
3878
3705
|
let from = start + opener[0].length;
|
|
3879
3706
|
if (openToGt) {
|
|
@@ -3884,19 +3711,16 @@ function replaceBlocks(str, open, openToGt, close, closeToGt, replacement) {
|
|
|
3884
3711
|
}
|
|
3885
3712
|
from = gt.index + 1;
|
|
3886
3713
|
}
|
|
3887
|
-
|
|
3888
3714
|
const closer = nextClose(from);
|
|
3889
3715
|
if (!closer) {
|
|
3890
3716
|
// no closer follows this opener, so none follows a later one either
|
|
3891
3717
|
break;
|
|
3892
3718
|
}
|
|
3893
|
-
|
|
3894
3719
|
// `.*?` can not reach the closer across a line terminator
|
|
3895
3720
|
const lineEnd = nextLineEnd(from);
|
|
3896
3721
|
if (lineEnd && lineEnd.index < closer.index) {
|
|
3897
3722
|
continue;
|
|
3898
3723
|
}
|
|
3899
|
-
|
|
3900
3724
|
let end = closer.index + closer[0].length;
|
|
3901
3725
|
if (closeToGt) {
|
|
3902
3726
|
const gt = nextGt(end);
|
|
@@ -3907,62 +3731,52 @@ function replaceBlocks(str, open, openToGt, close, closeToGt, replacement) {
|
|
|
3907
3731
|
}
|
|
3908
3732
|
end = gt.index + 1;
|
|
3909
3733
|
}
|
|
3910
|
-
|
|
3911
3734
|
parts.push(str.slice(copied, start), replacement);
|
|
3912
3735
|
copied = searchFrom = end;
|
|
3913
3736
|
}
|
|
3914
|
-
|
|
3915
3737
|
parts.push(str.slice(copied));
|
|
3916
3738
|
return parts.join('');
|
|
3917
3739
|
}
|
|
3918
|
-
|
|
3919
3740
|
/**
|
|
3920
3741
|
* `str.replace(pattern, '')` for `^.*<tag\b[^>]*>`, ie. drops everything up to and
|
|
3921
3742
|
* including the last such tag on the first line.
|
|
3922
3743
|
*
|
|
3923
|
-
* @param
|
|
3924
|
-
* @param
|
|
3925
|
-
* @return
|
|
3744
|
+
* @param str String to process
|
|
3745
|
+
* @param prefix Global regex for the start of the tag, which can not contain `>`
|
|
3746
|
+
* @return Processed string
|
|
3926
3747
|
*/
|
|
3927
3748
|
function stripThroughLastTag(str, prefix) {
|
|
3928
3749
|
const lineEnd = str.search(LINE_TERMINATOR);
|
|
3929
3750
|
// `.*` stops at the first line terminator, and `[^>]*>` needs a `>` after the tag
|
|
3930
3751
|
const limit = Math.min(lineEnd < 0 ? str.length : lineEnd, str.lastIndexOf('>'));
|
|
3931
|
-
|
|
3932
3752
|
let last = -1;
|
|
3933
3753
|
prefix.lastIndex = 0;
|
|
3934
3754
|
let match;
|
|
3935
3755
|
while ((match = prefix.exec(str)) && match.index < limit) {
|
|
3936
3756
|
last = match.index;
|
|
3937
3757
|
}
|
|
3938
|
-
|
|
3939
3758
|
return last < 0 ? str : str.slice(str.indexOf('>', last) + 1);
|
|
3940
3759
|
}
|
|
3941
|
-
|
|
3942
3760
|
/**
|
|
3943
3761
|
* `str.replace(pattern, '')` for `<tag\b[^>]*>.*$`, ie. drops the first such tag whose
|
|
3944
3762
|
* `>` is on the last line, and everything after it.
|
|
3945
3763
|
*
|
|
3946
|
-
* @param
|
|
3947
|
-
* @param
|
|
3948
|
-
* @return
|
|
3764
|
+
* @param str String to process
|
|
3765
|
+
* @param prefix Global regex for the start of the tag, which can not contain `>`
|
|
3766
|
+
* @return Processed string
|
|
3949
3767
|
*/
|
|
3950
3768
|
function stripFromFirstTag(str, prefix) {
|
|
3951
3769
|
// `.*$` has to reach the end of the string without crossing a line terminator, so the
|
|
3952
3770
|
// tag has to start after the last `>` that comes before the last line terminator
|
|
3953
3771
|
const lineEnd = lastLineTerminator(str);
|
|
3954
3772
|
prefix.lastIndex = lineEnd < 0 ? 0 : str.lastIndexOf('>', lineEnd) + 1;
|
|
3955
|
-
|
|
3956
3773
|
const match = prefix.exec(str);
|
|
3957
3774
|
return match && match.index < str.lastIndexOf('>') ? str.slice(0, match.index) : str;
|
|
3958
3775
|
}
|
|
3959
|
-
|
|
3960
3776
|
function htmlToText(str) {
|
|
3961
3777
|
// we can't process tags on multiple lines so remove newlines first
|
|
3962
3778
|
str = str.replace(/\r?\n/g, '\u0001');
|
|
3963
|
-
|
|
3964
3779
|
str = replaceBlocks(str, /<!--/g, false, /-->/g, false, ' ');
|
|
3965
|
-
|
|
3966
3780
|
str = replaceClosedTags(str, /<br\b[^>]*>/gi, '\n');
|
|
3967
3781
|
str = replaceClosedTags(str, /<\/?(p|div|table|tr|td|th)\b[^>]*>/gi, '\n\n');
|
|
3968
3782
|
str = replaceBlocks(str, /<script\b/gi, true, /<\/script\b/gi, true, ' ');
|
|
@@ -3971,35 +3785,23 @@ function htmlToText(str) {
|
|
|
3971
3785
|
str = stripThroughLastTag(str, /<!doctype\b/gi);
|
|
3972
3786
|
str = stripFromFirstTag(str, /<\/body\b/gi);
|
|
3973
3787
|
str = stripFromFirstTag(str, /<\/html\b/gi);
|
|
3974
|
-
|
|
3975
3788
|
str = replaceTags(str, /<a\b/gi, /<a\b[^>]*href\s*=\s*["']?([^\s"']+)[^>]*>/iy, (match, href) => ` (${href}) `);
|
|
3976
|
-
|
|
3977
3789
|
str = replaceClosedTags(str, /<\/?(span|em|i|strong|b|u|a)\b[^>]*>/gi, '');
|
|
3978
|
-
|
|
3979
3790
|
// the whitespace after the tag may run on past the last `>`
|
|
3980
3791
|
str = replaceTags(str, /<li\b/gi, /<li\b[^>]*>[\n\u0001\s]*/iy, () => '* ');
|
|
3981
|
-
|
|
3982
3792
|
str = replaceClosedTags(str, /<hr\b[^>]*>/g, '\n-------------\n');
|
|
3983
|
-
|
|
3984
3793
|
str = replaceClosedTags(str, /<[^>]*>/g, ' ');
|
|
3985
|
-
|
|
3986
3794
|
str = str
|
|
3987
3795
|
// convert linebreak placeholders back to newlines
|
|
3988
3796
|
.replace(/\u0001/g, '\n')
|
|
3989
|
-
|
|
3990
3797
|
.replace(/[ \t]+/g, ' ')
|
|
3991
|
-
|
|
3992
3798
|
.replace(/^\s+$/gm, '')
|
|
3993
|
-
|
|
3994
3799
|
.replace(/\n\n+/g, '\n\n')
|
|
3995
3800
|
.replace(/^\n+/, '\n')
|
|
3996
3801
|
.replace(/\n+$/, '\n');
|
|
3997
|
-
|
|
3998
3802
|
str = decodeHTMLEntities(str);
|
|
3999
|
-
|
|
4000
3803
|
return str;
|
|
4001
3804
|
}
|
|
4002
|
-
|
|
4003
3805
|
// A message date is not always a date. PostalMime keeps the raw header value when it does
|
|
4004
3806
|
// not parse, and Intl.DateTimeFormat throws a RangeError on that, which used to reject the
|
|
4005
3807
|
// whole parse of any message carrying a forwarded copy with a broken Date header.
|
|
@@ -4007,12 +3809,10 @@ function formatDate(date) {
|
|
|
4007
3809
|
if (typeof Intl === 'undefined') {
|
|
4008
3810
|
return date;
|
|
4009
3811
|
}
|
|
4010
|
-
|
|
4011
3812
|
const parsed = new Date(date);
|
|
4012
3813
|
if (isNaN(parsed.getTime())) {
|
|
4013
3814
|
return date;
|
|
4014
3815
|
}
|
|
4015
|
-
|
|
4016
3816
|
return new Intl.DateTimeFormat('default', {
|
|
4017
3817
|
year: 'numeric',
|
|
4018
3818
|
month: 'numeric',
|
|
@@ -4023,80 +3823,58 @@ function formatDate(date) {
|
|
|
4023
3823
|
hour12: false
|
|
4024
3824
|
}).format(parsed);
|
|
4025
3825
|
}
|
|
4026
|
-
|
|
4027
3826
|
function formatTextAddress(address) {
|
|
4028
|
-
return []
|
|
4029
|
-
.concat(address.name || [])
|
|
4030
|
-
.concat(address.name ? `<${address.address}>` : address.address)
|
|
4031
|
-
.join(' ');
|
|
3827
|
+
return (address.name ? [address.name, `<${address.address}>`] : [address.address]).join(' ');
|
|
4032
3828
|
}
|
|
4033
|
-
|
|
4034
3829
|
function formatTextAddresses(addresses) {
|
|
4035
3830
|
let parts = [];
|
|
4036
|
-
|
|
4037
3831
|
let processAddress = (address, partCounter) => {
|
|
4038
3832
|
if (partCounter) {
|
|
4039
3833
|
parts.push(', ');
|
|
4040
3834
|
}
|
|
4041
|
-
|
|
4042
3835
|
if (address.group) {
|
|
4043
3836
|
let groupStart = `${address.name}:`;
|
|
4044
3837
|
let groupEnd = `;`;
|
|
4045
|
-
|
|
4046
3838
|
parts.push(groupStart);
|
|
4047
3839
|
address.group.forEach(processAddress);
|
|
4048
3840
|
parts.push(groupEnd);
|
|
4049
|
-
}
|
|
3841
|
+
}
|
|
3842
|
+
else {
|
|
4050
3843
|
parts.push(formatTextAddress(address));
|
|
4051
3844
|
}
|
|
4052
3845
|
};
|
|
4053
|
-
|
|
4054
3846
|
addresses.forEach(processAddress);
|
|
4055
|
-
|
|
4056
3847
|
return parts.join('');
|
|
4057
3848
|
}
|
|
4058
|
-
|
|
4059
3849
|
function formatHtmlAddress(address) {
|
|
4060
3850
|
return `<a href="mailto:${escapeHtml(address.address)}" class="postal-email-address">${escapeHtml(address.name || `<${address.address}>`)}</a>`;
|
|
4061
3851
|
}
|
|
4062
|
-
|
|
4063
3852
|
function formatHtmlAddresses(addresses) {
|
|
4064
3853
|
let parts = [];
|
|
4065
|
-
|
|
4066
3854
|
let processAddress = (address, partCounter) => {
|
|
4067
3855
|
if (partCounter) {
|
|
4068
3856
|
parts.push('<span class="postal-email-address-separator">, </span>');
|
|
4069
3857
|
}
|
|
4070
|
-
|
|
4071
3858
|
if (address.group) {
|
|
4072
3859
|
let groupStart = `<span class="postal-email-address-group">${escapeHtml(address.name)}:</span>`;
|
|
4073
3860
|
let groupEnd = `<span class="postal-email-address-group">;</span>`;
|
|
4074
|
-
|
|
4075
3861
|
parts.push(groupStart);
|
|
4076
3862
|
address.group.forEach(processAddress);
|
|
4077
3863
|
parts.push(groupEnd);
|
|
4078
|
-
}
|
|
3864
|
+
}
|
|
3865
|
+
else {
|
|
4079
3866
|
parts.push(formatHtmlAddress(address));
|
|
4080
3867
|
}
|
|
4081
3868
|
};
|
|
4082
|
-
|
|
4083
3869
|
addresses.forEach(processAddress);
|
|
4084
|
-
|
|
4085
3870
|
return parts.join(' ');
|
|
4086
3871
|
}
|
|
4087
|
-
|
|
4088
3872
|
function foldLines(str, lineLength, afterSpace) {
|
|
4089
3873
|
str = (str || '').toString();
|
|
4090
3874
|
lineLength = lineLength || 76;
|
|
4091
|
-
|
|
4092
|
-
let pos = 0,
|
|
4093
|
-
len = str.length,
|
|
4094
|
-
result = '',
|
|
4095
|
-
line,
|
|
4096
|
-
match;
|
|
4097
|
-
|
|
3875
|
+
let pos = 0, len = str.length, result = '', line, match;
|
|
4098
3876
|
while (pos < len) {
|
|
4099
|
-
line = str.
|
|
3877
|
+
line = str.slice(pos, pos + lineLength);
|
|
4100
3878
|
if (line.length < lineLength) {
|
|
4101
3879
|
result += line;
|
|
4102
3880
|
break;
|
|
@@ -4106,54 +3884,44 @@ function foldLines(str, lineLength, afterSpace) {
|
|
|
4106
3884
|
result += line;
|
|
4107
3885
|
pos += line.length;
|
|
4108
3886
|
continue;
|
|
4109
|
-
} else if (
|
|
4110
|
-
(match = line.match(/(\s+)[^\s]*$/)) &&
|
|
4111
|
-
match[0].length - ((match[1] || '').length ) < line.length
|
|
4112
|
-
) {
|
|
4113
|
-
line = line.substr(0, line.length - (match[0].length - ((match[1] || '').length )));
|
|
4114
|
-
} else if ((match = str.substr(pos + line.length).match(/^[^\s]+(\s*)/))) {
|
|
4115
|
-
line = line + match[0].substr(0, match[0].length - (0));
|
|
4116
3887
|
}
|
|
4117
|
-
|
|
3888
|
+
else if ((match = line.match(/(\s+)[^\s]*$/)) &&
|
|
3889
|
+
match[0].length - ((match[1] || '').length ) < line.length) {
|
|
3890
|
+
line = line.slice(0, line.length - (match[0].length - ((match[1] || '').length )));
|
|
3891
|
+
}
|
|
3892
|
+
else if ((match = str.slice(pos + line.length).match(/^[^\s]+(\s*)/))) {
|
|
3893
|
+
line = line + match[0].slice(0, match[0].length - (0));
|
|
3894
|
+
}
|
|
4118
3895
|
result += line;
|
|
4119
3896
|
pos += line.length;
|
|
4120
3897
|
if (pos < len) {
|
|
4121
3898
|
result += '\r\n';
|
|
4122
3899
|
}
|
|
4123
3900
|
}
|
|
4124
|
-
|
|
4125
3901
|
return result;
|
|
4126
3902
|
}
|
|
4127
|
-
|
|
4128
3903
|
function formatTextHeader(message) {
|
|
4129
3904
|
let rows = [];
|
|
4130
|
-
|
|
4131
3905
|
if (message.from) {
|
|
4132
3906
|
// through the plural formatter, because `From:` may hold RFC 5322 group syntax
|
|
4133
3907
|
// and a group has no address of its own
|
|
4134
3908
|
rows.push({ key: 'From', val: formatTextAddresses([message.from]) });
|
|
4135
3909
|
}
|
|
4136
|
-
|
|
4137
3910
|
if (message.subject) {
|
|
4138
3911
|
rows.push({ key: 'Subject', val: message.subject });
|
|
4139
3912
|
}
|
|
4140
|
-
|
|
4141
3913
|
if (message.date) {
|
|
4142
3914
|
rows.push({ key: 'Date', val: formatDate(message.date) });
|
|
4143
3915
|
}
|
|
4144
|
-
|
|
4145
3916
|
if (message.to && message.to.length) {
|
|
4146
3917
|
rows.push({ key: 'To', val: formatTextAddresses(message.to) });
|
|
4147
3918
|
}
|
|
4148
|
-
|
|
4149
3919
|
if (message.cc && message.cc.length) {
|
|
4150
3920
|
rows.push({ key: 'Cc', val: formatTextAddresses(message.cc) });
|
|
4151
3921
|
}
|
|
4152
|
-
|
|
4153
3922
|
if (message.bcc && message.bcc.length) {
|
|
4154
3923
|
rows.push({ key: 'Bcc', val: formatTextAddresses(message.bcc) });
|
|
4155
3924
|
}
|
|
4156
|
-
|
|
4157
3925
|
// Align keys and values by adding space between these two
|
|
4158
3926
|
// Also make sure that the separator line is as long as the longest line
|
|
4159
3927
|
// Should end up with something like this:
|
|
@@ -4165,144 +3933,361 @@ function formatTextHeader(message) {
|
|
|
4165
3933
|
To: not@found.com
|
|
4166
3934
|
-----------------------------
|
|
4167
3935
|
*/
|
|
4168
|
-
|
|
4169
3936
|
let maxKeyLength = rows
|
|
4170
3937
|
.map(r => r.key.length)
|
|
4171
3938
|
.reduce((acc, cur) => {
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
rows = rows.flatMap(row => {
|
|
3939
|
+
return cur > acc ? cur : acc;
|
|
3940
|
+
}, 0);
|
|
3941
|
+
const lines = rows.flatMap(row => {
|
|
4176
3942
|
let sepLen = maxKeyLength - row.key.length;
|
|
4177
3943
|
let prefix = `${row.key}: ${' '.repeat(sepLen)}`;
|
|
4178
3944
|
let emptyPrefix = `${' '.repeat(row.key.length + 1)} ${' '.repeat(sepLen)}`;
|
|
4179
|
-
|
|
4180
3945
|
let foldedLines = foldLines(row.val, 80)
|
|
4181
3946
|
.split(/\r?\n/)
|
|
4182
3947
|
.map(line => line.trim());
|
|
4183
|
-
|
|
4184
3948
|
return foldedLines.map((line, i) => `${i ? emptyPrefix : prefix}${line}`);
|
|
4185
3949
|
});
|
|
4186
|
-
|
|
4187
|
-
let maxLineLength = rows
|
|
3950
|
+
let maxLineLength = lines
|
|
4188
3951
|
.map(r => r.length)
|
|
4189
3952
|
.reduce((acc, cur) => {
|
|
4190
|
-
|
|
4191
|
-
|
|
4192
|
-
|
|
3953
|
+
return cur > acc ? cur : acc;
|
|
3954
|
+
}, 0);
|
|
4193
3955
|
let lineMarker = '-'.repeat(maxLineLength);
|
|
4194
|
-
|
|
4195
3956
|
let template = `
|
|
4196
3957
|
${lineMarker}
|
|
4197
|
-
${
|
|
3958
|
+
${lines.join('\n')}
|
|
4198
3959
|
${lineMarker}
|
|
4199
3960
|
`;
|
|
4200
|
-
|
|
4201
3961
|
return template;
|
|
4202
3962
|
}
|
|
4203
|
-
|
|
4204
3963
|
function formatHtmlHeader(message) {
|
|
4205
3964
|
let rows = [];
|
|
4206
|
-
|
|
4207
3965
|
if (message.from) {
|
|
4208
3966
|
rows.push(
|
|
4209
|
-
|
|
4210
|
-
|
|
4211
|
-
|
|
4212
|
-
);
|
|
3967
|
+
// through the plural formatter, because `From:` may hold RFC 5322 group syntax
|
|
3968
|
+
// and a group has no address of its own
|
|
3969
|
+
`<div class="postal-email-header-key">From</div><div class="postal-email-header-value">${formatHtmlAddresses([message.from])}</div>`);
|
|
4213
3970
|
}
|
|
4214
|
-
|
|
4215
3971
|
if (message.subject) {
|
|
4216
|
-
rows.push(
|
|
4217
|
-
`<div class="postal-email-header-key">Subject</div><div class="postal-email-header-value postal-email-header-subject">${escapeHtml(
|
|
4218
|
-
message.subject
|
|
4219
|
-
)}</div>`
|
|
4220
|
-
);
|
|
3972
|
+
rows.push(`<div class="postal-email-header-key">Subject</div><div class="postal-email-header-value postal-email-header-subject">${escapeHtml(message.subject)}</div>`);
|
|
4221
3973
|
}
|
|
4222
|
-
|
|
4223
3974
|
if (message.date) {
|
|
4224
|
-
rows.push(
|
|
4225
|
-
`<div class="postal-email-header-key">Date</div><div class="postal-email-header-value postal-email-header-date" data-date="${escapeHtml(
|
|
4226
|
-
message.date
|
|
4227
|
-
)}">${escapeHtml(formatDate(message.date))}</div>`
|
|
4228
|
-
);
|
|
3975
|
+
rows.push(`<div class="postal-email-header-key">Date</div><div class="postal-email-header-value postal-email-header-date" data-date="${escapeHtml(message.date)}">${escapeHtml(formatDate(message.date))}</div>`);
|
|
4229
3976
|
}
|
|
4230
|
-
|
|
4231
3977
|
if (message.to && message.to.length) {
|
|
4232
|
-
rows.push(
|
|
4233
|
-
`<div class="postal-email-header-key">To</div><div class="postal-email-header-value">${formatHtmlAddresses(message.to)}</div>`
|
|
4234
|
-
);
|
|
3978
|
+
rows.push(`<div class="postal-email-header-key">To</div><div class="postal-email-header-value">${formatHtmlAddresses(message.to)}</div>`);
|
|
4235
3979
|
}
|
|
4236
|
-
|
|
4237
3980
|
if (message.cc && message.cc.length) {
|
|
4238
|
-
rows.push(
|
|
4239
|
-
`<div class="postal-email-header-key">Cc</div><div class="postal-email-header-value">${formatHtmlAddresses(message.cc)}</div>`
|
|
4240
|
-
);
|
|
3981
|
+
rows.push(`<div class="postal-email-header-key">Cc</div><div class="postal-email-header-value">${formatHtmlAddresses(message.cc)}</div>`);
|
|
4241
3982
|
}
|
|
4242
|
-
|
|
4243
3983
|
if (message.bcc && message.bcc.length) {
|
|
4244
|
-
rows.push(
|
|
4245
|
-
`<div class="postal-email-header-key">Bcc</div><div class="postal-email-header-value">${formatHtmlAddresses(message.bcc)}</div>`
|
|
4246
|
-
);
|
|
3984
|
+
rows.push(`<div class="postal-email-header-key">Bcc</div><div class="postal-email-header-value">${formatHtmlAddresses(message.bcc)}</div>`);
|
|
4247
3985
|
}
|
|
4248
|
-
|
|
4249
|
-
let template = `<div class="postal-email-header">${rows.length ? '<div class="postal-email-header-row">' : ''}${rows.join(
|
|
4250
|
-
'</div>\n<div class="postal-email-header-row">'
|
|
4251
|
-
)}${rows.length ? '</div>' : ''}</div>`;
|
|
4252
|
-
|
|
3986
|
+
let template = `<div class="postal-email-header">${rows.length ? '<div class="postal-email-header-row">' : ''}${rows.join('</div>\n<div class="postal-email-header-row">')}${rows.length ? '</div>' : ''}</div>`;
|
|
4253
3987
|
return template;
|
|
4254
3988
|
}
|
|
4255
3989
|
|
|
4256
|
-
const WORD_CHAR_REGEX = /\w/;
|
|
4257
|
-
const NON_SPACE_TOKEN_REGEX = /[^\s]+/g;
|
|
4258
|
-
|
|
4259
3990
|
/**
|
|
4260
|
-
*
|
|
3991
|
+
* Any whitespace, the test for an angle-addr that needs recovering
|
|
3992
|
+
*/
|
|
3993
|
+
const HAS_WHITESPACE = /\s/;
|
|
3994
|
+
/**
|
|
3995
|
+
* An addr-spec that carries its whitespace legally, inside a quoted local part. The
|
|
3996
|
+
* optional tail is the malformed shape: a real mailbox with wreckage trailing it.
|
|
3997
|
+
*/
|
|
3998
|
+
const QUOTED_LOCAL_ADDR = /^("(?:[^"\\]|\\[\s\S])*"@\S+)(?:\s+([\s\S]+))?$/;
|
|
3999
|
+
/**
|
|
4000
|
+
* One run holding a single '@' and no whitespace, the shape an addr-spec has to have
|
|
4001
|
+
*/
|
|
4002
|
+
const ADDR_SPEC = /^[^@\s]+@[^@\s]+$/;
|
|
4003
|
+
/**
|
|
4004
|
+
* The looser reading applied once the strict one finds nothing, which tolerates the
|
|
4005
|
+
* further '@' that a domain should not have but malformed headers carry anyway
|
|
4006
|
+
*/
|
|
4007
|
+
const LOOSE_ADDR_SPEC = /^[^@\s]+@\S+$/;
|
|
4008
|
+
/**
|
|
4009
|
+
* An addr-spec sitting inside free text, together with the whitespace around it. Sticky
|
|
4010
|
+
* on purpose: it is only run at the offset looseAddressStart picks, see there.
|
|
4011
|
+
*/
|
|
4012
|
+
const LOOSE_TEXT_ADDR = /\s*\b[^@\s]+@[^\s]+\b\s*/y;
|
|
4013
|
+
/**
|
|
4014
|
+
* Restores the quoting of a local part that was read out of a quoted string.
|
|
4261
4015
|
*
|
|
4262
|
-
*
|
|
4263
|
-
*
|
|
4264
|
-
*
|
|
4265
|
-
*
|
|
4016
|
+
* RFC 5321 allows '@' inside a quoted local part, so handing '"user@evil.com"@good.com'
|
|
4017
|
+
* on as the bare 'user@evil.com@good.com' leaves it to the consumer which '@' splits the
|
|
4018
|
+
* domain off. Getting that wrong is a misrouting vector, so the quotes go back on. The
|
|
4019
|
+
* same holds for the other specials: a ',' or a ';' that loses its quotes reads as a
|
|
4020
|
+
* recipient separator once the consumer puts the address back into a header.
|
|
4266
4021
|
*
|
|
4267
|
-
*
|
|
4268
|
-
*
|
|
4269
|
-
|
|
4022
|
+
* @param address Address with an unquoted local part
|
|
4023
|
+
* @return Address with the local part as a quoted-string
|
|
4024
|
+
*/
|
|
4025
|
+
function quoteLocalPart(address) {
|
|
4026
|
+
const lastAt = address.lastIndexOf('@');
|
|
4027
|
+
if (lastAt < 0) {
|
|
4028
|
+
// no domain to split off, nothing can be misrouted
|
|
4029
|
+
return address;
|
|
4030
|
+
}
|
|
4031
|
+
const user = address.substring(0, lastAt);
|
|
4032
|
+
if (/^[^\s"(),:;<>@[\\\]]+$/.test(user) || /^"(?:[^"\\]|\\[\s\S])*"$/.test(user)) {
|
|
4033
|
+
// a local part that carries no special reads the same with or without the quotes,
|
|
4034
|
+
// and one that is already a complete quoted-string needs nothing either
|
|
4035
|
+
return address;
|
|
4036
|
+
}
|
|
4037
|
+
return '"' + user.replace(/["\\]/g, '\\$&') + '"@' + address.substring(lastAt + 1);
|
|
4038
|
+
}
|
|
4039
|
+
/**
|
|
4040
|
+
* The characters JS `\s` matches, which the scan below has to agree with to land on the
|
|
4041
|
+
* same match LOOSE_TEXT_ADDR would.
|
|
4042
|
+
*/
|
|
4043
|
+
function isSpaceCode(code) {
|
|
4044
|
+
return (code === 0x20 ||
|
|
4045
|
+
(code >= 0x09 && code <= 0x0d) ||
|
|
4046
|
+
code === 0xa0 ||
|
|
4047
|
+
code === 0x1680 ||
|
|
4048
|
+
(code >= 0x2000 && code <= 0x200a) ||
|
|
4049
|
+
code === 0x2028 ||
|
|
4050
|
+
code === 0x2029 ||
|
|
4051
|
+
code === 0x202f ||
|
|
4052
|
+
code === 0x205f ||
|
|
4053
|
+
code === 0x3000 ||
|
|
4054
|
+
code === 0xfeff);
|
|
4055
|
+
}
|
|
4056
|
+
/**
|
|
4057
|
+
* The characters JS `\w` matches without the unicode flag, the set the `\b` in
|
|
4058
|
+
* LOOSE_TEXT_ADDR is read against. charCodeAt off either end of the string gives NaN,
|
|
4059
|
+
* which compares false throughout, so out of range reads as a non-word character.
|
|
4060
|
+
*/
|
|
4061
|
+
function isWordCode(code) {
|
|
4062
|
+
return ((code >= 0x30 && code <= 0x39) ||
|
|
4063
|
+
(code >= 0x41 && code <= 0x5a) ||
|
|
4064
|
+
(code >= 0x61 && code <= 0x7a) ||
|
|
4065
|
+
code === 0x5f);
|
|
4066
|
+
}
|
|
4067
|
+
/**
|
|
4068
|
+
* Whether `\b` holds at an offset
|
|
4069
|
+
*/
|
|
4070
|
+
function isBoundary(text, at) {
|
|
4071
|
+
return isWordCode(text.charCodeAt(at - 1)) !== isWordCode(text.charCodeAt(at));
|
|
4072
|
+
}
|
|
4073
|
+
/**
|
|
4074
|
+
* Offset of the first '@' in `text` between `from` and `to`, or -1 when the range holds none.
|
|
4075
|
+
* Bounded rather than indexOf, which would scan on to the end of the header once for every
|
|
4076
|
+
* whitespace delimited run and make the walk below quadratic.
|
|
4077
|
+
*/
|
|
4078
|
+
function indexOfAt(text, from, to) {
|
|
4079
|
+
for (let i = from; i < to; i++) {
|
|
4080
|
+
if (text.charCodeAt(i) === 0x40) {
|
|
4081
|
+
return i;
|
|
4082
|
+
}
|
|
4083
|
+
}
|
|
4084
|
+
return -1;
|
|
4085
|
+
}
|
|
4086
|
+
/**
|
|
4087
|
+
* Finds the offset LOOSE_TEXT_ADDR matches at, or -1 when it does not match at all.
|
|
4088
|
+
*
|
|
4089
|
+
* Letting the pattern search for itself is quadratic: '[^@\s]+' is retried from every
|
|
4090
|
+
* offset and rescans the run to the next '@' each time, so a single header well inside
|
|
4091
|
+
* the default size limit could hold a core busy for minutes. The search is also
|
|
4092
|
+
* unnecessary. '[^@\s]+' crosses neither whitespace nor a '@', so a match can only begin
|
|
4093
|
+
* at the head of a whitespace delimited run or just past a '@' inside one, and '[^\s]+\b'
|
|
4094
|
+
* gives characters back until it lands on a boundary, so the only end it can take in that
|
|
4095
|
+
* run is the last boundary in it. Both are found in one pass, and the pattern is then run
|
|
4096
|
+
* at that single offset.
|
|
4270
4097
|
*
|
|
4271
|
-
* @param
|
|
4272
|
-
* @return
|
|
4098
|
+
* @param text Free text to look in
|
|
4099
|
+
* @return Offset to match at, or -1
|
|
4273
4100
|
*/
|
|
4274
|
-
function
|
|
4275
|
-
|
|
4276
|
-
|
|
4277
|
-
|
|
4278
|
-
|
|
4279
|
-
|
|
4280
|
-
|
|
4281
|
-
|
|
4282
|
-
|
|
4283
|
-
|
|
4284
|
-
|
|
4285
|
-
|
|
4101
|
+
function looseAddressStart(text) {
|
|
4102
|
+
const len = text.length;
|
|
4103
|
+
let pos = 0;
|
|
4104
|
+
while (pos < len) {
|
|
4105
|
+
while (pos < len && isSpaceCode(text.charCodeAt(pos))) {
|
|
4106
|
+
pos++;
|
|
4107
|
+
}
|
|
4108
|
+
if (pos >= len) {
|
|
4109
|
+
break;
|
|
4110
|
+
}
|
|
4111
|
+
const runStart = pos;
|
|
4112
|
+
let runEnd = pos;
|
|
4113
|
+
while (runEnd < len && !isSpaceCode(text.charCodeAt(runEnd))) {
|
|
4114
|
+
runEnd++;
|
|
4115
|
+
}
|
|
4116
|
+
let at = indexOfAt(text, runStart, runEnd);
|
|
4117
|
+
if (at >= 0) {
|
|
4118
|
+
let lastBoundary = -1;
|
|
4119
|
+
for (let k = runEnd; k > runStart; k--) {
|
|
4120
|
+
if (isBoundary(text, k)) {
|
|
4121
|
+
lastBoundary = k;
|
|
4122
|
+
break;
|
|
4123
|
+
}
|
|
4124
|
+
}
|
|
4125
|
+
let atomStart = runStart;
|
|
4126
|
+
while (lastBoundary >= 0 && at >= 0) {
|
|
4127
|
+
// '[^@\s]+' has to cover a character before the '@' and '[^\s]+' one after it,
|
|
4128
|
+
// and the boundary that ends the match has to sit past both
|
|
4129
|
+
if (at > atomStart && runEnd > at + 1 && lastBoundary > at + 1) {
|
|
4130
|
+
for (let start = atomStart; start < at; start++) {
|
|
4131
|
+
if (isBoundary(text, start)) {
|
|
4132
|
+
if (start > runStart) {
|
|
4133
|
+
return start;
|
|
4134
|
+
}
|
|
4135
|
+
// the leading '\s*' is greedy, so a match that begins at the run
|
|
4136
|
+
// takes the whitespace in front of it along
|
|
4137
|
+
let padded = runStart;
|
|
4138
|
+
while (padded > 0 && isSpaceCode(text.charCodeAt(padded - 1))) {
|
|
4139
|
+
padded--;
|
|
4140
|
+
}
|
|
4141
|
+
return padded;
|
|
4142
|
+
}
|
|
4143
|
+
}
|
|
4144
|
+
}
|
|
4145
|
+
atomStart = at + 1;
|
|
4146
|
+
at = indexOfAt(text, atomStart, runEnd);
|
|
4147
|
+
}
|
|
4148
|
+
}
|
|
4149
|
+
pos = runEnd;
|
|
4150
|
+
}
|
|
4151
|
+
return -1;
|
|
4152
|
+
}
|
|
4153
|
+
/**
|
|
4154
|
+
* Recovers the addr-spec from an angle-addr that came back holding unquoted whitespace.
|
|
4155
|
+
*
|
|
4156
|
+
* A malformed header can put more than a mailbox between the angle brackets, most often
|
|
4157
|
+
* because the generator wrote the recipient twice: '<user@example.com user@example.com>'
|
|
4158
|
+
* or '<example.com user@example.com>'. Whitespace is not addr-spec, so the whole run can
|
|
4159
|
+
* never be a mailbox, and passing it on as the address loses the recipient that is
|
|
4160
|
+
* sitting right there in the header.
|
|
4161
|
+
*
|
|
4162
|
+
* The run that still reads as an addr-spec is kept and whatever is left over becomes
|
|
4163
|
+
* display text. Candidates are read strictly first and then under the looser grammar,
|
|
4164
|
+
* the same two tiers the unquoted text fallback applies, so that '<a@b@c.com junk>' and a
|
|
4165
|
+
* bare 'a@b@c.com junk' agree on the recipient.
|
|
4166
|
+
*
|
|
4167
|
+
* A quoted local part is left alone: RFC 5321 allows whitespace inside it, so
|
|
4168
|
+
* '<"user name"@example.com>' is well formed and means exactly what it says.
|
|
4169
|
+
*
|
|
4170
|
+
* @param address Collected address
|
|
4171
|
+
* @param text Collected display text
|
|
4172
|
+
* @return The address and the display text, with any leftovers moved to the text
|
|
4173
|
+
*/
|
|
4174
|
+
function recoverAddrSpec(address, text) {
|
|
4175
|
+
if (!HAS_WHITESPACE.test(address)) {
|
|
4176
|
+
return { address, text };
|
|
4177
|
+
}
|
|
4178
|
+
let rest;
|
|
4179
|
+
const quoted = address.match(QUOTED_LOCAL_ADDR);
|
|
4180
|
+
if (quoted) {
|
|
4181
|
+
if (!quoted[2]) {
|
|
4182
|
+
// the whitespace sits inside the quoted local part, this is a well formed mailbox
|
|
4183
|
+
return { address, text };
|
|
4286
4184
|
}
|
|
4287
|
-
|
|
4185
|
+
// a real mailbox with wreckage trailing it, so peel the addr-spec off whole rather
|
|
4186
|
+
// than splitting into the quotes
|
|
4187
|
+
address = quoted[1];
|
|
4188
|
+
rest = [quoted[2]];
|
|
4189
|
+
}
|
|
4190
|
+
else {
|
|
4191
|
+
if (address.indexOf('"') >= 0) {
|
|
4192
|
+
// Splitting on whitespace loses track of where a quoted string starts and ends,
|
|
4193
|
+
// and addresses are never taken out of quoted strings: the run picked out of
|
|
4194
|
+
// '<junk "user@evil.com b"@good.com>' would be an address from the domain the
|
|
4195
|
+
// quotes were hiding. Every well formed shape was handled above, so the original
|
|
4196
|
+
// is the honest answer
|
|
4197
|
+
return { address, text };
|
|
4198
|
+
}
|
|
4199
|
+
const parts = address.split(/\s+/);
|
|
4200
|
+
let addrIndex = parts.findIndex(part => ADDR_SPEC.test(part));
|
|
4201
|
+
if (addrIndex < 0) {
|
|
4202
|
+
addrIndex = parts.findIndex(part => LOOSE_ADDR_SPEC.test(part));
|
|
4203
|
+
}
|
|
4204
|
+
if (addrIndex < 0) {
|
|
4205
|
+
// nothing in there reads as an address, there is no better answer than the original
|
|
4206
|
+
return { address, text };
|
|
4207
|
+
}
|
|
4208
|
+
address = parts.splice(addrIndex, 1)[0];
|
|
4209
|
+
rest = parts;
|
|
4210
|
+
}
|
|
4211
|
+
return {
|
|
4212
|
+
address,
|
|
4213
|
+
text: [text, ...rest].filter(part => part).join(' ')
|
|
4214
|
+
};
|
|
4215
|
+
}
|
|
4216
|
+
/**
|
|
4217
|
+
* Takes the RFC 5322 comments out of the contents of an angle-addr.
|
|
4218
|
+
*
|
|
4219
|
+
* The tokenizer tracks a single open operator, so once '<' is open a '(' is plain text and
|
|
4220
|
+
* a comment inside the brackets reached the address verbatim: 'Name <user@example.com(x)evil.com>'
|
|
4221
|
+
* put 'user@example.com(x)evil.com' into the address, a value that is no mailbox and that a
|
|
4222
|
+
* consumer stripping the comment reads as 'user@example.comevil.com'. A comment is folding
|
|
4223
|
+
* whitespace, so it is read as a space that splits two atoms and as nothing next to an '@',
|
|
4224
|
+
* the same rule the token walk applies outside the brackets. Quoted strings and domain
|
|
4225
|
+
* literals are copied through untouched, a '(' in there is not a comment.
|
|
4226
|
+
*
|
|
4227
|
+
* @param address Contents of the angle brackets
|
|
4228
|
+
* @return The address with the comments removed, and the text of the comments
|
|
4229
|
+
*/
|
|
4230
|
+
function stripAddressComments(address) {
|
|
4231
|
+
const comments = [];
|
|
4232
|
+
let result = '';
|
|
4233
|
+
let comment = '';
|
|
4234
|
+
let depth = 0;
|
|
4235
|
+
let closer = '';
|
|
4236
|
+
// carried along rather than read back off the growing result, which would flatten it on
|
|
4237
|
+
// every comment (see lastChars in _handleAddress)
|
|
4238
|
+
let lastChar = '';
|
|
4239
|
+
for (let i = 0, len = address.length; i < len; i++) {
|
|
4240
|
+
const chr = address.charAt(i);
|
|
4241
|
+
if (depth) {
|
|
4242
|
+
if (chr === '\\' && i < len - 1) {
|
|
4243
|
+
comment += address.charAt(++i);
|
|
4244
|
+
}
|
|
4245
|
+
else if (chr === '(') {
|
|
4246
|
+
depth++;
|
|
4247
|
+
comment += chr;
|
|
4248
|
+
}
|
|
4249
|
+
else if (chr === ')' && !--depth) {
|
|
4250
|
+
comments.push(comment.trim());
|
|
4251
|
+
comment = '';
|
|
4252
|
+
if (lastChar !== '@' && address.charAt(i + 1) !== '@') {
|
|
4253
|
+
result += ' ';
|
|
4254
|
+
lastChar = ' ';
|
|
4255
|
+
}
|
|
4256
|
+
}
|
|
4257
|
+
else {
|
|
4258
|
+
comment += chr;
|
|
4259
|
+
}
|
|
4288
4260
|
continue;
|
|
4289
4261
|
}
|
|
4290
|
-
|
|
4291
|
-
|
|
4292
|
-
|
|
4293
|
-
|
|
4294
|
-
|
|
4262
|
+
if (closer) {
|
|
4263
|
+
if (chr === '\\' && closer === '"' && i < len - 1) {
|
|
4264
|
+
result += chr + address.charAt(++i);
|
|
4265
|
+
lastChar = address.charAt(i);
|
|
4266
|
+
continue;
|
|
4267
|
+
}
|
|
4268
|
+
if (chr === closer) {
|
|
4269
|
+
closer = '';
|
|
4270
|
+
}
|
|
4271
|
+
}
|
|
4272
|
+
else if (chr === '"') {
|
|
4273
|
+
closer = '"';
|
|
4274
|
+
}
|
|
4275
|
+
else if (chr === '[') {
|
|
4276
|
+
closer = ']';
|
|
4295
4277
|
}
|
|
4296
|
-
if (
|
|
4278
|
+
else if (chr === '(') {
|
|
4279
|
+
depth = 1;
|
|
4297
4280
|
continue;
|
|
4298
4281
|
}
|
|
4299
|
-
|
|
4300
|
-
|
|
4282
|
+
result += chr;
|
|
4283
|
+
lastChar = chr;
|
|
4301
4284
|
}
|
|
4302
|
-
|
|
4303
|
-
|
|
4285
|
+
if (depth) {
|
|
4286
|
+
// an unterminated comment runs to the end of the address
|
|
4287
|
+
comments.push(comment.trim());
|
|
4288
|
+
}
|
|
4289
|
+
return { address: result.trim(), comments: comments.filter(text => text) };
|
|
4304
4290
|
}
|
|
4305
|
-
|
|
4306
4291
|
/**
|
|
4307
4292
|
* Checks whether text holds an angle bracket address, ie. `<` and `>` around a run
|
|
4308
4293
|
* without other angle brackets that has an '@' with at least one character on each side.
|
|
@@ -4310,45 +4295,43 @@ function findAddressInText(text) {
|
|
|
4310
4295
|
* Same answer as `/<[^<>]+@[^<>]+>/.test(text)`, which backtracks quadratically on a `<`
|
|
4311
4296
|
* followed by a long run of '@' and no closing `>`.
|
|
4312
4297
|
*
|
|
4313
|
-
* @param
|
|
4314
|
-
* @return
|
|
4298
|
+
* @param text Text to check
|
|
4299
|
+
* @return true if an angle bracket address is present
|
|
4315
4300
|
*/
|
|
4316
4301
|
function hasAngleAddress(text) {
|
|
4317
4302
|
// position right after the `<` that opened the current bracket, or -1
|
|
4318
4303
|
let start = -1;
|
|
4319
4304
|
// first '@' inside the bracket that has a character before it, or -1
|
|
4320
4305
|
let at = -1;
|
|
4321
|
-
|
|
4322
4306
|
for (let i = 0; i < text.length; i++) {
|
|
4323
4307
|
const chr = text.charAt(i);
|
|
4324
4308
|
if (chr === '<') {
|
|
4325
4309
|
start = i + 1;
|
|
4326
4310
|
at = -1;
|
|
4327
|
-
}
|
|
4311
|
+
}
|
|
4312
|
+
else if (chr === '>') {
|
|
4328
4313
|
if (at >= 0 && at < i - 1) {
|
|
4329
4314
|
return true;
|
|
4330
4315
|
}
|
|
4331
4316
|
start = -1;
|
|
4332
4317
|
at = -1;
|
|
4333
|
-
}
|
|
4318
|
+
}
|
|
4319
|
+
else if (chr === '@' && start >= 0 && at < 0 && i > start) {
|
|
4334
4320
|
at = i;
|
|
4335
4321
|
}
|
|
4336
4322
|
}
|
|
4337
|
-
|
|
4338
4323
|
return false;
|
|
4339
4324
|
}
|
|
4340
|
-
|
|
4341
4325
|
/**
|
|
4342
4326
|
* Converts tokens for a single address into an address object
|
|
4343
4327
|
*
|
|
4344
|
-
* @param
|
|
4345
|
-
* @param
|
|
4346
|
-
* @return
|
|
4328
|
+
* @param tokens Tokens object
|
|
4329
|
+
* @param depth Current recursion depth for nested group protection
|
|
4330
|
+
* @return Address objects
|
|
4347
4331
|
*/
|
|
4348
4332
|
function _handleAddress(tokens, depth) {
|
|
4349
4333
|
let isGroup = false;
|
|
4350
4334
|
let state = 'text';
|
|
4351
|
-
let address;
|
|
4352
4335
|
let addresses = [];
|
|
4353
4336
|
let data = {
|
|
4354
4337
|
address: [],
|
|
@@ -4360,7 +4343,11 @@ function _handleAddress(tokens, depth) {
|
|
|
4360
4343
|
let i;
|
|
4361
4344
|
let len;
|
|
4362
4345
|
let insideQuotes = false; // Track if we're currently inside a quoted string
|
|
4363
|
-
|
|
4346
|
+
// Last character of the run each state is currently accumulating. Reading it back off
|
|
4347
|
+
// the accumulator with slice(-1) makes the engine flatten the whole growing string on
|
|
4348
|
+
// every token, which is quadratic over an address built from many comment-joined atoms.
|
|
4349
|
+
// A run only ever grows by the token appended below, so the character is carried along.
|
|
4350
|
+
const lastChars = { address: '', comment: '', group: '', text: '' };
|
|
4364
4351
|
// Filter out <addresses>, (comments) and regular text
|
|
4365
4352
|
for (i = 0, len = tokens.length; i < len; i++) {
|
|
4366
4353
|
let token = tokens[i];
|
|
@@ -4390,43 +4377,61 @@ function _handleAddress(tokens, depth) {
|
|
|
4390
4377
|
insideQuotes = false;
|
|
4391
4378
|
break;
|
|
4392
4379
|
}
|
|
4393
|
-
}
|
|
4380
|
+
}
|
|
4381
|
+
else if (token.value) {
|
|
4382
|
+
// An empty quoted string is dropped by the tokenizer, leaving no text token of its
|
|
4383
|
+
// own to record the quoting on, so the pair of quote operators right in front of
|
|
4384
|
+
// this token is all that is left of it and the run it opens carries the quoting
|
|
4385
|
+
// instead. Without this '""@example.com' reads as the bare '@example.com'. It only
|
|
4386
|
+
// ever opens a run: a run already holding unquoted text is not a quoted string
|
|
4387
|
+
const prevPrevToken = i > 1 ? tokens[i - 2] : null;
|
|
4388
|
+
const opensAfterEmptyQuotedString = prevToken?.type === 'operator' &&
|
|
4389
|
+
prevToken.value === '"' &&
|
|
4390
|
+
!!prevToken.noBreak &&
|
|
4391
|
+
prevPrevToken?.type === 'operator' &&
|
|
4392
|
+
prevPrevToken.value === '"';
|
|
4394
4393
|
if (state === 'address') {
|
|
4395
4394
|
// handle use case where unquoted name includes a "<"
|
|
4396
4395
|
// Apple Mail truncates everything between an unexpected < and an address
|
|
4397
4396
|
// and so will we
|
|
4398
4397
|
token.value = token.value.replace(/^[^<]*<\s*/, '');
|
|
4399
4398
|
}
|
|
4400
|
-
|
|
4401
|
-
|
|
4399
|
+
// A comment is folding whitespace. It may sit inside an addr-spec, on either side
|
|
4400
|
+
// of the '@', but it cannot join two atoms into one: gluing across it would read
|
|
4401
|
+
// 'user@example.com(x)evil.com' as the single domain 'example.comevil.com' and
|
|
4402
|
+
// name a domain the sender never wrote.
|
|
4403
|
+
const joins = prevToken &&
|
|
4404
|
+
prevToken.noBreak &&
|
|
4405
|
+
data[state].length &&
|
|
4406
|
+
(prevToken.value !== ')' || lastChars[state] === '@' || token.value.charAt(0) === '@');
|
|
4407
|
+
if (joins) {
|
|
4402
4408
|
// join values
|
|
4403
4409
|
data[state][data[state].length - 1] += token.value;
|
|
4404
4410
|
if (state === 'text' && insideQuotes) {
|
|
4405
4411
|
data.textWasQuoted[data.textWasQuoted.length - 1] = true;
|
|
4406
4412
|
}
|
|
4407
|
-
}
|
|
4413
|
+
}
|
|
4414
|
+
else {
|
|
4408
4415
|
data[state].push(token.value);
|
|
4409
4416
|
if (state === 'text') {
|
|
4410
|
-
data.textWasQuoted.push(insideQuotes);
|
|
4417
|
+
data.textWasQuoted.push(insideQuotes || opensAfterEmptyQuotedString);
|
|
4411
4418
|
}
|
|
4412
4419
|
}
|
|
4420
|
+
lastChars[state] = token.value.charAt(token.value.length - 1);
|
|
4413
4421
|
}
|
|
4414
4422
|
}
|
|
4415
|
-
|
|
4416
4423
|
// If there is no text but a comment, replace the two
|
|
4417
4424
|
if (!data.text.length && data.comment.length) {
|
|
4418
4425
|
data.text = data.comment;
|
|
4419
4426
|
data.comment = [];
|
|
4420
4427
|
}
|
|
4421
|
-
|
|
4422
4428
|
if (isGroup) {
|
|
4423
4429
|
// http://tools.ietf.org/html/rfc2822#appendix-A.1.3
|
|
4424
|
-
|
|
4425
|
-
|
|
4430
|
+
const text = data.text.join(' ');
|
|
4426
4431
|
// Parse group members, but flatten any nested groups (RFC 5322 doesn't allow nesting)
|
|
4427
4432
|
let groupMembers = [];
|
|
4428
4433
|
if (data.group.length) {
|
|
4429
|
-
let parsedGroup =
|
|
4434
|
+
let parsedGroup = parseAddressList(data.group.join(','), depth + 1);
|
|
4430
4435
|
// Flatten: if any member is itself a group, extract its members into the sequence
|
|
4431
4436
|
parsedGroup.forEach(member => {
|
|
4432
4437
|
if (member.group) {
|
|
@@ -4434,126 +4439,161 @@ function _handleAddress(tokens, depth) {
|
|
|
4434
4439
|
for (const nestedMember of member.group) {
|
|
4435
4440
|
groupMembers.push(nestedMember);
|
|
4436
4441
|
}
|
|
4437
|
-
}
|
|
4442
|
+
}
|
|
4443
|
+
else {
|
|
4438
4444
|
groupMembers.push(member);
|
|
4439
4445
|
}
|
|
4440
4446
|
});
|
|
4441
4447
|
}
|
|
4442
|
-
|
|
4443
4448
|
addresses.push({
|
|
4444
|
-
name: decodeWords(
|
|
4449
|
+
name: decodeWords(text),
|
|
4445
4450
|
group: groupMembers
|
|
4446
4451
|
});
|
|
4447
|
-
}
|
|
4452
|
+
}
|
|
4453
|
+
else {
|
|
4454
|
+
// Comments come out of the angle-addr before anything asks whether one was found, so
|
|
4455
|
+
// that brackets holding nothing but a comment read the same as empty ones
|
|
4456
|
+
const addressComments = [];
|
|
4457
|
+
const addressParts = [];
|
|
4458
|
+
for (const part of data.address) {
|
|
4459
|
+
if (part.indexOf('(') < 0) {
|
|
4460
|
+
addressParts.push(part);
|
|
4461
|
+
continue;
|
|
4462
|
+
}
|
|
4463
|
+
const stripped = stripAddressComments(part);
|
|
4464
|
+
for (const comment of stripped.comments) {
|
|
4465
|
+
addressComments.push(comment);
|
|
4466
|
+
}
|
|
4467
|
+
if (stripped.address) {
|
|
4468
|
+
addressParts.push(stripped.address);
|
|
4469
|
+
}
|
|
4470
|
+
}
|
|
4471
|
+
data.address = addressParts;
|
|
4448
4472
|
// If no address was found, try to detect one from regular text
|
|
4449
4473
|
if (!data.address.length && data.text.length) {
|
|
4450
4474
|
for (i = data.text.length - 1; i >= 0; i--) {
|
|
4451
4475
|
// Security fix: Do not extract email addresses from quoted strings
|
|
4452
4476
|
// RFC 5321 allows @ inside quoted local-parts like "user@domain"@example.com
|
|
4453
4477
|
// Extracting emails from quoted text leads to misrouting vulnerabilities
|
|
4454
|
-
if (!data.textWasQuoted[i] && data.text[i]
|
|
4478
|
+
if (!data.textWasQuoted[i] && ADDR_SPEC.test(data.text[i])) {
|
|
4455
4479
|
data.address = data.text.splice(i, 1);
|
|
4456
4480
|
data.textWasQuoted.splice(i, 1);
|
|
4457
4481
|
break;
|
|
4458
4482
|
}
|
|
4459
4483
|
}
|
|
4460
|
-
|
|
4461
4484
|
// still no address
|
|
4462
4485
|
if (!data.address.length) {
|
|
4463
4486
|
for (i = data.text.length - 1; i >= 0; i--) {
|
|
4464
4487
|
// Security fix: Do not extract email addresses from quoted strings
|
|
4465
4488
|
if (!data.textWasQuoted[i]) {
|
|
4466
4489
|
// handles an email address that has more than one @
|
|
4467
|
-
const
|
|
4468
|
-
|
|
4469
|
-
|
|
4470
|
-
|
|
4471
|
-
|
|
4472
|
-
|
|
4473
|
-
|
|
4474
|
-
|
|
4475
|
-
|
|
4476
|
-
data.text[i]
|
|
4477
|
-
|
|
4478
|
-
|
|
4490
|
+
const part = data.text[i];
|
|
4491
|
+
const start = looseAddressStart(part);
|
|
4492
|
+
if (start >= 0) {
|
|
4493
|
+
LOOSE_TEXT_ADDR.lastIndex = start;
|
|
4494
|
+
const match = LOOSE_TEXT_ADDR.exec(part);
|
|
4495
|
+
if (match) {
|
|
4496
|
+
data.address = [match[0].trim()];
|
|
4497
|
+
// the match takes the whitespace around the address along, so
|
|
4498
|
+
// the text on either side is joined with a single space
|
|
4499
|
+
data.text[i] = (part.slice(0, start) +
|
|
4500
|
+
' ' +
|
|
4501
|
+
part.slice(start + match[0].length)).trim();
|
|
4502
|
+
break;
|
|
4503
|
+
}
|
|
4479
4504
|
}
|
|
4480
|
-
data.text[i] =
|
|
4505
|
+
data.text[i] = part.trim();
|
|
4481
4506
|
}
|
|
4482
4507
|
}
|
|
4483
4508
|
}
|
|
4484
4509
|
}
|
|
4485
|
-
|
|
4486
4510
|
// If there's still no text but a comment exists, replace the two
|
|
4487
4511
|
if (!data.text.length && data.comment.length) {
|
|
4488
4512
|
data.text = data.comment;
|
|
4489
4513
|
data.comment = [];
|
|
4490
4514
|
}
|
|
4491
|
-
|
|
4492
4515
|
// Keep only the first address occurrence, push others to regular text
|
|
4493
4516
|
if (data.address.length > 1) {
|
|
4494
4517
|
data.text = data.text.concat(data.address.splice(1));
|
|
4495
4518
|
}
|
|
4496
|
-
|
|
4519
|
+
// An address is only taken from unquoted text, so anything left in the text at this
|
|
4520
|
+
// point that still has to serve as the address carries its quoting in this flag
|
|
4521
|
+
const addressFromQuotedText = !data.address.length && data.textWasQuoted.some(wasQuoted => wasQuoted);
|
|
4497
4522
|
// Join values with spaces
|
|
4498
|
-
|
|
4499
|
-
|
|
4500
|
-
|
|
4523
|
+
let text = data.text.join(' ');
|
|
4524
|
+
let addressValue = data.address.join(' ');
|
|
4501
4525
|
// `^=\?[^=]+?=$` could not match a base64 word whose padding puts an '=' inside it,
|
|
4502
4526
|
// so whether a bare encoded word was decoded or left to become the address itself
|
|
4503
4527
|
// came down to whether its payload happened to need padding.
|
|
4504
|
-
if (!
|
|
4528
|
+
if (!addressValue && isEncodedWordsOnly(text.trim())) {
|
|
4505
4529
|
// try to extract words from text content
|
|
4506
|
-
const decodedText = decodeWords(
|
|
4530
|
+
const decodedText = decodeWords(text);
|
|
4507
4531
|
// Security: only re-parse if decoded text contains angle-bracket addresses.
|
|
4508
4532
|
// Without this, a bare encoded email (e.g. =?utf-8?B?dGVzdEBldmlsLmNv?=)
|
|
4509
|
-
// would be fabricated into an address from attacker-controlled input.
|
|
4510
|
-
|
|
4511
|
-
|
|
4512
|
-
|
|
4533
|
+
// would be fabricated into an address from attacker-controlled input. A quoted
|
|
4534
|
+
// encoded word is never re-parsed, addresses are not taken out of quoted strings
|
|
4535
|
+
if (!addressFromQuotedText && hasAngleAddress(decodedText)) {
|
|
4536
|
+
const parsedSubAddresses = parseAddressList(decodedText, depth + 1);
|
|
4537
|
+
if (parsedSubAddresses.length) {
|
|
4513
4538
|
return parsedSubAddresses;
|
|
4514
4539
|
}
|
|
4515
4540
|
}
|
|
4516
4541
|
// No usable address found - treat decoded text as display name only
|
|
4517
4542
|
return [{ address: '', name: decodedText }];
|
|
4518
4543
|
}
|
|
4519
|
-
|
|
4520
|
-
|
|
4521
|
-
|
|
4522
|
-
|
|
4544
|
+
if (addressFromQuotedText && text.indexOf('@') >= 0) {
|
|
4545
|
+
// The mailbox is still sitting in the text, so it moves over here and is quoted
|
|
4546
|
+
// before the recovery below rather than after it. Anything else the text holds
|
|
4547
|
+
// came along with it: a comment ends the domain but leaves the atoms behind it in
|
|
4548
|
+
// the same text, so '"user"@example.com(x)evil.com' would otherwise hand on
|
|
4549
|
+
// 'user@example.com evil.com', a second domain riding along in a value that is no
|
|
4550
|
+
// addr-spec at all. Putting the quotes back first is what lets the recovery tell
|
|
4551
|
+
// the whitespace a quoted local part may carry from the wreckage trailing one
|
|
4552
|
+
addressValue = quoteLocalPart(text);
|
|
4553
|
+
text = '';
|
|
4554
|
+
}
|
|
4555
|
+
({ address: addressValue, text } = recoverAddrSpec(addressValue, text));
|
|
4556
|
+
// a comment names the mailbox only when nothing else does, as one outside the brackets
|
|
4557
|
+
if (!text && addressComments.length) {
|
|
4558
|
+
text = addressComments.join(' ');
|
|
4559
|
+
}
|
|
4560
|
+
const rawName = text || addressValue || '';
|
|
4561
|
+
const address = {
|
|
4562
|
+
address: addressValue || text || '',
|
|
4563
|
+
name: decodeWords(rawName)
|
|
4523
4564
|
};
|
|
4524
|
-
|
|
4525
|
-
|
|
4526
|
-
|
|
4565
|
+
// The raw comparison is the check Nodemailer makes, it holds when a single source
|
|
4566
|
+
// filled both fields. The decoded one is added because postal-mime decodes names,
|
|
4567
|
+
// which also catches a name that decodes to the address itself. Comparing only the
|
|
4568
|
+
// decoded name kept a name that mixes an encoded word with plain text as the address
|
|
4569
|
+
if (address.address === rawName || address.address === address.name) {
|
|
4570
|
+
if (address.address.match(/@/)) {
|
|
4527
4571
|
address.name = '';
|
|
4528
|
-
}
|
|
4572
|
+
}
|
|
4573
|
+
else {
|
|
4529
4574
|
address.address = '';
|
|
4530
4575
|
}
|
|
4531
4576
|
}
|
|
4532
|
-
|
|
4533
4577
|
addresses.push(address);
|
|
4534
4578
|
}
|
|
4535
|
-
|
|
4536
4579
|
return addresses;
|
|
4537
4580
|
}
|
|
4538
|
-
|
|
4539
4581
|
/**
|
|
4540
|
-
*
|
|
4541
|
-
*
|
|
4542
|
-
* @constructor
|
|
4543
|
-
* @param {String} str Address field string
|
|
4582
|
+
* Tokenizer for address field strings
|
|
4544
4583
|
*/
|
|
4545
4584
|
class Tokenizer {
|
|
4585
|
+
/**
|
|
4586
|
+
* @param str Address field string
|
|
4587
|
+
*/
|
|
4546
4588
|
constructor(str) {
|
|
4547
4589
|
this.str = (str || '').toString();
|
|
4548
4590
|
this.operatorCurrent = '';
|
|
4549
4591
|
this.operatorExpecting = '';
|
|
4550
4592
|
this.node = null;
|
|
4551
4593
|
this.escaped = false;
|
|
4552
|
-
|
|
4594
|
+
this.inDomainLiteral = false;
|
|
4595
|
+
this.afterAt = false;
|
|
4553
4596
|
this.list = [];
|
|
4554
|
-
/**
|
|
4555
|
-
* Operator tokens and which tokens are expected to end the sequence
|
|
4556
|
-
*/
|
|
4557
4597
|
this.operators = {
|
|
4558
4598
|
'"': '"',
|
|
4559
4599
|
'(': ')',
|
|
@@ -4569,68 +4609,89 @@ class Tokenizer {
|
|
|
4569
4609
|
';': ''
|
|
4570
4610
|
};
|
|
4571
4611
|
}
|
|
4572
|
-
|
|
4573
4612
|
/**
|
|
4574
4613
|
* Tokenizes the original input string
|
|
4575
4614
|
*
|
|
4576
|
-
* @return
|
|
4615
|
+
* @return An array of operator|text tokens
|
|
4577
4616
|
*/
|
|
4578
4617
|
tokenize() {
|
|
4579
4618
|
let list = [];
|
|
4580
|
-
|
|
4581
4619
|
for (let i = 0, len = this.str.length; i < len; i++) {
|
|
4582
4620
|
let chr = this.str.charAt(i);
|
|
4583
4621
|
let nextChr = i < len - 1 ? this.str.charAt(i + 1) : null;
|
|
4584
4622
|
this.checkChar(chr, nextChr);
|
|
4585
4623
|
}
|
|
4586
|
-
|
|
4587
4624
|
this.list.forEach(node => {
|
|
4588
4625
|
node.value = (node.value || '').toString().trim();
|
|
4589
4626
|
if (node.value) {
|
|
4590
4627
|
list.push(node);
|
|
4591
4628
|
}
|
|
4592
4629
|
});
|
|
4593
|
-
|
|
4594
4630
|
return list;
|
|
4595
4631
|
}
|
|
4596
|
-
|
|
4597
4632
|
/**
|
|
4598
4633
|
* Checks if a character is an operator or text and acts accordingly
|
|
4599
4634
|
*
|
|
4600
|
-
* @param
|
|
4635
|
+
* @param chr Character from the address field
|
|
4636
|
+
* @param nextChr The character that follows, or null at the end of the field
|
|
4601
4637
|
*/
|
|
4602
4638
|
checkChar(chr, nextChr) {
|
|
4603
|
-
|
|
4639
|
+
// Track RFC 5322 domain-literals ("[" *dtext "]"). The ":" of an IPv6 address-literal
|
|
4640
|
+
// (user@[IPv6:2001:db8::1]) is dtext and must not be read as the group delimiter while
|
|
4641
|
+
// inside the brackets. That is the only operator the literal hides: letting it hide
|
|
4642
|
+
// the others as well meant a "[" could turn the comment, quoted string or angle-addr
|
|
4643
|
+
// after it into text and pick a different mailbox, '[ ( ] <a@victim.com> ) <b@evil.com>'
|
|
4644
|
+
// yielded a@victim.com where RFC 5322 reads b@evil.com. A "[" also only opens a literal
|
|
4645
|
+
// right after the "@" of an addr-spec, so the group delimiter of a display name
|
|
4646
|
+
// holding one stays intact. Quoted strings and comments are handled via
|
|
4647
|
+
// operatorExpecting, so this state is only entered when no operator is open. The list
|
|
4648
|
+
// separators "," and ";" always end the literal, so that an unclosed "[" can not
|
|
4649
|
+
// swallow later recipients.
|
|
4650
|
+
if (!this.escaped && !this.operatorExpecting) {
|
|
4651
|
+
if (!this.inDomainLiteral && chr === '[' && this.afterAt) {
|
|
4652
|
+
this.inDomainLiteral = true;
|
|
4653
|
+
}
|
|
4654
|
+
else if (this.inDomainLiteral && (chr === ']' || chr === ',' || chr === ';')) {
|
|
4655
|
+
this.inDomainLiteral = false;
|
|
4656
|
+
}
|
|
4657
|
+
}
|
|
4658
|
+
if (this.escaped) ;
|
|
4659
|
+
else if (chr === this.operatorExpecting) {
|
|
4604
4660
|
this.node = {
|
|
4605
4661
|
type: 'operator',
|
|
4606
4662
|
value: chr
|
|
4607
4663
|
};
|
|
4608
|
-
|
|
4609
4664
|
if (nextChr && ![' ', '\t', '\r', '\n', ',', ';'].includes(nextChr)) {
|
|
4610
4665
|
this.node.noBreak = true;
|
|
4611
4666
|
}
|
|
4612
|
-
|
|
4613
4667
|
this.list.push(this.node);
|
|
4614
4668
|
this.node = null;
|
|
4669
|
+
if (chr !== ')') {
|
|
4670
|
+
// a comment is folding whitespace, it does not part the "@" from the "["
|
|
4671
|
+
this.afterAt = false;
|
|
4672
|
+
}
|
|
4615
4673
|
this.operatorExpecting = '';
|
|
4616
4674
|
this.escaped = false;
|
|
4617
|
-
|
|
4618
4675
|
return;
|
|
4619
|
-
}
|
|
4676
|
+
}
|
|
4677
|
+
else if (!this.operatorExpecting && !(this.inDomainLiteral && chr === ':') && chr in this.operators) {
|
|
4620
4678
|
this.node = {
|
|
4621
4679
|
type: 'operator',
|
|
4622
4680
|
value: chr
|
|
4623
4681
|
};
|
|
4624
4682
|
this.list.push(this.node);
|
|
4625
4683
|
this.node = null;
|
|
4684
|
+
if (chr !== '(') {
|
|
4685
|
+
this.afterAt = false;
|
|
4686
|
+
}
|
|
4626
4687
|
this.operatorExpecting = this.operators[chr];
|
|
4627
4688
|
this.escaped = false;
|
|
4628
4689
|
return;
|
|
4629
|
-
}
|
|
4690
|
+
}
|
|
4691
|
+
else if (this.operatorExpecting === '"' && chr === '\\') {
|
|
4630
4692
|
this.escaped = true;
|
|
4631
4693
|
return;
|
|
4632
4694
|
}
|
|
4633
|
-
|
|
4634
4695
|
if (!this.node) {
|
|
4635
4696
|
this.node = {
|
|
4636
4697
|
type: 'text',
|
|
@@ -4638,76 +4699,62 @@ class Tokenizer {
|
|
|
4638
4699
|
};
|
|
4639
4700
|
this.list.push(this.node);
|
|
4640
4701
|
}
|
|
4641
|
-
|
|
4642
4702
|
if (chr === '\n') {
|
|
4643
4703
|
// Convert newlines to spaces. Carriage return is ignored as \r and \n usually
|
|
4644
4704
|
// go together anyway and there already is a WS for \n. Lone \r means something is fishy.
|
|
4645
4705
|
chr = ' ';
|
|
4646
4706
|
}
|
|
4647
|
-
|
|
4648
4707
|
if (chr.charCodeAt(0) >= 0x21 || [' ', '\t'].includes(chr)) {
|
|
4649
4708
|
// skip command bytes
|
|
4650
4709
|
this.node.value += chr;
|
|
4710
|
+
// text inside a quoted string or a comment is not part of an addr-spec. Tracked
|
|
4711
|
+
// as it goes rather than read back off the value (see lastChars in _handleAddress)
|
|
4712
|
+
if (!this.operatorExpecting && chr !== ' ' && chr !== '\t') {
|
|
4713
|
+
this.afterAt = chr === '@';
|
|
4714
|
+
}
|
|
4651
4715
|
}
|
|
4652
|
-
|
|
4653
4716
|
this.escaped = false;
|
|
4654
4717
|
}
|
|
4655
4718
|
}
|
|
4656
|
-
|
|
4657
4719
|
/**
|
|
4658
4720
|
* Maximum recursion depth for parsing nested groups.
|
|
4659
4721
|
* RFC 5322 doesn't allow nested groups, so this is a safeguard against
|
|
4660
4722
|
* malicious input that could cause stack overflow.
|
|
4661
4723
|
*/
|
|
4662
4724
|
const MAX_NESTED_GROUP_DEPTH = 50;
|
|
4663
|
-
|
|
4664
4725
|
/**
|
|
4665
|
-
* Parses
|
|
4666
|
-
*
|
|
4667
|
-
*
|
|
4668
|
-
*
|
|
4669
|
-
* 'Name <address@domain>'
|
|
4670
|
-
*
|
|
4671
|
-
* will be converted to
|
|
4726
|
+
* Parses an address list, recursing into groups. The depth is threaded through the
|
|
4727
|
+
* calls rather than taken from an option, so a caller supplied options object can not
|
|
4728
|
+
* seed it and lift the recursion limit.
|
|
4672
4729
|
*
|
|
4673
|
-
*
|
|
4674
|
-
*
|
|
4675
|
-
* @
|
|
4676
|
-
* @param {Object} options Optional options object
|
|
4677
|
-
* @param {Number} options._depth Internal recursion depth counter (do not set manually)
|
|
4678
|
-
* @return {Array} An array of address objects
|
|
4730
|
+
* @param str Address field
|
|
4731
|
+
* @param depth Current recursion depth for nested group protection
|
|
4732
|
+
* @return An array of address objects
|
|
4679
4733
|
*/
|
|
4680
|
-
function
|
|
4681
|
-
options = options || {};
|
|
4682
|
-
let depth = options._depth || 0;
|
|
4683
|
-
|
|
4734
|
+
function parseAddressList(str, depth) {
|
|
4684
4735
|
// Prevent stack overflow from deeply nested groups (DoS protection)
|
|
4685
4736
|
if (depth > MAX_NESTED_GROUP_DEPTH) {
|
|
4686
4737
|
return [];
|
|
4687
4738
|
}
|
|
4688
|
-
|
|
4689
4739
|
let tokenizer = new Tokenizer(str);
|
|
4690
4740
|
let tokens = tokenizer.tokenize();
|
|
4691
|
-
|
|
4692
4741
|
let addresses = [];
|
|
4693
4742
|
let address = [];
|
|
4694
4743
|
const parsedAddresses = [];
|
|
4695
|
-
|
|
4696
4744
|
tokens.forEach(token => {
|
|
4697
4745
|
if (token.type === 'operator' && (token.value === ',' || token.value === ';')) {
|
|
4698
4746
|
if (address.length) {
|
|
4699
4747
|
addresses.push(address);
|
|
4700
4748
|
}
|
|
4701
4749
|
address = [];
|
|
4702
|
-
}
|
|
4750
|
+
}
|
|
4751
|
+
else {
|
|
4703
4752
|
address.push(token);
|
|
4704
4753
|
}
|
|
4705
4754
|
});
|
|
4706
|
-
|
|
4707
4755
|
if (address.length) {
|
|
4708
4756
|
addresses.push(address);
|
|
4709
4757
|
}
|
|
4710
|
-
|
|
4711
4758
|
// Appended in place. Rebuilding the list with concat() for every address copies
|
|
4712
4759
|
// everything parsed so far and is quadratic in the number of addresses.
|
|
4713
4760
|
for (const tokens of addresses) {
|
|
@@ -4715,31 +4762,49 @@ function addressParser(str, options) {
|
|
|
4715
4762
|
parsedAddresses.push(address);
|
|
4716
4763
|
}
|
|
4717
4764
|
}
|
|
4718
|
-
|
|
4719
|
-
|
|
4720
|
-
|
|
4721
|
-
|
|
4722
|
-
|
|
4723
|
-
|
|
4724
|
-
|
|
4725
|
-
|
|
4726
|
-
|
|
4727
|
-
|
|
4728
|
-
|
|
4729
|
-
|
|
4730
|
-
|
|
4731
|
-
|
|
4765
|
+
// Merge fragments produced when unquoted display names contain commas.
|
|
4766
|
+
// "Joe Foo, PhD <joe@example.com>" is split on the comma into
|
|
4767
|
+
// [{name:"Joe Foo", address:""}, {name:"PhD", address:"joe@example.com"}].
|
|
4768
|
+
// Recombine: a name-only entry followed by an entry with both name and address.
|
|
4769
|
+
// Walked back to front so that a run of fragments folds into one entry in a single
|
|
4770
|
+
// pass. Splicing each fragment out of the list instead would be quadratic.
|
|
4771
|
+
const mergedAddresses = [];
|
|
4772
|
+
for (let i = parsedAddresses.length - 1; i >= 0; i--) {
|
|
4773
|
+
const current = parsedAddresses[i];
|
|
4774
|
+
const next = mergedAddresses.length ? mergedAddresses[mergedAddresses.length - 1] : null;
|
|
4775
|
+
if (next && current.address === '' && current.name && !current.group && next.address && next.name) {
|
|
4776
|
+
next.name = current.name + ', ' + next.name;
|
|
4777
|
+
}
|
|
4778
|
+
else {
|
|
4779
|
+
mergedAddresses.push(current);
|
|
4780
|
+
}
|
|
4732
4781
|
}
|
|
4733
|
-
|
|
4782
|
+
return mergedAddresses.reverse();
|
|
4783
|
+
}
|
|
4784
|
+
/**
|
|
4785
|
+
* Parses structured e-mail addresses from an address field
|
|
4786
|
+
*
|
|
4787
|
+
* Example:
|
|
4788
|
+
*
|
|
4789
|
+
* 'Name <address@domain>'
|
|
4790
|
+
*
|
|
4791
|
+
* will be converted to
|
|
4792
|
+
*
|
|
4793
|
+
* [{name: 'Name', address: 'address@domain'}]
|
|
4794
|
+
*
|
|
4795
|
+
* @param str Address field
|
|
4796
|
+
* @param options Optional options object
|
|
4797
|
+
* @return An array of address objects
|
|
4798
|
+
*/
|
|
4799
|
+
function addressParser(str, options) {
|
|
4800
|
+
const parsedAddresses = parseAddressList(str, 0);
|
|
4734
4801
|
return parsedAddresses;
|
|
4735
4802
|
}
|
|
4736
4803
|
|
|
4737
4804
|
// Code from: https://gist.githubusercontent.com/jonleighton/958841/raw/fb05a8632efb75d85d43deb593df04367ce48371/base64ArrayBuffer.js
|
|
4738
|
-
|
|
4739
4805
|
// Converts an ArrayBuffer directly to base64, without any intermediate 'convert to string then
|
|
4740
4806
|
// use window.btoa' step. According to my tests, this appears to be a faster approach:
|
|
4741
4807
|
// http://jsperf.com/encoding-xhr-image-data/5
|
|
4742
|
-
|
|
4743
4808
|
/*
|
|
4744
4809
|
MIT LICENSE
|
|
4745
4810
|
|
|
@@ -4751,59 +4816,63 @@ The above copyright notice and this permission notice shall be included in all c
|
|
|
4751
4816
|
|
|
4752
4817
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
4753
4818
|
*/
|
|
4754
|
-
|
|
4755
4819
|
function base64ArrayBuffer(arrayBuffer) {
|
|
4756
|
-
|
|
4757
|
-
|
|
4758
|
-
|
|
4759
|
-
|
|
4760
|
-
|
|
4761
|
-
|
|
4762
|
-
|
|
4763
|
-
|
|
4764
|
-
var a, b, c, d;
|
|
4765
|
-
var chunk;
|
|
4766
|
-
|
|
4820
|
+
let base64 = '';
|
|
4821
|
+
const encodings = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
|
4822
|
+
const bytes = new Uint8Array(arrayBuffer);
|
|
4823
|
+
const byteLength = bytes.byteLength;
|
|
4824
|
+
const byteRemainder = byteLength % 3;
|
|
4825
|
+
const mainLength = byteLength - byteRemainder;
|
|
4826
|
+
let a, b, c, d;
|
|
4827
|
+
let chunk;
|
|
4767
4828
|
// Main loop deals with bytes in chunks of 3
|
|
4768
|
-
for (
|
|
4829
|
+
for (let i = 0; i < mainLength; i = i + 3) {
|
|
4769
4830
|
// Combine the three bytes into a single integer
|
|
4770
4831
|
chunk = (bytes[i] << 16) | (bytes[i + 1] << 8) | bytes[i + 2];
|
|
4771
|
-
|
|
4772
4832
|
// Use bitmasks to extract 6-bit segments from the triplet
|
|
4773
4833
|
a = (chunk & 16515072) >> 18; // 16515072 = (2^6 - 1) << 18
|
|
4774
4834
|
b = (chunk & 258048) >> 12; // 258048 = (2^6 - 1) << 12
|
|
4775
4835
|
c = (chunk & 4032) >> 6; // 4032 = (2^6 - 1) << 6
|
|
4776
4836
|
d = chunk & 63; // 63 = 2^6 - 1
|
|
4777
|
-
|
|
4778
4837
|
// Convert the raw binary segments to the appropriate ASCII encoding
|
|
4779
4838
|
base64 += encodings[a] + encodings[b] + encodings[c] + encodings[d];
|
|
4780
4839
|
}
|
|
4781
|
-
|
|
4782
4840
|
// Deal with the remaining bytes and padding
|
|
4783
|
-
if (byteRemainder
|
|
4841
|
+
if (byteRemainder === 1) {
|
|
4784
4842
|
chunk = bytes[mainLength];
|
|
4785
|
-
|
|
4786
4843
|
a = (chunk & 252) >> 2; // 252 = (2^6 - 1) << 2
|
|
4787
|
-
|
|
4788
4844
|
// Set the 4 least significant bits to zero
|
|
4789
4845
|
b = (chunk & 3) << 4; // 3 = 2^2 - 1
|
|
4790
|
-
|
|
4791
4846
|
base64 += encodings[a] + encodings[b] + '==';
|
|
4792
|
-
}
|
|
4847
|
+
}
|
|
4848
|
+
else if (byteRemainder === 2) {
|
|
4793
4849
|
chunk = (bytes[mainLength] << 8) | bytes[mainLength + 1];
|
|
4794
|
-
|
|
4795
4850
|
a = (chunk & 64512) >> 10; // 64512 = (2^6 - 1) << 10
|
|
4796
4851
|
b = (chunk & 1008) >> 4; // 1008 = (2^6 - 1) << 4
|
|
4797
|
-
|
|
4798
4852
|
// Set the 2 least significant bits to zero
|
|
4799
4853
|
c = (chunk & 15) << 2; // 15 = 2^4 - 1
|
|
4800
|
-
|
|
4801
4854
|
base64 += encodings[a] + encodings[b] + encodings[c] + '=';
|
|
4802
4855
|
}
|
|
4803
|
-
|
|
4804
4856
|
return base64;
|
|
4805
4857
|
}
|
|
4806
4858
|
|
|
4859
|
+
function isReadableStream(value) {
|
|
4860
|
+
return !!value && typeof value.getReader === 'function';
|
|
4861
|
+
}
|
|
4862
|
+
function isBlob(value) {
|
|
4863
|
+
return value instanceof Blob || Object.prototype.toString.call(value) === '[object Blob]';
|
|
4864
|
+
}
|
|
4865
|
+
// Renders one entry of a node as the requested text type. A text part of the other type
|
|
4866
|
+
// is converted, and a nested message becomes a header block in that format
|
|
4867
|
+
function renderEntry(textEntry, textType, convert) {
|
|
4868
|
+
if (textEntry.type === 'subMessage') {
|
|
4869
|
+
return textType === 'html' ? formatHtmlHeader(textEntry.value) : formatTextHeader(textEntry.value);
|
|
4870
|
+
}
|
|
4871
|
+
if (!convert) {
|
|
4872
|
+
return textEntry.value;
|
|
4873
|
+
}
|
|
4874
|
+
return textType === 'html' ? textToHtml(textEntry.value) : htmlToText(textEntry.value);
|
|
4875
|
+
}
|
|
4807
4876
|
const MAX_NESTING_DEPTH = 256;
|
|
4808
4877
|
const MAX_HEADERS_SIZE = 2 * 1024 * 1024;
|
|
4809
4878
|
// Inline message/rfc822 parts are parsed recursively. Without a dedicated limit
|
|
@@ -4811,11 +4880,6 @@ const MAX_HEADERS_SIZE = 2 * 1024 * 1024;
|
|
|
4811
4880
|
// so a small crafted email can exhaust memory (OOM crash). Cap the recursion and
|
|
4812
4881
|
// treat deeper nested messages as regular attachments instead.
|
|
4813
4882
|
const MAX_RFC822_NESTING_DEPTH = 10;
|
|
4814
|
-
|
|
4815
|
-
function toCamelCase(key) {
|
|
4816
|
-
return key.replace(/-(.)/g, (o, c) => c.toUpperCase());
|
|
4817
|
-
}
|
|
4818
|
-
|
|
4819
4883
|
// Limit options must be validated rather than falsy-coalesced. `0` would silently
|
|
4820
4884
|
// restore the default, and a string or NaN disables the limit altogether, because
|
|
4821
4885
|
// every `size > limit` comparison against such a value is false. Callers that
|
|
@@ -4825,82 +4889,75 @@ function parseLimitOption(value, defaultValue, name) {
|
|
|
4825
4889
|
if (value === undefined || value === null) {
|
|
4826
4890
|
return defaultValue;
|
|
4827
4891
|
}
|
|
4828
|
-
|
|
4829
4892
|
if (typeof value !== 'number' || !Number.isInteger(value) || value < 0) {
|
|
4830
4893
|
throw new TypeError(`${name} must be a non-negative integer`);
|
|
4831
4894
|
}
|
|
4832
|
-
|
|
4833
4895
|
return value;
|
|
4834
4896
|
}
|
|
4835
|
-
|
|
4836
4897
|
class PostalMime {
|
|
4898
|
+
/**
|
|
4899
|
+
* Parses a raw email message
|
|
4900
|
+
*
|
|
4901
|
+
* @param buf Raw email message
|
|
4902
|
+
* @param options Parser options
|
|
4903
|
+
* @returns The parsed email
|
|
4904
|
+
*/
|
|
4837
4905
|
// async so that an invalid option rejects the returned promise instead of throwing
|
|
4838
4906
|
// synchronously, which would escape a `.catch()` chain
|
|
4839
4907
|
static async parse(buf, options) {
|
|
4840
4908
|
const parser = new PostalMime(options);
|
|
4841
4909
|
return parser.parse(buf);
|
|
4842
4910
|
}
|
|
4843
|
-
|
|
4844
|
-
// rfc822NestingDepth is internal state that nested parsers receive from their parent.
|
|
4845
|
-
// It is deliberately a separate argument rather than an option, so that forwarding a
|
|
4846
|
-
// caller supplied options object can not seed it and switch the recursion limit off.
|
|
4847
|
-
constructor(options, rfc822NestingDepth = 0) {
|
|
4911
|
+
constructor(options) {
|
|
4848
4912
|
this.options = options || {};
|
|
4849
4913
|
this.mimeOptions = {
|
|
4850
4914
|
maxNestingDepth: parseLimitOption(this.options.maxNestingDepth, MAX_NESTING_DEPTH, 'maxNestingDepth'),
|
|
4851
4915
|
maxHeadersSize: parseLimitOption(this.options.maxHeadersSize, MAX_HEADERS_SIZE, 'maxHeadersSize')
|
|
4852
4916
|
};
|
|
4853
|
-
|
|
4854
4917
|
// A limit of 0 disables inline parsing entirely, so every message/rfc822 part
|
|
4855
4918
|
// becomes an attachment.
|
|
4856
|
-
this.maxRfc822NestingDepth = parseLimitOption(
|
|
4857
|
-
|
|
4858
|
-
|
|
4859
|
-
|
|
4860
|
-
|
|
4861
|
-
this.rfc822NestingDepth = rfc822NestingDepth;
|
|
4862
|
-
|
|
4919
|
+
this.maxRfc822NestingDepth = parseLimitOption(this.options.maxRfc822NestingDepth, MAX_RFC822_NESTING_DEPTH, 'maxRfc822NestingDepth');
|
|
4920
|
+
// Internal state that a nested parser receives from its parent, see collectSubMessage.
|
|
4921
|
+
// It is deliberately not an option, so that forwarding a caller supplied options
|
|
4922
|
+
// object can not seed it and switch the recursion limit off.
|
|
4923
|
+
this.rfc822NestingDepth = 0;
|
|
4863
4924
|
this.root = this.currentNode = new MimeNode({
|
|
4864
4925
|
postalMime: this,
|
|
4865
4926
|
...this.mimeOptions
|
|
4866
4927
|
});
|
|
4867
4928
|
this.boundaries = [];
|
|
4868
|
-
|
|
4869
4929
|
// Header bytes seen across every part of this message, see MimeNode.feed
|
|
4870
4930
|
this.headerSize = 0;
|
|
4871
|
-
|
|
4872
4931
|
this.textContent = {};
|
|
4932
|
+
this.textMap = new Map();
|
|
4933
|
+
this.textTypes = new Set();
|
|
4873
4934
|
this.attachments = [];
|
|
4874
|
-
|
|
4935
|
+
this.forceRfc822Attachments = false;
|
|
4875
4936
|
this.attachmentEncoding =
|
|
4876
4937
|
(this.options.attachmentEncoding || '')
|
|
4877
4938
|
.toString()
|
|
4878
4939
|
.replace(/[-_\s]/g, '')
|
|
4879
4940
|
.trim()
|
|
4880
4941
|
.toLowerCase() || 'arraybuffer';
|
|
4881
|
-
|
|
4882
4942
|
this.started = false;
|
|
4883
4943
|
}
|
|
4884
|
-
|
|
4944
|
+
/** @internal */
|
|
4885
4945
|
async finalize() {
|
|
4886
4946
|
// close all pending nodes
|
|
4887
4947
|
await this.root.finalize();
|
|
4888
4948
|
}
|
|
4889
|
-
|
|
4949
|
+
/** @internal */
|
|
4890
4950
|
async processLine(line, isFinal) {
|
|
4891
4951
|
let boundaries = this.boundaries;
|
|
4892
|
-
|
|
4893
4952
|
// check if this is a mime boundary
|
|
4894
4953
|
if (boundaries.length && line.length > 2 && line[0] === 0x2d && line[1] === 0x2d) {
|
|
4895
4954
|
// could be a boundary marker
|
|
4896
4955
|
for (let i = boundaries.length - 1; i >= 0; i--) {
|
|
4897
4956
|
let boundary = boundaries[i];
|
|
4898
|
-
|
|
4899
4957
|
// Line must be at least long enough for "--" + boundary
|
|
4900
4958
|
if (line.length < boundary.value.length + 2) {
|
|
4901
4959
|
continue;
|
|
4902
4960
|
}
|
|
4903
|
-
|
|
4904
4961
|
// Check if boundary value matches
|
|
4905
4962
|
let boundaryMatches = true;
|
|
4906
4963
|
for (let j = 0; j < boundary.value.length; j++) {
|
|
@@ -4912,20 +4969,15 @@ class PostalMime {
|
|
|
4912
4969
|
if (!boundaryMatches) {
|
|
4913
4970
|
continue;
|
|
4914
4971
|
}
|
|
4915
|
-
|
|
4916
4972
|
// Check for terminator (-- after boundary) and determine where boundary ends
|
|
4917
4973
|
let boundaryEnd = boundary.value.length + 2;
|
|
4918
4974
|
let isTerminator = false;
|
|
4919
|
-
|
|
4920
|
-
if (
|
|
4921
|
-
line.length >= boundary.value.length + 4 &&
|
|
4975
|
+
if (line.length >= boundary.value.length + 4 &&
|
|
4922
4976
|
line[boundary.value.length + 2] === 0x2d &&
|
|
4923
|
-
line[boundary.value.length + 3] === 0x2d
|
|
4924
|
-
) {
|
|
4977
|
+
line[boundary.value.length + 3] === 0x2d) {
|
|
4925
4978
|
isTerminator = true;
|
|
4926
4979
|
boundaryEnd = boundary.value.length + 4;
|
|
4927
4980
|
}
|
|
4928
|
-
|
|
4929
4981
|
// RFC 2046: boundary line may have trailing whitespace (space/tab) before CRLF
|
|
4930
4982
|
let hasValidTrailing = true;
|
|
4931
4983
|
for (let j = boundaryEnd; j < line.length; j++) {
|
|
@@ -4937,15 +4989,13 @@ class PostalMime {
|
|
|
4937
4989
|
if (!hasValidTrailing) {
|
|
4938
4990
|
continue;
|
|
4939
4991
|
}
|
|
4940
|
-
|
|
4941
4992
|
if (isTerminator) {
|
|
4942
4993
|
await boundary.node.finalize();
|
|
4943
|
-
|
|
4944
4994
|
this.currentNode = boundary.node.parentNode || this.root;
|
|
4945
|
-
}
|
|
4995
|
+
}
|
|
4996
|
+
else {
|
|
4946
4997
|
// finalize any open child nodes (should be just one though)
|
|
4947
4998
|
await boundary.node.finalizeChildNodes();
|
|
4948
|
-
|
|
4949
4999
|
this.currentNode = new MimeNode({
|
|
4950
5000
|
postalMime: this,
|
|
4951
5001
|
parentNode: boundary.node,
|
|
@@ -4953,33 +5003,26 @@ class PostalMime {
|
|
|
4953
5003
|
...this.mimeOptions
|
|
4954
5004
|
});
|
|
4955
5005
|
}
|
|
4956
|
-
|
|
4957
5006
|
if (isFinal) {
|
|
4958
5007
|
return this.finalize();
|
|
4959
5008
|
}
|
|
4960
|
-
|
|
4961
5009
|
return;
|
|
4962
5010
|
}
|
|
4963
5011
|
}
|
|
4964
|
-
|
|
4965
5012
|
this.currentNode.feed(line);
|
|
4966
|
-
|
|
4967
5013
|
if (isFinal) {
|
|
4968
5014
|
return this.finalize();
|
|
4969
5015
|
}
|
|
4970
5016
|
}
|
|
4971
|
-
|
|
5017
|
+
/** @internal */
|
|
4972
5018
|
readLine() {
|
|
4973
5019
|
let startPos = this.readPos;
|
|
4974
5020
|
let endPos = this.readPos;
|
|
4975
|
-
|
|
4976
5021
|
while (this.readPos < this.av.length) {
|
|
4977
5022
|
const c = this.av[this.readPos++];
|
|
4978
|
-
|
|
4979
5023
|
if (c !== 0x0d && c !== 0x0a) {
|
|
4980
5024
|
endPos = this.readPos;
|
|
4981
5025
|
}
|
|
4982
|
-
|
|
4983
5026
|
if (c === 0x0a) {
|
|
4984
5027
|
return {
|
|
4985
5028
|
bytes: new Uint8Array(this.buf, startPos, endPos - startPos),
|
|
@@ -4987,302 +5030,202 @@ class PostalMime {
|
|
|
4987
5030
|
};
|
|
4988
5031
|
}
|
|
4989
5032
|
}
|
|
4990
|
-
|
|
4991
5033
|
return {
|
|
4992
5034
|
bytes: new Uint8Array(this.buf, startPos, endPos - startPos),
|
|
4993
5035
|
done: this.readPos >= this.av.length
|
|
4994
5036
|
};
|
|
4995
5037
|
}
|
|
4996
|
-
|
|
4997
|
-
|
|
4998
|
-
|
|
4999
|
-
|
|
5000
|
-
|
|
5001
|
-
|
|
5002
|
-
|
|
5003
|
-
|
|
5004
|
-
|
|
5005
|
-
|
|
5006
|
-
|
|
5007
|
-
|
|
5008
|
-
|
|
5009
|
-
|
|
5010
|
-
|
|
5011
|
-
|
|
5012
|
-
|
|
5013
|
-
|
|
5014
|
-
|
|
5015
|
-
|
|
5016
|
-
|
|
5017
|
-
|
|
5018
|
-
|
|
5019
|
-
|
|
5020
|
-
|
|
5021
|
-
...this.mimeOptions,
|
|
5022
|
-
maxRfc822NestingDepth: this.maxRfc822NestingDepth,
|
|
5023
|
-
// attachments are encoded by the parent parser, keep raw buffers here
|
|
5024
|
-
attachmentEncoding: 'arraybuffer'
|
|
5025
|
-
},
|
|
5026
|
-
this.rfc822NestingDepth + 1
|
|
5027
|
-
);
|
|
5028
|
-
node.subMessage = await subParser.parse(node.content);
|
|
5029
|
-
|
|
5030
|
-
if (!textMap.has(node)) {
|
|
5031
|
-
textMap.set(node, {});
|
|
5032
|
-
}
|
|
5033
|
-
|
|
5034
|
-
let textEntry = textMap.get(node);
|
|
5035
|
-
|
|
5036
|
-
// default to text if there is no content
|
|
5037
|
-
if (node.subMessage.text || !node.subMessage.html) {
|
|
5038
|
-
textEntry.plain = textEntry.plain || [];
|
|
5039
|
-
textEntry.plain.push({ type: 'subMessage', value: node.subMessage });
|
|
5040
|
-
textTypes.add('plain');
|
|
5041
|
-
}
|
|
5042
|
-
|
|
5043
|
-
if (node.subMessage.html) {
|
|
5044
|
-
textEntry.html = textEntry.html || [];
|
|
5045
|
-
textEntry.html.push({ type: 'subMessage', value: node.subMessage });
|
|
5046
|
-
textTypes.add('html');
|
|
5047
|
-
}
|
|
5048
|
-
|
|
5049
|
-
if (subParser.textMap) {
|
|
5050
|
-
subParser.textMap.forEach((subTextEntry, subTextNode) => {
|
|
5051
|
-
textMap.set(subTextNode, subTextEntry);
|
|
5052
|
-
});
|
|
5053
|
-
}
|
|
5054
|
-
|
|
5055
|
-
for (let attachment of node.subMessage.attachments || []) {
|
|
5056
|
-
this.attachments.push(attachment);
|
|
5057
|
-
}
|
|
5058
|
-
}
|
|
5059
|
-
|
|
5060
|
-
// is it text?
|
|
5061
|
-
else if (this.isInlineTextNode(node)) {
|
|
5062
|
-
let textType = node.contentType.parsed.value.substr(node.contentType.parsed.value.indexOf('/') + 1);
|
|
5063
|
-
|
|
5064
|
-
let selectorNode = alternative || node;
|
|
5065
|
-
if (!textMap.has(selectorNode)) {
|
|
5066
|
-
textMap.set(selectorNode, {});
|
|
5067
|
-
}
|
|
5068
|
-
|
|
5069
|
-
let textEntry = textMap.get(selectorNode);
|
|
5070
|
-
textEntry[textType] = textEntry[textType] || [];
|
|
5071
|
-
textEntry[textType].push({ type: 'text', value: node.getTextContent() });
|
|
5072
|
-
textTypes.add(textType);
|
|
5073
|
-
}
|
|
5074
|
-
|
|
5075
|
-
// is it an attachment
|
|
5076
|
-
else if (node.content) {
|
|
5077
|
-
const filename =
|
|
5078
|
-
node.contentDisposition?.parsed?.params?.filename ||
|
|
5079
|
-
node.contentType.parsed.params.name ||
|
|
5080
|
-
null;
|
|
5081
|
-
const attachment = {
|
|
5082
|
-
filename: filename ? decodeWords(filename) : null,
|
|
5083
|
-
mimeType: node.contentType.parsed.value,
|
|
5084
|
-
disposition: node.contentDisposition?.parsed?.value || null
|
|
5085
|
-
};
|
|
5086
|
-
|
|
5087
|
-
// A nested message that was not parsed is not a renderable inline
|
|
5088
|
-
// resource, so it must not join the cid map behind an <img src>.
|
|
5089
|
-
if (related && node.contentId && !rfc822DepthExceeded) {
|
|
5090
|
-
attachment.related = true;
|
|
5091
|
-
}
|
|
5092
|
-
|
|
5093
|
-
if (rfc822DepthExceeded) {
|
|
5094
|
-
// Tell the caller this part would have been parsed inline but hit
|
|
5095
|
-
// maxRfc822NestingDepth, so anything inside it is not reflected in
|
|
5096
|
-
// email.text, email.html or email.attachments.
|
|
5097
|
-
attachment.rfc822DepthExceeded = true;
|
|
5098
|
-
}
|
|
5099
|
-
|
|
5100
|
-
if (node.contentDescription) {
|
|
5101
|
-
// decoded like filename, it is an unstructured header that may
|
|
5102
|
-
// carry encoded words
|
|
5103
|
-
attachment.description = decodeWords(node.contentDescription);
|
|
5104
|
-
}
|
|
5105
|
-
|
|
5106
|
-
if (node.contentId) {
|
|
5107
|
-
attachment.contentId = node.contentId;
|
|
5108
|
-
}
|
|
5109
|
-
|
|
5110
|
-
switch (node.contentType.parsed.value) {
|
|
5111
|
-
// Special handling for calendar events
|
|
5112
|
-
case 'text/calendar':
|
|
5113
|
-
case 'application/ics': {
|
|
5114
|
-
if (node.contentType.parsed.params.method) {
|
|
5115
|
-
attachment.method = node.contentType.parsed.params.method
|
|
5116
|
-
.toString()
|
|
5117
|
-
.toUpperCase()
|
|
5118
|
-
.trim();
|
|
5119
|
-
}
|
|
5120
|
-
|
|
5121
|
-
// Enforce into unicode, ending in exactly one newline. The trailing
|
|
5122
|
-
// newlines are counted rather than replaced with `/\n*$/`, which
|
|
5123
|
-
// retries at every newline of a run that does not end the text.
|
|
5124
|
-
const decodedText = node.getTextContent().replace(/\r?\n/g, '\n');
|
|
5125
|
-
let end = decodedText.length;
|
|
5126
|
-
while (end > 0 && decodedText.charCodeAt(end - 1) === 0x0a) {
|
|
5127
|
-
end--;
|
|
5128
|
-
}
|
|
5129
|
-
attachment.content = textEncoder.encode(decodedText.slice(0, end) + '\n');
|
|
5130
|
-
break;
|
|
5131
|
-
}
|
|
5132
|
-
|
|
5133
|
-
// Regular attachments
|
|
5134
|
-
default:
|
|
5135
|
-
attachment.content = node.content;
|
|
5136
|
-
}
|
|
5137
|
-
|
|
5138
|
-
this.attachments.push(attachment);
|
|
5139
|
-
}
|
|
5140
|
-
} else if (node.contentType.multipart === 'alternative') {
|
|
5141
|
-
alternative = node;
|
|
5142
|
-
} else if (node.contentType.multipart === 'related') {
|
|
5143
|
-
related = node;
|
|
5038
|
+
// Records a text part or a nested message under the node that selects it
|
|
5039
|
+
/** @internal */
|
|
5040
|
+
addTextEntry(selector, textType, item) {
|
|
5041
|
+
let textEntry = this.textMap.get(selector);
|
|
5042
|
+
if (!textEntry) {
|
|
5043
|
+
textEntry = {};
|
|
5044
|
+
this.textMap.set(selector, textEntry);
|
|
5045
|
+
}
|
|
5046
|
+
const entries = textEntry[textType] || [];
|
|
5047
|
+
textEntry[textType] = entries;
|
|
5048
|
+
entries.push(item);
|
|
5049
|
+
this.textTypes.add(textType);
|
|
5050
|
+
}
|
|
5051
|
+
// Sorts every leaf of the tree into text content, nested messages and attachments.
|
|
5052
|
+
// `alternative` is the closest enclosing multipart/alternative, if any: its text parts
|
|
5053
|
+
// are collected under the alternative itself, so that the body is assembled from one
|
|
5054
|
+
// representation of it rather than from every one. `related` tells whether the part
|
|
5055
|
+
// sits inside a multipart/related, where a Content-ID makes it an inline resource
|
|
5056
|
+
/** @internal */
|
|
5057
|
+
async collectNode(node, alternative, related) {
|
|
5058
|
+
if (!node.contentType.multipart) {
|
|
5059
|
+
const inlineRfc822 = this.isInlineMessageRfc822(node);
|
|
5060
|
+
const rfc822DepthExceeded = inlineRfc822 && this.rfc822NestingDepth >= this.maxRfc822NestingDepth;
|
|
5061
|
+
if (inlineRfc822 && !rfc822DepthExceeded) {
|
|
5062
|
+
await this.collectSubMessage(node);
|
|
5144
5063
|
}
|
|
5145
|
-
|
|
5146
|
-
|
|
5147
|
-
|
|
5064
|
+
else if (this.isInlineTextNode(node)) {
|
|
5065
|
+
const textType = node.contentType.parsed.value === 'text/html' ? 'html' : 'plain';
|
|
5066
|
+
this.addTextEntry(alternative || node, textType, { type: 'text', value: node.getTextContent() });
|
|
5067
|
+
}
|
|
5068
|
+
else if (node.content) {
|
|
5069
|
+
this.collectAttachment(node, node.content, related, rfc822DepthExceeded);
|
|
5148
5070
|
}
|
|
5071
|
+
}
|
|
5072
|
+
else if (node.contentType.multipart === 'alternative') {
|
|
5073
|
+
alternative = node;
|
|
5074
|
+
}
|
|
5075
|
+
else if (node.contentType.multipart === 'related') {
|
|
5076
|
+
related = true;
|
|
5077
|
+
}
|
|
5078
|
+
for (let childNode of node.childNodes) {
|
|
5079
|
+
await this.collectNode(childNode, alternative, related);
|
|
5080
|
+
}
|
|
5081
|
+
}
|
|
5082
|
+
// Parses an inline message/rfc822 part with a nested parser and takes over its text
|
|
5083
|
+
// parts and attachments
|
|
5084
|
+
/** @internal */
|
|
5085
|
+
async collectSubMessage(node) {
|
|
5086
|
+
const subParser = new PostalMime({
|
|
5087
|
+
// Only the limits are inherited. Options that decide how a part
|
|
5088
|
+
// is classified stay with the parser that was configured.
|
|
5089
|
+
...this.mimeOptions,
|
|
5090
|
+
maxRfc822NestingDepth: this.maxRfc822NestingDepth,
|
|
5091
|
+
// attachments are encoded by the parent parser, keep raw buffers here
|
|
5092
|
+
attachmentEncoding: 'arraybuffer'
|
|
5093
|
+
});
|
|
5094
|
+
subParser.rfc822NestingDepth = this.rfc822NestingDepth + 1;
|
|
5095
|
+
const subMessage = (node.subMessage = await subParser.parse(node.content || new ArrayBuffer(0)));
|
|
5096
|
+
// default to text if there is no content
|
|
5097
|
+
if (subMessage.text || !subMessage.html) {
|
|
5098
|
+
this.addTextEntry(node, 'plain', { type: 'subMessage', value: subMessage });
|
|
5099
|
+
}
|
|
5100
|
+
if (subMessage.html) {
|
|
5101
|
+
this.addTextEntry(node, 'html', { type: 'subMessage', value: subMessage });
|
|
5102
|
+
}
|
|
5103
|
+
subParser.textMap.forEach((subTextEntry, subTextNode) => {
|
|
5104
|
+
this.textMap.set(subTextNode, subTextEntry);
|
|
5105
|
+
});
|
|
5106
|
+
for (let attachment of subMessage.attachments) {
|
|
5107
|
+
this.attachments.push(attachment);
|
|
5108
|
+
}
|
|
5109
|
+
}
|
|
5110
|
+
/** @internal */
|
|
5111
|
+
collectAttachment(node, content, related, rfc822DepthExceeded) {
|
|
5112
|
+
const filename = node.contentDisposition.parsed.params.filename || node.contentType.parsed.params.name || null;
|
|
5113
|
+
// `content` is filled in below once the part type is known, hence the cast
|
|
5114
|
+
const attachment = {
|
|
5115
|
+
filename: filename ? decodeWords(filename) : null,
|
|
5116
|
+
mimeType: node.contentType.parsed.value,
|
|
5117
|
+
disposition: node.contentDisposition.parsed.value || null
|
|
5149
5118
|
};
|
|
5150
|
-
|
|
5151
|
-
|
|
5152
|
-
|
|
5153
|
-
|
|
5154
|
-
|
|
5155
|
-
|
|
5156
|
-
|
|
5119
|
+
// A nested message that was not parsed is not a renderable inline
|
|
5120
|
+
// resource, so it must not join the cid map behind an <img src>.
|
|
5121
|
+
if (related && node.contentId && !rfc822DepthExceeded) {
|
|
5122
|
+
attachment.related = true;
|
|
5123
|
+
}
|
|
5124
|
+
if (rfc822DepthExceeded) {
|
|
5125
|
+
// Tell the caller this part would have been parsed inline but hit
|
|
5126
|
+
// maxRfc822NestingDepth, so anything inside it is not reflected in
|
|
5127
|
+
// email.text, email.html or email.attachments.
|
|
5128
|
+
attachment.rfc822DepthExceeded = true;
|
|
5129
|
+
}
|
|
5130
|
+
if (node.contentDescription) {
|
|
5131
|
+
// decoded like filename, it is an unstructured header that may
|
|
5132
|
+
// carry encoded words
|
|
5133
|
+
attachment.description = decodeWords(node.contentDescription);
|
|
5134
|
+
}
|
|
5135
|
+
if (node.contentId) {
|
|
5136
|
+
attachment.contentId = node.contentId;
|
|
5137
|
+
}
|
|
5138
|
+
switch (node.contentType.parsed.value) {
|
|
5139
|
+
// Special handling for calendar events
|
|
5140
|
+
case 'text/calendar':
|
|
5141
|
+
case 'application/ics': {
|
|
5142
|
+
if (node.contentType.parsed.params.method) {
|
|
5143
|
+
attachment.method = node.contentType.parsed.params.method.toString().toUpperCase().trim();
|
|
5157
5144
|
}
|
|
5158
|
-
|
|
5159
|
-
|
|
5160
|
-
|
|
5161
|
-
|
|
5162
|
-
|
|
5163
|
-
|
|
5164
|
-
|
|
5165
|
-
|
|
5166
|
-
|
|
5167
|
-
|
|
5168
|
-
|
|
5169
|
-
|
|
5170
|
-
|
|
5171
|
-
|
|
5172
|
-
|
|
5173
|
-
|
|
5174
|
-
|
|
5175
|
-
|
|
5176
|
-
|
|
5177
|
-
|
|
5178
|
-
|
|
5179
|
-
|
|
5180
|
-
|
|
5181
|
-
|
|
5182
|
-
|
|
5183
|
-
|
|
5184
|
-
|
|
5185
|
-
|
|
5186
|
-
|
|
5187
|
-
|
|
5188
|
-
break;
|
|
5189
|
-
}
|
|
5190
|
-
|
|
5191
|
-
(mapEntry[alternativeType] || []).forEach(textEntry => {
|
|
5192
|
-
switch (textEntry.type) {
|
|
5193
|
-
case 'text':
|
|
5194
|
-
switch (textType) {
|
|
5195
|
-
case 'html':
|
|
5196
|
-
textContent[textType].push(textToHtml(textEntry.value));
|
|
5197
|
-
break;
|
|
5198
|
-
case 'plain':
|
|
5199
|
-
textContent[textType].push(htmlToText(textEntry.value));
|
|
5200
|
-
break;
|
|
5201
|
-
}
|
|
5202
|
-
break;
|
|
5203
|
-
|
|
5204
|
-
case 'subMessage':
|
|
5205
|
-
{
|
|
5206
|
-
switch (textType) {
|
|
5207
|
-
case 'html':
|
|
5208
|
-
textContent[textType].push(formatHtmlHeader(textEntry.value));
|
|
5209
|
-
break;
|
|
5210
|
-
case 'plain':
|
|
5211
|
-
textContent[textType].push(formatTextHeader(textEntry.value));
|
|
5212
|
-
break;
|
|
5213
|
-
}
|
|
5214
|
-
}
|
|
5215
|
-
break;
|
|
5216
|
-
}
|
|
5217
|
-
});
|
|
5145
|
+
// Enforce into unicode, ending in exactly one newline. The trailing
|
|
5146
|
+
// newlines are counted rather than replaced with `/\n*$/`, which
|
|
5147
|
+
// retries at every newline of a run that does not end the text.
|
|
5148
|
+
const decodedText = node.getTextContent().replace(/\r?\n/g, '\n');
|
|
5149
|
+
let end = decodedText.length;
|
|
5150
|
+
while (end > 0 && decodedText.charCodeAt(end - 1) === 0x0a) {
|
|
5151
|
+
end--;
|
|
5152
|
+
}
|
|
5153
|
+
attachment.content = textEncoder.encode(decodedText.slice(0, end) + '\n');
|
|
5154
|
+
break;
|
|
5155
|
+
}
|
|
5156
|
+
// Regular attachments
|
|
5157
|
+
default:
|
|
5158
|
+
attachment.content = content;
|
|
5159
|
+
}
|
|
5160
|
+
this.attachments.push(attachment);
|
|
5161
|
+
}
|
|
5162
|
+
// Joins the collected text parts into the plain and html bodies
|
|
5163
|
+
/** @internal */
|
|
5164
|
+
renderTextContent() {
|
|
5165
|
+
const textContent = {};
|
|
5166
|
+
this.textMap.forEach(mapEntry => {
|
|
5167
|
+
this.textTypes.forEach(textType => {
|
|
5168
|
+
const output = textContent[textType] || [];
|
|
5169
|
+
textContent[textType] = output;
|
|
5170
|
+
// a node without a part of this type is rendered from the other type
|
|
5171
|
+
const ownEntries = mapEntry[textType];
|
|
5172
|
+
const entries = ownEntries || mapEntry[textType === 'html' ? 'plain' : 'html'] || [];
|
|
5173
|
+
for (const textEntry of entries) {
|
|
5174
|
+
output.push(renderEntry(textEntry, textType, !ownEntries));
|
|
5218
5175
|
}
|
|
5219
5176
|
});
|
|
5220
5177
|
});
|
|
5221
|
-
|
|
5222
5178
|
Object.keys(textContent).forEach(textType => {
|
|
5223
|
-
textContent[textType] = textContent[textType].join('\n');
|
|
5179
|
+
this.textContent[textType] = textContent[textType].join('\n');
|
|
5224
5180
|
});
|
|
5225
|
-
|
|
5226
|
-
this.textContent = textContent;
|
|
5227
5181
|
}
|
|
5228
|
-
|
|
5182
|
+
/** @internal */
|
|
5229
5183
|
isInlineTextNode(node) {
|
|
5230
|
-
if (node.contentDisposition
|
|
5184
|
+
if (node.contentDisposition.parsed.value === 'attachment') {
|
|
5231
5185
|
// no matter the type, this is an attachment
|
|
5232
5186
|
return false;
|
|
5233
5187
|
}
|
|
5234
|
-
|
|
5235
|
-
switch (node.contentType.parsed?.value) {
|
|
5188
|
+
switch (node.contentType.parsed.value) {
|
|
5236
5189
|
case 'text/html':
|
|
5237
5190
|
case 'text/plain':
|
|
5238
5191
|
return true;
|
|
5239
|
-
|
|
5240
5192
|
case 'text/calendar':
|
|
5241
5193
|
case 'text/csv':
|
|
5242
5194
|
default:
|
|
5243
5195
|
return false;
|
|
5244
5196
|
}
|
|
5245
5197
|
}
|
|
5246
|
-
|
|
5198
|
+
/** @internal */
|
|
5247
5199
|
isInlineMessageRfc822(node) {
|
|
5248
|
-
if (node.contentType.parsed
|
|
5200
|
+
if (this.forceRfc822Attachments || node.contentType.parsed.value !== 'message/rfc822') {
|
|
5249
5201
|
return false;
|
|
5250
5202
|
}
|
|
5251
|
-
let disposition =
|
|
5252
|
-
node.contentDisposition?.parsed?.value || (this.options.rfc822Attachments ? 'attachment' : 'inline');
|
|
5203
|
+
let disposition = node.contentDisposition.parsed.value || (this.options.rfc822Attachments ? 'attachment' : 'inline');
|
|
5253
5204
|
return disposition === 'inline';
|
|
5254
5205
|
}
|
|
5255
|
-
|
|
5256
|
-
//
|
|
5257
|
-
|
|
5258
|
-
|
|
5259
|
-
|
|
5260
|
-
|
|
5261
|
-
|
|
5262
|
-
let forceRfc822Attachments = false;
|
|
5263
|
-
let walk = node => {
|
|
5206
|
+
// A delivery status or feedback report carries the offending message as a part. It is
|
|
5207
|
+
// evidence rather than content, so it is not inlined into the body of the report
|
|
5208
|
+
/** @internal */
|
|
5209
|
+
hasReportParts() {
|
|
5210
|
+
let found = false;
|
|
5211
|
+
let walk = (node) => {
|
|
5264
5212
|
if (!node.contentType.multipart) {
|
|
5265
|
-
if (
|
|
5266
|
-
|
|
5267
|
-
['message/delivery-status', 'message/feedback-report'].includes(node.contentType.parsed.value)
|
|
5268
|
-
) {
|
|
5269
|
-
forceRfc822Attachments = true;
|
|
5213
|
+
if (['message/delivery-status', 'message/feedback-report'].includes(node.contentType.parsed.value)) {
|
|
5214
|
+
found = true;
|
|
5270
5215
|
}
|
|
5271
5216
|
}
|
|
5272
|
-
|
|
5273
5217
|
for (let childNode of node.childNodes) {
|
|
5274
5218
|
walk(childNode);
|
|
5275
5219
|
}
|
|
5276
5220
|
};
|
|
5277
5221
|
walk(this.root);
|
|
5278
|
-
return
|
|
5222
|
+
return found;
|
|
5279
5223
|
}
|
|
5280
|
-
|
|
5224
|
+
/** @internal */
|
|
5281
5225
|
async resolveStream(stream) {
|
|
5282
5226
|
let chunkLen = 0;
|
|
5283
5227
|
let chunks = [];
|
|
5284
5228
|
const reader = stream.getReader();
|
|
5285
|
-
|
|
5286
5229
|
while (true) {
|
|
5287
5230
|
const { done, value } = await reader.read();
|
|
5288
5231
|
if (done) {
|
|
@@ -5291,72 +5234,54 @@ class PostalMime {
|
|
|
5291
5234
|
chunks.push(value);
|
|
5292
5235
|
chunkLen += value.length;
|
|
5293
5236
|
}
|
|
5294
|
-
|
|
5295
5237
|
const result = new Uint8Array(chunkLen);
|
|
5296
5238
|
let chunkPointer = 0;
|
|
5297
5239
|
for (let chunk of chunks) {
|
|
5298
5240
|
result.set(chunk, chunkPointer);
|
|
5299
5241
|
chunkPointer += chunk.length;
|
|
5300
5242
|
}
|
|
5301
|
-
|
|
5302
5243
|
return result;
|
|
5303
5244
|
}
|
|
5304
|
-
|
|
5305
|
-
async
|
|
5306
|
-
|
|
5307
|
-
throw new Error('Can not reuse parser, create a new PostalMime object');
|
|
5308
|
-
}
|
|
5309
|
-
this.started = true;
|
|
5310
|
-
|
|
5245
|
+
/** @internal */
|
|
5246
|
+
async resolveInput(buf) {
|
|
5247
|
+
let input = buf;
|
|
5311
5248
|
// Check if the input is a readable stream and resolve it into an ArrayBuffer
|
|
5312
|
-
if (
|
|
5313
|
-
|
|
5249
|
+
if (isReadableStream(input)) {
|
|
5250
|
+
input = await this.resolveStream(input);
|
|
5314
5251
|
}
|
|
5315
|
-
|
|
5316
5252
|
// Should it throw for an empty value instead of defaulting to an empty ArrayBuffer?
|
|
5317
|
-
|
|
5318
|
-
|
|
5253
|
+
input = input || new ArrayBuffer(0);
|
|
5319
5254
|
// Cast string input to Uint8Array
|
|
5320
|
-
if (typeof
|
|
5321
|
-
|
|
5255
|
+
if (typeof input === 'string') {
|
|
5256
|
+
input = textEncoder.encode(input);
|
|
5322
5257
|
}
|
|
5323
|
-
|
|
5324
5258
|
// Cast Blob to ArrayBuffer
|
|
5325
|
-
if (
|
|
5326
|
-
|
|
5259
|
+
if (isBlob(input)) {
|
|
5260
|
+
input = await blobToArrayBuffer(input);
|
|
5327
5261
|
}
|
|
5328
|
-
|
|
5329
5262
|
// Cast a Node.js Buffer, a typed array or a DataView into an ArrayBuffer.
|
|
5330
5263
|
// `new Uint8Array(view)` only works for array-likes, so a DataView produced an
|
|
5331
5264
|
// empty buffer and the message parsed to nothing without an error. Slicing off
|
|
5332
5265
|
// byteOffset also keeps views over a larger buffer from reading their neighbours.
|
|
5333
|
-
if (ArrayBuffer.isView(
|
|
5334
|
-
|
|
5335
|
-
}
|
|
5336
|
-
|
|
5337
|
-
this.buf = buf;
|
|
5338
|
-
|
|
5339
|
-
this.av = new Uint8Array(buf);
|
|
5340
|
-
this.readPos = 0;
|
|
5341
|
-
|
|
5342
|
-
while (this.readPos < this.av.length) {
|
|
5343
|
-
const line = this.readLine();
|
|
5344
|
-
|
|
5345
|
-
await this.processLine(line.bytes, line.done);
|
|
5266
|
+
if (ArrayBuffer.isView(input)) {
|
|
5267
|
+
return input.buffer.slice(input.byteOffset, input.byteOffset + input.byteLength);
|
|
5346
5268
|
}
|
|
5347
|
-
|
|
5348
|
-
|
|
5349
|
-
|
|
5269
|
+
return input;
|
|
5270
|
+
}
|
|
5271
|
+
// Properties are added in the order the output has always had them, so the required
|
|
5272
|
+
// ones are filled in below rather than in the literal
|
|
5273
|
+
/** @internal */
|
|
5274
|
+
buildMessage() {
|
|
5275
|
+
const headers = this.root.headers;
|
|
5350
5276
|
const message = {
|
|
5351
|
-
headers:
|
|
5277
|
+
headers: headers.map(entry => ({
|
|
5352
5278
|
key: entry.key,
|
|
5353
5279
|
originalKey: entry.originalKey,
|
|
5354
5280
|
value: entry.value
|
|
5355
5281
|
}))
|
|
5356
5282
|
};
|
|
5357
|
-
|
|
5358
5283
|
for (const key of ['from', 'sender']) {
|
|
5359
|
-
const addressHeader =
|
|
5284
|
+
const addressHeader = headers.find(line => line.key === key);
|
|
5360
5285
|
if (addressHeader && addressHeader.value) {
|
|
5361
5286
|
const addresses = addressParser(addressHeader.value);
|
|
5362
5287
|
if (addresses && addresses.length) {
|
|
@@ -5364,95 +5289,118 @@ class PostalMime {
|
|
|
5364
5289
|
}
|
|
5365
5290
|
}
|
|
5366
5291
|
}
|
|
5367
|
-
|
|
5368
|
-
|
|
5369
|
-
|
|
5292
|
+
for (const [key, camelKey] of [
|
|
5293
|
+
['delivered-to', 'deliveredTo'],
|
|
5294
|
+
['return-path', 'returnPath']
|
|
5295
|
+
]) {
|
|
5296
|
+
const addressHeader = headers.find(line => line.key === key);
|
|
5370
5297
|
if (addressHeader && addressHeader.value) {
|
|
5371
5298
|
const addresses = addressParser(addressHeader.value);
|
|
5372
5299
|
if (addresses && addresses.length && addresses[0].address) {
|
|
5373
|
-
const camelKey = toCamelCase(key);
|
|
5374
5300
|
message[camelKey] = addresses[0].address;
|
|
5375
5301
|
}
|
|
5376
5302
|
}
|
|
5377
5303
|
}
|
|
5378
|
-
|
|
5379
|
-
|
|
5304
|
+
for (const [key, camelKey] of [
|
|
5305
|
+
['to', 'to'],
|
|
5306
|
+
['cc', 'cc'],
|
|
5307
|
+
['bcc', 'bcc'],
|
|
5308
|
+
['reply-to', 'replyTo']
|
|
5309
|
+
]) {
|
|
5380
5310
|
// Appended in place, concat() copies the whole list for every header
|
|
5381
5311
|
const addresses = [];
|
|
5382
|
-
for (const entry of
|
|
5312
|
+
for (const entry of headers) {
|
|
5383
5313
|
if (entry.key === key && entry.value) {
|
|
5384
5314
|
for (const address of addressParser(entry.value)) {
|
|
5385
5315
|
addresses.push(address);
|
|
5386
5316
|
}
|
|
5387
5317
|
}
|
|
5388
5318
|
}
|
|
5389
|
-
|
|
5390
5319
|
if (addresses.length) {
|
|
5391
|
-
const camelKey = toCamelCase(key);
|
|
5392
5320
|
message[camelKey] = addresses;
|
|
5393
5321
|
}
|
|
5394
5322
|
}
|
|
5395
|
-
|
|
5396
|
-
|
|
5397
|
-
|
|
5323
|
+
for (const [key, camelKey] of [
|
|
5324
|
+
['subject', 'subject'],
|
|
5325
|
+
['message-id', 'messageId'],
|
|
5326
|
+
['in-reply-to', 'inReplyTo'],
|
|
5327
|
+
['references', 'references']
|
|
5328
|
+
]) {
|
|
5329
|
+
const header = headers.find(line => line.key === key);
|
|
5398
5330
|
if (header && header.value) {
|
|
5399
|
-
const camelKey = toCamelCase(key);
|
|
5400
5331
|
message[camelKey] = decodeWords(header.value);
|
|
5401
5332
|
}
|
|
5402
5333
|
}
|
|
5403
|
-
|
|
5404
|
-
let dateHeader = this.root.headers.find(line => line.key === 'date');
|
|
5334
|
+
let dateHeader = headers.find(line => line.key === 'date');
|
|
5405
5335
|
if (dateHeader) {
|
|
5406
5336
|
let date = new Date(dateHeader.value);
|
|
5407
|
-
if
|
|
5408
|
-
|
|
5409
|
-
} else {
|
|
5410
|
-
// enforce ISO format if seems to be a valid date
|
|
5411
|
-
date = date.toISOString();
|
|
5412
|
-
}
|
|
5413
|
-
message.date = date;
|
|
5337
|
+
// enforce ISO format if seems to be a valid date
|
|
5338
|
+
message.date = date.toString() === 'Invalid Date' ? dateHeader.value : date.toISOString();
|
|
5414
5339
|
}
|
|
5415
|
-
|
|
5416
|
-
if (this.textContent?.html) {
|
|
5340
|
+
if (this.textContent.html) {
|
|
5417
5341
|
message.html = this.textContent.html;
|
|
5418
5342
|
}
|
|
5419
|
-
|
|
5420
|
-
if (this.textContent?.plain) {
|
|
5343
|
+
if (this.textContent.plain) {
|
|
5421
5344
|
message.text = this.textContent.plain;
|
|
5422
5345
|
}
|
|
5423
|
-
|
|
5424
5346
|
message.attachments = this.attachments;
|
|
5425
|
-
|
|
5426
5347
|
// Expose raw header lines, in the same order as the headers array
|
|
5427
|
-
message.headerLines =
|
|
5428
|
-
|
|
5348
|
+
message.headerLines = this.root.rawHeaderLines.slice();
|
|
5349
|
+
return message;
|
|
5350
|
+
}
|
|
5351
|
+
// Every attachment holds binary content at this point, since nested parsers are
|
|
5352
|
+
// created with the arraybuffer encoding. The string check narrows the public content
|
|
5353
|
+
// type rather than handling a reachable case
|
|
5354
|
+
/** @internal */
|
|
5355
|
+
encodeAttachments() {
|
|
5429
5356
|
switch (this.attachmentEncoding) {
|
|
5430
5357
|
case 'arraybuffer':
|
|
5431
5358
|
break;
|
|
5432
|
-
|
|
5433
5359
|
case 'base64':
|
|
5434
|
-
for (let attachment of
|
|
5435
|
-
if (attachment
|
|
5360
|
+
for (let attachment of this.attachments) {
|
|
5361
|
+
if (attachment.content && typeof attachment.content !== 'string') {
|
|
5436
5362
|
attachment.content = base64ArrayBuffer(attachment.content);
|
|
5437
5363
|
attachment.encoding = 'base64';
|
|
5438
5364
|
}
|
|
5439
5365
|
}
|
|
5440
5366
|
break;
|
|
5441
|
-
|
|
5442
|
-
case 'utf8':
|
|
5367
|
+
case 'utf8': {
|
|
5443
5368
|
let attachmentDecoder = new TextDecoder('utf8');
|
|
5444
|
-
for (let attachment of
|
|
5445
|
-
if (attachment
|
|
5369
|
+
for (let attachment of this.attachments) {
|
|
5370
|
+
if (attachment.content && typeof attachment.content !== 'string') {
|
|
5446
5371
|
attachment.content = attachmentDecoder.decode(attachment.content);
|
|
5447
5372
|
attachment.encoding = 'utf8';
|
|
5448
5373
|
}
|
|
5449
5374
|
}
|
|
5450
5375
|
break;
|
|
5451
|
-
|
|
5376
|
+
}
|
|
5452
5377
|
default:
|
|
5453
5378
|
throw new Error('Unknown attachment encoding');
|
|
5454
5379
|
}
|
|
5455
|
-
|
|
5380
|
+
}
|
|
5381
|
+
/**
|
|
5382
|
+
* Parses a raw email message. A parser instance can be used once
|
|
5383
|
+
*
|
|
5384
|
+
* @param buf Raw email message
|
|
5385
|
+
* @returns The parsed email
|
|
5386
|
+
*/
|
|
5387
|
+
async parse(buf) {
|
|
5388
|
+
if (this.started) {
|
|
5389
|
+
throw new Error('Can not reuse parser, create a new PostalMime object');
|
|
5390
|
+
}
|
|
5391
|
+
this.started = true;
|
|
5392
|
+
this.buf = await this.resolveInput(buf);
|
|
5393
|
+
this.av = new Uint8Array(this.buf);
|
|
5394
|
+
this.readPos = 0;
|
|
5395
|
+
while (this.readPos < this.av.length) {
|
|
5396
|
+
const line = this.readLine();
|
|
5397
|
+
await this.processLine(line.bytes, line.done);
|
|
5398
|
+
}
|
|
5399
|
+
this.forceRfc822Attachments = Boolean(this.options.forceRfc822Attachments) || this.hasReportParts();
|
|
5400
|
+
await this.collectNode(this.root, false, false);
|
|
5401
|
+
this.renderTextContent();
|
|
5402
|
+
const message = this.buildMessage();
|
|
5403
|
+
this.encodeAttachments();
|
|
5456
5404
|
return message;
|
|
5457
5405
|
}
|
|
5458
5406
|
}
|
|
@@ -5604,10 +5552,7 @@ function getSafeEmailImageSrc(src) {
|
|
|
5604
5552
|
return;
|
|
5605
5553
|
}
|
|
5606
5554
|
const mimeType = getDataUrlMimeType(trimmedSrc);
|
|
5607
|
-
if (!mimeType) {
|
|
5608
|
-
return;
|
|
5609
|
-
}
|
|
5610
|
-
if (!allowedMimeTypes.has(mimeType)) {
|
|
5555
|
+
if (!mimeType || !allowedMimeTypes.has(mimeType)) {
|
|
5611
5556
|
return;
|
|
5612
5557
|
}
|
|
5613
5558
|
return trimmedSrc;
|
|
@@ -5743,7 +5688,9 @@ async function fetchEmailBuffer(url) {
|
|
|
5743
5688
|
}
|
|
5744
5689
|
catch (error) {
|
|
5745
5690
|
const message = error instanceof Error ? error.message : String(error);
|
|
5746
|
-
throw new Error(`Failed to load email from ${url}: ${message}
|
|
5691
|
+
throw new Error(`Failed to load email from ${url}: ${message}`, {
|
|
5692
|
+
cause: error,
|
|
5693
|
+
});
|
|
5747
5694
|
}
|
|
5748
5695
|
}
|
|
5749
5696
|
async function parseEmail(url, buffer) {
|
|
@@ -5754,7 +5701,9 @@ async function parseEmail(url, buffer) {
|
|
|
5754
5701
|
}
|
|
5755
5702
|
catch (error) {
|
|
5756
5703
|
const message = error instanceof Error ? error.message : String(error);
|
|
5757
|
-
throw new Error(`Failed to parse email from ${url}: ${message}
|
|
5704
|
+
throw new Error(`Failed to parse email from ${url}: ${message}`, {
|
|
5705
|
+
cause: error,
|
|
5706
|
+
});
|
|
5758
5707
|
}
|
|
5759
5708
|
}
|
|
5760
5709
|
function getRawHeader(email, name) {
|
|
@@ -5764,7 +5713,8 @@ function getRawHeader(email, name) {
|
|
|
5764
5713
|
function extractAttachments(email) {
|
|
5765
5714
|
const attachments = [];
|
|
5766
5715
|
const cidUrlById = new Map();
|
|
5767
|
-
|
|
5716
|
+
const emailAttachments = email.attachments || [];
|
|
5717
|
+
for (const attachment of emailAttachments) {
|
|
5768
5718
|
const contentId = normalizeContentId(attachment.contentId);
|
|
5769
5719
|
const hasContentId = Boolean(contentId);
|
|
5770
5720
|
const isInline = (hasContentId && attachment.disposition !== 'attachment') ||
|
|
@@ -5868,7 +5818,7 @@ function byteArrayToBase64(bytes) {
|
|
|
5868
5818
|
return btoa(binary);
|
|
5869
5819
|
}
|
|
5870
5820
|
// Jest/Node fallback
|
|
5871
|
-
return
|
|
5821
|
+
return Buffer.from(bytes).toString('base64');
|
|
5872
5822
|
}
|
|
5873
5823
|
function resolveDataUrlMimeType(mimeType, bytes, filename) {
|
|
5874
5824
|
const normalizedMimeType = typeof mimeType === 'string' ? mimeType.trim().toLowerCase() : '';
|
|
@@ -5891,10 +5841,7 @@ function resolveDataUrlMimeType(mimeType, bytes, filename) {
|
|
|
5891
5841
|
return normalizedMimeType || 'application/octet-stream';
|
|
5892
5842
|
}
|
|
5893
5843
|
function isTrustedDeclaredImageMimeType(declaredMimeType, detectedMimeType) {
|
|
5894
|
-
if (!detectedMimeType) {
|
|
5895
|
-
return true;
|
|
5896
|
-
}
|
|
5897
|
-
if (declaredMimeType === detectedMimeType) {
|
|
5844
|
+
if (!detectedMimeType || declaredMimeType === detectedMimeType) {
|
|
5898
5845
|
return true;
|
|
5899
5846
|
}
|
|
5900
5847
|
if (declaredMimeType === 'image/jpg' && detectedMimeType === 'image/jpeg') {
|
|
@@ -5939,7 +5886,7 @@ function detectSvgMimeType(bytes) {
|
|
|
5939
5886
|
if (bytes.length < 5) {
|
|
5940
5887
|
return;
|
|
5941
5888
|
}
|
|
5942
|
-
const utf8Prefix = new TextDecoder('
|
|
5889
|
+
const utf8Prefix = new TextDecoder('utf-8', { fatal: false }).decode(bytes.slice(0, Math.min(bytes.length, 256)));
|
|
5943
5890
|
const normalizedPrefix = utf8Prefix.trimStart().toLowerCase();
|
|
5944
5891
|
if (normalizedPrefix.startsWith('<svg') ||
|
|
5945
5892
|
(normalizedPrefix.startsWith('<?xml') &&
|
|
@@ -6031,7 +5978,9 @@ function quoteDisplayNameIfNeeded(name) {
|
|
|
6031
5978
|
if (!name.includes(',') && !name.includes('"')) {
|
|
6032
5979
|
return name;
|
|
6033
5980
|
}
|
|
6034
|
-
const escaped = name
|
|
5981
|
+
const escaped = name
|
|
5982
|
+
.replaceAll('\\', '\\\\')
|
|
5983
|
+
.replaceAll('"', String.raw `\"`);
|
|
6035
5984
|
return `"${escaped}"`;
|
|
6036
5985
|
}
|
|
6037
5986
|
|
|
@@ -6216,10 +6165,11 @@ const FileViewer = class {
|
|
|
6216
6165
|
this.fileUrl = this.url;
|
|
6217
6166
|
};
|
|
6218
6167
|
this.handleToggleFullscreen = () => {
|
|
6219
|
-
if (this.fullscreen.isSupported()) {
|
|
6220
|
-
|
|
6221
|
-
this.isFullscreen = !this.isFullscreen;
|
|
6168
|
+
if (!this.fullscreen.isSupported()) {
|
|
6169
|
+
return;
|
|
6222
6170
|
}
|
|
6171
|
+
this.fullscreen.toggle();
|
|
6172
|
+
this.isFullscreen = !this.isFullscreen;
|
|
6223
6173
|
};
|
|
6224
6174
|
this.emitOnAction = (event) => {
|
|
6225
6175
|
event.stopPropagation();
|
|
@@ -6237,10 +6187,11 @@ const FileViewer = class {
|
|
|
6237
6187
|
});
|
|
6238
6188
|
}
|
|
6239
6189
|
connectedCallback() {
|
|
6240
|
-
if (this.revokeAnimationFrame
|
|
6241
|
-
|
|
6242
|
-
this.revokeAnimationFrame = undefined;
|
|
6190
|
+
if (this.revokeAnimationFrame === undefined) {
|
|
6191
|
+
return;
|
|
6243
6192
|
}
|
|
6193
|
+
cancelAnimationFrame(this.revokeAnimationFrame);
|
|
6194
|
+
this.revokeAnimationFrame = undefined;
|
|
6244
6195
|
}
|
|
6245
6196
|
async componentWillLoad() {
|
|
6246
6197
|
this.fileType = detectExtension(this.filename, this.url);
|
|
@@ -6278,10 +6229,11 @@ const FileViewer = class {
|
|
|
6278
6229
|
return fileViewerFunction();
|
|
6279
6230
|
}
|
|
6280
6231
|
revokePdfBlobUrl() {
|
|
6281
|
-
if (this.pdfBlobUrl) {
|
|
6282
|
-
|
|
6283
|
-
this.pdfBlobUrl = undefined;
|
|
6232
|
+
if (!this.pdfBlobUrl) {
|
|
6233
|
+
return;
|
|
6284
6234
|
}
|
|
6235
|
+
URL.revokeObjectURL(this.pdfBlobUrl);
|
|
6236
|
+
this.pdfBlobUrl = undefined;
|
|
6285
6237
|
}
|
|
6286
6238
|
sanitizeUrl(url) {
|
|
6287
6239
|
if (!(url === null || url === void 0 ? void 0 : url.trim())) {
|
|
@@ -6297,7 +6249,7 @@ const FileViewer = class {
|
|
|
6297
6249
|
}
|
|
6298
6250
|
}
|
|
6299
6251
|
getTranslation(key) {
|
|
6300
|
-
return translations.
|
|
6252
|
+
return translations.translations.get(key, this.language);
|
|
6301
6253
|
}
|
|
6302
6254
|
get HostElement() { return index$1.getElement(this); }
|
|
6303
6255
|
static get watchers() { return {
|