@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
@@ -0,0 +1,250 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ /**
6
+ * Gets a picture the user chose - a phone's camera photo is typically 2-8 MB, may be HEIC and carries an EXIF orientation - into what the server takes for a
7
+ * contact's picture (JPEG, PNG, GIF or WebP, at most `CONTACT_PHOTO_MAX_BYTES`, checked by its magic bytes), in the browser, before it is uploaded.
8
+ */
9
+ import { CONTACT_PHOTO_MAX_BYTES, CONTACT_PHOTO_TYPES } from "./contactsApi.js";
10
+ /** The longest side (in pixels) an accepted picture may have and still be sent as it is. */
11
+ export const CONTACT_PHOTO_MAX_SIDE = 1024;
12
+ /** The side (in pixels) of the square picture made from one that has to be changed - an avatar is shown at 72 pixels at most, 512 covers a high-density screen. */
13
+ export const CONTACT_PHOTO_OUTPUT_SIDE = 512;
14
+ /** JPEG qualities tried in turn until the picture fits `CONTACT_PHOTO_MAX_BYTES`. */
15
+ export const CONTACT_PHOTO_QUALITIES = [0.85, 0.75, 0.65, 0.55, 0.45, 0.35];
16
+ /** The most pixels (width x height) a picture may have to be decoded: a few kilobytes of PNG can say 30000 x 30000, which takes gigabytes of memory to decode. */
17
+ export const CONTACT_PHOTO_MAX_PIXELS = 16000000;
18
+ /** How much of the start of a picture is read to find its size: a JPEG's EXIF segment (up to 64 KB) comes before the size. */
19
+ const HEADER_BYTES = 128 * 1024;
20
+ export const CONTACT_PHOTO_UNSUPPORTED_MESSAGE = "This picture's format isn't supported by your browser — choose a JPEG or PNG instead.";
21
+ export const CONTACT_PHOTO_TOO_LARGE_MESSAGE = "This picture could not be made small enough — choose another one.";
22
+ /** Why a chosen picture cannot be used (a format the browser cannot decode, such as HEIC, or one that stays too large); its `message` is fit to show the user. */
23
+ export class ContactPhotoError extends Error {
24
+ constructor(message = CONTACT_PHOTO_UNSUPPORTED_MESSAGE) {
25
+ super(message);
26
+ this.name = "ContactPhotoError";
27
+ }
28
+ }
29
+ /** How many JPEG segments are read, one small read each, past the start of a picture before giving up on finding its size. */
30
+ const MAX_JPEG_SEGMENTS = 4096;
31
+ /** What a JPEG segment starting at `view[0]` (`view` holds its first 9 bytes, fewer at the end of the file) says: how far to its
32
+ * next segment (`skip`), its size when it is the first frame header ("SOFn"), or `undefined` when there is nothing more to read -
33
+ * not a marker, the start of the scan, or a cut-off header. */
34
+ function jpegSegment(view) {
35
+ const be16 = (offset) => (view[offset] << 8) | view[offset + 1];
36
+ if (view.length < 4 || view[0] !== 0xff) {
37
+ return undefined;
38
+ }
39
+ const marker = view[1];
40
+ if (marker === 0xff) {
41
+ return { skip: 1 };
42
+ }
43
+ if (marker === 0x01 || (marker >= 0xd0 && marker <= 0xd7)) {
44
+ return { skip: 2 };
45
+ }
46
+ if (marker >= 0xc0 && marker <= 0xcf && marker !== 0xc4 && marker !== 0xc8 && marker !== 0xcc) {
47
+ return view.length >= 9 ? { width: be16(7), height: be16(5) } : undefined;
48
+ }
49
+ return marker === 0xda ? undefined : { skip: 2 + be16(2) };
50
+ }
51
+ /** The size of the JPEG `file`, read segment by segment from `offset` (where its start ran out) - an ICC profile or several EXIF/XMP
52
+ * segments can put the frame header well past the start. */
53
+ async function jpegSizeFrom(file, offset) {
54
+ for (let count = 0; count < MAX_JPEG_SEGMENTS && offset < file.size; count++) {
55
+ const segment = jpegSegment(new Uint8Array(await file.slice(offset, offset + 9).arrayBuffer()));
56
+ if (!segment) {
57
+ return undefined;
58
+ }
59
+ if ("width" in segment) {
60
+ return segment;
61
+ }
62
+ offset += segment.skip;
63
+ }
64
+ return undefined;
65
+ }
66
+ /** What the start of a picture says about it: whether it is a JPEG, its size when that can be read from it and, for a JPEG whose
67
+ * start ran out before its frame header, where to read on from (`resume`). */
68
+ function inspect(head) {
69
+ const at = (offset, text) => Array.from(text).every((char, i) => head[offset + i] === char.charCodeAt(0));
70
+ const be16 = (offset) => (head[offset] << 8) | head[offset + 1];
71
+ const be32 = (offset) => be16(offset) * 65536 + be16(offset + 2);
72
+ const le16 = (offset) => head[offset] | (head[offset + 1] << 8);
73
+ const le24 = (offset) => le16(offset) | (head[offset + 2] << 16);
74
+ const known = (width, height) => ({ jpeg: false, width, height });
75
+ if (at(0, "\x89PNG\r\n\x1a\n") && head.length >= 24) {
76
+ return known(be32(16), be32(20));
77
+ }
78
+ if (at(0, "GIF8") && head.length >= 10) {
79
+ return known(le16(6), le16(8));
80
+ }
81
+ if (at(0, "RIFF") && at(8, "WEBP") && head.length >= 30) {
82
+ if (at(12, "VP8 ")) {
83
+ return known(le16(26) & 0x3fff, le16(28) & 0x3fff);
84
+ }
85
+ if (at(12, "VP8L")) {
86
+ const bits = (le16(21) | (le16(23) << 16)) >>> 0;
87
+ return known((bits & 0x3fff) + 1, ((bits >>> 14) & 0x3fff) + 1);
88
+ }
89
+ if (at(12, "VP8X")) {
90
+ return known(le24(24) + 1, le24(27) + 1);
91
+ }
92
+ return { jpeg: false };
93
+ }
94
+ if (!(head[0] === 0xff && head[1] === 0xd8)) {
95
+ return { jpeg: false };
96
+ }
97
+ // A JPEG is a run of segments (`FF marker`, then a length unless the marker stands alone); the size is in the first frame header, "SOFn".
98
+ let offset = 2;
99
+ while (offset + 4 <= head.length) {
100
+ const segment = jpegSegment(head.subarray(offset, offset + 9));
101
+ if (!segment) {
102
+ return { jpeg: true };
103
+ }
104
+ if ("width" in segment) {
105
+ return { jpeg: true, ...segment };
106
+ }
107
+ offset += segment.skip;
108
+ }
109
+ return { jpeg: true, resume: offset };
110
+ }
111
+ /** Decodes with the orientation of the photo's EXIF applied, so a phone's sideways-stored picture comes out upright. */
112
+ async function decode(file) {
113
+ if (typeof createImageBitmap === "function") {
114
+ try {
115
+ const bitmap = await createImageBitmap(file, { imageOrientation: "from-image" });
116
+ return { source: bitmap, width: bitmap.width, height: bitmap.height, release: () => bitmap.close() };
117
+ }
118
+ catch {
119
+ // Fall through: an `<img>` may still read what this `createImageBitmap()` would not.
120
+ }
121
+ }
122
+ // An `<img>` applies the EXIF orientation itself (`image-orientation: from-image` is the default).
123
+ const url = URL.createObjectURL(file);
124
+ try {
125
+ const image = await new Promise((resolve, reject) => {
126
+ const element = new Image();
127
+ element.onload = () => resolve(element);
128
+ element.onerror = () => reject(new ContactPhotoError());
129
+ element.src = url;
130
+ });
131
+ return { source: image, width: image.naturalWidth, height: image.naturalHeight, release: () => undefined };
132
+ }
133
+ finally {
134
+ URL.revokeObjectURL(url);
135
+ }
136
+ }
137
+ function createSurface(side) {
138
+ if (typeof OffscreenCanvas === "function") {
139
+ const canvas = new OffscreenCanvas(side, side);
140
+ const context = canvas.getContext("2d");
141
+ if (!context) {
142
+ throw new ContactPhotoError();
143
+ }
144
+ return { context, encode: (type, quality) => canvas.convertToBlob({ type, quality }) };
145
+ }
146
+ const canvas = document.createElement("canvas");
147
+ canvas.width = side;
148
+ canvas.height = side;
149
+ const context = canvas.getContext("2d");
150
+ if (!context) {
151
+ throw new ContactPhotoError();
152
+ }
153
+ return { context, encode: (type, quality) => new Promise((resolve) => canvas.toBlob(resolve, type, quality)) };
154
+ }
155
+ function hasTransparency(context, side) {
156
+ const { data } = context.getImageData(0, 0, side, side);
157
+ for (let i = 3; i < data.length; i += 4) {
158
+ if (data[i] < 255) {
159
+ return true;
160
+ }
161
+ }
162
+ return false;
163
+ }
164
+ function renamed(name, extension) {
165
+ return `${name.replace(/\.[^./\\]*$/, "") || "photo"}.${extension}`;
166
+ }
167
+ /**
168
+ * The picture to upload for `file`. A picture the server already takes (an accepted type within the byte limit and at most `CONTACT_PHOTO_MAX_SIDE` pixels a
169
+ * side) is returned as it is - an animated GIF stays animated - unless it is a JPEG, which is always drawn again to drop its EXIF. Anything else (a phone's 6 MB photo, a HEIC, a huge PNG) is decoded with its EXIF orientation applied,
170
+ * cropped to the centered square - avatars are circular, so the corners are never seen - scaled down to at most `CONTACT_PHOTO_OUTPUT_SIDE` pixels and encoded as a
171
+ * JPEG (transparent parts on white, quality lowered step by step until it fits), or kept as PNG/WebP when it has transparency and still fits.
172
+ *
173
+ * @throws ContactPhotoError when the browser cannot decode it (HEIC, or not a picture at all), it says it has more than `CONTACT_PHOTO_MAX_PIXELS` pixels
174
+ * (it is not decoded at all then) or it cannot be made small enough.
175
+ */
176
+ export async function prepareContactPhoto(file) {
177
+ const accepted = CONTACT_PHOTO_TYPES.includes(file.type) && file.size <= CONTACT_PHOTO_MAX_BYTES;
178
+ let head = inspect(new Uint8Array(await file.slice(0, HEADER_BYTES).arrayBuffer()));
179
+ if (head.resume !== undefined) {
180
+ head = { ...head, ...(await jpegSizeFrom(file, head.resume)) };
181
+ }
182
+ if (head.jpeg && (!head.width || !head.height)) {
183
+ // A JPEG whose size is not found within the segments read (or is given as zero, to be redefined by a later marker) could be of any
184
+ // size, and would reach the decoder unchecked.
185
+ throw new ContactPhotoError();
186
+ }
187
+ if (head.width !== undefined && head.height !== undefined && head.width * head.height > CONTACT_PHOTO_MAX_PIXELS) {
188
+ throw new ContactPhotoError(CONTACT_PHOTO_TOO_LARGE_MESSAGE);
189
+ }
190
+ // A JPEG is always drawn again: its EXIF (the camera, the time, often where it was taken) would otherwise be uploaded and shown to whoever sees the contact.
191
+ const jpeg = head.jpeg || file.type === "image/jpeg";
192
+ let decoded;
193
+ try {
194
+ decoded = await decode(file);
195
+ }
196
+ catch (err) {
197
+ if (accepted && !jpeg) {
198
+ // It cannot be measured here but the server takes it as it is.
199
+ return file;
200
+ }
201
+ throw err;
202
+ }
203
+ try {
204
+ const { source, width, height } = decoded;
205
+ // The size could not be read from the file itself (a format with no reader above, or a size past the segments read): what
206
+ // the decoder made of it is the last check before it is drawn.
207
+ if (width * height > CONTACT_PHOTO_MAX_PIXELS) {
208
+ throw new ContactPhotoError(CONTACT_PHOTO_TOO_LARGE_MESSAGE);
209
+ }
210
+ if (accepted && !jpeg && Math.max(width, height) <= CONTACT_PHOTO_MAX_SIDE) {
211
+ return file;
212
+ }
213
+ const crop = Math.min(width, height);
214
+ // Never enlarged: a small picture only gets cropped.
215
+ const side = Math.min(crop, CONTACT_PHOTO_OUTPUT_SIDE);
216
+ const { context, encode } = createSurface(side);
217
+ const draw = () => {
218
+ context.imageSmoothingQuality = "high";
219
+ context.drawImage(source, (width - crop) / 2, (height - crop) / 2, crop, crop, 0, 0, side, side);
220
+ };
221
+ draw();
222
+ if (["image/png", "image/webp", "image/gif"].includes(file.type) && hasTransparency(context, side)) {
223
+ const type = file.type === "image/webp" ? "image/webp" : "image/png";
224
+ const blob = await encode(type);
225
+ if (blob && blob.size <= CONTACT_PHOTO_MAX_BYTES) {
226
+ // A browser that cannot encode WebP answers with a PNG instead.
227
+ const kept = blob.type || type;
228
+ return new File([blob], renamed(file.name, kept === "image/webp" ? "webp" : "png"), { type: kept });
229
+ }
230
+ // Too large to keep the transparency: a JPEG has none, so what showed through becomes white.
231
+ context.clearRect(0, 0, side, side);
232
+ }
233
+ context.fillStyle = "#ffffff";
234
+ context.fillRect(0, 0, side, side);
235
+ draw();
236
+ for (const quality of CONTACT_PHOTO_QUALITIES) {
237
+ const blob = await encode("image/jpeg", quality);
238
+ if (!blob) {
239
+ throw new ContactPhotoError();
240
+ }
241
+ if (blob.size <= CONTACT_PHOTO_MAX_BYTES) {
242
+ return new File([blob], renamed(file.name, "jpg"), { type: "image/jpeg" });
243
+ }
244
+ }
245
+ throw new ContactPhotoError(CONTACT_PHOTO_TOO_LARGE_MESSAGE);
246
+ }
247
+ finally {
248
+ decoded.release();
249
+ }
250
+ }
@@ -0,0 +1,19 @@
1
+ import type { ApiClient } from "../util/api.js";
2
+ import { type Contact } from "./contactsApi.js";
3
+ /** Forgets every cached picture (and revokes its URL). For a test, or when an account is signed out. */
4
+ export declare function clearContactPhotoCache(): void;
5
+ /**
6
+ * What to put in an `<img src>` for a contact's own picture, or `undefined` when they have none, it is still loading or it could not be loaded (the avatar
7
+ * then shows initials). Without an explicit `client` that is the plain `contactPhotoUrl()`: the browser sends the `jwt` cookie with the request. A
8
+ * token-authenticated client (the desktop and mobile apps) cannot do that - an `<img>` sends no `Authorization` header - so the picture is fetched through the client
9
+ * and shown as a `blob:` URL, cached per client, contact and version with a small LRU and revoked once it falls out of it and nothing shows it.
10
+ */
11
+ export declare function useContactPhotoSrc(contact: Pick<Contact, "uid" | "version" | "photoBlobKey"> | undefined, client?: ApiClient): string | undefined;
12
+ /**
13
+ * `useContactPhotoSrc()`, also saying whether a picture that has to be fetched could not be (`failed`: no `src` is coming), and held back until `active` - an avatar
14
+ * that is not on screen yet has no reason to fetch its picture.
15
+ */
16
+ export declare function useContactPhoto(contact: Pick<Contact, "uid" | "version" | "photoBlobKey"> | undefined, client?: ApiClient, active?: boolean): {
17
+ src: string | undefined;
18
+ failed: boolean;
19
+ };
@@ -0,0 +1,153 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ import { useEffect, useState } from "react";
6
+ import { contactPhotoUrl, fetchContactPhoto } from "./contactsApi.js";
7
+ /** How many pictures stay loaded (as `blob:` URLs) after nothing shows them any more, so a list scrolled back over does not fetch them all again. */
8
+ const CACHE_LIMIT = 50;
9
+ /** Least recently used first (a `Map` iterates in insertion order, and a use moves the entry to the end). */
10
+ const entries = new Map();
11
+ /** Fetches in flight, and how many avatars are waiting on each (they are all counted as showing the picture the moment it is stored). */
12
+ const pending = new Map();
13
+ const clientIds = new WeakMap();
14
+ let nextClientId = 0;
15
+ /** One key per picture of one account: the same uid never collides across the clients of a multi-account app, and a replaced picture (new version) is a new key. */
16
+ function keyOf(client, uid, version) {
17
+ let id = clientIds.get(client);
18
+ if (id === undefined) {
19
+ id = nextClientId++;
20
+ clientIds.set(client, id);
21
+ }
22
+ return `${id}:${uid}:${version}`;
23
+ }
24
+ /** Revokes the pictures beyond the limit that nothing shows, oldest first. */
25
+ function trim() {
26
+ for (const [key, entry] of entries) {
27
+ if (entries.size <= CACHE_LIMIT) {
28
+ return;
29
+ }
30
+ if (entry.refs === 0) {
31
+ URL.revokeObjectURL(entry.url);
32
+ entries.delete(key);
33
+ }
34
+ }
35
+ }
36
+ /** How many pictures are fetched at once; a list of a hundred contacts must not open a hundred requests together. */
37
+ const MAX_CONCURRENT_FETCHES = 4;
38
+ let fetching = 0;
39
+ const waiting = [];
40
+ /** Runs `task` once fewer than `MAX_CONCURRENT_FETCHES` others are running, in the order asked. */
41
+ function limited(task) {
42
+ return new Promise((resolve, reject) => {
43
+ const run = () => {
44
+ task()
45
+ .then(resolve, reject)
46
+ .finally(() => {
47
+ // A waiting task takes over the slot; it is only given up when none waits.
48
+ const next = waiting.shift();
49
+ if (next) {
50
+ next();
51
+ }
52
+ else {
53
+ fetching--;
54
+ }
55
+ });
56
+ };
57
+ if (fetching < MAX_CONCURRENT_FETCHES) {
58
+ fetching++;
59
+ run();
60
+ }
61
+ else {
62
+ waiting.push(run);
63
+ }
64
+ });
65
+ }
66
+ /** The `blob:` URL for `key`, fetched with `load` unless it is cached, and counted as shown until `release()`. Concurrent callers share one fetch. */
67
+ async function acquire(key, load) {
68
+ const entry = entries.get(key);
69
+ if (entry) {
70
+ entry.refs++;
71
+ entries.delete(key);
72
+ entries.set(key, entry);
73
+ return entry.url;
74
+ }
75
+ let shared = pending.get(key);
76
+ if (!shared) {
77
+ const created = {
78
+ waiters: 0,
79
+ request: limited(load)
80
+ .then((blob) => {
81
+ const url = URL.createObjectURL(blob);
82
+ // Claimed by everyone waiting from the start: an entry nobody holds yet could be trimmed before its avatar got to it.
83
+ entries.set(key, { url, refs: created.waiters });
84
+ return url;
85
+ })
86
+ .finally(() => pending.delete(key)),
87
+ };
88
+ pending.set(key, created);
89
+ shared = created;
90
+ }
91
+ shared.waiters++;
92
+ return shared.request;
93
+ }
94
+ function release(key) {
95
+ const entry = entries.get(key);
96
+ if (entry) {
97
+ entry.refs--;
98
+ }
99
+ trim();
100
+ }
101
+ /** Forgets every cached picture (and revokes its URL). For a test, or when an account is signed out. */
102
+ export function clearContactPhotoCache() {
103
+ for (const entry of entries.values()) {
104
+ URL.revokeObjectURL(entry.url);
105
+ }
106
+ entries.clear();
107
+ }
108
+ /**
109
+ * What to put in an `<img src>` for a contact's own picture, or `undefined` when they have none, it is still loading or it could not be loaded (the avatar
110
+ * then shows initials). Without an explicit `client` that is the plain `contactPhotoUrl()`: the browser sends the `jwt` cookie with the request. A
111
+ * token-authenticated client (the desktop and mobile apps) cannot do that - an `<img>` sends no `Authorization` header - so the picture is fetched through the client
112
+ * and shown as a `blob:` URL, cached per client, contact and version with a small LRU and revoked once it falls out of it and nothing shows it.
113
+ */
114
+ export function useContactPhotoSrc(contact, client) {
115
+ return useContactPhoto(contact, client).src;
116
+ }
117
+ /**
118
+ * `useContactPhotoSrc()`, also saying whether a picture that has to be fetched could not be (`failed`: no `src` is coming), and held back until `active` - an avatar
119
+ * that is not on screen yet has no reason to fetch its picture.
120
+ */
121
+ export function useContactPhoto(contact, client, active = true) {
122
+ const [loaded, setLoaded] = useState();
123
+ const [failedKey, setFailedKey] = useState();
124
+ const key = active && client && contact?.photoBlobKey ? keyOf(client, contact.uid, contact.version) : undefined;
125
+ const uid = contact?.uid;
126
+ const version = contact?.version;
127
+ useEffect(() => {
128
+ if (!key || !client) {
129
+ return;
130
+ }
131
+ let cancelled = false;
132
+ let held = false;
133
+ acquire(key, () => fetchContactPhoto(uid, version, client)).then((url) => {
134
+ if (cancelled) {
135
+ release(key);
136
+ }
137
+ else {
138
+ held = true;
139
+ setLoaded({ key, url });
140
+ }
141
+ }, () => !cancelled && setFailedKey(key));
142
+ return () => {
143
+ cancelled = true;
144
+ if (held) {
145
+ release(key);
146
+ }
147
+ };
148
+ }, [key]);
149
+ if (client) {
150
+ return { src: key !== undefined && loaded?.key === key ? loaded.url : undefined, failed: key !== undefined && failedKey === key };
151
+ }
152
+ return { src: contact && contactPhotoUrl(contact), failed: false };
153
+ }
@@ -12,6 +12,9 @@ export declare function contactsToVCardFile(contacts: Contact[]): string;
12
12
  /** A parsed vCard's fields, shaped as a partial `ContactInput` missing only the `mailboxUid`/`folderUid`
13
13
  * scope (the caller supplies those, since they're not part of the vCard format itself). */
