@mikkelscheike/email-provider-links 1.5.1 โ†’ 1.7.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 CHANGED
@@ -1,20 +1,24 @@
1
1
  # Email Provider Links
2
2
 
3
- ๐Ÿ”’ **Enterprise-grade secure email provider detection for login and password reset flows**
3
+ ๐Ÿ”’ **Enterprise-grade secure email provider detection with advanced alias normalization**
4
4
 
5
- A TypeScript package that provides direct links to email providers based on email addresses, with comprehensive security features to prevent malicious redirects and supply chain attacks.
5
+ A TypeScript package providing direct links to **93 email providers** (180+ domains) with **email alias detection**, comprehensive security features, and international coverage for login and password reset flows.
6
6
 
7
7
  ## โœจ Features
8
8
 
9
9
  - ๐Ÿš€ **Fast & Lightweight**: Zero dependencies, minimal footprint
10
- - ๐Ÿ“ง **64+ Email Providers**: Gmail, Outlook, Yahoo, ProtonMail, iCloud, and more
10
+ - ๐Ÿ“ง **93 Email Providers**: Gmail, Outlook, Yahoo, ProtonMail, iCloud, and many more
11
+ - ๐ŸŒ **180+ Domains Supported**: Comprehensive international coverage
11
12
  - ๐Ÿข **Business Domain Detection**: DNS-based detection for custom domains (Google Workspace, Microsoft 365, etc.)
12
13
  - ๐Ÿ”’ **Enterprise Security**: Multi-layer protection against malicious URLs and supply chain attacks
13
14
  - ๐Ÿ›ก๏ธ **URL Validation**: HTTPS-only enforcement with domain allowlisting
14
15
  - ๐Ÿ” **Integrity Verification**: Cryptographic hash verification for data integrity
15
16
  - ๐Ÿ“ **Type Safe**: Full TypeScript support with comprehensive interfaces
16
17
  - โšก **Performance Optimized**: Smart DNS fallback with configurable timeouts
17
- - ๐Ÿงช **Thoroughly Tested**: 83+ tests including comprehensive security coverage
18
+ - ๐Ÿšฆ **Rate Limiting**: Built-in DNS query rate limiting to prevent abuse
19
+ - ๐Ÿ”„ **Email Alias Detection**: Normalize Gmail dots, plus addressing, and provider-specific aliases
20
+ - ๐Ÿ›ก๏ธ **Fraud Prevention**: Detect duplicate accounts through email alias manipulation
21
+ - ๐Ÿงช **Thoroughly Tested**: 188+ tests with comprehensive coverage
18
22
 
19
23
  ## Installation
20
24
 
@@ -38,11 +42,34 @@ console.log(business.provider?.companyProvider); // "Google Workspace" (if detec
38
42
 
39
43
  ## Supported Providers
40
44
 
41
- **Consumer Email:**
42
- Gmail, Outlook, Yahoo Mail, iCloud, ProtonMail, Zoho, AOL, GMX, Web.de, Mail.ru, QQ Mail, NetEase, Yandex, and more.
45
+ **๐Ÿ“Š Current Coverage: 93 providers supporting 180+ domains**
46
+
47
+ **Consumer Email Providers:**
48
+ - **Gmail** (2 domains): gmail.com, googlemail.com
49
+ - **Microsoft Outlook** (15 domains): outlook.com, hotmail.com, live.com, msn.com, and 11 more
50
+ - **Yahoo Mail** (19 domains): yahoo.com, yahoo.co.uk, yahoo.fr, ymail.com, rocketmail.com, and 14 more
51
+ - **ProtonMail** (4 domains): proton.me, protonmail.com, protonmail.ch, pm.me
52
+ - **iCloud Mail** (3 domains): icloud.com, me.com, mac.com
53
+ - **Tutanota** (6 domains): tutanota.com, tutanota.de, tutamail.com, tuta.io, keemail.me, tuta.com
54
+ - **SimpleLogin** (10 domains): simplelogin.io, 8alias.com, aleeas.com, slmail.me, and 6 more
55
+ - **FastMail, Zoho Mail, AOL Mail, GMX, Web.de, Mail.ru, QQ Mail, NetEase, Yandex**, and many more
43
56
 
