@rapidmx/web-client 0.15.1 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (318) hide show
  1. package/README.md +381 -373
  2. package/apps/admin/branding/index.tsx +39 -39
  3. package/apps/admin/data-requests/index.tsx +482 -480
  4. package/apps/admin/distribution-lists/new/index.tsx +3 -1
  5. package/apps/admin/domains/[uid].tsx +166 -164
  6. package/apps/admin/domains/new/index.tsx +3 -1
  7. package/apps/admin/escrow-scopes/[uid].tsx +350 -348
  8. package/apps/admin/escrow-scopes/new/index.tsx +3 -1
  9. package/apps/admin/index.tsx +129 -124
  10. package/apps/admin/mailboxes/[uid].tsx +271 -224
  11. package/apps/admin/mailboxes/new/index.tsx +30 -28
  12. package/apps/admin/plugins/index.tsx +15 -15
  13. package/apps/admin/retention-policy/index.tsx +39 -39
  14. package/apps/admin/signing-certificates/index.tsx +343 -343
  15. package/apps/admin/transport-rules/new/index.tsx +3 -1
  16. package/apps/escrow/audit-log/index.tsx +196 -196
  17. package/apps/escrow/matters/[uid].tsx +621 -621
  18. package/apps/escrow/matters/new/index.tsx +3 -1
  19. package/apps/shared/auth/adminAccess.ts +99 -99
  20. package/apps/shared/components/admin/layout/AdminShell.tsx +381 -378
  21. package/apps/shared/components/admin/mailboxes/EraseLeftoverDataDialog.tsx +238 -0
  22. package/apps/shared/components/admin/mailboxes/EscrowScopeCard.tsx +152 -152
  23. package/apps/shared/components/admin/mailboxes/LeftoverMailboxesSection.tsx +194 -0
  24. package/apps/shared/components/admin/settings/BrandingForm.tsx +423 -423
  25. package/apps/shared/components/admin/settings/EncryptionPolicyForm.tsx +119 -119
  26. package/apps/shared/components/admin/settings/MailboxCreateForm.tsx +50 -3
  27. package/apps/shared/components/admin/settings/MailboxPolicyForm.tsx +198 -198
  28. package/apps/shared/components/admin/settings/PluginsManager.tsx +1545 -1545
  29. package/apps/shared/components/admin/settings/RetentionPolicyForm.tsx +182 -182
  30. package/apps/shared/components/admin/setup/EscrowSetupStep.tsx +288 -288
  31. package/apps/shared/components/admin/setup/SetupWizard.tsx +446 -444
  32. package/apps/shared/components/admin/usePagedList.tsx +129 -129
  33. package/apps/shared/components/calendar/EventEditor.tsx +5 -10
  34. package/apps/shared/components/calendar/EventFormParts.tsx +16 -47
  35. package/apps/shared/components/calendar/EventModal.tsx +206 -206
  36. package/apps/shared/components/calendar/GuestInput.tsx +100 -0
  37. package/apps/shared/components/calendar/MonthView.tsx +185 -185
  38. package/apps/shared/components/calendar/RecurrenceEditor.tsx +227 -227
  39. package/apps/shared/components/calendar/RequestChangeForm.tsx +23 -54
  40. package/apps/shared/components/calendar/SplitDayView.tsx +144 -144
  41. package/apps/shared/components/calendar/TimeGridView.tsx +246 -246
  42. package/apps/shared/components/calendar/allDay.ts +124 -124
  43. package/apps/shared/components/calendar/eventForm.ts +2 -2
  44. package/apps/shared/components/calendar/eventFormat.ts +49 -22
  45. package/apps/shared/components/calendar/layout/CalendarShell.tsx +201 -200
  46. package/apps/shared/components/contacts/ContactForm.tsx +383 -383
  47. package/apps/shared/components/contacts/ContactsSidebar.tsx +1 -1
  48. package/apps/shared/components/contacts/ContactsToolbar.tsx +103 -98
  49. package/apps/shared/components/contacts/layout/ContactsShell.tsx +218 -216
  50. package/apps/shared/components/escrow/layout/EscrowShell.tsx +155 -155
  51. package/apps/shared/components/layout/AppShell.tsx +461 -480
  52. package/apps/shared/components/layout/FloatingActionButton.tsx +31 -0
  53. package/apps/shared/components/layout/KeyEnrollmentGate.tsx +398 -398
  54. package/apps/shared/components/layout/MailboxProvisioning.tsx +168 -168
  55. package/apps/shared/components/layout/ResponsiveToolbar.tsx +315 -315
  56. package/apps/shared/components/layout/ThemeSwitch.tsx +84 -84
  57. package/apps/shared/components/layout/UserMenu.tsx +439 -424
  58. package/apps/shared/components/mail/ConversationList.tsx +323 -323
  59. package/apps/shared/components/mail/ConversationThreadPane.tsx +613 -491
  60. package/apps/shared/components/mail/EmptyFolderBar.tsx +41 -0
  61. package/apps/shared/components/mail/MailSelectionBar.tsx +240 -227
  62. package/apps/shared/components/mail/MenuButton.tsx +31 -11
  63. package/apps/shared/components/mail/MessageDetailPane.tsx +1757 -1559
  64. package/apps/shared/components/mail/PermanentDeleteDialog.tsx +47 -0
  65. package/apps/shared/components/mail/compose/ComposeContext.tsx +312 -312
  66. package/apps/shared/components/mail/compose/ComposeToolbar.tsx +422 -422
  67. package/apps/shared/components/mail/compose/ComposeWindow.tsx +1681 -1677
  68. package/apps/shared/components/mail/compose/RecipientInput.tsx +54 -11
  69. package/apps/shared/components/mail/compose/RichTextEditor.tsx +147 -147
  70. package/apps/shared/components/mail/compose/composeFlushRegistry.ts +60 -60
  71. package/apps/shared/components/mail/compose/quotedBody.ts +161 -161
  72. package/apps/shared/components/mail/layout/FolderBadgeChip.tsx +21 -0
  73. package/apps/shared/components/mail/layout/MailShell.tsx +161 -134
  74. package/apps/shared/components/mail/layout/SidebarSection.tsx +66 -0
  75. package/apps/shared/components/mail/reading/MessageMoreMenu.tsx +258 -0
  76. package/apps/shared/components/mail/reading/MessageSourceDialog.tsx +79 -0
  77. package/apps/shared/components/mail/reading/PendingMessageCard.tsx +68 -0
  78. package/apps/shared/components/mail/reading/messageExport.ts +59 -0
  79. package/apps/shared/components/mail/reading/printMessage.ts +99 -0
  80. package/apps/shared/components/mail/reading/useMessageActions.ts +443 -0
  81. package/apps/shared/components/mail/verificationSeals.ts +125 -125
  82. package/apps/shared/components/rules/RuleBuilder.tsx +311 -311
  83. package/apps/shared/components/settings/SigningCertificateCard.tsx +344 -344
  84. package/apps/shared/components/settings/layout/SettingsShell.tsx +257 -254
  85. package/apps/shared/components/tasks/TasksSidebar.tsx +1 -1
  86. package/apps/shared/components/tasks/layout/TasksShell.tsx +219 -217
  87. package/apps/shared/keyboard/GlobalShortcuts.tsx +51 -51
  88. package/apps/shared/keyboard/ShortcutProvider.tsx +62 -62
  89. package/apps/shared/keyboard/ShortcutsDialog.tsx +84 -84
  90. package/apps/shared/keyboard/dispatch.ts +124 -124
  91. package/apps/shared/keyboard/format.ts +89 -89
  92. package/apps/shared/keyboard/keymap.ts +114 -114
  93. package/apps/shared/keyboard/registry.ts +65 -65
  94. package/apps/shared/keyboard/targets.ts +79 -79
  95. package/apps/shared/mail/folderCounts.ts +17 -0
  96. package/apps/shared/mail/folderOfType.ts +49 -0
  97. package/apps/shared/mail/folderTree.ts +143 -143
  98. package/apps/shared/mail/listAllPages.ts +39 -39
  99. package/apps/shared/mail/newMailNotifications.ts +171 -171
  100. package/apps/shared/mail/outbox/outgoingReplies.ts +192 -0
  101. package/apps/shared/mail/outbox/sendJob.ts +19 -1
  102. package/apps/shared/mail/outbox/sendOutcomes.ts +13 -4
  103. package/apps/shared/mail/permanentDelete.ts +160 -0
  104. package/apps/shared/mail/primaryMailbox.ts +71 -0
  105. package/apps/shared/mail/reportNotices.ts +66 -0
  106. package/apps/shared/mail/senderBlocking.ts +141 -0
  107. package/apps/shared/mail/useCollapsedSections.ts +146 -0
  108. package/apps/shared/mail/useMailConnection.ts +205 -203
  109. package/apps/shared/mail/useMailLiveUpdates.ts +277 -277
  110. package/apps/shared/mail/useMailboxUpdateAccess.ts +60 -0
  111. package/apps/shared/mail/useMarkMessageRead.ts +47 -47
  112. package/apps/shared/mail/useNewMailNotifications.ts +178 -178
  113. package/apps/shared/mail/usePermanentDelete.tsx +160 -0
  114. package/apps/shared/navigation/frameContext.tsx +1 -1
  115. package/apps/shared/navigation/index.ts +43 -0
  116. package/apps/shared/navigation/pageTitle.ts +19 -0
  117. package/apps/shared/notifications/store.ts +560 -560
  118. package/apps/shared/search/LocalIndexLifecycle.tsx +114 -114
  119. package/apps/shared/search/crossMailboxSearch.ts +169 -0
  120. package/apps/shared/search/localIndexBuilder.ts +481 -481
  121. package/apps/shared/signing/enrollmentStorage.ts +33 -33
  122. package/apps/shared/signing/enrollmentTracker.ts +385 -385
  123. package/apps/shared/signing/enrollmentView.ts +251 -251
  124. package/apps/shared/signing/useNow.ts +19 -19
  125. package/apps/shared/signing/useSigningEnrollmentWatcher.ts +90 -90
  126. package/apps/shared/styles/app.css +396 -386
  127. package/apps/www/_shell.tsx +82 -0
  128. package/apps/www/calendar/index.tsx +581 -571
  129. package/apps/www/contacts/[uid].tsx +112 -109
  130. package/apps/www/contacts/index.tsx +14 -3
  131. package/apps/www/index.tsx +2962 -2479
  132. package/apps/www/messages/[uid].tsx +139 -135
  133. package/apps/www/settings/appearance/index.tsx +5 -2
  134. package/apps/www/settings/auto-reply/index.tsx +136 -133
  135. package/apps/www/settings/blocked-senders/index.tsx +303 -0
  136. package/apps/www/settings/encryption/index.tsx +1290 -1287
  137. package/apps/www/settings/filters/[uid].tsx +179 -176
  138. package/apps/www/settings/filters/index.tsx +105 -102
  139. package/apps/www/settings/filters/new/index.tsx +165 -140
  140. package/apps/www/settings/labels/index.tsx +207 -204
  141. package/apps/www/settings/privacy/index.tsx +495 -492
  142. package/apps/www/settings/profile/index.tsx +251 -248
  143. package/apps/www/settings/read-receipts/index.tsx +150 -147
  144. package/apps/www/settings/sharing/index.tsx +259 -256
  145. package/apps/www/settings/signatures/[uid].tsx +175 -172
  146. package/apps/www/settings/signatures/index.tsx +91 -88
  147. package/apps/www/settings/signatures/new/index.tsx +138 -135
  148. package/apps/www/tasks/index.tsx +654 -649
  149. package/dist/apps/admin/data-requests/index.js +1 -1
  150. package/dist/apps/admin/distribution-lists/new/index.js +3 -1
  151. package/dist/apps/admin/domains/[uid].js +3 -1
  152. package/dist/apps/admin/domains/new/index.js +3 -1
  153. package/dist/apps/admin/escrow-scopes/[uid].js +3 -1
  154. package/dist/apps/admin/escrow-scopes/new/index.js +3 -1
  155. package/dist/apps/admin/index.js +5 -2
  156. package/dist/apps/admin/mailboxes/[uid].js +15 -3
  157. package/dist/apps/admin/mailboxes/new/index.js +3 -1
  158. package/dist/apps/admin/transport-rules/new/index.js +3 -1
  159. package/dist/apps/escrow/matters/new/index.js +3 -1
  160. package/dist/apps/shared/components/admin/layout/AdminShell.js +6 -3
  161. package/dist/apps/shared/components/admin/mailboxes/EraseLeftoverDataDialog.d.ts +35 -0
  162. package/dist/apps/shared/components/admin/mailboxes/EraseLeftoverDataDialog.js +115 -0
  163. package/dist/apps/shared/components/admin/mailboxes/LeftoverMailboxesSection.d.ts +16 -0
  164. package/dist/apps/shared/components/admin/mailboxes/LeftoverMailboxesSection.js +92 -0
  165. package/dist/apps/shared/components/admin/settings/BrandingForm.js +3 -3
  166. package/dist/apps/shared/components/admin/settings/MailboxCreateForm.js +25 -3
  167. package/dist/apps/shared/components/admin/setup/SetupWizard.js +3 -1
  168. package/dist/apps/shared/components/calendar/EventEditor.js +5 -10
  169. package/dist/apps/shared/components/calendar/EventFormParts.d.ts +3 -2
  170. package/dist/apps/shared/components/calendar/EventFormParts.js +5 -16
  171. package/dist/apps/shared/components/calendar/GuestInput.d.ts +45 -0
  172. package/dist/apps/shared/components/calendar/GuestInput.js +30 -0
  173. package/dist/apps/shared/components/calendar/RequestChangeForm.js +11 -22
  174. package/dist/apps/shared/components/calendar/eventForm.d.ts +2 -2
  175. package/dist/apps/shared/components/calendar/eventFormat.d.ts +21 -8
  176. package/dist/apps/shared/components/calendar/eventFormat.js +50 -23
  177. package/dist/apps/shared/components/calendar/layout/CalendarShell.js +5 -4
  178. package/dist/apps/shared/components/contacts/ContactsSidebar.js +1 -1
  179. package/dist/apps/shared/components/contacts/ContactsToolbar.d.ts +3 -1
  180. package/dist/apps/shared/components/contacts/ContactsToolbar.js +4 -2
  181. package/dist/apps/shared/components/contacts/layout/ContactsShell.js +7 -5
  182. package/dist/apps/shared/components/layout/AppShell.d.ts +8 -11
  183. package/dist/apps/shared/components/layout/AppShell.js +24 -33
  184. package/dist/apps/shared/components/layout/FloatingActionButton.d.ts +13 -0
  185. package/dist/apps/shared/components/layout/FloatingActionButton.js +9 -0
  186. package/dist/apps/shared/components/layout/UserMenu.d.ts +4 -1
  187. package/dist/apps/shared/components/layout/UserMenu.js +6 -5
  188. package/dist/apps/shared/components/mail/ConversationThreadPane.js +127 -22
  189. package/dist/apps/shared/components/mail/EmptyFolderBar.d.ts +18 -0
  190. package/dist/apps/shared/components/mail/EmptyFolderBar.js +10 -0
  191. package/dist/apps/shared/components/mail/MailSelectionBar.d.ts +14 -4
  192. package/dist/apps/shared/components/mail/MailSelectionBar.js +11 -13
  193. package/dist/apps/shared/components/mail/MenuButton.d.ts +9 -2
  194. package/dist/apps/shared/components/mail/MenuButton.js +15 -11
  195. package/dist/apps/shared/components/mail/MessageDetailPane.d.ts +5 -0
  196. package/dist/apps/shared/components/mail/MessageDetailPane.js +147 -7
  197. package/dist/apps/shared/components/mail/PermanentDeleteDialog.d.ts +19 -0
  198. package/dist/apps/shared/components/mail/PermanentDeleteDialog.js +11 -0
  199. package/dist/apps/shared/components/mail/compose/ComposeWindow.js +8 -4
  200. package/dist/apps/shared/components/mail/compose/RecipientInput.d.ts +21 -1
  201. package/dist/apps/shared/components/mail/compose/RecipientInput.js +18 -10
  202. package/dist/apps/shared/components/mail/layout/FolderBadgeChip.d.ts +6 -0
  203. package/dist/apps/shared/components/mail/layout/FolderBadgeChip.js +9 -0
  204. package/dist/apps/shared/components/mail/layout/MailShell.d.ts +16 -7
  205. package/dist/apps/shared/components/mail/layout/MailShell.js +78 -65
  206. package/dist/apps/shared/components/mail/layout/SidebarSection.d.ts +23 -0
  207. package/dist/apps/shared/components/mail/layout/SidebarSection.js +16 -0
  208. package/dist/apps/shared/components/mail/reading/MessageMoreMenu.d.ts +56 -0
  209. package/dist/apps/shared/components/mail/reading/MessageMoreMenu.js +153 -0
  210. package/dist/apps/shared/components/mail/reading/MessageSourceDialog.d.ts +27 -0
  211. package/dist/apps/shared/components/mail/reading/MessageSourceDialog.js +27 -0
  212. package/dist/apps/shared/components/mail/reading/PendingMessageCard.d.ts +14 -0
  213. package/dist/apps/shared/components/mail/reading/PendingMessageCard.js +16 -0
  214. package/dist/apps/shared/components/mail/reading/messageExport.d.ts +19 -0
  215. package/dist/apps/shared/components/mail/reading/messageExport.js +51 -0
  216. package/dist/apps/shared/components/mail/reading/printMessage.d.ts +30 -0
  217. package/dist/apps/shared/components/mail/reading/printMessage.js +70 -0
  218. package/dist/apps/shared/components/mail/reading/useMessageActions.d.ts +57 -0
  219. package/dist/apps/shared/components/mail/reading/useMessageActions.js +366 -0
  220. package/dist/apps/shared/components/settings/layout/SettingsShell.js +10 -7
  221. package/dist/apps/shared/components/tasks/TasksSidebar.js +1 -1
  222. package/dist/apps/shared/components/tasks/layout/TasksShell.js +7 -5
  223. package/dist/apps/shared/keyboard/GlobalShortcuts.js +1 -1
  224. package/dist/apps/shared/mail/folderCounts.d.ts +10 -0
  225. package/dist/apps/shared/mail/folderCounts.js +12 -0
  226. package/dist/apps/shared/mail/folderOfType.d.ts +13 -0
  227. package/dist/apps/shared/mail/folderOfType.js +40 -0
  228. package/dist/apps/shared/mail/outbox/outgoingReplies.d.ts +80 -0
  229. package/dist/apps/shared/mail/outbox/outgoingReplies.js +131 -0
  230. package/dist/apps/shared/mail/outbox/sendJob.d.ts +5 -1
  231. package/dist/apps/shared/mail/outbox/sendJob.js +14 -1
  232. package/dist/apps/shared/mail/outbox/sendOutcomes.js +13 -4
  233. package/dist/apps/shared/mail/permanentDelete.d.ts +62 -0
  234. package/dist/apps/shared/mail/permanentDelete.js +127 -0
  235. package/dist/apps/shared/mail/primaryMailbox.d.ts +29 -0
  236. package/dist/apps/shared/mail/primaryMailbox.js +61 -0
  237. package/dist/apps/shared/mail/reportNotices.d.ts +21 -0
  238. package/dist/apps/shared/mail/reportNotices.js +47 -0
  239. package/dist/apps/shared/mail/senderBlocking.d.ts +26 -0
  240. package/dist/apps/shared/mail/senderBlocking.js +92 -0
  241. package/dist/apps/shared/mail/useCollapsedSections.d.ts +32 -0
  242. package/dist/apps/shared/mail/useCollapsedSections.js +110 -0
  243. package/dist/apps/shared/mail/useMailConnection.js +3 -1
  244. package/dist/apps/shared/mail/useMailboxUpdateAccess.d.ts +11 -0
  245. package/dist/apps/shared/mail/useMailboxUpdateAccess.js +52 -0
  246. package/dist/apps/shared/mail/usePermanentDelete.d.ts +35 -0
  247. package/dist/apps/shared/mail/usePermanentDelete.js +96 -0
  248. package/dist/apps/shared/navigation/frameContext.d.ts +1 -1
  249. package/dist/apps/shared/navigation/index.d.ts +16 -0
  250. package/dist/apps/shared/navigation/index.js +34 -0
  251. package/dist/apps/shared/navigation/pageTitle.d.ts +11 -0
  252. package/dist/apps/shared/navigation/pageTitle.js +12 -0
  253. package/dist/apps/shared/search/crossMailboxSearch.d.ts +72 -0
  254. package/dist/apps/shared/search/crossMailboxSearch.js +136 -0
  255. package/dist/apps/shared/styles/app.css +396 -386
  256. package/dist/apps/www/_shell.d.ts +26 -0
  257. package/dist/apps/www/_shell.js +31 -0
  258. package/dist/apps/www/calendar/index.d.ts +7 -2
  259. package/dist/apps/www/calendar/index.js +7 -4
  260. package/dist/apps/www/contacts/[uid].d.ts +14 -3
  261. package/dist/apps/www/contacts/[uid].js +5 -3
  262. package/dist/apps/www/contacts/index.d.ts +7 -2
  263. package/dist/apps/www/contacts/index.js +8 -4
  264. package/dist/apps/www/index.d.ts +7 -2
  265. package/dist/apps/www/index.js +542 -207
  266. package/dist/apps/www/messages/[uid].d.ts +14 -3
  267. package/dist/apps/www/messages/[uid].js +7 -5
  268. package/dist/apps/www/settings/appearance/index.d.ts +8 -2
  269. package/dist/apps/www/settings/appearance/index.js +4 -2
  270. package/dist/apps/www/settings/auto-reply/index.d.ts +7 -2
  271. package/dist/apps/www/settings/auto-reply/index.js +4 -2
  272. package/dist/apps/www/settings/blocked-senders/index.d.ts +9 -0
  273. package/dist/apps/www/settings/blocked-senders/index.js +134 -0
  274. package/dist/apps/www/settings/encryption/index.d.ts +7 -2
  275. package/dist/apps/www/settings/encryption/index.js +4 -2
  276. package/dist/apps/www/settings/filters/[uid].d.ts +7 -2
  277. package/dist/apps/www/settings/filters/[uid].js +4 -2
  278. package/dist/apps/www/settings/filters/index.d.ts +7 -2
  279. package/dist/apps/www/settings/filters/index.js +4 -2
  280. package/dist/apps/www/settings/filters/new/index.d.ts +7 -2
  281. package/dist/apps/www/settings/filters/new/index.js +28 -5
  282. package/dist/apps/www/settings/labels/index.d.ts +7 -2
  283. package/dist/apps/www/settings/labels/index.js +4 -2
  284. package/dist/apps/www/settings/privacy/index.d.ts +7 -2
  285. package/dist/apps/www/settings/privacy/index.js +4 -2
  286. package/dist/apps/www/settings/profile/index.d.ts +7 -2
  287. package/dist/apps/www/settings/profile/index.js +4 -2
  288. package/dist/apps/www/settings/read-receipts/index.d.ts +7 -2
  289. package/dist/apps/www/settings/read-receipts/index.js +4 -2
  290. package/dist/apps/www/settings/sharing/index.d.ts +7 -2
  291. package/dist/apps/www/settings/sharing/index.js +4 -2
  292. package/dist/apps/www/settings/signatures/[uid].d.ts +7 -2
  293. package/dist/apps/www/settings/signatures/[uid].js +4 -2
  294. package/dist/apps/www/settings/signatures/index.d.ts +7 -2
  295. package/dist/apps/www/settings/signatures/index.js +4 -2
  296. package/dist/apps/www/settings/signatures/new/index.d.ts +7 -2
  297. package/dist/apps/www/settings/signatures/new/index.js +5 -3
  298. package/dist/apps/www/tasks/index.d.ts +7 -2
  299. package/dist/apps/www/tasks/index.js +5 -3
  300. package/package.json +4 -2
  301. package/apps/shared/navigation/AppRouter.tsx +0 -300
  302. package/apps/shared/navigation/idle.ts +0 -45
  303. package/apps/shared/navigation/routerContext.tsx +0 -83
  304. package/apps/shared/navigation/routes.ts +0 -72
  305. package/apps/www/_routedPage.tsx +0 -24
  306. package/apps/www/_routes.ts +0 -37
  307. package/dist/apps/shared/navigation/AppRouter.d.ts +0 -52
  308. package/dist/apps/shared/navigation/AppRouter.js +0 -242
  309. package/dist/apps/shared/navigation/idle.d.ts +0 -14
  310. package/dist/apps/shared/navigation/idle.js +0 -43
  311. package/dist/apps/shared/navigation/routerContext.d.ts +0 -37
  312. package/dist/apps/shared/navigation/routerContext.js +0 -56
  313. package/dist/apps/shared/navigation/routes.d.ts +0 -32
  314. package/dist/apps/shared/navigation/routes.js +0 -37
  315. package/dist/apps/www/_routedPage.d.ts +0 -12
  316. package/dist/apps/www/_routedPage.js +0 -19
  317. package/dist/apps/www/_routes.d.ts +0 -11
  318. package/dist/apps/www/_routes.js +0 -31
