resolve-email 3.0.41 → 4.0.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/CHANGELOG.md +11 -1
- package/README.md +1 -0
- package/allow-list.json +11 -1
- package/build-throwaway-domain-list.cjs +97 -5
- package/disposable.json +6 -14
- package/fuzz-test.d.ts +2 -0
- package/fuzz-test.d.ts.map +1 -0
- package/fuzz-test.js +259 -0
- package/index.d.ts +4 -4
- package/index.d.ts.map +1 -1
- package/index.js +18 -10
- package/index.test.js +26 -1
- package/package.json +10 -7
- package/reasonable-email.d.ts +2 -0
- package/reasonable-email.d.ts.map +1 -0
- package/reasonable-email.js +11 -0
- package/tsconfig.json +6 -1
- package/wildcard-disposable.cjs +3 -0
- package/wildcard-disposable.d.cts +2 -0
- package/wildcard-disposable.d.cts.map +1 -0
- package/wildcard-disposable.json +5 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,7 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
|
9
9
|
|
|
10
|
-
## [
|
|
10
|
+
## [v4.0.0](https://github.com/bcomnes/resolve-email/compare/v3.0.41...v4.0.0)
|
|
11
|
+
|
|
12
|
+
### Merged
|
|
13
|
+
|
|
14
|
+
- Bump disposable-email-domains from `67be0b5` to `932053b` [`#92`](https://github.com/bcomnes/resolve-email/pull/92)
|
|
15
|
+
|
|
16
|
+
### Commits
|
|
17
|
+
|
|
18
|
+
- **Breaking change:** Filter emails with Zod reasonable email address filter [`af8248d`](https://github.com/bcomnes/resolve-email/commit/af8248d68af56699c8e8b0dfa897e4fc18fb5cb7)
|
|
19
|
+
|
|
20
|
+
## [v3.0.41](https://github.com/bcomnes/resolve-email/compare/v3.0.40...v3.0.41) - 2025-06-18
|
|
11
21
|
|
|
12
22
|
### Commits
|
|
13
23
|
|
package/README.md
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
[](https://socket.dev/npm/package/resolve-email)
|
|
8
8
|
|
|
9
9
|
Resolve the domain of a syntactically valid email address to see if there is even a chance of deliverability. Also checks against a large list of disposable email and other junk/unwated address domains and rejects those.
|
|
10
|
+
It also checks the email address against the (updated) [zod reasonable email regex](https://colinhacks.com/essays/reasonable-email-regex) and filters out unreasonable email addresses.
|
|
10
11
|
|
|
11
12
|
```
|
|
12
13
|
npm install resolve-email
|
package/allow-list.json
CHANGED
|
@@ -1,26 +1,91 @@
|
|
|
1
1
|
const { readFile, writeFile } = require('node:fs/promises')
|
|
2
2
|
const { join } = require('node:path')
|
|
3
3
|
const emailvalidDomains = require('emailvalid/domains.json')
|
|
4
|
+
const { reasonableEmail } = require('./reasonable-email.js')
|
|
4
5
|
|
|
6
|
+
/** @type {Set<string>} */
|
|
5
7
|
const disposableEmailDomains = new Set()
|
|
8
|
+
/** @type {Set<string>} */
|
|
9
|
+
const wildcardDomains = new Set()
|
|
6
10
|
|
|
7
11
|
const disposableEmailDomainsPath = join(__dirname, 'disposable-email-domains', 'disposable_email_blocklist.conf')
|
|
8
12
|
const allowList = join(__dirname, 'disposable-email-domains', 'allowlist.conf')
|
|
9
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Checks if a domain is a wildcard pattern (ends with .*)
|
|
16
|
+
*
|
|
17
|
+
* @param {string} domain - The domain to check
|
|
18
|
+
* @returns {boolean} - Whether the domain is a wildcard pattern
|
|
19
|
+
*/
|
|
20
|
+
function isWildcardDomain (domain) {
|
|
21
|
+
return domain.endsWith('.*')
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Extracts the base domain from a wildcard pattern (removes the .*)
|
|
26
|
+
*
|
|
27
|
+
* @param {string} wildcardDomain - The wildcard domain pattern (e.g., "example.*")
|
|
28
|
+
* @returns {string} - The base domain (e.g., "example")
|
|
29
|
+
*/
|
|
30
|
+
function getBaseDomain (wildcardDomain) {
|
|
31
|
+
// @ts-expect-error
|
|
32
|
+
return wildcardDomain.split('.*')[0] // Remove ".*"
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Checks if a domain would pass the reasonableEmail regex validation.
|
|
37
|
+
*
|
|
38
|
+
* @param {string} domain - The domain to check
|
|
39
|
+
* @returns {boolean} - Whether the domain would create a valid email address
|
|
40
|
+
*/
|
|
41
|
+
function isReasonableDomain (domain) {
|
|
42
|
+
// Skip checking wildcard domains with reasonableEmail
|
|
43
|
+
if (isWildcardDomain(domain)) return true
|
|
44
|
+
|
|
45
|
+
// Create a test email with the domain to check against the regex
|
|
46
|
+
const testEmail = `test@${domain}`
|
|
47
|
+
return reasonableEmail.test(testEmail)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Main function that builds the disposable email domain list.
|
|
52
|
+
*
|
|
53
|
+
* @returns {Promise<void>}
|
|
54
|
+
*/
|
|
10
55
|
const work = async () => {
|
|
11
56
|
console.log('Adding disposable-email-domains')
|
|
12
57
|
const disposableEmailDomainsRaw = await readFile(disposableEmailDomainsPath, { encoding: 'utf-8' })
|
|
58
|
+
/** @type {string[]} */
|
|
13
59
|
const disposableEmailDomainsList = disposableEmailDomainsRaw.split('\n').slice(0, -1)
|
|
60
|
+
/** @type {number} */
|
|
61
|
+
let skippedCount = 0
|
|
14
62
|
for (const domain of disposableEmailDomainsList) {
|
|
15
|
-
|
|
63
|
+
if (isWildcardDomain(domain)) {
|
|
64
|
+
wildcardDomains.add(getBaseDomain(domain))
|
|
65
|
+
} else if (isReasonableDomain(domain)) {
|
|
66
|
+
disposableEmailDomains.add(domain)
|
|
67
|
+
} else {
|
|
68
|
+
skippedCount++
|
|
69
|
+
}
|
|
16
70
|
}
|
|
71
|
+
console.log(`Skipped ${skippedCount} domains from disposable-email-domains that don't pass the reasonableEmail regex`)
|
|
17
72
|
|
|
18
73
|
console.log('Adding emailvalid')
|
|
74
|
+
/** @type {string[]} */
|
|
19
75
|
const disposableOnly = Object.entries(emailvalidDomains).filter(([_domain, type]) => type === 'disposable').map(([domain, _type]) => domain)
|
|
20
76
|
|
|
77
|
+
/** @type {number} */
|
|
78
|
+
let skippedEmailvalidCount = 0
|
|
21
79
|
for (const domain of disposableOnly) {
|
|
22
|
-
|
|
80
|
+
if (isWildcardDomain(domain)) {
|
|
81
|
+
wildcardDomains.add(getBaseDomain(domain))
|
|
82
|
+
} else if (isReasonableDomain(domain)) {
|
|
83
|
+
disposableEmailDomains.add(domain)
|
|
84
|
+
} else {
|
|
85
|
+
skippedEmailvalidCount++
|
|
86
|
+
}
|
|
23
87
|
}
|
|
88
|
+
console.log(`Skipped ${skippedEmailvalidCount} domains from emailvalid that don't pass the reasonableEmail regex`)
|
|
24
89
|
|
|
25
90
|
console.log('Removing anything in disposable-email-domains/allowlist.conf')
|
|
26
91
|
// I guess newlines are a format too
|
|
@@ -28,20 +93,47 @@ const work = async () => {
|
|
|
28
93
|
const allowData = allowDataRaw.split('\n').slice(0, -1)
|
|
29
94
|
for (const domain of allowData) {
|
|
30
95
|
disposableEmailDomains.delete(domain)
|
|
96
|
+
wildcardDomains.delete(getBaseDomain(domain))
|
|
31
97
|
}
|
|
32
98
|
|
|
99
|
+
/** @type {string[]} */
|
|
33
100
|
const denyListOverride = require('./deny-list.json')
|
|
101
|
+
/** @type {number} */
|
|
102
|
+
let skippedDenylistCount = 0
|
|
34
103
|
denyListOverride.forEach(domain => {
|
|
35
|
-
|
|
104
|
+
if (isWildcardDomain(domain)) {
|
|
105
|
+
wildcardDomains.add(getBaseDomain(domain))
|
|
106
|
+
} else if (isReasonableDomain(domain)) {
|
|
107
|
+
disposableEmailDomains.add(domain)
|
|
108
|
+
} else {
|
|
109
|
+
skippedDenylistCount++
|
|
110
|
+
}
|
|
36
111
|
})
|
|
112
|
+
console.log(`Skipped ${skippedDenylistCount} domains from deny-list that don't pass the reasonableEmail regex`)
|
|
37
113
|
|
|
114
|
+
/** @type {string[]} */
|
|
38
115
|
const allowListOverride = require('./allow-list.json')
|
|
39
116
|
allowListOverride.forEach(domain => {
|
|
40
117
|
disposableEmailDomains.delete(domain)
|
|
118
|
+
wildcardDomains.delete(getBaseDomain(domain))
|
|
41
119
|
})
|
|
42
120
|
|
|
43
|
-
|
|
44
|
-
|
|
121
|
+
/** @type {number} */
|
|
122
|
+
const finalCount = disposableEmailDomains.size
|
|
123
|
+
/** @type {number} */
|
|
124
|
+
const wildcardCount = wildcardDomains.size
|
|
125
|
+
|
|
126
|
+
// Sort the domains for consistency
|
|
127
|
+
const sortedDisposable = Array.from(disposableEmailDomains).sort()
|
|
128
|
+
const sortedWildcards = Array.from(wildcardDomains).sort()
|
|
129
|
+
|
|
130
|
+
// Create the disposable.json file (regular domains)
|
|
131
|
+
await writeFile('disposable.json', JSON.stringify(sortedDisposable, null, ' '))
|
|
132
|
+
|
|
133
|
+
// Create the wildcard-disposable.json file (base domains without the .*)
|
|
134
|
+
await writeFile('wildcard-disposable.json', JSON.stringify(sortedWildcards, null, ' '))
|
|
135
|
+
|
|
136
|
+
console.log(`Done! Final disposable domain list contains ${finalCount} domains and ${wildcardCount} wildcard base domains`)
|
|
45
137
|
}
|
|
46
138
|
|
|
47
139
|
work().catch(err => {
|
package/disposable.json
CHANGED
|
@@ -1563,7 +1563,6 @@
|
|
|
1563
1563
|
"alhajj.com",
|
|
1564
1564
|
"alhilal.net",
|
|
1565
1565
|
"aliaswe.us",
|
|
1566
|
-
"alibaba.com",
|
|
1567
1566
|
"alicemunro.com",
|
|
1568
1567
|
"alienware13.com",
|
|
1569
1568
|
"aligamel.com",
|
|
@@ -3307,7 +3306,6 @@
|
|
|
3307
3306
|
"crossroadsmail.com",
|
|
3308
3307
|
"crosswinds.net",
|
|
3309
3308
|
"cruel.co.uk",
|
|
3310
|
-
"cruelintentions",
|
|
3311
3309
|
"crumlin.com",
|
|
3312
3310
|
"crunchcompass.com",
|
|
3313
3311
|
"crusthost.com",
|
|
@@ -3626,7 +3624,6 @@
|
|
|
3626
3624
|
"discard.gq",
|
|
3627
3625
|
"discard.ml",
|
|
3628
3626
|
"discard.tk",
|
|
3629
|
-
"discardmail.*",
|
|
3630
3627
|
"discardmail.com",
|
|
3631
3628
|
"discardmail.de",
|
|
3632
3629
|
"discartmail.com",
|
|
@@ -3680,7 +3677,6 @@
|
|
|
3680
3677
|
"djing.co.uk",
|
|
3681
3678
|
"dldweb.info",
|
|
3682
3679
|
"dlemail.ru",
|
|
3683
|
-
"dm.w3internet.co.uk example.com",
|
|
3684
3680
|
"dmailman.com",
|
|
3685
3681
|
"dmarc.ro",
|
|
3686
3682
|
"dndent.com",
|
|
@@ -3863,6 +3859,7 @@
|
|
|
3863
3859
|
"duk33.com",
|
|
3864
3860
|
"dukedish.com",
|
|
3865
3861
|
"duluthmail.com",
|
|
3862
|
+
"dumalu.com",
|
|
3866
3863
|
"dumb.co.uk",
|
|
3867
3864
|
"dumbarton.net",
|
|
3868
3865
|
"dump-email.info",
|
|
@@ -5261,6 +5258,9 @@
|
|
|
5261
5258
|
"gonefishing.co.uk",
|
|
5262
5259
|
"goneshopping.co.uk",
|
|
5263
5260
|
"gonetopot.co.uk",
|
|
5261
|
+
"gonida.co.uk",
|
|
5262
|
+
"gonida.com",
|
|
5263
|
+
"gonida.uk",
|
|
5264
5264
|
"gonuggets.net",
|
|
5265
5265
|
"good-news.co.uk",
|
|
5266
5266
|
"goodbye.co.uk",
|
|
@@ -5277,7 +5277,6 @@
|
|
|
5277
5277
|
"goodtimegirl.co.uk",
|
|
5278
5278
|
"goodwork.co.uk",
|
|
5279
5279
|
"goodworkfella.co.uk",
|
|
5280
|
-
"googlemail.com",
|
|
5281
5280
|
"googly.co.uk",
|
|
5282
5281
|
"gooilers.net",
|
|
5283
5282
|
"goonby.com",
|
|
@@ -7243,7 +7242,6 @@
|
|
|
7243
7242
|
"mail.pf",
|
|
7244
7243
|
"mail.pt",
|
|
7245
7244
|
"mail.r-o-o-t.com",
|
|
7246
|
-
"mail.ru",
|
|
7247
7245
|
"mail.sisna.com",
|
|
7248
7246
|
"mail.svenz.eu",
|
|
7249
7247
|
"mail.usa.com",
|
|
@@ -8567,7 +8565,6 @@
|
|
|
8567
8565
|
"mailbucket.org",
|
|
8568
8566
|
"mailcalifornia.com",
|
|
8569
8567
|
"mailcat.biz",
|
|
8570
|
-
"mailcatch.*",
|
|
8571
8568
|
"mailcatch.com",
|
|
8572
8569
|
"mailchek.com",
|
|
8573
8570
|
"mailchoose.co",
|
|
@@ -10982,6 +10979,8 @@
|
|
|
10982
10979
|
"rotaniliam.com",
|
|
10983
10980
|
"rotfl.com",
|
|
10984
10981
|
"rothesay.net",
|
|
10982
|
+
"rotomails.co.uk",
|
|
10983
|
+
"rotomails.com",
|
|
10985
10984
|
"rotten.co.uk",
|
|
10986
10985
|
"roughdiamond.co.uk",
|
|
10987
10986
|
"roughnet.com",
|
|
@@ -11691,7 +11690,6 @@
|
|
|
11691
11690
|
"spambob.com",
|
|
11692
11691
|
"spambob.net",
|
|
11693
11692
|
"spambob.org",
|
|
11694
|
-
"spambog.*",
|
|
11695
11693
|
"spambog.com",
|
|
11696
11694
|
"spambog.de",
|
|
11697
11695
|
"spambog.net",
|
|
@@ -13635,12 +13633,6 @@
|
|
|
13635
13633
|
"yada-yada.com",
|
|
13636
13634
|
"yahmail.top",
|
|
13637
13635
|
"yaho.com",
|
|
13638
|
-
"yahoo.ca",
|
|
13639
|
-
"yahoo.co.in",
|
|
13640
|
-
"yahoo.co.jp",
|
|
13641
|
-
"yahoo.co.kr",
|
|
13642
|
-
"yahoo.co.nz",
|
|
13643
|
-
"yahoo.co.uk",
|
|
13644
13636
|
"yahoofs.com",
|
|
13645
13637
|
"yahoomails.site",
|
|
13646
13638
|
"yahooproduct.net",
|
package/fuzz-test.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fuzz-test.d.ts","sourceRoot":"","sources":["fuzz-test.js"],"names":[],"mappings":""}
|
package/fuzz-test.js
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import test from 'node:test'
|
|
2
|
+
import assert from 'node:assert'
|
|
3
|
+
import { resolveEmail } from './index.js'
|
|
4
|
+
|
|
5
|
+
// Regular disposable domains to test
|
|
6
|
+
const regularDomains = [
|
|
7
|
+
'0-mail.com',
|
|
8
|
+
'0clickemail.com',
|
|
9
|
+
'mailinator.com',
|
|
10
|
+
'guerrillamail.com'
|
|
11
|
+
// Note: tempmail.com is not in our block list
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
// Wildcard domains to test
|
|
15
|
+
const wildcardDomains = [
|
|
16
|
+
'spambog.com',
|
|
17
|
+
'spambog.net',
|
|
18
|
+
'spambog.org',
|
|
19
|
+
'discardmail.com',
|
|
20
|
+
'discardmail.net',
|
|
21
|
+
'mailcatch.com'
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
// Fuzzing variations to test
|
|
25
|
+
const variations = [
|
|
26
|
+
// Subdomains
|
|
27
|
+
(/** @type {string} */ domain) => `sub.${domain}`,
|
|
28
|
+
(/** @type {string} */ domain) => `random.${domain}`,
|
|
29
|
+
(/** @type {string} */ domain) => `mail.${domain}`,
|
|
30
|
+
(/** @type {string} */ domain) => `smtp.${domain}`,
|
|
31
|
+
(/** @type {string} */ domain) => `a.b.c.${domain}`,
|
|
32
|
+
|
|
33
|
+
// Case variations
|
|
34
|
+
(/** @type {string} */ domain) => domain.toUpperCase(),
|
|
35
|
+
(/** @type {string} */ domain) => domain.charAt(0).toUpperCase() + domain.slice(1),
|
|
36
|
+
(/** @type {string} */ domain) => domain.split('').map((c, i) => i % 2 ? c.toUpperCase() : c).join(''),
|
|
37
|
+
|
|
38
|
+
// Ports and weird formatting
|
|
39
|
+
(/** @type {string} */ domain) => `${domain}:25`,
|
|
40
|
+
(/** @type {string} */ domain) => `${domain}:587`,
|
|
41
|
+
(/** @type {string} */ domain) => `${domain}:invalid`,
|
|
42
|
+
|
|
43
|
+
// Domain modifications
|
|
44
|
+
(/** @type {string} */ domain) => domain.replace('.', '-'),
|
|
45
|
+
(/** @type {string} */ domain) => domain.replace('.', '_'),
|
|
46
|
+
(/** @type {string} */ domain) => `${domain}.extra`,
|
|
47
|
+
|
|
48
|
+
// Brackets
|
|
49
|
+
(/** @type {string} */ domain) => `[${domain}]`,
|
|
50
|
+
(/** @type {string} */ domain) => `[ipv4:${domain}]`
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
// Generate a test user for each domain and variation
|
|
54
|
+
/**
|
|
55
|
+
*
|
|
56
|
+
* @param {string[]} domainList
|
|
57
|
+
* @returns
|
|
58
|
+
*/
|
|
59
|
+
function generateTestCases (domainList) {
|
|
60
|
+
const testCases = []
|
|
61
|
+
|
|
62
|
+
for (const domain of domainList) {
|
|
63
|
+
// Add the original domain
|
|
64
|
+
testCases.push(`test@${domain}`)
|
|
65
|
+
|
|
66
|
+
// Add all variations
|
|
67
|
+
for (const variation of variations) {
|
|
68
|
+
try {
|
|
69
|
+
testCases.push(`test@${variation(domain)}`)
|
|
70
|
+
} catch (err) {
|
|
71
|
+
const error = err instanceof Error ? err : new Error('Unknown error', { cause: err })
|
|
72
|
+
// Skip if variation throws an error
|
|
73
|
+
console.warn(`Skipping invalid variation for ${domain}:`, error.message)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return testCases
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Create all the test cases
|
|
82
|
+
const disposableTestCases = [
|
|
83
|
+
...generateTestCases(regularDomains),
|
|
84
|
+
...generateTestCases(wildcardDomains)
|
|
85
|
+
]
|
|
86
|
+
|
|
87
|
+
// Also add some hand-crafted edge cases
|
|
88
|
+
const edgeCases = [
|
|
89
|
+
// Strange username parts
|
|
90
|
+
'user.name@mailinator.com',
|
|
91
|
+
'user+tag@guerrillamail.com',
|
|
92
|
+
'very.unusual."@".unusual.com@spambog.com',
|
|
93
|
+
'"very.(),:;<>[]".VERY."very@\\ "very".unusual"@discardmail.com',
|
|
94
|
+
|
|
95
|
+
// IP addresses in domain part (should be rejected by isIP check)
|
|
96
|
+
'test@[127.0.0.1]',
|
|
97
|
+
'test@[ipv6:2001:db8::1]',
|
|
98
|
+
|
|
99
|
+
// Unicode/IDN domains
|
|
100
|
+
'test@mаіlіnаtоr.com', // cyrillic characters that look like latin
|
|
101
|
+
'test@xn--80aacd1bhkfed3a8a5b.xn--p1ai', // Punycode
|
|
102
|
+
|
|
103
|
+
// Port and parameters
|
|
104
|
+
'test@mailinator.com:25',
|
|
105
|
+
'test@guerrillamail.com:587',
|
|
106
|
+
'test@mailinator.com?param=value',
|
|
107
|
+
|
|
108
|
+
// Path-like elements
|
|
109
|
+
'test@mailinator.com/path',
|
|
110
|
+
'test@guerrillamail.com/path/to/resource',
|
|
111
|
+
|
|
112
|
+
// Mixed case
|
|
113
|
+
'test@MaIlInAtOr.CoM',
|
|
114
|
+
'test@SPAMBOG.COM',
|
|
115
|
+
|
|
116
|
+
// Excess whitespace
|
|
117
|
+
'test@ mailinator.com',
|
|
118
|
+
'test@mailinator.com ',
|
|
119
|
+
' test@guerrillamail.com',
|
|
120
|
+
|
|
121
|
+
// URL-encoded characters
|
|
122
|
+
'test@mailinator%2Ecom',
|
|
123
|
+
'test@guerrillamail%2Ecom',
|
|
124
|
+
|
|
125
|
+
// Protocol prefixes
|
|
126
|
+
'test@http://mailinator.com',
|
|
127
|
+
'test@https://guerrillamail.com',
|
|
128
|
+
|
|
129
|
+
// Random gibberish that might be missed
|
|
130
|
+
'test@mailinatorcom',
|
|
131
|
+
'test@guerrillamailcom',
|
|
132
|
+
'test@spambogcom',
|
|
133
|
+
|
|
134
|
+
// Likely typos that should still be caught
|
|
135
|
+
'test@mailinat0r.com', // with number 0
|
|
136
|
+
]
|
|
137
|
+
|
|
138
|
+
disposableTestCases.push(...edgeCases)
|
|
139
|
+
|
|
140
|
+
// Run tests for each case
|
|
141
|
+
for (const testEmail of disposableTestCases) {
|
|
142
|
+
test(`Disposable domain should be rejected: ${testEmail}`, async (_t) => {
|
|
143
|
+
try {
|
|
144
|
+
const result = await resolveEmail(testEmail)
|
|
145
|
+
assert.ok(!result.emailResolves, `Email ${testEmail} should not resolve`)
|
|
146
|
+
assert.ok(result.error, `Email ${testEmail} should have an error`)
|
|
147
|
+
} catch (err) {
|
|
148
|
+
const error = err instanceof Error ? err : new Error('Unknown error', { cause: err })
|
|
149
|
+
// If the test throws (rather than returning a result with error),
|
|
150
|
+
// it's likely a syntax error or similar - log it but don't fail
|
|
151
|
+
console.log(`Exception testing ${testEmail}: ${error.message}`)
|
|
152
|
+
}
|
|
153
|
+
})
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Strict testing for specific cases that should fail as disposable
|
|
157
|
+
// but might be slipping through
|
|
158
|
+
const strictTestCases = [
|
|
159
|
+
// This domain is in our list but might not be getting caught
|
|
160
|
+
// Test these actual domains that should be caught
|
|
161
|
+
'test@mailinator.com',
|
|
162
|
+
'test@sub.mailinator.com',
|
|
163
|
+
'test@mail.mailinator.com',
|
|
164
|
+
'test@MAILINATOR.COM',
|
|
165
|
+
'test@mAiLiNaToR.CoM',
|
|
166
|
+
|
|
167
|
+
// Edge cases that might slip through
|
|
168
|
+
'test@mailinatorcom',
|
|
169
|
+
'test@guerrillamailcom',
|
|
170
|
+
'test@spambogcom',
|
|
171
|
+
|
|
172
|
+
// Malformed but should still be caught as disposable
|
|
173
|
+
'test@.mailinator.com',
|
|
174
|
+
'test@mailinator.com.',
|
|
175
|
+
'test@mailinator..com'
|
|
176
|
+
]
|
|
177
|
+
|
|
178
|
+
// Run strict tests that must always be rejected as disposable
|
|
179
|
+
for (const testEmail of strictTestCases) {
|
|
180
|
+
test(`STRICT: ${testEmail} must be rejected`, async (_t) => {
|
|
181
|
+
const result = await resolveEmail(testEmail)
|
|
182
|
+
|
|
183
|
+
// Must not resolve
|
|
184
|
+
assert.strictEqual(result.emailResolves, false, `Email ${testEmail} should not resolve`)
|
|
185
|
+
|
|
186
|
+
// Must have an error
|
|
187
|
+
assert.ok(result.error, `Email ${testEmail} should have an error`)
|
|
188
|
+
})
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Also add some legitimate domains that should pass
|
|
192
|
+
const legitimateDomains = [
|
|
193
|
+
// Common email providers
|
|
194
|
+
'test@gmail.com',
|
|
195
|
+
'user.name+tag@gmail.com', // Gmail with tag
|
|
196
|
+
'test@googlemail.com', // Gmail alias
|
|
197
|
+
'user.name@yahoo.com',
|
|
198
|
+
'test@outlook.com',
|
|
199
|
+
'test@hotmail.com',
|
|
200
|
+
'test@live.com',
|
|
201
|
+
'test@icloud.com',
|
|
202
|
+
'test@protonmail.com',
|
|
203
|
+
'test@aol.com',
|
|
204
|
+
'test@mail.ru',
|
|
205
|
+
'test@yandex.ru',
|
|
206
|
+
|
|
207
|
+
// Organizations
|
|
208
|
+
'someone@example.org',
|
|
209
|
+
'info@microsoft.com',
|
|
210
|
+
'support@apple.com',
|
|
211
|
+
'contact@amazon.com',
|
|
212
|
+
'help@twitter.com',
|
|
213
|
+
'business@facebook.com',
|
|
214
|
+
'developer@github.com',
|
|
215
|
+
'admin@gitlab.com',
|
|
216
|
+
'hello@stripe.com',
|
|
217
|
+
'noreply@zoom.us',
|
|
218
|
+
'webmaster@cloudflare.com',
|
|
219
|
+
'careers@netflix.com',
|
|
220
|
+
'team@slack.com',
|
|
221
|
+
'feedback@spotify.com',
|
|
222
|
+
'sales@salesforce.com',
|
|
223
|
+
'hello@digitalocean.com',
|
|
224
|
+
'support@dropbox.com',
|
|
225
|
+
'info@ibm.com',
|
|
226
|
+
|
|
227
|
+
// Educational and government domains
|
|
228
|
+
'student@harvard.edu',
|
|
229
|
+
'faculty@mit.edu',
|
|
230
|
+
'staff@stanford.edu',
|
|
231
|
+
'contact@nasa.gov',
|
|
232
|
+
'info@whitehouse.gov',
|
|
233
|
+
|
|
234
|
+
// Country-specific TLDs
|
|
235
|
+
'support@google.com', // Google - US tech giant
|
|
236
|
+
'info@microsoft.com', // Microsoft - US tech giant
|
|
237
|
+
'contact@amazon.co.uk', // Amazon UK
|
|
238
|
+
'support@apple.com', // Apple - US tech giant
|
|
239
|
+
'info@yahoo.co.jp', // Yahoo Japan
|
|
240
|
+
'contact@bbc.co.uk', // BBC - British Broadcasting Corporation
|
|
241
|
+
'info@sap.de', // SAP - German software company
|
|
242
|
+
'support@adobe.com', // Adobe - US software company
|
|
243
|
+
'contact@telstra.com.au', // Telstra - Australian telecom
|
|
244
|
+
'info@shopify.ca', // Shopify - Canadian e-commerce
|
|
245
|
+
'contact@alibaba.com', // Alibaba - Chinese e-commerce
|
|
246
|
+
'support@dropbox.com', // Dropbox - US cloud storage
|
|
247
|
+
'info@sony.jp', // Sony - Japanese electronics company
|
|
248
|
+
'contact@samsung.com', // Samsung - Korean electronics company
|
|
249
|
+
'support@spotify.com' // Spotify - Swedish streaming service
|
|
250
|
+
]
|
|
251
|
+
|
|
252
|
+
for (const testEmail of legitimateDomains) {
|
|
253
|
+
// Generate variations of legitimate domains with unusualr (const testEmail of legitimateDomains) {
|
|
254
|
+
test(`Legitimate domain should be accepted: ${testEmail}`, async (_t) => {
|
|
255
|
+
const result = await resolveEmail(testEmail)
|
|
256
|
+
assert.deepStrictEqual(result.error, undefined, `Email ${testEmail} should not have an error`)
|
|
257
|
+
assert.ok(result.emailResolves, `All of these should resolve ${testEmail}`)
|
|
258
|
+
})
|
|
259
|
+
}
|
package/index.d.ts
CHANGED
|
@@ -3,10 +3,6 @@ export function _resolveMx(email: string, opts?: ResolveOptions | null): Promise
|
|
|
3
3
|
exchange: string;
|
|
4
4
|
}>>;
|
|
5
5
|
export function resolveEmail(domain: string, opts?: ResolveOptions | null): Promise<ResolveResult>;
|
|
6
|
-
export type ResolveOptions = {
|
|
7
|
-
allowIps?: boolean;
|
|
8
|
-
allowDisposable?: boolean;
|
|
9
|
-
};
|
|
10
6
|
export type ResolveResult = {
|
|
11
7
|
emailResolves: boolean;
|
|
12
8
|
mxRecords?: Array<{
|
|
@@ -15,4 +11,8 @@ export type ResolveResult = {
|
|
|
15
11
|
}>;
|
|
16
12
|
error?: Error;
|
|
17
13
|
};
|
|
14
|
+
export type ResolveOptions = {
|
|
15
|
+
allowIps?: boolean;
|
|
16
|
+
allowDisposable?: boolean;
|
|
17
|
+
};
|
|
18
18
|
//# sourceMappingURL=index.d.ts.map
|
package/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["index.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["index.js"],"names":[],"mappings":"AA2BA,kCAJW,MAAM,SACN,cAAc,OAAC,GACb,OAAO,CAAC,KAAK,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAC,CAAC,CAAC,CAiDhE;AA0BD,qCAJW,MAAM,SACN,cAAc,OAAC,GACb,OAAO,CAAC,aAAa,CAAC,CAgBlC;;mBAnGa,OAAO;gBACP,KAAK,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAC,CAAC;YAC3C,KAAK;;;eAXL,OAAO;sBACP,OAAO"}
|
package/index.js
CHANGED
|
@@ -7,6 +7,9 @@ import { disposable } from './disposable.cjs'
|
|
|
7
7
|
* @property {boolean} [allowIps=true] Allow bare IPs as email addresses
|
|
8
8
|
* @property {boolean} [allowDisposable=false] Allow disposable email addresses
|
|
9
9
|
*/
|
|
10
|
+
import { wcDisposable } from './wildcard-disposable.cjs'
|
|
11
|
+
import { reasonableEmail } from './reasonable-email.js'
|
|
12
|
+
import { parse } from 'tldts'
|
|
10
13
|
|
|
11
14
|
/**
|
|
12
15
|
* @typedef {Object} ResolveResult
|
|
@@ -28,20 +31,25 @@ export async function _resolveMx (email, opts) {
|
|
|
28
31
|
allowDisposable: false,
|
|
29
32
|
...opts
|
|
30
33
|
}
|
|
34
|
+
if (typeof email !== 'string' || !email.match(reasonableEmail)) {
|
|
35
|
+
throw new Error('This email address is not reasonable')
|
|
36
|
+
}
|
|
37
|
+
|
|
31
38
|
const domain = (email.split('@').pop() || '').toLowerCase().trim().replace(/^\[(ipv6:)?|\]$/gi, '')
|
|
32
39
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
const parsed = parse(domain)
|
|
41
|
+
const domainWithSuffix = parsed.domain
|
|
42
|
+
const domainWithoutSuffix = parsed.domainWithoutSuffix
|
|
43
|
+
|
|
44
|
+
if (!domainWithoutSuffix || !domainWithSuffix) {
|
|
45
|
+
throw new Error('Invalid domain format', { cause: domain })
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (isIP(domain) || parsed.isIp) {
|
|
49
|
+
throw new Error('An email address with an IP address for the domain is disallowed')
|
|
42
50
|
}
|
|
43
51
|
|
|
44
|
-
if (disposable.has(
|
|
52
|
+
if (!opts.allowDisposable && (disposable.has(domainWithSuffix) || wcDisposable.has(domainWithoutSuffix))) {
|
|
45
53
|
throw new Error('Disposable email addresses are disallowed')
|
|
46
54
|
}
|
|
47
55
|
|
package/index.test.js
CHANGED
|
@@ -31,10 +31,35 @@ const inputs = [
|
|
|
31
31
|
in: 'example@Cock.li',
|
|
32
32
|
expect: false,
|
|
33
33
|
},
|
|
34
|
+
// Test wildcard domain patterns
|
|
35
|
+
{
|
|
36
|
+
in: 'test@spambog.com',
|
|
37
|
+
expect: false,
|
|
38
|
+
description: 'should match wildcard pattern spambog.*'
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
in: 'test@spambog.net',
|
|
42
|
+
expect: false,
|
|
43
|
+
description: 'should match wildcard pattern spambog.*'
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
in: 'test@discardmail.org',
|
|
47
|
+
expect: false,
|
|
48
|
+
description: 'should match wildcard pattern discardmail.*'
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
in: 'test@mailcatch.xyz',
|
|
52
|
+
expect: false,
|
|
53
|
+
description: 'should match wildcard pattern mailcatch.*'
|
|
54
|
+
}
|
|
34
55
|
]
|
|
35
56
|
|
|
36
57
|
for (const i of inputs) {
|
|
37
|
-
|
|
58
|
+
const testName = i.description
|
|
59
|
+
? `${i.in} ${i.expect ? 'resolves' : 'does not resolve'} (${i.description})`
|
|
60
|
+
: `${i.in} ${i.expect ? 'resolves' : 'does not resolve'}`
|
|
61
|
+
|
|
62
|
+
test(testName, async (/** @type {TestContext} */ _t) => {
|
|
38
63
|
const results = await resolveEmail(i.in)
|
|
39
64
|
|
|
40
65
|
assert.strictEqual(
|
package/package.json
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "resolve-email",
|
|
3
3
|
"description": "Resolve the domain of an email address to see if it even has a chance of delivering",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "4.0.0",
|
|
5
5
|
"author": "Bret Comnes <bcomnes@gmail.com> (https://bret.io)",
|
|
6
6
|
"bugs": {
|
|
7
7
|
"url": "https://github.com/bcomnes/resolve-email/issues"
|
|
8
8
|
},
|
|
9
9
|
"devDependencies": {
|
|
10
|
+
"@types/node": "^24.0.1",
|
|
11
|
+
"@voxpelli/tsconfig": "^15.0.0",
|
|
10
12
|
"auto-changelog": "^2.0.0",
|
|
11
13
|
"c8": "^10.0.0",
|
|
12
14
|
"emailvalid": "^1.0.4",
|
|
13
15
|
"gh-release": "^7.0.0",
|
|
14
|
-
"npm-run-all2": "^8.0.1",
|
|
15
|
-
"neostandard": "^0.12.0",
|
|
16
16
|
"installed-check": "^9.3.0",
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"typescript": "~5.8.
|
|
17
|
+
"neostandard": "^0.12.0",
|
|
18
|
+
"npm-run-all2": "^8.0.1",
|
|
19
|
+
"typescript": "~5.8.3"
|
|
20
20
|
},
|
|
21
21
|
"engines": {
|
|
22
22
|
"node": ">=18.0.0",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"test:installed-check": "installed-check --ignore-dev",
|
|
51
51
|
"version": "run-s prepare version:*",
|
|
52
52
|
"version:changelog": "auto-changelog -p --template keepachangelog auto-changelog --breaking-pattern 'BREAKING CHANGE:'",
|
|
53
|
-
"version:git": "git add CHANGELOG.md disposable.json"
|
|
53
|
+
"version:git": "git add CHANGELOG.md disposable.json wildcard-disposable.json"
|
|
54
54
|
},
|
|
55
55
|
"standard": {
|
|
56
56
|
"ignore": [
|
|
@@ -66,5 +66,8 @@
|
|
|
66
66
|
"lcov",
|
|
67
67
|
"text"
|
|
68
68
|
]
|
|
69
|
+
},
|
|
70
|
+
"dependencies": {
|
|
71
|
+
"tldts": "^7.0.9"
|
|
69
72
|
}
|
|
70
73
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reasonable-email.d.ts","sourceRoot":"","sources":["reasonable-email.js"],"names":[],"mappings":"AAQA,8BAFU,MAAM,CAIqF"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Practical email validation regular expression from zod
|
|
3
|
+
* https://colinhacks.com/essays/reasonable-email-regex
|
|
4
|
+
* https://github.com/colinhacks/zod/blob/ee5615d76b93aac15d7428a17b834a062235f6a1/packages/zod/src/v4/core/regexes.ts#L24
|
|
5
|
+
* Couldn't figure out the maze of exports in zod so I just vedored the regex directly.
|
|
6
|
+
*
|
|
7
|
+
* @type {RegExp} Regular expression for validating reasonable email addresses
|
|
8
|
+
*/
|
|
9
|
+
export const reasonableEmail =
|
|
10
|
+
// eslint-disable-next-line no-useless-escape
|
|
11
|
+
/^(?!\.)(?!.*\.\.)([A-Za-z0-9_'+\-\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\-]*\.)+[A-Za-z]{2,}$/
|
package/tsconfig.json
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"extends": "@voxpelli/tsconfig/node20.json",
|
|
3
3
|
"compilerOptions": {
|
|
4
|
-
"skipLibCheck": true
|
|
4
|
+
"skipLibCheck": true,
|
|
5
|
+
"erasableSyntaxOnly": true,
|
|
6
|
+
"allowImportingTsExtensions": true,
|
|
7
|
+
"rewriteRelativeImportExtensions": true,
|
|
8
|
+
"verbatimModuleSyntax": true,
|
|
9
|
+
"module": "nodenext"
|
|
5
10
|
},
|
|
6
11
|
"include": [
|
|
7
12
|
"./**/*"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wildcard-disposable.d.cts","sourceRoot":"","sources":["wildcard-disposable.cjs"],"names":[],"mappings":""}
|