@rapidmx/web-client 0.30.0 → 0.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (400) hide show
  1. package/apps/admin/audit-log/index.tsx +3 -3
  2. package/apps/admin/branding/index.tsx +3 -3
  3. package/apps/admin/data-requests/index.tsx +18 -7
  4. package/apps/admin/distribution-lists/[uid].tsx +3 -3
  5. package/apps/admin/distribution-lists/index.tsx +3 -3
  6. package/apps/admin/distribution-lists/new/index.tsx +3 -3
  7. package/apps/admin/domains/[uid].tsx +6 -5
  8. package/apps/admin/domains/index.tsx +3 -3
  9. package/apps/admin/domains/new/index.tsx +3 -3
  10. package/apps/admin/escrow-scopes/[uid].tsx +11 -5
  11. package/apps/admin/escrow-scopes/index.tsx +3 -3
  12. package/apps/admin/escrow-scopes/new/index.tsx +9 -3
  13. package/apps/admin/index.tsx +4 -4
  14. package/apps/admin/ingest-queue/index.tsx +3 -3
  15. package/apps/admin/mailboxes/[uid].tsx +15 -7
  16. package/apps/admin/quarantine/index.tsx +4 -4
  17. package/apps/admin/retention-policy/index.tsx +3 -3
  18. package/apps/admin/signing-certificates/index.tsx +3 -3
  19. package/apps/admin/transport-rules/[uid].tsx +4 -4
  20. package/apps/admin/transport-rules/index.tsx +3 -3
  21. package/apps/admin/transport-rules/new/index.tsx +3 -3
  22. package/apps/escrow/matters/[uid].tsx +26 -2
  23. package/apps/escrow/matters/new/index.tsx +4 -0
  24. package/apps/shared/calendar/calendarLiveUpdates.ts +11 -10
  25. package/apps/shared/calendar/useCalendarReminders.ts +2 -0
  26. package/apps/shared/components/admin/ActionAlert.tsx +32 -0
  27. package/apps/shared/components/admin/diagnostics/InformationPanel.tsx +1 -1
  28. package/apps/shared/components/admin/diagnostics/InstalledPlugins.tsx +1 -1
  29. package/apps/shared/components/admin/diagnostics/LogsPanel.tsx +1 -1
  30. package/apps/shared/components/admin/diagnostics/RuntimePanel.tsx +1 -1
  31. package/apps/shared/components/admin/diagnostics/SystemPanel.tsx +1 -1
  32. package/apps/shared/components/admin/distributionLists/MemberListCard.tsx +4 -4
  33. package/apps/shared/components/admin/elevation.ts +19 -0
  34. package/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.tsx +58 -3
  35. package/apps/shared/components/admin/layout/AdminShell.tsx +6 -1
  36. package/apps/shared/components/admin/mailboxes/EraseLeftoverDataDialog.tsx +3 -3
  37. package/apps/shared/components/admin/mailboxes/EscrowScopeCard.tsx +4 -4
  38. package/apps/shared/components/admin/mailboxes/LeftoverMailboxesSection.tsx +4 -4
  39. package/apps/shared/components/admin/mailboxes/ResourceSettingsCard.tsx +3 -3
  40. package/apps/shared/components/admin/mailboxes/ShareAccessCard.tsx +6 -6
  41. package/apps/shared/components/admin/settings/BrandingForm.tsx +22 -9
  42. package/apps/shared/components/admin/settings/DomainDnsSetup.tsx +4 -4
  43. package/apps/shared/components/admin/settings/EncryptionPolicyForm.tsx +33 -4
  44. package/apps/shared/components/admin/settings/LoadedSettingsForm.tsx +3 -3
  45. package/apps/shared/components/admin/settings/MailboxCreateForm.tsx +8 -3
  46. package/apps/shared/components/admin/settings/MailboxPolicyForm.tsx +3 -3
  47. package/apps/shared/components/admin/settings/PluginsManager.tsx +39 -17
  48. package/apps/shared/components/admin/settings/RetentionPolicyForm.tsx +25 -12
  49. package/apps/shared/components/admin/setup/EscrowSetupStep.tsx +40 -7
  50. package/apps/shared/components/admin/setup/SetupWizard.tsx +8 -6
  51. package/apps/shared/components/admin/signOut.ts +3 -0
  52. package/apps/shared/components/admin/usePagedList.tsx +2 -2
  53. package/apps/shared/components/calendar/DescriptionEditor.tsx +2 -1
  54. package/apps/shared/components/calendar/EventDetails.tsx +34 -8
  55. package/apps/shared/components/calendar/EventEditor.tsx +90 -40
  56. package/apps/shared/components/calendar/EventFormParts.tsx +5 -2
  57. package/apps/shared/components/calendar/EventMatchNav.tsx +14 -2
  58. package/apps/shared/components/calendar/EventModal.tsx +25 -1
  59. package/apps/shared/components/calendar/FindATime.tsx +10 -2
  60. package/apps/shared/components/calendar/ListView.tsx +99 -25
  61. package/apps/shared/components/calendar/RecurrenceEditor.tsx +19 -8
  62. package/apps/shared/components/calendar/SplitDayView.tsx +14 -18
  63. package/apps/shared/components/calendar/TaskQuickForm.tsx +3 -2
  64. package/apps/shared/components/calendar/TimeGridView.tsx +35 -19
  65. package/apps/shared/components/calendar/gridLayout.ts +94 -0
  66. package/apps/shared/components/calendar/layout/CalendarShell.tsx +1 -1
  67. package/apps/shared/components/calendar/timePicker.ts +5 -2
  68. package/apps/shared/components/contacts/ContactCard.tsx +15 -4
  69. package/apps/shared/components/contacts/ContactDetailPane.tsx +54 -15
  70. package/apps/shared/components/contacts/ContactForm.tsx +61 -17
  71. package/apps/shared/components/contacts/ContactPhotoEditor.tsx +88 -0
  72. package/apps/shared/components/contacts/ContactPhotoField.tsx +36 -39
  73. package/apps/shared/components/contacts/layout/ContactsShell.tsx +1 -1
  74. package/apps/shared/components/contacts/participantDetails.ts +18 -1
  75. package/apps/shared/components/layout/AppShell.tsx +9 -0
  76. package/apps/shared/components/layout/KeyEnrollmentGate.tsx +9 -4
  77. package/apps/shared/components/layout/MailboxProvisioning.tsx +31 -3
  78. package/apps/shared/components/layout/RecoveryCodeUnlock.tsx +4 -2
  79. package/apps/shared/components/mail/ConversationList.tsx +49 -6
  80. package/apps/shared/components/mail/ConversationThreadPane.tsx +1 -1
  81. package/apps/shared/components/mail/MenuButton.tsx +3 -400
  82. package/apps/shared/components/mail/MessageDetailPane.tsx +16 -13
  83. package/apps/shared/components/mail/compose/ComposeToolbar.tsx +11 -1
  84. package/apps/shared/components/mail/compose/ComposeWindow.tsx +57 -11
  85. package/apps/shared/components/mail/compose/GifPicker.tsx +10 -4
  86. package/apps/shared/components/mail/compose/RecipientInput.tsx +9 -7
  87. package/apps/shared/components/mail/compose/composeFlushRegistry.ts +14 -1
  88. package/apps/shared/components/mail/compose/encryptionRequirement.ts +5 -0
  89. package/apps/shared/components/mail/compose/quotedBody.ts +31 -6
  90. package/apps/shared/components/mail/layout/MailShell.tsx +1 -1
  91. package/apps/shared/components/mail/reading/MessageBody.tsx +13 -7
  92. package/apps/shared/components/mail/reading/bodyContent.ts +2 -0
  93. package/apps/shared/components/mail/reading/bodyHtml.ts +25 -2
  94. package/apps/shared/components/mail/writableMailboxes.ts +2 -0
  95. package/apps/shared/components/rules/RuleBuilder.tsx +11 -2
  96. package/apps/shared/components/settings/GravatarSetting.tsx +27 -0
  97. package/apps/shared/components/settings/layout/SettingsShell.tsx +1 -1
  98. package/apps/shared/components/tasks/dueDate.ts +29 -0
  99. package/apps/shared/components/tasks/layout/TasksShell.tsx +1 -1
  100. package/apps/shared/mail/decryptedMessages.ts +51 -2
  101. package/apps/shared/mail/listSnapshots.ts +2 -0
  102. package/apps/shared/mail/outbox/sendDecision.ts +14 -3
  103. package/apps/shared/mail/outbox/sendJob.ts +5 -0
  104. package/apps/shared/mail/useMailLiveUpdates.ts +7 -1
  105. package/apps/shared/mail/usePermanentDelete.tsx +3 -1
  106. package/apps/shared/notifications/apiErrors.ts +2 -0
  107. package/apps/shared/notifications/preferences.ts +1 -1
  108. package/apps/shared/notifications/store.ts +3 -1
  109. package/apps/shared/plugins/pluginNav.ts +4 -2
  110. package/apps/www/calendar/index.tsx +24 -12
  111. package/apps/www/contacts/[uid].tsx +6 -1
  112. package/apps/www/contacts/index.tsx +66 -12
  113. package/apps/www/index.tsx +91 -28
  114. package/apps/www/messages/[uid].tsx +8 -3
  115. package/apps/www/settings/auto-reply/index.tsx +13 -0
  116. package/apps/www/settings/blocked-senders/index.tsx +4 -0
  117. package/apps/www/settings/encryption/index.tsx +66 -4
  118. package/apps/www/settings/privacy/index.tsx +4 -1
  119. package/apps/www/settings/profile/index.tsx +12 -1
  120. package/apps/www/settings/sharing/index.tsx +11 -1
  121. package/apps/www/settings/signatures/[uid].tsx +6 -4
  122. package/apps/www/tasks/index.tsx +97 -29
  123. package/dist/apps/admin/audit-log/index.js +3 -3
  124. package/dist/apps/admin/branding/index.js +3 -3
  125. package/dist/apps/admin/data-requests/index.js +17 -7
  126. package/dist/apps/admin/distribution-lists/[uid].js +3 -3
  127. package/dist/apps/admin/distribution-lists/index.js +3 -3
  128. package/dist/apps/admin/distribution-lists/new/index.js +3 -3
  129. package/dist/apps/admin/domains/[uid].js +6 -5
  130. package/dist/apps/admin/domains/index.js +3 -3
  131. package/dist/apps/admin/domains/new/index.js +3 -3
  132. package/dist/apps/admin/escrow-scopes/[uid].js +11 -6
  133. package/dist/apps/admin/escrow-scopes/index.js +3 -3
  134. package/dist/apps/admin/escrow-scopes/new/index.js +9 -4
  135. package/dist/apps/admin/index.js +4 -4
  136. package/dist/apps/admin/ingest-queue/index.js +3 -3
  137. package/dist/apps/admin/mailboxes/[uid].js +15 -7
  138. package/dist/apps/admin/quarantine/index.js +4 -4
  139. package/dist/apps/admin/retention-policy/index.js +3 -3
  140. package/dist/apps/admin/signing-certificates/index.js +3 -3
  141. package/dist/apps/admin/transport-rules/[uid].js +4 -4
  142. package/dist/apps/admin/transport-rules/index.js +3 -3
  143. package/dist/apps/admin/transport-rules/new/index.js +3 -3
  144. package/dist/apps/escrow/matters/[uid].js +8 -3
  145. package/dist/apps/escrow/matters/new/index.js +4 -0
  146. package/dist/apps/shared/calendar/calendarLiveUpdates.d.ts +11 -7
  147. package/dist/apps/shared/calendar/calendarLiveUpdates.js +10 -9
  148. package/dist/apps/shared/calendar/useCalendarReminders.js +2 -0
  149. package/dist/apps/shared/components/admin/ActionAlert.d.ts +13 -0
  150. package/dist/apps/shared/components/admin/ActionAlert.js +24 -0
  151. package/dist/apps/shared/components/admin/diagnostics/InformationPanel.js +1 -1
  152. package/dist/apps/shared/components/admin/diagnostics/InstalledPlugins.js +1 -1
  153. package/dist/apps/shared/components/admin/diagnostics/LogsPanel.js +1 -1
  154. package/dist/apps/shared/components/admin/diagnostics/RuntimePanel.js +1 -1
  155. package/dist/apps/shared/components/admin/diagnostics/SystemPanel.js +1 -1
  156. package/dist/apps/shared/components/admin/distributionLists/MemberListCard.js +4 -4
  157. package/dist/apps/shared/components/admin/elevation.d.ts +9 -0
  158. package/dist/apps/shared/components/admin/elevation.js +16 -0
  159. package/dist/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.d.ts +2 -0
  160. package/dist/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.js +41 -1
  161. package/dist/apps/shared/components/admin/layout/AdminShell.js +2 -1
  162. package/dist/apps/shared/components/admin/mailboxes/EraseLeftoverDataDialog.js +3 -3
  163. package/dist/apps/shared/components/admin/mailboxes/EscrowScopeCard.js +4 -4
  164. package/dist/apps/shared/components/admin/mailboxes/LeftoverMailboxesSection.js +4 -4
  165. package/dist/apps/shared/components/admin/mailboxes/ResourceSettingsCard.js +3 -3
  166. package/dist/apps/shared/components/admin/mailboxes/ShareAccessCard.js +6 -6
  167. package/dist/apps/shared/components/admin/settings/BrandingForm.js +17 -7
  168. package/dist/apps/shared/components/admin/settings/DomainDnsSetup.js +4 -4
  169. package/dist/apps/shared/components/admin/settings/EncryptionPolicyForm.js +17 -5
  170. package/dist/apps/shared/components/admin/settings/LoadedSettingsForm.js +3 -3
  171. package/dist/apps/shared/components/admin/settings/MailboxCreateForm.js +8 -3
  172. package/dist/apps/shared/components/admin/settings/MailboxPolicyForm.js +3 -3
  173. package/dist/apps/shared/components/admin/settings/PluginsManager.js +26 -12
  174. package/dist/apps/shared/components/admin/settings/RetentionPolicyForm.js +17 -10
  175. package/dist/apps/shared/components/admin/setup/EscrowSetupStep.js +18 -8
  176. package/dist/apps/shared/components/admin/setup/SetupWizard.js +8 -6
  177. package/dist/apps/shared/components/admin/signOut.js +3 -0
  178. package/dist/apps/shared/components/admin/usePagedList.js +2 -2
  179. package/dist/apps/shared/components/calendar/DescriptionEditor.js +2 -1
  180. package/dist/apps/shared/components/calendar/EventDetails.d.ts +3 -1
  181. package/dist/apps/shared/components/calendar/EventDetails.js +7 -4
  182. package/dist/apps/shared/components/calendar/EventEditor.js +81 -33
  183. package/dist/apps/shared/components/calendar/EventFormParts.d.ts +3 -1
  184. package/dist/apps/shared/components/calendar/EventFormParts.js +3 -3
  185. package/dist/apps/shared/components/calendar/EventMatchNav.d.ts +1 -1
  186. package/dist/apps/shared/components/calendar/EventMatchNav.js +12 -2
  187. package/dist/apps/shared/components/calendar/EventModal.js +10 -2
  188. package/dist/apps/shared/components/calendar/FindATime.js +4 -3
  189. package/dist/apps/shared/components/calendar/ListView.d.ts +10 -4
  190. package/dist/apps/shared/components/calendar/ListView.js +92 -27
  191. package/dist/apps/shared/components/calendar/RecurrenceEditor.d.ts +4 -1
  192. package/dist/apps/shared/components/calendar/RecurrenceEditor.js +15 -10
  193. package/dist/apps/shared/components/calendar/SplitDayView.js +11 -17
  194. package/dist/apps/shared/components/calendar/TaskQuickForm.js +3 -2
  195. package/dist/apps/shared/components/calendar/TimeGridView.js +23 -21
  196. package/dist/apps/shared/components/calendar/gridLayout.d.ts +41 -0
  197. package/dist/apps/shared/components/calendar/gridLayout.js +83 -0
  198. package/dist/apps/shared/components/calendar/layout/CalendarShell.js +1 -1
  199. package/dist/apps/shared/components/calendar/timePicker.js +5 -2
  200. package/dist/apps/shared/components/contacts/ContactCard.js +9 -3
  201. package/dist/apps/shared/components/contacts/ContactDetailPane.d.ts +5 -3
  202. package/dist/apps/shared/components/contacts/ContactDetailPane.js +11 -5
  203. package/dist/apps/shared/components/contacts/ContactForm.js +54 -16
  204. package/dist/apps/shared/components/contacts/ContactPhotoEditor.d.ts +21 -0
  205. package/dist/apps/shared/components/contacts/ContactPhotoEditor.js +56 -0
  206. package/dist/apps/shared/components/contacts/ContactPhotoField.d.ts +7 -4
  207. package/dist/apps/shared/components/contacts/ContactPhotoField.js +19 -21
  208. package/dist/apps/shared/components/contacts/layout/ContactsShell.js +1 -1
  209. package/dist/apps/shared/components/contacts/participantDetails.d.ts +4 -1
  210. package/dist/apps/shared/components/contacts/participantDetails.js +17 -1
  211. package/dist/apps/shared/components/layout/AppShell.js +9 -0
  212. package/dist/apps/shared/components/layout/KeyEnrollmentGate.js +2 -3
  213. package/dist/apps/shared/components/layout/MailboxProvisioning.d.ts +4 -1
  214. package/dist/apps/shared/components/layout/MailboxProvisioning.js +24 -2
  215. package/dist/apps/shared/components/layout/RecoveryCodeUnlock.d.ts +4 -2
  216. package/dist/apps/shared/components/layout/RecoveryCodeUnlock.js +4 -2
  217. package/dist/apps/shared/components/mail/ConversationList.js +49 -6
  218. package/dist/apps/shared/components/mail/ConversationThreadPane.js +1 -1
  219. package/dist/apps/shared/components/mail/MenuButton.d.ts +2 -78
  220. package/dist/apps/shared/components/mail/MenuButton.js +2 -211
  221. package/dist/apps/shared/components/mail/MessageDetailPane.js +10 -6
  222. package/dist/apps/shared/components/mail/compose/ComposeToolbar.d.ts +3 -0
  223. package/dist/apps/shared/components/mail/compose/ComposeToolbar.js +10 -1
  224. package/dist/apps/shared/components/mail/compose/ComposeWindow.js +55 -10
  225. package/dist/apps/shared/components/mail/compose/GifPicker.js +10 -4
  226. package/dist/apps/shared/components/mail/compose/RecipientInput.js +9 -7
  227. package/dist/apps/shared/components/mail/compose/composeFlushRegistry.d.ts +7 -1
  228. package/dist/apps/shared/components/mail/compose/composeFlushRegistry.js +13 -1
  229. package/dist/apps/shared/components/mail/compose/encryptionRequirement.d.ts +5 -0
  230. package/dist/apps/shared/components/mail/compose/quotedBody.js +28 -5
  231. package/dist/apps/shared/components/mail/layout/MailShell.js +1 -1
  232. package/dist/apps/shared/components/mail/reading/MessageBody.js +11 -6
  233. package/dist/apps/shared/components/mail/reading/bodyContent.js +2 -0
  234. package/dist/apps/shared/components/mail/reading/bodyHtml.d.ts +5 -1
  235. package/dist/apps/shared/components/mail/reading/bodyHtml.js +23 -2
  236. package/dist/apps/shared/components/mail/writableMailboxes.js +2 -0
  237. package/dist/apps/shared/components/rules/RuleBuilder.js +9 -1
  238. package/dist/apps/shared/components/settings/GravatarSetting.d.ts +6 -0
  239. package/dist/apps/shared/components/settings/GravatarSetting.js +15 -0
  240. package/dist/apps/shared/components/settings/layout/SettingsShell.js +1 -1
  241. package/dist/apps/shared/components/tasks/dueDate.d.ts +12 -0
  242. package/dist/apps/shared/components/tasks/dueDate.js +26 -0
  243. package/dist/apps/shared/components/tasks/layout/TasksShell.js +1 -1
  244. package/dist/apps/shared/mail/decryptedMessages.d.ts +8 -0
  245. package/dist/apps/shared/mail/decryptedMessages.js +46 -2
  246. package/dist/apps/shared/mail/listSnapshots.js +2 -0
  247. package/dist/apps/shared/mail/outbox/sendDecision.d.ts +6 -3
  248. package/dist/apps/shared/mail/outbox/sendDecision.js +13 -3
  249. package/dist/apps/shared/mail/outbox/sendJob.d.ts +1 -0
  250. package/dist/apps/shared/mail/outbox/sendJob.js +5 -1
  251. package/dist/apps/shared/mail/useMailLiveUpdates.d.ts +3 -0
  252. package/dist/apps/shared/mail/useMailLiveUpdates.js +6 -1
  253. package/dist/apps/shared/mail/usePermanentDelete.d.ts +2 -0
  254. package/dist/apps/shared/mail/usePermanentDelete.js +1 -1
  255. package/dist/apps/shared/notifications/apiErrors.js +2 -0
  256. package/dist/apps/shared/notifications/preferences.d.ts +1 -1
  257. package/dist/apps/shared/notifications/preferences.js +1 -1
  258. package/dist/apps/shared/notifications/store.js +3 -1
  259. package/dist/apps/shared/plugins/pluginNav.d.ts +2 -1
  260. package/dist/apps/shared/plugins/pluginNav.js +4 -2
  261. package/dist/apps/www/calendar/index.js +24 -12
  262. package/dist/apps/www/contacts/[uid].js +6 -2
  263. package/dist/apps/www/contacts/index.js +62 -11
  264. package/dist/apps/www/index.js +85 -30
  265. package/dist/apps/www/messages/[uid].js +8 -3
  266. package/dist/apps/www/settings/auto-reply/index.js +13 -1
  267. package/dist/apps/www/settings/blocked-senders/index.js +4 -0
  268. package/dist/apps/www/settings/encryption/index.js +29 -5
  269. package/dist/apps/www/settings/privacy/index.js +3 -2
  270. package/dist/apps/www/settings/profile/index.js +4 -1
  271. package/dist/apps/www/settings/sharing/index.js +10 -1
  272. package/dist/apps/www/settings/signatures/[uid].js +6 -4
  273. package/dist/apps/www/tasks/index.js +94 -32
  274. package/dist/lib/admin/auditLogApi.js +2 -2
  275. package/dist/lib/admin/distributionListsApi.js +2 -2
  276. package/dist/lib/admin/domainsApi.js +2 -2
  277. package/dist/lib/admin/escrowAuditLogApi.js +2 -2
  278. package/dist/lib/admin/escrowScopesApi.js +2 -2
  279. package/dist/lib/admin/pluginsApi.d.ts +13 -2
  280. package/dist/lib/admin/pluginsApi.js +8 -0
  281. package/dist/lib/admin/retentionPolicyApi.d.ts +3 -0
  282. package/dist/lib/admin/retentionPolicyApi.js +3 -0
  283. package/dist/lib/admin/transportRulesApi.js +2 -2
  284. package/dist/lib/appearance/preferencesApi.d.ts +1 -1
  285. package/dist/lib/appearance/preferencesApi.js +3 -14
  286. package/dist/lib/auth/session.d.ts +51 -12
  287. package/dist/lib/auth/session.js +214 -38
  288. package/dist/lib/branding/brandingApi.d.ts +3 -3
  289. package/dist/lib/branding/brandingApi.js +9 -21
  290. package/dist/lib/calendar/calendarApi.js +3 -1
  291. package/dist/lib/calendar/eventSearch.js +43 -6
  292. package/dist/lib/calendar/freeBusyApi.d.ts +2 -1
  293. package/dist/lib/calendar/freeBusyApi.js +13 -5
  294. package/dist/lib/calendar/recurrence.js +51 -3
  295. package/dist/lib/calendar/useDayKey.d.ts +7 -0
  296. package/dist/lib/calendar/useDayKey.js +43 -0
  297. package/dist/lib/components/avatar/ContactAvatar.d.ts +6 -2
  298. package/dist/lib/components/avatar/ContactAvatar.js +37 -8
  299. package/dist/lib/components/avatar/ContactPhotoAvatar.d.ts +11 -0
  300. package/dist/lib/components/avatar/ContactPhotoAvatar.js +18 -0
  301. package/dist/lib/components/forms/CameraCaptureModal.d.ts +17 -0
  302. package/dist/lib/components/forms/CameraCaptureModal.js +88 -0
  303. package/dist/lib/components/forms/ImageEditBadge.d.ts +28 -0
  304. package/dist/lib/components/forms/ImageEditBadge.js +62 -0
  305. package/dist/lib/components/menus/MenuButton.d.ts +78 -0
  306. package/dist/lib/components/menus/MenuButton.js +219 -0
  307. package/dist/lib/components/navigation/BottomTabBar.d.ts +3 -1
  308. package/dist/lib/components/navigation/BottomTabBar.js +22 -3
  309. package/dist/lib/components/overlays/PopoverPortal.d.ts +2 -1
  310. package/dist/lib/components/overlays/PopoverPortal.js +3 -2
  311. package/dist/lib/components/pickers/TimeZonePicker.js +13 -1
  312. package/dist/lib/contacts/contactsApi.d.ts +5 -2
  313. package/dist/lib/contacts/contactsApi.js +16 -19
  314. package/dist/lib/contacts/gravatar.d.ts +14 -3
  315. package/dist/lib/contacts/gravatar.js +42 -5
  316. package/dist/lib/contacts/preparePhoto.d.ts +24 -0
  317. package/dist/lib/contacts/preparePhoto.js +250 -0
  318. package/dist/lib/contacts/useContactPhotoSrc.d.ts +19 -0
  319. package/dist/lib/contacts/useContactPhotoSrc.js +153 -0
  320. package/dist/lib/contacts/vcard.d.ts +3 -0
  321. package/dist/lib/contacts/vcard.js +59 -7
  322. package/dist/lib/crypto/escrowKeys.d.ts +8 -0
  323. package/dist/lib/crypto/escrowKeys.js +20 -1
  324. package/dist/lib/crypto/messageSecurity.d.ts +7 -4
  325. package/dist/lib/crypto/messageSecurity.js +23 -5
  326. package/dist/lib/crypto/passwordUnlock.d.ts +6 -1
  327. package/dist/lib/crypto/passwordUnlock.js +13 -2
  328. package/dist/lib/crypto/smime.js +12 -2
  329. package/dist/lib/mail/compose/composeQuoting.js +3 -2
  330. package/dist/lib/mail/compose/quotedImages.d.ts +6 -3
  331. package/dist/lib/mail/compose/quotedImages.js +35 -9
  332. package/dist/lib/mail/labelsApi.js +2 -2
  333. package/dist/lib/mail/mailApi.d.ts +8 -5
  334. package/dist/lib/mail/mailApi.js +73 -39
  335. package/dist/lib/mail/mailDetailHooks.js +11 -3
  336. package/dist/lib/mail/mailFilterRulesApi.js +2 -2
  337. package/dist/lib/mail/mailSignaturesApi.js +2 -2
  338. package/dist/lib/mail/mailboxImportApi.d.ts +1 -1
  339. package/dist/lib/mail/mailboxImportApi.js +3 -15
  340. package/dist/lib/mail/messageBodySanitizer.d.ts +6 -0
  341. package/dist/lib/mail/messageBodySanitizer.js +44 -1
  342. package/dist/lib/search/queryGrammar.js +12 -4
  343. package/dist/lib/search/searchTier3.d.ts +6 -1
  344. package/dist/lib/search/searchTier3.js +54 -25
  345. package/dist/lib/tasks/tasksApi.js +3 -3
  346. package/dist/lib/util/api.d.ts +26 -4
  347. package/dist/lib/util/api.js +73 -16
  348. package/dist/lib/util/apiQuery.d.ts +11 -0
  349. package/dist/lib/util/apiQuery.js +11 -0
  350. package/dist/lib/util/settleWithLimit.d.ts +5 -0
  351. package/dist/lib/util/settleWithLimit.js +25 -0
  352. package/lib/admin/auditLogApi.ts +2 -2
  353. package/lib/admin/distributionListsApi.ts +2 -2
  354. package/lib/admin/domainsApi.ts +2 -2
  355. package/lib/admin/escrowAuditLogApi.ts +2 -2
  356. package/lib/admin/escrowScopesApi.ts +2 -2
  357. package/lib/admin/pluginsApi.ts +21 -2
  358. package/lib/admin/retentionPolicyApi.ts +4 -0
  359. package/lib/admin/transportRulesApi.ts +2 -2
  360. package/lib/appearance/preferencesApi.ts +3 -14
  361. package/lib/auth/session.ts +239 -39
  362. package/lib/branding/brandingApi.ts +9 -21
  363. package/lib/calendar/calendarApi.ts +3 -1
  364. package/lib/calendar/eventSearch.ts +46 -6
  365. package/lib/calendar/freeBusyApi.ts +14 -6
  366. package/lib/calendar/recurrence.ts +59 -8
  367. package/lib/calendar/useDayKey.ts +45 -0
  368. package/lib/components/avatar/ContactAvatar.tsx +46 -8
  369. package/lib/components/avatar/ContactPhotoAvatar.tsx +26 -0
  370. package/lib/components/forms/CameraCaptureModal.tsx +133 -0
  371. package/lib/components/forms/ImageEditBadge.tsx +121 -0
  372. package/lib/components/menus/MenuButton.tsx +408 -0
  373. package/lib/components/navigation/BottomTabBar.tsx +30 -8
  374. package/lib/components/overlays/PopoverPortal.tsx +3 -2
  375. package/lib/components/pickers/TimeZonePicker.tsx +17 -1
  376. package/lib/contacts/contactsApi.ts +18 -19
  377. package/lib/contacts/gravatar.ts +47 -5
  378. package/lib/contacts/preparePhoto.ts +273 -0
  379. package/lib/contacts/useContactPhotoSrc.ts +177 -0
  380. package/lib/contacts/vcard.ts +60 -7
  381. package/lib/crypto/escrowKeys.ts +21 -1
  382. package/lib/crypto/messageSecurity.ts +33 -9
  383. package/lib/crypto/passwordUnlock.ts +14 -2
  384. package/lib/crypto/smime.ts +13 -2
  385. package/lib/mail/compose/composeQuoting.ts +3 -2
  386. package/lib/mail/compose/quotedImages.ts +39 -9
  387. package/lib/mail/labelsApi.ts +2 -2
  388. package/lib/mail/mailApi.ts +96 -39
  389. package/lib/mail/mailDetailHooks.ts +11 -3
  390. package/lib/mail/mailFilterRulesApi.ts +2 -2
  391. package/lib/mail/mailSignaturesApi.ts +2 -2
  392. package/lib/mail/mailboxImportApi.ts +9 -15
  393. package/lib/mail/messageBodySanitizer.ts +47 -1
  394. package/lib/search/queryGrammar.ts +13 -4
  395. package/lib/search/searchTier3.ts +65 -32
  396. package/lib/tasks/tasksApi.ts +3 -3
  397. package/lib/util/api.ts +99 -16
  398. package/lib/util/apiQuery.ts +13 -0
  399. package/lib/util/settleWithLimit.ts +25 -0
  400. package/package.json +1 -1
