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