Skip to content

DKIM / SPF / DMARC Builder & Checker — TypeScript source

Build the three email-authentication DNS records — SPF, DKIM, and DMARC — as ready-to-paste TXT values, or look up a domain's live records over DNS-over-HTTPS. Import existing records, catch publishing mistakes, share via URL. Entirely client-side.

This is the TypeScript implementation — the same logic the interactive tool runs, in a shareable, citable form.

// Email authentication (SPF / DKIM / DMARC) record parsing and lookup.
//
// Two layers, mirroring src/lib/whois.ts:
//   - Pure parsers (parseSPF / parseDKIM / parseDMARC) — deterministic, never
//     throw, unit-tested without network.
//   - Check functions (checkSPF / checkDKIM / checkDMARC) — one DNS-over-HTTPS
//     fetch to Cloudflare's public resolver from the browser, then a pure
//     parse. CosmoDev runs no backend for this tool.
//
// Specs: SPF RFC 7208, DKIM RFC 6376, DMARC RFC 7489.

// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------

export type CheckStatus = 'pass' | 'warn' | 'fail';

export type SPFQualifier = '+' | '-' | '~' | '?';

export interface SPFMechanism {
  qualifier: SPFQualifier;
  kind: 'all' | 'include' | 'a' | 'mx' | 'ip4' | 'ip6' | 'exists' | 'ptr';
  /** Domain, IP, or CIDR argument after the colon (e.g. `_spf.google.com`, `192.0.2.0/24`). */
  value?: string;
}

export interface SPFRecord {
  valid: boolean;
  version?: string;
  mechanisms: SPFMechanism[];
  redirect?: string;
  exp?: string;
  /** Mechanisms that cost a DNS query under RFC 7208 §4.6.4 (limit 10). */
  lookupCount: number;
  /** Number of `v=spf1` records found (more than one is a hard error for receivers). */
  recordCount: number;
  warnings: string[];
}

export interface DKIMRecord {
  valid: boolean;
  version?: string;
  keyType: string;
  /** Public key, base64, whitespace-stripped. */
  publicKey?: string;
  /** Approximate modulus size in bits (RSA only; derived from DER byte length). */
  keyBits?: number;
  hashes: string[];
  services: string[];
  flags: string[];
  warnings: string[];
}

export type DMARCPolicy = 'none' | 'quarantine' | 'reject';

export interface DMARCRecord {
  valid: boolean;
  policy?: DMARCPolicy;
  subdomainPolicy?: DMARCPolicy;
  aggregateUris: string[];
  forensicUris: string[];
  percent?: number;
  dkimAlignment?: 'r' | 's';
  spfAlignment?: 'r' | 's';
  warnings: string[];
}

export interface SPFResult {
  status: CheckStatus;
  found: boolean;
  record?: SPFRecord;
  message?: string;
}

export interface DKIMResult {
  status: CheckStatus;
  found: boolean;
  record?: DKIMRecord;
  message?: string;
}

export interface DMARCResult {
  status: CheckStatus;
  found: boolean;
  record?: DMARCRecord;
  message?: string;
}

// ---------------------------------------------------------------------------
// Shared helpers
// ---------------------------------------------------------------------------

