@rapidmx/web-client 0.30.0 → 0.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/apps/admin/audit-log/index.tsx +3 -3
- package/apps/admin/branding/index.tsx +3 -3
- package/apps/admin/data-requests/index.tsx +18 -7
- package/apps/admin/distribution-lists/[uid].tsx +3 -3
- package/apps/admin/distribution-lists/index.tsx +3 -3
- package/apps/admin/distribution-lists/new/index.tsx +3 -3
- package/apps/admin/domains/[uid].tsx +6 -5
- package/apps/admin/domains/index.tsx +3 -3
- package/apps/admin/domains/new/index.tsx +3 -3
- package/apps/admin/escrow-scopes/[uid].tsx +11 -5
- package/apps/admin/escrow-scopes/index.tsx +3 -3
- package/apps/admin/escrow-scopes/new/index.tsx +9 -3
- package/apps/admin/index.tsx +4 -4
- package/apps/admin/ingest-queue/index.tsx +3 -3
- package/apps/admin/mailboxes/[uid].tsx +15 -7
- package/apps/admin/quarantine/index.tsx +4 -4
- package/apps/admin/retention-policy/index.tsx +3 -3
- package/apps/admin/signing-certificates/index.tsx +3 -3
- package/apps/admin/transport-rules/[uid].tsx +4 -4
- package/apps/admin/transport-rules/index.tsx +3 -3
- package/apps/admin/transport-rules/new/index.tsx +3 -3
- package/apps/escrow/matters/[uid].tsx +26 -2
- package/apps/escrow/matters/new/index.tsx +4 -0
- package/apps/shared/calendar/calendarLiveUpdates.ts +11 -10
- package/apps/shared/calendar/useCalendarReminders.ts +2 -0
- package/apps/shared/components/admin/ActionAlert.tsx +32 -0
- package/apps/shared/components/admin/diagnostics/InformationPanel.tsx +1 -1
- package/apps/shared/components/admin/diagnostics/InstalledPlugins.tsx +1 -1
- package/apps/shared/components/admin/diagnostics/LogsPanel.tsx +1 -1
- package/apps/shared/components/admin/diagnostics/RuntimePanel.tsx +1 -1
- package/apps/shared/components/admin/diagnostics/SystemPanel.tsx +1 -1
- package/apps/shared/components/admin/distributionLists/MemberListCard.tsx +4 -4
- package/apps/shared/components/admin/elevation.ts +19 -0
- package/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.tsx +58 -3
- package/apps/shared/components/admin/layout/AdminShell.tsx +6 -1
- package/apps/shared/components/admin/mailboxes/EraseLeftoverDataDialog.tsx +3 -3
- package/apps/shared/components/admin/mailboxes/EscrowScopeCard.tsx +4 -4
- package/apps/shared/components/admin/mailboxes/LeftoverMailboxesSection.tsx +4 -4
- package/apps/shared/components/admin/mailboxes/ResourceSettingsCard.tsx +3 -3
- package/apps/shared/components/admin/mailboxes/ShareAccessCard.tsx +6 -6
- package/apps/shared/components/admin/settings/BrandingForm.tsx +22 -9
- package/apps/shared/components/admin/settings/DomainDnsSetup.tsx +4 -4
- package/apps/shared/components/admin/settings/EncryptionPolicyForm.tsx +33 -4
- package/apps/shared/components/admin/settings/LoadedSettingsForm.tsx +3 -3
- package/apps/shared/components/admin/settings/MailboxCreateForm.tsx +8 -3
- package/apps/shared/components/admin/settings/MailboxPolicyForm.tsx +3 -3
- package/apps/shared/components/admin/settings/PluginsManager.tsx +39 -17
- package/apps/shared/components/admin/settings/RetentionPolicyForm.tsx +25 -12
- package/apps/shared/components/admin/setup/EscrowSetupStep.tsx +40 -7
- package/apps/shared/components/admin/setup/SetupWizard.tsx +8 -6
- package/apps/shared/components/admin/signOut.ts +3 -0
- package/apps/shared/components/admin/usePagedList.tsx +2 -2
- package/apps/shared/components/calendar/DescriptionEditor.tsx +2 -1
- package/apps/shared/components/calendar/EventDetails.tsx +34 -8
- package/apps/shared/components/calendar/EventEditor.tsx +90 -40
- package/apps/shared/components/calendar/EventFormParts.tsx +5 -2
- package/apps/shared/components/calendar/EventMatchNav.tsx +14 -2
- package/apps/shared/components/calendar/EventModal.tsx +25 -1
- package/apps/shared/components/calendar/FindATime.tsx +10 -2
- package/apps/shared/components/calendar/ListView.tsx +99 -25
- package/apps/shared/components/calendar/RecurrenceEditor.tsx +19 -8
- package/apps/shared/components/calendar/SplitDayView.tsx +14 -18
- package/apps/shared/components/calendar/TaskQuickForm.tsx +3 -2
- package/apps/shared/components/calendar/TimeGridView.tsx +35 -19
- package/apps/shared/components/calendar/gridLayout.ts +94 -0
- package/apps/shared/components/calendar/layout/CalendarShell.tsx +1 -1
- package/apps/shared/components/calendar/timePicker.ts +5 -2
- package/apps/shared/components/contacts/ContactCard.tsx +15 -4
- package/apps/shared/components/contacts/ContactDetailPane.tsx +54 -15
- package/apps/shared/components/contacts/ContactForm.tsx +61 -17
- package/apps/shared/components/contacts/ContactPhotoEditor.tsx +88 -0
- package/apps/shared/components/contacts/ContactPhotoField.tsx +36 -39
- package/apps/shared/components/contacts/layout/ContactsShell.tsx +1 -1
- package/apps/shared/components/contacts/participantDetails.ts +18 -1
- package/apps/shared/components/layout/AppShell.tsx +9 -0
- package/apps/shared/components/layout/KeyEnrollmentGate.tsx +9 -4
- package/apps/shared/components/layout/MailboxProvisioning.tsx +31 -3
- package/apps/shared/components/layout/RecoveryCodeUnlock.tsx +4 -2
- package/apps/shared/components/mail/ConversationList.tsx +49 -6
- package/apps/shared/components/mail/ConversationThreadPane.tsx +1 -1
- package/apps/shared/components/mail/MenuButton.tsx +3 -400
- package/apps/shared/components/mail/MessageDetailPane.tsx +16 -13
- package/apps/shared/components/mail/compose/ComposeToolbar.tsx +11 -1
- package/apps/shared/components/mail/compose/ComposeWindow.tsx +57 -11
- package/apps/shared/components/mail/compose/GifPicker.tsx +10 -4
- package/apps/shared/components/mail/compose/RecipientInput.tsx +9 -7
- package/apps/shared/components/mail/compose/composeFlushRegistry.ts +14 -1
- package/apps/shared/components/mail/compose/encryptionRequirement.ts +5 -0
- package/apps/shared/components/mail/compose/quotedBody.ts +31 -6
- package/apps/shared/components/mail/layout/MailShell.tsx +1 -1
- package/apps/shared/components/mail/reading/MessageBody.tsx +13 -7
- package/apps/shared/components/mail/reading/bodyContent.ts +2 -0
- package/apps/shared/components/mail/reading/bodyHtml.ts +25 -2
- package/apps/shared/components/mail/writableMailboxes.ts +2 -0
- package/apps/shared/components/rules/RuleBuilder.tsx +11 -2
- package/apps/shared/components/settings/GravatarSetting.tsx +27 -0
- package/apps/shared/components/settings/layout/SettingsShell.tsx +1 -1
- package/apps/shared/components/tasks/dueDate.ts +29 -0
- package/apps/shared/components/tasks/layout/TasksShell.tsx +1 -1
- package/apps/shared/mail/decryptedMessages.ts +51 -2
- package/apps/shared/mail/listSnapshots.ts +2 -0
- package/apps/shared/mail/outbox/sendDecision.ts +14 -3
- package/apps/shared/mail/outbox/sendJob.ts +5 -0
- package/apps/shared/mail/useMailLiveUpdates.ts +7 -1
- package/apps/shared/mail/usePermanentDelete.tsx +3 -1
- package/apps/shared/notifications/apiErrors.ts +2 -0
- package/apps/shared/notifications/preferences.ts +1 -1
- package/apps/shared/notifications/store.ts +3 -1
- package/apps/shared/plugins/pluginNav.ts +4 -2
- package/apps/www/calendar/index.tsx +24 -12
- package/apps/www/contacts/[uid].tsx +6 -1
- package/apps/www/contacts/index.tsx +66 -12
- package/apps/www/index.tsx +91 -28
- package/apps/www/messages/[uid].tsx +8 -3
- package/apps/www/settings/auto-reply/index.tsx +13 -0
- package/apps/www/settings/blocked-senders/index.tsx +4 -0
- package/apps/www/settings/encryption/index.tsx +66 -4
- package/apps/www/settings/privacy/index.tsx +4 -1
- package/apps/www/settings/profile/index.tsx +12 -1
- package/apps/www/settings/sharing/index.tsx +11 -1
- package/apps/www/settings/signatures/[uid].tsx +6 -4
- package/apps/www/tasks/index.tsx +97 -29
- package/dist/apps/admin/audit-log/index.js +3 -3
- package/dist/apps/admin/branding/index.js +3 -3
- package/dist/apps/admin/data-requests/index.js +17 -7
- package/dist/apps/admin/distribution-lists/[uid].js +3 -3
- package/dist/apps/admin/distribution-lists/index.js +3 -3
- package/dist/apps/admin/distribution-lists/new/index.js +3 -3
- package/dist/apps/admin/domains/[uid].js +6 -5
- package/dist/apps/admin/domains/index.js +3 -3
- package/dist/apps/admin/domains/new/index.js +3 -3
- package/dist/apps/admin/escrow-scopes/[uid].js +11 -6
- package/dist/apps/admin/escrow-scopes/index.js +3 -3
- package/dist/apps/admin/escrow-scopes/new/index.js +9 -4
- package/dist/apps/admin/index.js +4 -4
- package/dist/apps/admin/ingest-queue/index.js +3 -3
- package/dist/apps/admin/mailboxes/[uid].js +15 -7
- package/dist/apps/admin/quarantine/index.js +4 -4
- package/dist/apps/admin/retention-policy/index.js +3 -3
- package/dist/apps/admin/signing-certificates/index.js +3 -3
- package/dist/apps/admin/transport-rules/[uid].js +4 -4
- package/dist/apps/admin/transport-rules/index.js +3 -3
- package/dist/apps/admin/transport-rules/new/index.js +3 -3
- package/dist/apps/escrow/matters/[uid].js +8 -3
- package/dist/apps/escrow/matters/new/index.js +4 -0
- package/dist/apps/shared/calendar/calendarLiveUpdates.d.ts +11 -7
- package/dist/apps/shared/calendar/calendarLiveUpdates.js +10 -9
- package/dist/apps/shared/calendar/useCalendarReminders.js +2 -0
- package/dist/apps/shared/components/admin/ActionAlert.d.ts +13 -0
- package/dist/apps/shared/components/admin/ActionAlert.js +24 -0
- package/dist/apps/shared/components/admin/diagnostics/InformationPanel.js +1 -1
- package/dist/apps/shared/components/admin/diagnostics/InstalledPlugins.js +1 -1
- package/dist/apps/shared/components/admin/diagnostics/LogsPanel.js +1 -1
- package/dist/apps/shared/components/admin/diagnostics/RuntimePanel.js +1 -1
- package/dist/apps/shared/components/admin/diagnostics/SystemPanel.js +1 -1
- package/dist/apps/shared/components/admin/distributionLists/MemberListCard.js +4 -4
- package/dist/apps/shared/components/admin/elevation.d.ts +9 -0
- package/dist/apps/shared/components/admin/elevation.js +16 -0
- package/dist/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.d.ts +2 -0
- package/dist/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.js +41 -1
- package/dist/apps/shared/components/admin/layout/AdminShell.js +2 -1
- package/dist/apps/shared/components/admin/mailboxes/EraseLeftoverDataDialog.js +3 -3
- package/dist/apps/shared/components/admin/mailboxes/EscrowScopeCard.js +4 -4
- package/dist/apps/shared/components/admin/mailboxes/LeftoverMailboxesSection.js +4 -4
- package/dist/apps/shared/components/admin/mailboxes/ResourceSettingsCard.js +3 -3
- package/dist/apps/shared/components/admin/mailboxes/ShareAccessCard.js +6 -6
- package/dist/apps/shared/components/admin/settings/BrandingForm.js +17 -7
- package/dist/apps/shared/components/admin/settings/DomainDnsSetup.js +4 -4
- package/dist/apps/shared/components/admin/settings/EncryptionPolicyForm.js +17 -5
- package/dist/apps/shared/components/admin/settings/LoadedSettingsForm.js +3 -3
- package/dist/apps/shared/components/admin/settings/MailboxCreateForm.js +8 -3
- package/dist/apps/shared/components/admin/settings/MailboxPolicyForm.js +3 -3
- package/dist/apps/shared/components/admin/settings/PluginsManager.js +26 -12
- package/dist/apps/shared/components/admin/settings/RetentionPolicyForm.js +17 -10
- package/dist/apps/shared/components/admin/setup/EscrowSetupStep.js +18 -8
- package/dist/apps/shared/components/admin/setup/SetupWizard.js +8 -6
- package/dist/apps/shared/components/admin/signOut.js +3 -0
- package/dist/apps/shared/components/admin/usePagedList.js +2 -2
- package/dist/apps/shared/components/calendar/DescriptionEditor.js +2 -1
- package/dist/apps/shared/components/calendar/EventDetails.d.ts +3 -1
- package/dist/apps/shared/components/calendar/EventDetails.js +7 -4
- package/dist/apps/shared/components/calendar/EventEditor.js +81 -33
- package/dist/apps/shared/components/calendar/EventFormParts.d.ts +3 -1
- package/dist/apps/shared/components/calendar/EventFormParts.js +3 -3
- package/dist/apps/shared/components/calendar/EventMatchNav.d.ts +1 -1
- package/dist/apps/shared/components/calendar/EventMatchNav.js +12 -2
- package/dist/apps/shared/components/calendar/EventModal.js +10 -2
- package/dist/apps/shared/components/calendar/FindATime.js +4 -3
- package/dist/apps/shared/components/calendar/ListView.d.ts +10 -4
- package/dist/apps/shared/components/calendar/ListView.js +92 -27
- package/dist/apps/shared/components/calendar/RecurrenceEditor.d.ts +4 -1
- package/dist/apps/shared/components/calendar/RecurrenceEditor.js +15 -10
- package/dist/apps/shared/components/calendar/SplitDayView.js +11 -17
- package/dist/apps/shared/components/calendar/TaskQuickForm.js +3 -2
- package/dist/apps/shared/components/calendar/TimeGridView.js +23 -21
- package/dist/apps/shared/components/calendar/gridLayout.d.ts +41 -0
- package/dist/apps/shared/components/calendar/gridLayout.js +83 -0
- package/dist/apps/shared/components/calendar/layout/CalendarShell.js +1 -1
- package/dist/apps/shared/components/calendar/timePicker.js +5 -2
- package/dist/apps/shared/components/contacts/ContactCard.js +9 -3
- package/dist/apps/shared/components/contacts/ContactDetailPane.d.ts +5 -3
- package/dist/apps/shared/components/contacts/ContactDetailPane.js +11 -5
- package/dist/apps/shared/components/contacts/ContactForm.js +54 -16
- package/dist/apps/shared/components/contacts/ContactPhotoEditor.d.ts +21 -0
- package/dist/apps/shared/components/contacts/ContactPhotoEditor.js +56 -0
- package/dist/apps/shared/components/contacts/ContactPhotoField.d.ts +7 -4
- package/dist/apps/shared/components/contacts/ContactPhotoField.js +19 -21
- package/dist/apps/shared/components/contacts/layout/ContactsShell.js +1 -1
- package/dist/apps/shared/components/contacts/participantDetails.d.ts +4 -1
- package/dist/apps/shared/components/contacts/participantDetails.js +17 -1
- package/dist/apps/shared/components/layout/AppShell.js +9 -0
- package/dist/apps/shared/components/layout/KeyEnrollmentGate.js +2 -3
- package/dist/apps/shared/components/layout/MailboxProvisioning.d.ts +4 -1
- package/dist/apps/shared/components/layout/MailboxProvisioning.js +24 -2
- package/dist/apps/shared/components/layout/RecoveryCodeUnlock.d.ts +4 -2
- package/dist/apps/shared/components/layout/RecoveryCodeUnlock.js +4 -2
- package/dist/apps/shared/components/mail/ConversationList.js +49 -6
- package/dist/apps/shared/components/mail/ConversationThreadPane.js +1 -1
- package/dist/apps/shared/components/mail/MenuButton.d.ts +2 -78
- package/dist/apps/shared/components/mail/MenuButton.js +2 -211
- package/dist/apps/shared/components/mail/MessageDetailPane.js +10 -6
- package/dist/apps/shared/components/mail/compose/ComposeToolbar.d.ts +3 -0
- package/dist/apps/shared/components/mail/compose/ComposeToolbar.js +10 -1
- package/dist/apps/shared/components/mail/compose/ComposeWindow.js +55 -10
- package/dist/apps/shared/components/mail/compose/GifPicker.js +10 -4
- package/dist/apps/shared/components/mail/compose/RecipientInput.js +9 -7
- package/dist/apps/shared/components/mail/compose/composeFlushRegistry.d.ts +7 -1
- package/dist/apps/shared/components/mail/compose/composeFlushRegistry.js +13 -1
- package/dist/apps/shared/components/mail/compose/encryptionRequirement.d.ts +5 -0
- package/dist/apps/shared/components/mail/compose/quotedBody.js +28 -5
- package/dist/apps/shared/components/mail/layout/MailShell.js +1 -1
- package/dist/apps/shared/components/mail/reading/MessageBody.js +11 -6
- package/dist/apps/shared/components/mail/reading/bodyContent.js +2 -0
- package/dist/apps/shared/components/mail/reading/bodyHtml.d.ts +5 -1
- package/dist/apps/shared/components/mail/reading/bodyHtml.js +23 -2
- package/dist/apps/shared/components/mail/writableMailboxes.js +2 -0
- package/dist/apps/shared/components/rules/RuleBuilder.js +9 -1
- package/dist/apps/shared/components/settings/GravatarSetting.d.ts +6 -0
- package/dist/apps/shared/components/settings/GravatarSetting.js +15 -0
- package/dist/apps/shared/components/settings/layout/SettingsShell.js +1 -1
- package/dist/apps/shared/components/tasks/dueDate.d.ts +12 -0
- package/dist/apps/shared/components/tasks/dueDate.js +26 -0
- package/dist/apps/shared/components/tasks/layout/TasksShell.js +1 -1
- package/dist/apps/shared/mail/decryptedMessages.d.ts +8 -0
- package/dist/apps/shared/mail/decryptedMessages.js +46 -2
- package/dist/apps/shared/mail/listSnapshots.js +2 -0
- package/dist/apps/shared/mail/outbox/sendDecision.d.ts +6 -3
- package/dist/apps/shared/mail/outbox/sendDecision.js +13 -3
- package/dist/apps/shared/mail/outbox/sendJob.d.ts +1 -0
- package/dist/apps/shared/mail/outbox/sendJob.js +5 -1
- package/dist/apps/shared/mail/useMailLiveUpdates.d.ts +3 -0
- package/dist/apps/shared/mail/useMailLiveUpdates.js +6 -1
- package/dist/apps/shared/mail/usePermanentDelete.d.ts +2 -0
- package/dist/apps/shared/mail/usePermanentDelete.js +1 -1
- package/dist/apps/shared/notifications/apiErrors.js +2 -0
- package/dist/apps/shared/notifications/preferences.d.ts +1 -1
- package/dist/apps/shared/notifications/preferences.js +1 -1
- package/dist/apps/shared/notifications/store.js +3 -1
- package/dist/apps/shared/plugins/pluginNav.d.ts +2 -1
- package/dist/apps/shared/plugins/pluginNav.js +4 -2
- package/dist/apps/www/calendar/index.js +24 -12
- package/dist/apps/www/contacts/[uid].js +6 -2
- package/dist/apps/www/contacts/index.js +62 -11
- package/dist/apps/www/index.js +85 -30
- package/dist/apps/www/messages/[uid].js +8 -3
- package/dist/apps/www/settings/auto-reply/index.js +13 -1
- package/dist/apps/www/settings/blocked-senders/index.js +4 -0
- package/dist/apps/www/settings/encryption/index.js +29 -5
- package/dist/apps/www/settings/privacy/index.js +3 -2
- package/dist/apps/www/settings/profile/index.js +4 -1
- package/dist/apps/www/settings/sharing/index.js +10 -1
- package/dist/apps/www/settings/signatures/[uid].js +6 -4
- package/dist/apps/www/tasks/index.js +94 -32
- package/dist/lib/admin/auditLogApi.js +2 -2
- package/dist/lib/admin/distributionListsApi.js +2 -2
- package/dist/lib/admin/domainsApi.js +2 -2
- package/dist/lib/admin/escrowAuditLogApi.js +2 -2
- package/dist/lib/admin/escrowScopesApi.js +2 -2
- package/dist/lib/admin/pluginsApi.d.ts +13 -2
- package/dist/lib/admin/pluginsApi.js +8 -0
- package/dist/lib/admin/retentionPolicyApi.d.ts +3 -0
- package/dist/lib/admin/retentionPolicyApi.js +3 -0
- package/dist/lib/admin/transportRulesApi.js +2 -2
- package/dist/lib/appearance/preferencesApi.d.ts +1 -1
- package/dist/lib/appearance/preferencesApi.js +3 -14
- package/dist/lib/auth/session.d.ts +51 -12
- package/dist/lib/auth/session.js +214 -38
- package/dist/lib/branding/brandingApi.d.ts +3 -3
- package/dist/lib/branding/brandingApi.js +9 -21
- package/dist/lib/calendar/calendarApi.js +3 -1
- package/dist/lib/calendar/eventSearch.js +43 -6
- package/dist/lib/calendar/freeBusyApi.d.ts +2 -1
- package/dist/lib/calendar/freeBusyApi.js +13 -5
- package/dist/lib/calendar/recurrence.js +51 -3
- package/dist/lib/calendar/useDayKey.d.ts +7 -0
- package/dist/lib/calendar/useDayKey.js +43 -0
- package/dist/lib/components/avatar/ContactAvatar.d.ts +6 -2
- package/dist/lib/components/avatar/ContactAvatar.js +37 -8
- package/dist/lib/components/avatar/ContactPhotoAvatar.d.ts +11 -0
- package/dist/lib/components/avatar/ContactPhotoAvatar.js +18 -0
- package/dist/lib/components/forms/CameraCaptureModal.d.ts +17 -0
- package/dist/lib/components/forms/CameraCaptureModal.js +88 -0
- package/dist/lib/components/forms/ImageEditBadge.d.ts +28 -0
- package/dist/lib/components/forms/ImageEditBadge.js +62 -0
- package/dist/lib/components/menus/MenuButton.d.ts +78 -0
- package/dist/lib/components/menus/MenuButton.js +219 -0
- package/dist/lib/components/navigation/BottomTabBar.d.ts +3 -1
- package/dist/lib/components/navigation/BottomTabBar.js +22 -3
- package/dist/lib/components/overlays/PopoverPortal.d.ts +2 -1
- package/dist/lib/components/overlays/PopoverPortal.js +3 -2
- package/dist/lib/components/pickers/TimeZonePicker.js +13 -1
- package/dist/lib/contacts/contactsApi.d.ts +5 -2
- package/dist/lib/contacts/contactsApi.js +16 -19
- package/dist/lib/contacts/gravatar.d.ts +14 -3
- package/dist/lib/contacts/gravatar.js +42 -5
- package/dist/lib/contacts/preparePhoto.d.ts +24 -0
- package/dist/lib/contacts/preparePhoto.js +250 -0
- package/dist/lib/contacts/useContactPhotoSrc.d.ts +19 -0
- package/dist/lib/contacts/useContactPhotoSrc.js +153 -0
- package/dist/lib/contacts/vcard.d.ts +3 -0
- package/dist/lib/contacts/vcard.js +59 -7
- package/dist/lib/crypto/escrowKeys.d.ts +8 -0
- package/dist/lib/crypto/escrowKeys.js +20 -1
- package/dist/lib/crypto/messageSecurity.d.ts +7 -4
- package/dist/lib/crypto/messageSecurity.js +23 -5
- package/dist/lib/crypto/passwordUnlock.d.ts +6 -1
- package/dist/lib/crypto/passwordUnlock.js +13 -2
- package/dist/lib/crypto/smime.js +12 -2
- package/dist/lib/mail/compose/composeQuoting.js +3 -2
- package/dist/lib/mail/compose/quotedImages.d.ts +6 -3
- package/dist/lib/mail/compose/quotedImages.js +35 -9
- package/dist/lib/mail/labelsApi.js +2 -2
- package/dist/lib/mail/mailApi.d.ts +8 -5
- package/dist/lib/mail/mailApi.js +73 -39
- package/dist/lib/mail/mailDetailHooks.js +11 -3
- package/dist/lib/mail/mailFilterRulesApi.js +2 -2
- package/dist/lib/mail/mailSignaturesApi.js +2 -2
- package/dist/lib/mail/mailboxImportApi.d.ts +1 -1
- package/dist/lib/mail/mailboxImportApi.js +3 -15
- package/dist/lib/mail/messageBodySanitizer.d.ts +6 -0
- package/dist/lib/mail/messageBodySanitizer.js +44 -1
- package/dist/lib/search/queryGrammar.js +12 -4
- package/dist/lib/search/searchTier3.d.ts +6 -1
- package/dist/lib/search/searchTier3.js +54 -25
- package/dist/lib/tasks/tasksApi.js +3 -3
- package/dist/lib/util/api.d.ts +26 -4
- package/dist/lib/util/api.js +73 -16
- package/dist/lib/util/apiQuery.d.ts +11 -0
- package/dist/lib/util/apiQuery.js +11 -0
- package/dist/lib/util/settleWithLimit.d.ts +5 -0
- package/dist/lib/util/settleWithLimit.js +25 -0
- package/lib/admin/auditLogApi.ts +2 -2
- package/lib/admin/distributionListsApi.ts +2 -2
- package/lib/admin/domainsApi.ts +2 -2
- package/lib/admin/escrowAuditLogApi.ts +2 -2
- package/lib/admin/escrowScopesApi.ts +2 -2
- package/lib/admin/pluginsApi.ts +21 -2
- package/lib/admin/retentionPolicyApi.ts +4 -0
- package/lib/admin/transportRulesApi.ts +2 -2
- package/lib/appearance/preferencesApi.ts +3 -14
- package/lib/auth/session.ts +239 -39
- package/lib/branding/brandingApi.ts +9 -21
- package/lib/calendar/calendarApi.ts +3 -1
- package/lib/calendar/eventSearch.ts +46 -6
- package/lib/calendar/freeBusyApi.ts +14 -6
- package/lib/calendar/recurrence.ts +59 -8
- package/lib/calendar/useDayKey.ts +45 -0
- package/lib/components/avatar/ContactAvatar.tsx +46 -8
- package/lib/components/avatar/ContactPhotoAvatar.tsx +26 -0
- package/lib/components/forms/CameraCaptureModal.tsx +133 -0
- package/lib/components/forms/ImageEditBadge.tsx +121 -0
- package/lib/components/menus/MenuButton.tsx +408 -0
- package/lib/components/navigation/BottomTabBar.tsx +30 -8
- package/lib/components/overlays/PopoverPortal.tsx +3 -2
- package/lib/components/pickers/TimeZonePicker.tsx +17 -1
- package/lib/contacts/contactsApi.ts +18 -19
- package/lib/contacts/gravatar.ts +47 -5
- package/lib/contacts/preparePhoto.ts +273 -0
- package/lib/contacts/useContactPhotoSrc.ts +177 -0
- package/lib/contacts/vcard.ts +60 -7
- package/lib/crypto/escrowKeys.ts +21 -1
- package/lib/crypto/messageSecurity.ts +33 -9
- package/lib/crypto/passwordUnlock.ts +14 -2
- package/lib/crypto/smime.ts +13 -2
- package/lib/mail/compose/composeQuoting.ts +3 -2
- package/lib/mail/compose/quotedImages.ts +39 -9
- package/lib/mail/labelsApi.ts +2 -2
- package/lib/mail/mailApi.ts +96 -39
- package/lib/mail/mailDetailHooks.ts +11 -3
- package/lib/mail/mailFilterRulesApi.ts +2 -2
- package/lib/mail/mailSignaturesApi.ts +2 -2
- package/lib/mail/mailboxImportApi.ts +9 -15
- package/lib/mail/messageBodySanitizer.ts +47 -1
- package/lib/search/queryGrammar.ts +13 -4
- package/lib/search/searchTier3.ts +65 -32
- package/lib/tasks/tasksApi.ts +3 -3
- package/lib/util/api.ts +99 -16
- package/lib/util/apiQuery.ts +13 -0
- package/lib/util/settleWithLimit.ts +25 -0
- package/package.json +1 -1
|
@@ -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 {
|
|
12
|
+
import { withClient, withClientRaw } from "../util/api.js";
|
|
13
13
|
import { buildRequestListQuery } from "../util/apiQuery.js";
|
|
14
14
|
/**
|
|
15
15
|
* Uploads `file`'s raw bytes as a new mailbox-import request. Bypasses `apiFetch` — that helper always
|
|
@@ -18,24 +18,12 @@ import { buildRequestListQuery } from "../util/apiQuery.js";
|
|
|
18
18
|
* with `format`/`targetFolderUid`/`mailboxUid` as query-string parameters, not a JSON body — the same
|
|
19
19
|
* shape `mailApi.ts`'s `uploadAttachment()` already establishes for a raw-bytes upload.
|
|
20
20
|
*/
|
|
21
|
-
export
|
|
21
|
+
export function uploadMailboxImport(file, input, client) {
|
|
22
22
|
const params = new URLSearchParams({ format: input.format, targetFolderUid: input.targetFolderUid });
|
|
23
23
|
if (input.mailboxUid) {
|
|
24
24
|
params.set("mailboxUid", input.mailboxUid);
|
|
25
25
|
}
|
|
26
|
-
|
|
27
|
-
method: "POST",
|
|
28
|
-
credentials: "include",
|
|
29
|
-
headers: withCsrfHeader({ "Content-Type": input.format === "pst" ? "application/vnd.ms-outlook" : "application/mbox" }),
|
|
30
|
-
body: file,
|
|
31
|
-
});
|
|
32
|
-
const contentType = res.headers.get("content-type") ?? "";
|
|
33
|
-
const responseBody = contentType.includes("application/json") ? await res.json().catch(() => undefined) : undefined;
|
|
34
|
-
if (!res.ok) {
|
|
35
|
-
const message = (responseBody && (responseBody.message || responseBody.error)) || res.statusText || "Upload failed.";
|
|
36
|
-
throw new ApiRequestError(message, res.status, responseBody?.code);
|
|
37
|
-
}
|
|
38
|
-
return responseBody;
|
|
26
|
+
return withClientRaw(client, `/mail/mailbox-import-requests?${params.toString()}`, "POST", file, input.format === "pst" ? "application/vnd.ms-outlook" : "application/mbox");
|
|
39
27
|
}
|
|
40
28
|
/** `client`, given by every function below that isn't a raw-bytes upload, is an explicit `ApiClient` from
|
|
41
29
|
* `createApiClient()` (e.g. one account of a multi-account app) to call instead of the default global
|
|
@@ -7,6 +7,12 @@ export declare function stripRemoteCssUrls(css: string): string;
|
|
|
7
7
|
* sanitize with - never the unsanitized input.
|
|
8
8
|
*/
|
|
9
9
|
export declare function sanitizeMessageBodyHtml(html: string): string;
|
|
10
|
+
/**
|
|
11
|
+
* Sanitizes a signature's HTML for the compose body: DOMPurify's defaults (no scripts, handlers or `javascript:` URLs) and
|
|
12
|
+
* the quote's tag list (nothing that styles, frames or submits). Unlike a quoted message, a signature is the sender's own
|
|
13
|
+
* and may carry pictures from the web, so remote images and links stay. Returns `""` when there is no DOM to sanitize with.
|
|
14
|
+
*/
|
|
15
|
+
export declare function sanitizeSignatureHtml(html: string): string;
|
|
10
16
|
/**
|
|
11
17
|
* Sanitizes message HTML for quoting into a compose body: the display policy (`sanitizeMessageBodyHtml()`), without
|
|
12
18
|
* the tags in `QUOTE_FORBIDDEN_TAGS`, and without any image that isn't embedded as a `data:` URI - a `cid:` image
|
|
@@ -22,6 +22,36 @@ export function stripRemoteCssUrls(css) {
|
|
|
22
22
|
.replace(/(?:-webkit-)?image-set\((?:[^()]|\([^()]*\))*\)/gi, (match) => [...match.matchAll(/(["'])(.*?)\1/g)].every(([, , uri]) => EMBEDDED_URI.test(uri)) ? match : "none")
|
|
23
23
|
.replace(/url\(\s*(["']?)(.*?)\1\s*\)/gi, (match, _quote, uri) => (EMBEDDED_URI.test(uri) ? match : "none"));
|
|
24
24
|
}
|
|
25
|
+
/** The candidate URLs of a `srcset` value, parsed as the HTML spec does: a candidate's URL is a run of non-whitespace
|
|
26
|
+
* characters (it may itself contain commas - a `data:` URI always does), so a comma that ends a URL or follows its
|
|
27
|
+
* descriptors separates candidates whether or not whitespace follows it. */
|
|
28
|
+
function srcsetUrls(value) {
|
|
29
|
+
const urls = [];
|
|
30
|
+
let position = 0;
|
|
31
|
+
while (position < value.length) {
|
|
32
|
+
const start = value.slice(position).search(/[^\s,]/);
|
|
33
|
+
if (start < 0) {
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
position += start;
|
|
37
|
+
const end = value.slice(position).search(/\s/);
|
|
38
|
+
const token = end < 0 ? value.slice(position) : value.slice(position, position + end);
|
|
39
|
+
position += token.length;
|
|
40
|
+
urls.push(token.replace(/,+$/, ""));
|
|
41
|
+
if (!token.endsWith(",")) {
|
|
42
|
+
// Skip the descriptors, up to the comma that ends the candidate.
|
|
43
|
+
const comma = value.indexOf(",", position);
|
|
44
|
+
const descriptors = value.slice(position, comma < 0 ? value.length : comma);
|
|
45
|
+
if (descriptors.includes("(")) {
|
|
46
|
+
// The spec's tokenizer ignores commas inside parentheses, so a browser may read the candidates that follow differently from
|
|
47
|
+
// the way this does. No valid descriptor has one: refuse the whole value.
|
|
48
|
+
urls.push(descriptors);
|
|
49
|
+
}
|
|
50
|
+
position = comma < 0 ? value.length : comma + 1;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return urls;
|
|
54
|
+
}
|
|
25
55
|
let bodyPurifier;
|
|
26
56
|
/** A dedicated DOMPurify instance (hooks registered here never leak into any other DOMPurify caller) that
|
|
27
57
|
* additionally strips every remote resource reference - see `stripRemoteCssUrls()`. `undefined` without a DOM
|
|
@@ -51,7 +81,7 @@ function getBodyPurifier() {
|
|
|
51
81
|
}
|
|
52
82
|
const isNavigationLink = name === "href" && ["a", "area"].includes(node.nodeName.toLowerCase());
|
|
53
83
|
if (RESOURCE_URI_ATTRIBUTES.has(name) || (name === "href" && !isNavigationLink)) {
|
|
54
|
-
const candidates = name === "srcset" ? data.attrValue
|
|
84
|
+
const candidates = name === "srcset" ? srcsetUrls(data.attrValue) : [data.attrValue];
|
|
55
85
|
if (!candidates.every((candidate) => EMBEDDED_URI.test(candidate))) {
|
|
56
86
|
data.keepAttr = false;
|
|
57
87
|
}
|
|
@@ -95,6 +125,19 @@ const QUOTE_FORBIDDEN_TAGS = [
|
|
|
95
125
|
"track",
|
|
96
126
|
// svg/math are already in DISPLAY_FORBIDDEN_TAGS, spread above — not repeated here.
|
|
97
127
|
];
|
|
128
|
+
let signaturePurifier;
|
|
129
|
+
/**
|
|
130
|
+
* Sanitizes a signature's HTML for the compose body: DOMPurify's defaults (no scripts, handlers or `javascript:` URLs) and
|
|
131
|
+
* the quote's tag list (nothing that styles, frames or submits). Unlike a quoted message, a signature is the sender's own
|
|
132
|
+
* and may carry pictures from the web, so remote images and links stay. Returns `""` when there is no DOM to sanitize with.
|
|
133
|
+
*/
|
|
134
|
+
export function sanitizeSignatureHtml(html) {
|
|
135
|
+
if (typeof window === "undefined") {
|
|
136
|
+
return "";
|
|
137
|
+
}
|
|
138
|
+
signaturePurifier ?? (signaturePurifier = DOMPurify(window));
|
|
139
|
+
return signaturePurifier.isSupported ? signaturePurifier.sanitize(html, { FORBID_TAGS: QUOTE_FORBIDDEN_TAGS }) : "";
|
|
140
|
+
}
|
|
98
141
|
/**
|
|
99
142
|
* Sanitizes message HTML for quoting into a compose body: the display policy (`sanitizeMessageBodyHtml()`), without
|
|
100
143
|
* the tags in `QUOTE_FORBIDDEN_TAGS`, and without any image that isn't embedded as a `data:` URI - a `cid:` image
|
|
@@ -7,14 +7,22 @@ const VALID_ENTITY_TYPES = new Set(["message", "contact", "calendarEvent", "note
|
|
|
7
7
|
* `parseSearchQuery()` itself, after every extraction has run. */
|
|
8
8
|
function extractFirst(input, key) {
|
|
9
9
|
const pattern = new RegExp(`(^|\\s)${key}:(?:"([^"]*)"|(\\S+))`, "i");
|
|
10
|
-
const
|
|
11
|
-
if (!
|
|
10
|
+
const found = pattern.exec(maskQuoted(input));
|
|
11
|
+
if (!found) {
|
|
12
12
|
return { rest: input };
|
|
13
13
|
}
|
|
14
|
+
// The masked text only says where the operator is - what it holds is read from the real one.
|
|
15
|
+
const match = pattern.exec(input.slice(found.index));
|
|
14
16
|
const value = match[2] ?? match[3] ?? "";
|
|
15
|
-
const rest = input.slice(0,
|
|
17
|
+
const rest = input.slice(0, found.index) + match[1] + input.slice(found.index + match[0].length);
|
|
16
18
|
return { value, rest };
|
|
17
19
|
}
|
|
20
|
+
/** `input` with the inside of every quoted phrase blanked out (the length stays the same, so positions carry over to
|
|
21
|
+
* `input`): a word that looks like an operator inside a phrase - `"meeting from:bob"` - is part of the phrase, never an
|
|
22
|
+
* operator. */
|
|
23
|
+
function maskQuoted(input) {
|
|
24
|
+
return input.replace(/"([^"]*)"/g, (_phrase, inner) => `"${"\0".repeat(inner.length)}"`);
|
|
25
|
+
}
|
|
18
26
|
/** Repeatedly applies `extractFirst()` for `key` until no more occurrences remain, splitting any
|
|
19
27
|
* comma-separated value into individual entries — so `is:read is:flagged` and `is:read,flagged` both
|
|
20
28
|
* produce `["read", "flagged"]`. */
|
|
@@ -39,7 +47,7 @@ function extractAll(input, key) {
|
|
|
39
47
|
* spec defines for this operator — there is no `has:<other value>` form. */
|
|
40
48
|
function extractHasAttachment(input) {
|
|
41
49
|
const pattern = /(^|\s)has:attachment\b/i;
|
|
42
|
-
const match = pattern.exec(input);
|
|
50
|
+
const match = pattern.exec(maskQuoted(input));
|
|
43
51
|
if (!match) {
|
|
44
52
|
return { found: false, rest: input };
|
|
45
53
|
}
|
|
@@ -38,13 +38,18 @@ export declare function stripHtml(html: string): string;
|
|
|
38
38
|
/** How many candidates are fetched/decrypted at once - bounds the burst of raw-MIME requests and
|
|
39
39
|
* concurrent WebCrypto work a large candidate page would otherwise fire all at once. */
|
|
40
40
|
export declare const TIER3_DECRYPT_CONCURRENCY = 4;
|
|
41
|
+
/** The most candidate pages one search walks (`nextCursor` onwards) while it is still short of `limit` results - a
|
|
42
|
+
* search that matches little must not download and decrypt a whole mailbox, but an encrypted message beyond the first
|
|
43
|
+
* page of candidates is still found. */
|
|
44
|
+
export declare const TIER3_MAX_CANDIDATE_PAGES = 4;
|
|
41
45
|
export interface SearchEncryptedCandidatesOptions {
|
|
42
46
|
/** Forwarded to `candidates()` - which accessible mailbox to search (e.g. the one currently open).
|
|
43
47
|
* Omitted, the server uses the caller's own mailbox. */
|
|
44
48
|
mailboxUid?: string;
|
|
45
49
|
}
|
|
46
50
|
/**
|
|
47
|
-
* Runs Tier 3 for one parsed query: fetches a bounded candidate set from the server
|
|
51
|
+
* Runs Tier 3 for one parsed query: fetches a bounded candidate set from the server (following `nextCursor` for up to
|
|
52
|
+
* `TIER3_MAX_CANDIDATE_PAGES` pages while fewer than `limit` messages have matched), decrypts each
|
|
48
53
|
* candidate this device can open (at most `TIER3_DECRYPT_CONCURRENCY` at a time), and returns only the
|
|
49
54
|
* ones whose real (decrypted) content actually matches - each with a raw (not yet normalized) score the
|
|
50
55
|
* caller should run through `searchScoring.ts#normalizeServerScores()` alongside Tier 1's own scores
|
|
@@ -224,6 +224,10 @@ function hasAnyFilter(parsed) {
|
|
|
224
224
|
/** How many candidates are fetched/decrypted at once - bounds the burst of raw-MIME requests and
|
|
225
225
|
* concurrent WebCrypto work a large candidate page would otherwise fire all at once. */
|
|
226
226
|
export const TIER3_DECRYPT_CONCURRENCY = 4;
|
|
227
|
+
/** The most candidate pages one search walks (`nextCursor` onwards) while it is still short of `limit` results - a
|
|
228
|
+
* search that matches little must not download and decrypt a whole mailbox, but an encrypted message beyond the first
|
|
229
|
+
* page of candidates is still found. */
|
|
230
|
+
export const TIER3_MAX_CANDIDATE_PAGES = 4;
|
|
227
231
|
/** `Promise.allSettled()` over `items`, but never running more than `concurrency` `task`s at a time.
|
|
228
232
|
* Outcomes are returned in `items`' own order. */
|
|
229
233
|
async function settleWithConcurrency(items, concurrency, task) {
|
|
@@ -244,7 +248,8 @@ async function settleWithConcurrency(items, concurrency, task) {
|
|
|
244
248
|
return outcomes;
|
|
245
249
|
}
|
|
246
250
|
/**
|
|
247
|
-
* Runs Tier 3 for one parsed query: fetches a bounded candidate set from the server
|
|
251
|
+
* Runs Tier 3 for one parsed query: fetches a bounded candidate set from the server (following `nextCursor` for up to
|
|
252
|
+
* `TIER3_MAX_CANDIDATE_PAGES` pages while fewer than `limit` messages have matched), decrypts each
|
|
248
253
|
* candidate this device can open (at most `TIER3_DECRYPT_CONCURRENCY` at a time), and returns only the
|
|
249
254
|
* ones whose real (decrypted) content actually matches - each with a raw (not yet normalized) score the
|
|
250
255
|
* caller should run through `searchScoring.ts#normalizeServerScores()` alongside Tier 1's own scores
|
|
@@ -266,35 +271,59 @@ export async function searchEncryptedCandidates(parsed, unlocked, limit = 50, op
|
|
|
266
271
|
return [];
|
|
267
272
|
}
|
|
268
273
|
const participants = [parsed.from, parsed.to, parsed.cc].filter((value) => Boolean(value));
|
|
269
|
-
const
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
274
|
+
const results = [];
|
|
275
|
+
let cursor;
|
|
276
|
+
for (let pageNumber = 0; pageNumber < TIER3_MAX_CANDIDATE_PAGES; pageNumber++) {
|
|
277
|
+
let page;
|
|
278
|
+
try {
|
|
279
|
+
page = await candidates({
|
|
280
|
+
types: ["message"],
|
|
281
|
+
participants: participants.length > 0 ? participants : undefined,
|
|
282
|
+
before: parsed.before,
|
|
283
|
+
after: parsed.after,
|
|
284
|
+
folderUid: parsed.folderUid,
|
|
285
|
+
flags: parsed.flags,
|
|
286
|
+
labels: parsed.labels,
|
|
287
|
+
limit,
|
|
288
|
+
cursor,
|
|
289
|
+
mailboxUid: options.mailboxUid,
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
catch (err) {
|
|
293
|
+
if (pageNumber === 0) {
|
|
294
|
+
throw err;
|
|
285
295
|
}
|
|
296
|
+
// A later page failing keeps what the earlier ones found.
|
|
297
|
+
break;
|
|
286
298
|
}
|
|
287
|
-
const
|
|
299
|
+
const settled = await settleWithConcurrency(page.candidates.filter((candidate) => candidate.entityType === "message"), TIER3_DECRYPT_CONCURRENCY, async (candidate) => {
|
|
300
|
+
if (parsed.hasAttachment !== undefined) {
|
|
301
|
+
const message = await getMessage(candidate.entityUid);
|
|
302
|
+
if (Boolean(message.hasAttachments) !== parsed.hasAttachment) {
|
|
303
|
+
return undefined;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
const rawMime = await getMessageRawContent(candidate.entityUid);
|
|
307
|
+
if (unlocked.destroyed) {
|
|
308
|
+
return undefined;
|
|
309
|
+
}
|
|
310
|
+
const security = await evaluateMessageSecurity(rawMime, unlocked);
|
|
311
|
+
return { entityUid: candidate.entityUid, security };
|
|
312
|
+
});
|
|
288
313
|
if (unlocked.destroyed) {
|
|
289
|
-
|
|
314
|
+
// Locked while candidates were being fetched/decrypted - don't surface decrypted content after a lock.
|
|
315
|
+
return [];
|
|
316
|
+
}
|
|
317
|
+
results.push(...matchDecrypted(parsed, settled));
|
|
318
|
+
cursor = page.nextCursor;
|
|
319
|
+
if (!cursor || results.length >= limit) {
|
|
320
|
+
break;
|
|
290
321
|
}
|
|
291
|
-
const security = await evaluateMessageSecurity(rawMime, unlocked);
|
|
292
|
-
return { entityUid: candidate.entityUid, security };
|
|
293
|
-
});
|
|
294
|
-
if (unlocked.destroyed) {
|
|
295
|
-
// Locked while candidates were being fetched/decrypted - don't surface decrypted content after a lock.
|
|
296
|
-
return [];
|
|
297
322
|
}
|
|
323
|
+
return results;
|
|
324
|
+
}
|
|
325
|
+
/** The decrypted candidates of one page whose real content matches `parsed`, scored and with their snippets. */
|
|
326
|
+
function matchDecrypted(parsed, settled) {
|
|
298
327
|
const subjectFilter = parsed.subject?.toLowerCase();
|
|
299
328
|
const results = [];
|
|
300
329
|
for (const outcome of settled) {
|
|
@@ -5,13 +5,13 @@
|
|
|
5
5
|
/** Typed wrappers over `@rapidmx/restapi`'s `/mail/tasks` REST surface — see `mailApi.ts`'s own header
|
|
6
6
|
* comment for the shared ACL/authorization model every wrapper file here follows. */
|
|
7
7
|
import { withClient } from "../util/api.js";
|
|
8
|
-
import { buildQuery } from "../util/apiQuery.js";
|
|
8
|
+
import { SETTINGS_LIST_LIMIT, buildQuery } from "../util/apiQuery.js";
|
|
9
9
|
/** `client`, given by every function below, is an explicit `ApiClient` from `createApiClient()` (e.g. one
|
|
10
10
|
* account of a multi-account app) to call instead of the default global `apiFetch()` - see `withClient()`'s
|
|
11
11
|
* own doc comment in `util/api.ts`. Omitted (the default), every function here behaves exactly as before. */
|
|
12
12
|
/** Lists a folder's tasks, soonest due date first (tasks with no due date sort last). */
|
|
13
13
|
export function listTasks(folderUid, params = {}, client) {
|
|
14
|
-
return withClient(client, `/mail/tasks?${buildQuery(params, { folderUid, sort: JSON.stringify({ dueDate: "ASC" }) })}`);
|
|
14
|
+
return withClient(client, `/mail/tasks?${buildQuery(params, { folderUid, sort: JSON.stringify({ dueDate: "ASC", uid: "ASC" }) })}`);
|
|
15
15
|
}
|
|
16
16
|
export function createTask(input, client) {
|
|
17
17
|
return withClient(client, "/mail/tasks", {
|
|
@@ -39,7 +39,7 @@ export function deleteTask(uid, version, client) {
|
|
|
39
39
|
}
|
|
40
40
|
/** Lists a mailbox's task lists (Outlook To-Do-style custom lists), alphabetically by name. */
|
|
41
41
|
export function listTaskLists(mailboxUid, params = {}, client) {
|
|
42
|
-
return withClient(client, `/mail/task-lists?${buildQuery(params, { mailboxUid, sort: JSON.stringify({ name: "ASC" }) })}`);
|
|
42
|
+
return withClient(client, `/mail/task-lists?${buildQuery({ limit: SETTINGS_LIST_LIMIT, ...params }, { mailboxUid, sort: JSON.stringify({ name: "ASC", uid: "ASC" }) })}`);
|
|
43
43
|
}
|
|
44
44
|
export function createTaskList(input, client) {
|
|
45
45
|
return withClient(client, "/mail/task-lists", { method: "POST", body: JSON.stringify(input) });
|
package/dist/lib/util/api.d.ts
CHANGED
|
@@ -86,10 +86,12 @@ export declare function setApiSessionRecovery(recovery: (() => Promise<boolean>)
|
|
|
86
86
|
* a deployment-level requirement owned by auth-server/the Helm chart, not configured here — and auth-server's
|
|
87
87
|
* CORS config must explicitly allow this app's origin with credentials.
|
|
88
88
|
*
|
|
89
|
-
* `
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
89
|
+
* No `x-csrf-token` header is sent, deliberately. A double-submit token is only meaningful to the server that issued it, and the `csrf`
|
|
90
|
+
* cookie this app's JavaScript can read is *this* host's own (`mail.example.com`), never auth-server's - so echoing it
|
|
91
|
+
* would be a wrong token for the wrong server. Worse, auth-server's CORS rules do not allow that header, so the browser
|
|
92
|
+
* blocked the preflight of every such call (the silent session refresh, sign-out) before it ever reached the server: "Request header
|
|
93
|
+
* field x-csrf-token is not allowed by Access-Control-Allow-Headers". Auth-server's CSRF protection for this cross-origin call shape is
|
|
94
|
+
* its Origin allow-list check, which needs no header.
|
|
93
95
|
*/
|
|
94
96
|
export declare function authApiFetch<T = unknown>(authServerUrl: string, path: string, init?: RequestInit): Promise<T>;
|
|
95
97
|
/**
|
|
@@ -110,6 +112,12 @@ export interface ApiClient {
|
|
|
110
112
|
/** Same contract as `apiFetch()`: prefixes `path` with `/api`, parses a JSON response, and rejects with
|
|
111
113
|
* `ApiRequestError` on a non-ok response. */
|
|
112
114
|
fetch<T = unknown>(path: string, init?: RequestInit): Promise<T>;
|
|
115
|
+
/**
|
|
116
|
+
* Like `fetch()` - same origin, authentication and error decoding - but resolves the response's own bytes instead of parsing JSON, for a picture or other
|
|
117
|
+
* file. An `<img src>` cannot send an `Authorization` header, so a token-authenticated client shows such a file by fetching it here and displaying a
|
|
118
|
+
* `blob:` URL. No `Content-Type` is added.
|
|
119
|
+
*/
|
|
120
|
+
fetchBlob(path: string, init?: RequestInit): Promise<Blob>;
|
|
113
121
|
/**
|
|
114
122
|
* This client instance's own `setApiUnauthorizedObserver()` equivalent - see `createApiClient()`'s doc
|
|
115
123
|
* comment for why it is per-instance rather than shared with the module-level `setApiUnauthorizedObserver()`
|
|
@@ -164,3 +172,17 @@ export declare function createApiClient(options: CreateApiClientOptions): ApiCli
|
|
|
164
172
|
* that call too, so a whole call chain stays pinned to one account.
|
|
165
173
|
*/
|
|
166
174
|
export declare function withClient<T = unknown>(client: ApiClient | undefined, path: string, init?: RequestInit): Promise<T>;
|
|
175
|
+
/**
|
|
176
|
+
* Sends a file's own bytes (not JSON) to `path` - the upload counterpart of `withClient()`. With an explicit `client` the request goes through it, so it
|
|
177
|
+
* reaches that account's origin with its token (the client keeps the `Content-Type` given here). Without one it is a plain `fetch()` to the global
|
|
178
|
+
* origin that carries the `jwt` cookie and the CSRF header, exactly as a same-origin upload always was. Resolves the decoded JSON response and rejects with
|
|
179
|
+
* an `ApiRequestError` (`failureMessage` when the server gave no message) on a non-ok one.
|
|
180
|
+
*/
|
|
181
|
+
export declare function withClientRaw<T = unknown>(client: ApiClient | undefined, path: string, method: string, file: Blob, contentType?: string, failureMessage?: string): Promise<T>;
|
|
182
|
+
/**
|
|
183
|
+
* Fetches `path`'s own bytes (a message's raw source, say) rather than JSON - the download counterpart of `withClient()`. With an explicit `client` the
|
|
184
|
+
* request goes through its `fetchBlob()`, so it reaches that account's origin with its token. Without one it is a plain `fetch()` to the global origin
|
|
185
|
+
* with the `jwt` cookie, renewing the session and asking again once on a `401`, as `apiFetch()` does. Rejects with an `ApiRequestError`
|
|
186
|
+
* (`failureMessage` when the server gave no message) on a non-ok response.
|
|
187
|
+
*/
|
|
188
|
+
export declare function withClientBlob(client: ApiClient | undefined, path: string, failureMessage?: string): Promise<Blob>;
|
package/dist/lib/util/api.js
CHANGED
|
@@ -137,12 +137,17 @@ export async function apiFetch(path, init = {}) {
|
|
|
137
137
|
headers.set("Content-Type", "application/json");
|
|
138
138
|
applyCsrfHeader(headers, init.method);
|
|
139
139
|
const credentials = apiBaseUrl ? "include" : init.credentials;
|
|
140
|
-
|
|
141
|
-
|
|
140
|
+
const res = await fetchWithRecovery(path, { ...init, headers, credentials });
|
|
141
|
+
return decodeApiResponse(res, (error) => unauthorizedObserver?.(error));
|
|
142
|
+
}
|
|
143
|
+
/** `fetch()` of `path` against the global origin. When the access token has run out (a sleeping tab or laptop stops the timer that renews it in
|
|
144
|
+
* time) it renews the session, and asks again once. */
|
|
145
|
+
async function fetchWithRecovery(path, init) {
|
|
146
|
+
let res = await fetch(apiUrl(path), init);
|
|
142
147
|
if (res.status === 401 && sessionRecovery && (await recoverSession())) {
|
|
143
|
-
res = await fetch(apiUrl(path),
|
|
148
|
+
res = await fetch(apiUrl(path), init);
|
|
144
149
|
}
|
|
145
|
-
return
|
|
150
|
+
return res;
|
|
146
151
|
}
|
|
147
152
|
let sessionRecovery;
|
|
148
153
|
/**
|
|
@@ -173,15 +178,16 @@ async function recoverSession() {
|
|
|
173
178
|
* a deployment-level requirement owned by auth-server/the Helm chart, not configured here — and auth-server's
|
|
174
179
|
* CORS config must explicitly allow this app's origin with credentials.
|
|
175
180
|
*
|
|
176
|
-
* `
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
181
|
+
* No `x-csrf-token` header is sent, deliberately. A double-submit token is only meaningful to the server that issued it, and the `csrf`
|
|
182
|
+
* cookie this app's JavaScript can read is *this* host's own (`mail.example.com`), never auth-server's - so echoing it
|
|
183
|
+
* would be a wrong token for the wrong server. Worse, auth-server's CORS rules do not allow that header, so the browser
|
|
184
|
+
* blocked the preflight of every such call (the silent session refresh, sign-out) before it ever reached the server: "Request header
|
|
185
|
+
* field x-csrf-token is not allowed by Access-Control-Allow-Headers". Auth-server's CSRF protection for this cross-origin call shape is
|
|
186
|
+
* its Origin allow-list check, which needs no header.
|
|
180
187
|
*/
|
|
181
188
|
export async function authApiFetch(authServerUrl, path, init = {}) {
|
|
182
189
|
const headers = new Headers(init.headers);
|
|
183
190
|
headers.set("Content-Type", "application/json");
|
|
184
|
-
applyCsrfHeader(headers, init.method);
|
|
185
191
|
const res = await fetch(`${authServerUrl}/api${path}`, { ...init, headers, credentials: "include" });
|
|
186
192
|
return decodeApiResponse(res);
|
|
187
193
|
}
|
|
@@ -201,12 +207,14 @@ export function setApiUnauthorizedObserver(observer) {
|
|
|
201
207
|
* `createApiClient()`-made client's `fetch()` (which forwards to that instance's own
|
|
202
208
|
* `setUnauthorizedObserver()`) pass their own. A throwing `onUnauthorized` never changes what the caller sees.
|
|
203
209
|
*/
|
|
204
|
-
async function decodeApiResponse(res, onUnauthorized) {
|
|
210
|
+
async function decodeApiResponse(res, onUnauthorized, failureMessage = "Request failed.") {
|
|
205
211
|
const contentType = res.headers.get("content-type") ?? "";
|
|
206
212
|
const body = contentType.includes("application/json") ? await res.json().catch(() => undefined) : undefined;
|
|
207
213
|
if (!res.ok) {
|
|
208
|
-
|
|
209
|
-
const
|
|
214
|
+
// A body is untrusted: only a non-empty string is a message (anything else would read "[object Object]"), only a string a code.
|
|
215
|
+
const reported = body && (body.message || body.error);
|
|
216
|
+
const message = (typeof reported === "string" && reported) || res.statusText || failureMessage;
|
|
217
|
+
const error = new ApiRequestError(message, res.status, typeof body?.code === "string" ? body.code : undefined, body);
|
|
210
218
|
if (res.status === 401 && onUnauthorized) {
|
|
211
219
|
try {
|
|
212
220
|
onUnauthorized(error);
|
|
@@ -242,16 +250,30 @@ export function createApiClient(options) {
|
|
|
242
250
|
const baseUrl = options.baseUrl.replace(/\/$/, "");
|
|
243
251
|
const { getAccessToken } = options;
|
|
244
252
|
let unauthorizedObserver;
|
|
245
|
-
async function
|
|
253
|
+
async function send(path, init, json) {
|
|
246
254
|
const headers = new Headers(init.headers);
|
|
247
|
-
|
|
255
|
+
// JSON unless the caller sent a body of its own kind (an upload names the file's type).
|
|
256
|
+
if (json && !headers.has("Content-Type")) {
|
|
257
|
+
headers.set("Content-Type", "application/json");
|
|
258
|
+
}
|
|
248
259
|
const token = await getAccessToken();
|
|
249
260
|
headers.set("Authorization", `jwt ${token}`);
|
|
250
|
-
|
|
251
|
-
|
|
261
|
+
return fetch(`${baseUrl}/api${path}`, { ...init, headers });
|
|
262
|
+
}
|
|
263
|
+
async function clientFetch(path, init = {}) {
|
|
264
|
+
return decodeApiResponse(await send(path, init, true), (error) => unauthorizedObserver?.(error));
|
|
265
|
+
}
|
|
266
|
+
async function clientFetchBlob(path, init = {}) {
|
|
267
|
+
const res = await send(path, init, false);
|
|
268
|
+
if (!res.ok) {
|
|
269
|
+
// Throws the same `ApiRequestError` (and tells the observer of a 401) as every other request.
|
|
270
|
+
await decodeApiResponse(res, (error) => unauthorizedObserver?.(error));
|
|
271
|
+
}
|
|
272
|
+
return res.blob();
|
|
252
273
|
}
|
|
253
274
|
return {
|
|
254
275
|
fetch: clientFetch,
|
|
276
|
+
fetchBlob: clientFetchBlob,
|
|
255
277
|
setUnauthorizedObserver(observer) {
|
|
256
278
|
unauthorizedObserver = observer;
|
|
257
279
|
},
|
|
@@ -270,3 +292,38 @@ export function createApiClient(options) {
|
|
|
270
292
|
export function withClient(client, path, init) {
|
|
271
293
|
return client ? client.fetch(path, init) : apiFetch(path, init);
|
|
272
294
|
}
|
|
295
|
+
/**
|
|
296
|
+
* Sends a file's own bytes (not JSON) to `path` - the upload counterpart of `withClient()`. With an explicit `client` the request goes through it, so it
|
|
297
|
+
* reaches that account's origin with its token (the client keeps the `Content-Type` given here). Without one it is a plain `fetch()` to the global
|
|
298
|
+
* origin that carries the `jwt` cookie and the CSRF header, exactly as a same-origin upload always was. Resolves the decoded JSON response and rejects with
|
|
299
|
+
* an `ApiRequestError` (`failureMessage` when the server gave no message) on a non-ok one.
|
|
300
|
+
*/
|
|
301
|
+
export async function withClientRaw(client, path, method, file, contentType = file.type, failureMessage = "Upload failed.") {
|
|
302
|
+
if (client) {
|
|
303
|
+
return client.fetch(path, { method, headers: { "Content-Type": contentType }, body: file });
|
|
304
|
+
}
|
|
305
|
+
const res = await fetchWithRecovery(path, {
|
|
306
|
+
method,
|
|
307
|
+
credentials: "include",
|
|
308
|
+
headers: withCsrfHeader({ "Content-Type": contentType }, method),
|
|
309
|
+
body: file,
|
|
310
|
+
});
|
|
311
|
+
return decodeApiResponse(res, (error) => unauthorizedObserver?.(error), failureMessage);
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Fetches `path`'s own bytes (a message's raw source, say) rather than JSON - the download counterpart of `withClient()`. With an explicit `client` the
|
|
315
|
+
* request goes through its `fetchBlob()`, so it reaches that account's origin with its token. Without one it is a plain `fetch()` to the global origin
|
|
316
|
+
* with the `jwt` cookie, renewing the session and asking again once on a `401`, as `apiFetch()` does. Rejects with an `ApiRequestError`
|
|
317
|
+
* (`failureMessage` when the server gave no message) on a non-ok response.
|
|
318
|
+
*/
|
|
319
|
+
export async function withClientBlob(client, path, failureMessage = "Download failed.") {
|
|
320
|
+
if (client) {
|
|
321
|
+
return client.fetchBlob(path);
|
|
322
|
+
}
|
|
323
|
+
const res = await fetchWithRecovery(path, { credentials: "include" });
|
|
324
|
+
if (!res.ok) {
|
|
325
|
+
// Throws the same `ApiRequestError` (and tells the observer of a 401) as every other request.
|
|
326
|
+
await decodeApiResponse(res, (error) => unauthorizedObserver?.(error), failureMessage);
|
|
327
|
+
}
|
|
328
|
+
return res.blob();
|
|
329
|
+
}
|
|
@@ -8,11 +8,22 @@ export interface ListParams {
|
|
|
8
8
|
page?: number;
|
|
9
9
|
limit?: number;
|
|
10
10
|
}
|
|
11
|
+
/** The page size of a list that a settings screen shows whole (labels, signatures, filter rules, task lists, contact lists): a few dozen rows at most in practice, so one
|
|
12
|
+
* large page rather than the default 25 that would hide everything after the 25th with no way to reach it. */
|
|
13
|
+
export declare const SETTINGS_LIST_LIMIT = 200;
|
|
11
14
|
/**
|
|
12
15
|
* Builds a `limit`/`page` query string, plus any extra scope/filter params (e.g. `folderUid`,
|
|
13
16
|
* or one of `@rapidmx/restapi`'s generic query-operator values like `startDate=lte(...)`).
|
|
14
17
|
*/
|
|
15
18
|
export declare function buildQuery(params: ListParams, extra?: Record<string, string>): string;
|
|
19
|
+
/**
|
|
20
|
+
* The `sort` query value for `buildQuery()`'s extra params, in the order the keys are given. Paging with no sort leaves the order to the
|
|
21
|
+
* database, which on SQL can repeat or skip a row between two pages and on MongoDB is the oldest first: end the sort with a unique key (`uid`)
|
|
22
|
+
* so every row lands on exactly one page.
|
|
23
|
+
*/
|
|
24
|
+
export declare function sortQuery(sort: Record<string, "ASC" | "DESC">): {
|
|
25
|
+
sort: string;
|
|
26
|
+
};
|
|
16
27
|
/**
|
|
17
28
|
* Optional paging/scope params for the async-request list endpoints (`listExportRequests()`,
|
|
18
29
|
* `listImportRequests()`, `listErasureRequests()`, `listMatterExportRequests()`,
|
|
@@ -9,6 +9,9 @@
|
|
|
9
9
|
* other than it happening to live there first.
|
|
10
10
|
*/
|
|
11
11
|
const DEFAULT_PAGE_SIZE = 25;
|
|
12
|
+
/** The page size of a list that a settings screen shows whole (labels, signatures, filter rules, task lists, contact lists): a few dozen rows at most in practice, so one
|
|
13
|
+
* large page rather than the default 25 that would hide everything after the 25th with no way to reach it. */
|
|
14
|
+
export const SETTINGS_LIST_LIMIT = 200;
|
|
12
15
|
/**
|
|
13
16
|
* Builds a `limit`/`page` query string, plus any extra scope/filter params (e.g. `folderUid`,
|
|
14
17
|
* or one of `@rapidmx/restapi`'s generic query-operator values like `startDate=lte(...)`).
|
|
@@ -20,6 +23,14 @@ export function buildQuery(params, extra = {}) {
|
|
|
20
23
|
}
|
|
21
24
|
return parts.join("&");
|
|
22
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* The `sort` query value for `buildQuery()`'s extra params, in the order the keys are given. Paging with no sort leaves the order to the
|
|
28
|
+
* database, which on SQL can repeat or skip a row between two pages and on MongoDB is the oldest first: end the sort with a unique key (`uid`)
|
|
29
|
+
* so every row lands on exactly one page.
|
|
30
|
+
*/
|
|
31
|
+
export function sortQuery(sort) {
|
|
32
|
+
return { sort: JSON.stringify(sort) };
|
|
33
|
+
}
|
|
23
34
|
/**
|
|
24
35
|
* Builds a `?limit=&page=&matterId=` suffix from only the params actually supplied - unlike
|
|
25
36
|
* `buildQuery()`, applies no default page size (these endpoints historically returned every request
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runs `task` for each of `items`, never more than `limit` of them at once, and resolves to what became of every one in the order of `items`, as
|
|
3
|
+
* `Promise.allSettled()` does - a task that fails does not stop the others. For a batch of requests too big to send all at once (a thousand rows ticked).
|
|
4
|
+
*/
|
|
5
|
+
export declare function settleWithLimit<T, R>(items: readonly T[], limit: number, task: (item: T) => Promise<R>): Promise<PromiseSettledResult<R>[]>;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
/**
|
|
6
|
+
* Runs `task` for each of `items`, never more than `limit` of them at once, and resolves to what became of every one in the order of `items`, as
|
|
7
|
+
* `Promise.allSettled()` does - a task that fails does not stop the others. For a batch of requests too big to send all at once (a thousand rows ticked).
|
|
8
|
+
*/
|
|
9
|
+
export async function settleWithLimit(items, limit, task) {
|
|
10
|
+
const outcomes = new Array(items.length);
|
|
11
|
+
let next = 0;
|
|
12
|
+
async function worker() {
|
|
13
|
+
while (next < items.length) {
|
|
14
|
+
const index = next++;
|
|
15
|
+
try {
|
|
16
|
+
outcomes[index] = { status: "fulfilled", value: await task(items[index]) };
|
|
17
|
+
}
|
|
18
|
+
catch (reason) {
|
|
19
|
+
outcomes[index] = { status: "rejected", reason };
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
|
|
24
|
+
return outcomes;
|
|
25
|
+
}
|
package/lib/admin/auditLogApi.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { ApiClient, withClient } from "../util/api.js";
|
|
13
|
-
import { ListParams, buildQuery } from "../util/apiQuery.js";
|
|
13
|
+
import { ListParams, buildQuery, sortQuery } from "../util/apiQuery.js";
|
|
14
14
|
|
|
15
15
|
export type { ListParams };
|
|
16
16
|
|
|
@@ -44,5 +44,5 @@ export function listAuditLog(filters: AuditLogFilters = {}, params: ListParams =
|
|
|
44
44
|
if (filters.actorUserUid) extra.actorUserUid = filters.actorUserUid;
|
|
45
45
|
if (filters.action) extra.action = filters.action;
|
|
46
46
|
if (filters.targetType) extra.targetType = filters.targetType;
|
|
47
|
-
return withClient(client, `/mail/audit-log?${buildQuery(params, extra)}`);
|
|
47
|
+
return withClient(client, `/mail/audit-log?${buildQuery(params, { ...extra, ...sortQuery({ dateCreated: "DESC", uid: "ASC" }) })}`);
|
|
48
48
|
}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { ApiClient, withClient } from "../util/api.js";
|
|
11
|
-
import { ListParams, buildQuery } from "../util/apiQuery.js";
|
|
11
|
+
import { ListParams, buildQuery, sortQuery } from "../util/apiQuery.js";
|
|
12
12
|
|
|
13
13
|
export type { ListParams };
|
|
14
14
|
|
|
@@ -31,7 +31,7 @@ export interface DistributionList {
|
|
|
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 listDistributionLists(params: ListParams = {}, client?: ApiClient): Promise<DistributionList[]> {
|
|
34
|
-
return withClient(client, `/mail/distribution-lists?${buildQuery(params)}`);
|
|
34
|
+
return withClient(client, `/mail/distribution-lists?${buildQuery(params, sortQuery({ uid: "ASC" }))}`);
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
export function getDistributionList(uid: string, client?: ApiClient): Promise<DistributionList> {
|
package/lib/admin/domainsApi.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { ApiClient, withClient } from "../util/api.js";
|
|
12
|
-
import { ListParams, buildQuery } from "../util/apiQuery.js";
|
|
12
|
+
import { ListParams, buildQuery, sortQuery } from "../util/apiQuery.js";
|
|
13
13
|
|
|
14
14
|
export type { ListParams };
|
|
15
15
|
|
|
@@ -37,7 +37,7 @@ export interface Domain {
|
|
|
37
37
|
* account of a multi-account app) to call instead of the default global `apiFetch()` - see `withClient()`'s
|
|
38
38
|
* own doc comment in `util/api.ts`. Omitted (the default), every function here behaves exactly as before. */
|
|
39
39
|
export function listDomains(params: ListParams = {}, client?: ApiClient): Promise<Domain[]> {
|
|
40
|
-
return withClient(client, `/mail/domains?${buildQuery(params)}`);
|
|
40
|
+
return withClient(client, `/mail/domains?${buildQuery(params, sortQuery({ uid: "ASC" }))}`);
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
export function getDomain(uid: string, client?: ApiClient): Promise<Domain> {
|