UNPKG

email-domain-check

Version:

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

124 lines (123 loc) 4.29 kB
import net from 'node:net'; import { domainToASCII } from 'node:url'; export var IPKind; (function (IPKind) { IPKind[IPKind["None"] = 0] = "None"; IPKind[IPKind["IPv4"] = 4] = "IPv4"; IPKind[IPKind["IPv6"] = 6] = "IPv6"; })(IPKind || (IPKind = {})); export class Address { source; ipKind; user; hostname; constructor(mailOrHost) { this.source = mailOrHost; const atPos = mailOrHost.indexOf('@'); if (atPos >= 0) { //email literal this.user = mailOrHost.slice(0, atPos); const domainPart = mailOrHost.slice(atPos + 1); this.ipKind = net.isIP(domainPart); if (this.ipKind !== IPKind.None) { this.hostname = domainPart; } else { // RFC 5321 / RFC 5322 // Check if the domain looks like an IP literal. IP addresses need to be enclosed in square brackets // user@[127.0.0.1] // user@[IPv6:2001:db8:1ff::a0b:dbd0] if (/^\[(ipv6:)?[^\]]+\]$/i.test(domainPart)) { const cleanDomain = domainPart.replace(/^\[(ipv6:)?|\]$/gi, ''); this.ipKind = net.isIP(cleanDomain); if (this.ipKind !== IPKind.None) { this.hostname = cleanDomain; } else { throw new Error(`Parse error source:${domainPart}`); } } else { const noDot = domainPart.replace(/\.$/, ''); this.hostname = domainToASCII(noDot).toLowerCase(); } } } else { // hostname literal this.ipKind = net.isIP(mailOrHost); if (this.ipKind !== IPKind.None) { this.hostname = mailOrHost; } else { const noDot = mailOrHost.replace(/\.$/, ''); this.hostname = domainToASCII(noDot).toLowerCase(); } } } static loadFromTarget(target) { if (target instanceof Address) { return target; } else if (target instanceof URL) { return new Address(target.hostname); } else { return new Address(target); } } get isIP() { return this.ipKind !== IPKind.None; } get hasPunycode() { if (this.hostname) { return this.hostname.split('.').some((part) => part.startsWith('xn--')); } return false; } // Special IPv4 address ranges. // See also https://en.wikipedia.org/wiki/Reserved_IP_addresses get isLocal() { if (this.ipKind === IPKind.IPv4) { const parts = this.hostname.split('.').map(Number); // Loopback: 127.0.0.0/8 if (parts[0] === 127) return true; // Private: 10.0.0.0/8 if (parts[0] === 10) return true; // Private: 172.16.0.0/12 (172.16.0.0 - 172.31.255.255) if (parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31) return true; // Private: 192.168.0.0/16 if (parts[0] === 192 && parts[1] === 168) return true; // Unspecified/This network: 0.0.0.0/8 if (parts[0] === 0) return true; // Link-local: 169.254.0.0/16 if (parts[0] === 169 && parts[1] === 254) return true; } else if (this.ipKind === IPKind.IPv6) { const lower = this.hostname.toLowerCase(); // Loopback: ::1 if (lower === '::1' || lower === '0:0:0:0:0:0:0:1') return true; // Link-local: fe80::/10 if (lower.startsWith('fe80:')) return true; // Unique local: fc00::/7 if (lower.startsWith('fc') || lower.startsWith('fd')) return true; // Unspecified: :: if (lower === '::') return true; } else { if (this.hostname === 'localhost') return true; } return false; } }