Help us shape the future of Go! Take the 2026 Go Developer Survey

emailrender

package
v0.3.5 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 27, 2026 License: AGPL-3.0 Imports: 20 Imported by: 0

Documentation

Overview

Package emailrender renders broadcast/template email content. Bodies are email-ready HTML with Liquid merge tags (ADR 0040); the pipeline is: Liquid merge tags → derived plain-text part. Each step is a maintained library (osteele/liquid, k3a/html2text, x/net/html) — we don't hand-roll templating or HTML stripping.

Tracking (open pixel, click rewriting, unsubscribe footer) is deliberately NOT done here: the caller layers it on AFTER this pipeline so re-parsing can never mangle the injected pixel or escape the click-URL query strings.

Index

Constants

This section is empty.

Variables

View Source
var ErrInvalidContent = errors.New("message content cannot be rendered")

ErrInvalidContent is what Lint fails with: the subject or body cannot render, so it is refused where it is saved and where a send is planned. The wrapped text is the reason shown to the author.

View Source
var ErrRenderUnavailable = errors.New("the email could not be rendered right now, try again")

ErrRenderUnavailable is a document render that could not run to its end for a reason that is not the document's: the request's context ended, or the render hit the time or memory limit of the server. It is retryable and never a claim that the document is invalid.

Functions

func FinalHTMLToText

func FinalHTMLToText(finalHTML string) string

FinalHTMLToText derives the text/plain part of a send from its final per-recipient HTML (ADR 0056): tracking and the unsubscribe footer are already in it, so a link reads as its text followed by the URL it carries and the footer's unsubscribe link and postal address appear as they do in the HTML. The style, script and head content is left out, and so is any element whose inline style hides it (the open pixel, a preheader; see hidingDeclarations).

func HTMLToText

func HTMLToText(markup string) string

HTMLToText derives a plain-text representation of an HTML body for the text/plain MIME part.

func PrepareFields

func PrepareFields(subject, body *string) (*string, error)

PrepareFields is what every save path calls (site API, external API and MCP all reach it through the broadcasts and templates modules): it refuses content that cannot render (Lint) and returns the body to store, sanitized (SanitizeHTML). A nil field is not provided, so it is not checked; a nil body is returned as nil.

func Render

func Render(tmpl string, bindings map[string]any) (string, error)

Render renders a Liquid template with the given bindings. On a template error it returns the original template unchanged together with the error (useful for previews); RenderEmail treats that error as fatal.

func SanitizeHTML

func SanitizeHTML(src string) string

SanitizeHTML is the sanitizer of ADR 0040: it strips script, frames, forms, event-handler attributes, dangerous URLs and dangerous CSS from an email body and keeps what email markup needs (tables, inline styles, style blocks, Outlook conditional comments and VML buttons). It runs where a body is saved and again on each recipient's rendered message (RenderEmail), so Liquid merge tags in the input are kept byte for byte: they are masked while the policy runs. Applying it to its own output changes nothing.

Types

type Authored

type Authored struct {
	// Body is the stored HTML: the editor's export, with its React markers removed and its merge
	// tags unescaped, validated as content and sanitized.
	Body string
	// EditorVersion is the version of the editor whose export code rendered the document.
	EditorVersion string
}

Authored is a message written as an editor document: the body the server built from it and the editor version that built it. A save stores both beside the document, so a stored document always has its HTML and they never disagree (ADR 0040).

func FromDocument

func FromDocument(ctx context.Context, subject *string, doc []byte) (Authored, error)

FromDocument is the one step from an editor document to the body to store, shared by every place that holds a `doc` beside a `body` (Templates, Broadcasts, Automation steps). It runs the document renderer (the only caller on the write path: the send path never renders a document), then makes the export storable: the render's own markers are dropped, the quotes the editor HTML-escapes inside {{ }} and {% %} are restored so Liquid can read them, and the result goes through PrepareFields like any body, so the stored HTML survives SanitizeHTML unchanged.

A document that is invalid or too large for the editor is ErrInvalidContent with the reason; subject, when not nil, is checked with the body. A render that could not run (the request ended, or a time or memory limit of ours stopped it) says nothing about the document: it is ErrRenderUnavailable, which a client may retry. A failure of the runtime itself is returned as it is.

type Brand