@@ -0,0 +1,62 @@
1
+ import { Message } from "@rapidmx/react-shared/mail/mailApi.js";
2
+ /**
3
+ * Permanent delete: what "Delete" does to a message that is already in Deleted Items (and what "Empty folder" does to Deleted Items and
4
+ * Junk Email). Unlike a move into Deleted Items it cannot be undone, so it is never reached without a confirmation (see
5
+ * `usePermanentDelete()`, which asks it, runs what is in here and tells the folder badges and the reader).
6
+ *
7
+ * Server side it is `DELETE /mail/messages/:uid?purge=true` (a hard delete - the row and its search document are gone, not a
8
+ * recoverable soft delete) per message, and `DELETE /mail/messages?folderUid=` (`truncate`) for a whole folder. Both refuse a message under
9
+ * an active legal hold with a `409`; the per-message form refuses one message at a time, so a selection is deleted as far as it can be and
10
+ * each refusal is reported (`PurgeFailure`), rather than the first stopping the rest.
11
+ */
12
+ /** How many purge requests are on the wire at once: a few at a time is quick, and the server publishes each folder's counts as they land. */
13
+ export declare const PURGE_CONCURRENCY = 4;
14
+ /** The folder types "Empty folder" is offered for: Outlook's Deleted Items and Junk Email. */
15
+ export declare const EMPTIABLE_FOLDER_TYPES: readonly string[];
16
+ /** A message the server refused to delete, and what it said. */
17
+ export interface PurgeFailure {
18
+ message: Message;
19
+ /** The server's own words (a legal hold names itself), or a stand-in when it did not answer. */
20
+ reason: string;
21
+ /** The response's status - `undefined` when there was no response. */
22
+ status?: number;
23
+ }
24
+ /** What deleting several messages came to: those that are gone, and those that are not (with why). Together they are all that was asked for. */
25
+ export interface PurgeOutcome {
26
+ deleted: Message[];
27
+ failed: PurgeFailure[];
28
+ }
29
+ /** What emptying a folder came to. */
30
+ export interface EmptyFolderOutcome extends PurgeOutcome {
31
+ /** Nothing is left in the folder as far as this knows: the whole-folder request went through, or every message listed was deleted. */
32
+ emptied: boolean;
33
+ }
34
+ /** `1 message`, `3 messages`. */
35
+ export declare function messageCount(count: number): string;
36
+ /**
37
+ * Permanently deletes `messages`, each in its own request, a few at a time. Never rejects: a refusal (a legal hold, a share without the right
38
+ * to delete, a message that is already gone) is recorded against its message and the rest carry on. `deleted` and `failed` keep the order of
39
+ * `messages`.
40
+ */
41
+ export declare function purgeMessages(messages: Message[]): Promise<PurgeOutcome>;
42
+ /**
43
+ * Permanently deletes everything in `folderUid`: one request for the whole folder, which is all or nothing. The server refuses it as a whole
44
+ * when a message in it is under a legal hold (`409`) or when the caller may delete messages but not empty a folder (`403`, a delegate) - and
45
+ * then this lists the folder and deletes message by message instead, so what may go does and each refusal is reported. Any other failure
46
+ * (the server is down) rejects, and the caller says so.
47
+ */
48
+ export declare function purgeFolder(folderUid: string, paging?: {
49
+ pageSize: number;
50
+ maxPages: number;
51
+ }): Promise<EmptyFolderOutcome>;
52
+ /**
53
+ * Tells the reader what a delete of several messages came to: "3 messages permanently deleted" (no Undo - there is none), or "2 deleted, 1
54
+ * could not be deleted" with the server's reasons.
55
+ */
56
+ export declare function notifyPurged(outcome: PurgeOutcome): void;
57
+ /**
58
+ * Tells the reader what emptying `folderName` came to. `count` is how many messages it held, when that was known: the whole-folder request
59
+ * does not say. A folder that had to be deleted message by message reports as `notifyPurged()` does, plus a word when it was too big to be
60
+ * done in one go.
61
+ */
62
+ export declare function notifyFolderEmptied(folderName: string, outcome: EmptyFolderOutcome, count?: number): void;
@@ -0,0 +1,127 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ import { emptyFolder, listMessages, purgeMessage } from "@rapidmx/react-shared/mail/mailApi.js";
6
+ import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
7
+ import { displaySubject } from "../components/mail/reading/EncryptedPreview.js";
8
+ import { notifySessionExpired } from "../notifications/apiErrors.js";
9
+ import { notify } from "../notifications/store.js";
10
+ import { LIST_PAGE_SIZE, MAX_LIST_PAGES, listAllPages } from "./listAllPages.js";
11
+ /**
12
+ * Permanent delete: what "Delete" does to a message that is already in Deleted Items (and what "Empty folder" does to Deleted Items and
13
+ * Junk Email). Unlike a move into Deleted Items it cannot be undone, so it is never reached without a confirmation (see
14
+ * `usePermanentDelete()`, which asks it, runs what is in here and tells the folder badges and the reader).
15
+ *
16
+ * Server side it is `DELETE /mail/messages/:uid?purge=true` (a hard delete - the row and its search document are gone, not a
17
+ * recoverable soft delete) per message, and `DELETE /mail/messages?folderUid=` (`truncate`) for a whole folder. Both refuse a message under
18
+ * an active legal hold with a `409`; the per-message form refuses one message at a time, so a selection is deleted as far as it can be and
19
+ * each refusal is reported (`PurgeFailure`), rather than the first stopping the rest.
20
+ */
21
+ /** How many purge requests are on the wire at once: a few at a time is quick, and the server publishes each folder's counts as they land. */
22
+ export const PURGE_CONCURRENCY = 4;
23
+ /** The folder types "Empty folder" is offered for: Outlook's Deleted Items and Junk Email. */
24
+ export const EMPTIABLE_FOLDER_TYPES = ["deleted_items", "junk"];
25
+ function describeFailure(err) {
26
+ if (err instanceof ApiRequestError) {
27
+ return { reason: err.message || `The server answered ${err.status}.`, status: err.status };
28
+ }
29
+ return { reason: "The server couldn't be reached." };
30
+ }
31
+ /** `1 message`, `3 messages`. */
32
+ export function messageCount(count) {
33
+ return `${count} message${count === 1 ? "" : "s"}`;
34
+ }
35
+ /**
36
+ * Permanently deletes `messages`, each in its own request, a few at a time. Never rejects: a refusal (a legal hold, a share without the right
37
+ * to delete, a message that is already gone) is recorded against its message and the rest carry on. `deleted` and `failed` keep the order of
38
+ * `messages`.
39
+ */
40
+ export async function purgeMessages(messages) {
41
+ const failures = new Array(messages.length).fill(null);
42
+ let next = 0;
43
+ async function worker() {
44
+ while (next < messages.length) {
45
+ const index = next++;
46
+ try {
47
+ await purgeMessage(messages[index].uid);
48
+ }
49
+ catch (err) {
50
+ failures[index] = { message: messages[index], ...describeFailure(err) };
51
+ }
52
+ }
53
+ }
54
+ await Promise.all(Array.from({ length: Math.min(PURGE_CONCURRENCY, messages.length) }, worker));
55
+ return {
56
+ deleted: messages.filter((_, index) => failures[index] === null),
57
+ failed: failures.filter((failure) => failure !== null),
58
+ };
59
+ }
60
+ /**
61
+ * Permanently deletes everything in `folderUid`: one request for the whole folder, which is all or nothing. The server refuses it as a whole
62
+ * when a message in it is under a legal hold (`409`) or when the caller may delete messages but not empty a folder (`403`, a delegate) - and
63
+ * then this lists the folder and deletes message by message instead, so what may go does and each refusal is reported. Any other failure
64
+ * (the server is down) rejects, and the caller says so.
65
+ */
66
+ export async function purgeFolder(folderUid, paging = { pageSize: LIST_PAGE_SIZE, maxPages: MAX_LIST_PAGES }) {
67
+ try {
68
+ await emptyFolder(folderUid);
69
+ return { emptied: true, deleted: [], failed: [] };
70
+ }
71
+ catch (err) {
72
+ if (!(err instanceof ApiRequestError) || (err.status !== 403 && err.status !== 409)) {
73
+ throw err;
74
+ }
75
+ }
76
+ const listed = await listAllPages((page) => listMessages(folderUid, { limit: paging.pageSize, page }), paging.pageSize, paging.maxPages);
77
+ const outcome = await purgeMessages(listed.items);
78
+ return { ...outcome, emptied: outcome.failed.length === 0 && !listed.truncated };
79
+ }
80
+ /** The line for one refusal, in the notification's details. */
81
+ function failureLine(failure) {
82
+ return `${displaySubject(failure.message.subject) || "(no subject)"}: ${failure.reason}`;
83
+ }
84
+ /**
85
+ * Tells the reader what a delete of several messages came to: "3 messages permanently deleted" (no Undo - there is none), or "2 deleted, 1
86
+ * could not be deleted" with the server's reasons.
87
+ */
88
+ export function notifyPurged(outcome) {
89
+ const { deleted, failed } = outcome;
90
+ if (failed.length === 0) {
91
+ notify({ kind: "success", title: `${messageCount(deleted.length)} permanently deleted` });
92
+ return;
93
+ }
94
+ if (failed.some((failure) => failure.status === 401)) {
95
+ notifySessionExpired();
96
+ return;
97
+ }
98
+ const reasons = [...new Set(failed.map((failure) => failure.reason))];
99
+ notify({
100
+ kind: deleted.length > 0 ? "warning" : "error",
101
+ title: deleted.length > 0
102
+ ? `${deleted.length} deleted, ${failed.length} could not be deleted`
103
+ : `Couldn't permanently delete ${failed.length === 1 ? "that message" : "those messages"}`,
104
+ message: reasons.slice(0, 3).join(" ") + (reasons.length > 3 ? ` (and ${reasons.length - 3} more reasons)` : ""),
105
+ details: failed.map(failureLine),
106
+ sticky: true,
107
+ });
108
+ }
109
+ /**
110
+ * Tells the reader what emptying `folderName` came to. `count` is how many messages it held, when that was known: the whole-folder request
111
+ * does not say. A folder that had to be deleted message by message reports as `notifyPurged()` does, plus a word when it was too big to be
112
+ * done in one go.
113
+ */
114
+ export function notifyFolderEmptied(folderName, outcome, count) {
115
+ if (outcome.deleted.length === 0 && outcome.failed.length === 0 && outcome.emptied) {
116
+ notify({
117
+ kind: "success",
118
+ title: count === undefined ? `${folderName} emptied` : `${messageCount(count)} permanently deleted`,
119
+ message: count === undefined ? "Everything in it was permanently deleted." : `${folderName} is now empty.`,
120
+ });
121
+ return;
122
+ }
123
+ notifyPurged(outcome);
124
+ if (!outcome.emptied && outcome.failed.length === 0) {
125
+ notify({ kind: "info", title: `${folderName} is not empty yet`, message: "It holds more than could be deleted in one go. Empty it again to carry on." });
126
+ }
127
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Which of the caller's mailboxes is "theirs", and the order every list of them is shown in - the one definition the mail, calendar,
3
+ * contacts, tasks and settings shells, the compose window's From picker and the account menu all use, so a shared mailbox that happens
4
+ * to sort first (or that the server lists first) is never the one an app opens on or a picker defaults to.
5
+ *
6
+ * An **own** mailbox is one whose `ownerUserUid` is the signed-in user.
7
+ *
8
+ * A **shared** mailbox is any other one the caller merely has access to: an ownerless (shared/organization) mailbox, or a delegate grant on
9
+ * somebody else's (`isSharedWithMe()`).
10
+ *
11
+ * The **primary** mailbox is the caller's own mailbox - the earliest created, when they own several - else the first mailbox that isn't
12
+ * shared, else the first one there is.
13
+ *
14
+ * `orderMailboxes()` puts the primary mailbox first, then the caller's other mailboxes, then the shared ones, each group alphabetical.
15
+ */
16
+ import { type Mailbox } from "@rapidmx/react-shared/mail/mailApi.js";
17
+ /** Whether `mailbox` is the signed-in user's own. */
18
+ export declare function isOwnMailbox(mailbox: Mailbox, userUid: string | undefined): boolean;
19
+ /** Whether `mailbox` is one the caller only has access to (an ownerless one, or somebody else's shared with them). */
20
+ export declare function isSharedMailbox(mailbox: Mailbox, userUid: string | undefined): boolean;
21
+ /** The caller's own mailboxes, earliest created first (the given order among ones created together). */
22
+ export declare function ownMailboxes(mailboxes: readonly Mailbox[], userUid: string | undefined): Mailbox[];
23
+ /** The mailbox to open and to default pickers to: see the rules at the top of this file. `undefined` only for no mailboxes at all. */
24
+ export declare function primaryMailbox(mailboxes: readonly Mailbox[], userUid: string | undefined): Mailbox | undefined;
25
+ /** `primaryMailbox()`'s uid. */
26
+ export declare function primaryMailboxUid(mailboxes: readonly Mailbox[], userUid: string | undefined): string | undefined;
27
+ /** `mailboxes` in display order: the primary mailbox, then the caller's other mailboxes, then the shared ones - each group by name. Never
28
+ * changes the list it is given. */
29
+ export declare function orderMailboxes(mailboxes: readonly Mailbox[], userUid: string | undefined): Mailbox[];
@@ -0,0 +1,61 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ /**
6
+ * Which of the caller's mailboxes is "theirs", and the order every list of them is shown in - the one definition the mail, calendar,
7
+ * contacts, tasks and settings shells, the compose window's From picker and the account menu all use, so a shared mailbox that happens
8
+ * to sort first (or that the server lists first) is never the one an app opens on or a picker defaults to.
9
+ *
10
+ * An **own** mailbox is one whose `ownerUserUid` is the signed-in user.
11
+ *
12
+ * A **shared** mailbox is any other one the caller merely has access to: an ownerless (shared/organization) mailbox, or a delegate grant on
13
+ * somebody else's (`isSharedWithMe()`).
14
+ *
15
+ * The **primary** mailbox is the caller's own mailbox - the earliest created, when they own several - else the first mailbox that isn't
16
+ * shared, else the first one there is.
17
+ *
18
+ * `orderMailboxes()` puts the primary mailbox first, then the caller's other mailboxes, then the shared ones, each group alphabetical.
19
+ */
20
+ import { isSharedWithMe } from "@rapidmx/react-shared/mail/mailApi.js";
21
+ /** Whether `mailbox` is the signed-in user's own. */
22
+ export function isOwnMailbox(mailbox, userUid) {
23
+ return !!userUid && mailbox.ownerUserUid === userUid;
24
+ }
25
+ /** Whether `mailbox` is one the caller only has access to (an ownerless one, or somebody else's shared with them). */
26
+ export function isSharedMailbox(mailbox, userUid) {
27
+ if (isOwnMailbox(mailbox, userUid)) {
28
+ return false;
29
+ }
30
+ // The server says how the caller reaches a mailbox (`accessRole`); an older one doesn't, and then a mailbox that isn't the known
31
+ // caller's own is somebody else's.
32
+ return mailbox.accessRole ? isSharedWithMe(mailbox) : !!userUid || isSharedWithMe(mailbox);
33
+ }
34
+ function createdAt(mailbox) {
35
+ const time = Date.parse(mailbox.dateCreated);
36
+ return Number.isNaN(time) ? Infinity : time;
37
+ }
38
+ /** The caller's own mailboxes, earliest created first (the given order among ones created together). */
39
+ export function ownMailboxes(mailboxes, userUid) {
40
+ return mailboxes.filter((mailbox) => isOwnMailbox(mailbox, userUid)).sort((a, b) => createdAt(a) - createdAt(b));
41
+ }
42
+ /** The mailbox to open and to default pickers to: see the rules at the top of this file. `undefined` only for no mailboxes at all. */
43
+ export function primaryMailbox(mailboxes, userUid) {
44
+ return ownMailboxes(mailboxes, userUid)[0] ?? mailboxes.find((mailbox) => !isSharedMailbox(mailbox, userUid)) ?? mailboxes[0];
45
+ }
46
+ /** `primaryMailbox()`'s uid. */
47
+ export function primaryMailboxUid(mailboxes, userUid) {
48
+ return primaryMailbox(mailboxes, userUid)?.uid;
49
+ }
50
+ /** `mailboxes` in display order: the primary mailbox, then the caller's other mailboxes, then the shared ones - each group by name. Never
51
+ * changes the list it is given. */
52
+ export function orderMailboxes(mailboxes, userUid) {
53
+ const primary = primaryMailbox(mailboxes, userUid);
54
+ const rank = (mailbox) => (mailbox === primary ? 0 : isSharedMailbox(mailbox, userUid) ? 2 : 1);
55
+ // Tolerates a mailbox that says nothing of its name or address (an older server's, or a test fixture's).
56
+ const name = (mailbox) => mailbox.displayName ?? "";
57
+ const address = (mailbox) => mailbox.primarySmtpAddress ?? "";
58
+ return [...mailboxes].sort((a, b) => rank(a) - rank(b) ||
59
+ name(a).localeCompare(name(b), undefined, { sensitivity: "base" }) ||
60
+ address(a).localeCompare(address(b)));
61
+ }
@@ -0,0 +1,21 @@
1
+ import type { MessageReportKind, MessageReportResult } from "@rapidmx/react-shared/mail/mailApi.js";
2
+ /** Where a notification's "Manage blocked senders" goes: the Blocked and safe senders page of the mailbox. */
3
+ export declare function senderListsHref(mailboxUid: string): string;
4
+ /** The noun of `kind` for a failure's title ("Couldn't report this message as phishing"). */
5
+ export declare function reportNoun(kind: MessageReportKind): string;
6
+ /** The title of a pop-up for a report of `kind`. */
7
+ export declare function reportTitle(kind: MessageReportKind): string;
8
+ /** The name of the folder a report of `kind` files the message in. */
9
+ export declare function reportFolderName(kind: MessageReportKind): string;
10
+ /** What a pop-up for a report says. `hint` is the small muted line: only for the two things a reader may want to know about (the filter could not be
11
+ * taught, or the message is encrypted so it was not), never for the ones that are simply how the server is set up. */
12
+ export interface ReportNotice {
13
+ title: string;
14
+ message: string;
15
+ hint?: string;
16
+ }
17
+ /**
18
+ * The words for what the server did with a report (`reportMessage()`'s answer): where the message went, whether the spam filter learned from it, and - for
19
+ * a report of phishing - that the server recorded it. `subject` names the message. `alwaysTrust` says the reader asked for the sender to be trusted.
20
+ */
21
+ export declare function reportNotice(result: MessageReportResult, subject: string, alwaysTrust: boolean): ReportNotice;
@@ -0,0 +1,47 @@
1
+ /** Where a notification's "Manage blocked senders" goes: the Blocked and safe senders page of the mailbox. */
2
+ export function senderListsHref(mailboxUid) {
3
+ return `/settings/blocked-senders?mailboxUid=${encodeURIComponent(mailboxUid)}`;
4
+ }
5
+ /** What a pop-up says about reporting `kind`: the title, and the folder the message goes to. */
6
+ const KINDS = {
7
+ junk: { title: "Reported as junk", noun: "junk", folder: "Junk Email" },
8
+ phishing: { title: "Reported as phishing", noun: "phishing", folder: "Junk Email" },
9
+ not_junk: { title: "Marked as not junk", noun: "not junk", folder: "the Inbox" },
10
+ };
11
+ /** The noun of `kind` for a failure's title ("Couldn't report this message as phishing"). */
12
+ export function reportNoun(kind) {
13
+ return KINDS[kind].noun;
14
+ }
15
+ /** The title of a pop-up for a report of `kind`. */
16
+ export function reportTitle(kind) {
17
+ return KINDS[kind].title;
18
+ }
19
+ /** The name of the folder a report of `kind` files the message in. */
20
+ export function reportFolderName(kind) {
21
+ return KINDS[kind].folder;
22
+ }
23
+ /**
24
+ * The words for what the server did with a report (`reportMessage()`'s answer): where the message went, whether the spam filter learned from it, and - for
25
+ * a report of phishing - that the server recorded it. `subject` names the message. `alwaysTrust` says the reader asked for the sender to be trusted.
26
+ */
27
+ export function reportNotice(result, subject, alwaysTrust) {
28
+ const kind = KINDS[result.kind];
29
+ const where = result.moved ? `was moved to ${kind.folder}` : `is already in ${kind.folder}`;
30
+ const learned = result.learned ? " and used to train the spam filter" : "";
31
+ const parts = [`“${subject}” ${where}${learned}.`];
32
+ if (result.kind === "phishing") {
33
+ parts.push("The report was recorded in the audit log.");
34
+ }
35
+ if (result.safeSender) {
36
+ parts.push(`Mail from ${result.safeSender} that passes authentication is no longer sent to Junk Email.`);
37
+ }
38
+ else if (alwaysTrust) {
39
+ parts.push("The message names no address that could be trusted.");
40
+ }
41
+ const hint = result.learnSkipped === "failed"
42
+ ? "The spam filter could not be trained from this report."
43
+ : result.learnSkipped === "encrypted"
44
+ ? "Not used to train the spam filter: the message is encrypted."
45
+ : undefined;
46
+ return { title: kind.title, message: parts.join(" "), hint };
47
+ }
@@ -0,0 +1,26 @@
1
+ import { MailFilterRule } from "@rapidmx/react-shared/mail/mailFilterRulesApi.js";
2
+ export declare const BLOCK_RULE_PREFIX = "Block ";
3
+ export declare const NEVER_BLOCK_RULE_PREFIX = "Never block ";
4
+ /** `addresses` trimmed, lowercased and without repeats, keeping only what has an `@`. */
5
+ export declare function normalizeAddresses(addresses: (string | undefined)[]): string[];
6
+ /** What blocking a sender did: `created` the rule, found it `existing` (and on), or turned an existing one back on. */
7
+ export interface BlockResult {
8
+ outcome: "created" | "existing" | "enabled";
9
+ rule: MailFilterRule;
10
+ }
11
+ /** What "Never block" did: the block rules it took away, and the rule that keeps the sender in the Inbox (`created` now or found). */
12
+ export interface NeverBlockResult {
13
+ removed: MailFilterRule[];
14
+ outcome: "created" | "existing" | "enabled";
15
+ rule: MailFilterRule;
16
+ }
17
+ /**
18
+ * Blocks a sender: mail from `addresses` (its envelope sender and From header address, when they differ - a mailing list or a bulk sender
19
+ * usually has both) goes to `junkFolderUid` from now on. Idempotent: the rule is made once, and a rule that was turned off is turned on.
20
+ * A "Never block" rule for the same sender is taken away, since the two would fight.
21
+ */
22
+ export declare function blockSender(mailboxUid: string, addresses: string[], junkFolderUid: string, inboxFolderUid: string): Promise<BlockResult>;
23
+ /** Never blocks a sender: takes away the block rule for `addresses` and makes (once) the rule that keeps their mail in the Inbox. */
24
+ export declare function neverBlockSender(mailboxUid: string, addresses: string[], junkFolderUid: string, inboxFolderUid: string): Promise<NeverBlockResult>;
25
+ /** Takes back a rule this module made (the notification's Undo). */
26
+ export declare function removeSenderRule(rule: MailFilterRule): Promise<void>;
@@ -0,0 +1,92 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ import { createMailFilterRule, deleteMailFilterRule, listMailFilterRules, updateMailFilterRule, } from "@rapidmx/react-shared/mail/mailFilterRulesApi.js";
6
+ /**
7
+ * "Block sender" and "Never block sender", expressed as the mailbox's own mail filter rules - the server has no blocked-senders list.
8
+ * A rule's "From contains" is a case-insensitive substring match on the From header (restapi's `matchesConditions()`), which is the
9
+ * closest thing there is to "from equals": it also matches a longer address that ends in this one (`joann@x.com` for `ann@x.com`).
10
+ *
11
+ * - **Block** is `From contains <address>` -> move to the mailbox's Junk Email, stop processing more rules, ahead of every other rule
12
+ * (a sequence below all of them), so nothing else files the sender's mail first.
13
+ * - **Never block** is `From contains <address>` -> move to the Inbox, stop processing, likewise first. It keeps the sender's mail out of
14
+ * the reader's own blocking and filing rules. It cannot keep it out of Junk when the spam filter sends it there: mail the spam filter
15
+ * has judged junk skips every rule (restapi's `ScanQueueJob.deliverMessage()`).
16
+ *
17
+ * Rules are found by what they do, not by their name, so one the reader renamed or made by hand is still the rule for that sender.
18
+ */
19
+ /** Every rule listing asks for up to this many, which is the most the mail server reads when it filters a delivery. */
20
+ const RULE_LIMIT = 500;
21
+ export const BLOCK_RULE_PREFIX = "Block ";
22
+ export const NEVER_BLOCK_RULE_PREFIX = "Never block ";
23
+ /** `addresses` trimmed, lowercased and without repeats, keeping only what has an `@`. */
24
+ export function normalizeAddresses(addresses) {
25
+ return [...new Set(addresses.map((address) => address?.trim().toLowerCase() ?? "").filter((address) => address.includes("@")))];
26
+ }
27
+ /** Whether `rule` is "From contains ..." (every address of `addresses` among them, and nothing else) and moves to `folderUid` and stops. */
28
+ function isSenderRule(rule, addresses, folderUid) {
29
+ const conditions = rule.conditions;
30
+ const only = Object.keys(conditions).every((key) => key === "fromContains" || conditions[key] === undefined);
31
+ const from = (rule.conditions.fromContains ?? []).map((entry) => entry.toLowerCase());
32
+ return (only &&
33
+ addresses.every((address) => from.includes(address)) &&
34
+ rule.stopProcessingRules &&
35
+ rule.actions.length === 1 &&
36
+ rule.actions[0].type === "move_to_folder" &&
37
+ rule.actions[0].folderUid === folderUid);
38
+ }
39
+ /** A sequence that puts a new rule ahead of every rule the mailbox has. */
40
+ function firstSequence(rules) {
41
+ return Math.min(0, ...rules.map((rule) => rule.sequence)) - 1;
42
+ }
43
+ /** Finds the rule that does `folderUid` for `addresses`, makes one (named `${prefix}${addresses[0]}`) if there is none, and turns a found one on. */
44
+ async function ensureRule(rules, mailboxUid, addresses, folderUid, prefix) {
45
+ const found = rules.find((rule) => isSenderRule(rule, addresses, folderUid));
46
+ if (found) {
47
+ if (found.enabled) {
48
+ return { outcome: "existing", rule: found };
49
+ }
50
+ // The whole rule, as the filters page saves one: the update replaces the record.
51
+ const { uid, version, name, sequence, stopProcessingRules, conditions, actions } = found;
52
+ return { outcome: "enabled", rule: await updateMailFilterRule({ uid, version, name, enabled: true, sequence, stopProcessingRules, conditions, actions }) };
53
+ }
54
+ const rule = await createMailFilterRule({
55
+ mailboxUid,
56
+ name: `${prefix}${addresses[0]}`,
57
+ sequence: firstSequence(rules),
58
+ stopProcessingRules: true,
59
+ conditions: { fromContains: addresses },
60
+ actions: [{ type: "move_to_folder", folderUid }],
61
+ });
62
+ return { outcome: "created", rule };
63
+ }
64
+ /** Takes away every rule that is `folderUid` for `addresses`. */
65
+ async function removeRules(rules, addresses, folderUid) {
66
+ const doomed = rules.filter((rule) => isSenderRule(rule, addresses, folderUid));
67
+ for (const rule of doomed) {
68
+ await deleteMailFilterRule(rule.uid, rule.version);
69
+ }
70
+ return doomed;
71
+ }
72
+ /**
73
+ * Blocks a sender: mail from `addresses` (its envelope sender and From header address, when they differ - a mailing list or a bulk sender
74
+ * usually has both) goes to `junkFolderUid` from now on. Idempotent: the rule is made once, and a rule that was turned off is turned on.
75
+ * A "Never block" rule for the same sender is taken away, since the two would fight.
76
+ */
77
+ export async function blockSender(mailboxUid, addresses, junkFolderUid, inboxFolderUid) {
78
+ const rules = await listMailFilterRules(mailboxUid, { limit: RULE_LIMIT });
79
+ await removeRules(rules, addresses, inboxFolderUid);
80
+ return ensureRule(rules, mailboxUid, addresses, junkFolderUid, BLOCK_RULE_PREFIX);
81
+ }
82
+ /** Never blocks a sender: takes away the block rule for `addresses` and makes (once) the rule that keeps their mail in the Inbox. */
83
+ export async function neverBlockSender(mailboxUid, addresses, junkFolderUid, inboxFolderUid) {
84
+ const rules = await listMailFilterRules(mailboxUid, { limit: RULE_LIMIT });
85
+ const removed = await removeRules(rules, addresses, junkFolderUid);
86
+ const kept = await ensureRule(rules.filter((rule) => !removed.includes(rule)), mailboxUid, addresses, inboxFolderUid, NEVER_BLOCK_RULE_PREFIX);
87
+ return { removed, ...kept };
88
+ }
89
+ /** Takes back a rule this module made (the notification's Undo). */
90
+ export function removeSenderRule(rule) {
91
+ return deleteMailFilterRule(rule.uid, rule.version);
92
+ }
@@ -0,0 +1,32 @@
1
+ /** The id of the "All mailboxes" section; every mailbox's section is its uid. */
2
+ export declare const ALL_MAILBOXES_SECTION = "all";
3
+ /** What the reader chose per section: `true` collapsed, `false` open. A section that is not there follows the default rule. */
4
+ export type SectionChoices = Record<string, boolean>;
5
+ /** The `localStorage` key one user's choices are kept under. */
6
+ export declare function collapsedSectionsKey(userUid: string): string;
7
+ /** Reads a user's stored choices - none for an unknown user, a missing entry, unparsable JSON, something that is not an object, or a store that throws. */
8
+ export declare function readSectionChoices(userUid: string | undefined): SectionChoices;
9
+ /** Stores a user's choices. Best-effort - see this module's own doc comment. */
10
+ export declare function writeSectionChoices(userUid: string | undefined, choices: SectionChoices): void;
11
+ /** Whether a section is open by the reader's choice, or by the default rule where they have made none (a navigation's override not considered). */
12
+ export declare function sectionChosenOpen(id: string, choices: SectionChoices, primaryUid: string | undefined): boolean;
13
+ export interface SidebarSectionsOptions {
14
+ /** The signed-in user, whose choices these are; `undefined` keeps them for this page view only. */
15
+ userUid: string | undefined;
16
+ /** The user's primary mailbox, which - with "All mailboxes" - is open until they choose otherwise. */
17
+ primaryUid: string | undefined;
18
+ /**
19
+ * The section holding the folder that is open: `ALL_MAILBOXES_SECTION` for an aggregate view, else that mailbox's uid; `undefined`
20
+ * while that is not known yet (the address not read, the mailboxes not loaded). Moving from one section to another opens the new one
21
+ * (see this module's doc comment); the first section that becomes known does not.
22
+ */
23
+ activeId: string | undefined;
24
+ }
25
+ export interface SidebarSections {
26
+ /** Whether the section shows its folders. */
27
+ isExpanded: (id: string) => boolean;
28
+ /** Opens a collapsed section, collapses an open one, and remembers it as the reader's choice. */
29
+ toggle: (id: string) => void;
30
+ }
31
+ /** See this module's doc comment. */
32
+ export declare function useSidebarSections({ userUid, primaryUid, activeId }: SidebarSectionsOptions): SidebarSections;
@@ -0,0 +1,110 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ /**
6
+ * Which of the Mail sidebar's sections - "All mailboxes" and one per mailbox - are open, and the reader's own choices about it.
7
+ *
8
+ * A section the reader has never touched follows the **default rule**: "All mailboxes" and the signed-in user's primary mailbox
9
+ * (`primaryMailboxUid()`) are open, every other mailbox is collapsed - so a mailbox that is newly shared with them arrives closed.
10
+ * Once they toggle a section the choice is remembered for that section, replacing the default, whatever the default later says.
11
+ *
12
+ * **Every** section can be collapsed, the one holding the open folder and the primary mailbox included, and a collapsed one stays collapsed
13
+ * across reloads even while its own folder is the selected one: the toggle always toggles and always remembers.
14
+ *
15
+ * The one exception is *navigation*: when the section holding the open folder (`activeId`) **changes** - the reader picks a folder in
16
+ * another section, or a search result or a link lands in one - that section is shown open, so the folder just highlighted is not hidden
17
+ * from them. That is an in-memory override of this page view, never written as the reader's choice: it does not apply to the first
18
+ * section the page resolves (a reload lands on whatever was chosen), and it ends when they navigate to yet another section. Toggling
19
+ * the section it opened forgets the override and remembers the reader's new choice (the opposite of what was showing).
20
+ *
21
+ * The choices are kept per signed-in user, per device, in `localStorage` (`rapidmx:mail-sidebar-collapsed:<userUid>`), the way
22
+ * `listPreferences.ts` keeps the list arrangement: `{ "<section id>": <collapsed> }`. Every read tolerates a missing, corrupt or
23
+ * blocked store (and keeps only the booleans of a partly-valid one), every write swallows its failure - a blocked store only means the
24
+ * choices do not survive a reload.
25
+ */
26
+ import { useCallback, useMemo, useRef, useState } from "react";
27
+ const STORAGE_KEY_PREFIX = "rapidmx:mail-sidebar-collapsed:";
28
+ /** The id of the "All mailboxes" section; every mailbox's section is its uid. */
29
+ export const ALL_MAILBOXES_SECTION = "all";
30
+ /** The `localStorage` key one user's choices are kept under. */
31
+ export function collapsedSectionsKey(userUid) {
32
+ return `${STORAGE_KEY_PREFIX}${userUid}`;
33
+ }
34
+ /** Reads a user's stored choices - none for an unknown user, a missing entry, unparsable JSON, something that is not an object, or a store that throws. */
35
+ export function readSectionChoices(userUid) {
36
+ if (!userUid) {
37
+ return {};
38
+ }
39
+ let stored;
40
+ try {
41
+ const raw = localStorage.getItem(collapsedSectionsKey(userUid));
42
+ stored = raw === null ? null : JSON.parse(raw);
43
+ }
44
+ catch {
45
+ return {};
46
+ }
47
+ if (!stored || typeof stored !== "object" || Array.isArray(stored)) {
48
+ return {};
49
+ }
50
+ // Only what is a real choice: a value that is not a boolean falls back to the default for its own section, not for all of them.
51
+ return Object.fromEntries(Object.entries(stored).filter((entry) => typeof entry[1] === "boolean"));
52
+ }
53
+ /** Stores a user's choices. Best-effort - see this module's own doc comment. */
54
+ export function writeSectionChoices(userUid, choices) {
55
+ if (!userUid) {
56
+ return;
57
+ }
58
+ try {
59
+ localStorage.setItem(collapsedSectionsKey(userUid), JSON.stringify(choices));
60
+ }
61
+ catch {
62
+ // Best-effort - see this module's own doc comment.
63
+ }
64
+ }
65
+ /** Whether a section is open by the reader's choice, or by the default rule where they have made none (a navigation's override not considered). */
66
+ export function sectionChosenOpen(id, choices, primaryUid) {
67
+ const collapsed = choices[id];
68
+ return collapsed === undefined ? id === ALL_MAILBOXES_SECTION || id === primaryUid : !collapsed;
69
+ }
70
+ /** See this module's doc comment. */
71
+ export function useSidebarSections({ userUid, primaryUid, activeId }) {
72
+ const [loaded, setLoaded] = useState(() => ({ userUid, choices: readSectionChoices(userUid) }));
73
+ let current = loaded;
74
+ if (loaded.userUid !== userUid) {
75
+ // Another user signed in on this page: theirs are not the last one's choices.
76
+ current = { userUid, choices: readSectionChoices(userUid) };
77
+ setLoaded(current);
78
+ }
79
+ // The section the last navigation opened, if any: derived while rendering from the change of `activeId` (no effect, so the section is
80
+ // already open in the render that highlights its folder). A first `activeId` (from undefined) is not a navigation.
81
+ const [nav, setNav] = useState({ activeId, opened: undefined });
82
+ let currentNav = nav;
83
+ if (nav.activeId !== activeId) {
84
+ currentNav = { activeId, opened: nav.activeId === undefined ? undefined : activeId };
85
+ setNav(currentNav);
86
+ }
87
+ // Kept in step with what was last rendered, and moved on by a toggle at once, so two toggles before a re-render both count.
88
+ const choicesRef = useRef(current.choices);
89
+ choicesRef.current = current.choices;
90
+ const openedRef = useRef(currentNav.opened);
91
+ openedRef.current = currentNav.opened;
92
+ const toggle = useCallback((id) => {
93
+ const expanded = openedRef.current === id || sectionChosenOpen(id, choicesRef.current, primaryUid);
94
+ const next = { ...choicesRef.current, [id]: expanded };
95
+ choicesRef.current = next;
96
+ setLoaded({ userUid, choices: next });
97
+ writeSectionChoices(userUid, next);
98
+ if (openedRef.current === id) {
99
+ // The reader's own choice replaces the override that had opened it.
100
+ openedRef.current = undefined;
101
+ setNav({ activeId, opened: undefined });
102
+ }
103
+ }, [activeId, primaryUid, userUid]);
104
+ const choices = current.choices;
105
+ const opened = currentNav.opened;
106
+ return useMemo(() => ({
107
+ isExpanded: (id) => id === opened || sectionChosenOpen(id, choices, primaryUid),
108
+ toggle,
109
+ }), [opened, choices, primaryUid, toggle]);
110
+ }
@@ -6,6 +6,7 @@ import { createContext, useCallback, useEffect, useRef, useState } from "react";
6
6
  import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
7
7
  import { listFolders, listMailboxes } from "@rapidmx/react-shared/mail/mailApi.js";
8
8
  import { FOLDER_ORDER, MAIL_FOLDER_TYPES, isMailFolder, knownFolderUids, reconcileFolders, removeFolder, upsertFolder } from "./folderTree.js";
9
+ import { orderMailboxes } from "./primaryMailbox.js";
9
10
  import { useMailLiveUpdates } from "./useMailLiveUpdates.js";
10
11
  import { useNewMailNotifications } from "./useNewMailNotifications.js";
11
12
  import { usePushConnectionNotice } from "../notifications/pushStatus.js";
@@ -38,7 +39,8 @@ export function useMailConnection({ userUid, enabled, open }) {
38
39
  }
39
40
  listMailboxes({ limit: MAILBOX_LIST_LIMIT })
40
41
  .then((result) => {
41
- setMailboxes(result);
42
+ // The caller's own mailbox first, then the others: every sidebar, picker and default below follows this order.
43
+ setMailboxes(orderMailboxes(result, userUid));
42
44
  setStatus("ready");
43
45
  })
44
46
  .catch((err) => {