@rapidmx/web-client 0.10.0 → 0.11.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/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/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/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/PrincipalPicker.tsx +145 -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/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/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/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/PrincipalPicker.d.ts +26 -0
- package/dist/apps/shared/components/sharing/PrincipalPicker.js +77 -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
|
@@ -11,6 +11,8 @@ import {
|
|
|
11
11
|
HiOutlineExclamationTriangle,
|
|
12
12
|
HiOutlineFolderArrowDown,
|
|
13
13
|
HiOutlineLockClosed,
|
|
14
|
+
HiOutlineMoon,
|
|
15
|
+
HiOutlineSun,
|
|
14
16
|
} from "react-icons/hi2";
|
|
15
17
|
import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
|
|
16
18
|
import {
|
|
@@ -40,7 +42,6 @@ import {
|
|
|
40
42
|
forwardSubject,
|
|
41
43
|
replySubject,
|
|
42
44
|
} from "@rapidmx/react-shared/mail/compose/composeQuoting.js";
|
|
43
|
-
import { sanitizeMessageBodyHtml } from "@rapidmx/react-shared/mail/messageBodySanitizer.js";
|
|
44
45
|
import MoveToFolderDialog from "./MoveToFolderDialog.js";
|
|
45
46
|
import { getUnlockedKeys, subscribeKeySession } from "@rapidmx/react-shared/crypto/keySession.js";
|
|
46
47
|
import type { MessageSecurityResult, SignatureFailureReason } from "@rapidmx/react-shared/crypto/messageSecurity.js";
|
|
@@ -56,7 +57,7 @@ import { ComposeLateInput, prefetchComposeWindow, useCompose } from "./compose/C
|
|
|
56
57
|
import { loadOriginalMessage, prefetchOriginalMessage } from "./compose/quotedBody.js";
|
|
57
58
|
import { formatRecipient } from "./compose/recipients.js";
|
|
58
59
|
import { formatMailAddress } from "@rapidmx/react-shared/mail/mailAddress.js";
|
|
59
|
-
import { RecipientLine } from "./MailAddress.js";
|
|
60
|
+
import MailAddress, { RecipientLine } from "./MailAddress.js";
|
|
60
61
|
import { useMailShell } from "./layout/MailShell.js";
|
|
61
62
|
import { ariaKeyShortcuts, withHint } from "../../keyboard/format.js";
|
|
62
63
|
import { SHORTCUTS, ShortcutDef } from "../../keyboard/keymap.js";
|
|
@@ -68,6 +69,13 @@ import Modal from "@rapidmx/react-shared/components/overlays/Modal.js";
|
|
|
68
69
|
import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
|
|
69
70
|
import LabelMenuButton from "./labelMenu.js";
|
|
70
71
|
import Button from "@rapidmx/react-shared/components/buttons/Button.js";
|
|
72
|
+
import EncryptedBody from "./reading/EncryptedBody.js";
|
|
73
|
+
import { displaySubject } from "./reading/EncryptedPreview.js";
|
|
74
|
+
import MessageBody, { BodySkeleton } from "./reading/MessageBody.js";
|
|
75
|
+
import { BODY_FONT_STYLE, CardShell, SenderAvatar, SubjectCard } from "./reading/MessageCard.js";
|
|
76
|
+
import type { BodyContent } from "./reading/bodyContent.js";
|
|
77
|
+
import { useViewOriginal } from "./reading/viewOriginal.js";
|
|
78
|
+
import { ROW_FOCUS_CLASS, UnreadLabel, dateClass, senderClass } from "./unreadStyle.js";
|
|
71
79
|
|
|
72
80
|
/** Labels/styling for `specs/end-to-end_encryption.md`'s "Message Security Indicators" table - kept as
|
|
73
81
|
* plain data (not JSX) so `SecurityIndicator` below stays a trivial lookup. "Signature failed" MUST NOT
|
|
@@ -229,7 +237,14 @@ function IconAction({
|
|
|
229
237
|
function SecurityIndicator({ security }: { security: MessageSecurityResult }) {
|
|
230
238
|
const { label, className } = SECURITY_INDICATOR[security.state];
|
|
231
239
|
if (security.state !== "verified_at_first_open") {
|
|
232
|
-
|
|
240
|
+
// Every encrypted state carries a small lock, so the badge reads at a glance; the words are unchanged.
|
|
241
|
+
const encrypted = security.state.startsWith("encrypted");
|
|
242
|
+
return (
|
|
243
|
+
<span className={`inline-flex items-center gap-1 text-xs font-medium shrink-0 py-1 px-2.5 rounded-pill ${className}`}>
|
|
244
|
+
{encrypted && <HiOutlineLockClosed size={12} aria-hidden="true" />}
|
|
245
|
+
{label}
|
|
246
|
+
</span>
|
|
247
|
+
);
|
|
233
248
|
}
|
|
234
249
|
return (
|
|
235
250
|
<span
|
|
@@ -267,18 +282,6 @@ const GENERIC_SIGNATURE_FAILURE_MESSAGE = "This message's digital signature coul
|
|
|
267
282
|
* message must never be mislabeled "Unprotected" just because loading failed. */
|
|
268
283
|
const ENCRYPTED_LOAD_ERROR = "Couldn't load this message's encrypted content.";
|
|
269
284
|
|
|
270
|
-
/** Content Security Policy prepended to every client-rendered (decrypted/verified) body's `srcDoc` - the
|
|
271
|
-
* backstop behind the sanitizer below: nothing in the document may load any remote resource at all, only
|
|
272
|
-
* inline styles and `data:`/`cid:` images. */
|
|
273
|
-
const BODY_CSP_META =
|
|
274
|
-
"<meta http-equiv=\"Content-Security-Policy\" content=\"default-src 'none'; img-src data: cid:; style-src 'unsafe-inline'\">";
|
|
275
|
-
|
|
276
|
-
/** Sanitizes a client-rendered body for `srcDoc` (DOMPurify plus every remote resource reference stripped - see
|
|
277
|
-
* react-shared's `messageBodySanitizer.ts`), with the CSP meta as its very first element. */
|
|
278
|
-
export function buildSecureSrcDoc(html: string): string {
|
|
279
|
-
return BODY_CSP_META + sanitizeMessageBodyHtml(html);
|
|
280
|
-
}
|
|
281
|
-
|
|
282
285
|
export interface MessageDetailPaneProps {
|
|
283
286
|
message: Message | null;
|
|
284
287
|
attachments: Attachment[];
|
|
@@ -338,10 +341,23 @@ export interface MessageDetailPaneProps {
|
|
|
338
341
|
/** Handed a label created from the Labels menu, for the caller to add to `labels`. Without it the menu
|
|
339
342
|
* offers no "New label" row, only the link to where labels are managed. */
|
|
340
343
|
onLabelCreated?: (label: Label) => void;
|
|
341
|
-
/** Rendered as one message of a thread (`ConversationThreadPane`) rather than as the pane itself: the
|
|
342
|
-
* subject
|
|
343
|
-
*
|
|
344
|
+
/** Rendered as one message of a thread (`ConversationThreadPane`) rather than as the pane itself: only its card is drawn - the thread
|
|
345
|
+
* owns the subject card, the scrolling and the `h1` - and the message's own subject appears (as an `h3`) only when it differs from the
|
|
346
|
+
* thread's (`threadSubject`). Everything else - the badges, the actions, the body - is identical. */
|
|
344
347
|
inThread?: boolean;
|
|
348
|
+
/** The thread's subject, for a message of a thread to tell whether its own subject is worth showing. */
|
|
349
|
+
threadSubject?: string;
|
|
350
|
+
/** In a thread, what makes the sender line of this expanded card the button that collapses it: the id of the body it controls, whether the
|
|
351
|
+
* message is unread (which draws the accent bar and tint), what it does, and where to put its ref. */
|
|
352
|
+
threadHeader?: {
|
|
353
|
+
bodyId: string;
|
|
354
|
+
unread: boolean;
|
|
355
|
+
onToggle: () => void;
|
|
356
|
+
buttonRef: (node: HTMLButtonElement | null) => void;
|
|
357
|
+
};
|
|
358
|
+
/** Draw the Reply and Forward buttons at the foot of the card - by default for the single-message pane, and in a thread only where the
|
|
359
|
+
* caller asks (the newest expanded message). */
|
|
360
|
+
footer?: boolean;
|
|
345
361
|
/**
|
|
346
362
|
* Registers this pane's keyboard shortcuts - Reply, Reply all, Forward, Archive, Move to - so they act on this message. Only the message
|
|
347
363
|
* the keyboard is acting on should set it: the one pane beside the list, or the opened message of a thread (each expanded message of a
|
|
@@ -350,6 +366,16 @@ export interface MessageDetailPaneProps {
|
|
|
350
366
|
shortcuts?: boolean;
|
|
351
367
|
}
|
|
352
368
|
|
|
369
|
+
/** A subject without its \`Re:\`/\`Fwd:\` prefixes, lower-cased - what makes two messages of one thread the same subject. */
|
|
370
|
+
function subjectCore(subject: string | undefined): string {
|
|
371
|
+
return (subject ?? "").replace(/^(\s*(re|fw|fwd|aw|sv)\s*:)+/i, "").trim().toLowerCase();
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/** Whether a message's subject says nothing its thread's subject doesn't. */
|
|
375
|
+
function sameSubject(subject: string, threadSubject: string | undefined): boolean {
|
|
376
|
+
return subjectCore(subject) === subjectCore(threadSubject);
|
|
377
|
+
}
|
|
378
|
+
|
|
353
379
|
function formatBytes(bytes: number): string {
|
|
354
380
|
if (bytes >= 1_000_000) return `${(bytes / 1_000_000).toFixed(1)} MB`;
|
|
355
381
|
if (bytes >= 1_000) return `${(bytes / 1_000).toFixed(1)} KB`;
|
|
@@ -357,7 +383,8 @@ function formatBytes(bytes: number): string {
|
|
|
357
383
|
}
|
|
358
384
|
|
|
359
385
|
/**
|
|
360
|
-
* A message's reading pane
|
|
386
|
+
* A message's reading pane - a subject card, and under it the message as a card: header (sender, recipients, time, actions), notices, attachments,
|
|
387
|
+
* the body (`reading/MessageBody`: an isolated frame exactly as tall as its content, or text) and a footer. Rendered with `inThread`, only the card.
|
|
361
388
|
* Shared by the desktop inline pane (`apps/www/index.tsx`, always visible alongside the message list),
|
|
362
389
|
* and the mobile detail route (`apps/www/messages/[uid].tsx`, a full page on its own reached by tapping
|
|
363
390
|
* a message row) — see each call site for how `message`/`attachments`/`isSentItems` are sourced. The
|
|
@@ -399,6 +426,9 @@ function MessageDetailContent({
|
|
|
399
426
|
onLabelsChanged,
|
|
400
427
|
onLabelCreated,
|
|
401
428
|
inThread,
|
|
429
|
+
threadSubject,
|
|
430
|
+
threadHeader,
|
|
431
|
+
footer,
|
|
402
432
|
shortcuts,
|
|
403
433
|
}: MessageDetailPaneProps & { message: Message }) {
|
|
404
434
|
// A copy re-read from the server after an Outbox action was refused (409/403) or a send lease ran out - see
|
|
@@ -450,6 +480,14 @@ function MessageDetailContent({
|
|
|
450
480
|
// effect could read reactively otherwise (getUnlockedKeys() is a plain module-level read, not React
|
|
451
481
|
// state; see keySession.ts's own doc comment).
|
|
452
482
|
const [unlockRefresh, setUnlockRefresh] = useState(0);
|
|
483
|
+
// "View original": this message exactly as its author wrote it instead of adapted to the theme, remembered for the session. Offered only
|
|
484
|
+
// once the body says adapting would change something (`MessageBody`'s `onAdaptable`), or when the reader already chose it.
|
|
485
|
+
const [viewOriginal, setViewOriginal] = useViewOriginal(messageProp.uid);
|
|
486
|
+
const [adaptable, setAdaptable] = useState(viewOriginal);
|
|
487
|
+
// The locked body's Unlock button is waiting on the unlock prompt; and, once it has answered, the focus is to move to the body region.
|
|
488
|
+
const [unlocking, setUnlocking] = useState(false);
|
|
489
|
+
const focusBodyRef = useRef(false);
|
|
490
|
+
const bodyRegionRef = useRef<HTMLDivElement>(null);
|
|
453
491
|
|
|
454
492
|
// The newest copy of this message known to the Labels popover - normally just the `message` prop,
|
|
455
493
|
// but a successful label toggle's server response is held here too, so a follow-up toggle always
|
|
@@ -576,15 +614,34 @@ function MessageDetailContent({
|
|
|
576
614
|
};
|
|
577
615
|
}, [message.uid, message.mailboxUid, message.from.address, message.encrypted, rawEvaluationNeeded, unlockRefresh, readerAddressesKey]);
|
|
578
616
|
|
|
617
|
+
// Whether the body is the locked state right now (an encrypted message this device holds no unlocked keys for), for the subscription below.
|
|
618
|
+
const lockedRef = useRef(false);
|
|
619
|
+
lockedRef.current = security?.decryptError !== undefined && !getUnlockedKeys(message.mailboxUid);
|
|
620
|
+
|
|
621
|
+
// After an unlock from the button, the message (or the reason it can't be read) is where the reader's attention is: the focus moves to it once it is
|
|
622
|
+
// there, since the button that had it is gone.
|
|
623
|
+
useEffect(() => {
|
|
624
|
+
if (focusBodyRef.current && security !== null) {
|
|
625
|
+
focusBodyRef.current = false;
|
|
626
|
+
bodyRegionRef.current?.focus({ preventScroll: true });
|
|
627
|
+
}
|
|
628
|
+
}, [security]);
|
|
629
|
+
|
|
579
630
|
// Decrypted plaintext must not outlive the key session that produced it: the moment this mailbox's
|
|
580
631
|
// keys are destroyed (logout, idle timeout, explicit lock), drop the recovered html/text and
|
|
581
|
-
// re-evaluate - an encrypted message then falls back to its
|
|
632
|
+
// re-evaluate - an encrypted message then falls back to its locked state. And the other way round: keys unlocked
|
|
633
|
+
// from anywhere (compose, another message, the unlock prompt) open a message that was waiting on them, in place.
|
|
582
634
|
useEffect(
|
|
583
635
|
() =>
|
|
584
636
|
subscribeKeySession((event) => {
|
|
585
|
-
if (event.mailboxUid
|
|
637
|
+
if (event.mailboxUid !== message.mailboxUid) {
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
if (event.state === "locked") {
|
|
586
641
|
setSecurity(null);
|
|
587
642
|
setUnlockRefresh((n) => n + 1);
|
|
643
|
+
} else if (lockedRef.current) {
|
|
644
|
+
setUnlockRefresh((n) => n + 1);
|
|
588
645
|
}
|
|
589
646
|
}),
|
|
590
647
|
[message.mailboxUid],
|
|
@@ -622,11 +679,16 @@ function MessageDetailContent({
|
|
|
622
679
|
async function handleUnlockToView() {
|
|
623
680
|
const mailboxUid = message.mailboxUid;
|
|
624
681
|
const mailboxKeys = mailboxes.find((mb) => mb.uid === mailboxUid)?.keys ?? [];
|
|
682
|
+
setUnlocking(true);
|
|
625
683
|
try {
|
|
626
684
|
await requestUnlock(mailboxUid, mailboxKeys);
|
|
685
|
+
// The Unlock button is about to be replaced by the message (or by why it can't be read): the focus goes there, once it has been evaluated.
|
|
686
|
+
focusBodyRef.current = true;
|
|
627
687
|
setUnlockRefresh((n) => n + 1);
|
|
628
688
|
} catch {
|
|
629
689
|
// User dismissed the unlock dialog - security state stays exactly as it was.
|
|
690
|
+
} finally {
|
|
691
|
+
setUnlocking(false);
|
|
630
692
|
}
|
|
631
693
|
}
|
|
632
694
|
|
|
@@ -915,113 +977,207 @@ function MessageDetailContent({
|
|
|
915
977
|
? senderKeyState!.conflict
|
|
916
978
|
: undefined;
|
|
917
979
|
|
|
918
|
-
const shownSubject = (protectedSubject ?? message.subject) || "(no subject)";
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
*/
|
|
934
|
-
const bodyClassName = "flex-1 min-h-0 w-full";
|
|
980
|
+
const shownSubject = (protectedSubject ?? displaySubject(message.subject)) || "(no subject)";
|
|
981
|
+
// What the body area shows. Text or HTML the client recovered from a signed or encrypted message is shown as it is (its own sanitizing
|
|
982
|
+
// happens in `MessageBody`, like every body's); anything else is the server's own `/content`. An encrypted message has nothing worth
|
|
983
|
+
// fetching until it has been decrypted (the server only has the ciphertext), so it shows the skeleton meanwhile.
|
|
984
|
+
const bodyContent: BodyContent | undefined =
|
|
985
|
+
security?.text !== undefined
|
|
986
|
+
? { kind: "text", text: security.text }
|
|
987
|
+
: security?.html !== undefined
|
|
988
|
+
? { kind: "html", html: security.html }
|
|
989
|
+
: undefined;
|
|
990
|
+
const bodyTitle = message.subject || "Message content";
|
|
991
|
+
const answered = message.flags.answered === true;
|
|
992
|
+
const forwarded = message.flags.forwarded === true;
|
|
993
|
+
const cardUnread = threadHeader?.unread ?? false;
|
|
994
|
+
const showFooter = footer ?? !inThread;
|
|
935
995
|
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
996
|
+
const senderRecipient = { displayName: senderName, address: senderAddress };
|
|
997
|
+
const sendingStatus = (
|
|
998
|
+
<>
|
|
999
|
+
{sendInProgress && (
|
|
1000
|
+
<span className="text-xs font-medium text-text-muted shrink-0 py-1 px-2.5 rounded-pill bg-surface-alt">Sending…</span>
|
|
1001
|
+
)}
|
|
1002
|
+
{isSentItems &&
|
|
1003
|
+
(message.recallRequestedAt ? (
|
|
1004
|
+
<span className="text-xs font-medium text-text-muted shrink-0 py-1 px-2.5 rounded-pill bg-surface-alt">Recall requested</span>
|
|
1005
|
+
) : (
|
|
1006
|
+
<Button type="button" variant="secondary" className="!w-auto shrink-0" onClick={() => setConfirming(true)}>
|
|
1007
|
+
Recall this message
|
|
1008
|
+
</Button>
|
|
1009
|
+
))}
|
|
1010
|
+
{inOutbox && !sendInProgress && message.scheduledSendTime && (
|
|
1011
|
+
<div className="flex items-center gap-2 shrink-0">
|
|
1012
|
+
<span className="text-xs font-medium text-text-muted py-1 px-2.5 rounded-pill bg-surface-alt">
|
|
1013
|
+
Scheduled for {new Date(message.scheduledSendTime).toLocaleString()}
|
|
1014
|
+
</span>
|
|
1015
|
+
<Button type="button" variant="secondary" className="!w-auto" loading={canceling} disabled={canceling} onClick={handleCancelScheduledSend}>
|
|
1016
|
+
Cancel
|
|
1017
|
+
</Button>
|
|
1018
|
+
</div>
|
|
1019
|
+
)}
|
|
1020
|
+
{/* A message left in Outbox with no active schedule - a scheduled send that failed or was refused
|
|
1021
|
+
(restapi's ScheduledSendJob clears scheduledSendTime and leaves it there), which can't be sent
|
|
1022
|
+
again from Outbox (409) or archived - would otherwise be stuck. Moving it back to Drafts works for
|
|
1023
|
+
any Outbox message. */}
|
|
1024
|
+
{inOutbox && !sendInProgress && !message.scheduledSendTime && draftsFolderUid && (
|
|
1025
|
+
<Button type="button" variant="secondary" className="!w-auto shrink-0" loading={canceling} disabled={canceling} onClick={handleCancelScheduledSend}>
|
|
1026
|
+
Move to Drafts
|
|
1027
|
+
</Button>
|
|
1028
|
+
)}
|
|
1029
|
+
</>
|
|
1030
|
+
);
|
|
1031
|
+
|
|
1032
|
+
const card = (
|
|
1033
|
+
<CardShell unread={cardUnread}>
|
|
1034
|
+
{/* Header row: who it is from, when, and what can be done with it. Wraps: on a phone the actions drop under the sender. */}
|
|
1035
|
+
<div className={["flex flex-wrap items-start gap-x-3 gap-y-1 px-4 pt-3 pb-2", cardUnread ? "bg-primary/[0.07]" : ""].join(" ")}>
|
|
1036
|
+
<SenderAvatar from={senderRecipient} />
|
|
1037
|
+
<div className="flex-1 min-w-[12rem]">
|
|
1038
|
+
<div className="flex flex-wrap items-center gap-x-2 gap-y-1">
|
|
1039
|
+
{threadHeader ? (
|
|
1040
|
+
// The sender line of a message in a thread is the button that collapses it again.
|
|
1041
|
+
<h2 className="min-w-0 flex-1" style={BODY_FONT_STYLE}>
|
|
1042
|
+
<button
|
|
1043
|
+
type="button"
|
|
1044
|
+
ref={threadHeader.buttonRef}
|
|
1045
|
+
onClick={threadHeader.onToggle}
|
|
1046
|
+
aria-expanded={true}
|
|
1047
|
+
aria-controls={threadHeader.bodyId}
|
|
1048
|
+
className={[
|
|
1049
|
+
"w-full text-left rounded-sm text-sm",
|
|
1050
|
+
ROW_FOCUS_CLASS,
|
|
1051
|
+
].join(" ")}
|
|
1052
|
+
>
|
|
1053
|
+
<UnreadLabel unread={cardUnread} />
|
|
1054
|
+
<MailAddress recipient={senderRecipient} className={senderClass(cardUnread)} />
|
|
1055
|
+
</button>
|
|
1056
|
+
</h2>
|
|
954
1057
|
) : (
|
|
955
|
-
<
|
|
1058
|
+
<p className="text-sm break-words min-w-0">
|
|
1059
|
+
From <span className="font-semibold text-text">{senderLabel}</span>
|
|
1060
|
+
</p>
|
|
956
1061
|
)}
|
|
957
1062
|
{security && <SecurityIndicator security={security} />}
|
|
958
1063
|
</div>
|
|
959
|
-
{
|
|
960
|
-
<
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
{isSentItems &&
|
|
965
|
-
(message.recallRequestedAt ? (
|
|
966
|
-
<span className="text-xs font-medium text-text-muted shrink-0 py-1 px-2.5 rounded-pill bg-surface-alt">
|
|
967
|
-
Recall requested
|
|
968
|
-
</span>
|
|
969
|
-
) : (
|
|
970
|
-
<Button
|
|
971
|
-
type="button"
|
|
972
|
-
variant="secondary"
|
|
973
|
-
className="!w-auto shrink-0"
|
|
974
|
-
onClick={() => setConfirming(true)}
|
|
975
|
-
>
|
|
976
|
-
Recall this message
|
|
977
|
-
</Button>
|
|
978
|
-
))}
|
|
979
|
-
{inOutbox && !sendInProgress && message.scheduledSendTime && (
|
|
980
|
-
<div className="flex items-center gap-2 shrink-0">
|
|
981
|
-
<span className="text-xs font-medium text-text-muted py-1 px-2.5 rounded-pill bg-surface-alt">
|
|
982
|
-
Scheduled for {new Date(message.scheduledSendTime).toLocaleString()}
|
|
983
|
-
</span>
|
|
984
|
-
<Button
|
|
985
|
-
type="button"
|
|
986
|
-
variant="secondary"
|
|
987
|
-
className="!w-auto"
|
|
988
|
-
loading={canceling}
|
|
989
|
-
disabled={canceling}
|
|
990
|
-
onClick={handleCancelScheduledSend}
|
|
991
|
-
>
|
|
992
|
-
Cancel
|
|
993
|
-
</Button>
|
|
994
|
-
</div>
|
|
1064
|
+
{senderNameCheck.misleading && (
|
|
1065
|
+
<p role="status" className="mt-2 py-2 px-3 rounded-sm text-sm bg-warning/15 text-text">
|
|
1066
|
+
The sender’s name “{senderName}” looks like an email address, but this message was
|
|
1067
|
+
sent from <span className="font-medium">{senderAddress}</span>. Don’t trust it based on the name.
|
|
1068
|
+
</p>
|
|
995
1069
|
)}
|
|
996
|
-
{/*
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1070
|
+
{/* Every recipient with their address, grouped as the sender addressed them; a long list folds. */}
|
|
1071
|
+
<RecipientLine label="To" recipients={message.recipients.filter((r) => r.type !== "cc" && r.type !== "bcc")} />
|
|
1072
|
+
<RecipientLine label="Cc" recipients={message.recipients.filter((r) => r.type === "cc")} />
|
|
1073
|
+
<RecipientLine label="Bcc" recipients={message.recipients.filter((r) => r.type === "bcc")} />
|
|
1074
|
+
</div>
|
|
1075
|
+
<div className="ml-auto flex flex-wrap items-center justify-end gap-1">
|
|
1076
|
+
<time dateTime={message.receivedDate} className={["text-xs mr-1", dateClass(cardUnread)].join(" ")}>
|
|
1077
|
+
{new Date(message.receivedDate).toLocaleString()}
|
|
1078
|
+
</time>
|
|
1079
|
+
{adaptable && (
|
|
1080
|
+
<button
|
|
1002
1081
|
type="button"
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1082
|
+
aria-label="View original"
|
|
1083
|
+
aria-pressed={viewOriginal}
|
|
1084
|
+
title={viewOriginal ? "Follow the theme" : "View original"}
|
|
1085
|
+
onClick={() => setViewOriginal(!viewOriginal)}
|
|
1086
|
+
className={[
|
|
1087
|
+
"inline-flex items-center justify-center p-1.5 rounded-md border border-border text-sm text-text hover:bg-surface-alt",
|
|
1088
|
+
viewOriginal ? "bg-primary/10" : "",
|
|
1089
|
+
].join(" ")}
|
|
1008
1090
|
>
|
|
1009
|
-
|
|
1010
|
-
</
|
|
1091
|
+
{viewOriginal ? <HiOutlineMoon size={16} aria-hidden="true" /> : <HiOutlineSun size={16} aria-hidden="true" />}
|
|
1092
|
+
</button>
|
|
1093
|
+
)}
|
|
1094
|
+
{/* Wraps: in a thread the pane can be as narrow as the reading pane gets (the list takes
|
|
1095
|
+
384px of it), and these are six controls. */}
|
|
1096
|
+
<IconAction
|
|
1097
|
+
icon={<HiOutlineArrowUturnLeft size={16} aria-hidden="true" />}
|
|
1098
|
+
label="Reply"
|
|
1099
|
+
shortcut={keyboard ? SHORTCUTS.mail.reply : undefined}
|
|
1100
|
+
disabled={preparingCompose}
|
|
1101
|
+
onPrefetch={prefetchReply}
|
|
1102
|
+
onClick={() => void handleReplyOrForward("reply")}
|
|
1103
|
+
/>
|
|
1104
|
+
<IconAction
|
|
1105
|
+
icon={
|
|
1106
|
+
// The conventional reply-all glyph: the reply arrow, doubled. `hi2` has no
|
|
1107
|
+
// reply-all icon of its own, and nothing else in it means "answer everyone".
|
|
1108
|
+
<span className="inline-flex items-center" aria-hidden="true">
|
|
1109
|
+
<HiOutlineArrowUturnLeft size={16} />
|
|
1110
|
+
<HiOutlineArrowUturnLeft size={16} className="-ml-2.5" />
|
|
1111
|
+
</span>
|
|
1112
|
+
}
|
|
1113
|
+
label="Reply All"
|
|
1114
|
+
shortcut={keyboard ? SHORTCUTS.mail.replyAll : undefined}
|
|
1115
|
+
disabled={preparingCompose}
|
|
1116
|
+
onPrefetch={prefetchReply}
|
|
1117
|
+
onClick={() => void handleReplyOrForward("replyAll")}
|
|
1118
|
+
/>
|
|
1119
|
+
<IconAction
|
|
1120
|
+
icon={<HiOutlineArrowUturnRight size={16} aria-hidden="true" />}
|
|
1121
|
+
label="Forward"
|
|
1122
|
+
shortcut={keyboard ? SHORTCUTS.mail.forward : undefined}
|
|
1123
|
+
disabled={preparingCompose}
|
|
1124
|
+
onPrefetch={prefetchReply}
|
|
1125
|
+
onClick={() => void handleReplyOrForward("forward")}
|
|
1126
|
+
/>
|
|
1127
|
+
{archivable && (
|
|
1128
|
+
<IconAction
|
|
1129
|
+
icon={<HiOutlineArchiveBox size={16} aria-hidden="true" />}
|
|
1130
|
+
label="Archive"
|
|
1131
|
+
shortcut={keyboard ? SHORTCUTS.mail.archive : undefined}
|
|
1132
|
+
busy={archiving}
|
|
1133
|
+
disabled={archiving}
|
|
1134
|
+
onClick={handleArchive}
|
|
1135
|
+
/>
|
|
1136
|
+
)}
|
|
1137
|
+
{movable && (
|
|
1138
|
+
<IconAction
|
|
1139
|
+
icon={<HiOutlineFolderArrowDown size={16} aria-hidden="true" />}
|
|
1140
|
+
label="Move to"
|
|
1141
|
+
shortcut={keyboard ? SHORTCUTS.mail.move : undefined}
|
|
1142
|
+
onClick={() => setMovePrompt(true)}
|
|
1143
|
+
/>
|
|
1144
|
+
)}
|
|
1145
|
+
{labels && labels.length > 0 && (
|
|
1146
|
+
<LabelMenuButton
|
|
1147
|
+
aria-label="Labels"
|
|
1148
|
+
label="Labels"
|
|
1149
|
+
className="border border-border py-1.5"
|
|
1150
|
+
labels={labels}
|
|
1151
|
+
mailboxUid={message.mailboxUid}
|
|
1152
|
+
onLabelCreated={onLabelCreated}
|
|
1153
|
+
applied={currentLabelsMessage.labelUids ?? []}
|
|
1154
|
+
onCommit={(labelUids) => void handleApplyLabels(labelUids)}
|
|
1155
|
+
busy={savingLabels}
|
|
1156
|
+
note="Ticked labels are applied to this message and unticked ones removed."
|
|
1157
|
+
emptyNote="This mailbox has no labels yet."
|
|
1158
|
+
commit={{ label: "Apply" }}
|
|
1159
|
+
clear={{ label: "Remove all labels" }}
|
|
1160
|
+
/>
|
|
1011
1161
|
)}
|
|
1012
1162
|
</div>
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1163
|
+
</div>
|
|
1164
|
+
|
|
1165
|
+
{/* A slim bar for what has been done with the message. */}
|
|
1166
|
+
{(answered || forwarded) && (
|
|
1167
|
+
<p className="px-4 py-1.5 text-xs text-text-muted bg-surface-alt border-y border-border">
|
|
1168
|
+
{answered && forwarded ? "You replied to and forwarded this message." : answered ? "You replied to this message." : "You forwarded this message."}
|
|
1169
|
+
</p>
|
|
1170
|
+
)}
|
|
1171
|
+
|
|
1172
|
+
<div id={threadHeader?.bodyId} className="px-4 pb-3 flex flex-col gap-2">
|
|
1173
|
+
{inThread && (subjectDiffers || !sameSubject(shownSubject, threadSubject)) && (
|
|
1174
|
+
<h3 className="text-sm font-semibold break-words">{shownSubject}</h3>
|
|
1022
1175
|
)}
|
|
1176
|
+
<div className="flex flex-wrap items-center gap-2 empty:hidden">{sendingStatus}</div>
|
|
1177
|
+
{inOutbox && message.scheduledSendError && <Alert>This message wasn’t sent: {message.scheduledSendError}</Alert>}
|
|
1178
|
+
{cancelError && <Alert>{cancelError}</Alert>}
|
|
1023
1179
|
{keyChangeNotice && (
|
|
1024
|
-
<p role="status" className="
|
|
1180
|
+
<p role="status" className="py-2 px-3 rounded-sm text-sm bg-surface-alt text-text">
|
|
1025
1181
|
{keyChangeNotice}
|
|
1026
1182
|
</p>
|
|
1027
1183
|
)}
|
|
@@ -1029,7 +1185,7 @@ function MessageDetailContent({
|
|
|
1029
1185
|
// Informational: the signature is intact, but nothing ties its certificate to this sender - anyone can
|
|
1030
1186
|
// create a certificate naming any address. "Trust this signer" (below) pins it once the reader has
|
|
1031
1187
|
// confirmed the fingerprint.
|
|
1032
|
-
<p role="status" className="
|
|
1188
|
+
<p role="status" className="py-2 px-3 rounded-sm text-sm bg-surface-alt text-text">
|
|
1033
1189
|
Signed, but the signer isn’t a trusted contact key, so the sender isn’t verified.
|
|
1034
1190
|
{security.signerEmails && security.signerEmails.length > 0 && <> Certificate for {security.signerEmails.join(", ")}.</>}
|
|
1035
1191
|
{security.signerFingerprint && (
|
|
@@ -1043,7 +1199,7 @@ function MessageDetailContent({
|
|
|
1043
1199
|
{pendingConflict && (
|
|
1044
1200
|
// Only an unverified signer's result loads the key state while the sender is unpinned, and a conflict
|
|
1045
1201
|
// always comes from a matching contact, so its uid is known.
|
|
1046
|
-
<p role="status" className="
|
|
1202
|
+
<p role="status" className="py-2 px-3 rounded-sm text-sm bg-warning/15 text-text">
|
|
1047
1203
|
This sender has a signing key change waiting for your review, so this signer can’t be trusted from
|
|
1048
1204
|
here.{" "}
|
|
1049
1205
|
<a
|
|
@@ -1055,7 +1211,7 @@ function MessageDetailContent({
|
|
|
1055
1211
|
</p>
|
|
1056
1212
|
)}
|
|
1057
1213
|
{trustableCertificate && (
|
|
1058
|
-
<div
|
|
1214
|
+
<div>
|
|
1059
1215
|
<Button
|
|
1060
1216
|
type="button"
|
|
1061
1217
|
variant="secondary"
|
|
@@ -1071,33 +1227,31 @@ function MessageDetailContent({
|
|
|
1071
1227
|
</div>
|
|
1072
1228
|
)}
|
|
1073
1229
|
{subjectDiffers && (
|
|
1074
|
-
<p role="status" className="
|
|
1230
|
+
<p role="status" className="py-2 px-3 rounded-sm text-sm bg-surface-alt text-text">
|
|
1075
1231
|
The subject shown above is the one the sender signed. It differs from the subject this message was
|
|
1076
1232
|
delivered with (“{message.subject}”), which may have been changed on the way, e.g. by a
|
|
1077
1233
|
mailing list.
|
|
1078
1234
|
</p>
|
|
1079
1235
|
)}
|
|
1080
1236
|
{security?.headerTamperDetected && (
|
|
1081
|
-
<
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
</Alert>
|
|
1087
|
-
</div>
|
|
1237
|
+
<Alert>
|
|
1238
|
+
This message's visible From/To/Cc/Date/Subject don't match what the sender actually signed or
|
|
1239
|
+
encrypted — an intermediary may have altered them after sending. Treat the fields shown above with
|
|
1240
|
+
caution.
|
|
1241
|
+
</Alert>
|
|
1088
1242
|
)}
|
|
1089
1243
|
{security?.state === "verified_at_first_open" && (
|
|
1090
1244
|
// A seal outranks only a key-status failure (see `evaluateMessageSecurityWithSeal()`). A signer key
|
|
1091
1245
|
// later reported compromised is a warning, never reassurance.
|
|
1092
1246
|
<p
|
|
1093
1247
|
role="status"
|
|
1094
|
-
className={`
|
|
1248
|
+
className={`py-2 px-3 rounded-sm text-sm text-text ${security.laterCompromised ? "bg-warning/15" : "bg-surface-alt"}`}
|
|
1095
1249
|
>
|
|
1096
1250
|
{verifiedAtFirstOpenMessage(security)}
|
|
1097
1251
|
</p>
|
|
1098
1252
|
)}
|
|
1099
1253
|
{keyChanged && (
|
|
1100
|
-
<section aria-label="Signing key changed" className="
|
|
1254
|
+
<section aria-label="Signing key changed" className="py-3 px-3 rounded-sm text-sm bg-warning/15 text-text flex flex-col gap-2">
|
|
1101
1255
|
<h2 className="font-semibold">This sender’s signing key changed</h2>
|
|
1102
1256
|
<p>
|
|
1103
1257
|
The signature on this message is valid and its certificate names {senderAddress}, but it was made
|
|
@@ -1127,7 +1281,7 @@ function MessageDetailContent({
|
|
|
1127
1281
|
{security?.state === "signature_failed" && !keyChanged && (
|
|
1128
1282
|
// Deliberately an informational notice, not an error `Alert`: an unverifiable signature
|
|
1129
1283
|
// means "don't trust the signer", not "this message is broken" - the body stays readable.
|
|
1130
|
-
<p role="status" className="
|
|
1284
|
+
<p role="status" className="py-2 px-3 rounded-sm text-sm bg-surface-alt text-text">
|
|
1131
1285
|
{security.signatureFailureReason
|
|
1132
1286
|
? SIGNATURE_FAILURE_MESSAGE[security.signatureFailureReason]
|
|
1133
1287
|
: GENERIC_SIGNATURE_FAILURE_MESSAGE}
|
|
@@ -1135,114 +1289,25 @@ function MessageDetailContent({
|
|
|
1135
1289
|
)}
|
|
1136
1290
|
{verifiedOrSealed && !security.protectedHeaders && (
|
|
1137
1291
|
// A legacy S/MIME sender signs only the body: the outer Subject/To/Cc shown here were never signed.
|
|
1138
|
-
<p role="status" className="
|
|
1292
|
+
<p role="status" className="py-2 px-3 rounded-sm text-sm bg-surface-alt text-text">
|
|
1139
1293
|
The signature covers this message’s content and attachments only. Its Subject, To and Cc
|
|
1140
1294
|
weren’t signed, so they could have been changed after it was sent.
|
|
1141
1295
|
</p>
|
|
1142
1296
|
)}
|
|
1143
|
-
{security?.notAddressedToReader && (
|
|
1144
|
-
// Informational, like the notice above: a Bcc recipient legitimately sees this too.
|
|
1145
|
-
|
|
1297
|
+
{security?.notAddressedToReader && !isSentItems && (
|
|
1298
|
+
// Informational, like the notice above: a Bcc recipient legitimately sees this too. Never shown in Sent
|
|
1299
|
+
// Items: the sender's own address is (by design, see ComposeWindow/sendJob) never one of the protected
|
|
1300
|
+
// To/Cc headers of a message it sent to someone else, so this would otherwise fire on every ordinary sent
|
|
1301
|
+
// message rather than the genuine forwarded/re-sent/Bcc cases it's meant to flag.
|
|
1302
|
+
<p role="status" className="py-2 px-3 rounded-sm text-sm bg-surface-alt text-text">
|
|
1146
1303
|
The recipients this message was signed or encrypted for don’t include this mailbox - it may have
|
|
1147
1304
|
been forwarded or re-sent to you unchanged, or you were Bcc’d.
|
|
1148
1305
|
</p>
|
|
1149
1306
|
)}
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
{new Date(message.receivedDate).toLocaleString()}
|
|
1153
|
-
</p>
|
|
1154
|
-
{senderNameCheck.misleading && (
|
|
1155
|
-
<p role="status" className="mt-2 py-2 px-3 rounded-sm text-sm bg-warning/15 text-text">
|
|
1156
|
-
The sender’s name “{senderName}” looks like an email address, but this message was
|
|
1157
|
-
sent from <span className="font-medium">{senderAddress}</span>. Don’t trust it based on the name.
|
|
1158
|
-
</p>
|
|
1159
|
-
)}
|
|
1160
|
-
{/* Every recipient with their address, grouped as the sender addressed them; a long list folds. */}
|
|
1161
|
-
<RecipientLine label="To" recipients={message.recipients.filter((r) => r.type !== "cc" && r.type !== "bcc")} />
|
|
1162
|
-
<RecipientLine label="Cc" recipients={message.recipients.filter((r) => r.type === "cc")} />
|
|
1163
|
-
<RecipientLine label="Bcc" recipients={message.recipients.filter((r) => r.type === "bcc")} />
|
|
1164
|
-
{/* Wraps: in a thread the pane can be as narrow as the reading pane gets (the list takes
|
|
1165
|
-
384px of it), and these are six controls. */}
|
|
1166
|
-
<div className="flex flex-wrap gap-1 mt-3">
|
|
1167
|
-
<IconAction
|
|
1168
|
-
icon={<HiOutlineArrowUturnLeft size={16} aria-hidden="true" />}
|
|
1169
|
-
label="Reply"
|
|
1170
|
-
shortcut={keyboard ? SHORTCUTS.mail.reply : undefined}
|
|
1171
|
-
disabled={preparingCompose}
|
|
1172
|
-
onPrefetch={prefetchReply}
|
|
1173
|
-
onClick={() => void handleReplyOrForward("reply")}
|
|
1174
|
-
/>
|
|
1175
|
-
<IconAction
|
|
1176
|
-
icon={
|
|
1177
|
-
// The conventional reply-all glyph: the reply arrow, doubled. `hi2` has no
|
|
1178
|
-
// reply-all icon of its own, and nothing else in it means "answer everyone".
|
|
1179
|
-
<span className="inline-flex items-center" aria-hidden="true">
|
|
1180
|
-
<HiOutlineArrowUturnLeft size={16} />
|
|
1181
|
-
<HiOutlineArrowUturnLeft size={16} className="-ml-2.5" />
|
|
1182
|
-
</span>
|
|
1183
|
-
}
|
|
1184
|
-
label="Reply All"
|
|
1185
|
-
shortcut={keyboard ? SHORTCUTS.mail.replyAll : undefined}
|
|
1186
|
-
disabled={preparingCompose}
|
|
1187
|
-
onPrefetch={prefetchReply}
|
|
1188
|
-
onClick={() => void handleReplyOrForward("replyAll")}
|
|
1189
|
-
/>
|
|
1190
|
-
<IconAction
|
|
1191
|
-
icon={<HiOutlineArrowUturnRight size={16} aria-hidden="true" />}
|
|
1192
|
-
label="Forward"
|
|
1193
|
-
shortcut={keyboard ? SHORTCUTS.mail.forward : undefined}
|
|
1194
|
-
disabled={preparingCompose}
|
|
1195
|
-
onPrefetch={prefetchReply}
|
|
1196
|
-
onClick={() => void handleReplyOrForward("forward")}
|
|
1197
|
-
/>
|
|
1198
|
-
{archivable && (
|
|
1199
|
-
<IconAction
|
|
1200
|
-
icon={<HiOutlineArchiveBox size={16} aria-hidden="true" />}
|
|
1201
|
-
label="Archive"
|
|
1202
|
-
shortcut={keyboard ? SHORTCUTS.mail.archive : undefined}
|
|
1203
|
-
busy={archiving}
|
|
1204
|
-
disabled={archiving}
|
|
1205
|
-
onClick={handleArchive}
|
|
1206
|
-
/>
|
|
1207
|
-
)}
|
|
1208
|
-
{movable && (
|
|
1209
|
-
<IconAction
|
|
1210
|
-
icon={<HiOutlineFolderArrowDown size={16} aria-hidden="true" />}
|
|
1211
|
-
label="Move to"
|
|
1212
|
-
shortcut={keyboard ? SHORTCUTS.mail.move : undefined}
|
|
1213
|
-
onClick={() => setMovePrompt(true)}
|
|
1214
|
-
/>
|
|
1215
|
-
)}
|
|
1216
|
-
{labels && labels.length > 0 && (
|
|
1217
|
-
<LabelMenuButton
|
|
1218
|
-
aria-label="Labels"
|
|
1219
|
-
label="Labels"
|
|
1220
|
-
className="border border-border py-1.5"
|
|
1221
|
-
labels={labels}
|
|
1222
|
-
mailboxUid={message.mailboxUid}
|
|
1223
|
-
onLabelCreated={onLabelCreated}
|
|
1224
|
-
applied={currentLabelsMessage.labelUids ?? []}
|
|
1225
|
-
onCommit={(labelUids) => void handleApplyLabels(labelUids)}
|
|
1226
|
-
busy={savingLabels}
|
|
1227
|
-
note="Ticked labels are applied to this message and unticked ones removed."
|
|
1228
|
-
emptyNote="This mailbox has no labels yet."
|
|
1229
|
-
commit={{ label: "Apply" }}
|
|
1230
|
-
clear={{ label: "Remove all labels" }}
|
|
1231
|
-
/>
|
|
1232
|
-
)}
|
|
1233
|
-
</div>
|
|
1234
|
-
{archiveError && (
|
|
1235
|
-
<div className="mt-2">
|
|
1236
|
-
<Alert>{archiveError}</Alert>
|
|
1237
|
-
</div>
|
|
1238
|
-
)}
|
|
1239
|
-
{labelsError && (
|
|
1240
|
-
<div className="mt-2">
|
|
1241
|
-
<Alert>{labelsError}</Alert>
|
|
1242
|
-
</div>
|
|
1243
|
-
)}
|
|
1307
|
+
{archiveError && <Alert>{archiveError}</Alert>}
|
|
1308
|
+
{labelsError && <Alert>{labelsError}</Alert>}
|
|
1244
1309
|
{labels && (currentLabelsMessage.labelUids?.length ?? 0) > 0 && (
|
|
1245
|
-
<div className="flex flex-wrap items-center gap-1.5
|
|
1310
|
+
<div className="flex flex-wrap items-center gap-1.5">
|
|
1246
1311
|
{currentLabelsMessage
|
|
1247
1312
|
.labelUids!.map((uid) => labels.find((l) => l.uid === uid))
|
|
1248
1313
|
.filter((l): l is Label => !!l)
|
|
@@ -1262,10 +1327,7 @@ function MessageDetailContent({
|
|
|
1262
1327
|
{(["delivery", "read"] as const)
|
|
1263
1328
|
.filter((type) => (type === "delivery" ? message.deliveryReceiptPending : message.readReceiptPending))
|
|
1264
1329
|
.map((type) => (
|
|
1265
|
-
<div
|
|
1266
|
-
key={type}
|
|
1267
|
-
className="flex flex-wrap items-center gap-2 mt-2 py-2 px-3 rounded-sm bg-surface-alt text-sm"
|
|
1268
|
-
>
|
|
1330
|
+
<div key={type} className="flex flex-wrap items-center gap-2 py-2 px-3 rounded-sm bg-surface-alt text-sm">
|
|
1269
1331
|
<span>
|
|
1270
1332
|
{senderLabel} requested a {type} receipt for this
|
|
1271
1333
|
message.
|
|
@@ -1280,30 +1342,21 @@ function MessageDetailContent({
|
|
|
1280
1342
|
>
|
|
1281
1343
|
Send receipt
|
|
1282
1344
|
</Button>
|
|
1283
|
-
<Button
|
|
1284
|
-
type="button"
|
|
1285
|
-
variant="text"
|
|
1286
|
-
disabled={receiptBusy !== null}
|
|
1287
|
-
onClick={() => handleReceipt(type, "decline")}
|
|
1288
|
-
>
|
|
1345
|
+
<Button type="button" variant="text" disabled={receiptBusy !== null} onClick={() => handleReceipt(type, "decline")}>
|
|
1289
1346
|
Decline
|
|
1290
1347
|
</Button>
|
|
1291
1348
|
</div>
|
|
1292
1349
|
))}
|
|
1293
|
-
{receiptError &&
|
|
1294
|
-
<div className="mt-2">
|
|
1295
|
-
<Alert>{receiptError}</Alert>
|
|
1296
|
-
</div>
|
|
1297
|
-
)}
|
|
1350
|
+
{receiptError && <Alert>{receiptError}</Alert>}
|
|
1298
1351
|
{security?.state === "signature_failed" && innerAttachments && innerAttachments.length > 0 && (
|
|
1299
|
-
<p role="status" className="
|
|
1352
|
+
<p role="status" className="py-2 px-3 rounded-sm text-sm bg-warning/15 text-text">
|
|
1300
1353
|
These attachments come from a message whose signature couldn’t be verified. Open them only if
|
|
1301
1354
|
you trust the sender.
|
|
1302
1355
|
</p>
|
|
1303
1356
|
)}
|
|
1304
1357
|
{innerAttachments
|
|
1305
1358
|
? innerAttachments.length > 0 && (
|
|
1306
|
-
<ul className="flex flex-wrap gap-2
|
|
1359
|
+
<ul className="flex flex-wrap gap-2">
|
|
1307
1360
|
{innerAttachments.map((attachment, index) => (
|
|
1308
1361
|
<li key={`${index}:${attachment.filename ?? ""}`}>
|
|
1309
1362
|
<button
|
|
@@ -1318,7 +1371,7 @@ function MessageDetailContent({
|
|
|
1318
1371
|
</ul>
|
|
1319
1372
|
)
|
|
1320
1373
|
: attachments.length > 0 && (
|
|
1321
|
-
<ul className="flex flex-wrap gap-2
|
|
1374
|
+
<ul className="flex flex-wrap gap-2">
|
|
1322
1375
|
{attachments.map((attachment) => (
|
|
1323
1376
|
<li key={attachment.uid}>
|
|
1324
1377
|
<a
|
|
@@ -1331,50 +1384,76 @@ function MessageDetailContent({
|
|
|
1331
1384
|
))}
|
|
1332
1385
|
</ul>
|
|
1333
1386
|
)}
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
{
|
|
1339
|
-
<
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1387
|
+
{/* The body: an isolated frame exactly as tall as its content (or text), in the card's own place. A decrypted or verified
|
|
1388
|
+
body never came through the server's own sanitizer (the server never saw the plaintext); `MessageBody` sanitizes every body
|
|
1389
|
+
client-side regardless, behind a no-remote-loads CSP, in a frame that runs no script. */}
|
|
1390
|
+
<div ref={bodyRegionRef} tabIndex={-1} className="outline-none">
|
|
1391
|
+
{message.encrypted && security === null ? (
|
|
1392
|
+
<BodySkeleton />
|
|
1393
|
+
) : security?.decryptError !== undefined ? (
|
|
1394
|
+
// An encrypted message that could not be read: locked (no unlocked keys - the button asks the app's own unlock prompt, and the message
|
|
1395
|
+
// then decrypts in place) or genuinely unreadable (keys unlocked and it still didn't open - the reason, and nothing to click).
|
|
1396
|
+
<EncryptedBody locked={lockedRef.current} reason={security.decryptError} unlocking={unlocking} onUnlock={() => void handleUnlockToView()} />
|
|
1397
|
+
) : (
|
|
1398
|
+
<MessageBody
|
|
1399
|
+
messageUid={message.uid}
|
|
1400
|
+
messageVersion={message.version}
|
|
1401
|
+
title={bodyTitle}
|
|
1402
|
+
content={bodyContent}
|
|
1403
|
+
attachments={attachments}
|
|
1404
|
+
inlineParts={innerAttachments}
|
|
1405
|
+
original={viewOriginal}
|
|
1406
|
+
onAdaptable={setAdaptable}
|
|
1407
|
+
/>
|
|
1347
1408
|
)}
|
|
1348
1409
|
</div>
|
|
1410
|
+
</div>
|
|
1411
|
+
|
|
1412
|
+
{showFooter && (
|
|
1413
|
+
<div className="flex flex-wrap gap-2 px-4 py-3 border-t border-border print:hidden">
|
|
1414
|
+
<Button
|
|
1415
|
+
type="button"
|
|
1416
|
+
variant="secondary"
|
|
1417
|
+
className="!w-auto"
|
|
1418
|
+
aria-label="Reply to this message"
|
|
1419
|
+
disabled={preparingCompose}
|
|
1420
|
+
onClick={() => void handleReplyOrForward("reply")}
|
|
1421
|
+
>
|
|
1422
|
+
Reply
|
|
1423
|
+
</Button>
|
|
1424
|
+
<Button
|
|
1425
|
+
type="button"
|
|
1426
|
+
variant="secondary"
|
|
1427
|
+
className="!w-auto"
|
|
1428
|
+
aria-label="Forward this message"
|
|
1429
|
+
disabled={preparingCompose}
|
|
1430
|
+
onClick={() => void handleReplyOrForward("forward")}
|
|
1431
|
+
>
|
|
1432
|
+
Forward
|
|
1433
|
+
</Button>
|
|
1434
|
+
</div>
|
|
1349
1435
|
)}
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
// couldn't - the server never saw the plaintext) - it's sanitized here, client-side, before
|
|
1361
|
-
// it ever touches the DOM (remote images/stylesheets/`url()`s stripped too, so opening it
|
|
1362
|
-
// can't ping a tracker), behind a no-remote-loads CSP, on top of the iframe's `sandbox=""`.
|
|
1363
|
-
<iframe
|
|
1364
|
-
key={message.uid}
|
|
1365
|
-
title={message.subject || "Message content"}
|
|
1366
|
-
srcDoc={buildSecureSrcDoc(security.html)}
|
|
1367
|
-
sandbox=""
|
|
1368
|
-
className={`${bodyClassName} border-0`}
|
|
1369
|
-
/>
|
|
1436
|
+
</CardShell>
|
|
1437
|
+
);
|
|
1438
|
+
|
|
1439
|
+
return (
|
|
1440
|
+
// `h-full` outside a thread only: the standalone message route renders this pane straight into a
|
|
1441
|
+
// *block* (`MailShell`'s own `<main>`), where nothing stretches it and a percentage of that block's
|
|
1442
|
+
// own resolved height is the height. Inside a thread the card is one row of the thread's own list.
|
|
1443
|
+
<div className={inThread ? "min-w-0" : "flex-1 min-w-0 min-h-0 flex flex-col h-full"}>
|
|
1444
|
+
{inThread ? (
|
|
1445
|
+
card
|
|
1370
1446
|
) : (
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1447
|
+
<>
|
|
1448
|
+
{backHref && (
|
|
1449
|
+
<a href={backHref} className="text-sm text-primary-dark hover:underline block px-4 pt-3">
|
|
1450
|
+
← Back to messages
|
|
1451
|
+
</a>
|
|
1452
|
+
)}
|
|
1453
|
+
{/* The subject has a card of its own, above the message, that stays put while the message scrolls. */}
|
|
1454
|
+
<SubjectCard subject={shownSubject} />
|
|
1455
|
+
<div className="flex-1 min-h-0 overflow-y-auto p-2 sm:p-3 flex flex-col gap-3">{card}</div>
|
|
1456
|
+
</>
|
|
1378
1457
|
)}
|
|
1379
1458
|
|
|
1380
1459
|
<MoveToFolderDialog
|