@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
@@ -1,5 +1,8 @@
1
1
  import React from "react";
2
2
  import type { Editor } from "@tiptap/react";
3
+ /** What a typed link target is stored as. One without a scheme (`example.com`) would be a relative link, which readers' clients drop as dead text: a bare
4
+ * email address becomes `mailto:`, anything else that is not a path or an anchor `https://`. A target with a scheme is kept as it is. */
5
+ export declare function linkHref(typed: string): string;
3
6
  export interface ComposeToolbarProps {
4
7
  /** `null` before the editor has mounted client-side (see `RichTextEditor`'s `immediatelyRender: false`
5
8
  * doc comment) — every button is disabled in that state rather than the toolbar rendering nothing, so
@@ -6,6 +6,15 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
6
6
  import { Suspense, lazy, useRef, useState } from "react";
7
7
  import { BsArrowClockwise, BsArrowCounterclockwise, BsEraser, BsHighlighter, BsImage, BsIndent, BsJustify, BsLink45Deg, BsListOl, BsListUl, BsPaletteFill, BsTable, BsTextCenter, BsTextLeft, BsTextRight, BsTypeBold, BsTypeItalic, BsTypeStrikethrough, BsTypeUnderline, BsUnindent, } from "react-icons/bs";
8
8
  import GifPicker from "./GifPicker.js";
9
+ /** What a typed link target is stored as. One without a scheme (`example.com`) would be a relative link, which readers' clients drop as dead text: a bare
10
+ * email address becomes `mailto:`, anything else that is not a path or an anchor `https://`. A target with a scheme is kept as it is. */
11
+ export function linkHref(typed) {
12
+ const target = typed.trim();
13
+ if (/^[a-z][a-z0-9+.-]*:/i.test(target) || /^[/#]/.test(target)) {
14
+ return target;
15
+ }
16
+ return /^[^\s@/]+@[^\s@/]+\.[^\s@/]+$/.test(target) ? `mailto:${target}` : `https://${target}`;
17
+ }
9
18
  /** Loaded when the picker is first opened: the emoji list is a half-megabyte JSON file that nothing else needs. */
10
19
  const EmojiPicker = lazy(() => import("./EmojiPicker.js"));
11
20
  const FONT_FAMILIES = [
@@ -73,7 +82,7 @@ export default function ComposeToolbar({ editor, onUploadImage }) {
73
82
  run((ed) => ed.chain().focus().extendMarkRange("link").unsetLink().run());
74
83
  }
75
84
  else {
76
- run((ed) => ed.chain().focus().extendMarkRange("link").setLink({ href: linkUrl.trim() }).run());
85
+ run((ed) => ed.chain().focus().extendMarkRange("link").setLink({ href: linkHref(linkUrl) }).run());
77
86
  }
78
87
  setOpenPopup(null);
79
88
  }
@@ -6,7 +6,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
6
6
  import { useEffect, useRef, useState } from "react";
7
7
  import { HiOutlineArrowsPointingIn, HiOutlineArrowsPointingOut, HiOutlineArrowTopRightOnSquare, HiOutlineChevronDown, HiOutlineIdentification, HiOutlineLockClosed, HiOutlineMinus, HiOutlinePaperClip, HiOutlineTrash, HiOutlineXMark, } from "react-icons/hi2";
8
8
  import { ApiRequestError } from "../../../../../lib/util/api.js";
9
- import { assembleDraft, attachmentContentUrl, createDraft, deleteMessage, getMailbox, getMessage, listFolders, listMailboxes, deleteAttachment, uploadAttachment, } from "../../../../../lib/mail/mailApi.js";
9
+ import { assembleDraft, attachmentContentUrl, createDraft, deleteMessage, forgetDraftImages, getMailbox, getMessage, listFolders, listMailboxes, deleteAttachment, uploadAttachment, } from "../../../../../lib/mail/mailApi.js";
10
10
  import { listMailSignatures } from "../../../../../lib/mail/mailSignaturesApi.js";
11
11
  import { buildComposeBodyHtml } from "../../../../../lib/mail/compose/composeQuoting.js";
12
12
  import { peekMailboxWritability, useMailboxWritability } from "../writableMailboxes.js";
@@ -223,6 +223,13 @@ export default function ComposeWindow({ session, onClose, onToggleMinimize, inli
223
223
  /** Attachment/inline-image uploads on the wire, and who is waiting for them to finish (Send pressed meanwhile). */
224
224
  const uploadsInFlightRef = useRef(0);
225
225
  const uploadWaitersRef = useRef([]);
226
+ /** How many uploads have failed so far: a Send that waited on uploads compares it to see whether one of them did. */
227
+ const uploadFailuresRef = useRef(0);
228
+ /** Set while a Send waits for uploads: Close and Discard call it to cancel that send. */
229
+ const cancelUploadWaitRef = useRef(null);
230
+ /** 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. */
231
+ const plaintextRequestedRef = useRef(null);
232
+ const [uploadsActive, setUploadsActive] = useState(false);
226
233
  /** The latest save request, if any - each save is chained after the one before it. Always settles (never
227
234
  * rejects), to the saved message, or `undefined` when that save failed. */
228
235
  const saveInFlightRef = useRef(Promise.resolve(undefined));
@@ -479,6 +486,7 @@ export default function ComposeWindow({ session, onClose, onToggleMinimize, inli
479
486
  const saved = await saveInFlightRef.current;
480
487
  // Once it's out of `supersededDraftsRef` a landing save can't update `superseded.version` any more.
481
488
  const version = saved?.uid === superseded.uid ? saved.version : superseded.version;
489
+ forgetDraftImages(superseded.uid);
482
490
  try {
483
491
  await deleteMessage(superseded.uid, version, client);
484
492
  }
@@ -516,7 +524,8 @@ export default function ComposeWindow({ session, onClose, onToggleMinimize, inli
516
524
  * the old draft is deleted only once the new one has been created. Recipients, subject, and body are
517
525
  * kept. */
518
526
  function handleFromChange(nextMailboxUid) {
519
- if (nextMailboxUid === mailboxUid || hasUploads) {
527
+ // An upload still on the wire counts: it would land on the draft this switch supersedes.
528
+ if (nextMailboxUid === mailboxUid || hasUploads || uploadsInFlightRef.current > 0) {
520
529
  return;
521
530
  }
522
531
  if (draft) {
@@ -590,12 +599,13 @@ export default function ComposeWindow({ session, onClose, onToggleMinimize, inli
590
599
  uploadStarted();
591
600
  try {
592
601
  // An inline part with a Content-ID of its own: without one the server has nothing to point the message's `cid:` reference at.
593
- const attachment = await uploadAttachment(draft.uid, file, { inline: true });
602
+ const attachment = await uploadAttachment(draft.uid, file, { inline: true }, client);
594
603
  setHasUploads(true);
595
604
  void refreshDraftVersion(draft.uid);
596
605
  return attachmentContentUrl(attachment.uid);
597
606
  }
598
607
  catch (err) {
608
+ uploadFailuresRef.current += 1;
599
609
  setAttachError(err instanceof ApiRequestError ? err.message : "Could not upload image.");
600
610
  return null;
601
611
  }
@@ -606,10 +616,12 @@ export default function ComposeWindow({ session, onClose, onToggleMinimize, inli
606
616
  /** Send waits for every upload started before it: see `submit()`. */
607
617
  function uploadStarted() {
608
618
  uploadsInFlightRef.current += 1;
619
+ setUploadsActive(true);
609
620
  }
610
621
  function uploadFinished() {
611
622
  uploadsInFlightRef.current -= 1;
612
623
  if (uploadsInFlightRef.current === 0) {
624
+ setUploadsActive(false);
613
625
  for (const resolve of uploadWaitersRef.current.splice(0)) {
614
626
  resolve();
615
627
  }
@@ -645,13 +657,14 @@ export default function ComposeWindow({ session, onClose, onToggleMinimize, inli
645
657
  try {
646
658
  for (const file of files) {
647
659
  try {
648
- const attachment = await uploadAttachment(draft.uid, file);
660
+ const attachment = await uploadAttachment(draft.uid, file, {}, client);
649
661
  setHasUploads(true);
650
662
  setAttachments((prev) => [...prev, attachment]);
651
663
  void refreshDraftVersion(draft.uid);
652
664
  }
653
665
  catch (err) {
654
666
  allAttached = false;
667
+ uploadFailuresRef.current += 1;
655
668
  setAttachError(err instanceof ApiRequestError ? err.message : "Could not upload attachment.");
656
669
  }
657
670
  }
@@ -769,7 +782,25 @@ export default function ComposeWindow({ session, onClose, onToggleMinimize, inli
769
782
  try {
770
783
  if (uploadsInFlightRef.current > 0) {
771
784
  setWaitingForUploads(true);
772
- await new Promise((resolve) => uploadWaitersRef.current.push(resolve));
785
+ const failuresBefore = uploadFailuresRef.current;
786
+ let cancelled = false;
787
+ await new Promise((resolve) => {
788
+ uploadWaitersRef.current.push(resolve);
789
+ cancelUploadWaitRef.current = () => {
790
+ cancelled = true;
791
+ resolve();
792
+ };
793
+ });
794
+ cancelUploadWaitRef.current = null;
795
+ // Close or Discard was pressed meanwhile: the message is not to be sent.
796
+ if (cancelled) {
797
+ return;
798
+ }
799
+ // An attachment the sender asked for did not arrive: say so rather than sending the message without it.
800
+ if (uploadFailuresRef.current > failuresBefore) {
801
+ setSendError("An attachment could not be uploaded, so the message was not sent. Remove it or try again.");
802
+ return;
803
+ }
773
804
  // The editor puts an image it has just uploaded into the message a moment after the upload resolves: let the window catch up.
774
805
  await new Promise((resolve) => setTimeout(resolve, 0));
775
806
  setWaitingForUploads(false);
@@ -876,6 +907,7 @@ export default function ComposeWindow({ session, onClose, onToggleMinimize, inli
876
907
  setSaveStatus("saved");
877
908
  return { message: before };
878
909
  }
910
+ plaintextRequestedRef.current = target.uid;
879
911
  try {
880
912
  const saved = await assembleDraft(target.uid, {
881
913
  to: parseAddresses(current.to),
@@ -924,6 +956,7 @@ export default function ComposeWindow({ session, onClose, onToggleMinimize, inli
924
956
  const version = Math.max(current.version, saved?.uid === current.uid ? saved.version : 0);
925
957
  try {
926
958
  await deleteMessage(current.uid, version, client);
959
+ forgetDraftImages(current.uid);
927
960
  }
928
961
  catch (err) {
929
962
  if (!(err instanceof ApiRequestError) || (err.status !== 404 && err.status !== 409)) {
@@ -936,6 +969,7 @@ export default function ComposeWindow({ session, onClose, onToggleMinimize, inli
936
969
  throw getErr;
937
970
  });
938
971
  if (!fresh) {
972
+ forgetDraftImages(current.uid);
939
973
  return;
940
974
  }
941
975
  // The version changed because another window sent or scheduled it - that message isn't ours to delete.
@@ -943,6 +977,7 @@ export default function ComposeWindow({ session, onClose, onToggleMinimize, inli
943
977
  throw new ApiRequestError(NO_LONGER_A_DRAFT_MESSAGE, 409);
944
978
  }
945
979
  await deleteMessage(fresh.uid, fresh.version, client);
980
+ forgetDraftImages(current.uid);
946
981
  }
947
982
  }
948
983
  /**
@@ -951,6 +986,7 @@ export default function ComposeWindow({ session, onClose, onToggleMinimize, inli
951
986
  * deleted, and a delete that fails keeps the window open with the error.
952
987
  */
953
988
  async function discardNow() {
989
+ cancelUploadWaitRef.current?.();
954
990
  finishedRef.current = true;
955
991
  pendingSaveRef.current = false;
956
992
  setClosePrompt(null);
@@ -990,6 +1026,7 @@ export default function ComposeWindow({ session, onClose, onToggleMinimize, inli
990
1026
  * *unknown* (settings loading or failed, a lookup pending) is not a reason to hold the window: it is saved like any
991
1027
  * other message. A failed save keeps the window open with Discard / Keep editing. */
992
1028
  async function handleClose() {
1029
+ cancelUploadWaitRef.current?.();
993
1030
  if (!hasUserContent) {
994
1031
  void discardNow();
995
1032
  return;
@@ -1084,10 +1121,13 @@ export default function ComposeWindow({ session, onClose, onToggleMinimize, inli
1084
1121
  * uid, so attachments and the folder are undisturbed), chained after any save still on the wire. Best effort - a failure is reported once. */
1085
1122
  function scrubSavedDraft(target) {
1086
1123
  lastSavedRef.current = null;
1124
+ plaintextRequestedRef.current = null;
1087
1125
  setSaveStatus("idle");
1088
1126
  const previous = saveInFlightRef.current;
1089
1127
  const scrub = (async () => {
1090
1128
  await previous;
1129
+ // A save that was on the wire has landed by now and recorded itself as the last one saved - which the scrub just replaced.
1130
+ lastSavedRef.current = null;
1091
1131
  try {
1092
1132
  // Its answer (the new version) is what a later delete waits for through `saveInFlightRef`, like any save's.
1093
1133
  return await assembleDraft(target.uid, { to: [], cc: [], bcc: [], subject: "", html: "" }, client);
@@ -1106,7 +1146,7 @@ export default function ComposeWindow({ session, onClose, onToggleMinimize, inli
1106
1146
  }
1107
1147
  // Only a message that was really saved (`lastSavedRef` names its draft) has a plaintext copy to replace.
1108
1148
  useEffect(() => {
1109
- if (encryptionDecided && draft && lastSavedRef.current?.startsWith(`${draft.uid}:`)) {
1149
+ if (encryptionDecided && draft && (lastSavedRef.current?.startsWith(`${draft.uid}:`) || plaintextRequestedRef.current === draft.uid)) {
1110
1150
  scrubSavedDraft(draft);
1111
1151
  }
1112
1152
  }, [encryptionDecided, draft?.uid]);
@@ -1223,6 +1263,9 @@ export default function ComposeWindow({ session, onClose, onToggleMinimize, inli
1223
1263
  // isn't mistaken for an edit (and autosaved) just because the editor was focused or clicked.
1224
1264
  /** The quote has been added: unless the reader has typed something already, that is still exactly what compose seeded. */
1225
1265
  function handleQuoteAppended(normalized) {
1266
+ // Added for good: an editor mounted again (after a minimize and restore) is seeded from the body that already holds the quote, and
1267
+ // must not be handed it a second time.
1268
+ setSeededWithoutQuote(false);
1226
1269
  const latest = latestRef.current;
1227
1270
  if (latest.html === latest.seededHtml) {
1228
1271
  setHtml(normalized);
@@ -1247,8 +1290,10 @@ export default function ComposeWindow({ session, onClose, onToggleMinimize, inli
1247
1290
  // 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 -
1248
1291
  // a shortcut pressed while its button would be disabled is consumed and does nothing.
1249
1292
  // Send never waits for the encryption settings to load (fail open, see `encryptionRequirement.ts`); it only needs the draft to exist. Pressed
1250
- // while an earlier press is still being handled it does nothing (`submit()` guards that itself).
1251
- const canSend = !!draft && !closing && !waitingForUploads;
1293
+ // while an earlier press is still being handled it does nothing (`submit()` guards that itself). A reply or forward waits for its quoted original
1294
+ // (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.
1295
+ // Nor before the editor is there (it waits for the signature): sent sooner a reply would go without its quote and signature.
1296
+ const canSend = !!draft && contentReady && !closing && !waitingForUploads && !quotePending;
1252
1297
  useShortcut(SHORTCUTS.compose.send, () => void (canSend && submit(false)), { container: windowRef });
1253
1298
  useShortcut(SHORTCUTS.compose.saveDraft, () => {
1254
1299
  // Autosave's own conditions: never a draft that must not be stored as plaintext.
@@ -1280,7 +1325,7 @@ export default function ComposeWindow({ session, onClose, onToggleMinimize, inli
1280
1325
  ].join(" "), children: [dropActive && (_jsxs("div", { "data-drop-overlay": "", className: "pointer-events-none absolute inset-0 z-30 flex flex-col items-center justify-center gap-1 border-2 border-dashed border-primary bg-primary/10 text-center text-primary-dark", children: [_jsx("span", { className: "text-base font-semibold", children: "Drop files to attach them" }), _jsx("span", { className: "text-sm", children: "Pictures go into the message where the cursor is" })] })), !inline && !isMobile && (_jsxs(_Fragment, { children: [_jsx("div", { onPointerDown: handleResizeStart("top"), "aria-hidden": "true", className: "absolute top-0 left-0 right-0 h-1.5 cursor-ns-resize z-20" }), _jsx("div", { onPointerDown: handleResizeStart("left"), "aria-hidden": "true", className: "absolute top-0 left-0 bottom-0 w-1.5 cursor-ew-resize z-20" }), _jsx("div", { onPointerDown: handleResizeStart("corner"), role: "separator", "aria-label": "Resize", className: "absolute top-0 left-0 w-3 h-3 cursor-nwse-resize z-30" })] })), _jsxs("div", { className: ["h-10 shrink-0 flex items-center justify-between gap-2 px-3 bg-primary-darker text-white", inline ? "" : "cursor-pointer"].join(" "), onClick: inline ? undefined : onToggleMinimize, children: [_jsx("span", { id: titleId, className: "text-sm font-medium truncate", children: title }), _jsxs("div", { className: "flex items-center gap-0.5", onClick: (e) => e.stopPropagation(), children: [inline ? (_jsx(HeaderButton, { label: "Pop out", onClick: () => onPopOut?.(), icon: HiOutlineArrowTopRightOnSquare })) : (_jsx(HeaderButton, { label: "Minimize", onClick: onToggleMinimize, icon: HiOutlineMinus })), !inline && !isMobile && (_jsx(HeaderButton, { label: expanded ? "Collapse" : "Expand", onClick: toggleExpanded, icon: expanded ? HiOutlineArrowsPointingIn : HiOutlineArrowsPointingOut })), _jsx(HeaderButton, { label: "Close", onClick: () => void handleClose(), icon: HiOutlineXMark, disabled: closing, shortcut: SHORTCUTS.compose.close })] })] }), _jsxs("div", { className: "flex-1 min-h-0 flex flex-col", children: [(folderError || draftError || sendError || attachError || discardError) && (_jsxs("div", { className: "px-3 pt-2 shrink-0 max-h-[45%] overflow-y-auto", children: [folderError && _jsx(Alert, { children: folderError }), draftError && _jsx(Alert, { children: draftError }), sendError && _jsx(Alert, { children: sendError }), attachError && _jsx(Alert, { children: attachError }), discardError && _jsx(Alert, { children: discardError })] })), securityBlock && (_jsx("div", { className: "px-3 pt-2", children: _jsxs(Alert, { children: [_jsx("p", { className: "mb-2", children: securityBlock.message }), _jsx(Button, { type: "button", onClick: () => void submit(true, blockedScheduleIso), children: securityBlock.overrideLabel })] }) })), fromOptions.length > 1 && (_jsxs("div", { className: FIELD_ROW, children: [_jsx("label", { htmlFor: `compose-from-${id}`, className: "text-xs text-text-muted shrink-0", children: "From" }), _jsx("select", { id: `compose-from-${id}`, className: `${FIELD_INPUT} disabled:opacity-55`,
1281
1326
  // Two or more options only exist once the mailboxes have loaded, and the same answer that lets them list
1282
1327
  // (one that isn't known to be view-only) is what picks the default sender, so a sender is always chosen by now.
1283
- value: mailboxUid, disabled: hasUploads, title: hasUploads ? "The sender can't be changed after adding attachments or images." : undefined, onChange: (e) => handleFromChange(e.target.value), children: fromOptions.map((mb) => (_jsxs("option", { value: mb.uid, children: [mb.displayName ? `${mb.displayName} <${mb.primarySmtpAddress}>` : mb.primarySmtpAddress, mb.ownerUserUid ? "" : " (shared)"] }, mb.uid))) })] })), _jsxs("div", { className: FIELD_ROW, children: [_jsx("label", { htmlFor: `compose-to-${id}`, className: "text-xs text-text-muted shrink-0", children: "To" }), _jsx(RecipientInput, { id: `compose-to-${id}`, label: "To", value: to, mailboxUid: mailboxUid, onChange: setTo, onBlur: (value) => checkRecipientDiscovery(parseAddresses(value)), onCommit: (value) => checkRecipientDiscovery(parseAddresses(value)), autoFocus: focusToOnMount }), !showCcBcc && (_jsx("button", { type: "button", onClick: () => setShowCcBcc(true), className: "text-xs text-text-muted hover:text-text shrink-0", children: "Cc Bcc" }))] }), showCcBcc && (_jsxs(_Fragment, { children: [_jsxs("div", { className: FIELD_ROW, children: [_jsx("label", { htmlFor: `compose-cc-${id}`, className: "text-xs text-text-muted shrink-0", children: "Cc" }), _jsx(RecipientInput, { id: `compose-cc-${id}`, label: "Cc", value: cc, mailboxUid: mailboxUid, onChange: setCc, onBlur: (value) => checkRecipientDiscovery(parseAddresses(value)), onCommit: (value) => checkRecipientDiscovery(parseAddresses(value)) })] }), _jsxs("div", { className: FIELD_ROW, children: [_jsx("label", { htmlFor: `compose-bcc-${id}`, className: "text-xs text-text-muted shrink-0", children: "Bcc" }), _jsx(RecipientInput, { id: `compose-bcc-${id}`, label: "Bcc", value: bcc, mailboxUid: mailboxUid, onChange: setBcc, onBlur: (value) => checkRecipientDiscovery(parseAddresses(value)), onCommit: (value) => checkRecipientDiscovery(parseAddresses(value)) })] })] })), (() => {
1328
+ value: mailboxUid, disabled: hasUploads || uploadsActive, title: hasUploads || uploadsActive ? "The sender can't be changed after adding attachments or images." : undefined, onChange: (e) => handleFromChange(e.target.value), children: fromOptions.map((mb) => (_jsxs("option", { value: mb.uid, children: [mb.displayName ? `${mb.displayName} <${mb.primarySmtpAddress}>` : mb.primarySmtpAddress, mb.ownerUserUid ? "" : " (shared)"] }, mb.uid))) })] })), _jsxs("div", { className: FIELD_ROW, children: [_jsx("label", { htmlFor: `compose-to-${id}`, className: "text-xs text-text-muted shrink-0", children: "To" }), _jsx(RecipientInput, { id: `compose-to-${id}`, label: "To", value: to, mailboxUid: mailboxUid, onChange: setTo, onBlur: (value) => checkRecipientDiscovery(parseAddresses(value)), onCommit: (value) => checkRecipientDiscovery(parseAddresses(value)), autoFocus: focusToOnMount }), !showCcBcc && (_jsx("button", { type: "button", onClick: () => setShowCcBcc(true), className: "text-xs text-text-muted hover:text-text shrink-0", children: "Cc Bcc" }))] }), showCcBcc && (_jsxs(_Fragment, { children: [_jsxs("div", { className: FIELD_ROW, children: [_jsx("label", { htmlFor: `compose-cc-${id}`, className: "text-xs text-text-muted shrink-0", children: "Cc" }), _jsx(RecipientInput, { id: `compose-cc-${id}`, label: "Cc", value: cc, mailboxUid: mailboxUid, onChange: setCc, onBlur: (value) => checkRecipientDiscovery(parseAddresses(value)), onCommit: (value) => checkRecipientDiscovery(parseAddresses(value)) })] }), _jsxs("div", { className: FIELD_ROW, children: [_jsx("label", { htmlFor: `compose-bcc-${id}`, className: "text-xs text-text-muted shrink-0", children: "Bcc" }), _jsx(RecipientInput, { id: `compose-bcc-${id}`, label: "Bcc", value: bcc, mailboxUid: mailboxUid, onChange: setBcc, onBlur: (value) => checkRecipientDiscovery(parseAddresses(value)), onCommit: (value) => checkRecipientDiscovery(parseAddresses(value)) })] })] })), (() => {
1284
1329
  // De-duplicated by the same normalized key `recipientStatuses` itself is keyed by - two
1285
1330
  // fields (or one field twice) naming the same mailbox in different casing now resolve to
1286
1331
  // one shared status object (see `addressCacheKey()`'s own doc comment), so without this
@@ -1302,5 +1347,5 @@ export default function ComposeWindow({ session, onClose, onToggleMinimize, inli
1302
1347
  return null;
1303
1348
  }
1304
1349
  return (_jsx("ul", { className: "flex flex-wrap gap-1.5 px-3 pt-1.5", "aria-label": "Recipient encryption availability", children: knownRecipients.map((status) => (_jsxs("li", { className: `text-xs font-medium py-0.5 px-2 rounded-pill ${status.canEncrypt ? "bg-primary/10 text-primary-dark" : "bg-surface-alt text-text-muted"}`, children: [status.address, " ", status.canEncrypt ? "supports encryption" : "no encryption key found"] }, status.address))) }));
1305
- })(), _jsx("div", { className: FIELD_ROW, children: _jsx("input", { "aria-label": "Subject", type: "text", placeholder: "Subject", className: FIELD_INPUT, value: subject, autoFocus: focusSubjectOnMount, onChange: (e) => setSubject(e.target.value) }) }), _jsx("div", { className: "flex-1 min-h-0 p-2", children: contentReady && (_jsx(RichTextEditor, { appendHtml: lateQuoteHtml, onAppended: handleQuoteAppended, value: html, onChange: setHtml, fill: true, onUploadImage: handleUploadImage, autoFocusStart: focusBodyOnMount, onInitialized: handleEditorInitialized, editorRef: editorRef })) }), quotePending && (_jsx("p", { role: "status", className: "px-3 pb-1 text-xs text-text-muted", children: "Loading the original message\u2026" })), attachments.length > 0 && (_jsx("ul", { className: "flex flex-wrap gap-2 px-3 pb-2", children: attachments.map((attachment) => (_jsxs("li", { className: "inline-flex items-center gap-1 text-xs font-medium py-1 pl-2.5 pr-1 rounded-pill bg-surface-alt text-text-muted", children: [attachment.filename, _jsx("button", { type: "button", "aria-label": `Remove ${attachment.filename}`, title: `Remove ${attachment.filename}`, disabled: removingAttachmentUid === attachment.uid, onClick: () => void handleRemoveAttachment(attachment), className: "inline-flex items-center justify-center rounded-pill p-0.5 hover:bg-border hover:text-text disabled:opacity-50 focus-visible:outline-2 focus-visible:outline-primary", children: _jsx(HiOutlineXMark, { size: 12, "aria-hidden": "true" }) })] }, attachment.uid))) })), _jsxs("label", { className: "flex items-center gap-1.5 px-3 pb-1 text-xs text-text-muted", children: [_jsx("input", { type: "checkbox", checked: requestReceipt, onChange: (e) => setRequestReceipt(e.target.checked) }), "Request a read receipt"] }), needsUnlockForCrypto && (_jsx("div", { className: "px-3 pb-1", children: _jsxs("button", { type: "button", onClick: handleUnlockForCrypto, className: "inline-flex items-center gap-1 text-xs font-medium text-primary-dark hover:underline", children: [_jsx(HiOutlineLockClosed, { size: 12, "aria-hidden": "true" }), "Unlock to sign or encrypt this message"] }) })), unlockedKeys?.signingPrivateKey && (_jsxs("label", { className: "flex items-center gap-1.5 px-3 pb-1 text-xs text-text-muted", children: [_jsx("input", { type: "checkbox", checked: signEnabled, onChange: (e) => setSignEnabled(e.target.checked) }), "Digitally sign this message"] })), unlockedKeys?.encryptionPrivateKey && (_jsxs("label", { className: "flex items-center gap-1.5 px-3 pb-1 text-xs text-text-muted", children: [_jsx("input", { type: "checkbox", checked: encryptRequested, onChange: (e) => setEncryptRequested(e.target.checked) }), "Encrypt this message"] })), _jsxs("div", { className: "shrink-0 flex items-center gap-1 px-3 py-2 border-t border-border", children: [_jsxs("div", { className: "flex items-center rounded-pill bg-primary text-white overflow-hidden", children: [_jsx("button", { type: "button", onClick: () => void submit(false), title: withHint("Send", SHORTCUTS.compose.send, keyEnv), "aria-keyshortcuts": ariaKeyShortcuts(SHORTCUTS.compose.send, keyEnv), disabled: !draft || closing || waitingForUploads, 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", children: "Send" }), _jsx("button", { ref: scheduleButtonRef, type: "button", "aria-label": "Send later", "aria-haspopup": "true", "aria-expanded": schedulePickerOpen, disabled: !draft || closing || waitingForUploads, onClick: () => setSchedulePickerOpen((o) => !o), className: "py-1.5 px-2 border-l border-white/30 hover:not-disabled:bg-primary-dark disabled:opacity-55 disabled:cursor-not-allowed", children: _jsx(HiOutlineChevronDown, { size: 14 }) })] }), schedulePickerOpen && (_jsx(ScheduleSendPicker, { anchorRef: scheduleButtonRef, onClose: () => setSchedulePickerOpen(false), onSchedule: handleScheduleSend })), _jsxs("label", { "aria-label": "Attach files", title: "Attach files", className: "w-8 h-8 flex items-center justify-center rounded-full text-text-muted hover:bg-surface-alt hover:text-text cursor-pointer has-[:disabled]:opacity-40 has-[:disabled]:cursor-not-allowed", children: [_jsx(HiOutlinePaperClip, { size: 18 }), _jsx("input", { type: "file", multiple: true, disabled: !draft, onChange: handleFilesSelected, className: "sr-only" })] }), _jsx("button", { type: "button", "aria-label": "Attach my contact card", title: ownCardAttached ? "Your contact card is attached" : "Attach my contact card", "aria-pressed": ownCardAttached, disabled: !draft || !mailbox || ownCardAttached, onClick: () => void handleAttachOwnCard(), className: "w-8 h-8 flex items-center justify-center rounded-full text-text-muted hover:not-disabled:bg-surface-alt hover:not-disabled:text-text disabled:opacity-40 disabled:cursor-not-allowed", children: _jsx(HiOutlineIdentification, { size: 18 }) }), _jsx("span", { role: "status", "aria-live": "polite", className: "ml-auto text-xs text-text-muted", children: waitingForUploads ? "Waiting for attachments to finish uploading…" : SAVE_STATUS_LABEL[saveStatus] }), _jsx("button", { type: "button", "aria-label": "Discard draft", title: "Discard draft", onClick: handleDiscard, disabled: closing, className: "w-8 h-8 flex items-center justify-center rounded-full text-text-muted hover:bg-surface-alt hover:text-text disabled:opacity-40 disabled:cursor-not-allowed", children: _jsx(HiOutlineTrash, { size: 18 }) })] })] })] }), discardModal] }));
1350
+ })(), _jsx("div", { className: FIELD_ROW, children: _jsx("input", { "aria-label": "Subject", type: "text", placeholder: "Subject", className: FIELD_INPUT, value: subject, autoFocus: focusSubjectOnMount, onChange: (e) => setSubject(e.target.value) }) }), _jsx("div", { className: "flex-1 min-h-0 p-2", children: contentReady && (_jsx(RichTextEditor, { appendHtml: lateQuoteHtml, onAppended: handleQuoteAppended, value: html, onChange: setHtml, fill: true, onUploadImage: handleUploadImage, autoFocusStart: focusBodyOnMount, onInitialized: handleEditorInitialized, editorRef: editorRef })) }), quotePending && (_jsx("p", { role: "status", className: "px-3 pb-1 text-xs text-text-muted", children: "Loading the original message\u2026" })), attachments.length > 0 && (_jsx("ul", { className: "flex flex-wrap gap-2 px-3 pb-2", children: attachments.map((attachment) => (_jsxs("li", { className: "inline-flex items-center gap-1 text-xs font-medium py-1 pl-2.5 pr-1 rounded-pill bg-surface-alt text-text-muted", children: [attachment.filename, _jsx("button", { type: "button", "aria-label": `Remove ${attachment.filename}`, title: `Remove ${attachment.filename}`, disabled: removingAttachmentUid === attachment.uid, onClick: () => void handleRemoveAttachment(attachment), className: "inline-flex items-center justify-center rounded-pill p-0.5 hover:bg-border hover:text-text disabled:opacity-50 focus-visible:outline-2 focus-visible:outline-primary", children: _jsx(HiOutlineXMark, { size: 12, "aria-hidden": "true" }) })] }, attachment.uid))) })), _jsxs("label", { className: "flex items-center gap-1.5 px-3 pb-1 text-xs text-text-muted", children: [_jsx("input", { type: "checkbox", checked: requestReceipt, onChange: (e) => setRequestReceipt(e.target.checked) }), "Request a read receipt"] }), needsUnlockForCrypto && (_jsx("div", { className: "px-3 pb-1", children: _jsxs("button", { type: "button", onClick: handleUnlockForCrypto, className: "inline-flex items-center gap-1 text-xs font-medium text-primary-dark hover:underline", children: [_jsx(HiOutlineLockClosed, { size: 12, "aria-hidden": "true" }), "Unlock to sign or encrypt this message"] }) })), unlockedKeys?.signingPrivateKey && (_jsxs("label", { className: "flex items-center gap-1.5 px-3 pb-1 text-xs text-text-muted", children: [_jsx("input", { type: "checkbox", checked: signEnabled, onChange: (e) => setSignEnabled(e.target.checked) }), "Digitally sign this message"] })), unlockedKeys?.encryptionPrivateKey && (_jsxs("label", { className: "flex items-center gap-1.5 px-3 pb-1 text-xs text-text-muted", children: [_jsx("input", { type: "checkbox", checked: encryptRequested, onChange: (e) => setEncryptRequested(e.target.checked) }), "Encrypt this message"] })), _jsxs("div", { className: "shrink-0 flex items-center gap-1 px-3 py-2 border-t border-border", children: [_jsxs("div", { className: "flex items-center rounded-pill bg-primary text-white overflow-hidden", children: [_jsx("button", { type: "button", onClick: () => void submit(false), title: withHint("Send", SHORTCUTS.compose.send, keyEnv), "aria-keyshortcuts": ariaKeyShortcuts(SHORTCUTS.compose.send, keyEnv), disabled: !canSend, 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", children: "Send" }), _jsx("button", { ref: scheduleButtonRef, type: "button", "aria-label": "Send later", "aria-haspopup": "true", "aria-expanded": schedulePickerOpen, disabled: !canSend, onClick: () => setSchedulePickerOpen((o) => !o), className: "py-1.5 px-2 border-l border-white/30 hover:not-disabled:bg-primary-dark disabled:opacity-55 disabled:cursor-not-allowed", children: _jsx(HiOutlineChevronDown, { size: 14 }) })] }), schedulePickerOpen && (_jsx(ScheduleSendPicker, { anchorRef: scheduleButtonRef, onClose: () => setSchedulePickerOpen(false), onSchedule: handleScheduleSend })), _jsxs("label", { "aria-label": "Attach files", title: "Attach files", className: "w-8 h-8 flex items-center justify-center rounded-full text-text-muted hover:bg-surface-alt hover:text-text cursor-pointer has-[:disabled]:opacity-40 has-[:disabled]:cursor-not-allowed", children: [_jsx(HiOutlinePaperClip, { size: 18 }), _jsx("input", { type: "file", multiple: true, disabled: !draft, onChange: handleFilesSelected, className: "sr-only" })] }), _jsx("button", { type: "button", "aria-label": "Attach my contact card", title: ownCardAttached ? "Your contact card is attached" : "Attach my contact card", "aria-pressed": ownCardAttached, disabled: !draft || !mailbox || ownCardAttached, onClick: () => void handleAttachOwnCard(), className: "w-8 h-8 flex items-center justify-center rounded-full text-text-muted hover:not-disabled:bg-surface-alt hover:not-disabled:text-text disabled:opacity-40 disabled:cursor-not-allowed", children: _jsx(HiOutlineIdentification, { size: 18 }) }), _jsx("span", { role: "status", "aria-live": "polite", className: "ml-auto text-xs text-text-muted", children: waitingForUploads ? "Waiting for attachments to finish uploading…" : SAVE_STATUS_LABEL[saveStatus] }), _jsx("button", { type: "button", "aria-label": "Discard draft", title: "Discard draft", onClick: handleDiscard, disabled: closing, className: "w-8 h-8 flex items-center justify-center rounded-full text-text-muted hover:bg-surface-alt hover:text-text disabled:opacity-40 disabled:cursor-not-allowed", children: _jsx(HiOutlineTrash, { size: 18 }) })] })] })] }), discardModal] }));
1306
1351
  }
@@ -24,13 +24,19 @@ export default function GifPicker({ anchorRef, onSelect, onClose }) {
24
24
  useEffect(() => {
25
25
  setLoading(true);
26
26
  setError(null);
27
+ // 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
28
+ // must not replace the results of the one now shown.
29
+ let stale = false;
27
30
  const handle = setTimeout(() => {
28
31
  searchGifs(query)
29
- .then(setGifs)
30
- .catch((err) => setError(err instanceof ApiRequestError ? err.message : "Could not load GIFs."))
31
- .finally(() => setLoading(false));
32
+ .then((found) => !stale && setGifs(found))
33
+ .catch((err) => !stale && setError(err instanceof ApiRequestError ? err.message : "Could not load GIFs."))
34
+ .finally(() => !stale && setLoading(false));
32
35
  }, SEARCH_DEBOUNCE_MS);
33
- return () => clearTimeout(handle);
36
+ return () => {
37
+ stale = true;
38
+ clearTimeout(handle);
39
+ };
34
40
  }, [query]);
35
41
  return (_jsxs(PopoverPortal, { anchorRef: anchorRef, onClose: onClose, width: WIDTH, height: HEIGHT, "aria-label": "Insert GIF", children: [_jsx("div", { className: "p-2 border-b border-border shrink-0", children: _jsx("input", { type: "text", autoFocus: true, "aria-label": "Search GIFs", placeholder: "Search GIFs", value: query, onChange: (e) => setQuery(e.target.value), className: "w-full text-sm py-1.5 px-2 border border-border rounded-sm bg-surface focus:outline-none focus:border-primary" }) }), _jsx("div", { className: "flex-1 overflow-y-auto p-2", children: error ? (_jsx("p", { className: "text-xs text-danger", children: error })) : loading ? (_jsx("p", { className: "text-xs text-text-muted", children: "Loading\u2026" })) : gifs.length === 0 ? (_jsx("p", { className: "text-xs text-text-muted", children: "No GIFs found." })) : (_jsx("div", { className: "grid grid-cols-2 gap-1.5", children: gifs.map((gif) => (_jsx("button", { type: "button", title: gif.title, onClick: () => onSelect(gif.url), className: "rounded-sm overflow-hidden hover:opacity-80", children: _jsx("img", { src: gif.previewUrl, alt: gif.title, className: "w-full h-20 object-cover" }) }, gif.id))) })) })] }));
36
42
  }
@@ -44,7 +44,8 @@ function dropdownPosition(anchor) {
44
44
  */
45
45
  export default function RecipientInput({ id, label, value, onChange, onBlur, onCommit, mailboxUid, fetchSuggestions = fetchRecipientSuggestions, debounceMs = RECIPIENT_SUGGESTION_DEBOUNCE_MS, disabled, autoFocus, onEdit, initialPending = "", hideChip, placeholder, ariaLabel, listLabel, className, dropdownZIndex, }) {
46
46
  const [pending, setPending] = useState(initialPending);
47
- const [suggestions, setSuggestions] = useState([]);
47
+ // 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).
48
+ const [suggestionList, setSuggestionList] = useState({ query: "", items: [] });
48
49
  const [open, setOpen] = useState(false);
49
50
  const [activeIndex, setActiveIndex] = useState(0);
50
51
  const [position, setPosition] = useState(null);
@@ -69,24 +70,24 @@ export default function RecipientInput({ id, label, value, onChange, onBlur, onC
69
70
  }
70
71
  }, [pending, pendingActive]);
71
72
  const committedAddresses = new Set(chips.map((chip) => parseRecipient(chip).address.toLowerCase()));
72
- const visible = suggestions.filter((suggestion) => !committedAddresses.has(suggestion.address.toLowerCase()));
73
+ const visible = (suggestionList.query === query ? suggestionList.items : []).filter((suggestion) => !committedAddresses.has(suggestion.address.toLowerCase()));
73
74
  const expanded = open && visible.length > 0 && query.length >= RECIPIENT_SUGGESTION_MIN_QUERY_LENGTH;
74
75
  const active = Math.min(activeIndex, visible.length - 1);
75
76
  useEffect(() => {
76
77
  if (query.length < RECIPIENT_SUGGESTION_MIN_QUERY_LENGTH) {
77
- setSuggestions([]);
78
+ setSuggestionList({ query: "", items: [] });
78
79
  return;
79
80
  }
80
81
  const controller = new AbortController();
81
82
  const timer = setTimeout(() => {
82
83
  fetchRef.current(query, { mailboxUid, signal: controller.signal }).then((result) => {
83
84
  if (!controller.signal.aborted) {
84
- setSuggestions(result);
85
+ setSuggestionList({ query, items: result });
85
86
  setActiveIndex(0);
86
87
  }
87
88
  }, () => {
88
89
  if (!controller.signal.aborted) {
89
- setSuggestions([]);
90
+ setSuggestionList({ query: "", items: [] });
90
91
  }
91
92
  });
92
93
  }, debounceMs);
@@ -139,7 +140,7 @@ export default function RecipientInput({ id, label, value, onChange, onBlur, onC
139
140
  const next = committed.join(", ");
140
141
  setPending("");
141
142
  setOpen(false);
142
- setSuggestions([]);
143
+ setSuggestionList({ query: "", items: [] });
143
144
  onChange(next);
144
145
  onEdit?.(committed, "");
145
146
  onCommit?.(next);
@@ -204,7 +205,8 @@ export default function RecipientInput({ id, label, value, onChange, onBlur, onC
204
205
  return (_jsxs("div", { ref: anchorRef, className: `flex-1 min-w-0 flex flex-wrap items-center gap-1${className ? ` ${className}` : ""}`, children: [drawn.length > 0 && (_jsx("ul", { role: "list", "aria-label": listLabel ?? `${label} recipients`, className: "contents", children: drawn.map(({ chip, index }) => {
205
206
  const recipient = parseRecipient(chip);
206
207
  const valid = isValidRecipientAddress(recipient.address);
207
- const name = recipient.displayName || recipient.address;
208
+ // 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.
209
+ const name = /[@@﹫]/.test(recipient.displayName ?? "") ? `${recipient.displayName} <${recipient.address}>` : recipient.displayName || recipient.address;
208
210
  return (_jsxs("li", { title: chip, className: `flex items-center gap-1 max-w-full min-w-0 rounded-pill border pl-2 pr-0.5 py-px text-xs ${valid ? "bg-surface-alt border-border text-text" : "bg-danger-bg border-danger text-danger"}`, children: [_jsx("span", { className: "truncate", children: name }), !valid && _jsx("span", { className: "sr-only", children: " (not a valid email address)" }), _jsx("button", { type: "button", tabIndex: -1, "aria-label": `Remove ${recipient.address}`, disabled: disabled, onMouseDown: (e) => e.preventDefault(), onClick: () => removeChip(index), className: "shrink-0 w-4 h-4 flex items-center justify-center rounded-pill text-text-muted hover:bg-border hover:text-text", children: _jsx(HiOutlineXMark, { size: 10 }) })] }, `${index}-${chip}`));
209
211
  }) })), _jsx("input", { ref: inputRef, id: id, type: "text", role: "combobox", autoComplete: "off", "aria-autocomplete": "list", "aria-label": ariaLabel, placeholder: placeholder, "aria-expanded": expanded, "aria-controls": listboxId, "aria-activedescendant": expanded ? optionId(active) : undefined, disabled: disabled, autoFocus: autoFocus, className: "flex-1 min-w-[6rem] text-sm bg-transparent outline-none py-0.5", value: inputValue, onChange: handleInputChange, onKeyDown: handleKeyDown, onBlur: handleBlur }), _jsx("span", { className: "sr-only", "aria-live": "polite", children: expanded ? `${visible.length} ${visible.length === 1 ? "suggestion" : "suggestions"}` : "" }), expanded &&
210
212
  position &&
@@ -1,11 +1,17 @@
1
1
  /** Saves a compose window's unsaved edits (or waits for a save already on the wire), resolving to `false` when
2
2
  * something the window holds is still unsaved afterwards (any other value counts as saved). Never rejects. */
3
3
  export type ComposeFlush = () => Promise<unknown>;
4
+ /**
5
+ * Registers something to run the moment the app starts signing out (explicitly, or forced by another tab): a cache that holds message content,
6
+ * which must not outlive the session it was read in. Called once, when the module that owns the cache is loaded.
7
+ */
8
+ export declare function onSigningOut(cleanup: () => void): void;
4
9
  /**
5
10
  * Marks the app as signing out (explicitly, or forced by a sign-out in another tab), so compose windows stop
6
11
  * asking the browser to confirm leaving the page - the "Leave site?" prompt would otherwise let the user cancel
7
12
  * the sign-out's own navigation and stay on a page whose session is being ended. Call it before flushing drafts
8
- * and navigating. `clearSigningOut()` undoes it (a sign-out that didn't leave the page after all).
13
+ * and navigating. `clearSigningOut()` undoes it (a sign-out that didn't leave the page after all). It also runs what was registered
14
+ * with `onSigningOut()`.
9
15
  */
10
16
  export declare function markSigningOut(): void;
11
17
  /** Undoes `markSigningOut()`. */
@@ -3,15 +3,27 @@
3
3
  // SPDX-License-Identifier: MPL-2.0
4
4
  ///////////////////////////////////////////////////////////////////////////////
5
5
  const flushers = new Set();
6
+ const signOutCleanups = new Set();
6
7
  let signingOut = false;
8
+ /**
9
+ * Registers something to run the moment the app starts signing out (explicitly, or forced by another tab): a cache that holds message content,
10
+ * which must not outlive the session it was read in. Called once, when the module that owns the cache is loaded.
11
+ */
12
+ export function onSigningOut(cleanup) {
13
+ signOutCleanups.add(cleanup);
14
+ }
7
15
  /**
8
16
  * Marks the app as signing out (explicitly, or forced by a sign-out in another tab), so compose windows stop
9
17
  * asking the browser to confirm leaving the page - the "Leave site?" prompt would otherwise let the user cancel
10
18
  * the sign-out's own navigation and stay on a page whose session is being ended. Call it before flushing drafts
11
- * and navigating. `clearSigningOut()` undoes it (a sign-out that didn't leave the page after all).
19
+ * and navigating. `clearSigningOut()` undoes it (a sign-out that didn't leave the page after all). It also runs what was registered
20
+ * with `onSigningOut()`.
12
21
  */
13
22
  export function markSigningOut() {
14
23
  signingOut = true;
24
+ for (const cleanup of signOutCleanups) {
25
+ cleanup();
26
+ }
15
27
  }
16
28
  /** Undoes `markSigningOut()`. */
17
29
  export function clearSigningOut() {
@@ -18,6 +18,11 @@ import type { EncryptionPolicy } from "../../../../../lib/crypto/keyvaultApi.js"
18
18
  * 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
19
19
  * message *does* need encryption acts on it (see `ComposeWindow`'s transition rules).
20
20
  *
21
+ * 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
22
+ * (`sendDecision.ts`'s `decideSend()` with `lookupsComplete`) reads `unknown` once its loads are in: a sender who has an encryption key, and
23
+ * whose policy or recipients' keys could not be checked, is blocked with the `lookup-failed` decision (the sender may still send without
24
+ * encryption) rather than sent in the clear on a guess.
25
+ *
21
26
  * Why this is safe enough: the requirement only ever protects *this client's* plaintext drafts and sends from being unencrypted when the
22
27
  * 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
23
28
  * policy therefore does not mean "encrypt or refuse" - it means the client cannot tell, and the honest default is the one that works.
@@ -7,12 +7,23 @@ import { attachmentContentUrl, getMessageRawContent, listAttachments } from "../
7
7
  import { MAX_QUOTED_IMAGE_BYTES, embedQuotedImages } from "../../../../../lib/mail/compose/quotedImages.js";
8
8
  import { EMBEDDED_IMAGE_TYPES, findInlineAttachment, toDataUri } from "../../../../../lib/mail/inlineImages.js";
9
9
  import { decodeHeaderText, extractDisplayBody, parseMimeEntity } from "../../../../../lib/crypto/mime.js";
10
+ import { onSigningOut } from "./composeFlushRegistry.js";
10
11
  import { parseRecipientList } from "./recipients.js";
12
+ /** The security states whose `protectedHeaders` a signature covers - the only ones Reply All takes recipients from. */
13
+ const TRUSTED_HEADER_STATES = new Set([
14
+ "signed_verified",
15
+ "encrypted_verified",
16
+ "signed_unverified_signer",
17
+ "encrypted_unverified_signer",
18
+ "verified_at_first_open",
19
+ ]);
11
20
  /** How long a reply waits for the original's body before it goes ahead with the preview - a slow or hung request must not
12
21
  * leave the window waiting on it (or, before it opened at once, keep it from opening) indefinitely. */
13
22
  export const QUOTE_FETCH_TIMEOUT_MS = 10000;
14
23
  /** How long a fetched (or prefetched) body is remembered, so hovering Reply and then clicking it costs one request. */
15
24
  export const QUOTE_CACHE_MS = 30000;
25
+ /** 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. */
26
+ const QUOTE_CACHE_MAX = 20;
16
27
  const contentCache = new Map();
17
28
  /**
18
29
  * The server's sanitized HTML body of a message (`GET /mail/messages/:id/content`), or `undefined` when it has none (a
@@ -38,6 +49,12 @@ function fetchContentHtml(uid) {
38
49
  clearTimeout(timer);
39
50
  }
40
51
  })();
52
+ // Whatever has expired goes, and the oldest of the rest once there are too many.
53
+ for (const [key, entry] of contentCache) {
54
+ if (Date.now() - entry.at >= QUOTE_CACHE_MS || contentCache.size >= QUOTE_CACHE_MAX) {
55
+ contentCache.delete(key);
56
+ }
57
+ }
41
58
  contentCache.set(uid, { at: Date.now(), promise });
42
59
  void promise.then((html) => {
43
60
  if (html === undefined && contentCache.get(uid)?.promise === promise) {
@@ -68,6 +85,7 @@ export function prefetchOriginalMessage(message) {
68
85
  export function clearOriginalMessageCache() {
69
86
  contentCache.clear();
70
87
  }
88
+ onSigningOut(clearOriginalMessageCache);
71
89
  /** The recipients one address-list header names, RFC 2047-decoded, with their display names. */
72
90
  function headerRecipients(value, type) {
73
91
  if (!value) {
@@ -81,7 +99,7 @@ function headerRecipients(value, type) {
81
99
  * or `undefined` when there is none, it is not a raster image, it is too large or it could not be had. Never rejects. */
82
100
  function serverImageResolver(message) {
83
101
  let attachments;
84
- return async (reference) => {
102
+ return async (reference, signal) => {
85
103
  try {
86
104
  attachments ?? (attachments = listAttachments(message.folderUid, message.uid));
87
105
  const attachment = findInlineAttachment(await attachments, reference);
@@ -89,7 +107,7 @@ function serverImageResolver(message) {
89
107
  if (!attachment || !type || !EMBEDDED_IMAGE_TYPES.test(type) || attachment.sizeBytes > MAX_QUOTED_IMAGE_BYTES) {
90
108
  return undefined;
91
109
  }
92
- const res = await fetch(attachmentContentUrl(attachment.uid), { credentials: "include" });
110
+ const res = await fetch(attachmentContentUrl(attachment.uid), { credentials: "include", signal });
93
111
  const bytes = res.ok ? new Uint8Array(await res.arrayBuffer()) : undefined;
94
112
  return bytes && bytes.length <= MAX_QUOTED_IMAGE_BYTES ? toDataUri(type, bytes) : undefined;
95
113
  }
@@ -136,7 +154,9 @@ function partImageResolver(parts) {
136
154
  */
137
155
  export async function loadOriginalMessage(message, security, options = {}) {
138
156
  const recovered = security?.text !== undefined ? { text: security.text } : security?.html !== undefined ? { html: security.html } : undefined;
139
- const protectedHeaders = security?.protectedHeaders;
157
+ // Headers inside an unsigned (or badly signed) encrypted message are whatever its encrypter chose to write, so only a
158
+ // verified signature makes them a source of recipients.
159
+ const protectedHeaders = security && TRUSTED_HEADER_STATES.has(security.state) ? security.protectedHeaders : undefined;
140
160
  const protectedRecipients = protectedHeaders
141
161
  ? [...headerRecipients(protectedHeaders.to, "to"), ...headerRecipients(protectedHeaders.cc, "cc")]
142
162
  : [];
@@ -176,8 +196,11 @@ export async function loadOriginalMessage(message, security, options = {}) {
176
196
  if (result.body.html !== undefined) {
177
197
  const resolver = recovered ? partImageResolver(security?.attachments ?? []) : serverImageResolver(message);
178
198
  const html = result.body.html;
179
- // Bounded like the body's own request: a picture that will not arrive is left out of the quote, not waited for.
180
- result.body = { html: await withTimeout(embedQuotedImages(html, resolver), QUOTE_FETCH_TIMEOUT_MS).catch(() => html) };
199
+ // 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 -
200
+ // the ones that have arrived stay.
201
+ const controller = new AbortController();
202
+ const timer = setTimeout(() => controller.abort(), QUOTE_FETCH_TIMEOUT_MS);
203
+ result.body = { html: await embedQuotedImages(html, resolver, controller.signal).finally(() => clearTimeout(timer)) };
181
204
  }
182
205
  return result;
183
206
  }
@@ -204,7 +204,7 @@ export default function MailShell({ userUid, authServerUrl, impersonating, imper
204
204
  // `mailboxes.length` directly (not `!mailboxUid`) since `mailboxUid` is legitimately undefined in
205
205
  // aggregate mode even with mailboxes present.
206
206
  if (userUid && status === "ready" && mailboxes.length === 0) {
207
- return _jsx(MailboxProvisioning, {});
207
+ return _jsx(MailboxProvisioning, { impersonating: impersonating, impersonationBaseUrl: impersonationBaseUrl });
208
208
  }
209
209
  let inner = null;
210
210
  if (userUid && status === "checking") {
@@ -39,7 +39,7 @@ const AUTHORED_CANVAS = "rgb(255, 255, 255)";
39
39
  export default function MessageBody({ messageUid, messageVersion, title, content, attachments, inlineParts, original = false, onAdaptable }) {
40
40
  const [remote, setRemote] = useState(() => {
41
41
  const cached = cachedBodyContent(messageUid, messageVersion);
42
- return cached ? { status: "ready", content: cached } : { status: "loading" };
42
+ return cached ? { status: "ready", content: cached, uid: messageUid } : { status: "loading" };
43
43
  });
44
44
  const [attempt, setAttempt] = useState(0);
45
45
  const supplied = content !== undefined;
@@ -49,19 +49,24 @@ export default function MessageBody({ messageUid, messageVersion, title, content
49
49
  }
50
50
  const cached = cachedBodyContent(messageUid, messageVersion);
51
51
  if (cached) {
52
- setRemote({ status: "ready", content: cached });
52
+ setRemote({ status: "ready", content: cached, uid: messageUid });
53
53
  return;
54
54
  }
55
55
  const controller = new AbortController();
56
- setRemote({ status: "loading" });
57
- fetchBodyContent(messageUid, messageVersion, controller.signal).then((loaded) => setRemote({ status: "ready", content: loaded }), (err) => {
58
- if (!controller.signal.aborted) {
56
+ // 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
57
+ // the frame is not torn down and laid out again - until the fresh copy is here, and a failed read leaves it too.
58
+ const revalidating = remote.status === "ready" && remote.uid === messageUid;
59
+ if (!revalidating) {
60
+ setRemote({ status: "loading" });
61
+ }
62
+ fetchBodyContent(messageUid, messageVersion, controller.signal).then((loaded) => setRemote({ status: "ready", content: loaded, uid: messageUid }), (err) => {
63
+ if (!controller.signal.aborted && !revalidating) {
59
64
  setRemote({ status: "error", message: err instanceof ApiRequestError ? err.message : "Could not load this message." });
60
65
  }
61
66
  });
62
67
  return () => controller.abort();
63
68
  }, [messageUid, messageVersion, supplied, attempt]);
64
- const shown = content ? { status: "ready", content } : remote;
69
+ const shown = content ? { status: "ready", content, uid: messageUid } : remote;
65
70
  if (shown.status === "loading") {
66
71
  return _jsx(BodySkeleton, {});
67
72
  }
@@ -5,6 +5,7 @@
5
5
  import { ApiRequestError, apiUrl } from "../../../../../lib/util/api.js";
6
6
  import { attachmentContentUrl } from "../../../../../lib/mail/mailApi.js";
7
7
  import { findInlineAttachment, toDataUri } from "../../../../../lib/mail/inlineImages.js";
8
+ import { onSigningOut } from "../compose/composeFlushRegistry.js";
8
9
  /** How many bodies are remembered for the session - a thread that is expanded, collapsed and expanded again does not ask the server twice. */
9
10
  const CACHE_LIMIT = 40;
10
11
  /** What the pane asks `/content` for. */
@@ -14,6 +15,7 @@ const cache = new Map();
14
15
  export function clearBodyContentCache() {
15
16
  cache.clear();
16
17
  }
18
+ onSigningOut(clearBodyContentCache);
17
19
  /** The remembered body for a message at a version, if any. */
18
20
  export function cachedBodyContent(uid, version) {
19
21
  return cache.get(`${uid}:${version}`);
@@ -33,7 +33,11 @@ export interface PrepareOptions {
33
33
  * server removes one it could not point at a part) is asked for with no `contentId` and its `alt`. */
34
34
  resolveCid?: (contentId: string | undefined, alt?: string) => string | undefined;
35
35
  }
36
- /** CSS with the constructs above neutralised. */
36
+ /**
37
+ * CSS with the constructs above neutralised. The patterns run on what a browser would read, not only on the raw text: `position:\66ixed` and
38
+ * `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
39
+ * decoded text holds one, that text (escapes resolved, comments dropped) is what is kept, neutralised; CSS whose escapes hide nothing is left as written.
40
+ */
37
41
  export declare function sanitizeCss(css: string): string;
38
42
  /**
39
43
  * Whether a mail's own markup declares that it supports a dark scheme: `<meta name="color-scheme">` naming dark,