@emailens/engine 0.7.0 → 0.8.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.
Files changed (41) hide show
  1. package/README.md +72 -3
  2. package/dist/{chunk-ZQF2XUIJ.js → chunk-2NMEKWO5.js} +2 -13
  3. package/dist/{chunk-3NDQOTPM.js → chunk-E4QE4WXE.js} +2 -2
  4. package/dist/{chunk-URZ4XPST.js → chunk-HBKZMR7V.js} +2 -2
  5. package/dist/{chunk-OY3DGZVU.js → chunk-PW6EUG2G.js} +2 -2
  6. package/dist/chunk-Z22AD2IU.js +14 -0
  7. package/dist/{chunk-ZQF2XUIJ.js.map → chunk-Z22AD2IU.js.map} +1 -1
  8. package/dist/compile/index.d.cts +2 -2
  9. package/dist/compile/index.d.ts +2 -2
  10. package/dist/compile/index.js +8 -7
  11. package/dist/compile/index.js.map +1 -1
  12. package/dist/index.cjs +3250 -511
  13. package/dist/index.cjs.map +1 -1
  14. package/dist/index.d.cts +41 -5
  15. package/dist/index.d.ts +41 -5
  16. package/dist/index.js +3247 -512
  17. package/dist/index.js.map +1 -1
  18. package/dist/maizzle-KKJF7XY7.js +9 -0
  19. package/dist/mjml-C46UWTTK.js +9 -0
  20. package/dist/react-email-C-p8ZL37.d.cts +58 -0
  21. package/dist/react-email-DcseUoOu.d.ts +58 -0
  22. package/dist/react-email-QKHRTEJW.js +9 -0
  23. package/dist/react-email-QKHRTEJW.js.map +1 -0
  24. package/dist/server.cjs +368 -0
  25. package/dist/server.cjs.map +1 -0
  26. package/dist/server.d.cts +80 -0
  27. package/dist/server.d.ts +80 -0
  28. package/dist/server.js +342 -0
  29. package/dist/server.js.map +1 -0
  30. package/dist/{react-email-CYOtHJch.d.cts → types-4P9AtKm_.d.cts} +18 -54
  31. package/dist/{react-email-CYOtHJch.d.ts → types-4P9AtKm_.d.ts} +18 -54
  32. package/package.json +13 -6
  33. package/dist/maizzle-Y2CLJ7GB.js +0 -8
  34. package/dist/mjml-D7IOYXXK.js +0 -8
  35. package/dist/react-email-4TW6IDAA.js +0 -8
  36. /package/dist/{maizzle-Y2CLJ7GB.js.map → chunk-2NMEKWO5.js.map} +0 -0
  37. /package/dist/{chunk-3NDQOTPM.js.map → chunk-E4QE4WXE.js.map} +0 -0
  38. /package/dist/{chunk-URZ4XPST.js.map → chunk-HBKZMR7V.js.map} +0 -0
  39. /package/dist/{chunk-OY3DGZVU.js.map → chunk-PW6EUG2G.js.map} +0 -0
  40. /package/dist/{mjml-D7IOYXXK.js.map → maizzle-KKJF7XY7.js.map} +0 -0
  41. /package/dist/{react-email-4TW6IDAA.js.map → mjml-C46UWTTK.js.map} +0 -0
