@rapidmx/web-client 0.9.0 → 0.11.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 (443) hide show
  1. package/README.md +373 -88
  2. package/apps/admin/_layout.tsx +8 -2
  3. package/apps/admin/index.tsx +7 -1
  4. package/apps/admin/ingest-queue/index.tsx +2 -1
  5. package/apps/admin/mailboxes/[uid].tsx +23 -10
  6. package/apps/admin/quarantine/index.tsx +2 -1
  7. package/apps/admin/signing-certificates/index.tsx +343 -0
  8. package/apps/escrow/_layout.tsx +8 -2
  9. package/apps/shared/appearance/AppearanceForm.tsx +392 -0
  10. package/apps/shared/appearance/AppearanceHead.tsx +49 -0
  11. package/apps/shared/appearance/AppearanceProvider.tsx +470 -0
  12. package/apps/shared/appearance/ColorField.tsx +107 -0
  13. package/apps/shared/appearance/appearanceCache.ts +106 -0
  14. package/apps/shared/appearance/appearanceContext.ts +77 -0
  15. package/apps/shared/appearance/bootScript.ts +35 -0
  16. package/apps/shared/appearance/color.ts +169 -0
  17. package/apps/shared/appearance/photo.ts +88 -0
  18. package/apps/shared/appearance/resolvedTheme.ts +67 -0
  19. package/apps/shared/appearance/theme.ts +355 -0
  20. package/apps/shared/auth/accountUrl.ts +9 -0
  21. package/apps/shared/auth/adminAccess.ts +99 -0
  22. package/apps/shared/components/admin/layout/AdminShell.tsx +22 -10
  23. package/apps/shared/components/admin/mailboxes/ShareAccessCard.tsx +107 -60
  24. package/apps/shared/components/admin/settings/BrandingForm.tsx +24 -0
  25. package/apps/shared/components/admin/settings/DomainDnsSetup.tsx +2 -1
  26. package/apps/shared/components/admin/settings/PluginsManager.tsx +228 -25
  27. package/apps/shared/components/admin/setup/SetupWizard.tsx +2 -1
  28. package/apps/shared/components/calendar/CalendarListSidebar.tsx +4 -5
  29. package/apps/shared/components/calendar/layout/CalendarShell.tsx +8 -2
  30. package/apps/shared/components/contacts/ContactForm.tsx +11 -5
  31. package/apps/shared/components/contacts/ContactsSidebar.tsx +5 -3
  32. package/apps/shared/components/contacts/ContactsToolbar.tsx +98 -115
  33. package/apps/shared/components/contacts/layout/ContactsShell.tsx +25 -8
  34. package/apps/shared/components/escrow/layout/EscrowShell.tsx +13 -6
  35. package/apps/shared/components/layout/AppShell.tsx +477 -288
  36. package/apps/shared/components/layout/BrandingChrome.tsx +200 -13
  37. package/apps/shared/components/layout/KeyEnrollmentGate.tsx +398 -389
  38. package/apps/shared/components/layout/MailboxProvisioning.tsx +168 -164
  39. package/apps/shared/components/layout/RailIcon.tsx +100 -0
  40. package/apps/shared/components/layout/ResponsiveToolbar.tsx +315 -0
  41. package/apps/shared/components/layout/ThemeSwitch.tsx +84 -0
  42. package/apps/shared/components/layout/UserMenu.tsx +372 -185
  43. package/apps/shared/components/mail/ConversationList.tsx +271 -262
  44. package/apps/shared/components/mail/ConversationThreadPane.tsx +147 -94
  45. package/apps/shared/components/mail/LazyReadingPane.tsx +113 -0
  46. package/apps/shared/components/mail/MailSelectionBar.tsx +227 -222
  47. package/apps/shared/components/mail/MessageDetailPane.tsx +1537 -1387
  48. package/apps/shared/components/mail/OutboxBadge.tsx +44 -0
  49. package/apps/shared/components/mail/OutboxRowStatus.tsx +31 -0
  50. package/apps/shared/components/mail/compose/ComposeContext.tsx +312 -160
  51. package/apps/shared/components/mail/compose/ComposeToolbar.tsx +422 -418
  52. package/apps/shared/components/mail/compose/ComposeWindow.tsx +1644 -1736
  53. package/apps/shared/components/mail/compose/ComposeWindowPlaceholder.tsx +111 -0
  54. package/apps/shared/components/mail/compose/RichTextEditor.tsx +147 -118
  55. package/apps/shared/components/mail/compose/composePerf.ts +46 -0
  56. package/apps/shared/components/mail/compose/encryptionRequirement.ts +86 -0
  57. package/apps/shared/components/mail/compose/quotedBody.ts +161 -100
  58. package/apps/shared/components/mail/layout/MailShell.tsx +490 -472
  59. package/apps/shared/components/mail/reading/EncryptedBody.tsx +65 -0
  60. package/apps/shared/components/mail/reading/EncryptedPreview.tsx +35 -0
  61. package/apps/shared/components/mail/reading/MessageBody.tsx +240 -0
  62. package/apps/shared/components/mail/reading/MessageCard.tsx +174 -0
  63. package/apps/shared/components/mail/reading/bodyContent.ts +100 -0
  64. package/apps/shared/components/mail/reading/bodyHtml.ts +298 -0
  65. package/apps/shared/components/mail/reading/color.ts +245 -0
  66. package/apps/shared/components/mail/reading/frameControl.ts +142 -0
  67. package/apps/shared/components/mail/reading/frameDocument.ts +123 -0
  68. package/apps/shared/components/mail/reading/safeDocument.ts +24 -0
  69. package/apps/shared/components/mail/reading/themeAdaptation.ts +221 -0
  70. package/apps/shared/components/mail/reading/themeSurface.ts +90 -0
  71. package/apps/shared/components/mail/reading/viewOriginal.ts +38 -0
  72. package/apps/shared/components/mail/unreadStyle.tsx +75 -0
  73. package/apps/shared/components/settings/SigningCertificateCard.tsx +344 -0
  74. package/apps/shared/components/settings/layout/SettingsShell.tsx +253 -240
  75. package/apps/shared/components/sharing/PrincipalPicker.tsx +145 -0
  76. package/apps/shared/components/tasks/TasksSidebar.tsx +5 -3
  77. package/apps/shared/components/tasks/layout/TasksShell.tsx +24 -8
  78. package/apps/shared/keyboard/GlobalShortcuts.tsx +51 -0
  79. package/apps/shared/keyboard/ShortcutProvider.tsx +62 -0
  80. package/apps/shared/keyboard/ShortcutsDialog.tsx +84 -0
  81. package/apps/shared/keyboard/dispatch.ts +124 -0
  82. package/apps/shared/keyboard/format.ts +89 -0
  83. package/apps/shared/keyboard/keymap.ts +114 -0
  84. package/apps/shared/keyboard/match.ts +44 -0
  85. package/apps/shared/keyboard/parse.ts +136 -0
  86. package/apps/shared/keyboard/platform.ts +34 -0
  87. package/apps/shared/keyboard/registry.ts +65 -0
  88. package/apps/shared/keyboard/targets.ts +79 -0
  89. package/apps/shared/keyboard/useShortcut.ts +50 -0
  90. package/apps/shared/keyboard/useShortcutProps.ts +17 -0
  91. package/apps/shared/mail/folderCounts.ts +308 -0
  92. package/apps/shared/mail/folderTree.ts +143 -0
  93. package/apps/shared/mail/listSnapshots.ts +87 -0
  94. package/apps/shared/mail/messageReadState.ts +85 -0
  95. package/apps/shared/mail/newMailNotifications.ts +183 -0
  96. package/apps/shared/mail/outbox/UnlockBridge.tsx +23 -0
  97. package/apps/shared/mail/outbox/composeBridge.ts +69 -0
  98. package/apps/shared/mail/outbox/outboxState.ts +88 -0
  99. package/apps/shared/mail/outbox/pendingSends.ts +146 -0
  100. package/apps/shared/mail/outbox/sendDecision.ts +157 -0
  101. package/apps/shared/mail/outbox/sendJob.ts +427 -0
  102. package/apps/shared/mail/outbox/sendOutcomes.ts +146 -0
  103. package/apps/shared/mail/outbox/sendState.ts +35 -0
  104. package/apps/shared/mail/outbox/useOutboxStatus.ts +61 -0
  105. package/apps/shared/mail/useMailConnection.ts +203 -0
  106. package/apps/shared/mail/useMailLiveUpdates.ts +277 -224
  107. package/apps/shared/mail/useMarkMessageRead.ts +47 -0
  108. package/apps/shared/mail/useNewMailNotifications.ts +178 -0
  109. package/apps/shared/mail/useUnreadTitle.ts +42 -0
  110. package/apps/shared/navigation/AppRouter.tsx +300 -0
  111. package/apps/shared/navigation/appHrefs.ts +23 -0
  112. package/apps/shared/navigation/frameContext.tsx +35 -0
  113. package/apps/shared/navigation/idle.ts +45 -0
  114. package/apps/shared/navigation/routerContext.tsx +83 -0
  115. package/apps/shared/navigation/routes.ts +72 -0
  116. package/apps/shared/notifications/NotificationCenter.tsx +215 -0
  117. package/apps/shared/notifications/NotificationHistoryDialog.tsx +90 -0
  118. package/apps/shared/notifications/apiErrors.ts +98 -0
  119. package/apps/shared/notifications/headerOffset.ts +35 -0
  120. package/apps/shared/notifications/pushStatus.ts +56 -0
  121. package/apps/shared/notifications/store.ts +549 -0
  122. package/apps/shared/notifications/systemErrors.ts +63 -0
  123. package/apps/shared/notifications/useNotifications.ts +41 -0
  124. package/apps/shared/search/LocalIndexLifecycle.tsx +114 -98
  125. package/apps/shared/signing/enrollmentStorage.ts +33 -0
  126. package/apps/shared/signing/enrollmentTracker.ts +385 -0
  127. package/apps/shared/signing/enrollmentView.ts +251 -0
  128. package/apps/shared/signing/signingInfo.ts +58 -0
  129. package/apps/shared/signing/useNow.ts +19 -0
  130. package/apps/shared/signing/useSigningEnrollmentWatcher.ts +90 -0
  131. package/apps/shared/styles/app.css +108 -10
  132. package/apps/www/_layout.tsx +8 -2
  133. package/apps/www/_routedPage.tsx +24 -0
  134. package/apps/www/_routes.ts +36 -0
  135. package/apps/www/calendar/index.tsx +64 -33
  136. package/apps/www/contacts/[uid].tsx +109 -107
  137. package/apps/www/contacts/index.tsx +62 -42
  138. package/apps/www/index.tsx +2291 -1917
  139. package/apps/www/messages/[uid].tsx +108 -101
  140. package/apps/www/settings/appearance/index.tsx +21 -0
  141. package/apps/www/settings/auto-reply/index.tsx +7 -8
  142. package/apps/www/settings/encryption/index.tsx +1287 -1249
  143. package/apps/www/settings/filters/[uid].tsx +7 -2
  144. package/apps/www/settings/filters/index.tsx +102 -99
  145. package/apps/www/settings/filters/new/index.tsx +140 -133
  146. package/apps/www/settings/labels/index.tsx +204 -201
  147. package/apps/www/settings/privacy/index.tsx +7 -9
  148. package/apps/www/settings/read-receipts/index.tsx +7 -8
  149. package/apps/www/settings/sharing/index.tsx +256 -274
  150. package/apps/www/settings/signatures/[uid].tsx +172 -167
  151. package/apps/www/settings/signatures/index.tsx +88 -85
  152. package/apps/www/settings/signatures/new/index.tsx +135 -129
  153. package/apps/www/tasks/index.tsx +29 -18
  154. package/dist/apps/admin/_layout.d.ts +5 -1
  155. package/dist/apps/admin/_layout.js +3 -2
  156. package/dist/apps/admin/index.js +3 -2
  157. package/dist/apps/admin/ingest-queue/index.js +2 -1
  158. package/dist/apps/admin/mailboxes/[uid].js +9 -6
  159. package/dist/apps/admin/quarantine/index.js +2 -1
  160. package/dist/apps/admin/signing-certificates/index.d.ts +3 -0
  161. package/dist/apps/admin/signing-certificates/index.js +161 -0
  162. package/dist/apps/escrow/_layout.d.ts +5 -1
  163. package/dist/apps/escrow/_layout.js +3 -2
  164. package/dist/apps/shared/appearance/AppearanceForm.d.ts +7 -0
  165. package/dist/apps/shared/appearance/AppearanceForm.js +131 -0
  166. package/dist/apps/shared/appearance/AppearanceHead.d.ts +15 -0
  167. package/dist/apps/shared/appearance/AppearanceHead.js +31 -0
  168. package/dist/apps/shared/appearance/AppearanceProvider.d.ts +32 -0
  169. package/dist/apps/shared/appearance/AppearanceProvider.js +390 -0
  170. package/dist/apps/shared/appearance/ColorField.d.ts +22 -0
  171. package/dist/apps/shared/appearance/ColorField.js +40 -0
  172. package/dist/apps/shared/appearance/appearanceCache.d.ts +45 -0
  173. package/dist/apps/shared/appearance/appearanceCache.js +80 -0
  174. package/dist/apps/shared/appearance/appearanceContext.d.ts +45 -0
  175. package/dist/apps/shared/appearance/appearanceContext.js +29 -0
  176. package/dist/apps/shared/appearance/bootScript.d.ts +16 -0
  177. package/dist/apps/shared/appearance/bootScript.js +33 -0
  178. package/dist/apps/shared/appearance/color.d.ts +65 -0
  179. package/dist/apps/shared/appearance/color.js +132 -0
  180. package/dist/apps/shared/appearance/photo.d.ts +17 -0
  181. package/dist/apps/shared/appearance/photo.js +79 -0
  182. package/dist/apps/shared/appearance/resolvedTheme.d.ts +21 -0
  183. package/dist/apps/shared/appearance/resolvedTheme.js +55 -0
  184. package/dist/apps/shared/appearance/theme.d.ts +111 -0
  185. package/dist/apps/shared/appearance/theme.js +251 -0
  186. package/dist/apps/shared/auth/accountUrl.d.ts +2 -0
  187. package/dist/apps/shared/auth/accountUrl.js +8 -0
  188. package/dist/apps/shared/auth/adminAccess.d.ts +30 -0
  189. package/dist/apps/shared/auth/adminAccess.js +89 -0
  190. package/dist/apps/shared/components/admin/layout/AdminShell.d.ts +1 -1
  191. package/dist/apps/shared/components/admin/layout/AdminShell.js +16 -6
  192. package/dist/apps/shared/components/admin/mailboxes/ShareAccessCard.d.ts +10 -5
  193. package/dist/apps/shared/components/admin/mailboxes/ShareAccessCard.js +49 -34
  194. package/dist/apps/shared/components/admin/settings/BrandingForm.d.ts +2 -0
  195. package/dist/apps/shared/components/admin/settings/BrandingForm.js +7 -1
  196. package/dist/apps/shared/components/admin/settings/DomainDnsSetup.js +1 -1
  197. package/dist/apps/shared/components/admin/settings/PluginsManager.js +106 -20
  198. package/dist/apps/shared/components/admin/setup/SetupWizard.js +2 -1
  199. package/dist/apps/shared/components/calendar/CalendarListSidebar.js +8 -8
  200. package/dist/apps/shared/components/calendar/layout/CalendarShell.d.ts +1 -1
  201. package/dist/apps/shared/components/calendar/layout/CalendarShell.js +8 -4
  202. package/dist/apps/shared/components/contacts/ContactForm.js +1 -1
  203. package/dist/apps/shared/components/contacts/ContactsSidebar.js +4 -2
  204. package/dist/apps/shared/components/contacts/ContactsToolbar.d.ts +6 -2
  205. package/dist/apps/shared/components/contacts/ContactsToolbar.js +28 -10
  206. package/dist/apps/shared/components/contacts/layout/ContactsShell.d.ts +1 -1
  207. package/dist/apps/shared/components/contacts/layout/ContactsShell.js +19 -6
  208. package/dist/apps/shared/components/escrow/layout/EscrowShell.js +7 -4
  209. package/dist/apps/shared/components/layout/AppShell.d.ts +34 -4
  210. package/dist/apps/shared/components/layout/AppShell.js +122 -17
  211. package/dist/apps/shared/components/layout/BrandingChrome.d.ts +69 -10
  212. package/dist/apps/shared/components/layout/BrandingChrome.js +140 -12
  213. package/dist/apps/shared/components/layout/KeyEnrollmentGate.js +10 -9
  214. package/dist/apps/shared/components/layout/MailboxProvisioning.js +3 -1
  215. package/dist/apps/shared/components/layout/RailIcon.d.ts +21 -0
  216. package/dist/apps/shared/components/layout/RailIcon.js +85 -0
  217. package/dist/apps/shared/components/layout/ResponsiveToolbar.d.ts +55 -0
  218. package/dist/apps/shared/components/layout/ResponsiveToolbar.js +199 -0
  219. package/dist/apps/shared/components/layout/ThemeSwitch.d.ts +9 -0
  220. package/dist/apps/shared/components/layout/ThemeSwitch.js +54 -0
  221. package/dist/apps/shared/components/layout/UserMenu.d.ts +25 -3
  222. package/dist/apps/shared/components/layout/UserMenu.js +88 -7
  223. package/dist/apps/shared/components/mail/ConversationList.js +5 -11
  224. package/dist/apps/shared/components/mail/ConversationThreadPane.d.ts +6 -2
  225. package/dist/apps/shared/components/mail/ConversationThreadPane.js +87 -40
  226. package/dist/apps/shared/components/mail/LazyReadingPane.d.ts +9 -0
  227. package/dist/apps/shared/components/mail/LazyReadingPane.js +83 -0
  228. package/dist/apps/shared/components/mail/MailSelectionBar.d.ts +6 -4
  229. package/dist/apps/shared/components/mail/MailSelectionBar.js +11 -4
  230. package/dist/apps/shared/components/mail/MessageDetailPane.d.ts +25 -8
  231. package/dist/apps/shared/components/mail/MessageDetailPane.js +167 -73
  232. package/dist/apps/shared/components/mail/OutboxBadge.d.ts +17 -0
  233. package/dist/apps/shared/components/mail/OutboxBadge.js +18 -0
  234. package/dist/apps/shared/components/mail/OutboxRowStatus.d.ts +10 -0
  235. package/dist/apps/shared/components/mail/OutboxRowStatus.js +19 -0
  236. package/dist/apps/shared/components/mail/compose/ComposeContext.d.ts +38 -0
  237. package/dist/apps/shared/components/mail/compose/ComposeContext.js +97 -6
  238. package/dist/apps/shared/components/mail/compose/ComposeToolbar.js +4 -3
  239. package/dist/apps/shared/components/mail/compose/ComposeWindow.d.ts +3 -2
  240. package/dist/apps/shared/components/mail/compose/ComposeWindow.js +316 -339
  241. package/dist/apps/shared/components/mail/compose/ComposeWindowPlaceholder.d.ts +19 -0
  242. package/dist/apps/shared/components/mail/compose/ComposeWindowPlaceholder.js +33 -0
  243. package/dist/apps/shared/components/mail/compose/RichTextEditor.d.ts +9 -1
  244. package/dist/apps/shared/components/mail/compose/RichTextEditor.js +20 -2
  245. package/dist/apps/shared/components/mail/compose/composePerf.d.ts +16 -0
  246. package/dist/apps/shared/components/mail/compose/composePerf.js +41 -0
  247. package/dist/apps/shared/components/mail/compose/encryptionRequirement.d.ts +49 -0
  248. package/dist/apps/shared/components/mail/compose/encryptionRequirement.js +35 -0
  249. package/dist/apps/shared/components/mail/compose/quotedBody.d.ts +13 -0
  250. package/dist/apps/shared/components/mail/compose/quotedBody.js +67 -11
  251. package/dist/apps/shared/components/mail/layout/MailShell.d.ts +14 -4
  252. package/dist/apps/shared/components/mail/layout/MailShell.js +90 -104
  253. package/dist/apps/shared/components/mail/reading/EncryptedBody.d.ts +21 -0
  254. package/dist/apps/shared/components/mail/reading/EncryptedBody.js +22 -0
  255. package/dist/apps/shared/components/mail/reading/EncryptedPreview.d.ts +18 -0
  256. package/dist/apps/shared/components/mail/reading/EncryptedPreview.js +22 -0
  257. package/dist/apps/shared/components/mail/reading/MessageBody.d.ts +32 -0
  258. package/dist/apps/shared/components/mail/reading/MessageBody.js +136 -0
  259. package/dist/apps/shared/components/mail/reading/MessageCard.d.ts +62 -0
  260. package/dist/apps/shared/components/mail/reading/MessageCard.js +58 -0
  261. package/dist/apps/shared/components/mail/reading/bodyContent.d.ts +38 -0
  262. package/dist/apps/shared/components/mail/reading/bodyContent.js +84 -0
  263. package/dist/apps/shared/components/mail/reading/bodyHtml.d.ts +47 -0
  264. package/dist/apps/shared/components/mail/reading/bodyHtml.js +267 -0
  265. package/dist/apps/shared/components/mail/reading/color.d.ts +63 -0
  266. package/dist/apps/shared/components/mail/reading/color.js +201 -0
  267. package/dist/apps/shared/components/mail/reading/frameControl.d.ts +33 -0
  268. package/dist/apps/shared/components/mail/reading/frameControl.js +120 -0
  269. package/dist/apps/shared/components/mail/reading/frameDocument.d.ts +52 -0
  270. package/dist/apps/shared/components/mail/reading/frameDocument.js +98 -0
  271. package/dist/apps/shared/components/mail/reading/safeDocument.d.ts +3 -0
  272. package/dist/apps/shared/components/mail/reading/safeDocument.js +12 -0
  273. package/dist/apps/shared/components/mail/reading/themeAdaptation.d.ts +56 -0
  274. package/dist/apps/shared/components/mail/reading/themeAdaptation.js +144 -0
  275. package/dist/apps/shared/components/mail/reading/themeSurface.d.ts +17 -0
  276. package/dist/apps/shared/components/mail/reading/themeSurface.js +83 -0
  277. package/dist/apps/shared/components/mail/reading/viewOriginal.d.ts +9 -0
  278. package/dist/apps/shared/components/mail/reading/viewOriginal.js +32 -0
  279. package/dist/apps/shared/components/mail/unreadStyle.d.ts +45 -0
  280. package/dist/apps/shared/components/mail/unreadStyle.js +61 -0
  281. package/dist/apps/shared/components/settings/SigningCertificateCard.d.ts +32 -0
  282. package/dist/apps/shared/components/settings/SigningCertificateCard.js +98 -0
  283. package/dist/apps/shared/components/settings/layout/SettingsShell.d.ts +1 -1
  284. package/dist/apps/shared/components/settings/layout/SettingsShell.js +16 -5
  285. package/dist/apps/shared/components/sharing/PrincipalPicker.d.ts +26 -0
  286. package/dist/apps/shared/components/sharing/PrincipalPicker.js +77 -0
  287. package/dist/apps/shared/components/tasks/TasksSidebar.js +4 -2
  288. package/dist/apps/shared/components/tasks/layout/TasksShell.d.ts +1 -1
  289. package/dist/apps/shared/components/tasks/layout/TasksShell.js +18 -6
  290. package/dist/apps/shared/keyboard/GlobalShortcuts.d.ts +14 -0
  291. package/dist/apps/shared/keyboard/GlobalShortcuts.js +37 -0
  292. package/dist/apps/shared/keyboard/ShortcutProvider.d.ts +20 -0
  293. package/dist/apps/shared/keyboard/ShortcutProvider.js +52 -0
  294. package/dist/apps/shared/keyboard/ShortcutsDialog.d.ts +14 -0
  295. package/dist/apps/shared/keyboard/ShortcutsDialog.js +42 -0
  296. package/dist/apps/shared/keyboard/dispatch.d.ts +16 -0
  297. package/dist/apps/shared/keyboard/dispatch.js +109 -0
  298. package/dist/apps/shared/keyboard/format.d.ts +12 -0
  299. package/dist/apps/shared/keyboard/format.js +74 -0
  300. package/dist/apps/shared/keyboard/keymap.d.ts +294 -0
  301. package/dist/apps/shared/keyboard/keymap.js +84 -0
  302. package/dist/apps/shared/keyboard/match.d.ts +20 -0
  303. package/dist/apps/shared/keyboard/match.js +31 -0
  304. package/dist/apps/shared/keyboard/parse.d.ts +31 -0
  305. package/dist/apps/shared/keyboard/parse.js +107 -0
  306. package/dist/apps/shared/keyboard/platform.d.ts +15 -0
  307. package/dist/apps/shared/keyboard/platform.js +22 -0
  308. package/dist/apps/shared/keyboard/registry.d.ts +39 -0
  309. package/dist/apps/shared/keyboard/registry.js +33 -0
  310. package/dist/apps/shared/keyboard/targets.d.ts +14 -0
  311. package/dist/apps/shared/keyboard/targets.js +67 -0
  312. package/dist/apps/shared/keyboard/useShortcut.d.ts +19 -0
  313. package/dist/apps/shared/keyboard/useShortcut.js +35 -0
  314. package/dist/apps/shared/keyboard/useShortcutProps.d.ts +10 -0
  315. package/dist/apps/shared/keyboard/useShortcutProps.js +15 -0
  316. package/dist/apps/shared/mail/folderCounts.d.ts +78 -0
  317. package/dist/apps/shared/mail/folderCounts.js +218 -0
  318. package/dist/apps/shared/mail/folderTree.d.ts +55 -0
  319. package/dist/apps/shared/mail/folderTree.js +116 -0
  320. package/dist/apps/shared/mail/listSnapshots.d.ts +46 -0
  321. package/dist/apps/shared/mail/listSnapshots.js +43 -0
  322. package/dist/apps/shared/mail/messageReadState.d.ts +32 -0
  323. package/dist/apps/shared/mail/messageReadState.js +63 -0
  324. package/dist/apps/shared/mail/newMailNotifications.d.ts +62 -0
  325. package/dist/apps/shared/mail/newMailNotifications.js +138 -0
  326. package/dist/apps/shared/mail/outbox/UnlockBridge.d.ts +5 -0
  327. package/dist/apps/shared/mail/outbox/UnlockBridge.js +18 -0
  328. package/dist/apps/shared/mail/outbox/composeBridge.d.ts +34 -0
  329. package/dist/apps/shared/mail/outbox/composeBridge.js +32 -0
  330. package/dist/apps/shared/mail/outbox/outboxState.d.ts +41 -0
  331. package/dist/apps/shared/mail/outbox/outboxState.js +42 -0
  332. package/dist/apps/shared/mail/outbox/pendingSends.d.ts +39 -0
  333. package/dist/apps/shared/mail/outbox/pendingSends.js +109 -0
  334. package/dist/apps/shared/mail/outbox/sendDecision.d.ts +80 -0
  335. package/dist/apps/shared/mail/outbox/sendDecision.js +94 -0
  336. package/dist/apps/shared/mail/outbox/sendJob.d.ts +68 -0
  337. package/dist/apps/shared/mail/outbox/sendJob.js +319 -0
  338. package/dist/apps/shared/mail/outbox/sendOutcomes.d.ts +3 -0
  339. package/dist/apps/shared/mail/outbox/sendOutcomes.js +141 -0
  340. package/dist/apps/shared/mail/outbox/sendState.d.ts +25 -0
  341. package/dist/apps/shared/mail/outbox/sendState.js +8 -0
  342. package/dist/apps/shared/mail/outbox/useOutboxStatus.d.ts +11 -0
  343. package/dist/apps/shared/mail/outbox/useOutboxStatus.js +49 -0
  344. package/dist/apps/shared/mail/useMailConnection.d.ts +56 -0
  345. package/dist/apps/shared/mail/useMailConnection.js +138 -0
  346. package/dist/apps/shared/mail/useMailLiveUpdates.d.ts +29 -9
  347. package/dist/apps/shared/mail/useMailLiveUpdates.js +60 -32
  348. package/dist/apps/shared/mail/useMarkMessageRead.d.ts +12 -0
  349. package/dist/apps/shared/mail/useMarkMessageRead.js +44 -0
  350. package/dist/apps/shared/mail/useNewMailNotifications.d.ts +35 -0
  351. package/dist/apps/shared/mail/useNewMailNotifications.js +130 -0
  352. package/dist/apps/shared/mail/useUnreadTitle.d.ts +18 -0
  353. package/dist/apps/shared/mail/useUnreadTitle.js +31 -0
  354. package/dist/apps/shared/navigation/AppRouter.d.ts +52 -0
  355. package/dist/apps/shared/navigation/AppRouter.js +242 -0
  356. package/dist/apps/shared/navigation/appHrefs.d.ts +14 -0
  357. package/dist/apps/shared/navigation/appHrefs.js +20 -0
  358. package/dist/apps/shared/navigation/frameContext.d.ts +19 -0
  359. package/dist/apps/shared/navigation/frameContext.js +24 -0
  360. package/dist/apps/shared/navigation/idle.d.ts +14 -0
  361. package/dist/apps/shared/navigation/idle.js +43 -0
  362. package/dist/apps/shared/navigation/routerContext.d.ts +37 -0
  363. package/dist/apps/shared/navigation/routerContext.js +56 -0
  364. package/dist/apps/shared/navigation/routes.d.ts +32 -0
  365. package/dist/apps/shared/navigation/routes.js +37 -0
  366. package/dist/apps/shared/notifications/NotificationCenter.d.ts +19 -0
  367. package/dist/apps/shared/notifications/NotificationCenter.js +90 -0
  368. package/dist/apps/shared/notifications/NotificationHistoryDialog.d.ts +10 -0
  369. package/dist/apps/shared/notifications/NotificationHistoryDialog.js +36 -0
  370. package/dist/apps/shared/notifications/apiErrors.d.ts +19 -0
  371. package/dist/apps/shared/notifications/apiErrors.js +86 -0
  372. package/dist/apps/shared/notifications/headerOffset.d.ts +8 -0
  373. package/dist/apps/shared/notifications/headerOffset.js +33 -0
  374. package/dist/apps/shared/notifications/pushStatus.d.ts +9 -0
  375. package/dist/apps/shared/notifications/pushStatus.js +54 -0
  376. package/dist/apps/shared/notifications/store.d.ts +136 -0
  377. package/dist/apps/shared/notifications/store.js +416 -0
  378. package/dist/apps/shared/notifications/systemErrors.d.ts +9 -0
  379. package/dist/apps/shared/notifications/systemErrors.js +58 -0
  380. package/dist/apps/shared/notifications/useNotifications.d.ts +17 -0
  381. package/dist/apps/shared/notifications/useNotifications.js +18 -0
  382. package/dist/apps/shared/search/LocalIndexLifecycle.js +17 -3
  383. package/dist/apps/shared/signing/enrollmentStorage.d.ts +10 -0
  384. package/dist/apps/shared/signing/enrollmentStorage.js +33 -0
  385. package/dist/apps/shared/signing/enrollmentTracker.d.ts +73 -0
  386. package/dist/apps/shared/signing/enrollmentTracker.js +307 -0
  387. package/dist/apps/shared/signing/enrollmentView.d.ts +57 -0
  388. package/dist/apps/shared/signing/enrollmentView.js +214 -0
  389. package/dist/apps/shared/signing/signingInfo.d.ts +11 -0
  390. package/dist/apps/shared/signing/signingInfo.js +48 -0
  391. package/dist/apps/shared/signing/useNow.d.ts +2 -0
  392. package/dist/apps/shared/signing/useNow.js +18 -0
  393. package/dist/apps/shared/signing/useSigningEnrollmentWatcher.d.ts +16 -0
  394. package/dist/apps/shared/signing/useSigningEnrollmentWatcher.js +87 -0
  395. package/dist/apps/shared/styles/app.css +108 -10
  396. package/dist/apps/www/_layout.d.ts +5 -1
  397. package/dist/apps/www/_layout.js +3 -2
  398. package/dist/apps/www/_routedPage.d.ts +12 -0
  399. package/dist/apps/www/_routedPage.js +19 -0
  400. package/dist/apps/www/_routes.d.ts +11 -0
  401. package/dist/apps/www/_routes.js +30 -0
  402. package/dist/apps/www/calendar/index.d.ts +2 -2
  403. package/dist/apps/www/calendar/index.js +41 -11
  404. package/dist/apps/www/contacts/[uid].d.ts +3 -9
  405. package/dist/apps/www/contacts/[uid].js +11 -9
  406. package/dist/apps/www/contacts/index.d.ts +2 -2
  407. package/dist/apps/www/contacts/index.js +30 -24
  408. package/dist/apps/www/index.d.ts +2 -2
  409. package/dist/apps/www/index.js +332 -48
  410. package/dist/apps/www/messages/[uid].d.ts +3 -7
  411. package/dist/apps/www/messages/[uid].js +10 -5
  412. package/dist/apps/www/settings/appearance/index.d.ts +4 -0
  413. package/dist/apps/www/settings/appearance/index.js +13 -0
  414. package/dist/apps/www/settings/auto-reply/index.d.ts +2 -2
  415. package/dist/apps/www/settings/auto-reply/index.js +6 -7
  416. package/dist/apps/www/settings/encryption/index.d.ts +2 -2
  417. package/dist/apps/www/settings/encryption/index.js +106 -96
  418. package/dist/apps/www/settings/filters/[uid].d.ts +2 -2
  419. package/dist/apps/www/settings/filters/[uid].js +6 -2
  420. package/dist/apps/www/settings/filters/index.d.ts +2 -2
  421. package/dist/apps/www/settings/filters/index.js +3 -1
  422. package/dist/apps/www/settings/filters/new/index.d.ts +2 -2
  423. package/dist/apps/www/settings/filters/new/index.js +9 -3
  424. package/dist/apps/www/settings/labels/index.d.ts +2 -2
  425. package/dist/apps/www/settings/labels/index.js +3 -1
  426. package/dist/apps/www/settings/privacy/index.d.ts +2 -2
  427. package/dist/apps/www/settings/privacy/index.js +8 -9
  428. package/dist/apps/www/settings/read-receipts/index.d.ts +2 -2
  429. package/dist/apps/www/settings/read-receipts/index.js +6 -7
  430. package/dist/apps/www/settings/sharing/index.d.ts +2 -2
  431. package/dist/apps/www/settings/sharing/index.js +22 -34
  432. package/dist/apps/www/settings/signatures/[uid].d.ts +2 -2
  433. package/dist/apps/www/settings/signatures/[uid].js +6 -2
  434. package/dist/apps/www/settings/signatures/index.d.ts +2 -2
  435. package/dist/apps/www/settings/signatures/index.js +3 -1
  436. package/dist/apps/www/settings/signatures/new/index.d.ts +2 -2
  437. package/dist/apps/www/settings/signatures/new/index.js +9 -4
  438. package/dist/apps/www/tasks/index.d.ts +2 -2
  439. package/dist/apps/www/tasks/index.js +22 -16
  440. package/package.json +2 -2
  441. package/apps/shared/components/mail/compose/SendFailureAlert.tsx +0 -48
  442. package/dist/apps/shared/components/mail/compose/SendFailureAlert.d.ts +0 -14
  443. package/dist/apps/shared/components/mail/compose/SendFailureAlert.js +0 -11
