@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
@@ -123,8 +123,11 @@ export interface MessageSecurityResult {
123
123
  * recovered and it carried a protected Subject. */
124
124
  subject?: string;
125
125
  /** Every protected header recovered from inside the signed/decrypted entity - present only when that entity
126
- * carries RFC 9788 protected headers (a protected `From`). Absent for a legacy S/MIME sender's signed-only
127
- * message: its outer Subject/To/Cc were then never signed, and a UI should not present them as verified. */
126
+ * carries RFC 9788 protected headers (a protected `From`) AND a signature over them verified (so also for a
127
+ * `"verified_at_first_open"` result). Absent for a legacy S/MIME sender's signed-only message (its outer
128
+ * Subject/To/Cc were then never signed, and a UI should not present them as verified), and for an encrypted
129
+ * message without a verified signature: anyone holding the reader's public certificate can encrypt headers of their
130
+ * own choosing, so they are never exposed. */
128
131
  protectedHeaders?: MessageProtectedHeaders;
129
132
  /** The attachments inside the verified/decrypted entity - for a signed-only message the only attachments the
130
133
  * signature covers, so a UI showing a verified badge should list these rather than the server's attachment
@@ -189,7 +192,7 @@ function isEncryptedContentType(contentType: { value: string; params: Record<str
189
192
 
190
193
  const NO_KEY_ERROR = "This device doesn't have the key needed to decrypt this message.";
191
194
  const UNSUPPORTED_ENCRYPTION_ERROR =
192
- "This message uses an older, unauthenticated encryption algorithm that RapidMX doesn't accept, so it can't be opened safely.";
195
+ "This message uses an older, unauthenticated encryption algorithm, or an encryption format, that RapidMX doesn't accept, so it can't be opened safely.";
193
196
 
194
197
  function renderDisplayBody(display: DisplayBody | undefined): Pick<MessageSecurityResult, "html" | "text"> {
195
198
  if (display?.html !== undefined) {
@@ -219,7 +222,7 @@ export interface SignerBindingInput {
219
222
  /** The certificate matched to the verified SignerInfo; `undefined` fails closed. */
220
223
  signerCertificateDer: Uint8Array | undefined;
221
224
  /** Headers recovered from inside the signed content (RFC 9788), if any. */
222
- protectedHeaders: (Pick<ProtectedHeaders, "from" | "to"> & Partial<Pick<ProtectedHeaders, "cc" | "subject">>) | undefined;
225
+ protectedHeaders: (Pick<ProtectedHeaders, "from" | "to"> & Partial<Pick<ProtectedHeaders, "cc" | "subject" | "date">>) | undefined;
223
226
  /** The received message's outer header map (lowercased names), as `parseMimeEntity()` returns it. */
224
227
  outerHeaders: Record<string, string>;
225
228
  /** Every outer header field in order (`parseMimeEntity()`'s `fields`) - checked for repeated
@@ -229,7 +232,7 @@ export interface SignerBindingInput {
229
232
  protectedFields?: MimeHeaderField[];
230
233
  /** One trusted fingerprint or several (any match is accepted). An empty array counts as none supplied. */
231
234
  pinnedSignerFingerprint?: string | string[];
232
- /** Also require the protected `Subject` (when protected headers are present) to equal the outer one - right for
235
+ /** Also require the protected `Subject` and `Date` (when protected headers are present) to equal the outer ones - right for
233
236
  * a signed-only (`hp="clear"`) message, wrong for an encrypted one whose outer Subject is obscured. */
234
237
  compareSubject?: boolean;
235
238
  }
@@ -246,6 +249,15 @@ function hasRepeatedAddressField(fields: MimeHeaderField[] | undefined): boolean
246
249
  return SINGLETON_ADDRESS_FIELDS.some((name) => (counts.get(name) ?? 0) > 1);
247
250
  }
248
251
 
252
+ /** Whether the unsigned (outer) `Date:` header value `outer` names the same instant as the signed one, `signed`. A signed date
253
+ * that is missing or unreadable leaves nothing to compare, but when there is one the outer must match it: a replayed message
254
+ * that dropped or garbled its outer Date is as changed as one that carries a new one. */
255
+ function sameInstant(signed: string | undefined, outer: string | undefined): boolean {
256
+ // `String(undefined)` is not a date either.
257
+ const left = Date.parse(String(signed));
258
+ return Number.isNaN(left) || left === Date.parse(String(outer));
259
+ }
260
+
249
261
  function normalizePins(pins: string | string[] | undefined): string[] {
250
262
  return (Array.isArray(pins) ? pins : pins === undefined ? [] : [pins]).map((pin) => pin.toLowerCase());
251
263
  }
