@mikkelscheike/email-provider-links 2.8.0 → 2.8.2

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 CHANGED
@@ -92,51 +92,53 @@ Fully compatible with the latest Node.js 24.x! The library is tested on:
92
92
 
93
93
  ## API Reference
94
94
 
95
- ### `getEmailProvider(email, timeout?)`
96
- **Recommended** - Detects any email provider including business domains.
95
+ ### Core Functions
96
+
97
+ #### `getEmailProvider(email, timeout?)`
98
+ **Recommended** - Complete provider detection with business domain support.
97
99
 
98
100
  ```typescript
99
- // 🚀 SAME CALL, DIFFERENT SCENARIOS:
100
-
101
- // For known providers (Gmail, Yahoo, etc.) - INSTANT response
102
- const gmail1 = await getEmailProvider('user@gmail.com'); // No timeout needed
103
- const gmail2 = await getEmailProvider('user@gmail.com', 3000); // Same speed - timeout ignored
104
- // Both return instantly: { provider: "Gmail", loginUrl: "https://mail.google.com/mail/" }
105
-
106
- // 🔍 For business domains - DNS lookup required, timeout matters
107
- const biz1 = await getEmailProvider('user@mycompany.com'); // 5000ms timeout (default)
108
- const biz2 = await getEmailProvider('user@mycompany.com', 2000); // 2000ms timeout (faster fail)
109
- const biz3 = await getEmailProvider('user@mycompany.com', 10000); // 10000ms timeout (slower networks)
110
- // All may detect: { provider: "Google Workspace", detectionMethod: "mx_record" }
111
-
112
- // 🎯 WHY USE CUSTOM TIMEOUT?
113
- // - Faster apps: Use 2000ms to fail fast on unknown domains
114
- // - Slower networks: Use 10000ms to avoid premature timeouts
115
- // - Enterprise: Use 1000ms for strict SLA requirements
101
+ // Known providers (instant response)
102
+ const result1 = await getEmailProvider('user@gmail.com');
103
+ // Returns: { provider: "Gmail", loginUrl: "https://mail.google.com/mail/" }
104
+
105
+ // Business domains (DNS lookup with timeout)
106
+ const result2 = await getEmailProvider('user@company.com', 2000);
107
+ // Returns: { provider: "Google Workspace", detectionMethod: "mx_record" }
116
108
  ```
117
109
 
118
- ### `getEmailProviderSync(email)`
119
- **Synchronous** - Only checks predefined domains (no DNS lookup).
110
+ #### `getEmailProviderSync(email)`
111
+ **Fast** - Instant checks for known providers (no DNS lookup).
120
112
 
121
113
  ```typescript
122
- const result = getEmailProviderSync('user@gmail.com');
123
- // Returns: { provider, loginUrl, email }
114
+ const result = getEmailProviderSync('user@outlook.com');
115
+ // Returns: { provider: "Outlook", loginUrl: "https://outlook.live.com/" }
124
116
  ```
125
117
 
126
- ### `getEmailProviderFast(email, options?)`
127
- **High-performance** - Concurrent DNS with detailed timing information.
118
+ ### Email Alias Support
119
+
120
+ The library handles provider-specific email alias rules:
128
121
 
129
122
  ```typescript
130
- const result = await getEmailProviderFast('user@mycompany.com', {
131
- enableParallel: true,
132
- collectDebugInfo: true,
133
- timeout: 3000
134
- });
135
-
136
- console.log(result.timing); // { mx: 120, txt: 95, total: 125 }
137
- console.log(result.confidence); // 0.9
123
+ // Gmail ignores dots and plus addressing
124
+ emailsMatch('user.name+work@gmail.com', 'username@gmail.com') // true
125
+
126
+ // Outlook preserves dots but ignores plus addressing
127
+ emailsMatch('user.name+work@outlook.com', 'username@outlook.com') // false
128
+
129
+ // Normalize emails to canonical form
130
+ const canonical = normalizeEmail('u.s.e.r+tag@gmail.com');
131
+ console.log(canonical); // 'user@gmail.com'
138
132
  ```
139
133
 
134
+ **Provider Rules Overview**:
135
+ - **Gmail**: Ignores dots, supports plus addressing
136
+ - **Outlook**: Preserves dots, supports plus addressing
137
+ - **Yahoo**: Preserves dots, supports plus addressing
138
+ - **ProtonMail**: Preserves dots, supports plus addressing
139
+ - **FastMail**: Preserves dots, supports plus addressing
140
+ - **AOL**: Preserves everything except case
141
+
140
142
  ## Real-World Example
141
143
 
142
144
  ```typescript
@@ -256,17 +258,32 @@ The library implements careful memory management:
256
258
 
257
259
  ### Performance Benchmarks
258
260
 
259
- This package is designed to be extremely memory efficient and fast:
260
-
261
- - **Provider loading**: ~0.08MB heap usage, ~0.5ms
262
- - **Email lookups**: ~0.03MB heap usage per 100 operations
263
- - **Concurrent DNS**: ~0.03MB heap usage, ~27ms for 10 lookups
264
- - **Large scale (1000 ops)**: ~0.03MB heap usage, ~1.1ms total
265
- - **International validation**: <1ms for complex IDN domains
266
-
267
- To run benchmarks locally:
261
+ Extensively optimized for both speed and memory efficiency:
262
+
263
+ **Speed Metrics**:
264
+ - Initial provider load: ~0.5ms
265
+ - Known provider lookup: <1ms
266
+ - DNS-based detection: ~27ms average
267
+ - Batch processing: 1000 operations in ~1.1ms
268
+ - Email validation: <1ms for complex IDN domains
269
+
270
+ **Memory Usage**:
271
+ - Initial footprint: ~0.08MB
272
+ - Per operation: ~0.03MB per 1000 lookups
273
+ - Peak usage: <25MB under heavy load
274
+ - Cache efficiency: >99% hit rate
275
+ - Garbage collection: Automatic optimization
276
+
277
+ **Real-World Performance**:
278
+ - 50,000+ operations/second for known providers
279
+ - 100 concurrent DNS lookups in <1 second
280
+ - Average latency: <1ms for cached lookups
281
+ - Maximum latency: <5ms per lookup
282
+
283
+ To run benchmarks:
268
284
  ```bash
