@emailens/engine 0.8.0 → 0.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +95 -603
- package/dist/index.cjs +53 -15
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +53 -15
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,514 +1,69 @@
|
|
|
1
1
|
# @emailens/engine
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/@emailens/engine)
|
|
4
|
+
[](./LICENSE)
|
|
5
|
+
[]()
|
|
6
|
+
[](https://nodejs.org/)
|
|
4
7
|
|
|
5
|
-
|
|
8
|
+
**Your email looks perfect in Apple Mail. Gmail strips half the CSS. Outlook renders it in Word.**
|
|
6
9
|
|
|
7
|
-
|
|
10
|
+
`@emailens/engine` analyzes your HTML against 250+ CSS properties across 12 email clients, scores compatibility, and shows you exactly what to fix — before you hit send.
|
|
11
|
+
|
|
12
|
+
> **[emailens.dev](https://emailens.dev)** — Try the hosted version. Paste HTML, get a full audit in seconds.
|
|
13
|
+
|
|
14
|
+
## Quick Start
|
|
8
15
|
|
|
9
16
|
```bash
|
|
10
17
|
npm install @emailens/engine
|
|
11
|
-
# or
|
|
12
|
-
bun add @emailens/engine
|
|
13
18
|
```
|
|
14
19
|
|
|
15
|
-
Requires Node.js >= 18.
|
|
16
|
-
|
|
17
|
-
## Quick Start
|
|
18
|
-
|
|
19
20
|
```typescript
|
|
20
21
|
import { auditEmail } from "@emailens/engine";
|
|
21
22
|
|
|
23
|
+
// Flexbox + gap + box-shadow — all Outlook killers
|
|
22
24
|
const html = `<html lang="en">
|
|
23
|
-
<head><title>
|
|
24
|
-
<style
|
|
25
|
+
<head><title>Weekly Update</title>
|
|
26
|
+
<style>
|
|
27
|
+
.card { border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
|
|
28
|
+
</style>
|
|
25
29
|
</head>
|
|
26
30
|
<body>
|
|
27
31
|
<div class="card" style="display: flex; gap: 16px;">
|
|
28
32
|
<div>Column A</div>
|
|
29
33
|
<div>Column B</div>
|
|
30
34
|
</div>
|
|
31
|
-
<a href="https://example.com/unsubscribe">Unsubscribe</a>
|
|
32
35
|
</body>
|
|
33
36
|
</html>`;
|
|
34
37
|
|
|
35
|
-
// Run all checks in one call
|
|
36
38
|
const report = auditEmail(html, { framework: "jsx" });
|
|
37
39
|
|
|
40
|
+
console.log(report.compatibility.scores["outlook-windows"]);
|
|
41
|
+
// { score: 30, errors: 3, warnings: 3, info: 1 }
|
|
42
|
+
// ↑ Outlook uses Word — flexbox, gap, box-shadow, border-radius all break
|
|
43
|
+
|
|
38
44
|
console.log(report.compatibility.scores["gmail-web"]);
|
|
39
45
|
// { score: 75, errors: 0, warnings: 5, info: 0 }
|
|
40
46
|
|
|
41
|
-
console.log(report.spam);
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
console.log(report.accessibility.score);
|
|
45
|
-
// 88
|
|
46
|
-
|
|
47
|
-
console.log(report.links.totalLinks);
|
|
48
|
-
// 1
|
|
49
|
-
|
|
50
|
-
console.log(report.images.total);
|
|
51
|
-
// 0
|
|
52
|
-
|
|
53
|
-
console.log(report.inboxPreview.subject);
|
|
54
|
-
// "Newsletter"
|
|
55
|
-
|
|
56
|
-
console.log(report.size.clipped);
|
|
57
|
-
// false
|
|
58
|
-
|
|
59
|
-
console.log(report.templateVariables.unresolvedCount);
|
|
60
|
-
// 0
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
## API Reference
|
|
64
|
-
|
|
65
|
-
### `auditEmail(html: string, options?: AuditOptions): AuditReport`
|
|
66
|
-
|
|
67
|
-
**Unified API** — runs all 8 email analysis checks in a single call. Returns compatibility warnings + scores, spam analysis, link validation, accessibility audit, image analysis, inbox preview extraction, size checking, and template variable detection.
|
|
68
|
-
|
|
69
|
-
Internally parses the HTML once and shares the DOM across all analyzers.
|
|
70
|
-
|
|
71
|
-
```typescript
|
|
72
|
-
import { auditEmail } from "@emailens/engine";
|
|
73
|
-
|
|
74
|
-
const report = auditEmail(html, {
|
|
75
|
-
framework: "jsx", // attach framework-specific fix snippets
|
|
76
|
-
spam: { emailType: "transactional" }, // skip unsubscribe check
|
|
77
|
-
skip: ["images"], // skip specific checks
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
// report.compatibility.warnings — CSSWarning[]
|
|
81
|
-
// report.compatibility.scores — Record<string, ClientScore>
|
|
82
|
-
// report.spam — SpamReport
|
|
83
|
-
// report.links — LinkReport
|
|
84
|
-
// report.accessibility — AccessibilityReport
|
|
85
|
-
// report.images — ImageReport
|
|
86
|
-
// report.inboxPreview — InboxPreview
|
|
87
|
-
// report.size — SizeReport
|
|
88
|
-
// report.templateVariables — TemplateReport
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
**`AuditOptions`:**
|
|
92
|
-
- `framework?: "jsx" | "mjml" | "maizzle"` — attach framework-specific fix snippets
|
|
93
|
-
- `spam?: SpamAnalysisOptions` — options for spam analysis
|
|
94
|
-
- `skip?: Array<"spam" | "links" | "accessibility" | "images" | "compatibility" | "inboxPreview" | "size" | "templateVariables">` — skip specific checks
|
|
95
|
-
|
|
96
|
-
---
|
|
97
|
-
|
|
98
|
-
### `createSession(html: string, options?: CreateSessionOptions): EmailSession`
|
|
99
|
-
|
|
100
|
-
**Session API** — pre-parses the HTML once and exposes all analysis methods on the shared DOM. Use this when you need to call multiple analysis functions on the same HTML to avoid redundant parsing.
|
|
101
|
-
|
|
102
|
-
```typescript
|
|
103
|
-
import { createSession } from "@emailens/engine";
|
|
104
|
-
|
|
105
|
-
const session = createSession(html, { framework: "jsx" });
|
|
106
|
-
|
|
107
|
-
// All analysis methods share a single DOM parse:
|
|
108
|
-
const warnings = session.analyze();
|
|
109
|
-
const scores = session.score(warnings);
|
|
110
|
-
const spam = session.analyzeSpam();
|
|
111
|
-
const links = session.validateLinks();
|
|
112
|
-
const a11y = session.checkAccessibility();
|
|
113
|
-
const images = session.analyzeImages();
|
|
114
|
-
const preview = session.extractInboxPreview();
|
|
115
|
-
const size = session.checkSize();
|
|
116
|
-
const templates = session.checkTemplateVariables();
|
|
117
|
-
|
|
118
|
-
// Or run everything at once:
|
|
119
|
-
const report = session.audit();
|
|
120
|
-
|
|
121
|
-
// Transforms and dark mode still work (parse internally per client):
|
|
122
|
-
const transforms = session.transformForAllClients();
|
|
123
|
-
const darkMode = session.simulateDarkMode("gmail-web");
|
|
124
|
-
```
|
|
125
|
-
|
|
126
|
-
**`CreateSessionOptions`:**
|
|
127
|
-
- `framework?: "jsx" | "mjml" | "maizzle"` — framework for fix snippets (applies to all session methods)
|
|
128
|
-
|
|
129
|
-
**`EmailSession` methods:**
|
|
130
|
-
|
|
131
|
-
| Method | Shares DOM | Description |
|
|
132
|
-
|---|---|---|
|
|
133
|
-
| `audit(options?)` | Yes | Run all checks (equivalent to `auditEmail`) |
|
|
134
|
-
| `analyze()` | Yes | CSS compatibility warnings |
|
|
135
|
-
| `score(warnings)` | — | Generate per-client scores |
|
|
136
|
-
| `analyzeSpam(options?)` | Yes | Spam indicator analysis |
|
|
137
|
-
| `validateLinks()` | Yes | Link validation |
|
|
138
|
-
| `checkAccessibility()` | Yes | Accessibility audit |
|
|
139
|
-
| `analyzeImages()` | Yes | Image analysis |
|
|
140
|
-
| `extractInboxPreview()` | Yes | Subject line and preheader extraction |
|
|
141
|
-
| `checkSize()` | Yes | Gmail clipping size check |
|
|
142
|
-
| `checkTemplateVariables()` | Yes | Unresolved template variable detection |
|
|
143
|
-
| `checkDeliverability(domain)` | — | DNS deliverability check (async, SPF/DKIM/DMARC/MX/BIMI) |
|
|
144
|
-
| `transformForClient(clientId)` | No | Transform for one client |
|
|
145
|
-
| `transformForAllClients()` | No | Transform for all 12 clients |
|
|
146
|
-
| `simulateDarkMode(clientId)` | No | Dark mode simulation |
|
|
147
|
-
|
|
148
|
-
**When to use sessions vs standalone functions:**
|
|
149
|
-
|
|
150
|
-
- **Multiple analysis calls on the same HTML** → use `createSession()` to avoid redundant parsing
|
|
151
|
-
- **Single analysis call** → use standalone functions (`auditEmail`, `analyzeEmail`, etc.)
|
|
152
|
-
- **Server-side batch processing** → use `createSession()` per email for best throughput
|
|
153
|
-
|
|
154
|
-
---
|
|
155
|
-
|
|
156
|
-
### `analyzeEmail(html: string, framework?: Framework): CSSWarning[]`
|
|
157
|
-
|
|
158
|
-
Analyzes an HTML email and returns CSS compatibility warnings for all 12 email clients. Detects `<style>`, `<link>`, `<svg>`, `<video>`, `<form>`, inline CSS properties, `@font-face`, `@media` queries, gradients, flexbox/grid, and more.
|
|
159
|
-
|
|
160
|
-
The optional `framework` parameter controls which fix snippets are attached to warnings. Analysis always runs on compiled HTML.
|
|
161
|
-
|
|
162
|
-
```typescript
|
|
163
|
-
const warnings = analyzeEmail(html); // Plain HTML
|
|
164
|
-
const warnings = analyzeEmail(html, "jsx"); // React Email fixes
|
|
165
|
-
const warnings = analyzeEmail(html, "mjml"); // MJML fixes
|
|
166
|
-
```
|
|
167
|
-
|
|
168
|
-
### `generateCompatibilityScore(warnings): Record<string, ClientScore>`
|
|
169
|
-
|
|
170
|
-
Generates a 0–100 compatibility score per email client. Formula: `100 - (errors × 15) - (warnings × 5) - (info × 1)`.
|
|
171
|
-
|
|
172
|
-
### `warningsForClient(warnings, clientId): CSSWarning[]`
|
|
173
|
-
|
|
174
|
-
Filter warnings for a specific client.
|
|
175
|
-
|
|
176
|
-
### `errorWarnings(warnings): CSSWarning[]`
|
|
177
|
-
|
|
178
|
-
Get only error-severity warnings.
|
|
179
|
-
|
|
180
|
-
### `structuralWarnings(warnings): CSSWarning[]`
|
|
181
|
-
|
|
182
|
-
Get only warnings that require HTML restructuring (`fixType: "structural"`).
|
|
183
|
-
|
|
184
|
-
---
|
|
185
|
-
|
|
186
|
-
### `analyzeSpam(html: string, options?: SpamAnalysisOptions): SpamReport`
|
|
187
|
-
|
|
188
|
-
Analyzes an HTML email for content hygiene issues. Returns a 0–100 score (100 = clean) and an array of issues. Uses heuristic rules modeled after SpamAssassin, CAN-SPAM, and GDPR.
|
|
189
|
-
|
|
190
|
-
> **Note:** Content hygiene heuristics — not a real spam filter. This checks for common anti-patterns that trigger spam filters but cannot predict actual inbox placement. For real spam testing, use the `checkSpamAssassin()` integration or a dedicated service.
|
|
191
|
-
|
|
192
|
-
```typescript
|
|
193
|
-
import { analyzeSpam } from "@emailens/engine";
|
|
194
|
-
|
|
195
|
-
const report = analyzeSpam(html, {
|
|
196
|
-
emailType: "transactional", // skip unsubscribe check
|
|
197
|
-
listUnsubscribeHeader: "...", // satisfies unsubscribe requirement
|
|
198
|
-
});
|
|
199
|
-
// { score: 95, level: "low", issues: [...] }
|
|
200
|
-
```
|
|
201
|
-
|
|
202
|
-
**Checks:** caps ratio, excessive punctuation, spam trigger phrases, missing unsubscribe link (with transactional email exemption), hidden text, URL shorteners, image-to-text ratio, deceptive links (with ESP tracking domain allowlist), all-caps subject.
|
|
203
|
-
|
|
204
|
-
### `validateLinks(html: string): LinkReport`
|
|
205
|
-
|
|
206
|
-
Static analysis of all links in an HTML email. No network requests.
|
|
207
|
-
|
|
208
|
-
```typescript
|
|
209
|
-
import { validateLinks } from "@emailens/engine";
|
|
210
|
-
|
|
211
|
-
const report = validateLinks(html);
|
|
212
|
-
// { totalLinks: 12, issues: [...], breakdown: { https: 10, http: 1, mailto: 1, ... } }
|
|
213
|
-
```
|
|
214
|
-
|
|
215
|
-
**Checks:** empty/placeholder hrefs, `javascript:` protocol, insecure HTTP, generic link text, missing accessible names, empty mailto/tel, very long URLs, duplicate links.
|
|
216
|
-
|
|
217
|
-
### `checkAccessibility(html: string): AccessibilityReport`
|
|
218
|
-
|
|
219
|
-
Audits an HTML email for accessibility issues. Returns a 0–100 score and detailed issues.
|
|
220
|
-
|
|
221
|
-
```typescript
|
|
222
|
-
import { checkAccessibility } from "@emailens/engine";
|
|
223
|
-
|
|
224
|
-
const report = checkAccessibility(html);
|
|
225
|
-
// { score: 88, issues: [...] }
|
|
226
|
-
```
|
|
227
|
-
|
|
228
|
-
**Checks:** missing `lang` attribute, missing `<title>`, image alt text, link accessibility, layout table roles, small text, color contrast (WCAG 2.1), heading hierarchy.
|
|
229
|
-
|
|
230
|
-
### `analyzeImages(html: string): ImageReport`
|
|
231
|
-
|
|
232
|
-
Analyzes images for email best practices.
|
|
233
|
-
|
|
234
|
-
```typescript
|
|
235
|
-
import { analyzeImages } from "@emailens/engine";
|
|
236
|
-
|
|
237
|
-
const report = analyzeImages(html);
|
|
238
|
-
// { total: 5, totalDataUriBytes: 0, issues: [...], images: [...] }
|
|
239
|
-
```
|
|
240
|
-
|
|
241
|
-
**Checks:** missing dimensions, oversized data URIs, missing alt, WebP/SVG format, missing `display:block`, tracking pixels, high image count.
|
|
242
|
-
|
|
243
|
-
### `extractInboxPreview(html: string): InboxPreview`
|
|
244
|
-
|
|
245
|
-
Extracts subject line (from `<title>`) and preheader text from the email HTML. Returns per-client truncation data showing how subject and preheader will appear across 8 email clients.
|
|
246
|
-
|
|
247
|
-
```typescript
|
|
248
|
-
import { extractInboxPreview } from "@emailens/engine";
|
|
249
|
-
|
|
250
|
-
const preview = extractInboxPreview(html);
|
|
251
|
-
// { subject: "Newsletter", preheader: "This week's highlights...",
|
|
252
|
-
// subjectLength: 10, preheaderLength: 28,
|
|
253
|
-
// truncation: [...], issues: [...] }
|
|
254
|
-
```
|
|
255
|
-
|
|
256
|
-
**Checks:** missing `<title>`, subject too long, missing preheader, preheader too short/long, `‌ ` padding hack, emoji in subject.
|
|
257
|
-
|
|
258
|
-
### `checkSize(html: string): SizeReport`
|
|
259
|
-
|
|
260
|
-
Checks email HTML byte size for Gmail clipping issues. Gmail clips messages larger than ~102KB, hiding content behind a "View entire message" link.
|
|
261
|
-
|
|
262
|
-
```typescript
|
|
263
|
-
import { checkSize } from "@emailens/engine";
|
|
264
|
-
|
|
265
|
-
const report = checkSize(html);
|
|
266
|
-
// { htmlBytes: 45230, humanSize: "44.2 KB", clipped: false, issues: [] }
|
|
267
|
-
```
|
|
268
|
-
|
|
269
|
-
**Checks:** Gmail clipping threshold (102KB), approaching clip threshold warning (90KB).
|
|
270
|
-
|
|
271
|
-
### `checkTemplateVariables(html: string): TemplateReport`
|
|
272
|
-
|
|
273
|
-
Scans email HTML for unresolved template/merge variables in text content and key attributes (`href`, `src`, `alt`).
|
|
274
|
-
|
|
275
|
-
```typescript
|
|
276
|
-
import { checkTemplateVariables } from "@emailens/engine";
|
|
277
|
-
|
|
278
|
-
const report = checkTemplateVariables(html);
|
|
279
|
-
// { unresolvedCount: 0, issues: [] }
|
|
47
|
+
console.log(report.spam.score); // 100 (clean)
|
|
48
|
+
console.log(report.accessibility.score); // 88
|
|
49
|
+
console.log(report.size.clipped); // false (under Gmail's 102KB limit)
|
|
280
50
|
```
|
|
281
51
|
|
|
282
|
-
|
|
52
|
+
## Score too low? Fix it
|
|
283
53
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
### `checkDeliverability(domain, options?): Promise<DeliverabilityReport>`
|
|
287
|
-
|
|
288
|
-
Validates email deliverability for a domain by checking MX, SPF, DKIM, DMARC, and BIMI DNS records. All DNS queries have a 5-second timeout. No external dependencies — uses `node:dns/promises`.
|
|
289
|
-
|
|
290
|
-
```typescript
|
|
291
|
-
import { checkDeliverability } from "@emailens/engine";
|
|
292
|
-
|
|
293
|
-
const report = await checkDeliverability("example.com");
|
|
294
|
-
console.log(report.score); // 0-100
|
|
295
|
-
console.log(report.checks); // individual check results
|
|
296
|
-
console.log(report.issues); // actionable issues
|
|
297
|
-
```
|
|
298
|
-
|
|
299
|
-
**Checks:**
|
|
300
|
-
- **MX** — domain can receive email
|
|
301
|
-
- **SPF** — authorized senders (`v=spf1`), flags dangerous `+all`
|
|
302
|
-
- **DKIM** — probes 15 common selectors (`google`, `selector1`, `default`, `dkim`, etc.)
|
|
303
|
-
- **DMARC** — policy enforcement (`v=DMARC1`), warns on `p=none`
|
|
304
|
-
- **BIMI** — brand indicator (optional, nice-to-have)
|
|
305
|
-
|
|
306
|
-
Also available as a session method: `session.checkDeliverability("example.com")`.
|
|
307
|
-
|
|
308
|
-
> **Note:** This is standalone async — not wired into the synchronous `auditEmail()` pipeline.
|
|
309
|
-
|
|
310
|
-
### `checkSpamAssassin(input, options?): Promise<SpamAssassinResult | null>`
|
|
311
|
-
|
|
312
|
-
Opt-in integration with a local SpamAssassin installation. Shells out to `spamc` (daemon) or `spamassassin` (standalone) via `execFile`. Returns `null` if SpamAssassin is not installed.
|
|
313
|
-
|
|
314
|
-
```typescript
|
|
315
|
-
import { checkSpamAssassin } from "@emailens/engine";
|
|
316
|
-
|
|
317
|
-
const result = await checkSpamAssassin(rawRfc2822Message);
|
|
318
|
-
if (result) {
|
|
319
|
-
console.log(result.score); // e.g. 3.2
|
|
320
|
-
console.log(result.isSpam); // true if score >= threshold
|
|
321
|
-
console.log(result.rules); // matched SpamAssassin rules
|
|
322
|
-
}
|
|
323
|
-
```
|
|
324
|
-
|
|
325
|
-
> **Note:** Requires a full RFC 2822 message (headers + body), not just HTML.
|
|
326
|
-
|
|
327
|
-
---
|
|
328
|
-
|
|
329
|
-
### `transformForClient(html, clientId, framework?): TransformResult`
|
|
330
|
-
|
|
331
|
-
Transforms HTML for a specific email client — strips unsupported CSS, inlines `<style>` blocks (for Gmail), removes unsupported elements.
|
|
332
|
-
|
|
333
|
-
### `transformForAllClients(html, framework?): TransformResult[]`
|
|
334
|
-
|
|
335
|
-
Transforms HTML for all 12 email clients at once.
|
|
336
|
-
|
|
337
|
-
### `simulateDarkMode(html, clientId): { html, warnings }`
|
|
338
|
-
|
|
339
|
-
Simulates how an email client applies dark mode using luminance-based color detection.
|
|
340
|
-
|
|
341
|
-
- **Full inversion** (Gmail Android, Samsung Mail): inverts all light backgrounds and dark text
|
|
342
|
-
- **Partial inversion** (Gmail Web, Apple Mail, Yahoo, Outlook.com, HEY, Superhuman): only inverts very light/dark colors
|
|
343
|
-
- **No dark mode** (Outlook Windows, Thunderbird)
|
|
344
|
-
|
|
345
|
-
### `getCodeFix(property, clientId, framework?): CodeFix | undefined`
|
|
346
|
-
|
|
347
|
-
Returns a paste-ready code fix for a CSS property + client combination. Fixes are tiered:
|
|
348
|
-
|
|
349
|
-
1. **Framework + client specific** (e.g., `border-radius` + Outlook + JSX → VML component)
|
|
350
|
-
2. **Framework specific** (e.g., `@font-face` + MJML → `<mj-font>`)
|
|
351
|
-
3. **Client specific** (e.g., `border-radius` + Outlook → VML roundrect)
|
|
352
|
-
4. **Generic HTML fallback**
|
|
353
|
-
|
|
354
|
-
### `diffResults(before, after): DiffResult[]`
|
|
355
|
-
|
|
356
|
-
Compares two sets of analysis results to show what improved, regressed, or stayed the same.
|
|
357
|
-
|
|
358
|
-
---
|
|
359
|
-
|
|
360
|
-
## Compile Module
|
|
361
|
-
|
|
362
|
-
Compile email templates from JSX, MJML, or Maizzle to HTML.
|
|
363
|
-
|
|
364
|
-
```typescript
|
|
365
|
-
import { compile, detectFormat, CompileError } from "@emailens/engine/compile";
|
|
366
|
-
|
|
367
|
-
// Auto-detect format and compile
|
|
368
|
-
const format = detectFormat("email.tsx"); // "jsx"
|
|
369
|
-
const html = await compile(source, format);
|
|
370
|
-
|
|
371
|
-
// Or use specific compilers
|
|
372
|
-
import { compileReactEmail, compileMjml, compileMaizzle } from "@emailens/engine/compile";
|
|
373
|
-
```
|
|
374
|
-
|
|
375
|
-
### `compile(source, format, filePath?): Promise<string>`
|
|
376
|
-
|
|
377
|
-
Compile source to HTML based on format. Lazily imports per-format compilers.
|
|
378
|
-
|
|
379
|
-
### `compileReactEmail(source, options?): Promise<string>`
|
|
380
|
-
|
|
381
|
-
Compile React Email JSX/TSX to HTML. Pipeline: validate → transpile (sucrase) → sandbox execute → render.
|
|
382
|
-
|
|
383
|
-
```typescript
|
|
384
|
-
import { compileReactEmail } from "@emailens/engine/compile";
|
|
385
|
-
|
|
386
|
-
const html = await compileReactEmail(jsxSource, {
|
|
387
|
-
sandbox: "isolated-vm", // "vm" | "isolated-vm" | "quickjs"
|
|
388
|
-
});
|
|
389
|
-
```
|
|
390
|
-
|
|
391
|
-
**Sandbox strategies:**
|
|
392
|
-
- `"isolated-vm"` (default) — Separate V8 isolate. True heap isolation. Requires `isolated-vm` native addon.
|
|
393
|
-
- `"vm"` — `node:vm` with hardened globals. Fast, zero-dependency, but NOT a true security boundary. Suitable for CLI/local use.
|
|
394
|
-
- `"quickjs"` — Validates code in WASM sandbox, then executes in `node:vm`. Security is equivalent to `"vm"`. No native addons needed.
|
|
395
|
-
|
|
396
|
-
**Peer dependencies:** `sucrase`, `react`, `@react-email/components`, `@react-email/render`. Plus `isolated-vm` or `quickjs-emscripten` depending on sandbox strategy.
|
|
397
|
-
|
|
398
|
-
### `compileMjml(source): Promise<string>`
|
|
399
|
-
|
|
400
|
-
Compile MJML to HTML. **Peer dependency:** `mjml`.
|
|
401
|
-
|
|
402
|
-
### `compileMaizzle(source): Promise<string>`
|
|
403
|
-
|
|
404
|
-
Compile Maizzle template to HTML. **Peer dependency:** `@maizzle/framework`.
|
|
405
|
-
|
|
406
|
-
**Security:** PostHTML file-system directives (`<extends>`, `<component>`, `<fetch>`, `<include>`, `<module>`, `<slot>`, `<fill>`, `<raw>`, `<block>`, `<yield>`) are rejected at validation time to prevent server-side file reads.
|
|
407
|
-
|
|
408
|
-
### `detectFormat(filePath): InputFormat`
|
|
409
|
-
|
|
410
|
-
Auto-detect input format from file extension (`.tsx`/`.jsx` → `"jsx"`, `.mjml` → `"mjml"`, `.html` → `"html"`).
|
|
411
|
-
|
|
412
|
-
### `CompileError`
|
|
413
|
-
|
|
414
|
-
Unified error class for all compilation failures. Available from both `@emailens/engine` and `@emailens/engine/compile`.
|
|
415
|
-
|
|
416
|
-
```typescript
|
|
417
|
-
import { CompileError } from "@emailens/engine";
|
|
418
|
-
|
|
419
|
-
try {
|
|
420
|
-
await compile(source, "jsx");
|
|
421
|
-
} catch (err) {
|
|
422
|
-
if (err instanceof CompileError) {
|
|
423
|
-
console.log(err.format); // "jsx" | "mjml" | "maizzle"
|
|
424
|
-
console.log(err.phase); // "validation" | "transpile" | "execution" | "render" | "compile"
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
```
|
|
428
|
-
|
|
429
|
-
---
|
|
430
|
-
|
|
431
|
-
## Performance
|
|
432
|
-
|
|
433
|
-
### Shared DOM parsing
|
|
434
|
-
|
|
435
|
-
The engine internally parses HTML using [Cheerio](https://cheerio.js.org/). For a typical 50–100KB email, each `cheerio.load()` call takes 5–15ms. Without optimization, calling multiple analysis functions on the same HTML would parse it repeatedly.
|
|
436
|
-
|
|
437
|
-
**`auditEmail()`** parses the HTML once and shares the DOM across all 8 analyzers (compatibility, spam, links, accessibility, images, inbox preview, size, template variables). Previously each analyzer parsed independently — this eliminates ~80% of parsing overhead in the audit path.
|
|
438
|
-
|
|
439
|
-
**`createSession()`** extends this optimization to any combination of calls. When you need to call `analyzeEmail()` + `analyzeSpam()` + `validateLinks()` + other checks on the same HTML, a session shares a single parse across all of them.
|
|
440
|
-
|
|
441
|
-
### Typical performance characteristics
|
|
442
|
-
|
|
443
|
-
| Operation | Complexity | Notes |
|
|
444
|
-
|---|---|---|
|
|
445
|
-
| `auditEmail()` | 1 parse + 8 analyses | Shared DOM, most efficient for full reports |
|
|
446
|
-
| `createSession()` | 1 parse upfront | Amortized across all subsequent analysis calls |
|
|
447
|
-
| `analyzeEmail()` | 1 parse + CSS property scan | Scans `<style>` blocks + inline styles × 12 clients |
|
|
448
|
-
| `transformForAllClients()` | 12 parses (1 per client) | Each client mutates its own DOM copy |
|
|
449
|
-
| `simulateDarkMode()` | 1 parse per call | Mutates DOM for color inversion |
|
|
450
|
-
|
|
451
|
-
### Optimization tips for consumers
|
|
452
|
-
|
|
453
|
-
```typescript
|
|
454
|
-
// Instead of this (6 separate HTML parses):
|
|
455
|
-
const warnings = analyzeEmail(html, "jsx");
|
|
456
|
-
const scores = generateCompatibilityScore(warnings);
|
|
457
|
-
const spam = analyzeSpam(html);
|
|
458
|
-
const links = validateLinks(html);
|
|
459
|
-
const a11y = checkAccessibility(html);
|
|
460
|
-
const images = analyzeImages(html);
|
|
461
|
-
|
|
462
|
-
// Do this (1 HTML parse):
|
|
463
|
-
const report = auditEmail(html, { framework: "jsx" });
|
|
464
|
-
|
|
465
|
-
// Or for selective analysis (1 HTML parse):
|
|
466
|
-
const session = createSession(html, { framework: "jsx" });
|
|
467
|
-
const warnings = session.analyze();
|
|
468
|
-
const scores = session.score(warnings);
|
|
469
|
-
const spam = session.analyzeSpam();
|
|
470
|
-
// ... pick only what you need
|
|
471
|
-
```
|
|
472
|
-
|
|
473
|
-
---
|
|
474
|
-
|
|
475
|
-
## Security Considerations
|
|
476
|
-
|
|
477
|
-
### Input Size Limits
|
|
478
|
-
|
|
479
|
-
All public functions enforce a 2MB (`MAX_HTML_SIZE`) input limit. Inputs exceeding this limit throw immediately. The limit is exported so consumers can check before calling:
|
|
480
|
-
|
|
481
|
-
```typescript
|
|
482
|
-
import { MAX_HTML_SIZE } from "@emailens/engine";
|
|
483
|
-
if (html.length > MAX_HTML_SIZE) {
|
|
484
|
-
// handle oversized input
|
|
485
|
-
}
|
|
486
|
-
```
|
|
487
|
-
|
|
488
|
-
### Compile Module Security
|
|
489
|
-
|
|
490
|
-
- **React Email JSX**: User code runs in a sandboxed environment. The `"isolated-vm"` strategy provides true heap isolation. The `"vm"` and `"quickjs"` strategies use `node:vm` which is NOT a security boundary — suitable for CLI use where users run their own code. For server deployments accepting untrusted input, use `"isolated-vm"`.
|
|
491
|
-
- **Maizzle**: PostHTML directives that access the filesystem (`<extends>`, `<fetch>`, `<include>`, `<raw>`, `<block>`, `<yield>`, etc.) are rejected at validation time.
|
|
492
|
-
- **MJML**: Compiled through the `mjml` package with default settings.
|
|
493
|
-
|
|
494
|
-
---
|
|
495
|
-
|
|
496
|
-
## AI-Powered Fixes
|
|
497
|
-
|
|
498
|
-
The engine classifies every warning as either `css` (CSS-only swap) or `structural` (requires HTML restructuring). For structural issues, the engine can generate a prompt and delegate to an LLM.
|
|
499
|
-
|
|
500
|
-
### `generateAiFix(options): Promise<AiFixResult>`
|
|
54
|
+
Score too low? Fix it automatically:
|
|
501
55
|
|
|
502
56
|
```typescript
|
|
503
57
|
import { generateAiFix, AI_FIX_SYSTEM_PROMPT } from "@emailens/engine";
|
|
504
58
|
|
|
505
|
-
const
|
|
59
|
+
const { code } = await generateAiFix({
|
|
506
60
|
originalHtml: html,
|
|
507
|
-
warnings,
|
|
508
|
-
scores,
|
|
509
|
-
scope: "
|
|
61
|
+
warnings: report.compatibility.warnings,
|
|
62
|
+
scores: report.compatibility.scores,
|
|
63
|
+
scope: "outlook-windows",
|
|
510
64
|
format: "jsx",
|
|
511
65
|
provider: async (prompt) => {
|
|
66
|
+
// Any LLM — Claude, GPT, etc.
|
|
512
67
|
const msg = await anthropic.messages.create({
|
|
513
68
|
model: "claude-sonnet-4-6",
|
|
514
69
|
max_tokens: 8192,
|
|
@@ -518,17 +73,51 @@ const result = await generateAiFix({
|
|
|
518
73
|
return msg.content[0].type === "text" ? msg.content[0].text : "";
|
|
519
74
|
},
|
|
520
75
|
});
|
|
76
|
+
// code → JSX with <Table> layout, VML roundrects, inline fallbacks
|
|
521
77
|
```
|
|
522
78
|
|
|
523
|
-
|
|
79
|
+
## What It Catches
|
|
80
|
+
|
|
81
|
+
8 analysis engines, one `auditEmail()` call.
|
|
524
82
|
|
|
525
|
-
|
|
83
|
+
- **CSS compatibility** — 250+ properties tested across 12 email clients, with fix snippets and AI-powered auto-fix
|
|
84
|
+
- **Spam scoring** — 45+ signals modeled after SpamAssassin, CAN-SPAM, and GDPR
|
|
85
|
+
- **Accessibility** — WCAG contrast ratios, alt text, semantic structure, heading hierarchy
|
|
86
|
+
- **Link validation** — broken hrefs, insecure HTTP, `javascript:` protocols, deceptive URLs
|
|
87
|
+
- **Image analysis** — missing dimensions, oversized data URIs, tracking pixels, WebP/SVG format
|
|
88
|
+
- **Inbox preview** — subject/preheader truncation per client, Gmail clipping detection
|
|
89
|
+
- **Domain authentication** — SPF, DKIM, DMARC, MX, and BIMI DNS record validation
|
|
90
|
+
- **Template variables** — unresolved merge tags across 6 template systems (Handlebars, ERB, Mailchimp, etc.)
|
|
526
91
|
|
|
527
|
-
|
|
92
|
+
## Installation
|
|
528
93
|
|
|
529
|
-
|
|
94
|
+
```bash
|
|
95
|
+
npm install @emailens/engine
|
|
96
|
+
```
|
|
530
97
|
|
|
531
|
-
|
|
98
|
+
Three entry points:
|
|
99
|
+
|
|
100
|
+
| Import | Description |
|
|
101
|
+
|---|---|
|
|
102
|
+
| `@emailens/engine` | Core analysis — CSS, spam, a11y, links, images, inbox preview, size, templates, AI fix |
|
|
103
|
+
| `@emailens/engine/compile` | JSX / MJML / Maizzle → HTML compilers |
|
|
104
|
+
| `@emailens/engine/server` | Node-only: DNS deliverability checks, SpamAssassin integration |
|
|
105
|
+
|
|
106
|
+
## Why Emailens?
|
|
107
|
+
|
|
108
|
+
- **Offline-first** — runs entirely locally, no network calls required (except DNS deliverability checks)
|
|
109
|
+
- **Unified audit** — one function call returns CSS compatibility, spam, accessibility, links, images, inbox preview, size, and template checks
|
|
110
|
+
- **Framework-aware** — fix snippets tailored to React Email (JSX), MJML, and Maizzle
|
|
111
|
+
- **AI-ready** — structural issues get LLM-powered auto-fix with any provider (Claude, GPT, etc.)
|
|
112
|
+
- **Programmable** — TypeScript API, not a GUI — integrate into CI, editors, or build pipelines
|
|
113
|
+
|
|
114
|
+
| | @emailens/engine | Litmus | Email on Acid | caniemail.com |
|
|
115
|
+
|---|---|---|---|---|
|
|
116
|
+
| Local/offline | Yes | No | No | Data only |
|
|
117
|
+
| Programmatic API | Yes | Limited | No | No |
|
|
118
|
+
| CSS + Spam + A11y | Yes | Separate tools | Separate tools | CSS only |
|
|
119
|
+
| AI auto-fix | Yes | No | No | No |
|
|
120
|
+
| Open source | MIT | No | No | Yes (data) |
|
|
532
121
|
|
|
533
122
|
## Supported Email Clients
|
|
534
123
|
|
|
@@ -547,135 +136,38 @@ Instant synchronous token estimate (~3.5 chars/token).
|
|
|
547
136
|
| HEY Mail | `hey-mail` | Webmail | WebKit | Yes |
|
|
548
137
|
| Superhuman | `superhuman` | Desktop | Blink | Yes |
|
|
549
138
|
|
|
550
|
-
##
|
|
139
|
+
## API Documentation
|
|
551
140
|
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
line?: number; // line number in <style> block
|
|
567
|
-
selector?: string; // element selector for inline styles
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
interface AuditReport {
|
|
571
|
-
compatibility: {
|
|
572
|
-
warnings: CSSWarning[];
|
|
573
|
-
scores: Record<string, { score: number; errors: number; warnings: number; info: number }>;
|
|
574
|
-
};
|
|
575
|
-
spam: SpamReport;
|
|
576
|
-
links: LinkReport;
|
|
577
|
-
accessibility: AccessibilityReport;
|
|
578
|
-
images: ImageReport;
|
|
579
|
-
inboxPreview: InboxPreview;
|
|
580
|
-
size: SizeReport;
|
|
581
|
-
templateVariables: TemplateReport;
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
interface EmailSession {
|
|
585
|
-
readonly html: string;
|
|
586
|
-
readonly framework: Framework | undefined;
|
|
587
|
-
audit(options?): AuditReport;
|
|
588
|
-
analyze(): CSSWarning[];
|
|
589
|
-
score(warnings): Record<string, ClientScore>;
|
|
590
|
-
analyzeSpam(options?): SpamReport;
|
|
591
|
-
validateLinks(): LinkReport;
|
|
592
|
-
checkAccessibility(): AccessibilityReport;
|
|
593
|
-
analyzeImages(): ImageReport;
|
|
594
|
-
extractInboxPreview(): InboxPreview;
|
|
595
|
-
checkSize(): SizeReport;
|
|
596
|
-
checkTemplateVariables(): TemplateReport;
|
|
597
|
-
transformForClient(clientId): TransformResult;
|
|
598
|
-
transformForAllClients(): TransformResult[];
|
|
599
|
-
simulateDarkMode(clientId): { html; warnings };
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
interface InboxPreview {
|
|
603
|
-
subject: string | null;
|
|
604
|
-
preheader: string | null;
|
|
605
|
-
subjectLength: number;
|
|
606
|
-
preheaderLength: number;
|
|
607
|
-
truncation: ClientTruncation[];
|
|
608
|
-
issues: InboxPreviewIssue[];
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
interface SizeReport {
|
|
612
|
-
htmlBytes: number;
|
|
613
|
-
humanSize: string;
|
|
614
|
-
clipped: boolean;
|
|
615
|
-
issues: SizeIssue[];
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
interface TemplateReport {
|
|
619
|
-
unresolvedCount: number;
|
|
620
|
-
issues: TemplateIssue[];
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
interface SpamReport {
|
|
624
|
-
score: number; // 0–100 (100 = clean)
|
|
625
|
-
level: "low" | "medium" | "high";
|
|
626
|
-
issues: SpamIssue[];
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
interface LinkReport {
|
|
630
|
-
totalLinks: number;
|
|
631
|
-
issues: LinkIssue[];
|
|
632
|
-
breakdown: { https: number; http: number; mailto: number; tel: number; ... };
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
interface AccessibilityReport {
|
|
636
|
-
score: number; // 0–100
|
|
637
|
-
issues: AccessibilityIssue[];
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
interface ImageReport {
|
|
641
|
-
total: number;
|
|
642
|
-
totalDataUriBytes: number;
|
|
643
|
-
issues: ImageIssue[];
|
|
644
|
-
images: ImageInfo[];
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
interface DeliverabilityReport {
|
|
648
|
-
domain: string;
|
|
649
|
-
checks: DeliverabilityCheck[];
|
|
650
|
-
score: number; // 0-100
|
|
651
|
-
issues: DeliverabilityIssue[];
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
interface DeliverabilityCheck {
|
|
655
|
-
name: "spf" | "dkim" | "dmarc" | "mx" | "bimi";
|
|
656
|
-
status: "pass" | "fail" | "warn" | "skip";
|
|
657
|
-
message: string;
|
|
658
|
-
detail?: string;
|
|
659
|
-
record?: string;
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
interface SpamAssassinResult {
|
|
663
|
-
score: number;
|
|
664
|
-
threshold: number;
|
|
665
|
-
isSpam: boolean;
|
|
666
|
-
rules: Array<{ name: string; score: number; description: string }>;
|
|
667
|
-
rawOutput: string;
|
|
668
|
-
}
|
|
669
|
-
```
|
|
141
|
+
Full API reference: **[docs/API.md](./docs/API.md)**
|
|
142
|
+
|
|
143
|
+
Covers:
|
|
144
|
+
- `auditEmail` and `createSession` — core analysis
|
|
145
|
+
- Standalone analyzers (CSS, spam, links, accessibility, images, inbox preview, size, templates)
|
|
146
|
+
- DNS deliverability and SpamAssassin integration
|
|
147
|
+
- Client transforms and dark mode simulation
|
|
148
|
+
- Compile module (JSX, MJML, Maizzle)
|
|
149
|
+
- AI-powered fixes and token estimation
|
|
150
|
+
- Performance optimization guide
|
|
151
|
+
- Security considerations
|
|
152
|
+
- Full TypeScript type definitions
|
|
153
|
+
|
|
154
|
+
## Roadmap
|
|
670
155
|
|
|
671
|
-
|
|
156
|
+
- [ ] Outlook VML auto-generation
|
|
157
|
+
- [ ] GitHub Actions integration (score thresholds in CI)
|
|
158
|
+
- [ ] Automated caniemail.com data sync
|
|
159
|
+
- [ ] Real-time rendering previews
|
|
160
|
+
- [ ] MJML/Maizzle source-level linting
|
|
161
|
+
- [ ] Plugin system for custom analyzers
|
|
162
|
+
|
|
163
|
+
## Contributing
|
|
164
|
+
|
|
165
|
+
Contributions are welcome! See **[CONTRIBUTING.md](./CONTRIBUTING.md)** for architecture overview, setup instructions, and PR guidelines.
|
|
672
166
|
|
|
673
167
|
```bash
|
|
674
|
-
bun test
|
|
168
|
+
bun install && bun test # 574 tests
|
|
675
169
|
```
|
|
676
170
|
|
|
677
|
-
574 tests covering analysis (250+ CSS properties), transformation, dark mode simulation, framework-aware fixes, AI fix generation, token estimation, content hygiene scoring, link validation, accessibility checking, image analysis, inbox preview extraction, size checking, template variable detection, DNS deliverability checking, session API, security hardening, integration pipelines, accuracy benchmarks, and battle tests.
|
|
678
|
-
|
|
679
171
|
## License
|
|
680
172
|
|
|
681
|
-
MIT
|
|
173
|
+
MIT — Copyright 2025 [Emailens](https://emailens.dev)
|