UNPKG

@mikkelscheike/email-provider-links

Version:

TypeScript library for email provider detection with 140 providers (259 domains), concurrent DNS resolution, alias normalization, and HTTPS login URL validation for login and password reset flows

465 lines 17.3 kB
"use strict"; /** * Email Provider Links API * * Simplified API with better error handling and performance improvements. * Clean function names and enhanced error context. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.Config = exports.emailsMatch = exports.normalizeEmail = void 0; exports.getEmailProvider = getEmailProvider; exports.getEmailProviderSync = getEmailProviderSync; exports.getEmailProviderFast = getEmailProviderFast; const provider_loader_1 = require("./provider-loader"); const idn_1 = require("./idn"); const alias_detection_1 = require("./alias-detection"); const constants_1 = require("./constants"); let cachedProvidersRef = null; let cachedDomainMap = null; function getDomainMapFromProviders(providers) { if (cachedProvidersRef === providers && cachedDomainMap) { return cachedDomainMap; } const domainMap = new Map(); for (const loadedProvider of providers) { for (const domain of loadedProvider.domains) { domainMap.set(domain.toLowerCase(), loadedProvider); } } cachedProvidersRef = providers; cachedDomainMap = domainMap; return domainMap; } /** Lazy-load DNS engine so sync-only consumers avoid parsing concurrent-dns. */ function detectProviderConcurrentLazy(domain, providers, config) { // CJS lazy require keeps cold import light and stays compatible with Jest mocks. // eslint-disable-next-line @typescript-eslint/no-require-imports const { detectProviderConcurrent } = require('./concurrent-dns'); return detectProviderConcurrent(domain, providers, config); } function normalizeValidatedEmail(trimmedEmail, domain) { return (0, alias_detection_1.normalizeEmail)(trimmedEmail, { alreadyValidated: true, punycodeDomain: domain }); } function lookupKnownProvider(domain) { try { const result = (0, provider_loader_1.loadProviders)(); if (!result.success) { if (process.env.NODE_ENV !== 'test' && !process.env.JEST_WORKER_ID) { console.error('Provider lookup blocked due to validation failure'); } return { ok: false, error: { type: 'NETWORK_ERROR', message: 'Service temporarily unavailable' } }; } const domainMap = result.domainMap ?? getDomainMapFromProviders(result.providers); return { ok: true, provider: domainMap.get(domain) || null }; } catch (error) { if (process.env.NODE_ENV !== 'test' && !process.env.JEST_WORKER_ID) { console.error('Provider lookup failed:', error); } return { ok: false, error: { type: 'NETWORK_ERROR', message: 'Service temporarily unavailable' } }; } } function validateAndParseEmailForLookup(email) { if (!email || typeof email !== 'string') { return { ok: false, email: email || '', error: { type: 'INVALID_EMAIL', message: 'Email address is required and must be a string' } }; } const trimmedEmail = email.trim(); // Strict validation: treat any IDN validation failure as invalid input. // Only surface IDN_VALIDATION_ERROR for true encoding issues. const idnError = (0, idn_1.validateInternationalEmail)(trimmedEmail); if (idnError) { if (idnError.code === idn_1.IDNValidationError.INVALID_ENCODING) { return { ok: false, email: trimmedEmail, error: { type: 'IDN_VALIDATION_ERROR', message: idnError.message, idnError: idnError.code } }; } return { ok: false, email: trimmedEmail, error: { type: 'INVALID_EMAIL', message: 'Invalid email format' } }; } const atIndex = trimmedEmail.lastIndexOf('@'); if (atIndex === -1) { return { ok: false, email: trimmedEmail, error: { type: 'INVALID_EMAIL', message: 'Invalid email format' } }; } const domainRaw = trimmedEmail.slice(atIndex + 1).toLowerCase(); const domain = (0, idn_1.domainToPunycode)(domainRaw); return { ok: true, trimmedEmail, domain }; } /** * Convert a full EmailProvider to a simplified version */ function simplifyProvider(provider) { if (!provider) return null; return { companyProvider: provider.companyProvider, loginUrl: provider.loginUrl, type: provider.type }; } async function getEmailProvider(email, options) { // Parse options - support both legacy (number) and new (object) format const timeout = typeof options === 'number' ? options : options?.timeout; const extended = typeof options === 'object' && options?.extended === true; try { const parsed = validateAndParseEmailForLookup(email); if (!parsed.ok) { let normalizedEmail = parsed.email; try { normalizedEmail = (0, alias_detection_1.normalizeEmail)(parsed.email); } catch { // keep original } const errorResult = { provider: null, email: normalizedEmail, ...(extended ? { loginUrl: null } : {}), error: parsed.error }; return extended ? errorResult : errorResult; } const { trimmedEmail, domain } = parsed; const normalizedEmail = normalizeValidatedEmail(trimmedEmail, domain); // Fast path: known domain map (no DNS module load) const known = lookupKnownProvider(domain); if (!known.ok) { const errorResult = { provider: null, email: normalizedEmail, ...(extended ? { loginUrl: null } : {}), error: known.error }; return extended ? errorResult : errorResult; } if (known.provider) { if (extended) { return { provider: known.provider, email: normalizedEmail, loginUrl: known.provider.loginUrl, detectionMethod: 'domain_match' }; } return { provider: simplifyProvider(known.provider), email: normalizedEmail, detectionMethod: 'domain_match' }; } // Fall back to DNS detection for business domains (lazy-loaded) const loadResult = (0, provider_loader_1.loadProviders)(); if (!loadResult.success) { const errorResult = { provider: null, email: normalizedEmail, ...(extended ? { loginUrl: null } : {}), error: { type: 'NETWORK_ERROR', message: 'Service temporarily unavailable' } }; return extended ? errorResult : errorResult; } const concurrentResult = await detectProviderConcurrentLazy(domain, loadResult.providers, { timeout: timeout || constants_1.DnsConstants.DEFAULT_TIMEOUT_MS, enableParallel: true, collectDebugInfo: false }); if (extended) { const result = { provider: concurrentResult.provider, email: normalizedEmail, loginUrl: concurrentResult.provider?.loginUrl || null, detectionMethod: concurrentResult.detectionMethod || 'mx_record' }; if (concurrentResult.proxyService) { result.proxyService = concurrentResult.proxyService; } if (!result.provider && !result.proxyService) { result.error = { type: 'UNKNOWN_DOMAIN', message: `No email provider found for domain: ${domain}` }; } return result; } const result = { provider: simplifyProvider(concurrentResult.provider), email: normalizedEmail, detectionMethod: concurrentResult.detectionMethod || 'mx_record' }; if (!result.provider) { result.error = { type: 'UNKNOWN_DOMAIN', message: `No email provider found for domain: ${domain}` }; } return result; } catch (error) { // Enhanced error handling const errorResult = { provider: null, email, error: {} }; if (extended) { errorResult.loginUrl = null; } if (error instanceof Error && error.message.includes('Rate limit exceeded')) { const retryMatch = error.message.match(/Try again in (\d+) seconds/); const retryAfter = retryMatch?.[1] ? parseInt(retryMatch[1], 10) : undefined; errorResult.error = { type: 'RATE_LIMITED', message: 'DNS query rate limit exceeded', ...(retryAfter !== undefined ? { retryAfter } : {}) }; return extended ? errorResult : errorResult; } if (error instanceof Error && error.message.includes('timeout')) { errorResult.error = { type: 'DNS_TIMEOUT', message: `DNS lookup timed out after ${timeout || 5000}ms` }; return extended ? errorResult : errorResult; } errorResult.error = { type: 'NETWORK_ERROR', message: error instanceof Error ? error.message : 'Unknown network error' }; return extended ? errorResult : errorResult; } } function getEmailProviderSync(email, options) { const extended = options?.extended === true; try { const parsed = validateAndParseEmailForLookup(email); if (!parsed.ok) { let normalizedEmail = parsed.email; try { normalizedEmail = (0, alias_detection_1.normalizeEmail)(parsed.email); } catch { // keep original } const errorResult = { provider: null, email: normalizedEmail, error: parsed.error, ...(extended ? { loginUrl: null } : {}) }; return errorResult; } const { trimmedEmail, domain } = parsed; const normalizedEmail = normalizeValidatedEmail(trimmedEmail, domain); const known = lookupKnownProvider(domain); if (!known.ok) { const errorResult = { provider: null, email: normalizedEmail, error: known.error, ...(extended ? { loginUrl: null } : {}) }; return errorResult; } const provider = known.provider; if (extended) { const result = { provider: provider || null, email: normalizedEmail, loginUrl: provider?.loginUrl || null, detectionMethod: 'domain_match' }; if (!result.provider) { result.error = { type: 'UNKNOWN_DOMAIN', message: `No email provider found for domain: ${domain} (sync mode - business domains not supported)` }; } return result; } const result = { provider: simplifyProvider(provider), email: normalizedEmail, detectionMethod: 'domain_match' }; if (!result.provider) { result.error = { type: 'UNKNOWN_DOMAIN', message: `No email provider found for domain: ${domain} (sync mode - business domains not supported)` }; } return result; } catch (error) { const errorResult = { provider: null, email, error: { type: 'INVALID_EMAIL', message: error instanceof Error ? error.message : 'Invalid email address' }, ...(extended ? { loginUrl: null } : {}) }; return errorResult; } } // Re-export alias detection functions from the dedicated module var alias_detection_2 = require("./alias-detection"); Object.defineProperty(exports, "normalizeEmail", { enumerable: true, get: function () { return alias_detection_2.normalizeEmail; } }); Object.defineProperty(exports, "emailsMatch", { enumerable: true, get: function () { return alias_detection_2.emailsMatch; } }); async function getEmailProviderFast(email, options = {}) { const { timeout = 5000, enableParallel = true, collectDebugInfo = false, extended = false } = options; try { const parsed = validateAndParseEmailForLookup(email); if (!parsed.ok) { return { provider: null, email: parsed.email, ...(extended ? { loginUrl: null } : {}), error: parsed.error }; } const { domain, trimmedEmail } = parsed; const normalizedEmail = normalizeValidatedEmail(trimmedEmail, domain); const known = lookupKnownProvider(domain); if (!known.ok) { return { provider: null, email: normalizedEmail, ...(extended ? { loginUrl: null } : {}), error: known.error }; } if (known.provider) { if (extended) { return { provider: known.provider, email: normalizedEmail, loginUrl: known.provider.loginUrl, detectionMethod: 'domain_match', timing: { mx: 0, txt: 0, total: 0 }, confidence: 1.0 }; } return { provider: simplifyProvider(known.provider), email: normalizedEmail, detectionMethod: 'domain_match', timing: { mx: 0, txt: 0, total: 0 }, confidence: 1.0 }; } const result = (0, provider_loader_1.loadProviders)(); if (!result.success) { return { provider: null, email: normalizedEmail, ...(extended ? { loginUrl: null } : {}), error: { type: 'NETWORK_ERROR', message: 'Service temporarily unavailable' } }; } const concurrentResult = await detectProviderConcurrentLazy(domain, result.providers, { timeout, enableParallel, collectDebugInfo }); if (extended) { const fastResult = { provider: concurrentResult.provider, email: normalizedEmail, loginUrl: concurrentResult.provider?.loginUrl || null, detectionMethod: concurrentResult.detectionMethod || 'mx_record', timing: concurrentResult.timing, confidence: concurrentResult.confidence, debug: concurrentResult.debug, error: !concurrentResult.provider && !concurrentResult.proxyService ? { type: 'UNKNOWN_DOMAIN', message: `No email provider found for domain: ${domain}` } : undefined }; if (concurrentResult.proxyService) { fastResult.proxyService = concurrentResult.proxyService; } return fastResult; } return { provider: simplifyProvider(concurrentResult.provider), email: normalizedEmail, detectionMethod: concurrentResult.detectionMethod || 'mx_record', timing: concurrentResult.timing, confidence: concurrentResult.confidence, debug: concurrentResult.debug, error: !concurrentResult.provider ? { type: 'UNKNOWN_DOMAIN', message: `No email provider found for domain: ${domain}` } : undefined }; } catch (error) { const errorResult = { provider: null, email, error: { type: 'NETWORK_ERROR', message: error instanceof Error ? error.message : 'DNS detection failed' }, ...(extended ? { loginUrl: null } : {}) }; return errorResult; } } /** * Configuration constants */ exports.Config = { DEFAULT_DNS_TIMEOUT: constants_1.DnsConstants.DEFAULT_TIMEOUT_MS, MAX_DNS_REQUESTS_PER_MINUTE: constants_1.DnsConstants.MAX_REQUESTS_PER_MINUTE, SUPPORTED_PROVIDERS_COUNT: 140, SUPPORTED_DOMAINS_COUNT: 259 }; //# sourceMappingURL=api.js.map