269
- npm run benchmark
285
+ npm run benchmark # Basic benchmarks
286
+ node --expose-gc benchmark/memory.ts # Detailed memory analysis
270
287
  ```
271
288
 
272
289
  ## Contributing
@@ -9,184 +9,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
9
9
  exports.detectEmailAlias = detectEmailAlias;
10
10
  exports.normalizeEmail = normalizeEmail;
11
11
  exports.emailsMatch = emailsMatch;
12
- /**
13
- * Email alias rules for major providers
14
- */
15
- const ALIAS_RULES = [
16
- {
17
- domains: ['gmail.com', 'googlemail.com'],
18
- supportsPlusAddressing: true,
19
- ignoresDots: true,
20
- normalize: (email) => {
21
- const parts = email.toLowerCase().split('@');
22
- const username = parts[0];
23
- const domain = parts[1];
24
- if (!username || !domain) {
25
- return email.toLowerCase();
26
- }
27
- // Remove dots and everything after +
28
- const cleanUsername = username.replace(/\./g, '').split('+')[0];
29
- return `${cleanUsername}@${domain}`;
30
- }
31
- },
32
- {
33
- domains: ['outlook.com', 'hotmail.com', 'live.com', 'msn.com', 'hotmail.co.uk', 'hotmail.fr', 'hotmail.it', 'hotmail.es', 'hotmail.de', 'live.co.uk', 'live.fr', 'live.it', 'live.nl', 'live.com.au', 'live.ca', 'live.jp'],
34
- supportsPlusAddressing: true,
35
- ignoresDots: false,
36
- normalize: (email) => {
37
- const parts = email.toLowerCase().split('@');
38
- const username = parts[0];
39
- const domain = parts[1];
40
- if (!username || !domain) {
41
- return email.toLowerCase();
42
- }
43
- // Only remove plus addressing for Outlook
44
- const cleanUsername = username.split('+')[0];
45
- return `${cleanUsername}@${domain}`;
46
- }
47
- },
48
- {
49
- domains: ['yahoo.com', 'yahoo.co.uk', 'yahoo.fr', 'yahoo.co.in', 'yahoo.com.br', 'yahoo.co.jp', 'yahoo.it', 'yahoo.de', 'yahoo.in', 'yahoo.es', 'yahoo.ca', 'yahoo.com.au', 'yahoo.com.ar', 'yahoo.com.mx', 'yahoo.co.id', 'yahoo.com.sg', 'ymail.com', 'rocketmail.com'],
50
- supportsPlusAddressing: true,
51
- ignoresDots: false,
52
- normalize: (email) => {
53
- const parts = email.toLowerCase().split('@');
54
- const username = parts[0];
55
- const domain = parts[1];
56
- if (!username || !domain) {
57
- return email.toLowerCase();
58
- }
59
- const cleanUsername = username.split('+')[0];
60
- return `${cleanUsername}@${domain}`;
61
- }
62
- },
63
- {
64
- domains: ['fastmail.com', 'fastmail.fm'],
65
- supportsPlusAddressing: true,
66
- ignoresDots: false,
67
- normalize: (email) => {
68
- const parts = email.toLowerCase().split('@');
69
- const username = parts[0];
70
- const domain = parts[1];
71
- if (!username || !domain) {
72
- return email.toLowerCase();
73
- }
74
- const cleanUsername = username.split('+')[0];
75
- return `${cleanUsername}@${domain}`;
76
- }
77
- },
78
- {
79
- domains: ['proton.me', 'protonmail.com', 'protonmail.ch', 'pm.me'],
80
- supportsPlusAddressing: true,
81
- ignoresDots: false,
82
- normalize: (email) => {
83
- const parts = email.toLowerCase().split('@');
84
- const username = parts[0];
85
- const domain = parts[1];
86
- if (!username || !domain) {
87
- return email.toLowerCase();
88
- }
89
- const cleanUsername = username.split('+')[0];
90
- return `${cleanUsername}@${domain}`;
91
- }
92
- },
93
- {
94
- domains: ['tutanota.com', 'tutanota.de', 'tutamail.com', 'tuta.io', 'keemail.me', 'tuta.com'],
95
- supportsPlusAddressing: true,
96
- ignoresDots: false,
97
- normalize: (email) => {
98
- const parts = email.toLowerCase().split('@');
99
- const username = parts[0];
100
- const domain = parts[1];
101
- if (!username || !domain) {
102
- return email.toLowerCase();
103
- }
104
- const cleanUsername = username.split('+')[0];
105
- return `${cleanUsername}@${domain}`;
106
- }
107
- },
108
- {
109
- domains: ['zoho.com', 'zohomail.com', 'zoho.eu'],
110
- supportsPlusAddressing: true,
111
- ignoresDots: false,
112
- normalize: (email) => {
113
- const parts = email.toLowerCase().split('@');
114
- const username = parts[0];
115
- const domain = parts[1];
116
- if (!username || !domain) {
117
- return email.toLowerCase();
118
- }
119
- const cleanUsername = username.split('+')[0];
120
- return `${cleanUsername}@${domain}`;
121
- }
122
- },
123
- {
124
- domains: ['icloud.com', 'me.com', 'mac.com'],
125
- supportsPlusAddressing: true,
126
- ignoresDots: false,
127
- normalize: (email) => {
128
- const parts = email.toLowerCase().split('@');
129
- const username = parts[0];
130
- const domain = parts[1];
131
- if (!username || !domain) {
132
- return email.toLowerCase();
133
- }
134
- const cleanUsername = username.split('+')[0];
135
- return `${cleanUsername}@${domain}`;
136
- }
137
- },
138
- {
139
- domains: ['mail.com'],
140
- supportsPlusAddressing: true,
141
- ignoresDots: false,
142
- normalize: (email) => {
143
- const parts = email.toLowerCase().split('@');
144
- const username = parts[0];
145
- const domain = parts[1];
146
- if (!username || !domain) {
147
- return email.toLowerCase();
148
- }
149
- const cleanUsername = username.split('+')[0];
150
- return `${cleanUsername}@${domain}`;
151
- }
152
- },
153
- {
154
- domains: ['aol.com', 'love.com', 'ygm.com', 'games.com', 'wow.com', 'aim.com'],
155
- supportsPlusAddressing: false,
156
- ignoresDots: false,
157
- normalize: (email) => email.toLowerCase()
158
- },
159
- {
160
- domains: ['mail.ru'],
161
- supportsPlusAddressing: true,
162
- ignoresDots: false,
163
- normalize: (email) => {
164
- const parts = email.toLowerCase().split('@');
165
- const username = parts[0];
166
- const domain = parts[1];
167
- if (!username || !domain) {
168
- return email.toLowerCase();
169
- }
170
- const cleanUsername = username.split('+')[0];
171
- return `${cleanUsername}@${domain}`;
172
- }
173
- },
174
- {
175
- domains: ['yandex.com', 'yandex.ru'],
176
- supportsPlusAddressing: true,
177
- ignoresDots: false,
178
- normalize: (email) => {
179
- const parts = email.toLowerCase().split('@');
180
- const username = parts[0];
181
- const domain = parts[1];
182
- if (!username || !domain) {
183
- return email.toLowerCase();
184
- }
185
- const cleanUsername = username.split('+')[0];
186
- return `${cleanUsername}@${domain}`;
187
- }
188
- }
189
- ];
12
+ const loader_1 = require("./loader");
190
13
  /**
191
14
  * Validates email format
192
15
  */
@@ -194,12 +17,6 @@ function isValidEmail(email) {
194
17
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
195
18
  return emailRegex.test(email);
196
19
  }
197
- /**
198
- * Gets the alias rule for a given domain
199
- */
200
- function getAliasRule(domain) {
201
- return ALIAS_RULES.find(rule => rule.domains.includes(domain.toLowerCase())) || null;
202
- }
203
20
  /**
204
21
  * Detects and analyzes email aliases
205
22
  *
@@ -217,49 +34,61 @@ function detectEmailAlias(email) {
217
34
  if (!username || !domain) {
218
35
  throw new Error('Invalid email format - missing username or domain');
219
36
  }
220
- const rule = getAliasRule(domain);
37
+ const { domainMap } = (0, loader_1.loadProviders)();
38
+ const provider = domainMap.get(domain);
221
39
  const result = {
222
40
  canonical: originalEmail.toLowerCase(),
223
41
  original: originalEmail,
224
42
  isAlias: false,
225
43
  aliasType: 'none'
226
44
  };
227
- if (!rule) {
228
- // No specific rule, just normalize case
229
- result.canonical = originalEmail.toLowerCase();
45
+ if (!provider?.alias) {
230
46
  return result;
231
47
  }
232
48
  result.provider = domain;
233
- // Check for plus addressing
234
- if (rule.supportsPlusAddressing && username.includes('+')) {
235
- const plusIndex = username.indexOf('+');
236
- const baseUsername = username.substring(0, plusIndex);
237
- const aliasPart = username.substring(plusIndex + 1);
238
- result.isAlias = true;
239
- result.aliasType = 'plus';
240
- result.aliasPart = aliasPart;
241
- result.canonical = rule.normalize ? rule.normalize(originalEmail) : `${baseUsername}@${domain}`;
242
- return result;
49
+ let normalizedUsername = username;
50
+ let isAlias = false;
51
+ let aliasType = 'none';
52
+ let aliasPart;
53
+ // Handle case sensitivity (all modern providers are case-insensitive)
54
+ if (provider.alias?.case?.ignore) {
55
+ if (provider.alias.case?.strip) {
56
+ normalizedUsername = normalizedUsername.toLowerCase();
57
+ }
243
58
  }
244
- // Check for dot variations (Gmail)
245
- if (rule.ignoresDots && username.includes('.')) {
246
- const baseUsername = username.replace(/\./g, '');
247
- if (baseUsername !== username) {
248
- result.isAlias = true;
249
- result.aliasType = 'dot';
250
- result.aliasPart = username;
251
- result.canonical = rule.normalize ? rule.normalize(originalEmail) : `${baseUsername}@${domain}`;
252
- return result;
59
+ // Handle plus addressing (common for Gmail, Outlook, Yahoo, etc.)
60
+ if (provider.alias?.plus?.ignore) {
61
+ const plusIndex = username.indexOf('+');
62
+ if (plusIndex !== -1) {
63
+ aliasPart = username.substring(plusIndex + 1);
64
+ isAlias = true;
65
+ aliasType = 'plus';
66
+ if (provider.alias.plus?.strip) {
67
+ normalizedUsername = username.slice(0, plusIndex);
68
+ }
253
69
  }
254
70
  }
255
- // Apply provider-specific normalization
256
- if (rule.normalize) {
257
- const normalized = rule.normalize(originalEmail);
258
- if (normalized !== originalEmail.toLowerCase()) {
259
- result.isAlias = true;
260
- result.canonical = normalized;
71
+ // Handle dots (primarily for Gmail)
72
+ if (provider.alias?.dots?.ignore) {
73
+ const hasDots = username.includes('.');
74
+ if (hasDots) {
75
+ if (!isAlias) {
76
+ aliasPart = username;
77
+ isAlias = true;
78
+ aliasType = 'dot';
79
+ }
80
+ if (provider.alias.dots?.strip) {
81
+ normalizedUsername = normalizedUsername.replace(/\./g, '');
82
+ }
261
83
  }
262
84
  }
85
+ // Build the canonical form
86
+ result.canonical = `${normalizedUsername}@${domain}`;
87
+ result.isAlias = isAlias;
88
+ result.aliasType = aliasType;
89
+ if (aliasPart !== undefined) {
90
+ result.aliasPart = aliasPart;
91
+ }
263
92
  return result;
264
93
  }
265
94
  /**
package/dist/api.d.ts CHANGED
@@ -4,10 +4,26 @@
4
4
  * Simplified API with better error handling and performance improvements.
5
5
  * Clean function names and enhanced error context.
6
6
  */
7
+ export type ProviderType = 'public_provider' | 'custom_provider' | 'proxy_service';
7
8
  export interface EmailProvider {
8
9
  companyProvider: string;
9
10
  loginUrl: string | null;
10
11
  domains: string[];
12
+ type: ProviderType;
13
+ alias?: {
14
+ dots?: {
15
+ ignore: boolean;
16
+ strip: boolean;
17
+ };
18
+ plus?: {
19
+ ignore: boolean;
20
+ strip: boolean;
21
+ };
22
+ case?: {
23
+ ignore: boolean;
24
+ strip: boolean;
25
+ };
26
+ };
11
27
  customDomainDetection?: {
12
28
  mxPatterns?: string[];
13
29
  txtPatterns?: string[];
@@ -50,14 +66,14 @@ export interface EmailProviderResult {
50
66
  * @example
51
67
  * ```typescript
