@emailens/engine 0.6.0 → 0.7.0

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/dist/index.js CHANGED
@@ -2747,6 +2747,32 @@ var GENERIC_LINK_TEXT = /* @__PURE__ */ new Set([
2747
2747
  "tap here",
2748
2748
  "this"
2749
2749
  ]);
2750
+ var GMAIL_CLIP_THRESHOLD = 102 * 1024;
2751
+ var GMAIL_CLIP_WARNING_THRESHOLD = 90 * 1024;
2752
+ var CLIENT_DISPLAY_LIMITS = [
2753
+ { client: "Gmail (Web)", subjectLimit: 70, preheaderLimit: 90 },
2754
+ { client: "Gmail (Mobile)", subjectLimit: 40, preheaderLimit: 90 },
2755
+ { client: "Outlook (Web)", subjectLimit: 60, preheaderLimit: 35 },
2756
+ { client: "Outlook (Desktop)", subjectLimit: 55, preheaderLimit: 35 },
2757
+ { client: "Apple Mail (macOS)", subjectLimit: 78, preheaderLimit: 140 },
2758
+ { client: "Apple Mail (iOS)", subjectLimit: 35, preheaderLimit: 90 },
2759
+ { client: "Yahoo Mail", subjectLimit: 46, preheaderLimit: 100 },
2760
+ { client: "Samsung Email", subjectLimit: 40, preheaderLimit: 70 }
2761
+ ];
2762
+ var TEMPLATE_VARIABLE_PATTERNS = [
2763
+ [/\{\{[\s\S]*?\}\}/g, "Handlebars/Mustache"],
2764
+ // {{var}}
2765
+ [/\$\{[^}]+\}/g, "ES template literal"],
2766
+ // ${var}
2767
+ [/<%=?\s*[^%]+%>/g, "ERB/EJS"],
2768
+ // <% %> / <%= %>
2769
+ [/\*\|[A-Z_][A-Z0-9_]*\|\*/g, "Mailchimp merge tag"],
2770
+ // *|TAG|*
2771
+ [/%%[A-Za-z_][A-Za-z0-9_]*%%/g, "Salesforce AMPscript"],
2772
+ // %%tag%%
2773
+ [/\{[A-Za-z_][A-Za-z0-9_.]{2,}\}/g, "Single-brace merge field"]
2774
+ // {merge_field} (3+ char names)
2775
+ ];
2750
2776
  var EMPTY_SPAM = { score: 100, level: "low", issues: [] };