@@ -0,0 +1,183 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ import type { Folder, Mailbox, Message } from "@rapidmx/react-shared/mail/mailApi.js";
6
+ import { formatMailAddress, splitMailAddress } from "@rapidmx/react-shared/mail/mailAddress.js";
7
+
8
+ /**
9
+ * What decides whether a new message is announced, what the announcement says, and the two settings behind it (both per
10
+ * browser, in `localStorage`): whether new-mail pop-ups are on at all, and what the user told the "turn on desktop
11
+ * notifications" offer.
12
+ *
13
+ * The announcement is built only from what the push event carries - the whole `Message`, including `bodyPreview` - and only
14
+ * ever as text: React escapes it on screen and the Notifications API takes plain strings, so nothing here is HTML.
15
+ */
16
+
17
+ /** `localStorage` key of the pop-ups switch. Absent means on; `"off"` means the user turned them off. */
18
+ export const NEW_MAIL_POPUPS_KEY = "rapidmx-new-mail-popups";
19
+
20
+ /** `localStorage` key of the answer to the desktop-notifications offer: `"later"` (not now) or `"asked"` (they were asked). */
21
+ export const DESKTOP_OFFER_KEY = "rapidmx-desktop-notifications-offer";
22
+
23
+ /** How much of the body an announcement quotes. */
24
+ export const PREVIEW_MAX_LENGTH = 140;
25
+
26
+ /** A message received longer ago than this is not "new": mail imported or migrated in bulk arrives as `create` events too. */
27
+ export const MAX_NEW_AGE_MS = 6 * 60 * 60 * 1000;
28
+
29
+ /** The subject the server stores for an encrypted message (the real one is inside the ciphertext). */
30
+ const ENCRYPTED_SUBJECT_PLACEHOLDER = "[...]";
31
+
32
+ function readStorage(key: string): string | null {
33
+ try {
34
+ return localStorage.getItem(key);
35
+ } catch {
36
+ return null;
37
+ }
38
+ }
39
+
40
+ function writeStorage(key: string, value: string | null): void {
41
+ try {
42
+ if (value === null) {
43
+ localStorage.removeItem(key);
44
+ } else {
45
+ localStorage.setItem(key, value);
46
+ }
47
+ } catch {
48
+ // Storage blocked or full: the choice lasts until the page is reloaded, no longer.
49
+ }
50
+ }
51
+
52
+ /** Whether new-mail pop-ups are on (the default). */
53
+ export function getNewMailPopupsEnabled(): boolean {
54
+ return readStorage(NEW_MAIL_POPUPS_KEY) !== "off";
55
+ }
56
+
57
+ export function setNewMailPopupsEnabled(enabled: boolean): void {
58
+ writeStorage(NEW_MAIL_POPUPS_KEY, enabled ? null : "off");
59
+ }
60
+
61
+ /** Whether the "turn on desktop notifications" offer has been put away with "Not now" (or answered). */
62
+ export function getDesktopOfferDismissed(): boolean {
63
+ return readStorage(DESKTOP_OFFER_KEY) !== null;
64
+ }
65
+
66
+ export function setDesktopOfferDismissed(dismissed: boolean): void {
67
+ writeStorage(DESKTOP_OFFER_KEY, dismissed ? "later" : null);
68
+ }
69
+
70
+ /** The browser's notification permission, or `"unsupported"` where there is no Notifications API. */
71
+ export type DesktopPermission = NotificationPermission | "unsupported";
72
+
73
+ export function desktopPermission(): DesktopPermission {
74
+ return typeof Notification === "undefined" ? "unsupported" : Notification.permission;
75
+ }
76
+
77
+ /** Asks the browser for permission - call it from a click. Resolves the answer; never rejects. */
78
+ export async function requestDesktopPermission(): Promise<DesktopPermission> {
79
+ if (typeof Notification === "undefined") {
80
+ return "unsupported";
81
+ }
82
+ try {
83
+ return await Notification.requestPermission();
84
+ } catch {
85
+ return desktopPermission();
86
+ }
87
+ }
88
+
89
+ /** What an announcement shows. */
90
+ export interface NewMailNotice {
91
+ /** The message's uid: the announcement's identity (and the desktop notification's `tag`). */
92
+ uid: string;
93
+ mailboxUid: string;
94
+ folderUid: string;
95
+ /** The sender's display name, `""` when there is none. */
96
+ senderName: string;
97
+ /** The sender's address - always shown, so a name can't stand in for who really sent it. */
98
+ senderAddress: string;
99
+ subject: string;
100
+ /** About `PREVIEW_MAX_LENGTH` characters of the body as plain text; `Encrypted message` for an encrypted one. */
101
+ preview: string;
102
+ /** Where opening it goes. */
103
+ href: string;
104
+ }
105
+
106
+ /** Control characters, and the invisible or bidirectional-override characters that can make text read as something else, as code point ranges. */
107
+ const UNWANTED_RANGES: [number, number][] = [
108
+ [0x0000, 0x001f],
109
+ [0x007f, 0x009f],
110
+ [0x200b, 0x200f],
111
+ [0x2028, 0x202e],
112
+ [0x2060, 0x2064],
113
+ [0x2066, 0x2069],
114
+ [0xfeff, 0xfeff],
115
+ ];
116
+ const hex = (code: number) => "\\u" + code.toString(16).padStart(4, "0");
117
+ const UNWANTED = new RegExp("[" + UNWANTED_RANGES.map(([from, to]) => hex(from) + "-" + hex(to)).join("") + "]+", "g");
118
+
119
+ /** `text` with those characters replaced by spaces, whitespace folded, and cut to `max` characters (with an ellipsis). */
120
+ export function cleanPreview(text: string, max: number = PREVIEW_MAX_LENGTH): string {
121
+ const cleaned = text.replace(UNWANTED, " ").replace(/\s+/g, " ").trim();
122
+ return cleaned.length > max ? cleaned.slice(0, max - 1).trimEnd() + String.fromCharCode(0x2026) : cleaned;
123
+ }
124
+
125
+ /** The text of an announcement of `message`. */
126
+ export function noticeFor(message: Message): NewMailNotice {
127
+ const { name, address } = splitMailAddress(message.from);
128
+ const encrypted = message.encrypted === true;
129
+ const rawSubject = cleanPreview(message.subject ?? "", 200);
130
+ const subject =
131
+ !rawSubject || rawSubject === ENCRYPTED_SUBJECT_PLACEHOLDER ? (encrypted ? "(encrypted subject)" : "(no subject)") : rawSubject;
132
+ return {
133
+ uid: message.uid,
134
+ mailboxUid: message.mailboxUid,
135
+ folderUid: message.folderUid,
136
+ // `splitMailAddress()` has already taken control and bidirectional-override characters out of both.
137
+ senderName: name ?? "",
138
+ senderAddress: address,
139
+ subject,
140
+ preview: encrypted ? "Encrypted message" : cleanPreview(message.bodyPreview ?? ""),
141
+ href: `/messages/${encodeURIComponent(message.uid)}`,
142
+ };
143
+ }
144
+
145
+ /** Every address that is this user's own, lowercased - a message from one of them is not new mail worth announcing. */
146
+ export function ownAddressesOf(mailboxes: Mailbox[]): Set<string> {
147
+ const own = new Set<string>();
148
+ for (const mailbox of mailboxes) {
149
+ for (const address of [mailbox.primarySmtpAddress, ...(mailbox.aliasAddresses ?? [])]) {
150
+ if (address) {
151
+ own.add(address.trim().toLowerCase());
152
+ }
153
+ }
154
+ }
155
+ return own;
156
+ }
157
+
158
+ /**
159
+ * Whether the arrival of `message` is worth announcing: unread mail in an Inbox (not Drafts, Sent Items, Outbox, Deleted Items,
160
+ * Junk Email or quarantine, which are other folders), that Focused Inbox did not put under Other, that its own user did not
161
+ * send, and that is not old (a bulk import).
162
+ */
163
+ export function shouldAnnounce(
164
+ message: Message,
165
+ context: { folders: Folder[]; ownAddresses: Set<string>; now?: number },
166
+ ): boolean {
167
+ if (context.folders.find((folder) => folder.uid === message.folderUid)?.type !== "inbox") {
168
+ return false;
169
+ }
170
+ if (message.flags.read === true || message.inferenceClassification === "other") {
171
+ return false;
172
+ }
173
+ if (context.ownAddresses.has((message.from?.address ?? "").trim().toLowerCase())) {
174
+ return false;
175
+ }
176
+ const received = Date.parse(message.receivedDate);
177
+ return Number.isNaN(received) || (context.now ?? Date.now()) - received < MAX_NEW_AGE_MS;
178
+ }
179
+
180
+ /** `Name <address>`, or the address alone - the desktop notification's title. */
181
+ export function noticeSender(notice: Pick<NewMailNotice, "senderName" | "senderAddress">): string {
182
+ return formatMailAddress({ address: notice.senderAddress, displayName: notice.senderName || undefined });
183
+ }
@@ -0,0 +1,23 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ import { useEffect } from "react";
6
+ import { useUnlockPrompt } from "../../components/layout/UnlockPromptProvider.js";
7
+ import { registerUnlockOpener } from "./composeBridge.js";
8
+
9
+ /**
10
+ * Lets code with no React context - a pop-up's "Unlock" action - open the app's one unlock prompt (`UnlockPromptProvider`). Renders nothing;
11
+ * mounted once, inside the provider it reaches.
12
+ */
13
+ export default function UnlockBridge(): null {
14
+ const { requestUnlock } = useUnlockPrompt();
15
+ useEffect(
16
+ () =>
17
+ registerUnlockOpener(async (mailboxUid, keys) => {
18
+ await requestUnlock(mailboxUid, keys);
19
+ }),
20
+ [requestUnlock],
21
+ );
22
+ return null;
23
+ }
@@ -0,0 +1,69 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ import type { Attachment, Message } from "@rapidmx/react-shared/mail/mailApi.js";
6
+ import type { PublicKey } from "@rapidmx/react-shared/crypto/keyvaultApi.js";
7
+
8
+ /**
9
+ * A compose window to re-open around a message that was already composed: a failed send's "Open draft". The message keeps its uid - the
10
+ * server draft it was assembled into - and everything the sender had typed comes back exactly as it was, including what could not have
11
+ * been read back from the server (the plain text of a message that was encrypted before it was stored).
12
+ */
13
+ export interface ResumeCompose {
14
+ /** The server draft the window continues - it must be in Drafts (a message that is in Outbox is moved back first). */
15
+ draft: Message;
16
+ mailboxUid: string;
17
+ to: string;
18
+ cc: string;
19
+ bcc: string;
20
+ subject: string;
21
+ html: string;
22
+ attachments: Attachment[];
23
+ requestReceipt: boolean;
24
+ signEnabled: boolean;
25
+ encryptRequested: boolean;
26
+ }
27
+
28
+ /** What the app frame's compose provider offers to code that has no React context (a pop-up's action, a push-event handler). */
29
+ export interface ComposeOpenerInput {
30
+ mailboxUid?: string;
31
+ resume?: ResumeCompose;
32
+ }
33
+
34
+ let opener: ((input: ComposeOpenerInput) => void) | undefined;
35
+ let unlocker: ((mailboxUid: string, keys: PublicKey[]) => Promise<void>) | undefined;
36
+
37
+ /** Registered by `ComposeProvider`; returns the function that unregisters it. */
38
+ export function registerComposeOpener(open: (input: ComposeOpenerInput) => void): () => void {
39
+ opener = open;
40
+ return () => {
41
+ if (opener === open) {
42
+ opener = undefined;
43
+ }
44
+ };
45
+ }
46
+
47
+ /** Opens a compose window from outside React. `false` when no compose provider is mounted (nothing was opened). */
48
+ export function openComposeFromOutside(input: ComposeOpenerInput): boolean {
49
+ if (!opener) {
50
+ return false;
51
+ }
52
+ opener(input);
53
+ return true;
54
+ }
55
+
56
+ /** Registered by the unlock prompt's owner; returns the function that unregisters it. */
57
+ export function registerUnlockOpener(unlock: (mailboxUid: string, keys: PublicKey[]) => Promise<void>): () => void {
58
+ unlocker = unlock;
59
+ return () => {
60
+ if (unlocker === unlock) {
61
+ unlocker = undefined;
62
+ }
63
+ };
64
+ }
65
+
66
+ /** Opens the existing unlock prompt from outside React. Resolves once the keys are unlocked; rejects when the user dismissed it (or there is no prompt). */
67
+ export function requestUnlockFromOutside(mailboxUid: string, keys: PublicKey[]): Promise<void> {
68
+ return unlocker ? unlocker(mailboxUid, keys) : Promise.reject(new Error("No unlock prompt is available."));
69
+ }
@@ -0,0 +1,88 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ import type { Message } from "@rapidmx/react-shared/mail/mailApi.js";
6
+
7
+ /**
8
+ * What a message in Outbox is doing, read from the fields the server keeps on it (`scheduledSend*`):
9
+ *
10
+ * These are the server's own definitions (`@rapidmx/restapi`'s background send):
11
+ *
12
+ * - `sending`: a lease (`scheduledSendLeaseExpiresAt`) is running, or a *due* `scheduledSendTime` with no failed attempts yet - a message
13
+ * queued for the server's worker (a background send is queued as an ordinary due message, `scheduledSendTime` = when it was queued);
14
+ * - `retrying`: `scheduledSendAttempts > 0` with a later `scheduledSendTime` (or a lease on one): a delivery attempt failed and the server
15
+ * will try again;
16
+ * - `failed`: `scheduledSendError` set and no `scheduledSendTime` - the server gave up; it stays in Outbox, waiting for the user to retry
17
+ * (the same send call again) or to open it (back to Drafts is allowed, it is not in flight);
18
+ * - `scheduled`: a send-later message whose time has not come.
19
+ *
20
+ * "Due" is judged against the message's own `dateModified` as well as the clock: a background send's `scheduledSendTime` is set by the server
21
+ * at the moment it queued the message, so a browser whose clock runs behind must not read it as a send scheduled for the future.
22
+ */
23
+ export type OutboxItemState = "sending" | "retrying" | "failed" | "scheduled";
24
+
25
+ export interface OutboxItemStatus {
26
+ state: OutboxItemState;
27
+ /** Failed delivery attempts so far (`retrying`). */
28
+ attempts: number;
29
+ /** Why it failed (`failed`). */
30
+ error?: string;
31
+ /** When it will be sent or tried again (`scheduled`, `retrying`), epoch ms. */
32
+ at?: number;
33
+ }
34
+
35
+ /** A `scheduledSendTime` this close to the message's last modification was set by the server as "now": a due message, not a scheduled one. */
36
+ export const DUE_SLACK_MS = 2_000;
37
+
38
+ export function outboxItemStatus(message: Message, now: number = Date.now()): OutboxItemStatus {
39
+ const attempts = message.scheduledSendAttempts ?? 0;
40
+ const lease = message.scheduledSendLeaseExpiresAt ? Date.parse(message.scheduledSendLeaseExpiresAt) : NaN;
41
+ const at = message.scheduledSendTime ? Date.parse(message.scheduledSendTime) : NaN;
42
+ if (message.scheduledSendError && !message.scheduledSendTime && !(lease > now)) {
43
+ return { state: "failed", attempts, error: message.scheduledSendError };
44
+ }
45
+ if (lease > now) {
46
+ return { state: attempts > 0 ? "retrying" : "sending", attempts };
47
+ }
48
+ if (attempts > 0) {
49
+ return { state: "retrying", attempts, at: Number.isNaN(at) ? undefined : at };
50
+ }
51
+ const modified = message.dateModified ? Date.parse(message.dateModified) : NaN;
52
+ const dueWhenModified = at - modified <= DUE_SLACK_MS;
53
+ if (at > now && !dueWhenModified) {
54
+ return { state: "scheduled", attempts, at };
55
+ }
56
+ return { state: "sending", attempts };
57
+ }
58
+
59
+ /** How many messages of each kind an Outbox folder holds. */
60
+ export interface OutboxFolderStatus {
61
+ sending: number;
62
+ retrying: number;
63
+ failed: number;
64
+ scheduled: number;
65
+ }
66
+
67
+ export const EMPTY_OUTBOX_STATUS: OutboxFolderStatus = { sending: 0, retrying: 0, failed: 0, scheduled: 0 };
68
+
69
+ export function summarizeOutbox(messages: Message[], now: number = Date.now()): OutboxFolderStatus {
70
+ const status = { ...EMPTY_OUTBOX_STATUS };
71
+ for (const message of messages) {
72
+ status[outboxItemStatus(message, now).state] += 1;
73
+ }
74
+ return status;
75
+ }
76
+
77
+ /** The text a screen reader gets for an Outbox count, and the tooltip: "2 messages sending", "3 messages, 1 failed". */
78
+ export function outboxLabel(total: number, status: OutboxFolderStatus | undefined, pendingHere: number): string {
79
+ const noun = `${total} ${total === 1 ? "message" : "messages"}`;
80
+ if (status && status.failed > 0) {
81
+ return `${noun}, ${status.failed} failed`;
82
+ }
83
+ const inFlight = (status ? status.sending + status.retrying : 0) + pendingHere;
84
+ if (inFlight > 0) {
85
+ return `${noun} sending`;
86
+ }
87
+ return status && status.scheduled > 0 && status.scheduled === total ? `${noun} scheduled` : noun;
88
+ }
@@ -0,0 +1,146 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ import { useSyncExternalStore } from "react";
6
+ import { registerComposeFlush } from "../../components/mail/compose/composeFlushRegistry.js";
7
+
8
+ /**
9
+ * The messages this tab has been asked to send whose *client-side* work is not finished: assembling (and encrypting/signing, which takes
10
+ * a moment) and the request that hands them to the server. Once the server has answered `202 queued` a message is safe - the server
11
+ * owns it, and the Outbox folder's own count, list and push events say the rest - so it leaves this set.
12
+ *
13
+ * What this is for: the Outbox indicator counts these at once (before the server has heard of the message), a message in this set is
14
+ * why the tab asks "Leave site?" (`beforeunload`) and why Sign Out waits a moment before ending the session, and a second Send of the same
15
+ * draft is refused while it is here.
16
+ */
17
+ export type PendingStage = "preparing" | "queuing";
18
+
19
+ export interface PendingSend {
20
+ /** The draft's uid: one send per draft at a time. */
21
+ draftUid: string;
22
+ mailboxUid: string;
23
+ subject: string;
24
+ /** Every recipient's address. */
25
+ recipients: string[];
26
+ stage: PendingStage;
27
+ /** A send-later message: it goes to Outbox and waits for its time, rather than being relayed. */
28
+ scheduled: boolean;
29
+ startedAt: number;
30
+ }
31
+
32
+ const NO_SENDS: readonly PendingSend[] = [];
33
+ let pending: PendingSend[] = [];
34
+ let snapshot: readonly PendingSend[] = pending;
35
+ const listeners = new Set<() => void>();
36
+ let stopFlush: (() => void) | undefined;
37
+ let waiters: (() => void)[] = [];
38
+
39
+ function emit(): void {
40
+ snapshot = [...pending];
41
+ for (const listener of [...listeners]) {
42
+ listener();
43
+ }
44
+ }
45
+
46
+ /** `beforeunload`: leaving now would lose a message the server has not received. */
47
+ function handleBeforeUnload(event: BeforeUnloadEvent): void {
48
+ // Only installed while something is pending (see `syncGuards()`).
49
+ event.preventDefault();
50
+ event.returnValue = "";
51
+ }
52
+
53
+ /** Installs the `beforeunload` prompt and the sign-out wait while a send is pending, and takes them away when none is. Only ever called from a browser. */
54
+ function syncGuards(): void {
55
+ if (pending.length > 0 && !stopFlush) {
56
+ window.addEventListener("beforeunload", handleBeforeUnload);
57
+ // Sign Out flushes compose windows before it ends the session; a message still being prepared is waited for there too.
58
+ stopFlush = registerComposeFlush(() => whenSettled(20_000));
59
+ } else if (pending.length === 0 && stopFlush) {
60
+ window.removeEventListener("beforeunload", handleBeforeUnload);
61
+ stopFlush();
62
+ stopFlush = undefined;
63
+ }
64
+ if (pending.length === 0) {
65
+ const done = waiters;
66
+ waiters = [];
67
+ for (const resolve of done) {
68
+ resolve();
69
+ }
70
+ }
71
+ }
72
+
73
+ /** Whether a send of this draft is under way in this tab. */
74
+ export function isSendPending(draftUid: string): boolean {
75
+ return pending.some((entry) => entry.draftUid === draftUid);
76
+ }
77
+
78
+ /** Starts tracking a send. `false` (and nothing tracked) when this draft is already being sent - the caller must not send it again. */
79
+ export function beginPendingSend(entry: Omit<PendingSend, "stage" | "startedAt">): boolean {
80
+ if (isSendPending(entry.draftUid)) {
81
+ return false;
82
+ }
83
+ pending.push({ ...entry, stage: "preparing", startedAt: Date.now() });
84
+ syncGuards();
85
+ emit();
86
+ return true;
87
+ }
88
+
89
+ export function setPendingStage(draftUid: string, stage: PendingStage): void {
90
+ pending = pending.map((entry) => (entry.draftUid === draftUid ? { ...entry, stage } : entry));
91
+ emit();
92
+ }
93
+
94
+ /** The server has the message (or the attempt failed and is now the user's to resolve): it is no longer this tab's work. */
95
+ export function finishPendingSend(draftUid: string): void {
96
+ pending = pending.filter((entry) => entry.draftUid !== draftUid);
97
+ syncGuards();
98
+ emit();
99
+ }
100
+
101
+ export function getPendingSends(): readonly PendingSend[] {
102
+ return snapshot;
103
+ }
104
+
105
+ export function subscribePendingSends(listener: () => void): () => void {
106
+ listeners.add(listener);
107
+ return () => {
108
+ listeners.delete(listener);
109
+ };
110
+ }
111
+
112
+ /** How many messages of `mailboxUid` are still being handed to the server: what the Outbox pill adds to its server count at once. */
113
+ export function pendingCountFor(mailboxUid: string, sends: readonly PendingSend[] = snapshot): number {
114
+ return sends.filter((entry) => entry.mailboxUid === mailboxUid).length;
115
+ }
116
+
117
+ /** Resolves (to whether everything settled) when no send is pending, or after `timeoutMs`. */
118
+ export function whenSettled(timeoutMs: number): Promise<boolean> {
119
+ if (pending.length === 0) {
120
+ return Promise.resolve(true);
121
+ }
122
+ return new Promise<boolean>((resolve) => {
123
+ const timer = setTimeout(() => {
124
+ waiters = waiters.filter((waiter) => waiter !== done);
125
+ resolve(false);
126
+ }, timeoutMs);
127
+ function done() {
128
+ clearTimeout(timer);
129
+ resolve(true);
130
+ }
131
+ waiters.push(done);
132
+ });
133
+ }
134
+
135
+ /** The messages being sent, for a component. */
136
+ export function usePendingSends(): readonly PendingSend[] {
137
+ return useSyncExternalStore(subscribePendingSends, getPendingSends, () => NO_SENDS);
138
+ }
139
+
140
+ /** Forgets everything - for a test. */
141
+ export function resetPendingSends(): void {
142
+ pending = [];
143
+ waiters = [];
144
+ syncGuards();
145
+ emit();
146
+ }