@rapidmx/web-client 0.10.0 → 0.12.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/README.md +183 -2
- package/apps/admin/_layout.tsx +8 -2
- package/apps/admin/index.tsx +7 -1
- package/apps/admin/ingest-queue/index.tsx +2 -1
- package/apps/admin/mailboxes/[uid].tsx +23 -10
- package/apps/admin/mailboxes/new/index.tsx +3 -3
- package/apps/admin/quarantine/index.tsx +2 -1
- package/apps/admin/signing-certificates/index.tsx +343 -0
- package/apps/escrow/_layout.tsx +8 -2
- package/apps/shared/appearance/AppearanceForm.tsx +392 -0
- package/apps/shared/appearance/AppearanceHead.tsx +49 -0
- package/apps/shared/appearance/AppearanceProvider.tsx +470 -0
- package/apps/shared/appearance/ColorField.tsx +107 -0
- package/apps/shared/appearance/appearanceCache.ts +106 -0
- package/apps/shared/appearance/appearanceContext.ts +77 -0
- package/apps/shared/appearance/bootScript.ts +35 -0
- package/apps/shared/appearance/color.ts +169 -0
- package/apps/shared/appearance/photo.ts +88 -0
- package/apps/shared/appearance/resolvedTheme.ts +67 -0
- package/apps/shared/appearance/theme.ts +355 -0
- package/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.tsx +4 -4
- package/apps/shared/components/admin/layout/AdminShell.tsx +22 -10
- package/apps/shared/components/admin/mailboxes/ShareAccessCard.tsx +107 -60
- package/apps/shared/components/admin/settings/BrandingForm.tsx +24 -0
- package/apps/shared/components/admin/settings/DomainDnsSetup.tsx +60 -14
- package/apps/shared/components/admin/settings/MailboxCreateForm.tsx +65 -16
- package/apps/shared/components/admin/settings/PluginsManager.tsx +228 -25
- package/apps/shared/components/admin/setup/SetupWizard.tsx +2 -1
- package/apps/shared/components/calendar/CalendarListSidebar.tsx +4 -5
- package/apps/shared/components/contacts/ContactForm.tsx +11 -5
- package/apps/shared/components/contacts/ContactsSidebar.tsx +5 -3
- package/apps/shared/components/contacts/ContactsToolbar.tsx +25 -52
- package/apps/shared/components/contacts/layout/ContactsShell.tsx +10 -5
- package/apps/shared/components/escrow/layout/EscrowShell.tsx +13 -6
- package/apps/shared/components/layout/AppShell.tsx +98 -32
- package/apps/shared/components/layout/BrandingChrome.tsx +200 -13
- package/apps/shared/components/layout/RailIcon.tsx +100 -0
- package/apps/shared/components/layout/ResponsiveToolbar.tsx +315 -0
- package/apps/shared/components/layout/ThemeSwitch.tsx +84 -0
- package/apps/shared/components/layout/UserMenu.tsx +75 -6
- package/apps/shared/components/mail/ConversationList.tsx +4 -2
- package/apps/shared/components/mail/ConversationThreadPane.tsx +487 -475
- package/apps/shared/components/mail/LazyReadingPane.tsx +14 -16
- package/apps/shared/components/mail/MailSelectionBar.tsx +2 -10
- package/apps/shared/components/mail/MessageDetailPane.tsx +366 -287
- package/apps/shared/components/mail/OutboxBadge.tsx +44 -0
- package/apps/shared/components/mail/OutboxRowStatus.tsx +31 -0
- package/apps/shared/components/mail/compose/ComposeContext.tsx +35 -2
- package/apps/shared/components/mail/compose/ComposeWindow.tsx +239 -437
- package/apps/shared/components/mail/compose/encryptionRequirement.ts +86 -0
- package/apps/shared/components/mail/layout/MailShell.tsx +39 -25
- package/apps/shared/components/mail/reading/EncryptedBody.tsx +65 -0
- package/apps/shared/components/mail/reading/EncryptedPreview.tsx +35 -0
- package/apps/shared/components/mail/reading/MessageBody.tsx +240 -0
- package/apps/shared/components/mail/reading/MessageCard.tsx +174 -0
- package/apps/shared/components/mail/reading/bodyContent.ts +100 -0
- package/apps/shared/components/mail/reading/bodyHtml.ts +298 -0
- package/apps/shared/components/mail/reading/color.ts +245 -0
- package/apps/shared/components/mail/reading/frameControl.ts +142 -0
- package/apps/shared/components/mail/reading/frameDocument.ts +123 -0
- package/apps/shared/components/mail/reading/safeDocument.ts +24 -0
- package/apps/shared/components/mail/reading/themeAdaptation.ts +221 -0
- package/apps/shared/components/mail/reading/themeSurface.ts +90 -0
- package/apps/shared/components/mail/reading/viewOriginal.ts +38 -0
- package/apps/shared/components/settings/SigningCertificateCard.tsx +344 -0
- package/apps/shared/components/settings/layout/SettingsShell.tsx +1 -0
- package/apps/shared/components/sharing/PrincipalListField.tsx +82 -0
- package/apps/shared/components/sharing/PrincipalPicker.tsx +76 -0
- package/apps/shared/components/sharing/PrincipalResolver.tsx +164 -0
- package/apps/shared/components/tasks/TasksSidebar.tsx +5 -3
- package/apps/shared/components/tasks/layout/TasksShell.tsx +9 -5
- package/apps/shared/mail/folderCounts.ts +9 -3
- package/apps/shared/mail/folderTree.ts +143 -0
- package/apps/shared/mail/outbox/UnlockBridge.tsx +23 -0
- package/apps/shared/mail/outbox/composeBridge.ts +69 -0
- package/apps/shared/mail/outbox/outboxState.ts +88 -0
- package/apps/shared/mail/outbox/pendingSends.ts +146 -0
- package/apps/shared/mail/outbox/sendDecision.ts +157 -0
- package/apps/shared/mail/outbox/sendJob.ts +427 -0
- package/apps/shared/mail/outbox/sendOutcomes.ts +146 -0
- package/apps/shared/mail/outbox/sendState.ts +35 -0
- package/apps/shared/mail/outbox/useOutboxStatus.ts +61 -0
- package/apps/shared/mail/useMailConnection.ts +81 -28
- package/apps/shared/mail/useMailLiveUpdates.ts +55 -10
- package/apps/shared/mail/useNewMailNotifications.ts +47 -32
- package/apps/shared/navigation/AppRouter.tsx +2 -2
- package/apps/shared/notifications/NotificationCenter.tsx +215 -0
- package/apps/shared/notifications/NotificationHistoryDialog.tsx +90 -0
- package/apps/shared/notifications/apiErrors.ts +98 -0
- package/apps/shared/notifications/headerOffset.ts +35 -0
- package/apps/shared/notifications/pushStatus.ts +56 -0
- package/apps/shared/notifications/store.ts +549 -0
- package/apps/shared/notifications/systemErrors.ts +63 -0
- package/apps/shared/notifications/useNotifications.ts +41 -0
- package/apps/shared/signing/enrollmentStorage.ts +33 -0
- package/apps/shared/signing/enrollmentTracker.ts +385 -0
- package/apps/shared/signing/enrollmentView.ts +251 -0
- package/apps/shared/signing/signingInfo.ts +58 -0
- package/apps/shared/signing/useNow.ts +19 -0
- package/apps/shared/signing/useSigningEnrollmentWatcher.ts +90 -0
- package/apps/shared/styles/app.css +80 -0
- package/apps/www/_layout.tsx +8 -2
- package/apps/www/_routes.ts +1 -0
- package/apps/www/calendar/index.tsx +9 -14
- package/apps/www/contacts/[uid].tsx +5 -8
- package/apps/www/contacts/index.tsx +587 -584
- package/apps/www/index.tsx +38 -27
- package/apps/www/messages/[uid].tsx +3 -1
- package/apps/www/settings/appearance/index.tsx +21 -0
- package/apps/www/settings/auto-reply/index.tsx +3 -7
- package/apps/www/settings/encryption/index.tsx +151 -116
- package/apps/www/settings/filters/[uid].tsx +3 -1
- package/apps/www/settings/filters/new/index.tsx +3 -1
- package/apps/www/settings/privacy/index.tsx +3 -8
- package/apps/www/settings/read-receipts/index.tsx +3 -7
- package/apps/www/settings/sharing/index.tsx +44 -65
- package/apps/www/settings/signatures/[uid].tsx +3 -1
- package/apps/www/settings/signatures/new/index.tsx +3 -2
- package/apps/www/tasks/index.tsx +11 -16
- package/dist/apps/admin/_layout.d.ts +5 -1
- package/dist/apps/admin/_layout.js +3 -2
- package/dist/apps/admin/index.js +3 -2
- package/dist/apps/admin/ingest-queue/index.js +2 -1
- package/dist/apps/admin/mailboxes/[uid].js +9 -6
- package/dist/apps/admin/mailboxes/new/index.js +1 -1
- package/dist/apps/admin/quarantine/index.js +2 -1
- package/dist/apps/admin/signing-certificates/index.d.ts +3 -0
- package/dist/apps/admin/signing-certificates/index.js +161 -0
- package/dist/apps/escrow/_layout.d.ts +5 -1
- package/dist/apps/escrow/_layout.js +3 -2
- package/dist/apps/shared/appearance/AppearanceForm.d.ts +7 -0
- package/dist/apps/shared/appearance/AppearanceForm.js +131 -0
- package/dist/apps/shared/appearance/AppearanceHead.d.ts +15 -0
- package/dist/apps/shared/appearance/AppearanceHead.js +31 -0
- package/dist/apps/shared/appearance/AppearanceProvider.d.ts +32 -0
- package/dist/apps/shared/appearance/AppearanceProvider.js +390 -0
- package/dist/apps/shared/appearance/ColorField.d.ts +22 -0
- package/dist/apps/shared/appearance/ColorField.js +40 -0
- package/dist/apps/shared/appearance/appearanceCache.d.ts +45 -0
- package/dist/apps/shared/appearance/appearanceCache.js +80 -0
- package/dist/apps/shared/appearance/appearanceContext.d.ts +45 -0
- package/dist/apps/shared/appearance/appearanceContext.js +29 -0
- package/dist/apps/shared/appearance/bootScript.d.ts +16 -0
- package/dist/apps/shared/appearance/bootScript.js +33 -0
- package/dist/apps/shared/appearance/color.d.ts +65 -0
- package/dist/apps/shared/appearance/color.js +132 -0
- package/dist/apps/shared/appearance/photo.d.ts +17 -0
- package/dist/apps/shared/appearance/photo.js +79 -0
- package/dist/apps/shared/appearance/resolvedTheme.d.ts +21 -0
- package/dist/apps/shared/appearance/resolvedTheme.js +55 -0
- package/dist/apps/shared/appearance/theme.d.ts +111 -0
- package/dist/apps/shared/appearance/theme.js +251 -0
- package/dist/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.js +3 -2
- package/dist/apps/shared/components/admin/layout/AdminShell.d.ts +1 -1
- package/dist/apps/shared/components/admin/layout/AdminShell.js +16 -6
- package/dist/apps/shared/components/admin/mailboxes/ShareAccessCard.d.ts +10 -5
- package/dist/apps/shared/components/admin/mailboxes/ShareAccessCard.js +49 -34
- package/dist/apps/shared/components/admin/settings/BrandingForm.d.ts +2 -0
- package/dist/apps/shared/components/admin/settings/BrandingForm.js +7 -1
- package/dist/apps/shared/components/admin/settings/DomainDnsSetup.js +25 -5
- package/dist/apps/shared/components/admin/settings/MailboxCreateForm.js +18 -7
- package/dist/apps/shared/components/admin/settings/PluginsManager.js +106 -20
- package/dist/apps/shared/components/admin/setup/SetupWizard.js +2 -1
- package/dist/apps/shared/components/calendar/CalendarListSidebar.js +8 -8
- package/dist/apps/shared/components/contacts/ContactForm.js +1 -1
- package/dist/apps/shared/components/contacts/ContactsSidebar.js +4 -2
- package/dist/apps/shared/components/contacts/ContactsToolbar.d.ts +3 -1
- package/dist/apps/shared/components/contacts/ContactsToolbar.js +24 -9
- package/dist/apps/shared/components/contacts/layout/ContactsShell.js +4 -1
- package/dist/apps/shared/components/escrow/layout/EscrowShell.js +7 -4
- package/dist/apps/shared/components/layout/AppShell.d.ts +6 -1
- package/dist/apps/shared/components/layout/AppShell.js +59 -13
- package/dist/apps/shared/components/layout/BrandingChrome.d.ts +69 -10
- package/dist/apps/shared/components/layout/BrandingChrome.js +140 -12
- package/dist/apps/shared/components/layout/RailIcon.d.ts +21 -0
- package/dist/apps/shared/components/layout/RailIcon.js +85 -0
- package/dist/apps/shared/components/layout/ResponsiveToolbar.d.ts +55 -0
- package/dist/apps/shared/components/layout/ResponsiveToolbar.js +199 -0
- package/dist/apps/shared/components/layout/ThemeSwitch.d.ts +9 -0
- package/dist/apps/shared/components/layout/ThemeSwitch.js +54 -0
- package/dist/apps/shared/components/layout/UserMenu.d.ts +7 -1
- package/dist/apps/shared/components/layout/UserMenu.js +41 -7
- package/dist/apps/shared/components/mail/ConversationList.js +3 -2
- package/dist/apps/shared/components/mail/ConversationThreadPane.d.ts +2 -1
- package/dist/apps/shared/components/mail/ConversationThreadPane.js +42 -20
- package/dist/apps/shared/components/mail/LazyReadingPane.js +9 -8
- package/dist/apps/shared/components/mail/MailSelectionBar.d.ts +3 -4
- package/dist/apps/shared/components/mail/MailSelectionBar.js +2 -3
- package/dist/apps/shared/components/mail/MessageDetailPane.d.ts +18 -7
- package/dist/apps/shared/components/mail/MessageDetailPane.js +86 -41
- package/dist/apps/shared/components/mail/OutboxBadge.d.ts +17 -0
- package/dist/apps/shared/components/mail/OutboxBadge.js +18 -0
- package/dist/apps/shared/components/mail/OutboxRowStatus.d.ts +10 -0
- package/dist/apps/shared/components/mail/OutboxRowStatus.js +19 -0
- package/dist/apps/shared/components/mail/compose/ComposeContext.d.ts +8 -0
- package/dist/apps/shared/components/mail/compose/ComposeContext.js +28 -3
- package/dist/apps/shared/components/mail/compose/ComposeWindow.d.ts +2 -1
- package/dist/apps/shared/components/mail/compose/ComposeWindow.js +229 -328
- package/dist/apps/shared/components/mail/compose/encryptionRequirement.d.ts +49 -0
- package/dist/apps/shared/components/mail/compose/encryptionRequirement.js +35 -0
- package/dist/apps/shared/components/mail/layout/MailShell.d.ts +2 -0
- package/dist/apps/shared/components/mail/layout/MailShell.js +20 -14
- package/dist/apps/shared/components/mail/reading/EncryptedBody.d.ts +21 -0
- package/dist/apps/shared/components/mail/reading/EncryptedBody.js +22 -0
- package/dist/apps/shared/components/mail/reading/EncryptedPreview.d.ts +18 -0
- package/dist/apps/shared/components/mail/reading/EncryptedPreview.js +22 -0
- package/dist/apps/shared/components/mail/reading/MessageBody.d.ts +32 -0
- package/dist/apps/shared/components/mail/reading/MessageBody.js +136 -0
- package/dist/apps/shared/components/mail/reading/MessageCard.d.ts +62 -0
- package/dist/apps/shared/components/mail/reading/MessageCard.js +58 -0
- package/dist/apps/shared/components/mail/reading/bodyContent.d.ts +38 -0
- package/dist/apps/shared/components/mail/reading/bodyContent.js +84 -0
- package/dist/apps/shared/components/mail/reading/bodyHtml.d.ts +47 -0
- package/dist/apps/shared/components/mail/reading/bodyHtml.js +267 -0
- package/dist/apps/shared/components/mail/reading/color.d.ts +63 -0
- package/dist/apps/shared/components/mail/reading/color.js +201 -0
- package/dist/apps/shared/components/mail/reading/frameControl.d.ts +33 -0
- package/dist/apps/shared/components/mail/reading/frameControl.js +120 -0
- package/dist/apps/shared/components/mail/reading/frameDocument.d.ts +52 -0
- package/dist/apps/shared/components/mail/reading/frameDocument.js +98 -0
- package/dist/apps/shared/components/mail/reading/safeDocument.d.ts +3 -0
- package/dist/apps/shared/components/mail/reading/safeDocument.js +12 -0
- package/dist/apps/shared/components/mail/reading/themeAdaptation.d.ts +56 -0
- package/dist/apps/shared/components/mail/reading/themeAdaptation.js +144 -0
- package/dist/apps/shared/components/mail/reading/themeSurface.d.ts +17 -0
- package/dist/apps/shared/components/mail/reading/themeSurface.js +83 -0
- package/dist/apps/shared/components/mail/reading/viewOriginal.d.ts +9 -0
- package/dist/apps/shared/components/mail/reading/viewOriginal.js +32 -0
- package/dist/apps/shared/components/settings/SigningCertificateCard.d.ts +32 -0
- package/dist/apps/shared/components/settings/SigningCertificateCard.js +98 -0
- package/dist/apps/shared/components/settings/layout/SettingsShell.js +1 -0
- package/dist/apps/shared/components/sharing/PrincipalListField.d.ts +24 -0
- package/dist/apps/shared/components/sharing/PrincipalListField.js +23 -0
- package/dist/apps/shared/components/sharing/PrincipalPicker.d.ts +28 -0
- package/dist/apps/shared/components/sharing/PrincipalPicker.js +24 -0
- package/dist/apps/shared/components/sharing/PrincipalResolver.d.ts +44 -0
- package/dist/apps/shared/components/sharing/PrincipalResolver.js +92 -0
- package/dist/apps/shared/components/tasks/TasksSidebar.js +4 -2
- package/dist/apps/shared/components/tasks/layout/TasksShell.js +3 -1
- package/dist/apps/shared/mail/folderCounts.d.ts +1 -1
- package/dist/apps/shared/mail/folderCounts.js +9 -3
- package/dist/apps/shared/mail/folderTree.d.ts +55 -0
- package/dist/apps/shared/mail/folderTree.js +116 -0
- package/dist/apps/shared/mail/outbox/UnlockBridge.d.ts +5 -0
- package/dist/apps/shared/mail/outbox/UnlockBridge.js +18 -0
- package/dist/apps/shared/mail/outbox/composeBridge.d.ts +34 -0
- package/dist/apps/shared/mail/outbox/composeBridge.js +32 -0
- package/dist/apps/shared/mail/outbox/outboxState.d.ts +41 -0
- package/dist/apps/shared/mail/outbox/outboxState.js +42 -0
- package/dist/apps/shared/mail/outbox/pendingSends.d.ts +39 -0
- package/dist/apps/shared/mail/outbox/pendingSends.js +109 -0
- package/dist/apps/shared/mail/outbox/sendDecision.d.ts +80 -0
- package/dist/apps/shared/mail/outbox/sendDecision.js +94 -0
- package/dist/apps/shared/mail/outbox/sendJob.d.ts +68 -0
- package/dist/apps/shared/mail/outbox/sendJob.js +319 -0
- package/dist/apps/shared/mail/outbox/sendOutcomes.d.ts +3 -0
- package/dist/apps/shared/mail/outbox/sendOutcomes.js +141 -0
- package/dist/apps/shared/mail/outbox/sendState.d.ts +25 -0
- package/dist/apps/shared/mail/outbox/sendState.js +8 -0
- package/dist/apps/shared/mail/outbox/useOutboxStatus.d.ts +11 -0
- package/dist/apps/shared/mail/outbox/useOutboxStatus.js +49 -0
- package/dist/apps/shared/mail/useMailConnection.d.ts +9 -8
- package/dist/apps/shared/mail/useMailConnection.js +64 -22
- package/dist/apps/shared/mail/useMailLiveUpdates.d.ts +17 -4
- package/dist/apps/shared/mail/useMailLiveUpdates.js +36 -10
- package/dist/apps/shared/mail/useNewMailNotifications.d.ts +4 -10
- package/dist/apps/shared/mail/useNewMailNotifications.js +43 -23
- package/dist/apps/shared/navigation/AppRouter.js +2 -2
- package/dist/apps/shared/notifications/NotificationCenter.d.ts +19 -0
- package/dist/apps/shared/notifications/NotificationCenter.js +90 -0
- package/dist/apps/shared/notifications/NotificationHistoryDialog.d.ts +10 -0
- package/dist/apps/shared/notifications/NotificationHistoryDialog.js +36 -0
- package/dist/apps/shared/notifications/apiErrors.d.ts +19 -0
- package/dist/apps/shared/notifications/apiErrors.js +86 -0
- package/dist/apps/shared/notifications/headerOffset.d.ts +8 -0
- package/dist/apps/shared/notifications/headerOffset.js +33 -0
- package/dist/apps/shared/notifications/pushStatus.d.ts +9 -0
- package/dist/apps/shared/notifications/pushStatus.js +54 -0
- package/dist/apps/shared/notifications/store.d.ts +136 -0
- package/dist/apps/shared/notifications/store.js +416 -0
- package/dist/apps/shared/notifications/systemErrors.d.ts +9 -0
- package/dist/apps/shared/notifications/systemErrors.js +58 -0
- package/dist/apps/shared/notifications/useNotifications.d.ts +17 -0
- package/dist/apps/shared/notifications/useNotifications.js +18 -0
- package/dist/apps/shared/signing/enrollmentStorage.d.ts +10 -0
- package/dist/apps/shared/signing/enrollmentStorage.js +33 -0
- package/dist/apps/shared/signing/enrollmentTracker.d.ts +73 -0
- package/dist/apps/shared/signing/enrollmentTracker.js +307 -0
- package/dist/apps/shared/signing/enrollmentView.d.ts +57 -0
- package/dist/apps/shared/signing/enrollmentView.js +214 -0
- package/dist/apps/shared/signing/signingInfo.d.ts +11 -0
- package/dist/apps/shared/signing/signingInfo.js +48 -0
- package/dist/apps/shared/signing/useNow.d.ts +2 -0
- package/dist/apps/shared/signing/useNow.js +18 -0
- package/dist/apps/shared/signing/useSigningEnrollmentWatcher.d.ts +16 -0
- package/dist/apps/shared/signing/useSigningEnrollmentWatcher.js +87 -0
- package/dist/apps/shared/styles/app.css +80 -0
- package/dist/apps/www/_layout.d.ts +5 -1
- package/dist/apps/www/_layout.js +3 -2
- package/dist/apps/www/_routes.js +1 -0
- package/dist/apps/www/calendar/index.js +3 -4
- package/dist/apps/www/contacts/[uid].js +5 -7
- package/dist/apps/www/contacts/index.js +14 -21
- package/dist/apps/www/index.js +37 -19
- package/dist/apps/www/messages/[uid].js +3 -1
- package/dist/apps/www/settings/appearance/index.d.ts +4 -0
- package/dist/apps/www/settings/appearance/index.js +13 -0
- package/dist/apps/www/settings/auto-reply/index.js +3 -6
- package/dist/apps/www/settings/encryption/index.js +103 -95
- package/dist/apps/www/settings/filters/[uid].js +3 -1
- package/dist/apps/www/settings/filters/new/index.js +3 -1
- package/dist/apps/www/settings/privacy/index.js +5 -8
- package/dist/apps/www/settings/read-receipts/index.js +3 -6
- package/dist/apps/www/settings/sharing/index.js +19 -33
- package/dist/apps/www/settings/signatures/[uid].js +3 -1
- package/dist/apps/www/settings/signatures/new/index.js +3 -2
- package/dist/apps/www/tasks/index.js +8 -14
- package/package.json +2 -2
- package/apps/shared/components/mail/NewMailToasts.tsx +0 -141
- package/apps/shared/components/mail/compose/SendFailureAlert.tsx +0 -48
- package/dist/apps/shared/components/mail/NewMailToasts.d.ts +0 -19
- package/dist/apps/shared/components/mail/NewMailToasts.js +0 -55
- package/dist/apps/shared/components/mail/compose/SendFailureAlert.d.ts +0 -14
- package/dist/apps/shared/components/mail/compose/SendFailureAlert.js +0 -11
|
@@ -6,7 +6,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
6
6
|
import { routedPage } from "../../_routedPage.js";
|
|
7
7
|
import { useEffect, useState } from "react";
|
|
8
8
|
import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
|
|
9
|
-
import { addMasterKeyWrap, cancelSignEnrollment,
|
|
9
|
+
import { addMasterKeyWrap, cancelSignEnrollment, findActivePublicKey, getCurrentSignEnrollment, getEscrowInfo, getKeyVault, rekey, removeMasterKeyWrap, startSignEnrollment, } from "@rapidmx/react-shared/crypto/keyvaultApi.js";
|
|
10
10
|
import { ENCRYPTION_PRIVATE_KEY_AAD_PURPOSE, SIGNING_PRIVATE_KEY_AAD_PURPOSE, destroyUnlockedKeys, getUnlockedKeys, unlockWithPassword, } from "@rapidmx/react-shared/crypto/keySession.js";
|
|
11
11
|
import { IDLE_TIMEOUT_OPTIONS_MINUTES, getIdleTimeoutMinutes, setIdleTimeoutMinutes } from "@rapidmx/react-shared/crypto/idleTimeout.js";
|
|
12
12
|
import { LOCAL_INDEX_SIZE_OPTIONS, getDefaultLocalIndexByteBudget, getLocalIndexByteBudget, setLocalIndexByteBudget, } from "../../../shared/search/localIndexSizePreference.js";
|
|
@@ -19,13 +19,22 @@ import SettingsShell, { useSettingsShell } from "../../../shared/components/sett
|
|
|
19
19
|
import KeyEnrollmentGate from "../../../shared/components/layout/KeyEnrollmentGate.js";
|
|
20
20
|
import { useUnlockPrompt } from "../../../shared/components/layout/UnlockPromptProvider.js";
|
|
21
21
|
import { destroyLocalIndex } from "../../../shared/search/localIndexRpcClient.js";
|
|
22
|
+
import { notifyApiError } from "../../../shared/notifications/apiErrors.js";
|
|
22
23
|
import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
|
|
23
24
|
import Button from "@rapidmx/react-shared/components/buttons/Button.js";
|
|
24
25
|
import Modal from "@rapidmx/react-shared/components/overlays/Modal.js";
|
|
26
|
+
import SigningCertificateCard from "../../../shared/components/settings/SigningCertificateCard.js";
|
|
27
|
+
import { readStoredSignEnrollment, storeSignEnrollment } from "../../../shared/signing/enrollmentStorage.js";
|
|
28
|
+
import { checkEnrollmentNow, forgetEnrollment, recordEnrollmentResult, useEnrollmentSnapshot, watchCurrentEnrollment, watchEnrollment, } from "../../../shared/signing/enrollmentTracker.js";
|
|
29
|
+
import { STALE_REQUEST_TEXT, beforeRequestText, healthWarning, isInstalling, timeOf } from "../../../shared/signing/enrollmentView.js";
|
|
30
|
+
import { useSigningEnrollmentInfo } from "../../../shared/signing/signingInfo.js";
|
|
25
31
|
const MIN_PASSWORD_LENGTH = 8;
|
|
26
32
|
/** Mirrors restapi's `BaseKeyVaultRoute` `MAX_MASTER_KEY_WRAPS` - `addMasterKeyWrap()` refuses a vault
|
|
27
33
|
* already holding this many wraps (escrow included). */
|
|
28
34
|
export const MAX_MASTER_KEY_WRAPS = 20;
|
|
35
|
+
/** How often, and how many times, the page asks for the mailbox's keys while a certificate is issued but not installed yet. */
|
|
36
|
+
const INSTALL_WATCH_INTERVAL_MS = 15000;
|
|
37
|
+
const INSTALL_WATCH_ATTEMPTS = 40;
|
|
29
38
|
const KEYS_LOCKED_MESSAGE = "Your encryption keys were locked before this could finish. Unlock them and try again.";
|
|
30
39
|
/** Thrown inside `handleRotateKeys()` when the escrow wrap of the new master key can't be built - the rotation is
|
|
31
40
|
* then abandoned before `rekey()`. */
|
|
@@ -123,38 +132,6 @@ async function rewrapVaultPrivateKeys(mailboxUid, currentMasterKey, wrappedKeys)
|
|
|
123
132
|
}
|
|
124
133
|
}
|
|
125
134
|
}
|
|
126
|
-
// RFC 8823 ACME issuance is a real email round-trip with a public CA - "likely minutes," not seconds -
|
|
127
|
-
// so this polls infrequently rather than hammering the endpoint.
|
|
128
|
-
const SIGNING_ENROLLMENT_POLL_INTERVAL_MS = 15000;
|
|
129
|
-
/**
|
|
130
|
-
* A started signing enrollment's id, per mailbox. Kept only so a reload can ask the server
|
|
131
|
-
* (`checkSignEnrollmentStatus()`) whether that enrollment is still pending: a rotation while it is pending
|
|
132
|
-
* would strand the enrollment's already-submitted private key under a master key that no longer exists. The
|
|
133
|
-
* stored id is never trusted on its own - the server's answer decides the state, and restapi's own `rekey()`
|
|
134
|
-
* refuses (409) a rotation during an enrollment this browser never saw (e.g. one started on another device).
|
|
135
|
-
*/
|
|
136
|
-
const SIGN_ENROLLMENT_STORAGE_PREFIX = "rapidmx.signEnrollment.";
|
|
137
|
-
function readStoredSignEnrollment(mailboxUid) {
|
|
138
|
-
try {
|
|
139
|
-
return localStorage.getItem(SIGN_ENROLLMENT_STORAGE_PREFIX + mailboxUid);
|
|
140
|
-
}
|
|
141
|
-
catch {
|
|
142
|
-
return null;
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
function storeSignEnrollment(mailboxUid, enrollmentId) {
|
|
146
|
-
try {
|
|
147
|
-
if (enrollmentId) {
|
|
148
|
-
localStorage.setItem(SIGN_ENROLLMENT_STORAGE_PREFIX + mailboxUid, enrollmentId);
|
|
149
|
-
}
|
|
150
|
-
else {
|
|
151
|
-
localStorage.removeItem(SIGN_ENROLLMENT_STORAGE_PREFIX + mailboxUid);
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
catch {
|
|
155
|
-
// Storage blocked - restapi's own 409 on rekey still guards a rotation.
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
135
|
const ROTATION_CONFLICT_MESSAGE = "Your keys were not rotated because the server reported a conflicting change. If a signing certificate enrollment is still in progress for this mailbox (it may have been started on another device), wait for it to finish, then try again. If this mailbox is under escrow, its escrow protection may have changed meanwhile - reload this page and try again. Nothing was changed.";
|
|
159
136
|
const METHOD_LABELS = {
|
|
160
137
|
password: "Password",
|
|
@@ -236,6 +213,12 @@ function EncryptionContent({ canManageKeys }) {
|
|
|
236
213
|
const [signingEnrollmentId, setSigningEnrollmentId] = useState(null);
|
|
237
214
|
const [signingError, setSigningError] = useState(null);
|
|
238
215
|
const rotationBlockedBySigning = signingStatus !== "idle";
|
|
216
|
+
// What is known of the mailbox's signing-certificate enrollment: kept current by the shared tracker (`signing/enrollmentTracker.ts`), which reads
|
|
217
|
+
// the server with backoff while this page is visible, stops when the enrollment ends, and goes on in the app frame when this page is left.
|
|
218
|
+
const enrollment = useEnrollmentSnapshot(mailboxUid);
|
|
219
|
+
const enrollmentResult = enrollment?.result ?? null;
|
|
220
|
+
// How this deployment issues certificates (a public CA by itself, or an administrator by hand): what the card and the request say follows it. Only the owner asks.
|
|
221
|
+
const signingInfo = useSigningEnrollmentInfo(canManageKeys);
|
|
239
222
|
const [cancelingEnrollment, setCancelingEnrollment] = useState(false);
|
|
240
223
|
const [cancelEnrollmentError, setCancelEnrollmentError] = useState(null);
|
|
241
224
|
// restapi's rekey() drops every old escrow wrap and requires a fresh one for the new master key (see
|
|
@@ -303,79 +286,80 @@ function EncryptionContent({ canManageKeys }) {
|
|
|
303
286
|
setWrappingEscrow(false);
|
|
304
287
|
}
|
|
305
288
|
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
if (result.status === "pending") {
|
|
309
|
-
setSigningEnrollmentId(enrollmentId);
|
|
310
|
-
setSigningStatus("pending");
|
|
311
|
-
return;
|
|
312
|
-
}
|
|
313
|
-
storeSignEnrollment(mailboxUid, null);
|
|
314
|
-
setSigningStatus("idle");
|
|
315
|
-
setSigningEnrollmentId(null);
|
|
316
|
-
if (result.status === "failed") {
|
|
317
|
-
setSigningError(result.error ?? "Signing certificate enrollment failed.");
|
|
318
|
-
return;
|
|
319
|
-
}
|
|
320
|
-
try {
|
|
321
|
-
const refreshed = await getMailbox(mailboxUid);
|
|
322
|
-
if (!isCancelled()) {
|
|
323
|
-
setRefreshedKeys(refreshed.keys ?? []);
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
catch {
|
|
327
|
-
// The new key shows on the next load - the enrollment itself is finished either way.
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
// Confirms an enrollment started before a reload with the server before rotation is offered again.
|
|
289
|
+
// Follows the enrollment this browser started (its stored id) - or, for the owner, the one the server names as the mailbox's current one (started
|
|
290
|
+
// on another device, or long ago), so a pending one blocks rotation and shows its progress here too.
|
|
331
291
|
useEffect(() => {
|
|
332
292
|
const storedId = readStoredSignEnrollment(mailboxUid);
|
|
333
|
-
if (!storedId) {
|
|
334
|
-
return;
|
|
335
|
-
}
|
|
336
293
|
let cancelled = false;
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
}
|
|
294
|
+
let release;
|
|
295
|
+
if (storedId) {
|
|
296
|
+
setSigningStatus("checking");
|
|
297
|
+
release = watchEnrollment(mailboxUid, storedId);
|
|
298
|
+
}
|
|
299
|
+
else if (canManageKeys) {
|
|
300
|
+
getCurrentSignEnrollment(mailboxUid)
|
|
301
|
+
.then((current) => {
|
|
302
|
+
if (!cancelled && current) {
|
|
303
|
+
if (current.status === "pending") {
|
|
304
|
+
storeSignEnrollment(mailboxUid, current.enrollmentId);
|
|
305
|
+
}
|
|
306
|
+
release = watchCurrentEnrollment(mailboxUid, current, current.status === "pending");
|
|
307
|
+
}
|
|
308
|
+
})
|
|
309
|
+
.catch(() => undefined);
|
|
310
|
+
}
|
|
354
311
|
return () => {
|
|
355
312
|
cancelled = true;
|
|
313
|
+
release?.();
|
|
356
314
|
};
|
|
357
|
-
}, [mailboxUid]);
|
|
315
|
+
}, [mailboxUid, canManageKeys]);
|
|
316
|
+
// What the tracker knows becomes this page's state: pending (or not heard from yet - assume it is still going, and let the polling keep asking) blocks
|
|
317
|
+
// rotation; an enrollment that ended, or that the server no longer knows, does not.
|
|
358
318
|
useEffect(() => {
|
|
359
|
-
if (
|
|
319
|
+
if (!enrollment) {
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
if (enrollmentResult?.status === "pending" || (!enrollmentResult && enrollment.offline)) {
|
|
323
|
+
setSigningEnrollmentId(enrollment.enrollmentId);
|
|
324
|
+
setSigningStatus("pending");
|
|
325
|
+
}
|
|
326
|
+
else if (enrollmentResult || enrollment.gone) {
|
|
327
|
+
setSigningEnrollmentId(null);
|
|
328
|
+
setSigningStatus("idle");
|
|
329
|
+
}
|
|
330
|
+
}, [enrollment?.enrollmentId, enrollmentResult?.status, enrollment?.gone, enrollment?.offline]);
|
|
331
|
+
// Once a certificate is issued the server has installed a new signing key that `mailbox.keys` (fetched once by the shell) never saw. The server installs it
|
|
332
|
+
// with a job a few minutes after the CA issues it (`installedAt`): until then the key is not there, so ask again every 15 s (for ten minutes at most).
|
|
333
|
+
const issuedUntil = timeOf(enrollmentResult?.notAfter);
|
|
334
|
+
const keyIsCurrent = !!activeSigningKey && (issuedUntil === undefined || issuedUntil <= activeSigningKey.notAfter + 60000);
|
|
335
|
+
useEffect(() => {
|
|
336
|
+
if (enrollmentResult?.status !== "issued" || keyIsCurrent) {
|
|
360
337
|
return;
|
|
361
338
|
}
|
|
362
339
|
let cancelled = false;
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
340
|
+
let timer;
|
|
341
|
+
let attempts = 0;
|
|
342
|
+
const refresh = () => {
|
|
343
|
+
getMailbox(mailboxUid)
|
|
344
|
+
.then((refreshed) => {
|
|
345
|
+
if (cancelled) {
|
|
346
|
+
return;
|
|
368
347
|
}
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
//
|
|
372
|
-
|
|
373
|
-
|
|
348
|
+
const keys = refreshed.keys ?? [];
|
|
349
|
+
setRefreshedKeys(keys);
|
|
350
|
+
// Not installed yet, and the server said it is on its way: look again.
|
|
351
|
+
if (isInstalling(enrollmentResult) && ++attempts < INSTALL_WATCH_ATTEMPTS && !findActivePublicKey(keys, "sign")) {
|
|
352
|
+
timer = setTimeout(refresh, INSTALL_WATCH_INTERVAL_MS);
|
|
353
|
+
}
|
|
354
|
+
})
|
|
355
|
+
.catch(() => undefined);
|
|
356
|
+
};
|
|
357
|
+
refresh();
|
|
374
358
|
return () => {
|
|
375
359
|
cancelled = true;
|
|
376
|
-
|
|
360
|
+
clearTimeout(timer);
|
|
377
361
|
};
|
|
378
|
-
}, [
|
|
362
|
+
}, [enrollmentResult?.status, enrollmentResult?.installedAt, enrollment?.enrollmentId, keyIsCurrent, mailboxUid]);
|
|
379
363
|
/** Cancels the pending enrollment (restapi's owner-only DELETE), which is what lets rotation run again. */
|
|
380
364
|
async function handleCancelEnrollment(enrollmentId) {
|
|
381
365
|
setCancelEnrollmentError(null);
|
|
@@ -387,18 +371,20 @@ function EncryptionContent({ canManageKeys }) {
|
|
|
387
371
|
}
|
|
388
372
|
else if (result.status === "failed") {
|
|
389
373
|
// Cancelled - the "failure" is the cancellation itself, not something to report.
|
|
374
|
+
forgetEnrollment(mailboxUid);
|
|
390
375
|
storeSignEnrollment(mailboxUid, null);
|
|
391
376
|
setSigningStatus("idle");
|
|
392
377
|
setSigningEnrollmentId(null);
|
|
393
378
|
}
|
|
394
379
|
else {
|
|
395
380
|
// Issued before the cancel landed: it finishes like any other issued enrollment.
|
|
396
|
-
|
|
381
|
+
recordEnrollmentResult(mailboxUid, enrollmentId, result);
|
|
397
382
|
}
|
|
398
383
|
}
|
|
399
384
|
catch (err) {
|
|
400
385
|
if (err instanceof ApiRequestError && err.status === 404) {
|
|
401
386
|
// The server no longer knows this enrollment, so nothing is pending.
|
|
387
|
+
forgetEnrollment(mailboxUid);
|
|
402
388
|
storeSignEnrollment(mailboxUid, null);
|
|
403
389
|
setSigningStatus("idle");
|
|
404
390
|
setSigningEnrollmentId(null);
|
|
@@ -424,6 +410,8 @@ function EncryptionContent({ canManageKeys }) {
|
|
|
424
410
|
wrappedKey: { ciphertext: wrappedKeySealed.ciphertext, nonce: wrappedKeySealed.nonce, algorithm: "AES-256-GCM" },
|
|
425
411
|
});
|
|
426
412
|
storeSignEnrollment(mailboxUid, enrollmentId);
|
|
413
|
+
// Followed from now on (and after this page is left): the server has just accepted it, so it is pending - no need to ask first.
|
|
414
|
+
watchEnrollment(mailboxUid, enrollmentId, { initial: { status: "pending" } });
|
|
427
415
|
setSigningEnrollmentId(enrollmentId);
|
|
428
416
|
setSigningStatus("pending");
|
|
429
417
|
}
|
|
@@ -472,7 +460,8 @@ function EncryptionContent({ canManageKeys }) {
|
|
|
472
460
|
await loadVault();
|
|
473
461
|
}
|
|
474
462
|
catch (err) {
|
|
475
|
-
|
|
463
|
+
// A pop-up (the confirmation is already closed); `actionError` keeps the guidance tied to the forms below.
|
|
464
|
+
notifyApiError(err, "Couldn't remove this unlock method");
|
|
476
465
|
}
|
|
477
466
|
finally {
|
|
478
467
|
setRemovingMethod(null);
|
|
@@ -764,8 +753,27 @@ function EncryptionContent({ canManageKeys }) {
|
|
|
764
753
|
return (_jsx("div", { className: "flex-1 min-w-0 overflow-y-auto p-6", children: _jsx("div", { className: "max-w-xl", children: _jsx(Alert, { children: rotationRelockReason ??
|
|
765
754
|
"Your encryption keys have been removed from this session. Reload the page (or open Mail again) to unlock them when you need to read or send encrypted mail." }) }) }));
|
|
766
755
|
}
|
|
756
|
+
// The "Digital signatures" section: a pending enrollment first (even while an older certificate is still active - it is a renewal); else the
|
|
757
|
+
// active certificate; else how the last enrollment ended (a failure, or a certificate that has since expired); else nothing has been requested.
|
|
758
|
+
const expiredSigningKey = displayedKeys
|
|
759
|
+
.filter((key) => key.useType === "sign" && !key.revokedAt && key.notAfter <= Date.now())
|
|
760
|
+
.sort((a, b) => b.notAfter - a.notAfter)[0];
|
|
761
|
+
// A certificate the CA has issued but the server has not installed yet counts as issued too (there is no key to show yet: the card says it is installing).
|
|
762
|
+
const installing = canManageKeys && isInstalling(enrollmentResult);
|
|
763
|
+
const certificateMode = signingStatus === "pending"
|
|
764
|
+
? "pending"
|
|
765
|
+
: activeSigningKey || installing
|
|
766
|
+
? canManageKeys
|
|
767
|
+
? "issued"
|
|
768
|
+
: null
|
|
769
|
+
: canManageKeys && enrollmentResult?.status === "failed"
|
|
770
|
+
? "failed"
|
|
771
|
+
: canManageKeys && expiredSigningKey
|
|
772
|
+
? "expired"
|
|
773
|
+
: null;
|
|
774
|
+
const certificateEnrollment = certificateMode === "issued" && enrollmentResult?.status !== "issued" ? null : certificateMode === "expired" ? null : enrollmentResult;
|
|
767
775
|
return (_jsx("div", { className: "flex-1 min-w-0 overflow-y-auto p-6", children: _jsxs("div", { className: "max-w-xl flex flex-col gap-6", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-lg font-bold tracking-tight mb-1", children: "Encryption" }), _jsxs("p", { className: "text-sm text-text-muted", children: ["Manage how ", mailbox.displayName, " unlocks its encryption keys on this and other devices."] })] }), loadError && _jsx(Alert, { children: loadError }), actionError && _jsx(Alert, { children: actionError }), !canManageKeys && (_jsx("p", { className: "text-sm text-text-muted", children: "Only this mailbox\u2019s owner, signed in as themselves, can change its encryption keys and unlock methods." })), _jsxs("div", { children: [_jsx("h2", { className: "text-sm font-semibold mb-2", children: "Encryption keys" }), displayedKeys.length > 0 ? (_jsx("ul", { className: "flex flex-col gap-1 text-sm", children: displayedKeys.map((key) => (_jsxs("li", { className: "font-mono text-xs", children: [key.useType === "sign" ? "Signing" : "Encryption", " key: ", key.fingerprint, key.revokedAt &&
|
|
768
|
-
(key.revocationReason === "superseded" ? (_jsx("span", { className: "text-text-muted", children: " (superseded)" })) : (_jsx("span", { className: "text-danger", children: " (revoked)" })))] }, key.fingerprint))) })) : (_jsx("p", { className: "text-sm text-text-muted", children: "No keys enrolled yet." }))] }), _jsxs("div", { children: [_jsx("h2", { className: "text-sm font-semibold mb-2", children: "Digital signatures" }), activeSigningKey
|
|
776
|
+
(key.revocationReason === "superseded" ? (_jsx("span", { className: "text-text-muted", children: " (superseded)" })) : (_jsx("span", { className: "text-danger", children: " (revoked)" })))] }, key.fingerprint))) })) : (_jsx("p", { className: "text-sm text-text-muted", children: "No keys enrolled yet." }))] }), _jsxs("div", { children: [_jsx("h2", { className: "text-sm font-semibold mb-2", children: "Digital signatures" }), activeSigningKey && signingStatus !== "pending" && (_jsx("p", { className: ["text-sm text-text-muted", certificateMode ? "mb-3" : ""].join(" "), children: "Enabled \u2014 outgoing mail from this mailbox is signed with a publicly-trusted certificate." })), certificateMode && (_jsxs("div", { className: "flex flex-col gap-2", children: [certificateMode === "failed" && signingError && _jsx(Alert, { children: signingError }), _jsx(SigningCertificateCard, { mode: certificateMode, enrollment: certificateEnrollment, snapshot: enrollment, info: signingInfo, address: mailbox.primarySmtpAddress, keyNotAfter: certificateMode === "expired" ? expiredSigningKey?.notAfter : activeSigningKey?.notAfter, canRequest: canManageKeys, requesting: signingStatus === "enrolling", onCheck: canManageKeys ? () => void checkEnrollmentNow(mailboxUid) : undefined, onRequest: handleEnrollSigning })] })), !activeSigningKey && !certificateMode && !canManageKeys ? (_jsx("p", { className: "text-sm text-text-muted", children: "Not enabled." })) : activeSigningKey || certificateMode ? null : (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx("p", { className: "text-xs text-text-muted", children: "Lets recipients verify that mail from this mailbox is genuinely from you. Optional \u2014 encryption already works without it." }), beforeRequestText(signingInfo, mailbox.primarySmtpAddress) && (_jsx("p", { className: "text-xs text-text-muted", children: beforeRequestText(signingInfo, mailbox.primarySmtpAddress) })), healthWarning(signingInfo) && _jsx(Alert, { children: healthWarning(signingInfo) }), enrollment?.gone && _jsx(Alert, { children: STALE_REQUEST_TEXT }), signingError && _jsx(Alert, { children: signingError }), signingInfo?.backend !== "none" && (_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", loading: signingStatus === "enrolling", disabled: signingStatus !== "idle", onClick: handleEnrollSigning, children: "Enable digital signatures" }))] }))] }), mailbox.escrowScopeId && (_jsxs("div", { children: [_jsx("h2", { className: "text-sm font-semibold mb-2", children: "Escrow" }), escrowError && _jsx(Alert, { children: escrowError }), hasEscrowWrap ? (_jsx("p", { className: "text-sm text-text-muted", children: "This mailbox is under legal/compliance escrow \u2014 an authorized holder in your organization can recover its encrypted mail if needed. This does not weaken protection against anyone else." })) : (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsxs("p", { className: "text-xs text-text-muted", children: ["Your organization has assigned this mailbox to an escrow scope, but nothing has been protected yet \u2014 an authorized holder cannot recover this mailbox\u2019s encrypted mail until you complete this step.", " ", "This does not weaken protection against anyone else."] }), canManageKeys && (_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", loading: wrappingEscrow, disabled: wrappingEscrow, onClick: handleWrapEscrow, children: "Add escrow protection" }))] }))] })), _jsxs("div", { children: [_jsx("h2", { className: "text-sm font-semibold mb-2", children: "Unlock methods" }), _jsxs("p", { className: "text-xs text-text-muted mb-3", children: ["Removing a method here stops it from being usable to unlock this mailbox going forward, but it is ", _jsx("strong", { children: "not" }), " full revocation \u2014 anyone who already captured a wrapped copy and knows its secret could still use it. For real revocation (e.g. after a lost device), rotate your keys entirely below instead."] }), vault && vault.masterKeyWraps.length > 0 ? (_jsx("ul", { className: "flex flex-col gap-2", children: [...passwordWraps, ...recoveryWraps, ...otherWraps].map((wrap) => {
|
|
769
777
|
const key = `${wrap.method}:${wrap.methodId ?? ""}`;
|
|
770
778
|
return (_jsxs("li", { className: "flex items-center justify-between gap-3 text-sm py-1.5 px-3 bg-surface-alt rounded-sm", children: [_jsx("span", { children: METHOD_LABELS[wrap.method] ?? wrap.method }), wrap.method !== "escrow" &&
|
|
771
779
|
canManageKeys &&
|
|
@@ -14,6 +14,7 @@ import { MAIL_FILTER_CONDITION_FIELDS, buildMailFilterActionTypes } from "./_mai
|
|
|
14
14
|
import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
|
|
15
15
|
import Button from "@rapidmx/react-shared/components/buttons/Button.js";
|
|
16
16
|
import FormField from "@rapidmx/react-shared/components/forms/FormField.js";
|
|
17
|
+
import { notifyApiError } from "../../../shared/notifications/apiErrors.js";
|
|
17
18
|
const INPUT_CLASS = "w-full text-sm py-2.5 px-3 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary";
|
|
18
19
|
function MailFilterDetailPage(props) {
|
|
19
20
|
return (_jsx(SettingsShell, { ...props, active: "filters", children: _jsx(MailFilterDetailContent, { uid: props.params.uid }) }));
|
|
@@ -87,7 +88,8 @@ function MailFilterDetailContent({ uid }) {
|
|
|
87
88
|
setSaved(true);
|
|
88
89
|
}
|
|
89
90
|
catch (err) {
|
|
90
|
-
|
|
91
|
+
// A pop-up: `error` is only this form's own validation (and the load failure above).
|
|
92
|
+
notifyApiError(err, "Couldn't save the mail filter");
|
|
91
93
|
}
|
|
92
94
|
finally {
|
|
93
95
|
setSaving(false);
|
|
@@ -15,6 +15,7 @@ import { MAIL_FILTER_CONDITION_FIELDS, buildMailFilterActionTypes } from "../_ma
|
|
|
15
15
|
import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
|
|
16
16
|
import Button from "@rapidmx/react-shared/components/buttons/Button.js";
|
|
17
17
|
import FormField from "@rapidmx/react-shared/components/forms/FormField.js";
|
|
18
|
+
import { notifyApiError } from "../../../../shared/notifications/apiErrors.js";
|
|
18
19
|
const INPUT_CLASS = "w-full text-sm py-2.5 px-3 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary";
|
|
19
20
|
function NewMailFilterPage(props) {
|
|
20
21
|
return (_jsx(SettingsShell, { ...props, active: "filters", children: _jsx(NewMailFilterForm, {}) }));
|
|
@@ -60,7 +61,8 @@ function NewMailFilterForm() {
|
|
|
60
61
|
navigate(`/settings/filters/${encodeURIComponent(created.uid)}?mailboxUid=${encodeURIComponent(mailboxUid)}`);
|
|
61
62
|
}
|
|
62
63
|
catch (err) {
|
|
63
|
-
|
|
64
|
+
// A pop-up: `error` above is only the form's own validation.
|
|
65
|
+
notifyApiError(err, "Couldn't create the mail filter");
|
|
64
66
|
}
|
|
65
67
|
finally {
|
|
66
68
|
setSaving(false);
|
|
@@ -14,6 +14,7 @@ import SettingsShell, { useSettingsShell } from "../../../shared/components/sett
|
|
|
14
14
|
import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
|
|
15
15
|
import Button from "@rapidmx/react-shared/components/buttons/Button.js";
|
|
16
16
|
import Modal from "@rapidmx/react-shared/components/overlays/Modal.js";
|
|
17
|
+
import { notifyApiError } from "../../../shared/notifications/apiErrors.js";
|
|
17
18
|
const NON_MAIL_FOLDER_TYPES = new Set(["calendar", "contacts", "tasks", "notes"]);
|
|
18
19
|
function importFormatFromFilename(filename) {
|
|
19
20
|
return filename.toLowerCase().endsWith(".pst") ? "pst" : "mbox";
|
|
@@ -69,7 +70,6 @@ function ExportSection({ mailboxUid, mailboxLabel }) {
|
|
|
69
70
|
const [loadError, setLoadError] = useState(null);
|
|
70
71
|
const [format, setFormat] = useState("json");
|
|
71
72
|
const [creating, setCreating] = useState(false);
|
|
72
|
-
const [createError, setCreateError] = useState(null);
|
|
73
73
|
// The create form below isn't gated behind `loading`, so a fast create-then-reload can resolve before
|
|
74
74
|
// the initial mount fetch does; without this guard the mount fetch's now-stale response would land
|
|
75
75
|
// last and silently revert the list. Only the most-recently-issued load's response is ever applied.
|
|
@@ -93,20 +93,19 @@ function ExportSection({ mailboxUid, mailboxLabel }) {
|
|
|
93
93
|
}, [mailboxUid]);
|
|
94
94
|
async function handleCreate(e) {
|
|
95
95
|
e.preventDefault();
|
|
96
|
-
setCreateError(null);
|
|
97
96
|
setCreating(true);
|
|
98
97
|
try {
|
|
99
98
|
await createExportRequest({ format });
|
|
100
99
|
await loadRequests();
|
|
101
100
|
}
|
|
102
101
|
catch (err) {
|
|
103
|
-
|
|
102
|
+
notifyApiError(err, "Couldn't start the export");
|
|
104
103
|
}
|
|
105
104
|
finally {
|
|
106
105
|
setCreating(false);
|
|
107
106
|
}
|
|
108
107
|
}
|
|
109
|
-
return (_jsxs("div", { children: [_jsx("h2", { className: "text-sm font-semibold mb-2", children: "Export my data" }), _jsxs("p", { className: "text-xs text-text-muted mb-3", children: ["Requests a copy of ", _jsx("strong", { children: mailboxLabel }), "\u2019s messages, contacts, calendar, tasks, and notes. This runs in the background and can take a few minutes \u2014 check back here for a download link once it\u2019s ready."] }), _jsxs("form", { onSubmit: handleCreate, className: "flex items-end gap-3 mb-5", children: [_jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Format" }), _jsxs("select", { "aria-label": "Export format", className: "text-sm border border-border rounded-sm py-1.5 px-2 bg-surface", value: format, onChange: (e) => setFormat(e.target.value), children: [_jsx("option", { value: "json", children: "JSON" }), _jsx("option", { value: "mbox", children: "Mbox" })] })] }), _jsx(Button, { type: "submit", variant: "secondary", className: "!w-auto", loading: creating, disabled: creating, children: "Request export" })] }),
|
|
108
|
+
return (_jsxs("div", { children: [_jsx("h2", { className: "text-sm font-semibold mb-2", children: "Export my data" }), _jsxs("p", { className: "text-xs text-text-muted mb-3", children: ["Requests a copy of ", _jsx("strong", { children: mailboxLabel }), "\u2019s messages, contacts, calendar, tasks, and notes. This runs in the background and can take a few minutes \u2014 check back here for a download link once it\u2019s ready."] }), _jsxs("form", { onSubmit: handleCreate, className: "flex items-end gap-3 mb-5", children: [_jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Format" }), _jsxs("select", { "aria-label": "Export format", className: "text-sm border border-border rounded-sm py-1.5 px-2 bg-surface", value: format, onChange: (e) => setFormat(e.target.value), children: [_jsx("option", { value: "json", children: "JSON" }), _jsx("option", { value: "mbox", children: "Mbox" })] })] }), _jsx(Button, { type: "submit", variant: "secondary", className: "!w-auto", loading: creating, disabled: creating, children: "Request export" })] }), loadError && _jsx(Alert, { children: loadError }), loading ? (_jsx("p", { className: "text-sm text-text-muted", children: "Loading\u2026" })) : requests.length === 0 ? (_jsx("p", { className: "text-sm text-text-muted", children: "No export requests yet." })) : (_jsx("ul", { className: "flex flex-col gap-2", children: requests.map((request) => (_jsxs("li", { className: "flex items-center justify-between gap-3 text-sm py-1.5 px-3 bg-surface-alt rounded-sm", children: [_jsxs("span", { children: [request.format.toUpperCase(), " \u00B7 ", new Date(request.dateCreated).toLocaleString(), request.status === "failed" && request.errorMessage && (_jsxs("span", { className: "text-danger", children: [" \u2014 ", request.errorMessage] }))] }), _jsxs("span", { className: "flex items-center gap-3", children: [_jsx("span", { className: `text-xs font-bold uppercase tracking-wide py-0.5 px-2 rounded-pill ${statusBadgeClass(request.status)}`, children: statusLabel(request.status) }), request.status === "ready" && (_jsx("a", { href: exportRequestDownloadUrl(request.uid), className: "text-primary-dark hover:underline font-medium", children: "Download" }))] })] }, request.uid))) }))] }));
|
|
110
109
|
}
|
|
111
110
|
function ImportSection({ mailboxUid }) {
|
|
112
111
|
const [folders, setFolders] = useState([]);
|
|
@@ -116,7 +115,6 @@ function ImportSection({ mailboxUid }) {
|
|
|
116
115
|
const [loading, setLoading] = useState(true);
|
|
117
116
|
const [loadError, setLoadError] = useState(null);
|
|
118
117
|
const [uploading, setUploading] = useState(false);
|
|
119
|
-
const [uploadError, setUploadError] = useState(null);
|
|
120
118
|
const fileInputRef = useRef(null);
|
|
121
119
|
// Same stale-response guard as ExportSection's loadRequests() - the Upload button is enabled
|
|
122
120
|
// independently of this section's own `loading`, so a fast upload-then-reload can resolve before the
|
|
@@ -156,14 +154,13 @@ function ImportSection({ mailboxUid }) {
|
|
|
156
154
|
if (!file || !targetFolderUid) {
|
|
157
155
|
return;
|
|
158
156
|
}
|
|
159
|
-
setUploadError(null);
|
|
160
157
|
setUploading(true);
|
|
161
158
|
uploadMailboxImport(file, { format: importFormatFromFilename(file.name), targetFolderUid })
|
|
162
159
|
.then(() => loadRequests())
|
|
163
|
-
.catch((err) =>
|
|
160
|
+
.catch((err) => notifyApiError(err, "Couldn't upload the file"))
|
|
164
161
|
.finally(() => setUploading(false));
|
|
165
162
|
}
|
|
166
|
-
return (_jsxs("div", { children: [_jsx("h2", { className: "text-sm font-semibold mb-2", children: "Import mail" }), _jsx("p", { className: "text-xs text-text-muted mb-3", children: "Uploads an Mbox or PST archive and imports every message it contains into the folder you choose below. This runs in the background \u2014 check back here for its progress." }), folders.length > 0 && (_jsxs("label", { className: "flex flex-col gap-1.5 text-sm mb-3 max-w-xs", children: [_jsx("span", { className: "font-semibold", children: "Destination folder" }), _jsx("select", { "aria-label": "Destination folder", className: "text-sm border border-border rounded-sm py-1.5 px-2 bg-surface", value: targetFolderUid, onChange: (e) => setTargetFolderUid(e.target.value), children: folders.map((folder) => (_jsx("option", { value: folder.uid, children: folder.name }, folder.uid))) })] })), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto mb-3", loading: uploading, disabled: uploading || !targetFolderUid, onClick: () => fileInputRef.current?.click(), children: "Upload Mbox or PST file" }), _jsx("input", { ref: fileInputRef, type: "file", accept: ".mbox,.pst", "aria-label": "Upload mail archive", className: "hidden", onChange: handleFileChange }), foldersError && _jsx(Alert, { children: foldersError }),
|
|
163
|
+
return (_jsxs("div", { children: [_jsx("h2", { className: "text-sm font-semibold mb-2", children: "Import mail" }), _jsx("p", { className: "text-xs text-text-muted mb-3", children: "Uploads an Mbox or PST archive and imports every message it contains into the folder you choose below. This runs in the background \u2014 check back here for its progress." }), folders.length > 0 && (_jsxs("label", { className: "flex flex-col gap-1.5 text-sm mb-3 max-w-xs", children: [_jsx("span", { className: "font-semibold", children: "Destination folder" }), _jsx("select", { "aria-label": "Destination folder", className: "text-sm border border-border rounded-sm py-1.5 px-2 bg-surface", value: targetFolderUid, onChange: (e) => setTargetFolderUid(e.target.value), children: folders.map((folder) => (_jsx("option", { value: folder.uid, children: folder.name }, folder.uid))) })] })), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto mb-3", loading: uploading, disabled: uploading || !targetFolderUid, onClick: () => fileInputRef.current?.click(), children: "Upload Mbox or PST file" }), _jsx("input", { ref: fileInputRef, type: "file", accept: ".mbox,.pst", "aria-label": "Upload mail archive", className: "hidden", onChange: handleFileChange }), foldersError && _jsx(Alert, { children: foldersError }), loadError && _jsx(Alert, { children: loadError }), loading ? (_jsx("p", { className: "text-sm text-text-muted", children: "Loading\u2026" })) : requests.length === 0 ? (_jsx("p", { className: "text-sm text-text-muted", children: "No import requests yet." })) : (_jsx("ul", { className: "flex flex-col gap-2", children: requests.map((request) => (_jsxs("li", { className: "flex items-center justify-between gap-3 text-sm py-1.5 px-3 bg-surface-alt rounded-sm", children: [_jsxs("span", { children: [request.format.toUpperCase(), " \u00B7 ", new Date(request.dateCreated).toLocaleString(), request.status === "completed" && (_jsxs("span", { className: "text-text-muted", children: [" ", "\u2014 ", request.importedCount ?? 0, " imported", request.failedCount ? `, ${request.failedCount} failed` : ""] })), request.status === "failed" && request.errorMessage && (_jsxs("span", { className: "text-danger", children: [" \u2014 ", request.errorMessage] }))] }), _jsx("span", { className: `text-xs font-bold uppercase tracking-wide py-0.5 px-2 rounded-pill ${statusBadgeClass(request.status)}`, children: statusLabel(request.status) })] }, request.uid))) }))] }));
|
|
167
164
|
}
|
|
168
165
|
function ErasureSection({ mailboxLabel }) {
|
|
169
166
|
const [requests, setRequests] = useState([]);
|
|
@@ -5,11 +5,10 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
5
5
|
///////////////////////////////////////////////////////////////////////////////
|
|
6
6
|
import { routedPage } from "../../_routedPage.js";
|
|
7
7
|
import { useState } from "react";
|
|
8
|
-
import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
|
|
9
8
|
import { updateMailbox } from "@rapidmx/react-shared/mail/mailApi.js";
|
|
10
9
|
import SettingsShell, { useSettingsShell } from "../../../shared/components/settings/layout/SettingsShell.js";
|
|
11
|
-
import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
|
|
12
10
|
import Button from "@rapidmx/react-shared/components/buttons/Button.js";
|
|
11
|
+
import { notifyApiError } from "../../../shared/notifications/apiErrors.js";
|
|
13
12
|
function SettingsReadReceiptsPage(props) {
|
|
14
13
|
return (_jsx(SettingsShell, { ...props, active: "read-receipts", children: _jsx(ReadReceiptsContent, {}) }));
|
|
15
14
|
}
|
|
@@ -26,12 +25,10 @@ function ReadReceiptsContent() {
|
|
|
26
25
|
const [autoSendReceiptsExternal, setAutoSendReceiptsExternal] = useState(mailbox.autoSendReceiptsExternal ?? false);
|
|
27
26
|
// See auto-reply/index.tsx's identical note - later saves must carry the version the previous save returned.
|
|
28
27
|
const [version, setVersion] = useState(mailbox.version);
|
|
29
|
-
const [error, setError] = useState(null);
|
|
30
28
|
const [saving, setSaving] = useState(false);
|
|
31
29
|
const [saved, setSaved] = useState(false);
|
|
32
30
|
async function handleSubmit(e) {
|
|
33
31
|
e.preventDefault();
|
|
34
|
-
setError(null);
|
|
35
32
|
setSaved(false);
|
|
36
33
|
setSaving(true);
|
|
37
34
|
try {
|
|
@@ -49,12 +46,12 @@ function ReadReceiptsContent() {
|
|
|
49
46
|
setSaved(true);
|
|
50
47
|
}
|
|
51
48
|
catch (err) {
|
|
52
|
-
|
|
49
|
+
notifyApiError(err, "Couldn't save the read receipt settings");
|
|
53
50
|
}
|
|
54
51
|
finally {
|
|
55
52
|
setSaving(false);
|
|
56
53
|
}
|
|
57
54
|
}
|
|
58
|
-
return (_jsx("div", { className: "flex-1 min-w-0 overflow-y-auto p-6", children: _jsxs("div", { className: "max-w-xl", children: [_jsx("h1", { className: "text-lg font-bold tracking-tight mb-1", children: "Read Receipts" }), _jsx("p", { className: "text-sm text-text-muted mb-4", children: "Controls when this mailbox requests a delivery/read receipt on mail it sends, and how it responds to a receipt requested of it. A per-message \u201CRequest a read receipt\u201D checkbox in Compose always overrides the request-side settings below for that one message." }),
|
|
55
|
+
return (_jsx("div", { className: "flex-1 min-w-0 overflow-y-auto p-6", children: _jsxs("div", { className: "max-w-xl", children: [_jsx("h1", { className: "text-lg font-bold tracking-tight mb-1", children: "Read Receipts" }), _jsx("p", { className: "text-sm text-text-muted mb-4", children: "Controls when this mailbox requests a delivery/read receipt on mail it sends, and how it responds to a receipt requested of it. A per-message \u201CRequest a read receipt\u201D checkbox in Compose always overrides the request-side settings below for that one message." }), saved && _jsx("div", { className: "mb-4 text-sm text-success font-medium", children: "Saved." }), _jsxs("form", { onSubmit: handleSubmit, className: "flex flex-col gap-5", children: [_jsxs("fieldset", { className: "flex flex-col gap-2", children: [_jsx("legend", { className: "text-sm font-semibold mb-1", children: "Request receipts when sending" }), _jsxs("label", { className: "flex items-center gap-2 text-sm", children: [_jsx("input", { type: "checkbox", checked: alwaysRequestReceiptInternal, onChange: (e) => setAlwaysRequestReceiptInternal(e.target.checked) }), "From internal recipients (mail to this organization)"] }), _jsxs("label", { className: "flex items-center gap-2 text-sm", children: [_jsx("input", { type: "checkbox", checked: alwaysRequestReceiptFederated, onChange: (e) => setAlwaysRequestReceiptFederated(e.target.checked) }), "From federated recipients (partner organizations)"] }), _jsxs("label", { className: "flex items-center gap-2 text-sm", children: [_jsx("input", { type: "checkbox", checked: alwaysRequestReceiptExternal, onChange: (e) => setAlwaysRequestReceiptExternal(e.target.checked) }), "From external recipients"] })] }), _jsxs("fieldset", { className: "flex flex-col gap-2", children: [_jsx("legend", { className: "text-sm font-semibold mb-1", children: "Respond automatically to requests" }), _jsxs("label", { className: "flex items-center gap-2 text-sm", children: [_jsx("input", { type: "checkbox", checked: autoSendReceiptsInternal, onChange: (e) => setAutoSendReceiptsInternal(e.target.checked) }), "From internal senders"] }), _jsxs("label", { className: "flex items-center gap-2 text-sm", children: [_jsx("input", { type: "checkbox", checked: autoSendReceiptsFederated, onChange: (e) => setAutoSendReceiptsFederated(e.target.checked) }), "From federated senders (partner organizations)"] }), _jsxs("label", { className: "flex items-center gap-2 text-sm", children: [_jsx("input", { type: "checkbox", checked: autoSendReceiptsExternal, onChange: (e) => setAutoSendReceiptsExternal(e.target.checked) }), "From external senders"] }), _jsx("p", { className: "text-xs text-text-muted", children: "Unchecked means a request from that sender waits in your Inbox for you to send or decline it explicitly." })] }), _jsx("div", { children: _jsx(Button, { type: "submit", loading: saving, disabled: saving, className: "!w-auto", children: "Save" }) })] })] }) }));
|
|
59
56
|
}
|
|
60
57
|
export default routedPage("/settings/read-receipts", SettingsReadReceiptsPage);
|
|
@@ -6,12 +6,14 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
6
6
|
import { routedPage } from "../../_routedPage.js";
|
|
7
7
|
import { useEffect, useState } from "react";
|
|
8
8
|
import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
|
|
9
|
-
import { listMailboxAccess,
|
|
9
|
+
import { listMailboxAccess, removeMailboxAccess, setMailboxAccess, } from "@rapidmx/react-shared/mail/mailboxAccessApi.js";
|
|
10
10
|
import SettingsShell, { useSettingsShell } from "../../../shared/components/settings/layout/SettingsShell.js";
|
|
11
|
+
import PrincipalPicker from "../../../shared/components/sharing/PrincipalPicker.js";
|
|
11
12
|
import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
|
|
12
13
|
import Button from "@rapidmx/react-shared/components/buttons/Button.js";
|
|
13
14
|
import Modal from "@rapidmx/react-shared/components/overlays/Modal.js";
|
|
14
15
|
import { SkeletonList } from "@rapidmx/react-shared/components/feedback/Skeleton.js";
|
|
16
|
+
import { notifyApiError } from "../../../shared/notifications/apiErrors.js";
|
|
15
17
|
const ROLE_LABELS = { viewer: "Can view", manager: "Can manage" };
|
|
16
18
|
function SettingsSharingPage(props) {
|
|
17
19
|
return (_jsx(SettingsShell, { ...props, active: "sharing", children: _jsx(SharingContent, {}) }));
|
|
@@ -30,10 +32,8 @@ function SharingContent() {
|
|
|
30
32
|
const [members, setMembers] = useState([]);
|
|
31
33
|
const [status, setStatus] = useState("loading");
|
|
32
34
|
const [error, setError] = useState(null);
|
|
33
|
-
|
|
34
|
-
const [
|
|
35
|
-
const [adding, setAdding] = useState(false);
|
|
36
|
-
const [addError, setAddError] = useState(null);
|
|
35
|
+
// An entry that is not a user uid, being replaced with the user it was meant for.
|
|
36
|
+
const [replacing, setReplacing] = useState(null);
|
|
37
37
|
const [pendingRoleChange, setPendingRoleChange] = useState(null);
|
|
38
38
|
const [removeTarget, setRemoveTarget] = useState(null);
|
|
39
39
|
const [removing, setRemoving] = useState(false);
|
|
@@ -61,41 +61,28 @@ function SharingContent() {
|
|
|
61
61
|
});
|
|
62
62
|
}
|
|
63
63
|
useEffect(reload, [mailboxUid]);
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
if (!found) {
|
|
75
|
-
setAddError("No one found with that email on this platform.");
|
|
76
|
-
return;
|
|
64
|
+
// Once the person is granted, the entry they replace (if any) is removed: the string it named never matched anyone.
|
|
65
|
+
async function handleGranted() {
|
|
66
|
+
const stale = replacing;
|
|
67
|
+
setReplacing(null);
|
|
68
|
+
if (stale) {
|
|
69
|
+
try {
|
|
70
|
+
await removeMailboxAccess(mailboxUid, stale.userOrRoleId);
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
notifyApiError(err, "Couldn't remove the old entry");
|
|
77
74
|
}
|
|
78
|
-
await setMailboxAccess(mailboxUid, found.userUid, newRole);
|
|
79
|
-
setEmailInput("");
|
|
80
|
-
setNewRole("viewer");
|
|
81
|
-
reload();
|
|
82
|
-
}
|
|
83
|
-
catch (err) {
|
|
84
|
-
setAddError(err instanceof ApiRequestError ? err.message : "Could not grant access.");
|
|
85
|
-
}
|
|
86
|
-
finally {
|
|
87
|
-
setAdding(false);
|
|
88
75
|
}
|
|
76
|
+
reload();
|
|
89
77
|
}
|
|
90
78
|
async function handleRoleChange(userOrRoleId, role) {
|
|
91
79
|
setPendingRoleChange(userOrRoleId);
|
|
92
|
-
setError(null);
|
|
93
80
|
try {
|
|
94
81
|
await setMailboxAccess(mailboxUid, userOrRoleId, role);
|
|
95
82
|
reload();
|
|
96
83
|
}
|
|
97
84
|
catch (err) {
|
|
98
|
-
|
|
85
|
+
notifyApiError(err, "Couldn't change this member's role");
|
|
99
86
|
}
|
|
100
87
|
finally {
|
|
101
88
|
setPendingRoleChange(null);
|
|
@@ -108,19 +95,18 @@ function SharingContent() {
|
|
|
108
95
|
// Only reachable from the confirm modal's own "Remove" button, and `Modal` renders nothing at all
|
|
109
96
|
// unless `removeTarget` is set, so `removeTarget` is always non-null here.
|
|
110
97
|
setRemoving(true);
|
|
111
|
-
setError(null);
|
|
112
98
|
try {
|
|
113
99
|
await removeMailboxAccess(mailboxUid, removeTarget.userOrRoleId);
|
|
114
100
|
setRemoveTarget(null);
|
|
115
101
|
reload();
|
|
116
102
|
}
|
|
117
103
|
catch (err) {
|
|
118
|
-
|
|
104
|
+
notifyApiError(err, "Couldn't remove this member");
|
|
119
105
|
}
|
|
120
106
|
finally {
|
|
121
107
|
setRemoving(false);
|
|
122
108
|
}
|
|
123
109
|
}
|
|
124
|
-
return (_jsx("div", { className: "flex-1 min-w-0 overflow-y-auto p-6", children: _jsxs("div", { className: "max-w-xl flex flex-col gap-6", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-lg font-bold tracking-tight mb-1", children: "Sharing" }), _jsxs("p", { className: "text-sm text-text-muted", children: ["Manage who else can access ", mailbox?.displayName ?? "this mailbox", "\u2019s mail, calendar, contacts, and tasks. Granting access here covers all of it at once - there\u2019s nothing else to configure per data type."] })] }), status === "loading" && _jsx(SkeletonList, { count: 3 }), status === "forbidden" && (_jsx(Alert, { children: "You don\u2019t have permission to manage sharing for this mailbox." })), status === "error" && error && _jsx(Alert, { children: error }), status === "ready" && (_jsxs(_Fragment, { children: [
|
|
110
|
+
return (_jsx("div", { className: "flex-1 min-w-0 overflow-y-auto p-6", children: _jsxs("div", { className: "max-w-xl flex flex-col gap-6", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-lg font-bold tracking-tight mb-1", children: "Sharing" }), _jsxs("p", { className: "text-sm text-text-muted", children: ["Manage who else can access ", mailbox?.displayName ?? "this mailbox", "\u2019s mail, calendar, contacts, and tasks. Granting access here covers all of it at once - there\u2019s nothing else to configure per data type."] })] }), status === "loading" && _jsx(SkeletonList, { count: 3 }), status === "forbidden" && (_jsx(Alert, { children: "You don\u2019t have permission to manage sharing for this mailbox." })), status === "error" && error && _jsx(Alert, { children: error }), status === "ready" && (_jsxs(_Fragment, { children: [_jsxs("div", { children: [_jsx("h2", { className: "text-sm font-semibold mb-2", children: "People with access" }), members.length === 0 ? (_jsx("p", { className: "text-sm text-text-muted", children: "No one else has access to this mailbox yet." })) : (_jsx("ul", { className: "flex flex-col gap-2", children: members.map((member) => (_jsxs("li", { className: "flex items-center justify-between gap-3 text-sm py-1.5 px-3 bg-surface-alt rounded-sm", children: [_jsxs("span", { className: "truncate", children: [member.userOrRoleId, member.noEffect && (_jsx("span", { className: "block text-xs text-danger", children: "Not a user - this entry has no effect. Replace it with the person it was meant for, or remove it." }))] }), _jsxs("span", { className: "flex items-center gap-2 shrink-0", children: [member.noEffect && (_jsx(Button, { type: "button", variant: "text", className: "!w-auto", onClick: () => setReplacing(member), children: "Replace with a user" })), _jsxs("select", { "aria-label": `Role for ${member.userOrRoleId}`, className: "text-sm border border-border rounded-sm py-1 px-2 bg-surface", value: member.role, disabled: pendingRoleChange === member.userOrRoleId, onChange: (e) => handleRoleChange(member.userOrRoleId, e.target.value), children: [member.role === "custom" && (_jsx("option", { value: "custom", disabled: true, children: "Custom access" })), Object.keys(ROLE_LABELS).map((role) => (_jsx("option", { value: role, children: ROLE_LABELS[role] }, role)))] }), _jsx(Button, { type: "button", variant: "text", className: "!w-auto text-danger", onClick: () => setRemoveTarget(member), children: "Remove" })] })] }, member.userOrRoleId))) }))] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx("h2", { className: "text-sm font-semibold", children: replacing ? `Replace ${replacing.userOrRoleId}` : "Add someone" }), _jsx(PrincipalPicker, { mailboxUid: mailboxUid, roleLabels: ROLE_LABELS, initialPrincipal: replacing?.userOrRoleId, defaultRole: replacing?.role === "manager" ? "manager" : "viewer", placeholder: "Email address or username", onGranted: handleGranted, onCancel: replacing ? () => setReplacing(null) : undefined }, replacing?.userOrRoleId ?? "new")] })] })), _jsxs(Modal, { open: !!removeTarget, onClose: closeRemoveModal, title: "Remove access", children: [_jsxs("p", { className: "text-sm mb-5", children: [removeTarget?.userOrRoleId, " will no longer be able to access ", mailbox?.displayName ?? "this mailbox", "."] }), _jsxs("div", { className: "flex gap-3 justify-end", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: removing, onClick: closeRemoveModal, children: "Cancel" }), _jsx(Button, { type: "button", className: "!w-auto !bg-none !bg-danger !border-danger hover:!bg-danger", loading: removing, disabled: removing, onClick: handleRemove, children: "Remove" })] })] })] }) }));
|
|
125
111
|
}
|
|
126
112
|
export default routedPage("/settings/sharing", SettingsSharingPage);
|