@emailens/engine 0.3.1 → 0.5.1

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # @emailens/engine
2
2
 
3
- Email compatibility engine that transforms CSS per email client, analyzes compatibility, scores results, simulates dark mode, and provides framework-aware fix snippets.
3
+ Email compatibility engine that transforms CSS per email client, analyzes compatibility, scores results, simulates dark mode, provides framework-aware fix snippets, and runs spam, accessibility, link, and image quality analysis.
4
4
 
5
5
  Supports **12 email clients**: Gmail (Web, Android, iOS), Outlook (365, Windows), Apple Mail (macOS, iOS), Yahoo Mail, Samsung Mail, Thunderbird, HEY Mail, and Superhuman.
6
6
 
@@ -12,240 +12,299 @@ npm install @emailens/engine
12
12
  bun add @emailens/engine
13
13
  ```
14
14
 
15
+ Requires Node.js >= 18.
16
+
15
17
  ## Quick Start
16
18
 
17
19
  ```typescript
18
- import {
19
- analyzeEmail,
20
- generateCompatibilityScore,
21
- transformForAllClients,
22
- simulateDarkMode,
23
- } from "@emailens/engine";
24
-
25
- const html = `
26
- <html>
27
- <head>
28
- <style>
29
- .card { border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
30
- </style>
20
+ import { auditEmail } from "@emailens/engine";
21
+
22
+ const html = `<html lang="en">
23
+ <head><title>Newsletter</title>
24
+ <style>.card { border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }</style>
31
25
  </head>
32
26
  <body>
33
27
  <div class="card" style="display: flex; gap: 16px;">
34
28
  <div>Column A</div>
35
29
  <div>Column B</div>
36
30
  </div>
31
+ <a href="https://example.com/unsubscribe">Unsubscribe</a>
37
32
  </body>
38
- </html>
39
- `;
40
-
41
- // 1. Analyze CSS compatibility across all clients
42
- const warnings = analyzeEmail(html);
43
- console.log(`Found ${warnings.length} warnings`);
44
-
45
- // 2. Generate per-client scores (0–100)
46
- const scores = generateCompatibilityScore(warnings);
47
- console.log(scores["gmail-web"]); // { score: 75, errors: 0, warnings: 5, info: 0 }
48
- console.log(scores["outlook-windows"]); // { score: 40, errors: 1, warnings: 8, info: 2 }
49
-
50
- // 3. Transform HTML per client (strips unsupported CSS, inlines styles)
51
- const transforms = transformForAllClients(html);
52
- for (const t of transforms) {
53
- console.log(`${t.clientId}: ${t.warnings.length} warnings`);
54
- }
33
+ </html>`;
34
+
35
+ // Run all checks in one call
36
+ const report = auditEmail(html, { framework: "jsx" });
37
+
38
+ console.log(report.compatibility.scores["gmail-web"]);
39
+ // { score: 75, errors: 0, warnings: 5, info: 0 }
40
+
41
+ console.log(report.spam);
42
+ // { score: 100, level: "low", issues: [] }
43
+
44
+ console.log(report.accessibility.score);
45
+ // 88
46
+
47
+ console.log(report.links.totalLinks);
48
+ // 1
55
49
 
56
- // 4. Simulate dark mode for a specific client
57
- const darkMode = simulateDarkMode(html, "gmail-android");
58
- console.log(darkMode.warnings); // Warns about transparent PNGs, missing prefers-color-scheme, etc.
50
+ console.log(report.images.total);
51
+ // 0
59
52
  ```
60
53
 
61
54
  ## API Reference
62
55
 
63
- ### `analyzeEmail(html: string, framework?: Framework): CSSWarning[]`
64
-
65
- Analyzes an HTML email and returns CSS compatibility warnings for all 12 email clients. Detects usage of `<style>`, `<link>`, `<svg>`, `<video>`, `<form>`, inline CSS properties, `@font-face`, `@media` queries, gradients, flexbox/grid, and more.
56
+ ### `auditEmail(html: string, options?: AuditOptions): AuditReport`
66
57
 
67
- The optional `framework` parameter (`"jsx"` | `"mjml"` | `"maizzle"`) controls which fix snippets are attached to warnings. Analysis always runs on compiled HTML fix snippets reference source-level constructs so you know how to modify your framework source code.
58
+ **Unified API** runs all email analysis checks in a single call. Returns compatibility warnings + scores, spam analysis, link validation, accessibility audit, and image analysis.
68
59
 
69
60
  ```typescript
