@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 { ApiClient, ApiRequestError, apiUrl, withClient, withCsrfHeader } from "../util/api.js";
12
+ import { ApiClient, withClient, withClientRaw } from "../util/api.js";
13
13
  import { RequestListParams, buildRequestListQuery } from "../util/apiQuery.js";
14
14
 
15
15
  export type { RequestListParams };
@@ -51,24 +51,18 @@ export interface UploadMailboxImportInput {
51
51
  * with `format`/`targetFolderUid`/`mailboxUid` as query-string parameters, not a JSON body — the same
52
52
  * shape `mailApi.ts`'s `uploadAttachment()` already establishes for a raw-bytes upload.
53
53
  */
54
- export async function uploadMailboxImport(file: File, input: UploadMailboxImportInput): Promise<MailboxImportRequest> {
54
+ export function uploadMailboxImport(file: File, input: UploadMailboxImportInput, client?: ApiClient): Promise<MailboxImportRequest> {
55
55
  const params = new URLSearchParams({ format: input.format, targetFolderUid: input.targetFolderUid });
56
56
  if (input.mailboxUid) {
57
57
  params.set("mailboxUid", input.mailboxUid);
58
58
  }
59
- const res = await fetch(apiUrl(`/mail/mailbox-import-requests?${params.toString()}`), {
60
- method: "POST",
61
- credentials: "include",
62
- headers: withCsrfHeader({ "Content-Type": input.format === "pst" ? "application/vnd.ms-outlook" : "application/mbox" }),
63
- body: file,
64
- });
65
- const contentType = res.headers.get("content-type") ?? "";
66
- const responseBody = contentType.includes("application/json") ? await res.json().catch(() => undefined) : undefined;
67
- if (!res.ok) {
68
- const message = (responseBody && (responseBody.message || responseBody.error)) || res.statusText || "Upload failed.";
69
- throw new ApiRequestError(message, res.status, responseBody?.code);
70
- }
71
- return responseBody as MailboxImportRequest;
59
+ return withClientRaw(
60
+ client,
61
+ `/mail/mailbox-import-requests?${params.toString()}`,
62
+ "POST",
63
+ file,
64
+ input.format === "pst" ? "application/vnd.ms-outlook" : "application/mbox",
65
+ );
72
66
  }
73
67
 
74
68
  /** `client`, given by every function below that isn't a raw-bytes upload, is an explicit `ApiClient` from
@@ -27,6 +27,37 @@ export function stripRemoteCssUrls(css: string): string {
27
27
  .replace(/url\(\s*(["']?)(.*?)\1\s*\)/gi, (match, _quote: string, uri: string) => (EMBEDDED_URI.test(uri) ? match : "none"));
28
28
  }
29
29
 
30
+ /** The candidate URLs of a `srcset` value, parsed as the HTML spec does: a candidate's URL is a run of non-whitespace
31
+ * characters (it may itself contain commas - a `data:` URI always does), so a comma that ends a URL or follows its
32
+ * descriptors separates candidates whether or not whitespace follows it. */
33
+ function srcsetUrls(value: string): string[] {
34
+ const urls: string[] = [];
35
+ let position = 0;
36
+ while (position < value.length) {
37
+ const start = value.slice(position).search(/[^\s,]/);
38
+ if (start < 0) {
39
+ break;
40
+ }
41
+ position += start;
42
+ const end = value.slice(position).search(/\s/);
43
+ const token = end < 0 ? value.slice(position) : value.slice(position, position + end);
44
+ position += token.length;
45
+ urls.push(token.replace(/,+$/, ""));
46
+ if (!token.endsWith(",")) {
47
+ // Skip the descriptors, up to the comma that ends the candidate.
48
+ const comma = value.indexOf(",", position);
49
+ const descriptors = value.slice(position, comma < 0 ? value.length : comma);
50
+ if (descriptors.includes("(")) {
51
+ // The spec's tokenizer ignores commas inside parentheses, so a browser may read the candidates that follow differently from
52
+ // the way this does. No valid descriptor has one: refuse the whole value.
53
+ urls.push(descriptors);
54
+ }
55
+ position = comma < 0 ? value.length : comma + 1;
56
+ }
57
+ }
58
+ return urls;
59
+ }
60
+
30
61
  type Purifier = ReturnType<typeof DOMPurify>;
31
62
  let bodyPurifier: Purifier | undefined;
32
63
 
@@ -58,7 +89,7 @@ function getBodyPurifier(): Purifier | undefined {
58
89
  }
59
90
  const isNavigationLink = name === "href" && ["a", "area"].includes(node.nodeName.toLowerCase());
60
91
  if (RESOURCE_URI_ATTRIBUTES.has(name) || (name === "href" && !isNavigationLink)) {
61
- const candidates = name === "srcset" ? data.attrValue.split(/,\s+/) : [data.attrValue];
92
+ const candidates = name === "srcset" ? srcsetUrls(data.attrValue) : [data.attrValue];
62
93
  if (!candidates.every((candidate) => EMBEDDED_URI.test(candidate))) {
63
94
  data.keepAttr = false;
64
95
  }
@@ -106,6 +137,21 @@ const QUOTE_FORBIDDEN_TAGS = [
106
137
  // svg/math are already in DISPLAY_FORBIDDEN_TAGS, spread above — not repeated here.
107
138
  ];
108
139
 
140
+ let signaturePurifier: Purifier | undefined;
141
+
142
+ /**
143
+ * Sanitizes a signature's HTML for the compose body: DOMPurify's defaults (no scripts, handlers or `javascript:` URLs) and
144
+ * the quote's tag list (nothing that styles, frames or submits). Unlike a quoted message, a signature is the sender's own
145
+ * and may carry pictures from the web, so remote images and links stay. Returns `""` when there is no DOM to sanitize with.
146
+ */
147
+ export function sanitizeSignatureHtml(html: string): string {
148
+ if (typeof window === "undefined") {
149
+ return "";
150
+ }
151
+ signaturePurifier ??= DOMPurify(window);
152
+ return signaturePurifier.isSupported ? signaturePurifier.sanitize(html, { FORBID_TAGS: QUOTE_FORBIDDEN_TAGS }) : "";
153
+ }
154
+
109
155
  /**
110
156
  * Sanitizes message HTML for quoting into a compose body: the display policy (`sanitizeMessageBodyHtml()`), without
111
157
  * the tags in `QUOTE_FORBIDDEN_TAGS`, and without any image that isn't embedded as a `data:` URI - a `cid:` image
@@ -64,15 +64,24 @@ const VALID_ENTITY_TYPES: ReadonlySet<string> = new Set<SearchEntityType>(["mess
64
64
  * `parseSearchQuery()` itself, after every extraction has run. */
65
65
  function extractFirst(input: string, key: string): { value?: string; rest: string } {
66
66
  const pattern = new RegExp(`(^|\\s)${key}:(?:"([^"]*)"|(\\S+))`, "i");
67
- const match = pattern.exec(input);
68
- if (!match) {
67
+ const found = pattern.exec(maskQuoted(input));
68
+ if (!found) {
69
69
  return { rest: input };
70
70
  }
71
+ // The masked text only says where the operator is - what it holds is read from the real one.
72
+ const match = pattern.exec(input.slice(found.index))!;
71
73
  const value = match[2] ?? match[3] ?? "";
72
- const rest = input.slice(0, match.index) + match[1] + input.slice(match.index + match[0].length);
74
+ const rest = input.slice(0, found.index) + match[1] + input.slice(found.index + match[0].length);
73
75
  return { value, rest };
74
76
  }
75
77
 
78
+ /** `input` with the inside of every quoted phrase blanked out (the length stays the same, so positions carry over to
79
+ * `input`): a word that looks like an operator inside a phrase - `"meeting from:bob"` - is part of the phrase, never an
80
+ * operator. */
81
+ function maskQuoted(input: string): string {
82
+ return input.replace(/"([^"]*)"/g, (_phrase, inner: string) => `"${"\0".repeat(inner.length)}"`);
83
+ }
84
+
76
85
  /** Repeatedly applies `extractFirst()` for `key` until no more occurrences remain, splitting any
77
86
  * comma-separated value into individual entries — so `is:read is:flagged` and `is:read,flagged` both
78
87
  * produce `["read", "flagged"]`. */
@@ -98,7 +107,7 @@ function extractAll(input: string, key: string): { values: string[]; rest: strin
98
107
  * spec defines for this operator — there is no `has:<other value>` form. */
99
108
  function extractHasAttachment(input: string): { found: boolean; rest: string } {
100
109
  const pattern = /(^|\s)has:attachment\b/i;
101
- const match = pattern.exec(input);
110
+ const match = pattern.exec(maskQuoted(input));
102
111
  if (!match) {
103
112
  return { found: false, rest: input };
104
113
  }
@@ -23,7 +23,7 @@ import { evaluateMessageSecurity, type MessageSecurityResult } from "../crypto/m
23
23
  import { getMessage, getMessageRawContent } from "../mail/mailApi.js";
24
24
  import type { ParsedSearchQuery } from "./queryGrammar.js";
25
25
  import { SEARCH_FIELD_WEIGHTS } from "./searchScoring.js";
26
- import { candidates, SearchResult } from "./searchApi.js";
26
+ import { candidates, type CandidateResultPage, SearchResult } from "./searchApi.js";
27
27
 
28
28
  /** Longest decrypted HTML body `stripHtml()` examines, in UTF-16 code units - content past it is ignored
29
29
  * for Tier 3 matching. A hostile sender controls this input entirely, so it is bounded on top of the
@@ -271,6 +271,11 @@ function hasAnyFilter(parsed: ParsedSearchQuery): boolean {
271
271
  * concurrent WebCrypto work a large candidate page would otherwise fire all at once. */
272
272
  export const TIER3_DECRYPT_CONCURRENCY = 4;
273
273
 
274
+ /** The most candidate pages one search walks (`nextCursor` onwards) while it is still short of `limit` results - a
275
+ * search that matches little must not download and decrypt a whole mailbox, but an encrypted message beyond the first
276
+ * page of candidates is still found. */
277
+ export const TIER3_MAX_CANDIDATE_PAGES = 4;
278
+
274
279
  /** `Promise.allSettled()` over `items`, but never running more than `concurrency` `task`s at a time.
275
280
  * Outcomes are returned in `items`' own order. */
276
281
  async function settleWithConcurrency<T, R>(
@@ -301,7 +306,8 @@ export interface SearchEncryptedCandidatesOptions {
301
306
  }
302
307
 
303
308
  /**
304
- * Runs Tier 3 for one parsed query: fetches a bounded candidate set from the server, decrypts each
309
+ * Runs Tier 3 for one parsed query: fetches a bounded candidate set from the server (following `nextCursor` for up to
310
+ * `TIER3_MAX_CANDIDATE_PAGES` pages while fewer than `limit` messages have matched), decrypts each
305
311
  * candidate this device can open (at most `TIER3_DECRYPT_CONCURRENCY` at a time), and returns only the
306
312
  * ones whose real (decrypted) content actually matches - each with a raw (not yet normalized) score the
307
313
  * caller should run through `searchScoring.ts#normalizeServerScores()` alongside Tier 1's own scores
@@ -330,41 +336,68 @@ export async function searchEncryptedCandidates(
330
336
 
331
337
  const participants = [parsed.from, parsed.to, parsed.cc].filter((value): value is string => Boolean(value));
332
338
 
333
- const page = await candidates({
334
- types: ["message"],
335
- participants: participants.length > 0 ? participants : undefined,
336
- before: parsed.before,
337
- after: parsed.after,
338
- folderUid: parsed.folderUid,
339
- flags: parsed.flags,
340
- labels: parsed.labels,
341
- limit,
342
- mailboxUid: options.mailboxUid,
343
- });
339
+ const results: SearchResult[] = [];
340
+ let cursor: string | undefined;
341
+ for (let pageNumber = 0; pageNumber < TIER3_MAX_CANDIDATE_PAGES; pageNumber++) {
342
+ let page: CandidateResultPage;
343
+ try {
344
+ page = await candidates({
345
+ types: ["message"],
346
+ participants: participants.length > 0 ? participants : undefined,
347
+ before: parsed.before,
348
+ after: parsed.after,
349
+ folderUid: parsed.folderUid,
350
+ flags: parsed.flags,
351
+ labels: parsed.labels,
352
+ limit,
353
+ cursor,
354
+ mailboxUid: options.mailboxUid,
355
+ });
356
+ } catch (err) {
357
+ if (pageNumber === 0) {
358
+ throw err;
359
+ }
360
+ // A later page failing keeps what the earlier ones found.
361
+ break;
362
+ }
344
363
 
345
- const settled = await settleWithConcurrency(
346
- page.candidates.filter((candidate) => candidate.entityType === "message"),
347
- TIER3_DECRYPT_CONCURRENCY,
348
- async (candidate) => {
349
- if (parsed.hasAttachment !== undefined) {
350
- const message = await getMessage(candidate.entityUid);
351
- if (Boolean(message.hasAttachments) !== parsed.hasAttachment) {
364
+ const settled = await settleWithConcurrency(
365
+ page.candidates.filter((candidate) => candidate.entityType === "message"),
366
+ TIER3_DECRYPT_CONCURRENCY,
367
+ async (candidate) => {
368
+ if (parsed.hasAttachment !== undefined) {
369
+ const message = await getMessage(candidate.entityUid);
370
+ if (Boolean(message.hasAttachments) !== parsed.hasAttachment) {
371
+ return undefined;
372
+ }
373
+ }
374
+ const rawMime = await getMessageRawContent(candidate.entityUid);
375
+ if (unlocked.destroyed) {
352
376
  return undefined;
353
377
  }
354
- }
355
- const rawMime = await getMessageRawContent(candidate.entityUid);
356
- if (unlocked.destroyed) {
357
- return undefined;
358
- }
359
- const security: MessageSecurityResult = await evaluateMessageSecurity(rawMime, unlocked);
360
- return { entityUid: candidate.entityUid, security };
361
- },
362
- );
378
+ const security: MessageSecurityResult = await evaluateMessageSecurity(rawMime, unlocked);
379
+ return { entityUid: candidate.entityUid, security };
380
+ },
381
+ );
363
382
 
364
- if (unlocked.destroyed) {
365
- // Locked while candidates were being fetched/decrypted - don't surface decrypted content after a lock.
366
- return [];
383
+ if (unlocked.destroyed) {
384
+ // Locked while candidates were being fetched/decrypted - don't surface decrypted content after a lock.
385
+ return [];
386
+ }
387
+ results.push(...matchDecrypted(parsed, settled));
388
+ cursor = page.nextCursor;
389
+ if (!cursor || results.length >= limit) {
390
+ break;
391
+ }
367
392
  }
393
+ return results;
394
+ }
395
+
396
+ /** The decrypted candidates of one page whose real content matches `parsed`, scored and with their snippets. */
397
+ function matchDecrypted(
398
+ parsed: ParsedSearchQuery,
399
+ settled: PromiseSettledResult<{ entityUid: string; security: MessageSecurityResult } | undefined>[],
400
+ ): SearchResult[] {
368
401
  const subjectFilter = parsed.subject?.toLowerCase();
369
402
  const results: SearchResult[] = [];
370
403
  for (const outcome of settled) {
@@ -6,7 +6,7 @@
6
6
  * comment for the shared ACL/authorization model every wrapper file here follows. */
7
7
 
8
8
  import { ApiClient, withClient } from "../util/api.js";
9
- import { ListParams, buildQuery } from "../util/apiQuery.js";
9
+ import { ListParams, SETTINGS_LIST_LIMIT, buildQuery } from "../util/apiQuery.js";
10
10
 
11
11
  export type TaskPriority = "low" | "normal" | "high";
12
12
 
@@ -39,7 +39,7 @@ export interface Task {
39
39
 
40
40
  /** Lists a folder's tasks, soonest due date first (tasks with no due date sort last). */
41
41
  export function listTasks(folderUid: string, params: ListParams = {}, client?: ApiClient): Promise<Task[]> {
42
- return withClient(client, `/mail/tasks?${buildQuery(params, { folderUid, sort: JSON.stringify({ dueDate: "ASC" }) })}`);
42
+ return withClient(client, `/mail/tasks?${buildQuery(params, { folderUid, sort: JSON.stringify({ dueDate: "ASC", uid: "ASC" }) })}`);
43
43
  }
44
44
 
45
45
  export interface CreateTaskInput {
@@ -109,7 +109,7 @@ export interface TaskList {
109
109
 
110
110
  /** Lists a mailbox's task lists (Outlook To-Do-style custom lists), alphabetically by name. */
111
111
  export function listTaskLists(mailboxUid: string, params: ListParams = {}, client?: ApiClient): Promise<TaskList[]> {
112
- return withClient(client, `/mail/task-lists?${buildQuery(params, { mailboxUid, sort: JSON.stringify({ name: "ASC" }) })}`);
112
+ return withClient(client, `/mail/task-lists?${buildQuery({ limit: SETTINGS_LIST_LIMIT, ...params }, { mailboxUid, sort: JSON.stringify({ name: "ASC", uid: "ASC" }) })}`);
113
113
  }
114
114
 
115
115
  export function createTaskList(input: { mailboxUid: string; name: string }, client?: ApiClient): Promise<TaskList> {
package/lib/util/api.ts CHANGED
@@ -157,12 +157,18 @@ export async function apiFetch<T = unknown>(path: string, init: RequestInit = {}
157
157
  applyCsrfHeader(headers, init.method);
158
158
  const credentials = apiBaseUrl ? "include" : init.credentials;
159
159
 
160
- let res = await fetch(apiUrl(path), { ...init, headers, credentials });
161
- // 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.
160
+ const res = await fetchWithRecovery(path, { ...init, headers, credentials });
161
+ return decodeApiResponse<T>(res, (error) => unauthorizedObserver?.(error));
162
+ }
163
+
164
+ /** `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
165
+ * time) it renews the session, and asks again once. */
166
+ async function fetchWithRecovery(path: string, init: RequestInit): Promise<Response> {
167
+ let res = await fetch(apiUrl(path), init);
162
168
  if (res.status === 401 && sessionRecovery && (await recoverSession())) {
163
- res = await fetch(apiUrl(path), { ...init, headers, credentials });
169
+ res = await fetch(apiUrl(path), init);
164
170
  }
165
- return decodeApiResponse<T>(res, (error) => unauthorizedObserver?.(error));
171
+ return res;
166
172
  }
167
173
 
168
174
  let sessionRecovery: (() => Promise<boolean>) | undefined;
@@ -196,15 +202,16 @@ async function recoverSession(): Promise<boolean> {
196
202
  * a deployment-level requirement owned by auth-server/the Helm chart, not configured here — and auth-server's
197
203
  * CORS config must explicitly allow this app's origin with credentials.
198
204
  *
199
- * `applyCsrfHeader()` is still called here for consistency, but it never actually finds a token for a truly
200
- * cross-origin call — see its own doc comment on `readCsrfCookie()` for why that's correct rather than a
201
- * gap: auth-server's CSRF protection for this call shape comes from its own Origin allow-list check, not a
202
- * double-submit cookie this app's JavaScript could never read in the first place.
205
+ * No `x-csrf-token` header is sent, deliberately. A double-submit token is only meaningful to the server that issued it, and the `csrf`
206
+ * cookie this app's JavaScript can read is *this* host's own (`mail.example.com`), never auth-server's - so echoing it
207
+ * would be a wrong token for the wrong server. Worse, auth-server's CORS rules do not allow that header, so the browser
208
+ * blocked the preflight of every such call (the silent session refresh, sign-out) before it ever reached the server: "Request header
209
+ * field x-csrf-token is not allowed by Access-Control-Allow-Headers". Auth-server's CSRF protection for this cross-origin call shape is
210
+ * its Origin allow-list check, which needs no header.
203
211
  */
204
212
  export async function authApiFetch<T = unknown>(authServerUrl: string, path: string, init: RequestInit = {}): Promise<T> {
205
213
  const headers = new Headers(init.headers);
206
214
  headers.set("Content-Type", "application/json");
207
- applyCsrfHeader(headers, init.method);
208
215
 
209
216
  const res = await fetch(`${authServerUrl}/api${path}`, { ...init, headers, credentials: "include" });
210
217
  return decodeApiResponse<T>(res);
@@ -228,13 +235,19 @@ export function setApiUnauthorizedObserver(observer: ((error: ApiRequestError) =
228
235
  * `createApiClient()`-made client's `fetch()` (which forwards to that instance's own
229
236
  * `setUnauthorizedObserver()`) pass their own. A throwing `onUnauthorized` never changes what the caller sees.
230
237
  */
231
- async function decodeApiResponse<T>(res: Response, onUnauthorized?: (error: ApiRequestError) => void): Promise<T> {
238
+ async function decodeApiResponse<T>(
239
+ res: Response,
240
+ onUnauthorized?: (error: ApiRequestError) => void,
241
+ failureMessage: string = "Request failed.",
242
+ ): Promise<T> {
232
243
  const contentType = res.headers.get("content-type") ?? "";
233
244
  const body = contentType.includes("application/json") ? await res.json().catch(() => undefined) : undefined;
234
245
 
235
246
  if (!res.ok) {
236
- const message = (body && (body.message || body.error)) || res.statusText || "Request failed.";
237
- const error = new ApiRequestError(message, res.status, body?.code, body);
247
+ // A body is untrusted: only a non-empty string is a message (anything else would read "[object Object]"), only a string a code.
248
+ const reported = body && (body.message || body.error);
249
+ const message = (typeof reported === "string" && reported) || res.statusText || failureMessage;
250
+ const error = new ApiRequestError(message, res.status, typeof body?.code === "string" ? body.code : undefined, body);
238
251
  if (res.status === 401 && onUnauthorized) {
239
252
  try {
240
253
  onUnauthorized(error);
@@ -259,6 +272,12 @@ export interface ApiClient {
259
272
  /** Same contract as `apiFetch()`: prefixes `path` with `/api`, parses a JSON response, and rejects with
260
273
  * `ApiRequestError` on a non-ok response. */
261
274
  fetch<T = unknown>(path: string, init?: RequestInit): Promise<T>;
275
+ /**
276
+ * Like `fetch()` - same origin, authentication and error decoding - but resolves the response's own bytes instead of parsing JSON, for a picture or other
277
+ * 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
278
+ * `blob:` URL. No `Content-Type` is added.
279
+ */
280
+ fetchBlob(path: string, init?: RequestInit): Promise<Blob>;
262
281
  /**
263
282
  * This client instance's own `setApiUnauthorizedObserver()` equivalent - see `createApiClient()`'s doc
264
283
  * comment for why it is per-instance rather than shared with the module-level `setApiUnauthorizedObserver()`
@@ -308,18 +327,34 @@ export function createApiClient(options: CreateApiClientOptions): ApiClient {
308
327
  const { getAccessToken } = options;
309
328
  let unauthorizedObserver: ((error: ApiRequestError) => void) | undefined;
310
329
 
311
- async function clientFetch<T = unknown>(path: string, init: RequestInit = {}): Promise<T> {
330
+ async function send(path: string, init: RequestInit, json: boolean): Promise<Response> {
312
331
  const headers = new Headers(init.headers);
313
- headers.set("Content-Type", "application/json");
332
+ // JSON unless the caller sent a body of its own kind (an upload names the file's type).
333
+ if (json && !headers.has("Content-Type")) {
334
+ headers.set("Content-Type", "application/json");
335
+ }
314
336
  const token = await getAccessToken();
315
337
  headers.set("Authorization", `jwt ${token}`);
316
338
 
317
- const res = await fetch(`${baseUrl}/api${path}`, { ...init, headers });
318
- return decodeApiResponse<T>(res, (error) => unauthorizedObserver?.(error));
339
+ return fetch(`${baseUrl}/api${path}`, { ...init, headers });
340
+ }
341
+
342
+ async function clientFetch<T = unknown>(path: string, init: RequestInit = {}): Promise<T> {
343
+ return decodeApiResponse<T>(await send(path, init, true), (error) => unauthorizedObserver?.(error));
344
+ }
345
+
346
+ async function clientFetchBlob(path: string, init: RequestInit = {}): Promise<Blob> {
347
+ const res = await send(path, init, false);
348
+ if (!res.ok) {
349
+ // Throws the same `ApiRequestError` (and tells the observer of a 401) as every other request.
350
+ await decodeApiResponse<never>(res, (error) => unauthorizedObserver?.(error));
351
+ }
352
+ return res.blob();
319
353
  }
320
354
 
321
355
  return {
322
356
  fetch: clientFetch,
357
+ fetchBlob: clientFetchBlob,
323
358
  setUnauthorizedObserver(observer) {
324
359
  unauthorizedObserver = observer;
325
360
  },
@@ -339,3 +374,51 @@ export function createApiClient(options: CreateApiClientOptions): ApiClient {
339
374
  export function withClient<T = unknown>(client: ApiClient | undefined, path: string, init?: RequestInit): Promise<T> {
340
375
  return client ? client.fetch<T>(path, init) : apiFetch<T>(path, init);
341
376
  }
377
+
378
+ /**
379
+ * 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
380
+ * 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
381
+ * 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
382
+ * an `ApiRequestError` (`failureMessage` when the server gave no message) on a non-ok one.
383
+ */
384
+ export async function withClientRaw<T = unknown>(
385
+ client: ApiClient | undefined,
386
+ path: string,
387
+ method: string,
388
+ file: Blob,
389
+ contentType: string = file.type,
390
+ failureMessage: string = "Upload failed.",
391
+ ): Promise<T> {
392
+ if (client) {
393
+ return client.fetch<T>(path, { method, headers: { "Content-Type": contentType }, body: file });
394
+ }
395
+ const res = await fetchWithRecovery(path, {
396
+ method,
397
+ credentials: "include",
398
+ headers: withCsrfHeader({ "Content-Type": contentType }, method),
399
+ body: file,
400
+ });
401
+ return decodeApiResponse<T>(res, (error) => unauthorizedObserver?.(error), failureMessage);
402
+ }
403
+
404
+ /**
405
+ * Fetches `path`'s own bytes (a message's raw source, say) rather than JSON - the download counterpart of `withClient()`. With an explicit `client` the
406
+ * 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
407
+ * with the `jwt` cookie, renewing the session and asking again once on a `401`, as `apiFetch()` does. Rejects with an `ApiRequestError`
408
+ * (`failureMessage` when the server gave no message) on a non-ok response.
409
+ */
410
+ export async function withClientBlob(
411
+ client: ApiClient | undefined,
412
+ path: string,
413
+ failureMessage: string = "Download failed.",
414
+ ): Promise<Blob> {
415
+ if (client) {
416
+ return client.fetchBlob(path);
417
+ }
418
+ const res = await fetchWithRecovery(path, { credentials: "include" });
419
+ if (!res.ok) {
420
+ // Throws the same `ApiRequestError` (and tells the observer of a 401) as every other request.
421
+ await decodeApiResponse<never>(res, (error) => unauthorizedObserver?.(error), failureMessage);
422
+ }
423
+ return res.blob();
424
+ }
@@ -16,6 +16,10 @@ export interface ListParams {
16
16
 
17
17
  const DEFAULT_PAGE_SIZE = 25;
18
18
 
19
+ /** 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
20
+ * large page rather than the default 25 that would hide everything after the 25th with no way to reach it. */
21
+ export const SETTINGS_LIST_LIMIT = 200;
22
+
19
23
  /**
20
24
  * Builds a `limit`/`page` query string, plus any extra scope/filter params (e.g. `folderUid`,
21
25
  * or one of `@rapidmx/restapi`'s generic query-operator values like `startDate=lte(...)`).
@@ -28,6 +32,15 @@ export function buildQuery(params: ListParams, extra: Record<string, string> = {
28
32
  return parts.join("&");
29
33
  }
30
34
 
35
+ /**
36
+ * 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
37
+ * 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`)
38
+ * so every row lands on exactly one page.
39
+ */
40
+ export function sortQuery(sort: Record<string, "ASC" | "DESC">): { sort: string } {
41
+ return { sort: JSON.stringify(sort) };
42
+ }
43
+
31
44
  /**
32
45
  * Optional paging/scope params for the async-request list endpoints (`listExportRequests()`,
33
46
  * `listImportRequests()`, `listErasureRequests()`, `listMatterExportRequests()`,
@@ -0,0 +1,25 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+
6
+ /**
7
+ * 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
8
+ * `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).
9
+ */
10
+ export async function settleWithLimit<T, R>(items: readonly T[], limit: number, task: (item: T) => Promise<R>): Promise<PromiseSettledResult<R>[]> {
11
+ const outcomes = new Array<PromiseSettledResult<R>>(items.length);
12
+ let next = 0;
13
+ async function worker() {
14
+ while (next < items.length) {
15
+ const index = next++;
16
+ try {
17
+ outcomes[index] = { status: "fulfilled", value: await task(items[index]) };
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rapidmx/web-client",
3
- "version": "0.30.0",
3
+ "version": "0.31.0",
4
4
  "description": "RapidMX's webmail and admin console React UI for web-based clients.",
5
5
  "type": "module",
6
6
  "repository": "https://github.com/rapidmx/web-client",