UNPKG

email-domain-check

Version:

Comprehensive email domain validation library with DNS, MX, SMTP, DKIM, DMARC and MTA-STS support.

352 lines (351 loc) 11.2 kB
export var RecordKind; (function (RecordKind) { RecordKind[RecordKind["Custom"] = 0] = "Custom"; RecordKind[RecordKind["SPF1"] = 1] = "SPF1"; RecordKind[RecordKind["DKIM1"] = 2] = "DKIM1"; RecordKind[RecordKind["DMARC1"] = 3] = "DMARC1"; RecordKind[RecordKind["STSv1"] = 4] = "STSv1"; RecordKind[RecordKind["CustomKV"] = 5] = "CustomKV"; })(RecordKind || (RecordKind = {})); export class TXTResult { records = []; getCustomRecords() { let result = null; for (const record of this.records) { if (record.kind === RecordKind.Custom) { result = result ?? []; result.push(record.parsed); } } return result; } getSPF() { let result = null; let count = 0; for (const record of this.records) { if (record.kind === RecordKind.SPF1) { count++; result = record.parsed; } } if (count > 1 && result !== null) { result.errors.push(`Multiple spf records.`); } return result; } getDMARC() { let result = null; let count = 0; for (const record of this.records) { if (record.kind === RecordKind.DMARC1) { count++; result = record.parsed; } } if (count > 1 && result !== null) { result.errors.push(`Multiple DMARC records.`); } return result; } getDKIM() { let result = null; let count = 0; for (const record of this.records) { if (record.kind === RecordKind.DKIM1) { count++; result = record.parsed; } } if (count > 1 && result !== null) { result.errors.push(`Multiple DKIM records.`); } return result; } getSTS() { let result = null; let count = 0; for (const record of this.records) { if (record.kind === RecordKind.STSv1) { count++; result = record.parsed; } } if (count > 1 && result !== null) { result.errors.push(`Multiple MTA-STS records.`); } return result; } getCustomKVRecord(key) { let result = null; for (const record of this.records) { if (record.kind === RecordKind.CustomKV) { const parsed = record.parsed; if (parsed.key === key) { result = result ?? []; result.push(parsed); } } } if (result !== null && result.length > 1) { result[0].errors.push(`Multiple unique key.`); } return result ? result[0] : null; } getAllKVRecord() { let result = null; for (const record of this.records) { if (record.kind === RecordKind.CustomKV) { const parsed = record.parsed; result = result ?? []; result.push(parsed); } } return result; } } export class TXTRecord { raw; kind; parsed = null; constructor(raw) { this.raw = raw; this.kind = RecordKind.Custom; this.parse(); } parse() { // Check for SPF record if (this.raw.startsWith('v=spf1')) { this.kind = RecordKind.SPF1; this.parsed = this.parseSPF1(this.raw); return; } // Check for DMARC record if (this.raw.startsWith('v=DMARC1')) { this.kind = RecordKind.DMARC1; this.parsed = this.parseDMARC1(this.raw); return; } // Check for DKIM record if (this.raw.includes('p=') && (this.raw.startsWith('v=DKIM1') || this.raw.includes('k='))) { this.kind = RecordKind.DKIM1; this.parsed = this.parseDKIM1(this.raw); return; } // Check for MTA-STS record if (this.raw.startsWith('v=STSv1')) { this.kind = RecordKind.STSv1; this.parsed = this.parseSTSv1(this.raw); return; } // Check for key=value format const keyValueMatch = this.raw.match(/^([^=]+)=(.+)$/); if (keyValueMatch) { this.kind = RecordKind.CustomKV; this.parsed = { raw: this.raw, errors: [], key: keyValueMatch[1].trim(), value: keyValueMatch[2].trim(), }; return; } this.kind = RecordKind.Custom; this.parsed = { raw: this.raw, errors: [], value: this.raw, }; } parseSPF1(raw) { const spf = { raw: raw, errors: [], valid: raw.length > 0, v: 'spf1', }; if (raw.length < 8) { spf.errors.push('Raw record empty.'); } const parts = raw.split(/\s+/); for (const part of parts) { if (part.startsWith('v=')) { spf.v = part.substring(2); continue; } // Remove qualifier prefix (+, -, ~, ?) const qualifier = part[0]; const hasQualifier = ['+', '-', '~', '?'].includes(qualifier); const mechanism = hasQualifier ? part.substring(1) : part; const qChar = hasQualifier ? qualifier : '+'; if (mechanism.toLowerCase() === 'a') { spf.a = true; } else if (mechanism.toLowerCase() === 'mx') { spf.mx = true; } else if (mechanism.toLowerCase() === 'all') { spf.all = qChar === '-' ? 'fail' : qChar === '~' ? 'softfail' : 'pass'; } else if (mechanism.startsWith('include:')) { spf.include = spf.include || []; spf.include.push(mechanism.substring(8)); } else if (mechanism.startsWith('ip4:')) { spf.ip4 = spf.ip4 || []; spf.ip4.push(mechanism.substring(4)); } else if (mechanism.startsWith('ip6:')) { spf.ip6 = spf.ip6 || []; spf.ip6.push(mechanism.substring(4)); } else if (mechanism.startsWith('a:')) { spf['a:domain'] = mechanism.substring(2); } else if (mechanism.startsWith('mx:')) { spf['mx:domain'] = mechanism.substring(3); } else if (mechanism.startsWith('ptr')) { spf.ptr = true; } else if (mechanism.startsWith('exists:')) { spf.exists = mechanism.substring(7); } else if (mechanism.startsWith('redirect=')) { spf.redirect = mechanism.substring(9); } else if (mechanism.startsWith('exp=')) { spf.exp = mechanism.substring(4); } } return spf; } parseDMARC1(raw) { const dmarc = { raw: raw, errors: [], v: 'DMARC1', }; const parts = raw .split(';') .map((p) => p.trim()) .filter((p) => p); for (const part of parts) { const [key, value] = part.split('=').map((s) => s.trim()); if (!key || !value) continue; switch (key.toLowerCase()) { case 'v': dmarc.v = value; break; case 'p': dmarc.p = value; // none, quarantine, reject break; case 'sp': dmarc.sp = value; // subdomain policy break; case 'rua': dmarc.rua = value.split(',').map((s) => s.trim()); break; case 'ruf': dmarc.ruf = value.split(',').map((s) => s.trim()); break; case 'pct': dmarc.pct = parseInt(value, 10); break; case 'adkim': dmarc.adkim = value; // r or s break; case 'aspf': dmarc.aspf = value; // r or s break; case 'ri': dmarc.ri = parseInt(value, 10); break; case 'fo': dmarc.fo = value.split(':').map((s) => s.trim()); break; case 'rf': dmarc.rf = value; break; default: dmarc[key] = value; } } return dmarc; } parseDKIM1(raw) { const dkim = { raw: raw, errors: [], }; const parts = raw .split(';') .map((p) => p.trim()) .filter((p) => p); for (const part of parts) { const [key, value] = part.split('=').map((s) => s.trim()); if (!key || !value) continue; switch (key.toLowerCase()) { case 'v': dkim.v = value; break; case 'k': dkim.k = value; // key type (rsa, ed25519) break; case 'p': dkim.p = value; // public key break; case 'h': dkim.h = value.split(':').map((s) => s.trim()); // hash algorithms break; case 's': dkim.s = value.split(':').map((s) => s.trim()); // service types break; case 't': dkim.t = value.split(':').map((s) => s.trim()); // flags break; case 'n': dkim.n = value; // notes break; default: dkim[key] = value; } } return dkim; } parseSTSv1(raw) { const sts = { raw: raw, errors: [], v: 'STSv1', id: '', }; const parts = raw .split(';') .map((p) => p.trim()) .filter((p) => p); for (const part of parts) { const [key, value] = part.split('=').map((s) => s.trim()); if (!key || !value) continue; switch (key.toLowerCase()) { case 'v': sts.v = value; // STSv1 break; case 'id': sts.id = value; // unique identifier (timestamp/version) break; default: sts[key] = value; } } return sts; } isValid() { return this.raw.length > 0; } toString() { return this.raw; } }