Skip to content

Commit

Permalink
feat: add timeout option for mx dns record lookup
Browse files Browse the repository at this point in the history
  • Loading branch information
jesselpalmer authored Aug 2, 2024
2 parents 6d3ed14 + 246f164 commit 5c08d6d
Show file tree
Hide file tree
Showing 4 changed files with 190 additions and 18 deletions.
63 changes: 52 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,18 @@
# Node Email Verifier

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

## Features

- **RFC 5322 Format Validation**: Validates email addresses against the standard email formatting rules.
- **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.

- **RFC 5322 Format Validation**: Validates email addresses against the standard
email formatting rules.
- **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.
- **Customizable Timeout**: Allows setting a custom timeout for MX record
checking.

## Installation

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

// Example with MX record checking and custom timeout
async function validateEmailWithMxTimeout(email) {
try {
const isValid = await emailValidator(email, { checkMx: true, timeout: '500ms' });
console.log(`Is "${email}" a valid email address with MX checking and custom timeout?`, 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 with custom timeout as a number
async function validateEmailWithMxTimeoutNumber(email) {
try {
const isValid = await emailValidator(email, { checkMx: true, timeout: 500 });
console.log(`Is "${email}" a valid email address with MX checking and custom timeout?`, 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 {
const isValid = await emailValidator(email, false);
const isValid = await emailValidator(email, { checkMx: false });
console.log(`Is "${email}" a valid email address without MX checking?`, isValid);
} catch (error) {
console.error('Error validating email without MX checking:', error);
}
}

validateEmailWithMx('[email protected]').then();
validateEmailWithMxTimeout('[email protected]').then();
validateEmailWithMxTimeoutNumber('[email protected]').then();
validateEmailWithoutMx('[email protected]').then();
```

## API

### ```async emailValidator(email, checkMx = true)```
### ```async emailValidator(email, [opts])```

Validates the given email address, with an option to skip MX record verification.
Validates the given email address, with an option to skip MX record verification
and set a custom timeout.

#### 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``` (string|number): The timeout for the DNS MX lookup, in
milliseconds or ms format (e.g., '2000ms' or '10s'). The default is 10 seconds
('10s').
- ```checkMx``` (boolean): Whether to check for MX records. This defaults to
true.

#### Returns

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

## Contributing

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: 36 additions & 6 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 @@ -36,19 +38,47 @@ const checkMxRecords = async (email) => {
/**
* A sophisticated email validator that checks both the format of the email
* address and the existence of MX records for the domain, depending on the
* checkMx parameter.
* options provided.
*
* @param {string} email - The email address to validate.
* @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
* @param {object} [opts={}] - An object containing options for the validator.
* @param {boolean} [opts.checkMx=true] - Determines whether to check for MX
* records.
* @param {string|number} [opts.timeout='10s'] - 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.
* @return {Promise<boolean>} - Promise that resolves to true if the email is
* valid, false otherwise.
*/
const emailValidator = async (email, checkMx = true) => {
const emailValidator = async (email, opts = {}) => {
// Handle the case where opts is a boolean for backward compatibility
if (typeof opts === 'boolean') {
opts = { checkMx: opts };
}

// Set default values for opts if not provided
const { checkMx = true, timeout = '10s' } = opts;

// Convert timeout to milliseconds
const timeoutMs = typeof timeout === 'string' ? ms(timeout) : timeout;

// Validate the email format
if (!validateRfc5322(email)) return false;

// Check MX records if required
if (checkMx) {
const hasMxRecords = await checkMxRecords(email);
const timeoutController = new AbortController();
const timeoutPromise = setTimeout(timeoutMs, undefined, { signal: timeoutController.signal })
.then(() => {
throw new Error('Domain MX lookup timed out');
});

const lookupMx = checkMxRecords(email).then((hasMxRecords) => {
timeoutController.abort();
return hasMxRecords;
});

const hasMxRecords = await Promise.race([lookupMx, timeoutPromise]);
if (!hasMxRecords) return false;
}

Expand Down
100 changes: 100 additions & 0 deletions test/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,48 @@ describe('Email Validator', () => {
expect(await emailValidator('[email protected]')).toBe(false);
});

test('should timeout MX record check with string timeout', async () => {
await expect(emailValidator('[email protected]', { timeout: '1ms' })).rejects.toThrow(/timed out/);
});

test('should timeout MX record check with number timeout', async () => {
await expect(emailValidator('[email protected]', { timeout: 1 })).rejects.toThrow(/timed out/);
});

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

test('should reject email with invalid domain format', async () => {
expect(await emailValidator('test@invalid-domain')).toBe(false);
});

test('should reject email with special characters in domain', async () => {
expect(await emailValidator('test@exam$ple.com')).toBe(false);
});

test('should reject email with spaces', async () => {
expect(await emailValidator('test @example.com')).toBe(false);
});

test('should reject email with double dots in domain', async () => {
expect(await emailValidator('[email protected]')).toBe(false);
});

test('should validate email with numeric local part', async () => {
expect(await emailValidator('[email protected]')).toBe(true);
});

test('should validate email with hyphen in domain', async () => {
expect(await emailValidator('[email protected]')).toBe(true);
});

test('should reject email with underscore in domain', async () => {
expect(await emailValidator('test@exam_ple.com')).toBe(false);
});
});

describe('without MX record check', () => {
Expand All @@ -37,5 +73,69 @@ describe('Email Validator', () => {
expect(await emailValidator(1234, false)).toBe(false);
expect(await emailValidator({}, false)).toBe(false);
});

test('should reject email with spaces', async () => {
expect(await emailValidator('test @example.com', false)).toBe(false);
});

test('should reject email with double dots in domain', async () => {
expect(await emailValidator('[email protected]', false)).toBe(false);
});

test('should validate email with numeric local part', async () => {
expect(await emailValidator('[email protected]', false)).toBe(true);
});

test('should validate email with hyphen in domain', async () => {
expect(await emailValidator('[email protected]', false)).toBe(true);
});

test('should reject email with underscore in domain', async () => {
expect(await emailValidator('test@exam_ple.com', false)).toBe(false);
});
});

describe('backward compatibility', () => {
test('should validate correct email format and MX record exists with boolean opts', async () => {
expect(await emailValidator('[email protected]', true)).toBe(true);
});

test('should validate correct email format without MX record check with boolean opts', async () => {
expect(await emailValidator('[email protected]', false)).toBe(true);
});
});

describe('options parameter', () => {
test('should validate correct email format with checkMx set to true', async () => {
expect(await emailValidator('[email protected]', { checkMx: true })).toBe(true);
});

test('should validate correct email format with checkMx set to false', async () => {
expect(await emailValidator('[email protected]', { checkMx: false })).toBe(true);
});

test('should timeout with custom timeout setting as string', async () => {
await expect(emailValidator('[email protected]', { timeout: '1ms' })).rejects.toThrow(/timed out/);
});

test('should timeout with custom timeout setting as number', async () => {
await expect(emailValidator('[email protected]', { timeout: 1 })).rejects.toThrow(/timed out/);
});

test('should validate correct email format with custom timeout setting as string', async () => {
expect(await emailValidator('[email protected]', { timeout: '5s' })).toBe(true);
});

test('should validate correct email format with custom timeout setting as number', async () => {
expect(await emailValidator('[email protected]', { timeout: 5000 })).toBe(true);
});

test('should validate correct email format and MX record exists with both options set', async () => {
expect(await emailValidator('[email protected]', { checkMx: true, timeout: '5s' })).toBe(true);
});

test('should validate correct email format without MX record check and custom timeout', async () => {
expect(await emailValidator('[email protected]', { checkMx: false, timeout: '5s' })).toBe(true);
});
});
});

0 comments on commit 5c08d6d

Please sign in to comment.