Skip to content
This repository has been archived by the owner on Oct 2, 2024. It is now read-only.

Commit

Permalink
Remove dns lookup (#153)
Browse files Browse the repository at this point in the history
Also introduces punycode as a dependency to handle unicode domain names.
  • Loading branch information
WesTyler authored and skeggse committed Jun 22, 2017
1 parent 0f795c5 commit ac9acbd
Show file tree
Hide file tree
Showing 6 changed files with 52 additions and 265 deletions.
15 changes: 6 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ API
validate(email, [options], [callback])
--------------------------------------

Determines whether the `email` is valid or not, for various definitions thereof. Optionally accepts an `options` object and a `callback` function. Options may include `errorLevel` and `checkDNS`. The `callback` function will always be called if specified, and the result of the operation supplied as the only parameter to the callback function. If `validate()` is not asked to check for the existence of the domain (`checkDNS`), it will also synchronously return the result of the operation.
Determines whether the `email` is valid or not, for various definitions thereof. Optionally accepts an `options` object and a `callback` function. Options may include `errorLevel`. The `callback` function will always be called if specified, and the result of the operation supplied as the only parameter to the callback function. `validate()` will synchronously return the result of the operation if no callback is provided.

Use `errorLevel` to specify the type of result for `validate()`. Passing a `false` literal will result in a true or false boolean indicating whether the email address is sufficiently defined for use in sending an email. Passing a `true` literal will result in a more granular numeric status, with zero being a perfectly valid email address. Passing a number will return `0` if the numeric status is below the `errorLevel` and the numeric status otherwise.

Expand All @@ -46,7 +46,7 @@ The `tldWhitelist` option can be either an object lookup table or an array of va

Only one of `tldBlacklist` and `tldWhitelist` will be consulted for TLD validity.

The `minDomainAtoms` option is an optional positive integer that specifies the minimum number of domain atoms that must be included for the email address to be considered valid. Be careful with the option, as some top-level domains, like `io`, directly support email addresses. To better handle fringe cases like the `io` TLD, use the `checkDNS` parameter, which will only allow email addresses for domains which have an MX record.
The `minDomainAtoms` option is an optional positive integer that specifies the minimum number of domain atoms that must be included for the email address to be considered valid. Be careful with the option, as some top-level domains, like `io`, directly support email addresses.

### Examples

Expand All @@ -61,25 +61,22 @@ true
> Isemail.validate('test@iana.org', log);
result true
true
> Isemail.validate('test@iana.org', {checkDNS: true});
undefined
> Isemail.validate('test@iana.org', {checkDNS: true}, log);
> Isemail.validate('test@iana.org');
undefined
result true
> Isemail.validate('test@iana.org', {errorLevel: true});
0
> Isemail.validate('test@iana.org', {errorLevel: true}, log);
result 0
0
> Isemail.validate('test@e.com');
true
> Isemail.validate('test@e.com', {checkDNS: true, errorLevel: true}, log);
> Isemail.validate('test@e.com', {errorLevel: true}, log);
undefined
result 6
> Isemail.validate('test@e.com', {checkDNS: true, errorLevel: 7}, log);
> Isemail.validate('test@e.com', {errorLevel: 7}, log);
undefined
result 0
> Isemail.validate('test@e.com', {checkDNS: true, errorLevel: 6}, log);
> Isemail.validate('test@e.com', {errorLevel: 6}, log);
undefined
result 6
```
Expand Down
152 changes: 24 additions & 128 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

// Load modules

const Dns = require('dns');
const Punycode = require('punycode');

// Declare internals
Expand All @@ -29,11 +28,6 @@ const internals = {

valid: 0,

// Address is valid, but the DNS check failed

dnsWarnNoMXRecord: 5,
dnsWarnNoRecord: 6,

// Address is valid for SMTP but has unusual elements

rfc5321TLD: 9,
Expand Down Expand Up @@ -113,16 +107,6 @@ const internals = {
};


// $lab:coverage:off$
internals.defer = typeof process !== 'undefined' && process && typeof process.nextTick === 'function' ?
process.nextTick.bind(process) :
function (callback) {

return setTimeout(callback, 0);
};
// $lab:coverage:on$


internals.specials = function () {

const specials = '()<>[]:;@\\,."'; // US-ASCII visible characters not valid for atext (http://tools.ietf.org/html/rfc5322#section-3.2.3)
Expand Down Expand Up @@ -226,8 +210,6 @@ internals.validDomain = function (tldAtom, options) {
*
* @param {string} email The email address to check. See README for specifics.
* @param {Object} options The (optional) options:
* {boolean} checkDNS If true then will check DNS for MX records. If
* true this call to isEmail _will_ be asynchronous.
* {*} errorLevel Determines the boundary between valid and invalid
* addresses.
* {*} tldBlacklist The set of domains to consider invalid.
Expand All @@ -249,10 +231,6 @@ exports.validate = internals.validate = function (email, options, callback) {
}

if (typeof callback !== 'function') {
if (options.checkDNS) {
throw new TypeError('expected callback function for checkDNS option');
}

callback = null;
}

Expand Down Expand Up @@ -586,6 +564,7 @@ exports.validate = internals.validate = function (email, options, callback) {

// Next dot-atom element
case '.':
const punycodeLength = Punycode.encode(atomData.domains[elementCount]).length;
if (elementLength === 0) {
// Another dot, already? Fatal error.
updateResult(elementCount === 0 ? internals.diagnoses.errDotStart : internals.diagnoses.errConsecutiveDots);
Expand All @@ -594,14 +573,12 @@ exports.validate = internals.validate = function (email, options, callback) {
// Previous subdomain ended in a hyphen. Fatal error.
updateResult(internals.diagnoses.errDomainHyphenEnd);
}
else if (elementLength > 63) {
// Nowhere in RFC 5321 does it say explicitly that the domain part of a Mailbox must be a valid domain according to the
// DNS standards set out in RFC 1035, but this *is* implied in several places. For instance, wherever the idea of host
// routing is discussed the RFC says that the domain must be looked up in the DNS. This would be nonsense unless the
// domain was designed to be a valid DNS domain. Hence we must conclude that the RFC 1035 restriction on label length
// also applies to RFC 5321 domains.
else if (punycodeLength > 63) {
// RFC 5890 specifies that domain labels that are encoded using the Punycode algorithm
// must adhere to the <= 63 octet requirement.
// This includes string prefixes from the Punycode algorithm.
//
// http://tools.ietf.org/html/rfc1035#section-2.3.4
// https://tools.ietf.org/html/rfc5890#section-2.3.2.1
// labels 63 octets or less

updateResult(internals.diagnoses.rfc5322LabelTooLong);
Expand Down Expand Up @@ -1238,6 +1215,7 @@ exports.validate = internals.validate = function (email, options, callback) {

// Check for errors
if (maxResult < internals.categories.rfc5322) {
const punycodeLength = Punycode.encode(parseData.domain).length;
// Fatal errors
if (context.now === internals.components.contextQuotedString) {
updateResult(internals.diagnoses.errUnclosedQuotedString);
Expand Down Expand Up @@ -1265,12 +1243,12 @@ exports.validate = internals.validate = function (email, options, callback) {
}

// Other errors
else if (Buffer.byteLength(parseData.domain, 'utf8') > 255) {
else if (punycodeLength > 255) {
// http://tools.ietf.org/html/rfc5321#section-4.5.3.1.2
// The maximum total length of a domain name or number is 255 octets.
updateResult(internals.diagnoses.rfc5322DomainTooLong);
}
else if (Buffer.byteLength(parseData.local, 'utf8') + Buffer.byteLength(parseData.domain, 'utf8') + /* '@' */ 1 > 254) {
else if (Buffer.byteLength(parseData.local, 'utf8') + punycodeLength + /* '@' */ 1 > 254) {
// http://tools.ietf.org/html/rfc5321#section-4.1.2
// Forward-path = Path
//
Expand Down Expand Up @@ -1305,109 +1283,27 @@ exports.validate = internals.validate = function (email, options, callback) {
}
} // Check for errors

let dnsPositive = false;
let finishImmediately = false;
// Finish
if (maxResult < internals.categories.dnsWarn) {
// Per RFC 5321, domain atoms are limited to letter-digit-hyphen, so we only need to check code <= 57 to check for a digit
const code = atomData.domains[elementCount].codePointAt(0);

const finish = () => {

if (!dnsPositive && maxResult < internals.categories.dnsWarn) {
// Per RFC 5321, domain atoms are limited to letter-digit-hyphen, so we only need to check code <= 57 to check for a digit
const code = atomData.domains[elementCount].codePointAt(0);
if (code <= 57) {
updateResult(internals.diagnoses.rfc5321TLDNumeric);
}
else if (elementCount === 0) {
updateResult(internals.diagnoses.rfc5321TLD);
}
}

if (maxResult < threshold) {
maxResult = internals.diagnoses.valid;
}

const finishResult = diagnose ? maxResult : maxResult < internals.defaultThreshold;

if (callback) {
if (finishImmediately) {
callback(finishResult);
}
else {
internals.defer(callback.bind(null, finishResult));
}
if (code <= 57) {
updateResult(internals.diagnoses.rfc5321TLDNumeric);
}
}

return finishResult;
}; // Finish

if (options.checkDNS && maxResult < internals.categories.dnsWarn) {
// http://tools.ietf.org/html/rfc5321#section-2.3.5
// Names that can be resolved to MX RRs or address (i.e., A or AAAA) RRs (as discussed in Section 5) are permitted, as are CNAME RRs whose
// targets can be resolved, in turn, to MX or address RRs.
//
// http://tools.ietf.org/html/rfc5321#section-5.1
// The lookup first attempts to locate an MX record associated with the name. If a CNAME record is found, the resulting name is processed
// as if it were the initial name. ... If an empty list of MXs is returned, the address is treated as if it was associated with an implicit
// MX RR, with a preference of 0, pointing to that host.
//
// isEmail() author's note: We will regard the existence of a CNAME to be sufficient evidence of the domain's existence. For performance
// reasons we will not repeat the DNS lookup for the CNAME's target, but we will raise a warning because we didn't immediately find an MX
// record.
if (elementCount === 0) {
// Checking TLD DNS only works if you explicitly check from the root
parseData.domain += '.';
}

const dnsDomain = Punycode.toASCII(parseData.domain);
Dns.resolveMx(dnsDomain, (err, mxRecords) => {

// If we have a fatal error, then we must assume that there are no records
if (err && err.code !== Dns.NODATA) {
updateResult(internals.diagnoses.dnsWarnNoRecord);
return finish();
}

if (mxRecords && mxRecords.length) {
dnsPositive = true;
return finish();
}

let count = 3;
let done = false;
updateResult(internals.diagnoses.dnsWarnNoMXRecord);

const handleRecords = (ignoreError, records) => {

if (done) {
return;
}

--count;

if (records && records.length) {
done = true;
return finish();
}

if (count === 0) {
// No usable records for the domain can be found
updateResult(internals.diagnoses.dnsWarnNoRecord);
done = true;
finish();
}
};
if (maxResult < threshold) {
maxResult = internals.diagnoses.valid;
}

Dns.resolveCname(dnsDomain, handleRecords);
Dns.resolve4(dnsDomain, handleRecords);
Dns.resolve6(dnsDomain, handleRecords);
});
const finishResult = diagnose ? maxResult : maxResult < internals.defaultThreshold;

finishImmediately = true;
if (callback) {
callback(finishResult);
}
else {
const result = finish();
finishImmediately = true;
return result;
} // CheckDNS

return finishResult;
};


Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"node": ">=4.0.0"
},
"dependencies": {
"punycode": "2.1.x"
"punycode": "2.x.x"
},
"devDependencies": {
"code": "3.x.x",
Expand Down
54 changes: 7 additions & 47 deletions test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,28 +48,20 @@ const tldExpectations = [
['shouldbe@example.com', diag.valid]
];

const noDNSExpectations = [
['伊昭傑@郵件.商務', diag.valid],
['ñoñó1234@ñomething.com', diag.valid]
];

describe('validate()', () => {

it('should check options.tldWhitelist', (done) => {

expect(Isemail.validate('person@top', {
tldWhitelist: 'top',
checkDNS: false
tldWhitelist: 'top'
})).to.equal(true);

expect(Isemail.validate('person@top', {
tldWhitelist: ['com'],
checkDNS: false
tldWhitelist: ['com']
})).to.equal(false);

expect(Isemail.validate('person@top', {
tldWhitelist: { com: true },
checkDNS: false
tldWhitelist: { com: true }
})).to.equal(false);

expect(() => {
Expand All @@ -84,18 +76,15 @@ describe('validate()', () => {
it('should check options.tldBlacklist', (done) => {

expect(Isemail.validate('person@top', {
tldBlacklist: 'top',
checkDNS: false
tldBlacklist: 'top'
})).to.equal(false);

expect(Isemail.validate('person@top', {
tldBlacklist: ['com'],
checkDNS: false
tldBlacklist: ['com']
})).to.equal(true);

expect(Isemail.validate('person@top', {
tldBlacklist: { com: true },
checkDNS: false
tldBlacklist: { com: true }
})).to.equal(true);

expect(() => {
Expand Down Expand Up @@ -137,17 +126,6 @@ describe('validate()', () => {
done();
});

it('should ensure callback provided with checkDNS', (done) => {

expect(() => {

Isemail.validate('person@top', {
checkDNS: true
});
}).to.throw(/(?=.*\bcheckDNS\b)(?=.*\bcallback\b)/);
done();
});

it('should handle omitted options', (done) => {

expect(Isemail.validate(expectations[0][0])).to.equal(expectations[0][1] < internals.defaultThreshold);
Expand All @@ -170,8 +148,7 @@ describe('validate()', () => {
it('should handle test ' + (i + 1), (done) => {

Isemail.validate(email, {
errorLevel: 0,
checkDNS: true
errorLevel: 0
}, (res) => {

expect(res).to.equal(result);
Expand Down Expand Up @@ -211,23 +188,6 @@ describe('validate()', () => {
});
});

noDNSExpectations.forEach((obj, i) => {

const email = obj[0];
const result = obj[1];
it('should handle noDNS test ' + (i + 1), (done) => {

Isemail.validate(email, {
errorLevel: 0,
checkDNS: false
}, (res) => {

expect(res).to.equal(result);
done();
});
});
});

it('should handle domain atom test 1', (done) => {

expect(Isemail.validate('shouldbe@invalid', {
Expand Down
Loading

0 comments on commit ac9acbd

Please sign in to comment.