@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,319 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
6
+ import { assembleDraft, assembleDraftRaw, getMailbox, getMessage, queueMessageSend, sendMessage, setMessageRequestReceipt, } from "@rapidmx/react-shared/mail/mailApi.js";
7
+ import { describeSendFailure } from "@rapidmx/react-shared/mail/sendFailure.js";
8
+ import { resolveRecipientEncryption } from "@rapidmx/react-shared/crypto/composeSecurity.js";
9
+ import { getUnlockedKeys } from "@rapidmx/react-shared/crypto/keySession.js";
10
+ import { findActivePublicKey, getEncryptionPolicy, lookupKeys } from "@rapidmx/react-shared/crypto/keyvaultApi.js";
11
+ import { fromBase64 } from "@rapidmx/react-shared/crypto/encoding.js";
12
+ import { policyCanAutoEncrypt } from "../../components/mail/compose/encryptionRequirement.js";
13
+ import { dismiss, notify } from "../../notifications/store.js";
14
+ import { INLINE_IMAGE_PATTERN, decideSend } from "./sendDecision.js";
15
+ import { beginPendingSend, finishPendingSend, setPendingStage } from "./pendingSends.js";
16
+ import { openComposeFromOutside, requestUnlockFromOutside } from "./composeBridge.js";
17
+ import { sendState } from "./sendState.js";
18
+ /** How long a background send waits for the mailbox / encryption policy it did not have, before it goes ahead treating them as unknown. Nobody waits for it. */
19
+ export const CONTEXT_GRACE_MS = 4000;
20
+ export function registerOutboxCounter(tracker, onQueued) {
21
+ sendState.countTracker = tracker;
22
+ sendState.queuedListener = onQueued;
23
+ return () => {
24
+ if (sendState.countTracker === tracker) {
25
+ sendState.countTracker = undefined;
26
+ sendState.queuedListener = undefined;
27
+ }
28
+ };
29
+ }
30
+ const RETAINED_LIMIT = 25;
31
+ /** The request a message was queued from, while this tab still holds it: what lets "Open draft" and "Retry" work on a failure the server reports later. */
32
+ export function retainedRequest(draftUid) {
33
+ return sendState.retained.get(draftUid);
34
+ }
35
+ export function forgetRetainedRequest(draftUid) {
36
+ sendState.retained.delete(draftUid);
37
+ }
38
+ function retain(request) {
39
+ sendState.retained.delete(request.draft.uid);
40
+ sendState.retained.set(request.draft.uid, request);
41
+ if (sendState.retained.size > RETAINED_LIMIT) {
42
+ sendState.retained.delete(sendState.retained.keys().next().value);
43
+ }
44
+ }
45
+ const UNSAFE_DISPLAY_NAME_PATTERN = /[@@﹫\r\n]/;
46
+ /** The sender's signing/encryption key state right now, read from the tab's key session (they can lock mid-send). */
47
+ export function readSendKeys(mailboxUid, signEnabled, mailbox) {
48
+ const stored = getUnlockedKeys(mailboxUid);
49
+ const unlocked = stored?.destroyed ? undefined : stored;
50
+ return {
51
+ unlocked,
52
+ canSign: signEnabled && !!mailbox && !!unlocked?.signingPrivateKey && !!unlocked.signingCertDer,
53
+ canEncryptSelf: !!unlocked?.encryptionPrivateKey && !!unlocked.encryptionCertDer,
54
+ };
55
+ }
56
+ /** Whether this sender could encrypt at all: an encryption key unlocked here, enrolled on the mailbox, or offered earlier this session. */
57
+ export function senderHasEncryptionKey(request, canEncryptSelf) {
58
+ return canEncryptSelf || request.offeredEncrypt || !!findActivePublicKey(request.mailbox?.keys ?? [], "encrypt");
59
+ }
60
+ /** The input `decideSend()` reads, from a request and what has been looked up. */
61
+ export function sendDecisionInput(request, mailbox, policy, recipients, lookupsComplete) {
62
+ const keys = readSendKeys(request.mailboxUid, request.signEnabled, mailbox);
63
+ return {
64
+ forcePlaintext: request.forcePlaintext,
65
+ signEnabled: request.signEnabled,
66
+ offeredSign: request.offeredSign,
67
+ encryptRequested: request.encryptRequested,
68
+ policy,
69
+ mailboxLoaded: !!mailbox,
70
+ hasEncryptionKey: senderHasEncryptionKey({ mailbox, offeredEncrypt: request.offeredEncrypt }, keys.canEncryptSelf),
71
+ keys: { canSign: keys.canSign, canEncryptSelf: keys.canEncryptSelf, unlocked: !!keys.unlocked },
72
+ recipients,
73
+ lookupsComplete,
74
+ hasBcc: request.bcc.length > 0,
75
+ hasAttachments: request.attachments.length > 0,
76
+ hasInlineImages: INLINE_IMAGE_PATTERN.test(request.html),
77
+ };
78
+ }
79
+ const OPEN_POLICY = { encryptSameOrg: "optional", encryptFederated: "optional", encryptExternal: "optional" };
80
+ /** The mailbox and policy this send is missing, loaded now: the sender is not waiting, so a slow answer costs nothing - but it is not waited on for ever. */
81
+ async function completeContext(request) {
82
+ let mailbox = request.mailbox;
83
+ let policy = request.policy;
84
+ const keysNow = readSendKeys(request.mailboxUid, request.signEnabled, mailbox);
85
+ const loads = [];
86
+ if (!mailbox) {
87
+ loads.push(getMailbox(request.mailboxUid).then((loaded) => void (mailbox = loaded)));
88
+ }
89
+ if (!policy && (!mailbox || senderHasEncryptionKey({ mailbox, offeredEncrypt: request.offeredEncrypt }, keysNow.canEncryptSelf))) {
90
+ loads.push(getEncryptionPolicy().then((loaded) => void (policy = loaded)));
91
+ }
92
+ if (loads.length > 0) {
93
+ let timer;
94
+ const grace = new Promise((resolve) => {
95
+ timer = setTimeout(resolve, CONTEXT_GRACE_MS);
96
+ });
97
+ await Promise.race([Promise.allSettled(loads), grace]);
98
+ clearTimeout(timer);
99
+ }
100
+ return { mailbox, policy };
101
+ }
102
+ /** Builds the message's final content into the draft (plain, signed, or signed and encrypted in this browser) and returns the stored message. */
103
+ async function assemble(request, mailbox, mode, statuses, withSignature) {
104
+ const { draft, to, cc, bcc, subject, html } = request;
105
+ if (mode === "plain") {
106
+ return assembleDraft(draft.uid, { to, cc, bcc, subject, html });
107
+ }
108
+ const keys = readSendKeys(request.mailboxUid, request.signEnabled, mailbox);
109
+ const own = mailbox;
110
+ const domain = own.primarySmtpAddress.split("@")[1] ?? "localhost";
111
+ const displayName = own.displayName && !UNSAFE_DISPLAY_NAME_PATTERN.test(own.displayName) ? own.displayName : "";
112
+ const protectedHeaders = {
113
+ from: displayName ? `"${displayName.replace(/"/g, '\\"')}" <${own.primarySmtpAddress}>` : own.primarySmtpAddress,
114
+ to: to.map((r) => r.address).join(", "),
115
+ cc: cc.length > 0 ? cc.map((r) => r.address).join(", ") : undefined,
116
+ date: new Date().toUTCString(),
117
+ subject,
118
+ messageId: `<${crypto.randomUUID()}@${domain}>`,
119
+ };
120
+ const bodyContentType = 'text/html; charset="utf-8"';
121
+ const signing = withSignature ? { certDer: keys.unlocked.signingCertDer, privateKey: keys.unlocked.signingPrivateKey } : undefined;
122
+ // The S/MIME code (PKI.js and the ASN.1/X.509 libraries, over half a megabyte) is only needed here, to sign or encrypt - a plain message
123
+ // never loads it, and it is kept out of the compose window's own chunk.
124
+ const { applyBaselineOuterHeaders, assembleOutboundMime, buildEncryptedMessage, buildSignedOnlyMessage } = await import("@rapidmx/react-shared/crypto/smimeMessage.js");
125
+ const encrypt = mode === "encrypt";
126
+ const outerHeaders = encrypt ? applyBaselineOuterHeaders(protectedHeaders) : protectedHeaders;
127
+ const mimePart = encrypt
128
+ ? await buildEncryptedMessage(bodyContentType, html, protectedHeaders, outerHeaders, [keys.unlocked.encryptionCertDer, ...statuses.map((status) => fromBase64(status.encryptCert.publicKey))], signing)
129
+ : await buildSignedOnlyMessage(bodyContentType, html, protectedHeaders, signing.certDer, signing.privateKey);
130
+ const rawMime = assembleOutboundMime(outerHeaders, mimePart);
131
+ return assembleDraftRaw(draft.uid, { to, cc, bcc, subject: outerHeaders.subject, rawMime });
132
+ }
133
+ function failureOf(err, fallback) {
134
+ const failure = describeSendFailure(err, fallback);
135
+ return { message: failure.message, details: failure.lines };
136
+ }
137
+ /** Whether the server already holds `request`'s message as accepted (out of Drafts, or leased) - the answer to "did that request that never returned actually land?". */
138
+ async function alreadyAccepted(request) {
139
+ try {
140
+ const fresh = await getMessage(request.draft.uid);
141
+ return fresh.folderUid !== request.draft.folderUid || !!fresh.scheduledSendLeaseExpiresAt || !!fresh.scheduledSendRelayedAt;
142
+ }
143
+ catch {
144
+ return false;
145
+ }
146
+ }
147
+ /** The whole client-side path of one send: settle, decide, assemble, hand over. Never rejects. */
148
+ async function attempt(request) {
149
+ try {
150
+ await request.saved?.catch(() => undefined);
151
+ const all = [...request.to, ...request.cc, ...request.bcc];
152
+ let mailbox = request.mailbox;
153
+ let policy = request.policy;
154
+ let statuses = all.map(() => undefined);
155
+ let lookupsComplete = false;
156
+ if (!request.forcePlaintext) {
157
+ ({ mailbox, policy } = await completeContext(request));
158
+ const keys = readSendKeys(request.mailboxUid, request.signEnabled, mailbox);
159
+ const hasKey = senderHasEncryptionKey({ mailbox, offeredEncrypt: request.offeredEncrypt }, keys.canEncryptSelf);
160
+ // Recipients' keys are only looked up when the answer can matter: encryption was asked for, or the loaded policy auto-applies.
161
+ if (mailbox && hasKey && (request.encryptRequested || (policy && policyCanAutoEncrypt(policy)))) {
162
+ const own = mailbox;
163
+ const ownPrefersMutual = own.encryptPreference?.preferEncrypt === "mutual";
164
+ const lookups = await Promise.all(all.map((recipient) => lookupKeys(request.mailboxUid, recipient.address).then((lookup) => ({ lookup }), () => null)));
165
+ statuses = all.map((recipient, index) => {
166
+ const found = lookups[index];
167
+ return found ? resolveRecipientEncryption(own.primarySmtpAddress, ownPrefersMutual, policy ?? OPEN_POLICY, recipient.address, found.lookup) : undefined;
168
+ });
169
+ }
170
+ lookupsComplete = true;
171
+ }
172
+ const decision = decideSend(sendDecisionInput(request, mailbox, policy, statuses, lookupsComplete));
173
+ if (decision.action === "blocked") {
174
+ return { kind: "failed", failure: { message: decision.block.message, details: [], block: decision.block } };
175
+ }
176
+ if (decision.action === "rejected") {
177
+ return { kind: "failed", failure: { message: decision.message, details: [] } };
178
+ }
179
+ const assembled = await assemble(request, mailbox, decision.action, statuses, decision.action === "sign" || (decision.action === "encrypt" && decision.sign));
180
+ const withReceipt = request.requestReceipt ? await setMessageRequestReceipt(assembled, true) : assembled;
181
+ setPendingStage(request.draft.uid, "queuing");
182
+ try {
183
+ if (request.scheduledSendTime) {
184
+ await sendMessage(withReceipt.uid, { scheduledSendTime: request.scheduledSendTime });
185
+ return { kind: "scheduled" };
186
+ }
187
+ const result = await queueMessageSend(withReceipt.uid);
188
+ return { kind: result.queued ? "queued" : "sent" };
189
+ }
190
+ catch (err) {
191
+ // A request that never came back may still have landed: never offer to send it twice if it did.
192
+ if (!(err instanceof ApiRequestError) && (await alreadyAccepted(request))) {
193
+ return { kind: request.scheduledSendTime ? "scheduled" : "queued" };
194
+ }
195
+ throw err;
196
+ }
197
+ }
198
+ catch (err) {
199
+ return { kind: "failed", failure: failureOf(err, request.scheduledSendTime ? "Could not schedule this message." : "Could not send this message.") };
200
+ }
201
+ }
202
+ function recipientSummary(request) {
203
+ const all = [...request.to, ...request.cc, ...request.bcc].map((recipient) => recipient.address);
204
+ return all.length <= 2 ? all.join(" and ") : `${all[0]} and ${all.length - 1} others`;
205
+ }
206
+ /** "Message to a@b.c and 2 others (\"Subject\")" - which message a pop-up is about. */
207
+ export function describeMessage(subject, recipients) {
208
+ return `${recipients ? `To ${recipients}` : "Message"}${subject ? ` - "${subject}"` : ""}`;
209
+ }
210
+ /** Re-opens the compose window around a request's message, with everything as it was typed. */
211
+ export function openDraftFromRequest(request) {
212
+ return openComposeFromOutside({
213
+ mailboxUid: request.mailboxUid,
214
+ resume: {
215
+ draft: request.draft,
216
+ mailboxUid: request.mailboxUid,
217
+ to: request.toText,
218
+ cc: request.ccText,
219
+ bcc: request.bccText,
220
+ subject: request.subject,
221
+ html: request.html,
222
+ attachments: request.attachments,
223
+ requestReceipt: request.requestReceipt,
224
+ signEnabled: request.signEnabled,
225
+ encryptRequested: request.encryptRequested,
226
+ },
227
+ });
228
+ }
229
+ function failureNotificationId(draftUid) {
230
+ return `send-failed:${draftUid}`;
231
+ }
232
+ /** The sticky "This message wasn't sent" pop-up for a send that failed on this side, with what can be done about it. */
233
+ function notifyFailure(request, failure) {
234
+ const uid = request.draft.uid;
235
+ const actions = [];
236
+ const { block } = failure;
237
+ if (block?.keysLocked && request.mailbox?.keys?.length) {
238
+ actions.push({
239
+ label: "Unlock",
240
+ onClick: () => {
241
+ void requestUnlockFromOutside(request.mailboxUid, request.mailbox.keys).then(() => void retrySend(request), () => undefined);
242
+ },
243
+ });
244
+ }
245
+ if (block) {
246
+ actions.push({ label: block.overrideLabel, onClick: () => void retrySend({ ...request, forcePlaintext: true }) });
247
+ }
248
+ else {
249
+ actions.push({ label: "Retry", onClick: () => void retrySend(request) });
250
+ }
251
+ actions.push({ label: "Open draft", onClick: () => void openDraftFromRequest(request) });
252
+ notify({
253
+ id: failureNotificationId(uid),
254
+ kind: "error",
255
+ title: "This message wasn't sent",
256
+ message: `${describeMessage(request.subject, recipientSummary(request))}: ${failure.message}`,
257
+ details: failure.details,
258
+ actions,
259
+ });
260
+ }
261
+ const SENT_BURST_MS = 6000;
262
+ /** The short "Message sent" confirmation - several sent close together are one pop-up counting up, never a stack of them. */
263
+ export function notifySent(message = "Message sent") {
264
+ const now = Date.now();
265
+ const { sentBurst } = sendState;
266
+ sendState.sentBurst = { count: now - sentBurst.at > SENT_BURST_MS ? 1 : sentBurst.count + 1, at: now };
267
+ notify({ id: "message-sent", kind: "success", title: sendState.sentBurst.count > 1 ? `${sendState.sentBurst.count} messages sent` : message, timeoutMs: 4000 });
268
+ }
269
+ /**
270
+ * Sends `request`'s draft in the background and returns at once, `true` - or `false` (doing nothing) when a send of this draft is already
271
+ * under way here. The outcome is shown as pop-ups and the Outbox indicator, never as a window that waits: a failure at any stage (assembling,
272
+ * encrypting, the request) is a sticky "This message wasn't sent" with the reason, and Retry / Open draft; the server's own later outcome
273
+ * (`send-succeeded`, `send-failed`, `send-retrying`) is `handleSendEvent()`'s.
274
+ */
275
+ export function startSend(request) {
276
+ const uid = request.draft.uid;
277
+ const recipients = [...request.to, ...request.cc, ...request.bcc].map((recipient) => recipient.address);
278
+ if (!beginPendingSend({ draftUid: uid, mailboxUid: request.mailboxUid, subject: request.subject, recipients, scheduled: !!request.scheduledSendTime })) {
279
+ return false;
280
+ }
281
+ // The failure of an earlier attempt is resolved by trying again.
282
+ dismiss(failureNotificationId(uid));
283
+ const tracker = sendState.countTracker?.(request.mailboxUid);
284
+ void attempt(request).then((outcome) => {
285
+ finishPendingSend(uid);
286
+ if (outcome.kind === "failed") {
287
+ tracker?.revert();
288
+ notifyFailure(request, outcome.failure);
289
+ return;
290
+ }
291
+ tracker?.settle();
292
+ if (outcome.kind === "queued") {
293
+ retain(request);
294
+ sendState.queuedListener?.(request.mailboxUid);
295
+ }
296
+ else if (outcome.kind === "sent") {
297
+ notifySent();
298
+ }
299
+ else {
300
+ notify({
301
+ id: `scheduled:${uid}`,
302
+ kind: "success",
303
+ title: "Message scheduled",
304
+ message: `It will be sent ${new Date(request.scheduledSendTime).toLocaleString()}.`,
305
+ timeoutMs: 5000,
306
+ });
307
+ }
308
+ });
309
+ return true;
310
+ }
311
+ /** Tries a failed send again - unless the server already has the message (an earlier request that seemed to fail did land), which it must not send twice. */
312
+ export async function retrySend(request) {
313
+ if (await alreadyAccepted(request)) {
314
+ dismiss(failureNotificationId(request.draft.uid));
315
+ notify({ id: `already-sent:${request.draft.uid}`, kind: "info", title: "This message is already on its way", timeoutMs: 5000 });
316
+ return;
317
+ }
318
+ startSend({ ...request, saved: undefined });
319
+ }
@@ -0,0 +1,3 @@
1
+ import { SendEvent } from "@rapidmx/react-shared/mail/sendEvents.js";
2
+ /** Shows what a send event means to the user. */
3
+ export declare function handleSendEvent(event: SendEvent): void;
@@ -0,0 +1,141 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ import { cancelScheduledSend, getMessage, listAttachments, listFolders, queueMessageSend } from "@rapidmx/react-shared/mail/mailApi.js";
6
+ import { describeSendEventError } from "@rapidmx/react-shared/mail/sendEvents.js";
7
+ import { formatRecipient } from "../../components/mail/compose/recipients.js";
8
+ import { loadOriginalMessage } from "../../components/mail/compose/quotedBody.js";
9
+ import { notifyApiError } from "../../notifications/apiErrors.js";
10
+ import { dismiss, notify } from "../../notifications/store.js";
11
+ import { beginPendingSend, finishPendingSend, setPendingStage } from "./pendingSends.js";
12
+ import { openComposeFromOutside } from "./composeBridge.js";
13
+ import { describeMessage, forgetRetainedRequest, notifySent, openDraftFromRequest, retainedRequest } from "./sendJob.js";
14
+ /**
15
+ * What the user is told when the server reports how a queued message went (`send-succeeded`, `send-failed`, `send-retrying` push events,
16
+ * see `@rapidmx/react-shared`'s `parseSendEvent()`): a quiet, single "Message sent" for a message this tab sent (several close together are
17
+ * one pop-up counting up), an info pop-up when a delivery is retried, and - only when it finally failed - the sticky "This message wasn't sent" with the
18
+ * server's reason, its technical details and what to do about it. The Outbox count, list and pill update themselves from the same events
19
+ * (`useMailLiveUpdates()` refreshes on them).
20
+ */
21
+ const retryId = (uid) => `send-retry:${uid}`;
22
+ const failedId = (uid) => `send-failed:${uid}`;
23
+ function summary(recipients) {
24
+ return recipients.length <= 2 ? recipients.join(" and ") : `${recipients[0]} and ${recipients.length - 1} others`;
25
+ }
26
+ /** Moves a message the server failed to relay back into Drafts (the only place it can be edited or sent again), and returns it as it is there. */
27
+ async function moveBackToDrafts(uid, mailboxUid) {
28
+ const message = await getMessage(uid);
29
+ const drafts = (await listFolders(mailboxUid)).find((folder) => folder.type === "drafts");
30
+ if (!drafts || message.folderUid === drafts.uid) {
31
+ return message;
32
+ }
33
+ return cancelScheduledSend(message, drafts.uid);
34
+ }
35
+ function escapeHtml(text) {
36
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
37
+ }
38
+ /** "Open draft" for a message the server failed to relay: back into Drafts, and into a compose window with what was written. */
39
+ async function openFailedDraft(event) {
40
+ try {
41
+ const moved = await moveBackToDrafts(event.uid, event.mailboxUid ?? "");
42
+ const request = retainedRequest(event.uid);
43
+ if (request) {
44
+ openDraftFromRequest({ ...request, draft: moved });
45
+ return;
46
+ }
47
+ if (moved.encrypted) {
48
+ notify({
49
+ id: `open-draft:${event.uid}`,
50
+ kind: "info",
51
+ title: "This message is encrypted",
52
+ message: "It was moved back to Drafts, but it can only be edited in the tab that wrote it. Open it from Drafts to send it again.",
53
+ timeoutMs: 8000,
54
+ });
55
+ return;
56
+ }
57
+ const original = await loadOriginalMessage(moved, null);
58
+ const attachments = await listAttachments(moved.folderUid, moved.uid).catch(() => []);
59
+ const byType = (type) => moved.recipients.filter((recipient) => recipient.type === type).map(formatRecipient).join(", ");
60
+ openComposeFromOutside({
61
+ mailboxUid: moved.mailboxUid,
62
+ resume: {
63
+ draft: moved,
64
+ mailboxUid: moved.mailboxUid,
65
+ to: byType("to"),
66
+ cc: byType("cc"),
67
+ bcc: byType("bcc"),
68
+ subject: moved.subject ?? event.subject,
69
+ html: original.body.html ?? (original.body.text ? `<p>${escapeHtml(original.body.text).replace(/\r?\n/g, "<br>")}</p>` : ""),
70
+ attachments,
71
+ requestReceipt: !!moved.requestReceipt,
72
+ signEnabled: true,
73
+ encryptRequested: false,
74
+ },
75
+ });
76
+ }
77
+ catch (err) {
78
+ notifyApiError(err, "Couldn't open this draft");
79
+ }
80
+ }
81
+ /** "Retry" for a message the server failed to relay: the same send call again - a failed message stays in Outbox and the server queues it afresh (with a new retry budget). */
82
+ async function retryFailed(event) {
83
+ if (!beginPendingSend({ draftUid: event.uid, mailboxUid: event.mailboxUid ?? "", subject: event.subject, recipients: event.recipients, scheduled: false })) {
84
+ return;
85
+ }
86
+ dismiss(failedId(event.uid));
87
+ try {
88
+ setPendingStage(event.uid, "queuing");
89
+ const result = await queueMessageSend(event.uid);
90
+ if (!result.queued) {
91
+ notifySent();
92
+ }
93
+ }
94
+ catch (err) {
95
+ notifyApiError(err, "This message wasn't sent", { dedupeKey: failedId(event.uid) });
96
+ }
97
+ finally {
98
+ finishPendingSend(event.uid);
99
+ }
100
+ }
101
+ /** Shows what a send event means to the user. */
102
+ export function handleSendEvent(event) {
103
+ if (event.action === "send-succeeded") {
104
+ dismiss(retryId(event.uid));
105
+ dismiss(failedId(event.uid));
106
+ // The server reports every message it relays, scheduled ones from long ago included: only a message this tab queued in the background is
107
+ // worth a confirmation (several are one pop-up counting up). Anything else - a send-later message going out, another tab's send - is quiet.
108
+ if (retainedRequest(event.uid)) {
109
+ forgetRetainedRequest(event.uid);
110
+ notifySent();
111
+ }
112
+ return;
113
+ }
114
+ const failure = describeSendEventError(event.error, event.action === "send-failed" ? "The server couldn't send this message." : "The server couldn't send this message yet.");
115
+ const what = describeMessage(event.subject, summary(event.recipients));
116
+ if (event.action === "send-retrying") {
117
+ const next = event.nextAttemptAt ? Date.parse(event.nextAttemptAt) : NaN;
118
+ notify({
119
+ id: retryId(event.uid),
120
+ kind: "info",
121
+ title: "Retrying to send",
122
+ message: `${what} (attempt ${event.attempt + 1}${Number.isNaN(next) ? "" : `, at ${new Date(next).toLocaleTimeString()}`}). ${failure.message}`,
123
+ details: failure.lines,
124
+ sticky: false,
125
+ timeoutMs: 8000,
126
+ });
127
+ return;
128
+ }
129
+ dismiss(retryId(event.uid));
130
+ notify({
131
+ id: failedId(event.uid),
132
+ kind: "error",
133
+ title: "This message wasn't sent",
134
+ message: `${what}: ${failure.message}`,
135
+ details: failure.lines,
136
+ actions: [
137
+ { label: "Retry", onClick: () => void retryFailed(event) },
138
+ { label: "Open draft", onClick: () => void openFailedDraft(event) },
139
+ ],
140
+ });
141
+ }
@@ -0,0 +1,25 @@
1
+ import type { SendRequest } from "./sendJob.js";
2
+ /**
3
+ * The module-level state of the background send (`sendJob.ts`), kept in a module with no imports of its own: a test setup can reset it after each
4
+ * test without loading the send code (and everything that code imports) before a test file has had the chance to mock it.
5
+ */
6
+ /** Registered by the frame: puts an optimistic +1 on the Outbox count for `mailboxUid`, at once, and says how to settle or take it back. */
7
+ export interface OutboxCountTracker {
8
+ settle(): void;
9
+ revert(): void;
10
+ }
11
+ export declare const sendState: {
12
+ countTracker: ((mailboxUid: string) => OutboxCountTracker) | undefined;
13
+ /** Called after the server accepted a message into Outbox - the frame reads the folders back (the Outbox may not have existed before). */
14
+ queuedListener: ((mailboxUid: string) => void) | undefined;
15
+ /** What is kept of each message the server accepted, so a later failure reported by push event can offer "Open draft" and "Retry" with
16
+ * the content intact (a message that was encrypted before it was stored can't be read back from the server). Bounded; this tab's only. */
17
+ retained: Map<string, SendRequest>;
18
+ /** "Message sent" confirmations close together are one pop-up counting up. */
19
+ sentBurst: {
20
+ count: number;
21
+ at: number;
22
+ };
23
+ };
24
+ /** Forgets everything - for a test. */
25
+ export declare function resetSendJobs(): void;
@@ -0,0 +1,8 @@
1
+ export const sendState = { countTracker: undefined, queuedListener: undefined, retained: new Map(), sentBurst: { count: 0, at: 0 } };
2
+ /** Forgets everything - for a test. */
3
+ export function resetSendJobs() {
4
+ sendState.countTracker = undefined;
5
+ sendState.queuedListener = undefined;
6
+ sendState.retained.clear();
7
+ sendState.sentBurst = { count: 0, at: 0 };
8
+ }
@@ -0,0 +1,11 @@
1
+ import type { MailboxFolders } from "../../components/mail/layout/MailShell.js";
2
+ import { FolderCount } from "../folderCounts.js";
3
+ import { OutboxFolderStatus } from "./outboxState.js";
4
+ /** The most Outbox messages read to say what state they are in; beyond this the pill still shows the folder's own total. */
5
+ export declare const OUTBOX_STATUS_LIMIT = 50;
6
+ /**
7
+ * What the messages in each Outbox folder are doing (sending / retrying / failed / scheduled), by folder uid - the second line of the Outbox
8
+ * indicator, next to the folder's own count. A folder only costs a request while it holds something (its count is above zero) and is read again
9
+ * whenever that count or the live updates change, so an empty Outbox - almost always - is free. A failed read keeps what was known.
10
+ */
11
+ export declare function useOutboxStatus(mailboxFolders: MailboxFolders[], counts: Record<string, FolderCount>, liveTick: number, enabled: boolean): Record<string, OutboxFolderStatus>;
@@ -0,0 +1,49 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ import { useEffect, useMemo, useState } from "react";
6
+ import { listMessages } from "@rapidmx/react-shared/mail/mailApi.js";
7
+ import { countOfFolder } from "../folderCounts.js";
8
+ import { summarizeOutbox } from "./outboxState.js";
9
+ /** The most Outbox messages read to say what state they are in; beyond this the pill still shows the folder's own total. */
10
+ export const OUTBOX_STATUS_LIMIT = 50;
11
+ /**
12
+ * What the messages in each Outbox folder are doing (sending / retrying / failed / scheduled), by folder uid - the second line of the Outbox
13
+ * indicator, next to the folder's own count. A folder only costs a request while it holds something (its count is above zero) and is read again
14
+ * whenever that count or the live updates change, so an empty Outbox - almost always - is free. A failed read keeps what was known.
15
+ */
16
+ export function useOutboxStatus(mailboxFolders, counts, liveTick, enabled) {
17
+ const [statuses, setStatuses] = useState({});
18
+ const outboxes = useMemo(() => mailboxFolders.flatMap((entry) => entry.folders.filter((folder) => folder.type === "outbox")).map((folder) => ({ uid: folder.uid, total: countOfFolder(folder, counts).total })), [mailboxFolders, counts]);
19
+ const key = outboxes.map((outbox) => `${outbox.uid}:${outbox.total}`).join("|");
20
+ useEffect(() => {
21
+ if (!enabled) {
22
+ return;
23
+ }
24
+ const occupied = outboxes.filter((outbox) => outbox.total > 0);
25
+ setStatuses((previous) => {
26
+ const emptied = Object.keys(previous).filter((uid) => !occupied.some((outbox) => outbox.uid === uid));
27
+ if (emptied.length === 0) {
28
+ return previous;
29
+ }
30
+ const next = { ...previous };
31
+ for (const uid of emptied) {
32
+ delete next[uid];
33
+ }
34
+ return next;
35
+ });
36
+ let cancelled = false;
37
+ for (const outbox of occupied) {
38
+ listMessages(outbox.uid, { limit: OUTBOX_STATUS_LIMIT }).then((messages) => {
39
+ if (!cancelled) {
40
+ setStatuses((previous) => ({ ...previous, [outbox.uid]: summarizeOutbox(messages) }));
41
+ }
42
+ }, () => undefined);
43
+ }
44
+ return () => {
45
+ cancelled = true;
46
+ };
47
+ }, [enabled, key, liveTick]);
48
+ return statuses;
49
+ }
@@ -0,0 +1,56 @@
1
+ import { Folder, Mailbox } from "@rapidmx/react-shared/mail/mailApi.js";
2
+ import { FOLDER_ORDER, MAIL_FOLDER_TYPES } from "./folderTree.js";
3
+ import type { MailboxFolders } from "../components/mail/layout/MailShell.js";
4
+ import { LiveUpdates } from "./useMailLiveUpdates.js";
5
+ import type { FolderCounts } from "./folderCounts.js";
6
+ import { NewMailNotifications } from "./useNewMailNotifications.js";
7
+ import { OutboxFolderStatus } from "./outbox/outboxState.js";
8
+ /** Page size of the one `listMailboxes()` call. */
9
+ export declare const MAILBOX_LIST_LIMIT = 100;
10
+ export { FOLDER_ORDER, MAIL_FOLDER_TYPES };
11
+ export type MailConnectionStatus = "checking" | "error" | "ready";
12
+ /** Everything the mail apps share about the user's mailboxes, and the one live connection that keeps it current. */
13
+ export interface MailConnection {
14
+ /** Where the one `listMailboxes()` call is. */
15
+ status: MailConnectionStatus;
16
+ /** Why `status` is `"error"`. */
17
+ error: string | null;
18
+ mailboxes: Mailbox[];
19
+ /** Every accessible mailbox's own mail folders, one entry per mailbox - see `MailboxFolders`. */
20
+ mailboxFolders: MailboxFolders[];
21
+ /** Their folder lists are still being fetched. */
22
+ foldersLoading: boolean;
23
+ /** Files a folder that was just created (by this client or another) under its mailbox, so the sidebar and every folder picker show it. */
24
+ onFolderCreated: (folder: Folder) => void;
25
+ /** Says which folders some message the page has met lives in (a list's rows, a conversation's folders): one the sidebar does not know is asked for
26
+ * - its mailbox's folders are listed again, once, debounced - and files it. Each unknown uid is asked about once, so a folder Mail never lists
27
+ * (another app's) cannot make the page ask again and again. */
28
+ noteFolderUids: (folderUids: Iterable<string>) => void;
29
+ /** Bumped whenever new mail may have arrived - see `useMailLiveUpdates()`. */
30
+ live: LiveUpdates;
31
+ /** The folder badges' numbers and how to change them. */
32
+ folderCounts: FolderCounts;
33
+ /** The new-mail pop-ups and desktop notifications, and what feeds them. */
34
+ notifications: NewMailNotifications;
35
+ /** What the messages in each Outbox folder are doing, by folder uid (the Outbox indicator's state) - see `useOutboxStatus()`. */
36
+ outbox: Record<string, OutboxFolderStatus>;
37
+ }
38
+ /** What the persistent app frame (`AppChrome`) offers to the Mail shell so that it doesn't open a second connection. `null` outside a frame. */
39
+ export declare const MailConnectionContext: import("react").Context<MailConnection | null>;
40
+ export interface UseMailConnectionOptions {
41
+ /** Nothing is fetched, and no socket opened, without a signed-in user. */
42
+ userUid?: string;
43
+ /** Runs only while true. The frame turns it on; a Mail shell rendered outside a frame turns it on for itself; and the one that isn't
44
+ * the owner leaves it off - so exactly one connection exists, whichever of them the page is rendered by. */
45
+ enabled: boolean;
46
+ /** Opens a message from a desktop notification's click (the router's `useNavigate()`); defaults to a plain navigation. */
47
+ open?: (href: string) => void;
48
+ }
49
+ /**
50
+ * The user's mailboxes and their mail folders, plus the live connection that keeps them - and the badges, the list, the new-mail
51
+ * pop-ups - current. It is everything `MailShell` used to do on mount, gathered so that it can live in the persistent app frame instead: the
52
+ * push socket (the server allows ten per user), the safety-net poll, the folder counters and the pop-ups then belong to the signed-in session
53
+ * rather than to whichever page happens to be showing, and keep working in Calendar, Contacts, Tasks and Settings. `MailShell` reads the same
54
+ * state from `MailConnectionContext` when there is a frame, and runs this hook itself when there isn't (a page rendered outside the router).
55
+ */
56
+ export declare function useMailConnection({ userUid, enabled, open }: UseMailConnectionOptions): MailConnection;