@rapidmx/web-client 0.30.0 → 0.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +1 -1
- 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/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 +39 -17
- 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 +1 -1
- 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/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 +26 -12
- 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 +13 -2
- package/dist/lib/admin/pluginsApi.js +8 -0
- 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 +21 -2
- 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
|
@@ -44,7 +44,8 @@ function dropdownPosition(anchor) {
|
|
|
44
44
|
*/
|
|
45
45
|
export default function RecipientInput({ id, label, value, onChange, onBlur, onCommit, mailboxUid, fetchSuggestions = fetchRecipientSuggestions, debounceMs = RECIPIENT_SUGGESTION_DEBOUNCE_MS, disabled, autoFocus, onEdit, initialPending = "", hideChip, placeholder, ariaLabel, listLabel, className, dropdownZIndex, }) {
|
|
46
46
|
const [pending, setPending] = useState(initialPending);
|
|
47
|
-
|
|
47
|
+
// With the query they were loaded for: a list for earlier text is not offered (Enter or Tab would put somebody else in place of what was typed).
|
|
48
|
+
const [suggestionList, setSuggestionList] = useState({ query: "", items: [] });
|
|
48
49
|
const [open, setOpen] = useState(false);
|
|
49
50
|
const [activeIndex, setActiveIndex] = useState(0);
|
|
50
51
|
const [position, setPosition] = useState(null);
|
|
@@ -69,24 +70,24 @@ export default function RecipientInput({ id, label, value, onChange, onBlur, onC
|
|
|
69
70
|
}
|
|
70
71
|
}, [pending, pendingActive]);
|
|
71
72
|
const committedAddresses = new Set(chips.map((chip) => parseRecipient(chip).address.toLowerCase()));
|
|
72
|
-
const visible =
|
|
73
|
+
const visible = (suggestionList.query === query ? suggestionList.items : []).filter((suggestion) => !committedAddresses.has(suggestion.address.toLowerCase()));
|
|
73
74
|
const expanded = open && visible.length > 0 && query.length >= RECIPIENT_SUGGESTION_MIN_QUERY_LENGTH;
|
|
74
75
|
const active = Math.min(activeIndex, visible.length - 1);
|
|
75
76
|
useEffect(() => {
|
|
76
77
|
if (query.length < RECIPIENT_SUGGESTION_MIN_QUERY_LENGTH) {
|
|
77
|
-
|
|
78
|
+
setSuggestionList({ query: "", items: [] });
|
|
78
79
|
return;
|
|
79
80
|
}
|
|
80
81
|
const controller = new AbortController();
|
|
81
82
|
const timer = setTimeout(() => {
|
|
82
83
|
fetchRef.current(query, { mailboxUid, signal: controller.signal }).then((result) => {
|
|
83
84
|
if (!controller.signal.aborted) {
|
|
84
|
-
|
|
85
|
+
setSuggestionList({ query, items: result });
|
|
85
86
|
setActiveIndex(0);
|
|
86
87
|
}
|
|
87
88
|
}, () => {
|
|
88
89
|
if (!controller.signal.aborted) {
|
|
89
|
-
|
|
90
|
+
setSuggestionList({ query: "", items: [] });
|
|
90
91
|
}
|
|
91
92
|
});
|
|
92
93
|
}, debounceMs);
|
|
@@ -139,7 +140,7 @@ export default function RecipientInput({ id, label, value, onChange, onBlur, onC
|
|
|
139
140
|
const next = committed.join(", ");
|
|
140
141
|
setPending("");
|
|
141
142
|
setOpen(false);
|
|
142
|
-
|
|
143
|
+
setSuggestionList({ query: "", items: [] });
|
|
143
144
|
onChange(next);
|
|
144
145
|
onEdit?.(committed, "");
|
|
145
146
|
onCommit?.(next);
|
|
@@ -204,7 +205,8 @@ export default function RecipientInput({ id, label, value, onChange, onBlur, onC
|
|
|
204
205
|
return (_jsxs("div", { ref: anchorRef, className: `flex-1 min-w-0 flex flex-wrap items-center gap-1${className ? ` ${className}` : ""}`, children: [drawn.length > 0 && (_jsx("ul", { role: "list", "aria-label": listLabel ?? `${label} recipients`, className: "contents", children: drawn.map(({ chip, index }) => {
|
|
205
206
|
const recipient = parseRecipient(chip);
|
|
206
207
|
const valid = isValidRecipientAddress(recipient.address);
|
|
207
|
-
|
|
208
|
+
// A name that is itself an address (`"billing@bank.com" <x@evil.tld>`) would hide the one it is really sent to: both are shown.
|
|
209
|
+
const name = /[@@﹫]/.test(recipient.displayName ?? "") ? `${recipient.displayName} <${recipient.address}>` : recipient.displayName || recipient.address;
|
|
208
210
|
return (_jsxs("li", { title: chip, className: `flex items-center gap-1 max-w-full min-w-0 rounded-pill border pl-2 pr-0.5 py-px text-xs ${valid ? "bg-surface-alt border-border text-text" : "bg-danger-bg border-danger text-danger"}`, children: [_jsx("span", { className: "truncate", children: name }), !valid && _jsx("span", { className: "sr-only", children: " (not a valid email address)" }), _jsx("button", { type: "button", tabIndex: -1, "aria-label": `Remove ${recipient.address}`, disabled: disabled, onMouseDown: (e) => e.preventDefault(), onClick: () => removeChip(index), className: "shrink-0 w-4 h-4 flex items-center justify-center rounded-pill text-text-muted hover:bg-border hover:text-text", children: _jsx(HiOutlineXMark, { size: 10 }) })] }, `${index}-${chip}`));
|
|
209
211
|
}) })), _jsx("input", { ref: inputRef, id: id, type: "text", role: "combobox", autoComplete: "off", "aria-autocomplete": "list", "aria-label": ariaLabel, placeholder: placeholder, "aria-expanded": expanded, "aria-controls": listboxId, "aria-activedescendant": expanded ? optionId(active) : undefined, disabled: disabled, autoFocus: autoFocus, className: "flex-1 min-w-[6rem] text-sm bg-transparent outline-none py-0.5", value: inputValue, onChange: handleInputChange, onKeyDown: handleKeyDown, onBlur: handleBlur }), _jsx("span", { className: "sr-only", "aria-live": "polite", children: expanded ? `${visible.length} ${visible.length === 1 ? "suggestion" : "suggestions"}` : "" }), expanded &&
|
|
210
212
|
position &&
|
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
/** Saves a compose window's unsaved edits (or waits for a save already on the wire), resolving to `false` when
|
|
2
2
|
* something the window holds is still unsaved afterwards (any other value counts as saved). Never rejects. */
|
|
3
3
|
export type ComposeFlush = () => Promise<unknown>;
|
|
4
|
+
/**
|
|
5
|
+
* Registers something to run the moment the app starts signing out (explicitly, or forced by another tab): a cache that holds message content,
|
|
6
|
+
* which must not outlive the session it was read in. Called once, when the module that owns the cache is loaded.
|
|
7
|
+
*/
|
|
8
|
+
export declare function onSigningOut(cleanup: () => void): void;
|
|
4
9
|
/**
|
|
5
10
|
* Marks the app as signing out (explicitly, or forced by a sign-out in another tab), so compose windows stop
|
|
6
11
|
* asking the browser to confirm leaving the page - the "Leave site?" prompt would otherwise let the user cancel
|
|
7
12
|
* the sign-out's own navigation and stay on a page whose session is being ended. Call it before flushing drafts
|
|
8
|
-
* and navigating. `clearSigningOut()` undoes it (a sign-out that didn't leave the page after all).
|
|
13
|
+
* and navigating. `clearSigningOut()` undoes it (a sign-out that didn't leave the page after all). It also runs what was registered
|
|
14
|
+
* with `onSigningOut()`.
|
|
9
15
|
*/
|
|
10
16
|
export declare function markSigningOut(): void;
|
|
11
17
|
/** Undoes `markSigningOut()`. */
|
|
@@ -3,15 +3,27 @@
|
|
|
3
3
|
// SPDX-License-Identifier: MPL-2.0
|
|
4
4
|
///////////////////////////////////////////////////////////////////////////////
|
|
5
5
|
const flushers = new Set();
|
|
6
|
+
const signOutCleanups = new Set();
|
|
6
7
|
let signingOut = false;
|
|
8
|
+
/**
|
|
9
|
+
* Registers something to run the moment the app starts signing out (explicitly, or forced by another tab): a cache that holds message content,
|
|
10
|
+
* which must not outlive the session it was read in. Called once, when the module that owns the cache is loaded.
|
|
11
|
+
*/
|
|
12
|
+
export function onSigningOut(cleanup) {
|
|
13
|
+
signOutCleanups.add(cleanup);
|
|
14
|
+
}
|
|
7
15
|
/**
|
|
8
16
|
* Marks the app as signing out (explicitly, or forced by a sign-out in another tab), so compose windows stop
|
|
9
17
|
* asking the browser to confirm leaving the page - the "Leave site?" prompt would otherwise let the user cancel
|
|
10
18
|
* the sign-out's own navigation and stay on a page whose session is being ended. Call it before flushing drafts
|
|
11
|
-
* and navigating. `clearSigningOut()` undoes it (a sign-out that didn't leave the page after all).
|
|
19
|
+
* and navigating. `clearSigningOut()` undoes it (a sign-out that didn't leave the page after all). It also runs what was registered
|
|
20
|
+
* with `onSigningOut()`.
|
|
12
21
|
*/
|
|
13
22
|
export function markSigningOut() {
|
|
14
23
|
signingOut = true;
|
|
24
|
+
for (const cleanup of signOutCleanups) {
|
|
25
|
+
cleanup();
|
|
26
|
+
}
|
|
15
27
|
}
|
|
16
28
|
/** Undoes `markSigningOut()`. */
|
|
17
29
|
export function clearSigningOut() {
|
|
@@ -18,6 +18,11 @@ import type { EncryptionPolicy } from "../../../../../lib/crypto/keyvaultApi.js"
|
|
|
18
18
|
* reported in `unknown`, so a caller that is about to send can give the loads a moment - and only a caller that finds out later that the
|
|
19
19
|
* message *does* need encryption acts on it (see `ComposeWindow`'s transition rules).
|
|
20
20
|
*
|
|
21
|
+
* That is this function's answer, and it stays fail open for everything that is about a draft or the compose window. The background send
|
|
22
|
+
* (`sendDecision.ts`'s `decideSend()` with `lookupsComplete`) reads `unknown` once its loads are in: a sender who has an encryption key, and
|
|
23
|
+
* whose policy or recipients' keys could not be checked, is blocked with the `lookup-failed` decision (the sender may still send without
|
|
24
|
+
* encryption) rather than sent in the clear on a guess.
|
|
25
|
+
*
|
|
21
26
|
* Why this is safe enough: the requirement only ever protects *this client's* plaintext drafts and sends from being unencrypted when the
|
|
22
27
|
* server's policy says they should be; it is not the enforcement point (the keys are the user's, the server never sees them). A missing
|
|
23
28
|
* policy therefore does not mean "encrypt or refuse" - it means the client cannot tell, and the honest default is the one that works.
|
|
@@ -7,12 +7,23 @@ import { attachmentContentUrl, getMessageRawContent, listAttachments } from "../
|
|
|
7
7
|
import { MAX_QUOTED_IMAGE_BYTES, embedQuotedImages } from "../../../../../lib/mail/compose/quotedImages.js";
|
|
8
8
|
import { EMBEDDED_IMAGE_TYPES, findInlineAttachment, toDataUri } from "../../../../../lib/mail/inlineImages.js";
|
|
9
9
|
import { decodeHeaderText, extractDisplayBody, parseMimeEntity } from "../../../../../lib/crypto/mime.js";
|
|
10
|
+
import { onSigningOut } from "./composeFlushRegistry.js";
|
|
10
11
|
import { parseRecipientList } from "./recipients.js";
|
|
12
|
+
/** The security states whose `protectedHeaders` a signature covers - the only ones Reply All takes recipients from. */
|
|
13
|
+
const TRUSTED_HEADER_STATES = new Set([
|
|
14
|
+
"signed_verified",
|
|
15
|
+
"encrypted_verified",
|
|
16
|
+
"signed_unverified_signer",
|
|
17
|
+
"encrypted_unverified_signer",
|
|
18
|
+
"verified_at_first_open",
|
|
19
|
+
]);
|
|
11
20
|
/** How long a reply waits for the original's body before it goes ahead with the preview - a slow or hung request must not
|
|
12
21
|
* leave the window waiting on it (or, before it opened at once, keep it from opening) indefinitely. */
|
|
13
22
|
export const QUOTE_FETCH_TIMEOUT_MS = 10000;
|
|
14
23
|
/** How long a fetched (or prefetched) body is remembered, so hovering Reply and then clicking it costs one request. */
|
|
15
24
|
export const QUOTE_CACHE_MS = 30000;
|
|
25
|
+
/** How many bodies are remembered at most: each is a whole message body, and a mailbox read for hours would otherwise keep every one it was asked about. */
|
|
26
|
+
const QUOTE_CACHE_MAX = 20;
|
|
16
27
|
const contentCache = new Map();
|
|
17
28
|
/**
|
|
18
29
|
* The server's sanitized HTML body of a message (`GET /mail/messages/:id/content`), or `undefined` when it has none (a
|
|
@@ -38,6 +49,12 @@ function fetchContentHtml(uid) {
|
|
|
38
49
|
clearTimeout(timer);
|
|
39
50
|
}
|
|
40
51
|
})();
|
|
52
|
+
// Whatever has expired goes, and the oldest of the rest once there are too many.
|
|
53
|
+
for (const [key, entry] of contentCache) {
|
|
54
|
+
if (Date.now() - entry.at >= QUOTE_CACHE_MS || contentCache.size >= QUOTE_CACHE_MAX) {
|
|
55
|
+
contentCache.delete(key);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
41
58
|
contentCache.set(uid, { at: Date.now(), promise });
|
|
42
59
|
void promise.then((html) => {
|
|
43
60
|
if (html === undefined && contentCache.get(uid)?.promise === promise) {
|
|
@@ -68,6 +85,7 @@ export function prefetchOriginalMessage(message) {
|
|
|
68
85
|
export function clearOriginalMessageCache() {
|
|
69
86
|
contentCache.clear();
|
|
70
87
|
}
|
|
88
|
+
onSigningOut(clearOriginalMessageCache);
|
|
71
89
|
/** The recipients one address-list header names, RFC 2047-decoded, with their display names. */
|
|
72
90
|
function headerRecipients(value, type) {
|
|
73
91
|
if (!value) {
|
|
@@ -81,7 +99,7 @@ function headerRecipients(value, type) {
|
|
|
81
99
|
* or `undefined` when there is none, it is not a raster image, it is too large or it could not be had. Never rejects. */
|
|
82
100
|
function serverImageResolver(message) {
|
|
83
101
|
let attachments;
|
|
84
|
-
return async (reference) => {
|
|
102
|
+
return async (reference, signal) => {
|
|
85
103
|
try {
|
|
86
104
|
attachments ?? (attachments = listAttachments(message.folderUid, message.uid));
|
|
87
105
|
const attachment = findInlineAttachment(await attachments, reference);
|
|
@@ -89,7 +107,7 @@ function serverImageResolver(message) {
|
|
|
89
107
|
if (!attachment || !type || !EMBEDDED_IMAGE_TYPES.test(type) || attachment.sizeBytes > MAX_QUOTED_IMAGE_BYTES) {
|
|
90
108
|
return undefined;
|
|
91
109
|
}
|
|
92
|
-
const res = await fetch(attachmentContentUrl(attachment.uid), { credentials: "include" });
|
|
110
|
+
const res = await fetch(attachmentContentUrl(attachment.uid), { credentials: "include", signal });
|
|
93
111
|
const bytes = res.ok ? new Uint8Array(await res.arrayBuffer()) : undefined;
|
|
94
112
|
return bytes && bytes.length <= MAX_QUOTED_IMAGE_BYTES ? toDataUri(type, bytes) : undefined;
|
|
95
113
|
}
|
|
@@ -136,7 +154,9 @@ function partImageResolver(parts) {
|
|
|
136
154
|
*/
|
|
137
155
|
export async function loadOriginalMessage(message, security, options = {}) {
|
|
138
156
|
const recovered = security?.text !== undefined ? { text: security.text } : security?.html !== undefined ? { html: security.html } : undefined;
|
|
139
|
-
|
|
157
|
+
// Headers inside an unsigned (or badly signed) encrypted message are whatever its encrypter chose to write, so only a
|
|
158
|
+
// verified signature makes them a source of recipients.
|
|
159
|
+
const protectedHeaders = security && TRUSTED_HEADER_STATES.has(security.state) ? security.protectedHeaders : undefined;
|
|
140
160
|
const protectedRecipients = protectedHeaders
|
|
141
161
|
? [...headerRecipients(protectedHeaders.to, "to"), ...headerRecipients(protectedHeaders.cc, "cc")]
|
|
142
162
|
: [];
|
|
@@ -176,8 +196,11 @@ export async function loadOriginalMessage(message, security, options = {}) {
|
|
|
176
196
|
if (result.body.html !== undefined) {
|
|
177
197
|
const resolver = recovered ? partImageResolver(security?.attachments ?? []) : serverImageResolver(message);
|
|
178
198
|
const html = result.body.html;
|
|
179
|
-
// Bounded like the body's own request: a picture that
|
|
180
|
-
|
|
199
|
+
// Bounded like the body's own request: a picture that has not arrived by then is cancelled and left out of the quote, not waited for -
|
|
200
|
+
// the ones that have arrived stay.
|
|
201
|
+
const controller = new AbortController();
|
|
202
|
+
const timer = setTimeout(() => controller.abort(), QUOTE_FETCH_TIMEOUT_MS);
|
|
203
|
+
result.body = { html: await embedQuotedImages(html, resolver, controller.signal).finally(() => clearTimeout(timer)) };
|
|
181
204
|
}
|
|
182
205
|
return result;
|
|
183
206
|
}
|
|
@@ -204,7 +204,7 @@ export default function MailShell({ userUid, authServerUrl, impersonating, imper
|
|
|
204
204
|
// `mailboxes.length` directly (not `!mailboxUid`) since `mailboxUid` is legitimately undefined in
|
|
205
205
|
// aggregate mode even with mailboxes present.
|
|
206
206
|
if (userUid && status === "ready" && mailboxes.length === 0) {
|
|
207
|
-
return _jsx(MailboxProvisioning, {});
|
|
207
|
+
return _jsx(MailboxProvisioning, { impersonating: impersonating, impersonationBaseUrl: impersonationBaseUrl });
|
|
208
208
|
}
|
|
209
209
|
let inner = null;
|
|
210
210
|
if (userUid && status === "checking") {
|
|
@@ -39,7 +39,7 @@ const AUTHORED_CANVAS = "rgb(255, 255, 255)";
|
|
|
39
39
|
export default function MessageBody({ messageUid, messageVersion, title, content, attachments, inlineParts, original = false, onAdaptable }) {
|
|
40
40
|
const [remote, setRemote] = useState(() => {
|
|
41
41
|
const cached = cachedBodyContent(messageUid, messageVersion);
|
|
42
|
-
return cached ? { status: "ready", content: cached } : { status: "loading" };
|
|
42
|
+
return cached ? { status: "ready", content: cached, uid: messageUid } : { status: "loading" };
|
|
43
43
|
});
|
|
44
44
|
const [attempt, setAttempt] = useState(0);
|
|
45
45
|
const supplied = content !== undefined;
|
|
@@ -49,19 +49,24 @@ export default function MessageBody({ messageUid, messageVersion, title, content
|
|
|
49
49
|
}
|
|
50
50
|
const cached = cachedBodyContent(messageUid, messageVersion);
|
|
51
51
|
if (cached) {
|
|
52
|
-
setRemote({ status: "ready", content: cached });
|
|
52
|
+
setRemote({ status: "ready", content: cached, uid: messageUid });
|
|
53
53
|
return;
|
|
54
54
|
}
|
|
55
55
|
const controller = new AbortController();
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
// The same message under a newer version (opening it marked it read, a flag or a label changed): what is shown stays - no skeleton, and
|
|
57
|
+
// the frame is not torn down and laid out again - until the fresh copy is here, and a failed read leaves it too.
|
|
58
|
+
const revalidating = remote.status === "ready" && remote.uid === messageUid;
|
|
59
|
+
if (!revalidating) {
|
|
60
|
+
setRemote({ status: "loading" });
|
|
61
|
+
}
|
|
62
|
+
fetchBodyContent(messageUid, messageVersion, controller.signal).then((loaded) => setRemote({ status: "ready", content: loaded, uid: messageUid }), (err) => {
|
|
63
|
+
if (!controller.signal.aborted && !revalidating) {
|
|
59
64
|
setRemote({ status: "error", message: err instanceof ApiRequestError ? err.message : "Could not load this message." });
|
|
60
65
|
}
|
|
61
66
|
});
|
|
62
67
|
return () => controller.abort();
|
|
63
68
|
}, [messageUid, messageVersion, supplied, attempt]);
|
|
64
|
-
const shown = content ? { status: "ready", content } : remote;
|
|
69
|
+
const shown = content ? { status: "ready", content, uid: messageUid } : remote;
|
|
65
70
|
if (shown.status === "loading") {
|
|
66
71
|
return _jsx(BodySkeleton, {});
|
|
67
72
|
}
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { ApiRequestError, apiUrl } from "../../../../../lib/util/api.js";
|
|
6
6
|
import { attachmentContentUrl } from "../../../../../lib/mail/mailApi.js";
|
|
7
7
|
import { findInlineAttachment, toDataUri } from "../../../../../lib/mail/inlineImages.js";
|
|
8
|
+
import { onSigningOut } from "../compose/composeFlushRegistry.js";
|
|
8
9
|
/** How many bodies are remembered for the session - a thread that is expanded, collapsed and expanded again does not ask the server twice. */
|
|
9
10
|
const CACHE_LIMIT = 40;
|
|
10
11
|
/** What the pane asks `/content` for. */
|
|
@@ -14,6 +15,7 @@ const cache = new Map();
|
|
|
14
15
|
export function clearBodyContentCache() {
|
|
15
16
|
cache.clear();
|
|
16
17
|
}
|
|
18
|
+
onSigningOut(clearBodyContentCache);
|
|
17
19
|
/** The remembered body for a message at a version, if any. */
|
|
18
20
|
export function cachedBodyContent(uid, version) {
|
|
19
21
|
return cache.get(`${uid}:${version}`);
|
|
@@ -33,7 +33,11 @@ export interface PrepareOptions {
|
|
|
33
33
|
* server removes one it could not point at a part) is asked for with no `contentId` and its `alt`. */
|
|
34
34
|
resolveCid?: (contentId: string | undefined, alt?: string) => string | undefined;
|
|
35
35
|
}
|
|
36
|
-
/**
|
|
36
|
+
/**
|
|
37
|
+
* CSS with the constructs above neutralised. The patterns run on what a browser would read, not only on the raw text: `position:\66ixed` and
|
|
38
|
+
* `min-height:200\76h` are `position:fixed` and `200vh` to it, and a comment inside a name (`position/**\/:fixed`) is no obstacle either. When the
|
|
39
|
+
* decoded text holds one, that text (escapes resolved, comments dropped) is what is kept, neutralised; CSS whose escapes hide nothing is left as written.
|
|
40
|
+
*/
|
|
37
41
|
export declare function sanitizeCss(css: string): string;
|
|
38
42
|
/**
|
|
39
43
|
* Whether a mail's own markup declares that it supports a dark scheme: `<meta name="color-scheme">` naming dark,
|
|
@@ -35,10 +35,31 @@ const STYLE_HAZARDS = [
|
|
|
35
35
|
[/behavior\s*:/gi, "x-behavior:"],
|
|
36
36
|
[/@import[^;]*;?/gi, ""],
|
|
37
37
|
];
|
|
38
|
-
|
|
39
|
-
export function sanitizeCss(css) {
|
|
38
|
+
function neutraliseHazards(css) {
|
|
40
39
|
return STYLE_HAZARDS.reduce((text, [pattern, replacement]) => text.replace(pattern, replacement), css);
|
|
41
40
|
}
|
|
41
|
+
/** `css` as a browser reads it: comments gone, and every escape resolved - repeatedly, since a decoded backslash can start another (`\5c 76h`). */
|
|
42
|
+
function readAsBrowser(css) {
|
|
43
|
+
let text = css;
|
|
44
|
+
for (let pass = 0; pass < 3; pass++) {
|
|
45
|
+
const next = unescapeCss(text.replace(/\/\*[\s\S]*?\*\//g, " "));
|
|
46
|
+
if (next === text) {
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
text = next;
|
|
50
|
+
}
|
|
51
|
+
return text;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* CSS with the constructs above neutralised. The patterns run on what a browser would read, not only on the raw text: `position:\66ixed` and
|
|
55
|
+
* `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
|
|
56
|
+
* decoded text holds one, that text (escapes resolved, comments dropped) is what is kept, neutralised; CSS whose escapes hide nothing is left as written.
|
|
57
|
+
*/
|
|
58
|
+
export function sanitizeCss(css) {
|
|
59
|
+
const decoded = readAsBrowser(css);
|
|
60
|
+
const decodedClean = neutraliseHazards(decoded);
|
|
61
|
+
return decodedClean !== decoded ? decodedClean : neutraliseHazards(css);
|
|
62
|
+
}
|
|
42
63
|
/** Elements removed together with everything inside them. */
|
|
43
64
|
const REMOVED_ELEMENTS = new Set([
|
|
44
65
|
"script",
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
import { useEffect, useMemo, useState } from "react";
|
|
21
21
|
import { getMyMailboxAccess } from "../../../../lib/mail/mailboxAccessApi.js";
|
|
22
22
|
import { useApiClient } from "../../../../lib/util/apiClientContext.js";
|
|
23
|
+
import { onSigningOut } from "./compose/composeFlushRegistry.js";
|
|
23
24
|
const inFlight = new Map();
|
|
24
25
|
const settled = new Map();
|
|
25
26
|
/** Forgets every cached answer - for tests, and anything that changes the caller's access mid-session. */
|
|
@@ -27,6 +28,7 @@ export function clearMailboxWritabilityCache() {
|
|
|
27
28
|
inFlight.clear();
|
|
28
29
|
settled.clear();
|
|
29
30
|
}
|
|
31
|
+
onSigningOut(clearMailboxWritabilityCache);
|
|
30
32
|
function isImplicitlyWritable(mailbox, userUid, trusted) {
|
|
31
33
|
return !!trusted || (!!userUid && mailbox.ownerUserUid === userUid);
|
|
32
34
|
}
|
|
@@ -36,6 +36,8 @@ export function hasConditions(conditions) {
|
|
|
36
36
|
export default function RuleBuilder({ value, onChange, conditionFields, actionTypes, showValidation = false, }) {
|
|
37
37
|
const [draftText, setDraftText] = useState({});
|
|
38
38
|
const [newActionType, setNewActionType] = useState(actionTypes[0]?.value ?? "");
|
|
39
|
+
// The sequence field's text while it is being edited (it may be empty, which is no number); `null` shows the rule's own.
|
|
40
|
+
const [sequenceDraft, setSequenceDraft] = useState(null);
|
|
39
41
|
const conditions = value.conditions;
|
|
40
42
|
function patch(next) {
|
|
41
43
|
onChange({ ...value, ...next });
|
|
@@ -102,5 +104,11 @@ export default function RuleBuilder({ value, onChange, conditionFields, actionTy
|
|
|
102
104
|
}) })] }), _jsxs("div", { className: "bg-surface border border-border rounded-md p-6", children: [_jsx("h2", { className: "text-base font-bold uppercase tracking-wide mb-4", children: "Actions" }), _jsxs("div", { className: "flex flex-col gap-3 mb-4", children: [value.actions.length === 0 && _jsx("p", { className: "text-sm text-text-muted", children: "No actions yet." }), value.actions.map((action, index) => {
|
|
103
105
|
const def = actionTypes.find((t) => t.value === action.type);
|
|
104
106
|
return (_jsxs("div", { className: "border border-border rounded-sm p-3", children: [_jsxs("div", { className: "flex items-center justify-between mb-2", children: [_jsx("span", { className: "text-sm font-semibold", children: def?.label ?? action.type }), _jsx("button", { type: "button", onClick: () => removeAction(index), className: "text-sm text-danger hover:underline", children: "Remove" })] }), def?.render(action, (next) => updateAction(index, next))] }, index));
|
|
105
|
-
})] }), _jsxs("div", { className: "flex gap-2", children: [_jsx("select", { "aria-label": "New action type", className: INPUT_CLASS, value: newActionType, onChange: (e) => setNewActionType(e.target.value), children: actionTypes.map((t) => (_jsx("option", { value: t.value, children: t.label }, t.value))) }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: addAction, children: "Add action" })] })] }), _jsxs("div", { className: "bg-surface border border-border rounded-md p-6", children: [_jsx("h2", { className: "text-base font-bold uppercase tracking-wide mb-4", children: "Settings" }), _jsxs("div", { className: "flex flex-col gap-3", children: [_jsxs("label", { className: "flex items-center gap-2 text-sm", children: [_jsx("input", { type: "checkbox", checked: value.enabled, onChange: (e) => patch({ enabled: e.target.checked }) }), "Enabled"] }), _jsxs("label", { className: "flex items-center gap-2 text-sm", children: [_jsx("input", { type: "checkbox", checked: value.stopProcessingRules, onChange: (e) => patch({ stopProcessingRules: e.target.checked }) }), "Stop processing more rules once this one matches"] }), _jsxs("label", { className: "flex items-center gap-2 text-sm", children: ["Sequence (evaluation order, ascending)", _jsx("input", { type: "number", 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", value: value.sequence, onChange: (e) =>
|
|
107
|
+
})] }), _jsxs("div", { className: "flex gap-2", children: [_jsx("select", { "aria-label": "New action type", className: INPUT_CLASS, value: newActionType, onChange: (e) => setNewActionType(e.target.value), children: actionTypes.map((t) => (_jsx("option", { value: t.value, children: t.label }, t.value))) }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: addAction, children: "Add action" })] })] }), _jsxs("div", { className: "bg-surface border border-border rounded-md p-6", children: [_jsx("h2", { className: "text-base font-bold uppercase tracking-wide mb-4", children: "Settings" }), _jsxs("div", { className: "flex flex-col gap-3", children: [_jsxs("label", { className: "flex items-center gap-2 text-sm", children: [_jsx("input", { type: "checkbox", checked: value.enabled, onChange: (e) => patch({ enabled: e.target.checked }) }), "Enabled"] }), _jsxs("label", { className: "flex items-center gap-2 text-sm", children: [_jsx("input", { type: "checkbox", checked: value.stopProcessingRules, onChange: (e) => patch({ stopProcessingRules: e.target.checked }) }), "Stop processing more rules once this one matches"] }), _jsxs("label", { className: "flex items-center gap-2 text-sm", children: ["Sequence (evaluation order, ascending)", _jsx("input", { type: "number", 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", value: sequenceDraft ?? value.sequence, onChange: (e) => {
|
|
108
|
+
// What is typed is kept as typed, so the field can be cleared and typed afresh; the rule only takes a number.
|
|
109
|
+
setSequenceDraft(e.target.value);
|
|
110
|
+
if (e.target.value !== "") {
|
|
111
|
+
patch({ sequence: Number(e.target.value) });
|
|
112
|
+
}
|
|
113
|
+
}, onBlur: () => setSequenceDraft(null) })] })] })] })] }));
|
|
106
114
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
/**
|
|
3
|
+
* The opt-in for Gravatar contact pictures. A per-browser preference (not the mailbox's), off until turned on, because looking a picture up tells
|
|
4
|
+
* gravatar.com the contact's address hash and the reader's IP address.
|
|
5
|
+
*/
|
|
6
|
+
export default function GravatarSetting(): React.JSX.Element;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
3
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
4
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
5
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
6
|
+
import { useSyncExternalStore } from "react";
|
|
7
|
+
import { gravatarEnabled, setGravatarEnabled, subscribeGravatarPreference } from "../../../../lib/contacts/gravatar.js";
|
|
8
|
+
/**
|
|
9
|
+
* The opt-in for Gravatar contact pictures. A per-browser preference (not the mailbox's), off until turned on, because looking a picture up tells
|
|
10
|
+
* gravatar.com the contact's address hash and the reader's IP address.
|
|
11
|
+
*/
|
|
12
|
+
export default function GravatarSetting() {
|
|
13
|
+
const enabled = useSyncExternalStore(subscribeGravatarPreference, gravatarEnabled, () => false);
|
|
14
|
+
return (_jsxs("div", { children: [_jsx("h2", { className: "text-sm font-semibold mb-2", children: "Contact pictures" }), _jsxs("label", { className: "flex items-center gap-2 text-sm", children: [_jsx("input", { type: "checkbox", checked: enabled, onChange: (e) => setGravatarEnabled(e.target.checked) }), "Show profile pictures from Gravatar"] }), _jsx("p", { className: "text-xs text-text-muted mt-2", children: "Looks up each contact's picture by sending a hash of their email address to gravatar.com, which also sees your IP address. Off by default; applies to this browser only." })] }));
|
|
15
|
+
}
|
|
@@ -93,7 +93,7 @@ export default function SettingsShell({ active, userUid, authServerUrl, imperson
|
|
|
93
93
|
// A full-screen takeover, not nested inside the rest of the app's chrome — same as ContactsShell/
|
|
94
94
|
// TasksShell, there's nothing else for a mailbox-less caller to configure here yet.
|
|
95
95
|
if (userUid && status === "ready" && !mailboxUid) {
|
|
96
|
-
return _jsx(MailboxProvisioning, {});
|
|
96
|
+
return _jsx(MailboxProvisioning, { impersonating: impersonating, impersonationBaseUrl: impersonationBaseUrl });
|
|
97
97
|
}
|
|
98
98
|
let inner = null;
|
|
99
99
|
if (userUid && status === "checking") {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The stored value of a date-only due date (a `yyyy-MM-dd` day from a date input): UTC midnight of that day, so the task is due on the same
|
|
3
|
+
* calendar day for everyone, whatever zone they read it in - the way an all-day event is stored. (The creator's local midnight would be a
|
|
4
|
+
* different day for a reader in another zone.)
|
|
5
|
+
*/
|
|
6
|
+
export declare function dueDateInstant(dateKey: string): string;
|
|
7
|
+
/**
|
|
8
|
+
* A task's `dueDate` as a local `Date`. An exact UTC midnight is a date-only due date (see `dueDateInstant()`) and reads as the start of that
|
|
9
|
+
* calendar day wherever the reader is; any other instant - a time on the day, or a date-only value an older version stored as the creator's local
|
|
10
|
+
* midnight - is that instant itself. (`parseISO()` rather than `new Date()`: a bare `yyyy-MM-dd` would be read as UTC.)
|
|
11
|
+
*/
|
|
12
|
+
export declare function parseDueDate(value: string): Date;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
import { parseISO } from "date-fns";
|
|
6
|
+
const MS_PER_DAY = 86400000;
|
|
7
|
+
/**
|
|
8
|
+
* The stored value of a date-only due date (a `yyyy-MM-dd` day from a date input): UTC midnight of that day, so the task is due on the same
|
|
9
|
+
* calendar day for everyone, whatever zone they read it in - the way an all-day event is stored. (The creator's local midnight would be a
|
|
10
|
+
* different day for a reader in another zone.)
|
|
11
|
+
*/
|
|
12
|
+
export function dueDateInstant(dateKey) {
|
|
13
|
+
return `${dateKey}T00:00:00.000Z`;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* A task's `dueDate` as a local `Date`. An exact UTC midnight is a date-only due date (see `dueDateInstant()`) and reads as the start of that
|
|
17
|
+
* calendar day wherever the reader is; any other instant - a time on the day, or a date-only value an older version stored as the creator's local
|
|
18
|
+
* midnight - is that instant itself. (`parseISO()` rather than `new Date()`: a bare `yyyy-MM-dd` would be read as UTC.)
|
|
19
|
+
*/
|
|
20
|
+
export function parseDueDate(value) {
|
|
21
|
+
const instant = parseISO(value);
|
|
22
|
+
if (instant.getTime() % MS_PER_DAY !== 0) {
|
|
23
|
+
return instant;
|
|
24
|
+
}
|
|
25
|
+
return new Date(instant.getUTCFullYear(), instant.getUTCMonth(), instant.getUTCDate());
|
|
26
|
+
}
|
|
@@ -79,7 +79,7 @@ export default function TasksShell({ userUid, authServerUrl, impersonating, impe
|
|
|
79
79
|
// A full-screen takeover, not nested inside the rest of the app's chrome — there's nothing else
|
|
80
80
|
// for a mailbox-less caller to do here yet, so the icon rail/header don't render at all.
|
|
81
81
|
if (userUid && status === "ready" && !mailboxUid) {
|
|
82
|
-
return _jsx(MailboxProvisioning, {});
|
|
82
|
+
return _jsx(MailboxProvisioning, { impersonating: impersonating, impersonationBaseUrl: impersonationBaseUrl });
|
|
83
83
|
}
|
|
84
84
|
let inner = null;
|
|
85
85
|
if (userUid && status === "checking") {
|
|
@@ -9,6 +9,14 @@ export interface DecryptedMessage {
|
|
|
9
9
|
subject?: string;
|
|
10
10
|
preview?: string;
|
|
11
11
|
}
|
|
12
|
+
/** Whether decrypting this message already came to nothing since the keys were last unlocked, recently enough that trying again would only repeat it. */
|
|
13
|
+
export declare function decryptFailedBefore(uid: string): boolean;
|
|
14
|
+
/** Remembers that decrypting this message came to nothing, so a list that refreshes does not download and try it again each time. */
|
|
15
|
+
export declare function markDecryptFailed(uid: string): void;
|
|
16
|
+
/** Forgets everything remembered about messages that could not be decrypted (sign-out, an account switch). */
|
|
17
|
+
export declare function clearDecryptedMessages(): void;
|
|
18
|
+
/** Runs `task` over `items` with at most `limit` running at once, resolving to the results in the order of `items`. */
|
|
19
|
+
export declare function mapWithConcurrency<T, R>(items: T[], limit: number, task: (item: T) => Promise<R>): Promise<R[]>;
|
|
12
20
|
/** Plain text of decrypted HTML for a one-line preview. Only ever feeds text display, never markup. */
|
|
13
21
|
export declare function previewOf(html: string): string;
|
|
14
22
|
/** Remembers what was recovered of one message. Ignored when there is nothing new to show. */
|
|
@@ -5,12 +5,52 @@
|
|
|
5
5
|
import { useSyncExternalStore } from "react";
|
|
6
6
|
import { getUnlockedKeys, subscribeKeySession } from "../../../lib/crypto/keySession.js";
|
|
7
7
|
import { getMessageRawContent } from "../../../lib/mail/mailApi.js";
|
|
8
|
+
import { onSigningOut } from "../components/mail/compose/composeFlushRegistry.js";
|
|
8
9
|
const PREVIEW_MAX_LENGTH = 160;
|
|
9
10
|
let entries = {};
|
|
10
11
|
/** Bumped whenever a mailbox's keys are unlocked, so a view can retry decrypting what it couldn't before. */
|
|
11
12
|
let unlockEpoch = 0;
|
|
12
13
|
const listeners = new Set();
|
|
13
14
|
const inFlight = new Set();
|
|
15
|
+
/** How long a message that could not be decrypted is left alone before it is tried again (it is also tried again after any unlock). */
|
|
16
|
+
const DECRYPT_RETRY_MS = 10 * 60000;
|
|
17
|
+
/** How many such messages are remembered. */
|
|
18
|
+
const MAX_FAILED = 1000;
|
|
19
|
+
/** The messages whose decrypting came to nothing (fetch failed, not for this key, nothing readable), by uid: when and in which unlock epoch. */
|
|
20
|
+
const failed = new Map();
|
|
21
|
+
/** Whether decrypting this message already came to nothing since the keys were last unlocked, recently enough that trying again would only repeat it. */
|
|
22
|
+
export function decryptFailedBefore(uid) {
|
|
23
|
+
const known = failed.get(uid);
|
|
24
|
+
return !!known && known.epoch === unlockEpoch && Date.now() - known.at < DECRYPT_RETRY_MS;
|
|
25
|
+
}
|
|
26
|
+
/** Remembers that decrypting this message came to nothing, so a list that refreshes does not download and try it again each time. */
|
|
27
|
+
export function markDecryptFailed(uid) {
|
|
28
|
+
failed.delete(uid);
|
|
29
|
+
if (failed.size >= MAX_FAILED) {
|
|
30
|
+
failed.delete(failed.keys().next().value);
|
|
31
|
+
}
|
|
32
|
+
failed.set(uid, { epoch: unlockEpoch, at: Date.now() });
|
|
33
|
+
}
|
|
34
|
+
/** Forgets everything remembered about messages that could not be decrypted (sign-out, an account switch). */
|
|
35
|
+
export function clearDecryptedMessages() {
|
|
36
|
+
entries = {};
|
|
37
|
+
failed.clear();
|
|
38
|
+
emit();
|
|
39
|
+
}
|
|
40
|
+
onSigningOut(clearDecryptedMessages);
|
|
41
|
+
/** Runs `task` over `items` with at most `limit` running at once, resolving to the results in the order of `items`. */
|
|
42
|
+
export async function mapWithConcurrency(items, limit, task) {
|
|
43
|
+
const results = new Array(items.length);
|
|
44
|
+
let next = 0;
|
|
45
|
+
async function worker() {
|
|
46
|
+
while (next < items.length) {
|
|
47
|
+
const index = next++;
|
|
48
|
+
results[index] = await task(items[index]);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
|
|
52
|
+
return results;
|
|
53
|
+
}
|
|
14
54
|
function emit() {
|
|
15
55
|
for (const listener of [...listeners]) {
|
|
16
56
|
listener();
|
|
@@ -75,7 +115,7 @@ export function useUnlockEpoch() {
|
|
|
75
115
|
*/
|
|
76
116
|
export async function decryptForDisplay(uid, mailboxUid) {
|
|
77
117
|
const unlocked = getUnlockedKeys(mailboxUid);
|
|
78
|
-
if (!unlocked || entries[uid] || inFlight.has(uid)) {
|
|
118
|
+
if (!unlocked || entries[uid] || inFlight.has(uid) || decryptFailedBefore(uid)) {
|
|
79
119
|
return;
|
|
80
120
|
}
|
|
81
121
|
inFlight.add(uid);
|
|
@@ -86,10 +126,14 @@ export async function decryptForDisplay(uid, mailboxUid) {
|
|
|
86
126
|
// A lock while this ran: the result must not go back on screen.
|
|
87
127
|
if (getUnlockedKeys(mailboxUid) === unlocked) {
|
|
88
128
|
rememberDecrypted(uid, mailboxUid, { subject: security.subject, preview: security.html ? previewOf(security.html) : security.text?.slice(0, PREVIEW_MAX_LENGTH) });
|
|
129
|
+
if (!security.subject && !security.html && !security.text) {
|
|
130
|
+
markDecryptFailed(uid);
|
|
131
|
+
}
|
|
89
132
|
}
|
|
90
133
|
}
|
|
91
134
|
catch {
|
|
92
|
-
// Left as the placeholder.
|
|
135
|
+
// Left as the placeholder - and not fetched again until the next unlock.
|
|
136
|
+
markDecryptFailed(uid);
|
|
93
137
|
}
|
|
94
138
|
finally {
|
|
95
139
|
inFlight.delete(uid);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { onSigningOut } from "../components/mail/compose/composeFlushRegistry.js";
|
|
1
2
|
/** How long a snapshot may be shown after it was last written. Longer than that the folder is shown as loading, as before. */
|
|
2
3
|
export const LIST_SNAPSHOT_TTL_MS = 5 * 60000;
|
|
3
4
|
/** How many folders are remembered; the least recently written is dropped first. */
|
|
@@ -41,3 +42,4 @@ export function saveListScroll(key, scrollTop) {
|
|
|
41
42
|
export function clearListSnapshots() {
|
|
42
43
|
snapshots.clear();
|
|
43
44
|
}
|
|
45
|
+
onSigningOut(clearListSnapshots);
|
|
@@ -73,8 +73,11 @@ export interface SendDecisionInput {
|
|
|
73
73
|
* can choose, or rejected outright. Used twice with the same rules - by the compose window, from what it already knows, to catch a problem
|
|
74
74
|
* *before* it closes (`lookupsComplete: false`), and by the background send, once its recipient lookups are in.
|
|
75
75
|
*
|
|
76
|
-
* Encryption follows `evaluateEncryptionRequirement()`
|
|
77
|
-
* encrypts, is encrypted; everything unknown is plain
|
|
78
|
-
*
|
|
76
|
+
* Encryption follows `evaluateEncryptionRequirement()`. The compose window's early check (`lookupsComplete: false`) is fail open: only a message
|
|
77
|
+
* the user asked to encrypt, or one the loaded policy encrypts, is encrypted there; everything unknown is plain, so a flapping gateway never
|
|
78
|
+
* blocks a draft or Close. The background send (`lookupsComplete: true`) is not: once its loads are in, a message that might have to be encrypted
|
|
79
|
+
* because the policy or a recipient's key could not be checked (the sender having a key to encrypt with) is blocked (`lookup-failed`) rather than
|
|
80
|
+
* sent in the clear - and so is one the user *explicitly* asked to encrypt whose recipients' keys could not be looked up. Either way the sender can
|
|
81
|
+
* choose to send it without encryption.
|
|
79
82
|
*/
|
|
80
83
|
export declare function decideSend(input: SendDecisionInput): SendDecision;
|
|
@@ -25,9 +25,12 @@ export const INLINE_IMAGES_UNSUPPORTED_MESSAGE = "A signed or encrypted message
|
|
|
25
25
|
* can choose, or rejected outright. Used twice with the same rules - by the compose window, from what it already knows, to catch a problem
|
|
26
26
|
* *before* it closes (`lookupsComplete: false`), and by the background send, once its recipient lookups are in.
|
|
27
27
|
*
|
|
28
|
-
* Encryption follows `evaluateEncryptionRequirement()`
|
|
29
|
-
* encrypts, is encrypted; everything unknown is plain
|
|
30
|
-
*
|
|
28
|
+
* Encryption follows `evaluateEncryptionRequirement()`. The compose window's early check (`lookupsComplete: false`) is fail open: only a message
|
|
29
|
+
* the user asked to encrypt, or one the loaded policy encrypts, is encrypted there; everything unknown is plain, so a flapping gateway never
|
|
30
|
+
* blocks a draft or Close. The background send (`lookupsComplete: true`) is not: once its loads are in, a message that might have to be encrypted
|
|
31
|
+
* because the policy or a recipient's key could not be checked (the sender having a key to encrypt with) is blocked (`lookup-failed`) rather than
|
|
32
|
+
* sent in the clear - and so is one the user *explicitly* asked to encrypt whose recipients' keys could not be looked up. Either way the sender can
|
|
33
|
+
* choose to send it without encryption.
|
|
31
34
|
*/
|
|
32
35
|
export function decideSend(input) {
|
|
33
36
|
const { keys } = input;
|
|
@@ -60,6 +63,13 @@ export function decideSend(input) {
|
|
|
60
63
|
recipients: input.recipients,
|
|
61
64
|
});
|
|
62
65
|
const encrypting = requirement.required;
|
|
66
|
+
if (!encrypting && input.lookupsComplete && requirement.unknown.some((fact) => fact !== "mailbox")) {
|
|
67
|
+
// The sender has a key to encrypt with, the loads are all in (or gave up), and the policy or a recipient's key is still unknown: the message
|
|
68
|
+
// might have to be encrypted, and plaintext must not go out on a guess. The sender can still choose to send it without encryption. (An unknown
|
|
69
|
+
// *mailbox* is not one of these: with no key known there is nothing to say encryption applies, and a mailbox that will not load must not
|
|
70
|
+
// stop everyone's mail.)
|
|
71
|
+
return { action: "blocked", block: { kind: "lookup-failed", message: LOOKUP_UNAVAILABLE_MESSAGE, overrideLabel: "Send without encryption", keysLocked: false } };
|
|
72
|
+
}
|
|
63
73
|
if (encrypting) {
|
|
64
74
|
const statuses = input.recipients.filter((status) => !!status);
|
|
65
75
|
const allResolved = statuses.length === input.recipients.length;
|