@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/dist/api.d.ts ADDED
@@ -0,0 +1,172 @@
1
+ /**
2
+ * Email Provider Links API
3
+ *
4
+ * Simplified API with better error handling and performance improvements.
5
+ * Clean function names and enhanced error context.
6
+ */
7
+ export interface EmailProvider {
8
+ companyProvider: string;
9
+ loginUrl: string;
10
+ domains: string[];
11
+ customDomainDetection?: {
12
+ mxPatterns?: string[];
13
+ txtPatterns?: string[];
14
+ };
15
+ }
16
+ /**
17
+ * Enhanced result interface with rich error context
18
+ */
19
+ export interface EmailProviderResult {
20
+ /** The detected email provider, or null if not found */
21
+ provider: EmailProvider | null;
22
+ /** The original email address that was analyzed */
23
+ email: string;
24
+ /** Direct URL to the email provider's login page, or null if unknown */
25
+ loginUrl: string | null;
26
+ /** Method used to detect the provider */
27
+ detectionMethod?: 'domain_match' | 'mx_record' | 'txt_record' | 'both' | 'proxy_detected';
28
+ /** If a proxy service was detected, which service (e.g., 'Cloudflare') */
29
+ proxyService?: string;
30
+ /** Error information if detection failed */
31
+ error?: {
32
+ type: 'INVALID_EMAIL' | 'DNS_TIMEOUT' | 'RATE_LIMITED' | 'UNKNOWN_DOMAIN' | 'NETWORK_ERROR';
33
+ message: string;
34
+ retryAfter?: number;
35
+ };
36
+ }
37
+ /**
38
+ * Get email provider information for any email address.
39
+ *
40
+ * This is the primary function that handles all email types:
41
+ * - Consumer emails (gmail.com, yahoo.com, etc.)
42
+ * - Business domains (mycompany.com using Google Workspace, etc.)
43
+ * - Unknown providers (graceful fallback)
44
+ *
45
+ * @param email - The email address to analyze
46
+ * @param timeout - Optional timeout for DNS queries in milliseconds (default: 5000ms)
47
+ * @returns Promise resolving to EmailProviderResult with provider info and error context
48
+ *
49
+ * @example
50
+ * ```typescript
51
+ * // Consumer email
52
+ * const gmail = await getEmailProvider('user@gmail.com');
53
+ * console.log(gmail.provider?.name); // "Gmail"
54
+ * console.log(gmail.loginUrl); // "https://mail.google.com/mail/"
55
+ *
56
+ * // Business domain
57
+ * const business = await getEmailProvider('user@mycompany.com');
58
+ * console.log(business.provider?.name); // "Google Workspace" (if detected)
59
+ * console.log(business.detectionMethod); // "mx_record"
60
+ *
61
+ * // Error handling
62
+ * const invalid = await getEmailProvider('invalid-email');
63
+ * console.log(invalid.error?.type); // "INVALID_EMAIL"
64
+ * console.log(invalid.error?.message); // "Invalid email format"
65
+ * ```
66
+ */
67
+ export declare function getEmailProvider(email: string, timeout?: number): Promise<EmailProviderResult>;
68
+ /**
69
+ * Get email provider information synchronously (no DNS lookup).
70
+ *
71
+ * This function only checks predefined domains and returns immediately.
72
+ * Use this when you can't use async functions or don't want DNS lookups.
73
+ *
74
+ * @param email - The email address to analyze
75
+ * @returns EmailProviderResult with provider info (limited to known domains)
76
+ *
77
+ * @example
78
+ * ```typescript
79
+ * // Works for known domains
80
+ * const gmail = getEmailProviderSync('user@gmail.com');
81
+ * console.log(gmail.provider?.name); // "Gmail"
82
+ *
83
+ * // Unknown domains return null
84
+ * const unknown = getEmailProviderSync('user@mycompany.com');
85
+ * console.log(unknown.provider); // null
86
+ * console.log(unknown.error?.type); // "UNKNOWN_DOMAIN"
87
+ * ```
88
+ */
89
+ export declare function getEmailProviderSync(email: string): EmailProviderResult;
90
+ /**
91
+ * Normalize an email address to its canonical form.
92
+ *
93
+ * This handles provider-specific aliasing rules:
94
+ * - Gmail: removes dots and plus addressing
95
+ * - Other providers: removes plus addressing only
96
+ *
97
+ * @param email - The email address to normalize
98
+ * @returns The canonical email address
99
+ *
100
+ * @example
101
+ * ```typescript
102
+ * const canonical = normalizeEmail('U.S.E.R+work@GMAIL.COM');
103
+ * console.log(canonical); // 'user@gmail.com'
104
+ *
105
+ * const outlook = normalizeEmail('user+newsletter@outlook.com');
106
+ * console.log(outlook); // 'user@outlook.com'
107
+ * ```
108
+ */
109
+ export declare function normalizeEmail(email: string): string;
110
+ /**
111
+ * Check if two email addresses are the same person (accounting for aliases).
112
+ *
113
+ * This normalizes both emails and compares their canonical forms.
114
+ * Useful for preventing duplicate accounts and matching login attempts.
115
+ *
116
+ * @param email1 - First email address
117
+ * @param email2 - Second email address
118
+ * @returns true if the emails represent the same person
119
+ *
120
+ * @example
121
+ * ```typescript
122
+ * const match = emailsMatch('user@gmail.com', 'u.s.e.r+work@gmail.com');
123
+ * console.log(match); // true
124
+ *
125
+ * const different = emailsMatch('user@gmail.com', 'other@gmail.com');
126
+ * console.log(different); // false
127
+ * ```
128
+ */
129
+ export declare function emailsMatch(email1: string, email2: string): boolean;
130
+ /**
131
+ * Enhanced email provider detection with concurrent DNS for maximum performance.
132
+ * This function uses parallel MX/TXT lookups for 2x faster business domain detection.
133
+ *
134
+ * @param email - The email address to analyze
135
+ * @param options - Configuration options for DNS detection
136
+ * @returns Promise resolving to EmailProviderResult with enhanced performance data
137
+ *
138
+ * @example
139
+ * ```typescript
140
+ * // High-performance detection with concurrent DNS
141
+ * const result = await getEmailProviderFast('user@mycompany.com', {
142
+ * enableParallel: true,
143
+ * collectDebugInfo: true
144
+ * });
145
+ *
146
+ * console.log(result.provider?.companyProvider); // "Google Workspace"
147
+ * console.log(result.detectionMethod); // "mx_record"
148
+ * console.log(result.timing); // { mx: 120, txt: 95, total: 125 }
149
+ * ```
150
+ */
151
+ export declare function getEmailProviderFast(email: string, options?: {
152
+ timeout?: number;
153
+ enableParallel?: boolean;
154
+ collectDebugInfo?: boolean;
155
+ }): Promise<EmailProviderResult & {
156
+ timing?: {
157
+ mx: number;
158
+ txt: number;
159
+ total: number;
160
+ };
161
+ confidence?: number;
162
+ debug?: any;
163
+ }>;
164
+ /**
165
+ * Configuration constants
166
+ */
167
+ export declare const Config: {
168
+ readonly DEFAULT_DNS_TIMEOUT: 5000;
169
+ readonly MAX_DNS_REQUESTS_PER_MINUTE: 10;
170
+ readonly SUPPORTED_PROVIDERS_COUNT: 93;
171
+ readonly SUPPORTED_DOMAINS_COUNT: 180;
172
+ };
package/dist/api.js ADDED
@@ -0,0 +1,439 @@
1
+ "use strict";
2
+ /**
3
+ * Email Provider Links API
4
+ *
5
+ * Simplified API with better error handling and performance improvements.
6
+ * Clean function names and enhanced error context.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.Config = void 0;
10
+ exports.getEmailProvider = getEmailProvider;
11
+ exports.getEmailProviderSync = getEmailProviderSync;
12
+ exports.normalizeEmail = normalizeEmail;
13
+ exports.emailsMatch = emailsMatch;
14
+ exports.getEmailProviderFast = getEmailProviderFast;
15
+ const concurrent_dns_1 = require("./concurrent-dns");
16
+ const loader_1 = require("./loader");
17
+ /**
18
+ * Get email provider information for any email address.
19
+ *
20
+ * This is the primary function that handles all email types:
21
+ * - Consumer emails (gmail.com, yahoo.com, etc.)
22
+ * - Business domains (mycompany.com using Google Workspace, etc.)
23
+ * - Unknown providers (graceful fallback)
24
+ *
25
+ * @param email - The email address to analyze
26
+ * @param timeout - Optional timeout for DNS queries in milliseconds (default: 5000ms)
27
+ * @returns Promise resolving to EmailProviderResult with provider info and error context
28
+ *
29
+ * @example
30
+ * ```typescript
31
+ * // Consumer email
32
+ * const gmail = await getEmailProvider('user@gmail.com');
33
+ * console.log(gmail.provider?.name); // "Gmail"
34
+ * console.log(gmail.loginUrl); // "https://mail.google.com/mail/"
35
+ *
36
+ * // Business domain
37
+ * const business = await getEmailProvider('user@mycompany.com');
38
+ * console.log(business.provider?.name); // "Google Workspace" (if detected)
39
+ * console.log(business.detectionMethod); // "mx_record"
40
+ *
41
+ * // Error handling
42
+ * const invalid = await getEmailProvider('invalid-email');
43
+ * console.log(invalid.error?.type); // "INVALID_EMAIL"
44
+ * console.log(invalid.error?.message); // "Invalid email format"
45
+ * ```
46
+ */
47
+ async function getEmailProvider(email, timeout) {
48
+ try {
49
+ // Input validation
50
+ if (!email || typeof email !== 'string') {
51
+ return {
52
+ provider: null,
53
+ email: email || '',
54
+ loginUrl: null,
55
+ error: {
56
+ type: 'INVALID_EMAIL',
57
+ message: 'Email address is required and must be a string'
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) {
76
+ return {
77
+ provider: null,
78
+ email,
79
+ loginUrl: null,
80
+ error: {
81
+ type: 'INVALID_EMAIL',
82
+ message: 'Invalid email format - missing domain'
83
+ }
84
+ };
85
+ }
86
+ // First try synchronous domain matching
87
+ const syncResult = getEmailProviderSync(email);
88
+ if (syncResult.provider) {
89
+ return {
90
+ ...syncResult,
91
+ detectionMethod: 'domain_match'
92
+ };
93
+ }
94
+ // Fall back to DNS detection for business domains
95
+ const { providers } = (0, loader_1.loadProviders)();
96
+ const concurrentResult = await (0, concurrent_dns_1.detectProviderConcurrent)(domain, providers, {
97
+ timeout: timeout || 5000,
98
+ enableParallel: true,
99
+ collectDebugInfo: false
100
+ });
101
+ const result = {
102
+ provider: concurrentResult.provider,
103
+ email,
104
+ loginUrl: concurrentResult.provider?.loginUrl || null,
105
+ detectionMethod: concurrentResult.detectionMethod || undefined,
106
+ proxyService: concurrentResult.proxyService
107
+ };
108
+ // Add error context for null results
109
+ if (!result.provider && !result.proxyService) {
110
+ result.error = {
111
+ type: 'UNKNOWN_DOMAIN',
112
+ message: `No email provider found for domain: ${domain}`
113
+ };
114
+ }
115
+ return result;
116
+ }
117
+ catch (error) {
118
+ // Enhanced error handling
119
+ if (error.message?.includes('Rate limit exceeded')) {
120
+ const retryMatch = error.message.match(/Try again in (\d+) seconds/);
121
+ const retryAfter = retryMatch ? parseInt(retryMatch[1]) : undefined;
122
+ return {
123
+ provider: null,
124
+ email,
125
+ loginUrl: null,
126
+ error: {
127
+ type: 'RATE_LIMITED',
128
+ message: 'DNS query rate limit exceeded',
129
+ retryAfter
130
+ }
131
+ };
132
+ }
133
+ if (error.message?.includes('timeout')) {
134
+ return {
135
+ provider: null,
136
+ email,
137
+ loginUrl: null,
138
+ error: {
139
+ type: 'DNS_TIMEOUT',
140
+ message: `DNS lookup timed out after ${timeout || 5000}ms`
141
+ }
142
+ };
143
+ }
144
+ return {
145
+ provider: null,
146
+ email,
147
+ loginUrl: null,
148
+ error: {
149
+ type: 'NETWORK_ERROR',
150
+ message: error.message || 'Unknown network error'
151
+ }
152
+ };
153
+ }
154
+ }
155
+ /**
156
+ * Get email provider information synchronously (no DNS lookup).
157
+ *
158
+ * This function only checks predefined domains and returns immediately.
159
+ * Use this when you can't use async functions or don't want DNS lookups.
160
+ *
161
+ * @param email - The email address to analyze
162
+ * @returns EmailProviderResult with provider info (limited to known domains)
163
+ *
164
+ * @example
165
+ * ```typescript
166
+ * // Works for known domains
167
+ * const gmail = getEmailProviderSync('user@gmail.com');
168
+ * console.log(gmail.provider?.name); // "Gmail"
169
+ *
170
+ * // Unknown domains return null
171
+ * const unknown = getEmailProviderSync('user@mycompany.com');
172
+ * console.log(unknown.provider); // null
173
+ * console.log(unknown.error?.type); // "UNKNOWN_DOMAIN"
174
+ * ```
175
+ */
176
+ function getEmailProviderSync(email) {
177
+ try {
178
+ // Input validation
179
+ if (!email || typeof email !== 'string') {
180
+ return {
181
+ provider: null,
182
+ email: email || '',
183
+ loginUrl: null,
184
+ error: {
185
+ type: 'INVALID_EMAIL',
186
+ message: 'Email address is required and must be a string'
187
+ }
188
+ };
189
+ }
190
+ // Basic email format validation
191
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
192
+ if (!emailRegex.test(email)) {
193
+ return {
194
+ provider: null,
195
+ email,
196
+ loginUrl: null,
197
+ error: {
198
+ type: 'INVALID_EMAIL',
199
+ message: 'Invalid email format'
200
+ }
201
+ };
202
+ }
203
+ // Pure synchronous domain matching
204
+ const domain = email.split('@')[1]?.toLowerCase();
205
+ if (!domain) {
206
+ return {
207
+ provider: null,
208
+ email,
209
+ loginUrl: null,
210
+ error: {
211
+ type: 'INVALID_EMAIL',
212
+ message: 'Invalid email format - missing domain'
213
+ }
214
+ };
215
+ }
216
+ // Load providers and find matching domain
217
+ const { providers } = (0, loader_1.loadProviders)();
218
+ const provider = providers.find(p => p.domains?.some(d => d.toLowerCase() === domain));
219
+ const result = {
220
+ provider: provider || null,
221
+ email,
222
+ loginUrl: provider?.loginUrl || null,
223
+ detectionMethod: 'domain_match'
224
+ };
225
+ // Add error context for null results
226
+ if (!result.provider) {
227
+ result.error = {
228
+ type: 'UNKNOWN_DOMAIN',
229
+ message: `No email provider found for domain: ${domain} (sync mode - business domains not supported)`
230
+ };
231
+ }
232
+ return result;
233
+ }
234
+ catch (error) {
235
+ return {
236
+ provider: null,
237
+ email,
238
+ loginUrl: null,
239
+ error: {
240
+ type: 'INVALID_EMAIL',
241
+ message: error.message || 'Invalid email address'
242
+ }
243
+ };
244
+ }
245
+ }
246
+ /**
247
+ * Normalize an email address to its canonical form.
248
+ *
249
+ * This handles provider-specific aliasing rules:
250
+ * - Gmail: removes dots and plus addressing
251
+ * - Other providers: removes plus addressing only
252
+ *
253
+ * @param email - The email address to normalize
254
+ * @returns The canonical email address
255
+ *
256
+ * @example
257
+ * ```typescript
258
+ * const canonical = normalizeEmail('U.S.E.R+work@GMAIL.COM');
259
+ * console.log(canonical); // 'user@gmail.com'
260
+ *
261
+ * const outlook = normalizeEmail('user+newsletter@outlook.com');
262
+ * console.log(outlook); // 'user@outlook.com'
263
+ * ```
264
+ */
265
+ function normalizeEmail(email) {
266
+ if (!email || typeof email !== 'string') {
267
+ return email;
268
+ }
269
+ // Convert to lowercase
270
+ const lowercaseEmail = email.toLowerCase().trim();
271
+ // Split email into local and domain parts
272
+ const atIndex = lowercaseEmail.lastIndexOf('@');
273
+ if (atIndex === -1) {
274
+ return lowercaseEmail;
275
+ }
276
+ let localPart = lowercaseEmail.slice(0, atIndex);
277
+ const domainPart = lowercaseEmail.slice(atIndex + 1);
278
+ // Gmail-specific rules: remove dots and plus addressing
279
+ if (domainPart === 'gmail.com' || domainPart === 'googlemail.com') {
280
+ // Remove all dots from local part
281
+ localPart = localPart.replace(/\./g, '');
282
+ // Remove plus addressing (everything after +)
283
+ const plusIndex = localPart.indexOf('+');
284
+ if (plusIndex !== -1) {
285
+ localPart = localPart.slice(0, plusIndex);
286
+ }
287
+ }
288
+ else {
289
+ // For other providers, only remove plus addressing
290
+ const plusIndex = localPart.indexOf('+');
291
+ if (plusIndex !== -1) {
292
+ localPart = localPart.slice(0, plusIndex);
293
+ }
294
+ }
295
+ return `${localPart}@${domainPart}`;
296
+ }
297
+ /**
298
+ * Check if two email addresses are the same person (accounting for aliases).
299
+ *
300
+ * This normalizes both emails and compares their canonical forms.
301
+ * Useful for preventing duplicate accounts and matching login attempts.
302
+ *
303
+ * @param email1 - First email address
304
+ * @param email2 - Second email address
305
+ * @returns true if the emails represent the same person
306
+ *
307
+ * @example
308
+ * ```typescript
309
+ * const match = emailsMatch('user@gmail.com', 'u.s.e.r+work@gmail.com');
310
+ * console.log(match); // true
311
+ *
312
+ * const different = emailsMatch('user@gmail.com', 'other@gmail.com');
313
+ * console.log(different); // false
314
+ * ```
315
+ */
316
+ function emailsMatch(email1, email2) {
317
+ if (!email1 || !email2 || typeof email1 !== 'string' || typeof email2 !== 'string') {
318
+ return false;
319
+ }
320
+ // Normalize both emails and compare
321
+ const normalized1 = normalizeEmail(email1);
322
+ const normalized2 = normalizeEmail(email2);
323
+ return normalized1 === normalized2;
324
+ }
325
+ // Note: EmailProvider type is defined above
326
+ /**
327
+ * Enhanced email provider detection with concurrent DNS for maximum performance.
328
+ * This function uses parallel MX/TXT lookups for 2x faster business domain detection.
329
+ *
330
+ * @param email - The email address to analyze
331
+ * @param options - Configuration options for DNS detection
332
+ * @returns Promise resolving to EmailProviderResult with enhanced performance data
333
+ *
334
+ * @example
335
+ * ```typescript
336
+ * // High-performance detection with concurrent DNS
337
+ * const result = await getEmailProviderFast('user@mycompany.com', {
338
+ * enableParallel: true,
339
+ * collectDebugInfo: true
340
+ * });
341
+ *
342
+ * console.log(result.provider?.companyProvider); // "Google Workspace"
343
+ * console.log(result.detectionMethod); // "mx_record"
344
+ * console.log(result.timing); // { mx: 120, txt: 95, total: 125 }
345
+ * ```
346
+ */
347
+ async function getEmailProviderFast(email, options = {}) {
348
+ const { timeout = 5000, enableParallel = true, collectDebugInfo = false } = options;
349
+ try {
350
+ // Input validation
351
+ if (!email || typeof email !== 'string') {
352
+ return {
353
+ provider: null,
354
+ email: email || '',
355
+ loginUrl: null,
356
+ error: {
357
+ type: 'INVALID_EMAIL',
358
+ message: 'Email address is required and must be a string'
359
+ }
360
+ };
361
+ }
362
+ // Basic email format validation
363
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
364
+ if (!emailRegex.test(email)) {
365
+ return {
366
+ provider: null,
367
+ email,
368
+ loginUrl: null,
369
+ error: {
370
+ type: 'INVALID_EMAIL',
371
+ message: 'Invalid email format'
372
+ }
373
+ };
374
+ }
375
+ const domain = email.split('@')[1]?.toLowerCase();
376
+ if (!domain) {
377
+ return {
378
+ provider: null,
379
+ email,
380
+ loginUrl: null,
381
+ error: {
382
+ type: 'INVALID_EMAIL',
383
+ message: 'Invalid email format - missing domain'
384
+ }
385
+ };
386
+ }
387
+ // First try standard domain matching (fast path)
388
+ const syncResult = getEmailProviderSync(email);
389
+ if (syncResult.provider) {
390
+ return {
391
+ ...syncResult,
392
+ detectionMethod: 'domain_match',
393
+ timing: { mx: 0, txt: 0, total: 0 },
394
+ confidence: 1.0
395
+ };
396
+ }
397
+ // Fall back to concurrent DNS detection for business domains
398
+ const { providers } = (0, loader_1.loadProviders)();
399
+ const concurrentResult = await (0, concurrent_dns_1.detectProviderConcurrent)(domain, providers, {
400
+ timeout,
401
+ enableParallel,
402
+ collectDebugInfo
403
+ });
404
+ return {
405
+ provider: concurrentResult.provider,
406
+ email,
407
+ loginUrl: concurrentResult.provider?.loginUrl || null,
408
+ detectionMethod: concurrentResult.detectionMethod || undefined,
409
+ proxyService: concurrentResult.proxyService,
410
+ timing: concurrentResult.timing,
411
+ confidence: concurrentResult.confidence,
412
+ debug: concurrentResult.debug,
413
+ error: !concurrentResult.provider && !concurrentResult.proxyService ? {
414
+ type: 'UNKNOWN_DOMAIN',
415
+ message: `No email provider found for domain: ${domain}`
416
+ } : undefined
417
+ };
418
+ }
419
+ catch (error) {
420
+ return {
421
+ provider: null,
422
+ email,
423
+ loginUrl: null,
424
+ error: {
425
+ type: 'NETWORK_ERROR',
426
+ message: error.message || 'DNS detection failed'
427
+ }
428
+ };
429
+ }
430
+ }
431
+ /**
432
+ * Configuration constants
433
+ */
434
+ exports.Config = {
435
+ DEFAULT_DNS_TIMEOUT: 5000,
436
+ MAX_DNS_REQUESTS_PER_MINUTE: 10,
437
+ SUPPORTED_PROVIDERS_COUNT: 93,
438
+ SUPPORTED_DOMAINS_COUNT: 180
439
+ };