@@ -21,6 +21,7 @@ import { searchLocalIndex } from "../shared/search/searchTier2.js";
21
21
  import { SEARCH_LIMITS, classifyFailure, describeFailures, hitKey, mergeSearchResults, runLimited, searchPageSize, tagHits, withTimeout, } from "../shared/search/crossMailboxSearch.js";
22
22
  import { getUnlockedKeys, subscribeKeySession } from "../../lib/crypto/keySession.js";
23
23
  import { useMessageAttachments } from "../../lib/mail/mailDetailHooks.js";
24
+ import { settleWithLimit } from "../../lib/util/settleWithLimit.js";
24
25
  import useIsMobile from "../../lib/util/useIsMobile.js";
25
26
  import MailShell, { useMailShell, } from "../shared/components/mail/layout/MailShell.js";
26
27
  import MailAddress from "../shared/components/mail/MailAddress.js";
@@ -34,7 +35,7 @@ import { setReadStateMany } from "../shared/mail/messageReadState.js";
34
35
  import { useMarkMessageRead } from "../shared/mail/useMarkMessageRead.js";
35
36
  import { ROW_FOCUS_CLASS, UnreadBar, UnreadLabel, dateClass, isUnread, rowClass, senderClass, subjectClass } from "../shared/components/mail/unreadStyle.js";
