@mikkelscheike/email-provider-links 1.7.0 → 1.9.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.
- package/README.md +106 -99
- package/dist/alias-detection.d.ts +30 -47
- package/dist/alias-detection.js +29 -104
- package/dist/api.d.ts +172 -0
- package/dist/api.js +439 -0
- package/dist/concurrent-dns.d.ts +130 -0
- package/dist/concurrent-dns.js +403 -0
- package/dist/index.d.ts +45 -353
- package/dist/index.js +65 -596
- package/dist/loader.d.ts +44 -0
- package/dist/loader.js +155 -0
- package/dist/schema.d.ts +66 -0
- package/dist/schema.js +73 -0
- package/dist/security/hash-verifier.js +2 -2
- package/package.json +2 -2
- package/providers/emailproviders.json +530 -479
|
@@ -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
|
+
}
|