'use strict'; const Punycode = require('punycode'); const Abnf = require('./abnf'); const Tlds = require('./tlds'); const internals = { nonAsciiRx: /[^\x00-\x7f]/, minDomainSegments: 2, defaultTlds: { allow: Tlds, deny: null } }; module.exports = { email: { analyze: function (email, options) { return internals.email(email, options); }, isValid: function (email, options) { return !internals.email(email, options); } }, domain: { analyze: function (domain, options = {}) { internals.options(domain, options); if (!domain) { return internals.error('Domain must be a non-empty string'); } if (domain.length > 256) { return internals.error('Domain too long'); } const ascii = !internals.nonAsciiRx.test(domain); if (!ascii) { if (options.allowUnicode === false) { // Defaults to true return internals.error('Domain contains forbidden Unicode characters'); } const normalized = domain.normalize('NFC'); domain = Punycode.toASCII(normalized); } return internals.domain(domain, options); }, isValid: function (domain, options) { return !module.exports.domain.analyze(domain, options); } } }; internals.email = function (email, options = {}) { internals.options(email, options); if (!email) { return internals.error('Address must be a non-empty string'); } // Unicode const ascii = !internals.nonAsciiRx.test(email); if (!ascii) { if (options.allowUnicode === false) { // Defaults to true return internals.error('Address contains forbidden Unicode characters'); } const normalized = email.normalize('NFC'); email = Punycode.toASCII(normalized); } // Basic structure const parts = email.split('@'); if (parts.length !== 2) { return internals.error(parts.length > 2 ? 'Address cannot contain more than one @ character' : 'Address must contain one @ character'); } const local = parts[0]; const domain = parts[1]; if (!local) { return internals.error('Address local part cannot be empty'); } if (!domain) { return internals.error('Domain cannot be empty'); } if (email.length > 254) { // http://tools.ietf.org/html/rfc5321#section-4.5.3.1.3 return internals.error('Address too long'); } if (Buffer.byteLength(local, 'utf-8') > 64) { // http://tools.ietf.org/html/rfc5321#section-4.5.3.1.1 return internals.error('Address local part too long'); } // Validate parts return internals.local(local, ascii) || internals.domain(domain, options); }; internals.options = function (value, options) { // Options validation if (options.tlds && options.tlds !== true) { if (typeof options.tlds !== 'object') { throw new Error('Invalid options: tlds must be a boolean or an object'); } if (options.tlds.allow !== undefined && options.tlds.allow !== true && options.tlds.allow instanceof Set === false) { throw new Error('Invalid options: tlds.allow must be a Set object or true'); } if (options.tlds.deny) { if (options.tlds.deny instanceof Set === false) { throw new Error('Invalid options: tlds.deny must be a Set object'); } if (options.tlds.allow instanceof Set) { throw new Error('Invalid options: cannot specify both tlds.allow and tlds.deny lists'); } } } // Input validation if (typeof value !== 'string') { throw new Error('Invalid input: value must be a string'); } }; internals.local = function (local, ascii) { const segments = local.split('.'); for (const segment of segments) { if (!segment.length) { return internals.error('Address local part contains empty dot-separated segment'); } if (ascii) { if (!Abnf.atextRx.test(segment)) { return internals.error('Address local part contains invalid character'); } } else { for (const char of segment) { const binary = Buffer.from(char).toString('binary'); if (!Abnf.atomRx.test(binary)) { return internals.error('Address local part contains invalid character'); } } } } }; 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])?$/; internals.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 internals.error('Domain lacks the minimum required number of segments'); } const tlds = internals.tlds(options); if (tlds) { const tld = segments[segments.length - 1].toLowerCase(); if (tlds.deny && tlds.deny.has(tld) || tlds.allow && !tlds.allow.has(tld)) { return internals.error('Domain uses forbidden TLD'); } } for (let i = 0; i < segments.length; ++i) { const segment = segments[i]; if (!segment.length) { return internals.error('Domain contains empty dot-separated segment'); } if (segment.length > 63) { return internals.error('Domain contains dot-separated segment that is too long'); } if (i < segments.length - 1) { if (!internals.domainSegmentRx.test(segment)) { return internals.error('Domain contains invalid character'); } } else { if (!internals.tldSegmentRx.test(segment)) { return internals.error('Domain contains invalid tld character'); } } } }; internals.tlds = function (options) { if (options.tlds === false) { // Defaults to true return null; } if (!options.tlds || options.tlds === true) { return internals.defaultTlds; } return { allow: options.tlds.allow === true ? null : options.tlds.allow || Tlds, deny: options.tlds.deny || null }; }; internals.error = function (reason) { return { error: reason }; };