70
- // Plain HTML analysis
71
- const warnings = analyzeEmail(html);
61
+ import { auditEmail } from "@emailens/engine";
72
62
 
73
- // Framework-aware: fixes reference React Email components
74
- const warnings = analyzeEmail(html, "jsx");
75
-
76
- // Framework-aware: fixes reference MJML elements
77
- const warnings = analyzeEmail(html, "mjml");
63
+ const report = auditEmail(html, {
64
+ framework: "jsx", // attach framework-specific fix snippets
65
+ spam: { emailType: "transactional" }, // skip unsubscribe check
66
+ skip: ["images"], // skip specific checks
67
+ });
78
68
 
79
- // Framework-aware: fixes reference Maizzle/Tailwind classes
80
- const warnings = analyzeEmail(html, "maizzle");
69
+ // report.compatibility.warnings — CSSWarning[]
70
+ // report.compatibility.scores — Record<string, ClientScore>
71
+ // report.spam — SpamReport
72
+ // report.links — LinkReport
73
+ // report.accessibility — AccessibilityReport
74
+ // report.images — ImageReport
81
75
  ```
82
76
 
83
- ### `generateCompatibilityScore(warnings: CSSWarning[]): Record<string, ClientScore>`
77
+ **`AuditOptions`:**
78
+ - `framework?: "jsx" | "mjml" | "maizzle"` — attach framework-specific fix snippets
79
+ - `spam?: SpamAnalysisOptions` — options for spam analysis
80
+ - `skip?: Array<"spam" | "links" | "accessibility" | "images" | "compatibility">` — skip specific checks
84
81
 
85
- Generates a 0–100 compatibility score per email client from a set of warnings.
82
+ ---
86
83
 
87
- **Scoring formula:** `score = 100 - (errors × 15) - (warnings × 5) - (info × 1)`, clamped to 0–100.
84
+ ### `analyzeEmail(html: string, framework?: Framework): CSSWarning[]`
85
+
86
+ 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.
87
+
88
+ The optional `framework` parameter controls which fix snippets are attached to warnings. Analysis always runs on compiled HTML.
88
89
 
89
90
  ```typescript
90
- const scores = generateCompatibilityScore(warnings);
91
- // {
92
- // "gmail-web": { score: 75, errors: 0, warnings: 5, info: 0 },
93
- // "outlook-windows": { score: 40, errors: 1, warnings: 8, info: 2 },
94
- // "apple-mail-macos": { score: 100, errors: 0, warnings: 0, info: 0 },
95
- // ...
96
- // }
91
+ const warnings = analyzeEmail(html); // Plain HTML
92
+ const warnings = analyzeEmail(html, "jsx"); // React Email fixes
93
+ const warnings = analyzeEmail(html, "mjml"); // MJML fixes
97
94
  ```
98
95
 
99
- ### `transformForClient(html: string, clientId: string, framework?: Framework): TransformResult`
96
+ ### `generateCompatibilityScore(warnings): Record<string, ClientScore>`
97
+
98
+ Generates a 0–100 compatibility score per email client. Formula: `100 - (errors × 15) - (warnings × 5) - (info × 1)`.
100
99
 
101
- Transforms HTML for a specific email client. Strips unsupported CSS, inlines `<style>` blocks (for Gmail), removes unsupported elements, and generates per-client warnings.
100
+ ### `warningsForClient(warnings, clientId): CSSWarning[]`
101
+
102
+ Filter warnings for a specific client.
103
+
104
+ ### `errorWarnings(warnings): CSSWarning[]`
105
+
106
+ Get only error-severity warnings.
107
+
108
+ ### `structuralWarnings(warnings): CSSWarning[]`
109
+
110
+ Get only warnings that require HTML restructuring (`fixType: "structural"`).
111
+
112
+ ---
113
+
114
+ ### `analyzeSpam(html: string, options?: SpamAnalysisOptions): SpamReport`
115
+
116
+ Analyzes an HTML email for spam indicators. Returns a 0–100 score (100 = clean) and an array of issues. Uses heuristic rules modeled after SpamAssassin, CAN-SPAM, and GDPR.
102
117
 
103
118
  ```typescript
