email-domain-check
Version:
Comprehensive email domain validation library with DNS, MX, SMTP, DKIM, DMARC and MTA-STS support.
395 lines (394 loc) • 15.3 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.DomainChecker = void 0;
const node_net_1 = require("node:net");
const tldts_1 = __importDefault(require("tldts"));
const address_js_1 = require("./address.js");
const mta_sts_js_1 = require("./mta-sts.js");
const resolver_js_1 = require("./resolver.js");
const txt_record_js_1 = require("./txt-record.js");
const error_js_1 = require("./types/error.js");
const options_js_1 = require("./types/options.js");
class DomainChecker {
options;
resolver;
failoverResolvers = [];
constructor(options) {
this.options = (0, options_js_1.setSafeDCOptions)(options);
this.resolver = new resolver_js_1.DNSResolver({
timeout: this.options.dnsTimeout,
tries: this.options.tries,
});
if (this.options.server) {
this.resolver.kind = resolver_js_1.ResolverKind.Custom;
this.resolver.setServers(this.options.server);
}
else {
this.resolver.kind = resolver_js_1.ResolverKind.System;
}
if (this.options.failoverServers) {
for (const failoverServer of this.options.failoverServers) {
const resolver = new resolver_js_1.DNSResolver({
timeout: this.options.dnsTimeout,
tries: this.options.tries,
kind: resolver_js_1.ResolverKind.Failover,
});
if (this.options.server) {
if (failoverServer.includes(this.options.server[0])) {
continue;
}
}
resolver.setServers(failoverServer);
this.failoverResolvers.push(resolver);
}
if (this.options.server) {
const defaultResolver = new resolver_js_1.DNSResolver({
timeout: this.options.dnsTimeout,
tries: this.options.tries,
kind: resolver_js_1.ResolverKind.FailoverSystem,
});
this.failoverResolvers.push(defaultResolver);
}
}
}
getParentDomain(domain) {
const index = domain.indexOf('.');
if (index === -1)
return null;
const parentDomain = domain.slice(index + 1);
const mainDomain = tldts_1.default.getDomain(domain);
if (mainDomain === null)
return null;
if (parentDomain.length > mainDomain.length) {
return parentDomain;
}
else {
return mainDomain;
}
}
async getNsResolver(target) {
const addr = address_js_1.Address.loadFromTarget(target);
try {
// ["ns1.example.net", ...]
const nsHosts = await this.resolver.resolveNs(addr.hostname).catch(() => []);
const ips = [];
for (const ns of nsHosts) {
try {
const v4 = await this.resolver.resolve4(ns);
ips.push(...v4);
}
catch {
/* ignore */
}
try {
const v6 = await this.resolver.resolve6(ns);
ips.push(...v6);
}
catch {
/* ignore */
}
}
const resolver = new resolver_js_1.DNSResolver({
timeout: this.options.dnsTimeout,
tries: this.options.tries,
kind: resolver_js_1.ResolverKind.DomainNS,
nsHosts,
});
if (ips.length > 0) {
resolver.setServers(ips);
return resolver;
}
else {
// mail.example.net -> example.net
const nsDomain = this.getParentDomain(addr.hostname);
if (nsDomain !== null && nsDomain !== addr.hostname) {
return await this.getNsResolver(nsDomain);
}
else {
return this.resolver;
}
}
}
catch {
// fallback: system resolver
return this.resolver;
}
}
async hasMxRecord(target) {
const addr = address_js_1.Address.loadFromTarget(target);
try {
const addresses = await this.getMxRecord({
target: addr,
useCache: false,
preferDomainNS: false,
});
return Array.isArray(addresses) && addresses.length > 0;
}
catch {
return false;
}
}
async cleanMxRecord(records, resolveOptions) {
if (!this.options.useMtaSts || records.length === 0) {
return records;
}
try {
// Get MTA-STS DNS record
const stsRecord = await this.getStsRecord(resolveOptions);
const policyId = stsRecord?.id;
if (!policyId) {
// No MTA-STS configured
return records;
}
// Fetch policy file
const policy = await (0, mta_sts_js_1.getMtaStsPolicy)(resolveOptions.target);
if (!policy) {
// Policy file not found or invalid
return records;
}
// Filter MX records based on policy
const allowedRecords = records.filter((record) => (0, mta_sts_js_1.isMxAllowed)(record.exchange, policy));
// If policy mode is 'enforce' and no MX matches, return empty (fail delivery)
if (policy.mode === 'enforce' && allowedRecords.length === 0) {
return [];
}
// If policy mode is 'testing', log but don't filter
if (policy.mode === 'testing') {
const blockedRecords = records.filter((record) => !(0, mta_sts_js_1.isMxAllowed)(record.exchange, policy));
if (blockedRecords.length > 0) {
// In testing mode, just log violations but return all records
console.warn(`MTA-STS testing mode: ${blockedRecords.length} MX records would be blocked:`, blockedRecords.map((r) => r.exchange));
}
return records;
}
// For 'enforce' mode, return only allowed records
return allowedRecords.length > 0 ? allowedRecords : records;
}
catch (error) {
// On error, return original records (fail open)
console.error('MTA-STS check failed:', error);
return records;
}
}
async getMxRecord(resolveOptions) {
let result = [];
resolveOptions.target = address_js_1.Address.loadFromTarget(resolveOptions.target);
let lastError;
try {
result = await this.resolveMxWithFailover(resolveOptions);
// Apply MTA-STS filtering
result = await this.cleanMxRecord(result, resolveOptions);
return result;
}
catch (error) {
lastError = error;
}
if (resolveOptions.preferDomainNS !== true) {
for (const resolver of this.failoverResolvers) {
try {
result = await this.resolveMxWithFailover(resolveOptions, resolver);
// Apply MTA-STS filtering
result = await this.cleanMxRecord(result, resolveOptions);
return result;
}
catch (error) {
lastError = error;
}
}
}
const err = lastError;
const code = err?.code;
if (code === error_js_1.DNS_ERRORS.NODATA || code === error_js_1.DNS_ERRORS.NOTFOUND) {
return [];
}
throw lastError;
}
async resolveMxWithFailover(resolveOptions, fallback) {
resolveOptions.target = address_js_1.Address.loadFromTarget(resolveOptions.target);
let resolver = fallback ? fallback : this.resolver;
if ((this.options.useDomainNS || resolveOptions.preferDomainNS) && !fallback) {
resolver = await this.getNsResolver(resolveOptions.target);
}
const result = await resolver.resolveMx(resolveOptions.target.hostname);
const sorted = [...result].sort((a, b) => a.priority - b.priority);
return sorted;
}
async getNameServers(target) {
const addr = address_js_1.Address.loadFromTarget(target);
const nsHosts = await this.resolver.resolveNs(addr.hostname);
return nsHosts;
}
async getSmtpConnection(target) {
const addr = address_js_1.Address.loadFromTarget(target);
// Get MX records
const mxRecords = await this.getMxRecord({
target: addr,
useCache: false,
preferDomainNS: false,
});
if (mxRecords.length === 0) {
throw new Error('No MX records found');
}
// Check for local IPs if blocking is enabled
if (this.options.blockLocalIPs) {
const mxAddr = new address_js_1.Address(mxRecords[0].exchange);
if (mxAddr.isLocal) {
throw new Error('Local IP addresses are blocked');
}
}
const port = this.options.deliveryPort;
let lastError = null;
// Try each MX record in priority order
for (const mx of mxRecords) {
try {
const socket = await this.connectToSmtp(mx.exchange, port);
return socket;
}
catch (error) {
lastError = error;
}
}
throw lastError || new Error('Failed to connect to any MX server');
}
connectToSmtp(host, port) {
return new Promise((resolve, reject) => {
const signal = AbortSignal.timeout(this.options.smtpTimeout);
const socket = (0, node_net_1.createConnection)({ host, port, keepAlive: true, signal });
if (this.options.socketIdleTimeout) {
socket.setTimeout(this.options.socketIdleTimeout);
}
// If timeout is 0, then the existing idle timeout is disabled.
socket.on('timeout', () => {
socket.end();
});
socket.once('connect', () => {
resolve(socket);
});
socket.once('error', (error) => {
socket.destroy();
reject(error);
});
});
}
async getCustomRecords(resolveOptions) {
const result = await this.getTxtRecord(resolveOptions);
const record = result?.getCustomRecords() ?? null;
return record;
}
async getCustomKVRecord(resolveOptions, key) {
const result = await this.getTxtRecord(resolveOptions);
const record = result?.getCustomKVRecord(key) ?? null;
return record;
}
async getAllKVRecords(resolveOptions) {
const result = await this.getTxtRecord(resolveOptions);
const record = result?.getAllKVRecord() ?? null;
return record;
}
async getSpfRecord(resolveOptions) {
const result = await this.getTxtRecord(resolveOptions);
const record = result?.getSPF() ?? null;
return record;
}
get_dkim_addr(addr, selector) {
if (addr.ipKind === address_js_1.IPKind.None && addr.hostname) {
selector = selector ? selector : this.options.dkimSelector;
return new address_js_1.Address(`${selector}._domainkey.${addr.hostname}`);
}
return null;
}
async getDkimRecord(resolveOptions) {
const addr = address_js_1.Address.loadFromTarget(resolveOptions.target);
const dkimAddr = this.get_dkim_addr(addr, resolveOptions.dkimSelector);
if (dkimAddr) {
resolveOptions.target = dkimAddr;
const result = await this.getTxtRecord(resolveOptions);
const record = result?.getDKIM() ?? null;
return record;
}
return null;
}
get_dmarc_addr(addr) {
// RFC 7489
if (addr.ipKind === address_js_1.IPKind.None && addr.hostname) {
return new address_js_1.Address(`_dmarc.${addr.hostname}`);
}
return null;
}
async getDmarcRecord(resolveOptions) {
const addr = address_js_1.Address.loadFromTarget(resolveOptions.target);
const dmarcAddr = this.get_dmarc_addr(addr);
if (dmarcAddr) {
resolveOptions.target = dmarcAddr;
const result = await this.getTxtRecord(resolveOptions);
const record = result?.getDMARC() ?? null;
return record;
}
return null;
}
get_sts_addr(addr) {
if (addr.ipKind === address_js_1.IPKind.None && addr.hostname) {
return new address_js_1.Address(`_mta-sts.${addr.hostname}`);
}
return null;
}
async getStsRecord(resolveOptions) {
const addr = address_js_1.Address.loadFromTarget(resolveOptions.target);
const mtaStsAddr = this.get_sts_addr(addr);
if (mtaStsAddr) {
const opts = { ...resolveOptions };
opts.target = mtaStsAddr;
const result = await this.getTxtRecord(opts);
const record = result?.getSTS() ?? null;
return record;
}
return null;
}
async getTxtRecord(resolveOptions) {
let lastError;
try {
return await this.getTxtRecordWithFailover(resolveOptions);
}
catch (error) {
lastError = error;
}
if (resolveOptions.preferDomainNS !== true) {
for (const resolver of this.failoverResolvers) {
try {
return await this.getTxtRecordWithFailover(resolveOptions, resolver);
}
catch (error) {
lastError = error;
}
}
}
const err = lastError;
const code = err?.code;
if (code === error_js_1.DNS_ERRORS.NODATA || code === error_js_1.DNS_ERRORS.NOTFOUND) {
return null;
}
throw lastError;
}
async getTxtRecordWithFailover(resolveOptions, fallback) {
resolveOptions.target = address_js_1.Address.loadFromTarget(resolveOptions.target);
let resolver = fallback ? fallback : this.resolver;
if ((this.options.useDomainNS || resolveOptions.preferDomainNS) && !fallback) {
resolver = await this.getNsResolver(resolveOptions.target);
}
const txtRecords = await resolver.resolveTxt(resolveOptions.target.hostname);
const flatRecords = txtRecords.flat();
const result = new txt_record_js_1.TXTResult();
for (let flatRecord of flatRecords) {
flatRecord = flatRecord?.trim();
if (!flatRecord)
continue;
const record = new txt_record_js_1.TXTRecord(flatRecord);
result.records.push(record);
}
return result;
}
}
exports.DomainChecker = DomainChecker;