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