diff --git a/README.md b/README.md index 934b6f2..c0200f2 100644 --- a/README.md +++ b/README.md @@ -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 @@ -31,17 +35,45 @@ 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); @@ -49,23 +81,32 @@ async function validateEmailWithoutMx(email) { } validateEmailWithMx('test@example.com').then(); +validateEmailWithMxTimeout('test@example.com').then(); +validateEmailWithMxTimeoutNumber('test@example.com').then(); validateEmailWithoutMx('test@example.com').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```: A promise that resolves to true if the email address is valid and, if checked, has MX records; false otherwise. +- ```Promise```: A promise that resolves to true if the email address +is valid and, if checked, has MX records; false otherwise. ## Contributing diff --git a/package.json b/package.json index bad1fe2..25fa5a6 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "jest": "^29.7.0" }, "dependencies": { - "validator": "^13.11.0" + "validator": "^13.11.0", + "ms": "^2.1.3" } } diff --git a/src/index.js b/src/index.js index 7a02f4b..049e982 100644 --- a/src/index.js +++ b/src/index.js @@ -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); @@ -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} - 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} - 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; } diff --git a/test/index.test.js b/test/index.test.js index d565224..3f744e4 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -10,12 +10,48 @@ describe('Email Validator', () => { expect(await emailValidator('test@adafwefewsd.com')).toBe(false); }); + test('should timeout MX record check with string timeout', async () => { + await expect(emailValidator('test@example.com', { timeout: '1ms' })).rejects.toThrow(/timed out/); + }); + + test('should timeout MX record check with number timeout', async () => { + await expect(emailValidator('test@example.com', { 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('test@exa..mple.com')).toBe(false); + }); + + test('should validate email with numeric local part', async () => { + expect(await emailValidator('12345@example.com')).toBe(true); + }); + + test('should validate email with hyphen in domain', async () => { + expect(await emailValidator('test@exam-ple.com')).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', () => { @@ -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('test@exa..mple.com', false)).toBe(false); + }); + + test('should validate email with numeric local part', async () => { + expect(await emailValidator('12345@example.com', false)).toBe(true); + }); + + test('should validate email with hyphen in domain', async () => { + expect(await emailValidator('test@exam-ple.com', 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('test@example.com', true)).toBe(true); + }); + + test('should validate correct email format without MX record check with boolean opts', async () => { + expect(await emailValidator('test@example.com', false)).toBe(true); + }); + }); + + describe('options parameter', () => { + test('should validate correct email format with checkMx set to true', async () => { + expect(await emailValidator('test@example.com', { checkMx: true })).toBe(true); + }); + + test('should validate correct email format with checkMx set to false', async () => { + expect(await emailValidator('test@example.com', { checkMx: false })).toBe(true); + }); + + test('should timeout with custom timeout setting as string', async () => { + await expect(emailValidator('test@example.com', { timeout: '1ms' })).rejects.toThrow(/timed out/); + }); + + test('should timeout with custom timeout setting as number', async () => { + await expect(emailValidator('test@example.com', { timeout: 1 })).rejects.toThrow(/timed out/); + }); + + test('should validate correct email format with custom timeout setting as string', async () => { + expect(await emailValidator('test@example.com', { timeout: '5s' })).toBe(true); + }); + + test('should validate correct email format with custom timeout setting as number', async () => { + expect(await emailValidator('test@example.com', { timeout: 5000 })).toBe(true); + }); + + test('should validate correct email format and MX record exists with both options set', async () => { + expect(await emailValidator('test@example.com', { checkMx: true, timeout: '5s' })).toBe(true); + }); + + test('should validate correct email format without MX record check and custom timeout', async () => { + expect(await emailValidator('test@example.com', { checkMx: false, timeout: '5s' })).toBe(true); + }); }); });