/** 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(
/(