@rapidmx/web-client 0.30.0 → 0.32.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 (402) 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 +3 -3
  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/diagnostics/diagnosticsApi.ts +2 -2
  33. package/apps/shared/components/admin/distributionLists/MemberListCard.tsx +4 -4
  34. package/apps/shared/components/admin/elevation.ts +19 -0
  35. package/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.tsx +58 -3
  36. package/apps/shared/components/admin/layout/AdminShell.tsx +6 -1
  37. package/apps/shared/components/admin/mailboxes/EraseLeftoverDataDialog.tsx +3 -3
  38. package/apps/shared/components/admin/mailboxes/EscrowScopeCard.tsx +4 -4
  39. package/apps/shared/components/admin/mailboxes/LeftoverMailboxesSection.tsx +4 -4
  40. package/apps/shared/components/admin/mailboxes/ResourceSettingsCard.tsx +3 -3
  41. package/apps/shared/components/admin/mailboxes/ShareAccessCard.tsx +6 -6
  42. package/apps/shared/components/admin/settings/BrandingForm.tsx +22 -9
  43. package/apps/shared/components/admin/settings/DomainDnsSetup.tsx +4 -4
  44. package/apps/shared/components/admin/settings/EncryptionPolicyForm.tsx +33 -4
  45. package/apps/shared/components/admin/settings/LoadedSettingsForm.tsx +3 -3
  46. package/apps/shared/components/admin/settings/MailboxCreateForm.tsx +8 -3
  47. package/apps/shared/components/admin/settings/MailboxPolicyForm.tsx +3 -3
  48. package/apps/shared/components/admin/settings/PluginsManager.tsx +195 -46
  49. package/apps/shared/components/admin/settings/RetentionPolicyForm.tsx +25 -12
  50. package/apps/shared/components/admin/setup/EscrowSetupStep.tsx +40 -7
  51. package/apps/shared/components/admin/setup/SetupWizard.tsx +8 -6
  52. package/apps/shared/components/admin/signOut.ts +3 -0
  53. package/apps/shared/components/admin/usePagedList.tsx +2 -2
  54. package/apps/shared/components/calendar/DescriptionEditor.tsx +2 -1
  55. package/apps/shared/components/calendar/EventDetails.tsx +34 -8
  56. package/apps/shared/components/calendar/EventEditor.tsx +90 -40
  57. package/apps/shared/components/calendar/EventFormParts.tsx +5 -2
  58. package/apps/shared/components/calendar/EventMatchNav.tsx +14 -2
  59. package/apps/shared/components/calendar/EventModal.tsx +25 -1
  60. package/apps/shared/components/calendar/FindATime.tsx +10 -2
  61. package/apps/shared/components/calendar/ListView.tsx +99 -25
  62. package/apps/shared/components/calendar/RecurrenceEditor.tsx +19 -8
  63. package/apps/shared/components/calendar/SplitDayView.tsx +14 -18
  64. package/apps/shared/components/calendar/TaskQuickForm.tsx +3 -2
  65. package/apps/shared/components/calendar/TimeGridView.tsx +35 -19
  66. package/apps/shared/components/calendar/gridLayout.ts +94 -0
  67. package/apps/shared/components/calendar/layout/CalendarShell.tsx +1 -1
  68. package/apps/shared/components/calendar/timePicker.ts +5 -2
  69. package/apps/shared/components/contacts/ContactCard.tsx +15 -4
  70. package/apps/shared/components/contacts/ContactDetailPane.tsx +54 -15
  71. package/apps/shared/components/contacts/ContactForm.tsx +61 -17
  72. package/apps/shared/components/contacts/ContactPhotoEditor.tsx +88 -0
  73. package/apps/shared/components/contacts/ContactPhotoField.tsx +36 -39
  74. package/apps/shared/components/contacts/layout/ContactsShell.tsx +1 -1
  75. package/apps/shared/components/contacts/participantDetails.ts +18 -1
  76. package/apps/shared/components/layout/AppShell.tsx +9 -0
  77. package/apps/shared/components/layout/KeyEnrollmentGate.tsx +9 -4
  78. package/apps/shared/components/layout/MailboxProvisioning.tsx +31 -3
  79. package/apps/shared/components/layout/RecoveryCodeUnlock.tsx +4 -2
  80. package/apps/shared/components/mail/ConversationList.tsx +49 -6
  81. package/apps/shared/components/mail/ConversationThreadPane.tsx +1 -1
  82. package/apps/shared/components/mail/MenuButton.tsx +3 -400
  83. package/apps/shared/components/mail/MessageDetailPane.tsx +16 -13
  84. package/apps/shared/components/mail/compose/ComposeToolbar.tsx +11 -1
  85. package/apps/shared/components/mail/compose/ComposeWindow.tsx +57 -11
  86. package/apps/shared/components/mail/compose/GifPicker.tsx +10 -4
  87. package/apps/shared/components/mail/compose/RecipientInput.tsx +9 -7
  88. package/apps/shared/components/mail/compose/composeFlushRegistry.ts +14 -1
  89. package/apps/shared/components/mail/compose/encryptionRequirement.ts +5 -0
  90. package/apps/shared/components/mail/compose/quotedBody.ts +31 -6
  91. package/apps/shared/components/mail/layout/MailShell.tsx +1 -1
  92. package/apps/shared/components/mail/reading/MessageBody.tsx +13 -7
  93. package/apps/shared/components/mail/reading/bodyContent.ts +2 -0
  94. package/apps/shared/components/mail/reading/bodyHtml.ts +25 -2
  95. package/apps/shared/components/mail/writableMailboxes.ts +2 -0
  96. package/apps/shared/components/rules/RuleBuilder.tsx +11 -2
  97. package/apps/shared/components/settings/GravatarSetting.tsx +27 -0
  98. package/apps/shared/components/settings/layout/SettingsShell.tsx +1 -1
  99. package/apps/shared/components/tasks/dueDate.ts +29 -0
  100. package/apps/shared/components/tasks/layout/TasksShell.tsx +1 -1
  101. package/apps/shared/mail/decryptedMessages.ts +51 -2
  102. package/apps/shared/mail/listSnapshots.ts +2 -0
  103. package/apps/shared/mail/outbox/sendDecision.ts +14 -3
  104. package/apps/shared/mail/outbox/sendJob.ts +5 -0
  105. package/apps/shared/mail/useMailLiveUpdates.ts +7 -1
  106. package/apps/shared/mail/usePermanentDelete.tsx +3 -1
  107. package/apps/shared/notifications/apiErrors.ts +2 -0
  108. package/apps/shared/notifications/preferences.ts +1 -1
  109. package/apps/shared/notifications/store.ts +3 -1
  110. package/apps/shared/plugins/pluginNav.ts +4 -2
  111. package/apps/www/calendar/index.tsx +24 -12
  112. package/apps/www/contacts/[uid].tsx +6 -1
  113. package/apps/www/contacts/index.tsx +66 -12
  114. package/apps/www/index.tsx +91 -28
  115. package/apps/www/messages/[uid].tsx +8 -3
  116. package/apps/www/settings/auto-reply/index.tsx +13 -0
  117. package/apps/www/settings/blocked-senders/index.tsx +4 -0
  118. package/apps/www/settings/encryption/index.tsx +66 -4
  119. package/apps/www/settings/privacy/index.tsx +4 -1
  120. package/apps/www/settings/profile/index.tsx +12 -1
  121. package/apps/www/settings/sharing/index.tsx +11 -1
  122. package/apps/www/settings/signatures/[uid].tsx +6 -4
  123. package/apps/www/tasks/index.tsx +97 -29
  124. package/dist/apps/admin/audit-log/index.js +3 -3
  125. package/dist/apps/admin/branding/index.js +3 -3
  126. package/dist/apps/admin/data-requests/index.js +17 -7
  127. package/dist/apps/admin/distribution-lists/[uid].js +3 -3
  128. package/dist/apps/admin/distribution-lists/index.js +3 -3
  129. package/dist/apps/admin/distribution-lists/new/index.js +3 -3
  130. package/dist/apps/admin/domains/[uid].js +6 -5
  131. package/dist/apps/admin/domains/index.js +3 -3
  132. package/dist/apps/admin/domains/new/index.js +3 -3
  133. package/dist/apps/admin/escrow-scopes/[uid].js +11 -6
  134. package/dist/apps/admin/escrow-scopes/index.js +3 -3
  135. package/dist/apps/admin/escrow-scopes/new/index.js +9 -4
  136. package/dist/apps/admin/index.js +4 -4
  137. package/dist/apps/admin/ingest-queue/index.js +3 -3
  138. package/dist/apps/admin/mailboxes/[uid].js +15 -7
  139. package/dist/apps/admin/quarantine/index.js +4 -4
  140. package/dist/apps/admin/retention-policy/index.js +3 -3
  141. package/dist/apps/admin/signing-certificates/index.js +3 -3
  142. package/dist/apps/admin/transport-rules/[uid].js +4 -4
  143. package/dist/apps/admin/transport-rules/index.js +3 -3
  144. package/dist/apps/admin/transport-rules/new/index.js +3 -3
  145. package/dist/apps/escrow/matters/[uid].js +8 -3
  146. package/dist/apps/escrow/matters/new/index.js +4 -0
  147. package/dist/apps/shared/calendar/calendarLiveUpdates.d.ts +11 -7
  148. package/dist/apps/shared/calendar/calendarLiveUpdates.js +10 -9
  149. package/dist/apps/shared/calendar/useCalendarReminders.js +2 -0
  150. package/dist/apps/shared/components/admin/ActionAlert.d.ts +13 -0
  151. package/dist/apps/shared/components/admin/ActionAlert.js +24 -0
  152. package/dist/apps/shared/components/admin/diagnostics/InformationPanel.js +2 -2
  153. package/dist/apps/shared/components/admin/diagnostics/InstalledPlugins.js +1 -1
  154. package/dist/apps/shared/components/admin/diagnostics/LogsPanel.js +1 -1
  155. package/dist/apps/shared/components/admin/diagnostics/RuntimePanel.js +1 -1
  156. package/dist/apps/shared/components/admin/diagnostics/SystemPanel.js +1 -1
  157. package/dist/apps/shared/components/admin/diagnostics/diagnosticsApi.d.ts +2 -2
  158. package/dist/apps/shared/components/admin/distributionLists/MemberListCard.js +4 -4
  159. package/dist/apps/shared/components/admin/elevation.d.ts +9 -0
  160. package/dist/apps/shared/components/admin/elevation.js +16 -0
  161. package/dist/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.d.ts +2 -0
  162. package/dist/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.js +41 -1
  163. package/dist/apps/shared/components/admin/layout/AdminShell.js +2 -1
  164. package/dist/apps/shared/components/admin/mailboxes/EraseLeftoverDataDialog.js +3 -3
  165. package/dist/apps/shared/components/admin/mailboxes/EscrowScopeCard.js +4 -4
  166. package/dist/apps/shared/components/admin/mailboxes/LeftoverMailboxesSection.js +4 -4
  167. package/dist/apps/shared/components/admin/mailboxes/ResourceSettingsCard.js +3 -3
  168. package/dist/apps/shared/components/admin/mailboxes/ShareAccessCard.js +6 -6
  169. package/dist/apps/shared/components/admin/settings/BrandingForm.js +17 -7
  170. package/dist/apps/shared/components/admin/settings/DomainDnsSetup.js +4 -4
  171. package/dist/apps/shared/components/admin/settings/EncryptionPolicyForm.js +17 -5
  172. package/dist/apps/shared/components/admin/settings/LoadedSettingsForm.js +3 -3
  173. package/dist/apps/shared/components/admin/settings/MailboxCreateForm.js +8 -3
  174. package/dist/apps/shared/components/admin/settings/MailboxPolicyForm.js +3 -3
  175. package/dist/apps/shared/components/admin/settings/PluginsManager.js +103 -18
  176. package/dist/apps/shared/components/admin/settings/RetentionPolicyForm.js +17 -10
  177. package/dist/apps/shared/components/admin/setup/EscrowSetupStep.js +18 -8
  178. package/dist/apps/shared/components/admin/setup/SetupWizard.js +8 -6
  179. package/dist/apps/shared/components/admin/signOut.js +3 -0
  180. package/dist/apps/shared/components/admin/usePagedList.js +2 -2
  181. package/dist/apps/shared/components/calendar/DescriptionEditor.js +2 -1
  182. package/dist/apps/shared/components/calendar/EventDetails.d.ts +3 -1
  183. package/dist/apps/shared/components/calendar/EventDetails.js +7 -4
  184. package/dist/apps/shared/components/calendar/EventEditor.js +81 -33
  185. package/dist/apps/shared/components/calendar/EventFormParts.d.ts +3 -1
  186. package/dist/apps/shared/components/calendar/EventFormParts.js +3 -3
  187. package/dist/apps/shared/components/calendar/EventMatchNav.d.ts +1 -1
  188. package/dist/apps/shared/components/calendar/EventMatchNav.js +12 -2
  189. package/dist/apps/shared/components/calendar/EventModal.js +10 -2
  190. package/dist/apps/shared/components/calendar/FindATime.js +4 -3
  191. package/dist/apps/shared/components/calendar/ListView.d.ts +10 -4
  192. package/dist/apps/shared/components/calendar/ListView.js +92 -27
  193. package/dist/apps/shared/components/calendar/RecurrenceEditor.d.ts +4 -1
  194. package/dist/apps/shared/components/calendar/RecurrenceEditor.js +15 -10
  195. package/dist/apps/shared/components/calendar/SplitDayView.js +11 -17
  196. package/dist/apps/shared/components/calendar/TaskQuickForm.js +3 -2
  197. package/dist/apps/shared/components/calendar/TimeGridView.js +23 -21
  198. package/dist/apps/shared/components/calendar/gridLayout.d.ts +41 -0
  199. package/dist/apps/shared/components/calendar/gridLayout.js +83 -0
  200. package/dist/apps/shared/components/calendar/layout/CalendarShell.js +1 -1
  201. package/dist/apps/shared/components/calendar/timePicker.js +5 -2
  202. package/dist/apps/shared/components/contacts/ContactCard.js +9 -3
  203. package/dist/apps/shared/components/contacts/ContactDetailPane.d.ts +5 -3
  204. package/dist/apps/shared/components/contacts/ContactDetailPane.js +11 -5
  205. package/dist/apps/shared/components/contacts/ContactForm.js +54 -16
  206. package/dist/apps/shared/components/contacts/ContactPhotoEditor.d.ts +21 -0
  207. package/dist/apps/shared/components/contacts/ContactPhotoEditor.js +56 -0
  208. package/dist/apps/shared/components/contacts/ContactPhotoField.d.ts +7 -4
  209. package/dist/apps/shared/components/contacts/ContactPhotoField.js +19 -21
  210. package/dist/apps/shared/components/contacts/layout/ContactsShell.js +1 -1
  211. package/dist/apps/shared/components/contacts/participantDetails.d.ts +4 -1
  212. package/dist/apps/shared/components/contacts/participantDetails.js +17 -1
  213. package/dist/apps/shared/components/layout/AppShell.js +9 -0
  214. package/dist/apps/shared/components/layout/KeyEnrollmentGate.js +2 -3
  215. package/dist/apps/shared/components/layout/MailboxProvisioning.d.ts +4 -1
  216. package/dist/apps/shared/components/layout/MailboxProvisioning.js +24 -2
  217. package/dist/apps/shared/components/layout/RecoveryCodeUnlock.d.ts +4 -2
  218. package/dist/apps/shared/components/layout/RecoveryCodeUnlock.js +4 -2
  219. package/dist/apps/shared/components/mail/ConversationList.js +49 -6
  220. package/dist/apps/shared/components/mail/ConversationThreadPane.js +1 -1
  221. package/dist/apps/shared/components/mail/MenuButton.d.ts +2 -78
  222. package/dist/apps/shared/components/mail/MenuButton.js +2 -211
  223. package/dist/apps/shared/components/mail/MessageDetailPane.js +10 -6
  224. package/dist/apps/shared/components/mail/compose/ComposeToolbar.d.ts +3 -0
  225. package/dist/apps/shared/components/mail/compose/ComposeToolbar.js +10 -1
  226. package/dist/apps/shared/components/mail/compose/ComposeWindow.js +55 -10
  227. package/dist/apps/shared/components/mail/compose/GifPicker.js +10 -4
  228. package/dist/apps/shared/components/mail/compose/RecipientInput.js +9 -7
  229. package/dist/apps/shared/components/mail/compose/composeFlushRegistry.d.ts +7 -1
  230. package/dist/apps/shared/components/mail/compose/composeFlushRegistry.js +13 -1
  231. package/dist/apps/shared/components/mail/compose/encryptionRequirement.d.ts +5 -0
  232. package/dist/apps/shared/components/mail/compose/quotedBody.js +28 -5
  233. package/dist/apps/shared/components/mail/layout/MailShell.js +1 -1
  234. package/dist/apps/shared/components/mail/reading/MessageBody.js +11 -6
  235. package/dist/apps/shared/components/mail/reading/bodyContent.js +2 -0
  236. package/dist/apps/shared/components/mail/reading/bodyHtml.d.ts +5 -1
  237. package/dist/apps/shared/components/mail/reading/bodyHtml.js +23 -2
  238. package/dist/apps/shared/components/mail/writableMailboxes.js +2 -0
  239. package/dist/apps/shared/components/rules/RuleBuilder.js +9 -1
  240. package/dist/apps/shared/components/settings/GravatarSetting.d.ts +6 -0
  241. package/dist/apps/shared/components/settings/GravatarSetting.js +15 -0
  242. package/dist/apps/shared/components/settings/layout/SettingsShell.js +1 -1
  243. package/dist/apps/shared/components/tasks/dueDate.d.ts +12 -0
  244. package/dist/apps/shared/components/tasks/dueDate.js +26 -0
  245. package/dist/apps/shared/components/tasks/layout/TasksShell.js +1 -1
  246. package/dist/apps/shared/mail/decryptedMessages.d.ts +8 -0
  247. package/dist/apps/shared/mail/decryptedMessages.js +46 -2
  248. package/dist/apps/shared/mail/listSnapshots.js +2 -0
  249. package/dist/apps/shared/mail/outbox/sendDecision.d.ts +6 -3
  250. package/dist/apps/shared/mail/outbox/sendDecision.js +13 -3
  251. package/dist/apps/shared/mail/outbox/sendJob.d.ts +1 -0
  252. package/dist/apps/shared/mail/outbox/sendJob.js +5 -1
  253. package/dist/apps/shared/mail/useMailLiveUpdates.d.ts +3 -0
  254. package/dist/apps/shared/mail/useMailLiveUpdates.js +6 -1
  255. package/dist/apps/shared/mail/usePermanentDelete.d.ts +2 -0
  256. package/dist/apps/shared/mail/usePermanentDelete.js +1 -1
  257. package/dist/apps/shared/notifications/apiErrors.js +2 -0
  258. package/dist/apps/shared/notifications/preferences.d.ts +1 -1
  259. package/dist/apps/shared/notifications/preferences.js +1 -1
  260. package/dist/apps/shared/notifications/store.js +3 -1
  261. package/dist/apps/shared/plugins/pluginNav.d.ts +2 -1
  262. package/dist/apps/shared/plugins/pluginNav.js +4 -2
  263. package/dist/apps/www/calendar/index.js +24 -12
  264. package/dist/apps/www/contacts/[uid].js +6 -2
  265. package/dist/apps/www/contacts/index.js +62 -11
  266. package/dist/apps/www/index.js +85 -30
  267. package/dist/apps/www/messages/[uid].js +8 -3
  268. package/dist/apps/www/settings/auto-reply/index.js +13 -1
  269. package/dist/apps/www/settings/blocked-senders/index.js +4 -0
  270. package/dist/apps/www/settings/encryption/index.js +29 -5
  271. package/dist/apps/www/settings/privacy/index.js +3 -2
  272. package/dist/apps/www/settings/profile/index.js +4 -1
  273. package/dist/apps/www/settings/sharing/index.js +10 -1
  274. package/dist/apps/www/settings/signatures/[uid].js +6 -4
  275. package/dist/apps/www/tasks/index.js +94 -32
  276. package/dist/lib/admin/auditLogApi.js +2 -2
  277. package/dist/lib/admin/distributionListsApi.js +2 -2
  278. package/dist/lib/admin/domainsApi.js +2 -2
  279. package/dist/lib/admin/escrowAuditLogApi.js +2 -2
  280. package/dist/lib/admin/escrowScopesApi.js +2 -2
  281. package/dist/lib/admin/pluginsApi.d.ts +31 -2
  282. package/dist/lib/admin/pluginsApi.js +23 -1
  283. package/dist/lib/admin/retentionPolicyApi.d.ts +3 -0
  284. package/dist/lib/admin/retentionPolicyApi.js +3 -0
  285. package/dist/lib/admin/transportRulesApi.js +2 -2
  286. package/dist/lib/appearance/preferencesApi.d.ts +1 -1
  287. package/dist/lib/appearance/preferencesApi.js +3 -14
  288. package/dist/lib/auth/session.d.ts +51 -12
  289. package/dist/lib/auth/session.js +214 -38
  290. package/dist/lib/branding/brandingApi.d.ts +3 -3
  291. package/dist/lib/branding/brandingApi.js +9 -21
  292. package/dist/lib/calendar/calendarApi.js +3 -1
  293. package/dist/lib/calendar/eventSearch.js +43 -6
  294. package/dist/lib/calendar/freeBusyApi.d.ts +2 -1
  295. package/dist/lib/calendar/freeBusyApi.js +13 -5
  296. package/dist/lib/calendar/recurrence.js +51 -3
  297. package/dist/lib/calendar/useDayKey.d.ts +7 -0
  298. package/dist/lib/calendar/useDayKey.js +43 -0
  299. package/dist/lib/components/avatar/ContactAvatar.d.ts +6 -2
  300. package/dist/lib/components/avatar/ContactAvatar.js +37 -8
  301. package/dist/lib/components/avatar/ContactPhotoAvatar.d.ts +11 -0
  302. package/dist/lib/components/avatar/ContactPhotoAvatar.js +18 -0
  303. package/dist/lib/components/forms/CameraCaptureModal.d.ts +17 -0
  304. package/dist/lib/components/forms/CameraCaptureModal.js +88 -0
  305. package/dist/lib/components/forms/ImageEditBadge.d.ts +28 -0
  306. package/dist/lib/components/forms/ImageEditBadge.js +62 -0
  307. package/dist/lib/components/menus/MenuButton.d.ts +78 -0
  308. package/dist/lib/components/menus/MenuButton.js +219 -0
  309. package/dist/lib/components/navigation/BottomTabBar.d.ts +3 -1
  310. package/dist/lib/components/navigation/BottomTabBar.js +22 -3
  311. package/dist/lib/components/overlays/PopoverPortal.d.ts +2 -1
  312. package/dist/lib/components/overlays/PopoverPortal.js +3 -2
  313. package/dist/lib/components/pickers/TimeZonePicker.js +13 -1
  314. package/dist/lib/contacts/contactsApi.d.ts +5 -2
  315. package/dist/lib/contacts/contactsApi.js +16 -19
  316. package/dist/lib/contacts/gravatar.d.ts +14 -3
  317. package/dist/lib/contacts/gravatar.js +42 -5
  318. package/dist/lib/contacts/preparePhoto.d.ts +24 -0
  319. package/dist/lib/contacts/preparePhoto.js +250 -0
  320. package/dist/lib/contacts/useContactPhotoSrc.d.ts +19 -0
  321. package/dist/lib/contacts/useContactPhotoSrc.js +153 -0
  322. package/dist/lib/contacts/vcard.d.ts +3 -0
  323. package/dist/lib/contacts/vcard.js +59 -7
  324. package/dist/lib/crypto/escrowKeys.d.ts +8 -0
  325. package/dist/lib/crypto/escrowKeys.js +20 -1
  326. package/dist/lib/crypto/messageSecurity.d.ts +7 -4
  327. package/dist/lib/crypto/messageSecurity.js +23 -5
  328. package/dist/lib/crypto/passwordUnlock.d.ts +6 -1
  329. package/dist/lib/crypto/passwordUnlock.js +13 -2
  330. package/dist/lib/crypto/smime.js +12 -2
  331. package/dist/lib/mail/compose/composeQuoting.js +3 -2
  332. package/dist/lib/mail/compose/quotedImages.d.ts +6 -3
  333. package/dist/lib/mail/compose/quotedImages.js +35 -9
  334. package/dist/lib/mail/labelsApi.js +2 -2
  335. package/dist/lib/mail/mailApi.d.ts +8 -5
  336. package/dist/lib/mail/mailApi.js +73 -39
  337. package/dist/lib/mail/mailDetailHooks.js +11 -3
  338. package/dist/lib/mail/mailFilterRulesApi.js +2 -2
  339. package/dist/lib/mail/mailSignaturesApi.js +2 -2
  340. package/dist/lib/mail/mailboxImportApi.d.ts +1 -1
  341. package/dist/lib/mail/mailboxImportApi.js +3 -15
  342. package/dist/lib/mail/messageBodySanitizer.d.ts +6 -0
  343. package/dist/lib/mail/messageBodySanitizer.js +44 -1
  344. package/dist/lib/search/queryGrammar.js +12 -4
  345. package/dist/lib/search/searchTier3.d.ts +6 -1
  346. package/dist/lib/search/searchTier3.js +54 -25
  347. package/dist/lib/tasks/tasksApi.js +3 -3
  348. package/dist/lib/util/api.d.ts +26 -4
  349. package/dist/lib/util/api.js +73 -16
  350. package/dist/lib/util/apiQuery.d.ts +11 -0
  351. package/dist/lib/util/apiQuery.js +11 -0
  352. package/dist/lib/util/settleWithLimit.d.ts +5 -0
  353. package/dist/lib/util/settleWithLimit.js +25 -0
  354. package/lib/admin/auditLogApi.ts +2 -2
  355. package/lib/admin/distributionListsApi.ts +2 -2
  356. package/lib/admin/domainsApi.ts +2 -2
  357. package/lib/admin/escrowAuditLogApi.ts +2 -2
  358. package/lib/admin/escrowScopesApi.ts +2 -2
  359. package/lib/admin/pluginsApi.ts +45 -3
  360. package/lib/admin/retentionPolicyApi.ts +4 -0
  361. package/lib/admin/transportRulesApi.ts +2 -2
  362. package/lib/appearance/preferencesApi.ts +3 -14
  363. package/lib/auth/session.ts +239 -39
  364. package/lib/branding/brandingApi.ts +9 -21
  365. package/lib/calendar/calendarApi.ts +3 -1
  366. package/lib/calendar/eventSearch.ts +46 -6
  367. package/lib/calendar/freeBusyApi.ts +14 -6
  368. package/lib/calendar/recurrence.ts +59 -8
  369. package/lib/calendar/useDayKey.ts +45 -0
  370. package/lib/components/avatar/ContactAvatar.tsx +46 -8
  371. package/lib/components/avatar/ContactPhotoAvatar.tsx +26 -0
  372. package/lib/components/forms/CameraCaptureModal.tsx +133 -0
  373. package/lib/components/forms/ImageEditBadge.tsx +121 -0
  374. package/lib/components/menus/MenuButton.tsx +408 -0
  375. package/lib/components/navigation/BottomTabBar.tsx +30 -8
  376. package/lib/components/overlays/PopoverPortal.tsx +3 -2
  377. package/lib/components/pickers/TimeZonePicker.tsx +17 -1
  378. package/lib/contacts/contactsApi.ts +18 -19
  379. package/lib/contacts/gravatar.ts +47 -5
  380. package/lib/contacts/preparePhoto.ts +273 -0
  381. package/lib/contacts/useContactPhotoSrc.ts +177 -0
  382. package/lib/contacts/vcard.ts +60 -7
  383. package/lib/crypto/escrowKeys.ts +21 -1
  384. package/lib/crypto/messageSecurity.ts +33 -9
  385. package/lib/crypto/passwordUnlock.ts +14 -2
  386. package/lib/crypto/smime.ts +13 -2
  387. package/lib/mail/compose/composeQuoting.ts +3 -2
  388. package/lib/mail/compose/quotedImages.ts +39 -9
  389. package/lib/mail/labelsApi.ts +2 -2
  390. package/lib/mail/mailApi.ts +96 -39
  391. package/lib/mail/mailDetailHooks.ts +11 -3
  392. package/lib/mail/mailFilterRulesApi.ts +2 -2
  393. package/lib/mail/mailSignaturesApi.ts +2 -2
  394. package/lib/mail/mailboxImportApi.ts +9 -15
  395. package/lib/mail/messageBodySanitizer.ts +47 -1
  396. package/lib/search/queryGrammar.ts +13 -4
  397. package/lib/search/searchTier3.ts +65 -32
  398. package/lib/tasks/tasksApi.ts +3 -3
  399. package/lib/util/api.ts +99 -16
  400. package/lib/util/apiQuery.ts +13 -0
  401. package/lib/util/settleWithLimit.ts +25 -0
  402. package/package.json +1 -1