44
57
  **Business Email (via DNS detection):**
45
- Microsoft 365, Google Workspace, ProtonMail Business, Hostinger, FastMail, GoDaddy, Tutanota, Zoho Workplace, and others.
58
+ - **Microsoft 365** (Business domains via MX/TXT records)
59
+ - **Google Workspace** (Custom domains via DNS patterns)
60
+ - **Amazon WorkMail** (AWS email infrastructure via awsapps.com patterns)
61
+ - **Zoho Workplace, FastMail Business, GoDaddy Email, Bluehost Email**
62
+ - **ProtonMail Business, Rackspace Email, IONOS**, and others
63
+
64
+ **Security & Privacy Focused:**
65
+ - **ProtonMail, Tutanota, Hushmail, CounterMail, Posteo**
66
+ - **Mailfence, SimpleLogin, AnonAddy**
67
+
68
+ **International Providers:**
69
+ - **Europe**: GMX, Web.de, Orange, Free.fr, T-Online, Libero, Virgilio, Telekom, Tiscali, Skynet, Telenet, Xs4All, Planet.nl, Bluewin, Eircom
70
+ - **Asia**: QQ Mail, NetEase, Sina Mail, Alibaba Mail, Rakuten, Nifty, **Naver** (Korea), **Daum** (Korea), **Biglobe** (Japan), Sify, IndiatTimes (India)
71
+ - **Eastern Europe**: Centrum (Czech/Slovak), Interia, Onet (Poland), Rambler (Russia)
72
+ - **Other Regions**: UOL, Terra (Brazil), Telkom (South Africa), Xtra (New Zealand)
46
73
 
47
74
  ## API
48
75
 
@@ -76,6 +103,94 @@ async function handlePasswordReset(email: string) {
76
103
  }