104
- const result = transformForClient(html, "gmail-web");
105
- console.log(result.html); // Transformed HTML
106
- console.log(result.warnings); // Client-specific warnings
107
- console.log(result.clientId); // "gmail-web"
119
+ import { analyzeSpam } from "@emailens/engine";
120
+
121
+ const report = analyzeSpam(html, {
122
+ emailType: "transactional", // skip unsubscribe check
123
+ listUnsubscribeHeader: "...", // satisfies unsubscribe requirement
124
+ });
125
+ // { score: 95, level: "low", issues: [...] }
108
126
  ```
109
127
 
110
- ### `transformForAllClients(html: string, framework?: Framework): TransformResult[]`
128
+ **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.
111
129
 
112
- Transforms HTML for all 12 email clients at once.
130
+ ### `validateLinks(html: string): LinkReport`
131
+
132
+ Static analysis of all links in an HTML email. No network requests.
113
133
 
114
134
  ```typescript
115
- const results = transformForAllClients(html);
116
- for (const r of results) {
117
- console.log(`${r.clientId}: ${r.warnings.length} issues`);
118
- }
135
+ import { validateLinks } from "@emailens/engine";
136
+
137
+ const report = validateLinks(html);
138
+ // { totalLinks: 12, issues: [...], breakdown: { https: 10, http: 1, mailto: 1, ... } }
119
139
  ```
120
140
 
121
- ### `simulateDarkMode(html: string, clientId: string): { html: string; warnings: CSSWarning[] }`
141
+ **Checks:** empty/placeholder hrefs, `javascript:` protocol, insecure HTTP, generic link text, missing accessible names, empty mailto/tel, very long URLs, duplicate links.
122
142
 
123
- Simulates how an email client applies dark mode. Different clients use different strategies:
143
+ ### `checkAccessibility(html: string): AccessibilityReport`
124
144
 
125
- - **Full inversion** (Gmail Android, Samsung Mail): swaps all light backgrounds to dark
126
- - **Partial inversion** (Gmail Web, Apple Mail, Yahoo, Outlook.com, HEY, Superhuman): only inverts white backgrounds
127
- - **No dark mode** (Outlook Windows, Thunderbird): no transformation
145
+ Audits an HTML email for accessibility issues. Returns a 0–100 score and detailed issues.
128
146
 
129
147
  ```typescript
130
- const { html: darkHtml, warnings } = simulateDarkMode(html, "gmail-android");
148
+ import { checkAccessibility } from "@emailens/engine";
149
+
150
+ const report = checkAccessibility(html);
151
+ // { score: 88, issues: [...] }
131
152
  ```
132
153
 
133
- ### `getCodeFix(property: string, clientId: string, framework?: Framework): CodeFix | undefined`
154
+ **Checks:** missing `lang` attribute, missing `<title>`, image alt text, link accessibility, layout table roles, small text, color contrast (WCAG 2.1), heading hierarchy.
134
155
 
135
- Returns a paste-ready code fix for a specific CSS property + client combination. Fixes are tiered:
156
+ ### `analyzeImages(html: string): ImageReport`
136
157
 
137
- 1. **Framework + client specific** (e.g., `border-radius` + Outlook + JSX → VML roundrect component)
138
- 2. **Framework specific** (e.g., `@font-face` + MJML → `<mj-font>`)
139
- 3. **Generic HTML fallback** (e.g., `display:flex` + Outlook → HTML table with MSO conditionals)
158
+ Analyzes images for email best practices.
140
159
 
141
160
  ```typescript