@@ -292,7 +304,13 @@ function checkIdentityAndHeaders(input: SignerBindingInput, certDer: Uint8Array)
292
304
  if (!sameAddressSet(extractAddresses(protectedHeaders?.cc), extractAddresses(outerHeaders["cc"]))) {
293
305
  return "header_mismatch";
294
306
  }
295
- if (input.compareSubject && normalizeSubject(protectedHeaders?.subject) !== normalizeSubject(outerHeaders["subject"])) {
307
+ // A signed-only message's outer Date is unsigned too: a signed message replayed later carries a new one. (An encrypted
308
+ // message's outer Date is checked against its HP-Outer copy instead - see `headerTamperDetected`.)
309
+ if (
310
+ input.compareSubject &&
311
+ (normalizeSubject(protectedHeaders?.subject) !== normalizeSubject(outerHeaders["subject"]) ||
312
+ !sameInstant(protectedHeaders?.date, outerHeaders["date"]))
313
+ ) {
296
314
  return "header_mismatch";
297
315
  }
298
316
  }
@@ -448,14 +466,16 @@ async function evaluateLive(
448
466
  if (!parsed.decrypted) {
449
467
  return { result: { state: "encrypted", decryptError: parsed.unsupportedContentEncryption ? UNSUPPORTED_ENCRYPTION_ERROR : NO_KEY_ERROR }, kind: "encrypted" };
450
468
  }
469
+ // Anyone holding the reader's public certificate can encrypt a message with any protected headers they like, so they
470
+ // are only exposed once a signature over them verified - and a key-changed message keeps them aside for a seal.
451
471
  const content = {
452
472
  ...renderDisplayBody(parsed.displayBody),
453
473
  headerTamperDetected: parsed.headerTamperDetected,
454
474
  subject: parsed.protectedHeaders?.subject || undefined,
455
- ...exposedHeaders(parsed.protectedHeaders),
456
475
  attachments: parsed.attachments,
457
476
  ...addressing(parsed.protectedHeaders),
458
477
  };
478
+ const signedHeaders = exposedHeaders(parsed.protectedHeaders);
459
479
  if (parsed.signatureVerified === undefined) {
460
480
  return { result: { state: "encrypted", ...content }, kind: "encrypted" };
461
481
  }
@@ -464,9 +484,13 @@ async function evaluateLive(
464
484
  }
465
485
  const { failure, trusted, signer, acceptedSigner } = await checkSigner(parsed.signerCertificateDer, parsed.protectedHeaders, parsed.protectedHeaderFields, false);
466
486
  if (failure) {
467
- return { result: { state: "signature_failed", signatureFailureReason: failure, ...content, ...(failure === "signer_key_changed" ? acceptedSigner : signer) }, kind: "encrypted" };
487
+ return {
488
+ result: { state: "signature_failed", signatureFailureReason: failure, ...content, ...(failure === "signer_key_changed" ? acceptedSigner : signer) },
489
+ kind: "encrypted",
490
+ ...(failure === "signer_key_changed" ? { signedContent: signedHeaders } : {}),
491
+ };
468
492
  }
469
- return { result: { state: trusted ? "encrypted_verified" : "encrypted_unverified_signer", ...content, ...acceptedSigner }, kind: "encrypted" };
493
+ return { result: { state: trusted ? "encrypted_verified" : "encrypted_unverified_signer", ...content, ...signedHeaders, ...acceptedSigner }, kind: "encrypted" };
470
494
  }
471
495
 
472
496
  return { result: { state: "unprotected" }, kind: "other" };