@@ -13,12 +13,23 @@
13
13
  * `@rapidmx/restapi` and this repo's `.claude/NOTES.md`.
14
14
  */
15
15
 
16
- import { ApiClient, ApiRequestError, apiFetch, apiUrl, authApiFetch, withClient, withCsrfHeader } from "../util/api.js";
17
- import { ListParams, buildQuery } from "../util/apiQuery.js";
16
+ import {
17
+ ApiClient,
18
+ ApiRequestError,
19
+ apiFetch,
20
+ apiUrl,
21
+ authApiFetch,
22
+ withClient,
23
+ withClientBlob,
24
+ withClientRaw,
25
+ withCsrfHeader,
26
+ } from "../util/api.js";
27
+ import { ListParams, buildQuery, sortQuery } from "../util/apiQuery.js";
18
28
  import { deviceTimeZone } from "../util/timeZone.js";
19
29
  import type { EncryptionPreference, PublicKey } from "../crypto/keyvaultApi.js";
20
30
  import { bytesToBinaryString } from "../crypto/mime.js";
21
- import { newContentId, parseDataImage } from "./inlineImages.js";
31
+ import { EMBEDDED_IMAGE_TYPES, newContentId, parseDataImage } from "./inlineImages.js";
32
+ import { MAX_QUOTED_IMAGE_BYTES } from "./compose/quotedImages.js";
22
33
  import type { ResolvedPrincipal } from "./mailboxAccessApi.js";
