@rapidmx/web-client 0.10.0 → 0.12.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 (324) hide show
  1. package/README.md +183 -2
  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/mailboxes/new/index.tsx +3 -3
  7. package/apps/admin/quarantine/index.tsx +2 -1
  8. package/apps/admin/signing-certificates/index.tsx +343 -0
  9. package/apps/escrow/_layout.tsx +8 -2
  10. package/apps/shared/appearance/AppearanceForm.tsx +392 -0
  11. package/apps/shared/appearance/AppearanceHead.tsx +49 -0
  12. package/apps/shared/appearance/AppearanceProvider.tsx +470 -0
  13. package/apps/shared/appearance/ColorField.tsx +107 -0
  14. package/apps/shared/appearance/appearanceCache.ts +106 -0
  15. package/apps/shared/appearance/appearanceContext.ts +77 -0
  16. package/apps/shared/appearance/bootScript.ts +35 -0
  17. package/apps/shared/appearance/color.ts +169 -0
  18. package/apps/shared/appearance/photo.ts +88 -0
  19. package/apps/shared/appearance/resolvedTheme.ts +67 -0
  20. package/apps/shared/appearance/theme.ts +355 -0
  21. package/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.tsx +4 -4
  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 +60 -14
  26. package/apps/shared/components/admin/settings/MailboxCreateForm.tsx +65 -16
  27. package/apps/shared/components/admin/settings/PluginsManager.tsx +228 -25
  28. package/apps/shared/components/admin/setup/SetupWizard.tsx +2 -1
  29. package/apps/shared/components/calendar/CalendarListSidebar.tsx +4 -5
  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 +25 -52
  33. package/apps/shared/components/contacts/layout/ContactsShell.tsx +10 -5
  34. package/apps/shared/components/escrow/layout/EscrowShell.tsx +13 -6
  35. package/apps/shared/components/layout/AppShell.tsx +98 -32
  36. package/apps/shared/components/layout/BrandingChrome.tsx +200 -13
  37. package/apps/shared/components/layout/RailIcon.tsx +100 -0
  38. package/apps/shared/components/layout/ResponsiveToolbar.tsx +315 -0
  39. package/apps/shared/components/layout/ThemeSwitch.tsx +84 -0
  40. package/apps/shared/components/layout/UserMenu.tsx +75 -6
  41. package/apps/shared/components/mail/ConversationList.tsx +4 -2
  42. package/apps/shared/components/mail/ConversationThreadPane.tsx +487 -475
  43. package/apps/shared/components/mail/LazyReadingPane.tsx +14 -16
  44. package/apps/shared/components/mail/MailSelectionBar.tsx +2 -10
  45. package/apps/shared/components/mail/MessageDetailPane.tsx +366 -287
  46. package/apps/shared/components/mail/OutboxBadge.tsx +44 -0
  47. package/apps/shared/components/mail/OutboxRowStatus.tsx +31 -0
  48. package/apps/shared/components/mail/compose/ComposeContext.tsx +35 -2
  49. package/apps/shared/components/mail/compose/ComposeWindow.tsx +239 -437
  50. package/apps/shared/components/mail/compose/encryptionRequirement.ts +86 -0
  51. package/apps/shared/components/mail/layout/MailShell.tsx +39 -25
  52. package/apps/shared/components/mail/reading/EncryptedBody.tsx +65 -0
  53. package/apps/shared/components/mail/reading/EncryptedPreview.tsx +35 -0
  54. package/apps/shared/components/mail/reading/MessageBody.tsx +240 -0
  55. package/apps/shared/components/mail/reading/MessageCard.tsx +174 -0
  56. package/apps/shared/components/mail/reading/bodyContent.ts +100 -0
  57. package/apps/shared/components/mail/reading/bodyHtml.ts +298 -0
  58. package/apps/shared/components/mail/reading/color.ts +245 -0
  59. package/apps/shared/components/mail/reading/frameControl.ts +142 -0
  60. package/apps/shared/components/mail/reading/frameDocument.ts +123 -0
  61. package/apps/shared/components/mail/reading/safeDocument.ts +24 -0
  62. package/apps/shared/components/mail/reading/themeAdaptation.ts +221 -0
  63. package/apps/shared/components/mail/reading/themeSurface.ts +90 -0
  64. package/apps/shared/components/mail/reading/viewOriginal.ts +38 -0
  65. package/apps/shared/components/settings/SigningCertificateCard.tsx +344 -0
  66. package/apps/shared/components/settings/layout/SettingsShell.tsx +1 -0
  67. package/apps/shared/components/sharing/PrincipalListField.tsx +82 -0
  68. package/apps/shared/components/sharing/PrincipalPicker.tsx +76 -0
  69. package/apps/shared/components/sharing/PrincipalResolver.tsx +164 -0
  70. package/apps/shared/components/tasks/TasksSidebar.tsx +5 -3
  71. package/apps/shared/components/tasks/layout/TasksShell.tsx +9 -5
  72. package/apps/shared/mail/folderCounts.ts +9 -3
  73. package/apps/shared/mail/folderTree.ts +143 -0
  74. package/apps/shared/mail/outbox/UnlockBridge.tsx +23 -0
  75. package/apps/shared/mail/outbox/composeBridge.ts +69 -0
  76. package/apps/shared/mail/outbox/outboxState.ts +88 -0
  77. package/apps/shared/mail/outbox/pendingSends.ts +146 -0
  78. package/apps/shared/mail/outbox/sendDecision.ts +157 -0
  79. package/apps/shared/mail/outbox/sendJob.ts +427 -0
  80. package/apps/shared/mail/outbox/sendOutcomes.ts +146 -0
  81. package/apps/shared/mail/outbox/sendState.ts +35 -0
  82. package/apps/shared/mail/outbox/useOutboxStatus.ts +61 -0
  83. package/apps/shared/mail/useMailConnection.ts +81 -28
  84. package/apps/shared/mail/useMailLiveUpdates.ts +55 -10
  85. package/apps/shared/mail/useNewMailNotifications.ts +47 -32
  86. package/apps/shared/navigation/AppRouter.tsx +2 -2
  87. package/apps/shared/notifications/NotificationCenter.tsx +215 -0
  88. package/apps/shared/notifications/NotificationHistoryDialog.tsx +90 -0
  89. package/apps/shared/notifications/apiErrors.ts +98 -0
  90. package/apps/shared/notifications/headerOffset.ts +35 -0
  91. package/apps/shared/notifications/pushStatus.ts +56 -0
  92. package/apps/shared/notifications/store.ts +549 -0
  93. package/apps/shared/notifications/systemErrors.ts +63 -0
  94. package/apps/shared/notifications/useNotifications.ts +41 -0
  95. package/apps/shared/signing/enrollmentStorage.ts +33 -0
  96. package/apps/shared/signing/enrollmentTracker.ts +385 -0
  97. package/apps/shared/signing/enrollmentView.ts +251 -0
  98. package/apps/shared/signing/signingInfo.ts +58 -0
  99. package/apps/shared/signing/useNow.ts +19 -0
  100. package/apps/shared/signing/useSigningEnrollmentWatcher.ts +90 -0
  101. package/apps/shared/styles/app.css +80 -0
  102. package/apps/www/_layout.tsx +8 -2
  103. package/apps/www/_routes.ts +1 -0
  104. package/apps/www/calendar/index.tsx +9 -14
  105. package/apps/www/contacts/[uid].tsx +5 -8
  106. package/apps/www/contacts/index.tsx +587 -584
  107. package/apps/www/index.tsx +38 -27
  108. package/apps/www/messages/[uid].tsx +3 -1
  109. package/apps/www/settings/appearance/index.tsx +21 -0
  110. package/apps/www/settings/auto-reply/index.tsx +3 -7
  111. package/apps/www/settings/encryption/index.tsx +151 -116
  112. package/apps/www/settings/filters/[uid].tsx +3 -1
  113. package/apps/www/settings/filters/new/index.tsx +3 -1
  114. package/apps/www/settings/privacy/index.tsx +3 -8
  115. package/apps/www/settings/read-receipts/index.tsx +3 -7
  116. package/apps/www/settings/sharing/index.tsx +44 -65
  117. package/apps/www/settings/signatures/[uid].tsx +3 -1
  118. package/apps/www/settings/signatures/new/index.tsx +3 -2
  119. package/apps/www/tasks/index.tsx +11 -16
  120. package/dist/apps/admin/_layout.d.ts +5 -1
  121. package/dist/apps/admin/_layout.js +3 -2
  122. package/dist/apps/admin/index.js +3 -2
  123. package/dist/apps/admin/ingest-queue/index.js +2 -1
  124. package/dist/apps/admin/mailboxes/[uid].js +9 -6
  125. package/dist/apps/admin/mailboxes/new/index.js +1 -1
  126. package/dist/apps/admin/quarantine/index.js +2 -1
  127. package/dist/apps/admin/signing-certificates/index.d.ts +3 -0
  128. package/dist/apps/admin/signing-certificates/index.js +161 -0
  129. package/dist/apps/escrow/_layout.d.ts +5 -1
  130. package/dist/apps/escrow/_layout.js +3 -2
  131. package/dist/apps/shared/appearance/AppearanceForm.d.ts +7 -0
  132. package/dist/apps/shared/appearance/AppearanceForm.js +131 -0
  133. package/dist/apps/shared/appearance/AppearanceHead.d.ts +15 -0
  134. package/dist/apps/shared/appearance/AppearanceHead.js +31 -0
  135. package/dist/apps/shared/appearance/AppearanceProvider.d.ts +32 -0
  136. package/dist/apps/shared/appearance/AppearanceProvider.js +390 -0
  137. package/dist/apps/shared/appearance/ColorField.d.ts +22 -0
  138. package/dist/apps/shared/appearance/ColorField.js +40 -0
  139. package/dist/apps/shared/appearance/appearanceCache.d.ts +45 -0
  140. package/dist/apps/shared/appearance/appearanceCache.js +80 -0
  141. package/dist/apps/shared/appearance/appearanceContext.d.ts +45 -0
  142. package/dist/apps/shared/appearance/appearanceContext.js +29 -0
  143. package/dist/apps/shared/appearance/bootScript.d.ts +16 -0
  144. package/dist/apps/shared/appearance/bootScript.js +33 -0
  145. package/dist/apps/shared/appearance/color.d.ts +65 -0
  146. package/dist/apps/shared/appearance/color.js +132 -0
  147. package/dist/apps/shared/appearance/photo.d.ts +17 -0
  148. package/dist/apps/shared/appearance/photo.js +79 -0
  149. package/dist/apps/shared/appearance/resolvedTheme.d.ts +21 -0
  150. package/dist/apps/shared/appearance/resolvedTheme.js +55 -0
  151. package/dist/apps/shared/appearance/theme.d.ts +111 -0
  152. package/dist/apps/shared/appearance/theme.js +251 -0
  153. package/dist/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.js +3 -2
  154. package/dist/apps/shared/components/admin/layout/AdminShell.d.ts +1 -1
  155. package/dist/apps/shared/components/admin/layout/AdminShell.js +16 -6
  156. package/dist/apps/shared/components/admin/mailboxes/ShareAccessCard.d.ts +10 -5
  157. package/dist/apps/shared/components/admin/mailboxes/ShareAccessCard.js +49 -34
  158. package/dist/apps/shared/components/admin/settings/BrandingForm.d.ts +2 -0
  159. package/dist/apps/shared/components/admin/settings/BrandingForm.js +7 -1
  160. package/dist/apps/shared/components/admin/settings/DomainDnsSetup.js +25 -5
  161. package/dist/apps/shared/components/admin/settings/MailboxCreateForm.js +18 -7
  162. package/dist/apps/shared/components/admin/settings/PluginsManager.js +106 -20
  163. package/dist/apps/shared/components/admin/setup/SetupWizard.js +2 -1
  164. package/dist/apps/shared/components/calendar/CalendarListSidebar.js +8 -8
  165. package/dist/apps/shared/components/contacts/ContactForm.js +1 -1
  166. package/dist/apps/shared/components/contacts/ContactsSidebar.js +4 -2
  167. package/dist/apps/shared/components/contacts/ContactsToolbar.d.ts +3 -1
  168. package/dist/apps/shared/components/contacts/ContactsToolbar.js +24 -9
  169. package/dist/apps/shared/components/contacts/layout/ContactsShell.js +4 -1
  170. package/dist/apps/shared/components/escrow/layout/EscrowShell.js +7 -4
  171. package/dist/apps/shared/components/layout/AppShell.d.ts +6 -1
  172. package/dist/apps/shared/components/layout/AppShell.js +59 -13
  173. package/dist/apps/shared/components/layout/BrandingChrome.d.ts +69 -10
  174. package/dist/apps/shared/components/layout/BrandingChrome.js +140 -12
  175. package/dist/apps/shared/components/layout/RailIcon.d.ts +21 -0
  176. package/dist/apps/shared/components/layout/RailIcon.js +85 -0
  177. package/dist/apps/shared/components/layout/ResponsiveToolbar.d.ts +55 -0
  178. package/dist/apps/shared/components/layout/ResponsiveToolbar.js +199 -0
  179. package/dist/apps/shared/components/layout/ThemeSwitch.d.ts +9 -0
  180. package/dist/apps/shared/components/layout/ThemeSwitch.js +54 -0
  181. package/dist/apps/shared/components/layout/UserMenu.d.ts +7 -1
  182. package/dist/apps/shared/components/layout/UserMenu.js +41 -7
  183. package/dist/apps/shared/components/mail/ConversationList.js +3 -2
  184. package/dist/apps/shared/components/mail/ConversationThreadPane.d.ts +2 -1
  185. package/dist/apps/shared/components/mail/ConversationThreadPane.js +42 -20
  186. package/dist/apps/shared/components/mail/LazyReadingPane.js +9 -8
  187. package/dist/apps/shared/components/mail/MailSelectionBar.d.ts +3 -4
  188. package/dist/apps/shared/components/mail/MailSelectionBar.js +2 -3
  189. package/dist/apps/shared/components/mail/MessageDetailPane.d.ts +18 -7
  190. package/dist/apps/shared/components/mail/MessageDetailPane.js +86 -41
  191. package/dist/apps/shared/components/mail/OutboxBadge.d.ts +17 -0
  192. package/dist/apps/shared/components/mail/OutboxBadge.js +18 -0
  193. package/dist/apps/shared/components/mail/OutboxRowStatus.d.ts +10 -0
  194. package/dist/apps/shared/components/mail/OutboxRowStatus.js +19 -0
  195. package/dist/apps/shared/components/mail/compose/ComposeContext.d.ts +8 -0
  196. package/dist/apps/shared/components/mail/compose/ComposeContext.js +28 -3
  197. package/dist/apps/shared/components/mail/compose/ComposeWindow.d.ts +2 -1
  198. package/dist/apps/shared/components/mail/compose/ComposeWindow.js +229 -328
  199. package/dist/apps/shared/components/mail/compose/encryptionRequirement.d.ts +49 -0
  200. package/dist/apps/shared/components/mail/compose/encryptionRequirement.js +35 -0
  201. package/dist/apps/shared/components/mail/layout/MailShell.d.ts +2 -0
  202. package/dist/apps/shared/components/mail/layout/MailShell.js +20 -14
  203. package/dist/apps/shared/components/mail/reading/EncryptedBody.d.ts +21 -0
  204. package/dist/apps/shared/components/mail/reading/EncryptedBody.js +22 -0
  205. package/dist/apps/shared/components/mail/reading/EncryptedPreview.d.ts +18 -0
  206. package/dist/apps/shared/components/mail/reading/EncryptedPreview.js +22 -0
  207. package/dist/apps/shared/components/mail/reading/MessageBody.d.ts +32 -0
  208. package/dist/apps/shared/components/mail/reading/MessageBody.js +136 -0
  209. package/dist/apps/shared/components/mail/reading/MessageCard.d.ts +62 -0
  210. package/dist/apps/shared/components/mail/reading/MessageCard.js +58 -0
  211. package/dist/apps/shared/components/mail/reading/bodyContent.d.ts +38 -0
  212. package/dist/apps/shared/components/mail/reading/bodyContent.js +84 -0
  213. package/dist/apps/shared/components/mail/reading/bodyHtml.d.ts +47 -0
  214. package/dist/apps/shared/components/mail/reading/bodyHtml.js +267 -0
  215. package/dist/apps/shared/components/mail/reading/color.d.ts +63 -0
  216. package/dist/apps/shared/components/mail/reading/color.js +201 -0
  217. package/dist/apps/shared/components/mail/reading/frameControl.d.ts +33 -0
  218. package/dist/apps/shared/components/mail/reading/frameControl.js +120 -0
  219. package/dist/apps/shared/components/mail/reading/frameDocument.d.ts +52 -0
  220. package/dist/apps/shared/components/mail/reading/frameDocument.js +98 -0
  221. package/dist/apps/shared/components/mail/reading/safeDocument.d.ts +3 -0
  222. package/dist/apps/shared/components/mail/reading/safeDocument.js +12 -0
  223. package/dist/apps/shared/components/mail/reading/themeAdaptation.d.ts +56 -0
  224. package/dist/apps/shared/components/mail/reading/themeAdaptation.js +144 -0
  225. package/dist/apps/shared/components/mail/reading/themeSurface.d.ts +17 -0
  226. package/dist/apps/shared/components/mail/reading/themeSurface.js +83 -0
  227. package/dist/apps/shared/components/mail/reading/viewOriginal.d.ts +9 -0
  228. package/dist/apps/shared/components/mail/reading/viewOriginal.js +32 -0
  229. package/dist/apps/shared/components/settings/SigningCertificateCard.d.ts +32 -0
  230. package/dist/apps/shared/components/settings/SigningCertificateCard.js +98 -0
  231. package/dist/apps/shared/components/settings/layout/SettingsShell.js +1 -0
  232. package/dist/apps/shared/components/sharing/PrincipalListField.d.ts +24 -0
  233. package/dist/apps/shared/components/sharing/PrincipalListField.js +23 -0
  234. package/dist/apps/shared/components/sharing/PrincipalPicker.d.ts +28 -0
  235. package/dist/apps/shared/components/sharing/PrincipalPicker.js +24 -0
  236. package/dist/apps/shared/components/sharing/PrincipalResolver.d.ts +44 -0
  237. package/dist/apps/shared/components/sharing/PrincipalResolver.js +92 -0
  238. package/dist/apps/shared/components/tasks/TasksSidebar.js +4 -2
  239. package/dist/apps/shared/components/tasks/layout/TasksShell.js +3 -1
  240. package/dist/apps/shared/mail/folderCounts.d.ts +1 -1
  241. package/dist/apps/shared/mail/folderCounts.js +9 -3
  242. package/dist/apps/shared/mail/folderTree.d.ts +55 -0
  243. package/dist/apps/shared/mail/folderTree.js +116 -0
  244. package/dist/apps/shared/mail/outbox/UnlockBridge.d.ts +5 -0
  245. package/dist/apps/shared/mail/outbox/UnlockBridge.js +18 -0
  246. package/dist/apps/shared/mail/outbox/composeBridge.d.ts +34 -0
  247. package/dist/apps/shared/mail/outbox/composeBridge.js +32 -0
  248. package/dist/apps/shared/mail/outbox/outboxState.d.ts +41 -0
  249. package/dist/apps/shared/mail/outbox/outboxState.js +42 -0
  250. package/dist/apps/shared/mail/outbox/pendingSends.d.ts +39 -0
  251. package/dist/apps/shared/mail/outbox/pendingSends.js +109 -0
  252. package/dist/apps/shared/mail/outbox/sendDecision.d.ts +80 -0
  253. package/dist/apps/shared/mail/outbox/sendDecision.js +94 -0
  254. package/dist/apps/shared/mail/outbox/sendJob.d.ts +68 -0
  255. package/dist/apps/shared/mail/outbox/sendJob.js +319 -0
  256. package/dist/apps/shared/mail/outbox/sendOutcomes.d.ts +3 -0
  257. package/dist/apps/shared/mail/outbox/sendOutcomes.js +141 -0
  258. package/dist/apps/shared/mail/outbox/sendState.d.ts +25 -0
  259. package/dist/apps/shared/mail/outbox/sendState.js +8 -0
  260. package/dist/apps/shared/mail/outbox/useOutboxStatus.d.ts +11 -0
  261. package/dist/apps/shared/mail/outbox/useOutboxStatus.js +49 -0
  262. package/dist/apps/shared/mail/useMailConnection.d.ts +9 -8
  263. package/dist/apps/shared/mail/useMailConnection.js +64 -22
  264. package/dist/apps/shared/mail/useMailLiveUpdates.d.ts +17 -4
  265. package/dist/apps/shared/mail/useMailLiveUpdates.js +36 -10
  266. package/dist/apps/shared/mail/useNewMailNotifications.d.ts +4 -10
  267. package/dist/apps/shared/mail/useNewMailNotifications.js +43 -23
  268. package/dist/apps/shared/navigation/AppRouter.js +2 -2
  269. package/dist/apps/shared/notifications/NotificationCenter.d.ts +19 -0
  270. package/dist/apps/shared/notifications/NotificationCenter.js +90 -0
  271. package/dist/apps/shared/notifications/NotificationHistoryDialog.d.ts +10 -0
  272. package/dist/apps/shared/notifications/NotificationHistoryDialog.js +36 -0
  273. package/dist/apps/shared/notifications/apiErrors.d.ts +19 -0
  274. package/dist/apps/shared/notifications/apiErrors.js +86 -0
  275. package/dist/apps/shared/notifications/headerOffset.d.ts +8 -0
  276. package/dist/apps/shared/notifications/headerOffset.js +33 -0
  277. package/dist/apps/shared/notifications/pushStatus.d.ts +9 -0
  278. package/dist/apps/shared/notifications/pushStatus.js +54 -0
  279. package/dist/apps/shared/notifications/store.d.ts +136 -0
  280. package/dist/apps/shared/notifications/store.js +416 -0
  281. package/dist/apps/shared/notifications/systemErrors.d.ts +9 -0
  282. package/dist/apps/shared/notifications/systemErrors.js +58 -0
  283. package/dist/apps/shared/notifications/useNotifications.d.ts +17 -0
  284. package/dist/apps/shared/notifications/useNotifications.js +18 -0
  285. package/dist/apps/shared/signing/enrollmentStorage.d.ts +10 -0
  286. package/dist/apps/shared/signing/enrollmentStorage.js +33 -0
  287. package/dist/apps/shared/signing/enrollmentTracker.d.ts +73 -0
  288. package/dist/apps/shared/signing/enrollmentTracker.js +307 -0
  289. package/dist/apps/shared/signing/enrollmentView.d.ts +57 -0
  290. package/dist/apps/shared/signing/enrollmentView.js +214 -0
  291. package/dist/apps/shared/signing/signingInfo.d.ts +11 -0
  292. package/dist/apps/shared/signing/signingInfo.js +48 -0
  293. package/dist/apps/shared/signing/useNow.d.ts +2 -0
  294. package/dist/apps/shared/signing/useNow.js +18 -0
  295. package/dist/apps/shared/signing/useSigningEnrollmentWatcher.d.ts +16 -0
  296. package/dist/apps/shared/signing/useSigningEnrollmentWatcher.js +87 -0
  297. package/dist/apps/shared/styles/app.css +80 -0
  298. package/dist/apps/www/_layout.d.ts +5 -1
  299. package/dist/apps/www/_layout.js +3 -2
  300. package/dist/apps/www/_routes.js +1 -0
  301. package/dist/apps/www/calendar/index.js +3 -4
  302. package/dist/apps/www/contacts/[uid].js +5 -7
  303. package/dist/apps/www/contacts/index.js +14 -21
  304. package/dist/apps/www/index.js +37 -19
  305. package/dist/apps/www/messages/[uid].js +3 -1
  306. package/dist/apps/www/settings/appearance/index.d.ts +4 -0
  307. package/dist/apps/www/settings/appearance/index.js +13 -0
  308. package/dist/apps/www/settings/auto-reply/index.js +3 -6
  309. package/dist/apps/www/settings/encryption/index.js +103 -95
  310. package/dist/apps/www/settings/filters/[uid].js +3 -1
  311. package/dist/apps/www/settings/filters/new/index.js +3 -1
  312. package/dist/apps/www/settings/privacy/index.js +5 -8
  313. package/dist/apps/www/settings/read-receipts/index.js +3 -6
  314. package/dist/apps/www/settings/sharing/index.js +19 -33
  315. package/dist/apps/www/settings/signatures/[uid].js +3 -1
  316. package/dist/apps/www/settings/signatures/new/index.js +3 -2
  317. package/dist/apps/www/tasks/index.js +8 -14
  318. package/package.json +2 -2
  319. package/apps/shared/components/mail/NewMailToasts.tsx +0 -141
  320. package/apps/shared/components/mail/compose/SendFailureAlert.tsx +0 -48
  321. package/dist/apps/shared/components/mail/NewMailToasts.d.ts +0 -19
  322. package/dist/apps/shared/components/mail/NewMailToasts.js +0 -55
  323. package/dist/apps/shared/components/mail/compose/SendFailureAlert.d.ts +0 -14
  324. package/dist/apps/shared/components/mail/compose/SendFailureAlert.js +0 -11