@@ -0,0 +1,80 @@
1
+ import { promises } from 'node:dns';
2
+ import { j as DeliverabilityReport } from './types-4P9AtKm_.js';
3
+
4
+ /**
5
+ * DNS-based email deliverability checker.
6
+ *
7
+ * Validates SPF, DKIM, DMARC, MX, and BIMI records for a domain
8
+ * using node:dns/promises. No external dependencies.
9
+ */
10
+
11
+ /** DNS resolver interface — injectable for testing. */
12
+ interface DnsResolver {
13
+ resolveMx: typeof promises.resolveMx;
14
+ resolveTxt: typeof promises.resolveTxt;
15
+ }
16
+ /**
17
+ * Check email deliverability for a domain.
18
+ *
19
+ * Validates MX, SPF, DKIM, DMARC, and BIMI DNS records.
20
+ * All DNS queries have a 5-second timeout.
21
+ *
22
+ * @example
23
+ * ```ts
24
+ * const report = await checkDeliverability("example.com");
25
+ * console.log(report.score); // 0-100
26
+ * console.log(report.checks); // individual check results
27
+ * ```
28
+ */
29
+ declare function checkDeliverability(domain: string, options?: {
30
+ _resolver?: DnsResolver;
31
+ }): Promise<DeliverabilityReport>;
32
+
33
+ /**
34
+ * Optional SpamAssassin integration.
35
+ *
36
+ * Shells out to `spamc` (daemon mode) or `spamassassin` (standalone mode)
37
+ * using execFile (safe — no shell interpolation).
38
+ * Returns null if neither is installed.
39
+ *
40
+ * Requires a full RFC 2822 message (headers + body), not just HTML.
41
+ */
42
+ interface SpamAssassinResult {
43
+ score: number;
44
+ threshold: number;
45
+ isSpam: boolean;
46
+ rules: Array<{
47
+ name: string;
48
+ score: number;
49
+ description: string;
50
+ }>;
51
+ rawOutput: string;
52
+ }
53
+ interface SpamAssassinOptions {
54
+ /** Timeout in ms (default: 30000) */
55
+ timeoutMs?: number;
56
+ }
57
+ /**
58
+ * Run a raw RFC 2822 email message through SpamAssassin.
59
+ *
60
+ * Tries `spamc -R` (daemon mode, faster) first, then falls back to
61
+ * `spamassassin -t` (standalone mode). Returns `null` if neither
62
+ * binary is available.
63
+ *
64
+ * Uses execFile (not exec) — no shell interpolation, safe from injection.
65
+ *
66
+ * @example
67
+ * ```ts
68
+ * const result = await checkSpamAssassin(rawMessage);
69
+ * if (result) {
70
+ * console.log(`Score: ${result.score}/${result.threshold}`);
71
+ * console.log(`Spam: ${result.isSpam}`);
72
+ * for (const rule of result.rules) {
73
+ * console.log(` ${rule.score} ${rule.name}: ${rule.description}`);
74
+ * }
75
+ * }
76
+ * ```
77
+ */
78
+ declare function checkSpamAssassin(rawMessage: string, options?: SpamAssassinOptions): Promise<SpamAssassinResult | null>;
79
+
80
+ export { type SpamAssassinOptions, type SpamAssassinResult, checkDeliverability, checkSpamAssassin };
package/dist/server.js ADDED
@@ -0,0 +1,342 @@
1
+ import "./chunk-2NMEKWO5.js";
2
+
3
+ // src/deliverability-checker.ts
4
+ import { promises as defaultDns } from "dns";
5
+ var DKIM_SELECTORS = [
6
+ "google",
7
+ "selector1",
8
+ "selector2",
9
+ "default",
10
+ "dkim",
11
+ "k1",
12
+ "k2",
13
+ "k3",
14
+ "mail",
15
+ "s1",
16
+ "s2",
17
+ "mandrill",
18
+ "pm",
19
+ "protonmail",
20
+ "smtp"
21
+ ];
22
+ var DNS_TIMEOUT_MS = 5e3;
23
+ async function withTimeout(fn, timeoutMs = DNS_TIMEOUT_MS) {
24
+ let timer;
25
+ const timeout = new Promise((_, reject) => {
26
+ timer = setTimeout(() => {
27
+ const err = Object.assign(new Error(`DNS timeout after ${timeoutMs}ms`), { code: "ABORT_ERR" });
28
+ reject(err);
29
+ }, timeoutMs);
30
+ });
31
+ try {
32
+ return await Promise.race([fn(), timeout]);
33
+ } finally {
34
+ clearTimeout(timer);
35
+ }
36
+ }
37
+ async function resolveTxtSafe(domain, dns) {
38
+ try {
39
+ const records = await withTimeout(() => dns.resolveTxt(domain));
40
+ return records.map((chunks) => chunks.join(""));
41
+ } catch (err) {
42
+ const code = err.code;
43
+ if (code === "ENOTFOUND" || code === "ENODATA" || code === "ETIMEOUT" || code === "ABORT_ERR" || code === "ESERVFAIL" || code === "ECONNREFUSED" || code === "EAI_AGAIN" || code === "ECANCELLED") {
44
+ return [];
45
+ }
46
+ throw err;
47
+ }
48
+ }
49
+ async function checkMX(domain, dns) {
50
+ try {
51
+ const records = await withTimeout(() => dns.resolveMx(domain));
52
+ if (records.length === 0) {
53
+ return {
54
+ name: "mx",
55
+ status: "fail",
56
+ message: "No MX records found \u2014 domain cannot receive email."
57
+ };
58
+ }
59
+ const sorted = records.sort((a, b) => a.priority - b.priority);
60
+ return {
61
+ name: "mx",
62
+ status: "pass",
63
+ message: `${records.length} MX record(s) found.`,
64
+ detail: sorted.map((r) => `${r.priority} ${r.exchange}`).join(", ")
65
+ };
66
+ } catch (err) {
67
+ const code = err.code;
68
+ if (code === "ENOTFOUND" || code === "ENODATA") {
69
+ return {
70
+ name: "mx",
71
+ status: "fail",
72
+ message: "No MX records found \u2014 domain cannot receive email."
73
+ };
74
+ }
75
+ return {
76
+ name: "mx",
77
+ status: "skip",
78
+ message: `MX lookup failed: ${err.message}`
79
+ };
80
+ }
81
+ }
82
+ async function checkSPF(domain, dns) {
83
+ const records = await resolveTxtSafe(domain, dns);
84
+ const spfRecord = records.find((r) => r.startsWith("v=spf1"));
85
+ if (!spfRecord) {
86
+ return {
87
+ name: "spf",
88
+ status: "fail",
89
+ message: "No SPF record found \u2014 receiving servers can't verify authorized senders.",
90
+ detail: 'Add a TXT record starting with "v=spf1" to your domain.'
91
+ };
92
+ }
93
+ if (spfRecord.includes("+all")) {
94
+ return {
95
+ name: "spf",
96
+ status: "fail",
97
+ message: "SPF record uses +all \u2014 this allows any server to send as your domain.",
98
+ record: spfRecord,
99
+ detail: "Replace +all with -all (hard fail) or ~all (soft fail) to restrict authorized senders."
100
+ };
101
+ }
102
+ if (!spfRecord.includes("~all") && !spfRecord.includes("-all") && !spfRecord.includes("?all")) {
103
+ return {
104
+ name: "spf",
105
+ status: "warn",
106
+ message: "SPF record found but missing enforcement mechanism (~all or -all).",
107
+ record: spfRecord,
108
+ detail: "Add ~all (soft fail) or -all (hard fail) to the end of your SPF record."
109
+ };
110
+ }
111
+ if (spfRecord.includes("~all")) {
112
+ return {
113
+ name: "spf",
114
+ status: "warn",
115
+ message: "SPF record uses ~all (soft fail) \u2014 consider upgrading to -all (hard fail) for stronger protection.",
116
+ record: spfRecord
117
+ };
118
+ }
119
+ return {
120
+ name: "spf",
121
+ status: "pass",
122
+ message: "SPF record found with -all enforcement.",
123
+ record: spfRecord
124
+ };
125
+ }
126
+ async function checkDKIM(domain, dns) {
127
+ const results = await Promise.allSettled(
128
+ DKIM_SELECTORS.map(async (selector) => {
129
+ const records = await resolveTxtSafe(`${selector}._domainkey.${domain}`, dns);
130
+ const dkimRecord = records.find((r) => r.includes("v=DKIM1") || r.includes("p="));
131
+ if (dkimRecord) {
132
+ return { selector, record: dkimRecord };
133
+ }
134
+ return null;
135
+ })
136
+ );
137
+ const matches = results.filter((r) => r.status === "fulfilled" && r.value !== null).map((r) => r.value);
138
+ if (matches.length > 0) {
139
+ return {
140
+ name: "dkim",
141
+ status: "pass",
142
+ message: `DKIM record(s) found for selector(s): ${matches.map((m) => m.selector).join(", ")}.`,
143
+ record: matches[0].record
144
+ };
145
+ }
146
+ return {
147
+ name: "dkim",
148
+ status: "warn",
149
+ message: `No DKIM records found for ${DKIM_SELECTORS.length} common selectors \u2014 DKIM may use a custom selector we didn't probe.`,
150
+ detail: "This doesn't mean DKIM is missing; the actual selector may differ from the common ones we checked."
151
+ };
152
+ }
153
+ async function checkDMARC(domain, dns) {
154
+ var _a;
155
+ const records = await resolveTxtSafe(`_dmarc.${domain}`, dns);
156
+ const dmarcRecord = records.find((r) => r.startsWith("v=DMARC1"));
157
+ if (!dmarcRecord) {
158
+ return {
159
+ name: "dmarc",
160
+ status: "fail",
161
+ message: "No DMARC record found \u2014 inbox providers may reject or quarantine your emails.",
162
+ detail: 'Add a TXT record at _dmarc.yourdomain.com starting with "v=DMARC1".'
163
+ };
164
+ }
165
+ const policyMatch = dmarcRecord.match(/(?:^|;)\s*p=(\w+)/);
166
+ const policy = (_a = policyMatch == null ? void 0 : policyMatch[1]) == null ? void 0 : _a.toLowerCase();
167
+ if (policy === "none") {
168
+ return {
169
+ name: "dmarc",
170
+ status: "warn",
171
+ message: "DMARC record found with p=none (monitoring only) \u2014 consider upgrading to p=quarantine or p=reject.",
172
+ record: dmarcRecord
173
+ };
174
+ }
175
+ return {
176
+ name: "dmarc",
177
+ status: "pass",
178
+ message: `DMARC record found with p=${policy || "unknown"}.`,
179
+ record: dmarcRecord
180
+ };
181
+ }
182
+ async function checkBIMI(domain, dns) {
183
+ const records = await resolveTxtSafe(`default._bimi.${domain}`, dns);
184
+ const bimiRecord = records.find((r) => r.startsWith("v=BIMI1"));
185
+ if (!bimiRecord) {
186
+ return {
187
+ name: "bimi",
188
+ status: "skip",
189
+ message: "No BIMI record found \u2014 optional brand indicator (logo in inbox).",
190
+ detail: "BIMI is a nice-to-have. It displays your brand logo next to emails in supported clients."
191
+ };
192
+ }
193
+ return {
194
+ name: "bimi",
195
+ status: "pass",
196
+ message: "BIMI record found \u2014 your brand logo may appear in supported email clients.",
197
+ record: bimiRecord
198
+ };
199
+ }
200
+ async function checkDeliverability(domain, options) {
201
+ var _a;
202
+ const dns = (_a = options == null ? void 0 : options._resolver) != null ? _a : defaultDns;
203
+ const cleanDomain = domain.trim().toLowerCase().replace(/^https?:\/\//, "").replace(/\/.*$/, "").replace(/:\d+$/, "");
204
+ if (!cleanDomain || !cleanDomain.includes(".")) {
205
+ return {
206
+ domain: cleanDomain,
207
+ checks: [],
208
+ score: 0,
209
+ issues: [{
210
+ rule: "invalid-domain",
211
+ severity: "error",
212
+ message: `Invalid domain: "${cleanDomain}"`
213
+ }]
214
+ };
215
+ }
216
+ const checkNames = ["mx", "spf", "dkim", "dmarc", "bimi"];
217
+ const settled = await Promise.allSettled([
218
+ checkMX(cleanDomain, dns),
219
+ checkSPF(cleanDomain, dns),
220
+ checkDKIM(cleanDomain, dns),
221
+ checkDMARC(cleanDomain, dns),
222
+ checkBIMI(cleanDomain, dns)
223
+ ]);
224
+ const checks = settled.map(
225
+ (result, i) => {
226
+ var _a2, _b;
227
+ return result.status === "fulfilled" ? result.value : { name: checkNames[i], status: "skip", message: `Check failed: ${(_b = (_a2 = result.reason) == null ? void 0 : _a2.message) != null ? _b : "unknown error"}` };
228
+ }
229
+ );
230
+ const weights = {
231
+ mx: 25,
232
+ spf: 25,
233
+ dkim: 20,
234
+ dmarc: 20,
235
+ bimi: 10
236
+ };
237
+ let totalWeight = 0;
238
+ let earnedScore = 0;
239
+ for (const check of checks) {
240
+ const weight = weights[check.name] || 0;
241
+ if (check.status === "skip") continue;
242
+ totalWeight += weight;
243
+ if (check.status === "pass") earnedScore += weight;
244
+ else if (check.status === "warn") earnedScore += weight * 0.5;
245
+ }
246
+ let score = totalWeight > 0 ? Math.round(earnedScore / totalWeight * 100) : 0;
247
+ score = Math.max(0, Math.min(100, score));
248
+ const issues = [];
249
+ for (const check of checks) {
250
+ if (check.status === "fail") {
251
+ issues.push({
252
+ rule: check.name,
253
+ severity: "error",
254
+ message: check.message,
255
+ detail: check.detail
256
+ });
257
+ } else if (check.status === "warn") {
258
+ issues.push({
259
+ rule: check.name,
260
+ severity: "warning",
261
+ message: check.message,
262
+ detail: check.detail
263
+ });
264
+ }
265
+ }
266
+ return { domain: cleanDomain, checks, score, issues };
267
+ }
268
+
269
+ // src/spamassassin.ts
270
+ import { execFile as nodeExecFile } from "child_process";
271
+ async function checkSpamAssassin(rawMessage, options) {
272
+ var _a;
273
+ const timeout = (_a = options == null ? void 0 : options.timeoutMs) != null ? _a : 3e4;
274
+ const spamcResult = await tryExecFile("spamc", ["-R"], rawMessage, timeout);
275
+ if (spamcResult !== null) return parseOutput(spamcResult);
276
+ const saResult = await tryExecFile("spamassassin", ["-t"], rawMessage, timeout);
277
+ if (saResult !== null) return parseOutput(saResult);
278
+ return null;
279
+ }
280
+ function tryExecFile(cmd, args, input, timeout) {
281
+ return new Promise((resolve) => {
282
+ try {
283
+ const child = nodeExecFile(cmd, args, {
284
+ timeout,
285
+ maxBuffer: 1024 * 1024,
286
+ encoding: "utf-8"
287
+ }, (error, stdout, stderr) => {
288
+ if (error) {
289
+ if (error.code === "ENOENT") {
290
+ resolve(null);
291
+ } else {
292
+ resolve(stdout || stderr || null);
293
+ }
294
+ } else {
295
+ resolve(stdout);
296
+ }
297
+ });
298
+ if (child.stdin) {
299
+ child.stdin.on("error", () => {
300
+ });
301
+ child.stdin.write(input);
302
+ child.stdin.end();
303
+ }
304
+ } catch (e) {
305
+ resolve(null);
306
+ }
307
+ });
308
+ }
309
+ function parseOutput(output) {
310
+ let score = 0;
311
+ let threshold = 5;
312
+ let isSpam = false;
313
+ const rules = [];
314
+ const statusMatch = output.match(/X-Spam-Status:\s*(Yes|No),\s*score=([\d.-]+)\s+required=([\d.-]+)/i);
315
+ if (statusMatch) {
316
+ isSpam = statusMatch[1].toLowerCase() === "yes";
317
+ score = parseFloat(statusMatch[2]);
318
+ threshold = parseFloat(statusMatch[3]);
319
+ } else {
320
+ const scoreMatch = output.match(/^([\d.-]+)\/([\d.-]+)/m);
321
+ if (scoreMatch) {
322
+ score = parseFloat(scoreMatch[1]);
323
+ threshold = parseFloat(scoreMatch[2]);
324
+ isSpam = score >= threshold;
325
+ }
326
+ }
327
+ const rulePattern = /^\s*([\d.-]+)\s+([A-Z_][A-Z0-9_]+)\s+(.+)$/gm;
328
+ let match;
329
+ while ((match = rulePattern.exec(output)) !== null) {
330
+ rules.push({
331
+ score: parseFloat(match[1]),
332
+ name: match[2],
333
+ description: match[3].trim()
334
+ });
335
+ }
336
+ return { score, threshold, isSpam, rules, rawOutput: output };
337
+ }
338
+ export {
339
+ checkDeliverability,
340
+ checkSpamAssassin
341
+ };
342
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/deliverability-checker.ts","../src/spamassassin.ts"],"sourcesContent":["/**\r\n * DNS-based email deliverability checker.\r\n *\r\n * Validates SPF, DKIM, DMARC, MX, and BIMI records for a domain\r\n * using node:dns/promises. No external dependencies.\r\n */\r\n\r\nimport { promises as defaultDns } from \"node:dns\";\r\nimport type { DeliverabilityCheck, DeliverabilityReport, DeliverabilityIssue } from \"./types\";\r\n\r\n/** DNS resolver interface — injectable for testing. */\r\nexport interface DnsResolver {\r\n resolveMx: typeof defaultDns.resolveMx;\r\n resolveTxt: typeof defaultDns.resolveTxt;\r\n}\r\n\r\n/** Common DKIM selectors to probe (we can't know the real selector). */\r\nconst DKIM_SELECTORS = [\r\n \"google\", \"selector1\", \"selector2\", \"default\", \"dkim\",\r\n \"k1\", \"k2\", \"k3\", \"mail\", \"s1\", \"s2\",\r\n \"mandrill\", \"pm\", \"protonmail\", \"smtp\",\r\n];\r\n\r\nconst DNS_TIMEOUT_MS = 5_000;\r\n\r\n/**\r\n * Race a DNS call against a timeout. Node's DNS API has no signal support,\r\n * so we use Promise.race — the underlying query may continue in the background\r\n * but we stop waiting.\r\n */\r\nasync function withTimeout<T>(\r\n fn: () => Promise<T>,\r\n timeoutMs = DNS_TIMEOUT_MS,\r\n): Promise<T> {\r\n let timer: ReturnType<typeof setTimeout> | undefined;\r\n const timeout = new Promise<never>((_, reject) => {\r\n timer = setTimeout(() => {\r\n const err = Object.assign(new Error(`DNS timeout after ${timeoutMs}ms`), { code: \"ABORT_ERR\" });\r\n reject(err);\r\n }, timeoutMs);\r\n });\r\n try {\r\n return await Promise.race([fn(), timeout]);\r\n } finally {\r\n clearTimeout(timer);\r\n }\r\n}\r\n\r\n/**\r\n * Safe DNS TXT lookup — returns empty array on ENOTFOUND/ENODATA.\r\n */\r\nasync function resolveTxtSafe(domain: string, dns: DnsResolver): Promise<string[]> {\r\n try {\r\n const records = await withTimeout(() => dns.resolveTxt(domain));\r\n return records.map((chunks) => chunks.join(\"\"));\r\n } catch (err: unknown) {\r\n const code = (err as NodeJS.ErrnoException).code;\r\n if (code === \"ENOTFOUND\" || code === \"ENODATA\" || code === \"ETIMEOUT\" || code === \"ABORT_ERR\"\r\n || code === \"ESERVFAIL\" || code === \"ECONNREFUSED\" || code === \"EAI_AGAIN\" || code === \"ECANCELLED\") {\r\n return [];\r\n }\r\n throw err;\r\n }\r\n}\r\n\r\n// ── Individual checks ────────────────────────────────────────────────────────\r\n\r\nasync function checkMX(domain: string, dns: DnsResolver): Promise<DeliverabilityCheck> {\r\n try {\r\n const records = await withTimeout(() => dns.resolveMx(domain));\r\n if (records.length === 0) {\r\n return {\r\n name: \"mx\",\r\n status: \"fail\",\r\n message: \"No MX records found — domain cannot receive email.\",\r\n };\r\n }\r\n const sorted = records.sort((a, b) => a.priority - b.priority);\r\n return {\r\n name: \"mx\",\r\n status: \"pass\",\r\n message: `${records.length} MX record(s) found.`,\r\n detail: sorted.map((r) => `${r.priority} ${r.exchange}`).join(\", \"),\r\n };\r\n } catch (err: unknown) {\r\n const code = (err as NodeJS.ErrnoException).code;\r\n if (code === \"ENOTFOUND\" || code === \"ENODATA\") {\r\n return {\r\n name: \"mx\",\r\n status: \"fail\",\r\n message: \"No MX records found — domain cannot receive email.\",\r\n };\r\n }\r\n // Gracefully handle all DNS failures (ESERVFAIL, ECONNREFUSED, timeouts, etc.)\r\n return {\r\n name: \"mx\",\r\n status: \"skip\",\r\n message: `MX lookup failed: ${(err as Error).message}`,\r\n };\r\n }\r\n}\r\n\r\nasync function checkSPF(domain: string, dns: DnsResolver): Promise<DeliverabilityCheck> {\r\n const records = await resolveTxtSafe(domain, dns);\r\n const spfRecord = records.find((r) => r.startsWith(\"v=spf1\"));\r\n\r\n if (!spfRecord) {\r\n return {\r\n name: \"spf\",\r\n status: \"fail\",\r\n message: \"No SPF record found — receiving servers can't verify authorized senders.\",\r\n detail: 'Add a TXT record starting with \"v=spf1\" to your domain.',\r\n };\r\n }\r\n\r\n // Detect dangerous +all (allows any server to send as your domain)\r\n if (spfRecord.includes(\"+all\")) {\r\n return {\r\n name: \"spf\",\r\n status: \"fail\",\r\n message: \"SPF record uses +all — this allows any server to send as your domain.\",\r\n record: spfRecord,\r\n detail: \"Replace +all with -all (hard fail) or ~all (soft fail) to restrict authorized senders.\",\r\n };\r\n }\r\n\r\n // Validate syntax\r\n if (!spfRecord.includes(\"~all\") && !spfRecord.includes(\"-all\") && !spfRecord.includes(\"?all\")) {\r\n return {\r\n name: \"spf\",\r\n status: \"warn\",\r\n message: \"SPF record found but missing enforcement mechanism (~all or -all).\",\r\n record: spfRecord,\r\n detail: \"Add ~all (soft fail) or -all (hard fail) to the end of your SPF record.\",\r\n };\r\n }\r\n\r\n if (spfRecord.includes(\"~all\")) {\r\n return {\r\n name: \"spf\",\r\n status: \"warn\",\r\n message: \"SPF record uses ~all (soft fail) — consider upgrading to -all (hard fail) for stronger protection.\",\r\n record: spfRecord,\r\n };\r\n }\r\n\r\n return {\r\n name: \"spf\",\r\n status: \"pass\",\r\n message: \"SPF record found with -all enforcement.\",\r\n record: spfRecord,\r\n };\r\n}\r\n\r\nasync function checkDKIM(domain: string, dns: DnsResolver): Promise<DeliverabilityCheck> {\r\n // Probe common selectors in parallel\r\n const results = await Promise.allSettled(\r\n DKIM_SELECTORS.map(async (selector) => {\r\n const records = await resolveTxtSafe(`${selector}._domainkey.${domain}`, dns);\r\n const dkimRecord = records.find((r) => r.includes(\"v=DKIM1\") || r.includes(\"p=\"));\r\n if (dkimRecord) {\r\n return { selector, record: dkimRecord };\r\n }\r\n return null;\r\n }),\r\n );\r\n\r\n const matches = results\r\n .filter((r): r is PromiseFulfilledResult<{ selector: string; record: string } | null> =>\r\n r.status === \"fulfilled\" && r.value !== null)\r\n .map((r) => r.value!);\r\n\r\n if (matches.length > 0) {\r\n return {\r\n name: \"dkim\",\r\n status: \"pass\",\r\n message: `DKIM record(s) found for selector(s): ${matches.map((m) => m.selector).join(\", \")}.`,\r\n record: matches[0].record,\r\n };\r\n }\r\n\r\n return {\r\n name: \"dkim\",\r\n status: \"warn\",\r\n message: `No DKIM records found for ${DKIM_SELECTORS.length} common selectors — DKIM may use a custom selector we didn't probe.`,\r\n detail: \"This doesn't mean DKIM is missing; the actual selector may differ from the common ones we checked.\",\r\n };\r\n}\r\n\r\nasync function checkDMARC(domain: string, dns: DnsResolver): Promise<DeliverabilityCheck> {\r\n const records = await resolveTxtSafe(`_dmarc.${domain}`, dns);\r\n const dmarcRecord = records.find((r) => r.startsWith(\"v=DMARC1\"));\r\n\r\n if (!dmarcRecord) {\r\n return {\r\n name: \"dmarc\",\r\n status: \"fail\",\r\n message: \"No DMARC record found — inbox providers may reject or quarantine your emails.\",\r\n detail: 'Add a TXT record at _dmarc.yourdomain.com starting with \"v=DMARC1\".',\r\n };\r\n }\r\n\r\n const policyMatch = dmarcRecord.match(/(?:^|;)\\s*p=(\\w+)/);\r\n const policy = policyMatch?.[1]?.toLowerCase();\r\n\r\n if (policy === \"none\") {\r\n return {\r\n name: \"dmarc\",\r\n status: \"warn\",\r\n message: 'DMARC record found with p=none (monitoring only) — consider upgrading to p=quarantine or p=reject.',\r\n record: dmarcRecord,\r\n };\r\n }\r\n\r\n return {\r\n name: \"dmarc\",\r\n status: \"pass\",\r\n message: `DMARC record found with p=${policy || \"unknown\"}.`,\r\n record: dmarcRecord,\r\n };\r\n}\r\n\r\nasync function checkBIMI(domain: string, dns: DnsResolver): Promise<DeliverabilityCheck> {\r\n const records = await resolveTxtSafe(`default._bimi.${domain}`, dns);\r\n const bimiRecord = records.find((r) => r.startsWith(\"v=BIMI1\"));\r\n\r\n if (!bimiRecord) {\r\n return {\r\n name: \"bimi\",\r\n status: \"skip\",\r\n message: \"No BIMI record found — optional brand indicator (logo in inbox).\",\r\n detail: \"BIMI is a nice-to-have. It displays your brand logo next to emails in supported clients.\",\r\n };\r\n }\r\n\r\n return {\r\n name: \"bimi\",\r\n status: \"pass\",\r\n message: \"BIMI record found — your brand logo may appear in supported email clients.\",\r\n record: bimiRecord,\r\n };\r\n}\r\n\r\n// ── Main API ─────────────────────────────────────────────────────────────────\r\n\r\n/**\r\n * Check email deliverability for a domain.\r\n *\r\n * Validates MX, SPF, DKIM, DMARC, and BIMI DNS records.\r\n * All DNS queries have a 5-second timeout.\r\n *\r\n * @example\r\n * ```ts\r\n * const report = await checkDeliverability(\"example.com\");\r\n * console.log(report.score); // 0-100\r\n * console.log(report.checks); // individual check results\r\n * ```\r\n */\r\nexport async function checkDeliverability(\r\n domain: string,\r\n options?: { _resolver?: DnsResolver },\r\n): Promise<DeliverabilityReport> {\r\n const dns: DnsResolver = options?._resolver ?? defaultDns;\r\n\r\n // Normalize domain — strip protocol, path, and port\r\n const cleanDomain = domain.trim().toLowerCase()\r\n .replace(/^https?:\\/\\//, \"\")\r\n .replace(/\\/.*$/, \"\")\r\n .replace(/:\\d+$/, \"\");\r\n\r\n if (!cleanDomain || !cleanDomain.includes(\".\")) {\r\n return {\r\n domain: cleanDomain,\r\n checks: [],\r\n score: 0,\r\n issues: [{\r\n rule: \"invalid-domain\",\r\n severity: \"error\",\r\n message: `Invalid domain: \"${cleanDomain}\"`,\r\n }],\r\n };\r\n }\r\n\r\n // Run all checks in parallel — use allSettled so one failure can't crash the rest\r\n const checkNames: DeliverabilityCheck[\"name\"][] = [\"mx\", \"spf\", \"dkim\", \"dmarc\", \"bimi\"];\r\n const settled = await Promise.allSettled([\r\n checkMX(cleanDomain, dns),\r\n checkSPF(cleanDomain, dns),\r\n checkDKIM(cleanDomain, dns),\r\n checkDMARC(cleanDomain, dns),\r\n checkBIMI(cleanDomain, dns),\r\n ]);\r\n\r\n const checks: DeliverabilityCheck[] = settled.map((result, i) =>\r\n result.status === \"fulfilled\"\r\n ? result.value\r\n : { name: checkNames[i], status: \"skip\" as const, message: `Check failed: ${result.reason?.message ?? \"unknown error\"}` },\r\n );\r\n\r\n // Calculate score — skip checks are excluded from the denominator\r\n const weights: Record<string, number> = {\r\n mx: 25,\r\n spf: 25,\r\n dkim: 20,\r\n dmarc: 20,\r\n bimi: 10,\r\n };\r\n\r\n let totalWeight = 0;\r\n let earnedScore = 0;\r\n for (const check of checks) {\r\n const weight = weights[check.name] || 0;\r\n if (check.status === \"skip\") continue; // exclude from denominator\r\n totalWeight += weight;\r\n if (check.status === \"pass\") earnedScore += weight;\r\n else if (check.status === \"warn\") earnedScore += weight * 0.5;\r\n // \"fail\" = 0 points\r\n }\r\n\r\n let score = totalWeight > 0 ? Math.round((earnedScore / totalWeight) * 100) : 0;\r\n score = Math.max(0, Math.min(100, score));\r\n\r\n // Build issues list\r\n const issues: DeliverabilityIssue[] = [];\r\n for (const check of checks) {\r\n if (check.status === \"fail\") {\r\n issues.push({\r\n rule: check.name,\r\n severity: \"error\",\r\n message: check.message,\r\n detail: check.detail,\r\n });\r\n } else if (check.status === \"warn\") {\r\n issues.push({\r\n rule: check.name,\r\n severity: \"warning\",\r\n message: check.message,\r\n detail: check.detail,\r\n });\r\n }\r\n }\r\n\r\n return { domain: cleanDomain, checks, score, issues };\r\n}\r\n","/**\r\n * Optional SpamAssassin integration.\r\n *\r\n * Shells out to `spamc` (daemon mode) or `spamassassin` (standalone mode)\r\n * using execFile (safe — no shell interpolation).\r\n * Returns null if neither is installed.\r\n *\r\n * Requires a full RFC 2822 message (headers + body), not just HTML.\r\n */\r\n\r\nimport { execFile as nodeExecFile } from \"node:child_process\";\r\n\r\nexport interface SpamAssassinResult {\r\n score: number;\r\n threshold: number;\r\n isSpam: boolean;\r\n rules: Array<{ name: string; score: number; description: string }>;\r\n rawOutput: string;\r\n}\r\n\r\nexport interface SpamAssassinOptions {\r\n /** Timeout in ms (default: 30000) */\r\n timeoutMs?: number;\r\n}\r\n\r\n/**\r\n * Run a raw RFC 2822 email message through SpamAssassin.\r\n *\r\n * Tries `spamc -R` (daemon mode, faster) first, then falls back to\r\n * `spamassassin -t` (standalone mode). Returns `null` if neither\r\n * binary is available.\r\n *\r\n * Uses execFile (not exec) — no shell interpolation, safe from injection.\r\n *\r\n * @example\r\n * ```ts\r\n * const result = await checkSpamAssassin(rawMessage);\r\n * if (result) {\r\n * console.log(`Score: ${result.score}/${result.threshold}`);\r\n * console.log(`Spam: ${result.isSpam}`);\r\n * for (const rule of result.rules) {\r\n * console.log(` ${rule.score} ${rule.name}: ${rule.description}`);\r\n * }\r\n * }\r\n * ```\r\n */\r\nexport async function checkSpamAssassin(\r\n rawMessage: string,\r\n options?: SpamAssassinOptions,\r\n): Promise<SpamAssassinResult | null> {\r\n const timeout = options?.timeoutMs ?? 30_000;\r\n\r\n // Try spamc first (daemon mode — faster)\r\n const spamcResult = await tryExecFile(\"spamc\", [\"-R\"], rawMessage, timeout);\r\n if (spamcResult !== null) return parseOutput(spamcResult);\r\n\r\n // Fall back to standalone spamassassin\r\n const saResult = await tryExecFile(\"spamassassin\", [\"-t\"], rawMessage, timeout);\r\n if (saResult !== null) return parseOutput(saResult);\r\n\r\n // Neither available\r\n return null;\r\n}\r\n\r\n/**\r\n * Safe wrapper around execFile — no shell, no interpolation.\r\n * Returns stdout on success, null if binary not found.\r\n */\r\nfunction tryExecFile(\r\n cmd: string,\r\n args: string[],\r\n input: string,\r\n timeout: number,\r\n): Promise<string | null> {\r\n return new Promise((resolve) => {\r\n try {\r\n const child = nodeExecFile(cmd, args, {\r\n timeout,\r\n maxBuffer: 1024 * 1024,\r\n encoding: \"utf-8\",\r\n }, (error, stdout, stderr) => {\r\n if (error) {\r\n if ((error as NodeJS.ErrnoException).code === \"ENOENT\") {\r\n resolve(null);\r\n } else {\r\n // SpamAssassin returns non-zero for spam — stdout is still valid\r\n resolve(stdout || stderr || null);\r\n }\r\n } else {\r\n resolve(stdout);\r\n }\r\n });\r\n\r\n if (child.stdin) {\r\n child.stdin.on(\"error\", () => {}); // swallow EPIPE if process dies before reading\r\n child.stdin.write(input);\r\n child.stdin.end();\r\n }\r\n } catch {\r\n resolve(null);\r\n }\r\n });\r\n}\r\n\r\nfunction parseOutput(output: string): SpamAssassinResult {\r\n let score = 0;\r\n let threshold = 5;\r\n let isSpam = false;\r\n const rules: SpamAssassinResult[\"rules\"] = [];\r\n\r\n // Parse X-Spam-Status line\r\n const statusMatch = output.match(/X-Spam-Status:\\s*(Yes|No),\\s*score=([\\d.-]+)\\s+required=([\\d.-]+)/i);\r\n if (statusMatch) {\r\n isSpam = statusMatch[1].toLowerCase() === \"yes\";\r\n score = parseFloat(statusMatch[2]);\r\n threshold = parseFloat(statusMatch[3]);\r\n } else {\r\n // spamc -R format: first line is \"score/threshold\"\r\n const scoreMatch = output.match(/^([\\d.-]+)\\/([\\d.-]+)/m);\r\n if (scoreMatch) {\r\n score = parseFloat(scoreMatch[1]);\r\n threshold = parseFloat(scoreMatch[2]);\r\n isSpam = score >= threshold;\r\n }\r\n }\r\n\r\n // Parse rules table\r\n const rulePattern = /^\\s*([\\d.-]+)\\s+([A-Z_][A-Z0-9_]+)\\s+(.+)$/gm;\r\n let match;\r\n while ((match = rulePattern.exec(output)) !== null) {\r\n rules.push({\r\n score: parseFloat(match[1]),\r\n name: match[2],\r\n description: match[3].trim(),\r\n });\r\n }\r\n\r\n return { score, threshold, isSpam, rules, rawOutput: output };\r\n}\r\n"],"mappings":"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","names":["_a"]}
@@ -277,60 +277,24 @@ interface TemplateReport {
277
277
  unresolvedCount: number;
278
278
  issues: TemplateIssue[];
279
279
  }