77
104
  ```
78
105
 
106
+ ## ๐Ÿ”„ Email Alias Detection
107
+
108
+ **NEW in v1.7.0** - Advanced email alias detection and normalization to prevent duplicate accounts and improve user experience.
109
+
110
+ ### Features
111
+
112
+ - **Gmail Dot Normalization**: `u.s.e.r@gmail.com` โ†’ `user@gmail.com`
113
+ - **Plus Addressing**: `user+newsletter@provider.com` โ†’ `user@provider.com`
114
+ - **Provider-Specific Rules**: Different providers have different aliasing capabilities
115
+ - **Fraud Prevention**: Detect when users try to create multiple accounts with aliases
116
+ - **Email Deduplication**: Normalize email lists to get accurate unique user counts
117
+
118
+ ### Quick Start
119
+
120
+ ```typescript
121
+ import {
122
+ detectEmailAlias,
123
+ normalizeEmail,
124
+ emailsMatch
125
+ } from '@mikkelscheike/email-provider-links';
126
+
127
+ // Detect and analyze aliases
128
+ const result = detectEmailAlias('u.s.e.r+work@gmail.com');
129
+ console.log(result.canonical); // 'user@gmail.com'
130
+ console.log(result.isAlias); // true
131
+ console.log(result.aliasType); // 'plus'
132
+ console.log(result.aliasPart); // 'work'
133
+
134
+ // Normalize any email to canonical form
135
+ const canonical = normalizeEmail('U.S.E.R+Newsletter@GMAIL.COM');
136
+ console.log(canonical); // 'user@gmail.com'
137
+
138
+ // Check if two emails are the same person
139
+ const match = emailsMatch('user@gmail.com', 'u.s.e.r+work@gmail.com');
140
+ console.log(match); // true
141
+ ```
142
+
143
+ ### Provider Support
144
+
145
+ | Provider | Plus Addressing | Dots Ignored | Example |
146
+ |----------|----------------|--------------|----------|
147
+ | **Gmail** | โœ… Yes | โœ… Yes | `u.s.e.r+tag@gmail.com` โ†’ `user@gmail.com` |
148
+ | **Outlook** | โœ… Yes | โŒ No | `user+tag@outlook.com` โ†’ `user@outlook.com` |
149
+ | **Yahoo** | โœ… Yes | โŒ No | `user+tag@yahoo.com` โ†’ `user@yahoo.com` |
150
+ | **ProtonMail** | โœ… Yes | โŒ No | `user+tag@proton.me` โ†’ `user@proton.me` |
151
+ | **AOL** | โŒ No | โŒ No | No aliasing support |
152
+
153
+ ### Advanced Usage
154
+
155
+ ```typescript
156
+ // Prevent duplicate account creation
157
+ async function registerUser(email: string) {
158
+ const canonical = normalizeEmail(email);
159
+ const existingUser = await findUserByEmail(canonical);
160
+
161
+ if (existingUser) {
162
+ throw new Error('Email already registered');
163
+ }
164
+
165
+ // Store canonical email to prevent future duplicates
166
+ await createUser({ email: canonical });
167
+ }
168
+
169
+ // Analyze email list for duplicates
170
+ import { analyzeEmailAliases } from '@mikkelscheike/email-provider-links';
171
+
172
+ const emailList = [
173
+ 'user@gmail.com',
174
+ 'u.s.e.r@gmail.com',
175
+ 'user+newsletter@gmail.com',
176
+ 'different@gmail.com'
177
+ ];
178
+
179
+ const analysis = analyzeEmailAliases(emailList);
180
+ console.log(analysis.totalEmails); // 4
181
+ console.log(analysis.uniqueCanonical); // 2 (actual unique users)
182
+
183
+ // Generate test aliases
184
+ import { generateAliases } from '@mikkelscheike/email-provider-links';
185
+
186
+ const aliases = generateAliases('user@gmail.com', {
187
+ plusAliases: ['work', 'personal'],
188
+ includeDotVariations: true,
189
+ maxDotVariations: 3
190
+ });
191
+ // Returns: ['user+work@gmail.com', 'user+personal@gmail.com', 'u.ser@gmail.com', ...]
192
+ ```
193
+
79
194
  ## Configuration
80
195
 
81
196
  ```typescript
@@ -85,6 +200,14 @@ const result = await getEmailProviderLinkWithDNS(email, 2000);
85
200
  // Check if provider is supported
86
201
  import { isEmailProviderSupported } from '@mikkelscheike/email-provider-links';
87
202
  const supported = isEmailProviderSupported('user@gmail.com');
203
+
204
+ // Rate limiting configuration
205
+ import { RateLimit } from '@mikkelscheike/email-provider-links';
206
+ console.log('Max requests:', RateLimit.MAX_REQUESTS); // 10
207
+ console.log('Time window:', RateLimit.WINDOW_MS); // 60000ms
208
+
209
+ // Custom rate limiter for specific use cases
210
+ const customLimiter = new RateLimit.SimpleRateLimiter(20, 120000); // 20 requests per 2 minutes
88
211
  ```
89
212
 
90
213
  ## TypeScript Support
@@ -97,6 +220,13 @@ interface EmailProviderResult {
97
220
  detectionMethod?: 'domain_match' | 'mx_record' | 'txt_record' | 'proxy_detected';
98
221
  proxyService?: string;
99
222
  }
223
+
224
+ interface RateLimitConfig {
225
+ MAX_REQUESTS: number; // 10 requests
226
+ WINDOW_MS: number; // 60000ms (1 minute)
227
+ SimpleRateLimiter: class; // Custom rate limiter class
228
+ getCurrentLimiter(): SimpleRateLimiter; // Get current global limiter
229
+ }
100
230
  ```
101
231
 