36
37
  import { LazyConversationThreadPane, LazyMessageDetailPane, prefetchReadingPane } from "../shared/components/mail/LazyReadingPane.js";
37
- import { rememberDecrypted } from "../shared/mail/decryptedMessages.js";
38
+ import { decryptFailedBefore, mapWithConcurrency, markDecryptFailed, rememberDecrypted } from "../shared/mail/decryptedMessages.js";
38
39
  import ConversationList, { conversationRowKey } from "../shared/components/mail/ConversationList.js";
39
40
  import SwipeRow from "../shared/components/mail/SwipeRow.js";
40
41
  import InviteRowChip from "../shared/components/mail/invite/InviteRowChip.js";
@@ -57,6 +58,8 @@ import { notifyApiError } from "../shared/notifications/apiErrors.js";
57
58
  const MESSAGE_PAGE_SIZE = 50;
58
59
  const SEARCH_DEBOUNCE_MS = 300;
59
60
  const LIST_PREVIEW_MAX_LENGTH = 160;
61
+ /** How many encrypted rows are fetched and decrypted at once. */
62
+ const DECRYPT_CONCURRENCY = 4;
60
63
  /** The literal outer-envelope `Subject` every encrypted message carries server-side - RFC 9788's
61
64
  * `hcp_baseline` policy obscures it to this exact string (see `smimeMessage.ts`'s
62
65
  * `applyBaselineOuterHeaders()`), which is also all `Message.subject` ever shows for one of these until
@@ -90,10 +93,12 @@ function stripHtmlToText(html) {
90
93
  * and `handleUnlockList()`), and a single row's fetch/decrypt failure never blocks the rest.
91
94
  */
