UNPKG

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
"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;