UNPKG

@mikkelscheike/email-provider-links

Version:

TypeScript library for email provider detection with 93 providers (207 domains), concurrent DNS resolution, optimized performance, 94.65% test coverage, and enterprise security for login and password reset flows

249 lines 8.89 kB
"use strict"; /** * IDN (Internationalized Domain Names) utilities * Zero-dependency implementation for domain name handling */ Object.defineProperty(exports, "__esModule", { value: true }); exports.IDNValidationError = void 0; exports.domainToPunycode = domainToPunycode; exports.emailToPunycode = emailToPunycode; exports.validateInternationalEmail = validateInternationalEmail; const BASE = 36; const INITIAL_N = 128; const INITIAL_BIAS = 72; const DAMP = 700; const TMIN = 1; const TMAX = 26; const SKEW = 38; const DELIMITER = '-'; function adaptBias(delta, numPoints, firstTime) { delta = firstTime ? Math.floor(delta / DAMP) : Math.floor(delta / 2); delta += Math.floor(delta / numPoints); let k = 0; while (delta > ((BASE - TMIN) * TMAX) / 2) { delta = Math.floor(delta / (BASE - TMIN)); k += BASE; } return k + Math.floor(((BASE - TMIN + 1) * delta) / (delta + SKEW)); } function digitToBasic(digit) { return digit + 22 + 75 * Number(digit < 26); } function encode(str) { const codePoints = Array.from(str).map(c => c.codePointAt(0)); let n = INITIAL_N; let delta = 0; let bias = INITIAL_BIAS; let output = ''; // Copy ASCII chars directly const basic = codePoints.filter(c => c < 0x80); let h = basic.length; let b = h; if (b > 0) { output = String.fromCodePoint(...basic); } if (b > 0) { output += DELIMITER; } while (h < codePoints.length) { let m = Number.MAX_SAFE_INTEGER; for (const c of codePoints) { if (c >= n && c < m) m = c; } delta += (m - n) * (h + 1); n = m; for (const c of codePoints) { if (c < n) { delta++; } else if (c === n) { let q = delta; for (let k = BASE;; k += BASE) { const t = k <= bias ? TMIN : k >= bias + TMAX ? TMAX : k - bias; if (q < t) break; output += String.fromCodePoint(digitToBasic(t + (q - t) % (BASE - t))); q = Math.floor((q - t) / (BASE - t)); } output += String.fromCodePoint(digitToBasic(q)); bias = adaptBias(delta, h + 1, h === b); delta = 0; h++; } } delta++; n++; } return output; } /** * Convert domain to Punycode format * @param domain Domain name to convert * @returns Punycode encoded domain */ function domainToPunycode(domain) { // Split domain into labels return domain.toLowerCase().split('.').map(label => { // Check if label needs encoding (contains non-ASCII) if (!/[^\x00-\x7F]/.test(label)) { return label; } return 'xn--' + encode(label); }).join('.'); } /** * Convert email address's domain to Punycode * @param email Email address to convert * @returns Email with Punycode encoded domain */ function emailToPunycode(email) { const [local, domain] = email.split('@'); if (!domain) return email; return `${local}@${domainToPunycode(domain)}`; } /** * Error codes for IDN validation * These can be used as keys for translation systems */ var IDNValidationError; (function (IDNValidationError) { IDNValidationError["MISSING_INPUT"] = "MISSING_INPUT"; IDNValidationError["EMAIL_TOO_LONG"] = "EMAIL_TOO_LONG"; IDNValidationError["MISSING_AT_SYMBOL"] = "MISSING_AT_SYMBOL"; IDNValidationError["LOCAL_PART_EMPTY"] = "LOCAL_PART_EMPTY"; IDNValidationError["LOCAL_PART_TOO_LONG"] = "LOCAL_PART_TOO_LONG"; IDNValidationError["LOCAL_PART_INVALID"] = "LOCAL_PART_INVALID"; IDNValidationError["DOMAIN_EMPTY"] = "DOMAIN_EMPTY"; IDNValidationError["DOMAIN_TOO_LONG"] = "DOMAIN_TOO_LONG"; IDNValidationError["DOMAIN_INVALID_FORMAT"] = "DOMAIN_INVALID_FORMAT"; IDNValidationError["MISSING_TLD"] = "MISSING_TLD"; IDNValidationError["NUMERIC_TLD"] = "NUMERIC_TLD"; IDNValidationError["INVALID_ENCODING"] = "INVALID_ENCODING"; })(IDNValidationError || (exports.IDNValidationError = IDNValidationError = {})); /** * Validates an email address according to international standards (IDNA) * This implementation follows RFC 5321, 5322, and 6530 standards for email addresses * * @param email The email address to validate * @returns Error information if validation fails, undefined if valid */ function validateInternationalEmail(email) { // Basic checks if (!email || typeof email !== 'string') { return { type: 'IDN_VALIDATION_ERROR', code: IDNValidationError.MISSING_INPUT, message: 'The email field cannot be empty' }; } // Split into local and domain parts const atIndex = email.lastIndexOf('@'); if (atIndex === -1) { return { type: 'IDN_VALIDATION_ERROR', code: IDNValidationError.MISSING_AT_SYMBOL, message: 'The email address must contain an @ symbol' }; } const local = email.slice(0, atIndex); const domain = email.slice(atIndex + 1); // Check for max length - RFC 5321 if (email.length > 254) { return { type: 'IDN_VALIDATION_ERROR', code: IDNValidationError.EMAIL_TOO_LONG, message: 'The email address is too long' }; } // Validate domain part if (domain.length === 0) { return { type: 'IDN_VALIDATION_ERROR', code: IDNValidationError.DOMAIN_EMPTY, message: 'The domain part of the email cannot be empty' }; } if (domain.length > 255) { return { type: 'IDN_VALIDATION_ERROR', code: IDNValidationError.DOMAIN_TOO_LONG, message: 'The domain part of the email is too long' }; } // Validate local part if (local.length === 0) { return { type: 'IDN_VALIDATION_ERROR', code: IDNValidationError.LOCAL_PART_EMPTY, message: 'The username part of the email cannot be empty' }; } if (local.length > 64) { return { type: 'IDN_VALIDATION_ERROR', code: IDNValidationError.LOCAL_PART_TOO_LONG, message: 'The username part of the email is too long' }; } // Check local part characters // Allows: letters, numbers, and !#$%&'*+-/=?^_`{|}~. // Dot can't be first, last, or consecutive if (!/^[a-zA-Z0-9!#$%&'*+\-/=?^_`{|}~]([a-zA-Z0-9!#$%&'*+\-/=?^_`{|}~.]*[a-zA-Z0-9!#$%&'*+\-/=?^_`{|}~])?$/.test(local) || local.includes('..')) { return { type: 'IDN_VALIDATION_ERROR', code: IDNValidationError.LOCAL_PART_INVALID, message: 'The username contains invalid characters or dots in wrong places' }; } // Check domain format (including IDN domains) try { // Check for lone surrogates and control characters if (/[\uD800-\uDFFF]/.test(domain)) { return { type: 'IDN_VALIDATION_ERROR', code: IDNValidationError.INVALID_ENCODING, message: 'The domain contains invalid characters or encoding' }; } // Convert to punycode to handle IDN const punycodeDomain = domainToPunycode(domain); // Check basic domain format // Allows: letters, numbers, hyphens (not first/last), dots separating labels if (!/^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?)*$/.test(punycodeDomain)) { return { type: 'IDN_VALIDATION_ERROR', code: IDNValidationError.DOMAIN_INVALID_FORMAT, message: 'The domain format is invalid' }; } // Check if domain has at least one dot (TLD required) if (!punycodeDomain.includes('.')) { return { type: 'IDN_VALIDATION_ERROR', code: IDNValidationError.MISSING_TLD, message: 'The email domain must include a top-level domain (like .com or .org)' }; } // Check TLD is not all-numeric const tld = punycodeDomain.split('.').pop(); if (/^[0-9]+$/.test(tld)) { return { type: 'IDN_VALIDATION_ERROR', code: IDNValidationError.NUMERIC_TLD, message: 'The top-level domain cannot be all numbers' }; } } catch (error) { return { type: 'IDN_VALIDATION_ERROR', code: IDNValidationError.INVALID_ENCODING, message: 'The domain contains invalid characters or encoding' }; } // If we get here, the email is valid return undefined; } //# sourceMappingURL=idn.js.map