Skip to content

Commit 5c08d6d

Browse files
authored
feat: add timeout option for mx dns record lookup
2 parents 6d3ed14 + 246f164 commit 5c08d6d

File tree

4 files changed

+190
-18
lines changed

4 files changed

+190
-18
lines changed

README.md

+52-11
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,18 @@
44
# Node Email Verifier
55

66
Node Email Verifier is an email validation library for Node.js that checks if an
7-
email address has a valid format and optionally verifies the domain's MX (Mail Exchange)
8-
records to ensure it can receive emails.
7+
email address has a valid format and optionally verifies the domain's MX
8+
(Mail Exchange) records to ensure it can receive emails.
99

1010
## Features
1111

12-
- **RFC 5322 Format Validation**: Validates email addresses against the standard email formatting rules.
13-
- **MX Record Checking**: Verifies that the domain of the email address has valid MX records indicating that it can receive emails. This check can be disabled using a parameter.
14-
12+
- **RFC 5322 Format Validation**: Validates email addresses against the standard
13+
email formatting rules.
14+
- **MX Record Checking**: Verifies that the domain of the email address has
15+
valid MX records indicating that it can receive emails. This check can be
16+
disabled using a parameter.
17+
- **Customizable Timeout**: Allows setting a custom timeout for MX record
18+
checking.
1519

1620
## Installation
1721

@@ -31,41 +35,78 @@ import emailValidator from 'node-email-verifier';
3135
// Example with MX record checking
3236
async function validateEmailWithMx(email) {
3337
try {
34-
const isValid = await emailValidator(email);
38+
const isValid = await emailValidator(email, { checkMx: true });
3539
console.log(`Is "${email}" a valid email address with MX checking?`, isValid);
3640
} catch (error) {
3741
console.error('Error validating email with MX checking:', error);
3842
}
3943
}
4044

45+
// Example with MX record checking and custom timeout
46+
async function validateEmailWithMxTimeout(email) {
47+
try {
48+
const isValid = await emailValidator(email, { checkMx: true, timeout: '500ms' });
49+
console.log(`Is "${email}" a valid email address with MX checking and custom timeout?`, isValid);
50+
} catch (error) {
51+
if (error.message.match(/timed out/)) {
52+
console.error('Timeout on DNS MX lookup.');
53+
} else {
54+
console.error('Error validating email with MX checking:', error);
55+
}
56+
}
57+
}
58+
59+
// Example with custom timeout as a number
60+
async function validateEmailWithMxTimeoutNumber(email) {
61+
try {
62+
const isValid = await emailValidator(email, { checkMx: true, timeout: 500 });
63+
console.log(`Is "${email}" a valid email address with MX checking and custom timeout?`, isValid);
64+
} catch (error) {
65+
if (error.message.match(/timed out/)) {
66+
console.error('Timeout on DNS MX lookup.');
67+
} else {
68+
console.error('Error validating email with MX checking:', error);
69+
}
70+
}
71+
}
72+
4173
// Example without MX record checking
4274
async function validateEmailWithoutMx(email) {
4375
try {
44-
const isValid = await emailValidator(email, false);
76+
const isValid = await emailValidator(email, { checkMx: false });
4577
console.log(`Is "${email}" a valid email address without MX checking?`, isValid);
4678
} catch (error) {
4779
console.error('Error validating email without MX checking:', error);
4880
}
4981
}
5082