23
34
 
24
35
  export type { ListParams };
@@ -156,7 +167,7 @@ export function safeSendersOf(mailbox: Pick<Mailbox, "safeSenders">): string[] {
156
167
  /** Lists the mailboxes the caller owns or has been granted - the same for everyone, an administrator included. With
157
168
  * `scope: "admin"` (admin console only) it lists every mailbox as administrative metadata instead. */
158
169
  export function listMailboxes(params: ListParams & AdminScopeParams = {}, client?: ApiClient): Promise<Mailbox[]> {
159
- return withClient(client, `/mail/mailboxes?${buildQuery(params, scopeQuery(params))}`);
170
+ return withClient(client, `/mail/mailboxes?${buildQuery(params, { ...scopeQuery(params), ...(params.scope === "admin" ? sortQuery({ uid: "ASC" }) : {}) })}`);
160
171
  }
161
172
 
162
173
  /** One mailbox the caller owns or has been granted; with `scope: "admin"` (admin console only), its administrative metadata. */
@@ -320,7 +331,7 @@ export function listQuarantine(
320
331
  params: ListParams & AdminScopeParams = {},
321
332
  client?: ApiClient,
322
333
  ): Promise<QuarantineEntry[]> {
323
- return withClient(client, `/mail/quarantine?${buildQuery(params, { mailboxUid, ...scopeQuery(params) })}`);
334
+ return withClient(client, `/mail/quarantine?${buildQuery(params, { mailboxUid, ...scopeQuery(params), ...sortQuery({ dateCreated: "DESC", uid: "ASC" }) })}`);
324
335
  }
325
336
 
326
337
  /**
@@ -368,7 +379,7 @@ export function listIngestQueue(
368
379
  params: ListParams & AdminScopeParams = {},
369
380
  client?: ApiClient,
370
381
  ): Promise<IngestQueueEntry[]> {
371
- return withClient(client, `/mail/ingest-queue?${buildQuery(params, { mailboxUid, ...scopeQuery(params) })}`);
382
+ return withClient(client, `/mail/ingest-queue?${buildQuery(params, { mailboxUid, ...scopeQuery(params), ...sortQuery({ dateCreated: "DESC", uid: "ASC" }) })}`);
372
383
  }
373
384
 
374
385
  export interface AclRecord {
@@ -1029,9 +1040,14 @@ export function attachmentContentUrl(uid: string): string {
1029
1040
  /**
1030
1041
  * Uploads a file's raw bytes as a new attachment on a not-yet-sent draft. Bypasses `apiFetch` — that helper
1031
1042
  * always forces `Content-Type: application/json`, which would corrupt binary content; this sends the file's
1032
- * own bytes/type directly instead, matching `BaseAttachmentRoute.upload`'s expectation of a raw request body.
1043
+ * own bytes/type directly instead (through `client` when one is given), matching `BaseAttachmentRoute.upload`'s expectation of a raw request body.
1033
1044
  */
1034
- export async function uploadAttachment(messageUid: string, file: File, options: { inline?: boolean; contentId?: string } = {}): Promise<Attachment> {
1045
+ export function uploadAttachment(
1046
+ messageUid: string,
1047
+ file: File,
1048
+ options: { inline?: boolean; contentId?: string } = {},
1049
+ client?: ApiClient,
1050
+ ): Promise<Attachment> {
1035
1051
  // Percent-encoded, not form-encoded (`URLSearchParams` writes a space as `+`, which the server reads as a plus sign: "My+photo.png").
1036
1052
  const query: Record<string, string> = { messageUid, filename: file.name, mimeType: file.type || "application/octet-stream" };
1037
1053
  if (options.inline) {
@@ -1043,19 +1059,7 @@ export async function uploadAttachment(messageUid: string, file: File, options:
1043
1059
  const params = Object.entries(query)
1044
1060
  .map(([key, value]) => `${key}=${encodeURIComponent(value)}`)
1045
1061
  .join("&");
1046
- const res = await fetch(apiUrl(`/mail/attachments/upload?${params}`), {
1047
- method: "POST",
1048
- credentials: "include",
1049
- headers: withCsrfHeader({ "Content-Type": file.type || "application/octet-stream" }),
1050
- body: file,
1051
- });
1052
- const contentType = res.headers.get("content-type") ?? "";
1053
- const responseBody = contentType.includes("application/json") ? await res.json().catch(() => undefined) : undefined;
1054
- if (!res.ok) {
1055
- const message = (responseBody && (responseBody.message || responseBody.error)) || res.statusText || "Upload failed.";
1056
- throw new ApiRequestError(message, res.status, responseBody?.code);
1057
- }
1058
- return responseBody as Attachment;
1062
+ return withClientRaw(client, `/mail/attachments/upload?${params}`, "POST", file, file.type || "application/octet-stream");
1059
1063
  }
1060
1064
 
1061
1065
  export interface ComposeRecipientInput {
@@ -1149,15 +1153,58 @@ export function emptyFolder(folderUid: string, client?: ApiClient): Promise<void
1149
1153
  * itself does no MIME composition). Does not send the message.
1150
1154
  */
1151
1155
  export async function assembleDraft(messageUid: string, input: AssembleDraftInput, client?: ApiClient): Promise<Message> {
1152
- const html = await attachDataImages(messageUid, input.html);
1156
+ const html = await attachDataImages(messageUid, input.html, client);
1153
1157
  return withClient(client, `/mail/compose/${encodeURIComponent(messageUid)}/assemble`, {
1154
1158
  method: "POST",
1155
1159
  body: JSON.stringify({ ...input, html }),
1156
1160
  });
1157
1161
  }
1158
1162
 
1159
- /** `data:` images already attached to a draft: its uid and the image, to the URL of the attachment it became. */
1163
+ /** `data:` images already attached to a draft: its uid and a fingerprint of the image, to the URL of the attachment it became. */
1160
1164
  const attachedDataImages = new Map<string, string>();
1165
+ /** The most pictures remembered; the oldest go first (a draft that was never sent or discarded here would otherwise keep its pictures forever). */
1166
+ const MAX_ATTACHED_DATA_IMAGES = 256;
1167
+
1168
+ /** The SHA-256 of the whole of `text`, in hex - what stands for a `data:` URI of up to megabytes without holding it, and which no two quoted pictures can be made to share. Where Web Crypto is not available the text itself stands for it. */
1169
+ async function fingerprint(text: string): Promise<string> {
1170
+ if (!globalThis.crypto?.subtle) {
1171
+ return text;
1172
+ }
1173
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(text));
1174
+ return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("");
1175
+ }
1176
+
1177
+ /** The most pictures attached to one draft from its text; a quote cannot make a person's reply upload (and be charged for) any number of them. */
1178
+ const MAX_DATA_IMAGES_PER_DRAFT = 20;
1179
+
1180
+ /** Whether `bytes` start the way a picture of `contentType` does - the type a quote declares is its own word, and the bytes are what the server stores and shows. */
1181
+ function hasImageSignature(contentType: string, bytes: Uint8Array): boolean {
1182
+ const at = (offset: number, text: string) => Array.from(text).every((char, i) => bytes[offset + i] === char.charCodeAt(0));
1183
+ switch (contentType.split("/")[1]) {
1184
+ case "png":
1185
+ return at(0, "\x89PNG");
1186
+ case "jpg":
1187
+ case "jpeg":
1188
+ return at(0, "\xff\xd8\xff");
1189
+ case "gif":
1190
+ return at(0, "GIF8");
1191
+ case "webp":
1192
+ return at(0, "RIFF") && at(8, "WEBP");
1193
+ case "avif":
1194
+ return at(4, "ftypavi");
1195
+ default:
1196
+ return at(0, "BM");
1197
+ }
1198
+ }
1199
+
1200
+ /** Forgets the pictures attached to draft `messageUid`, for when it has been sent or deleted (they are only remembered to avoid attaching one twice). */
1201
+ export function forgetDraftImages(messageUid: string): void {
1202
+ for (const key of Array.from(attachedDataImages.keys())) {
1203
+ if (key.startsWith(`${messageUid}\n`)) {
1204
+ attachedDataImages.delete(key);
1205
+ }
1206
+ }
1207
+ }
1161
1208
 
1162
1209
  const DATA_IMAGE_TAG = /(<img\b[^>]*?\bsrc\s*=\s*)(["'])(data:image\/[^"']*)\2/gi;
1163
1210
 
@@ -1169,28 +1216,39 @@ function pictureName(contentType: string): string {
1169
1216
  /**
1170
1217
  * `html` with each `<img>` that embeds its picture as a `data:` URI (a quoted original's image, one pasted into the editor) pointed at an
1171
1218
  * attachment of the draft instead: the server drops `data:` images from the message it composes, and an attachment is what it makes an
1172
- * inline part of. A picture is attached once per draft however often the draft is saved.
1219
+ * inline part of. A picture is attached once per draft however often the draft is saved. Only what a quote itself embeds is attached - a raster
1220
+ * image (`EMBEDDED_IMAGE_TYPES`, never SVG) of at most `MAX_QUOTED_IMAGE_BYTES` whose bytes are of the type it declares, and no more than
1221
+ * `MAX_DATA_IMAGES_PER_DRAFT` of them; any other is left as it is, and the server drops it.
1173
1222
  */
1174
- async function attachDataImages(messageUid: string, html: string): Promise<string> {
1223
+ async function attachDataImages(messageUid: string, html: string, client?: ApiClient): Promise<string> {
1175
1224
  const found = Array.from(html.matchAll(DATA_IMAGE_TAG));
1176
1225
  if (found.length === 0) {
1177
1226
  return html;
1178
1227
  }
1179
1228
  const urls = new Map<string, string>();
1180
1229
  for (const [, , , uri] of found) {
1181
- const key = `${messageUid}\n${uri}`;
1230
+ if (urls.has(uri)) {
1231
+ continue;
1232
+ }
1233
+ if (urls.size >= MAX_DATA_IMAGES_PER_DRAFT) {
1234
+ break;
1235
+ }
1236
+ const key = `${messageUid}\n${await fingerprint(uri)}`;
1182
1237
  const known = attachedDataImages.get(key);
1183
1238
  if (known) {
1184
1239
  urls.set(uri, known);
1185
1240
  continue;
1186
1241
  }
1187
1242
  const image = parseDataImage(uri);
1188
- if (!image) {
1243
+ if (!image || !EMBEDDED_IMAGE_TYPES.test(image.contentType) || image.bytes.length > MAX_QUOTED_IMAGE_BYTES || !hasImageSignature(image.contentType, image.bytes)) {
1189
1244
  continue;
1190
1245
  }
1191
- const attachment = await uploadAttachment(messageUid, new File([image.bytes as BlobPart], pictureName(image.contentType), { type: image.contentType }), { inline: true });
1246
+ const attachment = await uploadAttachment(messageUid, new File([image.bytes as BlobPart], pictureName(image.contentType), { type: image.contentType }), { inline: true }, client);
1192
1247
  const url = attachmentContentUrl(attachment.uid);
1193
1248
  attachedDataImages.set(key, url);
1249
+ if (attachedDataImages.size > MAX_ATTACHED_DATA_IMAGES) {
1250
+ attachedDataImages.delete(attachedDataImages.keys().next().value!);
1251
+ }
1194
1252
  urls.set(uri, url);
1195
1253
  }
1196
1254
  return html.replace(DATA_IMAGE_TAG, (match, start: string, quote: string, uri: string) => (urls.has(uri) ? `${start}${quote}${urls.get(uri)}${quote}` : match));
@@ -1283,8 +1341,9 @@ export async function queueMessageSend(messageUid: string, client?: ApiClient):
1283
1341
 
1284
1342
  /**
1285
1343
  * Fetches a message's raw RFC 5322 MIME source — for client-side E2E decrypt/signature-verification
1286
- * only (`crypto/smimeMessage.ts`), never for display. Bypasses `apiFetch()` (which only ever decodes a
1287
- * JSON response body): this server-local route (`BaseMessageRawContentRoute.ts` in `server`, mounted
1344
+ * only (`crypto/smimeMessage.ts`), never for display. Goes through `withClientBlob()` rather than `apiFetch()` (which
1345
+ * only ever decodes a JSON response body, and renews an expired session) - the same session renewal and, given a
1346
+ * `client`, the same account and token as every other call: this server-local route (`BaseMessageRawContentRoute.ts` in `server`, mounted
1288
1347
  * alongside `@rapidmx/restapi`'s own `MessageRoute`) returns `message/rfc822`, not JSON — the one thing
1289
1348
  * that library's own `GET /:id/content` deliberately never serves (see that route's own doc comment).
1290
1349
  *
@@ -1294,17 +1353,15 @@ export async function queueMessageSend(messageUid: string, client?: ApiClient):
1294
1353
  * straight to `evaluateMessageSecurity()` (see `crypto/mime.ts`'s doc comment on binary strings); it is
1295
1354
  * not display text.
1296
1355
  */
1297
- export async function getMessageRawContent(messageUid: string): Promise<string> {
1298
- const res = await fetch(apiUrl(`/mail/messages/${encodeURIComponent(messageUid)}/raw`), { credentials: "include" });
1299
- if (!res.ok) {
1300
- const contentType = res.headers.get("content-type") ?? "";
1301
- const body = contentType.includes("application/json") ? await res.json().catch(() => undefined) : undefined;
1302
- const message = (body && (body.message || body.error)) || res.statusText || "Could not load this message's raw content.";
1303
- throw new ApiRequestError(message, res.status, body?.code);
1304
- }
1356
+ export async function getMessageRawContent(messageUid: string, client?: ApiClient): Promise<string> {
1357
+ const raw = await withClientBlob(
1358
+ client,
1359
+ `/mail/messages/${encodeURIComponent(messageUid)}/raw`,
1360
+ "Could not load this message's raw content.",
1361
+ );
1305
1362
  // Not `new TextDecoder("latin1")`: WHATWG maps that label to windows-1252, which remaps 0x80-0x9F and so
1306
1363
  // isn't byte-preserving. `bytesToBinaryString()` is an exact byte -> code unit conversion.
1307
- return bytesToBinaryString(new Uint8Array(await res.arrayBuffer()));
1364
+ return bytesToBinaryString(new Uint8Array(await raw.arrayBuffer()));
1308
1365
  }
1309
1366
 
1310
1367
  export interface ImpersonationResult {
@@ -14,9 +14,14 @@ export function useMessageAttachments(message: Message | null): Attachment[] {
14
14
  setAttachments([]);
15
15
  return;
16
16
  }
17
+ // A response for a message since replaced must not overwrite the one now shown.
18
+ let cancelled = false;
17
19
  listAttachments(message.folderUid, message.uid)
18
- .then(setAttachments)
19
- .catch(() => setAttachments([]));
20
+ .then((listed) => !cancelled && setAttachments(listed))
21
+ .catch(() => !cancelled && setAttachments([]));
22
+ return () => {
23
+ cancelled = true;
24
+ };
20
25
  // Only the fields the fetch actually depends on — not the whole `message` object, which gets a new
21
26
  // reference on every metadata-only patch (e.g. `useMarkMessageRead`'s `onUpdated(updated)`, or a
22
27
  // star/flag/label change wired to the same state) and would otherwise trigger a needless re-fetch.
@@ -53,5 +58,8 @@ export function useMarkMessageRead(message: Message | null, onUpdated: (updated:
53
58
  return () => {
54
59
  cancelled = true;
55
60
  };
56
- }, [message]);
61
+ // Only the message's identity and read flag: a metadata-only patch swaps the object while the request is in flight, and
62
+ // restarting then would drop the first answer and send a second request with the old version (a 409 that leaves the
63
+ // message unread).
64
+ }, [message?.uid, message?.flags.read]);
57
65
  }
@@ -12,7 +12,7 @@
12
12
  */
13
13
 
14
14
  import { ApiClient, withClient } from "../util/api.js";
15
- import { ListParams, buildQuery } from "../util/apiQuery.js";
15
+ import { ListParams, SETTINGS_LIST_LIMIT, buildQuery } from "../util/apiQuery.js";
16
16
  import { MessageImportance } from "./mailApi.js";
17
17
 
18
18
  export type { ListParams };
@@ -62,7 +62,7 @@ export interface MailFilterRule {
62
62
  * account of a multi-account app) to call instead of the default global `apiFetch()` - see `withClient()`'s
63
63
  * own doc comment in `util/api.ts`. Omitted (the default), every function here behaves exactly as before. */
64
64
  export function listMailFilterRules(mailboxUid: string, params: ListParams = {}, client?: ApiClient): Promise<MailFilterRule[]> {
65
- return withClient(client, `/mail/mail-filter-rules?${buildQuery(params, { mailboxUid })}`);
65
+ return withClient(client, `/mail/mail-filter-rules?${buildQuery({ limit: SETTINGS_LIST_LIMIT, ...params }, { mailboxUid })}`);
66
66
  }
67
67
 
68
68
  export function getMailFilterRule(uid: string, client?: ApiClient): Promise<MailFilterRule> {
@@ -11,7 +11,7 @@
11
11
  */
12
12
 
13
13
  import { ApiClient, withClient } from "../util/api.js";
14
- import { ListParams, buildQuery } from "../util/apiQuery.js";
14
+ import { ListParams, SETTINGS_LIST_LIMIT, buildQuery } from "../util/apiQuery.js";
15
15
 
16
16
  export type { ListParams };
17
17
 
@@ -36,7 +36,7 @@ export interface MailSignature {
36
36
  * account of a multi-account app) to call instead of the default global `apiFetch()` - see `withClient()`'s
37
37
  * own doc comment in `util/api.ts`. Omitted (the default), every function here behaves exactly as before. */
38
38
  export function listMailSignatures(mailboxUid: string, params: ListParams = {}, client?: ApiClient): Promise<MailSignature[]> {
39
- return withClient(client, `/mail/mail-signatures?${buildQuery(params, { mailboxUid })}`);
39
+ return withClient(client, `/mail/mail-signatures?${buildQuery({ limit: SETTINGS_LIST_LIMIT, ...params }, { mailboxUid })}`);
40
40
  }
41
41
 
42
42
  export function getMailSignature(uid: string, client?: ApiClient): Promise<MailSignature> {
@@ -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> {