@rapidmx/web-client 0.30.0 → 0.31.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 (400) hide show
  1. package/apps/admin/audit-log/index.tsx +3 -3
  2. package/apps/admin/branding/index.tsx +3 -3
  3. package/apps/admin/data-requests/index.tsx +18 -7
  4. package/apps/admin/distribution-lists/[uid].tsx +3 -3
  5. package/apps/admin/distribution-lists/index.tsx +3 -3
  6. package/apps/admin/distribution-lists/new/index.tsx +3 -3
  7. package/apps/admin/domains/[uid].tsx +6 -5
  8. package/apps/admin/domains/index.tsx +3 -3
  9. package/apps/admin/domains/new/index.tsx +3 -3
  10. package/apps/admin/escrow-scopes/[uid].tsx +11 -5
  11. package/apps/admin/escrow-scopes/index.tsx +3 -3
  12. package/apps/admin/escrow-scopes/new/index.tsx +9 -3
  13. package/apps/admin/index.tsx +4 -4
  14. package/apps/admin/ingest-queue/index.tsx +3 -3
  15. package/apps/admin/mailboxes/[uid].tsx +15 -7
  16. package/apps/admin/quarantine/index.tsx +4 -4
  17. package/apps/admin/retention-policy/index.tsx +3 -3
  18. package/apps/admin/signing-certificates/index.tsx +3 -3
  19. package/apps/admin/transport-rules/[uid].tsx +4 -4
  20. package/apps/admin/transport-rules/index.tsx +3 -3
  21. package/apps/admin/transport-rules/new/index.tsx +3 -3
  22. package/apps/escrow/matters/[uid].tsx +26 -2
  23. package/apps/escrow/matters/new/index.tsx +4 -0
  24. package/apps/shared/calendar/calendarLiveUpdates.ts +11 -10
  25. package/apps/shared/calendar/useCalendarReminders.ts +2 -0
  26. package/apps/shared/components/admin/ActionAlert.tsx +32 -0
  27. package/apps/shared/components/admin/diagnostics/InformationPanel.tsx +1 -1
  28. package/apps/shared/components/admin/diagnostics/InstalledPlugins.tsx +1 -1
  29. package/apps/shared/components/admin/diagnostics/LogsPanel.tsx +1 -1
  30. package/apps/shared/components/admin/diagnostics/RuntimePanel.tsx +1 -1
  31. package/apps/shared/components/admin/diagnostics/SystemPanel.tsx +1 -1
  32. package/apps/shared/components/admin/distributionLists/MemberListCard.tsx +4 -4
  33. package/apps/shared/components/admin/elevation.ts +19 -0
  34. package/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.tsx +58 -3
  35. package/apps/shared/components/admin/layout/AdminShell.tsx +6 -1
  36. package/apps/shared/components/admin/mailboxes/EraseLeftoverDataDialog.tsx +3 -3
  37. package/apps/shared/components/admin/mailboxes/EscrowScopeCard.tsx +4 -4
  38. package/apps/shared/components/admin/mailboxes/LeftoverMailboxesSection.tsx +4 -4
  39. package/apps/shared/components/admin/mailboxes/ResourceSettingsCard.tsx +3 -3
  40. package/apps/shared/components/admin/mailboxes/ShareAccessCard.tsx +6 -6
  41. package/apps/shared/components/admin/settings/BrandingForm.tsx +22 -9
  42. package/apps/shared/components/admin/settings/DomainDnsSetup.tsx +4 -4
  43. package/apps/shared/components/admin/settings/EncryptionPolicyForm.tsx +33 -4
  44. package/apps/shared/components/admin/settings/LoadedSettingsForm.tsx +3 -3
  45. package/apps/shared/components/admin/settings/MailboxCreateForm.tsx +8 -3
  46. package/apps/shared/components/admin/settings/MailboxPolicyForm.tsx +3 -3
  47. package/apps/shared/components/admin/settings/PluginsManager.tsx +39 -17
  48. package/apps/shared/components/admin/settings/RetentionPolicyForm.tsx +25 -12
  49. package/apps/shared/components/admin/setup/EscrowSetupStep.tsx +40 -7
  50. package/apps/shared/components/admin/setup/SetupWizard.tsx +8 -6
  51. package/apps/shared/components/admin/signOut.ts +3 -0
  52. package/apps/shared/components/admin/usePagedList.tsx +2 -2
  53. package/apps/shared/components/calendar/DescriptionEditor.tsx +2 -1
  54. package/apps/shared/components/calendar/EventDetails.tsx +34 -8
  55. package/apps/shared/components/calendar/EventEditor.tsx +90 -40
  56. package/apps/shared/components/calendar/EventFormParts.tsx +5 -2
  57. package/apps/shared/components/calendar/EventMatchNav.tsx +14 -2
  58. package/apps/shared/components/calendar/EventModal.tsx +25 -1
  59. package/apps/shared/components/calendar/FindATime.tsx +10 -2
  60. package/apps/shared/components/calendar/ListView.tsx +99 -25
  61. package/apps/shared/components/calendar/RecurrenceEditor.tsx +19 -8
  62. package/apps/shared/components/calendar/SplitDayView.tsx +14 -18
  63. package/apps/shared/components/calendar/TaskQuickForm.tsx +3 -2
  64. package/apps/shared/components/calendar/TimeGridView.tsx +35 -19
  65. package/apps/shared/components/calendar/gridLayout.ts +94 -0
  66. package/apps/shared/components/calendar/layout/CalendarShell.tsx +1 -1
  67. package/apps/shared/components/calendar/timePicker.ts +5 -2
  68. package/apps/shared/components/contacts/ContactCard.tsx +15 -4
  69. package/apps/shared/components/contacts/ContactDetailPane.tsx +54 -15
  70. package/apps/shared/components/contacts/ContactForm.tsx +61 -17
  71. package/apps/shared/components/contacts/ContactPhotoEditor.tsx +88 -0
  72. package/apps/shared/components/contacts/ContactPhotoField.tsx +36 -39
  73. package/apps/shared/components/contacts/layout/ContactsShell.tsx +1 -1
  74. package/apps/shared/components/contacts/participantDetails.ts +18 -1
  75. package/apps/shared/components/layout/AppShell.tsx +9 -0
  76. package/apps/shared/components/layout/KeyEnrollmentGate.tsx +9 -4
  77. package/apps/shared/components/layout/MailboxProvisioning.tsx +31 -3
  78. package/apps/shared/components/layout/RecoveryCodeUnlock.tsx +4 -2
  79. package/apps/shared/components/mail/ConversationList.tsx +49 -6
  80. package/apps/shared/components/mail/ConversationThreadPane.tsx +1 -1
  81. package/apps/shared/components/mail/MenuButton.tsx +3 -400
  82. package/apps/shared/components/mail/MessageDetailPane.tsx +16 -13
  83. package/apps/shared/components/mail/compose/ComposeToolbar.tsx +11 -1
  84. package/apps/shared/components/mail/compose/ComposeWindow.tsx +57 -11
  85. package/apps/shared/components/mail/compose/GifPicker.tsx +10 -4
  86. package/apps/shared/components/mail/compose/RecipientInput.tsx +9 -7
  87. package/apps/shared/components/mail/compose/composeFlushRegistry.ts +14 -1
  88. package/apps/shared/components/mail/compose/encryptionRequirement.ts +5 -0
  89. package/apps/shared/components/mail/compose/quotedBody.ts +31 -6
  90. package/apps/shared/components/mail/layout/MailShell.tsx +1 -1
  91. package/apps/shared/components/mail/reading/MessageBody.tsx +13 -7
  92. package/apps/shared/components/mail/reading/bodyContent.ts +2 -0
  93. package/apps/shared/components/mail/reading/bodyHtml.ts +25 -2
  94. package/apps/shared/components/mail/writableMailboxes.ts +2 -0
  95. package/apps/shared/components/rules/RuleBuilder.tsx +11 -2
  96. package/apps/shared/components/settings/GravatarSetting.tsx +27 -0
  97. package/apps/shared/components/settings/layout/SettingsShell.tsx +1 -1
  98. package/apps/shared/components/tasks/dueDate.ts +29 -0
  99. package/apps/shared/components/tasks/layout/TasksShell.tsx +1 -1
  100. package/apps/shared/mail/decryptedMessages.ts +51 -2
  101. package/apps/shared/mail/listSnapshots.ts +2 -0
  102. package/apps/shared/mail/outbox/sendDecision.ts +14 -3
  103. package/apps/shared/mail/outbox/sendJob.ts +5 -0
  104. package/apps/shared/mail/useMailLiveUpdates.ts +7 -1
  105. package/apps/shared/mail/usePermanentDelete.tsx +3 -1
  106. package/apps/shared/notifications/apiErrors.ts +2 -0
  107. package/apps/shared/notifications/preferences.ts +1 -1
  108. package/apps/shared/notifications/store.ts +3 -1
  109. package/apps/shared/plugins/pluginNav.ts +4 -2
  110. package/apps/www/calendar/index.tsx +24 -12
  111. package/apps/www/contacts/[uid].tsx +6 -1
  112. package/apps/www/contacts/index.tsx +66 -12
  113. package/apps/www/index.tsx +91 -28
  114. package/apps/www/messages/[uid].tsx +8 -3
  115. package/apps/www/settings/auto-reply/index.tsx +13 -0
  116. package/apps/www/settings/blocked-senders/index.tsx +4 -0
  117. package/apps/www/settings/encryption/index.tsx +66 -4
  118. package/apps/www/settings/privacy/index.tsx +4 -1
  119. package/apps/www/settings/profile/index.tsx +12 -1
  120. package/apps/www/settings/sharing/index.tsx +11 -1
  121. package/apps/www/settings/signatures/[uid].tsx +6 -4
  122. package/apps/www/tasks/index.tsx +97 -29
  123. package/dist/apps/admin/audit-log/index.js +3 -3
  124. package/dist/apps/admin/branding/index.js +3 -3
  125. package/dist/apps/admin/data-requests/index.js +17 -7
  126. package/dist/apps/admin/distribution-lists/[uid].js +3 -3
  127. package/dist/apps/admin/distribution-lists/index.js +3 -3
  128. package/dist/apps/admin/distribution-lists/new/index.js +3 -3
  129. package/dist/apps/admin/domains/[uid].js +6 -5
  130. package/dist/apps/admin/domains/index.js +3 -3
  131. package/dist/apps/admin/domains/new/index.js +3 -3
  132. package/dist/apps/admin/escrow-scopes/[uid].js +11 -6
  133. package/dist/apps/admin/escrow-scopes/index.js +3 -3
  134. package/dist/apps/admin/escrow-scopes/new/index.js +9 -4
  135. package/dist/apps/admin/index.js +4 -4
  136. package/dist/apps/admin/ingest-queue/index.js +3 -3
  137. package/dist/apps/admin/mailboxes/[uid].js +15 -7
  138. package/dist/apps/admin/quarantine/index.js +4 -4
  139. package/dist/apps/admin/retention-policy/index.js +3 -3
  140. package/dist/apps/admin/signing-certificates/index.js +3 -3
  141. package/dist/apps/admin/transport-rules/[uid].js +4 -4
  142. package/dist/apps/admin/transport-rules/index.js +3 -3
  143. package/dist/apps/admin/transport-rules/new/index.js +3 -3
  144. package/dist/apps/escrow/matters/[uid].js +8 -3
  145. package/dist/apps/escrow/matters/new/index.js +4 -0
  146. package/dist/apps/shared/calendar/calendarLiveUpdates.d.ts +11 -7
  147. package/dist/apps/shared/calendar/calendarLiveUpdates.js +10 -9
  148. package/dist/apps/shared/calendar/useCalendarReminders.js +2 -0
  149. package/dist/apps/shared/components/admin/ActionAlert.d.ts +13 -0
  150. package/dist/apps/shared/components/admin/ActionAlert.js +24 -0
  151. package/dist/apps/shared/components/admin/diagnostics/InformationPanel.js +1 -1
  152. package/dist/apps/shared/components/admin/diagnostics/InstalledPlugins.js +1 -1
  153. package/dist/apps/shared/components/admin/diagnostics/LogsPanel.js +1 -1
  154. package/dist/apps/shared/components/admin/diagnostics/RuntimePanel.js +1 -1
  155. package/dist/apps/shared/components/admin/diagnostics/SystemPanel.js +1 -1
  156. package/dist/apps/shared/components/admin/distributionLists/MemberListCard.js +4 -4
  157. package/dist/apps/shared/components/admin/elevation.d.ts +9 -0
  158. package/dist/apps/shared/components/admin/elevation.js +16 -0
  159. package/dist/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.d.ts +2 -0
  160. package/dist/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.js +41 -1
  161. package/dist/apps/shared/components/admin/layout/AdminShell.js +2 -1
  162. package/dist/apps/shared/components/admin/mailboxes/EraseLeftoverDataDialog.js +3 -3
  163. package/dist/apps/shared/components/admin/mailboxes/EscrowScopeCard.js +4 -4
  164. package/dist/apps/shared/components/admin/mailboxes/LeftoverMailboxesSection.js +4 -4
  165. package/dist/apps/shared/components/admin/mailboxes/ResourceSettingsCard.js +3 -3
  166. package/dist/apps/shared/components/admin/mailboxes/ShareAccessCard.js +6 -6
  167. package/dist/apps/shared/components/admin/settings/BrandingForm.js +17 -7
  168. package/dist/apps/shared/components/admin/settings/DomainDnsSetup.js +4 -4
  169. package/dist/apps/shared/components/admin/settings/EncryptionPolicyForm.js +17 -5
  170. package/dist/apps/shared/components/admin/settings/LoadedSettingsForm.js +3 -3
  171. package/dist/apps/shared/components/admin/settings/MailboxCreateForm.js +8 -3
  172. package/dist/apps/shared/components/admin/settings/MailboxPolicyForm.js +3 -3
  173. package/dist/apps/shared/components/admin/settings/PluginsManager.js +26 -12
  174. package/dist/apps/shared/components/admin/settings/RetentionPolicyForm.js +17 -10
  175. package/dist/apps/shared/components/admin/setup/EscrowSetupStep.js +18 -8
  176. package/dist/apps/shared/components/admin/setup/SetupWizard.js +8 -6
  177. package/dist/apps/shared/components/admin/signOut.js +3 -0
  178. package/dist/apps/shared/components/admin/usePagedList.js +2 -2
  179. package/dist/apps/shared/components/calendar/DescriptionEditor.js +2 -1
  180. package/dist/apps/shared/components/calendar/EventDetails.d.ts +3 -1
  181. package/dist/apps/shared/components/calendar/EventDetails.js +7 -4
  182. package/dist/apps/shared/components/calendar/EventEditor.js +81 -33
  183. package/dist/apps/shared/components/calendar/EventFormParts.d.ts +3 -1
  184. package/dist/apps/shared/components/calendar/EventFormParts.js +3 -3
  185. package/dist/apps/shared/components/calendar/EventMatchNav.d.ts +1 -1
  186. package/dist/apps/shared/components/calendar/EventMatchNav.js +12 -2
  187. package/dist/apps/shared/components/calendar/EventModal.js +10 -2
  188. package/dist/apps/shared/components/calendar/FindATime.js +4 -3
  189. package/dist/apps/shared/components/calendar/ListView.d.ts +10 -4
  190. package/dist/apps/shared/components/calendar/ListView.js +92 -27
  191. package/dist/apps/shared/components/calendar/RecurrenceEditor.d.ts +4 -1
  192. package/dist/apps/shared/components/calendar/RecurrenceEditor.js +15 -10
  193. package/dist/apps/shared/components/calendar/SplitDayView.js +11 -17
  194. package/dist/apps/shared/components/calendar/TaskQuickForm.js +3 -2
  195. package/dist/apps/shared/components/calendar/TimeGridView.js +23 -21
  196. package/dist/apps/shared/components/calendar/gridLayout.d.ts +41 -0
  197. package/dist/apps/shared/components/calendar/gridLayout.js +83 -0
  198. package/dist/apps/shared/components/calendar/layout/CalendarShell.js +1 -1
  199. package/dist/apps/shared/components/calendar/timePicker.js +5 -2
  200. package/dist/apps/shared/components/contacts/ContactCard.js +9 -3
  201. package/dist/apps/shared/components/contacts/ContactDetailPane.d.ts +5 -3
  202. package/dist/apps/shared/components/contacts/ContactDetailPane.js +11 -5
  203. package/dist/apps/shared/components/contacts/ContactForm.js +54 -16
  204. package/dist/apps/shared/components/contacts/ContactPhotoEditor.d.ts +21 -0
  205. package/dist/apps/shared/components/contacts/ContactPhotoEditor.js +56 -0
  206. package/dist/apps/shared/components/contacts/ContactPhotoField.d.ts +7 -4
  207. package/dist/apps/shared/components/contacts/ContactPhotoField.js +19 -21
  208. package/dist/apps/shared/components/contacts/layout/ContactsShell.js +1 -1
  209. package/dist/apps/shared/components/contacts/participantDetails.d.ts +4 -1
  210. package/dist/apps/shared/components/contacts/participantDetails.js +17 -1
  211. package/dist/apps/shared/components/layout/AppShell.js +9 -0
  212. package/dist/apps/shared/components/layout/KeyEnrollmentGate.js +2 -3
  213. package/dist/apps/shared/components/layout/MailboxProvisioning.d.ts +4 -1
  214. package/dist/apps/shared/components/layout/MailboxProvisioning.js +24 -2
  215. package/dist/apps/shared/components/layout/RecoveryCodeUnlock.d.ts +4 -2
  216. package/dist/apps/shared/components/layout/RecoveryCodeUnlock.js +4 -2
  217. package/dist/apps/shared/components/mail/ConversationList.js +49 -6
  218. package/dist/apps/shared/components/mail/ConversationThreadPane.js +1 -1
  219. package/dist/apps/shared/components/mail/MenuButton.d.ts +2 -78
  220. package/dist/apps/shared/components/mail/MenuButton.js +2 -211
  221. package/dist/apps/shared/components/mail/MessageDetailPane.js +10 -6
  222. package/dist/apps/shared/components/mail/compose/ComposeToolbar.d.ts +3 -0
  223. package/dist/apps/shared/components/mail/compose/ComposeToolbar.js +10 -1
  224. package/dist/apps/shared/components/mail/compose/ComposeWindow.js +55 -10
  225. package/dist/apps/shared/components/mail/compose/GifPicker.js +10 -4
  226. package/dist/apps/shared/components/mail/compose/RecipientInput.js +9 -7
  227. package/dist/apps/shared/components/mail/compose/composeFlushRegistry.d.ts +7 -1
  228. package/dist/apps/shared/components/mail/compose/composeFlushRegistry.js +13 -1
  229. package/dist/apps/shared/components/mail/compose/encryptionRequirement.d.ts +5 -0
  230. package/dist/apps/shared/components/mail/compose/quotedBody.js +28 -5
  231. package/dist/apps/shared/components/mail/layout/MailShell.js +1 -1
  232. package/dist/apps/shared/components/mail/reading/MessageBody.js +11 -6
  233. package/dist/apps/shared/components/mail/reading/bodyContent.js +2 -0
  234. package/dist/apps/shared/components/mail/reading/bodyHtml.d.ts +5 -1
  235. package/dist/apps/shared/components/mail/reading/bodyHtml.js +23 -2
  236. package/dist/apps/shared/components/mail/writableMailboxes.js +2 -0
  237. package/dist/apps/shared/components/rules/RuleBuilder.js +9 -1
  238. package/dist/apps/shared/components/settings/GravatarSetting.d.ts +6 -0
  239. package/dist/apps/shared/components/settings/GravatarSetting.js +15 -0
  240. package/dist/apps/shared/components/settings/layout/SettingsShell.js +1 -1
  241. package/dist/apps/shared/components/tasks/dueDate.d.ts +12 -0
  242. package/dist/apps/shared/components/tasks/dueDate.js +26 -0
  243. package/dist/apps/shared/components/tasks/layout/TasksShell.js +1 -1
  244. package/dist/apps/shared/mail/decryptedMessages.d.ts +8 -0
  245. package/dist/apps/shared/mail/decryptedMessages.js +46 -2
  246. package/dist/apps/shared/mail/listSnapshots.js +2 -0
  247. package/dist/apps/shared/mail/outbox/sendDecision.d.ts +6 -3
  248. package/dist/apps/shared/mail/outbox/sendDecision.js +13 -3
  249. package/dist/apps/shared/mail/outbox/sendJob.d.ts +1 -0
  250. package/dist/apps/shared/mail/outbox/sendJob.js +5 -1
  251. package/dist/apps/shared/mail/useMailLiveUpdates.d.ts +3 -0
  252. package/dist/apps/shared/mail/useMailLiveUpdates.js +6 -1
  253. package/dist/apps/shared/mail/usePermanentDelete.d.ts +2 -0
  254. package/dist/apps/shared/mail/usePermanentDelete.js +1 -1
  255. package/dist/apps/shared/notifications/apiErrors.js +2 -0
  256. package/dist/apps/shared/notifications/preferences.d.ts +1 -1
  257. package/dist/apps/shared/notifications/preferences.js +1 -1
  258. package/dist/apps/shared/notifications/store.js +3 -1
  259. package/dist/apps/shared/plugins/pluginNav.d.ts +2 -1
  260. package/dist/apps/shared/plugins/pluginNav.js +4 -2
  261. package/dist/apps/www/calendar/index.js +24 -12
  262. package/dist/apps/www/contacts/[uid].js +6 -2
  263. package/dist/apps/www/contacts/index.js +62 -11
  264. package/dist/apps/www/index.js +85 -30
  265. package/dist/apps/www/messages/[uid].js +8 -3
  266. package/dist/apps/www/settings/auto-reply/index.js +13 -1
  267. package/dist/apps/www/settings/blocked-senders/index.js +4 -0
  268. package/dist/apps/www/settings/encryption/index.js +29 -5
  269. package/dist/apps/www/settings/privacy/index.js +3 -2
  270. package/dist/apps/www/settings/profile/index.js +4 -1
  271. package/dist/apps/www/settings/sharing/index.js +10 -1
  272. package/dist/apps/www/settings/signatures/[uid].js +6 -4
  273. package/dist/apps/www/tasks/index.js +94 -32
  274. package/dist/lib/admin/auditLogApi.js +2 -2
  275. package/dist/lib/admin/distributionListsApi.js +2 -2
  276. package/dist/lib/admin/domainsApi.js +2 -2
  277. package/dist/lib/admin/escrowAuditLogApi.js +2 -2
  278. package/dist/lib/admin/escrowScopesApi.js +2 -2
  279. package/dist/lib/admin/pluginsApi.d.ts +13 -2
  280. package/dist/lib/admin/pluginsApi.js +8 -0
  281. package/dist/lib/admin/retentionPolicyApi.d.ts +3 -0
  282. package/dist/lib/admin/retentionPolicyApi.js +3 -0
  283. package/dist/lib/admin/transportRulesApi.js +2 -2
  284. package/dist/lib/appearance/preferencesApi.d.ts +1 -1
  285. package/dist/lib/appearance/preferencesApi.js +3 -14
  286. package/dist/lib/auth/session.d.ts +51 -12
  287. package/dist/lib/auth/session.js +214 -38
  288. package/dist/lib/branding/brandingApi.d.ts +3 -3
  289. package/dist/lib/branding/brandingApi.js +9 -21
  290. package/dist/lib/calendar/calendarApi.js +3 -1
  291. package/dist/lib/calendar/eventSearch.js +43 -6
  292. package/dist/lib/calendar/freeBusyApi.d.ts +2 -1
  293. package/dist/lib/calendar/freeBusyApi.js +13 -5
  294. package/dist/lib/calendar/recurrence.js +51 -3
  295. package/dist/lib/calendar/useDayKey.d.ts +7 -0
  296. package/dist/lib/calendar/useDayKey.js +43 -0
  297. package/dist/lib/components/avatar/ContactAvatar.d.ts +6 -2
  298. package/dist/lib/components/avatar/ContactAvatar.js +37 -8
  299. package/dist/lib/components/avatar/ContactPhotoAvatar.d.ts +11 -0
  300. package/dist/lib/components/avatar/ContactPhotoAvatar.js +18 -0
  301. package/dist/lib/components/forms/CameraCaptureModal.d.ts +17 -0
  302. package/dist/lib/components/forms/CameraCaptureModal.js +88 -0
  303. package/dist/lib/components/forms/ImageEditBadge.d.ts +28 -0
  304. package/dist/lib/components/forms/ImageEditBadge.js +62 -0
  305. package/dist/lib/components/menus/MenuButton.d.ts +78 -0
  306. package/dist/lib/components/menus/MenuButton.js +219 -0
  307. package/dist/lib/components/navigation/BottomTabBar.d.ts +3 -1
  308. package/dist/lib/components/navigation/BottomTabBar.js +22 -3
  309. package/dist/lib/components/overlays/PopoverPortal.d.ts +2 -1
  310. package/dist/lib/components/overlays/PopoverPortal.js +3 -2
  311. package/dist/lib/components/pickers/TimeZonePicker.js +13 -1
  312. package/dist/lib/contacts/contactsApi.d.ts +5 -2
  313. package/dist/lib/contacts/contactsApi.js +16 -19
  314. package/dist/lib/contacts/gravatar.d.ts +14 -3
  315. package/dist/lib/contacts/gravatar.js +42 -5
  316. package/dist/lib/contacts/preparePhoto.d.ts +24 -0
  317. package/dist/lib/contacts/preparePhoto.js +250 -0
  318. package/dist/lib/contacts/useContactPhotoSrc.d.ts +19 -0
  319. package/dist/lib/contacts/useContactPhotoSrc.js +153 -0
  320. package/dist/lib/contacts/vcard.d.ts +3 -0
  321. package/dist/lib/contacts/vcard.js +59 -7
  322. package/dist/lib/crypto/escrowKeys.d.ts +8 -0
  323. package/dist/lib/crypto/escrowKeys.js +20 -1
  324. package/dist/lib/crypto/messageSecurity.d.ts +7 -4
  325. package/dist/lib/crypto/messageSecurity.js +23 -5
  326. package/dist/lib/crypto/passwordUnlock.d.ts +6 -1
  327. package/dist/lib/crypto/passwordUnlock.js +13 -2
  328. package/dist/lib/crypto/smime.js +12 -2
  329. package/dist/lib/mail/compose/composeQuoting.js +3 -2
  330. package/dist/lib/mail/compose/quotedImages.d.ts +6 -3
  331. package/dist/lib/mail/compose/quotedImages.js +35 -9
  332. package/dist/lib/mail/labelsApi.js +2 -2
  333. package/dist/lib/mail/mailApi.d.ts +8 -5
  334. package/dist/lib/mail/mailApi.js +73 -39
  335. package/dist/lib/mail/mailDetailHooks.js +11 -3
  336. package/dist/lib/mail/mailFilterRulesApi.js +2 -2
  337. package/dist/lib/mail/mailSignaturesApi.js +2 -2
  338. package/dist/lib/mail/mailboxImportApi.d.ts +1 -1
  339. package/dist/lib/mail/mailboxImportApi.js +3 -15
  340. package/dist/lib/mail/messageBodySanitizer.d.ts +6 -0
  341. package/dist/lib/mail/messageBodySanitizer.js +44 -1
  342. package/dist/lib/search/queryGrammar.js +12 -4
  343. package/dist/lib/search/searchTier3.d.ts +6 -1
  344. package/dist/lib/search/searchTier3.js +54 -25
  345. package/dist/lib/tasks/tasksApi.js +3 -3
  346. package/dist/lib/util/api.d.ts +26 -4
  347. package/dist/lib/util/api.js +73 -16
  348. package/dist/lib/util/apiQuery.d.ts +11 -0
  349. package/dist/lib/util/apiQuery.js +11 -0
  350. package/dist/lib/util/settleWithLimit.d.ts +5 -0
  351. package/dist/lib/util/settleWithLimit.js +25 -0
  352. package/lib/admin/auditLogApi.ts +2 -2
  353. package/lib/admin/distributionListsApi.ts +2 -2
  354. package/lib/admin/domainsApi.ts +2 -2
  355. package/lib/admin/escrowAuditLogApi.ts +2 -2
  356. package/lib/admin/escrowScopesApi.ts +2 -2
  357. package/lib/admin/pluginsApi.ts +21 -2
  358. package/lib/admin/retentionPolicyApi.ts +4 -0
  359. package/lib/admin/transportRulesApi.ts +2 -2
  360. package/lib/appearance/preferencesApi.ts +3 -14
  361. package/lib/auth/session.ts +239 -39
  362. package/lib/branding/brandingApi.ts +9 -21
  363. package/lib/calendar/calendarApi.ts +3 -1
  364. package/lib/calendar/eventSearch.ts +46 -6
  365. package/lib/calendar/freeBusyApi.ts +14 -6
  366. package/lib/calendar/recurrence.ts +59 -8
  367. package/lib/calendar/useDayKey.ts +45 -0
  368. package/lib/components/avatar/ContactAvatar.tsx +46 -8
  369. package/lib/components/avatar/ContactPhotoAvatar.tsx +26 -0
  370. package/lib/components/forms/CameraCaptureModal.tsx +133 -0
  371. package/lib/components/forms/ImageEditBadge.tsx +121 -0
  372. package/lib/components/menus/MenuButton.tsx +408 -0
  373. package/lib/components/navigation/BottomTabBar.tsx +30 -8
  374. package/lib/components/overlays/PopoverPortal.tsx +3 -2
  375. package/lib/components/pickers/TimeZonePicker.tsx +17 -1
  376. package/lib/contacts/contactsApi.ts +18 -19
  377. package/lib/contacts/gravatar.ts +47 -5
  378. package/lib/contacts/preparePhoto.ts +273 -0
  379. package/lib/contacts/useContactPhotoSrc.ts +177 -0
  380. package/lib/contacts/vcard.ts +60 -7
  381. package/lib/crypto/escrowKeys.ts +21 -1
  382. package/lib/crypto/messageSecurity.ts +33 -9
  383. package/lib/crypto/passwordUnlock.ts +14 -2
  384. package/lib/crypto/smime.ts +13 -2
  385. package/lib/mail/compose/composeQuoting.ts +3 -2
  386. package/lib/mail/compose/quotedImages.ts +39 -9
  387. package/lib/mail/labelsApi.ts +2 -2
  388. package/lib/mail/mailApi.ts +96 -39
  389. package/lib/mail/mailDetailHooks.ts +11 -3
  390. package/lib/mail/mailFilterRulesApi.ts +2 -2
  391. package/lib/mail/mailSignaturesApi.ts +2 -2
  392. package/lib/mail/mailboxImportApi.ts +9 -15
  393. package/lib/mail/messageBodySanitizer.ts +47 -1
  394. package/lib/search/queryGrammar.ts +13 -4
  395. package/lib/search/searchTier3.ts +65 -32
  396. package/lib/tasks/tasksApi.ts +3 -3
  397. package/lib/util/api.ts +99 -16
  398. package/lib/util/apiQuery.ts +13 -0
  399. package/lib/util/settleWithLimit.ts +25 -0
  400. package/package.json +1 -1
