"use client" import { useCallback, useEffect, useMemo, useRef, useState, type CSSProperties, } from "react" import { useTheme } from "next-themes" import { emailPreviewBaseCss, emailPreviewDarkOverrideCss, emailPreviewLightOverrideCss, emailPreviewWrapperCss, } from "@/lib/email-preview-dark-styles" import { prepareEmailHtmlForIframe, injectEmailHtmlIntoDocument, } from "@/lib/mail-html-iframe" import { buildEmailPreviewCsp } from "@/lib/mail-remote-content" const EMAIL_PREVIEW_IFRAME_STYLE: CSSProperties = { display: "block", background: "transparent", } function documentIsDark(): boolean { return document.documentElement.classList.contains("dark") } function measureIframeContentHeight(doc: Document): number { const body = doc.body const root = doc.documentElement if (!body) return 60 const heights = [ body.scrollHeight, body.offsetHeight, root?.scrollHeight ?? 0, root?.clientHeight ?? 0, ] return Math.max(60, ...heights) + 2 } export function SandboxedContent({ html, blockRemoteContent, restrictPopups = false, senderEmail, cidUrlMap, }: { html: string blockRemoteContent: boolean restrictPopups?: boolean senderEmail?: string cidUrlMap?: Record }) { const iframeRef = useRef(null) const [height, setHeight] = useState(120) const sandboxValue = restrictPopups ? "allow-same-origin" : "allow-same-origin allow-popups" const { resolvedTheme } = useTheme() const isDark = resolvedTheme === "dark" || ((resolvedTheme === "system" || resolvedTheme === undefined) && typeof document !== "undefined" && documentIsDark()) const parsedEmail = useMemo( () => prepareEmailHtmlForIframe(html, { blockRemoteContent, isDark, senderEmail, cidUrlMap, }), [html, blockRemoteContent, isDark, senderEmail, cidUrlMap] ) const themeCss = useMemo(() => { if (!blockRemoteContent) return emailPreviewWrapperCss() return `${emailPreviewBaseCss(isDark)}${ isDark ? emailPreviewDarkOverrideCss() : emailPreviewLightOverrideCss() }` }, [blockRemoteContent, isDark]) const cspContent = useMemo( () => buildEmailPreviewCsp(blockRemoteContent), [blockRemoteContent] ) const injectContent = useCallback(() => { const iframe = iframeRef.current if (!iframe) return const doc = iframe.contentDocument if (!doc) return injectEmailHtmlIntoDocument(doc, { csp: cspContent, documentBaseHref: parsedEmail.documentBaseHref, resolveBaseHref: parsedEmail.resolveBaseHref, headMarkup: parsedEmail.headMarkup, bodyHtml: parsedEmail.bodyHtml, wrapperCss: themeCss, }) const syncHeight = () => { const liveDoc = iframe.contentDocument if (!liveDoc) return const next = measureIframeContentHeight(liveDoc) setHeight((prev) => (prev === next ? prev : next)) } const resizeObserver = new ResizeObserver(syncHeight) if (doc.body) { resizeObserver.observe(doc.body) for (const img of doc.images) { if (!img.complete) { img.addEventListener("load", syncHeight, { once: true }) img.addEventListener("error", syncHeight, { once: true }) } } for (const link of doc.querySelectorAll('link[rel~="stylesheet"]')) { link.addEventListener("load", syncHeight, { once: true }) link.addEventListener("error", syncHeight, { once: true }) } syncHeight() requestAnimationFrame(syncHeight) setTimeout(syncHeight, 250) setTimeout(syncHeight, 1000) } return () => resizeObserver.disconnect() }, [parsedEmail, themeCss, cspContent]) useEffect(() => { const cleanup = injectContent() return () => cleanup?.() }, [injectContent]) return (