@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
438 lines • 16.2 kB
JavaScript
"use strict";
/**
* Concurrent DNS Detection Engine
*
* Implements parallel MX/TXT record lookups for 2x faster business domain detection.
* Uses Promise.allSettled for fault tolerance and intelligent result merging.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.ConcurrentDNSDetector = void 0;
exports.createConcurrentDNSDetector = createConcurrentDNSDetector;
exports.detectProviderConcurrent = detectProviderConcurrent;
const util_1 = require("util");
const dns_1 = require("dns");
// Convert Node.js callback-style DNS functions to Promise-based
const resolveMxAsync = (0, util_1.promisify)(dns_1.resolveMx);
const resolveTxtAsync = (0, util_1.promisify)(dns_1.resolveTxt);
/**
* Default configuration for concurrent DNS detection
*/
const DEFAULT_CONFIG = {
timeout: 5000,
enableParallel: true,
prioritizeMX: true,
collectDebugInfo: false,
fallbackToSequential: true
};
/**
* Concurrent DNS Detection Engine
*/
class ConcurrentDNSDetector {
// Store active query states
activeQueries = new Set();
// Cleanup method for tests
cleanup() {
// Cancel any in-progress timeouts
const timeoutError = new Error('Operation cancelled by cleanup');
for (const { reject } of this.activeQueries) {
reject(timeoutError);
}
this.activeQueries.clear();
return Promise.resolve();
}
config;
providers;
constructor(providers, config = {}) {
this.config = { ...DEFAULT_CONFIG, ...config };
this.providers = providers.filter(p => p.customDomainDetection &&
(p.customDomainDetection.mxPatterns || p.customDomainDetection.txtPatterns));
}
/**
* Detect provider for a domain using concurrent DNS lookups
*/
async detectProvider(domain) {
const startTime = Date.now();
const normalizedDomain = domain.toLowerCase().trim().replace(/\.+$/, '');
// Initialize result
const result = {
provider: null,
detectionMethod: null,
confidence: 0,
timing: { mx: 0, txt: 0, total: 0 },
debug: this.config.collectDebugInfo ? {
mxMatches: [],
txtMatches: [],
conflicts: false,
queries: [],
fallbackUsed: false
} : undefined
};
try {
let queries;
if (this.config.enableParallel) {
queries = await this.performParallelQueries(normalizedDomain);
}
else {
queries = await this.performSequentialQueries(normalizedDomain);
}
// Update timing information
result.timing = this.calculateTiming(queries, startTime);
if (this.config.collectDebugInfo && result.debug) {
result.debug.queries = queries;
}
// Find provider matches
const matches = this.findProviderMatches(queries);
if (this.config.collectDebugInfo && result.debug) {
result.debug.mxMatches = matches.filter(m => m.method === 'mx_record').map(m => m.provider.companyProvider);
result.debug.txtMatches = matches.filter(m => m.method === 'txt_record').map(m => m.provider.companyProvider);
result.debug.conflicts = matches.length > 1;
}
// Select best match
const bestMatch = this.selectBestMatch(matches);
if (bestMatch) {
result.provider = bestMatch.provider;
result.detectionMethod = bestMatch.method;
result.confidence = bestMatch.confidence;
}
else {
// Check for proxy services
const proxyResult = this.detectProxy(queries);
if (proxyResult) {
result.detectionMethod = 'proxy_detected';
result.proxyService = proxyResult;
result.confidence = 0.9; // High confidence in proxy detection
}
}
}
catch (error) {
// Handle fallback to sequential if parallel fails
if (this.config.enableParallel && this.config.fallbackToSequential) {
if (this.config.collectDebugInfo && result.debug) {
result.debug.fallbackUsed = true;
}
try {
const fallbackQueries = await this.performSequentialQueries(normalizedDomain);
result.timing = this.calculateTiming(fallbackQueries, startTime);
const matches = this.findProviderMatches(fallbackQueries);
const bestMatch = this.selectBestMatch(matches);
if (bestMatch) {
result.provider = bestMatch.provider;
result.detectionMethod = bestMatch.method;
result.confidence = bestMatch.confidence * 0.9; // Slightly lower confidence for fallback
}
}
catch (fallbackError) {
// Both parallel and sequential failed
console.warn('DNS detection failed:', fallbackError);
}
}
}
result.timing.total = Date.now() - startTime;
return result;
}
/**
* Perform DNS queries in parallel using Promise.allSettled with smart optimization
*/
async performParallelQueries(domain) {
const queries = [
this.queryMX(domain),
this.queryTXT(domain)
];
const results = await Promise.allSettled(queries);
const mappedResults = results.map((result, index) => {
if (result.status === 'fulfilled') {
return result.value;
}
else {
return {
type: index === 0 ? 'mx' : 'txt',
success: false,
error: result.reason,
timing: this.config.timeout
};
}
});
// If MX query succeeded and found a strong match, we can be confident
// and potentially ignore TXT timing for performance reporting
const mxResult = mappedResults[0];
const txtResult = mappedResults[1];
if (mxResult && mxResult.success && this.hasMXMatch(mxResult) && this.config.prioritizeMX) {
// Create an optimized TXT result that indicates it wasn't needed
const optimizedTxtResult = {
type: 'txt',
success: txtResult?.success || false,
records: txtResult?.records || [],
timing: 0 // Don't count TXT time if MX was sufficient
};
if (txtResult?.error) {
optimizedTxtResult.error = txtResult.error;
}
if (txtResult?.rawResponse) {
optimizedTxtResult.rawResponse = txtResult.rawResponse;
}
return [mxResult, optimizedTxtResult];
}
return mappedResults;
}
/**
* Perform DNS queries sequentially (fallback mode)
*/
async performSequentialQueries(domain) {
const results = [];
// Try MX first
try {
const mxResult = await this.queryMX(domain);
results.push(mxResult);
// If MX succeeds and finds a match, we might skip TXT for performance
if (mxResult.success && this.hasMXMatch(mxResult)) {
// Add a placeholder TXT result
results.push({
type: 'txt',
success: false,
timing: 0,
error: new Error('Skipped due to MX match')
});
return results;
}
}
catch (error) {
results.push({
type: 'mx',
success: false,
error: error,
timing: this.config.timeout
});
}
// Try TXT
try {
const txtResult = await this.queryTXT(domain);
results.push(txtResult);
}
catch (error) {
results.push({
type: 'txt',
success: false,
error: error,
timing: this.config.timeout
});
}
return results;
}
/**
* Query MX records with timeout
*/
async queryMX(domain) {
const startTime = Date.now();
try {
const records = await this.withTimeout(resolveMxAsync(domain), this.config.timeout);
return {
type: 'mx',
success: true,
records,
timing: Date.now() - startTime,
rawResponse: this.config.collectDebugInfo ? records : undefined
};
}
catch (error) {
return {
type: 'mx',
success: false,
error: error,
timing: Date.now() - startTime
};
}
}
/**
* Query TXT records with timeout
*/
async queryTXT(domain) {
const startTime = Date.now();
try {
const records = await this.withTimeout(resolveTxtAsync(domain), this.config.timeout);
const flatRecords = records.flat();
return {
type: 'txt',
success: true,
records: flatRecords,
timing: Date.now() - startTime,
rawResponse: this.config.collectDebugInfo ? records : undefined
};
}
catch (error) {
return {
type: 'txt',
success: false,
error: error,
timing: Date.now() - startTime
};
}
}
/**
* Find provider matches from DNS query results
*/
findProviderMatches(queries) {
const matches = [];
for (const query of queries) {
if (!query.success || !query.records)
continue;
for (const provider of this.providers) {
const match = this.matchProvider(provider, query);
if (match) {
matches.push(match);
}
}
}
return matches;
}
/**
* Match a provider against DNS query results
*/
matchProvider(provider, query) {
if (!provider.customDomainDetection || !query.records)
return null;
const detection = provider.customDomainDetection;
let matchedPatterns = [];
let confidence = 0;
if (query.type === 'mx' && detection.mxPatterns) {
for (const record of query.records) {
const exchange = record.exchange?.toLowerCase() || '';
for (const pattern of detection.mxPatterns) {
if (exchange.includes(pattern.toLowerCase())) {
matchedPatterns.push(pattern);
confidence = Math.max(confidence, 0.9); // High confidence for MX matches
}
}
}
}
else if (query.type === 'txt' && detection.txtPatterns) {
for (const record of query.records) {
const txtRecord = record.toLowerCase();
for (const pattern of detection.txtPatterns) {
if (txtRecord.includes(pattern.toLowerCase())) {
matchedPatterns.push(pattern);
confidence = Math.max(confidence, 0.7); // Medium confidence for TXT matches
}
}
}
}
if (matchedPatterns.length > 0) {
return {
provider,
method: query.type === 'mx' ? 'mx_record' : 'txt_record',
confidence: confidence * (matchedPatterns.length / (detection.mxPatterns?.length || detection.txtPatterns?.length || 1)),
matchedPatterns
};
}
return null;
}
/**
* Select the best provider match from multiple candidates
*/
selectBestMatch(matches) {
if (matches.length === 0)
return null;
if (matches.length === 1)
return matches[0] ?? null;
// Sort by confidence and preference for MX records
const sortedMatches = matches.sort((a, b) => {
// Prioritize MX records if configured
if (this.config.prioritizeMX) {
if (a.method === 'mx_record' && b.method !== 'mx_record')
return -1;
if (b.method === 'mx_record' && a.method !== 'mx_record')
return 1;
}
// Then by confidence
return b.confidence - a.confidence;
});
return sortedMatches.length > 0 ? (sortedMatches[0] ?? null) : null;
}
/**
* Check if MX result has potential matches (for sequential optimization)
*/
hasMXMatch(mxResult) {
if (!mxResult.success || !mxResult.records)
return false;
for (const provider of this.providers) {
const match = this.matchProvider(provider, mxResult);
if (match)
return true;
}
return false;
}
/**
* Detect proxy services from DNS results
*/
detectProxy(queries) {
const mxQuery = queries.find(q => q.type === 'mx' && q.success);
if (!mxQuery?.records)
return null;
for (const record of mxQuery.records) {
const exchange = record.exchange?.toLowerCase() || '';
for (const provider of this.providers) {
if (provider.type === 'proxy_service' && provider.customDomainDetection?.mxPatterns) {
for (const pattern of provider.customDomainDetection.mxPatterns) {
if (exchange.includes(pattern.toLowerCase())) {
return provider.companyProvider;
}
}
}
}
}
return null;
}
/**
* Calculate timing information from query results
*/
calculateTiming(queries, startTime) {
const mxQuery = queries.find(q => q.type === 'mx');
const txtQuery = queries.find(q => q.type === 'txt');
return {
mx: mxQuery?.timing || 0,
txt: txtQuery?.timing || 0,
total: Date.now() - startTime
};
}
/**
* Wrap a promise with a timeout
*/
withTimeout(promise, ms) {
let rejectFn;
const timeoutPromise = new Promise((resolve, reject) => {
rejectFn = reject;
const timeout = setTimeout(() => reject(new Error(`DNS query timeout after ${ms}ms`)), ms).unref();
promise
.then(resolve)
.catch(reject)
.finally(() => {
clearTimeout(timeout);
// Clean up active query
const queryEntry = Array.from(this.activeQueries).find(entry => entry.promise === timeoutPromise);
if (queryEntry) {
this.activeQueries.delete(queryEntry);
}
});
});
// Only add to active queries if we have a reject function
if (rejectFn) {
const queryEntry = { promise: timeoutPromise, reject: rejectFn };
this.activeQueries.add(queryEntry);
}
return timeoutPromise;
}
}
exports.ConcurrentDNSDetector = ConcurrentDNSDetector;
/**
* Factory function to create a concurrent DNS detector
*/
function createConcurrentDNSDetector(providers, config) {
return new ConcurrentDNSDetector(providers, config);
}
/**
* Utility function for quick concurrent DNS detection
*/
async function detectProviderConcurrent(domain, providers, config) {
const detector = createConcurrentDNSDetector(providers, config);
return detector.detectProvider(domain);
}
//# sourceMappingURL=concurrent-dns.js.map