// TODO: P3-F7 — Sanitize iframe HTML rendering to prevent XSS /** * Mail detail reading pane. * Shows mail headers, sanitized HTML body, and attachments. * Actions (reply/forward/delete/etc.) are in the global plugin toolbar. */ import React, { useMemo, useRef, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import type { Mail, MailAttachment } from '@/api/mail'; import { decodeMimeHeader } from '@/api/mail'; import { Button } from '@/components/ui/Button'; import { EmptyState } from '@/components/ui/EmptyState'; import { FileText, Loader2 } from 'lucide-react'; import { formatDateTime } from '@/utils/date'; export interface MailDetailProps { mail: Mail | null; loading: boolean; onReply: (mail: Mail) => void; onForward: (mail: Mail) => void; onCreateEvent: (mail: Mail) => void; onToggleFlag: (mail: Mail) => void; onDownloadAttachment: (mailId: string, attachment: MailAttachment) => void; downloadingAttachmentId: string | null; onDelete?: (mail: Mail) => void; onMove?: (mail: Mail) => void; onEditDraft?: (mail: Mail) => void; } function formatFullDate(dateStr: string): string { return formatDateTime(dateStr) || dateStr; } function formatBytes(bytes: number): string { if (bytes < 1024) return `${bytes} B`; if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; } export function MailDetail({ mail, loading, onReply, onForward, onDownloadAttachment, downloadingAttachmentId, }: MailDetailProps) { const { t } = useTranslation(); const safeHtml = useMemo(() => { if (!mail) return null; return mail.sanitized_html || mail.body_html; }, [mail]); const iframeRef = useRef(null); const handleIframeLoad = useCallback(() => { const iframe = iframeRef.current; if (!iframe) return; try { const doc = iframe.contentDocument || iframe.contentWindow?.document; if (doc) { const height = doc.documentElement.scrollHeight || doc.body.scrollHeight; iframe.style.height = `${height}px`; } } catch { // Cross-origin restrictions — leave default height } }, []); if (loading) { return (
); } if (!mail) { return (
); } return (
{/* Headers */}

{decodeMimeHeader(mail.subject) || t('mail.noSubject')}

{mail.labels && mail.labels.length > 0 && (
{mail.labels.map((label) => ( {label.name} ))}
)}
{t('mail.from')}: {decodeMimeHeader(mail.from_name) ? `${decodeMimeHeader(mail.from_name)} <${mail.from_address}>` : mail.from_address}
{t('mail.to')}: {(mail.to_addresses ?? []).join(', ')}
{mail.cc_addresses && mail.cc_addresses.length > 0 && (
{t('mail.cc')}: {(mail.cc_addresses ?? []).join(', ')}
)}
{t('mail.date')}: {formatFullDate(mail.date)}
{/* Body + Attachments — scrollable together */}
{safeHtml ? (