@mikkelscheike/email-provider-links 4.0.11 → 5.1.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 +62 -20
- package/dist/alias-detection.js +102 -8
- package/dist/api.d.ts +2 -41
- package/dist/api.js +147 -212
- package/dist/concurrent-dns.d.ts +7 -2
- package/dist/concurrent-dns.js +10 -3
- package/dist/constants.d.ts +26 -0
- package/dist/constants.js +29 -0
- package/dist/error-utils.d.ts +56 -0
- package/dist/error-utils.js +89 -0
- package/dist/hash-verifier.d.ts +7 -2
- package/dist/hash-verifier.js +12 -3
- package/dist/idn.js +10 -1
- package/dist/index.d.ts +14 -17
- package/dist/index.js +45 -34
- package/dist/provider-loader.d.ts +61 -2
- package/dist/provider-loader.js +134 -58
- package/dist/provider-store.d.ts +9 -0
- package/dist/provider-store.js +49 -0
- package/dist/url-validator.d.ts +12 -2
- package/dist/url-validator.js +32 -3
- package/package.json +3 -2
- package/dist/loader.d.ts +0 -45
- package/dist/loader.js +0 -163
package/dist/api.js
CHANGED
|
@@ -6,14 +6,81 @@
|
|
|
6
6
|
* Clean function names and enhanced error context.
|
|
7
7
|
*/
|
|
8
8
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
-
exports.Config = void 0;
|
|
9
|
+
exports.Config = exports.emailsMatch = exports.normalizeEmail = void 0;
|
|
10
10
|
exports.getEmailProvider = getEmailProvider;
|
|
11
11
|
exports.getEmailProviderSync = getEmailProviderSync;
|
|
12
|
-
exports.normalizeEmail = normalizeEmail;
|
|
13
|
-
exports.emailsMatch = emailsMatch;
|
|
14
12
|
exports.getEmailProviderFast = getEmailProviderFast;
|
|
15
13
|
const concurrent_dns_1 = require("./concurrent-dns");
|
|
16
14
|
const provider_loader_1 = require("./provider-loader");
|
|
15
|
+
const idn_1 = require("./idn");
|
|
16
|
+
const alias_detection_1 = require("./alias-detection");
|
|
17
|
+
let cachedProvidersRef = null;
|
|
18
|
+
let cachedDomainMap = null;
|
|
19
|
+
function getDomainMapFromProviders(providers) {
|
|
20
|
+
if (cachedProvidersRef === providers && cachedDomainMap) {
|
|
21
|
+
return cachedDomainMap;
|
|
22
|
+
}
|
|
23
|
+
const domainMap = new Map();
|
|
24
|
+
for (const loadedProvider of providers) {
|
|
25
|
+
for (const domain of loadedProvider.domains) {
|
|
26
|
+
domainMap.set(domain.toLowerCase(), loadedProvider);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
cachedProvidersRef = providers;
|
|
30
|
+
cachedDomainMap = domainMap;
|
|
31
|
+
return domainMap;
|
|
32
|
+
}
|
|
33
|
+
function validateAndParseEmailForLookup(email) {
|
|
34
|
+
if (!email || typeof email !== 'string') {
|
|
35
|
+
return {
|
|
36
|
+
ok: false,
|
|
37
|
+
email: email || '',
|
|
38
|
+
error: {
|
|
39
|
+
type: 'INVALID_EMAIL',
|
|
40
|
+
message: 'Email address is required and must be a string'
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
const trimmedEmail = email.trim();
|
|
45
|
+
// Strict validation: treat any IDN validation failure as invalid input.
|
|
46
|
+
// Only surface IDN_VALIDATION_ERROR for true encoding issues.
|
|
47
|
+
const idnError = (0, idn_1.validateInternationalEmail)(trimmedEmail);
|
|
48
|
+
if (idnError) {
|
|
49
|
+
if (idnError.code === idn_1.IDNValidationError.INVALID_ENCODING) {
|
|
50
|
+
return {
|
|
51
|
+
ok: false,
|
|
52
|
+
email: trimmedEmail,
|
|
53
|
+
error: {
|
|
54
|
+
type: 'IDN_VALIDATION_ERROR',
|
|
55
|
+
message: idnError.message,
|
|
56
|
+
idnError: idnError.code
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
ok: false,
|
|
62
|
+
email: trimmedEmail,
|
|
63
|
+
error: {
|
|
64
|
+
type: 'INVALID_EMAIL',
|
|
65
|
+
message: 'Invalid email format'
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
const atIndex = trimmedEmail.lastIndexOf('@');
|
|
70
|
+
if (atIndex === -1) {
|
|
71
|
+
return {
|
|
72
|
+
ok: false,
|
|
73
|
+
email: trimmedEmail,
|
|
74
|
+
error: {
|
|
75
|
+
type: 'INVALID_EMAIL',
|
|
76
|
+
message: 'Invalid email format'
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
const domainRaw = trimmedEmail.slice(atIndex + 1).toLowerCase();
|
|
81
|
+
const domain = (0, idn_1.domainToPunycode)(domainRaw);
|
|
82
|
+
return { ok: true, trimmedEmail, domain };
|
|
83
|
+
}
|
|
17
84
|
/**
|
|
18
85
|
* Get email provider information for any email address.
|
|
19
86
|
*
|
|
@@ -46,46 +113,28 @@ const provider_loader_1 = require("./provider-loader");
|
|
|
46
113
|
*/
|
|
47
114
|
async function getEmailProvider(email, timeout) {
|
|
48
115
|
try {
|
|
49
|
-
|
|
50
|
-
if (!
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
// Basic email format validation
|
|
62
|
-
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
63
|
-
if (!emailRegex.test(email)) {
|
|
64
|
-
return {
|
|
65
|
-
provider: null,
|
|
66
|
-
email,
|
|
67
|
-
loginUrl: null,
|
|
68
|
-
error: {
|
|
69
|
-
type: 'INVALID_EMAIL',
|
|
70
|
-
message: 'Invalid email format'
|
|
71
|
-
}
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
const domain = email.split('@')[1]?.toLowerCase();
|
|
75
|
-
if (!domain) {
|
|
116
|
+
const parsed = validateAndParseEmailForLookup(email);
|
|
117
|
+
if (!parsed.ok) {
|
|
118
|
+
// Try to normalize even invalid emails (may help with some edge cases)
|
|
119
|
+
let normalizedEmail = parsed.email;
|
|
120
|
+
try {
|
|
121
|
+
normalizedEmail = (0, alias_detection_1.normalizeEmail)(parsed.email);
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
// If normalization fails, use original email
|
|
125
|
+
}
|
|
76
126
|
return {
|
|
77
127
|
provider: null,
|
|
78
|
-
email,
|
|
128
|
+
email: normalizedEmail,
|
|
79
129
|
loginUrl: null,
|
|
80
|
-
error:
|
|
81
|
-
type: 'INVALID_EMAIL',
|
|
82
|
-
message: 'Invalid email format - missing domain'
|
|
83
|
-
}
|
|
130
|
+
error: parsed.error
|
|
84
131
|
};
|
|
85
132
|
}
|
|
133
|
+
const domain = parsed.domain;
|
|
86
134
|
// First try synchronous domain matching
|
|
87
135
|
const syncResult = getEmailProviderSync(email);
|
|
88
136
|
if (syncResult.provider) {
|
|
137
|
+
// Email is already normalized in getEmailProviderSync
|
|
89
138
|
return {
|
|
90
139
|
...syncResult,
|
|
91
140
|
detectionMethod: 'domain_match'
|
|
@@ -110,9 +159,18 @@ async function getEmailProvider(email, timeout) {
|
|
|
110
159
|
enableParallel: true,
|
|
111
160
|
collectDebugInfo: false
|
|
112
161
|
});
|
|
162
|
+
// Normalize email using alias detection (even if no provider found)
|
|
163
|
+
// This ensures consistent email format regardless of provider detection result
|
|
164
|
+
let normalizedEmail = email;
|
|
165
|
+
try {
|
|
166
|
+
normalizedEmail = (0, alias_detection_1.normalizeEmail)(email);
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
// If normalization fails, use original email
|
|
170
|
+
}
|
|
113
171
|
const result = {
|
|
114
172
|
provider: concurrentResult.provider,
|
|
115
|
-
email,
|
|
173
|
+
email: normalizedEmail,
|
|
116
174
|
loginUrl: concurrentResult.provider?.loginUrl || null,
|
|
117
175
|
detectionMethod: concurrentResult.detectionMethod || 'mx_record'
|
|
118
176
|
};
|
|
@@ -130,9 +188,9 @@ async function getEmailProvider(email, timeout) {
|
|
|
130
188
|
}
|
|
131
189
|
catch (error) {
|
|
132
190
|
// Enhanced error handling
|
|
133
|
-
if (error.message
|
|
191
|
+
if (error instanceof Error && error.message.includes('Rate limit exceeded')) {
|
|
134
192
|
const retryMatch = error.message.match(/Try again in (\d+) seconds/);
|
|
135
|
-
const retryAfter = retryMatch ? parseInt(retryMatch[1], 10) : undefined;
|
|
193
|
+
const retryAfter = retryMatch?.[1] ? parseInt(retryMatch[1], 10) : undefined;
|
|
136
194
|
return {
|
|
137
195
|
provider: null,
|
|
138
196
|
email,
|
|
@@ -144,7 +202,7 @@ async function getEmailProvider(email, timeout) {
|
|
|
144
202
|
}
|
|
145
203
|
};
|
|
146
204
|
}
|
|
147
|
-
if (error.message
|
|
205
|
+
if (error instanceof Error && error.message.includes('timeout')) {
|
|
148
206
|
return {
|
|
149
207
|
provider: null,
|
|
150
208
|
email,
|
|
@@ -161,7 +219,7 @@ async function getEmailProvider(email, timeout) {
|
|
|
161
219
|
loginUrl: null,
|
|
162
220
|
error: {
|
|
163
221
|
type: 'NETWORK_ERROR',
|
|
164
|
-
message: error.message
|
|
222
|
+
message: error instanceof Error ? error.message : 'Unknown network error'
|
|
165
223
|
}
|
|
166
224
|
};
|
|
167
225
|
}
|
|
@@ -189,44 +247,24 @@ async function getEmailProvider(email, timeout) {
|
|
|
189
247
|
*/
|
|
190
248
|
function getEmailProviderSync(email) {
|
|
191
249
|
try {
|
|
192
|
-
|
|
193
|
-
if (!
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
};
|
|
203
|
-
}
|
|
204
|
-
// Basic email format validation
|
|
205
|
-
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
206
|
-
if (!emailRegex.test(email)) {
|
|
207
|
-
return {
|
|
208
|
-
provider: null,
|
|
209
|
-
email,
|
|
210
|
-
loginUrl: null,
|
|
211
|
-
error: {
|
|
212
|
-
type: 'INVALID_EMAIL',
|
|
213
|
-
message: 'Invalid email format'
|
|
214
|
-
}
|
|
215
|
-
};
|
|
216
|
-
}
|
|
217
|
-
// Pure synchronous domain matching
|
|
218
|
-
const domain = email.split('@')[1]?.toLowerCase();
|
|
219
|
-
if (!domain) {
|
|
250
|
+
const parsed = validateAndParseEmailForLookup(email);
|
|
251
|
+
if (!parsed.ok) {
|
|
252
|
+
// Try to normalize even invalid emails (may help with some edge cases)
|
|
253
|
+
let normalizedEmail = parsed.email;
|
|
254
|
+
try {
|
|
255
|
+
normalizedEmail = (0, alias_detection_1.normalizeEmail)(parsed.email);
|
|
256
|
+
}
|
|
257
|
+
catch {
|
|
258
|
+
// If normalization fails, use original email
|
|
259
|
+
}
|
|
220
260
|
return {
|
|
221
261
|
provider: null,
|
|
222
|
-
email,
|
|
262
|
+
email: normalizedEmail,
|
|
223
263
|
loginUrl: null,
|
|
224
|
-
error:
|
|
225
|
-
type: 'INVALID_EMAIL',
|
|
226
|
-
message: 'Invalid email format - missing domain'
|
|
227
|
-
}
|
|
264
|
+
error: parsed.error
|
|
228
265
|
};
|
|
229
266
|
}
|
|
267
|
+
const domain = parsed.domain;
|
|
230
268
|
// Load providers with verification
|
|
231
269
|
let provider = null;
|
|
232
270
|
try {
|
|
@@ -246,13 +284,7 @@ function getEmailProviderSync(email) {
|
|
|
246
284
|
}
|
|
247
285
|
};
|
|
248
286
|
}
|
|
249
|
-
const domainMap =
|
|
250
|
-
// Build domain map from loaded providers
|
|
251
|
-
for (const loadedProvider of result.providers) {
|
|
252
|
-
for (const domain of loadedProvider.domains) {
|
|
253
|
-
domainMap.set(domain.toLowerCase(), loadedProvider);
|
|
254
|
-
}
|
|
255
|
-
}
|
|
287
|
+
const domainMap = getDomainMapFromProviders(result.providers);
|
|
256
288
|
provider = domainMap.get(domain) || null;
|
|
257
289
|
}
|
|
258
290
|
catch (error) {
|
|
@@ -269,9 +301,18 @@ function getEmailProviderSync(email) {
|
|
|
269
301
|
}
|
|
270
302
|
};
|
|
271
303
|
}
|
|
304
|
+
// Normalize email using alias detection (even if no provider found)
|
|
305
|
+
// This ensures consistent email format regardless of provider detection result
|
|
306
|
+
let normalizedEmail = email;
|
|
307
|
+
try {
|
|
308
|
+
normalizedEmail = (0, alias_detection_1.normalizeEmail)(email);
|
|
309
|
+
}
|
|
310
|
+
catch {
|
|
311
|
+
// If normalization fails, use original email
|
|
312
|
+
}
|
|
272
313
|
const result = {
|
|
273
314
|
provider: provider || null,
|
|
274
|
-
email,
|
|
315
|
+
email: normalizedEmail,
|
|
275
316
|
loginUrl: provider?.loginUrl || null,
|
|
276
317
|
detectionMethod: 'domain_match'
|
|
277
318
|
};
|
|
@@ -291,105 +332,15 @@ function getEmailProviderSync(email) {
|
|
|
291
332
|
loginUrl: null,
|
|
292
333
|
error: {
|
|
293
334
|
type: 'INVALID_EMAIL',
|
|
294
|
-
message: error.message
|
|
335
|
+
message: error instanceof Error ? error.message : 'Invalid email address'
|
|
295
336
|
}
|
|
296
337
|
};
|
|
297
338
|
}
|
|
298
339
|
}
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
* - Gmail: removes dots and plus addressing
|
|
304
|
-
* - Other providers: removes plus addressing only
|
|
305
|
-
*
|
|
306
|
-
* @param email - The email address to normalize
|
|
307
|
-
* @returns The canonical email address
|
|
308
|
-
*
|
|
309
|
-
* @example
|
|
310
|
-
* ```typescript
|
|
311
|
-
* const canonical = normalizeEmail('L.O.C.A.L+work@DOMAIN.TLD');
|
|
312
|
-
* console.log(canonical); // 'local@domain.tld'
|
|
313
|
-
*
|
|
314
|
-
* const provider = normalizeEmail('local+newsletter@provider.tld');
|
|
315
|
-
* console.log(provider); // 'local@provider.tld'
|
|
316
|
-
* ```
|
|
317
|
-
*/
|
|
318
|
-
function normalizeEmail(email) {
|
|
319
|
-
if (!email || typeof email !== 'string') {
|
|
320
|
-
return email;
|
|
321
|
-
}
|
|
322
|
-
// Convert to lowercase
|
|
323
|
-
const lowercaseEmail = email.toLowerCase().trim();
|
|
324
|
-
// Split email into local and domain parts
|
|
325
|
-
const atIndex = lowercaseEmail.lastIndexOf('@');
|
|
326
|
-
if (atIndex === -1) {
|
|
327
|
-
return lowercaseEmail;
|
|
328
|
-
}
|
|
329
|
-
let localPart = lowercaseEmail.slice(0, atIndex);
|
|
330
|
-
const domainPart = lowercaseEmail.slice(atIndex + 1);
|
|
331
|
-
// Use providers for domain lookup
|
|
332
|
-
let provider = null;
|
|
333
|
-
try {
|
|
334
|
-
const result = (0, provider_loader_1.loadProviders)();
|
|
335
|
-
if (!result.success) {
|
|
336
|
-
return lowercaseEmail; // Return as-is if providers can't be loaded
|
|
337
|
-
}
|
|
338
|
-
const domainMap = new Map();
|
|
339
|
-
for (const loadedProvider of result.providers) {
|
|
340
|
-
for (const domain of loadedProvider.domains) {
|
|
341
|
-
domainMap.set(domain.toLowerCase(), loadedProvider);
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
provider = domainMap.get(domainPart) || null;
|
|
345
|
-
}
|
|
346
|
-
catch (error) {
|
|
347
|
-
return lowercaseEmail; // Return as-is if error occurs
|
|
348
|
-
}
|
|
349
|
-
if (provider?.alias) {
|
|
350
|
-
// Provider supports aliasing
|
|
351
|
-
if (provider.alias.dots) {
|
|
352
|
-
// Remove all dots from local part (e.g. Gmail)
|
|
353
|
-
localPart = localPart.replace(/\./g, '');
|
|
354
|
-
}
|
|
355
|
-
if (provider.alias.plus) {
|
|
356
|
-
// Remove plus addressing (everything after +)
|
|
357
|
-
const plusIndex = localPart.indexOf('+');
|
|
358
|
-
if (plusIndex !== -1) {
|
|
359
|
-
localPart = localPart.slice(0, plusIndex);
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
return `${localPart}@${domainPart}`;
|
|
364
|
-
}
|
|
365
|
-
/**
|
|
366
|
-
* Check if two email addresses are the same person (accounting for aliases).
|
|
367
|
-
*
|
|
368
|
-
* This normalizes both emails and compares their canonical forms.
|
|
369
|
-
* Useful for preventing duplicate accounts and matching login attempts.
|
|
370
|
-
*
|
|
371
|
-
* @param email1 - First email address
|
|
372
|
-
* @param email2 - Second email address
|
|
373
|
-
* @returns true if the emails represent the same person
|
|
374
|
-
*
|
|
375
|
-
* @example
|
|
376
|
-
* ```typescript
|
|
377
|
-
* const match = emailsMatch('local@domain.tld', 'l.o.c.a.l+work@domain.tld');
|
|
378
|
-
* console.log(match); // true
|
|
379
|
-
*
|
|
380
|
-
* const different = emailsMatch('local@domain.tld', 'other@domain.tld');
|
|
381
|
-
* console.log(different); // false
|
|
382
|
-
* ```
|
|
383
|
-
*/
|
|
384
|
-
function emailsMatch(email1, email2) {
|
|
385
|
-
if (!email1 || !email2 || typeof email1 !== 'string' || typeof email2 !== 'string') {
|
|
386
|
-
return false;
|
|
387
|
-
}
|
|
388
|
-
// Normalize both emails and compare
|
|
389
|
-
const normalized1 = normalizeEmail(email1);
|
|
390
|
-
const normalized2 = normalizeEmail(email2);
|
|
391
|
-
return normalized1 === normalized2;
|
|
392
|
-
}
|
|
340
|
+
// Re-export alias detection functions from the dedicated module
|
|
341
|
+
var alias_detection_2 = require("./alias-detection");
|
|
342
|
+
Object.defineProperty(exports, "normalizeEmail", { enumerable: true, get: function () { return alias_detection_2.normalizeEmail; } });
|
|
343
|
+
Object.defineProperty(exports, "emailsMatch", { enumerable: true, get: function () { return alias_detection_2.emailsMatch; } });
|
|
393
344
|
/**
|
|
394
345
|
* Enhanced email provider detection with concurrent DNS for maximum performance.
|
|
395
346
|
* This function uses parallel MX/TXT lookups for 2x faster business domain detection.
|
|
@@ -414,46 +365,21 @@ function emailsMatch(email1, email2) {
|
|
|
414
365
|
async function getEmailProviderFast(email, options = {}) {
|
|
415
366
|
const { timeout = 5000, enableParallel = true, collectDebugInfo = false } = options;
|
|
416
367
|
try {
|
|
417
|
-
|
|
418
|
-
if (!
|
|
368
|
+
const parsed = validateAndParseEmailForLookup(email);
|
|
369
|
+
if (!parsed.ok) {
|
|
419
370
|
return {
|
|
420
371
|
provider: null,
|
|
421
|
-
email: email
|
|
372
|
+
email: parsed.email,
|
|
422
373
|
loginUrl: null,
|
|
423
|
-
error:
|
|
424
|
-
type: 'INVALID_EMAIL',
|
|
425
|
-
message: 'Email address is required and must be a string'
|
|
426
|
-
}
|
|
427
|
-
};
|
|
428
|
-
}
|
|
429
|
-
// Basic email format validation
|
|
430
|
-
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
431
|
-
if (!emailRegex.test(email)) {
|
|
432
|
-
return {
|
|
433
|
-
provider: null,
|
|
434
|
-
email,
|
|
435
|
-
loginUrl: null,
|
|
436
|
-
error: {
|
|
437
|
-
type: 'INVALID_EMAIL',
|
|
438
|
-
message: 'Invalid email format'
|
|
439
|
-
}
|
|
440
|
-
};
|
|
441
|
-
}
|
|
442
|
-
const domain = email.split('@')[1]?.toLowerCase();
|
|
443
|
-
if (!domain) {
|
|
444
|
-
return {
|
|
445
|
-
provider: null,
|
|
446
|
-
email,
|
|
447
|
-
loginUrl: null,
|
|
448
|
-
error: {
|
|
449
|
-
type: 'INVALID_EMAIL',
|
|
450
|
-
message: 'Invalid email format - missing domain'
|
|
451
|
-
}
|
|
374
|
+
error: parsed.error
|
|
452
375
|
};
|
|
453
376
|
}
|
|
377
|
+
const domain = parsed.domain;
|
|
378
|
+
const trimmedEmail = parsed.trimmedEmail;
|
|
454
379
|
// First try standard domain matching (fast path)
|
|
455
|
-
const syncResult = getEmailProviderSync(
|
|
380
|
+
const syncResult = getEmailProviderSync(trimmedEmail);
|
|
456
381
|
if (syncResult.provider) {
|
|
382
|
+
// Email is already normalized in getEmailProviderSync
|
|
457
383
|
return {
|
|
458
384
|
...syncResult,
|
|
459
385
|
detectionMethod: 'domain_match',
|
|
@@ -466,7 +392,7 @@ async function getEmailProviderFast(email, options = {}) {
|
|
|
466
392
|
if (!result.success) {
|
|
467
393
|
return {
|
|
468
394
|
provider: null,
|
|
469
|
-
email,
|
|
395
|
+
email: trimmedEmail,
|
|
470
396
|
loginUrl: null,
|
|
471
397
|
error: {
|
|
472
398
|
type: 'NETWORK_ERROR',
|
|
@@ -480,9 +406,18 @@ async function getEmailProviderFast(email, options = {}) {
|
|
|
480
406
|
enableParallel,
|
|
481
407
|
collectDebugInfo
|
|
482
408
|
});
|
|
409
|
+
// Normalize email using alias detection (even if no provider found)
|
|
410
|
+
// This ensures consistent email format regardless of provider detection result
|
|
411
|
+
let normalizedEmail = trimmedEmail;
|
|
412
|
+
try {
|
|
413
|
+
normalizedEmail = (0, alias_detection_1.normalizeEmail)(trimmedEmail);
|
|
414
|
+
}
|
|
415
|
+
catch {
|
|
416
|
+
// If normalization fails, use original email
|
|
417
|
+
}
|
|
483
418
|
const fastResult = {
|
|
484
419
|
provider: concurrentResult.provider,
|
|
485
|
-
email,
|
|
420
|
+
email: normalizedEmail,
|
|
486
421
|
loginUrl: concurrentResult.provider?.loginUrl || null,
|
|
487
422
|
detectionMethod: concurrentResult.detectionMethod || 'mx_record',
|
|
488
423
|
timing: concurrentResult.timing,
|
|
@@ -505,7 +440,7 @@ async function getEmailProviderFast(email, options = {}) {
|
|
|
505
440
|
loginUrl: null,
|
|
506
441
|
error: {
|
|
507
442
|
type: 'NETWORK_ERROR',
|
|
508
|
-
message: error.message
|
|
443
|
+
message: error instanceof Error ? error.message : 'DNS detection failed'
|
|
509
444
|
}
|
|
510
445
|
};
|
|
511
446
|
}
|
package/dist/concurrent-dns.d.ts
CHANGED
|
@@ -5,6 +5,10 @@
|
|
|
5
5
|
* Uses Promise.allSettled for fault tolerance and intelligent result merging.
|
|
6
6
|
*/
|
|
7
7
|
import { EmailProvider } from './api';
|
|
8
|
+
type MXRecordLike = {
|
|
9
|
+
exchange?: string;
|
|
10
|
+
};
|
|
11
|
+
type TXTRecordLike = string;
|
|
8
12
|
/**
|
|
9
13
|
* Configuration for concurrent DNS detection
|
|
10
14
|
*/
|
|
@@ -29,13 +33,13 @@ export interface DNSQueryResult {
|
|
|
29
33
|
/** Whether the query succeeded */
|
|
30
34
|
success: boolean;
|
|
31
35
|
/** DNS records returned (if successful) */
|
|
32
|
-
records?:
|
|
36
|
+
records?: MXRecordLike[] | TXTRecordLike[];
|
|
33
37
|
/** Error information (if failed) */
|
|
34
38
|
error?: Error;
|
|
35
39
|
/** Query execution time in milliseconds */
|
|
36
40
|
timing: number;
|
|
37
41
|
/** Raw DNS response for debugging */
|
|
38
|
-
rawResponse?:
|
|
42
|
+
rawResponse?: unknown;
|
|
39
43
|
}
|
|
40
44
|
/**
|
|
41
45
|
* Result from concurrent DNS detection
|
|
@@ -130,4 +134,5 @@ export declare function createConcurrentDNSDetector(providers: EmailProvider[],
|
|
|
130
134
|
* Utility function for quick concurrent DNS detection
|
|
131
135
|
*/
|
|
132
136
|
export declare function detectProviderConcurrent(domain: string, providers: EmailProvider[], config?: Partial<ConcurrentDNSConfig>): Promise<ConcurrentDNSResult>;
|
|
137
|
+
export {};
|
|
133
138
|
//# sourceMappingURL=concurrent-dns.d.ts.map
|
package/dist/concurrent-dns.js
CHANGED
|
@@ -295,6 +295,8 @@ class ConcurrentDNSDetector {
|
|
|
295
295
|
let confidence = 0;
|
|
296
296
|
if (query.type === 'mx' && detection.mxPatterns) {
|
|
297
297
|
for (const record of query.records) {
|
|
298
|
+
if (typeof record !== 'object' || record === null)
|
|
299
|
+
continue;
|
|
298
300
|
const exchange = record.exchange?.toLowerCase() || '';
|
|
299
301
|
for (const pattern of detection.mxPatterns) {
|
|
300
302
|
if (exchange.includes(pattern.toLowerCase())) {
|
|
@@ -306,6 +308,8 @@ class ConcurrentDNSDetector {
|
|
|
306
308
|
}
|
|
307
309
|
else if (query.type === 'txt' && detection.txtPatterns) {
|
|
308
310
|
for (const record of query.records) {
|
|
311
|
+
if (typeof record !== 'string')
|
|
312
|
+
continue;
|
|
309
313
|
const txtRecord = record.toLowerCase();
|
|
310
314
|
for (const pattern of detection.txtPatterns) {
|
|
311
315
|
if (txtRecord.includes(pattern.toLowerCase())) {
|
|
@@ -368,6 +372,8 @@ class ConcurrentDNSDetector {
|
|
|
368
372
|
if (!mxQuery?.records)
|
|
369
373
|
return null;
|
|
370
374
|
for (const record of mxQuery.records) {
|
|
375
|
+
if (typeof record !== 'object' || record === null)
|
|
376
|
+
continue;
|
|
371
377
|
const exchange = record.exchange?.toLowerCase() || '';
|
|
372
378
|
for (const provider of this.providers) {
|
|
373
379
|
if (provider.type === 'proxy_service' && provider.customDomainDetection?.mxPatterns) {
|
|
@@ -402,9 +408,11 @@ class ConcurrentDNSDetector {
|
|
|
402
408
|
return Promise.reject(new Error(`DNS query timeout after ${ms}ms`));
|
|
403
409
|
}
|
|
404
410
|
let rejectFn;
|
|
411
|
+
let queryEntry;
|
|
405
412
|
const wrappedPromise = new Promise((resolve, reject) => {
|
|
406
413
|
rejectFn = reject;
|
|
407
|
-
const timeout = setTimeout(() => reject(new Error(`DNS query timeout after ${ms}ms`)), ms)
|
|
414
|
+
const timeout = setTimeout(() => reject(new Error(`DNS query timeout after ${ms}ms`)), ms);
|
|
415
|
+
timeout.unref?.();
|
|
408
416
|
// Start the underlying operation only after setting up the timeout
|
|
409
417
|
fn()
|
|
410
418
|
.then(resolve)
|
|
@@ -412,7 +420,6 @@ class ConcurrentDNSDetector {
|
|
|
412
420
|
.finally(() => {
|
|
413
421
|
clearTimeout(timeout);
|
|
414
422
|
// Clean up active query
|
|
415
|
-
const queryEntry = Array.from(this.activeQueries).find(entry => entry.promise === wrappedPromise);
|
|
416
423
|
if (queryEntry) {
|
|
417
424
|
this.activeQueries.delete(queryEntry);
|
|
418
425
|
}
|
|
@@ -420,7 +427,7 @@ class ConcurrentDNSDetector {
|
|
|
420
427
|
});
|
|
421
428
|
// Track active query for potential cleanup in tests
|
|
422
429
|
if (rejectFn) {
|
|
423
|
-
|
|
430
|
+
queryEntry = { promise: wrappedPromise, reject: rejectFn };
|
|
424
431
|
this.activeQueries.add(queryEntry);
|
|
425
432
|
}
|
|
426
433
|
return wrappedPromise;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Email validation constants based on RFC 5321
|
|
3
|
+
*
|
|
4
|
+
* These limits are defined in RFC 5321 Section 4.5.3.1:
|
|
5
|
+
* - Maximum total email length: 254 characters
|
|
6
|
+
* - Maximum local part length: 64 characters
|
|
7
|
+
* - Maximum domain length: 253 characters
|
|
8
|
+
*/
|
|
9
|
+
export declare const EmailLimits: {
|
|
10
|
+
/** Maximum total email address length (local + @ + domain) */
|
|
11
|
+
readonly MAX_EMAIL_LENGTH: 254;
|
|
12
|
+
/** Maximum local part length (before @) */
|
|
13
|
+
readonly MAX_LOCAL_PART_LENGTH: 64;
|
|
14
|
+
/** Maximum domain length (after @) */
|
|
15
|
+
readonly MAX_DOMAIN_LENGTH: 253;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Memory calculation constants
|
|
19
|
+
*/
|
|
20
|
+
export declare const MemoryConstants: {
|
|
21
|
+
/** Bytes per kilobyte */
|
|
22
|
+
readonly BYTES_PER_KB: 1024;
|
|
23
|
+
/** Kilobytes per megabyte */
|
|
24
|
+
readonly KB_PER_MB: 1024;
|
|
25
|
+
};
|
|
26
|
+
//# sourceMappingURL=constants.d.ts.map
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MemoryConstants = exports.EmailLimits = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Email validation constants based on RFC 5321
|
|
6
|
+
*
|
|
7
|
+
* These limits are defined in RFC 5321 Section 4.5.3.1:
|
|
8
|
+
* - Maximum total email length: 254 characters
|
|
9
|
+
* - Maximum local part length: 64 characters
|
|
10
|
+
* - Maximum domain length: 253 characters
|
|
11
|
+
*/
|
|
12
|
+
exports.EmailLimits = {
|
|
13
|
+
/** Maximum total email address length (local + @ + domain) */
|
|
14
|
+
MAX_EMAIL_LENGTH: 254,
|
|
15
|
+
/** Maximum local part length (before @) */
|
|
16
|
+
MAX_LOCAL_PART_LENGTH: 64,
|
|
17
|
+
/** Maximum domain length (after @) */
|
|
18
|
+
MAX_DOMAIN_LENGTH: 253,
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Memory calculation constants
|
|
22
|
+
*/
|
|
23
|
+
exports.MemoryConstants = {
|
|
24
|
+
/** Bytes per kilobyte */
|
|
25
|
+
BYTES_PER_KB: 1024,
|
|
26
|
+
/** Kilobytes per megabyte */
|
|
27
|
+
KB_PER_MB: 1024,
|
|
28
|
+
};
|
|
29
|
+
//# sourceMappingURL=constants.js.map
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error handling utilities
|
|
3
|
+
*
|
|
4
|
+
* Provides consistent error handling patterns across the codebase.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Extracts a human-readable error message from an unknown error value.
|
|
8
|
+
*
|
|
9
|
+
* @param error - The error value (Error, string, or unknown)
|
|
10
|
+
* @returns A string error message
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```typescript
|
|
14
|
+
* try {
|
|
15
|
+
* // some operation
|
|
16
|
+
* } catch (error: unknown) {
|
|
17
|
+
* const message = getErrorMessage(error);
|
|
18
|
+
* console.error(message);
|
|
19
|
+
* }
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export declare function getErrorMessage(error: unknown): string;
|
|
23
|
+
/**
|
|
24
|
+
* Extracts an error code from an unknown error value.
|
|
25
|
+
*
|
|
26
|
+
* @param error - The error value
|
|
27
|
+
* @returns The error code if available, undefined otherwise
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```typescript
|
|
31
|
+
* try {
|
|
32
|
+
* // some operation
|
|
33
|
+
* } catch (error: unknown) {
|
|
34
|
+
* const code = getErrorCode(error);
|
|
35
|
+
* if (code === 'ENOENT') {
|
|
36
|
+
* // handle file not found
|
|
37
|
+
* }
|
|
38
|
+
* }
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
export declare function getErrorCode(error: unknown): string | undefined;
|
|
42
|
+
/**
|
|
43
|
+
* Checks if an error is a file not found error (ENOENT).
|
|
44
|
+
*
|
|
45
|
+
* @param error - The error value
|
|
46
|
+
* @returns true if the error is a file not found error
|
|
47
|
+
*/
|
|
48
|
+
export declare function isFileNotFoundError(error: unknown): boolean;
|
|
49
|
+
/**
|
|
50
|
+
* Checks if an error is a JSON parsing error.
|
|
51
|
+
*
|
|
52
|
+
* @param error - The error value
|
|
53
|
+
* @returns true if the error is a JSON parsing error
|
|
54
|
+
*/
|
|
55
|
+
export declare function isJsonError(error: unknown): boolean;
|
|
56
|
+
//# sourceMappingURL=error-utils.d.ts.map
|