5183
validateEmailWithMx('[email protected]').then();
84+
validateEmailWithMxTimeout('[email protected]').then();
85+
validateEmailWithMxTimeoutNumber('[email protected]').then();
5286
validateEmailWithoutMx('[email protected]').then();
5387
```
5488

5589
## API
5690

57-
### ```async emailValidator(email, checkMx = true)```
91+
### ```async emailValidator(email, [opts])```
5892

59-
Validates the given email address, with an option to skip MX record verification.
93+
Validates the given email address, with an option to skip MX record verification
94+
and set a custom timeout.
6095

6196
#### Parameters
6297

6398
- ```email``` (string): The email address to validate.
64-
- ```checkMx``` (boolean): Whether to check for MX records, this defaults to true.
99+
- ```opts``` (object): Optional configuration options.
100+
- ```timeout``` (string|number): The timeout for the DNS MX lookup, in
101+
milliseconds or ms format (e.g., '2000ms' or '10s'). The default is 10 seconds
102+
('10s').
103+
- ```checkMx``` (boolean): Whether to check for MX records. This defaults to
104+
true.
65105

66106
#### Returns
67107

68-
- ```Promise<boolean>```: A promise that resolves to true if the email address is valid and, if checked, has MX records; false otherwise.
108+
- ```Promise<boolean>```: A promise that resolves to true if the email address
109+
is valid and, if checked, has MX records; false otherwise.
69110

70111
## Contributing
71112

package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
"jest": "^29.7.0"
2828
},
2929
"dependencies": {
30-
"validator": "^13.11.0"
30+
"validator": "^13.11.0",
31+
"ms": "^2.1.3"
3132
}
3233
}

src/index.js

+36-6
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import dns from 'dns';
22
import util from 'util';
33
import validator from 'validator';
4+
import ms from 'ms';
5+
import { setTimeout } from 'timers/promises';
46

57
// Convert the callback-based dns.resolveMx function into a promise-based one
68
const resolveMx = util.promisify(dns.resolveMx);
@@ -36,19 +38,47 @@ const checkMxRecords = async (email) => {
3638
/**
3739
* A sophisticated email validator that checks both the format of the email
3840
* address and the existence of MX records for the domain, depending on the
39-
* checkMx parameter.
41+
* options provided.
4042
*
4143
* @param {string} email - The email address to validate.
42-
* @param {boolean} checkMx - Determines whether to check for MX records.
43-
* Defaults to true.
44-
* @return {Promise<boolean>} - Promise that resolves to true if the email is
44+
* @param {object} [opts={}] - An object containing options for the validator.
45+
* @param {boolean} [opts.checkMx=true] - Determines whether to check for MX
46+
* records.
47+
* @param {string|number} [opts.timeout='10s'] - The time in ms module format,
48+
* such as '2000ms' or '10s', after which the MX validation will be aborted.
49+
* The default timeout is 10 seconds.
50+
* @return {Promise<boolean>} - Promise that resolves to true if the email is
4551
* valid, false otherwise.
4652
*/
47-
const emailValidator = async (email, checkMx = true) => {
53+
const emailValidator = async (email, opts = {}) => {
54+
// Handle the case where opts is a boolean for backward compatibility
55+
if (typeof opts === 'boolean') {
56+
opts = { checkMx: opts };
57+
}
58+
59+
// Set default values for opts if not provided
60+
const { checkMx = true, timeout = '10s' } = opts;
61+
62+
// Convert timeout to milliseconds
63+
const timeoutMs = typeof timeout === 'string' ? ms(timeout) : timeout;
64+
65+
// Validate the email format
4866
if (!validateRfc5322(email)) return false;
4967

68+
// Check MX records if required
5069
if (checkMx) {
51-
const hasMxRecords = await checkMxRecords(email);
70+
const timeoutController = new AbortController();
71+
const timeoutPromise = setTimeout(timeoutMs, undefined, { signal: timeoutController.signal })
72+
.then(() => {
73+
throw new Error('Domain MX lookup timed out');
74+
});
75+
76+
const lookupMx = checkMxRecords(email).then((hasMxRecords) => {
77+
timeoutController.abort();
78+
return hasMxRecords;
79+
});
80+
81+
const hasMxRecords = await Promise.race([lookupMx, timeoutPromise]);
5282
if (!hasMxRecords) return false;
5383
}
5484

test/index.test.js

+100
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,48 @@ describe('Email Validator', () => {
1010
expect(await emailValidator('[email protected]')).toBe(false);
1111
});
1212

13+
test('should timeout MX record check with string timeout', async () => {
14+
await expect(emailValidator('[email protected]', { timeout: '1ms' })).rejects.toThrow(/timed out/);
15+
});
16+
17+
test('should timeout MX record check with number timeout', async () => {
18+
await expect(emailValidator('[email protected]', { timeout: 1 })).rejects.toThrow(/timed out/);
19+
});
20+
1321
test('should reject non-string inputs', async () => {
1422
expect(await emailValidator(undefined)).toBe(false);
1523
expect(await emailValidator(null)).toBe(false);
1624
expect(await emailValidator(1234)).toBe(false);
1725
expect(await emailValidator({})).toBe(false);
1826
});
27+
28+
test('should reject email with invalid domain format', async () => {
29+
expect(await emailValidator('test@invalid-domain')).toBe(false);
30+
});
31+
32+
test('should reject email with special characters in domain', async () => {
33+
expect(await emailValidator('test@exam$ple.com')).toBe(false);
34+
});
35+
36+
test('should reject email with spaces', async () => {
37+
expect(await emailValidator('test @example.com')).toBe(false);
38+
});
39+
40+
test('should reject email with double dots in domain', async () => {
41+
expect(await emailValidator('[email protected]')).toBe(false);
42+
});
43+
44+
test('should validate email with numeric local part', async () => {
45+
expect(await emailValidator('[email protected]')).toBe(true);
46+
});
47+
48+
test('should validate email with hyphen in domain', async () => {
49+
expect(await emailValidator('[email protected]')).toBe(true);
50+
});
51+
52+
test('should reject email with underscore in domain', async () => {
53+
expect(await emailValidator('test@exam_ple.com')).toBe(false);
54+
});
1955
});
2056

2157
describe('without MX record check', () => {
@@ -37,5 +73,69 @@ describe('Email Validator', () => {
3773
expect(await emailValidator(1234, false)).toBe(false);
3874
expect(await emailValidator({}, false)).toBe(false);
3975
});
76+
77+
test('should reject email with spaces', async () => {
78+
expect(await emailValidator('test @example.com', false)).toBe(false);
79+
});
80+
81+
test('should reject email with double dots in domain', async () => {
82+
expect(await emailValidator('[email protected]', false)).toBe(false);
83+
});
84+
85+
test('should validate email with numeric local part', async () => {
86+
expect(await emailValidator('[email protected]', false)).toBe(true);
87+
});
88+
89+
test('should validate email with hyphen in domain', async () => {
90+
expect(await emailValidator('[email protected]', false)).toBe(true);
91+
});
92+
93+
test('should reject email with underscore in domain', async () => {
94+
expect(await emailValidator('test@exam_ple.com', false)).toBe(false);
95+
});
96+
});
97+
98+
describe('backward compatibility', () => {
99+
test('should validate correct email format and MX record exists with boolean opts', async () => {
100+
expect(await emailValidator('[email protected]', true)).toBe(true);
101+
});
102+
103+
test('should validate correct email format without MX record check with boolean opts', async () => {
104+
expect(await emailValidator('[email protected]', false)).toBe(true);
105+
});
106+
});
107+
108+
describe('options parameter', () => {
109+
test('should validate correct email format with checkMx set to true', async () => {
110+
expect(await emailValidator('[email protected]', { checkMx: true })).toBe(true);
111+
});
112+
113+
test('should validate correct email format with checkMx set to false', async () => {
114+
expect(await emailValidator('[email protected]', { checkMx: false })).toBe(true);
115+
});
116+
117+
test('should timeout with custom timeout setting as string', async () => {
118+
await expect(emailValidator('[email protected]', { timeout: '1ms' })).rejects.toThrow(/timed out/);
119+
});
120+
121+
test('should timeout with custom timeout setting as number', async () => {
122+
await expect(emailValidator('[email protected]', { timeout: 1 })).rejects.toThrow(/timed out/);
123+
});
124+
125+
test('should validate correct email format with custom timeout setting as string', async () => {
126+
expect(await emailValidator('[email protected]', { timeout: '5s' })).toBe(true);
127+
});
128+
129+
test('should validate correct email format with custom timeout setting as number', async () => {
130+
expect(await emailValidator('[email protected]', { timeout: 5000 })).toBe(true);
131+
});
132+
133+
test('should validate correct email format and MX record exists with both options set', async () => {
134+
expect(await emailValidator('[email protected]', { checkMx: true, timeout: '5s' })).toBe(true);
135+
});
136+
137+
test('should validate correct email format without MX record check and custom timeout', async () => {
138+
expect(await emailValidator('[email protected]', { checkMx: false, timeout: '5s' })).toBe(true);
139+
});
40140
});
41141
});

0 commit comments

Comments
 (0)