2751
2777
  var EMPTY_LINKS = {
2752
2778
  totalLinks: 0,
@@ -2755,6 +2781,9 @@ var EMPTY_LINKS = {
2755
2781
  };
2756
2782
  var EMPTY_ACCESSIBILITY = { score: 100, issues: [] };
2757
2783
  var EMPTY_IMAGES = { total: 0, totalDataUriBytes: 0, issues: [], images: [] };
2784
+ var EMPTY_INBOX_PREVIEW = { subject: null, preheader: null, subjectLength: 0, preheaderLength: 0, truncation: [], issues: [] };
2785
+ var EMPTY_SIZE = { htmlBytes: 0, humanSize: "0 B", clipped: false, issues: [] };
2786
+ var EMPTY_TEMPLATE = { unresolvedCount: 0, issues: [] };
2758
2787
 
2759
2788
  // src/transform.ts
2760
2789
  function inlineStyles($) {
@@ -4417,7 +4446,9 @@ var WEIGHTS = {
4417
4446
  "image-only": 20,
4418
4447
  "high-image-ratio": 10,
4419
4448
  "deceptive-link": 15,
4420
- "all-caps-subject": 10
4449
+ "all-caps-subject": 10,
4450
+ "missing-physical-address": 8,
4451
+ "missing-one-click-unsubscribe": 5
4421
4452
  };
4422
4453
  function extractVisibleText($) {
4423
4454
  const clone = $.root().clone();
@@ -4634,6 +4665,47 @@ Href: ${href}`
4634
4665
  });
4635
4666
  return issues;
4636
4667
  }
4668
+ var STREET_ADDRESS_PATTERN = /\b\d{1,5}\s+[A-Za-z]+\s+(St(reet)?|Ave(nue)?|Blvd|Boulevard|Dr(ive)?|Rd|Road|Ln|Lane|Way|Ct|Court|Pl(ace)?|Pkwy|Parkway|Cir(cle)?|Terr(ace)?|Loop)\b/i;
4669
+ var PO_BOX_PATTERN = /\bP\.?\s*O\.?\s*Box\s+\d+/i;
4670
+ var ZIP_CODE_PATTERN = /\b\d{5}(-\d{4})?\b/;
4671
+ var ADDRESS_CLASS_PATTERN = /address|mailing|postal|footer-address|physical-address/i;
4672
+ function checkPhysicalAddress($, text, options) {
4673
+ if ((options == null ? void 0 : options.emailType) === "transactional") return null;
4674
+ if (STREET_ADDRESS_PATTERN.test(text) && ZIP_CODE_PATTERN.test(text)) return null;
4675
+ if (PO_BOX_PATTERN.test(text) && ZIP_CODE_PATTERN.test(text)) return null;
4676
+ let foundByClass = false;
4677
+ $("[class]").each((_, el) => {
4678
+ const cls = $(el).attr("class") || "";
4679
+ if (ADDRESS_CLASS_PATTERN.test(cls)) {
4680
+ const elText = $(el).text().trim();
4681
+ if (elText.length > 5) {
4682
+ foundByClass = true;
4683
+ return false;
4684
+ }
4685
+ }
4686
+ });
4687
+ if (foundByClass) return null;
4688
+ if ($("address").length > 0 && $("address").text().trim().length > 5) return null;
4689
+ return {
4690
+ rule: "missing-physical-address",
4691
+ severity: "warning",
4692
+ message: "No physical mailing address detected \u2014 required by CAN-SPAM for marketing emails.",
4693
+ detail: "Include a street address or P.O. Box in the email footer."
4694
+ };
4695
+ }
4696
+ function checkOneClickUnsubscribe(options) {
4697
+ var _a, _b;
4698
+ if (!((_a = options == null ? void 0 : options.listUnsubscribeHeader) == null ? void 0 : _a.trim())) return null;
4699
+ if (!((_b = options == null ? void 0 : options.listUnsubscribePostHeader) == null ? void 0 : _b.trim())) {
4700
+ return {
4701
+ rule: "missing-one-click-unsubscribe",
4702
+ severity: "warning",
4703
+ message: "List-Unsubscribe header present but missing List-Unsubscribe-Post header (RFC 8058). Gmail and Yahoo require one-click unsubscribe.",
4704
+ detail: 'Add "List-Unsubscribe-Post: List-Unsubscribe=One-Click" header.'
4705
+ };
4706
+ }
4707
+ return null;
4708
+ }
4637
4709
  function checkAllCapsTitle($) {
4638
4710
  const title = $("title").text().trim();
4639
4711
  if (title.length > 5 && title === title.toUpperCase() && /[A-Z]/.test(title)) {
@@ -4663,6 +4735,10 @@ function analyzeSpamFromDom($, options) {
4663
4735
  issues.push(...checkDeceptiveLinks($));
4664
4736
  const capsTitle = checkAllCapsTitle($);
4665
4737
  if (capsTitle) issues.push(capsTitle);
4738
+ const addressIssue = checkPhysicalAddress($, text, options);
4739
+ if (addressIssue) issues.push(addressIssue);
4740
+ const oneClickIssue = checkOneClickUnsubscribe(options);
4741
+ if (oneClickIssue) issues.push(oneClickIssue);
4666
4742
  let penalty = 0;
4667
4743
  const seenRules = /* @__PURE__ */ new Map();
4668
4744
  for (const issue of issues) {
@@ -4851,6 +4927,23 @@ function validateLinksFromDom($) {
4851
4927
  });
4852
4928
  }
4853
4929
  });
4930
+ links.each((_, el) => {
4931
+ const href = $(el).attr("href") || "";
4932
+ const trimmed = href.trim();
4933
+ if (trimmed.startsWith("#") && trimmed.length > 1) {
4934
+ const targetId = trimmed.slice(1);
4935
+ const target = $(`[id="${targetId}"]`);
4936
+ if (target.length === 0) {
4937
+ issues.push({
4938
+ severity: "error",
4939
+ rule: "broken-anchor",
4940
+ message: `Anchor link "${trimmed}" points to an element that does not exist`,
4941
+ href: trimmed,
4942
+ text: $(el).text().trim().slice(0, 80) || "(no text)"
4943
+ });
4944
+ }
4945
+ }
4946
+ });
4854
4947
  for (const [href, count] of hrefCounts) {
4855
4948
  if (count > 5) {
4856
4949
  issues.push({
@@ -5104,6 +5197,21 @@ function checkTextSizeAndContrast($) {
5104
5197
  }
5105
5198
  return issues;
5106
5199
  }
5200
+ function checkCharsetDeclaration($) {
5201
+ const metaCharset = $("meta[charset]");
5202
+ if (metaCharset.length > 0) return null;
5203
+ const httpEquiv = $('meta[http-equiv="Content-Type"]');
5204
+ if (httpEquiv.length > 0) {
5205
+ const content = httpEquiv.attr("content") || "";
5206
+ if (/charset\s*=/i.test(content)) return null;
5207
+ }
5208
+ return {
5209
+ severity: "warning",
5210
+ rule: "missing-charset",
5211
+ message: "Missing charset declaration",
5212
+ details: 'Add <meta charset="utf-8"> in <head> to prevent encoding issues across email clients.'
5213
+ };
5214
+ }
5107
5215
  function checkSemanticStructure($) {
5108
5216
  const issues = [];
5109
5217
  const headings = [];
@@ -5136,6 +5244,8 @@ function checkAccessibilityFromDom($) {
5136
5244
  issues.push(...checkTableAccessibility($));
5137
5245
  issues.push(...checkTextSizeAndContrast($));
5138
5246
  issues.push(...checkSemanticStructure($));
5247
+ const charsetIssue = checkCharsetDeclaration($);
5248
+ if (charsetIssue) issues.push(charsetIssue);
5139
5249
  let penalty = 0;
5140
5250
  const seenRules = /* @__PURE__ */ new Map();
5141
5251
  for (const issue of issues) {
@@ -5338,8 +5448,307 @@ function analyzeImages(html) {
5338
5448
  return analyzeImagesFromDom($);
5339
5449
  }
5340
5450
 
5341
- // src/audit.ts
5451
+ // src/inbox-preview.ts
5342
5452
  import * as cheerio8 from "cheerio";
5453
+ var MAX_SUBJECT_LENGTH = 60;
5454
+ var MAX_PREHEADER_LENGTH = 100;
5455
+ var MIN_PREHEADER_LENGTH = 30;
5456
+ var EMOJI_PATTERN = /[\u{1F600}-\u{1F64F}\u{1F300}-\u{1F5FF}\u{1F680}-\u{1F6FF}\u{1F1E0}-\u{1F1FF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}\u{2B50}-\u{2B55}\u{FE00}-\u{FE0F}\u{1F900}-\u{1F9FF}\u{1FA00}-\u{1FA6F}\u{1FA70}-\u{1FAFF}\u{200D}\u{20E3}\u{E0020}-\u{E007F}]/u;
5457
+ var ZWNJ_PADDING_PATTERN = /(\u200C\s*(&nbsp;|\u00A0)\s*){2,}|(&zwnj;\s*&nbsp;\s*){2,}/;
5458
+ function extractInboxPreview(html) {
5459
+ if (!html || !html.trim()) {
5460
+ return {
5461
+ subject: null,
5462
+ preheader: null,
5463
+ subjectLength: 0,
5464
+ preheaderLength: 0,
5465
+ truncation: [],
5466
+ issues: [{ rule: "missing-subject", severity: "warning", message: "No <title> tag found. Most email clients use this as the subject line." }]
5467
+ };
5468
+ }
5469
+ if (html.length > MAX_HTML_SIZE) {
5470
+ throw new Error(`HTML input exceeds ${MAX_HTML_SIZE / 1024}KB limit.`);
5471
+ }
5472
+ const $ = cheerio8.load(html);
5473
+ return extractInboxPreviewFromDom($);
5474
+ }
5475
+ function extractInboxPreviewFromDom($) {
5476
+ var _a, _b;
5477
+ const issues = [];
5478
+ const titleEl = $("title");
5479
+ const subject = titleEl.length > 0 ? titleEl.first().text().trim() || null : null;
5480
+ const subjectLength = (_a = subject == null ? void 0 : subject.length) != null ? _a : 0;
5481
+ if (!subject) {
5482
+ issues.push({
5483
+ rule: "missing-subject",
5484
+ severity: "warning",
5485
+ message: "No <title> tag found. Most email clients use this as the subject line."
5486
+ });
5487
+ } else if (subjectLength > MAX_SUBJECT_LENGTH) {
5488
+ issues.push({
5489
+ rule: "subject-too-long",
5490
+ severity: "warning",
5491
+ message: `Subject is ${subjectLength} characters \u2014 may be truncated in inboxes (recommended: ${MAX_SUBJECT_LENGTH} or fewer).`
5492
+ });
5493
+ }
5494
+ const preheader = extractPreheaderText($);
5495
+ const preheaderLength = (_b = preheader == null ? void 0 : preheader.length) != null ? _b : 0;
5496
+ if (!preheader) {
5497
+ issues.push({
5498
+ rule: "missing-preheader",
5499
+ severity: "info",
5500
+ message: "No preheader text detected. Adding preview text improves open rates in crowded inboxes."
5501
+ });
5502
+ } else if (preheaderLength < MIN_PREHEADER_LENGTH) {
5503
+ issues.push({
5504
+ rule: "preheader-too-short",
5505
+ severity: "warning",
5506
+ message: `Preheader is only ${preheaderLength} characters \u2014 email clients may backfill with body text (recommended: ${MIN_PREHEADER_LENGTH}+).`
5507
+ });
5508
+ } else if (preheaderLength > MAX_PREHEADER_LENGTH) {
5509
+ issues.push({
5510
+ rule: "preheader-too-long",
5511
+ severity: "info",
5512
+ message: `Preheader is ${preheaderLength} characters \u2014 most clients show 40\u2013100 characters. Text beyond that is hidden.`
5513
+ });
5514
+ }
5515
+ if (hasZwnjPadding($)) {
5516
+ issues.push({
5517
+ rule: "zwnj-padding",
5518
+ severity: "info",
5519
+ message: "Preheader uses &zwnj;&nbsp; padding hack \u2014 works in most clients but may show garbled text in some."
5520
+ });
5521
+ }
5522
+ if (subject && EMOJI_PATTERN.test(subject)) {
5523
+ issues.push({
5524
+ rule: "emoji-in-subject",
5525
+ severity: "info",
5526
+ message: "Subject line contains emoji \u2014 renders inconsistently across email clients and may trigger spam filters."
5527
+ });
5528
+ }
5529
+ const truncation = computeTruncation(subject, preheader);
5530
+ return {
5531
+ subject,
5532
+ preheader,
5533
+ subjectLength,
5534
+ preheaderLength,
5535
+ truncation,
5536
+ issues
5537
+ };
5538
+ }
5539
+ function computeTruncation(subject, preheader) {
5540
+ return CLIENT_DISPLAY_LIMITS.map((limit) => {
5541
+ const truncatedSubject = subject && subject.length > limit.subjectLimit ? subject.slice(0, limit.subjectLimit - 1) + "\u2026" : subject;
5542
+ const truncatedPreheader = preheader && preheader.length > limit.preheaderLimit ? preheader.slice(0, limit.preheaderLimit - 1) + "\u2026" : preheader;
5543
+ return {
5544
+ client: limit.client,
5545
+ subjectLimit: limit.subjectLimit,
5546
+ preheaderLimit: limit.preheaderLimit,
5547
+ truncatedSubject: truncatedSubject != null ? truncatedSubject : null,
5548
+ truncatedPreheader: truncatedPreheader != null ? truncatedPreheader : null,
5549
+ subjectTruncated: !!subject && subject.length > limit.subjectLimit,
5550
+ preheaderTruncated: !!preheader && preheader.length > limit.preheaderLimit
5551
+ };
5552
+ });
5553
+ }
5554
+ function hasZwnjPadding($) {
5555
+ const body = $("body");
5556
+ if (!body.length) return false;
5557
+ const hiddenSelectors = [
5558
+ 'div[style*="display:none"]',
5559
+ 'div[style*="display: none"]',
5560
+ 'span[style*="display:none"]',
5561
+ 'span[style*="display: none"]',
5562
+ 'div[style*="max-height:0"]',
5563
+ 'div[style*="max-height: 0"]',
5564
+ 'span[style*="max-height:0"]',
5565
+ 'span[style*="max-height: 0"]',
5566
+ '[class*="preheader"]',
5567
+ '[class*="preview-text"]',
5568
+ '[class*="previewText"]'
5569
+ ];
5570
+ for (const sel of hiddenSelectors) {
5571
+ const el = body.find(sel).first();
5572
+ if (el.length) {
5573
+ const rawHtml = el.html() || "";
5574
+ if (ZWNJ_PADDING_PATTERN.test(rawHtml)) return true;
5575
+ }
5576
+ }
5577
+ return false;
5578
+ }
5579
+ function extractPreheaderText($) {
5580
+ const body = $("body");
5581
+ if (!body.length) return null;
5582
+ const hiddenSelectors = [
5583
+ 'div[style*="display:none"]',
5584
+ 'div[style*="display: none"]',
5585
+ 'span[style*="display:none"]',
5586
+ 'span[style*="display: none"]',
5587
+ 'div[style*="visibility:hidden"]',
5588
+ 'div[style*="visibility: hidden"]',
5589
+ 'span[style*="visibility:hidden"]',
5590
+ 'span[style*="visibility: hidden"]',
5591
+ 'div[style*="max-height:0"]',
5592
+ 'div[style*="max-height: 0"]',
5593
+ 'span[style*="max-height:0"]',
5594
+ 'span[style*="max-height: 0"]',
5595
+ 'div[style*="mso-hide:all"]',
5596
+ 'div[style*="mso-hide: all"]'
5597
+ ];
5598
+ for (const sel of hiddenSelectors) {
5599
+ const el = body.find(sel).first();
5600
+ if (el.length) {
5601
+ const text = el.text().trim();
5602
+ if (text) return text;
5603
+ }
5604
+ }
5605
+ const preheaderClasses = body.find(
5606
+ '[class*="preheader"], [class*="preview-text"], [class*="previewText"]'
5607
+ ).first();
5608
+ if (preheaderClasses.length) {
5609
+ const text = preheaderClasses.text().trim();
5610
+ if (text) return text;
5611
+ }
5612
+ const firstText = getFirstVisibleText($, body);
5613
+ return firstText || null;
5614
+ }
5615
+ function getFirstVisibleText($, body) {
5616
+ const skipTags = /* @__PURE__ */ new Set(["style", "script", "head", "title"]);
5617
+ let result = null;
5618
+ body.find("*").each((_, el) => {
5619
+ var _a;
5620
+ if (result) return false;
5621
+ const tagName = (_a = el.tagName) == null ? void 0 : _a.toLowerCase();
5622
+ if (skipTags.has(tagName)) return;
5623
+ const style = $(el).attr("style") || "";
5624
+ if (style.includes("display:none") || style.includes("display: none") || style.includes("visibility:hidden") || style.includes("visibility: hidden")) {
5625
+ return;
5626
+ }
5627
+ const directText = $(el).contents().filter((_2, node) => node.type === "text").text().trim();
5628
+ if (directText && directText.length > 1) {
5629
+ const cleaned = directText.replace(/[\u200B\u00A0\s]+/g, " ").trim();
5630
+ if (cleaned.length > 1) {
5631
+ result = cleaned;
5632
+ return false;
5633
+ }
5634
+ }
5635
+ });
5636
+ return result;
5637
+ }
5638
+
5639
+ // src/size-checker.ts
5640
+ import * as cheerio9 from "cheerio";
5641
+ function humanizeBytes(bytes) {
5642
+ if (bytes < 1024) return `${bytes} B`;
5643
+ const kb = bytes / 1024;
5644
+ if (kb < 1024) return `${kb.toFixed(1)} KB`;
5645
+ const mb = kb / 1024;
5646
+ return `${mb.toFixed(2)} MB`;
5647
+ }
5648
+ function checkSizeFromDom(_$, html) {
5649
+ const htmlBytes = new TextEncoder().encode(html).length;
5650
+ const humanSize = humanizeBytes(htmlBytes);
5651
+ const issues = [];
5652
+ let clipped = false;
5653
+ if (htmlBytes > GMAIL_CLIP_THRESHOLD) {
5654
+ clipped = true;
5655
+ issues.push({
5656
+ rule: "gmail-clipped",
5657
+ severity: "error",
5658
+ message: `Email is ${humanSize} \u2014 Gmail will clip it at ~102 KB. Recipients see a "View entire message" link instead of your content.`,
5659
+ detail: `${htmlBytes} bytes exceeds the ${GMAIL_CLIP_THRESHOLD} byte threshold.`
5660
+ });
5661
+ } else if (htmlBytes > GMAIL_CLIP_WARNING_THRESHOLD) {
5662
+ issues.push({
5663
+ rule: "gmail-clip-warning",
5664
+ severity: "warning",
5665
+ message: `Email is ${humanSize} \u2014 approaching Gmail's ~102 KB clip threshold. Consider trimming.`,
5666
+ detail: `${htmlBytes} bytes is within ${GMAIL_CLIP_THRESHOLD - htmlBytes} bytes of the clip threshold.`
5667
+ });
5668
+ }
5669
+ return { htmlBytes, humanSize, clipped, issues };
5670
+ }
5671
+ function checkSize(html) {
5672
+ if (!html || !html.trim()) {
5673
+ return { htmlBytes: 0, humanSize: "0 B", clipped: false, issues: [] };
5674
+ }
5675
+ if (html.length > MAX_HTML_SIZE) {
5676
+ throw new Error(`HTML input exceeds ${MAX_HTML_SIZE / 1024}KB limit.`);
5677
+ }
5678
+ const $ = cheerio9.load(html);
5679
+ return checkSizeFromDom($, html);
5680
+ }
5681
+
5682
+ // src/template-checker.ts
5683
+ import * as cheerio10 from "cheerio";
5684
+ function checkTemplateVariablesFromDom($) {
5685
+ const issues = [];
5686
+ const seen = /* @__PURE__ */ new Set();
5687
+ const textContent = extractTextContent($);
5688
+ for (const [pattern, label] of TEMPLATE_VARIABLE_PATTERNS) {
5689
+ pattern.lastIndex = 0;
5690
+ let match;
5691
+ while ((match = pattern.exec(textContent)) !== null) {
5692
+ const variable = match[0];
5693
+ const key = `text:${variable}`;
5694
+ if (seen.has(key)) continue;
5695
+ seen.add(key);
5696
+ issues.push({
5697
+ rule: "unresolved-variable",
5698
+ severity: "error",
5699
+ message: `Unresolved ${label} variable "${variable}" found in text content.`,
5700
+ variable,
5701
+ location: "text"
5702
+ });
5703
+ }
5704
+ }
5705
+ const attrSelectors = ["[href]", "[src]", "[alt]"];
5706
+ for (const sel of attrSelectors) {
5707
+ $(sel).each((_, el) => {
5708
+ const attrs = ["href", "src", "alt"];
5709
+ for (const attr of attrs) {
5710
+ const value = $(el).attr(attr);
5711
+ if (!value) continue;
5712
+ for (const [pattern, label] of TEMPLATE_VARIABLE_PATTERNS) {
5713
+ pattern.lastIndex = 0;
5714
+ let match;
5715
+ while ((match = pattern.exec(value)) !== null) {
5716
+ const variable = match[0];
5717
+ const key = `attr:${attr}:${variable}`;
5718
+ if (seen.has(key)) continue;
5719
+ seen.add(key);
5720
+ issues.push({
5721
+ rule: "unresolved-variable",
5722
+ severity: "error",
5723
+ message: `Unresolved ${label} variable "${variable}" found in ${attr} attribute.`,
5724
+ variable,
5725
+ location: "attribute"
5726
+ });
5727
+ }
5728
+ }
5729
+ }
5730
+ });
5731
+ }
5732
+ return { unresolvedCount: issues.length, issues };
5733
+ }
5734
+ function extractTextContent($) {
5735
+ const clone = $.root().clone();
5736
+ clone.find("style, script, head").remove();
5737
+ return clone.text();
5738
+ }
5739
+ function checkTemplateVariables(html) {
5740
+ if (!html || !html.trim()) {
5741
+ return { unresolvedCount: 0, issues: [] };
5742
+ }
5743
+ if (html.length > MAX_HTML_SIZE) {
5744
+ throw new Error(`HTML input exceeds ${MAX_HTML_SIZE / 1024}KB limit.`);
5745
+ }
5746
+ const $ = cheerio10.load(html);
5747
+ return checkTemplateVariablesFromDom($);
5748
+ }
5749
+
5750
+ // src/audit.ts
5751
+ import * as cheerio11 from "cheerio";
5343
5752
  function auditEmail(html, options) {
5344
5753
  var _a;
5345
5754
  if (!html || !html.trim()) {
@@ -5348,7 +5757,10 @@ function auditEmail(html, options) {
5348
5757
  spam: EMPTY_SPAM,
5349
5758
  links: EMPTY_LINKS,
5350
5759
  accessibility: EMPTY_ACCESSIBILITY,
5351
- images: EMPTY_IMAGES
5760
+ images: EMPTY_IMAGES,
5761
+ inboxPreview: EMPTY_INBOX_PREVIEW,
5762
+ size: EMPTY_SIZE,
5763
+ templateVariables: EMPTY_TEMPLATE
5352
5764
  };
5353
5765
  }
5354
5766
  if (html.length > MAX_HTML_SIZE) {
@@ -5356,18 +5768,21 @@ function auditEmail(html, options) {
5356
5768
  }
5357
5769
  const framework = options == null ? void 0 : options.framework;
5358
5770
  const skip = new Set((_a = options == null ? void 0 : options.skip) != null ? _a : []);
5359
- const $ = cheerio8.load(html);
5771
+ const $ = cheerio11.load(html);
5360
5772
  const warnings = skip.has("compatibility") ? [] : analyzeEmailFromDom($, framework);
5361
5773
  const scores = skip.has("compatibility") ? {} : generateCompatibilityScore(warnings);
5362
5774
  const spam = skip.has("spam") ? EMPTY_SPAM : analyzeSpamFromDom($, options == null ? void 0 : options.spam);
5363
5775
  const links = skip.has("links") ? EMPTY_LINKS : validateLinksFromDom($);
5364
5776
  const accessibility = skip.has("accessibility") ? EMPTY_ACCESSIBILITY : checkAccessibilityFromDom($);
5365
5777
  const images = skip.has("images") ? EMPTY_IMAGES : analyzeImagesFromDom($);
5366
- return { compatibility: { warnings, scores }, spam, links, accessibility, images };
5778
+ const inboxPreview = skip.has("inboxPreview") ? EMPTY_INBOX_PREVIEW : extractInboxPreviewFromDom($);
5779
+ const size = skip.has("size") ? EMPTY_SIZE : checkSizeFromDom($, html);
5780
+ const templateVariables = skip.has("templateVariables") ? EMPTY_TEMPLATE : checkTemplateVariablesFromDom($);
5781
+ return { compatibility: { warnings, scores }, spam, links, accessibility, images, inboxPreview, size, templateVariables };
5367
5782
  }
5368
5783
 
5369
5784
  // src/session.ts
5370
- import * as cheerio9 from "cheerio";
5785
+ import * as cheerio12 from "cheerio";
5371
5786
  function createSession(html, options) {
5372
5787
  if (!html || !html.trim()) {
5373
5788
  const fw = options == null ? void 0 : options.framework;
@@ -5379,7 +5794,10 @@ function createSession(html, options) {
5379
5794
  spam: EMPTY_SPAM,
5380
5795
  links: EMPTY_LINKS,
5381
5796
  accessibility: EMPTY_ACCESSIBILITY,
5382
- images: EMPTY_IMAGES
5797
+ images: EMPTY_IMAGES,
5798
+ inboxPreview: EMPTY_INBOX_PREVIEW,
5799
+ size: EMPTY_SIZE,
5800
+ templateVariables: EMPTY_TEMPLATE
5383
5801
  }),
5384
5802
  analyze: () => [],
5385
5803
  score: () => ({}),
@@ -5387,6 +5805,9 @@ function createSession(html, options) {
5387
5805
  validateLinks: () => EMPTY_LINKS,
5388
5806
  checkAccessibility: () => EMPTY_ACCESSIBILITY,
5389
5807
  analyzeImages: () => EMPTY_IMAGES,
5808
+ extractInboxPreview: () => EMPTY_INBOX_PREVIEW,
5809
+ checkSize: () => EMPTY_SIZE,
5810
+ checkTemplateVariables: () => EMPTY_TEMPLATE,
5390
5811
  transformForClient: (clientId) => ({ clientId, html: html || "", warnings: [] }),
5391
5812
  transformForAllClients: () => [],
5392
5813
  simulateDarkMode: (clientId) => ({ html: html || "", warnings: [] })
@@ -5395,7 +5816,7 @@ function createSession(html, options) {
5395
5816
  if (html.length > MAX_HTML_SIZE) {
5396
5817
  throw new Error(`HTML input exceeds ${MAX_HTML_SIZE / 1024}KB limit.`);
5397
5818
  }
5398
- const $ = cheerio9.load(html);
5819
+ const $ = cheerio12.load(html);
5399
5820
  const framework = options == null ? void 0 : options.framework;
5400
5821
  return {
5401
5822
  html,
@@ -5409,7 +5830,10 @@ function createSession(html, options) {
5409
5830
  const links = skip.has("links") ? EMPTY_LINKS : validateLinksFromDom($);
5410
5831
  const accessibility = skip.has("accessibility") ? EMPTY_ACCESSIBILITY : checkAccessibilityFromDom($);
5411
5832
  const images = skip.has("images") ? EMPTY_IMAGES : analyzeImagesFromDom($);
5412
- return { compatibility: { warnings, scores }, spam, links, accessibility, images };
5833
+ const inboxPreview = skip.has("inboxPreview") ? EMPTY_INBOX_PREVIEW : extractInboxPreviewFromDom($);
5834
+ const size = skip.has("size") ? EMPTY_SIZE : checkSizeFromDom($, html);
5835
+ const templateVariables = skip.has("templateVariables") ? EMPTY_TEMPLATE : checkTemplateVariablesFromDom($);
5836
+ return { compatibility: { warnings, scores }, spam, links, accessibility, images, inboxPreview, size, templateVariables };
5413
5837
  },
5414
5838
  analyze() {
5415
5839
  return analyzeEmailFromDom($, framework);
@@ -5429,6 +5853,15 @@ function createSession(html, options) {
5429
5853
  analyzeImages() {
5430
5854
  return analyzeImagesFromDom($);
5431
5855
  },
5856
+ extractInboxPreview() {
5857
+ return extractInboxPreviewFromDom($);
5858
+ },
5859
+ checkSize() {
5860
+ return checkSizeFromDom($, html);
5861
+ },
5862
+ checkTemplateVariables() {
5863
+ return checkTemplateVariablesFromDom($);
5864
+ },
5432
5865
  // Transforms create isolated copies since they mutate the DOM
5433
5866
  transformForClient(clientId) {
5434
5867
  return transformForClient(html, clientId, framework);
@@ -5454,11 +5887,14 @@ export {
5454
5887
  analyzeSpam,
5455
5888
  auditEmail,
5456
5889
  checkAccessibility,
5890
+ checkSize,
5891
+ checkTemplateVariables,
5457
5892
  contrastRatio,
5458
5893
  createSession,
5459
5894
  diffResults,
5460
5895
  errorWarnings,
5461
5896
  estimateAiFixTokens,
5897
+ extractInboxPreview,
5462
5898
  generateAiFix,
5463
5899
  generateCompatibilityScore,
5464
5900
  generateFixPrompt,