Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add timeout option for MX DNS record lookup #8

Merged
merged 6 commits into from
Aug 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
});
});
});
Loading