@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.
- 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
package/dist/index.js
CHANGED
|
@@ -2,625 +2,94 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Email Provider Links
|
|
4
4
|
*
|
|
5
|
-
* A
|
|
6
|
-
*
|
|
5
|
+
* A clean, modern email provider detection library with:
|
|
6
|
+
* - 93+ verified email providers covering 180+ domains
|
|
7
|
+
* - Concurrent DNS detection for business domains
|
|
8
|
+
* - Zero runtime dependencies
|
|
9
|
+
* - Comprehensive error handling
|
|
10
|
+
* - Email alias normalization
|
|
7
11
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* 2. DNS-based detection for custom business domains using MX/TXT record analysis
|
|
11
|
-
*
|
|
12
|
-
* @packageDocumentation
|
|
12
|
+
* @author Email Provider Links Team
|
|
13
|
+
* @license MIT
|
|
13
14
|
*/
|
|
14
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
15
|
-
if (k2 === undefined) k2 = k;
|
|
16
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
17
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
18
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
19
|
-
}
|
|
20
|
-
Object.defineProperty(o, k2, desc);
|
|
21
|
-
}) : (function(o, m, k, k2) {
|
|
22
|
-
if (k2 === undefined) k2 = k;
|
|
23
|
-
o[k2] = m[k];
|
|
24
|
-
}));
|
|
25
|
-
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
26
|
-
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
27
|
-
};
|
|
28
15
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
|
-
exports.
|
|
30
|
-
exports.isValidEmail = isValidEmail;
|
|
31
|
-
exports.extractDomain = extractDomain;
|
|
32
|
-
exports.findEmailProvider = findEmailProvider;
|
|
33
|
-
exports.getEmailProviderLink = getEmailProviderLink;
|
|
16
|
+
exports.DOMAIN_COUNT = exports.PROVIDER_COUNT = exports.detectProviderConcurrent = exports.loadProviders = exports.Config = exports.emailsMatch = exports.normalizeEmail = exports.getEmailProviderFast = exports.getEmailProviderSync = exports.getEmailProvider = void 0;
|
|
34
17
|
exports.getSupportedProviders = getSupportedProviders;
|
|
35
18
|
exports.isEmailProviderSupported = isEmailProviderSupported;
|
|
36
|
-
exports.
|
|
37
|
-
exports.
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
this.maxRequests = maxRequests;
|
|
61
|
-
this.windowMs = windowMs;
|
|
62
|
-
this.requestTimestamps = [];
|
|
63
|
-
}
|
|
64
|
-
/**
|
|
65
|
-
* Checks if a request is allowed under the current rate limit.
|
|
66
|
-
* @returns true if request is allowed, false if rate limited
|
|
67
|
-
*/
|
|
68
|
-
isAllowed() {
|
|
69
|
-
const now = Date.now();
|
|
70
|
-
// Remove old timestamps outside the current window
|
|
71
|
-
this.requestTimestamps = this.requestTimestamps.filter(timestamp => now - timestamp < this.windowMs);
|
|
72
|
-
// Check if we're under the limit
|
|
73
|
-
if (this.requestTimestamps.length < this.maxRequests) {
|
|
74
|
-
this.requestTimestamps.push(now);
|
|
75
|
-
return true;
|
|
76
|
-
}
|
|
77
|
-
return false;
|
|
78
|
-
}
|
|
79
|
-
/**
|
|
80
|
-
* Gets the current number of requests in the window.
|
|
81
|
-
*/
|
|
82
|
-
getCurrentCount() {
|
|
83
|
-
const now = Date.now();
|
|
84
|
-
this.requestTimestamps = this.requestTimestamps.filter(timestamp => now - timestamp < this.windowMs);
|
|
85
|
-
return this.requestTimestamps.length;
|
|
86
|
-
}
|
|
87
|
-
/**
|
|
88
|
-
* Gets the time until the rate limit resets (when oldest request expires).
|
|
89
|
-
* @returns milliseconds until reset, or 0 if not rate limited
|
|
90
|
-
*/
|
|
91
|
-
getTimeUntilReset() {
|
|
92
|
-
if (this.requestTimestamps.length === 0)
|
|
93
|
-
return 0;
|
|
94
|
-
const oldestTimestamp = Math.min(...this.requestTimestamps);
|
|
95
|
-
const resetTime = oldestTimestamp + this.windowMs;
|
|
96
|
-
const now = Date.now();
|
|
97
|
-
return Math.max(0, resetTime - now);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
// Global rate limiter instance
|
|
101
|
-
const dnsRateLimiter = new SimpleRateLimiter();
|
|
102
|
-
/**
|
|
103
|
-
* Creates a Promise that rejects after the specified timeout.
|
|
104
|
-
*
|
|
105
|
-
* @param ms - Timeout in milliseconds
|
|
106
|
-
* @returns Promise that rejects with timeout error and a cleanup function
|
|
107
|
-
* @internal
|
|
108
|
-
*/
|
|
109
|
-
function createTimeout(ms) {
|
|
110
|
-
let timeoutId;
|
|
111
|
-
const promise = new Promise((_, reject) => {
|
|
112
|
-
timeoutId = setTimeout(() => reject(new Error(`DNS query timeout after ${ms}ms`)), ms);
|
|
113
|
-
});
|
|
114
|
-
const cleanup = () => {
|
|
115
|
-
if (timeoutId) {
|
|
116
|
-
clearTimeout(timeoutId);
|
|
117
|
-
}
|
|
118
|
-
};
|
|
119
|
-
return { promise, cleanup };
|
|
120
|
-
}
|
|
121
|
-
/**
|
|
122
|
-
* Wraps a DNS query with a timeout.
|
|
123
|
-
*
|
|
124
|
-
* @param promise - The DNS query promise
|
|
125
|
-
* @param timeoutMs - Timeout in milliseconds
|
|
126
|
-
* @returns Promise that resolves with DNS result or rejects on timeout
|
|
127
|
-
* @internal
|
|
128
|
-
*/
|
|
129
|
-
function withTimeout(promise, timeoutMs) {
|
|
130
|
-
const { promise: timeoutPromise, cleanup } = createTimeout(timeoutMs);
|
|
131
|
-
return Promise.race([
|
|
132
|
-
promise.finally(() => cleanup()), // Clean up timeout when original promise settles
|
|
133
|
-
timeoutPromise
|
|
134
|
-
]);
|
|
135
|
-
}
|
|
136
|
-
// Load providers from external JSON file
|
|
137
|
-
let EMAIL_PROVIDERS = [];
|
|
138
|
-
// Performance optimization: Create a domain-to-provider Map for O(1) lookups
|
|
139
|
-
let DOMAIN_TO_PROVIDER_MAP = new Map();
|
|
140
|
-
/**
|
|
141
|
-
* Builds a Map for fast domain-to-provider lookups.
|
|
142
|
-
* This optimization improves lookup performance from O(n*m) to O(1)
|
|
143
|
-
* where n = number of providers, m = average domains per provider.
|
|
144
|
-
*
|
|
145
|
-
* @internal
|
|
146
|
-
*/
|
|
147
|
-
function buildDomainMap() {
|
|
148
|
-
DOMAIN_TO_PROVIDER_MAP.clear();
|
|
149
|
-
for (const provider of EMAIL_PROVIDERS) {
|
|
150
|
-
for (const domain of provider.domains) {
|
|
151
|
-
DOMAIN_TO_PROVIDER_MAP.set(domain.toLowerCase(), provider);
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
try {
|
|
156
|
-
const providersPath = (0, path_1.join)(__dirname, '..', 'providers', 'emailproviders.json');
|
|
157
|
-
const providersData = JSON.parse((0, fs_1.readFileSync)(providersPath, 'utf8'));
|
|
158
|
-
EMAIL_PROVIDERS = providersData.providers;
|
|
159
|
-
buildDomainMap(); // Build optimized lookup map
|
|
160
|
-
}
|
|
161
|
-
catch (error) {
|
|
162
|
-
// Fallback to hardcoded providers if JSON file is not found
|
|
163
|
-
console.warn('Could not load providers from JSON file, using fallback providers', error instanceof Error ? error.message : 'Unknown error');
|
|
164
|
-
EMAIL_PROVIDERS = [
|
|
165
|
-
{
|
|
166
|
-
companyProvider: 'Google',
|
|
167
|
-
loginUrl: 'https://accounts.google.com/signin',
|
|
168
|
-
domains: ['gmail.com', 'googlemail.com']
|
|
169
|
-
},
|
|
170
|
-
{
|
|
171
|
-
companyProvider: 'Microsoft',
|
|
172
|
-
loginUrl: 'https://outlook.live.com/owa/',
|
|
173
|
-
domains: ['outlook.com', 'hotmail.com', 'live.com', 'msn.com']
|
|
174
|
-
},
|
|
175
|
-
{
|
|
176
|
-
companyProvider: 'Yahoo',
|
|
177
|
-
loginUrl: 'https://login.yahoo.com/',
|
|
178
|
-
domains: ['yahoo.com', 'yahoo.co.uk', 'yahoo.ca', 'yahoo.com.au', 'ymail.com', 'rocketmail.com']
|
|
179
|
-
}
|
|
180
|
-
];
|
|
181
|
-
buildDomainMap(); // Build optimized lookup map for fallback too
|
|
182
|
-
}
|
|
183
|
-
/**
|
|
184
|
-
* Validates if a string is a valid email address using a basic regex pattern.
|
|
185
|
-
*
|
|
186
|
-
* @param email - The string to validate as an email address
|
|
187
|
-
* @returns true if the string matches basic email format, false otherwise
|
|
188
|
-
*
|
|
189
|
-
* @example
|
|
190
|
-
* ```typescript
|
|
191
|
-
* isValidEmail('user@gmail.com'); // true
|
|
192
|
-
* isValidEmail('invalid-email'); // false
|
|
193
|
-
* isValidEmail('user@domain'); // false
|
|
194
|
-
* ```
|
|
195
|
-
*
|
|
196
|
-
* @remarks
|
|
197
|
-
* This uses a simple regex pattern that covers most common email formats.
|
|
198
|
-
* It may not catch all edge cases defined in RFC 5322, but works for
|
|
199
|
-
* standard email addresses.
|
|
200
|
-
*/
|
|
201
|
-
function isValidEmail(email) {
|
|
202
|
-
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
203
|
-
return emailRegex.test(email);
|
|
204
|
-
}
|
|
205
|
-
/**
|
|
206
|
-
* Extracts the domain portion from an email address.
|
|
207
|
-
*
|
|
208
|
-
* @param email - The email address to extract the domain from
|
|
209
|
-
* @returns The domain in lowercase, or null if the email is invalid
|
|
210
|
-
*
|
|
211
|
-
* @example
|
|
212
|
-
* ```typescript
|
|
213
|
-
* extractDomain('user@Gmail.com'); // 'gmail.com'
|
|
214
|
-
* extractDomain('test@yahoo.co.uk'); // 'yahoo.co.uk'
|
|
215
|
-
* extractDomain('invalid-email'); // null
|
|
216
|
-
* ```
|
|
217
|
-
*
|
|
218
|
-
* @remarks
|
|
219
|
-
* The domain is automatically normalized to lowercase for consistent matching.
|
|
220
|
-
*/
|
|
221
|
-
function extractDomain(email) {
|
|
222
|
-
if (!isValidEmail(email)) {
|
|
223
|
-
return null;
|
|
224
|
-
}
|
|
225
|
-
const parts = email.split('@');
|
|
226
|
-
return parts.length === 2 ? parts[1].toLowerCase() : null;
|
|
227
|
-
}
|
|
228
|
-
/**
|
|
229
|
-
* Finds an email provider by matching the domain against the known providers database.
|
|
230
|
-
* Uses an optimized Map for O(1) lookup performance.
|
|
231
|
-
*
|
|
232
|
-
* @param domain - The email domain to look up (e.g., 'gmail.com')
|
|
233
|
-
* @returns The matching EmailProvider object, or null if not found
|
|
234
|
-
*
|
|
235
|
-
* @example
|
|
236
|
-
* ```typescript
|
|
237
|
-
* const provider = findEmailProvider('gmail.com');
|
|
238
|
-
* console.log(provider?.companyProvider); // 'Gmail'
|
|
239
|
-
* console.log(provider?.loginUrl); // 'https://mail.google.com/mail/'
|
|
240
|
-
* ```
|
|
241
|
-
*
|
|
242
|
-
* @remarks
|
|
243
|
-
* This function performs a case-insensitive O(1) lookup using a pre-built Map.
|
|
244
|
-
* It only checks the predefined JSON database, not DNS records.
|
|
245
|
-
* Performance optimized from O(n*m) to O(1) where n=providers, m=domains per provider.
|
|
246
|
-
*/
|
|
247
|
-
function findEmailProvider(domain) {
|
|
248
|
-
const normalizedDomain = domain.toLowerCase();
|
|
249
|
-
return DOMAIN_TO_PROVIDER_MAP.get(normalizedDomain) || null;
|
|
250
|
-
}
|
|
251
|
-
/**
|
|
252
|
-
* Gets email provider information and login URL for a given email address.
|
|
253
|
-
* This is the basic/synchronous version that only checks predefined domains.
|
|
254
|
-
*
|
|
255
|
-
* @param email - The email address to analyze
|
|
256
|
-
* @returns EmailProviderResult containing provider info and login URL
|
|
257
|
-
*
|
|
258
|
-
* @example
|
|
259
|
-
* ```typescript
|
|
260
|
-
* const result = getEmailProviderLink('user@gmail.com');
|
|
261
|
-
* console.log(result.loginUrl); // 'https://mail.google.com/mail/'
|
|
262
|
-
* console.log(result.provider?.companyProvider); // 'Gmail'
|
|
263
|
-
* ```
|
|
264
|
-
*
|
|
265
|
-
* @remarks
|
|
266
|
-
* This function only checks against the predefined JSON database of known domains.
|
|
267
|
-
* It will NOT detect business domains that use major email providers (e.g.,
|
|
268
|
-
* mycompany.com using Google Workspace). For comprehensive detection including
|
|
269
|
-
* business domains, use {@link getEmailProviderLinkWithDNS} instead.
|
|
270
|
-
*
|
|
271
|
-
* **Limitations:**
|
|
272
|
-
* - Only synchronous operation (no DNS lookups)
|
|
273
|
-
* - Limited to domains in the JSON database
|
|
274
|
-
* - Won't detect custom business domains
|
|
275
|
-
* - No proxy service detection
|
|
276
|
-
*/
|
|
277
|
-
function getEmailProviderLink(email) {
|
|
278
|
-
const domain = extractDomain(email);
|
|
279
|
-
if (!domain) {
|
|
280
|
-
return {
|
|
281
|
-
provider: null,
|
|
282
|
-
email,
|
|
283
|
-
loginUrl: null
|
|
284
|
-
};
|
|
285
|
-
}
|
|
286
|
-
const provider = findEmailProvider(domain);
|
|
287
|
-
return {
|
|
288
|
-
provider,
|
|
289
|
-
email,
|
|
290
|
-
loginUrl: provider ? provider.loginUrl : null
|
|
291
|
-
};
|
|
292
|
-
}
|
|
293
|
-
/**
|
|
294
|
-
* Returns an array of all supported email providers.
|
|
295
|
-
*
|
|
296
|
-
* @returns A copy of the EMAIL_PROVIDERS array
|
|
297
|
-
*
|
|
298
|
-
* @example
|
|
299
|
-
* ```typescript
|
|
300
|
-
* const providers = getSupportedProviders();
|
|
301
|
-
* console.log(providers.length); // 55+
|
|
302
|
-
* console.log(providers[0].companyProvider); // 'Gmail'
|
|
303
|
-
* ```
|
|
304
|
-
*
|
|
305
|
-
* @remarks
|
|
306
|
-
* Returns a shallow copy to prevent external modification of the internal
|
|
307
|
-
* providers array. The returned array includes both consumer email providers
|
|
308
|
-
* (gmail.com, yahoo.com) and business email providers with DNS detection patterns.
|
|
19
|
+
exports.extractDomain = extractDomain;
|
|
20
|
+
exports.isValidEmail = isValidEmail;
|
|
21
|
+
// ===== PRIMARY API =====
|
|
22
|
+
// These are the functions 95% of users need
|
|
23
|
+
var api_1 = require("./api");
|
|
24
|
+
Object.defineProperty(exports, "getEmailProvider", { enumerable: true, get: function () { return api_1.getEmailProvider; } });
|
|
25
|
+
Object.defineProperty(exports, "getEmailProviderSync", { enumerable: true, get: function () { return api_1.getEmailProviderSync; } });
|
|
26
|
+
Object.defineProperty(exports, "getEmailProviderFast", { enumerable: true, get: function () { return api_1.getEmailProviderFast; } });
|
|
27
|
+
Object.defineProperty(exports, "normalizeEmail", { enumerable: true, get: function () { return api_1.normalizeEmail; } });
|
|
28
|
+
Object.defineProperty(exports, "emailsMatch", { enumerable: true, get: function () { return api_1.emailsMatch; } });
|
|
29
|
+
Object.defineProperty(exports, "Config", { enumerable: true, get: function () { return api_1.Config; } });
|
|
30
|
+
// ===== ADVANCED FEATURES =====
|
|
31
|
+
// Export utility functions for advanced use cases
|
|
32
|
+
var loader_1 = require("./loader");
|
|
33
|
+
Object.defineProperty(exports, "loadProviders", { enumerable: true, get: function () { return loader_1.loadProviders; } });
|
|
34
|
+
var concurrent_dns_1 = require("./concurrent-dns");
|
|
35
|
+
Object.defineProperty(exports, "detectProviderConcurrent", { enumerable: true, get: function () { return concurrent_dns_1.detectProviderConcurrent; } });
|
|
36
|
+
// ===== UTILITY FUNCTIONS =====
|
|
37
|
+
// Helper functions for common tasks
|
|
38
|
+
const loader_2 = require("./loader");
|
|
39
|
+
const api_2 = require("./api");
|
|
40
|
+
/**
|
|
41
|
+
* Get list of all supported email providers
|
|
42
|
+
* @returns Array of all email providers in the database
|
|
309
43
|
*/
|
|
310
44
|
function getSupportedProviders() {
|
|
311
|
-
|
|
45
|
+
const { providers } = (0, loader_2.loadProviders)();
|
|
46
|
+
return [...providers]; // Return a copy to prevent external mutations
|
|
312
47
|
}
|
|
313
48
|
/**
|
|
314
|
-
*
|
|
315
|
-
*
|
|
316
|
-
* @
|
|
317
|
-
* @returns true if the provider is supported, false otherwise
|
|
318
|
-
*
|
|
319
|
-
* @example
|
|
320
|
-
* ```typescript
|
|
321
|
-
* isEmailProviderSupported('user@gmail.com'); // true
|
|
322
|
-
* isEmailProviderSupported('user@unknown.com'); // false
|
|
323
|
-
* ```
|
|
324
|
-
*
|
|
325
|
-
* @remarks
|
|
326
|
-
* This is a convenience function that uses {@link getEmailProviderLink} internally.
|
|
327
|
-
* It only checks predefined domains, not DNS-based detection. For business
|
|
328
|
-
* domain support checking, you would need to use {@link getEmailProviderLinkWithDNS}.
|
|
49
|
+
* Check if an email provider is supported
|
|
50
|
+
* @param email - Email address to check
|
|
51
|
+
* @returns true if the provider is supported
|
|
329
52
|
*/
|
|
330
53
|
function isEmailProviderSupported(email) {
|
|
331
|
-
const result =
|
|
54
|
+
const result = (0, api_2.getEmailProviderSync)(email);
|
|
332
55
|
return result.provider !== null;
|
|
333
56
|
}
|
|
334
57
|
/**
|
|
335
|
-
*
|
|
336
|
-
*
|
|
337
|
-
* @
|
|
338
|
-
* @returns The name of the detected proxy service, or null if none detected
|
|
339
|
-
*
|
|
340
|
-
* @internal
|
|
341
|
-
* @remarks
|
|
342
|
-
* This function checks MX record patterns against known proxy/CDN services.
|
|
343
|
-
* When a proxy is detected, it means we cannot determine the actual email
|
|
344
|
-
* provider behind the proxy service. Currently detects:
|
|
345
|
-
* - Cloudflare Email Routing
|
|
346
|
-
* - CloudFront (AWS)
|
|
347
|
-
* - Fastly
|
|
348
|
-
* - MaxCDN
|
|
349
|
-
* - KeyCDN
|
|
350
|
-
* - Mailgun proxy configurations
|
|
351
|
-
* - SendGrid proxy configurations
|
|
352
|
-
*
|
|
353
|
-
* @example
|
|
354
|
-
* ```typescript
|
|
355
|
-
* const mxRecords = [{ exchange: 'isaac.mx.cloudflare.net', priority: 10 }];
|
|
356
|
-
* const proxy = detectProxyService(mxRecords);
|
|
357
|
-
* console.log(proxy); // 'Cloudflare'
|
|
358
|
-
* ```
|
|
58
|
+
* Extract domain from email address
|
|
59
|
+
* @param email - Email address
|
|
60
|
+
* @returns Domain portion or null if invalid
|
|
359
61
|
*/
|
|
360
|
-
function
|
|
361
|
-
const
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
{ service: 'Fastly', patterns: ['fastly.com'] },
|
|
365
|
-
{ service: 'MaxCDN', patterns: ['maxcdn.com'] },
|
|
366
|
-
{ service: 'KeyCDN', patterns: ['keycdn.com'] },
|
|
367
|
-
{ service: 'Mailgun Proxy', patterns: ['mailgun.org', 'mg.', 'mailgun'] },
|
|
368
|
-
{ service: 'SendGrid Proxy', patterns: ['sendgrid.net', 'sendgrid.com'] }
|
|
369
|
-
];
|
|
370
|
-
for (const mxRecord of mxRecords) {
|
|
371
|
-
const exchange = mxRecord.exchange.toLowerCase();
|
|
372
|
-
for (const proxyService of proxyPatterns) {
|
|
373
|
-
for (const pattern of proxyService.patterns) {
|
|
374
|
-
if (exchange.includes(pattern.toLowerCase())) {
|
|
375
|
-
return proxyService.service;
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
}
|
|
62
|
+
function extractDomain(email) {
|
|
63
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
64
|
+
if (!emailRegex.test(email)) {
|
|
65
|
+
return null;
|
|
379
66
|
}
|
|
380
|
-
return null;
|
|
67
|
+
return email.split('@')[1]?.toLowerCase() || null;
|
|
381
68
|
}
|
|
382
69
|
/**
|
|
383
|
-
*
|
|
384
|
-
*
|
|
385
|
-
*
|
|
386
|
-
*
|
|
387
|
-
* @param domain - The domain to analyze (e.g., 'mycompany.com')
|
|
388
|
-
* @param timeoutMs - Optional timeout for DNS queries in milliseconds (default: 5000ms)
|
|
389
|
-
* @returns Promise resolving to DNSDetectionResult with provider info or proxy detection
|
|
390
|
-
*
|
|
391
|
-
* @example
|
|
392
|
-
* ```typescript
|
|
393
|
-
* const result = await detectProviderByDNS('microsoft.com');
|
|
394
|
-
* console.log(result.provider?.companyProvider); // 'Microsoft 365 (Business)'
|
|
395
|
-
* console.log(result.detectionMethod); // 'mx_record'
|
|
396
|
-
* ```
|
|
397
|
-
*
|
|
398
|
-
* @remarks
|
|
399
|
-
* **Detection Algorithm:**
|
|
400
|
-
* 1. Checks rate limiting (max 10 requests per minute)
|
|
401
|
-
* 2. Performs MX record lookup for the domain
|
|
402
|
-
* 3. Checks if MX records match known proxy services (Cloudflare, etc.)
|
|
403
|
-
* 4. If proxy detected, returns null provider with proxy info
|
|
404
|
-
* 5. Otherwise, matches MX records against business email provider patterns
|
|
405
|
-
* 6. If no MX match, falls back to TXT record analysis (SPF records, etc.)
|
|
406
|
-
* 7. Returns the first matching provider or null if none found
|
|
407
|
-
*
|
|
408
|
-
* **Rate Limiting:**
|
|
409
|
-
* - Maximum 10 DNS requests per 60-second window
|
|
410
|
-
* - Rate limit exceeded returns error with retry information
|
|
411
|
-
* - Prevents abuse and excessive DNS queries
|
|
412
|
-
*
|
|
413
|
-
* **Provider Patterns Checked:**
|
|
414
|
-
* - Google Workspace: aspmx.l.google.com, aspmx2.googlemail.com, etc.
|
|
415
|
-
* - Microsoft 365: *.protection.outlook.com, *.outlook.com
|
|
416
|
-
* - ProtonMail: mail.protonmail.ch, mailsec.protonmail.ch
|
|
417
|
-
* - FastMail: *.messagingengine.com
|
|
418
|
-
* - And many others...
|
|
419
|
-
*
|
|
420
|
-
* **Error Handling:**
|
|
421
|
-
* DNS lookup failures are caught and the function gracefully falls back
|
|
422
|
-
* to the next detection method or returns null if all methods fail.
|
|
70
|
+
* Validate email format
|
|
71
|
+
* @param email - Email address to validate
|
|
72
|
+
* @returns true if valid format
|
|
423
73
|
*/
|
|
424
|
-
|
|
425
|
-
const
|
|
426
|
-
|
|
427
|
-
if (!dnsRateLimiter.isAllowed()) {
|
|
428
|
-
const retryAfter = Math.ceil(dnsRateLimiter.getTimeUntilReset() / 1000);
|
|
429
|
-
throw new Error(`Rate limit exceeded. DNS queries limited to ${RATE_LIMIT_MAX_REQUESTS} requests per minute. Try again in ${retryAfter} seconds.`);
|
|
430
|
-
}
|
|
431
|
-
// Get providers that support custom domain detection
|
|
432
|
-
const customDomainProviders = EMAIL_PROVIDERS.filter(provider => provider.customDomainDetection &&
|
|
433
|
-
(provider.customDomainDetection.mxPatterns || provider.customDomainDetection.txtPatterns));
|
|
434
|
-
// Try MX record detection first with timeout
|
|
435
|
-
try {
|
|
436
|
-
const mxRecords = await withTimeout(resolveMxAsync(normalizedDomain), timeoutMs);
|
|
437
|
-
// Check for proxy services first
|
|
438
|
-
const proxyService = detectProxyService(mxRecords);
|
|
439
|
-
if (proxyService) {
|
|
440
|
-
return {
|
|
441
|
-
provider: null,
|
|
442
|
-
detectionMethod: 'proxy_detected',
|
|
443
|
-
proxyService
|
|
444
|
-
};
|
|
445
|
-
}
|
|
446
|
-
for (const provider of customDomainProviders) {
|
|
447
|
-
if (provider.customDomainDetection?.mxPatterns) {
|
|
448
|
-
for (const mxRecord of mxRecords) {
|
|
449
|
-
const exchange = mxRecord.exchange.toLowerCase();
|
|
450
|
-
for (const pattern of provider.customDomainDetection.mxPatterns) {
|
|
451
|
-
if (exchange.includes(pattern.toLowerCase())) {
|
|
452
|
-
return {
|
|
453
|
-
provider,
|
|
454
|
-
detectionMethod: 'mx_record'
|
|
455
|
-
};
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
catch (error) {
|
|
463
|
-
// MX lookup failed, continue to TXT records
|
|
464
|
-
}
|
|
465
|
-
// Try TXT record detection with timeout
|
|
466
|
-
try {
|
|
467
|
-
const txtRecords = await withTimeout(resolveTxtAsync(normalizedDomain), timeoutMs);
|
|
468
|
-
const flatTxtRecords = txtRecords.flat();
|
|
469
|
-
for (const provider of customDomainProviders) {
|
|
470
|
-
if (provider.customDomainDetection?.txtPatterns) {
|
|
471
|
-
for (const txtRecord of flatTxtRecords) {
|
|
472
|
-
const record = txtRecord.toLowerCase();
|
|
473
|
-
for (const pattern of provider.customDomainDetection.txtPatterns) {
|
|
474
|
-
if (record.includes(pattern.toLowerCase())) {
|
|
475
|
-
return {
|
|
476
|
-
provider,
|
|
477
|
-
detectionMethod: 'txt_record'
|
|
478
|
-
};
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
catch (error) {
|
|
486
|
-
// TXT lookup failed
|
|
487
|
-
}
|
|
488
|
-
return {
|
|
489
|
-
provider: null,
|
|
490
|
-
detectionMethod: null
|
|
491
|
-
};
|
|
74
|
+
function isValidEmail(email) {
|
|
75
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
76
|
+
return emailRegex.test(email);
|
|
492
77
|
}
|
|
493
78
|
/**
|
|
494
|
-
*
|
|
495
|
-
* This is the recommended function for most use cases as it provides comprehensive
|
|
496
|
-
* detection coverage including business domains and proxy services.
|
|
497
|
-
*
|
|
498
|
-
* @param email - The email address to analyze
|
|
499
|
-
* @param timeoutMs - Optional timeout for DNS queries in milliseconds (default: 5000ms)
|
|
500
|
-
* @returns Promise resolving to EmailProviderResult with provider info and detection method
|
|
501
|
-
*
|
|
502
|
-
* @example
|
|
503
|
-
* ```typescript
|
|
504
|
-
* // Consumer email (fast domain match)
|
|
505
|
-
* const gmail = await getEmailProviderLinkWithDNS('user@gmail.com');
|
|
506
|
-
* console.log(gmail.provider?.companyProvider); // 'Gmail'
|
|
507
|
-
* console.log(gmail.detectionMethod); // 'domain_match'
|
|
508
|
-
*
|
|
509
|
-
* // Business domain (DNS detection)
|
|
510
|
-
* const business = await getEmailProviderLinkWithDNS('user@mycompany.com');
|
|
511
|
-
* console.log(business.provider?.companyProvider); // 'Google Workspace' (if detected)
|
|
512
|
-
* console.log(business.detectionMethod); // 'mx_record'
|
|
513
|
-
*
|
|
514
|
-
* // Proxied domain (proxy detection)
|
|
515
|
-
* const proxied = await getEmailProviderLinkWithDNS('user@proxied-domain.com');
|
|
516
|
-
* console.log(proxied.provider); // null
|
|
517
|
-
* console.log(proxied.proxyService); // 'Cloudflare'
|
|
518
|
-
* console.log(proxied.detectionMethod); // 'proxy_detected'
|
|
519
|
-
* ```
|
|
520
|
-
*
|
|
521
|
-
* @remarks
|
|
522
|
-
* **Detection Hierarchy (in order):**
|
|
523
|
-
* 1. **Domain Match**: Fast lookup against predefined domains (gmail.com, yahoo.com, etc.)
|
|
524
|
-
* 2. **DNS MX Records**: Analyzes mail exchange records for business email providers
|
|
525
|
-
* 3. **DNS TXT Records**: Checks SPF and verification records as fallback
|
|
526
|
-
* 4. **Proxy Detection**: Identifies when domains are behind CDN/proxy services
|
|
527
|
-
*
|
|
528
|
-
* **Supported Detection Cases:**
|
|
529
|
-
* - ✅ Consumer emails: gmail.com, yahoo.com, outlook.com, etc.
|
|
530
|
-
* - ✅ Business domains: Google Workspace, Microsoft 365, ProtonMail Business, etc.
|
|
531
|
-
* - ✅ Proxy services: Cloudflare, CloudFront, Fastly, etc.
|
|
532
|
-
* - ✅ International providers: QQ Mail, NetEase, Yandex, etc.
|
|
533
|
-
*
|
|
534
|
-
* **Performance:**
|
|
535
|
-
* - Fast for known consumer domains (synchronous JSON lookup)
|
|
536
|
-
* - Additional DNS lookup time for unknown domains (~100-500ms)
|
|
537
|
-
* - Graceful degradation if DNS lookups fail
|
|
538
|
-
*
|
|
539
|
-
* **Error Handling:**
|
|
540
|
-
* - Invalid email addresses return null provider
|
|
541
|
-
* - DNS lookup failures are caught and don't throw errors
|
|
542
|
-
* - Network timeouts gracefully fall back to null detection
|
|
543
|
-
*
|
|
544
|
-
* **Use Cases:**
|
|
545
|
-
* - Password reset flows ("Check your Gmail inbox")
|
|
546
|
-
* - Login form enhancements (direct links to email providers)
|
|
547
|
-
* - Email client detection for support purposes
|
|
548
|
-
* - Business domain analysis for enterprise features
|
|
79
|
+
* Library metadata
|
|
549
80
|
*/
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
if (!domain) {
|
|
553
|
-
return {
|
|
554
|
-
provider: null,
|
|
555
|
-
email,
|
|
556
|
-
loginUrl: null
|
|
557
|
-
};
|
|
558
|
-
}
|
|
559
|
-
// First try standard domain matching
|
|
560
|
-
const provider = findEmailProvider(domain);
|
|
561
|
-
if (provider) {
|
|
562
|
-
return {
|
|
563
|
-
provider,
|
|
564
|
-
email,
|
|
565
|
-
loginUrl: provider.loginUrl,
|
|
566
|
-
detectionMethod: 'domain_match'
|
|
567
|
-
};
|
|
568
|
-
}
|
|
569
|
-
// If no direct match, try DNS-based detection for custom domains
|
|
570
|
-
const dnsResult = await detectProviderByDNS(domain, timeoutMs);
|
|
571
|
-
if (dnsResult.provider) {
|
|
572
|
-
return {
|
|
573
|
-
provider: dnsResult.provider,
|
|
574
|
-
email,
|
|
575
|
-
loginUrl: dnsResult.provider.loginUrl,
|
|
576
|
-
detectionMethod: dnsResult.detectionMethod || 'mx_record'
|
|
577
|
-
};
|
|
578
|
-
}
|
|
579
|
-
// Handle proxy detection case
|
|
580
|
-
if (dnsResult.detectionMethod === 'proxy_detected') {
|
|
581
|
-
return {
|
|
582
|
-
provider: null,
|
|
583
|
-
email,
|
|
584
|
-
loginUrl: null,
|
|
585
|
-
detectionMethod: 'proxy_detected',
|
|
586
|
-
proxyService: dnsResult.proxyService
|
|
587
|
-
};
|
|
588
|
-
}
|
|
589
|
-
return {
|
|
590
|
-
provider: null,
|
|
591
|
-
email,
|
|
592
|
-
loginUrl: null
|
|
593
|
-
};
|
|
594
|
-
}
|
|
81
|
+
exports.PROVIDER_COUNT = 93;
|
|
82
|
+
exports.DOMAIN_COUNT = 178;
|
|
595
83
|
/**
|
|
596
|
-
*
|
|
597
|
-
* @public
|
|
84
|
+
* Default export for convenience
|
|
598
85
|
*/
|
|
599
|
-
exports.RateLimit = {
|
|
600
|
-
MAX_REQUESTS: RATE_LIMIT_MAX_REQUESTS,
|
|
601
|
-
WINDOW_MS: RATE_LIMIT_WINDOW_MS,
|
|
602
|
-
SimpleRateLimiter,
|
|
603
|
-
/**
|
|
604
|
-
* Gets the current rate limiter instance for inspection or testing.
|
|
605
|
-
* @internal
|
|
606
|
-
*/
|
|
607
|
-
getCurrentLimiter: () => dnsRateLimiter
|
|
608
|
-
};
|
|
609
|
-
// Export alias detection module
|
|
610
|
-
__exportStar(require("./alias-detection"), exports);
|
|
611
|
-
// Export security modules
|
|
612
|
-
__exportStar(require("./security/url-validator"), exports);
|
|
613
|
-
__exportStar(require("./security/hash-verifier"), exports);
|
|
614
|
-
__exportStar(require("./security/secure-loader"), exports);
|
|
615
|
-
// Default export for convenience
|
|
616
86
|
exports.default = {
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
RateLimit: exports.RateLimit
|
|
87
|
+
getEmailProvider: api_2.getEmailProvider,
|
|
88
|
+
getEmailProviderSync: api_2.getEmailProviderSync,
|
|
89
|
+
getEmailProviderFast: api_2.getEmailProviderFast,
|
|
90
|
+
normalizeEmail: api_2.normalizeEmail,
|
|
91
|
+
emailsMatch: api_2.emailsMatch,
|
|
92
|
+
Config: api_2.Config,
|
|
93
|
+
PROVIDER_COUNT: exports.PROVIDER_COUNT,
|
|
94
|
+
DOMAIN_COUNT: exports.DOMAIN_COUNT
|
|
626
95
|
};
|