@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
package/lib/mail/mailApi.ts
CHANGED
|
@@ -13,12 +13,23 @@
|
|
|
13
13
|
* `@rapidmx/restapi` and this repo's `.claude/NOTES.md`.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
import {
|
|
17
|
-
|
|
16
|
+
import {
|
|
17
|
+
ApiClient,
|
|
18
|
+
ApiRequestError,
|
|
19
|
+
apiFetch,
|
|
20
|
+
apiUrl,
|
|
21
|
+
authApiFetch,
|
|
22
|
+
withClient,
|
|
23
|
+
withClientBlob,
|
|
24
|
+
withClientRaw,
|
|
25
|
+
withCsrfHeader,
|
|
26
|
+
} from "../util/api.js";
|
|
27
|
+
import { ListParams, buildQuery, sortQuery } from "../util/apiQuery.js";
|
|
18
28
|
import { deviceTimeZone } from "../util/timeZone.js";
|
|
19
29
|
import type { EncryptionPreference, PublicKey } from "../crypto/keyvaultApi.js";
|
|
20
30
|
import { bytesToBinaryString } from "../crypto/mime.js";
|
|
21
|
-
import { newContentId, parseDataImage } from "./inlineImages.js";
|
|
31
|
+
import { EMBEDDED_IMAGE_TYPES, newContentId, parseDataImage } from "./inlineImages.js";
|
|
32
|
+
import { MAX_QUOTED_IMAGE_BYTES } from "./compose/quotedImages.js";
|
|
22
33
|
import type { ResolvedPrincipal } from "./mailboxAccessApi.js";
|
|
23
34
|
|
|
24
35
|
export type { ListParams };
|
|
@@ -156,7 +167,7 @@ export function safeSendersOf(mailbox: Pick<Mailbox, "safeSenders">): string[] {
|
|
|
156
167
|
/** Lists the mailboxes the caller owns or has been granted - the same for everyone, an administrator included. With
|
|
157
168
|
* `scope: "admin"` (admin console only) it lists every mailbox as administrative metadata instead. */
|
|
158
169
|
export function listMailboxes(params: ListParams & AdminScopeParams = {}, client?: ApiClient): Promise<Mailbox[]> {
|
|
159
|
-
return withClient(client, `/mail/mailboxes?${buildQuery(params, scopeQuery(params))}`);
|
|
170
|
+
return withClient(client, `/mail/mailboxes?${buildQuery(params, { ...scopeQuery(params), ...(params.scope === "admin" ? sortQuery({ uid: "ASC" }) : {}) })}`);
|
|
160
171
|
}
|
|
161
172
|
|
|
162
173
|
/** One mailbox the caller owns or has been granted; with `scope: "admin"` (admin console only), its administrative metadata. */
|
|
@@ -320,7 +331,7 @@ export function listQuarantine(
|
|
|
320
331
|
params: ListParams & AdminScopeParams = {},
|
|
321
332
|
client?: ApiClient,
|
|
322
333
|
): Promise<QuarantineEntry[]> {
|
|
323
|
-
return withClient(client, `/mail/quarantine?${buildQuery(params, { mailboxUid, ...scopeQuery(params) })}`);
|
|
334
|
+
return withClient(client, `/mail/quarantine?${buildQuery(params, { mailboxUid, ...scopeQuery(params), ...sortQuery({ dateCreated: "DESC", uid: "ASC" }) })}`);
|
|
324
335
|
}
|
|
325
336
|
|
|
326
337
|
/**
|
|
@@ -368,7 +379,7 @@ export function listIngestQueue(
|
|
|
368
379
|
params: ListParams & AdminScopeParams = {},
|
|
369
380
|
client?: ApiClient,
|
|
370
381
|
): Promise<IngestQueueEntry[]> {
|
|
371
|
-
return withClient(client, `/mail/ingest-queue?${buildQuery(params, { mailboxUid, ...scopeQuery(params) })}`);
|
|
382
|
+
return withClient(client, `/mail/ingest-queue?${buildQuery(params, { mailboxUid, ...scopeQuery(params), ...sortQuery({ dateCreated: "DESC", uid: "ASC" }) })}`);
|
|
372
383
|
}
|
|
373
384
|
|
|
374
385
|
export interface AclRecord {
|
|
@@ -1029,9 +1040,14 @@ export function attachmentContentUrl(uid: string): string {
|
|
|
1029
1040
|
/**
|
|
1030
1041
|
* Uploads a file's raw bytes as a new attachment on a not-yet-sent draft. Bypasses `apiFetch` — that helper
|
|
1031
1042
|
* always forces `Content-Type: application/json`, which would corrupt binary content; this sends the file's
|
|
1032
|
-
* own bytes/type directly instead, matching `BaseAttachmentRoute.upload`'s expectation of a raw request body.
|
|
1043
|
+
* own bytes/type directly instead (through `client` when one is given), matching `BaseAttachmentRoute.upload`'s expectation of a raw request body.
|
|
1033
1044
|
*/
|
|
1034
|
-
export
|
|
1045
|
+
export function uploadAttachment(
|
|
1046
|
+
messageUid: string,
|
|
1047
|
+
file: File,
|
|
1048
|
+
options: { inline?: boolean; contentId?: string } = {},
|
|
1049
|
+
client?: ApiClient,
|
|
1050
|
+
): Promise<Attachment> {
|
|
1035
1051
|
// Percent-encoded, not form-encoded (`URLSearchParams` writes a space as `+`, which the server reads as a plus sign: "My+photo.png").
|
|
1036
1052
|
const query: Record<string, string> = { messageUid, filename: file.name, mimeType: file.type || "application/octet-stream" };
|
|
1037
1053
|
if (options.inline) {
|
|
@@ -1043,19 +1059,7 @@ export async function uploadAttachment(messageUid: string, file: File, options:
|
|
|
1043
1059
|
const params = Object.entries(query)
|
|
1044
1060
|
.map(([key, value]) => `${key}=${encodeURIComponent(value)}`)
|
|
1045
1061
|
.join("&");
|
|
1046
|
-
|
|
1047
|
-
method: "POST",
|
|
1048
|
-
credentials: "include",
|
|
1049
|
-
headers: withCsrfHeader({ "Content-Type": file.type || "application/octet-stream" }),
|
|
1050
|
-
body: file,
|
|
1051
|
-
});
|
|
1052
|
-
const contentType = res.headers.get("content-type") ?? "";
|
|
1053
|
-
const responseBody = contentType.includes("application/json") ? await res.json().catch(() => undefined) : undefined;
|
|
1054
|
-
if (!res.ok) {
|
|
1055
|
-
const message = (responseBody && (responseBody.message || responseBody.error)) || res.statusText || "Upload failed.";
|
|
1056
|
-
throw new ApiRequestError(message, res.status, responseBody?.code);
|
|
1057
|
-
}
|
|
1058
|
-
return responseBody as Attachment;
|
|
1062
|
+
return withClientRaw(client, `/mail/attachments/upload?${params}`, "POST", file, file.type || "application/octet-stream");
|
|
1059
1063
|
}
|
|
1060
1064
|
|
|
1061
1065
|
export interface ComposeRecipientInput {
|
|
@@ -1149,15 +1153,58 @@ export function emptyFolder(folderUid: string, client?: ApiClient): Promise<void
|
|
|
1149
1153
|
* itself does no MIME composition). Does not send the message.
|
|
1150
1154
|
*/
|
|
1151
1155
|
export async function assembleDraft(messageUid: string, input: AssembleDraftInput, client?: ApiClient): Promise<Message> {
|
|
1152
|
-
const html = await attachDataImages(messageUid, input.html);
|
|
1156
|
+
const html = await attachDataImages(messageUid, input.html, client);
|
|
1153
1157
|
return withClient(client, `/mail/compose/${encodeURIComponent(messageUid)}/assemble`, {
|
|
1154
1158
|
method: "POST",
|
|
1155
1159
|
body: JSON.stringify({ ...input, html }),
|
|
1156
1160
|
});
|
|
1157
1161
|
}
|
|
1158
1162
|
|
|
1159
|
-
/** `data:` images already attached to a draft: its uid and the image, to the URL of the attachment it became. */
|
|
1163
|
+
/** `data:` images already attached to a draft: its uid and a fingerprint of the image, to the URL of the attachment it became. */
|
|
1160
1164
|
const attachedDataImages = new Map<string, string>();
|
|
1165
|
+
/** The most pictures remembered; the oldest go first (a draft that was never sent or discarded here would otherwise keep its pictures forever). */
|
|
1166
|
+
const MAX_ATTACHED_DATA_IMAGES = 256;
|
|
1167
|
+
|
|
1168
|
+
/** The SHA-256 of the whole of `text`, in hex - what stands for a `data:` URI of up to megabytes without holding it, and which no two quoted pictures can be made to share. Where Web Crypto is not available the text itself stands for it. */
|
|
1169
|
+
async function fingerprint(text: string): Promise<string> {
|
|
1170
|
+
if (!globalThis.crypto?.subtle) {
|
|
1171
|
+
return text;
|
|
1172
|
+
}
|
|
1173
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(text));
|
|
1174
|
+
return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
/** The most pictures attached to one draft from its text; a quote cannot make a person's reply upload (and be charged for) any number of them. */
|
|
1178
|
+
const MAX_DATA_IMAGES_PER_DRAFT = 20;
|
|
1179
|
+
|
|
1180
|
+
/** Whether `bytes` start the way a picture of `contentType` does - the type a quote declares is its own word, and the bytes are what the server stores and shows. */
|
|
1181
|
+
function hasImageSignature(contentType: string, bytes: Uint8Array): boolean {
|
|
1182
|
+
const at = (offset: number, text: string) => Array.from(text).every((char, i) => bytes[offset + i] === char.charCodeAt(0));
|
|
1183
|
+
switch (contentType.split("/")[1]) {
|
|
1184
|
+
case "png":
|
|
1185
|
+
return at(0, "\x89PNG");
|
|
1186
|
+
case "jpg":
|
|
1187
|
+
case "jpeg":
|
|
1188
|
+
return at(0, "\xff\xd8\xff");
|
|
1189
|
+
case "gif":
|
|
1190
|
+
return at(0, "GIF8");
|
|
1191
|
+
case "webp":
|
|
1192
|
+
return at(0, "RIFF") && at(8, "WEBP");
|
|
1193
|
+
case "avif":
|
|
1194
|
+
return at(4, "ftypavi");
|
|
1195
|
+
default:
|
|
1196
|
+
return at(0, "BM");
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
/** Forgets the pictures attached to draft `messageUid`, for when it has been sent or deleted (they are only remembered to avoid attaching one twice). */
|
|
1201
|
+
export function forgetDraftImages(messageUid: string): void {
|
|
1202
|
+
for (const key of Array.from(attachedDataImages.keys())) {
|
|
1203
|
+
if (key.startsWith(`${messageUid}\n`)) {
|
|
1204
|
+
attachedDataImages.delete(key);
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1161
1208
|
|
|
1162
1209
|
const DATA_IMAGE_TAG = /(<img\b[^>]*?\bsrc\s*=\s*)(["'])(data:image\/[^"']*)\2/gi;
|
|
1163
1210
|
|
|
@@ -1169,28 +1216,39 @@ function pictureName(contentType: string): string {
|
|
|
1169
1216
|
/**
|
|
1170
1217
|
* `html` with each `<img>` that embeds its picture as a `data:` URI (a quoted original's image, one pasted into the editor) pointed at an
|
|
1171
1218
|
* attachment of the draft instead: the server drops `data:` images from the message it composes, and an attachment is what it makes an
|
|
1172
|
-
* inline part of. A picture is attached once per draft however often the draft is saved.
|
|
1219
|
+
* inline part of. A picture is attached once per draft however often the draft is saved. Only what a quote itself embeds is attached - a raster
|
|
1220
|
+
* image (`EMBEDDED_IMAGE_TYPES`, never SVG) of at most `MAX_QUOTED_IMAGE_BYTES` whose bytes are of the type it declares, and no more than
|
|
1221
|
+
* `MAX_DATA_IMAGES_PER_DRAFT` of them; any other is left as it is, and the server drops it.
|
|
1173
1222
|
*/
|
|
1174
|
-
async function attachDataImages(messageUid: string, html: string): Promise<string> {
|
|
1223
|
+
async function attachDataImages(messageUid: string, html: string, client?: ApiClient): Promise<string> {
|
|
1175
1224
|
const found = Array.from(html.matchAll(DATA_IMAGE_TAG));
|
|
1176
1225
|
if (found.length === 0) {
|
|
1177
1226
|
return html;
|
|
1178
1227
|
}
|
|
1179
1228
|
const urls = new Map<string, string>();
|
|
1180
1229
|
for (const [, , , uri] of found) {
|
|
1181
|
-
|
|
1230
|
+
if (urls.has(uri)) {
|
|
1231
|
+
continue;
|
|
1232
|
+
}
|
|
1233
|
+
if (urls.size >= MAX_DATA_IMAGES_PER_DRAFT) {
|
|
1234
|
+
break;
|
|
1235
|
+
}
|
|
1236
|
+
const key = `${messageUid}\n${await fingerprint(uri)}`;
|
|
1182
1237
|
const known = attachedDataImages.get(key);
|
|
1183
1238
|
if (known) {
|
|
1184
1239
|
urls.set(uri, known);
|
|
1185
1240
|
continue;
|
|
1186
1241
|
}
|
|
1187
1242
|
const image = parseDataImage(uri);
|
|
1188
|
-
if (!image) {
|
|
1243
|
+
if (!image || !EMBEDDED_IMAGE_TYPES.test(image.contentType) || image.bytes.length > MAX_QUOTED_IMAGE_BYTES || !hasImageSignature(image.contentType, image.bytes)) {
|
|
1189
1244
|
continue;
|
|
1190
1245
|
}
|
|
1191
|
-
const attachment = await uploadAttachment(messageUid, new File([image.bytes as BlobPart], pictureName(image.contentType), { type: image.contentType }), { inline: true });
|
|
1246
|
+
const attachment = await uploadAttachment(messageUid, new File([image.bytes as BlobPart], pictureName(image.contentType), { type: image.contentType }), { inline: true }, client);
|
|
1192
1247
|
const url = attachmentContentUrl(attachment.uid);
|
|
1193
1248
|
attachedDataImages.set(key, url);
|
|
1249
|
+
if (attachedDataImages.size > MAX_ATTACHED_DATA_IMAGES) {
|
|
1250
|
+
attachedDataImages.delete(attachedDataImages.keys().next().value!);
|
|
1251
|
+
}
|
|
1194
1252
|
urls.set(uri, url);
|
|
1195
1253
|
}
|
|
1196
1254
|
return html.replace(DATA_IMAGE_TAG, (match, start: string, quote: string, uri: string) => (urls.has(uri) ? `${start}${quote}${urls.get(uri)}${quote}` : match));
|
|
@@ -1283,8 +1341,9 @@ export async function queueMessageSend(messageUid: string, client?: ApiClient):
|
|
|
1283
1341
|
|
|
1284
1342
|
/**
|
|
1285
1343
|
* Fetches a message's raw RFC 5322 MIME source — for client-side E2E decrypt/signature-verification
|
|
1286
|
-
* only (`crypto/smimeMessage.ts`), never for display.
|
|
1287
|
-
* JSON response body)
|
|
1344
|
+
* only (`crypto/smimeMessage.ts`), never for display. Goes through `withClientBlob()` rather than `apiFetch()` (which
|
|
1345
|
+
* only ever decodes a JSON response body, and renews an expired session) - the same session renewal and, given a
|
|
1346
|
+
* `client`, the same account and token as every other call: this server-local route (`BaseMessageRawContentRoute.ts` in `server`, mounted
|
|
1288
1347
|
* alongside `@rapidmx/restapi`'s own `MessageRoute`) returns `message/rfc822`, not JSON — the one thing
|
|
1289
1348
|
* that library's own `GET /:id/content` deliberately never serves (see that route's own doc comment).
|
|
1290
1349
|
*
|
|
@@ -1294,17 +1353,15 @@ export async function queueMessageSend(messageUid: string, client?: ApiClient):
|
|
|
1294
1353
|
* straight to `evaluateMessageSecurity()` (see `crypto/mime.ts`'s doc comment on binary strings); it is
|
|
1295
1354
|
* not display text.
|
|
1296
1355
|
*/
|
|
1297
|
-
export async function getMessageRawContent(messageUid: string): Promise<string> {
|
|
1298
|
-
const
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
throw new ApiRequestError(message, res.status, body?.code);
|
|
1304
|
-
}
|
|
1356
|
+
export async function getMessageRawContent(messageUid: string, client?: ApiClient): Promise<string> {
|
|
1357
|
+
const raw = await withClientBlob(
|
|
1358
|
+
client,
|
|
1359
|
+
`/mail/messages/${encodeURIComponent(messageUid)}/raw`,
|
|
1360
|
+
"Could not load this message's raw content.",
|
|
1361
|
+
);
|
|
1305
1362
|
// Not `new TextDecoder("latin1")`: WHATWG maps that label to windows-1252, which remaps 0x80-0x9F and so
|
|
1306
1363
|
// isn't byte-preserving. `bytesToBinaryString()` is an exact byte -> code unit conversion.
|
|
1307
|
-
return bytesToBinaryString(new Uint8Array(await
|
|
1364
|
+
return bytesToBinaryString(new Uint8Array(await raw.arrayBuffer()));
|
|
1308
1365
|
}
|
|
1309
1366
|
|
|
1310
1367
|
export interface ImpersonationResult {
|
|
@@ -14,9 +14,14 @@ export function useMessageAttachments(message: Message | null): Attachment[] {
|
|
|
14
14
|
setAttachments([]);
|
|
15
15
|
return;
|
|
16
16
|
}
|
|
17
|
+
// A response for a message since replaced must not overwrite the one now shown.
|
|
18
|
+
let cancelled = false;
|
|
17
19
|
listAttachments(message.folderUid, message.uid)
|
|
18
|
-
.then(setAttachments)
|
|
19
|
-
.catch(() => setAttachments([]));
|
|
20
|
+
.then((listed) => !cancelled && setAttachments(listed))
|
|
21
|
+
.catch(() => !cancelled && setAttachments([]));
|
|
22
|
+
return () => {
|
|
23
|
+
cancelled = true;
|
|
24
|
+
};
|
|
20
25
|
// Only the fields the fetch actually depends on — not the whole `message` object, which gets a new
|
|
21
26
|
// reference on every metadata-only patch (e.g. `useMarkMessageRead`'s `onUpdated(updated)`, or a
|
|
22
27
|
// star/flag/label change wired to the same state) and would otherwise trigger a needless re-fetch.
|
|
@@ -53,5 +58,8 @@ export function useMarkMessageRead(message: Message | null, onUpdated: (updated:
|
|
|
53
58
|
return () => {
|
|
54
59
|
cancelled = true;
|
|
55
60
|
};
|
|
56
|
-
|
|
61
|
+
// Only the message's identity and read flag: a metadata-only patch swaps the object while the request is in flight, and
|
|
62
|
+
// restarting then would drop the first answer and send a second request with the old version (a 409 that leaves the
|
|
63
|
+
// message unread).
|
|
64
|
+
}, [message?.uid, message?.flags.read]);
|
|
57
65
|
}
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { ApiClient, withClient } from "../util/api.js";
|
|
15
|
-
import { ListParams, buildQuery } from "../util/apiQuery.js";
|
|
15
|
+
import { ListParams, SETTINGS_LIST_LIMIT, buildQuery } from "../util/apiQuery.js";
|
|
16
16
|
import { MessageImportance } from "./mailApi.js";
|
|
17
17
|
|
|
18
18
|
export type { ListParams };
|
|
@@ -62,7 +62,7 @@ export interface MailFilterRule {
|
|
|
62
62
|
* account of a multi-account app) to call instead of the default global `apiFetch()` - see `withClient()`'s
|
|
63
63
|
* own doc comment in `util/api.ts`. Omitted (the default), every function here behaves exactly as before. */
|
|
64
64
|
export function listMailFilterRules(mailboxUid: string, params: ListParams = {}, client?: ApiClient): Promise<MailFilterRule[]> {
|
|
65
|
-
return withClient(client, `/mail/mail-filter-rules?${buildQuery(params, { mailboxUid })}`);
|
|
65
|
+
return withClient(client, `/mail/mail-filter-rules?${buildQuery({ limit: SETTINGS_LIST_LIMIT, ...params }, { mailboxUid })}`);
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
export function getMailFilterRule(uid: string, client?: ApiClient): Promise<MailFilterRule> {
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { ApiClient, withClient } from "../util/api.js";
|
|
14
|
-
import { ListParams, buildQuery } from "../util/apiQuery.js";
|
|
14
|
+
import { ListParams, SETTINGS_LIST_LIMIT, buildQuery } from "../util/apiQuery.js";
|
|
15
15
|
|
|
16
16
|
export type { ListParams };
|
|
17
17
|
|
|
@@ -36,7 +36,7 @@ export interface MailSignature {
|
|
|
36
36
|
* account of a multi-account app) to call instead of the default global `apiFetch()` - see `withClient()`'s
|
|
37
37
|
* own doc comment in `util/api.ts`. Omitted (the default), every function here behaves exactly as before. */
|
|
38
38
|
export function listMailSignatures(mailboxUid: string, params: ListParams = {}, client?: ApiClient): Promise<MailSignature[]> {
|
|
39
|
-
return withClient(client, `/mail/mail-signatures?${buildQuery(params, { mailboxUid })}`);
|
|
39
|
+
return withClient(client, `/mail/mail-signatures?${buildQuery({ limit: SETTINGS_LIST_LIMIT, ...params }, { mailboxUid })}`);
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
export function getMailSignature(uid: string, client?: ApiClient): Promise<MailSignature> {
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* stages the upload and returns the `pending` request; poll `getImportRequest()`/`listImportRequests()`
|
|
10
10
|
* for `status` to become `"completed"` (with `importedCount`/`failedCount`) or `"failed"`.
|
|
11
11
|
*/
|
|
12
|
-
import { ApiClient,
|
|
12
|
+
import { ApiClient, withClient, withClientRaw } from "../util/api.js";
|
|
13
13
|
import { RequestListParams, buildRequestListQuery } from "../util/apiQuery.js";
|
|
14
14
|
|
|
15
15
|
export type { RequestListParams };
|
|
@@ -51,24 +51,18 @@ export interface UploadMailboxImportInput {
|
|
|
51
51
|
* with `format`/`targetFolderUid`/`mailboxUid` as query-string parameters, not a JSON body — the same
|
|
52
52
|
* shape `mailApi.ts`'s `uploadAttachment()` already establishes for a raw-bytes upload.
|
|
53
53
|
*/
|
|
54
|
-
export
|
|
54
|
+
export function uploadMailboxImport(file: File, input: UploadMailboxImportInput, client?: ApiClient): Promise<MailboxImportRequest> {
|
|
55
55
|
const params = new URLSearchParams({ format: input.format, targetFolderUid: input.targetFolderUid });
|
|
56
56
|
if (input.mailboxUid) {
|
|
57
57
|
params.set("mailboxUid", input.mailboxUid);
|
|
58
58
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const responseBody = contentType.includes("application/json") ? await res.json().catch(() => undefined) : undefined;
|
|
67
|
-
if (!res.ok) {
|
|
68
|
-
const message = (responseBody && (responseBody.message || responseBody.error)) || res.statusText || "Upload failed.";
|
|
69
|
-
throw new ApiRequestError(message, res.status, responseBody?.code);
|
|
70
|
-
}
|
|
71
|
-
return responseBody as MailboxImportRequest;
|
|
59
|
+
return withClientRaw(
|
|
60
|
+
client,
|
|
61
|
+
`/mail/mailbox-import-requests?${params.toString()}`,
|
|
62
|
+
"POST",
|
|
63
|
+
file,
|
|
64
|
+
input.format === "pst" ? "application/vnd.ms-outlook" : "application/mbox",
|
|
65
|
+
);
|
|
72
66
|
}
|
|
73
67
|
|
|
74
68
|
/** `client`, given by every function below that isn't a raw-bytes upload, is an explicit `ApiClient` from
|
|
@@ -27,6 +27,37 @@ export function stripRemoteCssUrls(css: string): string {
|
|
|
27
27
|
.replace(/url\(\s*(["']?)(.*?)\1\s*\)/gi, (match, _quote: string, uri: string) => (EMBEDDED_URI.test(uri) ? match : "none"));
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
/** The candidate URLs of a `srcset` value, parsed as the HTML spec does: a candidate's URL is a run of non-whitespace
|
|
31
|
+
* characters (it may itself contain commas - a `data:` URI always does), so a comma that ends a URL or follows its
|
|
32
|
+
* descriptors separates candidates whether or not whitespace follows it. */
|
|
33
|
+
function srcsetUrls(value: string): string[] {
|
|
34
|
+
const urls: string[] = [];
|
|
35
|
+
let position = 0;
|
|
36
|
+
while (position < value.length) {
|
|
37
|
+
const start = value.slice(position).search(/[^\s,]/);
|
|
38
|
+
if (start < 0) {
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
position += start;
|
|
42
|
+
const end = value.slice(position).search(/\s/);
|
|
43
|
+
const token = end < 0 ? value.slice(position) : value.slice(position, position + end);
|
|
44
|
+
position += token.length;
|
|
45
|
+
urls.push(token.replace(/,+$/, ""));
|
|
46
|
+
if (!token.endsWith(",")) {
|
|
47
|
+
// Skip the descriptors, up to the comma that ends the candidate.
|
|
48
|
+
const comma = value.indexOf(",", position);
|
|
49
|
+
const descriptors = value.slice(position, comma < 0 ? value.length : comma);
|
|
50
|
+
if (descriptors.includes("(")) {
|
|
51
|
+
// The spec's tokenizer ignores commas inside parentheses, so a browser may read the candidates that follow differently from
|
|
52
|
+
// the way this does. No valid descriptor has one: refuse the whole value.
|
|
53
|
+
urls.push(descriptors);
|
|
54
|
+
}
|
|
55
|
+
position = comma < 0 ? value.length : comma + 1;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return urls;
|
|
59
|
+
}
|
|
60
|
+
|
|
30
61
|
type Purifier = ReturnType<typeof DOMPurify>;
|
|
31
62
|
let bodyPurifier: Purifier | undefined;
|
|
32
63
|
|
|
@@ -58,7 +89,7 @@ function getBodyPurifier(): Purifier | undefined {
|
|
|
58
89
|
}
|
|
59
90
|
const isNavigationLink = name === "href" && ["a", "area"].includes(node.nodeName.toLowerCase());
|
|
60
91
|
if (RESOURCE_URI_ATTRIBUTES.has(name) || (name === "href" && !isNavigationLink)) {
|
|
61
|
-
const candidates = name === "srcset" ? data.attrValue
|
|
92
|
+
const candidates = name === "srcset" ? srcsetUrls(data.attrValue) : [data.attrValue];
|
|
62
93
|
if (!candidates.every((candidate) => EMBEDDED_URI.test(candidate))) {
|
|
63
94
|
data.keepAttr = false;
|
|
64
95
|
}
|
|
@@ -106,6 +137,21 @@ const QUOTE_FORBIDDEN_TAGS = [
|
|
|
106
137
|
// svg/math are already in DISPLAY_FORBIDDEN_TAGS, spread above — not repeated here.
|
|
107
138
|
];
|
|
108
139
|
|
|
140
|
+
let signaturePurifier: Purifier | undefined;
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Sanitizes a signature's HTML for the compose body: DOMPurify's defaults (no scripts, handlers or `javascript:` URLs) and
|
|
144
|
+
* the quote's tag list (nothing that styles, frames or submits). Unlike a quoted message, a signature is the sender's own
|
|
145
|
+
* and may carry pictures from the web, so remote images and links stay. Returns `""` when there is no DOM to sanitize with.
|
|
146
|
+
*/
|
|
147
|
+
export function sanitizeSignatureHtml(html: string): string {
|
|
148
|
+
if (typeof window === "undefined") {
|
|
149
|
+
return "";
|
|
150
|
+
}
|
|
151
|
+
signaturePurifier ??= DOMPurify(window);
|
|
152
|
+
return signaturePurifier.isSupported ? signaturePurifier.sanitize(html, { FORBID_TAGS: QUOTE_FORBIDDEN_TAGS }) : "";
|
|
153
|
+
}
|
|
154
|
+
|
|
109
155
|
/**
|
|
110
156
|
* Sanitizes message HTML for quoting into a compose body: the display policy (`sanitizeMessageBodyHtml()`), without
|
|
111
157
|
* the tags in `QUOTE_FORBIDDEN_TAGS`, and without any image that isn't embedded as a `data:` URI - a `cid:` image
|
|
@@ -64,15 +64,24 @@ const VALID_ENTITY_TYPES: ReadonlySet<string> = new Set<SearchEntityType>(["mess
|
|
|
64
64
|
* `parseSearchQuery()` itself, after every extraction has run. */
|
|
65
65
|
function extractFirst(input: string, key: string): { value?: string; rest: string } {
|
|
66
66
|
const pattern = new RegExp(`(^|\\s)${key}:(?:"([^"]*)"|(\\S+))`, "i");
|
|
67
|
-
const
|
|
68
|
-
if (!
|
|
67
|
+
const found = pattern.exec(maskQuoted(input));
|
|
68
|
+
if (!found) {
|
|
69
69
|
return { rest: input };
|
|
70
70
|
}
|
|
71
|
+
// The masked text only says where the operator is - what it holds is read from the real one.
|
|
72
|
+
const match = pattern.exec(input.slice(found.index))!;
|
|
71
73
|
const value = match[2] ?? match[3] ?? "";
|
|
72
|
-
const rest = input.slice(0,
|
|
74
|
+
const rest = input.slice(0, found.index) + match[1] + input.slice(found.index + match[0].length);
|
|
73
75
|
return { value, rest };
|
|
74
76
|
}
|
|
75
77
|
|
|
78
|
+
/** `input` with the inside of every quoted phrase blanked out (the length stays the same, so positions carry over to
|
|
79
|
+
* `input`): a word that looks like an operator inside a phrase - `"meeting from:bob"` - is part of the phrase, never an
|
|
80
|
+
* operator. */
|
|
81
|
+
function maskQuoted(input: string): string {
|
|
82
|
+
return input.replace(/"([^"]*)"/g, (_phrase, inner: string) => `"${"\0".repeat(inner.length)}"`);
|
|
83
|
+
}
|
|
84
|
+
|
|
76
85
|
/** Repeatedly applies `extractFirst()` for `key` until no more occurrences remain, splitting any
|
|
77
86
|
* comma-separated value into individual entries — so `is:read is:flagged` and `is:read,flagged` both
|
|
78
87
|
* produce `["read", "flagged"]`. */
|
|
@@ -98,7 +107,7 @@ function extractAll(input: string, key: string): { values: string[]; rest: strin
|
|
|
98
107
|
* spec defines for this operator — there is no `has:<other value>` form. */
|
|
99
108
|
function extractHasAttachment(input: string): { found: boolean; rest: string } {
|
|
100
109
|
const pattern = /(^|\s)has:attachment\b/i;
|
|
101
|
-
const match = pattern.exec(input);
|
|
110
|
+
const match = pattern.exec(maskQuoted(input));
|
|
102
111
|
if (!match) {
|
|
103
112
|
return { found: false, rest: input };
|
|
104
113
|
}
|
|
@@ -23,7 +23,7 @@ import { evaluateMessageSecurity, type MessageSecurityResult } from "../crypto/m
|
|
|
23
23
|
import { getMessage, getMessageRawContent } from "../mail/mailApi.js";
|
|
24
24
|
import type { ParsedSearchQuery } from "./queryGrammar.js";
|
|
25
25
|
import { SEARCH_FIELD_WEIGHTS } from "./searchScoring.js";
|
|
26
|
-
import { candidates, SearchResult } from "./searchApi.js";
|
|
26
|
+
import { candidates, type CandidateResultPage, SearchResult } from "./searchApi.js";
|
|
27
27
|
|
|
28
28
|
/** Longest decrypted HTML body `stripHtml()` examines, in UTF-16 code units - content past it is ignored
|
|
29
29
|
* for Tier 3 matching. A hostile sender controls this input entirely, so it is bounded on top of the
|
|
@@ -271,6 +271,11 @@ function hasAnyFilter(parsed: ParsedSearchQuery): boolean {
|
|
|
271
271
|
* concurrent WebCrypto work a large candidate page would otherwise fire all at once. */
|
|
272
272
|
export const TIER3_DECRYPT_CONCURRENCY = 4;
|
|
273
273
|
|
|
274
|
+
/** The most candidate pages one search walks (`nextCursor` onwards) while it is still short of `limit` results - a
|
|
275
|
+
* search that matches little must not download and decrypt a whole mailbox, but an encrypted message beyond the first
|
|
276
|
+
* page of candidates is still found. */
|
|
277
|
+
export const TIER3_MAX_CANDIDATE_PAGES = 4;
|
|
278
|
+
|
|
274
279
|
/** `Promise.allSettled()` over `items`, but never running more than `concurrency` `task`s at a time.
|
|
275
280
|
* Outcomes are returned in `items`' own order. */
|
|
276
281
|
async function settleWithConcurrency<T, R>(
|
|
@@ -301,7 +306,8 @@ export interface SearchEncryptedCandidatesOptions {
|
|
|
301
306
|
}
|
|
302
307
|
|
|
303
308
|
/**
|
|
304
|
-
* Runs Tier 3 for one parsed query: fetches a bounded candidate set from the server
|
|
309
|
+
* Runs Tier 3 for one parsed query: fetches a bounded candidate set from the server (following `nextCursor` for up to
|
|
310
|
+
* `TIER3_MAX_CANDIDATE_PAGES` pages while fewer than `limit` messages have matched), decrypts each
|
|
305
311
|
* candidate this device can open (at most `TIER3_DECRYPT_CONCURRENCY` at a time), and returns only the
|
|
306
312
|
* ones whose real (decrypted) content actually matches - each with a raw (not yet normalized) score the
|
|
307
313
|
* caller should run through `searchScoring.ts#normalizeServerScores()` alongside Tier 1's own scores
|
|
@@ -330,41 +336,68 @@ export async function searchEncryptedCandidates(
|
|
|
330
336
|
|
|
331
337
|
const participants = [parsed.from, parsed.to, parsed.cc].filter((value): value is string => Boolean(value));
|
|
332
338
|
|
|
333
|
-
const
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
339
|
+
const results: SearchResult[] = [];
|
|
340
|
+
let cursor: string | undefined;
|
|
341
|
+
for (let pageNumber = 0; pageNumber < TIER3_MAX_CANDIDATE_PAGES; pageNumber++) {
|
|
342
|
+
let page: CandidateResultPage;
|
|
343
|
+
try {
|
|
344
|
+
page = await candidates({
|
|
345
|
+
types: ["message"],
|
|
346
|
+
participants: participants.length > 0 ? participants : undefined,
|
|
347
|
+
before: parsed.before,
|
|
348
|
+
after: parsed.after,
|
|
349
|
+
folderUid: parsed.folderUid,
|
|
350
|
+
flags: parsed.flags,
|
|
351
|
+
labels: parsed.labels,
|
|
352
|
+
limit,
|
|
353
|
+
cursor,
|
|
354
|
+
mailboxUid: options.mailboxUid,
|
|
355
|
+
});
|
|
356
|
+
} catch (err) {
|
|
357
|
+
if (pageNumber === 0) {
|
|
358
|
+
throw err;
|
|
359
|
+
}
|
|
360
|
+
// A later page failing keeps what the earlier ones found.
|
|
361
|
+
break;
|
|
362
|
+
}
|
|
344
363
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
364
|
+
const settled = await settleWithConcurrency(
|
|
365
|
+
page.candidates.filter((candidate) => candidate.entityType === "message"),
|
|
366
|
+
TIER3_DECRYPT_CONCURRENCY,
|
|
367
|
+
async (candidate) => {
|
|
368
|
+
if (parsed.hasAttachment !== undefined) {
|
|
369
|
+
const message = await getMessage(candidate.entityUid);
|
|
370
|
+
if (Boolean(message.hasAttachments) !== parsed.hasAttachment) {
|
|
371
|
+
return undefined;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
const rawMime = await getMessageRawContent(candidate.entityUid);
|
|
375
|
+
if (unlocked.destroyed) {
|
|
352
376
|
return undefined;
|
|
353
377
|
}
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
}
|
|
359
|
-
const security: MessageSecurityResult = await evaluateMessageSecurity(rawMime, unlocked);
|
|
360
|
-
return { entityUid: candidate.entityUid, security };
|
|
361
|
-
},
|
|
362
|
-
);
|
|
378
|
+
const security: MessageSecurityResult = await evaluateMessageSecurity(rawMime, unlocked);
|
|
379
|
+
return { entityUid: candidate.entityUid, security };
|
|
380
|
+
},
|
|
381
|
+
);
|
|
363
382
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
383
|
+
if (unlocked.destroyed) {
|
|
384
|
+
// Locked while candidates were being fetched/decrypted - don't surface decrypted content after a lock.
|
|
385
|
+
return [];
|
|
386
|
+
}
|
|
387
|
+
results.push(...matchDecrypted(parsed, settled));
|
|
388
|
+
cursor = page.nextCursor;
|
|
389
|
+
if (!cursor || results.length >= limit) {
|
|
390
|
+
break;
|
|
391
|
+
}
|
|
367
392
|
}
|
|
393
|
+
return results;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/** The decrypted candidates of one page whose real content matches `parsed`, scored and with their snippets. */
|
|
397
|
+
function matchDecrypted(
|
|
398
|
+
parsed: ParsedSearchQuery,
|
|
399
|
+
settled: PromiseSettledResult<{ entityUid: string; security: MessageSecurityResult } | undefined>[],
|
|
400
|
+
): SearchResult[] {
|
|
368
401
|
const subjectFilter = parsed.subject?.toLowerCase();
|
|
369
402
|
const results: SearchResult[] = [];
|
|
370
403
|
for (const outcome of settled) {
|
package/lib/tasks/tasksApi.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* comment for the shared ACL/authorization model every wrapper file here follows. */
|
|
7
7
|
|
|
8
8
|
import { ApiClient, withClient } from "../util/api.js";
|
|
9
|
-
import { ListParams, buildQuery } from "../util/apiQuery.js";
|
|
9
|
+
import { ListParams, SETTINGS_LIST_LIMIT, buildQuery } from "../util/apiQuery.js";
|
|
10
10
|
|
|
11
11
|
export type TaskPriority = "low" | "normal" | "high";
|
|
12
12
|
|
|
@@ -39,7 +39,7 @@ export interface Task {
|
|
|
39
39
|
|
|
40
40
|
/** Lists a folder's tasks, soonest due date first (tasks with no due date sort last). */
|
|
41
41
|
export function listTasks(folderUid: string, params: ListParams = {}, client?: ApiClient): Promise<Task[]> {
|
|
42
|
-
return withClient(client, `/mail/tasks?${buildQuery(params, { folderUid, sort: JSON.stringify({ dueDate: "ASC" }) })}`);
|
|
42
|
+
return withClient(client, `/mail/tasks?${buildQuery(params, { folderUid, sort: JSON.stringify({ dueDate: "ASC", uid: "ASC" }) })}`);
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
export interface CreateTaskInput {
|
|
@@ -109,7 +109,7 @@ export interface TaskList {
|
|
|
109
109
|
|
|
110
110
|
/** Lists a mailbox's task lists (Outlook To-Do-style custom lists), alphabetically by name. */
|
|
111
111
|
export function listTaskLists(mailboxUid: string, params: ListParams = {}, client?: ApiClient): Promise<TaskList[]> {
|
|
112
|
-
return withClient(client, `/mail/task-lists?${buildQuery(params, { mailboxUid, sort: JSON.stringify({ name: "ASC" }) })}`);
|
|
112
|
+
return withClient(client, `/mail/task-lists?${buildQuery({ limit: SETTINGS_LIST_LIMIT, ...params }, { mailboxUid, sort: JSON.stringify({ name: "ASC", uid: "ASC" }) })}`);
|
|
113
113
|
}
|
|
114
114
|
|
|
115
115
|
export function createTaskList(input: { mailboxUid: string; name: string }, client?: ApiClient): Promise<TaskList> {
|