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