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

522 lines 18 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 = void 0; exports.getEmailProvider = getEmailProvider; exports.getEmailProviderSync = getEmailProviderSync; exports.normalizeEmail = normalizeEmail; exports.emailsMatch = emailsMatch; exports.getEmailProviderFast = getEmailProviderFast; const concurrent_dns_1 = require("./concurrent-dns"); const provider_loader_1 = require("./provider-loader"); /** * Get email provider information for any email address. * * This is the primary function that handles all email types: * - Consumer emails (gmail.com, yahoo.com, etc.) * - Business domains (mycompany.com using Google Workspace, etc.) * - Unknown providers (graceful fallback) * * @param email - The email address to analyze * @param timeout - Optional timeout for DNS queries in milliseconds (default: 5000ms) * @returns Promise resolving to EmailProviderResult with provider info and error context * * @example * ```typescript * // Consumer email * const result = await getEmailProvider('local@domain.tld'); * console.log(result.provider?.companyProvider); // Provider name * console.log(result.loginUrl); // Login URL * * // Business domain * const business = await getEmailProvider('local@business.tld'); * console.log(business.provider?.companyProvider); // Detected provider * console.log(business.detectionMethod); // Detection method * * // Error handling * const invalid = await getEmailProvider('invalid-email'); * console.log(invalid.error?.type); // "INVALID_EMAIL" * console.log(invalid.error?.message); // "Invalid email format" * ``` */ async function getEmailProvider(email, timeout) { try { // Input validation if (!email || typeof email !== 'string') { return { provider: null, email: email || '', loginUrl: null, error: { type: 'INVALID_EMAIL', message: 'Email address is required and must be a string' } }; } // Basic email format validation const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(email)) { return { provider: null, email, loginUrl: null, error: { type: 'INVALID_EMAIL', message: 'Invalid email format' } }; } const domain = email.split('@')[1]?.toLowerCase(); if (!domain) { return { provider: null, email, loginUrl: null, error: { type: 'INVALID_EMAIL', message: 'Invalid email format - missing domain' } }; } // First try synchronous domain matching const syncResult = getEmailProviderSync(email); if (syncResult.provider) { return { ...syncResult, detectionMethod: 'domain_match' }; } // Fall back to DNS detection for business domains const loadResult = (0, provider_loader_1.loadProviders)(); if (!loadResult.success) { return { provider: null, email, loginUrl: null, error: { type: 'NETWORK_ERROR', message: 'Service temporarily unavailable' } }; } const providers = loadResult.providers; const concurrentResult = await (0, concurrent_dns_1.detectProviderConcurrent)(domain, providers, { timeout: timeout || 5000, enableParallel: true, collectDebugInfo: false }); const result = { provider: concurrentResult.provider, email, loginUrl: concurrentResult.provider?.loginUrl || null, detectionMethod: concurrentResult.detectionMethod || 'mx_record' }; if (concurrentResult.proxyService) { result.proxyService = concurrentResult.proxyService; } // Add error context for null results if (!result.provider && !result.proxyService) { result.error = { type: 'UNKNOWN_DOMAIN', message: `No email provider found for domain: ${domain}` }; } return result; } catch (error) { // Enhanced error handling if (error.message?.includes('Rate limit exceeded')) { const retryMatch = error.message.match(/Try again in (\d+) seconds/); const retryAfter = retryMatch ? parseInt(retryMatch[1], 10) : undefined; return { provider: null, email, loginUrl: null, error: { type: 'RATE_LIMITED', message: 'DNS query rate limit exceeded', ...(retryAfter !== undefined ? { retryAfter } : {}) } }; } if (error.message?.includes('timeout')) { return { provider: null, email, loginUrl: null, error: { type: 'DNS_TIMEOUT', message: `DNS lookup timed out after ${timeout || 5000}ms` } }; } return { provider: null, email, loginUrl: null, error: { type: 'NETWORK_ERROR', message: error.message || 'Unknown network error' } }; } } /** * Get email provider information synchronously (no DNS lookup). * * This function only checks predefined domains and returns immediately. * Use this when you can't use async functions or don't want DNS lookups. * * @param email - The email address to analyze * @returns EmailProviderResult with provider info (limited to known domains) * * @example * ```typescript * // Works for known domains * const gmail = getEmailProviderSync('user@gmail.com'); * console.log(gmail.provider?.companyProvider); // "Gmail" * * // Unknown domains return null * const unknown = getEmailProviderSync('user@mycompany.com'); * console.log(unknown.provider); // null * console.log(unknown.error?.type); // "UNKNOWN_DOMAIN" * ``` */ function getEmailProviderSync(email) { try { // Input validation if (!email || typeof email !== 'string') { return { provider: null, email: email || '', loginUrl: null, error: { type: 'INVALID_EMAIL', message: 'Email address is required and must be a string' } }; } // Basic email format validation const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(email)) { return { provider: null, email, loginUrl: null, error: { type: 'INVALID_EMAIL', message: 'Invalid email format' } }; } // Pure synchronous domain matching const domain = email.split('@')[1]?.toLowerCase(); if (!domain) { return { provider: null, email, loginUrl: null, error: { type: 'INVALID_EMAIL', message: 'Invalid email format - missing domain' } }; } // Load providers with verification let provider = null; try { const result = (0, provider_loader_1.loadProviders)(); // Ensure providers loaded successfully if (!result.success) { if (process.env.NODE_ENV !== 'test' && !process.env.JEST_WORKER_ID) { console.error('🚨 Provider lookup blocked due to validation failure'); } return { provider: null, email, loginUrl: null, error: { type: 'NETWORK_ERROR', message: 'Service temporarily unavailable' } }; } const domainMap = new Map(); // Build domain map from loaded providers for (const loadedProvider of result.providers) { for (const domain of loadedProvider.domains) { domainMap.set(domain.toLowerCase(), loadedProvider); } } 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 { provider: null, email, loginUrl: null, error: { type: 'NETWORK_ERROR', message: 'Service temporarily unavailable' } }; } const result = { provider: provider || null, email, loginUrl: provider?.loginUrl || null, detectionMethod: 'domain_match' }; // Add error context for null results 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) { return { provider: null, email, loginUrl: null, error: { type: 'INVALID_EMAIL', message: error.message || 'Invalid email address' } }; } } /** * Normalize an email address to its canonical form. * * This handles provider-specific aliasing rules: * - Gmail: removes dots and plus addressing * - Other providers: removes plus addressing only * * @param email - The email address to normalize * @returns The canonical email address * * @example * ```typescript * const canonical = normalizeEmail('L.O.C.A.L+work@DOMAIN.TLD'); * console.log(canonical); // 'local@domain.tld' * * const provider = normalizeEmail('local+newsletter@provider.tld'); * console.log(provider); // 'local@provider.tld' * ``` */ function normalizeEmail(email) { if (!email || typeof email !== 'string') { return email; } // Convert to lowercase const lowercaseEmail = email.toLowerCase().trim(); // Split email into local and domain parts const atIndex = lowercaseEmail.lastIndexOf('@'); if (atIndex === -1) { return lowercaseEmail; } let localPart = lowercaseEmail.slice(0, atIndex); const domainPart = lowercaseEmail.slice(atIndex + 1); // Use providers for domain lookup let provider = null; try { const result = (0, provider_loader_1.loadProviders)(); if (!result.success) { return lowercaseEmail; // Return as-is if providers can't be loaded } const domainMap = new Map(); for (const loadedProvider of result.providers) { for (const domain of loadedProvider.domains) { domainMap.set(domain.toLowerCase(), loadedProvider); } } provider = domainMap.get(domainPart) || null; } catch (error) { return lowercaseEmail; // Return as-is if error occurs } if (provider?.alias) { // Provider supports aliasing if (provider.alias.dots) { // Remove all dots from local part (e.g. Gmail) localPart = localPart.replace(/\./g, ''); } if (provider.alias.plus) { // Remove plus addressing (everything after +) const plusIndex = localPart.indexOf('+'); if (plusIndex !== -1) { localPart = localPart.slice(0, plusIndex); } } } return `${localPart}@${domainPart}`; } /** * Check if two email addresses are the same person (accounting for aliases). * * This normalizes both emails and compares their canonical forms. * Useful for preventing duplicate accounts and matching login attempts. * * @param email1 - First email address * @param email2 - Second email address * @returns true if the emails represent the same person * * @example * ```typescript * const match = emailsMatch('local@domain.tld', 'l.o.c.a.l+work@domain.tld'); * console.log(match); // true * * const different = emailsMatch('local@domain.tld', 'other@domain.tld'); * console.log(different); // false * ``` */ function emailsMatch(email1, email2) { if (!email1 || !email2 || typeof email1 !== 'string' || typeof email2 !== 'string') { return false; } // Normalize both emails and compare const normalized1 = normalizeEmail(email1); const normalized2 = normalizeEmail(email2); return normalized1 === normalized2; } /** * Enhanced email provider detection with concurrent DNS for maximum performance. * This function uses parallel MX/TXT lookups for 2x faster business domain detection. * * @param email - The email address to analyze * @param options - Configuration options for DNS detection * @returns Promise resolving to EmailProviderResult with enhanced performance data * * @example * ```typescript * // High-performance detection with concurrent DNS * const result = await getEmailProviderFast('user@mycompany.com', { * enableParallel: true, * collectDebugInfo: true * }); * * console.log(result.provider?.companyProvider); // "Google Workspace" * console.log(result.detectionMethod); // "mx_record" * console.log(result.timing); // { mx: 120, txt: 95, total: 125 } * ``` */ async function getEmailProviderFast(email, options = {}) { const { timeout = 5000, enableParallel = true, collectDebugInfo = false } = options; try { // Input validation if (!email || typeof email !== 'string') { return { provider: null, email: email || '', loginUrl: null, error: { type: 'INVALID_EMAIL', message: 'Email address is required and must be a string' } }; } // Basic email format validation const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(email)) { return { provider: null, email, loginUrl: null, error: { type: 'INVALID_EMAIL', message: 'Invalid email format' } }; } const domain = email.split('@')[1]?.toLowerCase(); if (!domain) { return { provider: null, email, loginUrl: null, error: { type: 'INVALID_EMAIL', message: 'Invalid email format - missing domain' } }; } // First try standard domain matching (fast path) const syncResult = getEmailProviderSync(email); if (syncResult.provider) { return { ...syncResult, detectionMethod: 'domain_match', timing: { mx: 0, txt: 0, total: 0 }, confidence: 1.0 }; } // Fall back to concurrent DNS detection for business domains const result = (0, provider_loader_1.loadProviders)(); if (!result.success) { return { provider: null, email, loginUrl: null, error: { type: 'NETWORK_ERROR', message: 'Service temporarily unavailable' } }; } const providers = result.providers; const concurrentResult = await (0, concurrent_dns_1.detectProviderConcurrent)(domain, providers, { timeout, enableParallel, collectDebugInfo }); const fastResult = { provider: concurrentResult.provider, email, 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; } catch (error) { return { provider: null, email, loginUrl: null, error: { type: 'NETWORK_ERROR', message: error.message || 'DNS detection failed' } }; } } /** * Configuration constants */ exports.Config = { DEFAULT_DNS_TIMEOUT: 5000, MAX_DNS_REQUESTS_PER_MINUTE: 10, SUPPORTED_PROVIDERS_COUNT: 93, SUPPORTED_DOMAINS_COUNT: 180 }; //# sourceMappingURL=api.js.map