@@ -33,6 +33,11 @@ export const DEFAULT_ARGON2ID_PARAMS: Argon2idParams = { memorySize: 65536, iter
33
33
 
34
34
  const ARGON2ID_HASH_LENGTH_BYTES = 32;
35
35
 
36
+ /** The most a `MasterKeyWrap.kdf` label read back from the server may ask this device to spend: 1 GiB (in KiB),
37
+ * 10 passes and 16 lanes - well above every default this system has used, but far from what a hostile or buggy
38
+ * server could name (`m=4294967295`) to freeze the tab or exhaust WebAssembly memory. */
39
+ export const MAX_ARGON2ID_PARAMS: Argon2idParams = { memorySize: 1048576, iterations: 10, parallelism: 16 };
40
+
36
41
  /** Builds the `kdf` string stored alongside a password-method `MasterKeyWrap`, so parameters can be
37
42
  * upgraded over time without breaking existing accounts (each wrap records the parameters it was
38
43
  * actually created with). */
@@ -44,7 +49,8 @@ export function argon2idKdfLabel(params: Argon2idParams): string {
44
49
  * created with, so unlocking always re-derives with the *original* parameters even if
45
50
  * `DEFAULT_ARGON2ID_PARAMS` has since changed (the spec's own "KDF parameters ... MUST be stored
46
51
  * alongside each wrap, so costs can be raised over time without breaking existing accounts"). Returns
47
- * `undefined` for anything that isn't a well-formed `argon2id:m=...,t=...,p=...` label — including a
52
+ * `undefined` for anything that isn't a well-formed `argon2id:m=...,t=...,p=...` label, or whose parameters are
53
+ * zero or beyond `MAX_ARGON2ID_PARAMS` — including a
48
54
  * different KDF entirely (e.g. recovery codes' own `"hkdf-sha256"` label), which callers must not
49
55
  * attempt to unlock as if it were password-derived.
50
56
  */
@@ -53,7 +59,13 @@ export function parseArgon2idKdfLabel(label: string): Argon2idParams | undefined
53
59
  if (!match) {
54
60
  return undefined;
55
61
  }
56
- return { memorySize: Number(match[1]), iterations: Number(match[2]), parallelism: Number(match[3]) };
62
+ const params = { memorySize: Number(match[1]), iterations: Number(match[2]), parallelism: Number(match[3]) };
63
+ const inRange = (value: number, max: number) => value >= 1 && value <= max;
64
+ return inRange(params.memorySize, MAX_ARGON2ID_PARAMS.memorySize) &&
65
+ inRange(params.iterations, MAX_ARGON2ID_PARAMS.iterations) &&
66
+ inRange(params.parallelism, MAX_ARGON2ID_PARAMS.parallelism)
67
+ ? params
68
+ : undefined;
57
69
  }
58
70
 
59
71
  /** A fresh random salt for a new password enrollment. 16 bytes is Argon2id's own recommended minimum. */
@@ -41,6 +41,10 @@ const ID_DATA = pkijs.ContentInfo.DATA;
41
41
 
42
42
  /** AES-GCM content-encryption OIDs (id-aes128-GCM, id-aes192-GCM, id-aes256-GCM - RFC 5084). The only
43
43
  * content encryption `decryptEnvelopedData()` accepts: see `UnsupportedContentEncryptionError`. */
44
+ /** The CMS content type of RFC 5083 `AuthEnvelopedData` (`smime-type="authenveloped-data"`): authenticated, but not the
45
+ * structure `decryptEnvelopedDataWithKeys()` reads - it is reported as unsupported rather than as a missing key. */
46
+ const AUTH_ENVELOPED_DATA_OID = "1.2.840.113549.1.9.16.1.23";
47
+
44
48
  const AEAD_CONTENT_ENCRYPTION_OIDS = new Set(["2.16.840.1.101.3.4.1.6", "2.16.840.1.101.3.4.1.26", "2.16.840.1.101.3.4.1.46"]);
45
49
 
46
50
  /**
@@ -56,7 +60,7 @@ export class UnsupportedContentEncryptionError extends Error {
56
60
  public readonly algorithmOid: string;
57
61
 
58
62
  constructor(algorithmOid: string) {
59
- super(`This message uses unauthenticated content encryption (${algorithmOid}), which is not accepted.`);
63
+ super(`This message uses content encryption (${algorithmOid}) that is not accepted: only AES-GCM EnvelopedData is.`);
60
64
  this.name = "UnsupportedContentEncryptionError";
61
65
  this.algorithmOid = algorithmOid;
62
66
  }
@@ -178,10 +182,12 @@ export function extractCertificateEmails(certDer: Uint8Array): string[] {
178
182
  return [];
179
183
  }
180
184
  const sanEmails: string[] = [];
185
+ let hasSan = false;
181
186
  for (const extension of cert.extensions ?? []) {
182
187
  if (extension.extnID !== "2.5.29.17") {
183
188
  continue;
184
189
  }
190
+ hasSan = true;
185
191
  // pkijs parses a well-formed SubjectAlternativeName into an AltName automatically, and leaves
186
192
  // `parsedValue` unset for a malformed one - a certificate asserting an unreadable SAN names nobody.
187
193
  const altNames = (extension.parsedValue as pkijs.AltName | undefined)?.altNames;
@@ -195,7 +201,9 @@ export function extractCertificateEmails(certDer: Uint8Array): string[] {
195
201
  }
196
202
  }
197
203
  }
198
- if (sanEmails.length > 0) {
204
+ // The SAN is authoritative whenever the certificate has one: DNS names alone name no mailbox, so a subject email
205
+ // address next to them must not stand in for it.
206
+ if (hasSan) {
199
207
  return sanEmails;
200
208
  }
201
209
  const subjectEmails: string[] = [];
@@ -376,6 +384,9 @@ function recipientMatchesCertificate(recipientInfo: pkijs.RecipientInfo, certifi
376
384
  */
377
385
  export async function decryptEnvelopedDataWithKeys(envelopedDer: Uint8Array, keys: DecryptionKey[]): Promise<Uint8Array> {
378
386
  const contentInfo = pkijs.ContentInfo.fromBER(toArrayBuffer(envelopedDer));
387
+ if (contentInfo.contentType === AUTH_ENVELOPED_DATA_OID) {
388
+ throw new UnsupportedContentEncryptionError(AUTH_ENVELOPED_DATA_OID);
389
+ }
379
390
  if (contentInfo.contentType !== pkijs.ContentInfo.ENVELOPED_DATA) {
380
391
  throw new Error("This CMS content is not EnvelopedData.");
381
392
  }
@@ -15,7 +15,7 @@
15
15
  */
16
16
 
17
17
  import { Message, Recipient } from "../mailApi.js";
18
- import { sanitizeQuotedHtml } from "../messageBodySanitizer.js";
18
+ import { sanitizeQuotedHtml, sanitizeSignatureHtml } from "../messageBodySanitizer.js";
19
19
 
20
20
  function escapeHtml(text: string): string {
21
21
  return text
@@ -154,7 +154,8 @@ export function buildComposeBodyHtml(signatureHtml?: string, quotedHtml?: string
154
154
  }
155
155
  const separator = signatureHtml && quotedHtml ? "<p></p>" : "";
156
156
  const aboveQuote = quotedHtml ? "<p></p>" : "";
157
- return `<p></p>${signatureHtml ?? ""}${separator}${aboveQuote}${quotedHtml ?? ""}`;
157
+ // A signature can be written by someone else (a shared mailbox's delegate): it is sanitized like anything else the editor is given.
158
+ return `<p></p>${signatureHtml ? sanitizeSignatureHtml(signatureHtml) : ""}${separator}${aboveQuote}${quotedHtml ?? ""}`;
158
159
  }
159
160
 
160
161
  /**
@@ -13,6 +13,8 @@
13
13
  export const MAX_QUOTED_IMAGE_BYTES = 2_000_000;
14
14
  /** The most picture data a quote carries, in all (as `data:` URI characters). */
15
15
  export const MAX_QUOTED_IMAGES_CHARS = 6_000_000;
16
+ /** How many pictures are fetched at once. */
17
+ const QUOTED_IMAGE_CONCURRENCY = 4;
16
18
 
17
19
  export interface QuotedImageReference {
18
20
  /** The token of a `cid:` reference, absent for an image with no source. */
@@ -22,24 +24,52 @@ export interface QuotedImageReference {
22
24
 
23
25
  /**
24
26
  * `html` with each of its embedded images (`cid:`, or no source at all) replaced by the `data:` URI `resolve` gives for it. An image
25
- * `resolve` has no picture for is left as it is (the quote's sanitizer then drops it); images that already are `data:` URIs and remote
26
- * ones are not asked about. `html` is returned as given when it has no such image, or where there is no DOM to parse it with.
27
+ * `resolve` has no picture for (or fails on) is left as it is (the quote's sanitizer then drops it); images that already are `data:` URIs and
28
+ * remote ones are not asked about. `html` is returned as given when it has no such image, or where there is no DOM to parse it with.
29
+ *
30
+ * The images are resolved a few at a time. When `signal` aborts (the caller's deadline) the wait ends there: the pictures that have arrived
31
+ * are embedded, the rest left as they were, and `resolve` - which is given `signal` to cancel its own request with - is asked for no more.
27
32
  */
28
- export async function embedQuotedImages(html: string, resolve: (reference: QuotedImageReference) => Promise<string | undefined>): Promise<string> {
33
+ export async function embedQuotedImages(
34
+ html: string,
35
+ resolve: (reference: QuotedImageReference, signal?: AbortSignal) => Promise<string | undefined>,
36
+ signal?: AbortSignal,
37
+ ): Promise<string> {
29
38
  if (typeof DOMParser === "undefined" || !/<img\b/i.test(html)) {
30
39
  return html;
31
40
  }
32
41
  const doc = new DOMParser().parseFromString(html, "text/html");
33
- let budget = MAX_QUOTED_IMAGES_CHARS;
34
- let changed = false;
42
+ const targets: { image: Element; reference: QuotedImageReference; uri?: string }[] = [];
35
43
  for (const image of Array.from(doc.querySelectorAll("img"))) {
36
44
  const src = (image.getAttribute("src") ?? "").trim();
37
45
  const cid = /^cid:/i.test(src);
38
- if (!cid && src !== "") {
39
- continue;
46
+ if (cid || src === "") {
47
+ targets.push({ image, reference: { ...(cid ? { cid: src.slice(4) } : {}), alt: image.getAttribute("alt") ?? undefined } });
48
+ }
49
+ }
50
+ let next = 0;
51
+ const worker = async () => {
52
+ while (next < targets.length && !signal?.aborted) {
53
+ const target = targets[next++];
54
+ target.uri = await resolve(target.reference, signal).catch(() => undefined);
40
55
  }
41
- const uri = await resolve({ ...(cid ? { cid: src.slice(4) } : {}), alt: image.getAttribute("alt") ?? undefined });
42
- if (uri !== undefined && uri.length <= budget) {
56
+ };
57
+ let onAbort!: () => void;
58
+ const deadline = new Promise<void>((done) => {
59
+ onAbort = done;
60
+ if (signal?.aborted) {
61
+ done();
62
+ }
63
+ signal?.addEventListener("abort", onAbort, { once: true });
64
+ });
65
+ await Promise.race([Promise.all(Array.from({ length: Math.min(QUOTED_IMAGE_CONCURRENCY, targets.length) }, worker)), deadline]);
66
+ signal?.removeEventListener("abort", onAbort);
67
+ // Whatever has arrived by now is embedded, in document order, for as long as the budget lasts.
68
+ let budget = MAX_QUOTED_IMAGES_CHARS;
69
+ let changed = false;
70
+ for (const { image, uri } of targets) {
71
+ // Only a picture of the original's own is ever put in: whatever a resolver answers, a `src` that is not a `data:` image never gets in.
72
+ if (uri !== undefined && /^data:image\//i.test(uri) && uri.length <= budget) {
43
73
  budget -= uri.length;
44
74
  image.setAttribute("src", uri);
45
75
  changed = true;
@@ -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
 
@@ -31,7 +31,7 @@ export interface Label {
31
31
  * account of a multi-account app) to call instead of the default global `apiFetch()` - see `withClient()`'s
32
32
  * own doc comment in `util/api.ts`. Omitted (the default), every function here behaves exactly as before. */
33
33
  export function listLabels(mailboxUid: string, params: ListParams = {}, client?: ApiClient): Promise<Label[]> {
34
- return withClient(client, `/mail/labels?${buildQuery(params, { mailboxUid })}`);
34
+ return withClient(client, `/mail/labels?${buildQuery({ limit: SETTINGS_LIST_LIMIT, ...params }, { mailboxUid })}`);
35
35
  }
36
36
 
37
37
  export function getLabel(uid: string, client?: ApiClient): Promise<Label> {
@@ -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> {