14
14
  export type ParsedVCardContact = Omit<ContactInput, "mailboxUid" | "folderUid">;
15
+ /** How much of a file's text `parseVCards()` reads, and how many records it takes from it (the rest is ignored). */
16
+ export declare const VCARD_MAX_CHARS = 5000000;
17
+ export declare const VCARD_MAX_CONTACTS = 2000;
15
18
  /**
16
19
  * Parses one or more vCard 2.1/3.0/4.0 records from a `.vcf` file's text content. Deliberately tolerant of
17
20
  * fields this app doesn't model (PHOTO, BDAY, etc. — silently ignored) and of the `TYPE=` parameter being
@@ -86,6 +86,47 @@ function valueSeparatorIndex(line) {
86
86
  }
87
87
  return -1;
88
88
  }
89
+ /** How much of a file's text `parseVCards()` reads, and how many records it takes from it (the rest is ignored). */
90
+ export const VCARD_MAX_CHARS = 5000000;
91
+ export const VCARD_MAX_CONTACTS = 2000;
92
+ /** Decodes a quoted-printable value: each `=XX` is one byte, and the bytes are text in `charset` (UTF-8 when none is named or the name is unknown). */
93
+ function decodeQuotedPrintable(value, charset) {
94
+ const encoder = new TextEncoder();
95
+ const bytes = [];
96
+ for (const [, hex, literal] of value.matchAll(/=([0-9a-f]{2})|([\s\S])/giu)) {
97
+ if (hex) {
98
+ bytes.push(parseInt(hex, 16));
99
+ }
100
+ else {
101
+ bytes.push(...encoder.encode(literal));
102
+ }
103
+ }
104
+ try {
105
+ return new TextDecoder(charset || "utf-8").decode(new Uint8Array(bytes));
106
+ }
107
+ catch {
108
+ return new TextDecoder().decode(new Uint8Array(bytes));
109
+ }
110
+ }
111
+ /** `lines` with each quoted-printable line that ends in `=` (a soft line break) joined to the line that carries on from it. */
112
+ function joinQuotedPrintableLines(lines) {
113
+ const joined = [];
114
+ for (let i = 0; i < lines.length; i++) {
115
+ let line = lines[i];
116
+ if (/^[^:]*QUOTED-PRINTABLE/i.test(line)) {
117
+ // Collected and joined once: appending to a growing string a line at a time is quadratic in a value of many soft breaks.
118
+ const parts = [];
119
+ while (line.endsWith("=") && i + 1 < lines.length) {
120
+ parts.push(line.slice(0, -1));
121
+ line = lines[++i];
122
+ }
123
+ parts.push(line);
124
+ line = parts.join("");
125
+ }
126
+ joined.push(line);
127
+ }
128
+ return joined;
129
+ }
89
130
  /**
90
131
  * Maps a property's type parameters onto `ContactAddressKind`: every `TYPE=` value (vCard 3/4, including
91
132
  * comma lists and quoted lists like `TYPE="work,voice"`) plus vCard 2.1's bare parameters (`EMAIL;WORK:`)
@@ -115,13 +156,15 @@ function normalizeType(params) {
115
156
  * `normalizeType()`); structured values are split on unescaped `;` only.
116
157
  */
