From 9a44babcf1b1397185ab112ac7ad8604fa24f5dc Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Tue, 21 Apr 2026 21:21:42 +0200 Subject: [PATCH] feat: add collapsible details to calendar invitation banner and update sanitization config for iframe rendering --- .../email/calendar-invitation-banner.tsx | 20 +++++++++++++++++++ components/email/email-viewer.tsx | 12 +++++------ lib/email-sanitization.ts | 20 +++++++++++++++++++ locales/en/common.json | 4 +++- 4 files changed, 49 insertions(+), 7 deletions(-) diff --git a/components/email/calendar-invitation-banner.tsx b/components/email/calendar-invitation-banner.tsx index 58f4b2ea..5e35705d 100644 --- a/components/email/calendar-invitation-banner.tsx +++ b/components/email/calendar-invitation-banner.tsx @@ -15,6 +15,7 @@ import { X, AlertCircle, ChevronDown, + ChevronUp, } from 'lucide-react'; import { useTranslations, useFormatter } from 'next-intl'; import { useRouter } from '@/i18n/navigation'; @@ -372,6 +373,7 @@ export function CalendarInvitationBanner({ email }: CalendarInvitationBannerProp const [showCalendarPicker, setShowCalendarPicker] = useState(false); const [selectedCalendarId, setSelectedCalendarId] = useState(''); const [rawIcsMethod, setRawIcsMethod] = useState('unknown'); + const [isCollapsed, setIsCollapsed] = useState(true); const attachment = findCalendarAttachment(email); @@ -442,6 +444,8 @@ export function CalendarInvitationBanner({ email }: CalendarInvitationBannerProp const summary = parsedEvent ? formatEventSummary(parsedEvent) : null; const isCancellation = method === 'cancel'; const isResponseOnly = method === 'reply' || method === 'refresh' || method === 'counter' || method === 'declinecounter'; + const canCollapse = method === 'reply'; + const showDetails = !canCollapse || !isCollapsed; const allowsRsvp = method === 'request'; const allowsImport = method === 'request' || method === 'publish' || method === 'add' || method === 'unknown'; @@ -750,9 +754,22 @@ export function CalendarInvitationBanner({ email }: CalendarInvitationBannerProp )} {bannerTitle} + {canCollapse && ( + + )} {/* Content */} + {showDetails && (
{/* Left: Event info */} @@ -894,8 +911,10 @@ export function CalendarInvitationBanner({ email }: CalendarInvitationBannerProp
)}
+ )} {/* Actions */} + {showDetails && (
{canRespond && ( <> @@ -1020,6 +1039,7 @@ export function CalendarInvitationBanner({ email }: CalendarInvitationBannerProp )}
+ )} ); } diff --git a/components/email/email-viewer.tsx b/components/email/email-viewer.tsx index fe989524..ff1c5b07 100644 --- a/components/email/email-viewer.tsx +++ b/components/email/email-viewer.tsx @@ -4,7 +4,7 @@ import { useState, useEffect, useLayoutEffect, useMemo, useRef, useCallback } fr import ReactDOM from "react-dom"; import DOMPurify from "dompurify"; import { Email, ContactCard, Mailbox } from "@/lib/jmap/types"; -import { EMAIL_SANITIZE_CONFIG, collapseBlockedImageContainers, plainTextToSafeHtml } from "@/lib/email-sanitization"; +import { EMAIL_IFRAME_SANITIZE_CONFIG, collapseBlockedImageContainers, plainTextToSafeHtml } from "@/lib/email-sanitization"; import { hasMeaningfulHtmlBody } from "@/lib/signature-utils"; import { Button } from "@/components/ui/button"; import { Avatar } from "@/components/ui/avatar"; @@ -2322,7 +2322,7 @@ export function EmailViewer({ let blockedExternalContent = false; // Use shared sanitization config as base (more secure) - const sanitizeConfig = { ...EMAIL_SANITIZE_CONFIG }; + const sanitizeConfig = { ...EMAIL_IFRAME_SANITIZE_CONFIG }; // Check if sender is trusted (localStorage list or address book) const senderEmail = email.from?.[0]?.email?.toLowerCase(); @@ -2438,7 +2438,7 @@ export function EmailViewer({ return cidBlobUrls[cidRef] || 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'; } ); - const cleanHtml = DOMPurify.sanitize(htmlWithCidUrls, EMAIL_SANITIZE_CONFIG); + const cleanHtml = DOMPurify.sanitize(htmlWithCidUrls, EMAIL_IFRAME_SANITIZE_CONFIG); return { html: cleanHtml, isHtml: true }; } if (smimeDecryptedText) { @@ -2446,7 +2446,7 @@ export function EmailViewer({ } // TNEF (winmail.dat) extracted content if (tnefHtml) { - const cleanHtml = DOMPurify.sanitize(tnefHtml, EMAIL_SANITIZE_CONFIG); + const cleanHtml = DOMPurify.sanitize(tnefHtml, EMAIL_IFRAME_SANITIZE_CONFIG); return { html: cleanHtml, isHtml: true }; } if (tnefText) { @@ -2454,7 +2454,7 @@ export function EmailViewer({ } // Embedded message/rfc822 unwrapped content if (embeddedEmailHtml) { - const cleanHtml = DOMPurify.sanitize(embeddedEmailHtml, EMAIL_SANITIZE_CONFIG); + const cleanHtml = DOMPurify.sanitize(embeddedEmailHtml, EMAIL_IFRAME_SANITIZE_CONFIG); return { html: cleanHtml, isHtml: true }; } if (embeddedEmailText) { @@ -2603,7 +2603,7 @@ export function EmailViewer({ return `