feat(mail): 优化邮件正文渲染与样式保留

- 邮件正文改为在 `iframe` 中渲染,提升排版隔离和显示稳定性。
- 扩展前端清洗配置,保留邮件常见布局样式与表格属性。
- 收紧后端 HTML 策略的样式白名单,兼顾邮件样式保留与安全过滤。
- 补充测试,验证邮件布局样式可保留且危险内容仍会被拦截。
This commit is contained in:
LanQin_
2026-06-24 13:55:09 +08:00
parent 295a34881d
commit 18db36d937
4 changed files with 142 additions and 4 deletions
+28 -1
View File
@@ -22,7 +22,20 @@ type HTMLPolicy struct{ policy *bluemonday.Policy }
func NewHTMLPolicy() *HTMLPolicy {
p := bluemonday.UGCPolicy()
p.AllowAttrs("style").OnElements("p", "span", "div", "table", "td", "th")
p.AllowAttrs("style").Globally()
p.AllowAttrs("class").Matching(bluemonday.SpaceSeparatedTokens).Globally()
p.AllowAttrs("align", "valign").Matching(bluemonday.Paragraph).Globally()
p.AllowAttrs("width", "height").Matching(bluemonday.NumberOrPercent).Globally()
p.AllowAttrs("bgcolor", "color").Matching(regexp.MustCompile(`(?i)^#[0-9a-f]{3,8}$|^[a-z][a-z0-9 -]{0,31}$`)).Globally()
p.AllowAttrs("border", "cellpadding", "cellspacing").Matching(bluemonday.Number).OnElements("table")
p.AllowStyles(
"background", "background-color", "background-image", "border", "border-collapse", "border-color",
"border-radius", "border-spacing", "border-style", "border-width", "box-shadow", "color", "display",
"font", "font-family", "font-size", "font-style", "font-weight", "height", "letter-spacing",
"line-height", "margin", "margin-bottom", "margin-left", "margin-right", "margin-top", "max-width",
"min-width", "opacity", "padding", "padding-bottom", "padding-left", "padding-right", "padding-top",
"text-align", "text-decoration", "text-transform", "vertical-align", "white-space", "width",
).MatchingHandler(safeEmailCSSValue).Globally()
return &HTMLPolicy{policy: p}
}
@@ -33,6 +46,20 @@ func (p *HTMLPolicy) Sanitize(s string) string {
return p.policy.Sanitize(s)
}
func safeEmailCSSValue(value string) bool {
value = strings.ToLower(strings.TrimSpace(value))
if value == "" || len(value) > 512 {
return false
}
unsafe := []string{"expression", "javascript:", "vbscript:", "data:", "behavior", "-moz-binding", "@import", "</", "url("}
for _, token := range unsafe {
if strings.Contains(value, token) {
return false
}
}
return true
}
func newID(prefix string) string {
buf := make([]byte, 16)
_, _ = rand.Read(buf)