Documentation
¶
Overview ¶
Package mailrender turns a message model into the two body parts an email carries: sanitized, CSS-inlined HTML and a plain-text alternative.
It is a leaf, in the sense internal/calfeed is: pure model to bytes, holding no state, reading nothing from a store, and importing no storage, metamodel or application types. Callers assemble the model from data they have already read (and, from TKT-U2R7GU, already gated); this package only formats it.
The pipeline order is load-bearing ¶
Rendering runs in exactly this order, and the order is a security property, not a convenience:
markdown -> goldmark -> bluemonday(CONTENT ONLY) -> trusted template -> douceur inline
Two verified library behaviors force it (TKT-332QZY design review):
- bluemonday strips style attributes unconditionally, and AllowStyling does not restore them. Sanitizing after inlining therefore deletes every inlined declaration and produces unstyled mail. Sanitizing the assembled document additionally strips the cellpadding/cellspacing/border/role attributes that table-based email layout depends on, and drops cid: image sources, which would break the embedded logo.
- douceur performs no CSS value validation whatsoever. It will happily materialize url('javascript:...'), behavior:url(...) and expression(...) into style attributes. Because it runs last, nothing sanitizes its output, so only trusted CSS may reach it.
The consequences for anyone editing this package: sanitize the untrusted fragment, never the assembled document; keep the <style> block operator- and template-authored; and validate every value interpolated into CSS. Reversing any of those is a silent downgrade — mail still sends, it is merely unstyled or unsafe — which is why this is written down rather than left to be rediscovered.
The logo is embedded, and raster only ¶
The operator logo is referenced as cid:<LogoCID> and travels with the message as an attached part, never as a URL: rela serves it from an authenticated endpoint, so a mail client could not fetch it.
Callers must supply raster bytes (PNG/JPEG/WebP) and must NOT pass an SVG. SVG has near-zero support across mail clients — Gmail, Outlook and Apple Mail strip or fail it — and it is an active-content format that can carry a script element, so embedding operator-uploaded SVG would ship script-capable bytes into inboxes for no rendering benefit. A message renders without a logo rather than with an unsafe one.
Trust ¶
Content passed in is UNTRUSTED: it originates from entity bodies and properties. The template and its stylesheet are TRUSTED: they ship with rela. Palette tokens sit in between — operator-supplied, but they land in CSS, so they are validated as colors and rejected otherwise (see ValidatePalette).
Nil: Renderer.Render rejects a nil Message; New rejects a nil Options.
Index ¶
Constants ¶
This section is empty.
Variables ¶
var ErrNilMessage = errors.New("mailrender: nil message")
ErrNilMessage is returned by Render when handed a nil message.
Functions ¶
func ValidatePalette ¶
ValidatePalette checks that every value is a color, and returns an error naming the first key that is not.
This is not defensive tidiness. Palette values are interpolated into the stylesheet, and douceur — which runs last and validates nothing — will materialize whatever it finds into a style attribute. A token of url('javascript:alert(1)') reaches the recipient's mail client verbatim. So values are checked against an ALLOWLIST and REJECTED; they are never escaped or silently replaced with a default, because a caller that supplied a bad color has a bug worth surfacing.
Types ¶
type Message ¶
type Message struct {
// Subject is the mail subject. Header-safety (no CR/LF) is the sending
// package's concern, not the renderer's.
Subject string
// Intro is optional markdown shown above the sections. UNTRUSTED.
Intro string
// Sections are rendered in order.
Sections []Section
Footer string
}
Message is the model a caller assembles and this package formats.
type Options ¶
type Options struct {
// Palette maps CSS custom-property names (e.g. "--accent-color") to color
// values. Values MUST be colors; see [ValidatePalette]. Keys absent from
// the map fall back to the built-in defaults.
Palette map[string]string
// LogoCID, when non-empty, is the Content-ID of a logo part the caller has
// attached to the message. The template references it as cid:<LogoCID>.
//
// Raster only. See "The logo is embedded, and raster only" in the package
// doc for why an SVG must never be passed here.
LogoCID string
// LogoAlt is the logo's alt text. Defaults to "logo".
LogoAlt string
// BaseURL prefixes relative links in rendered output. Mail is read outside
// the app, so a relative href is dead; callers that emit links should set
// this.
BaseURL string
}
Options configures a Renderer. The zero value is usable: it renders with rela's default palette and no logo.
type Renderer ¶
type Renderer struct {
// contains filtered or unexported fields
}
Renderer formats messages. It is immutable after construction and safe for concurrent use.
type Section ¶
type Section struct {
// Title is an optional heading rendered above the section.
Title string
// Body is markdown. It is UNTRUSTED and is sanitized during rendering.
Body string
// Columns are table header labels. Empty means no table.
Columns []string
// Rows are table cells, each row aligned to Columns. Cell text is escaped,
// not markdown-rendered — a table cell is a value, not a document.
Rows [][]string
// Links, when set, gives each row an href. A nil or short slice leaves the
// corresponding rows unlinked.
Links []string
}
Section is one block of a message body. A Section renders either as a paragraph of prose (Body only) or as a table (Rows non-empty); a Section with both renders the prose above the table.