142
- const fix = getCodeFix("display:flex", "outlook-windows", "jsx");
143
- // {
144
- // language: "jsx",
145
- // description: "Use Row and Column from @react-email/components",
146
- // before: "<div style={{ display: 'flex' }}>...",
147
- // after: "<Row><Column>..."
148
- // }
161
+ import { analyzeImages } from "@emailens/engine";
162
+
163
+ const report = analyzeImages(html);
164
+ // { total: 5, totalDataUriBytes: 0, issues: [...], images: [...] }
149
165
  ```
150
166
 
151
- ### `getSuggestion(property: string, clientId: string, framework?: Framework): { text: string; isGenericFallback?: boolean }`
167
+ **Checks:** missing dimensions, oversized data URIs, missing alt, WebP/SVG format, missing `display:block`, tracking pixels, high image count.
168
+
169
+ ---
170
+
171
+ ### `transformForClient(html, clientId, framework?): TransformResult`
172
+
173
+ Transforms HTML for a specific email client — strips unsupported CSS, inlines `<style>` blocks (for Gmail), removes unsupported elements.
174
+
175
+ ### `transformForAllClients(html, framework?): TransformResult[]`
176
+
177
+ Transforms HTML for all 12 email clients at once.
178
+
179
+ ### `simulateDarkMode(html, clientId): { html, warnings }`
152
180
 
153
- Returns a human-readable suggestion for fixing a compatibility issue. Lighter than `getCodeFix` — returns a text description rather than before/after code.
181
+ Simulates how an email client applies dark mode using luminance-based color detection.
182
+
183
+ - **Full inversion** (Gmail Android, Samsung Mail): inverts all light backgrounds and dark text
184
+ - **Partial inversion** (Gmail Web, Apple Mail, Yahoo, Outlook.com, HEY, Superhuman): only inverts very light/dark colors
185
+ - **No dark mode** (Outlook Windows, Thunderbird)
186
+
187
+ ### `getCodeFix(property, clientId, framework?): CodeFix | undefined`
188
+
189
+ Returns a paste-ready code fix for a CSS property + client combination. Fixes are tiered:
190
+
191
+ 1. **Framework + client specific** (e.g., `border-radius` + Outlook + JSX → VML component)
192
+ 2. **Framework specific** (e.g., `@font-face` + MJML → `<mj-font>`)
193
+ 3. **Client specific** (e.g., `border-radius` + Outlook → VML roundrect)
194
+ 4. **Generic HTML fallback**
154
195
 
155
196
  ### `diffResults(before, after): DiffResult[]`
156
197
 
157
- Compares two sets of analysis results (before and after a fix). Shows what improved, regressed, or stayed the same per client.
198
+ Compares two sets of analysis results to show what improved, regressed, or stayed the same.
199
+
200
+ ---
201
+
202
+ ## Compile Module
203
+
204
+ Compile email templates from JSX, MJML, or Maizzle to HTML.
158
205
 
159
206
  ```typescript
160
- const before = { scores: generateCompatibilityScore(warningsBefore), warnings: warningsBefore };
161
- const after = { scores: generateCompatibilityScore(warningsAfter), warnings: warningsAfter };
162
- const diffs = diffResults(before, after);
207
+ import { compile, detectFormat, CompileError } from "@emailens/engine/compile";
163
208
 
164
- for (const d of diffs) {
165
- console.log(`${d.clientId}: ${d.scoreBefore} ${d.scoreAfter} (${d.scoreDelta > 0 ? "+" : ""}${d.scoreDelta})`);
166
- console.log(` Fixed: ${d.fixed.length}, Introduced: ${d.introduced.length}`);
167
- }
209
+ // Auto-detect format and compile
210
+ const format = detectFormat("email.tsx"); // "jsx"
211
+ const html = await compile(source, format);
212
+
213
+ // Or use specific compilers
214
+ import { compileReactEmail, compileMjml, compileMaizzle } from "@emailens/engine/compile";
168
215
  ```
169
216
 
170
- ### `generateFixPrompt(options: ExportPromptOptions): string`
217
+ ### `compile(source, format, filePath?): Promise<string>`
218
+
219
+ Compile source to HTML based on format. Lazily imports per-format compilers.
220
+
221
+ ### `compileReactEmail(source, options?): Promise<string>`
171
222
 
172
- Generates a markdown prompt suitable for passing to an AI assistant to fix compatibility issues. Includes the original HTML, compatibility scores table, all detected issues with fix suggestions, and format-specific instructions.
223
+ Compile React Email JSX/TSX to HTML. Pipeline: validate transpile (sucrase) sandbox execute render.
173
224
 
174
225
  ```typescript
