fix(mail): 优化邮件 HTML 与样式保留
Docker Release / Check web and api (push) Waiting to run
Docker Release / Resolve release tag (push) Blocked by required conditions
Docker Release / Build and publish all-in-one (push) Blocked by required conditions
Docker Release / Build and publish api (push) Blocked by required conditions
Docker Release / Build and publish web (push) Blocked by required conditions
Docker Release / Build and publish dovecot (push) Blocked by required conditions
Docker Release / Build and publish postfix (push) Blocked by required conditions
Docker Release / Build and publish rspamd (push) Blocked by required conditions
Docker Release / Create GitHub release (push) Blocked by required conditions

- 支持整页邮件文档的清洗与渲染,保留 `<html>`、`<head>`、`<body>` 和 `<style>` 等结构。
- 提取并校验安全的邮件样式块,阻止包含外链、脚本式内容的 CSS。
- 补充 `dompurify` 类型声明以支持 `WHOLE_DOCUMENT` 配置。
This commit is contained in:
LanQin_
2026-06-24 14:19:31 +08:00
parent 18db36d937
commit adcc822c9a
4 changed files with 76 additions and 12 deletions
+19 -8
View File
@@ -3114,17 +3114,31 @@ function escapeHtml(value: string) {
}
function buildMailFrameSrcDoc(bodyHtml: string, bodyText: string) {
const rawBody = bodyHtml.trim() ? bodyHtml : `<pre>${escapeHtml(bodyText || "")}</pre>`
const sanitizedBody = DOMPurify.sanitize(rawBody, {
ADD_ATTR: ["style", "align", "valign", "bgcolor", "border", "cellpadding", "cellspacing", "width", "height"],
ADD_TAGS: ["center"],
const sanitized = DOMPurify.sanitize(rawBody, {
ADD_ATTR: ["style", "type", "align", "valign", "bgcolor", "border", "cellpadding", "cellspacing", "width", "height"],
ADD_TAGS: ["html", "head", "body", "style", "center", "font"],
WHOLE_DOCUMENT: /<html[\s>]/i.test(rawBody) || /<body[\s>]/i.test(rawBody),
})
if (/<html[\s>]/i.test(sanitized) || /<body[\s>]/i.test(sanitized)) {
const hasHead = /<head[\s>]/i.test(sanitized)
const withBase = hasHead
? sanitized.replace(/<head([^>]*)>/i, `<head$1><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><base target="_blank">${mailFrameBaseStyle()}`)
: sanitized.replace(/<html([^>]*)>/i, `<html$1><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><base target="_blank">${mailFrameBaseStyle()}</head>`)
return /<!doctype/i.test(withBase) ? withBase : `<!doctype html>${withBase}`
}
return `<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<base target="_blank">
<style>
${mailFrameBaseStyle()}
</head>
<body>${sanitized}</body>
</html>`
}
function mailFrameBaseStyle() {
return `<style>
html, body { margin: 0; padding: 0; background: #fff; color: #111827; }
body {
box-sizing: border-box;
@@ -3139,10 +3153,7 @@ function buildMailFrameSrcDoc(bodyHtml: string, bodyText: string) {
table { max-width: 100%; }
pre { white-space: pre-wrap; word-break: break-word; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
a { color: #2563eb; }
</style>
</head>
<body>${sanitizedBody}</body>
</html>`
</style>`
}
function sanitizeComposerHtml(value: string) {
return DOMPurify.sanitize(value || "")
+1
View File
@@ -2,6 +2,7 @@ declare module "dompurify" {
type SanitizeConfig = {
ADD_ATTR?: string[]
ADD_TAGS?: string[]
WHOLE_DOCUMENT?: boolean
}
const DOMPurify: { sanitize: (source: string, config?: SanitizeConfig) => string }
export default DOMPurify