102
232
  ## ๐Ÿ›ก๏ธ Security Features
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Email Alias Detection Module
3
+ *
4
+ * Detects and normalizes email aliases across different providers.
5
+ * Supports plus addressing (+), dot variations, and provider-specific aliasing.
6
+ */
7
+ export interface AliasDetectionResult {
8
+ /** The normalized/canonical email address */
9
+ canonical: string;
10
+ /** The original email address */
11
+ original: string;
12
+ /** Whether an alias was detected */
13
+ isAlias: boolean;
14
+ /** Type of alias detected */
15
+ aliasType: 'plus' | 'dot' | 'subdomain' | 'none';
16
+ /** The alias part (if any) */
17
+ aliasPart?: string;
18
+ /** The provider that supports this alias type */
19
+ provider?: string;
20
+ }
21
+ export interface AliasRule {
22
+ /** Email domains this rule applies to */
23
+ domains: string[];
24
+ /** Whether this provider supports plus addressing (user+alias@domain.com) */
25
+ supportsPlusAddressing: boolean;
26
+ /** Whether this provider ignores dots in username (user.name@domain.com = username@domain.com) */
27
+ ignoresDots: boolean;
28
+ /** Whether this provider supports subdomain aliases (user@alias.domain.com) */
29
+ supportsSubdomainAlias: boolean;
30
+ /** Custom alias patterns specific to this provider */
31
+ customPatterns?: RegExp[];
32
+ /** Function to normalize email for this provider */
33
+ normalize?: (email: string) => string;
34
+ }
35
+ /**
36
+ * Detects and analyzes email aliases
37
+ */
38
+ export declare function detectEmailAlias(email: string): AliasDetectionResult;
39
+ /**
40
+ * Normalizes an email address to its canonical form
41
+ */
42
+ export declare function normalizeEmail(email: string): string;
43
+ /**
44
+ * Checks if two email addresses are the same when normalized
45
+ */
46
+ export declare function emailsMatch(email1: string, email2: string): boolean;
47
+ /**
48
+ * Gets alias capabilities for a domain
49
+ */
50
+ export declare function getAliasCapabilities(domain: string): AliasRule | null;
51
+ /**
52
+ * Generates potential aliases for an email address
53
+ */
54
+ export declare function generateAliases(email: string, options?: {
55
+ plusAliases?: string[];
56
+ includeDotVariations?: boolean;
57
+ maxDotVariations?: number;
58
+ }): string[];
59
+ /**
60
+ * Analyzes email aliasing patterns in a list of emails
61
+ */
62
+ export declare function analyzeEmailAliases(emails: string[]): {
63
+ totalEmails: number;
64
+ uniqueCanonical: number;
65
+ aliasGroups: Array<{
66
+ canonical: string;
67
+ aliases: string[];
68
+ count: number;
69
+ }>;
70
+ providerStats: Record<string, {
71
+ total: number;
72
+ aliases: number;
73
+ types: Record<string, number>;
74
+ }>;
75
+ };
@@ -0,0 +1,320 @@
1
+ "use strict";
2
+ /**
3
+ * Email Alias Detection Module
4
+ *
5
+ * Detects and normalizes email aliases across different providers.
6
+ * Supports plus addressing (+), dot variations, and provider-specific aliasing.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.detectEmailAlias = detectEmailAlias;
10
+ exports.normalizeEmail = normalizeEmail;
11
+ exports.emailsMatch = emailsMatch;
12
+ exports.getAliasCapabilities = getAliasCapabilities;
13
+ exports.generateAliases = generateAliases;
14
+ exports.analyzeEmailAliases = analyzeEmailAliases;
15
+ /**
16
+ * Email alias rules for major providers
17
+ */
18
+ const ALIAS_RULES = [
19
+ {
20
+ domains: ['gmail.com', 'googlemail.com'],
21
+ supportsPlusAddressing: true,
22
+ ignoresDots: true,
23
+ supportsSubdomainAlias: false,
24
+ normalize: (email) => {
25
+ const [username, domain] = email.toLowerCase().split('@');
26
+ // Remove dots and everything after +
27
+ const cleanUsername = username.replace(/\./g, '').split('+')[0];
28
+ return `${cleanUsername}@${domain}`;
29
+ }
30
+ },
31
+ {
32
+ 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'],
33
+ supportsPlusAddressing: true,
34
+ ignoresDots: false,
35
+ supportsSubdomainAlias: false,
36
+ normalize: (email) => {
37
+ const [username, domain] = email.toLowerCase().split('@');
38
+ // Only remove plus addressing for Outlook
39
+ const cleanUsername = username.split('+')[0];
40
+ return `${cleanUsername}@${domain}`;
41
+ }
42
+ },
43
+ {
44
+ 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'],
45
+ supportsPlusAddressing: true,
46
+ ignoresDots: false,
47
+ supportsSubdomainAlias: false,
48
+ normalize: (email) => {
49
+ const [username, domain] = email.toLowerCase().split('@');
50
+ const cleanUsername = username.split('+')[0];
51
+ return `${cleanUsername}@${domain}`;
52
+ }
53
+ },
54
+ {
55
+ domains: ['fastmail.com', 'fastmail.fm'],
56
+ supportsPlusAddressing: true,
57
+ ignoresDots: false,
58
+ supportsSubdomainAlias: true,
59
+ normalize: (email) => {
60
+ const [username, domain] = email.toLowerCase().split('@');
61
+ const cleanUsername = username.split('+')[0];
62
+ return `${cleanUsername}@${domain}`;
63
+ }
64
+ },
65
+ {
66
+ domains: ['proton.me', 'protonmail.com', 'protonmail.ch', 'pm.me'],
67
+ supportsPlusAddressing: true,
68
+ ignoresDots: false,
69
+ supportsSubdomainAlias: false,
70
+ normalize: (email) => {
71
+ const [username, domain] = email.toLowerCase().split('@');
72
+ const cleanUsername = username.split('+')[0];
73
+ return `${cleanUsername}@${domain}`;
74
+ }
75
+ },
76
+ {
77
+ domains: ['tutanota.com', 'tutanota.de', 'tutamail.com', 'tuta.io', 'keemail.me', 'tuta.com'],
78
+ supportsPlusAddressing: true,
79
+ ignoresDots: false,
80
+ supportsSubdomainAlias: false,
81
+ normalize: (email) => {
82
+ const [username, domain] = email.toLowerCase().split('@');
83
+ const cleanUsername = username.split('+')[0];
84
+ return `${cleanUsername}@${domain}`;
85
+ }
86
+ },
87
+ {
88
+ domains: ['zoho.com', 'zohomail.com', 'zoho.eu'],
89
+ supportsPlusAddressing: true,
90
+ ignoresDots: false,
91
+ supportsSubdomainAlias: false,
92
+ normalize: (email) => {
93
+ const [username, domain] = email.toLowerCase().split('@');
94
+ const cleanUsername = username.split('+')[0];
95
+ return `${cleanUsername}@${domain}`;
96
+ }
97
+ },
98
+ {
99
+ domains: ['icloud.com', 'me.com', 'mac.com'],
100
+ supportsPlusAddressing: true,
101
+ ignoresDots: false,
102
+ supportsSubdomainAlias: false,
103
+ normalize: (email) => {
104
+ const [username, domain] = email.toLowerCase().split('@');
105
+ const cleanUsername = username.split('+')[0];
106
+ return `${cleanUsername}@${domain}`;
107
+ }
108
+ },
109
+ {
110
+ domains: ['mail.com'],
111
+ supportsPlusAddressing: true,
112
+ ignoresDots: false,
113
+ supportsSubdomainAlias: false,
114
+ normalize: (email) => {
115
+ const [username, domain] = email.toLowerCase().split('@');
116
+ const cleanUsername = username.split('+')[0];
117
+ return `${cleanUsername}@${domain}`;
118
+ }
119
+ },
120
+ {
121
+ domains: ['aol.com', 'love.com', 'ygm.com', 'games.com', 'wow.com', 'aim.com'],
122
+ supportsPlusAddressing: false,
123
+ ignoresDots: false,
124
+ supportsSubdomainAlias: false,
125
+ normalize: (email) => email.toLowerCase()
126
+ },
127
+ {
128
+ domains: ['mail.ru'],
129
+ supportsPlusAddressing: true,
130
+ ignoresDots: false,
131
+ supportsSubdomainAlias: false,
132
+ normalize: (email) => {
133
+ const [username, domain] = email.toLowerCase().split('@');
134
+ const cleanUsername = username.split('+')[0];
135
+ return `${cleanUsername}@${domain}`;
136
+ }
137
+ },
138
+ {
139
+ domains: ['yandex.com', 'yandex.ru'],
140
+ supportsPlusAddressing: true,
141
+ ignoresDots: false,
142
+ supportsSubdomainAlias: false,
143
+ normalize: (email) => {
144
+ const [username, domain] = email.toLowerCase().split('@');
145
+ const cleanUsername = username.split('+')[0];
146
+ return `${cleanUsername}@${domain}`;
147
+ }
148
+ }
149
+ ];
150
+ /**
151
+ * Validates email format
152
+ */
153
+ function isValidEmail(email) {
154
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
155
+ return emailRegex.test(email);
156
+ }
157
+ /**
158
+ * Gets the alias rule for a given domain
159
+ */
160
+ function getAliasRule(domain) {
161
+ return ALIAS_RULES.find(rule => rule.domains.includes(domain.toLowerCase()));
162
+ }
163
+ /**
164
+ * Detects and analyzes email aliases
165
+ */
166
+ function detectEmailAlias(email) {
167
+ if (!isValidEmail(email)) {
168
+ throw new Error('Invalid email format');
169
+ }
170
+ const originalEmail = email.trim();
171
+ const [username, domain] = originalEmail.toLowerCase().split('@');
172
+ const rule = getAliasRule(domain);
173
+ const result = {
174
+ canonical: originalEmail.toLowerCase(),
175
+ original: originalEmail,
176
+ isAlias: false,
177
+ aliasType: 'none'
178
+ };
179
+ if (!rule) {
180
+ // No specific rule, just normalize case
181
+ result.canonical = originalEmail.toLowerCase();
182
+ return result;
183
+ }
184
+ result.provider = domain;
185
+ // Check for plus addressing
186
+ if (rule.supportsPlusAddressing && username.includes('+')) {
187
+ const plusIndex = username.indexOf('+');
188
+ const baseUsername = username.substring(0, plusIndex);
189
+ const aliasPart = username.substring(plusIndex + 1);
190
+ result.isAlias = true;
191
+ result.aliasType = 'plus';
192
+ result.aliasPart = aliasPart;
193
+ result.canonical = rule.normalize ? rule.normalize(originalEmail) : `${baseUsername}@${domain}`;
194
+ return result;
195
+ }
196
+ // Check for dot variations (Gmail)
197
+ if (rule.ignoresDots && username.includes('.')) {
198
+ const baseUsername = username.replace(/\./g, '');
199
+ if (baseUsername !== username) {
200
+ result.isAlias = true;
201
+ result.aliasType = 'dot';
202
+ result.aliasPart = username;
203
+ result.canonical = rule.normalize ? rule.normalize(originalEmail) : `${baseUsername}@${domain}`;
204
+ return result;
205
+ }
206
+ }
207
+ // Apply provider-specific normalization
208
+ if (rule.normalize) {
209
+ const normalized = rule.normalize(originalEmail);
210
+ if (normalized !== originalEmail.toLowerCase()) {
211
+ result.isAlias = true;
212
+ result.canonical = normalized;
213
+ }
214
+ }
215
+ return result;
216
+ }
217
+ /**
218
+ * Normalizes an email address to its canonical form
219
+ */
220
+ function normalizeEmail(email) {
221
+ const result = detectEmailAlias(email);
222
+ return result.canonical;
223
+ }
224
+ /**
225
+ * Checks if two email addresses are the same when normalized
226
+ */
227
+ function emailsMatch(email1, email2) {
228
+ try {
229
+ return normalizeEmail(email1) === normalizeEmail(email2);
230
+ }
231
+ catch {
232
+ return false;
233
+ }
234
+ }
235
+ /**
236
+ * Gets alias capabilities for a domain
237
+ */
238
+ function getAliasCapabilities(domain) {
239
+ const rule = getAliasRule(domain.toLowerCase());
240
+ return rule ? { ...rule } : null;
241
+ }
242
+ /**
243
+ * Generates potential aliases for an email address
244
+ */
245
+ function generateAliases(email, options = {}) {
246
+ if (!isValidEmail(email)) {
247
+ throw new Error('Invalid email format');
248
+ }
249
+ const [username, domain] = email.toLowerCase().split('@');
250
+ const rule = getAliasRule(domain);
251
+ const aliases = [];
252
+ if (!rule) {
253
+ return [email.toLowerCase()];
254
+ }
255
+ // Generate plus aliases
256
+ if (rule.supportsPlusAddressing && options.plusAliases) {
257
+ for (const alias of options.plusAliases) {
258
+ aliases.push(`${username}+${alias}@${domain}`);
259
+ }
260
+ }
261
+ // Generate dot variations (Gmail only)
262
+ if (rule.ignoresDots && options.includeDotVariations && username.length > 1) {
263
+ const maxVariations = Math.min(options.maxDotVariations ?? 5, username.length - 1);
264
+ const variations = new Set();
265
+ // Simple dot variations (insert dots between characters)
266
+ for (let i = 1; i < username.length && variations.size < maxVariations; i++) {
267
+ const withDot = username.slice(0, i) + '.' + username.slice(i);
268
+ variations.add(`${withDot}@${domain}`);
269
+ }
270
+ aliases.push(...Array.from(variations));
271
+ }
272
+ return aliases.length > 0 ? aliases : [email.toLowerCase()];
273
+ }
274
+ /**
275
+ * Analyzes email aliasing patterns in a list of emails
276
+ */
277
+ function analyzeEmailAliases(emails) {
278
+ const canonicalMap = new Map();
279
+ const providerStats = {};
280
+ for (const email of emails) {
281
+ try {
282
+ const result = detectEmailAlias(email);
283
+ const canonical = result.canonical;
284
+ if (!canonicalMap.has(canonical)) {
285
+ canonicalMap.set(canonical, []);
286
+ }
287
+ canonicalMap.get(canonical).push(email);
288
+ // Update provider stats
289
+ if (result.provider) {
290
+ if (!providerStats[result.provider]) {
291
+ providerStats[result.provider] = {
292
+ total: 0,
293
+ aliases: 0,
294
+ types: {}
295
+ };
296
+ }
297
+ providerStats[result.provider].total++;
298
+ if (result.isAlias) {
299
+ providerStats[result.provider].aliases++;
300
+ const type = result.aliasType;
301
+ providerStats[result.provider].types[type] = (providerStats[result.provider].types[type] || 0) + 1;
302
+ }
303
+ }
304
+ }
305
+ catch {
306
+ // Skip invalid emails
307
+ }
308
+ }
309
+ const aliasGroups = Array.from(canonicalMap.entries()).map(([canonical, aliases]) => ({
310
+ canonical,
311
+ aliases,
312
+ count: aliases.length
313
+ })).sort((a, b) => b.count - a.count);
314
+ return {
315
+ totalEmails: emails.length,
316
+ uniqueCanonical: canonicalMap.size,
317
+ aliasGroups,
318
+ providerStats
319
+ };
320
+ }
package/dist/index.d.ts CHANGED
@@ -10,6 +10,30 @@
10
10
  *
