package security import ( "strings" "unicode" "github.com/microcosm-cc/bluemonday" ) const MaxEmailHTMLRunes = 500_000 // emailHTMLPolicy is a parser-grade allowlist for campaign/email HTML. // It strips scripts, event handlers, javascript:/data: URLs, and high-risk tags // while preserving a safe email HTML subset (tables, typography, remote images). var emailHTMLPolicy = newEmailHTMLPolicy() func newEmailHTMLPolicy() *bluemonday.Policy { p := bluemonday.UGCPolicy() // Layout tags common in email HTML (beyond UGC defaults). p.AllowElements( "div", "table", "thead", "tbody", "tfoot", "tr", "th", "td", "caption", "colgroup", "col", "center", "font", ) p.AllowAttrs( "width", "height", "align", "valign", "bgcolor", "cellpadding", "cellspacing", "border", "colspan", "rowspan", "role", ).OnElements("table", "tr", "td", "th", "thead", "tbody", "tfoot", "col", "colgroup", "div", "p", "h1", "h2", "h3", "h4", "h5", "h6", "hr", "center") p.AllowAttrs("color", "face", "size").OnElements("font") p.AllowAttrs("span").OnElements("col", "colgroup") // Safe inline CSS only — style tags and event handlers remain disallowed. p.AllowAttrs("style").Globally() p.AllowStyles( "background-color", "color", "font-family", "font-size", "font-weight", "font-style", "text-align", "text-decoration", "line-height", "letter-spacing", "margin", "margin-top", "margin-right", "margin-bottom", "margin-left", "padding", "padding-top", "padding-right", "padding-bottom", "padding-left", "border", "border-top", "border-right", "border-bottom", "border-left", "border-color", "border-style", "border-width", "border-collapse", "border-spacing", "width", "height", "max-width", "min-width", "max-height", "min-height", "display", "vertical-align", "white-space", ).Globally() // http(s)/mailto only; data: images stay off (do not call AllowDataURIImages). // Reject relative URLs that could be redirected via a stripped . p.AllowRelativeURLs(false) p.RequireParseableURLs(true) p.AllowURLSchemes("http", "https", "mailto") return p } // SanitizeEmailHTML removes high-risk tags/attrs from generated campaign HTML before store/send. // Parser-grade allowlist (bluemonday). Empty input stays empty. func SanitizeEmailHTML(html string) string { html = strings.TrimSpace(html) if html == "" { return "" } html = stripControlsKeepNewlines(html) html = emailHTMLPolicy.Sanitize(html) return TruncateRunes(html, MaxEmailHTMLRunes) } func stripControlsKeepNewlines(s string) string { var b strings.Builder b.Grow(len(s)) for _, r := range s { if r == '\n' || r == '\r' || r == '\t' || unicode.IsPrint(r) { b.WriteRune(r) } } return b.String() }