92
95
  async function decryptEncryptedRows(messages, unlocked) {
93
- const encrypted = messages.filter((m) => m.subject === ENCRYPTED_SUBJECT_PLACEHOLDER);
96
+ // A row that already came to nothing since the last unlock is not downloaded again by every refresh of the list.
97
+ const encrypted = messages.filter((m) => m.subject === ENCRYPTED_SUBJECT_PLACEHOLDER && !decryptFailedBefore(m.uid));
94
98
  // Imported once for all the rows, by whichever needs it first.
95
99
  let securityModule;
96
- const entries = await Promise.all(encrypted.map(async (message) => {
100
+ // A few at a time: each is a download and a public-key decryption.
101
+ const entries = await mapWithConcurrency(encrypted, DECRYPT_CONCURRENCY, async (message) => {
97
102
  try {
98
103
  const rawMime = await getMessageRawContent(message.uid);
99
104
  // Loaded here, on first use: the S/MIME code (PKI.js, ASN.1, X.509) is over half a megabyte and an inbox with
@@ -101,6 +106,7 @@ async function decryptEncryptedRows(messages, unlocked) {
101
106
  const { evaluateMessageSecurity } = await (securityModule ?? (securityModule = import("../../lib/crypto/messageSecurity.js")));
102
107
  const security = await evaluateMessageSecurity(rawMime, unlocked);
103
108
  if (!security.subject && !security.html) {
109
+ markDecryptFailed(message.uid);
104
110
  return null;
105
111
  }
106
112
  const preview = security.html ? stripHtmlToText(security.html).slice(0, LIST_PREVIEW_MAX_LENGTH) : undefined;
@@ -108,9 +114,10 @@ async function decryptEncryptedRows(messages, unlocked) {
108
114
  return [message.uid, { subject: security.subject, preview }];
109
115
  }
110
116
  catch {
117
+ markDecryptFailed(message.uid);
111
118
  return null;
112
119
  }
113
- }));
120
+ });
114
121
  const result = {};
115
122
  for (const entry of entries) {
116
123
  if (entry) {
@@ -319,6 +326,10 @@ function appendUnseenRows(shown, more, idOf, cap) {
319
326
  }
320
327
  const messageUid = (message) => message.uid;
321
328
  const conversationKey = conversationRowKey;
329
+ /** How many conversations' messages are requested at once when many are ticked together (Select all on a long list). */
330
+ const RESOLVE_CONVERSATIONS_AT_ONCE = 5;
331
+ /** What of a conversation's summary changes when one of its messages does: a different one means the messages fetched for it are out of date. */
332
+ const conversationSignature = (row) => [row.messageUids.join(","), row.unreadCount, row.flagged, row.folderUids.join(","), row.latestFolderUid].join("|");
322
333
  /** Flattens and sorts a per-mailbox fetch into one merged, newest-first list - the aggregate ("All
323
334
  * Inboxes" etc.) equivalent of `mergeSearchResults()` above, but simpler: an aggregated message has no
324
335
  * natural relevance score to normalize, so this only ever sorts by `receivedDate`. */
@@ -330,7 +341,9 @@ function mergeInboxMessages(perMailbox) {
330
341
  /**
331
342
  * Fans out one `listMessages()` call per accessible mailbox that has a folder of `type`, merges the
332
343
  * results newest-first. A mailbox with no matching folder, or whose fetch fails, simply contributes
333
- * nothing - one mailbox's absence/failure must not blank out every other mailbox's messages.
344
+ * nothing - one mailbox's absence/failure must not blank out every other mailbox's messages. `listed`, the
345
+ * rows on screen when this is a refresh of them, makes a mailbox whose fetch fails keep the rows it has there
346
+ * instead of contributing none (a timeout must not make its mail vanish until the next success).
334
347
  *
335
348
  * **Pagination scope trim (deliberate, matching this file's own documented Tier 2/3 tradeoffs)**: there is
336
349
  * no composite cursor across an arbitrary number of independently-paginated mailboxes in this pass - this
@@ -339,7 +352,7 @@ function mergeInboxMessages(perMailbox) {
339
352
  * composite-cursor "load more" per mailbox is a natural v2 if usage shows people scrolling past the first
340
353
  * page in aggregate view often.
341
354
  */
342
- async function fetchAggregateMessages(mailboxFolders, type, filter, client) {
355
+ async function fetchAggregateMessages(mailboxFolders, type, filter, client, listed) {
343
356
  const perMailbox = await Promise.all(mailboxFolders.map(async ({ mailbox, folders }) => {
344
357
  const folder = folders.find((f) => f.type === type);
345
358
  if (!folder) {
@@ -349,7 +362,7 @@ async function fetchAggregateMessages(mailboxFolders, type, filter, client) {
349
362
  // this function's own pagination scope trim) - each mailbox contributes its own newest page and
350
363
  // they're merged newest-first, which a different sort key couldn't be made honest across an
351
364
  // arbitrary number of independently-paged folders.
352
- const messages = await listMessages(folder.uid, { limit: MESSAGE_PAGE_SIZE, filter }, client).catch(() => []);
365
+ const messages = await listMessages(folder.uid, { limit: MESSAGE_PAGE_SIZE, filter }, client).catch(() => (listed ?? []).filter((m) => m.mailboxUid === mailbox.uid));
353
366
  return { mailbox, messages };
354
367
  }));
355
368
  return mergeInboxMessages(perMailbox);
@@ -358,16 +371,17 @@ async function fetchAggregateMessages(mailboxFolders, type, filter, client) {
358
371
  * The conversation-list counterpart of `fetchAggregateMessages()`: one `listConversations()` per accessible mailbox that has a folder of
359
372
  * `type`, each scoped to *that folder* (so "All Inboxes" groups only Inbox mail - not the Sent Items copy of a reply, which a mailbox-wide
360
373
  * grouping pulls in) and each row tagged with its mailbox, then merged newest activity first. Same first-page-only scope, and the same
361
- * rule that a mailbox that has no such folder or fails to answer contributes nothing rather than blanking the rest.
374
+ * rule that a mailbox that has no such folder or fails to answer contributes nothing rather than blanking the rest - or, with `listed` (the rows on
375
+ * screen, for a refresh of them), keeps the rows it has there.
362
376
  */
363
- async function fetchAggregateConversations(mailboxFolders, type, params, client) {
377
+ async function fetchAggregateConversations(mailboxFolders, type, params, client, listed) {
364
378
  const perMailbox = await Promise.all(mailboxFolders.map(async ({ mailbox, folders }) => {
365
379
  const folder = folders.find((f) => f.type === type);
366
380
  if (!folder) {
367
381
  return [];
368
382
  }
369
- const rows = await listConversations(mailbox.uid, { ...params, folderUid: folder.uid, page: 0 }, client).catch(() => []);
370
- return rows.map((row) => ({ ...row, mailboxUid: mailbox.uid }));
383
+ const rows = await listConversations(mailbox.uid, { ...params, folderUid: folder.uid, page: 0 }, client).catch(() => null);
384
+ return rows ? rows.map((row) => ({ ...row, mailboxUid: mailbox.uid })) : (listed ?? []).filter((row) => row.mailboxUid === mailbox.uid);
371
385
  }));
372
386
  return perMailbox.flat().sort((a, b) => Date.parse(b.latestDate) - Date.parse(a.latestDate));
373
387
  }
@@ -621,6 +635,8 @@ function InboxContent({ userUid }) {
621
635
  messagesRef.current = messages;
622
636
  const conversationsRef = useRef(conversations);
623
637
  conversationsRef.current = conversations;
638
+ const selectedConversationIdsRef = useRef(selectedConversationIds);
639
+ selectedConversationIdsRef.current = selectedConversationIds;
624
640
  // Reset to a fresh Map at the start of every new search pass (see the search effect below) - see
625
641
  // resolveHitsToMessages()'s own doc comment on why this needs to persist *within* one pass but not
626
642
  // across passes (a stale `null` for a uid that's since become resolvable elsewhere must not stick).
@@ -1256,8 +1272,9 @@ function InboxContent({ userUid }) {
1256
1272
  try {
1257
1273
  if (asConversations && aggregateFolderType) {
1258
1274
  // No paging here either: the fresh merged first pages are the list.
1259
- const fresh = await fetchAggregateConversations(mailboxFolders, aggregateFolderType, conversationParams(0), client);
1275
+ const fresh = await fetchAggregateConversations(mailboxFolders, aggregateFolderType, conversationParams(0), client, conversationsRef.current);
1260
1276
  if (isCurrent()) {
1277
+ refreshConversationMessages(conversationsRef.current, fresh);
1261
1278
  setConversations(fresh);
1262
1279
  }
1263
1280
  }
@@ -1267,13 +1284,14 @@ function InboxContent({ userUid }) {
1267
1284
  return;
1268
1285
  }
1269
1286
  const merged = mergeFirstPage(conversationsRef.current, fresh, conversationKey, MESSAGE_PAGE_SIZE);
1287
+ refreshConversationMessages(conversationsRef.current, merged.rows);
1270
1288
  setConversations(merged.rows);
1271
1289
  listedOffsetRef.current = merged.complete ? fresh.length : listedOffsetRef.current + merged.added;
1272
1290
  setHasMore(!merged.complete);
1273
1291
  }
1274
1292
  else if (aggregateFolderType) {
1275
1293
  // No paging here (see `fetchAggregateMessages()`): the fresh merged first pages are the list.
1276
- const fresh = await fetchAggregateMessages(mailboxFolders, aggregateFolderType, effectiveFilter, client);
1294
+ const fresh = await fetchAggregateMessages(mailboxFolders, aggregateFolderType, effectiveFilter, client, messagesRef.current);
1277
1295
  if (isCurrent()) {
1278
1296
  setMessages(fresh);
1279
1297
  }
@@ -1685,35 +1703,66 @@ function InboxContent({ userUid }) {
1685
1703
  return next;
1686
1704
  });
1687
1705
  }
1688
- /** Loads (once) the messages behind each of `ids`, so a ticked conversation resolves to the messages
1689
- * every bulk action below acts on. If any of them fails to load, none of that batch stays ticked and
1690
- * a pop-up says why - better than acting on the part of a selection that happened to arrive. */
1691
- async function resolveConversations(rows) {
1692
- const missing = rows.filter((row) => !conversationMessagesById[conversationKey(row)]);
1706
+ /** Loads (once, or again with `refetch`) the messages behind each of `rows`, so a ticked conversation resolves to the messages
1707
+ * every bulk action below acts on - a few requests at a time (Select all on a long list is hundreds of rows). A conversation
1708
+ * that fails to load is unticked, with one pop-up saying why - better than acting on a part of it - and the rest stay ticked.
1709
+ * What arrives after the list was loaded again is dropped: those rows are gone. */
1710
+ async function resolveConversations(rows, refetch = false) {
1711
+ const missing = rows.filter((row) => refetch || !conversationMessagesById[conversationKey(row)]);
1693
1712
  if (missing.length === 0) {
1694
1713
  return;
1695
1714
  }
1715
+ const listRun = searchRunIdRef.current;
1696
1716
  setResolvingSelection((n) => n + 1);
1697
1717
  try {
1698
- const loaded = await Promise.all(
1718
+ const outcomes = await settleWithLimit(missing, RESOLVE_CONVERSATIONS_AT_ONCE, (row) =>
1699
1719
  // A row of the "All mailboxes" view names its own mailbox; any other list is the open mailbox's.
1700
- missing.map(async (row) => [conversationKey(row), await listConversationMessages(row.mailboxUid ?? activeMailboxUid, row.conversationId, {}, client)]));
1701
- setConversationMessagesById((prev) => ({ ...prev, ...Object.fromEntries(loaded) }));
1702
- }
1703
- catch (err) {
1704
- notifyApiError(err, "Couldn't load the messages in one of those conversations");
1705
- setSelectedConversationIds((prev) => {
1706
- const next = new Set(prev);
1707
- for (const row of missing) {
1708
- next.delete(conversationKey(row));
1720
+ searchRunIdRef.current === listRun
1721
+ ? listConversationMessages(row.mailboxUid ?? activeMailboxUid, row.conversationId, {}, client)
1722
+ : Promise.reject(new Error("The list was loaded again")));
1723
+ if (searchRunIdRef.current !== listRun) {
1724
+ return;
1725
+ }
1726
+ const loaded = {};
1727
+ const failed = new Set();
1728
+ let firstFailure;
1729
+ outcomes.forEach((outcome, index) => {
1730
+ const key = conversationKey(missing[index]);
1731
+ if (outcome.status === "fulfilled") {
1732
+ loaded[key] = outcome.value;
1733
+ }
1734
+ else {
1735
+ failed.add(key);
1736
+ firstFailure ?? (firstFailure = outcome.reason);
1709
1737
  }
1710
- return next;
1711
1738
  });
1739
+ setConversationMessagesById((prev) => ({ ...prev, ...loaded }));
1740
+ if (failed.size > 0) {
1741
+ notifyApiError(firstFailure, "Couldn't load the messages in one of those conversations");
1742
+ setSelectedConversationIds((prev) => new Set([...prev].filter((key) => !failed.has(key))));
1743
+ }
1712
1744
  }
1713
1745
  finally {
1714
1746
  setResolvingSelection((n) => n - 1);
1715
1747
  }
1716
1748
  }
1749
+ /**
1750
+ * Called as a refresh of the list swaps in `after` for `before`: the messages kept for a conversation whose summary changed (a reply arrived, a
1751
+ * message was read or moved) are out of date - the next action on it would act on a list missing the new message, or send an old `version`. The
1752
+ * cached messages of one that is not ticked are dropped, to be fetched when next wanted; a ticked one's are fetched again now, the old ones standing
1753
+ * until they arrive.
1754
+ */
1755
+ function refreshConversationMessages(before, after) {
1756
+ const was = new Map(before.map((row) => [conversationKey(row), conversationSignature(row)]));
1757
+ const changed = after.filter((row) => was.has(conversationKey(row)) && was.get(conversationKey(row)) !== conversationSignature(row));
1758
+ if (changed.length === 0) {
1759
+ return;
1760
+ }
1761
+ const ticked = changed.filter((row) => selectedConversationIdsRef.current.has(conversationKey(row)));
1762
+ const stale = new Set(changed.filter((row) => !ticked.includes(row)).flatMap((row) => [conversationKey(row), row.conversationId]));
1763
+ setConversationMessagesById((prev) => Object.fromEntries(Object.entries(prev).filter(([key]) => !stale.has(key))));
1764
+ void resolveConversations(ticked, true);
1765
+ }
1717
1766
  function toggleConversationSelected(conversation) {
1718
1767
  const id = conversationKey(conversation);
1719
1768
  const ticking = !selectedConversationIds.has(id);
@@ -1955,7 +2004,7 @@ function InboxContent({ userUid }) {
1955
2004
  }
1956
2005
  /** Empty folder: permanently deletes everything in the folder being listed (Deleted Items or Junk Email) after the reader has confirmed. */
1957
2006
  async function emptyListedFolder(folder, count) {
1958
- const outcome = await permanent.requestEmptyFolder({ uid: folder.uid, name: folder.name }, count);
2007
+ const outcome = await permanent.requestEmptyFolder(folder, count);
1959
2008
  if (!outcome) {
1960
2009
  return;
1961
2010
  }
@@ -2325,6 +2374,12 @@ function InboxContent({ userUid }) {
2325
2374
  uid: emptiableFolders[0].uid,
2326
2375
  name: aggregateFolderType ? (emptiableFolders[0].type === "junk" ? "Junk Email" : "Deleted Items") : emptiableFolders[0].name,
2327
2376
  uids: aggregateFolderType ? emptiableFolders.map((f) => f.uid) : undefined,
2377
+ // The mailboxes whose folders these are, for the confirmation to name (it empties each of them, a share included).
2378
+ scope: aggregateFolderType
2379
+ ? mailboxFolders
2380
+ .filter((entry) => entry.folders.some((f) => f.type === aggregateFolderType))
2381
+ .map(({ mailbox }) => `${mailbox.displayName}${mailbox.ownerUserUid ? "" : " (shared)"}`)
2382
+ : undefined,
2328
2383
  };
2329
2384
  // How many it holds as the sidebar's counts have it (kept right as messages leave), unless that says none while the list shows some.
2330
2385
  const emptiableTotal = emptiableFolders.reduce((sum, f) => sum + folderCountOf(f).total, 0);
@@ -34,12 +34,17 @@ function MessageDetailContent({ uid }) {
34
34
  const [labels, setLabels] = useState([]);
35
35
  const conversationId = useSearchParams()[0].get("conversation");
36
36
  useEffect(() => {
37
+ // An answer for a message since replaced (the uid changed without the page remounting) must not be shown as the current one.
38
+ let cancelled = false;
37
39
  setLoading(true);
38
40
  setError(null);
39
41
  getMessage(uid, client)
40
- .then(setMessage)
41
- .catch((err) => setError(err instanceof ApiRequestError ? err.message : "Could not load this message."))
42
- .finally(() => setLoading(false));
42
+ .then((loaded) => !cancelled && setMessage(loaded))
43
+ .catch((err) => !cancelled && setError(err instanceof ApiRequestError ? err.message : "Could not load this message."))
44
+ .finally(() => !cancelled && setLoading(false));
45
+ return () => {
46
+ cancelled = true;
47
+ };
43
48
  }, [uid, client]);
44
49
  // See `apps/www/index.tsx`'s identical effect's own doc comment - a failure here just hides the
45
50
  // Labels control rather than blocking the rest of the page. Keyed on the message's own mailbox, not the
@@ -10,6 +10,7 @@ import { updateMailbox } from "../../../../lib/mail/mailApi.js";
10
10
  import { useApiClient } from "../../../../lib/util/apiClientContext.js";
11
11
  import SettingsShell, { useSettingsShell } from "../../../shared/components/settings/layout/SettingsShell.js";
12
12
  import Button from "../../../../lib/components/buttons/Button.js";
13
+ import Alert from "../../../../lib/components/feedback/Alert.js";
13
14
  import { notifyApiError } from "../../../shared/notifications/apiErrors.js";
14
15
  const INPUT_CLASS = "w-full text-sm py-2 px-3 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary";
15
16
  function SettingsAutoReplyPage(props) {
@@ -31,9 +32,20 @@ function AutoReplyContent() {
31
32
  const [version, setVersion] = useState(mailbox.version);
32
33
  const [saving, setSaving] = useState(false);
33
34
  const [saved, setSaved] = useState(false);
35
+ const [error, setError] = useState(null);
34
36
  async function handleSubmit(e) {
35
37
  e.preventDefault();
36
38
  setSaved(false);
39
+ // Turned on with nothing to send, or over a window that ends before it starts, would answer people with an empty message or never at all.
40
+ if (oofEnabled && !oofMessage.trim()) {
41
+ setError("Write the message to send while automatic replies are on.");
42
+ return;
43
+ }
44
+ if (oofEnabled && oofStartTime && oofEndTime && new Date(oofEndTime) <= new Date(oofStartTime)) {
45
+ setError("The end must be after the start.");
46
+ return;
47
+ }
48
+ setError(null);
37
49
  setSaving(true);
38
50
  try {
39
51
  const updated = await updateMailbox({
@@ -55,7 +67,7 @@ function AutoReplyContent() {
55
67
  setSaving(false);
56
68
  }
57
69
  }
58
- return (_jsx("div", { className: "flex-1 min-w-0 overflow-y-auto p-6", children: _jsxs("div", { className: "max-w-xl", children: [_jsx("h1", { className: "text-lg font-bold tracking-tight mb-1", children: "Automatic Replies" }), _jsxs("p", { className: "text-sm text-text-muted mb-4", children: ["Send an automatic reply to anyone who emails ", mailbox.displayName, " while you\u2019re away. A calendar event with its own automatic reply enabled takes over for its own start/end window."] }), saved && _jsx("div", { className: "mb-4 text-sm text-success font-medium", children: "Saved." }), _jsxs("form", { onSubmit: handleSubmit, className: "flex flex-col gap-4", children: [_jsxs("label", { className: "flex items-center gap-2 text-sm font-medium", children: [_jsx("input", { type: "checkbox", checked: oofEnabled, onChange: (e) => setOofEnabled(e.target.checked) }), "Automatic replies are on"] }), oofEnabled && (_jsxs(_Fragment, { children: [_jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Message" }), _jsx("textarea", { "aria-label": "Automatic reply message", className: INPUT_CLASS, rows: 5, value: oofMessage, onChange: (e) => setOofMessage(e.target.value) })] }), _jsxs("div", { className: "grid grid-cols-2 gap-3", children: [_jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Start (optional)" }), _jsx("input", { type: "datetime-local", "aria-label": "Automatic reply start", className: INPUT_CLASS, value: oofStartTime, onChange: (e) => setOofStartTime(e.target.value) })] }), _jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "End (optional)" }), _jsx("input", { type: "datetime-local", "aria-label": "Automatic reply end", className: INPUT_CLASS, value: oofEndTime, onChange: (e) => setOofEndTime(e.target.value) })] })] }), _jsx("p", { className: "text-xs text-text-muted -mt-2", children: "Leave both blank to stay on indefinitely until turned off." })] })), _jsx("div", { children: _jsx(Button, { type: "submit", loading: saving, disabled: saving, className: "!w-auto", children: "Save" }) })] })] }) }));
70
+ return (_jsx("div", { className: "flex-1 min-w-0 overflow-y-auto p-6", children: _jsxs("div", { className: "max-w-xl", children: [_jsx("h1", { className: "text-lg font-bold tracking-tight mb-1", children: "Automatic Replies" }), _jsxs("p", { className: "text-sm text-text-muted mb-4", children: ["Send an automatic reply to anyone who emails ", mailbox.displayName, " while you\u2019re away. A calendar event with its own automatic reply enabled takes over for its own start/end window."] }), saved && _jsx("div", { className: "mb-4 text-sm text-success font-medium", children: "Saved." }), error && _jsx(Alert, { children: error }), _jsxs("form", { onSubmit: handleSubmit, className: "flex flex-col gap-4", children: [_jsxs("label", { className: "flex items-center gap-2 text-sm font-medium", children: [_jsx("input", { type: "checkbox", checked: oofEnabled, onChange: (e) => setOofEnabled(e.target.checked) }), "Automatic replies are on"] }), oofEnabled && (_jsxs(_Fragment, { children: [_jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Message" }), _jsx("textarea", { "aria-label": "Automatic reply message", className: INPUT_CLASS, rows: 5, value: oofMessage, onChange: (e) => setOofMessage(e.target.value) })] }), _jsxs("div", { className: "grid grid-cols-2 gap-3", children: [_jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Start (optional)" }), _jsx("input", { type: "datetime-local", "aria-label": "Automatic reply start", className: INPUT_CLASS, value: oofStartTime, onChange: (e) => setOofStartTime(e.target.value) })] }), _jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "End (optional)" }), _jsx("input", { type: "datetime-local", "aria-label": "Automatic reply end", className: INPUT_CLASS, value: oofEndTime, onChange: (e) => setOofEndTime(e.target.value) })] })] }), _jsx("p", { className: "text-xs text-text-muted -mt-2", children: "Leave both blank to stay on indefinitely until turned off." })] })), _jsx("div", { children: _jsx(Button, { type: "submit", loading: saving, disabled: saving, className: "!w-auto", children: "Save" }) })] })] }) }));
59
71
  }
