/* ============================================================ EmailBody.jsx — render a candidate email as HTML, the way a mail client does. Two layers of defence, because one is not enough: 1. A sanitiser pass strips scripts, embedded frames, form controls and every event handler / javascript: URL before the markup is handed over. 2. The result renders inside an iframe whose sandbox never includes allow-scripts, so even a miss in layer 1 cannot execute. A Content-Security- Policy meta inside the document blocks every outbound request by default. allow-scripts is the one token that must never appear here. Paired with allow-same-origin it lets the frame reach into its own sandbox attribute and remove it, which hands the attacker the parent origin. allow-same-origin on its own is safe and is what lets the parent measure scrollHeight to size the frame — no scripts run either way. The iframe also isolates CSS. Emails ship ${html}` } /** True when the string carries real markup rather than incidental angle brackets. */ export function looksLikeHtml(value) { return /<[a-z!/][\s\S]*>/i.test(String(value || '')) } const MIN_FRAME_HEIGHT = 48 export default function EmailBody({ html, maxHeight }) { const ref = useRef(null) const [allowRemoteImages, setAllowRemoteImages] = useState(false) const [height, setHeight] = useState(MIN_FRAME_HEIGHT) const [blockedImages, setBlockedImages] = useState(0) const themeVersion = useThemeVersion() // Both memoised because srcDoc IS the frame's source: handing React a new but // equal string tears the document down and rebuilds it. Unmemoised, every // unrelated parent render re-parsed the mail and reloaded the frame. const clean = useMemo(() => sanitize(html), [html]) const srcDoc = useMemo( // themeVersion is the entire reason this list exists. frameStyles() // snapshots the CSS variables into the frame's