11
11
  * @packageDocumentation
12
12
  */
13
+ /**
14
+ * Simple rate limiter to prevent excessive DNS queries.
15
+ * Tracks request timestamps and enforces a maximum number of requests per time window.
16
+ */
17
+ declare class SimpleRateLimiter {
18
+ private maxRequests;
19
+ private windowMs;
20
+ private requestTimestamps;
21
+ constructor(maxRequests?: number, windowMs?: number);
22
+ /**
23
+ * Checks if a request is allowed under the current rate limit.
24
+ * @returns true if request is allowed, false if rate limited
25
+ */
26
+ isAllowed(): boolean;
27
+ /**
28
+ * Gets the current number of requests in the window.
29
+ */
30
+ getCurrentCount(): number;
31
+ /**
32
+ * Gets the time until the rate limit resets (when oldest request expires).
33
+ * @returns milliseconds until reset, or 0 if not rate limited
34
+ */
35
+ getTimeUntilReset(): number;
36
+ }
13
37
  /**
14
38
  * Represents an email provider with its associated domains and login URL.
15
39
  *
@@ -226,12 +250,18 @@ export declare function isEmailProviderSupported(email: string): boolean;
226
250
  *
227
251
  * @remarks
228
252
  * **Detection Algorithm:**
229
- * 1. Performs MX record lookup for the domain
230
- * 2. Checks if MX records match known proxy services (Cloudflare, etc.)
231
- * 3. If proxy detected, returns null provider with proxy info
232
- * 4. Otherwise, matches MX records against business email provider patterns
233
- * 5. If no MX match, falls back to TXT record analysis (SPF records, etc.)
234
- * 6. Returns the first matching provider or null if none found
253
+ * 1. Checks rate limiting (max 10 requests per minute)
254
+ * 2. Performs MX record lookup for the domain
255
+ * 3. Checks if MX records match known proxy services (Cloudflare, etc.)
256
+ * 4. If proxy detected, returns null provider with proxy info
257
+ * 5. Otherwise, matches MX records against business email provider patterns
258
+ * 6. If no MX match, falls back to TXT record analysis (SPF records, etc.)
259
+ * 7. Returns the first matching provider or null if none found
260
+ *
261
+ * **Rate Limiting:**
262
+ * - Maximum 10 DNS requests per 60-second window
263
+ * - Rate limit exceeded returns error with retry information
264
+ * - Prevents abuse and excessive DNS queries
235
265
  *
236
266
  * **Provider Patterns Checked:**
237
267
  * - Google Workspace: aspmx.l.google.com, aspmx2.googlemail.com, etc.
@@ -303,6 +333,24 @@ export declare function detectProviderByDNS(domain: string, timeoutMs?: number):
303
333
  * - Business domain analysis for enterprise features
304
334
  */
