@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 { ApiClient,
|
|
12
|
+
import { ApiClient, withClient, withClientRaw } from "../util/api.js";
|
|
13
13
|
import { RequestListParams, buildRequestListQuery } from "../util/apiQuery.js";
|
|
14
14
|
|
|
15
15
|
export type { RequestListParams };
|
|
@@ -51,24 +51,18 @@ export interface UploadMailboxImportInput {
|
|
|
51
51
|
* with `format`/`targetFolderUid`/`mailboxUid` as query-string parameters, not a JSON body — the same
|
|
52
52
|
* shape `mailApi.ts`'s `uploadAttachment()` already establishes for a raw-bytes upload.
|
|
53
53
|
*/
|
|
54
|
-
export
|
|
54
|
+
export function uploadMailboxImport(file: File, input: UploadMailboxImportInput, client?: ApiClient): Promise<MailboxImportRequest> {
|
|
55
55
|
const params = new URLSearchParams({ format: input.format, targetFolderUid: input.targetFolderUid });
|
|
56
56
|
if (input.mailboxUid) {
|
|
57
57
|
params.set("mailboxUid", input.mailboxUid);
|
|
58
58
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const responseBody = contentType.includes("application/json") ? await res.json().catch(() => undefined) : undefined;
|
|
67
|
-
if (!res.ok) {
|
|
68
|
-
const message = (responseBody && (responseBody.message || responseBody.error)) || res.statusText || "Upload failed.";
|
|
69
|
-
throw new ApiRequestError(message, res.status, responseBody?.code);
|
|
70
|
-
}
|
|
71
|
-
return responseBody as MailboxImportRequest;
|
|
59
|
+
return withClientRaw(
|
|
60
|
+
client,
|
|
61
|
+
`/mail/mailbox-import-requests?${params.toString()}`,
|
|
62
|
+
"POST",
|
|
63
|
+
file,
|
|
64
|
+
input.format === "pst" ? "application/vnd.ms-outlook" : "application/mbox",
|
|
65
|
+
);
|
|
72
66
|
}
|
|
73
67
|
|
|
74
68
|
/** `client`, given by every function below that isn't a raw-bytes upload, is an explicit `ApiClient` from
|
|
@@ -27,6 +27,37 @@ export function stripRemoteCssUrls(css: string): string {
|
|
|
27
27
|
.replace(/url\(\s*(["']?)(.*?)\1\s*\)/gi, (match, _quote: string, uri: string) => (EMBEDDED_URI.test(uri) ? match : "none"));
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
/** The candidate URLs of a `srcset` value, parsed as the HTML spec does: a candidate's URL is a run of non-whitespace
|
|
31
|
+
* characters (it may itself contain commas - a `data:` URI always does), so a comma that ends a URL or follows its
|
|
32
|
+
* descriptors separates candidates whether or not whitespace follows it. */
|
|
33
|
+
function srcsetUrls(value: string): string[] {
|
|
34
|
+
const urls: string[] = [];
|
|
35
|
+
let position = 0;
|
|
36
|
+
while (position < value.length) {
|
|
37
|
+
const start = value.slice(position).search(/[^\s,]/);
|
|
38
|
+
if (start < 0) {
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
position += start;
|
|
42
|
+
const end = value.slice(position).search(/\s/);
|
|
43
|
+
const token = end < 0 ? value.slice(position) : value.slice(position, position + end);
|
|
44
|
+
position += token.length;
|
|
45
|
+
urls.push(token.replace(/,+$/, ""));
|
|
46
|
+
if (!token.endsWith(",")) {
|
|
47
|
+
// Skip the descriptors, up to the comma that ends the candidate.
|
|
48
|
+
const comma = value.indexOf(",", position);
|
|
49
|
+
const descriptors = value.slice(position, comma < 0 ? value.length : comma);
|
|
50
|
+
if (descriptors.includes("(")) {
|
|
51
|
+
// The spec's tokenizer ignores commas inside parentheses, so a browser may read the candidates that follow differently from
|
|
52
|
+
// the way this does. No valid descriptor has one: refuse the whole value.
|
|
53
|
+
urls.push(descriptors);
|
|
54
|
+
}
|
|
55
|
+
position = comma < 0 ? value.length : comma + 1;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return urls;
|
|
59
|
+
}
|
|
60
|
+
|
|
30
61
|
type Purifier = ReturnType<typeof DOMPurify>;
|
|
31
62
|
let bodyPurifier: Purifier | undefined;
|
|
32
63
|
|
|
@@ -58,7 +89,7 @@ function getBodyPurifier(): Purifier | undefined {
|
|
|
58
89
|
}
|
|
59
90
|
const isNavigationLink = name === "href" && ["a", "area"].includes(node.nodeName.toLowerCase());
|
|
60
91
|
if (RESOURCE_URI_ATTRIBUTES.has(name) || (name === "href" && !isNavigationLink)) {
|
|
61
|
-
const candidates = name === "srcset" ? data.attrValue
|
|
92
|
+
const candidates = name === "srcset" ? srcsetUrls(data.attrValue) : [data.attrValue];
|
|
62
93
|
if (!candidates.every((candidate) => EMBEDDED_URI.test(candidate))) {
|
|
63
94
|
data.keepAttr = false;
|
|
64
95
|
}
|
|
@@ -106,6 +137,21 @@ const QUOTE_FORBIDDEN_TAGS = [
|
|
|
106
137
|
// svg/math are already in DISPLAY_FORBIDDEN_TAGS, spread above — not repeated here.
|
|
107
138
|
];
|
|
108
139
|
|
|
140
|
+
let signaturePurifier: Purifier | undefined;
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Sanitizes a signature's HTML for the compose body: DOMPurify's defaults (no scripts, handlers or `javascript:` URLs) and
|
|
144
|
+
* the quote's tag list (nothing that styles, frames or submits). Unlike a quoted message, a signature is the sender's own
|
|
145
|
+
* and may carry pictures from the web, so remote images and links stay. Returns `""` when there is no DOM to sanitize with.
|
|
146
|
+
*/
|
|
147
|
+
export function sanitizeSignatureHtml(html: string): string {
|
|
148
|
+
if (typeof window === "undefined") {
|
|
149
|
+
return "";
|
|
150
|
+
}
|
|
151
|
+
signaturePurifier ??= DOMPurify(window);
|
|
152
|
+
return signaturePurifier.isSupported ? signaturePurifier.sanitize(html, { FORBID_TAGS: QUOTE_FORBIDDEN_TAGS }) : "";
|
|
153
|
+
}
|
|
154
|
+
|
|
109
155
|
/**
|
|
110
156
|
* Sanitizes message HTML for quoting into a compose body: the display policy (`sanitizeMessageBodyHtml()`), without
|
|
111
157
|
* the tags in `QUOTE_FORBIDDEN_TAGS`, and without any image that isn't embedded as a `data:` URI - a `cid:` image
|
|
@@ -64,15 +64,24 @@ const VALID_ENTITY_TYPES: ReadonlySet<string> = new Set<SearchEntityType>(["mess
|
|
|
64
64
|
* `parseSearchQuery()` itself, after every extraction has run. */
|
|
65
65
|
function extractFirst(input: string, key: string): { value?: string; rest: string } {
|
|
66
66
|
const pattern = new RegExp(`(^|\\s)${key}:(?:"([^"]*)"|(\\S+))`, "i");
|
|
67
|
-
const
|
|
68
|
-
if (!
|
|
67
|
+
const found = pattern.exec(maskQuoted(input));
|
|
68
|
+
if (!found) {
|
|
69
69
|
return { rest: input };
|
|
70
70
|
}
|
|
71
|
+
// The masked text only says where the operator is - what it holds is read from the real one.
|
|
72
|
+
const match = pattern.exec(input.slice(found.index))!;
|
|
71
73
|
const value = match[2] ?? match[3] ?? "";
|
|
72
|
-
const rest = input.slice(0,
|
|
74
|
+
const rest = input.slice(0, found.index) + match[1] + input.slice(found.index + match[0].length);
|
|
73
75
|
return { value, rest };
|
|
74
76
|
}
|
|
75
77
|
|
|
78
|
+
/** `input` with the inside of every quoted phrase blanked out (the length stays the same, so positions carry over to
|
|
79
|
+
* `input`): a word that looks like an operator inside a phrase - `"meeting from:bob"` - is part of the phrase, never an
|
|
80
|
+
* operator. */
|
|
81
|
+
function maskQuoted(input: string): string {
|
|
82
|
+
return input.replace(/"([^"]*)"/g, (_phrase, inner: string) => `"${"\0".repeat(inner.length)}"`);
|
|
83
|
+
}
|
|
84
|
+
|
|
76
85
|
/** Repeatedly applies `extractFirst()` for `key` until no more occurrences remain, splitting any
|
|
77
86
|
* comma-separated value into individual entries — so `is:read is:flagged` and `is:read,flagged` both
|
|
78
87
|
* produce `["read", "flagged"]`. */
|
|
@@ -98,7 +107,7 @@ function extractAll(input: string, key: string): { values: string[]; rest: strin
|
|
|
98
107
|
* spec defines for this operator — there is no `has:<other value>` form. */
|
|
99
108
|
function extractHasAttachment(input: string): { found: boolean; rest: string } {
|
|
100
109
|
const pattern = /(^|\s)has:attachment\b/i;
|
|
101
|
-
const match = pattern.exec(input);
|
|
110
|
+
const match = pattern.exec(maskQuoted(input));
|
|
102
111
|
if (!match) {
|
|
103
112
|
return { found: false, rest: input };
|
|
104
113
|
}
|
|
@@ -23,7 +23,7 @@ import { evaluateMessageSecurity, type MessageSecurityResult } from "../crypto/m
|
|
|
23
23
|
import { getMessage, getMessageRawContent } from "../mail/mailApi.js";
|
|
24
24
|
import type { ParsedSearchQuery } from "./queryGrammar.js";
|
|
25
25
|
import { SEARCH_FIELD_WEIGHTS } from "./searchScoring.js";
|
|
26
|
-
import { candidates, SearchResult } from "./searchApi.js";
|
|
26
|
+
import { candidates, type CandidateResultPage, SearchResult } from "./searchApi.js";
|
|
27
27
|
|
|
28
28
|
/** Longest decrypted HTML body `stripHtml()` examines, in UTF-16 code units - content past it is ignored
|
|
29
29
|
* for Tier 3 matching. A hostile sender controls this input entirely, so it is bounded on top of the
|
|
@@ -271,6 +271,11 @@ function hasAnyFilter(parsed: ParsedSearchQuery): boolean {
|
|
|
271
271
|
* concurrent WebCrypto work a large candidate page would otherwise fire all at once. */
|
|
272
272
|
export const TIER3_DECRYPT_CONCURRENCY = 4;
|
|
273
273
|
|
|
274
|
+
/** The most candidate pages one search walks (`nextCursor` onwards) while it is still short of `limit` results - a
|
|
275
|
+
* search that matches little must not download and decrypt a whole mailbox, but an encrypted message beyond the first
|
|
276
|
+
* page of candidates is still found. */
|
|
277
|
+
export const TIER3_MAX_CANDIDATE_PAGES = 4;
|
|
278
|
+
|
|
274
279
|
/** `Promise.allSettled()` over `items`, but never running more than `concurrency` `task`s at a time.
|
|
275
280
|
* Outcomes are returned in `items`' own order. */
|
|
276
281
|
async function settleWithConcurrency<T, R>(
|
|
@@ -301,7 +306,8 @@ export interface SearchEncryptedCandidatesOptions {
|
|
|
301
306
|
}
|
|
302
307
|
|
|
303
308
|
/**
|
|
304
|
-
* Runs Tier 3 for one parsed query: fetches a bounded candidate set from the server
|
|
309
|
+
* Runs Tier 3 for one parsed query: fetches a bounded candidate set from the server (following `nextCursor` for up to
|
|
310
|
+
* `TIER3_MAX_CANDIDATE_PAGES` pages while fewer than `limit` messages have matched), decrypts each
|
|
305
311
|
* candidate this device can open (at most `TIER3_DECRYPT_CONCURRENCY` at a time), and returns only the
|
|
306
312
|
* ones whose real (decrypted) content actually matches - each with a raw (not yet normalized) score the
|
|
307
313
|
* caller should run through `searchScoring.ts#normalizeServerScores()` alongside Tier 1's own scores
|
|
@@ -330,41 +336,68 @@ export async function searchEncryptedCandidates(
|
|
|
330
336
|
|
|
331
337
|
const participants = [parsed.from, parsed.to, parsed.cc].filter((value): value is string => Boolean(value));
|
|
332
338
|
|
|
333
|
-
const
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
339
|
+
const results: SearchResult[] = [];
|
|
340
|
+
let cursor: string | undefined;
|
|
341
|
+
for (let pageNumber = 0; pageNumber < TIER3_MAX_CANDIDATE_PAGES; pageNumber++) {
|
|
342
|
+
let page: CandidateResultPage;
|
|
343
|
+
try {
|
|
344
|
+
page = await candidates({
|
|
345
|
+
types: ["message"],
|
|
346
|
+
participants: participants.length > 0 ? participants : undefined,
|
|
347
|
+
before: parsed.before,
|
|
348
|
+
after: parsed.after,
|
|
349
|
+
folderUid: parsed.folderUid,
|
|
350
|
+
flags: parsed.flags,
|
|
351
|
+
labels: parsed.labels,
|
|
352
|
+
limit,
|
|
353
|
+
cursor,
|
|
354
|
+
mailboxUid: options.mailboxUid,
|
|
355
|
+
});
|
|
356
|
+
} catch (err) {
|
|
357
|
+
if (pageNumber === 0) {
|
|
358
|
+
throw err;
|
|
359
|
+
}
|
|
360
|
+
// A later page failing keeps what the earlier ones found.
|
|
361
|
+
break;
|
|
362
|
+
}
|
|
344
363
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
364
|
+
const settled = await settleWithConcurrency(
|
|
365
|
+
page.candidates.filter((candidate) => candidate.entityType === "message"),
|
|
366
|
+
TIER3_DECRYPT_CONCURRENCY,
|
|
367
|
+
async (candidate) => {
|
|
368
|
+
if (parsed.hasAttachment !== undefined) {
|
|
369
|
+
const message = await getMessage(candidate.entityUid);
|
|
370
|
+
if (Boolean(message.hasAttachments) !== parsed.hasAttachment) {
|
|
371
|
+
return undefined;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
const rawMime = await getMessageRawContent(candidate.entityUid);
|
|
375
|
+
if (unlocked.destroyed) {
|
|
352
376
|
return undefined;
|
|
353
377
|
}
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
}
|
|
359
|
-
const security: MessageSecurityResult = await evaluateMessageSecurity(rawMime, unlocked);
|
|
360
|
-
return { entityUid: candidate.entityUid, security };
|
|
361
|
-
},
|
|
362
|
-
);
|
|
378
|
+
const security: MessageSecurityResult = await evaluateMessageSecurity(rawMime, unlocked);
|
|
379
|
+
return { entityUid: candidate.entityUid, security };
|
|
380
|
+
},
|
|
381
|
+
);
|
|
363
382
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
383
|
+
if (unlocked.destroyed) {
|
|
384
|
+
// Locked while candidates were being fetched/decrypted - don't surface decrypted content after a lock.
|
|
385
|
+
return [];
|
|
386
|
+
}
|
|
387
|
+
results.push(...matchDecrypted(parsed, settled));
|
|
388
|
+
cursor = page.nextCursor;
|
|
389
|
+
if (!cursor || results.length >= limit) {
|
|
390
|
+
break;
|
|
391
|
+
}
|
|
367
392
|
}
|
|
393
|
+
return results;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/** The decrypted candidates of one page whose real content matches `parsed`, scored and with their snippets. */
|
|
397
|
+
function matchDecrypted(
|
|
398
|
+
parsed: ParsedSearchQuery,
|
|
399
|
+
settled: PromiseSettledResult<{ entityUid: string; security: MessageSecurityResult } | undefined>[],
|
|
400
|
+
): SearchResult[] {
|
|
368
401
|
const subjectFilter = parsed.subject?.toLowerCase();
|
|
369
402
|
const results: SearchResult[] = [];
|
|
370
403
|
for (const outcome of settled) {
|
package/lib/tasks/tasksApi.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* comment for the shared ACL/authorization model every wrapper file here follows. */
|
|
7
7
|
|
|
8
8
|
import { ApiClient, withClient } from "../util/api.js";
|
|
9
|
-
import { ListParams, buildQuery } from "../util/apiQuery.js";
|
|
9
|
+
import { ListParams, SETTINGS_LIST_LIMIT, buildQuery } from "../util/apiQuery.js";
|
|
10
10
|
|
|
11
11
|
export type TaskPriority = "low" | "normal" | "high";
|
|
12
12
|
|
|
@@ -39,7 +39,7 @@ export interface Task {
|
|
|
39
39
|
|
|
40
40
|
/** Lists a folder's tasks, soonest due date first (tasks with no due date sort last). */
|
|
41
41
|
export function listTasks(folderUid: string, params: ListParams = {}, client?: ApiClient): Promise<Task[]> {
|
|
42
|
-
return withClient(client, `/mail/tasks?${buildQuery(params, { folderUid, sort: JSON.stringify({ dueDate: "ASC" }) })}`);
|
|
42
|
+
return withClient(client, `/mail/tasks?${buildQuery(params, { folderUid, sort: JSON.stringify({ dueDate: "ASC", uid: "ASC" }) })}`);
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
export interface CreateTaskInput {
|
|
@@ -109,7 +109,7 @@ export interface TaskList {
|
|
|
109
109
|
|
|
110
110
|
/** Lists a mailbox's task lists (Outlook To-Do-style custom lists), alphabetically by name. */
|
|
111
111
|
export function listTaskLists(mailboxUid: string, params: ListParams = {}, client?: ApiClient): Promise<TaskList[]> {
|
|
112
|
-
return withClient(client, `/mail/task-lists?${buildQuery(params, { mailboxUid, sort: JSON.stringify({ name: "ASC" }) })}`);
|
|
112
|
+
return withClient(client, `/mail/task-lists?${buildQuery({ limit: SETTINGS_LIST_LIMIT, ...params }, { mailboxUid, sort: JSON.stringify({ name: "ASC", uid: "ASC" }) })}`);
|
|
113
113
|
}
|
|
114
114
|
|
|
115
115
|
export function createTaskList(input: { mailboxUid: string; name: string }, client?: ApiClient): Promise<TaskList> {
|
package/lib/util/api.ts
CHANGED
|
@@ -157,12 +157,18 @@ export async function apiFetch<T = unknown>(path: string, init: RequestInit = {}
|
|
|
157
157
|
applyCsrfHeader(headers, init.method);
|
|
158
158
|
const credentials = apiBaseUrl ? "include" : init.credentials;
|
|
159
159
|
|
|
160
|
-
|
|
161
|
-
|
|
160
|
+
const res = await fetchWithRecovery(path, { ...init, headers, credentials });
|
|
161
|
+
return decodeApiResponse<T>(res, (error) => unauthorizedObserver?.(error));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** `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
|
|
165
|
+
* time) it renews the session, and asks again once. */
|
|
166
|
+
async function fetchWithRecovery(path: string, init: RequestInit): Promise<Response> {
|
|
167
|
+
let res = await fetch(apiUrl(path), init);
|
|
162
168
|
if (res.status === 401 && sessionRecovery && (await recoverSession())) {
|
|
163
|
-
res = await fetch(apiUrl(path),
|
|
169
|
+
res = await fetch(apiUrl(path), init);
|
|
164
170
|
}
|
|
165
|
-
return
|
|
171
|
+
return res;
|
|
166
172
|
}
|
|
167
173
|
|
|
168
174
|
let sessionRecovery: (() => Promise<boolean>) | undefined;
|
|
@@ -196,15 +202,16 @@ async function recoverSession(): Promise<boolean> {
|
|
|
196
202
|
* a deployment-level requirement owned by auth-server/the Helm chart, not configured here — and auth-server's
|
|
197
203
|
* CORS config must explicitly allow this app's origin with credentials.
|
|
198
204
|
*
|
|
199
|
-
* `
|
|
200
|
-
*
|
|
201
|
-
*
|
|
202
|
-
*
|
|
205
|
+
* No `x-csrf-token` header is sent, deliberately. A double-submit token is only meaningful to the server that issued it, and the `csrf`
|
|
206
|
+
* cookie this app's JavaScript can read is *this* host's own (`mail.example.com`), never auth-server's - so echoing it
|
|
207
|
+
* would be a wrong token for the wrong server. Worse, auth-server's CORS rules do not allow that header, so the browser
|
|
208
|
+
* blocked the preflight of every such call (the silent session refresh, sign-out) before it ever reached the server: "Request header
|
|
209
|
+
* field x-csrf-token is not allowed by Access-Control-Allow-Headers". Auth-server's CSRF protection for this cross-origin call shape is
|
|
210
|
+
* its Origin allow-list check, which needs no header.
|
|
203
211
|
*/
|
|
204
212
|
export async function authApiFetch<T = unknown>(authServerUrl: string, path: string, init: RequestInit = {}): Promise<T> {
|
|
205
213
|
const headers = new Headers(init.headers);
|
|
206
214
|
headers.set("Content-Type", "application/json");
|
|
207
|
-
applyCsrfHeader(headers, init.method);
|
|
208
215
|
|
|
209
216
|
const res = await fetch(`${authServerUrl}/api${path}`, { ...init, headers, credentials: "include" });
|
|
210
217
|
return decodeApiResponse<T>(res);
|
|
@@ -228,13 +235,19 @@ export function setApiUnauthorizedObserver(observer: ((error: ApiRequestError) =
|
|
|
228
235
|
* `createApiClient()`-made client's `fetch()` (which forwards to that instance's own
|
|
229
236
|
* `setUnauthorizedObserver()`) pass their own. A throwing `onUnauthorized` never changes what the caller sees.
|
|
230
237
|
*/
|
|
231
|
-
async function decodeApiResponse<T>(
|
|
238
|
+
async function decodeApiResponse<T>(
|
|
239
|
+
res: Response,
|
|
240
|
+
onUnauthorized?: (error: ApiRequestError) => void,
|
|
241
|
+
failureMessage: string = "Request failed.",
|
|
242
|
+
): Promise<T> {
|
|
232
243
|
const contentType = res.headers.get("content-type") ?? "";
|
|
233
244
|
const body = contentType.includes("application/json") ? await res.json().catch(() => undefined) : undefined;
|
|
234
245
|
|
|
235
246
|
if (!res.ok) {
|
|
236
|
-
|
|
237
|
-
const
|
|
247
|
+
// A body is untrusted: only a non-empty string is a message (anything else would read "[object Object]"), only a string a code.
|
|
248
|
+
const reported = body && (body.message || body.error);
|
|
249
|
+
const message = (typeof reported === "string" && reported) || res.statusText || failureMessage;
|
|
250
|
+
const error = new ApiRequestError(message, res.status, typeof body?.code === "string" ? body.code : undefined, body);
|
|
238
251
|
if (res.status === 401 && onUnauthorized) {
|
|
239
252
|
try {
|
|
240
253
|
onUnauthorized(error);
|
|
@@ -259,6 +272,12 @@ export interface ApiClient {
|
|
|
259
272
|
/** Same contract as `apiFetch()`: prefixes `path` with `/api`, parses a JSON response, and rejects with
|
|
260
273
|
* `ApiRequestError` on a non-ok response. */
|
|
261
274
|
fetch<T = unknown>(path: string, init?: RequestInit): Promise<T>;
|
|
275
|
+
/**
|
|
276
|
+
* Like `fetch()` - same origin, authentication and error decoding - but resolves the response's own bytes instead of parsing JSON, for a picture or other
|
|
277
|
+
* 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
|
|
278
|
+
* `blob:` URL. No `Content-Type` is added.
|
|
279
|
+
*/
|
|
280
|
+
fetchBlob(path: string, init?: RequestInit): Promise<Blob>;
|
|
262
281
|
/**
|
|
263
282
|
* This client instance's own `setApiUnauthorizedObserver()` equivalent - see `createApiClient()`'s doc
|
|
264
283
|
* comment for why it is per-instance rather than shared with the module-level `setApiUnauthorizedObserver()`
|
|
@@ -308,18 +327,34 @@ export function createApiClient(options: CreateApiClientOptions): ApiClient {
|
|
|
308
327
|
const { getAccessToken } = options;
|
|
309
328
|
let unauthorizedObserver: ((error: ApiRequestError) => void) | undefined;
|
|
310
329
|
|
|
311
|
-
async function
|
|
330
|
+
async function send(path: string, init: RequestInit, json: boolean): Promise<Response> {
|
|
312
331
|
const headers = new Headers(init.headers);
|
|
313
|
-
|
|
332
|
+
// JSON unless the caller sent a body of its own kind (an upload names the file's type).
|
|
333
|
+
if (json && !headers.has("Content-Type")) {
|
|
334
|
+
headers.set("Content-Type", "application/json");
|
|
335
|
+
}
|
|
314
336
|
const token = await getAccessToken();
|
|
315
337
|
headers.set("Authorization", `jwt ${token}`);
|
|
316
338
|
|
|
317
|
-
|
|
318
|
-
|
|
339
|
+
return fetch(`${baseUrl}/api${path}`, { ...init, headers });
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
async function clientFetch<T = unknown>(path: string, init: RequestInit = {}): Promise<T> {
|
|
343
|
+
return decodeApiResponse<T>(await send(path, init, true), (error) => unauthorizedObserver?.(error));
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
async function clientFetchBlob(path: string, init: RequestInit = {}): Promise<Blob> {
|
|
347
|
+
const res = await send(path, init, false);
|
|
348
|
+
if (!res.ok) {
|
|
349
|
+
// Throws the same `ApiRequestError` (and tells the observer of a 401) as every other request.
|
|
350
|
+
await decodeApiResponse<never>(res, (error) => unauthorizedObserver?.(error));
|
|
351
|
+
}
|
|
352
|
+
return res.blob();
|
|
319
353
|
}
|
|
320
354
|
|
|
321
355
|
return {
|
|
322
356
|
fetch: clientFetch,
|
|
357
|
+
fetchBlob: clientFetchBlob,
|
|
323
358
|
setUnauthorizedObserver(observer) {
|
|
324
359
|
unauthorizedObserver = observer;
|
|
325
360
|
},
|
|
@@ -339,3 +374,51 @@ export function createApiClient(options: CreateApiClientOptions): ApiClient {
|
|
|
339
374
|
export function withClient<T = unknown>(client: ApiClient | undefined, path: string, init?: RequestInit): Promise<T> {
|
|
340
375
|
return client ? client.fetch<T>(path, init) : apiFetch<T>(path, init);
|
|
341
376
|
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* 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
|
|
380
|
+
* 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
|
|
381
|
+
* 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
|
|
382
|
+
* an `ApiRequestError` (`failureMessage` when the server gave no message) on a non-ok one.
|
|
383
|
+
*/
|
|
384
|
+
export async function withClientRaw<T = unknown>(
|
|
385
|
+
client: ApiClient | undefined,
|
|
386
|
+
path: string,
|
|
387
|
+
method: string,
|
|
388
|
+
file: Blob,
|
|
389
|
+
contentType: string = file.type,
|
|
390
|
+
failureMessage: string = "Upload failed.",
|
|
391
|
+
): Promise<T> {
|
|
392
|
+
if (client) {
|
|
393
|
+
return client.fetch<T>(path, { method, headers: { "Content-Type": contentType }, body: file });
|
|
394
|
+
}
|
|
395
|
+
const res = await fetchWithRecovery(path, {
|
|
396
|
+
method,
|
|
397
|
+
credentials: "include",
|
|
398
|
+
headers: withCsrfHeader({ "Content-Type": contentType }, method),
|
|
399
|
+
body: file,
|
|
400
|
+
});
|
|
401
|
+
return decodeApiResponse<T>(res, (error) => unauthorizedObserver?.(error), failureMessage);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Fetches `path`'s own bytes (a message's raw source, say) rather than JSON - the download counterpart of `withClient()`. With an explicit `client` the
|
|
406
|
+
* 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
|
|
407
|
+
* with the `jwt` cookie, renewing the session and asking again once on a `401`, as `apiFetch()` does. Rejects with an `ApiRequestError`
|
|
408
|
+
* (`failureMessage` when the server gave no message) on a non-ok response.
|
|
409
|
+
*/
|
|
410
|
+
export async function withClientBlob(
|
|
411
|
+
client: ApiClient | undefined,
|
|
412
|
+
path: string,
|
|
413
|
+
failureMessage: string = "Download failed.",
|
|
414
|
+
): Promise<Blob> {
|
|
415
|
+
if (client) {
|
|
416
|
+
return client.fetchBlob(path);
|
|
417
|
+
}
|
|
418
|
+
const res = await fetchWithRecovery(path, { credentials: "include" });
|
|
419
|
+
if (!res.ok) {
|
|
420
|
+
// Throws the same `ApiRequestError` (and tells the observer of a 401) as every other request.
|
|
421
|
+
await decodeApiResponse<never>(res, (error) => unauthorizedObserver?.(error), failureMessage);
|
|
422
|
+
}
|
|
423
|
+
return res.blob();
|
|
424
|
+
}
|
package/lib/util/apiQuery.ts
CHANGED
|
@@ -16,6 +16,10 @@ export interface ListParams {
|
|
|
16
16
|
|
|
17
17
|
const DEFAULT_PAGE_SIZE = 25;
|
|
18
18
|
|
|
19
|
+
/** 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
|
|
20
|
+
* large page rather than the default 25 that would hide everything after the 25th with no way to reach it. */
|
|
21
|
+
export const SETTINGS_LIST_LIMIT = 200;
|
|
22
|
+
|
|
19
23
|
/**
|
|
20
24
|
* Builds a `limit`/`page` query string, plus any extra scope/filter params (e.g. `folderUid`,
|
|
21
25
|
* or one of `@rapidmx/restapi`'s generic query-operator values like `startDate=lte(...)`).
|
|
@@ -28,6 +32,15 @@ export function buildQuery(params: ListParams, extra: Record<string, string> = {
|
|
|
28
32
|
return parts.join("&");
|
|
29
33
|
}
|
|
30
34
|
|
|
35
|
+
/**
|
|
36
|
+
* 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
|
|
37
|
+
* 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`)
|
|
38
|
+
* so every row lands on exactly one page.
|
|
39
|
+
*/
|
|
40
|
+
export function sortQuery(sort: Record<string, "ASC" | "DESC">): { sort: string } {
|
|
41
|
+
return { sort: JSON.stringify(sort) };
|
|
42
|
+
}
|
|
43
|
+
|
|
31
44
|
/**
|
|
32
45
|
* Optional paging/scope params for the async-request list endpoints (`listExportRequests()`,
|
|
33
46
|
* `listImportRequests()`, `listErasureRequests()`, `listMatterExportRequests()`,
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 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
|
|
8
|
+
* `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).
|
|
9
|
+
*/
|
|
10
|
+
export async function settleWithLimit<T, R>(items: readonly T[], limit: number, task: (item: T) => Promise<R>): Promise<PromiseSettledResult<R>[]> {
|
|
11
|
+
const outcomes = new Array<PromiseSettledResult<R>>(items.length);
|
|
12
|
+
let next = 0;
|
|
13
|
+
async function worker() {
|
|
14
|
+
while (next < items.length) {
|
|
15
|
+
const index = next++;
|
|
16
|
+
try {
|
|
17
|
+
outcomes[index] = { status: "fulfilled", value: await task(items[index]) };
|
|
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/package.json
CHANGED