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 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
- ## [v3.0.41](https://github.com/bcomnes/resolve-email/compare/v3.0.40...v3.0.41)
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
  [![Socket Badge](https://socket.dev/api/badge/npm/package/resolve-email)](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,3 +1,13 @@
1
1
  [
2
- "rocketmail.com"
2
+ "rocketmail.com",
3
+ "googlemail.com",
4
+ "mail.ru",
5
+ "yahoo.co.jp",
6
+ "yahoo.ca",
7
+ "yahoo.co.in",
8
+ "yahoo.co.jp",
9
+ "yahoo.co.kr",
10
+ "yahoo.co.nz",
11
+ "yahoo.co.uk",
12
+ "alibaba.com"
3
13
  ]
@@ -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
- disposableEmailDomains.add(domain)
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
- disposableEmailDomains.add(domain)
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
- disposableEmailDomains.add(domain)
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
- await writeFile('disposable.json', JSON.stringify(Array.from(disposableEmailDomains).sort(), null, ' '))
44
- console.log('done')
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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=fuzz-test.d.ts.map
@@ -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":"AAwBA,kCAJW,MAAM,SACN,cAAc,OAAC,GACb,OAAO,CAAC,KAAK,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAC,CAAC,CAAC,CA4ChE;AA0BD,qCAJW,MAAM,SACN,cAAc,OAAC,GACb,OAAO,CAAC,aAAa,CAAC,CAgBlC;;eApGa,OAAO;sBACP,OAAO;;;mBAKP,OAAO;gBACP,KAAK,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAC,CAAC;YAC3C,KAAK"}
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
- if (isIP(domain)) {
34
- if (opts.allowIps) {
35
- return [{
36
- priority: 0,
37
- exchange: domain
38
- }]
39
- } else {
40
- throw new Error('An email address with an IP address for the domain is disallowed')
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(domain) && !opts.allowDisposable) {
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
- test(`${i.in} ${i.expect ? 'resolves' : 'does not resolve'}`, async (/** @type {TestContext} */ _t) => {
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": "3.0.41",
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
- "@voxpelli/tsconfig": "^15.0.0",
18
- "@types/node": "^24.0.1",
19
- "typescript": "~5.8.2"
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,2 @@
1
+ export const reasonableEmail: RegExp;
2
+ //# sourceMappingURL=reasonable-email.d.ts.map
@@ -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,3 @@
1
+ const wcDisposable = require('./wildcard-disposable.json')
2
+
3
+ module.exports.wcDisposable = new Set(wcDisposable)
@@ -0,0 +1,2 @@
1
+ export const wcDisposable: Set<string>;
2
+ //# sourceMappingURL=wildcard-disposable.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wildcard-disposable.d.cts","sourceRoot":"","sources":["wildcard-disposable.cjs"],"names":[],"mappings":""}
@@ -0,0 +1,5 @@
1
+ [
2
+ "discardmail",
3
+ "mailcatch",
4
+ "spambog"
5
+ ]