@mikkelscheike/email-provider-links 2.7.1 → 2.8.1

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 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
- loginUrl: string;
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');
@@ -215,9 +215,9 @@ function getEmailProviderSync(email) {
215
215
  }
216
216
  };
217
217
  }
218
- // Load providers and find matching domain
219
- const { providers } = (0, loader_1.loadProviders)();
220
- const provider = providers.find(p => p.domains?.some(d => d.toLowerCase() === domain));
218
+ // Use cached providers and domain map for efficient lookup
219
+ const { domainMap } = (0, loader_1.loadProviders)();
220
+ const provider = domainMap.get(domain);
221
221
  const result = {
222
222
  provider: provider || null,
223
223
  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
  */
@@ -4,7 +4,7 @@
4
4
  * Implements parallel MX/TXT record lookups for 2x faster business domain detection.
5
5
  * Uses Promise.allSettled for fault tolerance and intelligent result merging.
6
6
  */
7
- import { EmailProvider } from './index';
7
+ import { EmailProvider } from './api';
8
8
  /**
9
9
  * Configuration for concurrent DNS detection
10
10
  */
@@ -367,11 +367,12 @@ 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
- ];
370
+ // Get proxy services from provider data
371
+ const proxyProviders = this.providers.filter(p => p.type === 'proxy_service');
372
+ const proxyPatterns = proxyProviders.map(provider => ({
373
+ service: provider.companyProvider,
374
+ patterns: [...(provider.customDomainDetection?.mxPatterns || []), ...(provider.domains || [])].map(p => p.toLowerCase())
375
+ }));
375
376
  for (const record of mxQuery.records) {
376
377
  const exchange = record.exchange?.toLowerCase() || '';
377
378
  for (const proxy of proxyPatterns) {
@@ -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': 'f77814bf0537019c6f38bf2744ae21640f04a2d39cb67c5116f6e03160c9486f',
30
+ 'emailproviders.json': 'c9c3cb1590820989071ec2bea8a7560496188031f8fa6367153e642315824cdb',
31
31
  // You can add hashes for other critical files
32
- 'package.json': '67927e7e27636b55dc1fee5e34f16bbbf721a33103b0bc94c3c42923e2325cef'
32
+ 'package.json': 'da08eadfe33e8a5c5bcc3db0f0dccc402b4d8ab8440ff57d2e9aa986921ac66d'
33
33
  };
34
34
  /**
35
35
  * Calculates SHA-256 hash of a file or string content
package/dist/loader.d.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Handles loading email provider data with performance optimizations.
5
5
  */
6
- import { EmailProvider } from './index';
6
+ import { EmailProvider } from './api';
7
7
  /**
8
8
  * Loading statistics for performance monitoring
9
9
  */
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
- companyProvider: compressedProvider.name,
34
- loginUrl: compressedProvider.url,
35
- domains: compressedProvider.domains || []
36
+ companyProvider: compressedProvider.companyProvider,
37
+ loginUrl: compressedProvider.loginUrl || null,
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;
@@ -92,6 +99,10 @@ function loadProvidersInternal(config = {}) {
92
99
  console.log(`⚡ Loading completed in ${loadTime}ms`);
93
100
  console.log(`📊 Stats: ${providers.length} providers, ${domainCount} domains`);
94
101
  }
102
+ if (process.env.NODE_ENV === 'development') {
103
+ const memoryUsageInMB = process.memoryUsage().heapUsed / 1024 / 1024;
104
+ console.log(`🚀 Current memory usage: ${memoryUsageInMB.toFixed(2)} MB`);
105
+ }
95
106
  return {
96
107
  providers,
97
108
  stats: loadingStats
package/dist/schema.d.ts CHANGED
@@ -8,22 +8,41 @@
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;
14
21
  /** Provider display name */
15
- name: string;
16
- /** Login/webmail URL */
17
- url: string;
22
+ companyProvider: string;
23
+ /** Login/webmail URL (or null if not available) */
24
+ loginUrl: string | null;
18
25
  /** Email domains (omitted if empty) */
19
26
  domains?: string[];
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
  /**
package/dist/schema.js CHANGED
@@ -51,14 +51,11 @@ function validateProvider(provider) {
51
51
  if (!provider.id || typeof provider.id !== 'string') {
52
52
  errors.push('Provider ID is required and must be a string');
53
53
  }
54
- if (!provider.name || typeof provider.name !== 'string') {
55
- errors.push('Provider name is required and must be a string');
54
+ if (!provider.companyProvider || typeof provider.companyProvider !== 'string') {
55
+ errors.push('Company provider is required and must be a string');
56
56
  }
57
- if (!provider.url || typeof provider.url !== 'string') {
58
- errors.push('Provider URL is required and must be a string');
59
- }
60
- else if (!provider.url.startsWith('https://')) {
61
- errors.push('Provider URL must use HTTPS');
57
+ if (provider.loginUrl !== null && (typeof provider.loginUrl !== 'string' || !provider.loginUrl.startsWith('https://'))) {
58
+ errors.push('Login URL must be null or a string starting with HTTPS');
62
59
  }
63
60
  if (provider.domains && !Array.isArray(provider.domains)) {
64
61
  errors.push('Domains must be an array');
@@ -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,7 +1,7 @@
1
1
  {
2
2
  "name": "@mikkelscheike/email-provider-links",
3
- "version": "2.7.1",
4
- "description": "TypeScript library for email provider detection with 93 providers (178 domains), concurrent DNS resolution, optimized performance, 91.75% test coverage, and enterprise security for login and password reset flows",
3
+ "version": "2.8.1",
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",
7
7
  "files": [
@@ -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",
@@ -62,17 +63,17 @@
62
63
  "access": "public"
63
64
  },
64
65
  "devDependencies": {
65
- "@jest/globals": "^30.0.2",
66
+ "@jest/globals": "^30.0.3",
66
67
  "@semantic-release/commit-analyzer": "^13.0.1",
67
68
  "@semantic-release/exec": "^7.1.0",
68
69
  "@semantic-release/git": "^10.0.1",
69
70
  "@semantic-release/github": "^11.0.3",
70
- "@semantic-release/npm": "^12.0.1",
71
+ "@semantic-release/npm": "^12.0.2",
71
72
  "@semantic-release/release-notes-generator": "^14.0.3",
72
73
  "@types/jest": "^30.0.0",
73
- "@types/node": "^24.0.3",
74
+ "@types/node": "^24.0.4",
74
75
  "conventional-changelog-conventionalcommits": "^9.0.0",
75
- "jest": "^30.0.2",
76
+ "jest": "^30.0.3",
76
77
  "semantic-release": "^24.2.5",
77
78
  "ts-jest": "^29.4.0",
78
79
  "tsx": "^4.20.3",