@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,11 @@
1
+ import type { Mailbox } from "@rapidmx/react-shared/mail/mailApi.js";
2
+ /** Forgets every remembered answer - for the tests, and anything that changes the caller's access mid-session. */
3
+ export declare function clearMailboxUpdateAccessCache(): void;
4
+ /**
5
+ * Whether the reader may change the messages of `mailbox`: `false` only for a mailbox shared with them (`accessRole: "delegate"`) that
6
+ * the server says they may not update - a view-only share. Everything else is `true` from the first render: the reader's own mailbox
7
+ * needs no request, and a mailbox that is unknown (the shell has not listed it), or whose access could not be read, is treated as
8
+ * writable - the server still refuses what is not allowed, and hiding a usable action over a failed lookup is worse than offering one
9
+ * that then says why it failed.
10
+ */
11
+ export declare function useMailboxUpdateAccess(mailbox: Mailbox | undefined): boolean;
@@ -0,0 +1,52 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ import { useEffect, useState } from "react";
6
+ import { getMyMailboxAccess } from "@rapidmx/react-shared/mail/mailboxAccessApi.js";
7
+ /** What the server said about the caller's update access to a mailbox, remembered per mailbox for the page load. */
8
+ const answers = new Map();
9
+ /** Forgets every remembered answer - for the tests, and anything that changes the caller's access mid-session. */
10
+ export function clearMailboxUpdateAccessCache() {
11
+ answers.clear();
12
+ }
13
+ /** Whether the caller may change things in `mailboxUid` (move, mark and flag its messages, add its filters): the server's own ACL check. Never rejects; `undefined` when it could not be asked (which is not cached). */
14
+ function fetchUpdateAccess(mailboxUid) {
15
+ let answer = answers.get(mailboxUid);
16
+ if (!answer) {
17
+ answer = getMyMailboxAccess(mailboxUid).then((access) => access.canUpdate, () => {
18
+ answers.delete(mailboxUid);
19
+ return undefined;
20
+ });
21
+ answers.set(mailboxUid, answer);
22
+ }
23
+ return answer;
24
+ }
25
+ /**
26
+ * Whether the reader may change the messages of `mailbox`: `false` only for a mailbox shared with them (`accessRole: "delegate"`) that
27
+ * the server says they may not update - a view-only share. Everything else is `true` from the first render: the reader's own mailbox
28
+ * needs no request, and a mailbox that is unknown (the shell has not listed it), or whose access could not be read, is treated as
29
+ * writable - the server still refuses what is not allowed, and hiding a usable action over a failed lookup is worse than offering one
30
+ * that then says why it failed.
31
+ */
32
+ export function useMailboxUpdateAccess(mailbox) {
33
+ const uid = mailbox?.uid;
34
+ const delegated = mailbox?.accessRole === "delegate";
35
+ const [writable, setWritable] = useState(true);
36
+ useEffect(() => {
37
+ setWritable(true);
38
+ if (!uid || !delegated) {
39
+ return;
40
+ }
41
+ let cancelled = false;
42
+ void fetchUpdateAccess(uid).then((canUpdate) => {
43
+ if (!cancelled && canUpdate === false) {
44
+ setWritable(false);
45
+ }
46
+ });
47
+ return () => {
48
+ cancelled = true;
49
+ };
50
+ }, [uid, delegated]);
51
+ return writable;
52
+ }
@@ -0,0 +1,35 @@
1
+ import { ReactElement } from "react";
2
+ import type { Message } from "@rapidmx/react-shared/mail/mailApi.js";
3
+ import { EmptyFolderOutcome, PurgeOutcome } from "./permanentDelete.js";
4
+ /** The folder "Empty folder" is about. */
5
+ export interface EmptiableFolder {
6
+ uid: string;
7
+ name: string;
8
+ }
9
+ export interface PermanentDelete {
10
+ /**
11
+ * Asks, then permanently deletes `messages` (Delete in Deleted Items - the Delete menu item of a message card calls this too). Resolves
12
+ * with what came of it once the dialog is gone, and everything a delete has to tell has been told: the folder badges follow, the local
13
+ * search index drops the messages, and a notification says "N messages permanently deleted" or which could not be and why. Resolves
14
+ * `null` - and sends nothing - when the reader cancels, when `messages` is empty, or while another request is already open.
15
+ */
16
+ requestPermanentDelete: (messages: Message[]) => Promise<PurgeOutcome | null>;
17
+ /**
18
+ * Asks, then permanently deletes everything in `folder`. `count` is how many messages it holds when that is known (the dialog and the
19
+ * notification then say so). Resolves as `requestPermanentDelete()` does; a request that failed outright (the server is down) has said so and resolves an
20
+ * outcome with nothing deleted, so the caller reloads what it lists.
21
+ */
22
+ requestEmptyFolder: (folder: EmptiableFolder, count?: number) => Promise<EmptyFolderOutcome | null>;
23
+ /** A request is open (the dialog is up, or the delete is on the wire): what should hold every other action of the list meanwhile. */
24
+ busy: boolean;
25
+ /** The confirmation dialog - render it once, anywhere (it is a portal). */
26
+ dialog: ReactElement;
27
+ }
28
+ /**
29
+ * The one place a permanent delete is confirmed and carried out - for the mail list (the selection bar, the Delete key, Empty folder) and
30
+ * for a message card's own Delete, so all of them ask the same question, delete the same way and report the same way. Call it inside the
31
+ * mail shell (it keeps that shell's folder badges right).
32
+ *
33
+ * The reader always confirms: nothing is remembered between requests.
34
+ */
35
+ export declare function usePermanentDelete(): PermanentDelete;
@@ -0,0 +1,96 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ ///////////////////////////////////////////////////////////////////////////////
3
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
4
+ // SPDX-License-Identifier: MPL-2.0
5
+ ///////////////////////////////////////////////////////////////////////////////
6
+ import { useRef, useState } from "react";
7
+ import PermanentDeleteDialog from "../components/mail/PermanentDeleteDialog.js";
8
+ import { useMailShell } from "../components/mail/layout/MailShell.js";
9
+ import { notifyApiError } from "../notifications/apiErrors.js";
10
+ import { removeLocalEntity } from "../search/localIndexRpcClient.js";
11
+ import { messageCount, notifyFolderEmptied, notifyPurged, purgeFolder, purgeMessages, } from "./permanentDelete.js";
12
+ /**
13
+ * The one place a permanent delete is confirmed and carried out - for the mail list (the selection bar, the Delete key, Empty folder) and
14
+ * for a message card's own Delete, so all of them ask the same question, delete the same way and report the same way. Call it inside the
15
+ * mail shell (it keeps that shell's folder badges right).
16
+ *
17
+ * The reader always confirms: nothing is remembered between requests.
18
+ */
19
+ export function usePermanentDelete() {
20
+ const { trackMessageChange, refreshFolderCounts } = useMailShell();
21
+ const [pending, setPending] = useState(null);
22
+ const [running, setRunning] = useState(false);
23
+ // Set at once (state is a render later), so a key pressed twice does not open two dialogs.
24
+ const pendingRef = useRef(null);
25
+ function ask(request) {
26
+ if (pendingRef.current) {
27
+ return Promise.resolve(null);
28
+ }
29
+ return new Promise((resolve) => {
30
+ const next = { request, resolve };
31
+ pendingRef.current = next;
32
+ setPending(next);
33
+ });
34
+ }
35
+ function requestPermanentDelete(messages) {
36
+ return messages.length === 0 ? Promise.resolve(null) : ask({ kind: "messages", messages });
37
+ }
38
+ function requestEmptyFolder(folder, count) {
39
+ return ask({ kind: "folder", folder, count });
40
+ }
41
+ /** Closes the dialog and hands the caller what came of it. */
42
+ function finish(outcome) {
43
+ const current = pendingRef.current;
44
+ pendingRef.current = null;
45
+ setPending(null);
46
+ setRunning(false);
47
+ current.resolve(outcome);
48
+ }
49
+ /** What every delete tells the rest of the page: the badges (each message is one fewer in its folder) and the local search index. */
50
+ function afterDeleted(deleted) {
51
+ for (const message of deleted) {
52
+ trackMessageChange(message, null).settle();
53
+ void removeLocalEntity(message.mailboxUid, message.uid);
54
+ }
55
+ }
56
+ async function confirm() {
57
+ const { request } = pendingRef.current;
58
+ setRunning(true);
59
+ if (request.kind === "messages") {
60
+ const outcome = await purgeMessages(request.messages);
61
+ afterDeleted(outcome.deleted);
62
+ notifyPurged(outcome);
63
+ finish(outcome);
64
+ return;
65
+ }
66
+ let outcome;
67
+ try {
68
+ outcome = await purgeFolder(request.folder.uid);
69
+ }
70
+ catch (err) {
71
+ notifyApiError(err, `Couldn't empty ${request.folder.name}`);
72
+ // Part of it may have gone before the failure: the caller reloads rather than assume nothing did.
73
+ outcome = { emptied: false, deleted: [], failed: [] };
74
+ }
75
+ afterDeleted(outcome.deleted);
76
+ // The whole-folder request does not say which messages went, so the counts are read back rather than adjusted.
77
+ refreshFolderCounts();
78
+ if (outcome.emptied || outcome.deleted.length > 0 || outcome.failed.length > 0) {
79
+ notifyFolderEmptied(request.folder.name, outcome, request.count);
80
+ }
81
+ finish(outcome);
82
+ }
83
+ function cancel() {
84
+ finish(null);
85
+ }
86
+ const request = pending?.request;
87
+ const folderRequest = request?.kind === "folder" ? request : undefined;
88
+ const dialog = (_jsx(PermanentDeleteDialog, { open: pending !== null, title: folderRequest ? `Empty ${folderRequest.folder.name}` : "Delete permanently", message: folderRequest
89
+ ? `Permanently delete ${folderRequest.count === undefined
90
+ ? "all items"
91
+ : folderRequest.count === 1
92
+ ? "the only item"
93
+ : `all ${folderRequest.count} items`} in ${folderRequest.folder.name}? This can't be undone.`
94
+ : `Permanently delete ${messageCount(request?.kind === "messages" ? request.messages.length : 0)}? This can't be undone.`, confirmLabel: folderRequest ? "Delete all permanently" : "Delete permanently", busy: running, onConfirm: () => void confirm(), onCancel: cancel }));
95
+ return { requestPermanentDelete, requestEmptyFolder, busy: pending !== null, dialog };
96
+ }
@@ -1,6 +1,6 @@
1
1
  import React, { PropsWithChildren } from "react";