117
158
  export function parseVCards(text) {
159
+ // A file is read only so far: nothing legitimate is this large, and a huge one would be minutes of parsing and thousands of records.
160
+ const source = text.length > VCARD_MAX_CHARS ? text.slice(0, VCARD_MAX_CHARS) : text;
118
161
  // Unfold first, then split only on a line that is exactly BEGIN:VCARD - a NOTE (or a folded line) that
119
162
  // merely contains the text "BEGIN:VCARD" must not start a bogus record.
120
- const unfolded = text.replace(/(?:\r\n|\r|\n)[ \t]/g, "");
121
- const cards = unfolded.split(/^[ \t]*BEGIN:VCARD[ \t]*$/im).slice(1);
163
+ const unfolded = source.replace(/(?:\r\n|\r|\n)[ \t]/g, "");
164
+ const cards = unfolded.split(/^[ \t]*BEGIN:VCARD[ \t]*$/im).slice(1, VCARD_MAX_CONTACTS + 1);
122
165
  return cards.map((card) => {
123
166
  const contact = { displayName: "", emails: [], phones: [], addresses: [] };
124
- const lines = card.split(/\r\n|\r|\n/);
167
+ const lines = joinQuotedPrintableLines(card.split(/\r\n|\r|\n/));
125
168
  for (const rawLine of lines) {
126
169
  const line = rawLine.trim();
127
170
  const colonIndex = valueSeparatorIndex(line);
@@ -129,9 +172,13 @@ export function parseVCards(text) {
129
172
  continue;
130
173
  }
131
174
  const key = line.slice(0, colonIndex);
132
- const rawValue = line.slice(colonIndex + 1);
133
- const value = unescapeVCardValue(rawValue);
134
175
  const [groupedName, ...params] = key.split(";");
176
+ // vCard 2.1 may encode a value as quoted-printable (`ENCODING=QUOTED-PRINTABLE`, or the bare parameter), in a named charset;
177
+ // such a value has no backslash escapes.
178
+ const quotedPrintable = params.some((param) => /^(?:ENCODING=)?QUOTED-PRINTABLE$/i.test(param.trim()));
179
+ const charset = params.find((param) => /^CHARSET=/i.test(param.trim()))?.trim().slice("CHARSET=".length).replace(/"/g, "");
180
+ const rawValue = quotedPrintable ? decodeQuotedPrintable(line.slice(colonIndex + 1), charset) : line.slice(colonIndex + 1);
181
+ const value = quotedPrintable ? rawValue : unescapeVCardValue(rawValue);
135
182
  const name = groupedName.slice(groupedName.lastIndexOf(".") + 1).toUpperCase();
136
183
  const type = normalizeType(params);
137
184
  switch (name) {
@@ -154,10 +201,15 @@ export function parseVCards(text) {
154
201
  contact.jobTitle = value;
155
202
  break;
156
203
  case "EMAIL":
157
- contact.emails.push({ address: value, type });
204
+ // An empty one would make the server refuse the whole contact.
205
+ if (value.trim()) {
206
+ contact.emails.push({ address: value.trim(), type });
207
+ }
158
208
  break;
159
209
  case "TEL":
160
- contact.phones.push({ phoneNumber: value, type });
210
+ if (value.trim()) {
211
+ contact.phones.push({ phoneNumber: value, type });
212
+ }
161
213
  break;
162
214
  case "ADR": {
163
215
  const [, , street, city, state, postalCode, country] = splitStructuredValue(rawValue);
@@ -18,6 +18,14 @@ export interface GeneratedEscrowKeys {
18
18
  /** Ready to pass as `createEscrowScope()`'s `publicKey`. */
19
19
  publicKey: EscrowScopePublicKey;
20
20
  }
21
+ /**
22
+ * The SHA-256 fingerprint (lower-case hex, as `computeCertFingerprint()` gives it) of the certificate that `publicKey` - an escrow scope's pasted public key,
23
+ * the base64 of its DER - holds, or `null` when it is not a certificate. The holders compare a scope's fingerprint out of band, so one typed beside a
24
+ * different key makes that check worthless: this is what it has to be.
25
+ */
26
+ export declare function escrowPublicKeyFingerprint(publicKey: string): Promise<string | null>;
27
+ /** A fingerprint as `escrowPublicKeyFingerprint()` writes it: lower-case hex only, whatever separators (`AB:CD`, spaces) it was typed with. */
28
+ export declare function normalizeFingerprint(fingerprint: string): string;
21
29
  export declare function generateEscrowKeyPair(options: {
22
30
  name: string;
23
31
  validDays: number;
@@ -14,10 +14,29 @@
14
14
  */
15
15
  import "reflect-metadata";
16
16
  import * as x509 from "@peculiar/x509";
17
- import { toBase64 } from "./encoding.js";
17
+ import { fromBase64, toBase64 } from "./encoding.js";
18
18
  import { computeCertFingerprint } from "./smime.js";
19
19
  x509.cryptoProvider.set(crypto);
20
20
  const DAY_MS = 24 * 60 * 60 * 1000;
21
+ /**
22
+ * The SHA-256 fingerprint (lower-case hex, as `computeCertFingerprint()` gives it) of the certificate that `publicKey` - an escrow scope's pasted public key,
23
+ * the base64 of its DER - holds, or `null` when it is not a certificate. The holders compare a scope's fingerprint out of band, so one typed beside a
24
+ * different key makes that check worthless: this is what it has to be.
25
+ */
26
+ export async function escrowPublicKeyFingerprint(publicKey) {
27
+ try {
28
+ const der = fromBase64(publicKey.trim());
29
+ void new x509.X509Certificate(new Uint8Array(der));
30
+ return await computeCertFingerprint(der);
31
+ }
32
+ catch {
33
+ return null;
34
+ }
35
+ }
36
+ /** A fingerprint as `escrowPublicKeyFingerprint()` writes it: lower-case hex only, whatever separators (`AB:CD`, spaces) it was typed with. */
37
+ export function normalizeFingerprint(fingerprint) {
38
+ return fingerprint.replace(/[^0-9a-f]/gi, "").toLowerCase();
39
+ }
21
40
  function toPem(label, der) {
22
41
  const lines = toBase64(der).match(/.{1,64}/g) ?? [];
23
42
  return `-----BEGIN ${label}-----\n${lines.join("\n")}\n-----END ${label}-----\n`;
@@ -102,8 +102,11 @@ export interface MessageSecurityResult {
102
102
  * recovered and it carried a protected Subject. */
103
103
  subject?: string;
104
104
  /** Every protected header recovered from inside the signed/decrypted entity - present only when that entity
105
- * carries RFC 9788 protected headers (a protected `From`). Absent for a legacy S/MIME sender's signed-only
106
- * message: its outer Subject/To/Cc were then never signed, and a UI should not present them as verified. */
105
+ * carries RFC 9788 protected headers (a protected `From`) AND a signature over them verified (so also for a
106
+ * `"verified_at_first_open"` result). Absent for a legacy S/MIME sender's signed-only message (its outer
107
+ * Subject/To/Cc were then never signed, and a UI should not present them as verified), and for an encrypted
108
+ * message without a verified signature: anyone holding the reader's public certificate can encrypt headers of their
109
+ * own choosing, so they are never exposed. */
107
110
  protectedHeaders?: MessageProtectedHeaders;
108
111
  /** The attachments inside the verified/decrypted entity - for a signed-only message the only attachments the
109
112
  * signature covers, so a UI showing a verified badge should list these rather than the server's attachment
@@ -149,7 +152,7 @@ export interface SignerBindingInput {
149
152
  /** The certificate matched to the verified SignerInfo; `undefined` fails closed. */
150
153
  signerCertificateDer: Uint8Array | undefined;
151
154
  /** Headers recovered from inside the signed content (RFC 9788), if any. */
152
- protectedHeaders: (Pick<ProtectedHeaders, "from" | "to"> & Partial<Pick<ProtectedHeaders, "cc" | "subject">>) | undefined;
155
+ protectedHeaders: (Pick<ProtectedHeaders, "from" | "to"> & Partial<Pick<ProtectedHeaders, "cc" | "subject" | "date">>) | undefined;
153
156
  /** The received message's outer header map (lowercased names), as `parseMimeEntity()` returns it. */
154
157
  outerHeaders: Record<string, string>;
155
158
  /** Every outer header field in order (`parseMimeEntity()`'s `fields`) - checked for repeated
@@ -159,7 +162,7 @@ export interface SignerBindingInput {
159
162
  protectedFields?: MimeHeaderField[];
160
163
  /** One trusted fingerprint or several (any match is accepted). An empty array counts as none supplied. */
161
164
  pinnedSignerFingerprint?: string | string[];
162
- /** Also require the protected `Subject` (when protected headers are present) to equal the outer one - right for
165
+ /** Also require the protected `Subject` and `Date` (when protected headers are present) to equal the outer ones - right for
163
166
  * a signed-only (`hp="clear"`) message, wrong for an encrypted one whose outer Subject is obscured. */
164
167
  compareSubject?: boolean;
165
168
  }