52
68
  * // Consumer email
53
- * const gmail = await getEmailProvider('user@gmail.com');
54
- * console.log(gmail.provider?.companyProvider); // "Gmail"
55
- * console.log(gmail.loginUrl); // "https://mail.google.com/mail/"
69
+ * const result = await getEmailProvider('local@domain.tld');
70
+ * console.log(result.provider?.companyProvider); // Provider name
71
+ * console.log(result.loginUrl); // Login URL
56
72
  *
57
73
  * // Business domain
58
- * const business = await getEmailProvider('user@mycompany.com');
59
- * console.log(business.provider?.companyProvider); // "Google Workspace" (if detected)
60
- * console.log(business.detectionMethod); // "mx_record"
74
+ * const business = await getEmailProvider('local@business.tld');
75
+ * console.log(business.provider?.companyProvider); // Detected provider
76
+ * console.log(business.detectionMethod); // Detection method
61
77
  *
62
78
  * // Error handling
63
79
  * const invalid = await getEmailProvider('invalid-email');
@@ -100,11 +116,11 @@ export declare function getEmailProviderSync(email: string): EmailProviderResult
100
116
  *
101
117
  * @example
102
118
  * ```typescript
103
- * const canonical = normalizeEmail('U.S.E.R+work@GMAIL.COM');
104
- * console.log(canonical); // 'user@gmail.com'
119
+ * const canonical = normalizeEmail('L.O.C.A.L+work@DOMAIN.TLD');
120
+ * console.log(canonical); // 'local@domain.tld'
105
121
  *
106
- * const outlook = normalizeEmail('user+newsletter@outlook.com');
107
- * console.log(outlook); // 'user@outlook.com'
122
+ * const provider = normalizeEmail('local+newsletter@provider.tld');
123
+ * console.log(provider); // 'local@provider.tld'
108
124
  * ```
