/** Prépare le HTML d'un mail pour affichage dans une iframe sandboxée. */ import { stripHiddenEmailHtml } from "@/lib/strip-hidden-email-html" import { preprocessEmailHtmlForTheme } from "@/lib/email-preview-dark-styles" const REMOTE_URL = /^https?:\/\//i const PROTOCOL_RELATIVE = /^\/\// export type ParsedEmailHtml = { headMarkup: string bodyHtml: string /** Uniquement si le mail définit explicitement . */ documentBaseHref?: string /** Base pour résoudre les URLs relatives dans le HTML. */ resolveBaseHref?: string } function escapeHtmlAttr(value: string): string { return value .replace(/&/g, "&") .replace(/"/g, """) .replace(/]+href=["']([^"']+)["']/i) if (baseMatch?.[1]) { try { return new URL(baseMatch[1]).href } catch { return baseMatch[1] } } const stylesheetMatch = html.match( /]+rel=["'][^"']*stylesheet[^"']*["'][^>]+href=["']([^"']+)["']/i ) ?? html.match( /]+href=["']([^"']+)["'][^>]+rel=["'][^"']*stylesheet/i ) if (stylesheetMatch?.[1]) { try { const u = new URL(stylesheetMatch[1], "https://example.com/") if (stylesheetMatch[1].startsWith("http") || stylesheetMatch[1].startsWith("//")) { return `${u.origin}${u.pathname.replace(/\/[^/]*$/, "/")}` } } catch { /* ignore */ } } const absoluteMatch = html.match( /(?:src|href)=["'](https?:\/\/[^/"']+)[^"']*["']/i ) if (absoluteMatch?.[1]) { try { const u = new URL(absoluteMatch[1]) return `${u.origin}/` } catch { /* ignore */ } } return undefined } /** Extrait styles/CSS + body d'un document HTML complet ou fragment. */ export function parseEmailHtmlForIframe( html: string, resolveBaseHref?: string ): ParsedEmailHtml { if (!html.trim()) { return { headMarkup: "", bodyHtml: "" } } if (typeof DOMParser === "undefined") { return { headMarkup: "", bodyHtml: html } } try { const doc = new DOMParser().parseFromString(html, "text/html") const explicitBase = doc.querySelector("base[href]")?.getAttribute("href") ?? undefined const documentBaseHref = explicitBase ? resolveUrl(explicitBase, undefined) : undefined doc.querySelector("base[href]")?.remove() const headMarkup = collectHeadResourceMarkup(doc, resolveBaseHref) let bodyHtml = doc.body?.innerHTML?.trim() ?? "" if (!bodyHtml) bodyHtml = html.trim() return { headMarkup, bodyHtml, documentBaseHref, resolveBaseHref, } } catch { return { headMarkup: "", bodyHtml: html } } } function rewriteUrlList(value: string, baseHref?: string): string { return value .split(",") .map((part) => { const trimmed = part.trim() const space = trimmed.indexOf(" ") const url = space === -1 ? trimmed : trimmed.slice(0, space) const descriptor = space === -1 ? "" : trimmed.slice(space + 1) const resolved = resolveUrl(url, baseHref) return descriptor ? `${resolved} ${descriptor}` : resolved }) .join(", ") } function rewriteStyleRemoteUrls(style: string, baseHref?: string): string { return style.replace( /url\s*\(\s*['"]?([^'")]+)['"]?\s*\)/gi, (_match, url: string) => { const trimmed = url.trim() if ( trimmed.startsWith("data:") || trimmed.startsWith("#") || trimmed.startsWith("cid:") ) { return `url(${trimmed})` } return `url(${resolveUrl(trimmed, baseHref)})` } ) } function rewriteHeadMarkupUrls(headMarkup: string, baseHref?: string): string { if (!headMarkup.trim() || !baseHref) return headMarkup return headMarkup .replace( /(]+href=)(["'])([^"']+)\2/gi, (_m, prefix: string, quote: string, href: string) => `${prefix}${quote}${resolveUrl(href, baseHref)}${quote}` ) .replace( /(]*>)([\s\S]*?)(<\/style>)/gi, (_m, open: string, css: string, close: string) => `${open}${rewriteStyleRemoteUrls(css, baseHref)}${close}` ) } function isRemoteOrRelativeUrl(url: string): boolean { const t = url.trim() return ( REMOTE_URL.test(t) || PROTOCOL_RELATIVE.test(t) || (!t.startsWith("data:") && !t.startsWith("cid:") && !t.startsWith("mailto:") && !t.startsWith("#") && !t.startsWith("javascript:")) ) } /** Active images lazy-load et résout les URLs relatives / http. */ export function activateRemoteResourcesInHtml( html: string, options: { baseHref?: string } = {} ): string { const { baseHref } = options if (!html.trim() || typeof DOMParser === "undefined") return html try { const doc = new DOMParser().parseFromString(html, "text/html") for (const img of doc.querySelectorAll("img")) { const lazySrc = firstLazyImageSrc(img) const src = img.getAttribute("src") ?? "" const srcIsUseful = !!src.trim() && !isPlaceholderImageSrc(src) && (src.startsWith("data:") || src.startsWith("cid:") || isRemoteOrRelativeUrl(src)) if (lazySrc && isRemoteOrRelativeUrl(lazySrc) && !srcIsUseful) { img.setAttribute("src", resolveUrl(lazySrc, baseHref)) } else if (src.trim() && (src.startsWith("data:") || src.startsWith("cid:"))) { /* keep inline / embedded sources */ } else if (src && isRemoteOrRelativeUrl(src)) { img.setAttribute("src", resolveUrl(src, baseHref)) } else if (lazySrc && isRemoteOrRelativeUrl(lazySrc)) { img.setAttribute("src", resolveUrl(lazySrc, baseHref)) } const srcset = img.getAttribute("srcset") if (srcset) img.setAttribute("srcset", rewriteUrlList(srcset, baseHref)) const dataSrcset = img.getAttribute("data-srcset") if (dataSrcset && !img.getAttribute("srcset")) { img.setAttribute("srcset", rewriteUrlList(dataSrcset, baseHref)) } img.setAttribute("referrerpolicy", "no-referrer") } for (const el of doc.querySelectorAll("[background]")) { const bg = el.getAttribute("background") if (!bg || !isRemoteOrRelativeUrl(bg)) continue const resolved = resolveUrl(bg, baseHref) el.removeAttribute("background") const existing = el.getAttribute("style")?.trim() ?? "" const bgRule = `background-image:url(${resolved})` const nextStyle = existing ? existing.endsWith(";") ? `${existing}${bgRule}` : `${existing};${bgRule}` : bgRule el.setAttribute("style", nextStyle) } for (const el of doc.querySelectorAll("[style]")) { const style = el.getAttribute("style") if (style?.includes("url(")) { el.setAttribute("style", rewriteStyleRemoteUrls(style, baseHref)) } } for (const source of doc.querySelectorAll("source[src], source[srcset]")) { const src = source.getAttribute("src") if (src && isRemoteOrRelativeUrl(src)) { source.setAttribute("src", resolveUrl(src, baseHref)) } const srcset = source.getAttribute("srcset") if (srcset) source.setAttribute("srcset", rewriteUrlList(srcset, baseHref)) } return doc.body.innerHTML } catch { return html } } /** Injecte le mail dans le document iframe sans doc.write (préserve les