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
JavaScript
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;
}
}