/** Strip a leading scheme, userinfo, path, query, port, and trailing dot from user input. */
export function normalizeDomain(raw: string): string {
  let s = raw.trim();
  s = s.replace(/^[a-z][a-z0-9+.-]*:\/\//i, ''); // scheme://
  s = s.replace(/^mailto:/i, ''); // mailto:user@domain
  if (s.includes('@')) s = s.slice(s.lastIndexOf('@') + 1); // keep host of user@host
  s = s.split('/')[0]; // drop path
  s = s.split('?')[0]; // drop query
  s = s.split(':')[0]; // drop port
  s = s.replace(/\.+$/, ''); // trailing dot(s)
  return s.toLowerCase();
}

const DOMAIN_RE = /^(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z]{2,}$/;

/** True when the string looks like a plausible multi-label domain (example.com). */
export function isDomainLike(domain: string): boolean {
  return DOMAIN_RE.test(domain) && domain.length <= 253;
}

const SELECTOR_RE = /^[a-z0-9][a-z0-9._-]*$/i;

/** True when the selector is a safe single DNS label chain (no spaces, no traversal). */
export function isValidSelector(selector: string): boolean {
  const s = selector.trim();
  return s.length > 0 && s.length <= 100 && SELECTOR_RE.test(s) && !s.includes('..');
}

// ---------------------------------------------------------------------------
// SPF — RFC 7208
// ---------------------------------------------------------------------------

const SPF_LOOKUP_KINDS = new Set(['include', 'a', 'mx', 'exists', 'ptr']);

/** Parse one or more (newline-joined) TXT record strings for SPF. Pure. */
export function parseSPF(txt: string): SPFRecord {
  const lines = txt
    .split('\n')
    .map((l) => l.trim().replace(/^"(.*)"$/, '$1'))
    .filter(Boolean);
  const spfLines = lines.filter((l) => /^v=spf1(?:\s|$)/i.test(l));
  const warnings: string[] = [];

  if (spfLines.length === 0) {
    return { valid: false, mechanisms: [], lookupCount: 0, recordCount: 0, warnings: ['No v=spf1 record found in the supplied text.'] };
  }
  if (spfLines.length > 1) {
    warnings.push(`${spfLines.length} SPF records found — RFC 7208 allows exactly one. Receivers treat this as a permanent error and ignore SPF for the whole domain.`);
  }

  const terms = spfLines[0].split(/\s+/);
  const version = terms[0];
  const mechanisms: SPFMechanism[] = [];
  let redirect: string | undefined;
  let exp: string | undefined;

  for (const term of terms.slice(1)) {
    // Modifiers use '=': redirect= and exp=
    const modifier = /^([a-z][a-z0-9-]*)=(.*)$/i.exec(term);
    if (modifier) {
      const name = modifier[1].toLowerCase();
      if (name === 'redirect') redirect = modifier[2];
      else if (name === 'exp') exp = modifier[2];
      else warnings.push(`Unknown modifier "${term}" ignored.`);
      continue;
    }
    const m = /^([+\-~?])?([a-z0-9]+)(?::(.*))?$/i.exec(term);
    if (!m) {
      warnings.push(`Unrecognized term "${term}" ignored.`);
      continue;
    }
    const qualifier = (m[1] ?? '+') as SPFQualifier;
    const kind = m[2].toLowerCase();
    const value = m[3];
    if (!['all', 'include', 'a', 'mx', 'ip4', 'ip6', 'exists', 'ptr'].includes(kind)) {
      warnings.push(`Unknown mechanism "${term}" ignored.`);
      continue;
    }
    if (!value && (kind === 'include' || kind === 'exists')) {
      warnings.push(`Mechanism "${term}" is missing its required value.`);
      continue;
    }
    mechanisms.push({ qualifier, kind: kind as SPFMechanism['kind'], value });
  }

  const lookupCount = mechanisms.filter((mech) => SPF_LOOKUP_KINDS.has(mech.kind)).length + (redirect ? 1 : 0);

  const all = mechanisms.find((mech) => mech.kind === 'all');
  if (!all && !redirect) {
    warnings.push('No "all" mechanism and no "redirect=" — unmatched senders get a Neutral result, so anyone can still send mail that looks like this domain.');
  }
  if (all && all.qualifier === '+') {
    warnings.push('"+all" explicitly allows every host on the internet to send mail as this domain — this defeats SPF entirely.');
  } else if (all && all.qualifier === '?') {
    warnings.push('"?all" (Neutral) lets unmatched senders through with no protection. Prefer "~all" or "-all".');
  }
  if (mechanisms.some((mech) => mech.kind === 'ptr')) {
    warnings.push('The "ptr" mechanism is deprecated (RFC 7208 §5.5) and should not be used.');
  }
  if (redirect && all) {
    warnings.push('A "redirect=" modifier is ignored when an "all" mechanism is present.');
  }
  if (lookupCount > 10) {
    warnings.push(`${lookupCount} DNS-lookup mechanisms — RFC 7208 §4.6.4 caps SPF at 10. Receivers that hit the cap return permerror and ignore the record.`);
  }

  return { valid: true, version, mechanisms, redirect, exp, lookupCount, recordCount: spfLines.length, warnings };
}

// ---------------------------------------------------------------------------
// DKIM — RFC 6376
// ---------------------------------------------------------------------------

/** Decode base64 to a byte string without throwing on bad input. */
function decodeBase64Lenient(b64: string): string | undefined {
  // Callers pass trimmed, non-empty values (empty p= is handled before this).
  const clean = b64.replace(/\s+/g, '');
  const padded = clean + '='.repeat((4 - (clean.length % 4)) % 4);
  try {
    return atob(padded);
  } catch {
    return undefined;
  }
}

/** Parse a `<selector>._domainkey` TXT record. Pure. */
export function parseDKIM(txt: string): DKIMRecord {
  const warnings: string[] = [];
  const tags = new Map<string, string>();
  for (const part of txt.split(';')) {
    const term = part.trim().replace(/^"(.*)"$/, '$1').trim();
    if (!term) continue;
    const eq = term.indexOf('=');
    if (eq <= 0) {
      warnings.push(`Malformed tag "${term}" ignored.`);
      continue;
    }
    tags.set(term.slice(0, eq).trim().toLowerCase(), term.slice(eq + 1).trim());
  }

  const version = tags.get('v');
  if (version && version.toUpperCase() !== 'DKIM1') {
    warnings.push(`Unusual version tag v=${version} (expected DKIM1).`);
  }
  const keyType = tags.get('k') ?? 'rsa';
  const p = tags.get('p');
  const hashes = (tags.get('h') ?? '').split(':').map((s) => s.trim()).filter(Boolean);
  const services = (tags.get('s') ?? '').split(':').map((s) => s.trim()).filter(Boolean);
  const flags = (tags.get('t') ?? '').split(':').map((s) => s.trim()).filter(Boolean);

  const record: DKIMRecord = { valid: true, keyType, hashes, services, flags, warnings };
  if (version) record.version = version.toUpperCase();

  if (p === undefined) {
    record.valid = false;
    warnings.push('No p= tag — this record is not a usable DKIM key.');
    return record;
  }
  if (p === '') {
    warnings.push('p= is empty — the key is revoked. Receivers will treat mail signed with this selector as unsigned.');
    return record;
  }

  const decoded = decodeBase64Lenient(p);
  if (decoded === undefined) {
    warnings.push('The p= value is not valid base64 — the key could not be read.');
    return record;
  }
  record.publicKey = p.replace(/\s+/g, '');

  if (keyType === 'rsa') {
    // SubjectPublicKeyInfo DER ≈ modulus bits/8 + ~24 bytes of ASN.1 overhead.
    // The estimate is close enough to classify 512/1024/2048/4096-bit keys.
    const bits = Math.max(0, decoded.length - 24) * 8;
    record.keyBits = bits;
    if (bits < 1024) {
      warnings.push(`Weak RSA key (~${bits} bits). Keys under 1024 bits are considered breakable; RFC 8301 discourages short keys.`);
    } else if (bits < 2048) {
      warnings.push(`RSA key of ~${bits} bits works today but is below the recommended 2048 bits (RFC 8301).`);
    }
  }
  if (flags.includes('y')) {
    warnings.push('t=y — the key is in test mode: receivers must treat signatures as if unsigned.');
  }
  if (flags.includes('s')) {
    warnings.push('t=s — strict domain matching: the key cannot be used for subdomain signatures (informational).');
  }
  return record;
}

// ---------------------------------------------------------------------------
// DMARC — RFC 7489
// ---------------------------------------------------------------------------

const DMARC_POLICIES: DMARCPolicy[] = ['none', 'quarantine', 'reject'];

/** Parse a `_dmarc` TXT record. Pure. */
export function parseDMARC(txt: string): DMARCRecord {
  const warnings: string[] = [];
  const tags = new Map<string, string>();
  for (const part of txt.split(';')) {
    const term = part.trim().replace(/^"(.*)"$/, '$1').trim();
    if (!term) continue;
    const eq = term.indexOf('=');
    if (eq <= 0) {
      warnings.push(`Malformed tag "${term}" ignored.`);
      continue;
    }
    tags.set(term.slice(0, eq).trim().toLowerCase(), term.slice(eq + 1).trim());
  }

  const record: DMARCRecord = { valid: true, aggregateUris: [], forensicUris: [], warnings };
  const version = tags.get('v');

  if (!version) {
    record.valid = false;
    warnings.push('No v= tag — this is not a DMARC record.');
    return record;
  }
  if (version.toUpperCase() !== 'DMARC1') {
    record.valid = false;
    warnings.push(`Unknown version v=${version} (expected DMARC1).`);
    return record;
  }

  const p = tags.get('p')?.toLowerCase();
  if (!p) {
    record.valid = false;
    warnings.push('No p= policy tag — DMARC requires it.');
    return record;
  }
  if (!DMARC_POLICIES.includes(p as DMARCPolicy)) {
    record.valid = false;
    warnings.push(`Invalid policy p=${p} (expected none, quarantine, or reject).`);
    return record;
  }
  record.policy = p as DMARCPolicy;

  const sp = tags.get('sp')?.toLowerCase();
  if (sp) {
    if (DMARC_POLICIES.includes(sp as DMARCPolicy)) record.subdomainPolicy = sp as DMARCPolicy;
    else warnings.push(`Invalid sp=${sp} ignored (expected none, quarantine, or reject).`);
  }

  const rua = tags.get('rua');
  if (rua) record.aggregateUris = rua.split(',').map((u) => u.trim()).filter(Boolean);
  const ruf = tags.get('ruf');
  if (ruf) record.forensicUris = ruf.split(',').map((u) => u.trim()).filter(Boolean);

  const pct = tags.get('pct');
  if (pct !== undefined) {
    const n = Number(pct);
    if (!Number.isInteger(n) || n < 0 || n > 100) {
      warnings.push(`Invalid pct=${pct} ignored (must be 0-100).`);
    } else {
      record.percent = n;
    }
  }

  const adkim = tags.get('adkim');
  if (adkim) {
    if (adkim === 'r' || adkim === 's') record.dkimAlignment = adkim;
    else warnings.push(`Invalid adkim=${adkim} ignored (expected r or s).`);
  }
  const aspf = tags.get('aspf');
  if (aspf) {
    if (aspf === 'r' || aspf === 's') record.spfAlignment = aspf;
    else warnings.push(`Invalid aspf=${aspf} ignored (expected r or s).`);
  }

  // Policy guidance
  if (record.policy === 'none') {
    warnings.push('p=none is monitor-only — no mail is quarantined or rejected, but you still need SPF/DKIM to pass for reports to be useful.');
  }
  if (record.aggregateUris.length === 0) {
    warnings.push('No rua= address — without aggregate reports you cannot see who is failing authentication. Add rua=mailto:reports@example.com.');
  } else if (record.forensicUris.length > 0) {
    warnings.push('ruf= (forensic reports) is supported by few receivers and may leak message content to the report address (informational).');
  }
  if (record.percent !== undefined && record.percent < 100 && record.policy !== 'none') {
    warnings.push(`pct=${record.percent} applies the policy to only ${record.percent}% of mail — the other ${100 - record.percent}% is unaffected.`);
  }
  return record;
}

// ---------------------------------------------------------------------------
// DNS-over-HTTPS lookup
// ---------------------------------------------------------------------------

const DOH_ENDPOINT = 'https://cloudflare-dns.com/dns-query';

interface DohAnswer {
  name: string;
  type: number;
  TTL: number;
  data: string;
}

interface DohResponse {
  Status?: number;
  Answer?: DohAnswer[];
}

/** Query TXT records for a name via Cloudflare's DoH JSON API. Returns unquoted strings. */
async function queryTxt(name: string): Promise<string[]> {
  const res = await fetch(`${DOH_ENDPOINT}?name=${encodeURIComponent(name)}&type=TXT`, {
    headers: { Accept: 'application/dns-json' },
  });
  if (!res.ok) throw new Error(`DNS resolver responded with HTTP ${res.status}.`);
  const json = (await res.json()) as DohResponse;
  if (json.Status === 3) return []; // NXDOMAIN — no such domain
  if (json.Status !== 0) throw new Error(`DNS query failed with status ${json.Status}.`);
  return (json.Answer ?? [])
    .filter((a) => a.type === 16)
    .map((a) => a.data.replace(/^"(.*)"$/, '$1').replace(/" "/g, '')); // multi-chunk TXT: "part1" "part2"
}

function warnIf(f: boolean, message: string): string | undefined {
  return f ? message : undefined;
}

/** Look up and evaluate a domain's SPF record. */
export async function checkSPF(domain: string): Promise<SPFResult> {
  const host = normalizeDomain(domain);
  if (!isDomainLike(host)) return { status: 'fail', found: false, message: 'Enter a valid domain, e.g. example.com.' };
  try {
    const txts = await queryTxt(host);
    const joined = txts.join('\n');
    const record = parseSPF(joined);
    if (!record.valid) return { status: 'fail', found: false, message: `No SPF record found for ${host}. Receivers cannot verify which servers may send mail for it.` };
    const status: CheckStatus = record.warnings.length > 0 ? 'warn' : 'pass';
    return { status, found: true, record, message: warnIf(status === 'pass', 'SPF record found and looks healthy.') };
  } catch (err) {
    return { status: 'fail', found: false, message: err instanceof Error ? err.message : 'DNS lookup failed.' };
  }
}

/** Look up and evaluate a domain's DKIM public key for one selector. */
export async function checkDKIM(domain: string, selector: string): Promise<DKIMResult> {
  const host = normalizeDomain(domain);
  if (!isDomainLike(host)) return { status: 'fail', found: false, message: 'Enter a valid domain, e.g. example.com.' };
  const sel = selector.trim().toLowerCase();
  if (!isValidSelector(sel)) return { status: 'fail', found: false, message: 'Enter a valid selector (letters, digits, dots, hyphens, underscores).' };
  try {
    const txts = await queryTxt(`${sel}._domainkey.${host}`);
    const record = parseDKIM(txts.join('\n'));
    if (!record.valid) {
      return { status: 'fail', found: false, message: `No DKIM record found at ${sel}._domainkey.${host}. Try another selector — only one is checked per lookup.` };
    }
    const revoked = record.warnings.some((w) => w.includes('revoked'));
    const status: CheckStatus = revoked ? 'fail' : record.warnings.length > 0 ? 'warn' : 'pass';
    return { status, found: true, record };
  } catch (err) {
    return { status: 'fail', found: false, message: err instanceof Error ? err.message : 'DNS lookup failed.' };
  }
}

/** Look up and evaluate a domain's DMARC policy. */
export async function checkDMARC(domain: string): Promise<DMARCResult> {
  const host = normalizeDomain(domain);
  if (!isDomainLike(host)) return { status: 'fail', found: false, message: 'Enter a valid domain, e.g. example.com.' };
  try {
    const txts = await queryTxt(`_dmarc.${host}`);
    const record = parseDMARC(txts.join('\n'));
    if (!record.valid) {
      return { status: 'fail', found: false, message: `No DMARC record found at _dmarc.${host}. Receivers have no policy to apply when SPF or DKIM fails.` };
    }
    const status: CheckStatus = record.warnings.length > 0 ? 'warn' : 'pass';
    return { status, found: true, record };
  } catch (err) {
    return { status: 'fail', found: false, message: err instanceof Error ? err.message : 'DNS lookup failed.' };
  }
}

// ---------------------------------------------------------------------------
// Record builder — composes the three records as ready-to-paste TXT values
// ---------------------------------------------------------------------------

export interface SpfPart {
  enabled: boolean;
  /** Raw terms after "v=spf1" — mechanisms and modifiers, e.g. ['include:_spf.google.com', '~all']. */
  terms: string[];
}

export interface DkimPart {
  enabled: boolean;
  /** k= tag — 'rsa' or 'ed25519'. */
  keyType: string;
  /** p= tag — the base64 public key from the mail provider. */
  publicKey: string;
  /** h= tag values, e.g. ['sha256']. */
  hashes: string[];
  /** t=y test mode. */
  testMode: boolean;
}

export type DmarcPolicy = '' | 'none' | 'quarantine' | 'reject';

export interface DmarcPart {
  enabled: boolean;
  /** p= tag. '' means not chosen yet. */
  policy: DmarcPolicy;
  /** sp= tag. '' omits it (subdomains inherit p=). */
  subdomainPolicy: DmarcPolicy;
  /** rua= aggregate report URIs. */
  rua: string[];
  /** ruf= forensic report URIs. */
  ruf: string[];
  /** pct= tag; null omits it. */
  percent: number | null;
  dkimAlignment: 'r' | 's';
  spfAlignment: 'r' | 's';
}

export interface DnsRecordsConfig {
  domain: string;
  selector: string;
  dkim: DkimPart;
  spf: SpfPart;
  dmarc: DmarcPart;
}

export type DnsIssueCode =
  | 'selector-missing'
  | 'spf-multiple-records'
  | 'spf-all-missing'
  | 'dmarc-policy-missing'
  | 'domain-not-fqdn';

export interface DnsIssue {
  code: DnsIssueCode;
  severity: 'warn' | 'info';
  value?: string;
}

/** Fresh builder state: all three records on, Google-Workspace-shaped defaults. */
export function defaultDnsRecordsConfig(): DnsRecordsConfig {
  return {
    domain: 'example.com',
    selector: 'google',
    spf: { enabled: true, terms: ['include:_spf.google.com', '~all'] },
    dkim: { enabled: true, keyType: 'rsa', publicKey: '', hashes: ['sha256'], testMode: false },
    dmarc: {
      enabled: true,
      policy: 'none',
      subdomainPolicy: '',
      rua: ['mailto:dmarc@example.com'],
      ruf: [],
      percent: null,
      dkimAlignment: 'r',
      spfAlignment: 'r',
    },
  };
}

const cloneConfig = (c: DnsRecordsConfig): DnsRecordsConfig =>
  JSON.parse(JSON.stringify(c)) as DnsRecordsConfig;

/** SPF TXT value ("v=spf1 include:… ~all"). */
export function spfValue(part: SpfPart): string {
  return `v=spf1 ${part.terms.map((t) => t.trim()).filter(Boolean).join(' ')}`.trimEnd();
}

/** DKIM TXT value ("v=DKIM1; k=rsa; p=…"). */
export function dkimValue(part: DkimPart): string {
  const tags = ['v=DKIM1', `k=${part.keyType.trim() || 'rsa'}`];
  const hashes = part.hashes.map((h) => h.trim()).filter(Boolean);
  if (hashes.length > 0) tags.push(`h=${hashes.join(':')}`);
  if (part.testMode) tags.push('t=y');
  tags.push(`p=${part.publicKey.replace(/\s+/g, '')}`);
  return tags.join('; ');
}

/** DMARC TXT value ("v=DMARC1; p=…; rua=…"). */
export function dmarcValue(part: DmarcPart): string {
  const tags = ['v=DMARC1', `p=${part.policy || 'none'}`];
  if (part.subdomainPolicy) tags.push(`sp=${part.subdomainPolicy}`);
  if (part.rua.length > 0) tags.push(`rua=${part.rua.join(',')}`);
  if (part.ruf.length > 0) tags.push(`ruf=${part.ruf.join(',')}`);
  if (part.percent != null) tags.push(`pct=${part.percent}`);
  tags.push(`adkim=${part.dkimAlignment}`, `aspf=${part.spfAlignment}`);
  return tags.join('; ');
}

/**
 * Zone-file style output: every enabled record preceded by a comment naming
 * the TXT record's host, so the whole block can be pasted at a provider.
 * Never throws.
 */
export function generateDnsRecords(config: DnsRecordsConfig): string {
  const domain = normalizeDomain(config.domain) || 'example.com';
  const selector = config.selector.trim().toLowerCase() || 'default';
  const out: string[] = [
    `; Email authentication (SPF / DKIM / DMARC) records for ${domain}`,
    '; Publish each block as a TXT record at your DNS provider.',
    '',
  ];
  if (config.spf.enabled) {
    out.push(`; SPF — TXT at ${domain} (the zone apex; often written "@")`);
    out.push(`${domain}. IN TXT "${spfValue(config.spf)}"`, '');
  }
  if (config.dkim.enabled) {
    out.push(`; DKIM — TXT at ${selector}._domainkey.${domain}`);
    out.push(`${selector}._domainkey.${domain}. IN TXT "${dkimValue(config.dkim)}"`, '');
  }
  if (config.dmarc.enabled) {
    out.push(`; DMARC — TXT at _dmarc.${domain}`);
    out.push(`_dmarc.${domain}. IN TXT "${dmarcValue(config.dmarc)}"`, '');
  }
  return `${out.join('\n').trimEnd()}\n`;
}

/** Builder validation — the five classic publishing mistakes. */
export function validateDnsRecords(config: DnsRecordsConfig): DnsIssue[] {
  const issues: DnsIssue[] = [];
  const domain = config.domain.trim();
  if (domain !== '' && !isDomainLike(normalizeDomain(domain))) {
    issues.push({ code: 'domain-not-fqdn', severity: 'warn', value: domain });
  }
  if (config.spf.enabled) {
    // More than one "v=spf1" token means more than one SPF record was pasted
    // in — receivers treat that as a permanent error.
    const spfLines = spfValue(config.spf).split(/\s+/).filter((t) => /^v=spf1$/i.test(t)).length;
    if (spfLines > 1) issues.push({ code: 'spf-multiple-records', severity: 'warn', value: String(spfLines) });
    const rec = parseSPF(spfValue(config.spf));
    if (!rec.mechanisms.some((m) => m.kind === 'all') && !rec.redirect) {
      issues.push({ code: 'spf-all-missing', severity: 'warn' });
    }
  }
  if (config.dkim.enabled && config.selector.trim() === '') {
    issues.push({ code: 'selector-missing', severity: 'warn' });
  }
  if (config.dmarc.enabled && config.dmarc.policy === '') {
    issues.push({ code: 'dmarc-policy-missing', severity: 'warn' });
  }
  return issues;
}

export interface ParsedDnsRecords {
  config: DnsRecordsConfig;
  found: { spf: boolean; dkim: boolean; dmarc: boolean };
}

/** Strip a zone-file prefix (`name IN TXT "…"`) down to the record value itself. */
function txtPayload(line: string): string {
  const q = line.indexOf('"');
  if (q === -1) return line.trim();
  const end = line.lastIndexOf('"');
  return (end > q ? line.slice(q + 1, end) : line.slice(q + 1)).trim();
}

/** Record host of a zone line (`_dmarc.example.com.` → `_dmarc.example.com`); '' when the line is a bare value. */
function recordName(line: string): string {
  const q = line.indexOf('"');
  const head = (q === -1 ? line : line.slice(0, q)).trim();
  if (!/\s/.test(head)) return '';
  return head.split(/\s+/)[0].replace(/\.+$/, '');
}

/**
 * Parse pasted TXT record values (bare values or full zone lines) into
 * builder state. Parts not present keep their `prev` values. Never throws.
 */
export function parseDnsRecords(text: string, prev: DnsRecordsConfig): ParsedDnsRecords {
  const config = cloneConfig(prev);
  const found = { spf: false, dkim: false, dmarc: false };
  const spfLines: string[] = [];
  let spfName = '';
  let dkimPayload = '';
  let dkimName = '';
  let dmarcPayload = '';
  let dmarcName = '';

  for (const raw of (text ?? '').split('\n')) {
    const line = raw.trim();
    if (!line) continue;
    const payload = txtPayload(line);
    const name = recordName(line);
    if (/v=spf1(\s|$)/i.test(payload)) {
      spfLines.push(payload);
      if (!spfName) spfName = name;
    } else if (/v=DKIM/i.test(payload) || name.includes('._domainkey')) {
      dkimPayload = payload;
      dkimName = name;
    } else if (/v=DMARC/i.test(payload) || name.startsWith('_dmarc.')) {
      dmarcPayload = payload;
      dmarcName = name;
    }
  }

  // Sniff the record host for domain and selector: an explicit name wins.
  const dmarcDomain = dmarcName.startsWith('_dmarc.') ? dmarcName.slice('_dmarc.'.length) : '';
  const dkimAt = dkimName.indexOf('._domainkey.');
  const sniffDomain = dmarcDomain || (dkimAt > 0 ? dkimName.slice(dkimAt + '._domainkey.'.length) : '') || spfName;
  if (sniffDomain !== '' && isDomainLike(sniffDomain)) config.domain = sniffDomain;
  if (dkimAt > 0) config.selector = dkimName.slice(0, dkimAt);

  if (spfLines.length > 0) {
    // First record's terms are unpacked; any further whole records stay as
    // terms so validateDnsRecords can flag them.
    const terms: string[] = [];
    spfLines.forEach((l, i) => {
      if (i === 0) {
        terms.push(...l.replace(/^\s*v=spf1\s*/i, '').split(/\s+/).map((t) => t.trim()).filter(Boolean));
      } else {
        terms.push(l.trim());
      }
    });
    config.spf = { enabled: true, terms };
    found.spf = true;
  }

  if (dkimPayload !== '') {
    const rec = parseDKIM(dkimPayload);
    if (rec.valid) {
      config.dkim = {
        enabled: true,
        keyType: rec.keyType,
        publicKey: rec.publicKey ?? '',
        hashes: rec.hashes,
        testMode: rec.flags.includes('y'),
      };
      found.dkim = true;
    }
  }

  if (dmarcPayload !== '') {
    const rec = parseDMARC(dmarcPayload);
    if (rec.valid) {
      config.dmarc = {
        enabled: true,
        // parseDMARC only reports valid=true once p= is set — policy is defined.
        policy: rec.policy!,
        subdomainPolicy: rec.subdomainPolicy ?? '',
        rua: rec.aggregateUris,
        ruf: rec.forensicUris,
        percent: rec.percent ?? null,
        dkimAlignment: rec.dkimAlignment ?? 'r',
        spfAlignment: rec.spfAlignment ?? 'r',
      };
      found.dmarc = true;
    }
  }

  return { config, found };
}

// URL codecs for shareable state. Each component is encodeURIComponent'd
// BEFORE the compact format is assembled, so the '|'/','/'=' delimiters can
// never appear inside a component after decoding. Param presence (spf/dkim/dm)
// carries the enabled flags; absent sections fall back to defaults.

const enc = (s: string) => encodeURIComponent(s);
const dec = (s: string): string => {
  try {
    return decodeURIComponent(s);
  } catch {
    return s; // malformed escape — keep verbatim rather than throw
  }
};

/** Split "k=rsa|h=sha256|…" sections into a tag → value map. */
function sections(param: string): Map<string, string> {
  return new Map(
    param.split('|').map((s) => {
      const eq = s.indexOf('=');
      return [s.slice(0, eq), s.slice(eq + 1)];
    }),
  );
}

const POLICY_VALUES: readonly string[] = ['none', 'quarantine', 'reject'];

export function toQuery(config: DnsRecordsConfig): string {
  const p = new URLSearchParams();
  if (config.domain.trim() !== '') p.set('d', enc(config.domain.trim()));
  if (config.selector.trim() !== '') p.set('sel', enc(config.selector.trim()));
  if (config.spf.enabled) {
    p.set('spf', config.spf.terms.map((t) => enc(t.trim())).join(','));
  }
  if (config.dkim.enabled) {
    p.set(
      'dkim',
      [
        `k=${enc(config.dkim.keyType)}`,
        `h=${config.dkim.hashes.map(enc).join(',')}`,
        config.dkim.testMode ? 't=1' : 't=0',
        `p=${enc(config.dkim.publicKey)}`,
      ].join('|'),
    );
  }
  if (config.dmarc.enabled) {
    p.set(
      'dm',
      [
        `p=${config.dmarc.policy}`,
        `sp=${config.dmarc.subdomainPolicy}`,
        `rua=${config.dmarc.rua.map(enc).join(',')}`,
        `ruf=${config.dmarc.ruf.map(enc).join(',')}`,
        config.dmarc.percent != null ? `pct=${config.dmarc.percent}` : 'pct=',
        `adkim=${config.dmarc.dkimAlignment}`,
        `aspf=${config.dmarc.spfAlignment}`,
      ].join('|'),
    );
  }
  return p.toString();
}

export function fromQuery(params: URLSearchParams): DnsRecordsConfig | null {
  if (!params.has('d') && !params.has('sel') && !params.has('spf') && !params.has('dkim') && !params.has('dm')) {
    return null;
  }
  const config = defaultDnsRecordsConfig();
  const d = params.get('d');
  if (d !== null) config.domain = dec(d);
  const sel = params.get('sel');
  if (sel !== null) config.selector = dec(sel);
  const spf = params.get('spf');
  if (spf !== null) {
    config.spf = { enabled: true, terms: spf.split(',').filter((t) => t !== '').map(dec) };
  }
  const dkim = params.get('dkim');
  if (dkim !== null) {
    const sec = sections(dkim);
    config.dkim = {
      enabled: true,
      keyType: dec(sec.get('k') ?? 'rsa'),
      hashes: (sec.get('h') ?? '').split(',').filter((t) => t !== '').map(dec),
      testMode: sec.get('t') === '1',
      publicKey: dec(sec.get('p') ?? ''),
    };
  }
  const dm = params.get('dm');
  if (dm !== null) {
    const sec = sections(dm);
    const policy = sec.get('p') ?? 'none';
    const sub = sec.get('sp') ?? '';
    const pct = Number(sec.get('pct'));
    config.dmarc = {
      enabled: true,
      policy: POLICY_VALUES.includes(policy) ? (policy as DmarcPolicy) : 'none',
      subdomainPolicy: POLICY_VALUES.includes(sub) ? (sub as DmarcPolicy) : '',
      rua: (sec.get('rua') ?? '').split(',').filter((t) => t !== '').map(dec),
      ruf: (sec.get('ruf') ?? '').split(',').filter((t) => t !== '').map(dec),
      percent: sec.get('pct') !== '' && Number.isFinite(pct) ? pct : null,
      dkimAlignment: sec.get('adkim') === 's' ? 's' : 'r',
      spfAlignment: sec.get('aspf') === 's' ? 's' : 'r',
    };
  }
  return config;
}

Also available in 9 other languages

Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →