Hacker News new | ask | show | jobs
Show HN: Open-sourced an email QA lib 8 checks across 12 clients in 1 audit call (github.com)
1 points by tikkatenders 104 days ago
Hey HN, I built this because I think $500 for litmus just to check whether my emails would break in Outlook. After Sinch acquired them and pushed prices even higher, I started wondering how much of that could be a library instead of a SaaS dashboard. Turns out: most of it.

@emailens/engine is a single npm package that runs 8 analysis passes on your email HTML in one call:

1. CSS compatibility: 250+ properties checked against 12 email clients, with per-client scores (0–100). Each warning includes severity, the specific client, and a fix snippet tailored to your framework (React Email, MJML, Maizzle, or plain HTML).

2. Spam scoring: 45+ heuristic signals modeled after SpamAssassin, CAN-SPAM, and GDPR. Catches caps ratio, hidden text, URL shorteners, missing unsubscribe (with transactional email exemption), deceptive links, image-to-text ratio, and more.

3. Accessibility: WCAG contrast, alt text, heading hierarchy, layout table roles, small text, missing lang attribute.

4. Link validation: broken hrefs, insecure HTTP, javascript: protocols, deceptive URLs, empty mailto/tel, generic link text.

5. Image analysis: missing dimensions, oversized data URIs, WebP/SVG in email, tracking pixels, missing display:block.

6. Inbox preview: subject/preheader truncation per client, Gmail clipping detection (102KB threshold).

7. Domain deliverability: SPF, DKIM, DMARC, MX, and BIMI DNS validation. No external dependencies, uses node:dns/promises.

8. Template variables: catches unresolved merge tags across Handlebars, ERB, Mailchimp, Salesforce, etc.

The engine parses the HTML once and shares the DOM across all analyzers, running all 8 checks is basically the cost of one parse. Framework-aware fixes are where it gets interesting. Every CSS warning comes with a fix snippet tailored to your framework:

React Email JSX → component-level fixes (e.g., VML components for Outlook border-radius) MJML → <mj-*> element suggestions Maizzle → utility class alternatives Plain HTML → inline CSS / VML fallbacks

Fixes are classified as css (simple property swap) or structural (needs HTML restructuring). For structural issues, there's an AI fix layer, the engine builds a context-rich prompt from the warnings, scores, and original source, and delegates to whatever LLM you provide. It's BYOK: you pass a provider function, the engine handles the prompt engineering.

React Email compilation runs in a sandboxed environment (isolated-vm for true heap isolation on servers, node:vm for local/CLI use, or QuickJS via WASM). Maizzle blocks filesystem-accessing PostHTML directives at validation time. MJML uses the standard compiler. There's also a CSS transformation layer, not just flagging issues, but rewriting the HTML per client. Inlines <style> blocks for Gmail, strips unsupported properties for Outlook, and can simulate dark mode (full inversion for Gmail Android, partial for Apple Mail, none for Outlook Windows). 574 tests covering all of the above. MIT licensed. No accounts, no dashboards, no vendor lock-in.

I'd love feedback on:

The API surface, does auditEmail() (all-in-one) vs createSession() (selective checks, shared parse) make sense as the two entry points?

The scoring formula: 100 - (errors × 15) - (warnings × 5) - (info × 1), too aggressive? Too lenient?

Missing email clients or checks you'd want to see?

Repo: https://github.com/emailens/engine

Docs: https://docs.emailens.dev

If you'd rather not wire it up yourself: https://emailens.dev

1 comments

The spam scoring caught my eye — 45+ heuristic signals is a lot. How do you handle false positives for transactional emails? A password reset or order confirmation might legitimately trigger some of those signals (no unsubscribe, image-heavy, urgent language) even though they're completely clean emails. Does the transactional exemption you mention cover most of those cases or is there still manual tuning needed?
Good question! The engine handles this at two levels:

Auto-detection: It scans for transactional signals ("reset your password", "order confirmation", OTP codes, etc.). If it finds 2+, it automatically downgrades the missing-unsubscribe penalty to zero.

Explicit declaration: Callers can pass `emailType: "transactional"`, which fully exempts from both unsubscribe and CAN-SPAM physical address checks.

The compliance rules (unsubscribe, physical address) get exempted because they don't legally apply to transactional mail. But deliverability rules (image ratio, hidden text, deceptive links) stay active intentionally. A transactional email that's 100% images is still a deliverability problem regardless of type.

In practice, well-crafted transactional emails rarely trigger those remaining rules since they tend to be simple, text-forward templates.