60
72
  export default SettingsAutoReplyPage;
61
73
  /** The tab's title: `Brand: Settings` (see `pageTitle()`). */
@@ -61,6 +61,10 @@ function ListSection({ kind, entries, otherEntries, ownAddresses, editable, onAd
61
61
  setError("That is one of your own addresses, so it cannot be blocked.");
62
62
  return;
63
63
  }
64
+ if (kind === "blocked" && checked.entry.startsWith("@") && ownAddresses.some((address) => address.toLowerCase().endsWith(checked.entry))) {
65
+ setError("That is your own domain: blocking it would send the mail of everyone at it - yours included - to Junk.");
66
+ return;
67
+ }
64
68
  setError(null);
65
69
  setBusy(true);
66
70
  try {
@@ -18,6 +18,7 @@ import { getMailbox } from "../../../../lib/mail/mailApi.js";
18
18
  import { useApiClient } from "../../../../lib/util/apiClientContext.js";
19
19
  import SettingsShell, { useSettingsShell } from "../../../shared/components/settings/layout/SettingsShell.js";
20
20
  import KeyEnrollmentGate from "../../../shared/components/layout/KeyEnrollmentGate.js";
21
+ import { MIN_PASSWORD_LENGTH } from "../../../shared/components/layout/RecoveryCodeUnlock.js";
21
22
  import { useUnlockPrompt } from "../../../shared/components/layout/UnlockPromptProvider.js";
22
23
  import { destroyLocalIndex } from "../../../shared/search/localIndexRpcClient.js";
23
24
  import { notifyApiError } from "../../../shared/notifications/apiErrors.js";
@@ -29,7 +30,6 @@ import { readStoredSignEnrollment, storeSignEnrollment } from "../../../shared/s
29
30
  import { checkEnrollmentNow, forgetEnrollment, recordEnrollmentResult, useEnrollmentSnapshot, watchCurrentEnrollment, watchEnrollment, } from "../../../shared/signing/enrollmentTracker.js";
30
31
  import { STALE_REQUEST_TEXT, beforeRequestText, healthWarning, isInstalling, timeOf } from "../../../shared/signing/enrollmentView.js";
31
32
  import { useSigningEnrollmentInfo } from "../../../shared/signing/signingInfo.js";
32
- const MIN_PASSWORD_LENGTH = 8;
33
33
  /** Mirrors restapi's `BaseKeyVaultRoute` `MAX_MASTER_KEY_WRAPS` - `addMasterKeyWrap()` refuses a vault
34
34
  * already holding this many wraps (escrow included). */
35
35
  export const MAX_MASTER_KEY_WRAPS = 20;
@@ -185,6 +185,7 @@ function EncryptionContent({ canManageKeys }) {
185
185
  // Distinguishes the "Save your new recovery codes" screen's copy for the two different actions that
186
186
  // land on it - a plain regeneration only invalidates old recovery codes, while a rotation also
187
187
  // invalidates every other unlock method, which the copy needs to say plainly.
188
+ const [confirmingRotation, setConfirmingRotation] = useState(false);
188
189
  const [recoveryCodesReason, setRecoveryCodesReason] = useState("regenerate");
189
190
  const [codesSaved, setCodesSaved] = useState(false);
190
191
  const [codesCopied, setCodesCopied] = useState(false);
@@ -228,6 +229,7 @@ function EncryptionContent({ canManageKeys }) {
228
229
  const hasEscrowWrap = vault?.masterKeyWraps.some((w) => w.method === "escrow") ?? false;
229
230
  const [wrappingEscrow, setWrappingEscrow] = useState(false);
230
231
  const [escrowError, setEscrowError] = useState(null);
232
+ const [pendingEscrow, setPendingEscrow] = useState(null);
231
233
  /**
232
234
  * This mailbox's unlocked keys *right now* - never a copy read at render time, which a lock (idle
233
235
  * timeout, another tab's sign-out) may since have destroyed. Prompts for the password when locked;
@@ -270,13 +272,29 @@ function EncryptionContent({ canManageKeys }) {
270
272
  }
271
273
  return err instanceof ApiRequestError ? err.message : fallback;
272
274
  }
275
+ /** Looks up the certificate the server says this mailbox's escrow scope holds, and asks the owner to confirm it
276
+ * before the master key is wrapped to it: nothing else vouches for that certificate, and whoever holds its
277
+ * private key can read this mailbox's encrypted mail. */
273
278
  async function handleWrapEscrow() {
274
279
  setEscrowError(null);
275
280
  setWrappingEscrow(true);
276
281
  try {
277
282
  // Only reachable when mailbox.escrowScopeId is set (see the render guard below).
283
+ await verifiedUnlockedKeys();
284
+ setPendingEscrow(await getEscrowInfo(mailboxUid, client));
285
+ }
286
+ catch (err) {
287
+ setEscrowError(errorMessage(err, "Could not add escrow protection for this mailbox."));
288
+ }
289
+ finally {
290
+ setWrappingEscrow(false);
291
+ }
292
+ }
293
+ async function handleConfirmEscrow(escrowInfo) {
294
+ setPendingEscrow(null);
295
+ setWrappingEscrow(true);
296
+ try {
278
297
  const { current } = await verifiedUnlockedKeys();
279
- const escrowInfo = await getEscrowInfo(mailboxUid, client);
280
298
  const wrap = await buildEscrowWrap(current.masterKey, escrowInfo.escrowScopeId, fromBase64(escrowInfo.publicKey.publicKey));
281
299
  await addMasterKeyWrap(mailboxUid, wrap, undefined, client);
282
300
  await loadVault();
@@ -635,7 +653,7 @@ function EncryptionContent({ canManageKeys }) {
635
653
  * working the instant this succeeds, since `rekey()` replaces `masterKeyWraps` wholesale - the whole
636
654
  * point, for a captured-wrap scenario where it's unclear which method was compromised.
637
655
  */
638
- async function handleRotateKeys(e) {
656
+ function handleRotateKeys(e) {
639
657
  e.preventDefault();
640
658
  if (rotationBlockedBySigning) {
641
659
  // The form's controls are disabled in this state too; this also covers an implicit submit.
@@ -650,6 +668,12 @@ function EncryptionContent({ canManageKeys }) {
650
668
  return;
651
669
  }
652
670
  setActionError(null);
671
+ // One press must not end every other unlock method and every other device's password: say so, and wait for a yes.
672
+ setConfirmingRotation(true);
673
+ }
674
+ async function rotateKeys() {
675
+ setConfirmingRotation(false);
676
+ setActionError(null);
653
677
  setEscrowError(null);
654
678
  setRotating(true);
655
679
  let keys;
@@ -775,12 +799,12 @@ function EncryptionContent({ canManageKeys }) {
775
799
  : null;
776
800
  const certificateEnrollment = certificateMode === "issued" && enrollmentResult?.status !== "issued" ? null : certificateMode === "expired" ? null : enrollmentResult;
777
801
  return (_jsx("div", { className: "flex-1 min-w-0 overflow-y-auto p-6", children: _jsxs("div", { className: "max-w-xl flex flex-col gap-6", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-lg font-bold tracking-tight mb-1", children: "Encryption" }), _jsxs("p", { className: "text-sm text-text-muted", children: ["Manage how ", mailbox.displayName, " unlocks its encryption keys on this and other devices."] })] }), loadError && _jsx(Alert, { children: loadError }), actionError && _jsx(Alert, { children: actionError }), !canManageKeys && (_jsx("p", { className: "text-sm text-text-muted", children: "Only this mailbox\u2019s owner, signed in as themselves, can change its encryption keys and unlock methods." })), _jsxs("div", { children: [_jsx("h2", { className: "text-sm font-semibold mb-2", children: "Encryption keys" }), displayedKeys.length > 0 ? (_jsx("ul", { className: "flex flex-col gap-1 text-sm", children: displayedKeys.map((key) => (_jsxs("li", { className: "font-mono text-xs", children: [key.useType === "sign" ? "Signing" : "Encryption", " key: ", key.fingerprint, key.revokedAt &&
778
- (key.revocationReason === "superseded" ? (_jsx("span", { className: "text-text-muted", children: " (superseded)" })) : (_jsx("span", { className: "text-danger", children: " (revoked)" })))] }, key.fingerprint))) })) : (_jsx("p", { className: "text-sm text-text-muted", children: "No keys enrolled yet." }))] }), _jsxs("div", { children: [_jsx("h2", { className: "text-sm font-semibold mb-2", children: "Digital signatures" }), activeSigningKey && signingStatus !== "pending" && (_jsx("p", { className: ["text-sm text-text-muted", certificateMode ? "mb-3" : ""].join(" "), children: "Enabled \u2014 outgoing mail from this mailbox is signed with a publicly-trusted certificate." })), certificateMode && (_jsxs("div", { className: "flex flex-col gap-2", children: [certificateMode === "failed" && signingError && _jsx(Alert, { children: signingError }), _jsx(SigningCertificateCard, { mode: certificateMode, enrollment: certificateEnrollment, snapshot: enrollment, info: signingInfo, address: mailbox.primarySmtpAddress, keyNotAfter: certificateMode === "expired" ? expiredSigningKey?.notAfter : activeSigningKey?.notAfter, canRequest: canManageKeys, requesting: signingStatus === "enrolling", onCheck: canManageKeys ? () => void checkEnrollmentNow(mailboxUid) : undefined, onRequest: handleEnrollSigning })] })), !activeSigningKey && !certificateMode && !canManageKeys ? (_jsx("p", { className: "text-sm text-text-muted", children: "Not enabled." })) : activeSigningKey || certificateMode ? null : (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx("p", { className: "text-xs text-text-muted", children: "Lets recipients verify that mail from this mailbox is genuinely from you. Optional \u2014 encryption already works without it." }), beforeRequestText(signingInfo, mailbox.primarySmtpAddress) && (_jsx("p", { className: "text-xs text-text-muted", children: beforeRequestText(signingInfo, mailbox.primarySmtpAddress) })), healthWarning(signingInfo) && _jsx(Alert, { children: healthWarning(signingInfo) }), enrollment?.gone && _jsx(Alert, { children: STALE_REQUEST_TEXT }), signingError && _jsx(Alert, { children: signingError }), signingInfo?.backend !== "none" && (_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", loading: signingStatus === "enrolling", disabled: signingStatus !== "idle", onClick: handleEnrollSigning, children: "Enable digital signatures" }))] }))] }), mailbox.escrowScopeId && (_jsxs("div", { children: [_jsx("h2", { className: "text-sm font-semibold mb-2", children: "Escrow" }), escrowError && _jsx(Alert, { children: escrowError }), hasEscrowWrap ? (_jsx("p", { className: "text-sm text-text-muted", children: "This mailbox is under legal/compliance escrow \u2014 an authorized holder in your organization can recover its encrypted mail if needed. This does not weaken protection against anyone else." })) : (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsxs("p", { className: "text-xs text-text-muted", children: ["Your organization has assigned this mailbox to an escrow scope, but nothing has been protected yet \u2014 an authorized holder cannot recover this mailbox\u2019s encrypted mail until you complete this step.", " ", "This does not weaken protection against anyone else."] }), canManageKeys && (_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", loading: wrappingEscrow, disabled: wrappingEscrow, onClick: handleWrapEscrow, children: "Add escrow protection" }))] }))] })), _jsxs("div", { children: [_jsx("h2", { className: "text-sm font-semibold mb-2", children: "Unlock methods" }), _jsxs("p", { className: "text-xs text-text-muted mb-3", children: ["Removing a method here stops it from being usable to unlock this mailbox going forward, but it is ", _jsx("strong", { children: "not" }), " full revocation \u2014 anyone who already captured a wrapped copy and knows its secret could still use it. For real revocation (e.g. after a lost device), rotate your keys entirely below instead."] }), vault && vault.masterKeyWraps.length > 0 ? (_jsx("ul", { className: "flex flex-col gap-2", children: [...passwordWraps, ...recoveryWraps, ...otherWraps].map((wrap) => {
802
+ (key.revocationReason === "superseded" ? (_jsx("span", { className: "text-text-muted", children: " (superseded)" })) : (_jsx("span", { className: "text-danger", children: " (revoked)" })))] }, key.fingerprint))) })) : (_jsx("p", { className: "text-sm text-text-muted", children: "No keys enrolled yet." }))] }), _jsxs("div", { children: [_jsx("h2", { className: "text-sm font-semibold mb-2", children: "Digital signatures" }), activeSigningKey && signingStatus !== "pending" && (_jsx("p", { className: ["text-sm text-text-muted", certificateMode ? "mb-3" : ""].join(" "), children: "Enabled \u2014 outgoing mail from this mailbox is signed with a publicly-trusted certificate." })), certificateMode && (_jsxs("div", { className: "flex flex-col gap-2", children: [certificateMode === "failed" && signingError && _jsx(Alert, { children: signingError }), _jsx(SigningCertificateCard, { mode: certificateMode, enrollment: certificateEnrollment, snapshot: enrollment, info: signingInfo, address: mailbox.primarySmtpAddress, keyNotAfter: certificateMode === "expired" ? expiredSigningKey?.notAfter : activeSigningKey?.notAfter, canRequest: canManageKeys, requesting: signingStatus === "enrolling", onCheck: canManageKeys ? () => void checkEnrollmentNow(mailboxUid) : undefined, onRequest: handleEnrollSigning })] })), !activeSigningKey && !certificateMode && !canManageKeys ? (_jsx("p", { className: "text-sm text-text-muted", children: "Not enabled." })) : activeSigningKey || certificateMode ? null : (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx("p", { className: "text-xs text-text-muted", children: "Lets recipients verify that mail from this mailbox is genuinely from you. Optional \u2014 encryption already works without it." }), beforeRequestText(signingInfo, mailbox.primarySmtpAddress) && (_jsx("p", { className: "text-xs text-text-muted", children: beforeRequestText(signingInfo, mailbox.primarySmtpAddress) })), healthWarning(signingInfo) && _jsx(Alert, { children: healthWarning(signingInfo) }), enrollment?.gone && _jsx(Alert, { children: STALE_REQUEST_TEXT }), signingError && _jsx(Alert, { children: signingError }), signingInfo?.backend !== "none" && (_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", loading: signingStatus === "enrolling", disabled: signingStatus !== "idle", onClick: handleEnrollSigning, children: "Enable digital signatures" }))] }))] }), mailbox.escrowScopeId && (_jsxs("div", { children: [_jsx("h2", { className: "text-sm font-semibold mb-2", children: "Escrow" }), escrowError && _jsx(Alert, { children: escrowError }), hasEscrowWrap ? (_jsx("p", { className: "text-sm text-text-muted", children: "This mailbox is under legal/compliance escrow \u2014 an authorized holder in your organization can recover its encrypted mail if needed. This does not weaken protection against anyone else." })) : (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsxs("p", { className: "text-xs text-text-muted", children: ["Your organization has assigned this mailbox to an escrow scope, but nothing has been protected yet \u2014 an authorized holder cannot recover this mailbox\u2019s encrypted mail until you complete this step.", " ", "This does not weaken protection against anyone else."] }), canManageKeys && (_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", loading: wrappingEscrow, disabled: wrappingEscrow, onClick: handleWrapEscrow, children: "Add escrow protection" }))] })), _jsxs(Modal, { open: pendingEscrow !== null, onClose: () => setPendingEscrow(null), title: "Confirm escrow certificate", children: [_jsx("p", { className: "text-sm mb-3", children: "Anyone holding the private key for this certificate will be able to recover this mailbox\u2019s encrypted mail. Confirm it is the escrow certificate your organization expects before continuing." }), _jsxs("dl", { className: "text-sm mb-5 flex flex-col gap-1", children: [_jsxs("div", { children: [_jsx("dt", { className: "text-xs text-text-muted", children: "Escrow scope" }), _jsx("dd", { className: "break-all", children: pendingEscrow?.escrowScopeId })] }), _jsxs("div", { children: [_jsx("dt", { className: "text-xs text-text-muted", children: "Certificate fingerprint" }), _jsx("dd", { className: "font-mono break-all", children: pendingEscrow?.publicKey.fingerprint })] })] }), _jsxs("div", { className: "flex gap-3 justify-end", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: () => setPendingEscrow(null), children: "Cancel" }), _jsx(Button, { type: "button", className: "!w-auto", onClick: () => void handleConfirmEscrow(pendingEscrow), children: "Trust this certificate" })] })] })] })), _jsxs("div", { children: [_jsx("h2", { className: "text-sm font-semibold mb-2", children: "Unlock methods" }), _jsxs("p", { className: "text-xs text-text-muted mb-3", children: ["Removing a method here stops it from being usable to unlock this mailbox going forward, but it is ", _jsx("strong", { children: "not" }), " full revocation \u2014 anyone who already captured a wrapped copy and knows its secret could still use it. For real revocation (e.g. after a lost device), rotate your keys entirely below instead."] }), vault && vault.masterKeyWraps.length > 0 ? (_jsx("ul", { className: "flex flex-col gap-2", children: [...passwordWraps, ...recoveryWraps, ...otherWraps].map((wrap) => {
779
803
  const key = `${wrap.method}:${wrap.methodId ?? ""}`;
780
804
  return (_jsxs("li", { className: "flex items-center justify-between gap-3 text-sm py-1.5 px-3 bg-surface-alt rounded-sm", children: [_jsx("span", { children: METHOD_LABELS[wrap.method] ?? wrap.method }), wrap.method !== "escrow" &&
781
805
  canManageKeys &&
782
806
  (canRemoveWrap(wrap) ? (_jsx(Button, { type: "button", variant: "text", className: "!w-auto text-danger", loading: removingMethod === key, disabled: removingMethod !== null, onClick: () => setPendingRemoval(wrap), children: "Remove" })) : (_jsx("span", { className: "text-xs text-text-muted", children: ownUnlockWrapCount > 1 ? "Needed to unlock" : "Your only unlock method" })))] }, key));
783
- }) })) : (_jsx("p", { className: "text-sm text-text-muted", children: "No unlock methods on file." })), _jsxs(Modal, { open: pendingRemoval !== null, onClose: () => setPendingRemoval(null), title: "Remove unlock method", children: [_jsx("p", { className: "text-sm mb-5", children: "Remove this unlock method? It will no longer unlock this mailbox\u2019s encrypted mail on any device." }), _jsxs("div", { className: "flex gap-3 justify-end", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: () => setPendingRemoval(null), children: "Cancel" }), _jsx(Button, { type: "button", className: "!w-auto !bg-none !bg-danger !border-danger hover:!bg-danger", onClick: () => handleRemove(pendingRemoval), children: "Remove method" })] })] })] }), canManageKeys && (_jsxs(_Fragment, { children: [passwordWraps.length === 0 ? (vault && (_jsxs("form", { onSubmit: handleAddPassword, className: "flex flex-col gap-2", children: [_jsx("h2", { className: "text-sm font-semibold", children: "Add a password" }), _jsx("input", { type: "password", "aria-label": "New password", placeholder: "New password", className: "w-full text-sm border border-border rounded-sm py-1.5 px-2 bg-surface", value: newPassword, onChange: (e) => setNewPassword(e.target.value), disabled: addingPassword, autoComplete: "new-password" }), _jsx("input", { type: "password", "aria-label": "Confirm new password", placeholder: "Confirm new password", className: "w-full text-sm border border-border rounded-sm py-1.5 px-2 bg-surface", value: confirmNewPassword, onChange: (e) => setConfirmNewPassword(e.target.value), disabled: addingPassword, autoComplete: "new-password" }), _jsx("div", { children: _jsx(Button, { type: "submit", loading: addingPassword, disabled: addingPassword, className: "!w-auto", children: "Add password" }) })] }))) : (_jsxs("div", { children: [_jsx("h2", { className: "text-sm font-semibold mb-2", children: "Changing your password" }), _jsx("p", { className: "text-xs text-text-muted", children: "Only one password can unlock this mailbox. To change it, rotate your keys below - that sets a new password and replaces every other unlock method." })] })), _jsxs("div", { children: [_jsx("h2", { className: "text-sm font-semibold mb-2", children: "Recovery codes" }), _jsx("p", { className: "text-xs text-text-muted mb-3", children: "Regenerating replaces all of your existing recovery codes \u2014 old ones stop working immediately." }), _jsx(Button, { type: "button", variant: "secondary", loading: regenerating, disabled: regenerating || !vault, onClick: handleRegenerateRecoveryCodes, className: "!w-auto", children: "Regenerate recovery codes" })] }), _jsxs("form", { onSubmit: handleRotateKeys, className: "flex flex-col gap-2 border-t border-border pt-6", children: [_jsx("h2", { className: "text-sm font-semibold", children: "Rotate keys" }), _jsx("p", { className: "text-xs text-text-muted mb-1", children: "Real revocation, for when a device or an unlock method may have been compromised. Re-protects your existing encryption key under a brand new master key - your signing/ encryption keypair itself doesn\u2019t change, so mail you\u2019ve already sent or received still decrypts normally. Every current unlock method (password, recovery codes, and anything else on file) stops working immediately; you\u2019ll set a new password and get new recovery codes below." }), rotationBlockedBySigning && (_jsx("p", { className: "text-xs text-text-muted", children: signingStatus === "checking"
807
+ }) })) : (_jsx("p", { className: "text-sm text-text-muted", children: "No unlock methods on file." })), _jsxs(Modal, { open: pendingRemoval !== null, onClose: () => setPendingRemoval(null), title: "Remove unlock method", children: [_jsx("p", { className: "text-sm mb-5", children: "Remove this unlock method? It will no longer unlock this mailbox\u2019s encrypted mail on any device." }), _jsxs("div", { className: "flex gap-3 justify-end", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: () => setPendingRemoval(null), children: "Cancel" }), _jsx(Button, { type: "button", className: "!w-auto !bg-none !bg-danger !border-danger hover:!bg-danger", onClick: () => handleRemove(pendingRemoval), children: "Remove method" })] })] })] }), canManageKeys && (_jsxs(_Fragment, { children: [passwordWraps.length === 0 ? (vault && (_jsxs("form", { onSubmit: handleAddPassword, className: "flex flex-col gap-2", children: [_jsx("h2", { className: "text-sm font-semibold", children: "Add a password" }), _jsx("input", { type: "password", "aria-label": "New password", placeholder: "New password", className: "w-full text-sm border border-border rounded-sm py-1.5 px-2 bg-surface", value: newPassword, onChange: (e) => setNewPassword(e.target.value), disabled: addingPassword, autoComplete: "new-password" }), _jsx("input", { type: "password", "aria-label": "Confirm new password", placeholder: "Confirm new password", className: "w-full text-sm border border-border rounded-sm py-1.5 px-2 bg-surface", value: confirmNewPassword, onChange: (e) => setConfirmNewPassword(e.target.value), disabled: addingPassword, autoComplete: "new-password" }), _jsx("div", { children: _jsx(Button, { type: "submit", loading: addingPassword, disabled: addingPassword, className: "!w-auto", children: "Add password" }) })] }))) : (_jsxs("div", { children: [_jsx("h2", { className: "text-sm font-semibold mb-2", children: "Changing your password" }), _jsx("p", { className: "text-xs text-text-muted", children: "Only one password can unlock this mailbox. To change it, rotate your keys below - that sets a new password and replaces every other unlock method." })] })), _jsxs("div", { children: [_jsx("h2", { className: "text-sm font-semibold mb-2", children: "Recovery codes" }), _jsx("p", { className: "text-xs text-text-muted mb-3", children: "Regenerating replaces all of your existing recovery codes \u2014 old ones stop working immediately." }), _jsx(Button, { type: "button", variant: "secondary", loading: regenerating, disabled: regenerating || !vault, onClick: handleRegenerateRecoveryCodes, className: "!w-auto", children: "Regenerate recovery codes" })] }), _jsxs(Modal, { open: confirmingRotation, onClose: () => setConfirmingRotation(false), title: "Rotate your keys?", children: [_jsx("p", { className: "text-sm mb-3", children: "Every other way of unlocking this mailbox - its password, its recovery codes, and every other device\u2019s saved password - stops working the moment this finishes, and cannot be brought back. You will be shown new recovery codes once." }), _jsxs("div", { className: "flex gap-3 justify-end", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: () => setConfirmingRotation(false), children: "Cancel" }), _jsx(Button, { type: "button", className: "!w-auto !bg-none !bg-danger !border-danger hover:!bg-danger", onClick: () => void rotateKeys(), children: "Yes, rotate my keys" })] })] }), _jsxs("form", { onSubmit: handleRotateKeys, className: "flex flex-col gap-2 border-t border-border pt-6", children: [_jsx("h2", { className: "text-sm font-semibold", children: "Rotate keys" }), _jsx("p", { className: "text-xs text-text-muted mb-1", children: "Real revocation, for when a device or an unlock method may have been compromised. Re-protects your existing encryption key under a brand new master key - your signing/ encryption keypair itself doesn\u2019t change, so mail you\u2019ve already sent or received still decrypts normally. Every current unlock method (password, recovery codes, and anything else on file) stops working immediately; you\u2019ll set a new password and get new recovery codes below." }), rotationBlockedBySigning && (_jsx("p", { className: "text-xs text-text-muted", children: signingStatus === "checking"
784
808
  ? "Checking whether a signing certificate enrollment is still in progress..."
785
809
  : "Rotation is unavailable while a signing certificate enrollment is in progress - rotating now would lose the key being enrolled. Try again once it finishes, or cancel the enrollment." })), signingStatus === "pending" && signingEnrollmentId && (_jsxs("div", { className: "flex flex-col gap-2", children: [cancelEnrollmentError && _jsx(Alert, { children: cancelEnrollmentError }), _jsx("div", { children: _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", loading: cancelingEnrollment, disabled: cancelingEnrollment, onClick: () => handleCancelEnrollment(signingEnrollmentId), children: "Cancel enrollment" }) })] })), _jsx("input", { type: "password", "aria-label": "New password for rotated keys", placeholder: "New password", className: "w-full text-sm border border-border rounded-sm py-1.5 px-2 bg-surface", value: rotationPassword, onChange: (e) => setRotationPassword(e.target.value), disabled: rotating || rotationBlockedBySigning, autoComplete: "new-password" }), _jsx("input", { type: "password", "aria-label": "Confirm new password for rotated keys", placeholder: "Confirm new password", className: "w-full text-sm border border-border rounded-sm py-1.5 px-2 bg-surface", value: rotationConfirmPassword, onChange: (e) => setRotationConfirmPassword(e.target.value), disabled: rotating || rotationBlockedBySigning, autoComplete: "new-password" }), _jsx("div", { children: _jsx(Button, { type: "submit", variant: "secondary", className: "!w-auto text-danger", loading: rotating, disabled: rotating || rotationBlockedBySigning, children: "Rotate keys now" }) })] })] })), _jsxs("div", { children: [_jsx("h2", { className: "text-sm font-semibold mb-2", children: "Session timeout" }), _jsx("p", { className: "text-xs text-text-muted mb-3", children: "Automatically destroys your unlocked keys on this device after this much time with no activity anywhere in the app - not just Mail or Settings. Applies the next time you open or reload the app." }), _jsx("select", { "aria-label": "Session timeout", className: "text-sm border border-border rounded-sm py-1.5 px-2 bg-surface", value: idleTimeoutMinutes, onChange: handleIdleTimeoutChange, children: IDLE_TIMEOUT_OPTIONS_MINUTES.map((minutes) => (_jsx("option", { value: minutes, children: idleTimeoutLabel(minutes) }, minutes))) })] }), _jsxs("div", { children: [_jsx("h2", { className: "text-sm font-semibold mb-2", children: "Local search index size" }), _jsxs("p", { className: "text-xs text-text-muted mb-3", children: ["How much decrypted mail this device keeps in a local encrypted search index, so recent search results and inbox previews work instantly without contacting the server every time. Defaults to ", localIndexDefaultByteBudgetLabel, " on this device. Applies the next time this mailbox\u2019s index rebuilds (e.g. the next time you unlock it) \u2014 lowering it doesn\u2019t delete anything already indexed elsewhere, it just narrows what this device keeps a local copy of."] }), _jsx("select", { "aria-label": "Local search index size", className: "text-sm border border-border rounded-sm py-1.5 px-2 bg-surface", value: localIndexByteBudget, onChange: handleLocalIndexByteBudgetChange, children: LOCAL_INDEX_SIZE_OPTIONS.map((option) => (_jsx("option", { value: option.bytes, children: option.label }, option.bytes))) })] }), _jsxs("div", { children: [_jsx("h2", { className: "text-sm font-semibold mb-2", children: "This session" }), _jsx("p", { className: "text-xs text-text-muted mb-3", children: "Removes your unlocked keys from this browser tab\u2019s memory right now, without affecting any other device. You\u2019ll be asked to unlock again the next time you read or send encrypted mail here." }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto text-danger", onClick: handleDestroyKeysNow, children: "Destroy keys on this device now" })] })] }) }));