2
2
  /**
3
- * What the persistent app frame (`AppRouter`'s one mounted `AppShell` chrome) offers to what is rendered inside it. `null`
3
+ * What the persistent app frame (the webmail's router shell, `apps/www/_shell.tsx`, whose one mounted `AppChrome` never unmounts) offers to what is rendered inside it. `null`
4
4
  * outside a frame - a page not shown by the router (an admin page, a plugin page, a test) renders its own chrome.
5
5
  */
6
6
  export interface AppFrameContextValue {
@@ -0,0 +1,16 @@
1
+ export interface NavigateOptions {
2
+ /** Replaces the current history entry instead of adding one (a redirect, or a state change that isn't a "place"). */
3
+ replace?: boolean;
4
+ }
5
+ /** Goes to `href`: without a page load when it is a page of the app, else as an ordinary navigation. */
6
+ export type NavigateFn = (href: string, options?: NavigateOptions) => void;
7
+ /**
8
+ * The function to go somewhere with, for what code decides (after a save, a select, a folder change). `navigate("/calendar")`
9
+ * changes the page and the URL without a page load when the router knows the page (Mail, Calendar, Contacts, Tasks, Settings
10
+ * and their subpages), and loads it normally otherwise (another app, a plugin's page); outside a router it is always a
11
+ * normal navigation. A destination with the path the page is at - only its query string is different (`/?mailboxUid=a&folderUid=b`,
12
+ * `/tasks?mailboxUid=b`) - is a shallow navigation: the page instance keeps its state, and what reads the location follows. Prefer
13
+ * a plain `<a href>` for anything the user clicks - the router takes it over, and it stays a real link. The returned function is
14
+ * stable across renders.
15
+ */
16
+ export declare function useNavigate(): NavigateFn;
@@ -0,0 +1,34 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ /**
6
+ * What the web client's code uses to go somewhere. The client-side router is `@rapidrest/react`'s (`router = true` on the
7
+ * route, the app's `_shell.tsx` as the persistent frame - see `apps/www/_shell.tsx`); this is the one thing on top of it: a
8
+ * `navigate()` that keeps the page when only the query string changes.
9
+ */
10
+ import { useCallback, useRef } from "react";
11
+ import { useRouter } from "@rapidrest/react/client";
12
+ /**
13
+ * The function to go somewhere with, for what code decides (after a save, a select, a folder change). `navigate("/calendar")`
14
+ * changes the page and the URL without a page load when the router knows the page (Mail, Calendar, Contacts, Tasks, Settings
15
+ * and their subpages), and loads it normally otherwise (another app, a plugin's page); outside a router it is always a
16
+ * normal navigation. A destination with the path the page is at - only its query string is different (`/?mailboxUid=a&folderUid=b`,
17
+ * `/tasks?mailboxUid=b`) - is a shallow navigation: the page instance keeps its state, and what reads the location follows. Prefer
18
+ * a plain `<a href>` for anything the user clicks - the router takes it over, and it stays a real link. The returned function is
19
+ * stable across renders.
20
+ */
21
+ export function useNavigate() {
22
+ const { navigate, pathname } = useRouter();
23
+ // The latest, through a ref: the router value is replaced whenever the location or a navigation's pending state changes, and
24
+ // callers keep this function in effect dependencies (the mail connection's `open`).
25
+ const latest = useRef({ navigate, pathname });
26
+ latest.current = { navigate, pathname };
27
+ return useCallback((href, options = {}) => {
28
+ const { navigate: go, pathname: here } = latest.current;
29
+ // A relative or absolute-path destination is resolved against the page's own address, and only ever compared by path.
30
+ const destination = new URL(href, `http://router.invalid${here}`);
31
+ const shallow = destination.origin === "http://router.invalid" && destination.pathname === here;
32
+ void go(href, { replace: options.replace, shallow });
33
+ }, []);
34
+ }
@@ -0,0 +1,11 @@
1
+ import type { Branding } from "@rapidmx/react-shared/branding/brandingApi.js";
2
+ /** The name the deployment goes by in the browser tab: its branding title, else its company name, else RapidMX. */
3
+ export declare function brandName(branding?: Pick<Branding, "title" | "companyName"> | null): string;
4
+ /**
5
+ * The `title` export of a webmail page: `export const title = pageTitle("Calendar")`. `@rapidrest/react` renders it into the
6
+ * document's `<title>` on the server - so the tab is right from the first byte, and a crawler sees it - and sends it with every
7
+ * client navigation, so the tab follows the page. `label` is the app's name (`Acme: Calendar`), the same for all of an app's pages.
8
+ */
9
+ export declare function pageTitle(label: string): (props: {
10
+ branding?: Pick<Branding, "title" | "companyName"> | null;
11
+ }) => string;
@@ -0,0 +1,12 @@
1
+ /** The name the deployment goes by in the browser tab: its branding title, else its company name, else RapidMX. */
2
+ export function brandName(branding) {
3
+ return branding?.title || branding?.companyName || "RapidMX";
4
+ }
5
+ /**
6
+ * The `title` export of a webmail page: `export const title = pageTitle("Calendar")`. `@rapidrest/react` renders it into the
7
+ * document's `<title>` on the server - so the tab is right from the first byte, and a crawler sees it - and sends it with every
8
+ * client navigation, so the tab follows the page. `label` is the app's name (`Acme: Calendar`), the same for all of an app's pages.
9
+ */
10
+ export function pageTitle(label) {
11
+ return ({ branding }) => `${brandName(branding)}: ${label}`;
12
+ }
@@ -0,0 +1,72 @@
1
+ import type { SearchResult } from "@rapidmx/react-shared/search/searchApi.js";
2
+ /**
3
+ * How far a cross-mailbox search fans out. Exported (and mutable) so a test can shrink them instead of building 26 mailboxes or waiting a
4
+ * minute for a timeout; nothing else writes to it.
5
+ */
6
+ export declare const SEARCH_LIMITS: {
7
+ /** How many mailboxes are worked on at once. */
8
+ concurrency: number;
9
+ /** The most mailboxes one search covers - the caller's own first, then the shared ones (`orderMailboxes()`); a note says how many were left out. */
10
+ maxMailboxes: number;
11
+ /** How long one mailbox's Tier 1 (server) request may take before that mailbox is given up on, so one wedged mailbox cannot hold the search. */
12
+ tier1TimeoutMs: number;
13
+ /** The same for a mailbox's Tier 3 pass (fetching and decrypting up to a couple of hundred candidates, so much longer). */
14
+ tier3TimeoutMs: number;
15
+ /** The most result rows a search keeps on screen (the page's own cap for a list, `MAX_LOADED_ROWS`, applies to a search too). */
16
+ maxRows: number;
17
+ };
18
+ /**
19
+ * How many results each mailbox contributes to one page. One mailbox gives the whole page; with several the page is shared out between them
20
+ * (never below `MIN_MAILBOX_PAGE_SIZE`), so the merged first page stays about one page long instead of growing by the number of mailboxes.
21
+ */
22
+ export declare function searchPageSize(mailboxCount: number, pageSize: number): number;
23
+ /** A search hit that remembers which mailbox it was found in. */
24
+ export interface MailboxHit extends SearchResult {
25
+ mailboxUid: string;
26
+ }
27
+ export declare function tagHits(mailboxUid: string, hits: SearchResult[]): MailboxHit[];
28
+ /** One hit's identity across mailboxes: the same entity uid found in two mailboxes is two hits. */
29
+ export declare function hitKey(hit: {
30
+ mailboxUid: string;
31
+ entityUid: string;
32
+ }): string;
33
+ /**
34
+ * Merges Tier 1 (server, possibly `metadataOnly` for an encrypted message), Tier 2 (local index, fully decrypted and re-scored) and Tier 3
35
+ * (server-narrowed candidates, decrypted and re-scored) results into one ranked list, per `specs/search.md` §7's "client MUST re-score all
36
+ * results it can see... normalise into the same space rather than interleaving raw scores": each tier is normalized independently via
37
+ * `normalizeServerScores()` before merging, since a Postgres/OpenSearch score, a local `bm25()` score, and Tier 3's own term-count score all
38
+ * occupy unrelated ranges. A hit present in more than one list keeps only the last-inserted entry (Tier 2 wins over Tier 3 wins over Tier 1) -
39
+ * Tier 2 and Tier 3 both represent genuine, content-verified scores for the same message, so which one "wins" on overlap doesn't change
40
+ * correctness, only which of two equally-valid scores is shown; either supersedes Tier 1's metadata-only guess for the same message.
41
+ *
42
+ * Each tier's list is pooled across every mailbox searched before it is normalized, so one mailbox's best hit is not automatically "1.0"
43
+ * next to another's: the ranking is the same rule as for one mailbox, applied to the merged lists. A hit's identity is its mailbox and its
44
+ * entity uid (`hitKey()`), so the same uid can only collapse within a mailbox. Ties keep the order the lists were given in (a stable sort), which
45
+ * the caller makes the mailboxes' display order.
46
+ *
47
+ * Called progressively - each time with whatever tiers have reported so far (an empty array for the rest). Pure and stateless.
48
+ */
49
+ export declare function mergeSearchResults(tier1: MailboxHit[], tier2: MailboxHit[], tier3: MailboxHit[]): MailboxHit[];
50
+ /** Raised by `withTimeout()`. */
51
+ export declare class SearchTimeoutError extends Error {
52
+ constructor();
53
+ }
54
+ /** `promise`, or a rejection with `SearchTimeoutError` if it has not settled after `ms` (never, for a non-finite `ms`). The work is not aborted - a late answer is just dropped. */
55
+ export declare function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T>;
56
+ /**
57
+ * Runs `task` for every item, at most `concurrency` at a time, starting them in order. Stops starting new ones once `isCancelled()` (the search
58
+ * was superseded) or once one has rejected - which is then this function's own rejection, after the ones already running have settled.
59
+ */
60
+ export declare function runLimited<T>(items: readonly T[], concurrency: number, task: (item: T) => Promise<void>, isCancelled: () => boolean): Promise<void>;
61
+ /** Why one mailbox's search failed, as a short phrase for the notice: the class of failure, never the server's own text. */
62
+ export declare function classifyFailure(err: unknown): string;
63
+ /** A mailbox that could not be (fully) searched. `encrypted` is set when only its Tier 3 pass (the encrypted mail) failed - its other results are in. */
64
+ export interface MailboxFailure {
65
+ mailboxUid: string;
66
+ /** The mailbox's display name, as it was when the search ran. */
67
+ mailboxName: string;
68
+ encrypted: boolean;
69
+ reason: string;
70
+ }
71
+ /** The body of the "Some results may be missing" notice: which mailboxes, and why each. */
72
+ export declare function describeFailures(failures: readonly MailboxFailure[]): string;
@@ -0,0 +1,136 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ /**
6
+ * The pieces of searching several mailboxes at once (Mail's "All mailboxes" views, and a search widened from one mailbox's folder to every
7
+ * mailbox the reader can read). Nothing here talks to the server: `apps/www/index.tsx` fans its own per-mailbox pipeline (Tier 1 server search,
8
+ * Tier 2 local index, Tier 3 server-narrowed encrypted candidates) out across mailboxes and uses these to bound that fan-out, to merge what
9
+ * comes back into one ranked list and to say, in words, which mailbox could not be searched and why.
10
+ *
11
+ * A search of one mailbox is the same code with a list of one: nothing here changes what that returns.
12
+ */
13
+ import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
14
+ import { normalizeServerScores } from "@rapidmx/react-shared/search/searchScoring.js";
15
+ /**
16
+ * How far a cross-mailbox search fans out. Exported (and mutable) so a test can shrink them instead of building 26 mailboxes or waiting a
17
+ * minute for a timeout; nothing else writes to it.
18
+ */
19
+ export const SEARCH_LIMITS = {
20
+ /** How many mailboxes are worked on at once. */
21
+ concurrency: 4,
22
+ /** The most mailboxes one search covers - the caller's own first, then the shared ones (`orderMailboxes()`); a note says how many were left out. */
23
+ maxMailboxes: 25,
24
+ /** How long one mailbox's Tier 1 (server) request may take before that mailbox is given up on, so one wedged mailbox cannot hold the search. */
25
+ tier1TimeoutMs: 15000,
26
+ /** The same for a mailbox's Tier 3 pass (fetching and decrypting up to a couple of hundred candidates, so much longer). */
27
+ tier3TimeoutMs: 60000,
28
+ /** The most result rows a search keeps on screen (the page's own cap for a list, `MAX_LOADED_ROWS`, applies to a search too). */
29
+ maxRows: 500,
30
+ };
31
+ /** The least a mailbox is asked for on one page while several are searched, so a search over many mailboxes still moves at a sensible pace. */
32
+ const MIN_MAILBOX_PAGE_SIZE = 10;
33
+ /**
34
+ * How many results each mailbox contributes to one page. One mailbox gives the whole page; with several the page is shared out between them
35
+ * (never below `MIN_MAILBOX_PAGE_SIZE`), so the merged first page stays about one page long instead of growing by the number of mailboxes.
36
+ */
37
+ export function searchPageSize(mailboxCount, pageSize) {
38
+ return Math.max(MIN_MAILBOX_PAGE_SIZE, Math.ceil(pageSize / Math.max(1, mailboxCount)));
39
+ }
40
+ export function tagHits(mailboxUid, hits) {
41
+ return hits.map((hit) => ({ ...hit, mailboxUid }));
42
+ }
43
+ /** One hit's identity across mailboxes: the same entity uid found in two mailboxes is two hits. */
44
+ export function hitKey(hit) {
45
+ return `${hit.mailboxUid}\u0000${hit.entityUid}`;
46
+ }
47
+ /**
48
+ * Merges Tier 1 (server, possibly `metadataOnly` for an encrypted message), Tier 2 (local index, fully decrypted and re-scored) and Tier 3
49
+ * (server-narrowed candidates, decrypted and re-scored) results into one ranked list, per `specs/search.md` §7's "client MUST re-score all
50
+ * results it can see... normalise into the same space rather than interleaving raw scores": each tier is normalized independently via
51
+ * `normalizeServerScores()` before merging, since a Postgres/OpenSearch score, a local `bm25()` score, and Tier 3's own term-count score all
52
+ * occupy unrelated ranges. A hit present in more than one list keeps only the last-inserted entry (Tier 2 wins over Tier 3 wins over Tier 1) -
53
+ * Tier 2 and Tier 3 both represent genuine, content-verified scores for the same message, so which one "wins" on overlap doesn't change
54
+ * correctness, only which of two equally-valid scores is shown; either supersedes Tier 1's metadata-only guess for the same message.
55
+ *
56
+ * Each tier's list is pooled across every mailbox searched before it is normalized, so one mailbox's best hit is not automatically "1.0"
57
+ * next to another's: the ranking is the same rule as for one mailbox, applied to the merged lists. A hit's identity is its mailbox and its
58
+ * entity uid (`hitKey()`), so the same uid can only collapse within a mailbox. Ties keep the order the lists were given in (a stable sort), which
59
+ * the caller makes the mailboxes' display order.
60
+ *
61
+ * Called progressively - each time with whatever tiers have reported so far (an empty array for the rest). Pure and stateless.
62
+ */
63
+ export function mergeSearchResults(tier1, tier2, tier3) {
64
+ const merged = new Map();
65
+ for (const tier of [tier1, tier3, tier2]) {
66
+ for (const entry of normalizeServerScores(tier)) {
67
+ merged.set(hitKey(entry.result), entry);
68
+ }
69
+ }
70
+ return Array.from(merged.values())
71
+ .sort((a, b) => b.normalizedScore - a.normalizedScore)
72
+ .map((entry) => entry.result);
73
+ }
74
+ /** Raised by `withTimeout()`. */
75
+ export class SearchTimeoutError extends Error {
76
+ constructor() {
77
+ super("The search took too long.");
78
+ this.name = "SearchTimeoutError";
79
+ }
80
+ }
81
+ /** `promise`, or a rejection with `SearchTimeoutError` if it has not settled after `ms` (never, for a non-finite `ms`). The work is not aborted - a late answer is just dropped. */
82
+ export function withTimeout(promise, ms) {
83
+ if (!Number.isFinite(ms)) {
84
+ return promise;
85
+ }
86
+ return new Promise((resolve, reject) => {
87
+ const timer = setTimeout(() => reject(new SearchTimeoutError()), ms);
88
+ promise.then((value) => {
89
+ clearTimeout(timer);
90
+ resolve(value);
91
+ }, (reason) => {
92
+ clearTimeout(timer);
93
+ reject(reason);
94
+ });
95
+ });
96
+ }
97
+ /**
98
+ * Runs `task` for every item, at most `concurrency` at a time, starting them in order. Stops starting new ones once `isCancelled()` (the search
99
+ * was superseded) or once one has rejected - which is then this function's own rejection, after the ones already running have settled.
100
+ */
101
+ export async function runLimited(items, concurrency, task, isCancelled) {
102
+ let next = 0;
103
+ let failed = false;
104
+ const worker = async () => {
105
+ while (!failed && !isCancelled() && next < items.length) {
106
+ const item = items[next++];
107
+ try {
108
+ await task(item);
109
+ }
110
+ catch (err) {
111
+ failed = true;
112
+ throw err;
113
+ }
114
+ }
115
+ };
116
+ await Promise.all(Array.from({ length: Math.min(concurrency, items.length) }, worker));
117
+ }
118
+ /** Why one mailbox's search failed, as a short phrase for the notice: the class of failure, never the server's own text. */
119
+ export function classifyFailure(err) {
120
+ if (err instanceof SearchTimeoutError) {
121
+ return "timed out";
122
+ }
123
+ if (err instanceof ApiRequestError) {
124
+ if (err.status === 401 || err.status === 403) {
125
+ return "access denied";
126
+ }
127
+ return err.status >= 500 ? "server error" : "rejected by the server";
128
+ }
129
+ return "network error";
130
+ }
131
+ /** The body of the "Some results may be missing" notice: which mailboxes, and why each. */
132
+ export function describeFailures(failures) {
133
+ return `${failures
134
+ .map((failure) => `${failure.mailboxName} (${failure.encrypted ? "encrypted mail: " : ""}${failure.reason})`)
135
+ .join(", ")} could not be searched.`;
136
+ }