@rapidmx/web-client 0.29.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 +58 -8
- 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/SwipeRow.tsx +26 -4
- 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/gestures/useLongPress.ts +138 -0
- 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 +139 -43
- 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.d.ts +8 -3
- package/dist/apps/shared/components/mail/ConversationList.js +51 -8
- 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/SwipeRow.d.ts +5 -1
- package/dist/apps/shared/components/mail/SwipeRow.js +17 -3
- 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/gestures/useLongPress.d.ts +41 -0
- package/dist/apps/shared/gestures/useLongPress.js +112 -0
- 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 +124 -40
- 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
|
@@ -123,8 +123,11 @@ export interface MessageSecurityResult {
|
|
|
123
123
|
* recovered and it carried a protected Subject. */
|
|
124
124
|
subject?: string;
|
|
125
125
|
/** Every protected header recovered from inside the signed/decrypted entity - present only when that entity
|
|
126
|
-
* carries RFC 9788 protected headers (a protected `From`)
|
|
127
|
-
*
|
|
126
|
+
* carries RFC 9788 protected headers (a protected `From`) AND a signature over them verified (so also for a
|
|
127
|
+
* `"verified_at_first_open"` result). Absent for a legacy S/MIME sender's signed-only message (its outer
|
|
128
|
+
* Subject/To/Cc were then never signed, and a UI should not present them as verified), and for an encrypted
|
|
129
|
+
* message without a verified signature: anyone holding the reader's public certificate can encrypt headers of their
|
|
130
|
+
* own choosing, so they are never exposed. */
|
|
128
131
|
protectedHeaders?: MessageProtectedHeaders;
|
|
129
132
|
/** The attachments inside the verified/decrypted entity - for a signed-only message the only attachments the
|
|
130
133
|
* signature covers, so a UI showing a verified badge should list these rather than the server's attachment
|
|
@@ -189,7 +192,7 @@ function isEncryptedContentType(contentType: { value: string; params: Record<str
|
|
|
189
192
|
|
|
190
193
|
const NO_KEY_ERROR = "This device doesn't have the key needed to decrypt this message.";
|
|
191
194
|
const UNSUPPORTED_ENCRYPTION_ERROR =
|
|
192
|
-
"This message uses an older, unauthenticated encryption algorithm that RapidMX doesn't accept, so it can't be opened safely.";
|
|
195
|
+
"This message uses an older, unauthenticated encryption algorithm, or an encryption format, that RapidMX doesn't accept, so it can't be opened safely.";
|
|
193
196
|
|
|
194
197
|
function renderDisplayBody(display: DisplayBody | undefined): Pick<MessageSecurityResult, "html" | "text"> {
|
|
195
198
|
if (display?.html !== undefined) {
|
|
@@ -219,7 +222,7 @@ export interface SignerBindingInput {
|
|
|
219
222
|
/** The certificate matched to the verified SignerInfo; `undefined` fails closed. */
|
|
220
223
|
signerCertificateDer: Uint8Array | undefined;
|
|
221
224
|
/** Headers recovered from inside the signed content (RFC 9788), if any. */
|
|
222
|
-
protectedHeaders: (Pick<ProtectedHeaders, "from" | "to"> & Partial<Pick<ProtectedHeaders, "cc" | "subject">>) | undefined;
|
|
225
|
+
protectedHeaders: (Pick<ProtectedHeaders, "from" | "to"> & Partial<Pick<ProtectedHeaders, "cc" | "subject" | "date">>) | undefined;
|
|
223
226
|
/** The received message's outer header map (lowercased names), as `parseMimeEntity()` returns it. */
|
|
224
227
|
outerHeaders: Record<string, string>;
|
|
225
228
|
/** Every outer header field in order (`parseMimeEntity()`'s `fields`) - checked for repeated
|
|
@@ -229,7 +232,7 @@ export interface SignerBindingInput {
|
|
|
229
232
|
protectedFields?: MimeHeaderField[];
|
|
230
233
|
/** One trusted fingerprint or several (any match is accepted). An empty array counts as none supplied. */
|
|
231
234
|
pinnedSignerFingerprint?: string | string[];
|
|
232
|
-
/** Also require the protected `Subject` (when protected headers are present) to equal the outer
|
|
235
|
+
/** Also require the protected `Subject` and `Date` (when protected headers are present) to equal the outer ones - right for
|
|
233
236
|
* a signed-only (`hp="clear"`) message, wrong for an encrypted one whose outer Subject is obscured. */
|
|
234
237
|
compareSubject?: boolean;
|
|
235
238
|
}
|
|
@@ -246,6 +249,15 @@ function hasRepeatedAddressField(fields: MimeHeaderField[] | undefined): boolean
|
|
|
246
249
|
return SINGLETON_ADDRESS_FIELDS.some((name) => (counts.get(name) ?? 0) > 1);
|
|
247
250
|
}
|
|
248
251
|
|
|
252
|
+
/** Whether the unsigned (outer) `Date:` header value `outer` names the same instant as the signed one, `signed`. A signed date
|
|
253
|
+
* that is missing or unreadable leaves nothing to compare, but when there is one the outer must match it: a replayed message
|
|
254
|
+
* that dropped or garbled its outer Date is as changed as one that carries a new one. */
|
|
255
|
+
function sameInstant(signed: string | undefined, outer: string | undefined): boolean {
|
|
256
|
+
// `String(undefined)` is not a date either.
|
|
257
|
+
const left = Date.parse(String(signed));
|
|
258
|
+
return Number.isNaN(left) || left === Date.parse(String(outer));
|
|
259
|
+
}
|
|
260
|
+
|
|
249
261
|
function normalizePins(pins: string | string[] | undefined): string[] {
|
|
250
262
|
return (Array.isArray(pins) ? pins : pins === undefined ? [] : [pins]).map((pin) => pin.toLowerCase());
|
|
251
263
|
}
|
|
@@ -292,7 +304,13 @@ function checkIdentityAndHeaders(input: SignerBindingInput, certDer: Uint8Array)
|
|
|
292
304
|
if (!sameAddressSet(extractAddresses(protectedHeaders?.cc), extractAddresses(outerHeaders["cc"]))) {
|
|
293
305
|
return "header_mismatch";
|
|
294
306
|
}
|
|
295
|
-
|
|
307
|
+
// A signed-only message's outer Date is unsigned too: a signed message replayed later carries a new one. (An encrypted
|
|
308
|
+
// message's outer Date is checked against its HP-Outer copy instead - see `headerTamperDetected`.)
|
|
309
|
+
if (
|
|
310
|
+
input.compareSubject &&
|
|
311
|
+
(normalizeSubject(protectedHeaders?.subject) !== normalizeSubject(outerHeaders["subject"]) ||
|
|
312
|
+
!sameInstant(protectedHeaders?.date, outerHeaders["date"]))
|
|
313
|
+
) {
|
|
296
314
|
return "header_mismatch";
|
|
297
315
|
}
|
|
298
316
|
}
|
|
@@ -448,14 +466,16 @@ async function evaluateLive(
|
|
|
448
466
|
if (!parsed.decrypted) {
|
|
449
467
|
return { result: { state: "encrypted", decryptError: parsed.unsupportedContentEncryption ? UNSUPPORTED_ENCRYPTION_ERROR : NO_KEY_ERROR }, kind: "encrypted" };
|
|
450
468
|
}
|
|
469
|
+
// Anyone holding the reader's public certificate can encrypt a message with any protected headers they like, so they
|
|
470
|
+
// are only exposed once a signature over them verified - and a key-changed message keeps them aside for a seal.
|
|
451
471
|
const content = {
|
|
452
472
|
...renderDisplayBody(parsed.displayBody),
|
|
453
473
|
headerTamperDetected: parsed.headerTamperDetected,
|
|
454
474
|
subject: parsed.protectedHeaders?.subject || undefined,
|
|
455
|
-
...exposedHeaders(parsed.protectedHeaders),
|
|
456
475
|
attachments: parsed.attachments,
|
|
457
476
|
...addressing(parsed.protectedHeaders),
|
|
458
477
|
};
|
|
478
|
+
const signedHeaders = exposedHeaders(parsed.protectedHeaders);
|
|
459
479
|
if (parsed.signatureVerified === undefined) {
|
|
460
480
|
return { result: { state: "encrypted", ...content }, kind: "encrypted" };
|
|
461
481
|
}
|
|
@@ -464,9 +484,13 @@ async function evaluateLive(
|
|
|
464
484
|
}
|
|
465
485
|
const { failure, trusted, signer, acceptedSigner } = await checkSigner(parsed.signerCertificateDer, parsed.protectedHeaders, parsed.protectedHeaderFields, false);
|
|
466
486
|
if (failure) {
|
|
467
|
-
return {
|
|
487
|
+
return {
|
|
488
|
+
result: { state: "signature_failed", signatureFailureReason: failure, ...content, ...(failure === "signer_key_changed" ? acceptedSigner : signer) },
|
|
489
|
+
kind: "encrypted",
|
|
490
|
+
...(failure === "signer_key_changed" ? { signedContent: signedHeaders } : {}),
|
|
491
|
+
};
|
|
468
492
|
}
|
|
469
|
-
return { result: { state: trusted ? "encrypted_verified" : "encrypted_unverified_signer", ...content, ...acceptedSigner }, kind: "encrypted" };
|
|
493
|
+
return { result: { state: trusted ? "encrypted_verified" : "encrypted_unverified_signer", ...content, ...signedHeaders, ...acceptedSigner }, kind: "encrypted" };
|
|
470
494
|
}
|
|
471
495
|
|
|
472
496
|
return { result: { state: "unprotected" }, kind: "other" };
|
|
@@ -33,6 +33,11 @@ export const DEFAULT_ARGON2ID_PARAMS: Argon2idParams = { memorySize: 65536, iter
|
|
|
33
33
|
|
|
34
34
|
const ARGON2ID_HASH_LENGTH_BYTES = 32;
|
|
35
35
|
|
|
36
|
+
/** The most a `MasterKeyWrap.kdf` label read back from the server may ask this device to spend: 1 GiB (in KiB),
|
|
37
|
+
* 10 passes and 16 lanes - well above every default this system has used, but far from what a hostile or buggy
|
|
38
|
+
* server could name (`m=4294967295`) to freeze the tab or exhaust WebAssembly memory. */
|
|
39
|
+
export const MAX_ARGON2ID_PARAMS: Argon2idParams = { memorySize: 1048576, iterations: 10, parallelism: 16 };
|
|
40
|
+
|
|
36
41
|
/** Builds the `kdf` string stored alongside a password-method `MasterKeyWrap`, so parameters can be
|
|
37
42
|
* upgraded over time without breaking existing accounts (each wrap records the parameters it was
|
|
38
43
|
* actually created with). */
|
|
@@ -44,7 +49,8 @@ export function argon2idKdfLabel(params: Argon2idParams): string {
|
|
|
44
49
|
* created with, so unlocking always re-derives with the *original* parameters even if
|
|
45
50
|
* `DEFAULT_ARGON2ID_PARAMS` has since changed (the spec's own "KDF parameters ... MUST be stored
|
|
46
51
|
* alongside each wrap, so costs can be raised over time without breaking existing accounts"). Returns
|
|
47
|
-
* `undefined` for anything that isn't a well-formed `argon2id:m=...,t=...,p=...` label
|
|
52
|
+
* `undefined` for anything that isn't a well-formed `argon2id:m=...,t=...,p=...` label, or whose parameters are
|
|
53
|
+
* zero or beyond `MAX_ARGON2ID_PARAMS` — including a
|
|
48
54
|
* different KDF entirely (e.g. recovery codes' own `"hkdf-sha256"` label), which callers must not
|
|
49
55
|
* attempt to unlock as if it were password-derived.
|
|
50
56
|
*/
|
|
@@ -53,7 +59,13 @@ export function parseArgon2idKdfLabel(label: string): Argon2idParams | undefined
|
|
|
53
59
|
if (!match) {
|
|
54
60
|
return undefined;
|
|
55
61
|
}
|
|
56
|
-
|
|
62
|
+
const params = { memorySize: Number(match[1]), iterations: Number(match[2]), parallelism: Number(match[3]) };
|
|
63
|
+
const inRange = (value: number, max: number) => value >= 1 && value <= max;
|
|
64
|
+
return inRange(params.memorySize, MAX_ARGON2ID_PARAMS.memorySize) &&
|
|
65
|
+
inRange(params.iterations, MAX_ARGON2ID_PARAMS.iterations) &&
|
|
66
|
+
inRange(params.parallelism, MAX_ARGON2ID_PARAMS.parallelism)
|
|
67
|
+
? params
|
|
68
|
+
: undefined;
|
|
57
69
|
}
|
|
58
70
|
|
|
59
71
|
/** A fresh random salt for a new password enrollment. 16 bytes is Argon2id's own recommended minimum. */
|
package/lib/crypto/smime.ts
CHANGED
|
@@ -41,6 +41,10 @@ const ID_DATA = pkijs.ContentInfo.DATA;
|
|
|
41
41
|
|
|
42
42
|
/** AES-GCM content-encryption OIDs (id-aes128-GCM, id-aes192-GCM, id-aes256-GCM - RFC 5084). The only
|
|
43
43
|
* content encryption `decryptEnvelopedData()` accepts: see `UnsupportedContentEncryptionError`. */
|
|
44
|
+
/** The CMS content type of RFC 5083 `AuthEnvelopedData` (`smime-type="authenveloped-data"`): authenticated, but not the
|
|
45
|
+
* structure `decryptEnvelopedDataWithKeys()` reads - it is reported as unsupported rather than as a missing key. */
|
|
46
|
+
const AUTH_ENVELOPED_DATA_OID = "1.2.840.113549.1.9.16.1.23";
|
|
47
|
+
|
|
44
48
|
const AEAD_CONTENT_ENCRYPTION_OIDS = new Set(["2.16.840.1.101.3.4.1.6", "2.16.840.1.101.3.4.1.26", "2.16.840.1.101.3.4.1.46"]);
|
|
45
49
|
|
|
46
50
|
/**
|
|
@@ -56,7 +60,7 @@ export class UnsupportedContentEncryptionError extends Error {
|
|
|
56
60
|
public readonly algorithmOid: string;
|
|
57
61
|
|
|
58
62
|
constructor(algorithmOid: string) {
|
|
59
|
-
super(`This message uses
|
|
63
|
+
super(`This message uses content encryption (${algorithmOid}) that is not accepted: only AES-GCM EnvelopedData is.`);
|
|
60
64
|
this.name = "UnsupportedContentEncryptionError";
|
|
61
65
|
this.algorithmOid = algorithmOid;
|
|
62
66
|
}
|
|
@@ -178,10 +182,12 @@ export function extractCertificateEmails(certDer: Uint8Array): string[] {
|
|
|
178
182
|
return [];
|
|
179
183
|
}
|
|
180
184
|
const sanEmails: string[] = [];
|
|
185
|
+
let hasSan = false;
|
|
181
186
|
for (const extension of cert.extensions ?? []) {
|
|
182
187
|
if (extension.extnID !== "2.5.29.17") {
|
|
183
188
|
continue;
|
|
184
189
|
}
|
|
190
|
+
hasSan = true;
|
|
185
191
|
// pkijs parses a well-formed SubjectAlternativeName into an AltName automatically, and leaves
|
|
186
192
|
// `parsedValue` unset for a malformed one - a certificate asserting an unreadable SAN names nobody.
|
|
187
193
|
const altNames = (extension.parsedValue as pkijs.AltName | undefined)?.altNames;
|
|
@@ -195,7 +201,9 @@ export function extractCertificateEmails(certDer: Uint8Array): string[] {
|
|
|
195
201
|
}
|
|
196
202
|
}
|
|
197
203
|
}
|
|
198
|
-
|
|
204
|
+
// The SAN is authoritative whenever the certificate has one: DNS names alone name no mailbox, so a subject email
|
|
205
|
+
// address next to them must not stand in for it.
|
|
206
|
+
if (hasSan) {
|
|
199
207
|
return sanEmails;
|
|
200
208
|
}
|
|
201
209
|
const subjectEmails: string[] = [];
|
|
@@ -376,6 +384,9 @@ function recipientMatchesCertificate(recipientInfo: pkijs.RecipientInfo, certifi
|
|
|
376
384
|
*/
|
|
377
385
|
export async function decryptEnvelopedDataWithKeys(envelopedDer: Uint8Array, keys: DecryptionKey[]): Promise<Uint8Array> {
|
|
378
386
|
const contentInfo = pkijs.ContentInfo.fromBER(toArrayBuffer(envelopedDer));
|
|
387
|
+
if (contentInfo.contentType === AUTH_ENVELOPED_DATA_OID) {
|
|
388
|
+
throw new UnsupportedContentEncryptionError(AUTH_ENVELOPED_DATA_OID);
|
|
389
|
+
}
|
|
379
390
|
if (contentInfo.contentType !== pkijs.ContentInfo.ENVELOPED_DATA) {
|
|
380
391
|
throw new Error("This CMS content is not EnvelopedData.");
|
|
381
392
|
}
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import { Message, Recipient } from "../mailApi.js";
|
|
18
|
-
import { sanitizeQuotedHtml } from "../messageBodySanitizer.js";
|
|
18
|
+
import { sanitizeQuotedHtml, sanitizeSignatureHtml } from "../messageBodySanitizer.js";
|
|
19
19
|
|
|
20
20
|
function escapeHtml(text: string): string {
|
|
21
21
|
return text
|
|
@@ -154,7 +154,8 @@ export function buildComposeBodyHtml(signatureHtml?: string, quotedHtml?: string
|
|
|
154
154
|
}
|
|
155
155
|
const separator = signatureHtml && quotedHtml ? "<p></p>" : "";
|
|
156
156
|
const aboveQuote = quotedHtml ? "<p></p>" : "";
|
|
157
|
-
|
|
157
|
+
// A signature can be written by someone else (a shared mailbox's delegate): it is sanitized like anything else the editor is given.
|
|
158
|
+
return `<p></p>${signatureHtml ? sanitizeSignatureHtml(signatureHtml) : ""}${separator}${aboveQuote}${quotedHtml ?? ""}`;
|
|
158
159
|
}
|
|
159
160
|
|
|
160
161
|
/**
|
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
export const MAX_QUOTED_IMAGE_BYTES = 2_000_000;
|
|
14
14
|
/** The most picture data a quote carries, in all (as `data:` URI characters). */
|
|
15
15
|
export const MAX_QUOTED_IMAGES_CHARS = 6_000_000;
|
|
16
|
+
/** How many pictures are fetched at once. */
|
|
17
|
+
const QUOTED_IMAGE_CONCURRENCY = 4;
|
|
16
18
|
|
|
17
19
|
export interface QuotedImageReference {
|
|
18
20
|
/** The token of a `cid:` reference, absent for an image with no source. */
|
|
@@ -22,24 +24,52 @@ export interface QuotedImageReference {
|
|
|
22
24
|
|
|
23
25
|
/**
|
|
24
26
|
* `html` with each of its embedded images (`cid:`, or no source at all) replaced by the `data:` URI `resolve` gives for it. An image
|
|
25
|
-
* `resolve` has no picture for is left as it is (the quote's sanitizer then drops it); images that already are `data:` URIs and
|
|
26
|
-
* ones are not asked about. `html` is returned as given when it has no such image, or where there is no DOM to parse it with.
|
|
27
|
+
* `resolve` has no picture for (or fails on) is left as it is (the quote's sanitizer then drops it); images that already are `data:` URIs and
|
|
28
|
+
* remote ones are not asked about. `html` is returned as given when it has no such image, or where there is no DOM to parse it with.
|
|
29
|
+
*
|
|
30
|
+
* The images are resolved a few at a time. When `signal` aborts (the caller's deadline) the wait ends there: the pictures that have arrived
|
|
31
|
+
* are embedded, the rest left as they were, and `resolve` - which is given `signal` to cancel its own request with - is asked for no more.
|
|
27
32
|
*/
|
|
28
|
-
export async function embedQuotedImages(
|
|
33
|
+
export async function embedQuotedImages(
|
|
34
|
+
html: string,
|
|
35
|
+
resolve: (reference: QuotedImageReference, signal?: AbortSignal) => Promise<string | undefined>,
|
|
36
|
+
signal?: AbortSignal,
|
|
37
|
+
): Promise<string> {
|
|
29
38
|
if (typeof DOMParser === "undefined" || !/<img\b/i.test(html)) {
|
|
30
39
|
return html;
|
|
31
40
|
}
|
|
32
41
|
const doc = new DOMParser().parseFromString(html, "text/html");
|
|
33
|
-
|
|
34
|
-
let changed = false;
|
|
42
|
+
const targets: { image: Element; reference: QuotedImageReference; uri?: string }[] = [];
|
|
35
43
|
for (const image of Array.from(doc.querySelectorAll("img"))) {
|
|
36
44
|
const src = (image.getAttribute("src") ?? "").trim();
|
|
37
45
|
const cid = /^cid:/i.test(src);
|
|
38
|
-
if (
|
|
39
|
-
|
|
46
|
+
if (cid || src === "") {
|
|
47
|
+
targets.push({ image, reference: { ...(cid ? { cid: src.slice(4) } : {}), alt: image.getAttribute("alt") ?? undefined } });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
let next = 0;
|
|
51
|
+
const worker = async () => {
|
|
52
|
+
while (next < targets.length && !signal?.aborted) {
|
|
53
|
+
const target = targets[next++];
|
|
54
|
+
target.uri = await resolve(target.reference, signal).catch(() => undefined);
|
|
40
55
|
}
|
|
41
|
-
|
|
42
|
-
|
|
56
|
+
};
|
|
57
|
+
let onAbort!: () => void;
|
|
58
|
+
const deadline = new Promise<void>((done) => {
|
|
59
|
+
onAbort = done;
|
|
60
|
+
if (signal?.aborted) {
|
|
61
|
+
done();
|
|
62
|
+
}
|
|
63
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
64
|
+
});
|
|
65
|
+
await Promise.race([Promise.all(Array.from({ length: Math.min(QUOTED_IMAGE_CONCURRENCY, targets.length) }, worker)), deadline]);
|
|
66
|
+
signal?.removeEventListener("abort", onAbort);
|
|
67
|
+
// Whatever has arrived by now is embedded, in document order, for as long as the budget lasts.
|
|
68
|
+
let budget = MAX_QUOTED_IMAGES_CHARS;
|
|
69
|
+
let changed = false;
|
|
70
|
+
for (const { image, uri } of targets) {
|
|
71
|
+
// Only a picture of the original's own is ever put in: whatever a resolver answers, a `src` that is not a `data:` image never gets in.
|
|
72
|
+
if (uri !== undefined && /^data:image\//i.test(uri) && uri.length <= budget) {
|
|
43
73
|
budget -= uri.length;
|
|
44
74
|
image.setAttribute("src", uri);
|
|
45
75
|
changed = true;
|
package/lib/mail/labelsApi.ts
CHANGED
|
@@ -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
|
|
|
@@ -31,7 +31,7 @@ export interface Label {
|
|
|
31
31
|
* account of a multi-account app) to call instead of the default global `apiFetch()` - see `withClient()`'s
|
|
32
32
|
* own doc comment in `util/api.ts`. Omitted (the default), every function here behaves exactly as before. */
|
|
33
33
|
export function listLabels(mailboxUid: string, params: ListParams = {}, client?: ApiClient): Promise<Label[]> {
|
|
34
|
-
return withClient(client, `/mail/labels?${buildQuery(params, { mailboxUid })}`);
|
|
34
|
+
return withClient(client, `/mail/labels?${buildQuery({ limit: SETTINGS_LIST_LIMIT, ...params }, { mailboxUid })}`);
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
export function getLabel(uid: string, client?: ApiClient): Promise<Label> {
|
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> {
|