786
810
  }
@@ -16,6 +16,7 @@ import Alert from "../../../../lib/components/feedback/Alert.js";
16
16
  import Button from "../../../../lib/components/buttons/Button.js";
17
17
  import Modal from "../../../../lib/components/overlays/Modal.js";
18
18
  import { notifyApiError } from "../../../shared/notifications/apiErrors.js";
19
+ import GravatarSetting from "../../../shared/components/settings/GravatarSetting.js";
19
20
  const NON_MAIL_FOLDER_TYPES = new Set(["calendar", "contacts", "tasks", "notes"]);
20
21
  function importFormatFromFilename(filename) {
21
22
  return filename.toLowerCase().endsWith(".pst") ? "pst" : "mbox";
@@ -49,7 +50,7 @@ function PrivacyContent({ userUid }) {
49
50
  const ownedMailboxes = mailboxes.filter((mb) => mb.ownerUserUid !== undefined && mb.ownerUserUid === userUid);
50
51
  const ownMailbox = ownedMailboxes.length === 1 ? ownedMailboxes[0] : undefined;
51
52
  const selectedIsOwn = selected.uid === ownMailbox?.uid;
52
- return (_jsx("div", { className: "flex-1 min-w-0 overflow-y-auto p-6", children: _jsxs("div", { className: "max-w-xl flex flex-col gap-8", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-lg font-bold tracking-tight mb-1", children: "Privacy & Data" }), _jsx("p", { className: "text-sm text-text-muted", children: "Export or manage the data associated with this mailbox." })] }), selectedIsOwn ? (_jsxs(_Fragment, { children: [_jsx(ExportSection, { mailboxUid: mailboxUid, mailboxLabel: `${selected.displayName} (${selected.primarySmtpAddress})` }), _jsx(ImportSection, { mailboxUid: mailboxUid }), _jsx(ErasureSection, { mailboxLabel: `${selected.displayName} (${selected.primarySmtpAddress})` })] })) : (_jsx(OwnMailboxOnlyNotice, { ownedMailboxCount: ownedMailboxes.length, ownMailboxLabel: ownMailbox && `${ownMailbox.displayName} (${ownMailbox.primarySmtpAddress})` }))] }) }));
53
+ return (_jsx("div", { className: "flex-1 min-w-0 overflow-y-auto p-6", children: _jsxs("div", { className: "max-w-xl flex flex-col gap-8", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-lg font-bold tracking-tight mb-1", children: "Privacy & Data" }), _jsx("p", { className: "text-sm text-text-muted", children: "Export or manage the data associated with this mailbox." })] }), _jsx(GravatarSetting, {}), selectedIsOwn ? (_jsxs(_Fragment, { children: [_jsx(ExportSection, { mailboxUid: mailboxUid, mailboxLabel: `${selected.displayName} (${selected.primarySmtpAddress})` }), _jsx(ImportSection, { mailboxUid: mailboxUid }), _jsx(ErasureSection, { mailboxLabel: `${selected.displayName} (${selected.primarySmtpAddress})` })] })) : (_jsx(OwnMailboxOnlyNotice, { ownedMailboxCount: ownedMailboxes.length, ownMailboxLabel: ownMailbox && `${ownMailbox.displayName} (${ownMailbox.primarySmtpAddress})` }))] }) }));
53
54
  }
54
55
  function OwnMailboxOnlyNotice({ ownedMailboxCount, ownMailboxLabel }) {
55
56
  let detail;
@@ -158,7 +159,7 @@ function ImportSection({ mailboxUid }) {
158
159
  return;
159
160
  }
160
161
  setUploading(true);
161
- uploadMailboxImport(file, { format: importFormatFromFilename(file.name), targetFolderUid })
162
+ uploadMailboxImport(file, { format: importFormatFromFilename(file.name), targetFolderUid, mailboxUid: mailboxUid }, client)
162
163
  .then(() => loadRequests())
163
164
  .catch((err) => notifyApiError(err, "Couldn't upload the file"))
164
165
  .finally(() => setUploading(false));
@@ -119,7 +119,10 @@ function ProfileContent() {
119
119
  }, "aria-describedby": "profile-time-zone-help" }), timezone !== deviceZone && (_jsxs(Button, { type: "button", variant: "text", className: "!w-auto", onClick: () => {
120
120
  setTimezone(deviceZone);
121
121
  setSaved(false);
122
- }, children: ["Use this device\u2019s time zone (", describeTimeZone(deviceZone), ")"] }))] }), zoneWasPreselected && (_jsx("p", { className: "mt-1 text-xs text-text-muted", children: "Your mailbox has no time zone chosen yet; this device\u2019s is preselected. Save to keep it." })), _jsx("p", { id: "profile-time-zone-help", className: "mt-1 text-xs text-text-muted", children: "How calendar times and reminders are shown and scheduled for this mailbox, such as when an event or a reminder falls." })] }), _jsxs(FormField, { label: "Free/busy visibility", htmlFor: "profile-free-busy", children: [_jsx("select", { id: "profile-free-busy", className: SELECT_CLASS, value: freeBusy, disabled: !canChangeFreeBusy, onChange: (e) => {
122
+ }, children: ["Use this device\u2019s time zone (", describeTimeZone(deviceZone), ")"] }))] }), zoneWasPreselected && (_jsxs("p", { className: "mt-1 text-xs text-text-muted", children: ["Your mailbox has no time zone chosen yet (it uses UTC); this device\u2019s is preselected, and Save sets it - whatever else you change.", " ", _jsx(Button, { type: "button", variant: "text", className: "!w-auto !inline !p-0 !text-xs", onClick: () => {
123
+ setTimezone(DEFAULT_TIME_ZONE);
124
+ setSaved(false);
125
+ }, children: "Keep UTC" })] })), _jsx("p", { id: "profile-time-zone-help", className: "mt-1 text-xs text-text-muted", children: "How calendar times and reminders are shown and scheduled for this mailbox, such as when an event or a reminder falls." })] }), _jsxs(FormField, { label: "Free/busy visibility", htmlFor: "profile-free-busy", children: [_jsx("select", { id: "profile-free-busy", className: SELECT_CLASS, value: freeBusy, disabled: !canChangeFreeBusy, onChange: (e) => {
123
126
  setFreeBusy(e.target.value);
124
127
  setSaved(false);
125
128
  }, "aria-describedby": "profile-free-busy-help", children: FREE_BUSY_CHOICES.map((choice) => (_jsx("option", { value: choice.value, children: choice.label }, choice.value))) }), _jsx("p", { id: "profile-free-busy-help", className: "mt-1 text-xs text-text-muted", children: FREE_BUSY_CHOICES.find((choice) => choice.value === freeBusy).help }), _jsx("p", { className: "mt-1 text-xs text-text-muted", children: "Who can see the hours you are busy when they look for a time to meet - never the title, place or guests of an event." }), !canChangeFreeBusy && (_jsx("p", { className: "mt-1 text-xs text-text-muted", children: "This mailbox is shared with you: only its owner can change who sees its free/busy times." }))] }), _jsx("div", { children: _jsx(Button, { type: "submit", loading: saving, disabled: saving, className: "!w-auto", children: "Save" }) })] })] }) }));
