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, rempx 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

Email-client compatibility

HTML defaults and cleanup

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:

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)
end

Inspired by

License

MIT