109
125
  */
110
126
  export declare function normalizeEmail(email: string): string;
@@ -120,10 +136,10 @@ export declare function normalizeEmail(email: string): string;
120
136
  *
121
137
  * @example
122
138
  * ```typescript
123
- * const match = emailsMatch('user@gmail.com', 'u.s.e.r+work@gmail.com');
139
+ * const match = emailsMatch('local@domain.tld', 'l.o.c.a.l+work@domain.tld');
124
140
  * console.log(match); // true
125
141
  *
126
- * const different = emailsMatch('user@gmail.com', 'other@gmail.com');
142
+ * const different = emailsMatch('local@domain.tld', 'other@domain.tld');
127
143
  * console.log(different); // false
128
144
  * ```
129
145
  */
package/dist/api.js CHANGED
@@ -29,14 +29,14 @@ const loader_1 = require("./loader");
29
29
  * @example
30
30
  * ```typescript
31
31
  * // Consumer email
32
- * const gmail = await getEmailProvider('user@gmail.com');
33
- * console.log(gmail.provider?.companyProvider); // "Gmail"
34
- * console.log(gmail.loginUrl); // "https://mail.google.com/mail/"
32
+ * const result = await getEmailProvider('local@domain.tld');
33
+ * console.log(result.provider?.companyProvider); // Provider name
34
+ * console.log(result.loginUrl); // Login URL
35
35
  *
36
36
  * // Business domain
37
- * const business = await getEmailProvider('user@mycompany.com');
38
- * console.log(business.provider?.companyProvider); // "Google Workspace" (if detected)
39
- * console.log(business.detectionMethod); // "mx_record"
37
+ * const business = await getEmailProvider('local@business.tld');
38
+ * console.log(business.provider?.companyProvider); // Detected provider
39
+ * console.log(business.detectionMethod); // Detection method
40
40
  *
41
41
  * // Error handling
42
42
  * const invalid = await getEmailProvider('invalid-email');
@@ -257,11 +257,11 @@ function getEmailProviderSync(email) {
257
257
  *
258
258
  * @example
259
259
  * ```typescript
260
- * const canonical = normalizeEmail('U.S.E.R+work@GMAIL.COM');
261
- * console.log(canonical); // 'user@gmail.com'
260
+ * const canonical = normalizeEmail('L.O.C.A.L+work@DOMAIN.TLD');
261
+ * console.log(canonical); // 'local@domain.tld'
262
262
  *
263
- * const outlook = normalizeEmail('user+newsletter@outlook.com');
264
- * console.log(outlook); // 'user@outlook.com'
263
+ * const provider = normalizeEmail('local+newsletter@provider.tld');
264
+ * console.log(provider); // 'local@provider.tld'
265
265
  * ```
266
266
  */
267
267
  function normalizeEmail(email) {
@@ -277,21 +277,21 @@ function normalizeEmail(email) {
277
277
  }
278
278
  let localPart = lowercaseEmail.slice(0, atIndex);
279
279
  const domainPart = lowercaseEmail.slice(atIndex + 1);
280
- // Gmail-specific rules: remove dots and plus addressing
281
- if (domainPart === 'gmail.com' || domainPart === 'googlemail.com') {
282
- // Remove all dots from local part
283
- localPart = localPart.replace(/\./g, '');
284
- // Remove plus addressing (everything after +)
285
- const plusIndex = localPart.indexOf('+');
286
- if (plusIndex !== -1) {
287
- localPart = localPart.slice(0, plusIndex);
280
+ // Use cached providers for domain lookup
281
+ const { domainMap } = (0, loader_1.loadProviders)();
282
+ const provider = domainMap.get(domainPart);
283
+ if (provider?.alias) {
284
+ // Provider supports aliasing
285
+ if (provider.alias.dots) {
286
+ // Remove all dots from local part (e.g. Gmail)
287
+ localPart = localPart.replace(/\./g, '');
288
288
  }
289
- }
290
- else {
291
- // For other providers, only remove plus addressing
292
- const plusIndex = localPart.indexOf('+');
293
- if (plusIndex !== -1) {
294
- localPart = localPart.slice(0, plusIndex);
289
+ if (provider.alias.plus) {
290
+ // Remove plus addressing (everything after +)
291
+ const plusIndex = localPart.indexOf('+');
292
+ if (plusIndex !== -1) {
293
+ localPart = localPart.slice(0, plusIndex);
294
+ }
295
295
  }
296
296
  }
297
297
  return `${localPart}@${domainPart}`;
@@ -308,10 +308,10 @@ function normalizeEmail(email) {
308
308
  *
309
309
  * @example
310
310
  * ```typescript