@@ -9,7 +9,7 @@
9
9
  * stages the upload and returns the `pending` request; poll `getImportRequest()`/`listImportRequests()`
10
10
  * for `status` to become `"completed"` (with `importedCount`/`failedCount`) or `"failed"`.
11
11
  */
12
- import { ApiRequestError, apiUrl, withClient, withCsrfHeader } from "../util/api.js";
12
+ import { withClient, withClientRaw } from "../util/api.js";
13
13
  import { buildRequestListQuery } from "../util/apiQuery.js";
14
14
  /**
15
15
  * Uploads `file`'s raw bytes as a new mailbox-import request. Bypasses `apiFetch` — that helper always
@@ -18,24 +18,12 @@ import { buildRequestListQuery } from "../util/apiQuery.js";
18
18
  * with `format`/`targetFolderUid`/`mailboxUid` as query-string parameters, not a JSON body — the same
19
19
  * shape `mailApi.ts`'s `uploadAttachment()` already establishes for a raw-bytes upload.
20
20
  */
21
- export async function uploadMailboxImport(file, input) {
21
+ export function uploadMailboxImport(file, input, client) {
22
22
  const params = new URLSearchParams({ format: input.format, targetFolderUid: input.targetFolderUid });
23
23
  if (input.mailboxUid) {
24
24
  params.set("mailboxUid", input.mailboxUid);
25
25
  }
26
- const res = await fetch(apiUrl(`/mail/mailbox-import-requests?${params.toString()}`), {
27
- method: "POST",
28
- credentials: "include",
29
- headers: withCsrfHeader({ "Content-Type": input.format === "pst" ? "application/vnd.ms-outlook" : "application/mbox" }),
30
- body: file,
31
- });
32
- const contentType = res.headers.get("content-type") ?? "";
33
- const responseBody = contentType.includes("application/json") ? await res.json().catch(() => undefined) : undefined;
34
- if (!res.ok) {
35
- const message = (responseBody && (responseBody.message || responseBody.error)) || res.statusText || "Upload failed.";
36
- throw new ApiRequestError(message, res.status, responseBody?.code);
37
- }
38
- return responseBody;
26
+ return withClientRaw(client, `/mail/mailbox-import-requests?${params.toString()}`, "POST", file, input.format === "pst" ? "application/vnd.ms-outlook" : "application/mbox");
39
27
  }
40
28
  /** `client`, given by every function below that isn't a raw-bytes upload, is an explicit `ApiClient` from
41
29
  * `createApiClient()` (e.g. one account of a multi-account app) to call instead of the default global
@@ -7,6 +7,12 @@ export declare function stripRemoteCssUrls(css: string): string;
7
7
  * sanitize with - never the unsanitized input.
8
8
  */
9
9
  export declare function sanitizeMessageBodyHtml(html: string): string;
10
+ /**
11
+ * Sanitizes a signature's HTML for the compose body: DOMPurify's defaults (no scripts, handlers or `javascript:` URLs) and
12
+ * the quote's tag list (nothing that styles, frames or submits). Unlike a quoted message, a signature is the sender's own
13
+ * and may carry pictures from the web, so remote images and links stay. Returns `""` when there is no DOM to sanitize with.
14
+ */
15
+ export declare function sanitizeSignatureHtml(html: string): string;
10
16
  /**
11
17
  * Sanitizes message HTML for quoting into a compose body: the display policy (`sanitizeMessageBodyHtml()`), without
12
18
  * the tags in `QUOTE_FORBIDDEN_TAGS`, and without any image that isn't embedded as a `data:` URI - a `cid:` image
@@ -22,6 +22,36 @@ export function stripRemoteCssUrls(css) {
22
22
  .replace(/(?:-webkit-)?image-set\((?:[^()]|\([^()]*\))*\)/gi, (match) => [...match.matchAll(/(["'])(.*?)\1/g)].every(([, , uri]) => EMBEDDED_URI.test(uri)) ? match : "none")
23
23
  .replace(/url\(\s*(["']?)(.*?)\1\s*\)/gi, (match, _quote, uri) => (EMBEDDED_URI.test(uri) ? match : "none"));
24
24
  }
25
+ /** The candidate URLs of a `srcset` value, parsed as the HTML spec does: a candidate's URL is a run of non-whitespace
26
+ * characters (it may itself contain commas - a `data:` URI always does), so a comma that ends a URL or follows its
27
+ * descriptors separates candidates whether or not whitespace follows it. */
28
+ function srcsetUrls(value) {
29
+ const urls = [];
30
+ let position = 0;
31
+ while (position < value.length) {
32
+ const start = value.slice(position).search(/[^\s,]/);
33
+ if (start < 0) {
34
+ break;
35
+ }
36
+ position += start;
37
+ const end = value.slice(position).search(/\s/);
38
+ const token = end < 0 ? value.slice(position) : value.slice(position, position + end);
39
+ position += token.length;
40
+ urls.push(token.replace(/,+$/, ""));
41
+ if (!token.endsWith(",")) {
42
+ // Skip the descriptors, up to the comma that ends the candidate.
43
+ const comma = value.indexOf(",", position);
44
+ const descriptors = value.slice(position, comma < 0 ? value.length : comma);
45
+ if (descriptors.includes("(")) {
46
+ // The spec's tokenizer ignores commas inside parentheses, so a browser may read the candidates that follow differently from
47
+ // the way this does. No valid descriptor has one: refuse the whole value.
48
+ urls.push(descriptors);
49
+ }
50
+ position = comma < 0 ? value.length : comma + 1;
51
+ }
52
+ }
53
+ return urls;
54
+ }
25
55
  let bodyPurifier;
26
56
  /** A dedicated DOMPurify instance (hooks registered here never leak into any other DOMPurify caller) that
27
57
  * additionally strips every remote resource reference - see `stripRemoteCssUrls()`. `undefined` without a DOM
@@ -51,7 +81,7 @@ function getBodyPurifier() {
51
81
  }
52
82
  const isNavigationLink = name === "href" && ["a", "area"].includes(node.nodeName.toLowerCase());
53
83
  if (RESOURCE_URI_ATTRIBUTES.has(name) || (name === "href" && !isNavigationLink)) {
54
- const candidates = name === "srcset" ? data.attrValue.split(/,\s+/) : [data.attrValue];
84
+ const candidates = name === "srcset" ? srcsetUrls(data.attrValue) : [data.attrValue];
55
85
  if (!candidates.every((candidate) => EMBEDDED_URI.test(candidate))) {
56
86
  data.keepAttr = false;
57
87
  }
@@ -95,6 +125,19 @@ const QUOTE_FORBIDDEN_TAGS = [
95
125
  "track",
96
126
  // svg/math are already in DISPLAY_FORBIDDEN_TAGS, spread above — not repeated here.
97
127
  ];
128
+ let signaturePurifier;
129
+ /**
130
+ * Sanitizes a signature's HTML for the compose body: DOMPurify's defaults (no scripts, handlers or `javascript:` URLs) and
131
+ * the quote's tag list (nothing that styles, frames or submits). Unlike a quoted message, a signature is the sender's own
132
+ * and may carry pictures from the web, so remote images and links stay. Returns `""` when there is no DOM to sanitize with.
133
+ */
134
+ export function sanitizeSignatureHtml(html) {
135
+ if (typeof window === "undefined") {
136
+ return "";
137
+ }
138
+ signaturePurifier ?? (signaturePurifier = DOMPurify(window));
139
+ return signaturePurifier.isSupported ? signaturePurifier.sanitize(html, { FORBID_TAGS: QUOTE_FORBIDDEN_TAGS }) : "";
140
+ }
98
141
  /**
99
142
  * Sanitizes message HTML for quoting into a compose body: the display policy (`sanitizeMessageBodyHtml()`), without
100
143
  * the tags in `QUOTE_FORBIDDEN_TAGS`, and without any image that isn't embedded as a `data:` URI - a `cid:` image
@@ -7,14 +7,22 @@ const VALID_ENTITY_TYPES = new Set(["message", "contact", "calendarEvent", "note
7
7
  * `parseSearchQuery()` itself, after every extraction has run. */
8
8
  function extractFirst(input, key) {
9
9
  const pattern = new RegExp(`(^|\\s)${key}:(?:"([^"]*)"|(\\S+))`, "i");
10
- const match = pattern.exec(input);
11
- if (!match) {
10
+ const found = pattern.exec(maskQuoted(input));
11
+ if (!found) {
12
12
  return { rest: input };
13
13
  }
14
+ // The masked text only says where the operator is - what it holds is read from the real one.
15
+ const match = pattern.exec(input.slice(found.index));
14
16
  const value = match[2] ?? match[3] ?? "";
15
- const rest = input.slice(0, match.index) + match[1] + input.slice(match.index + match[0].length);
17
+ const rest = input.slice(0, found.index) + match[1] + input.slice(found.index + match[0].length);
16
18
  return { value, rest };
17
19
  }
20
+ /** `input` with the inside of every quoted phrase blanked out (the length stays the same, so positions carry over to
21
+ * `input`): a word that looks like an operator inside a phrase - `"meeting from:bob"` - is part of the phrase, never an
22
+ * operator. */
23
+ function maskQuoted(input) {
24
+ return input.replace(/"([^"]*)"/g, (_phrase, inner) => `"${"\0".repeat(inner.length)}"`);
25
+ }
18
26
  /** Repeatedly applies `extractFirst()` for `key` until no more occurrences remain, splitting any
19
27
  * comma-separated value into individual entries — so `is:read is:flagged` and `is:read,flagged` both
20
28
  * produce `["read", "flagged"]`. */
@@ -39,7 +47,7 @@ function extractAll(input, key) {
39
47
  * spec defines for this operator — there is no `has:<other value>` form. */
40
48
  function extractHasAttachment(input) {
41
49
  const pattern = /(^|\s)has:attachment\b/i;
42
- const match = pattern.exec(input);
50
+ const match = pattern.exec(maskQuoted(input));
43
51
  if (!match) {
44
52
  return { found: false, rest: input };
45
53
  }
@@ -38,13 +38,18 @@ export declare function stripHtml(html: string): string;
38
38
  /** How many candidates are fetched/decrypted at once - bounds the burst of raw-MIME requests and
39
39
  * concurrent WebCrypto work a large candidate page would otherwise fire all at once. */
40
40
  export declare const TIER3_DECRYPT_CONCURRENCY = 4;
41
+ /** The most candidate pages one search walks (`nextCursor` onwards) while it is still short of `limit` results - a
42
+ * search that matches little must not download and decrypt a whole mailbox, but an encrypted message beyond the first
43
+ * page of candidates is still found. */
44
+ export declare const TIER3_MAX_CANDIDATE_PAGES = 4;
41
45
  export interface SearchEncryptedCandidatesOptions {
42
46
  /** Forwarded to `candidates()` - which accessible mailbox to search (e.g. the one currently open).
43
47
  * Omitted, the server uses the caller's own mailbox. */
44
48
  mailboxUid?: string;
45
49
  }
46
50
  /**
47
- * Runs Tier 3 for one parsed query: fetches a bounded candidate set from the server, decrypts each
51
+ * Runs Tier 3 for one parsed query: fetches a bounded candidate set from the server (following `nextCursor` for up to
52
+ * `TIER3_MAX_CANDIDATE_PAGES` pages while fewer than `limit` messages have matched), decrypts each
48
53
  * candidate this device can open (at most `TIER3_DECRYPT_CONCURRENCY` at a time), and returns only the
49
54
  * ones whose real (decrypted) content actually matches - each with a raw (not yet normalized) score the
50
55
  * caller should run through `searchScoring.ts#normalizeServerScores()` alongside Tier 1's own scores
@@ -224,6 +224,10 @@ function hasAnyFilter(parsed) {
224
224
  /** How many candidates are fetched/decrypted at once - bounds the burst of raw-MIME requests and
225
225
  * concurrent WebCrypto work a large candidate page would otherwise fire all at once. */
226
226
  export const TIER3_DECRYPT_CONCURRENCY = 4;
227
+ /** The most candidate pages one search walks (`nextCursor` onwards) while it is still short of `limit` results - a
228
+ * search that matches little must not download and decrypt a whole mailbox, but an encrypted message beyond the first
229
+ * page of candidates is still found. */
230
+ export const TIER3_MAX_CANDIDATE_PAGES = 4;
227
231
  /** `Promise.allSettled()` over `items`, but never running more than `concurrency` `task`s at a time.
228
232
  * Outcomes are returned in `items`' own order. */
229
233
  async function settleWithConcurrency(items, concurrency, task) {
@@ -244,7 +248,8 @@ async function settleWithConcurrency(items, concurrency, task) {
244
248
  return outcomes;
245
249
  }
246
250
  /**
247
- * Runs Tier 3 for one parsed query: fetches a bounded candidate set from the server, decrypts each
251
+ * Runs Tier 3 for one parsed query: fetches a bounded candidate set from the server (following `nextCursor` for up to
252
+ * `TIER3_MAX_CANDIDATE_PAGES` pages while fewer than `limit` messages have matched), decrypts each
248
253
  * candidate this device can open (at most `TIER3_DECRYPT_CONCURRENCY` at a time), and returns only the
249
254
  * ones whose real (decrypted) content actually matches - each with a raw (not yet normalized) score the
250
255
  * caller should run through `searchScoring.ts#normalizeServerScores()` alongside Tier 1's own scores
@@ -266,35 +271,59 @@ export async function searchEncryptedCandidates(parsed, unlocked, limit = 50, op
266
271
  return [];
267
272
  }
268
273
  const participants = [parsed.from, parsed.to, parsed.cc].filter((value) => Boolean(value));
269
- const page = await candidates({
270
- types: ["message"],
271
- participants: participants.length > 0 ? participants : undefined,
272
- before: parsed.before,
273
- after: parsed.after,
274
- folderUid: parsed.folderUid,
275
- flags: parsed.flags,
276
- labels: parsed.labels,
277
- limit,
278
- mailboxUid: options.mailboxUid,
279
- });
280
- const settled = await settleWithConcurrency(page.candidates.filter((candidate) => candidate.entityType === "message"), TIER3_DECRYPT_CONCURRENCY, async (candidate) => {
281
- if (parsed.hasAttachment !== undefined) {
282
- const message = await getMessage(candidate.entityUid);
283
- if (Boolean(message.hasAttachments) !== parsed.hasAttachment) {
284
- return undefined;
274
+ const results = [];
275
+ let cursor;
276
+ for (let pageNumber = 0; pageNumber < TIER3_MAX_CANDIDATE_PAGES; pageNumber++) {
277
+ let page;
278
+ try {
279
+ page = await candidates({
280
+ types: ["message"],
281
+ participants: participants.length > 0 ? participants : undefined,
282
+ before: parsed.before,
283
+ after: parsed.after,
284
+ folderUid: parsed.folderUid,
285
+ flags: parsed.flags,
286
+ labels: parsed.labels,
287
+ limit,
288
+ cursor,
289
+ mailboxUid: options.mailboxUid,
290
+ });
291
+ }
292
+ catch (err) {
293
+ if (pageNumber === 0) {
294
+ throw err;
285
295
  }
296
+ // A later page failing keeps what the earlier ones found.
297
+ break;
286
298
  }
287
- const rawMime = await getMessageRawContent(candidate.entityUid);
299
+ const settled = await settleWithConcurrency(page.candidates.filter((candidate) => candidate.entityType === "message"), TIER3_DECRYPT_CONCURRENCY, async (candidate) => {
300
+ if (parsed.hasAttachment !== undefined) {
301
+ const message = await getMessage(candidate.entityUid);
302
+ if (Boolean(message.hasAttachments) !== parsed.hasAttachment) {
303
+ return undefined;
304
+ }
305
+ }
306
+ const rawMime = await getMessageRawContent(candidate.entityUid);
307
+ if (unlocked.destroyed) {
308
+ return undefined;
309
+ }
310
+ const security = await evaluateMessageSecurity(rawMime, unlocked);
311
+ return { entityUid: candidate.entityUid, security };
312
+ });
288
313
  if (unlocked.destroyed) {
289
- return undefined;
314
+ // Locked while candidates were being fetched/decrypted - don't surface decrypted content after a lock.
315
+ return [];
316
+ }
317
+ results.push(...matchDecrypted(parsed, settled));
318
+ cursor = page.nextCursor;
319
+ if (!cursor || results.length >= limit) {
320
+ break;
290
321
  }
291
- const security = await evaluateMessageSecurity(rawMime, unlocked);
292
- return { entityUid: candidate.entityUid, security };
293
- });
294
- if (unlocked.destroyed) {
295
- // Locked while candidates were being fetched/decrypted - don't surface decrypted content after a lock.
296
- return [];
297
322
  }
323
+ return results;
324
+ }
325
+ /** The decrypted candidates of one page whose real content matches `parsed`, scored and with their snippets. */
326
+ function matchDecrypted(parsed, settled) {
298
327
  const subjectFilter = parsed.subject?.toLowerCase();
299
328
  const results = [];
300
329
  for (const outcome of settled) {
@@ -5,13 +5,13 @@
5
5
  /** Typed wrappers over `@rapidmx/restapi`'s `/mail/tasks` REST surface — see `mailApi.ts`'s own header
6
6
  * comment for the shared ACL/authorization model every wrapper file here follows. */
7
7
  import { withClient } from "../util/api.js";
8
- import { buildQuery } from "../util/apiQuery.js";
8
+ import { SETTINGS_LIST_LIMIT, buildQuery } from "../util/apiQuery.js";
9
9
  /** `client`, given by every function below, is an explicit `ApiClient` from `createApiClient()` (e.g. one
10
10
  * account of a multi-account app) to call instead of the default global `apiFetch()` - see `withClient()`'s
11
11
  * own doc comment in `util/api.ts`. Omitted (the default), every function here behaves exactly as before. */
12
12
  /** Lists a folder's tasks, soonest due date first (tasks with no due date sort last). */
13
13
  export function listTasks(folderUid, params = {}, client) {
14
- return withClient(client, `/mail/tasks?${buildQuery(params, { folderUid, sort: JSON.stringify({ dueDate: "ASC" }) })}`);
14
+ return withClient(client, `/mail/tasks?${buildQuery(params, { folderUid, sort: JSON.stringify({ dueDate: "ASC", uid: "ASC" }) })}`);
15
15
  }
16
16
  export function createTask(input, client) {
17
17
  return withClient(client, "/mail/tasks", {
@@ -39,7 +39,7 @@ export function deleteTask(uid, version, client) {
39
39
  }
40
40
  /** Lists a mailbox's task lists (Outlook To-Do-style custom lists), alphabetically by name. */
41
41
  export function listTaskLists(mailboxUid, params = {}, client) {
42
- return withClient(client, `/mail/task-lists?${buildQuery(params, { mailboxUid, sort: JSON.stringify({ name: "ASC" }) })}`);
42
+ return withClient(client, `/mail/task-lists?${buildQuery({ limit: SETTINGS_LIST_LIMIT, ...params }, { mailboxUid, sort: JSON.stringify({ name: "ASC", uid: "ASC" }) })}`);
43
43
  }
44
44
  export function createTaskList(input, client) {
45
45
  return withClient(client, "/mail/task-lists", { method: "POST", body: JSON.stringify(input) });
@@ -86,10 +86,12 @@ export declare function setApiSessionRecovery(recovery: (() => Promise<boolean>)
86
86
  * a deployment-level requirement owned by auth-server/the Helm chart, not configured here — and auth-server's
87
87
  * CORS config must explicitly allow this app's origin with credentials.
88
88
  *
89
- * `applyCsrfHeader()` is still called here for consistency, but it never actually finds a token for a truly
90
- * cross-origin call — see its own doc comment on `readCsrfCookie()` for why that's correct rather than a
91
- * gap: auth-server's CSRF protection for this call shape comes from its own Origin allow-list check, not a
92
- * double-submit cookie this app's JavaScript could never read in the first place.
89
+ * No `x-csrf-token` header is sent, deliberately. A double-submit token is only meaningful to the server that issued it, and the `csrf`
90
+ * cookie this app's JavaScript can read is *this* host's own (`mail.example.com`), never auth-server's - so echoing it
91
+ * would be a wrong token for the wrong server. Worse, auth-server's CORS rules do not allow that header, so the browser
92
+ * blocked the preflight of every such call (the silent session refresh, sign-out) before it ever reached the server: "Request header
93
+ * field x-csrf-token is not allowed by Access-Control-Allow-Headers". Auth-server's CSRF protection for this cross-origin call shape is
94
+ * its Origin allow-list check, which needs no header.
93
95
  */
94
96
  export declare function authApiFetch<T = unknown>(authServerUrl: string, path: string, init?: RequestInit): Promise<T>;
95
97
  /**
@@ -110,6 +112,12 @@ export interface ApiClient {
110
112
  /** Same contract as `apiFetch()`: prefixes `path` with `/api`, parses a JSON response, and rejects with
111
113
  * `ApiRequestError` on a non-ok response. */
112
114
  fetch<T = unknown>(path: string, init?: RequestInit): Promise<T>;
115
+ /**
116
+ * Like `fetch()` - same origin, authentication and error decoding - but resolves the response's own bytes instead of parsing JSON, for a picture or other
117
+ * file. An `<img src>` cannot send an `Authorization` header, so a token-authenticated client shows such a file by fetching it here and displaying a
118
+ * `blob:` URL. No `Content-Type` is added.
119
+ */
120
+ fetchBlob(path: string, init?: RequestInit): Promise<Blob>;
113
121
  /**
114
122
  * This client instance's own `setApiUnauthorizedObserver()` equivalent - see `createApiClient()`'s doc
115
123
  * comment for why it is per-instance rather than shared with the module-level `setApiUnauthorizedObserver()`
@@ -164,3 +172,17 @@ export declare function createApiClient(options: CreateApiClientOptions): ApiCli
164
172
  * that call too, so a whole call chain stays pinned to one account.
165
173
  */
166
174
  export declare function withClient<T = unknown>(client: ApiClient | undefined, path: string, init?: RequestInit): Promise<T>;
175
+ /**
176
+ * Sends a file's own bytes (not JSON) to `path` - the upload counterpart of `withClient()`. With an explicit `client` the request goes through it, so it
177
+ * reaches that account's origin with its token (the client keeps the `Content-Type` given here). Without one it is a plain `fetch()` to the global
178
+ * origin that carries the `jwt` cookie and the CSRF header, exactly as a same-origin upload always was. Resolves the decoded JSON response and rejects with
179
+ * an `ApiRequestError` (`failureMessage` when the server gave no message) on a non-ok one.
180
+ */
181
+ export declare function withClientRaw<T = unknown>(client: ApiClient | undefined, path: string, method: string, file: Blob, contentType?: string, failureMessage?: string): Promise<T>;
182
+ /**
183
+ * Fetches `path`'s own bytes (a message's raw source, say) rather than JSON - the download counterpart of `withClient()`. With an explicit `client` the
184
+ * request goes through its `fetchBlob()`, so it reaches that account's origin with its token. Without one it is a plain `fetch()` to the global origin
185
+ * with the `jwt` cookie, renewing the session and asking again once on a `401`, as `apiFetch()` does. Rejects with an `ApiRequestError`
186
+ * (`failureMessage` when the server gave no message) on a non-ok response.
187
+ */
188
+ export declare function withClientBlob(client: ApiClient | undefined, path: string, failureMessage?: string): Promise<Blob>;
@@ -137,12 +137,17 @@ export async function apiFetch(path, init = {}) {
137
137
  headers.set("Content-Type", "application/json");
138
138
  applyCsrfHeader(headers, init.method);
139
139
  const credentials = apiBaseUrl ? "include" : init.credentials;
140
- let res = await fetch(apiUrl(path), { ...init, headers, credentials });
141
- // The access token has run out (a sleeping tab or laptop stops the timer that renews it in time): renew the session, and ask again once.
140
+ const res = await fetchWithRecovery(path, { ...init, headers, credentials });
141
+ return decodeApiResponse(res, (error) => unauthorizedObserver?.(error));
142
+ }
143
+ /** `fetch()` of `path` against the global origin. When the access token has run out (a sleeping tab or laptop stops the timer that renews it in
144
+ * time) it renews the session, and asks again once. */
145
+ async function fetchWithRecovery(path, init) {
146
+ let res = await fetch(apiUrl(path), init);
142
147
  if (res.status === 401 && sessionRecovery && (await recoverSession())) {
143
- res = await fetch(apiUrl(path), { ...init, headers, credentials });
148
+ res = await fetch(apiUrl(path), init);
144
149
  }
145
- return decodeApiResponse(res, (error) => unauthorizedObserver?.(error));
150
+ return res;
146
151
  }
147
152
  let sessionRecovery;
148
153
  /**
@@ -173,15 +178,16 @@ async function recoverSession() {
173
178
  * a deployment-level requirement owned by auth-server/the Helm chart, not configured here — and auth-server's
174
179
  * CORS config must explicitly allow this app's origin with credentials.
175
180
  *
176
- * `applyCsrfHeader()` is still called here for consistency, but it never actually finds a token for a truly
177
- * cross-origin call — see its own doc comment on `readCsrfCookie()` for why that's correct rather than a
178
- * gap: auth-server's CSRF protection for this call shape comes from its own Origin allow-list check, not a
179
- * double-submit cookie this app's JavaScript could never read in the first place.
181
+ * No `x-csrf-token` header is sent, deliberately. A double-submit token is only meaningful to the server that issued it, and the `csrf`
182
+ * cookie this app's JavaScript can read is *this* host's own (`mail.example.com`), never auth-server's - so echoing it
183
+ * would be a wrong token for the wrong server. Worse, auth-server's CORS rules do not allow that header, so the browser
184
+ * blocked the preflight of every such call (the silent session refresh, sign-out) before it ever reached the server: "Request header
185
+ * field x-csrf-token is not allowed by Access-Control-Allow-Headers". Auth-server's CSRF protection for this cross-origin call shape is
186
+ * its Origin allow-list check, which needs no header.
180
187
  */
181
188
  export async function authApiFetch(authServerUrl, path, init = {}) {
182
189
  const headers = new Headers(init.headers);
183
190
  headers.set("Content-Type", "application/json");
184
- applyCsrfHeader(headers, init.method);
185
191
  const res = await fetch(`${authServerUrl}/api${path}`, { ...init, headers, credentials: "include" });
186
192
  return decodeApiResponse(res);
187
193
  }
@@ -201,12 +207,14 @@ export function setApiUnauthorizedObserver(observer) {
201
207
  * `createApiClient()`-made client's `fetch()` (which forwards to that instance's own
202
208
  * `setUnauthorizedObserver()`) pass their own. A throwing `onUnauthorized` never changes what the caller sees.
203
209
  */
204
- async function decodeApiResponse(res, onUnauthorized) {
210
+ async function decodeApiResponse(res, onUnauthorized, failureMessage = "Request failed.") {
205
211
  const contentType = res.headers.get("content-type") ?? "";
206
212
  const body = contentType.includes("application/json") ? await res.json().catch(() => undefined) : undefined;
207
213
  if (!res.ok) {
208
- const message = (body && (body.message || body.error)) || res.statusText || "Request failed.";
209
- const error = new ApiRequestError(message, res.status, body?.code, body);
214
+ // A body is untrusted: only a non-empty string is a message (anything else would read "[object Object]"), only a string a code.
215
+ const reported = body && (body.message || body.error);
216
+ const message = (typeof reported === "string" && reported) || res.statusText || failureMessage;
217
+ const error = new ApiRequestError(message, res.status, typeof body?.code === "string" ? body.code : undefined, body);
210
218
  if (res.status === 401 && onUnauthorized) {
211
219
  try {
212
220
  onUnauthorized(error);
@@ -242,16 +250,30 @@ export function createApiClient(options) {
242
250
  const baseUrl = options.baseUrl.replace(/\/$/, "");
243
251
  const { getAccessToken } = options;
244
252
  let unauthorizedObserver;
245
- async function clientFetch(path, init = {}) {
253
+ async function send(path, init, json) {
246
254
  const headers = new Headers(init.headers);
247
- headers.set("Content-Type", "application/json");
255
+ // JSON unless the caller sent a body of its own kind (an upload names the file's type).
256
+ if (json && !headers.has("Content-Type")) {
257
+ headers.set("Content-Type", "application/json");
258
+ }
248
259
  const token = await getAccessToken();
249
260
  headers.set("Authorization", `jwt ${token}`);
250
- const res = await fetch(`${baseUrl}/api${path}`, { ...init, headers });
251
- return decodeApiResponse(res, (error) => unauthorizedObserver?.(error));
261
+ return fetch(`${baseUrl}/api${path}`, { ...init, headers });
262
+ }
263
+ async function clientFetch(path, init = {}) {
264
+ return decodeApiResponse(await send(path, init, true), (error) => unauthorizedObserver?.(error));
265
+ }
266
+ async function clientFetchBlob(path, init = {}) {
267
+ const res = await send(path, init, false);
268
+ if (!res.ok) {
269
+ // Throws the same `ApiRequestError` (and tells the observer of a 401) as every other request.
270
+ await decodeApiResponse(res, (error) => unauthorizedObserver?.(error));
271
+ }
272
+ return res.blob();
252
273
  }
253
274
  return {
254
275
  fetch: clientFetch,
276
+ fetchBlob: clientFetchBlob,
255
277
  setUnauthorizedObserver(observer) {
256
278
  unauthorizedObserver = observer;
257
279
  },
@@ -270,3 +292,38 @@ export function createApiClient(options) {
270
292
  export function withClient(client, path, init) {
271
293
  return client ? client.fetch(path, init) : apiFetch(path, init);
272
294
  }
295
+ /**
296
+ * Sends a file's own bytes (not JSON) to `path` - the upload counterpart of `withClient()`. With an explicit `client` the request goes through it, so it
297
+ * reaches that account's origin with its token (the client keeps the `Content-Type` given here). Without one it is a plain `fetch()` to the global
298
+ * origin that carries the `jwt` cookie and the CSRF header, exactly as a same-origin upload always was. Resolves the decoded JSON response and rejects with
299
+ * an `ApiRequestError` (`failureMessage` when the server gave no message) on a non-ok one.
300
+ */
301
+ export async function withClientRaw(client, path, method, file, contentType = file.type, failureMessage = "Upload failed.") {
302
+ if (client) {
303
+ return client.fetch(path, { method, headers: { "Content-Type": contentType }, body: file });
304
+ }
305
+ const res = await fetchWithRecovery(path, {
306
+ method,
307
+ credentials: "include",
308
+ headers: withCsrfHeader({ "Content-Type": contentType }, method),
309
+ body: file,
310
+ });
311
+ return decodeApiResponse(res, (error) => unauthorizedObserver?.(error), failureMessage);
312
+ }
313
+ /**
314
+ * Fetches `path`'s own bytes (a message's raw source, say) rather than JSON - the download counterpart of `withClient()`. With an explicit `client` the
315
+ * request goes through its `fetchBlob()`, so it reaches that account's origin with its token. Without one it is a plain `fetch()` to the global origin
316
+ * with the `jwt` cookie, renewing the session and asking again once on a `401`, as `apiFetch()` does. Rejects with an `ApiRequestError`
317
+ * (`failureMessage` when the server gave no message) on a non-ok response.
318
+ */
319
+ export async function withClientBlob(client, path, failureMessage = "Download failed.") {
320
+ if (client) {
321
+ return client.fetchBlob(path);
322
+ }
323
+ const res = await fetchWithRecovery(path, { credentials: "include" });
324
+ if (!res.ok) {
325
+ // Throws the same `ApiRequestError` (and tells the observer of a 401) as every other request.
326
+ await decodeApiResponse(res, (error) => unauthorizedObserver?.(error), failureMessage);
327
+ }
328
+ return res.blob();
329
+ }
@@ -8,11 +8,22 @@ export interface ListParams {
8
8
  page?: number;
9
9
  limit?: number;
10
10
  }
11
+ /** The page size of a list that a settings screen shows whole (labels, signatures, filter rules, task lists, contact lists): a few dozen rows at most in practice, so one
12
+ * large page rather than the default 25 that would hide everything after the 25th with no way to reach it. */
13
+ export declare const SETTINGS_LIST_LIMIT = 200;
11
14
  /**
12
15
  * Builds a `limit`/`page` query string, plus any extra scope/filter params (e.g. `folderUid`,
13
16
  * or one of `@rapidmx/restapi`'s generic query-operator values like `startDate=lte(...)`).
14
17
  */
15
18
  export declare function buildQuery(params: ListParams, extra?: Record<string, string>): string;
19
+ /**
20
+ * The `sort` query value for `buildQuery()`'s extra params, in the order the keys are given. Paging with no sort leaves the order to the
21
+ * database, which on SQL can repeat or skip a row between two pages and on MongoDB is the oldest first: end the sort with a unique key (`uid`)
22
+ * so every row lands on exactly one page.
23
+ */
24
+ export declare function sortQuery(sort: Record<string, "ASC" | "DESC">): {
25
+ sort: string;
26
+ };
16
27
  /**
17
28
  * Optional paging/scope params for the async-request list endpoints (`listExportRequests()`,
18
29
  * `listImportRequests()`, `listErasureRequests()`, `listMatterExportRequests()`,
@@ -9,6 +9,9 @@
9
9
  * other than it happening to live there first.
10
10
  */
11
11
  const DEFAULT_PAGE_SIZE = 25;
12
+ /** The page size of a list that a settings screen shows whole (labels, signatures, filter rules, task lists, contact lists): a few dozen rows at most in practice, so one
13
+ * large page rather than the default 25 that would hide everything after the 25th with no way to reach it. */
14
+ export const SETTINGS_LIST_LIMIT = 200;
12
15
  /**
13
16
  * Builds a `limit`/`page` query string, plus any extra scope/filter params (e.g. `folderUid`,
14
17
  * or one of `@rapidmx/restapi`'s generic query-operator values like `startDate=lte(...)`).
@@ -20,6 +23,14 @@ export function buildQuery(params, extra = {}) {
20
23
  }
21
24
  return parts.join("&");
22
25
  }
26
+ /**
27
+ * The `sort` query value for `buildQuery()`'s extra params, in the order the keys are given. Paging with no sort leaves the order to the
28
+ * database, which on SQL can repeat or skip a row between two pages and on MongoDB is the oldest first: end the sort with a unique key (`uid`)
29
+ * so every row lands on exactly one page.
30
+ */
31
+ export function sortQuery(sort) {
32
+ return { sort: JSON.stringify(sort) };
33
+ }
23
34
  /**
24
35
  * Builds a `?limit=&page=&matterId=` suffix from only the params actually supplied - unlike
25
36
  * `buildQuery()`, applies no default page size (these endpoints historically returned every request
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Runs `task` for each of `items`, never more than `limit` of them at once, and resolves to what became of every one in the order of `items`, as
3
+ * `Promise.allSettled()` does - a task that fails does not stop the others. For a batch of requests too big to send all at once (a thousand rows ticked).
4
+ */
5
+ export declare function settleWithLimit<T, R>(items: readonly T[], limit: number, task: (item: T) => Promise<R>): Promise<PromiseSettledResult<R>[]>;
@@ -0,0 +1,25 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ /**
6
+ * Runs `task` for each of `items`, never more than `limit` of them at once, and resolves to what became of every one in the order of `items`, as
7
+ * `Promise.allSettled()` does - a task that fails does not stop the others. For a batch of requests too big to send all at once (a thousand rows ticked).
8
+ */
9
+ export async function settleWithLimit(items, limit, task) {
10
+ const outcomes = new Array(items.length);
11
+ let next = 0;
12
+ async function worker() {
13
+ while (next < items.length) {
14
+ const index = next++;
15
+ try {
16
+ outcomes[index] = { status: "fulfilled", value: await task(items[index]) };
17
+ }
18
+ catch (reason) {
19
+ outcomes[index] = { status: "rejected", reason };
20
+ }
21
+ }
22
+ }
23
+ await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
24
+ return outcomes;
25
+ }
@@ -10,7 +10,7 @@
10
10
  */
11
11
 
12
12
  import { ApiClient, withClient } from "../util/api.js";
13
- import { ListParams, buildQuery } from "../util/apiQuery.js";
13
+ import { ListParams, buildQuery, sortQuery } from "../util/apiQuery.js";
14
14
 
15
15
  export type { ListParams };
16
16
 
@@ -44,5 +44,5 @@ export function listAuditLog(filters: AuditLogFilters = {}, params: ListParams =
44
44
  if (filters.actorUserUid) extra.actorUserUid = filters.actorUserUid;
45
45
  if (filters.action) extra.action = filters.action;
46
46
  if (filters.targetType) extra.targetType = filters.targetType;
47
- return withClient(client, `/mail/audit-log?${buildQuery(params, extra)}`);
47
+ return withClient(client, `/mail/audit-log?${buildQuery(params, { ...extra, ...sortQuery({ dateCreated: "DESC", uid: "ASC" }) })}`);
48
48
  }
@@ -8,7 +8,7 @@
8
8
  */
9
9
 
10
10
  import { ApiClient, withClient } from "../util/api.js";
11
- import { ListParams, buildQuery } from "../util/apiQuery.js";
11
+ import { ListParams, buildQuery, sortQuery } from "../util/apiQuery.js";
12
12
 
13
13
  export type { ListParams };
14
14
 
@@ -31,7 +31,7 @@ export interface DistributionList {
31
31
  * account of a multi-account app) to call instead of the default global `apiFetch()` - see `withClient()`'s
32
32
  * own doc comment in `util/api.ts`. Omitted (the default), every function here behaves exactly as before. */
33
33
  export function listDistributionLists(params: ListParams = {}, client?: ApiClient): Promise<DistributionList[]> {
34
- return withClient(client, `/mail/distribution-lists?${buildQuery(params)}`);
34
+ return withClient(client, `/mail/distribution-lists?${buildQuery(params, sortQuery({ uid: "ASC" }))}`);
35
35
  }
36
36
 
37
37
  export function getDistributionList(uid: string, client?: ApiClient): Promise<DistributionList> {
@@ -9,7 +9,7 @@
9
9
  */
10
10
 
11
11
  import { ApiClient, withClient } from "../util/api.js";
12
- import { ListParams, buildQuery } from "../util/apiQuery.js";
12
+ import { ListParams, buildQuery, sortQuery } from "../util/apiQuery.js";
13
13
 
14
14
  export type { ListParams };
15
15
 
@@ -37,7 +37,7 @@ export interface Domain {
37
37
  * account of a multi-account app) to call instead of the default global `apiFetch()` - see `withClient()`'s
38
38
  * own doc comment in `util/api.ts`. Omitted (the default), every function here behaves exactly as before. */
39
39
  export function listDomains(params: ListParams = {}, client?: ApiClient): Promise<Domain[]> {
40
- return withClient(client, `/mail/domains?${buildQuery(params)}`);
40
+ return withClient(client, `/mail/domains?${buildQuery(params, sortQuery({ uid: "ASC" }))}`);
41
41
  }
42
42
 
43
43
  export function getDomain(uid: string, client?: ApiClient): Promise<Domain> {