Sigra.Email.CssLint (Sigra v1.20.0)

Copy Markdown View Source

Rendered email CSS allowlist / deny-list gate backed by vendored caniemail data.

Validates rendered email HTML against a curated subset of caniemail.com support data for the Phase 86 locked client set: Gmail web, new Outlook web, and Apple Mail on macOS.

The policy is read from priv/sigra/email/caniemail-allowlist.json at compile time — no network access occurs during CI. Call lint/1 from ExUnit tests to fail the build when rendered HTML uses unsupported CSS constructs.

Usage in ExUnit

test "email CSS passes caniemail lint" do
  email = Emails.suspicious_login_email(user, details)
  assert :ok = Sigra.Email.CssLint.lint(email.html_body)
end

Scope

  • Clients: Gmail web, new Outlook web (Microsoft 365), Apple Mail macOS
  • Residual (documented, not waived): Legacy Outlook desktop Word-engine is partially addressed by the deny-list but is explicitly out-of-scope per D-86-09. Microsoft EOL: October 2026.
  • Policy source: vendored priv/sigra/email/caniemail-allowlist.json (MIT-licensed caniemail data, curated 2026-04-26)

See 86-CONTEXT.md D-86-02, D-86-03, D-86-09 for full rationale.

Summary

Functions

Returns the vendored caniemail policy map.

Lints rendered email HTML against the vendored caniemail policy.

Functions

allowlist()

@spec allowlist() :: map()

Returns the vendored caniemail policy map.

The map has three top-level keys:

  • "clients" — list of client identifiers this policy applies to
  • "allow_css" — CSS properties safe to use in inline styles
  • "deny_css" — CSS property patterns that break layout in one or more clients

lint(html)

@spec lint(String.t()) :: :ok | {:error, [String.t()]}

Lints rendered email HTML against the vendored caniemail policy.

Returns :ok when no violations are found. Returns {:error, [String.t()]} with a list of human-readable violation descriptions when deny-listed CSS constructs are detected.

This function is intentionally simple and pattern-based — it does not parse the CSS AST. That is sufficient for the deny-list constructs that matter (they produce large, obvious substrings in inline styles).

Examples

iex> Sigra.Email.CssLint.lint("<div style='background-color: #fff;'>Hi</div>")
:ok

iex> Sigra.Email.CssLint.lint("<div style='display: flex;'>Hi</div>")
{:error, ["display:flex or display:flex found (not supported by Gmail web / Outlook web)"]}