@@ -4,7 +4,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
4
4
  // SPDX-License-Identifier: MPL-2.0
5
5
  ///////////////////////////////////////////////////////////////////////////////
6
6
  import { pageTitle } from "../../../shared/navigation/pageTitle.js";
7
- import { useEffect, useState } from "react";
7
+ import { useEffect, useRef, useState } from "react";
8
8
  import { ApiRequestError } from "../../../../lib/util/api.js";
9
9
  import { listMailboxAccess, removeMailboxAccess, setMailboxAccess, } from "../../../../lib/mail/mailboxAccessApi.js";
10
10
  import SettingsShell, { useSettingsShell } from "../../../shared/components/settings/layout/SettingsShell.js";
@@ -40,15 +40,24 @@ function SharingContent() {
40
40
  // `SettingsShell` only ever renders its children once `mailboxUid` has resolved (a mailbox-less caller
41
41
  // gets `<MailboxProvisioning />` instead) - same established precedent as the Labels/Focused Inbox
42
42
  // settings pages, so none of the handlers below need their own `!mailboxUid` guard.
43
+ // Only the latest request's answer is shown (a reload after a change can overtake a slower earlier one).
44
+ const latestLoad = useRef(0);
43
45
  function reload() {
46
+ const load = ++latestLoad.current;
44
47
  setStatus("loading");
45
48
  setError(null);
46
49
  listMailboxAccess(mailboxUid)
47
50
  .then((result) => {
51
+ if (load !== latestLoad.current) {
52
+ return;
53
+ }
48
54
  setMembers(result);
49
55
  setStatus("ready");
50
56
  })
51
57
  .catch((err) => {
58
+ if (load !== latestLoad.current) {
59
+ return;
60
+ }
52
61
  // A 403 here is the live, server-verified "can this caller manage this mailbox's sharing"
53
62
  // signal - the same permission threshold (ACLAction.UPDATE) this route itself requires, so
54
63
  // no separate probe call is needed to decide which state to render.
@@ -63,10 +63,7 @@ function SignatureDetailContent({ uid }) {
63
63
  setSaving(true);
64
64
  setSaved(false);
65
65
  try {
66
- if (isDefaultForNewMessages || isDefaultForReplyForward) {
67
- const existing = await listMailSignatures(mailboxUid, { limit: 100 });
68
- await clearPreviousDefaults(existing, original.uid, isDefaultForNewMessages, isDefaultForReplyForward);
69
- }
66
+ // This signature is saved first: were it refused (a 409, a 400) after the others had been cleared, the mailbox would be left with no default at all.
70
67
  const updated = await updateMailSignature({
71
68
  uid: original.uid,
72
69
  version: original.version,
@@ -76,6 +73,11 @@ function SignatureDetailContent({ uid }) {
76
73
  isDefaultForReplyForward,
77
74
  });
78
75
  setOriginal(updated);
76
+ if (isDefaultForNewMessages || isDefaultForReplyForward) {
77
+ // The signature's own mailbox, not the one the page happens to have selected.
78
+ const existing = await listMailSignatures(original.mailboxUid, { limit: 100 });
79
+ await clearPreviousDefaults(existing, original.uid, isDefaultForNewMessages, isDefaultForReplyForward);
80
+ }
79
81
  setSaved(true);
80
82
  }
81
83
  catch (err) {