← back to Email Deliverability Agent

audit.js

117 lines

#!/usr/bin/env node
'use strict';

/**
 * audit.js — Purelymail domain deliverability audit.
 *
 * Reads the Purelymail MCP snapshot fixtures, evaluates every domain, and
 * flags:
 *   - domains failing MX        (cannot physically receive email)
 *   - domains failing DKIM      (sends look unauthenticated -> spam)
 *   - domains failing DMARC     (no alignment policy -> spam / spoofable)
 *   - domains with no routing   (MX may pass but mail goes nowhere)
 *
 * Writes output/domain-health.json and prints a human summary.
 *
 *   node audit.js
 */

const fs = require('fs');
const path = require('path');
const {
  loadDomainSnapshot,
  loadRoutingRules,
  domainHealth,
  ensureOutputDir,
} = require('./lib/data');

function daysSince(iso) {
  if (!iso) return null;
  const ms = Date.now() - new Date(iso).getTime();
  return Number.isFinite(ms) ? Math.floor(ms / 86400000) : null;
}

function main() {
  const snapshot = loadDomainSnapshot();
  const routing = loadRoutingRules();
  const health = domainHealth(snapshot, routing.rules);

  const failMx = health.filter((h) => !h.passesMx);
  const failDkim = health.filter((h) => !h.passesDkim);
  const failDmarc = health.filter((h) => !h.passesDmarc);
  const failSpf = health.filter((h) => !h.passesSpf);
  const noRouting = health.filter((h) => !h.hasRouting);
  const undeliverable = health.filter((h) => !h.deliverable);
  const healthy = health.filter(
    (h) => h.deliverable && h.passesDkim && h.passesDmarc
  );

  const report = {
    generatedAt: new Date().toISOString(),
    snapshotCapturedAt: snapshot.capturedAt || null,
    routingCapturedAt: routing.capturedAt || null,
    totals: {
      domains: health.length,
      failMx: failMx.length,
      failDkim: failDkim.length,
      failDmarc: failDmarc.length,
      failSpf: failSpf.length,
      noRouting: noRouting.length,
      undeliverable: undeliverable.length,
      fullyHealthy: healthy.length,
    },
    failMx: failMx.map((h) => h.domain),
    noRouting: noRouting.map((h) => h.domain),
    undeliverable: undeliverable.map((h) => ({
      domain: h.domain,
      issues: h.issues,
    })),
    domains: health,
  };

  const outDir = ensureOutputDir();
  const outFile = path.join(outDir, 'domain-health.json');
  fs.writeFileSync(outFile, JSON.stringify(report, null, 2));

  // ---- human summary ----
  const t = report.totals;
  console.log('\n=== Purelymail Domain Deliverability Audit ===');
  console.log(`snapshot captured : ${report.snapshotCapturedAt || 'unknown'}`);
  const stale = daysSince(report.snapshotCapturedAt);
  if (stale != null && stale > 7) {
    console.log(`  WARNING: snapshot is ${stale} days old — re-run the MCP tools.`);
  }
  console.log(`total domains     : ${t.domains}`);
  console.log(`fail MX           : ${t.failMx}  (cannot receive email at all)`);
  console.log(`no routing rule   : ${t.noRouting}  (mail accepted but routed nowhere)`);
  console.log(`UNDELIVERABLE     : ${t.undeliverable}  (fail MX OR no routing)`);
  console.log(`fail DKIM         : ${t.failDkim}  (sends unauthenticated)`);
  console.log(`fail DMARC        : ${t.failDmarc}  (no alignment policy)`);
  console.log(`fail SPF          : ${t.failSpf}`);
  console.log(`fully healthy     : ${t.fullyHealthy}  (MX+routing+DKIM+DMARC all pass)`);

  if (failMx.length) {
    console.log('\n--- Domains FAILING MX (top 30) ---');
    failMx.slice(0, 30).forEach((h) => console.log(`  ${h.domain}`));
    if (failMx.length > 30) console.log(`  ...and ${failMx.length - 30} more`);
  }
  if (noRouting.length) {
    console.log('\n--- Domains with NO routing rule ---');
    noRouting.forEach((h) => console.log(`  ${h.domain}`));
  }

  console.log(`\nWrote ${outFile}`);
  console.log('Next: run `node contact-audit.js` to find sister sites losing leads.\n');
}

if (require.main === module) {
  try {
    main();
  } catch (err) {
    console.error('audit.js failed:', err.message);
    process.exit(1);
  }
}

module.exports = { main };