Skip to content

Commit

Permalink
Add timeout option for MX DNS record lookup
Browse files Browse the repository at this point in the history
Add optional second parameter `opts`, an object that can contain the
properties `timeout` and `checkMx`. The `checkMx` property of the `opts`
object overrides the `checkMx` function parameter. The `timeout`
property, in `ms` module format, specifies the timeout for the DNS MX
record lookup, the default is 10 seconds.

Add test, example and update the API documentation.

Signed-off-by: Rafael Kitover <rkitover@gmail.com>
  • Loading branch information
rkitover committed Jun 16, 2024
1 parent 6d3ed14 commit 9f8d654
Show file tree
Hide file tree
Showing 4 changed files with 67 additions and 6 deletions.
24 changes: 22 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,20 @@ async function validateEmailWithMx(email) {
}
}

// Example with MX record checking with timeout
async function validateEmailWithMx(email) {
try {
const isValid = await emailValidator(email, { timeout: '500ms' });
console.log(`Is "${email}" a valid email address with MX checking?`, isValid);
} catch (error) {
if (error.message.match(/timed out/) {
console.error('Timeout on DNS MX lookup.');
} else {
console.error('Error validating email with MX checking:', error);
}
}
}

// Example without MX record checking
async function validateEmailWithoutMx(email) {
try {
Expand All @@ -54,14 +68,20 @@ validateEmailWithoutMx('test@example.com').then();
## API
### ```async emailValidator(email, checkMx = true)```
### ```async emailValidator(email, [opts], checkMx = true)```
Validates the given email address, with an option to skip MX record verification.
#### Parameters
- ```email``` (string): The email address to validate.
- ```checkMx``` (boolean): Whether to check for MX records, this defaults to true.
- ```opts``` (object): Optional configuration options.
- ```timeout``` (number): The timeout in
[ms](https://www.npmjs.com/package/ms) module format, e.g. ```500ms``` for the
the DNS MX lookup, the default is 10 seconds.
- ```checkMx``` (boolean): Whether to check for MX records, overrides parameter.
- ```checkMx``` (boolean): Whether to check for MX records, this defaults to
true, overridden by opts.
#### Returns
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"jest": "^29.7.0"
},
"dependencies": {
"validator": "^13.11.0"
"validator": "^13.11.0",
"ms": "^2.1.3"
}
}
42 changes: 39 additions & 3 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import dns from 'dns';
import util from 'util';
import validator from 'validator';
import ms from 'ms';
import { setTimeout } from 'timers/promises';

// Convert the callback-based dns.resolveMx function into a promise-based one
const resolveMx = util.promisify(dns.resolveMx);
Expand Down Expand Up @@ -39,16 +41,50 @@ const checkMxRecords = async (email) => {
* checkMx parameter.
*
* @param {string} email - The email address to validate.
* @param {object} opts - An object containing options for the validator,
* curently supported options are:
* - checkMx: boolean - Determines whether to check for MX records. Defaults to
* true. This option overrides the checkMx parameter.
* - timeout: number - The time in ms module format, such as '2000ms' or '10s',
* after which the MX validation will be aborted. The default timeout is 10
* seconds.
* @param {boolean} checkMx - Determines whether to check for MX records.
* Defaults to true.
* @return {Promise<boolean>} - Promise that resolves to true if the email is
* valid, false otherwise.
*/
const emailValidator = async (email, checkMx = true) => {
async function emailValidator(email, opts, checkMx) {
if (arguments.length === 2 && typeof opts === 'boolean') {
checkMx = opts;
}
else if (arguments.length < 3) {
checkMx = true;
}

opts ||= {};

if (!('checkMx' in opts)) {
opts.checkMx = checkMx;
}

if (!('timeout' in opts)) {
opts.timeout = '10s';
}
opts.timeout = ms(opts.timeout);

if (!validateRfc5322(email)) return false;

if (checkMx) {
const hasMxRecords = await checkMxRecords(email);
if (opts.checkMx) {
let timeout_controller = new AbortController();
let timeout = setTimeout(opts.timeout, undefined, { signal: timeout_controller.signal }).then(() => {
throw new Error('Domain MX lookup timed out');
});
let hasMxRecords = false;
let lookupMx = checkMxRecords(email).then((res) => {
hasMxRecords = res;
timeout_controller.abort();
});
await Promise.race([lookupMx, timeout]);
if (!hasMxRecords) return false;
}

Expand Down
4 changes: 4 additions & 0 deletions test/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ describe('Email Validator', () => {
expect(await emailValidator('test@adafwefewsd.com')).toBe(false);
});

test('should timeout MX record check', async () => {
expect(async () => { await emailValidator('test@example.com', { timeout: '1ms' }) }).rejects.toThrow(/timed out/);
});

test('should reject non-string inputs', async () => {
expect(await emailValidator(undefined)).toBe(false);
expect(await emailValidator(null)).toBe(false);
Expand Down

0 comments on commit 9f8d654

Please sign in to comment.