123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248 |
- '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 };
- };
|