280
-
281
- /**
282
- * Unified error class for all email compilation failures.
283
- *
284
- * Replaces the per-format error classes (ReactEmailCompileError,
285
- * MjmlCompileError, MaizzleCompileError) with a single class that
286
- * carries the source format and failure phase.
287
- */
288
- declare class CompileError extends Error {
289
- name: string;
290
- readonly format: Exclude<InputFormat, "html">;
291
- readonly phase: "validation" | "transpile" | "execution" | "render" | "compile";
292
- constructor(message: string, format: CompileError["format"], phase: CompileError["phase"]);
280
+ interface DeliverabilityCheck {
281
+ name: "spf" | "dkim" | "dmarc" | "mx" | "bimi";
282
+ status: "pass" | "fail" | "warn" | "skip";
283
+ message: string;
284
+ detail?: string;
285
+ record?: string;
293
286
  }
294
-
295
- /**
296
- * Sandbox strategy for JSX execution.
297
- *
298
- * - `"vm"` — `node:vm` with hardened globals. Fast, zero-dependency,
299
- * but NOT a true security boundary (prototype-chain escapes are possible).
300
- * Suitable for CLI / local use where users run their own code.
301
- *
302
- * - `"isolated-vm"` (default) — Separate V8 isolate via the `isolated-vm`
303
- * npm package. True heap isolation; escapes require a V8 engine bug.
304
- * Requires `isolated-vm` to be installed (native addon).
305
- *
306
- * - `"quickjs"` — Validates code structure in a QuickJS WASM sandbox, then
307
- * executes in `node:vm` for React rendering. Security is equivalent to
308
- * `node:vm` — the QuickJS phase validates import restrictions only.
309
- * No native addons needed, but only supports ES2020 and is slower.
310
- * For true isolation on servers, use `isolated-vm`.
311
- */
312
- type SandboxStrategy = "vm" | "isolated-vm" | "quickjs";
313
- interface CompileReactEmailOptions {
314
- /**
315
- * Sandbox strategy to use for executing user JSX code.
316
- * @default "isolated-vm"
317
- */
318
- sandbox?: SandboxStrategy;
287
+ interface DeliverabilityReport {
288
+ domain: string;
289
+ checks: DeliverabilityCheck[];
290
+ score: number;
291
+ issues: DeliverabilityIssue[];
292
+ }
293
+ interface DeliverabilityIssue {
294
+ rule: string;
295
+ severity: "error" | "warning" | "info";
296
+ message: string;
297
+ detail?: string;
319
298
  }
320
- /**
321
- * Compile a React Email JSX/TSX source string into an HTML email string.
322
- *
323
- * Pipeline:
324
- * 1. Validate input (size, basic checks)
325
- * 2. Transpile JSX/TSX → CommonJS JS using sucrase
326
- * 3. Execute inside a sandbox (configurable strategy)
327
- * 4. Render the exported component to a full HTML email string
328
- *
329
- * Requires peer dependencies: sucrase, react, @react-email/components,
330
- * @react-email/render. Additionally:
331
- * - sandbox "isolated-vm" requires `isolated-vm`
332
- * - sandbox "quickjs" requires `quickjs-emscripten`
333
- */
334
- declare function compileReactEmail(source: string, options?: CompileReactEmailOptions): Promise<string>;
335
299
 
336
- export { type AiProvider as A, type TokenEstimateWithWarnings as B, type CSSWarning as C, type DiffResult as D, type EmailClient as E, type Framework as F, estimateAiFixTokens as G, generateFixPrompt as H, type ImageReport as I, heuristicTokenCount as J, compileReactEmail as K, type LinkReport as L, type PreviewResult as P, type SpamAnalysisOptions as S, type TransformResult as T, type CodeFix as a, type ExportPromptOptions as b, type AiFixResult as c, type SpamReport as d, type AccessibilityReport as e, type InboxPreview as f, type SizeReport as g, type TemplateReport as h, type AccessibilityIssue as i, type ClientTruncation as j, CompileError as k, type CompileReactEmailOptions as l, type EstimateOptions as m, type ExportScope as n, type FixType as o, type ImageInfo as p, type ImageIssue as q, type InboxPreviewIssue as r, type InputFormat as s, type LinkIssue as t, type SandboxStrategy as u, type SizeIssue as v, type SpamIssue as w, type SupportLevel as x, type TemplateIssue as y, type TokenEstimate as z };
300
+ export { type AiProvider as A, type TokenEstimateWithWarnings as B, type CSSWarning as C, type DiffResult as D, type EmailClient as E, type Framework as F, estimateAiFixTokens as G, generateFixPrompt as H, type ImageReport as I, heuristicTokenCount as J, type LinkReport as L, type PreviewResult as P, type SupportLevel as S, type TransformResult as T, type CodeFix as a, type ExportPromptOptions as b, type AiFixResult as c, type SpamAnalysisOptions as d, type SpamReport as e, type AccessibilityReport as f, type InboxPreview as g, type SizeReport as h, type TemplateReport as i, type DeliverabilityReport as j, type AccessibilityIssue as k, type ClientTruncation as l, type DeliverabilityCheck as m, type DeliverabilityIssue as n, type EstimateOptions as o, type ExportScope as p, type FixType as q, type ImageInfo as r, type ImageIssue as s, type InboxPreviewIssue as t, type InputFormat as u, type LinkIssue as v, type SizeIssue as w, type SpamIssue as x, type TemplateIssue as y, type TokenEstimate as z };
@@ -277,60 +277,24 @@ interface TemplateReport {
277
277
  unresolvedCount: number;
278
278
  issues: TemplateIssue[];
279
279
  }
280
-
281
- /**
282
- * Unified error class for all email compilation failures.
283
- *
284
- * Replaces the per-format error classes (ReactEmailCompileError,
285
- * MjmlCompileError, MaizzleCompileError) with a single class that
286
- * carries the source format and failure phase.
287
- */
288
- declare class CompileError extends Error {
289
- name: string;
290
- readonly format: Exclude<InputFormat, "html">;
291
- readonly phase: "validation" | "transpile" | "execution" | "render" | "compile";
292
- constructor(message: string, format: CompileError["format"], phase: CompileError["phase"]);
280
+ interface DeliverabilityCheck {
281
+ name: "spf" | "dkim" | "dmarc" | "mx" | "bimi";
282
+ status: "pass" | "fail" | "warn" | "skip";
283
+ message: string;
284
+ detail?: string;
285
+ record?: string;
293
286
  }
294
-
295
- /**
296
- * Sandbox strategy for JSX execution.
297
- *
298
- * - `"vm"` — `node:vm` with hardened globals. Fast, zero-dependency,
299
- * but NOT a true security boundary (prototype-chain escapes are possible).
300
- * Suitable for CLI / local use where users run their own code.
301
- *
302
- * - `"isolated-vm"` (default) — Separate V8 isolate via the `isolated-vm`
303
- * npm package. True heap isolation; escapes require a V8 engine bug.
304
- * Requires `isolated-vm` to be installed (native addon).
305
- *
306
- * - `"quickjs"` — Validates code structure in a QuickJS WASM sandbox, then
307
- * executes in `node:vm` for React rendering. Security is equivalent to
308
- * `node:vm` — the QuickJS phase validates import restrictions only.
309
- * No native addons needed, but only supports ES2020 and is slower.
310
- * For true isolation on servers, use `isolated-vm`.
311
- */
312
- type SandboxStrategy = "vm" | "isolated-vm" | "quickjs";
313
- interface CompileReactEmailOptions {
314
- /**
315
- * Sandbox strategy to use for executing user JSX code.
316
- * @default "isolated-vm"
317
- */
318
- sandbox?: SandboxStrategy;
287
+ interface DeliverabilityReport {
288
+ domain: string;
289
+ checks: DeliverabilityCheck[];
290
+ score: number;
291
+ issues: DeliverabilityIssue[];
292
+ }
293
+ interface DeliverabilityIssue {
294
+ rule: string;
295
+ severity: "error" | "warning" | "info";
296
+ message: string;
297
+ detail?: string;
319
298
  }
320
- /**
321
- * Compile a React Email JSX/TSX source string into an HTML email string.
322
- *
323
- * Pipeline:
324
- * 1. Validate input (size, basic checks)
325
- * 2. Transpile JSX/TSX → CommonJS JS using sucrase
326
- * 3. Execute inside a sandbox (configurable strategy)
327
- * 4. Render the exported component to a full HTML email string
328
- *
329
- * Requires peer dependencies: sucrase, react, @react-email/components,
330
- * @react-email/render. Additionally:
331
- * - sandbox "isolated-vm" requires `isolated-vm`
332
- * - sandbox "quickjs" requires `quickjs-emscripten`
333
- */
334
- declare function compileReactEmail(source: string, options?: CompileReactEmailOptions): Promise<string>;
335
299
 
336
- export { type AiProvider as A, type TokenEstimateWithWarnings as B, type CSSWarning as C, type DiffResult as D, type EmailClient as E, type Framework as F, estimateAiFixTokens as G, generateFixPrompt as H, type ImageReport as I, heuristicTokenCount as J, compileReactEmail as K, type LinkReport as L, type PreviewResult as P, type SpamAnalysisOptions as S, type TransformResult as T, type CodeFix as a, type ExportPromptOptions as b, type AiFixResult as c, type SpamReport as d, type AccessibilityReport as e, type InboxPreview as f, type SizeReport as g, type TemplateReport as h, type AccessibilityIssue as i, type ClientTruncation as j, CompileError as k, type CompileReactEmailOptions as l, type EstimateOptions as m, type ExportScope as n, type FixType as o, type ImageInfo as p, type ImageIssue as q, type InboxPreviewIssue as r, type InputFormat as s, type LinkIssue as t, type SandboxStrategy as u, type SizeIssue as v, type SpamIssue as w, type SupportLevel as x, type TemplateIssue as y, type TokenEstimate as z };
300
+ export { type AiProvider as A, type TokenEstimateWithWarnings as B, type CSSWarning as C, type DiffResult as D, type EmailClient as E, type Framework as F, estimateAiFixTokens as G, generateFixPrompt as H, type ImageReport as I, heuristicTokenCount as J, type LinkReport as L, type PreviewResult as P, type SupportLevel as S, type TransformResult as T, type CodeFix as a, type ExportPromptOptions as b, type AiFixResult as c, type SpamAnalysisOptions as d, type SpamReport as e, type AccessibilityReport as f, type InboxPreview as g, type SizeReport as h, type TemplateReport as i, type DeliverabilityReport as j, type AccessibilityIssue as k, type ClientTruncation as l, type DeliverabilityCheck as m, type DeliverabilityIssue as n, type EstimateOptions as o, type ExportScope as p, type FixType as q, type ImageInfo as r, type ImageIssue as s, type InboxPreviewIssue as t, type InputFormat as u, type LinkIssue as v, type SizeIssue as w, type SpamIssue as x, type TemplateIssue as y, type TokenEstimate as z };