@rapidmx/web-client 0.15.1 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (318) hide show
  1. package/README.md +381 -373
  2. package/apps/admin/branding/index.tsx +39 -39
  3. package/apps/admin/data-requests/index.tsx +482 -480
  4. package/apps/admin/distribution-lists/new/index.tsx +3 -1
  5. package/apps/admin/domains/[uid].tsx +166 -164
  6. package/apps/admin/domains/new/index.tsx +3 -1
  7. package/apps/admin/escrow-scopes/[uid].tsx +350 -348
  8. package/apps/admin/escrow-scopes/new/index.tsx +3 -1
  9. package/apps/admin/index.tsx +129 -124
  10. package/apps/admin/mailboxes/[uid].tsx +271 -224
  11. package/apps/admin/mailboxes/new/index.tsx +30 -28
  12. package/apps/admin/plugins/index.tsx +15 -15
  13. package/apps/admin/retention-policy/index.tsx +39 -39
  14. package/apps/admin/signing-certificates/index.tsx +343 -343
  15. package/apps/admin/transport-rules/new/index.tsx +3 -1
  16. package/apps/escrow/audit-log/index.tsx +196 -196
  17. package/apps/escrow/matters/[uid].tsx +621 -621
  18. package/apps/escrow/matters/new/index.tsx +3 -1
  19. package/apps/shared/auth/adminAccess.ts +99 -99
  20. package/apps/shared/components/admin/layout/AdminShell.tsx +381 -378
  21. package/apps/shared/components/admin/mailboxes/EraseLeftoverDataDialog.tsx +238 -0
  22. package/apps/shared/components/admin/mailboxes/EscrowScopeCard.tsx +152 -152
  23. package/apps/shared/components/admin/mailboxes/LeftoverMailboxesSection.tsx +194 -0
  24. package/apps/shared/components/admin/settings/BrandingForm.tsx +423 -423
  25. package/apps/shared/components/admin/settings/EncryptionPolicyForm.tsx +119 -119
  26. package/apps/shared/components/admin/settings/MailboxCreateForm.tsx +50 -3
  27. package/apps/shared/components/admin/settings/MailboxPolicyForm.tsx +198 -198
  28. package/apps/shared/components/admin/settings/PluginsManager.tsx +1545 -1545
  29. package/apps/shared/components/admin/settings/RetentionPolicyForm.tsx +182 -182
  30. package/apps/shared/components/admin/setup/EscrowSetupStep.tsx +288 -288
  31. package/apps/shared/components/admin/setup/SetupWizard.tsx +446 -444
  32. package/apps/shared/components/admin/usePagedList.tsx +129 -129
  33. package/apps/shared/components/calendar/EventEditor.tsx +5 -10
  34. package/apps/shared/components/calendar/EventFormParts.tsx +16 -47
  35. package/apps/shared/components/calendar/EventModal.tsx +206 -206
  36. package/apps/shared/components/calendar/GuestInput.tsx +100 -0
  37. package/apps/shared/components/calendar/MonthView.tsx +185 -185
  38. package/apps/shared/components/calendar/RecurrenceEditor.tsx +227 -227
  39. package/apps/shared/components/calendar/RequestChangeForm.tsx +23 -54
  40. package/apps/shared/components/calendar/SplitDayView.tsx +144 -144
  41. package/apps/shared/components/calendar/TimeGridView.tsx +246 -246
  42. package/apps/shared/components/calendar/allDay.ts +124 -124
  43. package/apps/shared/components/calendar/eventForm.ts +2 -2
  44. package/apps/shared/components/calendar/eventFormat.ts +49 -22
  45. package/apps/shared/components/calendar/layout/CalendarShell.tsx +201 -200
  46. package/apps/shared/components/contacts/ContactForm.tsx +383 -383
  47. package/apps/shared/components/contacts/ContactsSidebar.tsx +1 -1
  48. package/apps/shared/components/contacts/ContactsToolbar.tsx +103 -98
  49. package/apps/shared/components/contacts/layout/ContactsShell.tsx +218 -216
  50. package/apps/shared/components/escrow/layout/EscrowShell.tsx +155 -155
  51. package/apps/shared/components/layout/AppShell.tsx +461 -480
  52. package/apps/shared/components/layout/FloatingActionButton.tsx +31 -0
  53. package/apps/shared/components/layout/KeyEnrollmentGate.tsx +398 -398
  54. package/apps/shared/components/layout/MailboxProvisioning.tsx +168 -168
  55. package/apps/shared/components/layout/ResponsiveToolbar.tsx +315 -315
  56. package/apps/shared/components/layout/ThemeSwitch.tsx +84 -84
  57. package/apps/shared/components/layout/UserMenu.tsx +439 -424
  58. package/apps/shared/components/mail/ConversationList.tsx +323 -323
  59. package/apps/shared/components/mail/ConversationThreadPane.tsx +613 -491
  60. package/apps/shared/components/mail/EmptyFolderBar.tsx +41 -0
  61. package/apps/shared/components/mail/MailSelectionBar.tsx +240 -227
  62. package/apps/shared/components/mail/MenuButton.tsx +31 -11
  63. package/apps/shared/components/mail/MessageDetailPane.tsx +1757 -1559
  64. package/apps/shared/components/mail/PermanentDeleteDialog.tsx +47 -0
  65. package/apps/shared/components/mail/compose/ComposeContext.tsx +312 -312
  66. package/apps/shared/components/mail/compose/ComposeToolbar.tsx +422 -422
  67. package/apps/shared/components/mail/compose/ComposeWindow.tsx +1681 -1677
  68. package/apps/shared/components/mail/compose/RecipientInput.tsx +54 -11
  69. package/apps/shared/components/mail/compose/RichTextEditor.tsx +147 -147
  70. package/apps/shared/components/mail/compose/composeFlushRegistry.ts +60 -60
  71. package/apps/shared/components/mail/compose/quotedBody.ts +161 -161
  72. package/apps/shared/components/mail/layout/FolderBadgeChip.tsx +21 -0
  73. package/apps/shared/components/mail/layout/MailShell.tsx +161 -134
  74. package/apps/shared/components/mail/layout/SidebarSection.tsx +66 -0
  75. package/apps/shared/components/mail/reading/MessageMoreMenu.tsx +258 -0
  76. package/apps/shared/components/mail/reading/MessageSourceDialog.tsx +79 -0
  77. package/apps/shared/components/mail/reading/PendingMessageCard.tsx +68 -0
  78. package/apps/shared/components/mail/reading/messageExport.ts +59 -0
  79. package/apps/shared/components/mail/reading/printMessage.ts +99 -0
  80. package/apps/shared/components/mail/reading/useMessageActions.ts +443 -0
  81. package/apps/shared/components/mail/verificationSeals.ts +125 -125
  82. package/apps/shared/components/rules/RuleBuilder.tsx +311 -311
  83. package/apps/shared/components/settings/SigningCertificateCard.tsx +344 -344
  84. package/apps/shared/components/settings/layout/SettingsShell.tsx +257 -254
  85. package/apps/shared/components/tasks/TasksSidebar.tsx +1 -1
  86. package/apps/shared/components/tasks/layout/TasksShell.tsx +219 -217
  87. package/apps/shared/keyboard/GlobalShortcuts.tsx +51 -51
  88. package/apps/shared/keyboard/ShortcutProvider.tsx +62 -62
  89. package/apps/shared/keyboard/ShortcutsDialog.tsx +84 -84
  90. package/apps/shared/keyboard/dispatch.ts +124 -124
  91. package/apps/shared/keyboard/format.ts +89 -89
  92. package/apps/shared/keyboard/keymap.ts +114 -114
  93. package/apps/shared/keyboard/registry.ts +65 -65
  94. package/apps/shared/keyboard/targets.ts +79 -79
  95. package/apps/shared/mail/folderCounts.ts +17 -0
  96. package/apps/shared/mail/folderOfType.ts +49 -0
  97. package/apps/shared/mail/folderTree.ts +143 -143
  98. package/apps/shared/mail/listAllPages.ts +39 -39
  99. package/apps/shared/mail/newMailNotifications.ts +171 -171
  100. package/apps/shared/mail/outbox/outgoingReplies.ts +192 -0
  101. package/apps/shared/mail/outbox/sendJob.ts +19 -1
  102. package/apps/shared/mail/outbox/sendOutcomes.ts +13 -4
  103. package/apps/shared/mail/permanentDelete.ts +160 -0
  104. package/apps/shared/mail/primaryMailbox.ts +71 -0
  105. package/apps/shared/mail/reportNotices.ts +66 -0
  106. package/apps/shared/mail/senderBlocking.ts +141 -0
  107. package/apps/shared/mail/useCollapsedSections.ts +146 -0
  108. package/apps/shared/mail/useMailConnection.ts +205 -203
  109. package/apps/shared/mail/useMailLiveUpdates.ts +277 -277
  110. package/apps/shared/mail/useMailboxUpdateAccess.ts +60 -0
  111. package/apps/shared/mail/useMarkMessageRead.ts +47 -47
  112. package/apps/shared/mail/useNewMailNotifications.ts +178 -178
  113. package/apps/shared/mail/usePermanentDelete.tsx +160 -0
  114. package/apps/shared/navigation/frameContext.tsx +1 -1
  115. package/apps/shared/navigation/index.ts +43 -0
  116. package/apps/shared/navigation/pageTitle.ts +19 -0
  117. package/apps/shared/notifications/store.ts +560 -560
  118. package/apps/shared/search/LocalIndexLifecycle.tsx +114 -114
  119. package/apps/shared/search/crossMailboxSearch.ts +169 -0
  120. package/apps/shared/search/localIndexBuilder.ts +481 -481
  121. package/apps/shared/signing/enrollmentStorage.ts +33 -33
  122. package/apps/shared/signing/enrollmentTracker.ts +385 -385
  123. package/apps/shared/signing/enrollmentView.ts +251 -251
  124. package/apps/shared/signing/useNow.ts +19 -19
  125. package/apps/shared/signing/useSigningEnrollmentWatcher.ts +90 -90
  126. package/apps/shared/styles/app.css +396 -386
  127. package/apps/www/_shell.tsx +82 -0
  128. package/apps/www/calendar/index.tsx +581 -571
  129. package/apps/www/contacts/[uid].tsx +112 -109
  130. package/apps/www/contacts/index.tsx +14 -3
  131. package/apps/www/index.tsx +2962 -2479
  132. package/apps/www/messages/[uid].tsx +139 -135
  133. package/apps/www/settings/appearance/index.tsx +5 -2
  134. package/apps/www/settings/auto-reply/index.tsx +136 -133
  135. package/apps/www/settings/blocked-senders/index.tsx +303 -0
  136. package/apps/www/settings/encryption/index.tsx +1290 -1287
  137. package/apps/www/settings/filters/[uid].tsx +179 -176
  138. package/apps/www/settings/filters/index.tsx +105 -102
  139. package/apps/www/settings/filters/new/index.tsx +165 -140
  140. package/apps/www/settings/labels/index.tsx +207 -204
  141. package/apps/www/settings/privacy/index.tsx +495 -492
  142. package/apps/www/settings/profile/index.tsx +251 -248
  143. package/apps/www/settings/read-receipts/index.tsx +150 -147
  144. package/apps/www/settings/sharing/index.tsx +259 -256
  145. package/apps/www/settings/signatures/[uid].tsx +175 -172
  146. package/apps/www/settings/signatures/index.tsx +91 -88
  147. package/apps/www/settings/signatures/new/index.tsx +138 -135
  148. package/apps/www/tasks/index.tsx +654 -649
  149. package/dist/apps/admin/data-requests/index.js +1 -1
  150. package/dist/apps/admin/distribution-lists/new/index.js +3 -1
  151. package/dist/apps/admin/domains/[uid].js +3 -1
  152. package/dist/apps/admin/domains/new/index.js +3 -1
  153. package/dist/apps/admin/escrow-scopes/[uid].js +3 -1
  154. package/dist/apps/admin/escrow-scopes/new/index.js +3 -1
  155. package/dist/apps/admin/index.js +5 -2
  156. package/dist/apps/admin/mailboxes/[uid].js +15 -3
  157. package/dist/apps/admin/mailboxes/new/index.js +3 -1
  158. package/dist/apps/admin/transport-rules/new/index.js +3 -1
  159. package/dist/apps/escrow/matters/new/index.js +3 -1
  160. package/dist/apps/shared/components/admin/layout/AdminShell.js +6 -3
  161. package/dist/apps/shared/components/admin/mailboxes/EraseLeftoverDataDialog.d.ts +35 -0
  162. package/dist/apps/shared/components/admin/mailboxes/EraseLeftoverDataDialog.js +115 -0
  163. package/dist/apps/shared/components/admin/mailboxes/LeftoverMailboxesSection.d.ts +16 -0
  164. package/dist/apps/shared/components/admin/mailboxes/LeftoverMailboxesSection.js +92 -0
  165. package/dist/apps/shared/components/admin/settings/BrandingForm.js +3 -3
  166. package/dist/apps/shared/components/admin/settings/MailboxCreateForm.js +25 -3
  167. package/dist/apps/shared/components/admin/setup/SetupWizard.js +3 -1
  168. package/dist/apps/shared/components/calendar/EventEditor.js +5 -10
  169. package/dist/apps/shared/components/calendar/EventFormParts.d.ts +3 -2
  170. package/dist/apps/shared/components/calendar/EventFormParts.js +5 -16
  171. package/dist/apps/shared/components/calendar/GuestInput.d.ts +45 -0
  172. package/dist/apps/shared/components/calendar/GuestInput.js +30 -0
  173. package/dist/apps/shared/components/calendar/RequestChangeForm.js +11 -22
  174. package/dist/apps/shared/components/calendar/eventForm.d.ts +2 -2
  175. package/dist/apps/shared/components/calendar/eventFormat.d.ts +21 -8
  176. package/dist/apps/shared/components/calendar/eventFormat.js +50 -23
  177. package/dist/apps/shared/components/calendar/layout/CalendarShell.js +5 -4
  178. package/dist/apps/shared/components/contacts/ContactsSidebar.js +1 -1
  179. package/dist/apps/shared/components/contacts/ContactsToolbar.d.ts +3 -1
  180. package/dist/apps/shared/components/contacts/ContactsToolbar.js +4 -2
  181. package/dist/apps/shared/components/contacts/layout/ContactsShell.js +7 -5
  182. package/dist/apps/shared/components/layout/AppShell.d.ts +8 -11
  183. package/dist/apps/shared/components/layout/AppShell.js +24 -33
  184. package/dist/apps/shared/components/layout/FloatingActionButton.d.ts +13 -0
  185. package/dist/apps/shared/components/layout/FloatingActionButton.js +9 -0
  186. package/dist/apps/shared/components/layout/UserMenu.d.ts +4 -1
  187. package/dist/apps/shared/components/layout/UserMenu.js +6 -5
  188. package/dist/apps/shared/components/mail/ConversationThreadPane.js +127 -22
  189. package/dist/apps/shared/components/mail/EmptyFolderBar.d.ts +18 -0
  190. package/dist/apps/shared/components/mail/EmptyFolderBar.js +10 -0
  191. package/dist/apps/shared/components/mail/MailSelectionBar.d.ts +14 -4
  192. package/dist/apps/shared/components/mail/MailSelectionBar.js +11 -13
  193. package/dist/apps/shared/components/mail/MenuButton.d.ts +9 -2
  194. package/dist/apps/shared/components/mail/MenuButton.js +15 -11
  195. package/dist/apps/shared/components/mail/MessageDetailPane.d.ts +5 -0
  196. package/dist/apps/shared/components/mail/MessageDetailPane.js +147 -7
  197. package/dist/apps/shared/components/mail/PermanentDeleteDialog.d.ts +19 -0
  198. package/dist/apps/shared/components/mail/PermanentDeleteDialog.js +11 -0
  199. package/dist/apps/shared/components/mail/compose/ComposeWindow.js +8 -4
  200. package/dist/apps/shared/components/mail/compose/RecipientInput.d.ts +21 -1
  201. package/dist/apps/shared/components/mail/compose/RecipientInput.js +18 -10
  202. package/dist/apps/shared/components/mail/layout/FolderBadgeChip.d.ts +6 -0
  203. package/dist/apps/shared/components/mail/layout/FolderBadgeChip.js +9 -0
  204. package/dist/apps/shared/components/mail/layout/MailShell.d.ts +16 -7
  205. package/dist/apps/shared/components/mail/layout/MailShell.js +78 -65
  206. package/dist/apps/shared/components/mail/layout/SidebarSection.d.ts +23 -0
  207. package/dist/apps/shared/components/mail/layout/SidebarSection.js +16 -0
  208. package/dist/apps/shared/components/mail/reading/MessageMoreMenu.d.ts +56 -0
  209. package/dist/apps/shared/components/mail/reading/MessageMoreMenu.js +153 -0
  210. package/dist/apps/shared/components/mail/reading/MessageSourceDialog.d.ts +27 -0
  211. package/dist/apps/shared/components/mail/reading/MessageSourceDialog.js +27 -0
  212. package/dist/apps/shared/components/mail/reading/PendingMessageCard.d.ts +14 -0
  213. package/dist/apps/shared/components/mail/reading/PendingMessageCard.js +16 -0
  214. package/dist/apps/shared/components/mail/reading/messageExport.d.ts +19 -0
  215. package/dist/apps/shared/components/mail/reading/messageExport.js +51 -0
  216. package/dist/apps/shared/components/mail/reading/printMessage.d.ts +30 -0
  217. package/dist/apps/shared/components/mail/reading/printMessage.js +70 -0
  218. package/dist/apps/shared/components/mail/reading/useMessageActions.d.ts +57 -0
  219. package/dist/apps/shared/components/mail/reading/useMessageActions.js +366 -0
  220. package/dist/apps/shared/components/settings/layout/SettingsShell.js +10 -7
  221. package/dist/apps/shared/components/tasks/TasksSidebar.js +1 -1
  222. package/dist/apps/shared/components/tasks/layout/TasksShell.js +7 -5
  223. package/dist/apps/shared/keyboard/GlobalShortcuts.js +1 -1
  224. package/dist/apps/shared/mail/folderCounts.d.ts +10 -0
  225. package/dist/apps/shared/mail/folderCounts.js +12 -0
  226. package/dist/apps/shared/mail/folderOfType.d.ts +13 -0
  227. package/dist/apps/shared/mail/folderOfType.js +40 -0
  228. package/dist/apps/shared/mail/outbox/outgoingReplies.d.ts +80 -0
  229. package/dist/apps/shared/mail/outbox/outgoingReplies.js +131 -0
  230. package/dist/apps/shared/mail/outbox/sendJob.d.ts +5 -1
  231. package/dist/apps/shared/mail/outbox/sendJob.js +14 -1
  232. package/dist/apps/shared/mail/outbox/sendOutcomes.js +13 -4
  233. package/dist/apps/shared/mail/permanentDelete.d.ts +62 -0
  234. package/dist/apps/shared/mail/permanentDelete.js +127 -0
  235. package/dist/apps/shared/mail/primaryMailbox.d.ts +29 -0
  236. package/dist/apps/shared/mail/primaryMailbox.js +61 -0
  237. package/dist/apps/shared/mail/reportNotices.d.ts +21 -0
  238. package/dist/apps/shared/mail/reportNotices.js +47 -0
  239. package/dist/apps/shared/mail/senderBlocking.d.ts +26 -0
  240. package/dist/apps/shared/mail/senderBlocking.js +92 -0
  241. package/dist/apps/shared/mail/useCollapsedSections.d.ts +32 -0
  242. package/dist/apps/shared/mail/useCollapsedSections.js +110 -0
  243. package/dist/apps/shared/mail/useMailConnection.js +3 -1
  244. package/dist/apps/shared/mail/useMailboxUpdateAccess.d.ts +11 -0
  245. package/dist/apps/shared/mail/useMailboxUpdateAccess.js +52 -0
  246. package/dist/apps/shared/mail/usePermanentDelete.d.ts +35 -0
  247. package/dist/apps/shared/mail/usePermanentDelete.js +96 -0
  248. package/dist/apps/shared/navigation/frameContext.d.ts +1 -1
  249. package/dist/apps/shared/navigation/index.d.ts +16 -0
  250. package/dist/apps/shared/navigation/index.js +34 -0
  251. package/dist/apps/shared/navigation/pageTitle.d.ts +11 -0
  252. package/dist/apps/shared/navigation/pageTitle.js +12 -0
  253. package/dist/apps/shared/search/crossMailboxSearch.d.ts +72 -0
  254. package/dist/apps/shared/search/crossMailboxSearch.js +136 -0
  255. package/dist/apps/shared/styles/app.css +396 -386
  256. package/dist/apps/www/_shell.d.ts +26 -0
  257. package/dist/apps/www/_shell.js +31 -0
  258. package/dist/apps/www/calendar/index.d.ts +7 -2
  259. package/dist/apps/www/calendar/index.js +7 -4
  260. package/dist/apps/www/contacts/[uid].d.ts +14 -3
  261. package/dist/apps/www/contacts/[uid].js +5 -3
  262. package/dist/apps/www/contacts/index.d.ts +7 -2
  263. package/dist/apps/www/contacts/index.js +8 -4
  264. package/dist/apps/www/index.d.ts +7 -2
  265. package/dist/apps/www/index.js +542 -207
  266. package/dist/apps/www/messages/[uid].d.ts +14 -3
  267. package/dist/apps/www/messages/[uid].js +7 -5
  268. package/dist/apps/www/settings/appearance/index.d.ts +8 -2
  269. package/dist/apps/www/settings/appearance/index.js +4 -2
  270. package/dist/apps/www/settings/auto-reply/index.d.ts +7 -2
  271. package/dist/apps/www/settings/auto-reply/index.js +4 -2
  272. package/dist/apps/www/settings/blocked-senders/index.d.ts +9 -0
  273. package/dist/apps/www/settings/blocked-senders/index.js +134 -0
  274. package/dist/apps/www/settings/encryption/index.d.ts +7 -2
  275. package/dist/apps/www/settings/encryption/index.js +4 -2
  276. package/dist/apps/www/settings/filters/[uid].d.ts +7 -2
  277. package/dist/apps/www/settings/filters/[uid].js +4 -2
  278. package/dist/apps/www/settings/filters/index.d.ts +7 -2
  279. package/dist/apps/www/settings/filters/index.js +4 -2
  280. package/dist/apps/www/settings/filters/new/index.d.ts +7 -2
  281. package/dist/apps/www/settings/filters/new/index.js +28 -5
  282. package/dist/apps/www/settings/labels/index.d.ts +7 -2
  283. package/dist/apps/www/settings/labels/index.js +4 -2
  284. package/dist/apps/www/settings/privacy/index.d.ts +7 -2
  285. package/dist/apps/www/settings/privacy/index.js +4 -2
  286. package/dist/apps/www/settings/profile/index.d.ts +7 -2
  287. package/dist/apps/www/settings/profile/index.js +4 -2
  288. package/dist/apps/www/settings/read-receipts/index.d.ts +7 -2
  289. package/dist/apps/www/settings/read-receipts/index.js +4 -2
  290. package/dist/apps/www/settings/sharing/index.d.ts +7 -2
  291. package/dist/apps/www/settings/sharing/index.js +4 -2
  292. package/dist/apps/www/settings/signatures/[uid].d.ts +7 -2
  293. package/dist/apps/www/settings/signatures/[uid].js +4 -2
  294. package/dist/apps/www/settings/signatures/index.d.ts +7 -2
  295. package/dist/apps/www/settings/signatures/index.js +4 -2
  296. package/dist/apps/www/settings/signatures/new/index.d.ts +7 -2
  297. package/dist/apps/www/settings/signatures/new/index.js +5 -3
  298. package/dist/apps/www/tasks/index.d.ts +7 -2
  299. package/dist/apps/www/tasks/index.js +5 -3
  300. package/package.json +4 -2
  301. package/apps/shared/navigation/AppRouter.tsx +0 -300
  302. package/apps/shared/navigation/idle.ts +0 -45
  303. package/apps/shared/navigation/routerContext.tsx +0 -83
  304. package/apps/shared/navigation/routes.ts +0 -72
  305. package/apps/www/_routedPage.tsx +0 -24
  306. package/apps/www/_routes.ts +0 -37
  307. package/dist/apps/shared/navigation/AppRouter.d.ts +0 -52
  308. package/dist/apps/shared/navigation/AppRouter.js +0 -242
  309. package/dist/apps/shared/navigation/idle.d.ts +0 -14
  310. package/dist/apps/shared/navigation/idle.js +0 -43
  311. package/dist/apps/shared/navigation/routerContext.d.ts +0 -37
  312. package/dist/apps/shared/navigation/routerContext.js +0 -56
  313. package/dist/apps/shared/navigation/routes.d.ts +0 -32
  314. package/dist/apps/shared/navigation/routes.js +0 -37
  315. package/dist/apps/www/_routedPage.d.ts +0 -12
  316. package/dist/apps/www/_routedPage.js +0 -19
  317. package/dist/apps/www/_routes.d.ts +0 -11
  318. package/dist/apps/www/_routes.js +0 -31
