Emailens
Integrations

React Email

Lint React Email templates against 21 email clients, locally and in CI, by checking the HTML that React Email exports.

React Email turns your components into HTML. Emailens tells you what 21 email clients do to that HTML.

npx @emailens/cli react-email

It finds the emails folder email dev uses (emails/ or src/emails/), renders every template with your project's own React Email, and checks the HTML. Nothing to export or configure:

  • PreviewProps are applied, so a prop with no default renders.
  • Local imports, any npm package and tsconfig paths work: the template is built with your project's own esbuild, the way email dev builds it.
  • Subfolders are included. Folders starting with _, static/, and files with no default export (shared components) are skipped.
  • It runs offline: no API key, no quota, no network call to Emailens.
npx @emailens/cli react-email --watch                      # re-check on save
npx @emailens/cli react-email --email-type transactional   # sign-in, receipts, alerts
npx @emailens/cli react-email path/to/emails               # another folder

--email-type transactional drops the unsubscribe requirement, which sign-in, receipt and alert emails do not have. Put "emailType": "transactional" in .emailensrc to make it the project default.

A template that throws while rendering fails the run with its file name and the error. A /static/... image is reported too: React Email serves that folder only in its preview server, so the image is broken in every inbox until the base URL points somewhere public.

In CI

# .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/cli react-email --reporter github

--reporter github puts each finding on the pull request. The exit code is 1 on any error, so the step fails the build.

Regressions, with the gate

EMAILENS_API_KEY=ek_live_... npx @emailens/cli react-email --gate

--gate also sends each rendered email to the pre-send gate under its file path. The gate keeps the last version of each file that passed, so a change that makes Outlook worse fails even when the score stays above the threshold. Pro plan.

Other ways to connect them

Lint the exported HTMLSend JSX directly
Works with local imports (./components/footer)YesNo
Works with any npm packageYesNo, only react, react-email and @react-email/components
Extra dependenciesNonesucrase and isolated-vm (CLI only)
Runs offlineYesCLI yes, API no
Best forProjects already exporting HTMLSingle self-contained files, the web app, AI agents

Lint the exported HTML

If you already run email export, lint its output.

npm install -D @emailens/cli

npx email export --outDir out --pretty
npx emailens lint 'out/*.html'

--pretty keeps line breaks in the exported HTML, so the line and column in each finding point at something you can read.

out/welcome.html
  error          spam            missing-unsubscribe  No unsubscribe link found; required by CAN-SPAM and GDPR.
  warn    37:47  protonmail      text-decoration      Proton Mail does not support "text-decoration".
  warn   37:124  outlook-windows,outlook-windows-legacy  border-radius  Outlook (New) does not support "border-radius".

lint exits 1 when it finds an error, so the same command gates CI.

Give every prop a default

email export renders each template with no props. It does not apply PreviewProps. A template that reads a prop without a default fails the export:

Error: TypeError: Cannot read properties of undefined (reading 'toUpperCase')

Default the props in the signature and the export, and the lint after it, both work:

export const Welcome = ({ name = "Ada" }: Partial<Props>) => (
  // ...
);

Templates in subfolders

emailens lint takes one path or one single-level glob. It does not expand **, and it ignores any argument after the first. If your emails live in subfolders, lint each exported file:

find out -name '*.html' -print0 | xargs -0 -n1 npx emailens lint

xargs exits non-zero if any file fails, so this also works as a CI gate.

GitHub Actions, exported HTML

# .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 email export --outDir out --pretty
      - run: find out -name '*.html' -print0 | xargs -0 -n1 npx emailens lint

This runs entirely on the runner. No API key, no quota, no network call to Emailens.

Tune what fails the build with .emailensrc, for example to skip the spam checks on transactional emails.

Alternative: send JSX directly

The API, the web app, the MCP server, and the CLI can also compile a React Email file themselves. The file runs in a sandbox, which is what makes it safe to accept from anyone, and also what limits it:

  • Imports: only react, react-email (where React Email 6 puts the components) and @react-email/components. A relative import or any other package is rejected.
  • Props: the default export is rendered with no props. PreviewProps is not applied.

So this path fits a single self-contained file, such as one an AI agent just wrote.

API

curl -X POST https://app.emailens.dev/api/preview \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer ek_your_api_key" \
  -d '{
    "format": "jsx",
    "html": "import { Html, Head, Body, Container, Text } from \"@react-email/components\";\nexport default function Email() {\n  return (\n    <Html>\n      <Head />\n      <Body style={{ fontFamily: \"Arial\" }}>\n        <Container>\n          <Text>Hello from React Email!</Text>\n        </Container>\n      </Body>\n    </Html>\n  );\n}"
  }'

The response adds a compiledHtml field to the usual transforms, warnings, and scores.

Without an API key the endpoint allows 3 previews a day per IP.

CLI

npm install -D @emailens/cli sucrase isolated-vm
npx emailens lint emails/welcome.tsx

sucrase transpiles the JSX and isolated-vm is the sandbox it runs in. react and @react-email/components are already in a React Email project.

Errors

A file that fails to compile returns 422 with the reason and the phase it failed in:

{
  "error": "JSX execution error: Import of \"./footer\" is not allowed. Only \"react\" and \"@react-email/components\" can be imported.",
  "phase": "execution"
}
phaseMeaning
transpileThe JSX has a syntax error
executionThe module failed to load, usually a disallowed import
renderThe component threw while rendering, usually a prop with no default

Fix snippets in React Email syntax

With format: "jsx", fixes are written as React Email components rather than raw HTML tables and VML:

ProblemGeneric HTML fixReact Email fix
Flexbox in Outlook<table> with <!--[if mso]><Row> + <Column> components
CSS Grid<table> layout<Row> + <Column> components
@font-faceWeb-safe font stack<Font> component in <Head>
<svg><img> replacement<Img> component
<video>Linked thumbnail<Img> + <Link> components
max-width in OutlookMSO conditional table wrapper<Container> component
border-radius in OutlookVML <v:roundrect>dangerouslySetInnerHTML with VML
gap propertyPadding on <td> elementspaddingRight on <Column>
<style> in GmailInline CSSstyle={{ }} props
<link> stylesheetInline <style> block<Head> component

To get these for exported HTML, call the engine with the framework set:

import { analyzeEmail, getCodeFix } from "@emailens/engine";

// Warnings carry React Email fix snippets (fix.language === "jsx")
const warnings = analyzeEmail(exportedHtml, "jsx");

// Or look up one fix
const fix = getCodeFix("display:flex", "outlook-windows-legacy", "jsx");
console.log(fix.description);
// Use React Email Row + Column components instead of flexbox (Outlook-safe)

On this page