175
- const prompt = generateFixPrompt({
176
- originalHtml: html,
177
- warnings,
178
- scores,
179
- scope: "all", // or "current" with selectedClientId
180
- format: "jsx", // "html" | "jsx" | "mjml" | "maizzle"
226
+ import { compileReactEmail } from "@emailens/engine/compile";
227
+
228
+ const html = await compileReactEmail(jsxSource, {
229
+ sandbox: "isolated-vm", // "vm" | "isolated-vm" | "quickjs"
181
230
  });
182
231
  ```
183
232
 
184
- ### `EMAIL_CLIENTS: EmailClient[]`
233
+ **Sandbox strategies:**
234
+ - `"isolated-vm"` (default) — Separate V8 isolate. True heap isolation. Requires `isolated-vm` native addon.
235
+ - `"vm"` — `node:vm` with hardened globals. Fast, zero-dependency, but NOT a true security boundary. Suitable for CLI/local use.
236
+ - `"quickjs"` — Validates code in WASM sandbox, then executes in `node:vm`. Security is equivalent to `"vm"`. No native addons needed.
185
237
 
186
- Array of all 12 supported email client definitions.
238
+ **Peer dependencies:** `sucrase`, `react`, `@react-email/components`, `@react-email/render`. Plus `isolated-vm` or `quickjs-emscripten` depending on sandbox strategy.
187
239
 
188
- ```typescript
189
- import { EMAIL_CLIENTS } from "@emailens/engine";
240
+ ### `compileMjml(source): Promise<string>`
241
+
242
+ Compile MJML to HTML. **Peer dependency:** `mjml`.
243
+
244
+ ### `compileMaizzle(source): Promise<string>`
245
+
246
+ Compile Maizzle template to HTML. **Peer dependency:** `@maizzle/framework`.
247
+
248
+ **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.
249
+
250
+ ### `detectFormat(filePath): InputFormat`
251
+
252
+ Auto-detect input format from file extension (`.tsx`/`.jsx` → `"jsx"`, `.mjml` → `"mjml"`, `.html` → `"html"`).
253
+
254
+ ### `CompileError`
190
255
 
191
- for (const client of EMAIL_CLIENTS) {
192
- console.log(`${client.name} (${client.category}) — ${client.engine}`);
256
+ Unified error class for all compilation failures. Available from both `@emailens/engine` and `@emailens/engine/compile`.
257
+
258
+ ```typescript
259
+ import { CompileError } from "@emailens/engine";
260
+
261
+ try {
262
+ await compile(source, "jsx");
263
+ } catch (err) {
264
+ if (err instanceof CompileError) {
265
+ console.log(err.format); // "jsx" | "mjml" | "maizzle"
266
+ console.log(err.phase); // "validation" | "transpile" | "execution" | "render" | "compile"
267
+ }
193
268
  }
194
- // Gmail (webmail) — Gmail Web
195
- // Outlook Windows (desktop) — Microsoft Word
196
- // Apple Mail (desktop) — WebKit
197
- // ...
198
269
  ```
199
270
 
200
- ### `getClient(id: string): EmailClient | undefined`
271
+ ---
201
272
 
202
- Look up a client by ID.
273
+ ## Security Considerations
203
274
 
204
- ## Supported Email Clients
275
+ ### Input Size Limits
205
276
 
206
- | Client | ID | Category | Engine | Dark Mode |
207
- |---|---|---|---|---|
208
- | Gmail | `gmail-web` | Webmail | Gmail Web | Yes |
209
- | Gmail Android | `gmail-android` | Mobile | Gmail Mobile | Yes |
210
- | Gmail iOS | `gmail-ios` | Mobile | Gmail Mobile | Yes |
211
- | Outlook 365 | `outlook-web` | Webmail | Outlook Web | Yes |
212
- | Outlook Windows | `outlook-windows` | Desktop | Microsoft Word | No |
213
- | Apple Mail | `apple-mail-macos` | Desktop | WebKit | Yes |
214
- | Apple Mail iOS | `apple-mail-ios` | Mobile | WebKit | Yes |
215
- | Yahoo Mail | `yahoo-mail` | Webmail | Yahoo | Yes |
216
- | Samsung Mail | `samsung-mail` | Mobile | Samsung | Yes |
217
- | Thunderbird | `thunderbird` | Desktop | Gecko | No |
218
- | HEY Mail | `hey-mail` | Webmail | WebKit | Yes |
219
- | Superhuman | `superhuman` | Desktop | Blink | Yes |
277
+ 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:
278
+
279
+ ```typescript
280
+ import { MAX_HTML_SIZE } from "@emailens/engine";
281
+ if (html.length > MAX_HTML_SIZE) {
282
+ // handle oversized input
283
+ }
284
+ ```
220
285
 
221
- ## AI-Powered Fixes (v0.2.0)
286
+ ### Compile Module Security
222
287
 
223
- The engine classifies every warning as either `css` (CSS-only swap) or `structural` (requires HTML restructuring tables, VML, conditionals). For structural issues that static snippets can't solve, the engine can generate a structured prompt and delegate to an LLM.
288
+ - **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"`.
289
+ - **Maizzle**: PostHTML directives that access the filesystem (`<extends>`, `<fetch>`, `<include>`, `<raw>`, `<block>`, `<yield>`, etc.) are rejected at validation time.
290
+ - **MJML**: Compiled through the `mjml` package with default settings.
224
291
 
225
- The engine is **provider-agnostic** — you bring your own AI provider via a simple callback.
292
+ ---
226
293
 
227
- ### `generateAiFix(options): Promise<AiFixResult>`
294
+ ## AI-Powered Fixes
228
295
 
229
- Builds a fix prompt from the engine's analysis, sends it to your AI provider, and extracts the fixed code.
296
+ 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.
297
+
298
+ ### `generateAiFix(options): Promise<AiFixResult>`
230
299
 
231
300
  ```typescript
232
- import Anthropic from "@anthropic-ai/sdk";
233
- import {
234
- analyzeEmail,
235
- generateCompatibilityScore,
236
- generateAiFix,
237
- AI_FIX_SYSTEM_PROMPT,
238
- } from "@emailens/engine";
239
-
240
- const anthropic = new Anthropic();
241
- const warnings = analyzeEmail(html, "jsx");
242
- const scores = generateCompatibilityScore(warnings);
301
+ import { generateAiFix, AI_FIX_SYSTEM_PROMPT } from "@emailens/engine";
243
302
 
244
303
  const result = await generateAiFix({
245
304
  originalHtml: html,
246
305
  warnings,
247
306
  scores,
248
- scope: "all", // or "current" with selectedClientId
307
+ scope: "all",
249
308
  format: "jsx",
250
309
  provider: async (prompt) => {
251
310
  const msg = await anthropic.messages.create({
@@ -257,68 +316,34 @@ const result = await generateAiFix({
257
316
  return msg.content[0].type === "text" ? msg.content[0].text : "";
258
317
  },
259
318
  });
260
-
261
- console.log(result.code); // Fixed email code
262
- console.log(result.targetedWarnings); // 23
263
- console.log(result.structuralCount); // 5
264
319
  ```
265
320
 
266
321
  ### `estimateAiFixTokens(options): Promise<TokenEstimate>`
267
322
 
268
- Estimate tokens **before** making an API call. Use for cost estimates, limit checks, and UI feedback.
269
-
270
- ```typescript
271
- import { estimateAiFixTokens } from "@emailens/engine";
272
-
273
- const estimate = await estimateAiFixTokens({
274
- originalHtml: html,
275
- warnings,
276
- scores,
277
- scope: "all",
278
- format: "jsx",
279
- maxInputTokens: 16000, // optional, triggers smart truncation
280
- });
281
-
282
- console.log(`~${estimate.inputTokens} input tokens`);
283
- console.log(`~${estimate.estimatedOutputTokens} output tokens`);
284
- console.log(`${estimate.warningCount} warnings (${estimate.structuralCount} structural)`);
285
- console.log(`Truncated: ${estimate.truncated}`);
286
- ```
287
-
288
- **Smart truncation** kicks in when the prompt exceeds `maxInputTokens`:
289
- 1. Deduplicates warnings (same property × severity)
290
- 2. Removes `info`-level warnings
291
- 3. Removes CSS-only warnings (keeps structural + errors)
292
- 4. Trims long fix snippets
323
+ Estimate tokens before making an API call.
293
324
 
294
325
  ### `heuristicTokenCount(text): number`
295
326
 
296
- Instant synchronous token estimate (~3.5 chars/token). Within ~10-15% of real Claude tokenizer for HTML/CSS.
297
-
298
- ```typescript
299
- import { heuristicTokenCount } from "@emailens/engine";
300
- const tokens = heuristicTokenCount(html); // instant, no deps
301
- ```
302
-
303
- ### `AI_FIX_SYSTEM_PROMPT`
304
-
305
- Expert system prompt for email compatibility fixes. Pass as the `system` parameter to your LLM call for best results. Includes structural fix patterns (table layouts, VML, MSO conditionals).
306
-
307
- ### `STRUCTURAL_FIX_PROPERTIES`
308
-
309
- `Set<string>` of CSS properties that require HTML restructuring (not just CSS swaps). Includes `display:flex`, `display:grid`, `word-break`, `position`, `border-radius` (Outlook), `background-image` (Outlook), and more.
310
-
311
- ```typescript
312
- import { STRUCTURAL_FIX_PROPERTIES } from "@emailens/engine";
313
- STRUCTURAL_FIX_PROPERTIES.has("word-break"); // true
314
- STRUCTURAL_FIX_PROPERTIES.has("color"); // false
315
- ```
327
+ Instant synchronous token estimate (~3.5 chars/token).
316
328
 
317
- ## CSS Support Matrix
329
+ ---
318
330
 
319
- The engine includes a comprehensive CSS support matrix (`src/rules/css-support.ts`) covering 45+ CSS properties and HTML elements across all 12 clients. Data sourced from [caniemail.com](https://www.caniemail.com/) with inferred values for HEY Mail and Superhuman based on their rendering engines.
331
+ ## Supported Email Clients
320
332
 
321
- Properties added in v0.2.0: `word-break`, `overflow-wrap`, `white-space`, `text-overflow`, `vertical-align`, `border-spacing`, `min-width`, `min-height`, `max-height`, `text-shadow`, `background-size`, `background-position`.
333
+ | Client | ID | Category | Engine | Dark Mode |
334
+ |---|---|---|---|---|
335
+ | Gmail | `gmail-web` | Webmail | Gmail Web | Yes |
336
+ | Gmail Android | `gmail-android` | Mobile | Gmail Mobile | Yes |
337
+ | Gmail iOS | `gmail-ios` | Mobile | Gmail Mobile | Yes |
338
+ | Outlook 365 | `outlook-web` | Webmail | Outlook Web | Yes |
339
+ | Outlook Windows | `outlook-windows` | Desktop | Microsoft Word | No |
340
+ | Apple Mail | `apple-mail-macos` | Desktop | WebKit | Yes |
341
+ | Apple Mail iOS | `apple-mail-ios` | Mobile | WebKit | Yes |
342
+ | Yahoo Mail | `yahoo-mail` | Webmail | Yahoo | Yes |
343
+ | Samsung Mail | `samsung-mail` | Mobile | Samsung | Yes |
344
+ | Thunderbird | `thunderbird` | Desktop | Gecko | No |
345
+ | HEY Mail | `hey-mail` | Webmail | WebKit | Yes |
346
+ | Superhuman | `superhuman` | Desktop | Blink | Yes |
322
347
 
323
348
  ## Types
324
349
 
@@ -327,16 +352,6 @@ type SupportLevel = "supported" | "partial" | "unsupported" | "unknown";
327
352
  type Framework = "jsx" | "mjml" | "maizzle";
328
353
  type InputFormat = "html" | Framework;
329
354
  type FixType = "css" | "structural";
330
- type AiProvider = (prompt: string) => Promise<string>;
331
-
332
- interface EmailClient {
333
- id: string;
334
- name: string;
335
- category: "webmail" | "desktop" | "mobile";
336
- engine: string;
337
- darkModeSupport: boolean;
338
- icon: string;
339
- }
340
355
 
341
356
  interface CSSWarning {
342
357
  severity: "error" | "warning" | "info";
@@ -345,50 +360,44 @@ interface CSSWarning {
345
360
  message: string;
346
361
  suggestion?: string;
347
362
  fix?: CodeFix;
348
- fixIsGenericFallback?: boolean;
349
- fixType?: FixType; // "css" or "structural" (v0.2.0)
363
+ fixType?: FixType;
364
+ line?: number; // line number in <style> block
365
+ selector?: string; // element selector for inline styles
350
366
  }
351
367
 
352
- interface CodeFix {
353
- before: string;
354
- after: string;
355
- language: "html" | "css" | "jsx" | "mjml" | "maizzle";
356
- description: string;
368
+ interface AuditReport {
369
+ compatibility: {
370
+ warnings: CSSWarning[];
371
+ scores: Record<string, { score: number; errors: number; warnings: number; info: number }>;
372
+ };
373
+ spam: SpamReport;
374
+ links: LinkReport;
375
+ accessibility: AccessibilityReport;
376
+ images: ImageReport;
357
377
  }
358
378
 
359
- interface AiFixResult {
360
- code: string;
361
- prompt: string;
362
- targetedWarnings: number;
363
- structuralCount: number;
364
- tokenEstimate: TokenEstimate;
379
+ interface SpamReport {
380
+ score: number; // 0–100 (100 = clean)
381
+ level: "low" | "medium" | "high";
382
+ issues: SpamIssue[];
365
383
  }
366
384
 
367
- interface TokenEstimate {
368
- inputTokens: number;
369
- estimatedOutputTokens: number;
370
- promptCharacters: number;
371
- htmlCharacters: number;
372
- warningCount: number;
373
- structuralCount: number;
374
- truncated: boolean;
375
- warningsRemoved: number;
385
+ interface LinkReport {
386
+ totalLinks: number;
387
+ issues: LinkIssue[];
388
+ breakdown: { https: number; http: number; mailto: number; tel: number; ... };
376
389
  }
377
390
 
378
- interface TransformResult {
379
- clientId: string;
380
- html: string;
381
- warnings: CSSWarning[];
391
+ interface AccessibilityReport {
392
+ score: number; // 0–100
393
+ issues: AccessibilityIssue[];
382
394
  }
383
395
 
384
- interface DiffResult {
385
- clientId: string;
386
- scoreBefore: number;
387
- scoreAfter: number;
388
- scoreDelta: number;
389
- fixed: CSSWarning[];
390
- introduced: CSSWarning[];
391
- unchanged: CSSWarning[];
396
+ interface ImageReport {
397
+ total: number;
398
+ totalDataUriBytes: number;
399
+ issues: ImageIssue[];
400
+ images: ImageInfo[];
392
401
  }
393
402
  ```
394
403
 
@@ -398,7 +407,7 @@ interface DiffResult {
398
407
  bun test
399
408
  ```
400
409
 
401
- 166 tests covering analysis, transformation, dark mode simulation, framework-aware fixes, AI fix generation, token estimation, smart truncation, fixType classification, and accuracy benchmarks against real-world email templates.
410
+ 449 tests covering analysis, transformation, dark mode simulation, framework-aware fixes, AI fix generation, token estimation, spam scoring, link validation, accessibility checking, image analysis, security hardening, integration pipelines, and accuracy benchmarks.
402
411
 
403
412
  ## License
404
413