diff --git a/lib/common.js b/lib/common.js deleted file mode 100755 index 6aa1c09..0000000 --- a/lib/common.js +++ /dev/null @@ -1,91 +0,0 @@ -'use strict'; - -const Punycode = require('punycode'); - -// Util loaded as needed - - -const internals = { - minDomainSegments: 2 -}; - - -exports.nonAsciiRx = /[^\x00-\x7f]/; - - -internals.tldSegmentRx = /^[a-zA-Z](?:[a-zA-Z0-9\-]*[a-zA-Z0-9])?$/; - - -internals.domainSegmentRx = /^[a-zA-Z0-9](?:[a-zA-Z0-9\-]*[a-zA-Z0-9])?$/; - - -exports.domain = function (domain, options) { - - // https://tools.ietf.org/html/rfc1035 section 2.3.1 - - const minDomainSegments = (options.minDomainSegments || internals.minDomainSegments); - - const segments = domain.split('.'); - if (segments.length < minDomainSegments) { - return exports.error('Domain lacks the minimum required number of segments'); - } - - const tlds = options.tlds; - if (tlds) { - const tld = segments[segments.length - 1].toLowerCase(); - if (tlds.deny && tlds.deny.has(tld) || - tlds.allow && !tlds.allow.has(tld)) { - - return exports.error('Domain uses forbidden TLD'); - } - } - - for (let i = 0; i < segments.length; ++i) { - const segment = segments[i]; - - if (!segment.length) { - return exports.error('Domain contains empty dot-separated segment'); - } - - if (segment.length > 63) { - return exports.error('Domain contains dot-separated segment that is too long'); - } - - if (i < segments.length - 1) { - if (!internals.domainSegmentRx.test(segment)) { - return exports.error('Domain contains invalid character'); - } - } - else { - if (!internals.tldSegmentRx.test(segment)) { - return exports.error('Domain contains invalid tld character'); - } - } - } -}; - - -exports.error = function (reason) { - - return { error: reason }; -}; - - -exports.punycode = function (domain) { - - return Punycode.toASCII(domain); -}; - - -exports.encoder = function () { - - // $lab:coverage:off$ - - if (typeof TextEncoder !== 'undefined') { - return new TextEncoder(); - } - - return new (require('util').TextEncoder)(); - - // $lab:coverage:on$ -}(); diff --git a/lib/domain.js b/lib/domain.js index c1b0b21..8c6e538 100755 --- a/lib/domain.js +++ b/lib/domain.js @@ -1,9 +1,15 @@ 'use strict'; -const Common = require('./common'); +// Url is loaded as needed -const internals = {}; +const internals = { + minDomainSegments: 2, + nonAsciiRx: /[^\x00-\x7f]/, + domainControlRx: /[\x00-\x20]/, // Control + space + tldSegmentRx: /^[a-zA-Z](?:[a-zA-Z0-9\-]*[a-zA-Z0-9])?$/, + domainSegmentRx: /^[a-zA-Z0-9](?:[a-zA-Z0-9\-]*[a-zA-Z0-9])?$/ +}; exports.analyze = function (domain, options = {}) { @@ -13,24 +19,69 @@ exports.analyze = function (domain, options = {}) { } if (!domain) { - return Common.error('Domain must be a non-empty string'); + return { error: 'Domain must be a non-empty string' }; } if (domain.length > 256) { - return Common.error('Domain too long'); + return { error: 'Domain too long' }; } - const ascii = !Common.nonAsciiRx.test(domain); + const ascii = !internals.nonAsciiRx.test(domain); if (!ascii) { if (options.allowUnicode === false) { // Defaults to true - return Common.error('Domain contains forbidden Unicode characters'); + return { error: 'Domain contains forbidden Unicode characters' }; } - const normalized = domain.normalize('NFC'); - domain = Common.punycode(normalized); + domain = domain.normalize('NFC'); + } + + if (internals.domainControlRx.test(domain)) { + return { error: 'Domain contains invalid character' }; + } + + domain = internals.punycode(domain); + + // https://tools.ietf.org/html/rfc1035 section 2.3.1 + + const minDomainSegments = options.minDomainSegments || internals.minDomainSegments; + + const segments = domain.split('.'); + if (segments.length < minDomainSegments) { + return { error: 'Domain lacks the minimum required number of segments' }; } - return Common.domain(domain, options); + const tlds = options.tlds; + if (tlds) { + const tld = segments[segments.length - 1].toLowerCase(); + if (tlds.deny && tlds.deny.has(tld) || + tlds.allow && !tlds.allow.has(tld)) { + + return { error: 'Domain uses forbidden TLD' }; + } + } + + for (let i = 0; i < segments.length; ++i) { + const segment = segments[i]; + + if (!segment.length) { + return { error: 'Domain contains empty dot-separated segment' }; + } + + if (segment.length > 63) { + return { error: 'Domain contains dot-separated segment that is too long' }; + } + + if (i < segments.length - 1) { + if (!internals.domainSegmentRx.test(segment)) { + return { error: 'Domain contains invalid character' }; + } + } + else { + if (!internals.tldSegmentRx.test(segment)) { + return { error: 'Domain contains invalid tld character' }; + } + } + } }; @@ -38,3 +89,18 @@ exports.isValid = function (domain, options) { return !exports.analyze(domain, options); }; + + +internals.punycode = function (domain) { + + // $lab:coverage:off$ + const url = typeof Url !== 'undefined' ? Url : require('url'); // eslint-disable-line no-undef + // $lab:coverage:on$ + + try { + return new url.URL(`http://${domain}`).host; + } + catch (err) { + return domain; + } +}; diff --git a/lib/email.js b/lib/email.js index c5b7ef4..4b69346 100755 --- a/lib/email.js +++ b/lib/email.js @@ -1,9 +1,13 @@ 'use strict'; -const Common = require('./common'); +const Domain = require('./domain'); +// Util loaded as needed -const internals = {}; + +const internals = { + nonAsciiRx: /[^\x00-\x7f]/ +}; exports.analyze = function (email, options) { @@ -25,52 +29,46 @@ internals.email = function (email, options = {}) { } if (!email) { - return Common.error('Address must be a non-empty string'); + return { error: 'Address must be a non-empty string' }; } // Unicode - const ascii = !Common.nonAsciiRx.test(email); + const ascii = !internals.nonAsciiRx.test(email); if (!ascii) { if (options.allowUnicode === false) { // Defaults to true - return Common.error('Address contains forbidden Unicode characters'); + return { error: 'Address contains forbidden Unicode characters' }; } - const normalized = email.normalize('NFC'); - email = Common.punycode(normalized); + email = email.normalize('NFC'); } // Basic structure const parts = email.split('@'); if (parts.length !== 2) { - return Common.error(parts.length > 2 ? 'Address cannot contain more than one @ character' : 'Address must contain one @ character'); + return { error: parts.length > 2 ? 'Address cannot contain more than one @ character' : 'Address must contain one @ character' }; } - const local = parts[0]; - const domain = parts[1]; + const [local, domain] = parts; if (!local) { - return Common.error('Address local part cannot be empty'); - } - - if (!domain) { - return Common.error('Domain cannot be empty'); + return { error: 'Address local part cannot be empty' }; } if (!options.ignoreLength) { if (email.length > 254) { // http://tools.ietf.org/html/rfc5321#section-4.5.3.1.3 - return Common.error('Address too long'); + return { error: 'Address too long' }; } - if (Common.encoder.encode(local).length > 64) { // http://tools.ietf.org/html/rfc5321#section-4.5.3.1.1 - return Common.error('Address local part too long'); + if (internals.encoder.encode(local).length > 64) { // http://tools.ietf.org/html/rfc5321#section-4.5.3.1.1 + return { error: 'Address local part too long' }; } } // Validate parts - return internals.local(local, ascii) || Common.domain(domain, options); + return internals.local(local, ascii) || Domain.analyze(domain, options); }; @@ -79,12 +77,12 @@ internals.local = function (local, ascii) { const segments = local.split('.'); for (const segment of segments) { if (!segment.length) { - return Common.error('Address local part contains empty dot-separated segment'); + return { error: 'Address local part contains empty dot-separated segment' }; } if (ascii) { if (!internals.atextRx.test(segment)) { - return Common.error('Address local part contains invalid character'); + return { error: 'Address local part contains invalid character' }; } continue; @@ -97,7 +95,7 @@ internals.local = function (local, ascii) { const binary = internals.binary(char); if (!internals.atomRx.test(binary)) { - return Common.error('Address local part contains invalid character'); + return { error: 'Address local part contains invalid character' }; } } } @@ -106,10 +104,24 @@ internals.local = function (local, ascii) { internals.binary = function (char) { - return Array.from(Common.encoder.encode(char)).map((v) => String.fromCharCode(v)).join(''); + return Array.from(internals.encoder.encode(char)).map((v) => String.fromCharCode(v)).join(''); }; +internals.encoder = function () { + + // $lab:coverage:off$ + + if (typeof TextEncoder !== 'undefined') { + return new TextEncoder(); + } + + return new (require('util').TextEncoder)(); + + // $lab:coverage:on$ +}(); + + /* From RFC 5321: diff --git a/lib/tlds.js b/lib/tlds.js index 0d0aab2..77a00e3 100755 --- a/lib/tlds.js +++ b/lib/tlds.js @@ -4,7 +4,7 @@ const internals = {}; // http://data.iana.org/TLD/tlds-alpha-by-domain.txt -// # Version 2019032300, Last Updated Sat Mar 23 07:07:02 2019 UTC +// # Version 2019091902, Last Updated Fri Sep 20 07: 07: 02 2019 UTC internals.tlds = [ @@ -161,7 +161,6 @@ internals.tlds = [ 'BMS', 'BMW', 'BN', - 'BNL', 'BNPPARIBAS', 'BO', 'BOATS', @@ -368,7 +367,6 @@ internals.tlds = [ 'DOCTOR', 'DODGE', 'DOG', - 'DOHA', 'DOMAINS', 'DOT', 'DOWNLOAD', @@ -377,7 +375,6 @@ internals.tlds = [ 'DUBAI', 'DUCK', 'DUNLOP', - 'DUNS', 'DUPONT', 'DURBAN', 'DVAG', @@ -493,6 +490,7 @@ internals.tlds = [ 'GAMES', 'GAP', 'GARDEN', + 'GAY', 'GB', 'GBIZ', 'GD', @@ -585,7 +583,6 @@ internals.tlds = [ 'HOMES', 'HOMESENSE', 'HONDA', - 'HONEYWELL', 'HORSE', 'HOSPITAL', 'HOST', @@ -639,7 +636,6 @@ internals.tlds = [ 'IR', 'IRISH', 'IS', - 'ISELECT', 'ISMAILI', 'IST', 'ISTANBUL', @@ -824,7 +820,6 @@ internals.tlds = [ 'MO', 'MOBI', 'MOBILE', - 'MOBILY', 'MODA', 'MOE', 'MOI', @@ -1168,7 +1163,6 @@ internals.tlds = [ 'STADA', 'STAPLES', 'STAR', - 'STARHUB', 'STATEBANK', 'STATEFARM', 'STC', @@ -1460,7 +1454,6 @@ internals.tlds = [ 'XN--MGBAH1A3HJKRD', 'XN--MGBAI9AZGQP6J', 'XN--MGBAYH7GPA', - 'XN--MGBB9FBPOB', 'XN--MGBBH1A', 'XN--MGBBH1A71E', 'XN--MGBC0A9AZCG', @@ -1492,6 +1485,7 @@ internals.tlds = [ 'XN--PSSY2U', 'XN--Q9JYB4C', 'XN--QCKA1PMC', + 'XN--QXA6A', 'XN--QXAM', 'XN--RHQV96G', 'XN--ROVU88B', diff --git a/test/index.js b/test/index.js index 0f8a3d9..63de1c3 100755 --- a/test/index.js +++ b/test/index.js @@ -31,7 +31,7 @@ describe('email', () => { ['test@test@test', 'Address cannot contain more than one @ character'], ['test', 'Address must contain one @ character'], ['@example.com', 'Address local part cannot be empty'], - ['test@', 'Domain cannot be empty'], + ['test@', 'Domain must be a non-empty string'], ['1234567890@abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz.com', 'Address too long'], ['1234567890123456789012345678901234567890123456789012345678901234567890@example.com', 'Address local part too long'], ['x..y@example.com', 'Address local part contains empty dot-separated segment'],