305
335
  export declare function getEmailProviderLinkWithDNS(email: string, timeoutMs?: number): Promise<EmailProviderResult>;
336
+ /**
337
+ * Rate limiting configuration constants and utilities.
338
+ * @public
339
+ */
340
+ export declare const RateLimit: {
341
+ MAX_REQUESTS: number;
342
+ WINDOW_MS: number;
343
+ SimpleRateLimiter: typeof SimpleRateLimiter;
344
+ /**
345
+ * Gets the current rate limiter instance for inspection or testing.
346
+ * @internal
347
+ */
348
+ getCurrentLimiter: () => SimpleRateLimiter;
349
+ };
350
+ export * from './alias-detection';
351
+ export * from './security/url-validator';
352
+ export * from './security/hash-verifier';
353
+ export * from './security/secure-loader';
306
354
  declare const _default: {
307
355
  getEmailProviderLink: typeof getEmailProviderLink;
308
356
  getEmailProviderLinkWithDNS: typeof getEmailProviderLinkWithDNS;
@@ -312,5 +360,15 @@ declare const _default: {
312
360
  findEmailProvider: typeof findEmailProvider;
313
361
  getSupportedProviders: typeof getSupportedProviders;
314
362
  isEmailProviderSupported: typeof isEmailProviderSupported;
363
+ RateLimit: {
364
+ MAX_REQUESTS: number;
365
+ WINDOW_MS: number;
366
+ SimpleRateLimiter: typeof SimpleRateLimiter;
367
+ /**
368
+ * Gets the current rate limiter instance for inspection or testing.
369
+ * @internal
370
+ */
371
+ getCurrentLimiter: () => SimpleRateLimiter;
372
+ };
315
373
  };
316
374
  export default _default;