MJML compiles the HTML.
21 clients rewrite it.
Emailens compiles your .mjml files with your own mjml package and tells you what Gmail, Outlook and 19 other clients will do to the result. One command. Runs locally. No account.
npm install -D @emailens/cli mjml
npx emailens lint 'emails/*.mjml' Findings with a client and a reason
This is an excerpt of the output for a template built from mj-font, mj-section, mj-image and mj-button. The command exits non-zero when it finds an error.
emails/welcome.mjml
error gmail-web,gmail-android,gmail-ios+13 <link> Gmail does not support external stylesheets.
error spam missing-unsubscribe No unsubscribe link found; required by CAN-SPAM and GDPR.
warn gmail-web,gmail-android,gmail-ios+3 <style> Gmail has partial <style> support (head only, with limitations).
warn outlook-windows border-radius Outlook (New) does not support "border-radius". The gap between your preview and the inbox
MJML handles the table layout and the Outlook conditionals for you. What you put inside the components still reaches each client as-is. These are findings from a template built with stock components.
<mj-font> It compiles to a stylesheet <link>, which Gmail and 13 other clients drop. The web font never loads there, so the fallback in font-family is what most readers see.
border-radius Outlook on Windows does not support it, so a rounded mj-button or mj-section ships square.
<style> Gmail reads a <style> block only in the head, and with limits. Rules you add through mj-style can be dropped where MJML's inline attributes are not.
text-decoration Proton Mail does not support it, and it is what removes the underline from a button or link.
The email as a whole
Missing unsubscribe link, missing physical address, text too small to read: the things spam filters and inbox previews look at, which MJML has no opinion on.
Block the pull request that breaks Outlook
The same command, on the runner. No API key, no quota, no network call to Emailens.
One thing to set up first: install mjml beside the CLI
Compilation failed (compile): MJML compilation requires "mjml". Install it:
npm install mjml The CLI does not bundle a compiler. It uses the mjml version in your project, so the HTML it lints is the HTML you ship.
GitHub Actions
# .github/workflows/email-lint.yml
name: Email lint
on:
pull_request:
paths: ["emails/**"]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- run: npm ci
- run: npx emailens lint 'emails/**/*.mjml' Your AI writes the email. Emailens checks it.
Add the MCP server and Claude Code or Cursor can lint the template it just wrote, read the findings, and fix them before you review the diff.
$ claude mcp add emailens -- npx -y @emailens/mcp Common questions
Does Emailens replace MJML? +
No. MJML is how you write and compile the email. Emailens checks the HTML it produces against 21 email clients. You keep your templates, your build, and your sending provider.
Do I need an account or an API key? +
No. The CLI runs the open-source @emailens/engine on your machine or your CI runner. There is no quota and nothing is uploaded. An account is only needed for hosted screenshots, share links, and the REST API.
MJML already handles Outlook. What is left to check? +
MJML solves layout: columns, widths, the conditional tables Outlook needs. It passes your styling choices through. Web fonts, rounded corners, anything in mj-style or mj-raw, and the content checks (unsubscribe link, contrast, link and image hygiene) are still yours.
Can I lint compiled HTML instead of the .mjml source? +
Yes. Point the same command at the .html files your build writes. Linting compiled HTML also gives each finding a line and column, and does not need mjml installed beside the CLI.
Are the suggested fixes written for MJML? +
In the hosted preview and the engine, yes. Send the template with format mjml and a fix comes back as MJML: mj-section and mj-column in place of flexbox, mj-font in the head, background-url on mj-section in place of a hand-written VML block.
How is the score calculated? +
Each client gets a score from 0 to 100. The headline score is the worst client, not an average, because an email that renders in twenty clients and breaks in Outlook is a broken email.
Lint your next template
Runs locally. No signup required.
$ npx emailens lint 'emails/*.mjml' Using something else? Lint React Email templates or Lint Maizzle templates