311
- * const match = emailsMatch('user@gmail.com', 'u.s.e.r+work@gmail.com');
311
+ * const match = emailsMatch('local@domain.tld', 'l.o.c.a.l+work@domain.tld');
312
312
  * console.log(match); // true
313
313
  *
314
- * const different = emailsMatch('user@gmail.com', 'other@gmail.com');
314
+ * const different = emailsMatch('local@domain.tld', 'other@domain.tld');
315
315
  * console.log(different); // false
316
316
  * ```
317
317
  */
@@ -367,17 +367,14 @@ class ConcurrentDNSDetector {
367
367
  const mxQuery = queries.find(q => q.type === 'mx' && q.success);
368
368
  if (!mxQuery?.records)
369
369
  return null;
370
- const proxyPatterns = [
371
- { service: 'Cloudflare', patterns: ['mxrecord.io', 'mxrecord.mx', 'cloudflare'] },
372
- { service: 'CloudFront', patterns: ['cloudfront.net'] },
373
- { service: 'Fastly', patterns: ['fastly.com'] }
374
- ];
375
370
  for (const record of mxQuery.records) {
376
371
  const exchange = record.exchange?.toLowerCase() || '';
377
- for (const proxy of proxyPatterns) {
378
- for (const pattern of proxy.patterns) {
379
- if (exchange.includes(pattern)) {
380
- return proxy.service;
372
+ for (const provider of this.providers) {
373
+ if (provider.type === 'proxy_service' && provider.customDomainDetection?.mxPatterns) {
374
+ for (const pattern of provider.customDomainDetection.mxPatterns) {
375
+ if (exchange.includes(pattern.toLowerCase())) {
376
+ return provider.companyProvider;
377
+ }
381
378
  }
382
379
  }
383
380
  }
@@ -27,9 +27,9 @@ const path_1 = require("path");
27
27
  */
28
28
  const KNOWN_GOOD_HASHES = {
29
29
  // SHA-256 hash of the legitimate emailproviders.json
30
- 'emailproviders.json': 'bdd4fe7d32a8760db2c2a2fcc9d2c07b32cc309f17eb6fd6c57753de0b5d623d',
30
+ 'emailproviders.json': '288a17c8e186f9c16dca4c9d752ab8a55b9b1e240b991b490cb41c928b4366a6',
31
31
  // You can add hashes for other critical files
32
- 'package.json': '86b76c6907a39775d96677b6d76719c8de6cadd256945134195513101beef489'
32
+ 'package.json': 'a1a44dfdda78bb08ac5d2cb489d9bf577b57006a9f9df7d2e7b9251060c4cf4c'
33
33
  };
34
34
  /**
35
35
  * Calculates SHA-256 hash of a file or string content
package/dist/loader.js CHANGED
@@ -29,13 +29,20 @@ const DEFAULT_CONFIG = {
29
29
  * Convert compressed provider to EmailProvider format
30
30
  */
31
31
  function convertProviderToEmailProvider(compressedProvider) {
32
+ if (!compressedProvider.type) {
33
+ console.warn(`Missing type for provider ${compressedProvider.id}`);
34
+ }
32
35
  const provider = {
33
36
  companyProvider: compressedProvider.companyProvider,
34
37
  loginUrl: compressedProvider.loginUrl || null,
35
- domains: compressedProvider.domains || []
38
+ domains: compressedProvider.domains || [],
39
+ type: compressedProvider.type,
40
+ alias: compressedProvider.alias
36
41
  };
37
- // Convert DNS detection patterns
38
- if (compressedProvider.mx?.length || compressedProvider.txt?.length) {
42
+ // Include DNS detection patterns for business email services and proxy services
43
+ const needsCustomDomainDetection = compressedProvider.type === 'custom_provider' ||
44
+ compressedProvider.type === 'proxy_service';
45
+ if (needsCustomDomainDetection && (compressedProvider.mx?.length || compressedProvider.txt?.length)) {
39
46
  provider.customDomainDetection = {};
40
47
  if (compressedProvider.mx?.length) {
41
48
  provider.customDomainDetection.mxPatterns = compressedProvider.mx;
package/dist/schema.d.ts CHANGED
@@ -8,6 +8,13 @@
8
8
  * Provider interface
9
9
  * Uses compact field names for smaller JSON size
10
10
  */
11
+ /**
12
+ * Provider types:
13
+ * - public_provider: Regular email providers (Gmail, Yahoo, etc.)
14
+ * - custom_provider: Business email services (Google Workspace, Microsoft 365)
15
+ * - proxy_service: Email proxy services (Cloudflare, etc.)
16
+ */
17
+ export type ProviderType = 'public_provider' | 'custom_provider' | 'proxy_service';
11
18
  export interface Provider {
12
19
  /** Provider ID (short identifier) */
13
20
  id: string;
@@ -20,10 +27,22 @@ export interface Provider {
20
27
  /** DNS detection patterns (flattened) */
21
28
  mx?: string[];
22
29
  txt?: string[];
23
- /** Alias capabilities */
30
+ /** Provider type */
31
+ type: ProviderType;
32
+ /** Alias rules for username part */
24
33
  alias?: {
25
- dots?: boolean;
26
- plus?: boolean;
34
+ dots?: {
35
+ ignore: boolean;
36
+ strip: boolean;
37
+ };
38
+ plus?: {
39
+ ignore: boolean;
40
+ strip: boolean;
41
+ };
42
+ case?: {
43
+ ignore: boolean;
44
+ strip: boolean;
45
+ };
27
46
  };
28
47
  }
29
48
  /**
@@ -9,106 +9,28 @@ Object.defineProperty(exports, "__esModule", { value: true });
9
9
  exports.validateEmailProviderUrl = validateEmailProviderUrl;
10
10
  exports.validateAllProviderUrls = validateAllProviderUrls;
11
11
  exports.auditProviderSecurity = auditProviderSecurity;
12
+ const loader_1 = require("./loader");
12
13
  /**
13
- * Allowlisted domains for email providers.
14
+ * Get allowlisted domains from provider data
14
15
  * Only URLs from these domains will be considered safe.
15
- *
16
- * NOTE: This list should be maintained carefully and updated only
17
- * through security review processes.
18
16
  */
19
- const ALLOWED_DOMAINS = [
20
- // Google services
21
- 'google.com',
22
- 'gmail.com',
23
- 'googlemail.com',
24
- 'mail.google.com',
25
- 'accounts.google.com',
26
- // Microsoft services
27
- 'microsoft.com',
28
- 'outlook.com',
29
- 'outlook.office365.com',
30
- 'hotmail.com',
31
- 'live.com',
32
- 'office.com',
33
- // Yahoo services
34
- 'yahoo.com',
35
- 'yahoo.co.uk',
36
- 'yahoo.fr',
37
- 'yahoo.de',
38
- 'login.yahoo.com',
39
- // Privacy-focused providers
40
- 'proton.me',
41
- 'protonmail.com',
42
- 'protonmail.ch',
43
- 'tutanota.com',
44
- 'tutanota.de',
45
- 'posteo.de',
46
- 'runbox.com',
47
- 'countermail.com',
48
- 'hushmail.com',
49
- // Business providers
50
- 'zoho.com',
51
- 'fastmail.com',
52
- 'rackspace.com',
53
- 'apps.rackspace.com',
54
- // Other legitimate providers
55
- 'aol.com',
56
- 'mail.aol.com',
57
- 'gmx.com',
58
- 'gmx.net',
59
- 'mail.com',
60
- 'yandex.com',
61
- 'yandex.ru',
62
- 'web.de',
63
- 'mail.ru',
64
- 'libero.it',
65
- 'orange.fr',
66
- 'free.fr',
67
- 't-online.de',
68
- 'comcast.net',
69
- 'att.net',
70
- 'verizon.net',
71
- 'bluehost.com',
72
- 'godaddy.com',
73
- 'secureserver.net',
74
- // Additional providers from security audit
75
- 'kolabnow.com',
76
- 'connect.xfinity.com',
77
- 'login.verizon.com',
78
- 'www.simply.com',
79
- 'www.one.com',
80
- 'mailfence.com',
81
- 'neo.space',
82
- 'mail.126.com',
83
- 'mail.qq.com',
84
- 'mail.sina.com.cn',
85
- 'www.xtra.co.nz',
86
- 'mail.rediff.com',
87
- 'mail.rakuten.co.jp',
88
- 'mail.nifty.com',
89
- 'mail.iij.ad.jp',
90
- 'email.uol.com.br',
91
- 'email.bol.com.br',
92
- 'email.globo.com',
93
- 'webmail.terra.com.br',
94
- 'webmail.movistar.es',
95
- 'webmail.ono.com',
96
- 'webmail.telkom.co.za',
97
- 'webmail.vodacom.co.za',
98
- 'webmail.mtnonline.com',
99
- 'bdmail.net',
100
- 'mail.aamra.com.bd',
101
- 'mail.link3.net',
102
- 'mail.ionos.com',
103
- 'www.icloud.com',
104
- 'icloud.com',
105
- 'mail.hostinger.com',
106
- 'ngx257.inmotionhosting.com',
107
- 'privateemail.com',
108
- 'app.titan.email',
109
- 'tools.siteground.com',
110
- 'portal.hostgator.com'
111
- ];
17
+ function getAllowedDomains() {
18
+ const { providers } = (0, loader_1.loadProviders)();
19
+ const allowedDomains = new Set();
20
+ for (const provider of providers) {
21
+ if (provider.loginUrl) {
22
+ try {
23
+ const url = new URL(provider.loginUrl);
24
+ allowedDomains.add(url.hostname);
25
+ }
26
+ catch {
27
+ // Skip invalid URLs
28
+ continue;
29
+ }
30
+ }
31
+ }
32
+ return allowedDomains;
33
+ }
112
34
  /**
113
35
  * Suspicious URL patterns that should always be rejected
114
36
  */
@@ -208,12 +130,9 @@ function validateEmailProviderUrl(url) {
208
130
  domain
209
131
  };
210
132
  }
211
- // Check against allowlist
212
- const isAllowed = ALLOWED_DOMAINS.some(allowedDomain => {
213
- // Exact match or subdomain match
214
- return domain === allowedDomain || domain.endsWith(`.${allowedDomain}`);
215
- });
216
- if (!isAllowed) {
133
+ // Check if the domain is allowed
134
+ const allowedDomains = getAllowedDomains();
135
+ if (!allowedDomains.has(domain)) {
217
136
  return {
218
137
  isValid: false,
219
138
  reason: `Domain '${domain}' is not in the allowlist`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mikkelscheike/email-provider-links",
3
- "version": "2.8.0",
3
+ "version": "2.8.2",
4
4
  "description": "TypeScript library for email provider detection with 93 providers (207 domains), concurrent DNS resolution, optimized performance, 91.75% test coverage, and enterprise security for login and password reset flows",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -11,9 +11,10 @@
11
11
  "!dist/**/*.map"
12
12
  ],
13
13
  "scripts": {
14
+ "clean": "rm -rf dist",
14
15
  "verify-hashes": "tsx scripts/verify-hashes.ts",
15
- "build": "tsx scripts/verify-hashes.ts && tsc",
16
- "test": "jest --silent",
16
+ "build": "npm run clean && tsx scripts/verify-hashes.ts && tsc",
17
+ "test": "jest",
17
18
  "test:watch": "jest --watch",
18
19
  "test:coverage": "jest --coverage",
19
20
  "prepublishOnly": "npm run verify-hashes && npm run build",
@@ -1,6 +1,22 @@
1
1
  {
2
2
  "version": "2.0",
3
3
  "providers": [
4
+ {
5
+ "id": "cloudflare",
6
+ "mx": [
7
+ "mx.cloudflare.com",
8
+ "route1.mx.cloudflare.net",
9
+ "route2.mx.cloudflare.net",
10
+ "route3.mx.cloudflare.net"
11
+ ],
12
+ "txt": [
13
+ "spf:_spf.cloudflare.com",
14
+ "cloudflare-verify="
15
+ ],
16
+ "type": "proxy_service",
17
+ "companyProvider": "Cloudflare Email Routing",
18
+ "loginUrl": null
19
+ },
4
20
  {
5
21
  "id": "aamra",
6
22
  "domains": [
@@ -25,7 +41,7 @@
25
41
  "spf:amazonses.com",
26
42
  "spf:workmail.us-east-1.amazonaws.com"
27
43
  ],
28
- "type": "proxy_service",
44
+ "type": "custom_provider",
29
45
  "companyProvider": "Amazon WorkMail",
30
46
  "loginUrl": "https://workmail.aws.amazon.com/"
31
47
  },
@@ -50,7 +66,21 @@
50
66
  ],
51
67
  "type": "public_provider",
52
68
  "companyProvider": "AOL Mail",
53
- "loginUrl": "https://mail.aol.com"
69
+ "loginUrl": "https://mail.aol.com",
70
+ "alias": {
71
+ "dots": {
72
+ "ignore": false,
73
+ "strip": false
74
+ },
75
+ "plus": {
76
+ "ignore": false,
77
+ "strip": false
78
+ },
79
+ "case": {
80
+ "ignore": true,
81
+ "strip": true
82
+ }
83
+ }
54
84
  },
55
85
  {
56
86
  "id": "att",
@@ -95,7 +125,7 @@
95
125
  "txt": [
96
126
  "spf:spf.bluehost.com"
97
127
  ],
98
- "type": "proxy_service",
128
+ "type": "custom_provider",
99
129
  "companyProvider": "Bluehost Email",
100
130
  "loginUrl": "https://webmail.bluehost.com"
101
131
  },
@@ -157,7 +187,21 @@
157
187
  ],
158
188
  "type": "public_provider",
159
189
  "companyProvider": "FastMail",
160
- "loginUrl": "https://www.fastmail.com"
190
+ "loginUrl": "https://www.fastmail.com",
191
+ "alias": {
192
+ "dots": {
193
+ "ignore": false,
194
+ "strip": false
195
+ },
196
+ "plus": {
197
+ "ignore": true,
198
+ "strip": true
199
+ },
200
+ "case": {
201
+ "ignore": true,
202
+ "strip": true
203
+ }
204
+ }
161
205
  },
162
206
  {
163
207
  "id": "freefr",
@@ -183,13 +227,23 @@
183
227
  "gmail.com",
184
228
  "googlemail.com"
185
229
  ],
186
- "alias": {
187
- "dots": true,
188
- "plus": true
189
- },
190
230
  "type": "public_provider",
191
231
  "companyProvider": "Gmail",
192
- "loginUrl": "https://mail.google.com/mail/"
232
+ "loginUrl": "https://mail.google.com/mail/",
233
+ "alias": {
234
+ "dots": {
235
+ "ignore": true,
236
+ "strip": true
237
+ },
238
+ "plus": {
239
+ "ignore": true,
240
+ "strip": true
241
+ },
242
+ "case": {
243
+ "ignore": true,
244
+ "strip": true
245
+ }
246
+ }
193
247
  },
194
248
  {
195
249
  "id": "gmx",
@@ -241,7 +295,7 @@
241
295
  "spf:_spf.google.com",
242
296
  "gsv:"
243
297
  ],
244
- "type": "proxy_service",
298
+ "type": "custom_provider",
245
299
  "companyProvider": "Google Workspace",
246
300
  "loginUrl": "https://mail.google.com"
247
301
  },
@@ -295,7 +349,7 @@
295
349
  "loginUrl": "https://www.hushmail.com/signin/"
296
350
  },
297
351
  {
298
- "id": "icloud",
352
+ "id": "icloud",
299
353
  "domains": [
300
354
  "icloud.com",
301
355
  "me.com",
@@ -311,7 +365,21 @@
311
365
  ],
312
366
  "type": "public_provider",
313
367
  "companyProvider": "iCloud Mail",
314
- "loginUrl": "https://www.icloud.com/mail"
368
+ "loginUrl": "https://www.icloud.com/mail",
369
+ "alias": {
370
+ "dots": {
371
+ "ignore": false,
372
+ "strip": false
373
+ },
374
+ "plus": {
375
+ "ignore": true,
376
+ "strip": true
377
+ },
378
+ "case": {
379
+ "ignore": true,
380
+ "strip": true
381
+ }
382
+ }
315
383
  },
316
384
  {
317
385
  "id": "iij",
@@ -416,7 +484,7 @@
416
484
  "loginUrl": "https://www.mail.bg"
417
485
  },
418
486
  {
419
- "id": "com",
487
+ "id": "com",
420
488
  "domains": [
421
489
  "mail.com"
422
490
  ],
@@ -431,7 +499,21 @@
431
499
  ],
432
500
  "type": "public_provider",
433
501
  "companyProvider": "Mail.ru",
434
- "loginUrl": "https://mail.ru/"
502
+ "loginUrl": "https://mail.ru/",
503
+ "alias": {
504
+ "dots": {
505
+ "ignore": false,
506
+ "strip": false
507
+ },
508
+ "plus": {
509
+ "ignore": true,
510
+ "strip": true
511
+ },
512
+ "case": {
513
+ "ignore": true,
514
+ "strip": true
515
+ }
516
+ }
435
517
  },
436
518
  {
437
519
  "id": "fence",
@@ -464,7 +546,7 @@
464
546
  "ms",
465
547
  "microsoft-domain-verification"
466
548
  ],
467
- "type": "proxy_service",
549
+ "type": "custom_provider",
468
550
  "companyProvider": "Microsoft 365 (Business)",
469
551
  "loginUrl": "https://outlook.office365.com"
470
552
  },
@@ -487,12 +569,23 @@
487
569
  "live.com.au",
488
570
  "live.ca"
489
571
  ],
490
- "alias": {
491
- "plus": true
492
- },
493
572
  "type": "public_provider",
494
573
  "companyProvider": "Microsoft Outlook",
495
- "loginUrl": "https://outlook.office365.com"
574
+ "loginUrl": "https://outlook.office365.com",
575
+ "alias": {
576
+ "dots": {
577
+ "ignore": false,
578
+ "strip": false
579
+ },
580
+ "plus": {
581
+ "ignore": true,
582
+ "strip": true
583
+ },
584
+ "case": {
585
+ "ignore": true,
586
+ "strip": true
587
+ }
588
+ }
496
589
  },
497
590
  {
498
591
  "id": "migadu",
@@ -604,7 +697,7 @@
604
697
  "txt": [
605
698
  "include:_spf.one.com"
606
699
  ],
607
- "type": "proxy_service",
700
+ "type": "custom_provider",
608
701
  "companyProvider": "One.com Email",
609
702
  "loginUrl": "https://www.one.com/mail/"
610
703
  },
@@ -659,12 +752,23 @@
659
752
  "spf:_spf.protonmail.ch",
660
753
  "protonmail-verification="
661
754
  ],
662
- "alias": {
663
- "plus": true
664
- },
665
755
  "type": "public_provider",
666
756
  "companyProvider": "ProtonMail",
667
- "loginUrl": "https://mail.proton.me"
757
+ "loginUrl": "https://mail.proton.me",
758
+ "alias": {
759
+ "dots": {
760
+ "ignore": false,
761
+ "strip": false
762
+ },
763
+ "plus": {
764
+ "ignore": true,
765
+ "strip": true
766
+ },
767
+ "case": {
768
+ "ignore": true,
769
+ "strip": true
770
+ }
771
+ }
668
772
  },
669
773
  {
670
774
  "id": "qq",
@@ -686,7 +790,7 @@
686
790
  "txt": [
687
791
  "spf:emailsrvr.com"
688
792
  ],
689
- "type": "proxy_service",
793
+ "type": "custom_provider",
690
794
  "companyProvider": "Rackspace Email",
691
795
  "loginUrl": "https://apps.rackspace.com/index.php"
692
796
  },
@@ -756,7 +860,7 @@
756
860
  "txt": [
757
861
  "include:spf.key-systems.net"
758
862
  ],
759
- "type": "proxy_service",
863
+ "type": "custom_provider",
760
864
  "companyProvider": "Simply.com Email",
761
865
  "loginUrl": "https://www.simply.com/en/email"
762
866
  },
@@ -833,7 +937,7 @@
833
937
  "loginUrl": "https://app.titan.email"
834
938
  },
835
939
  {
836
- "id": "tutano",
940
+ "id": "tutano",
837
941
  "domains": [
838
942
  "tutanota.com",
839
943
  "tutanota.de",
@@ -851,7 +955,21 @@
851
955
  ],
852
956
  "type": "public_provider",
853
957
  "companyProvider": "Tutanota",
854
- "loginUrl": "https://mail.tutanota.com"
958
+ "loginUrl": "https://mail.tutanota.com",
959
+ "alias": {
960
+ "dots": {
961
+ "ignore": false,
962
+ "strip": false
963
+ },
964
+ "plus": {
965
+ "ignore": false,
966
+ "strip": false
967
+ },
968
+ "case": {
969
+ "ignore": true,
970
+ "strip": true
971
+ }
972
+ }
855
973
  },
856
974
  {
857
975
  "id": "uol",
@@ -921,19 +1039,44 @@
921
1039
  "rocketmail.com",
922
1040
  "myyahoo.com"
923
1041
  ],
924
- "alias": {
925
- "plus": true
926
- },
927
1042
  "type": "public_provider",
928
1043
  "companyProvider": "Yahoo Mail",
929
- "loginUrl": "https://login.yahoo.com"
930
- },
931
- {
932
- "id": "yandex",
1044
+ "loginUrl": "https://login.yahoo.com",
1045
+ "alias": {
1046
+ "dots": {
1047
+ "ignore": false,
1048
+ "strip": false
1049
+ },
1050
+ "plus": {
1051
+ "ignore": true,
1052
+ "strip": true
1053
+ },
1054
+ "case": {
1055
+ "ignore": true,
1056
+ "strip": true
1057
+ }
1058
+ }
1059
+ },
1060
+ {
1061
+ "id": "yandex",
933
1062
  "domains": [
934
1063
  "yandex.ru",
935
1064
  "yandex.com"
936
1065
  ],
1066
+ "alias": {
1067
+ "dots": {
1068
+ "ignore": false,
1069
+ "strip": false
1070
+ },
1071
+ "plus": {
1072
+ "ignore": true,
1073
+ "strip": true
1074
+ },
1075
+ "case": {
1076
+ "ignore": true,
1077
+ "strip": true
1078
+ }
1079
+ },
937
1080
  "type": "public_provider",
938
1081
  "companyProvider": "Yandex Mail",
939
1082
  "loginUrl": "https://mail.yandex.com"
@@ -946,10 +1089,24 @@
946
1089
  ],
947
1090
  "type": "public_provider",
948
1091
  "companyProvider": "Zoho Mail",
949
- "loginUrl": "https://mail.zoho.com"
950
- },
951
- {
952
- "id": "zohoeu",
1092
+ "loginUrl": "https://mail.zoho.com",
1093
+ "alias": {
1094
+ "dots": {
1095
+ "ignore": false,
1096
+ "strip": false
1097
+ },
1098
+ "plus": {
1099
+ "ignore": true,
1100
+ "strip": true
1101
+ },
1102
+ "case": {
1103
+ "ignore": true,
1104
+ "strip": true
1105
+ }
1106
+ }
1107
+ },
1108
+ {
1109
+ "id": "zohoeu",
953
1110
  "domains": [
954
1111
  "zoho.eu"
955
1112
  ],