hemmer
A Rust pipeline for transforming HTML into email-client-ready output.
hemmer takes HTML — optionally with Tailwind utility classes — and runs it
through a configurable set of transformations that paper over the quirks of
email clients: CSS inlining, table attribute defaults, Outlook conditional
comments, rem → px conversion, CSS variable resolution, and a long list
of smaller fixes.
Status
Early but functional. 151 tests passing. The API will probably keep shifting
until it stabilizes around a 0.x release.
Origin
hemmer was built to enable HTML + Tailwind email templates as a
replacement for MJML in an Elixir/Phoenix app. MJML
makes it hard to picture what you'll end up with until it's compiled, and
LLM-based assistants struggle with its custom markup — both humans and
models are far more comfortable in plain HTML and Tailwind.
Maizzle was the original inspiration for the transformer pipeline, but it brings a Node.js dependency and a templating layer we didn't need (HEEx already handles that), and we wanted runtime processing instead of a build step. Building this in Rust gave us all of that and a clean Elixir NIF integration via Rustler.
The name hemmer is a sewing term — a machine attachment that folds the edge of fabric to create a clean hem. Fitting for a tool that takes raw HTML and gives it a clean edge for the limitations of email clients.
What it does
hemmer runs a pipeline of independent transformers. Most are enabled by
default; a few are opt-in. They're applied in a fixed order optimized for
email output.
CSS generation and inlining
tailwind— runtime Tailwind CSS generation via encre-css, with email-safe hex color overrides for all 22 Tailwind v3 color scales (nooklch())safe_class_names— rewrites Tailwind escaped characters (\:,\/,[,],%,#, …) to email-safe equivalents in bothclassattributes and<style>tag contentsinline_css— moves<style>rules into elementstyleattributes via css-inlineclass_cleanup— removes inlined classes from elements while preserving classes referenced by@mediaqueriespurge_css— removes unused rules from<style>blocks (opt-in; the Tailwind generator already produces only used CSS)
Email-client compatibility
email_compat_css— convertsrem→pxand CSS logical properties (padding-inline,margin-block, …) to physical equivalents. Outlook, Gmail, and Yahoo support neither.resolve_props— replacesvar(--name)references with the static values from:rootdeclarations. Outlook desktop doesn't support custom properties.resolve_calc— evaluatescalc()expressions with same-unit arithmetic to constants. Outlook desktop doesn't supportcalc().style_to_attr— copies CSSwidth/height/bgcolor/alignvalues into HTML attributes. Outlook desktop's Word renderer often honors HTML attributes when it ignores CSS.attribute_to_style— the opposite direction: copies HTML presentational attributes into inline CSS for modern clients (opt-in).
HTML defaults and cleanup
html_transforms— addscellpadding="0" cellspacing="0" role="none"to tables, ensures<img>has analtattribute, expands 3-digit hex colors inbgcolorandcolorattributesoutlook_tags—<outlook>and<not-outlook>tags become MSO conditional comments. Supports version names (only="2013"→[if mso 15]) and the fullonly/not/lt/lte/gt/gteattribute set.widows— inserts between the last two words in elements marked withprevent-widowsorno-widowsbase_url— resolves relative URLs in HTML attributes (src,href,srcset, …) and CSSurl()values inside both inline styles and<style>tagsurl_params— appends UTM/tracking parameters to absolute URLs in<a>tagsmeta_tags— auto-injects DOCTYPE, charset, viewport, and format-detection meta tags if missingremove_attributes— configurable removal with empty / always-remove / exact-value / regex-match rulesminify— final HTML minification (opt-in)
What it doesn't do
hemmer is not a templating engine. Bring your own — HEEx, Tera,
MiniJinja, plain string formatting, anything. The pipeline runs after
templating, on a complete HTML string.
It also doesn't:
- generate plaintext versions of emails (do this in the layer above)
- send the email (use Swoosh, lettre, etc.)
- convert MJML markup (it's an alternative to MJML, not a converter)
Usage
use hemmer::Pipeline;
let html = r#"
<html>
<head></head>
<body>
<table>
<tr>
<td class="p-6 bg-indigo-600 text-white text-center">
<h1 class="text-xl font-bold">Welcome!</h1>
</td>
</tr>
</table>
</body>
</html>
"#;
let result = Pipeline::with_tailwind().process(html)?;
println!("{}", result.html);This generates Tailwind CSS for the classes used in the document, inlines it, applies all the email-compatibility transforms, and injects DOCTYPE and meta tags.
For a minimal pipeline that only does what you opt into:
use hemmer::{Pipeline, InlineCssConfig};
let result = Pipeline::minimal()
.inline_css(InlineCssConfig::default())
.minify(true)
.process(html)?;
The full builder API lets you toggle every transformer individually. See
src/pipeline.rs for the available methods.
Using from Elixir
hemmer is designed to be wrapped as a
Rustler NIF for Elixir
applications. There's no first-party Elixir package yet — wire it up in your
project's native/ directory with a thin Rust wrapper:
# native/email_nif/Cargo.toml
[package]
name = "email_nif"
version = "0.1.0"
edition = "2024"
[lib]
crate-type = ["cdylib"]
[dependencies]
rustler = "0.36"
hemmer = "0.1" # or path = "..." for local development// native/email_nif/src/lib.rs
#[rustler::nif(schedule = "DirtyCpu")]
fn process(html: &str) -> Result<String, String> {
hemmer::Pipeline::with_tailwind()
.process(html)
.map(|r| r.html)
.map_err(|e| e.to_string())
}
rustler::init!("Elixir.YourApp.EmailTransformer");# lib/your_app/email_transformer.ex
defmodule YourApp.EmailTransformer do
use Rustler, otp_app: :your_app, crate: "email_nif"
def process(_html), do: :erlang.nif_error(:nif_not_loaded)
endInspired by
- Maizzle — for the transformer-pipeline
architecture and a long list of email-client gotchas to work around.
Several transformers in
hemmermirror Maizzle's defaults closely. - encre-css — runtime Tailwind CSS generation in Rust, which makes the whole pipeline possible without a Node-based build step.
- css-inline — the actual CSS inlining engine, built on Servo's CSS parser.
- lol_html — Cloudflare's
streaming HTML rewriter, used everywhere
hemmerneeds to manipulate the DOM.
License
MIT