type Brand struct {
	// Colors are the kit's colors; a message may use these.
	Colors []string
	// Fonts are the kit's font-family lists; a message's first family must be the first
	// family of one of them.
	Fonts []string
	// TextColor and BackgroundColor are what text and a page get when the message names none,
	// for the contrast check.
	TextColor, BackgroundColor string
}

Brand is what Lint compares a message's colors and fonts to: the Workspace's Brand kit. A nil *Brand (no Brand kit saved) switches the brand warnings off.

type Code

type Code string

Code is what a lint finding is about.

const (
	// CodeInvalidContent is the only error: the subject or body cannot render.
	CodeInvalidContent Code = "invalid_content"
	// CodeMissingAlt: an <img> has no alt attribute (alt="" is a decorative image and is fine).
	CodeMissingAlt Code = "missing_alt"
	// CodeLowContrast: text is below the WCAG AA contrast ratio against its background.
	CodeLowContrast Code = "low_contrast"
	// CodeOffBrandColor: a color is not one of the Brand kit's.
	CodeOffBrandColor Code = "off_brand_color"
	// CodeOffBrandFont: a font is not one of the Brand kit's.
	CodeOffBrandFont Code = "off_brand_font"
	// CodeUnknownConstruct: an element the sanitizer removes on save.
	CodeUnknownConstruct Code = "unknown_construct"
)

type Content

type Content struct {
	// Body is the body to store; nil when the write names none.
	Body *string
	// Authored is set when the content is a document: its body is Body.
	Authored *Authored
}

Content is what a write stores of a message's content.

func ResolveContent

func ResolveContent(ctx context.Context, subject, body *string, doc []byte, errBoth error) (Content, error)

ResolveContent resolves the body of a write: the one place Templates, Broadcasts and Automation steps decide it. A document is rendered to it (the one render of the save, never on the send path); a body is validated and sanitized; neither leaves the content alone; both is errBoth, the caller's own refusal.

type Email

type Email struct {
	Subject string
	HTML    string
	Text    string
}

Email is a fully-rendered message ready for tracking + delivery.

func RenderEmail

func RenderEmail(subject, body string, bindings map[string]any) (Email, error)

RenderEmail renders subject + HTML body for one recipient: Liquid merge tags first (so a {% if %} can change the markup per recipient; the subject is plain text and unescaped, body values are HTML-escaped), then the whole rendered body is sanitized again (a variable can carry markup or a URL the save-time pass never saw), then the text part is derived from that HTML (before any tracking is layered on). A non-nil error means the Liquid merge tags failed to render — the caller must not send that message (sending the raw template to a recipient is worse than not sending).

type Finding

type Finding struct {
	Code    Code
	Message string
	// Element is a short description of the element, such as `<img src="…">`.
	Element string
	// Detail is the offending value or the reason, such as `#ff0000` or `3.2:1, needs 4.5:1`.
	Detail string
}

Finding is one result of Lint. Message is fixed text of the server; Element and Detail quote the message or the Brand kit and are untrusted data wherever an agent reads them.

type Report

type Report struct {
	// Error is non-nil only when the message cannot render (ErrInvalidContent wrapped): it
	// blocks a save and a send.
	Error error
	// Warnings are everything else; they never block anything.
	Warnings []Finding
}

Report is the result of Lint over one message.

func Lint

func Lint(subject, body string, brand *Brand) Report

Lint is the one lint of a message (ADR 0040), over its rendered HTML: the save paths and the send planner use its Error, the editor's responses and the MCP tool email_lint return its Warnings too. Error is a Liquid syntax error or HTML that does not parse; the warnings are a missing image description, low contrast, a color or font outside the Brand kit, and an element the sanitizer removes. It runs on the message as the author wrote it, before SanitizeHTML (sanitizing would repair what it should refuse and hide what it should name), and with no recipient data: unbound merge tags render empty.

Message weight is deliberately not checked: no first-party source gives a size at which a mail client clips or penalizes a message (the only figure found is Beefree's own 80 KB setting).

Contrast follows WCAG 2.2 success criterion 1.4.3 (Contrast (Minimum), Level AA): 4.5:1 for text, 3:1 for large text (at least 18 pt, or 14 pt bold), with the relative luminance and contrast ratio formulas of https://www.w3.org/TR/WCAG22/#dfn-relative-luminance and https://www.w3.org/TR/WCAG22/#dfn-contrast-ratio.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL