@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
@@ -26,6 +26,7 @@ import {
26
26
  attachmentContentUrl,
27
27
  createDraft,
28
28
  deleteMessage,
29
+ forgetDraftImages,
29
30
  getMailbox,
30
31
  getMessage,
31
32
  listFolders,
@@ -338,6 +339,13 @@ export default function ComposeWindow({
338
339
  /** Attachment/inline-image uploads on the wire, and who is waiting for them to finish (Send pressed meanwhile). */
339
340
  const uploadsInFlightRef = useRef(0);
340
341
  const uploadWaitersRef = useRef<(() => void)[]>([]);
342
+ /** How many uploads have failed so far: a Send that waited on uploads compares it to see whether one of them did. */
343
+ const uploadFailuresRef = useRef(0);
344
+ /** Set while a Send waits for uploads: Close and Discard call it to cancel that send. */
345
+ const cancelUploadWaitRef = useRef<(() => void) | null>(null);
346
+ /** The draft a plaintext save was last sent for, so a message that turns out to need encryption knows a copy may be on the server. */
347
+ const plaintextRequestedRef = useRef<string | null>(null);
348
+ const [uploadsActive, setUploadsActive] = useState(false);
341
349
  /** The latest save request, if any - each save is chained after the one before it. Always settles (never
342
350
  * rejects), to the saved message, or `undefined` when that save failed. */
343
351
  const saveInFlightRef = useRef<Promise<Message | undefined>>(Promise.resolve(undefined));
@@ -624,6 +632,7 @@ export default function ComposeWindow({
624
632
  const saved = await saveInFlightRef.current;
625
633
  // Once it's out of `supersededDraftsRef` a landing save can't update `superseded.version` any more.
626
634
  const version = saved?.uid === superseded.uid ? saved.version : superseded.version;
635
+ forgetDraftImages(superseded.uid);
627
636
  try {
628
637
  await deleteMessage(superseded.uid, version, client);
629
638
  } catch {
@@ -662,7 +671,8 @@ export default function ComposeWindow({
662
671
  * the old draft is deleted only once the new one has been created. Recipients, subject, and body are
663
672
  * kept. */
664
673
  function handleFromChange(nextMailboxUid: string) {
665
- if (nextMailboxUid === mailboxUid || hasUploads) {
674
+ // An upload still on the wire counts: it would land on the draft this switch supersedes.
675
+ if (nextMailboxUid === mailboxUid || hasUploads || uploadsInFlightRef.current > 0) {
666
676
  return;
667
677
  }
668
678
  if (draft) {
@@ -740,11 +750,12 @@ export default function ComposeWindow({
740
750
  uploadStarted();
741
751
  try {
742
752
  // An inline part with a Content-ID of its own: without one the server has nothing to point the message's `cid:` reference at.
743
- const attachment = await uploadAttachment(draft.uid, file, { inline: true });
753
+ const attachment = await uploadAttachment(draft.uid, file, { inline: true }, client);
744
754
  setHasUploads(true);
745
755
  void refreshDraftVersion(draft.uid);
746
756
  return attachmentContentUrl(attachment.uid);
747
757
  } catch (err) {
758
+ uploadFailuresRef.current += 1;
748
759
  setAttachError(err instanceof ApiRequestError ? err.message : "Could not upload image.");
749
760
  return null;
750
761
  } finally {
@@ -755,10 +766,12 @@ export default function ComposeWindow({
755
766
  /** Send waits for every upload started before it: see `submit()`. */
756
767
  function uploadStarted() {
757
768
  uploadsInFlightRef.current += 1;
769
+ setUploadsActive(true);
758
770
  }
759
771
  function uploadFinished() {
760
772
  uploadsInFlightRef.current -= 1;
761
773
  if (uploadsInFlightRef.current === 0) {
774
+ setUploadsActive(false);
762
775
  for (const resolve of uploadWaitersRef.current.splice(0)) {
763
776
  resolve();
764
777
  }
@@ -796,12 +809,13 @@ export default function ComposeWindow({
796
809
  try {
797
810
  for (const file of files) {
798
811
  try {
799
- const attachment = await uploadAttachment(draft!.uid, file);
812
+ const attachment = await uploadAttachment(draft!.uid, file, {}, client);
800
813
  setHasUploads(true);
801
814
  setAttachments((prev) => [...prev, attachment]);
802
815
  void refreshDraftVersion(draft!.uid);
803
816
  } catch (err) {
804
817
  allAttached = false;
818
+ uploadFailuresRef.current += 1;
805
819
  setAttachError(err instanceof ApiRequestError ? err.message : "Could not upload attachment.");
806
820
  }
807
821
  }
@@ -924,7 +938,25 @@ export default function ComposeWindow({
924
938
  try {
925
939
  if (uploadsInFlightRef.current > 0) {
926
940
  setWaitingForUploads(true);
927
- await new Promise<void>((resolve) => uploadWaitersRef.current.push(resolve));
941
+ const failuresBefore = uploadFailuresRef.current;
942
+ let cancelled = false;
943
+ await new Promise<void>((resolve) => {
944
+ uploadWaitersRef.current.push(resolve);
945
+ cancelUploadWaitRef.current = () => {
946
+ cancelled = true;
947
+ resolve();
948
+ };
949
+ });
950
+ cancelUploadWaitRef.current = null;
951
+ // Close or Discard was pressed meanwhile: the message is not to be sent.
952
+ if (cancelled) {
953
+ return;
954
+ }
955
+ // An attachment the sender asked for did not arrive: say so rather than sending the message without it.
956
+ if (uploadFailuresRef.current > failuresBefore) {
957
+ setSendError("An attachment could not be uploaded, so the message was not sent. Remove it or try again.");
958
+ return;
959
+ }
928
960
  // The editor puts an image it has just uploaded into the message a moment after the upload resolves: let the window catch up.
929
961
  await new Promise((resolve) => setTimeout(resolve, 0));
930
962
  setWaitingForUploads(false);
@@ -1032,6 +1064,7 @@ export default function ComposeWindow({
1032
1064
  setSaveStatus("saved");
1033
1065
  return { message: before };
1034
1066
  }
1067
+ plaintextRequestedRef.current = target.uid;
1035
1068
  try {
1036
1069
  const saved = await assembleDraft(target.uid, {
1037
1070
  to: parseAddresses(current.to),
@@ -1081,6 +1114,7 @@ export default function ComposeWindow({
1081
1114
  const version = Math.max(current.version, saved?.uid === current.uid ? saved.version : 0);
1082
1115
  try {
1083
1116
  await deleteMessage(current.uid, version, client);
1117
+ forgetDraftImages(current.uid);
1084
1118
  } catch (err) {
1085
1119
  if (!(err instanceof ApiRequestError) || (err.status !== 404 && err.status !== 409)) {
1086
1120
  throw err;
@@ -1092,6 +1126,7 @@ export default function ComposeWindow({
1092
1126
  throw getErr;
1093
1127
  });
1094
1128
  if (!fresh) {
1129
+ forgetDraftImages(current.uid);
1095
1130
  return;
1096
1131
  }
1097
1132
  // The version changed because another window sent or scheduled it - that message isn't ours to delete.
@@ -1099,6 +1134,7 @@ export default function ComposeWindow({
1099
1134
  throw new ApiRequestError(NO_LONGER_A_DRAFT_MESSAGE, 409);
1100
1135
  }
1101
1136
  await deleteMessage(fresh.uid, fresh.version, client);
1137
+ forgetDraftImages(current.uid);
1102
1138
  }
1103
1139
  }
1104
1140
 
@@ -1108,6 +1144,7 @@ export default function ComposeWindow({
1108
1144
  * deleted, and a delete that fails keeps the window open with the error.
1109
1145
  */
1110
1146
  async function discardNow() {
1147
+ cancelUploadWaitRef.current?.();
1111
1148
  finishedRef.current = true;
1112
1149
  pendingSaveRef.current = false;
1113
1150
  setClosePrompt(null);
@@ -1147,6 +1184,7 @@ export default function ComposeWindow({
1147
1184
  * *unknown* (settings loading or failed, a lookup pending) is not a reason to hold the window: it is saved like any
1148
1185
  * other message. A failed save keeps the window open with Discard / Keep editing. */
1149
1186
  async function handleClose() {
1187
+ cancelUploadWaitRef.current?.();
1150
1188
  if (!hasUserContent) {
1151
1189
  void discardNow();
1152
1190
  return;
@@ -1246,10 +1284,13 @@ export default function ComposeWindow({
1246
1284
  * uid, so attachments and the folder are undisturbed), chained after any save still on the wire. Best effort - a failure is reported once. */
1247
1285
  function scrubSavedDraft(target: Message) {
1248
1286
  lastSavedRef.current = null;
1287
+ plaintextRequestedRef.current = null;
1249
1288
  setSaveStatus("idle");
1250
1289
  const previous = saveInFlightRef.current;
1251
1290
  const scrub = (async (): Promise<Message | undefined> => {
1252
1291
  await previous;
1292
+ // A save that was on the wire has landed by now and recorded itself as the last one saved - which the scrub just replaced.
1293
+ lastSavedRef.current = null;
1253
1294
  try {
1254
1295
  // Its answer (the new version) is what a later delete waits for through `saveInFlightRef`, like any save's.
1255
1296
  return await assembleDraft(target.uid, { to: [], cc: [], bcc: [], subject: "", html: "" }, client);
@@ -1267,7 +1308,7 @@ export default function ComposeWindow({
1267
1308
  }
1268
1309
  // Only a message that was really saved (`lastSavedRef` names its draft) has a plaintext copy to replace.
1269
1310
  useEffect(() => {
1270
- if (encryptionDecided && draft && lastSavedRef.current?.startsWith(`${draft.uid}:`)) {
1311
+ if (encryptionDecided && draft && (lastSavedRef.current?.startsWith(`${draft.uid}:`) || plaintextRequestedRef.current === draft.uid)) {
1271
1312
  scrubSavedDraft(draft);
1272
1313
  }
1273
1314
  }, [encryptionDecided, draft?.uid]);
@@ -1397,6 +1438,9 @@ export default function ComposeWindow({
1397
1438
  // isn't mistaken for an edit (and autosaved) just because the editor was focused or clicked.
1398
1439
  /** The quote has been added: unless the reader has typed something already, that is still exactly what compose seeded. */
1399
1440
  function handleQuoteAppended(normalized: string) {
1441
+ // Added for good: an editor mounted again (after a minimize and restore) is seeded from the body that already holds the quote, and
1442
+ // must not be handed it a second time.
1443
+ setSeededWithoutQuote(false);
1400
1444
  const latest = latestRef.current;
1401
1445
  if (latest.html === latest.seededHtml) {
1402
1446
  setHtml(normalized);
@@ -1422,8 +1466,10 @@ export default function ComposeWindow({
1422
1466
  // dispatcher the focus is in a compose window at all). They do what the Send and Close buttons and autosave do, under the same conditions -
1423
1467
  // a shortcut pressed while its button would be disabled is consumed and does nothing.
1424
1468
  // Send never waits for the encryption settings to load (fail open, see `encryptionRequirement.ts`); it only needs the draft to exist. Pressed
1425
- // while an earlier press is still being handled it does nothing (`submit()` guards that itself).
1426
- const canSend = !!draft && !closing && !waitingForUploads;
1469
+ // while an earlier press is still being handled it does nothing (`submit()` guards that itself). A reply or forward waits for its quoted original
1470
+ // (at most `QUOTE_FETCH_TIMEOUT_MS`): sent sooner it would go without the quote, a forward without its content, a Reply All to the sender alone.
1471
+ // Nor before the editor is there (it waits for the signature): sent sooner a reply would go without its quote and signature.
1472
+ const canSend = !!draft && contentReady && !closing && !waitingForUploads && !quotePending;
1427
1473
  useShortcut(SHORTCUTS.compose.send, () => void (canSend && submit(false)), { container: windowRef });
1428
1474
  useShortcut(
1429
1475
  SHORTCUTS.compose.saveDraft,
@@ -1595,8 +1641,8 @@ export default function ComposeWindow({
1595
1641
  // Two or more options only exist once the mailboxes have loaded, and the same answer that lets them list
1596
1642
  // (one that isn't known to be view-only) is what picks the default sender, so a sender is always chosen by now.
1597
1643
  value={mailboxUid}
1598
- disabled={hasUploads}
1599
- title={hasUploads ? "The sender can't be changed after adding attachments or images." : undefined}
1644
+ disabled={hasUploads || uploadsActive}
1645
+ title={hasUploads || uploadsActive ? "The sender can't be changed after adding attachments or images." : undefined}
1600
1646
  onChange={(e) => handleFromChange(e.target.value)}
1601
1647
  >
1602
1648
  {fromOptions.map((mb) => (
@@ -1802,7 +1848,7 @@ export default function ComposeWindow({
1802
1848
  onClick={() => void submit(false)}
1803
1849
  title={withHint("Send", SHORTCUTS.compose.send, keyEnv)}
1804
1850
  aria-keyshortcuts={ariaKeyShortcuts(SHORTCUTS.compose.send, keyEnv)}
1805
- disabled={!draft || closing || waitingForUploads}
1851
+ disabled={!canSend}
1806
1852
  className="py-1.5 pl-5 pr-3 font-semibold text-sm hover:not-disabled:bg-primary-dark disabled:opacity-55 disabled:cursor-not-allowed"
1807
1853
  >
1808
1854
  Send
@@ -1813,7 +1859,7 @@ export default function ComposeWindow({
1813
1859
  aria-label="Send later"
1814
1860
  aria-haspopup="true"
1815
1861
  aria-expanded={schedulePickerOpen}
1816
- disabled={!draft || closing || waitingForUploads}
1862
+ disabled={!canSend}
1817
1863
  onClick={() => setSchedulePickerOpen((o) => !o)}
1818
1864
  className="py-1.5 px-2 border-l border-white/30 hover:not-disabled:bg-primary-dark disabled:opacity-55 disabled:cursor-not-allowed"
1819
1865
  >
@@ -32,13 +32,19 @@ export default function GifPicker({ anchorRef, onSelect, onClose }: GifPickerPro
32
32
  useEffect(() => {
33
33
  setLoading(true);
34
34
  setError(null);
35
+ // A search that was still on the wire when the query changed (or the popup closed) is not heard: a slow answer to an earlier query
36
+ // must not replace the results of the one now shown.
37
+ let stale = false;
35
38
  const handle = setTimeout(() => {
36
39
  searchGifs(query)
37
- .then(setGifs)
38
- .catch((err) => setError(err instanceof ApiRequestError ? err.message : "Could not load GIFs."))
39
- .finally(() => setLoading(false));
40
+ .then((found) => !stale && setGifs(found))
41
+ .catch((err) => !stale && setError(err instanceof ApiRequestError ? err.message : "Could not load GIFs."))
42
+ .finally(() => !stale && setLoading(false));
40
43
  }, SEARCH_DEBOUNCE_MS);
41
- return () => clearTimeout(handle);
44
+ return () => {
45
+ stale = true;
46
+ clearTimeout(handle);
47
+ };
42
48
  }, [query]);
43
49
 
44
50
  return (
@@ -125,7 +125,8 @@ export default function RecipientInput({
125
125
  dropdownZIndex,
126
126
  }: RecipientInputProps) {
127
127
  const [pending, setPending] = useState(initialPending);
128
- const [suggestions, setSuggestions] = useState<RecipientSuggestion[]>([]);
128
+ // With the query they were loaded for: a list for earlier text is not offered (Enter or Tab would put somebody else in place of what was typed).
129
+ const [suggestionList, setSuggestionList] = useState<{ query: string; items: RecipientSuggestion[] }>({ query: "", items: [] });
129
130
  const [open, setOpen] = useState(false);
130
131
  const [activeIndex, setActiveIndex] = useState(0);
131
132
  const [position, setPosition] = useState<DropdownPosition | null>(null);
@@ -153,13 +154,13 @@ export default function RecipientInput({
153
154
  }, [pending, pendingActive]);
154
155
 
155
156
  const committedAddresses = new Set(chips.map((chip) => parseRecipient(chip).address.toLowerCase()));
156
- const visible = suggestions.filter((suggestion) => !committedAddresses.has(suggestion.address.toLowerCase()));
157
+ const visible = (suggestionList.query === query ? suggestionList.items : []).filter((suggestion) => !committedAddresses.has(suggestion.address.toLowerCase()));
157
158
  const expanded = open && visible.length > 0 && query.length >= RECIPIENT_SUGGESTION_MIN_QUERY_LENGTH;
158
159
  const active = Math.min(activeIndex, visible.length - 1);
159
160
 
160
161
  useEffect(() => {
161
162
  if (query.length < RECIPIENT_SUGGESTION_MIN_QUERY_LENGTH) {
162
- setSuggestions([]);
163
+ setSuggestionList({ query: "", items: [] });
163
164
  return;
164
165
  }
165
166
  const controller = new AbortController();
@@ -167,13 +168,13 @@ export default function RecipientInput({
167
168
  fetchRef.current(query, { mailboxUid, signal: controller.signal }).then(
168
169
  (result) => {
169
170
  if (!controller.signal.aborted) {
170
- setSuggestions(result);
171
+ setSuggestionList({ query, items: result });
171
172
  setActiveIndex(0);
172
173
  }
173
174
  },
174
175
  () => {
175
176
  if (!controller.signal.aborted) {
176
- setSuggestions([]);
177
+ setSuggestionList({ query: "", items: [] });
177
178
  }
178
179
  },
179
180
  );
@@ -233,7 +234,7 @@ export default function RecipientInput({
233
234
  const next = committed.join(", ");
234
235
  setPending("");
235
236
  setOpen(false);
236
- setSuggestions([]);
237
+ setSuggestionList({ query: "", items: [] });
237
238
  onChange(next);
238
239
  onEdit?.(committed, "");
239
240
  onCommit?.(next);
@@ -311,7 +312,8 @@ export default function RecipientInput({
311
312
  {drawn.map(({ chip, index }) => {
312
313
  const recipient = parseRecipient(chip);
313
314
  const valid = isValidRecipientAddress(recipient.address);
314
- const name = recipient.displayName || recipient.address;
315
+ // A name that is itself an address (`"billing@bank.com" <x@evil.tld>`) would hide the one it is really sent to: both are shown.
316
+ const name = /[@@﹫]/.test(recipient.displayName ?? "") ? `${recipient.displayName} <${recipient.address}>` : recipient.displayName || recipient.address;
315
317
  return (
316
318
  <li
317
319
  key={`${index}-${chip}`}
@@ -8,16 +8,29 @@
8
8
  export type ComposeFlush = () => Promise<unknown>;
9
9
 
10
10
  const flushers = new Set<ComposeFlush>();
11
+ const signOutCleanups = new Set<() => void>();
11
12
  let signingOut = false;
12
13
 
14
+ /**
15
+ * Registers something to run the moment the app starts signing out (explicitly, or forced by another tab): a cache that holds message content,
16
+ * which must not outlive the session it was read in. Called once, when the module that owns the cache is loaded.
17
+ */
18
+ export function onSigningOut(cleanup: () => void): void {
19
+ signOutCleanups.add(cleanup);
20
+ }
21
+
13
22
  /**
14
23
  * Marks the app as signing out (explicitly, or forced by a sign-out in another tab), so compose windows stop
15
24
  * asking the browser to confirm leaving the page - the "Leave site?" prompt would otherwise let the user cancel
16
25
  * the sign-out's own navigation and stay on a page whose session is being ended. Call it before flushing drafts
17
- * and navigating. `clearSigningOut()` undoes it (a sign-out that didn't leave the page after all).
26
+ * and navigating. `clearSigningOut()` undoes it (a sign-out that didn't leave the page after all). It also runs what was registered
27
+ * with `onSigningOut()`.
18
28
  */
19
29
  export function markSigningOut(): void {
20
30
  signingOut = true;
31
+ for (const cleanup of signOutCleanups) {
32
+ cleanup();
33
+ }
21
34
  }
22
35
 
23
36
  /** Undoes `markSigningOut()`. */
@@ -23,6 +23,11 @@ import type { EncryptionPolicy } from "../../../../../lib/crypto/keyvaultApi.js"
23
23
  * reported in `unknown`, so a caller that is about to send can give the loads a moment - and only a caller that finds out later that the
24
24
  * message *does* need encryption acts on it (see `ComposeWindow`'s transition rules).
25
25
  *
26
+ * That is this function's answer, and it stays fail open for everything that is about a draft or the compose window. The background send
27
+ * (`sendDecision.ts`'s `decideSend()` with `lookupsComplete`) reads `unknown` once its loads are in: a sender who has an encryption key, and
28
+ * whose policy or recipients' keys could not be checked, is blocked with the `lookup-failed` decision (the sender may still send without
29
+ * encryption) rather than sent in the clear on a guess.
30
+ *
26
31
  * Why this is safe enough: the requirement only ever protects *this client's* plaintext drafts and sends from being unencrypted when the
27
32
  * server's policy says they should be; it is not the enforcement point (the keys are the user's, the server never sees them). A missing
28
33
  * policy therefore does not mean "encrypt or refuse" - it means the client cannot tell, and the honest default is the one that works.
@@ -9,8 +9,18 @@ import { MAX_QUOTED_IMAGE_BYTES, QuotedImageReference, embedQuotedImages } from
9
9
  import { EMBEDDED_IMAGE_TYPES, findInlineAttachment, toDataUri } from "../../../../../lib/mail/inlineImages.js";
10
10
  import type { MessageSecurityResult } from "../../../../../lib/crypto/messageSecurity.js";
11
11
  import { MimeAttachment, decodeHeaderText, extractDisplayBody, parseMimeEntity } from "../../../../../lib/crypto/mime.js";
12
+ import { onSigningOut } from "./composeFlushRegistry.js";
12
13
  import { parseRecipientList } from "./recipients.js";
13
14
 
15
+ /** The security states whose `protectedHeaders` a signature covers - the only ones Reply All takes recipients from. */
16
+ const TRUSTED_HEADER_STATES = new Set<MessageSecurityResult["state"]>([
17
+ "signed_verified",
18
+ "encrypted_verified",
19
+ "signed_unverified_signer",
20
+ "encrypted_unverified_signer",
21
+ "verified_at_first_open",
22
+ ]);
23
+
14
24
  /** What a Reply/Reply All/Forward needs from the message being answered, beyond what its `Message` record holds. */
15
25
  export interface OriginalMessage {
16
26
  /** The body to quote. Empty when none could be loaded - the quote builders then fall back to `bodyPreview`. */
@@ -26,6 +36,9 @@ export const QUOTE_FETCH_TIMEOUT_MS = 10_000;
26
36
  /** How long a fetched (or prefetched) body is remembered, so hovering Reply and then clicking it costs one request. */
27
37
  export const QUOTE_CACHE_MS = 30_000;
28
38
 
39
+ /** How many bodies are remembered at most: each is a whole message body, and a mailbox read for hours would otherwise keep every one it was asked about. */
40
+ const QUOTE_CACHE_MAX = 20;
41
+
29
42
  const contentCache = new Map<string, { at: number; promise: Promise<string | undefined> }>();
30
43
 
31
44
  /**
@@ -50,6 +63,12 @@ function fetchContentHtml(uid: string): Promise<string | undefined> {
50
63
  clearTimeout(timer);
51
64
  }
52
65
  })();
66
+ // Whatever has expired goes, and the oldest of the rest once there are too many.
67
+ for (const [key, entry] of contentCache) {
68
+ if (Date.now() - entry.at >= QUOTE_CACHE_MS || contentCache.size >= QUOTE_CACHE_MAX) {
69
+ contentCache.delete(key);
70
+ }
71
+ }
53
72
  contentCache.set(uid, { at: Date.now(), promise });
54
73
  void promise.then((html) => {
55
74
  if (html === undefined && contentCache.get(uid)?.promise === promise) {
@@ -83,6 +102,7 @@ export function prefetchOriginalMessage(message: Message): void {
83
102
  export function clearOriginalMessageCache(): void {
84
103
  contentCache.clear();
85
104
  }
105
+ onSigningOut(clearOriginalMessageCache);
86
106
 
87
107
  /** The recipients one address-list header names, RFC 2047-decoded, with their display names. */
88
108
  function headerRecipients(value: string | undefined, type: "to" | "cc"): Recipient[] {
@@ -96,9 +116,9 @@ function headerRecipients(value: string | undefined, type: "to" | "cc"): Recipie
96
116
 
97
117
  /** The `data:` URI of the picture a quoted image of `message` stands for - one of the message's own attachments, fetched with the session -
98
118
  * or `undefined` when there is none, it is not a raster image, it is too large or it could not be had. Never rejects. */
99
- function serverImageResolver(message: Message): (reference: QuotedImageReference) => Promise<string | undefined> {
119
+ function serverImageResolver(message: Message): (reference: QuotedImageReference, signal?: AbortSignal) => Promise<string | undefined> {
100
120
  let attachments: Promise<Awaited<ReturnType<typeof listAttachments>>> | undefined;
101
- return async (reference) => {
121
+ return async (reference, signal) => {
102
122
  try {
103
123
  attachments ??= listAttachments(message.folderUid, message.uid);
104
124
  const attachment = findInlineAttachment(await attachments, reference);
@@ -106,7 +126,7 @@ function serverImageResolver(message: Message): (reference: QuotedImageReference
106
126
  if (!attachment || !type || !EMBEDDED_IMAGE_TYPES.test(type) || attachment.sizeBytes > MAX_QUOTED_IMAGE_BYTES) {
107
127
  return undefined;
108
128
  }
109
- const res = await fetch(attachmentContentUrl(attachment.uid), { credentials: "include" });
129
+ const res = await fetch(attachmentContentUrl(attachment.uid), { credentials: "include", signal });
110
130
  const bytes = res.ok ? new Uint8Array(await res.arrayBuffer()) : undefined;
111
131
  return bytes && bytes.length <= MAX_QUOTED_IMAGE_BYTES ? toDataUri(type, bytes) : undefined;
112
132
  } catch {
@@ -159,7 +179,9 @@ export async function loadOriginalMessage(
159
179
  ): Promise<OriginalMessage> {
160
180
  const recovered: QuotedBody | undefined =
161
181
  security?.text !== undefined ? { text: security.text } : security?.html !== undefined ? { html: security.html } : undefined;
162
- const protectedHeaders = security?.protectedHeaders;
182
+ // Headers inside an unsigned (or badly signed) encrypted message are whatever its encrypter chose to write, so only a
183
+ // verified signature makes them a source of recipients.
184
+ const protectedHeaders = security && TRUSTED_HEADER_STATES.has(security.state) ? security.protectedHeaders : undefined;
163
185
  const protectedRecipients = protectedHeaders
164
186
  ? [...headerRecipients(protectedHeaders.to, "to"), ...headerRecipients(protectedHeaders.cc, "cc")]
165
187
  : [];
@@ -197,8 +219,11 @@ export async function loadOriginalMessage(
197
219
  if (result.body.html !== undefined) {
198
220
  const resolver = recovered ? partImageResolver(security?.attachments ?? []) : serverImageResolver(message);
199
221
  const html = result.body.html;
200
- // Bounded like the body's own request: a picture that will not arrive is left out of the quote, not waited for.
201
- result.body = { html: await withTimeout(embedQuotedImages(html, resolver), QUOTE_FETCH_TIMEOUT_MS).catch(() => html) };
222
+ // Bounded like the body's own request: a picture that has not arrived by then is cancelled and left out of the quote, not waited for -
223
+ // the ones that have arrived stay.
224
+ const controller = new AbortController();
225
+ const timer = setTimeout(() => controller.abort(), QUOTE_FETCH_TIMEOUT_MS);
226
+ result.body = { html: await embedQuotedImages(html, resolver, controller.signal).finally(() => clearTimeout(timer)) };
202
227
  }
203
228
  return result;
204
229
  }
@@ -338,7 +338,7 @@ export default function MailShell({
338
338
  // `mailboxes.length` directly (not `!mailboxUid`) since `mailboxUid` is legitimately undefined in
339
339
  // aggregate mode even with mailboxes present.
340
340
  if (userUid && status === "ready" && mailboxes.length === 0) {
341
- return <MailboxProvisioning />;
341
+ return <MailboxProvisioning impersonating={impersonating} impersonationBaseUrl={impersonationBaseUrl} />;
342
342
  }
343
343
 
344
344
  let inner: ReactNode = null;
@@ -36,7 +36,8 @@ export interface MessageBodyProps {
36
36
  onAdaptable?: (adaptable: boolean) => void;
37
37
  }
38
38
 
39
- type Remote = { status: "loading" } | { status: "ready"; content: BodyContent } | { status: "error"; message: string };
39
+ /** `uid` is the message a ready body is of, so a later fetch for the same message (its version moved on) can leave what is shown in place. */
40
+ type Remote = { status: "loading" } | { status: "ready"; content: BodyContent; uid: string } | { status: "error"; message: string };
40
41
 
41
42
  /** Placeholder lines shown where the body will be, in the body's own place, until it has arrived and been laid out. */
42
43
  export function BodySkeleton() {
@@ -73,7 +74,7 @@ const AUTHORED_CANVAS = "rgb(255, 255, 255)";
73
74
  export default function MessageBody({ messageUid, messageVersion, title, content, attachments, inlineParts, original = false, onAdaptable }: MessageBodyProps) {
74
75
  const [remote, setRemote] = useState<Remote>(() => {
75
76
  const cached = cachedBodyContent(messageUid, messageVersion);
76
- return cached ? { status: "ready", content: cached } : { status: "loading" };
77
+ return cached ? { status: "ready", content: cached, uid: messageUid } : { status: "loading" };
77
78
  });
78
79
  const [attempt, setAttempt] = useState(0);
79
80
  const supplied = content !== undefined;
@@ -84,15 +85,20 @@ export default function MessageBody({ messageUid, messageVersion, title, content
84
85
  }
85
86
  const cached = cachedBodyContent(messageUid, messageVersion);
86
87
  if (cached) {
87
- setRemote({ status: "ready", content: cached });
88
+ setRemote({ status: "ready", content: cached, uid: messageUid });
88
89
  return;
89
90
  }
90
91
  const controller = new AbortController();
91
- setRemote({ status: "loading" });
92
+ // The same message under a newer version (opening it marked it read, a flag or a label changed): what is shown stays - no skeleton, and
93
+ // the frame is not torn down and laid out again - until the fresh copy is here, and a failed read leaves it too.
94
+ const revalidating = remote.status === "ready" && remote.uid === messageUid;
95
+ if (!revalidating) {
96
+ setRemote({ status: "loading" });
97
+ }
92
98
  fetchBodyContent(messageUid, messageVersion, controller.signal).then(
93
- (loaded) => setRemote({ status: "ready", content: loaded }),
99
+ (loaded) => setRemote({ status: "ready", content: loaded, uid: messageUid }),
94
100
  (err) => {
95
- if (!controller.signal.aborted) {
101
+ if (!controller.signal.aborted && !revalidating) {
96
102
  setRemote({ status: "error", message: err instanceof ApiRequestError ? err.message : "Could not load this message." });
97
103
  }
98
104
  },
@@ -100,7 +106,7 @@ export default function MessageBody({ messageUid, messageVersion, title, content
100
106
  return () => controller.abort();
101
107
  }, [messageUid, messageVersion, supplied, attempt]);
102
108
 
103
- const shown: Remote = content ? { status: "ready", content } : remote;
109
+ const shown: Remote = content ? { status: "ready", content, uid: messageUid } : remote;
104
110
  if (shown.status === "loading") {
105
111
  return <BodySkeleton />;
106
112
  }
@@ -6,6 +6,7 @@ import { ApiRequestError, apiUrl } from "../../../../../lib/util/api.js";
6
6
  import { Attachment, attachmentContentUrl } from "../../../../../lib/mail/mailApi.js";
7
7
  import type { MimeAttachment } from "../../../../../lib/crypto/mime.js";
8
8
  import { bytesToBase64, findInlineAttachment, toDataUri } from "../../../../../lib/mail/inlineImages.js";
9
+ import { onSigningOut } from "../compose/composeFlushRegistry.js";
9
10
 
10
11
  /** What a message's body is: server-sanitized HTML, or plain text (a message with no HTML part, or the server's plain-text fallback). */
11
12
  export type BodyContent = { kind: "html"; html: string } | { kind: "text"; text: string };
@@ -20,6 +21,7 @@ const cache = new Map<string, BodyContent>();
20
21
  export function clearBodyContentCache(): void {
21
22
  cache.clear();
22
23
  }
24
+ onSigningOut(clearBodyContentCache);
23
25
 
24
26
  /** The remembered body for a message at a version, if any. */
25
27
  export function cachedBodyContent(uid: string, version: number): BodyContent | undefined {
@@ -57,11 +57,34 @@ const STYLE_HAZARDS: [RegExp, string][] = [
57
57
  [/@import[^;]*;?/gi, ""],
58
58
  ];
59
59
 
60
- /** CSS with the constructs above neutralised. */
61
- export function sanitizeCss(css: string): string {
60
+ function neutraliseHazards(css: string): string {
62
61
  return STYLE_HAZARDS.reduce((text, [pattern, replacement]) => text.replace(pattern, replacement), css);
63
62
  }
64
63
 
64
+ /** `css` as a browser reads it: comments gone, and every escape resolved - repeatedly, since a decoded backslash can start another (`\5c 76h`). */
65
+ function readAsBrowser(css: string): string {
66
+ let text = css;
67
+ for (let pass = 0; pass < 3; pass++) {
68
+ const next = unescapeCss(text.replace(/\/\*[\s\S]*?\*\//g, " "));
69
+ if (next === text) {
70
+ break;
71
+ }
72
+ text = next;
73
+ }
74
+ return text;
75
+ }
76
+
77
+ /**
78
+ * CSS with the constructs above neutralised. The patterns run on what a browser would read, not only on the raw text: `position:\66ixed` and
79
+ * `min-height:200\76h` are `position:fixed` and `200vh` to it, and a comment inside a name (`position/**\/:fixed`) is no obstacle either. When the
80
+ * decoded text holds one, that text (escapes resolved, comments dropped) is what is kept, neutralised; CSS whose escapes hide nothing is left as written.
81
+ */
82
+ export function sanitizeCss(css: string): string {
83
+ const decoded = readAsBrowser(css);
84
+ const decodedClean = neutraliseHazards(decoded);
85
+ return decodedClean !== decoded ? decodedClean : neutraliseHazards(css);
86
+ }
87
+
65
88
  /** Elements removed together with everything inside them. */
66
89
  const REMOVED_ELEMENTS = new Set([
67
90
  "script",
@@ -22,6 +22,7 @@ import type { Mailbox } from "../../../../lib/mail/mailApi.js";
22
22
  import { getMyMailboxAccess } from "../../../../lib/mail/mailboxAccessApi.js";
23
23
  import type { ApiClient } from "../../../../lib/util/api.js";
24
24
  import { useApiClient } from "../../../../lib/util/apiClientContext.js";
25
+ import { onSigningOut } from "./compose/composeFlushRegistry.js";
25
26
 
26
27
  /** `true` writable, `false` known view-only, `undefined` couldn't tell (treated as writable). */
27
28
  export type MailboxWritability = boolean | undefined;
@@ -34,6 +35,7 @@ export function clearMailboxWritabilityCache(): void {
34
35
  inFlight.clear();
35
36
  settled.clear();
36
37
  }
38
+ onSigningOut(clearMailboxWritabilityCache);
37
39
 
38
40
  function isImplicitlyWritable(mailbox: Mailbox, userUid: string | undefined, trusted: boolean | undefined): boolean {
39
41
  return !!trusted || (!!userUid && mailbox.ownerUserUid === userUid);
@@ -87,6 +87,8 @@ export default function RuleBuilder<C extends object, A extends { type: string }
87
87
  }: RuleBuilderProps<C, A>) {
88
88
  const [draftText, setDraftText] = useState<Record<string, string>>({});
89
89
  const [newActionType, setNewActionType] = useState(actionTypes[0]?.value ?? "");
90
+ // The sequence field's text while it is being edited (it may be empty, which is no number); `null` shows the rule's own.
91
+ const [sequenceDraft, setSequenceDraft] = useState<string | null>(null);
90
92
  const conditions = value.conditions as Record<string, unknown>;
91
93
 
92
94
  function patch(next: Partial<RuleBuilderValue<C, A>>) {
@@ -300,8 +302,15 @@ export default function RuleBuilder<C extends object, A extends { type: string }
300
302
  <input
301
303
  type="number"
302
304
  className="w-20 text-sm py-1.5 px-2 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary"
303
- value={value.sequence}
304
- onChange={(e) => patch({ sequence: Number(e.target.value) })}
305
+ value={sequenceDraft ?? value.sequence}
306
+ onChange={(e) => {
307
+ // What is typed is kept as typed, so the field can be cleared and typed afresh; the rule only takes a number.
308
+ setSequenceDraft(e.target.value);
309
+ if (e.target.value !== "") {
310
+ patch({ sequence: Number(e.target.value) });
311
+ }
312
+ }}
313
+ onBlur={() => setSequenceDraft(null)}
305
314
  />
306
315
  </label>
307
316
  </div>