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 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
- ## [v3.0.41](https://github.com/bcomnes/resolve-email/compare/v3.0.40...v3.0.41)
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
  [![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",
@@ -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,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.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
- "@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
+ ]