@mikkelscheike/email-provider-links 1.7.0 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Concurrent DNS Detection Engine
3
+ *
4
+ * Implements parallel MX/TXT record lookups for 2x faster business domain detection.
5
+ * Uses Promise.allSettled for fault tolerance and intelligent result merging.
6
+ */
7
+ import { EmailProvider } from './index';
8
+ /**
9
+ * Configuration for concurrent DNS detection
10
+ */
11
+ export interface ConcurrentDNSConfig {
12
+ /** Timeout for DNS queries in milliseconds */
13
+ timeout: number;
14
+ /** Enable parallel DNS queries (vs sequential fallback) */
15
+ enableParallel: boolean;
16
+ /** Prioritize MX record matches over TXT */
17
+ prioritizeMX: boolean;
18
+ /** Collect detailed debugging information */
19
+ collectDebugInfo: boolean;
20
+ /** Fall back to sequential mode on parallel failure */
21
+ fallbackToSequential: boolean;
22
+ }
23
+ /**
24
+ * Result from a single DNS query (MX or TXT)
25
+ */
26
+ export interface DNSQueryResult {
27
+ /** Type of DNS record queried */
28
+ type: 'mx' | 'txt';
29
+ /** Whether the query succeeded */
30
+ success: boolean;
31
+ /** DNS records returned (if successful) */
32
+ records?: any[];
33
+ /** Error information (if failed) */
34
+ error?: Error;
35
+ /** Query execution time in milliseconds */
36
+ timing: number;
37
+ /** Raw DNS response for debugging */
38
+ rawResponse?: any;
39
+ }
40
+ /**
41
+ * Result from concurrent DNS detection
42
+ */
43
+ export interface ConcurrentDNSResult {
44
+ /** Detected email provider (null if none found) */
45
+ provider: EmailProvider | null;
46
+ /** Method used for detection */
47
+ detectionMethod: 'mx_record' | 'txt_record' | 'both' | 'proxy_detected' | null;
48
+ /** Confidence score (0-1, higher = more confident) */
49
+ confidence: number;
50
+ /** Proxy service detected (if any) */
51
+ proxyService?: string;
52
+ /** Detailed timing information */
53
+ timing: {
54
+ mx: number;
55
+ txt: number;
56
+ total: number;
57
+ };
58
+ /** Debug information (if enabled) */
59
+ debug?: {
60
+ mxMatches: string[];
61
+ txtMatches: string[];
62
+ conflicts: boolean;
63
+ queries: DNSQueryResult[];
64
+ fallbackUsed: boolean;
65
+ };
66
+ }
67
+ /**
68
+ * Concurrent DNS Detection Engine
69
+ */
70
+ export declare class ConcurrentDNSDetector {
71
+ private config;
72
+ private providers;
73
+ constructor(providers: EmailProvider[], config?: Partial<ConcurrentDNSConfig>);
74
+ /**
75
+ * Detect provider for a domain using concurrent DNS lookups
76
+ */
77
+ detectProvider(domain: string): Promise<ConcurrentDNSResult>;
78
+ /**
79
+ * Perform DNS queries in parallel using Promise.allSettled with smart optimization
80
+ */
81
+ private performParallelQueries;
82
+ /**
83
+ * Perform DNS queries sequentially (fallback mode)
84
+ */
85
+ private performSequentialQueries;
86
+ /**
87
+ * Query MX records with timeout
88
+ */
89
+ private queryMX;
90
+ /**
91
+ * Query TXT records with timeout
92
+ */
93
+ private queryTXT;
94
+ /**
95
+ * Find provider matches from DNS query results
96
+ */
97
+ private findProviderMatches;
98
+ /**
99
+ * Match a provider against DNS query results
100
+ */
101
+ private matchProvider;
102
+ /**
103
+ * Select the best provider match from multiple candidates
104
+ */
105
+ private selectBestMatch;
106
+ /**
107
+ * Check if MX result has potential matches (for sequential optimization)
108
+ */
109
+ private hasMXMatch;
110
+ /**
111
+ * Detect proxy services from DNS results
112
+ */
113
+ private detectProxy;
114
+ /**
115
+ * Calculate timing information from query results
116
+ */
117
+ private calculateTiming;
118
+ /**
119
+ * Wrap a promise with a timeout
120
+ */
121
+ private withTimeout;
122
+ }
123
+ /**
124
+ * Factory function to create a concurrent DNS detector
125
+ */
126
+ export declare function createConcurrentDNSDetector(providers: EmailProvider[], config?: Partial<ConcurrentDNSConfig>): ConcurrentDNSDetector;
127
+ /**
128
+ * Utility function for quick concurrent DNS detection
129
+ */
130
+ export declare function detectProviderConcurrent(domain: string, providers: EmailProvider[], config?: Partial<ConcurrentDNSConfig>): Promise<ConcurrentDNSResult>;
@@ -0,0 +1,403 @@
1
+ "use strict";
2
+ /**
3
+ * Concurrent DNS Detection Engine
4
+ *
5
+ * Implements parallel MX/TXT record lookups for 2x faster business domain detection.
6
+ * Uses Promise.allSettled for fault tolerance and intelligent result merging.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.ConcurrentDNSDetector = void 0;
10
+ exports.createConcurrentDNSDetector = createConcurrentDNSDetector;
11
+ exports.detectProviderConcurrent = detectProviderConcurrent;
12
+ const util_1 = require("util");
13
+ const dns_1 = require("dns");
14
+ // Convert Node.js callback-style DNS functions to Promise-based
15
+ const resolveMxAsync = (0, util_1.promisify)(dns_1.resolveMx);
16
+ const resolveTxtAsync = (0, util_1.promisify)(dns_1.resolveTxt);
17
+ /**
18
+ * Default configuration for concurrent DNS detection
19
+ */
20
+ const DEFAULT_CONFIG = {
21
+ timeout: 5000,
22
+ enableParallel: true,
23
+ prioritizeMX: true,
24
+ collectDebugInfo: false,
25
+ fallbackToSequential: true
26
+ };
27
+ /**
28
+ * Concurrent DNS Detection Engine
29
+ */
30
+ class ConcurrentDNSDetector {
31
+ constructor(providers, config = {}) {
32
+ this.config = { ...DEFAULT_CONFIG, ...config };
33
+ this.providers = providers.filter(p => p.customDomainDetection &&
34
+ (p.customDomainDetection.mxPatterns || p.customDomainDetection.txtPatterns));
35
+ }
36
+ /**
37
+ * Detect provider for a domain using concurrent DNS lookups
38
+ */
39
+ async detectProvider(domain) {
40
+ const startTime = Date.now();
41
+ const normalizedDomain = domain.toLowerCase();
42
+ // Initialize result
43
+ const result = {
44
+ provider: null,
45
+ detectionMethod: null,
46
+ confidence: 0,
47
+ timing: { mx: 0, txt: 0, total: 0 },
48
+ debug: this.config.collectDebugInfo ? {
49
+ mxMatches: [],
50
+ txtMatches: [],
51
+ conflicts: false,
52
+ queries: [],
53
+ fallbackUsed: false
54
+ } : undefined
55
+ };
56
+ try {
57
+ let queries;
58
+ if (this.config.enableParallel) {
59
+ queries = await this.performParallelQueries(normalizedDomain);
60
+ }
61
+ else {
62
+ queries = await this.performSequentialQueries(normalizedDomain);
63
+ }
64
+ // Update timing information
65
+ result.timing = this.calculateTiming(queries, startTime);
66
+ if (this.config.collectDebugInfo && result.debug) {
67
+ result.debug.queries = queries;
68
+ }
69
+ // Find provider matches
70
+ const matches = this.findProviderMatches(queries);
71
+ if (this.config.collectDebugInfo && result.debug) {
72
+ result.debug.mxMatches = matches.filter(m => m.method === 'mx_record').map(m => m.provider.companyProvider);
73
+ result.debug.txtMatches = matches.filter(m => m.method === 'txt_record').map(m => m.provider.companyProvider);
74
+ result.debug.conflicts = matches.length > 1;
75
+ }
76
+ // Select best match
77
+ const bestMatch = this.selectBestMatch(matches);
78
+ if (bestMatch) {
79
+ result.provider = bestMatch.provider;
80
+ result.detectionMethod = bestMatch.method;
81
+ result.confidence = bestMatch.confidence;
82
+ }
83
+ else {
84
+ // Check for proxy services
85
+ const proxyResult = this.detectProxy(queries);
86
+ if (proxyResult) {
87
+ result.detectionMethod = 'proxy_detected';
88
+ result.proxyService = proxyResult;
89
+ result.confidence = 0.9; // High confidence in proxy detection
90
+ }
91
+ }
92
+ }
93
+ catch (error) {
94
+ // Handle fallback to sequential if parallel fails
95
+ if (this.config.enableParallel && this.config.fallbackToSequential) {
96
+ if (this.config.collectDebugInfo && result.debug) {
97
+ result.debug.fallbackUsed = true;
98
+ }
99
+ try {
100
+ const fallbackQueries = await this.performSequentialQueries(normalizedDomain);
101
+ result.timing = this.calculateTiming(fallbackQueries, startTime);
102
+ const matches = this.findProviderMatches(fallbackQueries);
103
+ const bestMatch = this.selectBestMatch(matches);
104
+ if (bestMatch) {
105
+ result.provider = bestMatch.provider;
106
+ result.detectionMethod = bestMatch.method;
107
+ result.confidence = bestMatch.confidence * 0.9; // Slightly lower confidence for fallback
108
+ }
109
+ }
110
+ catch (fallbackError) {
111
+ // Both parallel and sequential failed
112
+ console.warn('DNS detection failed:', fallbackError);
113
+ }
114
+ }
115
+ }
116
+ result.timing.total = Date.now() - startTime;
117
+ return result;
118
+ }
119
+ /**
120
+ * Perform DNS queries in parallel using Promise.allSettled with smart optimization
121
+ */
122
+ async performParallelQueries(domain) {
123
+ const queries = [
124
+ this.queryMX(domain),
125
+ this.queryTXT(domain)
126
+ ];
127
+ const results = await Promise.allSettled(queries);
128
+ const mappedResults = results.map((result, index) => {
129
+ if (result.status === 'fulfilled') {
130
+ return result.value;
131
+ }
132
+ else {
133
+ return {
134
+ type: index === 0 ? 'mx' : 'txt',
135
+ success: false,
136
+ error: result.reason,
137
+ timing: this.config.timeout
138
+ };
139
+ }
140
+ });
141
+ // If MX query succeeded and found a strong match, we can be confident
142
+ // and potentially ignore TXT timing for performance reporting
143
+ const mxResult = mappedResults[0];
144
+ const txtResult = mappedResults[1];
145
+ if (mxResult.success && this.hasMXMatch(mxResult) && this.config.prioritizeMX) {
146
+ // Create an optimized TXT result that indicates it wasn't needed
147
+ const optimizedTxtResult = {
148
+ ...txtResult,
149
+ timing: 0, // Don't count TXT time if MX was sufficient
150
+ optimized: true
151
+ };
152
+ return [mxResult, optimizedTxtResult];
153
+ }
154
+ return mappedResults;
155
+ }
156
+ /**
157
+ * Perform DNS queries sequentially (fallback mode)
158
+ */
159
+ async performSequentialQueries(domain) {
160
+ const results = [];
161
+ // Try MX first
162
+ try {
163
+ const mxResult = await this.queryMX(domain);
164
+ results.push(mxResult);
165
+ // If MX succeeds and finds a match, we might skip TXT for performance
166
+ if (mxResult.success && this.hasMXMatch(mxResult)) {
167
+ // Add a placeholder TXT result
168
+ results.push({
169
+ type: 'txt',
170
+ success: false,
171
+ timing: 0,
172
+ error: new Error('Skipped due to MX match')
173
+ });
174
+ return results;
175
+ }
176
+ }
177
+ catch (error) {
178
+ results.push({
179
+ type: 'mx',
180
+ success: false,
181
+ error: error,
182
+ timing: this.config.timeout
183
+ });
184
+ }
185
+ // Try TXT
186
+ try {
187
+ const txtResult = await this.queryTXT(domain);
188
+ results.push(txtResult);
189
+ }
190
+ catch (error) {
191
+ results.push({
192
+ type: 'txt',
193
+ success: false,
194
+ error: error,
195
+ timing: this.config.timeout
196
+ });
197
+ }
198
+ return results;
199
+ }
200
+ /**
201
+ * Query MX records with timeout
202
+ */
203
+ async queryMX(domain) {
204
+ const startTime = Date.now();
205
+ try {
206
+ const records = await this.withTimeout(resolveMxAsync(domain), this.config.timeout);
207
+ return {
208
+ type: 'mx',
209
+ success: true,
210
+ records,
211
+ timing: Date.now() - startTime,
212
+ rawResponse: this.config.collectDebugInfo ? records : undefined
213
+ };
214
+ }
215
+ catch (error) {
216
+ return {
217
+ type: 'mx',
218
+ success: false,
219
+ error: error,
220
+ timing: Date.now() - startTime
221
+ };
222
+ }
223
+ }
224
+ /**
225
+ * Query TXT records with timeout
226
+ */
227
+ async queryTXT(domain) {
228
+ const startTime = Date.now();
229
+ try {
230
+ const records = await this.withTimeout(resolveTxtAsync(domain), this.config.timeout);
231
+ const flatRecords = records.flat();
232
+ return {
233
+ type: 'txt',
234
+ success: true,
235
+ records: flatRecords,
236
+ timing: Date.now() - startTime,
237
+ rawResponse: this.config.collectDebugInfo ? records : undefined
238
+ };
239
+ }
240
+ catch (error) {
241
+ return {
242
+ type: 'txt',
243
+ success: false,
244
+ error: error,
245
+ timing: Date.now() - startTime
246
+ };
247
+ }
248
+ }
249
+ /**
250
+ * Find provider matches from DNS query results
251
+ */
252
+ findProviderMatches(queries) {
253
+ const matches = [];
254
+ for (const query of queries) {
255
+ if (!query.success || !query.records)
256
+ continue;
257
+ for (const provider of this.providers) {
258
+ const match = this.matchProvider(provider, query);
259
+ if (match) {
260
+ matches.push(match);
261
+ }
262
+ }
263
+ }
264
+ return matches;
265
+ }
266
+ /**
267
+ * Match a provider against DNS query results
268
+ */
269
+ matchProvider(provider, query) {
270
+ if (!provider.customDomainDetection || !query.records)
271
+ return null;
272
+ const detection = provider.customDomainDetection;
273
+ let matchedPatterns = [];
274
+ let confidence = 0;
275
+ if (query.type === 'mx' && detection.mxPatterns) {
276
+ for (const record of query.records) {
277
+ const exchange = record.exchange?.toLowerCase() || '';
278
+ for (const pattern of detection.mxPatterns) {
279
+ if (exchange.includes(pattern.toLowerCase())) {
280
+ matchedPatterns.push(pattern);
281
+ confidence = Math.max(confidence, 0.9); // High confidence for MX matches
282
+ }
283
+ }
284
+ }
285
+ }
286
+ else if (query.type === 'txt' && detection.txtPatterns) {
287
+ for (const record of query.records) {
288
+ const txtRecord = record.toLowerCase();
289
+ for (const pattern of detection.txtPatterns) {
290
+ if (txtRecord.includes(pattern.toLowerCase())) {
291
+ matchedPatterns.push(pattern);
292
+ confidence = Math.max(confidence, 0.7); // Medium confidence for TXT matches
293
+ }
294
+ }
295
+ }
296
+ }
297
+ if (matchedPatterns.length > 0) {
298
+ return {
299
+ provider,
300
+ method: query.type === 'mx' ? 'mx_record' : 'txt_record',
301
+ confidence: confidence * (matchedPatterns.length / (detection.mxPatterns?.length || detection.txtPatterns?.length || 1)),
302
+ matchedPatterns
303
+ };
304
+ }
305
+ return null;
306
+ }
307
+ /**
308
+ * Select the best provider match from multiple candidates
309
+ */
310
+ selectBestMatch(matches) {
311
+ if (matches.length === 0)
312
+ return null;
313
+ if (matches.length === 1)
314
+ return matches[0];
315
+ // Sort by confidence and preference for MX records
316
+ return matches.sort((a, b) => {
317
+ // Prioritize MX records if configured
318
+ if (this.config.prioritizeMX) {
319
+ if (a.method === 'mx_record' && b.method !== 'mx_record')
320
+ return -1;
321
+ if (b.method === 'mx_record' && a.method !== 'mx_record')
322
+ return 1;
323
+ }
324
+ // Then by confidence
325
+ return b.confidence - a.confidence;
326
+ })[0];
327
+ }
328
+ /**
329
+ * Check if MX result has potential matches (for sequential optimization)
330
+ */
331
+ hasMXMatch(mxResult) {
332
+ if (!mxResult.success || !mxResult.records)
333
+ return false;
334
+ for (const provider of this.providers) {
335
+ const match = this.matchProvider(provider, mxResult);
336
+ if (match)
337
+ return true;
338
+ }
339
+ return false;
340
+ }
341
+ /**
342
+ * Detect proxy services from DNS results
343
+ */
344
+ detectProxy(queries) {
345
+ const mxQuery = queries.find(q => q.type === 'mx' && q.success);
346
+ if (!mxQuery?.records)
347
+ return null;
348
+ const proxyPatterns = [
349
+ { service: 'Cloudflare', patterns: ['mxrecord.io', 'mxrecord.mx', 'cloudflare'] },
350
+ { service: 'CloudFront', patterns: ['cloudfront.net'] },
351
+ { service: 'Fastly', patterns: ['fastly.com'] }
352
+ ];
353
+ for (const record of mxQuery.records) {
354
+ const exchange = record.exchange?.toLowerCase() || '';
355
+ for (const proxy of proxyPatterns) {
356
+ for (const pattern of proxy.patterns) {
357
+ if (exchange.includes(pattern)) {
358
+ return proxy.service;
359
+ }
360
+ }
361
+ }
362
+ }
363
+ return null;
364
+ }
365
+ /**
366
+ * Calculate timing information from query results
367
+ */
368
+ calculateTiming(queries, startTime) {
369
+ const mxQuery = queries.find(q => q.type === 'mx');
370
+ const txtQuery = queries.find(q => q.type === 'txt');
371
+ return {
372
+ mx: mxQuery?.timing || 0,
373
+ txt: txtQuery?.timing || 0,
374
+ total: Date.now() - startTime
375
+ };
376
+ }
377
+ /**
378
+ * Wrap a promise with a timeout
379
+ */
380
+ withTimeout(promise, ms) {
381
+ return new Promise((resolve, reject) => {
382
+ const timeout = setTimeout(() => reject(new Error(`DNS query timeout after ${ms}ms`)), ms);
383
+ promise
384
+ .then(resolve)
385
+ .catch(reject)
386
+ .finally(() => clearTimeout(timeout));
387
+ });
388
+ }
389
+ }
390
+ exports.ConcurrentDNSDetector = ConcurrentDNSDetector;
391
+ /**
392
+ * Factory function to create a concurrent DNS detector
393
+ */
394
+ function createConcurrentDNSDetector(providers, config) {
395
+ return new ConcurrentDNSDetector(providers, config);
396
+ }
397
+ /**
398
+ * Utility function for quick concurrent DNS detection
399
+ */
400
+ async function detectProviderConcurrent(domain, providers, config) {
401
+ const detector = createConcurrentDNSDetector(providers, config);
402
+ return detector.detectProvider(domain);
403
+ }