@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
JavaScript
;
/**
* 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