diff --git a/components/email/email-viewer.tsx b/components/email/email-viewer.tsx index ca98cffd..88ff7781 100644 --- a/components/email/email-viewer.tsx +++ b/components/email/email-viewer.tsx @@ -809,6 +809,15 @@ export function EmailViewer({ const [pluginRenderedHtml, setPluginRenderedHtml] = useState(null); const [pluginRenderedText, setPluginRenderedText] = useState(null); const [pluginRenderedAttachments, setPluginRenderedAttachments] = useState([]); + // Bumped when a plugin calls `api.ui.rerenderEmail` (e.g. the S/MIME plugin + // after the user unlocks a key from the banner) to force the onRenderEmailBody + // hook to run again for the open message so the body re-decrypts. + const [pluginRenderNonce, setPluginRenderNonce] = useState(0); + useEffect(() => { + const bump = () => setPluginRenderNonce((n) => n + 1); + window.addEventListener('plugin:rerender-email', bump); + return () => window.removeEventListener('plugin:rerender-email', bump); + }, []); // TNEF (winmail.dat) support const [tnefHtml, setTnefHtml] = useState(null); @@ -1205,7 +1214,7 @@ export function EmailViewer({ })(); return () => { cancelled = true; }; - }, [email]); + }, [email, pluginRenderNonce]); // TNEF (winmail.dat) detection and processing useEffect(() => { diff --git a/components/email/read-receipt-banner.tsx b/components/email/read-receipt-banner.tsx index ff4acc4a..dfaff891 100644 --- a/components/email/read-receipt-banner.tsx +++ b/components/email/read-receipt-banner.tsx @@ -1,7 +1,7 @@ 'use client'; import { useState } from 'react'; -import { MailCheck, Loader2, CheckCircle } from 'lucide-react'; +import { MailCheck, Loader2, CheckCircle, X } from 'lucide-react'; import { useTranslations } from 'next-intl'; interface ReadReceiptBannerProps { @@ -17,43 +17,61 @@ export function ReadReceiptBanner({ requestedBy, onSend, onIgnore }: ReadReceipt const t = useTranslations('email_viewer.read_receipt'); const [state, setState] = useState<'idle' | 'sending' | 'sent'>('idle'); + // Matches the host's "External Content" banner row: a round tinted icon chip, + // an uppercase eyebrow, a foreground message, and neutral bordered actions. if (state === 'sent') { return ( -
- - {t('sent')} +
+
+ +
+ {t('sent')}
); } return ( -
- - {t('prompt')} - {requestedBy} -
- - +
+
+ +
+
+
+
+ Read receipt +
+
+ {t('prompt')} +
+
+ Requested by {requestedBy} +
+
+
+ + +
); diff --git a/lib/plugin-sandbox/host-api.ts b/lib/plugin-sandbox/host-api.ts index 1a983616..6bcd2870 100644 --- a/lib/plugin-sandbox/host-api.ts +++ b/lib/plugin-sandbox/host-api.ts @@ -56,6 +56,7 @@ const PERM_PER_METHOD: Record = { 'ui.confirm': null, 'ui.alert': null, 'ui.prompt': null, + 'ui.rerenderEmail': null, 'ui.openExternalUrl': null, }; @@ -410,6 +411,14 @@ export async function dispatchApiCall( fields, }); } + case 'ui.rerenderEmail': { + // Re-run the onRenderEmailBody hook for the currently open message. Used + // by crypto plugins after they change decryption state (e.g. an S/MIME key + // was just unlocked) so the body re-decrypts without a full reload — which + // would wipe the in-memory session keys. + window.dispatchEvent(new CustomEvent('plugin:rerender-email')); + return undefined; + } case 'ui.openExternalUrl': { const url = String(args[0] ?? ''); // Only http(s) - the sandbox should not be able to navigate the host diff --git a/lib/plugin-sandbox/protocol.ts b/lib/plugin-sandbox/protocol.ts index 9385aa76..fe61555f 100644 --- a/lib/plugin-sandbox/protocol.ts +++ b/lib/plugin-sandbox/protocol.ts @@ -244,7 +244,7 @@ export const API_METHODS = [ 'jmap.fetchBlob', 'jmap.sendRaw', 'admin.getConfig', 'admin.getAllConfig', 'admin.setConfig', 'admin.deleteConfig', 'toast.success', 'toast.error', 'toast.info', 'toast.warning', - 'ui.confirm', 'ui.alert', 'ui.prompt', 'ui.openExternalUrl', + 'ui.confirm', 'ui.alert', 'ui.prompt', 'ui.rerenderEmail', 'ui.openExternalUrl', ] as const; export type ApiMethod = (typeof API_METHODS)[number]; diff --git a/lib/plugin-sandbox/runtime.tsx b/lib/plugin-sandbox/runtime.tsx index 03f37692..fa9343e5 100644 --- a/lib/plugin-sandbox/runtime.tsx +++ b/lib/plugin-sandbox/runtime.tsx @@ -223,6 +223,9 @@ function buildPluginApi(manifest: PluginManifest) { cancelLabel?: string; fields?: Array<{ name: string; label: string; type?: 'text' | 'password'; placeholder?: string; required?: boolean }>; }) => callApi('ui.prompt', [opts], 0) as Promise | null>, + /** Re-runs the onRenderEmailBody hook for the open message (e.g. after a + * crypto plugin unlocks a key) so its body re-renders without a reload. */ + rerenderEmail: () => callApi('ui.rerenderEmail', []) as Promise, /** Opens an http/https URL in a new tab via host `window.open`. */ openExternalUrl: (url: string, target?: string) => callApi('ui.openExternalUrl', [url, target]) as Promise, diff --git a/locales/en/common.json b/locales/en/common.json index 324bdf18..7de12348 100644 --- a/locales/en/common.json +++ b/locales/en/common.json @@ -257,7 +257,7 @@ }, "email_viewer": { "read_receipt": { - "prompt": "The sender requested a read receipt:", + "prompt": "The sender asked to be notified when you open this message.", "send": "Send receipt", "ignore": "Ignore", "sent": "Read receipt sent.",