resolve-email 3.0.41 → 4.0.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/CHANGELOG.md +17 -1
- package/README.md +1 -0
- package/allow-list.json +11 -1
- package/build-throwaway-domain-list.cjs +97 -5
- package/disposable.json +12 -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,23 @@ 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.1](https://github.com/bcomnes/resolve-email/compare/v4.0.0...v4.0.1)
|
|
11
|
+
|
|
12
|
+
### Merged
|
|
13
|
+
|
|
14
|
+
- Bump disposable-email-domains from `932053b` to `8579448` [`#93`](https://github.com/bcomnes/resolve-email/pull/93)
|
|
15
|
+
|
|
16
|
+
## [v4.0.0](https://github.com/bcomnes/resolve-email/compare/v3.0.41...v4.0.0) - 2025-06-19
|
|
17
|
+
|
|
18
|
+
### Merged
|
|
19
|
+
|
|
20
|
+
- Bump disposable-email-domains from `67be0b5` to `932053b` [`#92`](https://github.com/bcomnes/resolve-email/pull/92)
|
|
21
|
+
|
|
22
|
+
### Commits
|
|
23
|
+
|
|
24
|
+
- **Breaking change:** Filter emails with Zod reasonable email address filter [`af8248d`](https://github.com/bcomnes/resolve-email/commit/af8248d68af56699c8e8b0dfa897e4fc18fb5cb7)
|
|
25
|
+
|
|
26
|
+
## [v3.0.41](https://github.com/bcomnes/resolve-email/compare/v3.0.40...v3.0.41) - 2025-06-18
|
|
11
27
|
|
|
12
28
|
### Commits
|
|
13
29
|
|
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",
|
|
@@ -2450,6 +2449,7 @@
|
|
|
2450
2449
|
"bootmail.com",
|
|
2451
2450
|
"bootssl.com",
|
|
2452
2451
|
"bootybay.de",
|
|
2452
|
+
"bored.lol",
|
|
2453
2453
|
"boredstiff.co.uk",
|
|
2454
2454
|
"boreen.com",
|
|
2455
2455
|
"borged.com",
|
|
@@ -3307,7 +3307,6 @@
|
|
|
3307
3307
|
"crossroadsmail.com",
|
|
3308
3308
|
"crosswinds.net",
|
|
3309
3309
|
"cruel.co.uk",
|
|
3310
|
-
"cruelintentions",
|
|
3311
3310
|
"crumlin.com",
|
|
3312
3311
|
"crunchcompass.com",
|
|
3313
3312
|
"crusthost.com",
|
|
@@ -3626,7 +3625,6 @@
|
|
|
3626
3625
|
"discard.gq",
|
|
3627
3626
|
"discard.ml",
|
|
3628
3627
|
"discard.tk",
|
|
3629
|
-
"discardmail.*",
|
|
3630
3628
|
"discardmail.com",
|
|
3631
3629
|
"discardmail.de",
|
|
3632
3630
|
"discartmail.com",
|
|
@@ -3680,7 +3678,6 @@
|
|
|
3680
3678
|
"djing.co.uk",
|
|
3681
3679
|
"dldweb.info",
|
|
3682
3680
|
"dlemail.ru",
|
|
3683
|
-
"dm.w3internet.co.uk example.com",
|
|
3684
3681
|
"dmailman.com",
|
|
3685
3682
|
"dmarc.ro",
|
|
3686
3683
|
"dndent.com",
|
|
@@ -3863,6 +3860,7 @@
|
|
|
3863
3860
|
"duk33.com",
|
|
3864
3861
|
"dukedish.com",
|
|
3865
3862
|
"duluthmail.com",
|
|
3863
|
+
"dumalu.com",
|
|
3866
3864
|
"dumb.co.uk",
|
|
3867
3865
|
"dumbarton.net",
|
|
3868
3866
|
"dump-email.info",
|
|
@@ -4616,6 +4614,7 @@
|
|
|
4616
4614
|
"fnmail.com",
|
|
4617
4615
|
"fnusa.com",
|
|
4618
4616
|
"fnworld.com",
|
|
4617
|
+
"fog.one",
|
|
4619
4618
|
"folkfan.com",
|
|
4620
4619
|
"foobarbot.net",
|
|
4621
4620
|
"food4u.com",
|
|
@@ -5159,6 +5158,7 @@
|
|
|
5159
5158
|
"gmxmail.top",
|
|
5160
5159
|
"gmxmail.win",
|
|
5161
5160
|
"gnctr-calgary.com",
|
|
5161
|
+
"gni8.com",
|
|
5162
5162
|
"gnwmail.com",
|
|
5163
5163
|
"go.beamteam.com",
|
|
5164
5164
|
"go.com",
|
|
@@ -5261,6 +5261,9 @@
|
|
|
5261
5261
|
"gonefishing.co.uk",
|
|
5262
5262
|
"goneshopping.co.uk",
|
|
5263
5263
|
"gonetopot.co.uk",
|
|
5264
|
+
"gonida.co.uk",
|
|
5265
|
+
"gonida.com",
|
|
5266
|
+
"gonida.uk",
|
|
5264
5267
|
"gonuggets.net",
|
|
5265
5268
|
"good-news.co.uk",
|
|
5266
5269
|
"goodbye.co.uk",
|
|
@@ -5277,7 +5280,6 @@
|
|
|
5277
5280
|
"goodtimegirl.co.uk",
|
|
5278
5281
|
"goodwork.co.uk",
|
|
5279
5282
|
"goodworkfella.co.uk",
|
|
5280
|
-
"googlemail.com",
|
|
5281
5283
|
"googly.co.uk",
|
|
5282
5284
|
"gooilers.net",
|
|
5283
5285
|
"goonby.com",
|
|
@@ -6649,6 +6651,7 @@
|
|
|
6649
6651
|
"killmail.net",
|
|
6650
6652
|
"killorglin.net",
|
|
6651
6653
|
"killybegs.com",
|
|
6654
|
+
"killyourtime.com",
|
|
6652
6655
|
"kilmallock.com",
|
|
6653
6656
|
"kilmarnock.net",
|
|
6654
6657
|
"kilronan.com",
|
|
@@ -7243,7 +7246,6 @@
|
|
|
7243
7246
|
"mail.pf",
|
|
7244
7247
|
"mail.pt",
|
|
7245
7248
|
"mail.r-o-o-t.com",
|
|
7246
|
-
"mail.ru",
|
|
7247
7249
|
"mail.sisna.com",
|
|
7248
7250
|
"mail.svenz.eu",
|
|
7249
7251
|
"mail.usa.com",
|
|
@@ -7251,6 +7253,7 @@
|
|
|
7251
7253
|
"mail.wtf",
|
|
7252
7254
|
"mail0.ga",
|
|
7253
7255
|
"mail1.top",
|
|
7256
|
+
"mail10m.com",
|
|
7254
7257
|
"mail114.net",
|
|
7255
7258
|
"mail15.com",
|
|
7256
7259
|
"mail1a.de",
|
|
@@ -8567,7 +8570,6 @@
|
|
|
8567
8570
|
"mailbucket.org",
|
|
8568
8571
|
"mailcalifornia.com",
|
|
8569
8572
|
"mailcat.biz",
|
|
8570
|
-
"mailcatch.*",
|
|
8571
8573
|
"mailcatch.com",
|
|
8572
8574
|
"mailchek.com",
|
|
8573
8575
|
"mailchoose.co",
|
|
@@ -10982,6 +10984,8 @@
|
|
|
10982
10984
|
"rotaniliam.com",
|
|
10983
10985
|
"rotfl.com",
|
|
10984
10986
|
"rothesay.net",
|
|
10987
|
+
"rotomails.co.uk",
|
|
10988
|
+
"rotomails.com",
|
|
10985
10989
|
"rotten.co.uk",
|
|
10986
10990
|
"roughdiamond.co.uk",
|
|
10987
10991
|
"roughnet.com",
|
|
@@ -11691,7 +11695,6 @@
|
|
|
11691
11695
|
"spambob.com",
|
|
11692
11696
|
"spambob.net",
|
|
11693
11697
|
"spambob.org",
|
|
11694
|
-
"spambog.*",
|
|
11695
11698
|
"spambog.com",
|
|
11696
11699
|
"spambog.de",
|
|
11697
11700
|
"spambog.net",
|
|
@@ -12839,6 +12842,7 @@
|
|
|
12839
12842
|
"ucupdong.ml",
|
|
12840
12843
|
"uemail99.com",
|
|
12841
12844
|
"ufacturing.com",
|
|
12845
|
+
"ug.wtf",
|
|
12842
12846
|
"uga.com",
|
|
12843
12847
|
"uggsrock.com",
|
|
12844
12848
|
"uglyduckling.co.uk",
|
|
@@ -13635,12 +13639,6 @@
|
|
|
13635
13639
|
"yada-yada.com",
|
|
13636
13640
|
"yahmail.top",
|
|
13637
13641
|
"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
13642
|
"yahoofs.com",
|
|
13645
13643
|
"yahoomails.site",
|
|
13646
13644
|
"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.1",
|
|
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":""}
|