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