@@ -0,0 +1,116 @@
1
+ /**
2
+ * The sidebar's order of a mailbox's well-known folders; anything else (`user`) follows, alphabetically. Also the order push channels are asked
3
+ * for (`pushChannelsFor()`), so when the server's channel cap bites it is the folders lowest in this list that go without live updates.
4
+ */
5
+ export const FOLDER_ORDER = ["inbox", "drafts", "outbox", "sent_items", "deleted_items", "junk", "archive"];
6
+ /**
7
+ * A mailbox's `calendar`/`contacts`/`tasks`/`notes` folders back their own dedicated apps (see `CalendarShell`/`ContactsShell`/`TasksShell`), not
8
+ * Mail - `listFolders()` returns every well-known folder for the mailbox regardless of which app owns it, so Mail's own folder tree filters down
9
+ * to just the mail ones itself, or those other apps' folders leak into its sidebar.
10
+ */
11
+ export const MAIL_FOLDER_TYPES = new Set([...FOLDER_ORDER, "user"]);
12
+ /** The well-known folders the server creates lazily around a first send: what the sidebar shows a placeholder for while a message is on its way. */
13
+ export const SEND_FOLDER_TYPES = ["outbox", "sent_items"];
14
+ export function isMailFolder(folder) {
15
+ return MAIL_FOLDER_TYPES.has(folder.type);
16
+ }
17
+ function sortKey(type) {
18
+ const index = FOLDER_ORDER.indexOf(type);
19
+ return index === -1 ? FOLDER_ORDER.length : index;
20
+ }
21
+ /** Well-known folders in `FOLDER_ORDER`, then the rest by name. A new array; the input is left alone. */
22
+ export function sortFolders(folders) {
23
+ return [...folders].sort((a, b) => sortKey(a.type) - sortKey(b.type) || a.name.localeCompare(b.name));
24
+ }
25
+ /**
26
+ * The rows of one mailbox's sidebar section, in order: every folder that exists, and - while `sending` messages are on their way and the
27
+ * server has not made the folder yet - a placeholder for each of the Outbox and Sent Items that is missing. The one place a folder that has not
28
+ * arrived is drawn, so it can never appear twice: the moment the real folder is in `folders` its placeholder is not built.
29
+ */
30
+ export function folderRows(folders, sending) {
31
+ const rows = sortFolders(folders).map((folder) => ({ kind: "folder", folder }));
32
+ if (sending > 0) {
33
+ for (const type of SEND_FOLDER_TYPES) {
34
+ if (!folders.some((folder) => folder.type === type)) {
35
+ rows.push({ kind: "placeholder", type });
36
+ }
37
+ }
38
+ rows.sort((a, b) => sortKey(rowType(a)) - sortKey(rowType(b)));
39
+ }
40
+ return rows;
41
+ }
42
+ function rowType(row) {
43
+ return row.kind === "folder" ? row.folder.type : row.type;
44
+ }
45
+ const CHANGEABLE = ["name", "type", "parentFolderUid", "color"];
46
+ /**
47
+ * Files `folder` in the tree: added under its mailbox when unknown (a type Mail does not list is ignored), else its name, type, parent and
48
+ * colour brought up to date (a folder that became a type Mail does not list leaves the tree). Returns `entries` itself - the same array - when
49
+ * nothing changed, because everything on screen that depends on the tree starts over when it is a new one.
50
+ */
51
+ export function upsertFolder(entries, folder) {
52
+ let changed = false;
53
+ const next = entries.map((entry) => {
54
+ const existing = entry.folders.find((f) => f.uid === folder.uid);
55
+ if (!existing) {
56
+ // Only a whole folder can be added: an update for one that is not known has nothing to add it with (the refetch it causes will).
57
+ if (entry.mailbox.uid !== folder.mailboxUid || !isFullFolder(folder) || !isMailFolder(folder)) {
58
+ return entry;
59
+ }
60
+ // A well-known folder is one per mailbox - the server treats the oldest as the real one - so a second of the same type (made by a client that did
61
+ // not know it existed) is not a second row: the older of the two is the one shown.
62
+ const sameType = folder.type === "user" ? undefined : entry.folders.find((f) => f.type === folder.type);
63
+ if (sameType) {
64
+ if (!isOlder(folder, sameType)) {
65
+ return entry;
66
+ }
67
+ changed = true;
68
+ return { ...entry, folders: entry.folders.map((f) => (f === sameType ? folder : f)) };
69
+ }
70
+ changed = true;
71
+ return { ...entry, folders: [...entry.folders, folder] };
72
+ }
73
+ const patch = {};
74
+ for (const key of CHANGEABLE) {
75
+ if (key in folder && folder[key] !== undefined && folder[key] !== existing[key]) {
76
+ patch[key] = folder[key];
77
+ }
78
+ }
79
+ if (Object.keys(patch).length === 0) {
80
+ return entry;
81
+ }
82
+ changed = true;
83
+ const updated = { ...existing, ...patch };
84
+ return { ...entry, folders: isMailFolder(updated) ? entry.folders.map((f) => (f === existing ? updated : f)) : entry.folders.filter((f) => f !== existing) };
85
+ });
86
+ return changed ? next : entries;
87
+ }
88
+ function isFullFolder(folder) {
89
+ return typeof folder.name === "string" && typeof folder.type === "string";
90
+ }
91
+ /** Whether `a` was created before `b`: only when both say when. */
92
+ function isOlder(a, b) {
93
+ // (A folder from the network may lack the date: `Date.parse(undefined)` is NaN, which is "cannot tell".)
94
+ const first = Date.parse(a.dateCreated);
95
+ const second = Date.parse(b.dateCreated);
96
+ return Number.isFinite(first) && Number.isFinite(second) && first < second;
97
+ }
98
+ /** Takes a deleted folder out of the tree. The same array when it was not in it. */
99
+ export function removeFolder(entries, folderUid) {
100
+ if (!entries.some((entry) => entry.folders.some((folder) => folder.uid === folderUid))) {
101
+ return entries;
102
+ }
103
+ return entries.map((entry) => (entry.folders.some((folder) => folder.uid === folderUid) ? { ...entry, folders: entry.folders.filter((folder) => folder.uid !== folderUid) } : entry));
104
+ }
105
+ /**
106
+ * Brings one mailbox's part of the tree up to date with a listing the server just gave (`listFolders()`): folders it has that the tree lacks
107
+ * are added, changed names and types are taken. Nothing is removed - a listing that began before a folder was created here would take it out
108
+ * again, and deletions arrive as events - and the counts are not touched (see `useFolderCounts()`). The same array when there is nothing to do.
109
+ */
110
+ export function reconcileFolders(entries, listed) {
111
+ return listed.reduce((tree, folder) => upsertFolder(tree, folder), entries);
112
+ }
113
+ /** Every folder uid in the tree. */
114
+ export function knownFolderUids(entries) {
115
+ return new Set(entries.flatMap((entry) => entry.folders.map((folder) => folder.uid)));
116
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Lets code with no React context - a pop-up's "Unlock" action - open the app's one unlock prompt (`UnlockPromptProvider`). Renders nothing;
3
+ * mounted once, inside the provider it reaches.
4
+ */
5
+ export default function UnlockBridge(): null;
@@ -0,0 +1,18 @@
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
+ * Lets code with no React context - a pop-up's "Unlock" action - open the app's one unlock prompt (`UnlockPromptProvider`). Renders nothing;
10
+ * mounted once, inside the provider it reaches.
11
+ */
12
+ export default function UnlockBridge() {
13
+ const { requestUnlock } = useUnlockPrompt();
14
+ useEffect(() => registerUnlockOpener(async (mailboxUid, keys) => {
15
+ await requestUnlock(mailboxUid, keys);
16
+ }), [requestUnlock]);
17
+ return null;
18
+ }
@@ -0,0 +1,34 @@
1
+ import type { Attachment, Message } from "@rapidmx/react-shared/mail/mailApi.js";
2
+ import type { PublicKey } from "@rapidmx/react-shared/crypto/keyvaultApi.js";
3
+ /**
4
+ * 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
5
+ * server draft it was assembled into - and everything the sender had typed comes back exactly as it was, including what could not have
6
+ * been read back from the server (the plain text of a message that was encrypted before it was stored).
7
+ */
8
+ export interface ResumeCompose {
9
+ /** The server draft the window continues - it must be in Drafts (a message that is in Outbox is moved back first). */
10
+ draft: Message;
11
+ mailboxUid: string;
12
+ to: string;
13
+ cc: string;
14
+ bcc: string;
15
+ subject: string;
16
+ html: string;
17
+ attachments: Attachment[];
18
+ requestReceipt: boolean;
19
+ signEnabled: boolean;
20
+ encryptRequested: boolean;
21
+ }
22
+ /** What the app frame's compose provider offers to code that has no React context (a pop-up's action, a push-event handler). */
23
+ export interface ComposeOpenerInput {
24
+ mailboxUid?: string;
25
+ resume?: ResumeCompose;
26
+ }
27
+ /** Registered by `ComposeProvider`; returns the function that unregisters it. */
28
+ export declare function registerComposeOpener(open: (input: ComposeOpenerInput) => void): () => void;
29
+ /** Opens a compose window from outside React. `false` when no compose provider is mounted (nothing was opened). */
30
+ export declare function openComposeFromOutside(input: ComposeOpenerInput): boolean;
31
+ /** Registered by the unlock prompt's owner; returns the function that unregisters it. */
32
+ export declare function registerUnlockOpener(unlock: (mailboxUid: string, keys: PublicKey[]) => Promise<void>): () => void;
33
+ /** 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). */
34
+ export declare function requestUnlockFromOutside(mailboxUid: string, keys: PublicKey[]): Promise<void>;
@@ -0,0 +1,32 @@
1
+ let opener;
2
+ let unlocker;
3
+ /** Registered by `ComposeProvider`; returns the function that unregisters it. */
4
+ export function registerComposeOpener(open) {
5
+ opener = open;
6
+ return () => {
7
+ if (opener === open) {
8
+ opener = undefined;
9
+ }
10
+ };
11
+ }
12
+ /** Opens a compose window from outside React. `false` when no compose provider is mounted (nothing was opened). */
13
+ export function openComposeFromOutside(input) {
14
+ if (!opener) {
15
+ return false;
16
+ }
17
+ opener(input);
18
+ return true;
19
+ }
20
+ /** Registered by the unlock prompt's owner; returns the function that unregisters it. */
21
+ export function registerUnlockOpener(unlock) {
22
+ unlocker = unlock;
23
+ return () => {
24
+ if (unlocker === unlock) {
25
+ unlocker = undefined;
26
+ }
27
+ };
28
+ }
29
+ /** 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). */
30
+ export function requestUnlockFromOutside(mailboxUid, keys) {
31
+ return unlocker ? unlocker(mailboxUid, keys) : Promise.reject(new Error("No unlock prompt is available."));
32
+ }
@@ -0,0 +1,41 @@
1
+ import type { Message } from "@rapidmx/react-shared/mail/mailApi.js";
2
+ /**
3
+ * What a message in Outbox is doing, read from the fields the server keeps on it (`scheduledSend*`):
4
+ *
5
+ * These are the server's own definitions (`@rapidmx/restapi`'s background send):
6
+ *
7
+ * - `sending`: a lease (`scheduledSendLeaseExpiresAt`) is running, or a *due* `scheduledSendTime` with no failed attempts yet - a message
8
+ * queued for the server's worker (a background send is queued as an ordinary due message, `scheduledSendTime` = when it was queued);
9
+ * - `retrying`: `scheduledSendAttempts > 0` with a later `scheduledSendTime` (or a lease on one): a delivery attempt failed and the server
10
+ * will try again;
11
+ * - `failed`: `scheduledSendError` set and no `scheduledSendTime` - the server gave up; it stays in Outbox, waiting for the user to retry
12
+ * (the same send call again) or to open it (back to Drafts is allowed, it is not in flight);
13
+ * - `scheduled`: a send-later message whose time has not come.
14
+ *
15
+ * "Due" is judged against the message's own `dateModified` as well as the clock: a background send's `scheduledSendTime` is set by the server
16
+ * 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.
17
+ */
18
+ export type OutboxItemState = "sending" | "retrying" | "failed" | "scheduled";
19
+ export interface OutboxItemStatus {
20
+ state: OutboxItemState;
21
+ /** Failed delivery attempts so far (`retrying`). */
22
+ attempts: number;
23
+ /** Why it failed (`failed`). */
24
+ error?: string;
25
+ /** When it will be sent or tried again (`scheduled`, `retrying`), epoch ms. */
26
+ at?: number;
27
+ }
28
+ /** A `scheduledSendTime` this close to the message's last modification was set by the server as "now": a due message, not a scheduled one. */
29
+ export declare const DUE_SLACK_MS = 2000;
30
+ export declare function outboxItemStatus(message: Message, now?: number): OutboxItemStatus;
31
+ /** How many messages of each kind an Outbox folder holds. */
32
+ export interface OutboxFolderStatus {
33
+ sending: number;
34
+ retrying: number;
35
+ failed: number;
36
+ scheduled: number;
37
+ }
38
+ export declare const EMPTY_OUTBOX_STATUS: OutboxFolderStatus;
39
+ export declare function summarizeOutbox(messages: Message[], now?: number): OutboxFolderStatus;
40
+ /** The text a screen reader gets for an Outbox count, and the tooltip: "2 messages sending", "3 messages, 1 failed". */
41
+ export declare function outboxLabel(total: number, status: OutboxFolderStatus | undefined, pendingHere: number): string;
@@ -0,0 +1,42 @@
1
+ /** A `scheduledSendTime` this close to the message's last modification was set by the server as "now": a due message, not a scheduled one. */
2
+ export const DUE_SLACK_MS = 2000;
3
+ export function outboxItemStatus(message, now = Date.now()) {
4
+ const attempts = message.scheduledSendAttempts ?? 0;
5
+ const lease = message.scheduledSendLeaseExpiresAt ? Date.parse(message.scheduledSendLeaseExpiresAt) : NaN;
6
+ const at = message.scheduledSendTime ? Date.parse(message.scheduledSendTime) : NaN;
7
+ if (message.scheduledSendError && !message.scheduledSendTime && !(lease > now)) {
8
+ return { state: "failed", attempts, error: message.scheduledSendError };
9
+ }
10
+ if (lease > now) {
11
+ return { state: attempts > 0 ? "retrying" : "sending", attempts };
12
+ }
13
+ if (attempts > 0) {
14
+ return { state: "retrying", attempts, at: Number.isNaN(at) ? undefined : at };
15
+ }
16
+ const modified = message.dateModified ? Date.parse(message.dateModified) : NaN;
17
+ const dueWhenModified = at - modified <= DUE_SLACK_MS;
18
+ if (at > now && !dueWhenModified) {
19
+ return { state: "scheduled", attempts, at };
20
+ }
21
+ return { state: "sending", attempts };
22
+ }
23
+ export const EMPTY_OUTBOX_STATUS = { sending: 0, retrying: 0, failed: 0, scheduled: 0 };
24
+ export function summarizeOutbox(messages, now = Date.now()) {
25
+ const status = { ...EMPTY_OUTBOX_STATUS };
26
+ for (const message of messages) {
27
+ status[outboxItemStatus(message, now).state] += 1;
28
+ }
29
+ return status;
30
+ }
31
+ /** The text a screen reader gets for an Outbox count, and the tooltip: "2 messages sending", "3 messages, 1 failed". */
32
+ export function outboxLabel(total, status, pendingHere) {
33
+ const noun = `${total} ${total === 1 ? "message" : "messages"}`;
34
+ if (status && status.failed > 0) {
35
+ return `${noun}, ${status.failed} failed`;
36
+ }
37
+ const inFlight = (status ? status.sending + status.retrying : 0) + pendingHere;
38
+ if (inFlight > 0) {
39
+ return `${noun} sending`;
40
+ }
41
+ return status && status.scheduled > 0 && status.scheduled === total ? `${noun} scheduled` : noun;
42
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * The messages this tab has been asked to send whose *client-side* work is not finished: assembling (and encrypting/signing, which takes
3
+ * a moment) and the request that hands them to the server. Once the server has answered `202 queued` a message is safe - the server
4
+ * owns it, and the Outbox folder's own count, list and push events say the rest - so it leaves this set.
5
+ *
6
+ * 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
7
+ * 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
8
+ * draft is refused while it is here.
9
+ */
10
+ export type PendingStage = "preparing" | "queuing";
11
+ export interface PendingSend {
12
+ /** The draft's uid: one send per draft at a time. */
13
+ draftUid: string;
14
+ mailboxUid: string;
15
+ subject: string;
16
+ /** Every recipient's address. */
17
+ recipients: string[];
18
+ stage: PendingStage;
19
+ /** A send-later message: it goes to Outbox and waits for its time, rather than being relayed. */
20
+ scheduled: boolean;
21
+ startedAt: number;
22
+ }
23
+ /** Whether a send of this draft is under way in this tab. */
24
+ export declare function isSendPending(draftUid: string): boolean;
25
+ /** Starts tracking a send. `false` (and nothing tracked) when this draft is already being sent - the caller must not send it again. */
26
+ export declare function beginPendingSend(entry: Omit<PendingSend, "stage" | "startedAt">): boolean;
27
+ export declare function setPendingStage(draftUid: string, stage: PendingStage): void;
28
+ /** 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. */
29
+ export declare function finishPendingSend(draftUid: string): void;
30
+ export declare function getPendingSends(): readonly PendingSend[];
31
+ export declare function subscribePendingSends(listener: () => void): () => void;
32
+ /** How many messages of `mailboxUid` are still being handed to the server: what the Outbox pill adds to its server count at once. */
33
+ export declare function pendingCountFor(mailboxUid: string, sends?: readonly PendingSend[]): number;
34
+ /** Resolves (to whether everything settled) when no send is pending, or after `timeoutMs`. */
35
+ export declare function whenSettled(timeoutMs: number): Promise<boolean>;
36
+ /** The messages being sent, for a component. */
37
+ export declare function usePendingSends(): readonly PendingSend[];
38
+ /** Forgets everything - for a test. */
39
+ export declare function resetPendingSends(): void;
@@ -0,0 +1,109 @@
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
+ const NO_SENDS = [];
8
+ let pending = [];
9
+ let snapshot = pending;
10
+ const listeners = new Set();
11
+ let stopFlush;
12
+ let waiters = [];
13
+ function emit() {
14
+ snapshot = [...pending];
15
+ for (const listener of [...listeners]) {
16
+ listener();
17
+ }
18
+ }
19
+ /** `beforeunload`: leaving now would lose a message the server has not received. */
20
+ function handleBeforeUnload(event) {
21
+ // Only installed while something is pending (see `syncGuards()`).
22
+ event.preventDefault();
23
+ event.returnValue = "";
24
+ }
25
+ /** 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. */
26
+ function syncGuards() {
27
+ if (pending.length > 0 && !stopFlush) {
28
+ window.addEventListener("beforeunload", handleBeforeUnload);
29
+ // Sign Out flushes compose windows before it ends the session; a message still being prepared is waited for there too.
30
+ stopFlush = registerComposeFlush(() => whenSettled(20000));
31
+ }
32
+ else if (pending.length === 0 && stopFlush) {
33
+ window.removeEventListener("beforeunload", handleBeforeUnload);
34
+ stopFlush();
35
+ stopFlush = undefined;
36
+ }
37
+ if (pending.length === 0) {
38
+ const done = waiters;
39
+ waiters = [];
40
+ for (const resolve of done) {
41
+ resolve();
42
+ }
43
+ }
44
+ }
45
+ /** Whether a send of this draft is under way in this tab. */
46
+ export function isSendPending(draftUid) {
47
+ return pending.some((entry) => entry.draftUid === draftUid);
48
+ }
49
+ /** Starts tracking a send. `false` (and nothing tracked) when this draft is already being sent - the caller must not send it again. */
50
+ export function beginPendingSend(entry) {
51
+ if (isSendPending(entry.draftUid)) {
52
+ return false;
53
+ }
54
+ pending.push({ ...entry, stage: "preparing", startedAt: Date.now() });
55
+ syncGuards();
56
+ emit();
57
+ return true;
58
+ }
59
+ export function setPendingStage(draftUid, stage) {
60
+ pending = pending.map((entry) => (entry.draftUid === draftUid ? { ...entry, stage } : entry));
61
+ emit();
62
+ }
63
+ /** 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. */
64
+ export function finishPendingSend(draftUid) {
65
+ pending = pending.filter((entry) => entry.draftUid !== draftUid);
66
+ syncGuards();
67
+ emit();
68
+ }
69
+ export function getPendingSends() {
70
+ return snapshot;
71
+ }
72
+ export function subscribePendingSends(listener) {
73
+ listeners.add(listener);
74
+ return () => {
75
+ listeners.delete(listener);
76
+ };
77
+ }
78
+ /** How many messages of `mailboxUid` are still being handed to the server: what the Outbox pill adds to its server count at once. */
79
+ export function pendingCountFor(mailboxUid, sends = snapshot) {
80
+ return sends.filter((entry) => entry.mailboxUid === mailboxUid).length;
81
+ }
82
+ /** Resolves (to whether everything settled) when no send is pending, or after `timeoutMs`. */
83
+ export function whenSettled(timeoutMs) {
84
+ if (pending.length === 0) {
85
+ return Promise.resolve(true);
86
+ }
87
+ return new Promise((resolve) => {
88
+ const timer = setTimeout(() => {
89
+ waiters = waiters.filter((waiter) => waiter !== done);
90
+ resolve(false);
91
+ }, timeoutMs);
92
+ function done() {
93
+ clearTimeout(timer);
94
+ resolve(true);
95
+ }
96
+ waiters.push(done);
97
+ });
98
+ }
99
+ /** The messages being sent, for a component. */
100
+ export function usePendingSends() {
101
+ return useSyncExternalStore(subscribePendingSends, getPendingSends, () => NO_SENDS);
102
+ }
103
+ /** Forgets everything - for a test. */
104
+ export function resetPendingSends() {
105
+ pending = [];
106
+ waiters = [];
107
+ syncGuards();
108
+ emit();
109
+ }
@@ -0,0 +1,80 @@
1
+ import { RecipientEncryptionStatus } from "@rapidmx/react-shared/crypto/composeSecurity.js";
2
+ import type { EncryptionPolicy } from "@rapidmx/react-shared/crypto/keyvaultApi.js";
3
+ /** Signed/encrypted bodies are built client-side from the editor's HTML verbatim, so an inline image (an uploaded attachment previewed via
4
+ * its content URL, or a `cid:` reference) would never be carried - only `assembleDraft()`'s server-side plaintext path rewrites those into
5
+ * real MIME parts. */
6
+ export declare const INLINE_IMAGE_PATTERN: RegExp;
7
+ export declare const KEYS_LOCKED_SIGN_MESSAGE = "This message can't be signed right now - your signing key is locked or your mailbox details couldn't be loaded. Unlock and send again, or send it without signing.";
8
+ export declare const KEYS_LOCKED_ENCRYPT_MESSAGE = "This message can't be encrypted right now - your encryption key is locked. Unlock and send again, or send it without encryption.";
9
+ export declare const BCC_ENCRYPTED_MESSAGE = "Bcc recipients can't be used with encrypted messages. Remove Bcc recipients or turn off encryption.";
10
+ export declare const LOOKUP_UNAVAILABLE_MESSAGE = "Your recipients' encryption keys couldn't be checked, so this message can't be encrypted right now. Try again, or send it without encryption.";
11
+ /** The plain sentence for recipients an encrypted message can't reach (no key on file, or the policy prohibits it). */
12
+ export declare function recipientsBlockedMessage(blocked: RecipientEncryptionStatus[]): string;
13
+ export declare const MAILBOX_UNAVAILABLE_MESSAGE = "Your mailbox details couldn't be loaded, so this message can't be encrypted right now. Try again, or send it without encryption.";
14
+ export declare const ATTACHMENTS_UNSUPPORTED_MESSAGE = "A signed or encrypted message cannot include file attachments yet - remove them before sending.";
15
+ export declare const INLINE_IMAGES_UNSUPPORTED_MESSAGE = "A signed or encrypted message cannot include inline images yet - remove them before sending.";
16
+ /** Why a send is refused *before* anything is sent: signing/encryption was asked for (or applies) but cannot happen right now. Never a silent
17
+ * downgrade to plaintext - the sender explicitly picks the override. */
18
+ export interface SendBlock {
19
+ kind: "signing-locked" | "encryption-locked" | "lookup-failed" | "mailbox-unavailable" | "bcc" | "recipients";
20
+ message: string;
21
+ /** The button that sends it anyway, without encryption (and without signing when that is what was blocked). */
22
+ overrideLabel: string;
23
+ /** The cause is keys that are enrolled but not unlocked in this tab: an Unlock action helps. */
24
+ keysLocked: boolean;
25
+ /** `recipients`: who has no key (or is not allowed one). */
26
+ blockedRecipients?: RecipientEncryptionStatus[];
27
+ }
28
+ export type SendDecision = {
29
+ action: "plain";
30
+ } | {
31
+ action: "sign";
32
+ } | {
33
+ action: "encrypt";
34
+ sign: boolean;
35
+ } | {
36
+ action: "blocked";
37
+ block: SendBlock;
38
+ }
39
+ /** Not sendable as composed, and no override helps: fix the message. */
40
+ | {
41
+ action: "rejected";
42
+ message: string;
43
+ };
44
+ export interface SendDecisionInput {
45
+ /** The sender's explicit choice to skip every encryption/signing check (an override button): it never encrypts, and still signs if it can. */
46
+ forcePlaintext: boolean;
47
+ /** The "sign" checkbox. */
48
+ signEnabled: boolean;
49
+ /** The signing option was shown at some point this compose session (the keys were unlocked): losing the keys then blocks, never silently unsigns. */
50
+ offeredSign: boolean;
51
+ encryptRequested: boolean;
52
+ policy: EncryptionPolicy | undefined;
53
+ mailboxLoaded: boolean;
54
+ hasEncryptionKey: boolean;
55
+ keys: {
56
+ /** Signing is possible now: enabled, mailbox loaded (for the protected headers), signing key unlocked. */
57
+ canSign: boolean;
58
+ /** The encryption key is unlocked. */
59
+ canEncryptSelf: boolean;
60
+ /** Any key material is unlocked in this tab. */
61
+ unlocked: boolean;
62
+ };
63
+ /** Every recipient (To, Cc and Bcc), resolved; `undefined` where the lookup failed - or, with `lookupsComplete: false`, was not made. */
64
+ recipients: (RecipientEncryptionStatus | undefined)[];
65
+ /** Whether the lookups were all *attempted*: `false` for the early check a compose window makes from what it already knows. */
66
+ lookupsComplete: boolean;
67
+ hasBcc: boolean;
68
+ hasAttachments: boolean;
69
+ hasInlineImages: boolean;
70
+ }
71
+ /**
72
+ * How a message may go out, decided from the facts alone (no requests, no React): plain, signed, encrypted, refused with an override the sender
73
+ * can choose, or rejected outright. Used twice with the same rules - by the compose window, from what it already knows, to catch a problem
74
+ * *before* it closes (`lookupsComplete: false`), and by the background send, once its recipient lookups are in.
75
+ *
76
+ * Encryption follows `evaluateEncryptionRequirement()` - fail open: only a message the user asked to encrypt, or one the loaded policy
77
+ * encrypts, is encrypted; everything unknown is plain and sends. The one place unknown blocks is a message the user *explicitly* asked to
78
+ * encrypt whose recipients' keys could not be looked up: they asked for encryption, and it cannot be delivered.
79
+ */
80
+ export declare function decideSend(input: SendDecisionInput): SendDecision;
@@ -0,0 +1,94 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ import { decideMessageEncryption } from "@rapidmx/react-shared/crypto/composeSecurity.js";
6
+ import { evaluateEncryptionRequirement } from "../../components/mail/compose/encryptionRequirement.js";
7
+ /** Signed/encrypted bodies are built client-side from the editor's HTML verbatim, so an inline image (an uploaded attachment previewed via
8
+ * its content URL, or a `cid:` reference) would never be carried - only `assembleDraft()`'s server-side plaintext path rewrites those into
9
+ * real MIME parts. */
10
+ export const INLINE_IMAGE_PATTERN = /<img\b[^>]*\bsrc\s*=\s*["']?(?:cid:|[^"'\s>]*\/mail\/attachments\/[^"'\s>]+\/content)/i;
11
+ export const KEYS_LOCKED_SIGN_MESSAGE = "This message can't be signed right now - your signing key is locked or your mailbox details couldn't be loaded. Unlock and send again, or send it without signing.";
12
+ export const KEYS_LOCKED_ENCRYPT_MESSAGE = "This message can't be encrypted right now - your encryption key is locked. Unlock and send again, or send it without encryption.";
13
+ export const BCC_ENCRYPTED_MESSAGE = "Bcc recipients can't be used with encrypted messages. Remove Bcc recipients or turn off encryption.";
14
+ export const LOOKUP_UNAVAILABLE_MESSAGE = "Your recipients' encryption keys couldn't be checked, so this message can't be encrypted right now. Try again, or send it without encryption.";
15
+ /** The plain sentence for recipients an encrypted message can't reach (no key on file, or the policy prohibits it). */
16
+ export function recipientsBlockedMessage(blocked) {
17
+ const reason = blocked[0]?.prohibitedReason ? ` — ${blocked[0].prohibitedReason}` : " has no encryption key on file";
18
+ return `This message can’t be encrypted for everyone: ${blocked.map((r) => r.address).join(", ")}${reason}. Remove ${blocked.length > 1 ? "these recipients" : "this recipient"} from To/Cc/Bcc, or send the whole message in plaintext.`;
19
+ }
20
+ export const MAILBOX_UNAVAILABLE_MESSAGE = "Your mailbox details couldn't be loaded, so this message can't be encrypted right now. Try again, or send it without encryption.";
21
+ export const ATTACHMENTS_UNSUPPORTED_MESSAGE = "A signed or encrypted message cannot include file attachments yet - remove them before sending.";
22
+ export const INLINE_IMAGES_UNSUPPORTED_MESSAGE = "A signed or encrypted message cannot include inline images yet - remove them before sending.";
23
+ /**
24
+ * How a message may go out, decided from the facts alone (no requests, no React): plain, signed, encrypted, refused with an override the sender
25
+ * can choose, or rejected outright. Used twice with the same rules - by the compose window, from what it already knows, to catch a problem
26
+ * *before* it closes (`lookupsComplete: false`), and by the background send, once its recipient lookups are in.
27
+ *
28
+ * Encryption follows `evaluateEncryptionRequirement()` - fail open: only a message the user asked to encrypt, or one the loaded policy
29
+ * encrypts, is encrypted; everything unknown is plain and sends. The one place unknown blocks is a message the user *explicitly* asked to
30
+ * encrypt whose recipients' keys could not be looked up: they asked for encryption, and it cannot be delivered.
31
+ */
32
+ export function decideSend(input) {
33
+ const { keys } = input;
34
+ const attachmentsAreAProblem = (encrypting) => (encrypting || keys.canSign) && input.hasAttachments;
35
+ const imagesAreAProblem = (encrypting) => (encrypting || keys.canSign) && input.hasInlineImages;
36
+ if (input.forcePlaintext) {
37
+ if (attachmentsAreAProblem(false)) {
38
+ return { action: "rejected", message: ATTACHMENTS_UNSUPPORTED_MESSAGE };
39
+ }
40
+ if (imagesAreAProblem(false)) {
41
+ return { action: "rejected", message: INLINE_IMAGES_UNSUPPORTED_MESSAGE };
42
+ }
43
+ return keys.canSign ? { action: "sign" } : { action: "plain" };
44
+ }
45
+ if (input.signEnabled && input.offeredSign && !keys.canSign) {
46
+ return {
47
+ action: "blocked",
48
+ block: { kind: "signing-locked", message: KEYS_LOCKED_SIGN_MESSAGE, overrideLabel: "Send without signing or encryption", keysLocked: !keys.unlocked },
49
+ };
50
+ }
51
+ if (input.encryptRequested && !input.mailboxLoaded) {
52
+ // Asked for explicitly, and there is nothing to encrypt with (the sender's address and keys come from the mailbox).
53
+ return { action: "blocked", block: { kind: "mailbox-unavailable", message: MAILBOX_UNAVAILABLE_MESSAGE, overrideLabel: "Send without encryption", keysLocked: false } };
54
+ }
55
+ const requirement = evaluateEncryptionRequirement({
56
+ encryptRequested: input.encryptRequested,
57
+ policy: input.policy,
58
+ mailboxLoaded: input.mailboxLoaded,
59
+ hasEncryptionKey: input.hasEncryptionKey,
60
+ recipients: input.recipients,
61
+ });
62
+ const encrypting = requirement.required;
63
+ if (encrypting) {
64
+ const statuses = input.recipients.filter((status) => !!status);
65
+ const allResolved = statuses.length === input.recipients.length;
66
+ if (!allResolved && input.lookupsComplete) {
67
+ // Only reachable for an explicit request (a policy requirement needs every status): the sender asked for encryption and the
68
+ // recipients' keys could not be checked.
69
+ return { action: "blocked", block: { kind: "lookup-failed", message: LOOKUP_UNAVAILABLE_MESSAGE, overrideLabel: "Send without encryption", keysLocked: false } };
70
+ }
71
+ if (!keys.canEncryptSelf) {
72
+ return { action: "blocked", block: { kind: "encryption-locked", message: KEYS_LOCKED_ENCRYPT_MESSAGE, overrideLabel: "Send without encryption", keysLocked: !keys.unlocked } };
73
+ }
74
+ if (allResolved) {
75
+ const decision = decideMessageEncryption(statuses);
76
+ if (!decision.canEncryptAll) {
77
+ return {
78
+ action: "blocked",
79
+ block: { kind: "recipients", message: recipientsBlockedMessage(decision.blockedRecipients), overrideLabel: "Send without encryption", keysLocked: false, blockedRecipients: decision.blockedRecipients },
80
+ };
81
+ }
82
+ }
83
+ if (input.hasBcc) {
84
+ return { action: "blocked", block: { kind: "bcc", message: BCC_ENCRYPTED_MESSAGE, overrideLabel: "Send without encryption", keysLocked: false } };
85
+ }
86
+ }
87
+ if (attachmentsAreAProblem(encrypting)) {
88
+ return { action: "rejected", message: ATTACHMENTS_UNSUPPORTED_MESSAGE };
89
+ }
90
+ if (imagesAreAProblem(encrypting)) {
91
+ return { action: "rejected", message: INLINE_IMAGES_UNSUPPORTED_MESSAGE };
92
+ }
93
+ return encrypting ? { action: "encrypt", sign: keys.canSign } : keys.canSign ? { action: "sign" } : { action: "plain" };
94
+ }