← back to Email Deliverability Agent

lib/data.js

112 lines

'use strict';

/**
 * Shared data loaders for the email-deliverability-agent.
 *
 * The Purelymail MCP tools cannot be called directly from a plain Node
 * script, so the agent reads two fixture files captured from the MCP:
 *
 *   data/purelymail-snapshot.json  <- mcp__purelymail__purelymail_list_domains
 *   data/purelymail-routing.json   <- mcp__purelymail__purelymail_list_routing_rules
 *
 * To refresh them, re-run those MCP tools and overwrite the JSON files
 * (see README "Refreshing the snapshot"). Each fixture records capturedAt
 * so audit.js can warn when the data is stale.
 */

const fs = require('fs');
const path = require('path');

const ROOT = path.resolve(__dirname, '..');
const DATA_DIR = path.join(ROOT, 'data');
const OUTPUT_DIR = path.join(ROOT, 'output');

function readJson(file) {
  const full = path.join(DATA_DIR, file);
  if (!fs.existsSync(full)) {
    throw new Error(
      `Missing fixture: ${full}\n` +
      `Re-run the Purelymail MCP tools and save the output here. See README.`
    );
  }
  return JSON.parse(fs.readFileSync(full, 'utf8'));
}

/** Returns { capturedAt, domains: [{ name, dnsSummary: {passesMx,...} }] } */
function loadDomainSnapshot() {
  const snap = readJson('purelymail-snapshot.json');
  if (!Array.isArray(snap.domains)) {
    throw new Error('purelymail-snapshot.json: expected a "domains" array');
  }
  return snap;
}

/** Returns { capturedAt, rules: [{ domainName, matchUser, catchall, targetAddresses }] } */
function loadRoutingRules() {
  const r = readJson('purelymail-routing.json');
  if (!Array.isArray(r.rules)) {
    throw new Error('purelymail-routing.json: expected a "rules" array');
  }
  return r;
}

/** Index routing rules by domain -> array of rules. */
function routingByDomain(rules) {
  const map = new Map();
  for (const rule of rules) {
    const d = (rule.domainName || '').toLowerCase();
    if (!map.has(d)) map.set(d, []);
    map.get(d).push(rule);
  }
  return map;
}

/**
 * Per-domain health verdict from the snapshot + routing data.
 * A domain is "deliverable" only when MX passes AND at least one routing
 * rule exists to actually deliver the mail somewhere.
 */
function domainHealth(snapshot, rules) {
  const byDomain = routingByDomain(rules);
  return snapshot.domains.map((d) => {
    const dns = d.dnsSummary || {};
    const domainRules = byDomain.get((d.name || '').toLowerCase()) || [];
    const hasRouting = domainRules.length > 0;
    const passesMx = dns.passesMx === true;
    const issues = [];
    if (!passesMx) issues.push('NO_MX');
    if (!hasRouting) issues.push('NO_ROUTING');
    if (dns.passesDkim !== true) issues.push('NO_DKIM');
    if (dns.passesDmarc !== true) issues.push('NO_DMARC');
    if (dns.passesSpf !== true) issues.push('NO_SPF');
    return {
      domain: d.name,
      passesMx,
      passesSpf: dns.passesSpf === true,
      passesDkim: dns.passesDkim === true,
      passesDmarc: dns.passesDmarc === true,
      hasRouting,
      routingRuleCount: domainRules.length,
      // The hard gate: can a lead emailed here actually be received & routed?
      deliverable: passesMx && hasRouting,
      issues,
    };
  });
}

function ensureOutputDir() {
  if (!fs.existsSync(OUTPUT_DIR)) fs.mkdirSync(OUTPUT_DIR, { recursive: true });
  return OUTPUT_DIR;
}

module.exports = {
  ROOT,
  DATA_DIR,
  OUTPUT_DIR,
  loadDomainSnapshot,
  loadRoutingRules,
  routingByDomain,
  domainHealth,
  ensureOutputDir,
};