@@ -0,0 +1,258 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ import React from "react";
6
+ import {
7
+ HiOutlineArrowDownTray,
8
+ HiOutlineArrowUturnLeft,
9
+ HiOutlineArrowUturnRight,
10
+ HiOutlineCodeBracket,
11
+ HiOutlineCog6Tooth,
12
+ HiOutlineDocumentText,
13
+ HiOutlineEllipsisHorizontal,
14
+ HiOutlineEnvelope,
15
+ HiOutlineEnvelopeOpen,
16
+ HiOutlineEye,
17
+ HiOutlineFlag,
18
+ HiOutlineFunnel,
19
+ HiOutlineHandRaised,
20
+ HiOutlineInboxArrowDown,
21
+ HiOutlineInformationCircle,
22
+ HiOutlineNoSymbol,
23
+ HiOutlinePrinter,
24
+ HiOutlineShieldCheck,
25
+ HiOutlineShieldExclamation,
26
+ HiOutlineTrash,
27
+ } from "react-icons/hi2";
28
+ import MenuButton, { MenuItemSpec, MenuSectionSpec } from "../MenuButton.js";
29
+
30
+ /** What each row of the menu does - the reading pane's own handlers. */
31
+ export interface MessageMenuActions {
32
+ replyAll: () => void;
33
+ forward: () => void;
34
+ deleteMessage: () => void;
35
+ toggleRead: () => void;
36
+ toggleFlag: () => void;
37
+ reportJunk: () => void;
38
+ reportPhishing: () => void;
39
+ notJunk: () => void;
40
+ notJunkAndTrust: () => void;
41
+ blockSender: () => void;
42
+ neverBlockSender: () => void;
43
+ print: () => void;
44
+ viewSource: () => void;
45
+ viewDetails: () => void;
46
+ saveAsEml: () => void;
47
+ createRule: () => void;
48
+ }
49
+
50
+ export interface MessageMoreMenuProps {
51
+ actions: MessageMenuActions;
52
+ /** The sender's address the Block rows name. */
53
+ senderAddress: string;
54
+ read: boolean;
55
+ flagged: boolean;
56
+ /** The reader may change this mailbox's messages and filters (it is not a view-only share). */
57
+ writable: boolean;
58
+ /** Something is already being done to the message: its rows wait. */
59
+ busy: boolean;
60
+ inJunk: boolean;
61
+ /** The message is in Deleted Items: the Delete row reads "Delete permanently". */
62
+ inDeletedItems: boolean;
63
+ /** Mail the reader sent: it is not junk, and its sender is the reader. */
64
+ sent: boolean;
65
+ /** The sender is one of the reader's own addresses, which is not blocked. */
66
+ ownSender: boolean;
67
+ /** The reader may add a sender to this mailbox's Safe Senders: they have full access, or the server has not said which access they have (it then refuses, with its reason). */
68
+ canTrustSender: boolean;
69
+ /** Why the message cannot be printed right now (an encrypted one that is locked or still opening), or `undefined`. */
70
+ printReason: string | undefined;
71
+ /** The compose window is opening for a reply or forward: Reply all and Forward wait for it. */
72
+ composing: boolean;
73
+ /** The classes of the round "..." button - the reading pane's own icon-button look. */
74
+ triggerClassName: string;
75
+ }
76
+
77
+ const VIEW_ONLY = "View-only mailbox";
78
+
79
+ /** A row, disabled with `reason` (shown under the label, where a keyboard or phone user sees it as well) when there is one. */
80
+ function row(key: string, label: string, icon: React.ReactNode, onSelect: () => void, reason?: string, extra: Partial<MenuItemSpec> = {}): MenuItemSpec {
81
+ return { key, label, icon, onSelect, disabled: reason !== undefined, description: reason, ...extra } as MenuItemSpec;
82
+ }
83
+
84
+ /** The sections of the menu - Outlook's "More actions", cut to what this product really does. */
85
+ export function buildMessageMenu(props: MessageMoreMenuProps): MenuSectionSpec[] {
86
+ const { actions, read, flagged, writable, busy, inJunk, inDeletedItems, sent, ownSender, printReason, composing } = props;
87
+ // The reason a row that changes the message is unavailable, if it is: the first that applies wins.
88
+ const changeReason = !writable ? VIEW_ONLY : undefined;
89
+ const junkReason = changeReason ?? (sent ? "Not available for mail you sent" : inJunk ? "Already in Junk Email" : undefined);
90
+ const blockReason = changeReason ?? (sent ? "Not available for mail you sent" : ownSender ? "This is your own address" : undefined);
91
+ const waiting = (reason: string | undefined) => reason ?? (busy ? "Working on this message" : undefined);
92
+ const trustLabel = `Not junk, and always trust ${props.senderAddress}`;
93
+ const blockLabel = `Block ${props.senderAddress}`;
94
+ const neverBlockLabel = `Never block ${props.senderAddress}`;
95
+ return [
96
+ {
97
+ key: "message",
98
+ items: [
99
+ {
100
+ key: "reply-actions",
101
+ label: "Other reply actions",
102
+ icon: <HiOutlineArrowUturnLeft size={16} />,
103
+ submenu: [
104
+ {
105
+ key: "reply-actions-items",
106
+ items: [
107
+ row("reply-all", "Reply all", <HiOutlineArrowUturnLeft size={16} />, actions.replyAll, composing ? "Opening the compose window" : undefined),
108
+ row("forward", "Forward", <HiOutlineArrowUturnRight size={16} />, actions.forward, composing ? "Opening the compose window" : undefined),
109
+ ],
110
+ },
111
+ ],
112
+ },
113
+ row(
114
+ "delete",
115
+ // In Deleted Items there is nowhere further to move it to: Delete is the permanent one, and asks first.
116
+ inDeletedItems ? "Delete permanently" : "Delete",
117
+ <HiOutlineTrash size={16} />,
118
+ actions.deleteMessage,
119
+ waiting(changeReason),
120
+ ),
121
+ row(
122
+ "toggle-read",
123
+ read ? "Mark as unread" : "Mark as read",
124
+ read ? <HiOutlineEnvelope size={16} /> : <HiOutlineEnvelopeOpen size={16} />,
125
+ actions.toggleRead,
126
+ waiting(changeReason),
127
+ ),
128
+ row("toggle-flag", flagged ? "Unflag" : "Flag", <HiOutlineFlag size={16} />, actions.toggleFlag, waiting(changeReason)),
129
+ ],
130
+ },
131
+ {
132
+ key: "sender",
133
+ items: [
134
+ {
135
+ key: "report",
136
+ label: "Report",
137
+ icon: <HiOutlineNoSymbol size={16} />,
138
+ submenu: [
139
+ {
140
+ key: "report-items",
141
+ items: [
142
+ row("report-junk", "Report junk", <HiOutlineNoSymbol size={16} />, actions.reportJunk, waiting(junkReason)),
143
+ row("report-phishing", "Report phishing", <HiOutlineShieldExclamation size={16} />, actions.reportPhishing, waiting(junkReason)),
144
+ // In Junk Email the way back: to the Inbox, telling the spam filter it was wrong - and, when the reader may, trusting the sender.
145
+ ...(inJunk
146
+ ? [
147
+ row("not-junk", "Not junk", <HiOutlineInboxArrowDown size={16} />, actions.notJunk, waiting(changeReason)),
148
+ ...(props.canTrustSender && !ownSender
149
+ ? [
150
+ row("not-junk-trust", trustLabel, <HiOutlineShieldCheck size={16} />, actions.notJunkAndTrust, waiting(changeReason), {
151
+ title: trustLabel,
152
+ }),
153
+ ]
154
+ : []),
155
+ ]
156
+ : []),
157
+ ],
158
+ },
159
+ ],
160
+ },
161
+ {
162
+ key: "block",
163
+ label: "Block",
164
+ icon: <HiOutlineHandRaised size={16} />,
165
+ submenu: [
166
+ {
167
+ key: "block-items",
168
+ items: [
169
+ row("block-sender", blockLabel, <HiOutlineHandRaised size={16} />, actions.blockSender, waiting(blockReason), { title: blockLabel }),
170
+ row(
171
+ "never-block-sender",
172
+ neverBlockLabel,
173
+ <HiOutlineShieldCheck size={16} />,
174
+ actions.neverBlockSender,
175
+ waiting(blockReason),
176
+ { title: neverBlockLabel },
177
+ ),
178
+ ],
179
+ },
180
+ ],
181
+ },
182
+ ],
183
+ },
184
+ {
185
+ key: "output",
186
+ items: [
187
+ row("print", "Print", <HiOutlinePrinter size={16} />, actions.print, printReason),
188
+ {
189
+ key: "view",
190
+ label: "View",
191
+ icon: <HiOutlineEye size={16} />,
192
+ submenu: [
193
+ {
194
+ key: "view-items",
195
+ items: [
196
+ row("view-source", "View message source", <HiOutlineCodeBracket size={16} />, actions.viewSource),
197
+ row("view-details", "Message details", <HiOutlineInformationCircle size={16} />, actions.viewDetails),
198
+ ],
199
+ },
200
+ ],
201
+ },
202
+ {
203
+ key: "save-as",
204
+ label: "Save as",
205
+ icon: <HiOutlineArrowDownTray size={16} />,
206
+ submenu: [
207
+ {
208
+ key: "save-as-items",
209
+ items: [
210
+ row("save-eml", "Save as .eml", <HiOutlineEnvelope size={16} />, actions.saveAsEml),
211
+ row("save-pdf", "Save as PDF", <HiOutlineDocumentText size={16} />, actions.print, printReason, {
212
+ description: printReason ?? "Opens the print dialog: choose Save as PDF",
213
+ }),
214
+ ],
215
+ },
216
+ ],
217
+ },
218
+ ],
219
+ },
220
+ {
221
+ key: "advanced",
222
+ items: [
223
+ {
224
+ key: "advanced-actions",
225
+ label: "Advanced actions",
226
+ icon: <HiOutlineCog6Tooth size={16} />,
227
+ submenu: [
228
+ {
229
+ key: "advanced-items",
230
+ items: [row("create-rule", "Create rule", <HiOutlineFunnel size={16} />, actions.createRule, changeReason)],
231
+ },
232
+ ],
233
+ },
234
+ ],
235
+ },
236
+ ];
237
+ }
238
+
239
+ /**
240
+ * The "..." at the end of a message card's icon row: Outlook's More actions menu, holding the actions there is no room for beside the
241
+ * message and the ones that are less often wanted. An ARIA menu (`MenuButton`): arrows, Home and End move, Right or Enter opens a submenu, Left or
242
+ * Escape leaves it (Escape at the top closes the menu and the focus goes back to this button), and a click outside closes it. A row that cannot be used right now
243
+ * is disabled and says why under its label.
244
+ */
245
+ export default function MessageMoreMenu(props: MessageMoreMenuProps) {
246
+ return (
247
+ <MenuButton
248
+ iconOnly
249
+ label="More actions"
250
+ aria-label="More actions"
251
+ title="More actions"
252
+ icon={<HiOutlineEllipsisHorizontal size={16} aria-hidden="true" />}
253
+ className={props.triggerClassName}
254
+ sections={buildMessageMenu(props)}
255
+ width={288}
256
+ />
257
+ );
258
+ }
@@ -0,0 +1,79 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ import React, { useMemo } from "react";
6
+ import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
7
+ import CopyButton from "@rapidmx/react-shared/components/buttons/CopyButton.js";
8
+ import Drawer from "@rapidmx/react-shared/components/overlays/Drawer.js";
9
+ import { MAX_SOURCE_DISPLAY_CHARS, headerText, sourceText } from "./messageExport.js";
10
+
11
+ /** What the dialog is showing a message's source for: the whole of it, or only its header lines ("Message details"). */
12
+ export type SourceMode = "source" | "headers";
13
+
14
+ /** What has been fetched of the raw message: on its way, here, or refused. */
15
+ export type SourceState = { status: "loading" } | { status: "ready"; raw: string } | { status: "error"; message: string };
16
+
17
+ export interface MessageSourceDialogProps {
18
+ open: boolean;
19
+ onClose: () => void;
20
+ mode: SourceMode;
21
+ source: SourceState;
22
+ /** The message is end-to-end encrypted: what is shown is the ciphertext the server holds, and the dialog says so. */
23
+ encrypted: boolean;
24
+ }
25
+
26
+ const TITLES: Record<SourceMode, string> = { source: "Message source", headers: "Message details" };
27
+
28
+ /**
29
+ * A message's raw source (or just its headers) in a full-window dialog, in a monospace block that can be scrolled and selected, with a Copy
30
+ * button. It is the RFC 5322 text exactly as the server stores it - for an encrypted message that is the encrypted message, never a
31
+ * decrypted one, and the dialog says so. A very long source is cut for display (`MAX_SOURCE_DISPLAY_CHARS`); Copy has all of it.
32
+ */
33
+ export default function MessageSourceDialog({ open, onClose, mode, source, encrypted }: MessageSourceDialogProps) {
34
+ const text = useMemo(() => {
35
+ if (source.status !== "ready") {
36
+ return "";
37
+ }
38
+ return mode === "headers" ? headerText(source.raw) : sourceText(source.raw);
39
+ }, [source, mode]);
40
+ const cut = text.length > MAX_SOURCE_DISPLAY_CHARS;
41
+ const title = TITLES[mode];
42
+ return (
43
+ <Drawer open={open} onClose={onClose} title={title} fullScreen>
44
+ <div className="flex flex-col gap-3 text-sm">
45
+ {encrypted && (
46
+ <p role="note" className="py-2 px-3 rounded-sm bg-surface-alt text-text">
47
+ This message is encrypted. This is the encrypted message exactly as the server stores it; it is not decrypted here.
48
+ </p>
49
+ )}
50
+ {source.status === "loading" && (
51
+ <p role="status" className="text-text-muted">
52
+ Loading the message&hellip;
53
+ </p>
54
+ )}
55
+ {source.status === "error" && <Alert>{source.message}</Alert>}
56
+ {source.status === "ready" && (
57
+ <>
58
+ <div className="flex flex-wrap items-center gap-3">
59
+ <CopyButton value={text} label={`Copy the ${mode === "headers" ? "message headers" : "message source"}`} />
60
+ {cut && (
61
+ <span className="text-xs text-text-muted">
62
+ Showing the first {MAX_SOURCE_DISPLAY_CHARS.toLocaleString()} of {text.length.toLocaleString()} characters. Copy and Save as
63
+ .eml keep all of it.
64
+ </span>
65
+ )}
66
+ </div>
67
+ <pre
68
+ tabIndex={0}
69
+ aria-label={title}
70
+ className="m-0 max-h-[70vh] overflow-auto rounded-sm border border-border bg-surface-alt p-3 font-mono text-xs whitespace-pre-wrap break-all"
71
+ >
72
+ {cut ? text.slice(0, MAX_SOURCE_DISPLAY_CHARS) : text}
73
+ </pre>
74
+ </>
75
+ )}
76
+ </div>
77
+ </Drawer>
78
+ );
79
+ }
@@ -0,0 +1,68 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ import React from "react";
6
+ import Button from "@rapidmx/react-shared/components/buttons/Button.js";
7
+ import type { OutgoingReply } from "../../../mail/outbox/outgoingReplies.js";
8
+ import MailAddress, { RecipientLine } from "../MailAddress.js";
9
+ import MessageBody from "./MessageBody.js";
10
+ import { BODY_FONT_STYLE, CardShell, SenderAvatar } from "./MessageCard.js";
11
+
12
+ /**
13
+ * A reply or forward that has just been sent, drawn where its message will be once the server has filed it: the sender, who it went to, its body as
14
+ * it was composed, and one line saying where it is - "Sending...", "Sent", or, when it did not go, "Not sent: <why>" with the same ways out the
15
+ * pop-up offers (Retry, Open draft ...). It reads like the message's own card, so the real message replaces it without the thread rearranging.
16
+ *
17
+ * The body goes through the reading pane's own sanitizing frame (`MessageBody`), exactly as a received message's does.
18
+ */
19
+ export default function PendingMessageCard({
20
+ reply,
21
+ headerRef,
22
+ }: {
23
+ reply: OutgoingReply;
24
+ /** Registered so the pane can put the focus here when the message first appears. Focusable, but not in the tab order. */
25
+ headerRef: (node: HTMLHeadingElement | null) => void;
26
+ }) {
27
+ const failed = reply.state === "failed";
28
+ return (
29
+ <CardShell>
30
+ <div className="flex items-center gap-3 px-4 py-3">
31
+ <SenderAvatar from={reply.sender} />
32
+ <div className="flex-1 min-w-0">
33
+ <div className="flex flex-wrap items-baseline justify-between gap-x-2 text-sm">
34
+ <h2 ref={headerRef} tabIndex={-1} style={BODY_FONT_STYLE} className="flex-1 min-w-[13rem] font-semibold outline-none focus-visible:outline-2 focus-visible:outline-primary">
35
+ <MailAddress recipient={reply.sender} />
36
+ </h2>
37
+ <div
38
+ // Its own element for each: an alert is announced when it appears, which a status that changed its role would not be.
39
+ key={failed ? "failed" : "progress"}
40
+ role={failed ? "alert" : "status"}
41
+ data-testid="pending-message-status"
42
+ data-state={reply.state}
43
+ className={["flex items-center gap-1.5 text-xs shrink-0", failed ? "font-semibold text-danger" : "text-text-muted"].join(" ")}
44
+ >
45
+ {reply.state === "sending" && <span aria-hidden="true" className="rr-sending-dot h-1.5 w-1.5 shrink-0 rounded-full bg-primary" />}
46
+ <span>{failed ? `Not sent: ${reply.failure}` : reply.state === "sending" ? "Sending…" : "Sent"}</span>
47
+ </div>
48
+ </div>
49
+ <RecipientLine label="To" recipients={reply.to} />
50
+ <RecipientLine label="Cc" recipients={reply.cc} />
51
+ <RecipientLine label="Bcc" recipients={reply.bcc} />
52
+ </div>
53
+ </div>
54
+ {failed && (
55
+ <div className="flex flex-wrap gap-2 px-4 pb-3">
56
+ {reply.actions.map((action) => (
57
+ <Button key={action.label} type="button" variant="secondary" className="!w-auto" onClick={action.onClick}>
58
+ {action.label}
59
+ </Button>
60
+ ))}
61
+ </div>
62
+ )}
63
+ <div className="px-4 pb-4">
64
+ <MessageBody messageUid={reply.uid} messageVersion={0} title={`Message: ${reply.subject}`} content={{ kind: "html", html: reply.html }} />
65
+ </div>
66
+ </CardShell>
67
+ );
68
+ }
@@ -0,0 +1,59 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ import { binaryStringToBytes, extractAddresses, parseMimeEntity } from "@rapidmx/react-shared/crypto/mime.js";
6
+
7
+ /**
8
+ * What the reading pane does with a message's raw RFC 5322 source (`getMessageRawContent()`, a binary string): show it, save it as a
9
+ * file, and read the From header out of it. Nothing here interprets the message's content beyond its header block.
10
+ */
11
+
12
+ /** How much of a message's source the "View message source" dialog draws: a message with attachments is mostly base64, and a
13
+ * megabyte of it in a `<pre>` helps nobody. Saving as .eml and Copy always have all of it. */
14
+ export const MAX_SOURCE_DISPLAY_CHARS = 200_000;
15
+
16
+ /** The file name "Save as .eml" gives a message: its subject without what a file system refuses, or `message`. */
17
+ export function emlFileName(subject: string | undefined): string {
18
+ const cleaned = (subject ?? "")
19
+ .replace(/[\p{Cc}\\/:*?"<>|]+/gu, " ")
20
+ .replace(/\s+/g, " ")
21
+ .trim()
22
+ .slice(0, 100)
23
+ .replace(/^\.+/, "")
24
+ .replace(/[. ]+$/, "");
25
+ return `${cleaned || "message"}.eml`;
26
+ }
27
+
28
+ /** Hands `bytes` to the browser as a download called `fileName`. */
29
+ export function downloadBytes(bytes: Uint8Array, fileName: string, type: string): void {
30
+ const url = URL.createObjectURL(new Blob([bytes as BlobPart], { type }));
31
+ const a = document.createElement("a");
32
+ a.href = url;
33
+ a.download = fileName;
34
+ document.body.appendChild(a);
35
+ a.click();
36
+ a.remove();
37
+ // Some browsers are still reading the blob after `click()` returns.
38
+ setTimeout(() => URL.revokeObjectURL(url), 60_000);
39
+ }
40
+
41
+ /** Saves a raw message (a binary string, exactly as the server has it) as a `.eml` file, byte for byte. */
42
+ export function saveAsEml(raw: string, subject: string | undefined): void {
43
+ downloadBytes(binaryStringToBytes(raw), emlFileName(subject), "message/rfc822");
44
+ }
45
+
46
+ /** A raw message as text to read: its bytes decoded as UTF-8, an invalid byte shown as U+FFFD. */
47
+ export function sourceText(raw: string): string {
48
+ return new TextDecoder("utf-8").decode(binaryStringToBytes(raw));
49
+ }
50
+
51
+ /** The header block of a raw message, as text to read. */
52
+ export function headerText(raw: string): string {
53
+ return sourceText(parseMimeEntity(raw).rawHeaderBlock);
54
+ }
55
+
56
+ /** The address in a raw message's own From header (which is not always the envelope sender the server files the message under), lowercased. */
57
+ export function headerFromAddress(raw: string): string | undefined {
58
+ return extractAddresses(parseMimeEntity(raw).headers.from)[0];
59
+ }
@@ -0,0 +1,99 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ import type { Attachment } from "@rapidmx/react-shared/mail/mailApi.js";
6
+ import type { MimeAttachment } from "@rapidmx/react-shared/crypto/mime.js";
7
+ import { BodyContent, attachmentSource, makeCidResolver } from "./bodyContent.js";
8
+ import { prepareBodyHtml } from "./bodyHtml.js";
9
+ import { frameCsp } from "./frameDocument.js";
10
+
11
+ /**
12
+ * Printing one message: a document of its own - the message's header lines, and under them the same sanitized body the reading pane draws -
13
+ * shown in a frame that nobody sees and printed from there, so the print is the message and not the app around it.
14
+ *
15
+ * The frame is as isolated as the reading pane's own (`frameDocument.ts`): the body has been through `prepareBodyHtml()`, the document
16
+ * carries the same Content-Security-Policy (no script, nothing fetched from anywhere but this server's own attachment URLs), and the
17
+ * sandbox runs no code. `allow-same-origin` only lets the app call `print()` on it, and `allow-modals` is what a browser asks of a
18
+ * sandboxed document for the print dialog to open at all. Paper is white, so the message is drawn as its author wrote it.
19
+ */
20
+
21
+ export interface PrintableMessage {
22
+ subject: string;
23
+ /** The header lines above the body - `From`, `To`, `Cc`, `Date` - in the order shown; a line with no value is left out. */
24
+ headers: { name: string; value: string }[];
25
+ content: BodyContent;
26
+ attachments?: Attachment[];
27
+ inlineParts?: MimeAttachment[];
28
+ }
29
+
30
+ const PRINT_CSS = `
31
+ html{font:14px/1.5 system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;color:#000;background:#fff}
32
+ body{margin:0;padding:24px}
33
+ h1{font-size:20px;margin:0 0 12px}
34
+ dl{margin:0 0 12px;display:grid;grid-template-columns:max-content 1fr;gap:2px 12px}
35
+ dt{font-weight:600;color:#444}
36
+ dd{margin:0;overflow-wrap:anywhere}
37
+ hr{border:0;border-top:1px solid #999;margin:0 0 16px}
38
+ #rr-body{overflow-wrap:break-word}
39
+ #rr-body img{max-width:100%;height:auto}
40
+ #rr-body pre,pre.rr-text{white-space:pre-wrap;font:inherit;margin:0}
41
+ `;
42
+
43
+ export function escapeHtml(text: string): string {
44
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
45
+ }
46
+
47
+ /**
48
+ * The document to print, or `undefined` for a body too large to prepare (the pane does not draw it either).
49
+ */
50
+ export function buildPrintDocument(message: PrintableMessage): string | undefined {
51
+ let body: string;
52
+ let imageSource: string | undefined;
53
+ if (message.content.kind === "html") {
54
+ const prepared = prepareBodyHtml(message.content.html, { resolveCid: makeCidResolver(message.attachments, message.inlineParts) });
55
+ if (prepared.status === "too_large") {
56
+ return undefined;
57
+ }
58
+ const source = prepared.inlineImages > 0 ? attachmentSource() : undefined;
59
+ imageSource = source && prepared.html.includes(source) ? source : undefined;
60
+ body = `<div id="rr-body">${prepared.html}</div>`;
61
+ } else {
62
+ body = `<pre class="rr-text">${escapeHtml(message.content.text)}</pre>`;
63
+ }
64
+ const lines = message.headers
65
+ .filter((header) => header.value !== "")
66
+ .map((header) => `<dt>${escapeHtml(header.name)}</dt><dd>${escapeHtml(header.value)}</dd>`)
67
+ .join("");
68
+ return [
69
+ "<!doctype html>",
70
+ '<html lang="en"><head><meta charset="utf-8">',
71
+ `<meta http-equiv="Content-Security-Policy" content="${frameCsp(imageSource)}">`,
72
+ `<title>${escapeHtml(message.subject)}</title>`,
73
+ `<style>${PRINT_CSS}</style></head>`,
74
+ `<body><h1>${escapeHtml(message.subject)}</h1><dl>${lines}</dl><hr>${body}</body></html>`,
75
+ ].join("");
76
+ }
77
+
78
+ /** How long a print frame is kept when the browser never says it has finished with it (`afterprint`). */
79
+ const FRAME_LIFETIME_MS = 60_000;
80
+
81
+ /** Prints `html` (from `buildPrintDocument()`) from a frame nobody sees, and removes the frame when the print is over. */
82
+ export function printDocument(html: string): void {
83
+ const frame = document.createElement("iframe");
84
+ frame.setAttribute("sandbox", "allow-same-origin allow-modals");
85
+ frame.setAttribute("aria-hidden", "true");
86
+ frame.tabIndex = -1;
87
+ frame.title = "Message to print";
88
+ frame.style.cssText = "position:fixed;right:0;bottom:0;width:0;height:0;border:0;visibility:hidden";
89
+ const cleanup = () => frame.remove();
90
+ frame.addEventListener("load", () => {
91
+ const view = frame.contentWindow!;
92
+ view.addEventListener("afterprint", cleanup);
93
+ view.focus();
94
+ view.print();
95
+ setTimeout(cleanup, FRAME_LIFETIME_MS);
96
+ });
97
+ frame.srcdoc = html;
98
+ document.body.appendChild(frame);
99
+ }