| | 'use strict'; |
| |
|
| | const Util = require('util'); |
| |
|
| | const Domain = require('./domain'); |
| | const Errors = require('./errors'); |
| |
|
| |
|
| | const internals = { |
| | nonAsciiRx: /[^\x00-\x7f]/, |
| | encoder: new (Util.TextEncoder || TextEncoder)() |
| | }; |
| |
|
| |
|
| | exports.analyze = function (email, options) { |
| |
|
| | return internals.email(email, options); |
| | }; |
| |
|
| |
|
| | exports.isValid = function (email, options) { |
| |
|
| | return !internals.email(email, options); |
| | }; |
| |
|
| |
|
| | internals.email = function (email, options = {}) { |
| |
|
| | if (typeof email !== 'string') { |
| | throw new Error('Invalid input: email must be a string'); |
| | } |
| |
|
| | if (!email) { |
| | return Errors.code('EMPTY_STRING'); |
| | } |
| |
|
| | |
| |
|
| | const ascii = !internals.nonAsciiRx.test(email); |
| | if (!ascii) { |
| | if (options.allowUnicode === false) { |
| | return Errors.code('FORBIDDEN_UNICODE'); |
| | } |
| |
|
| | email = email.normalize('NFC'); |
| | } |
| |
|
| | |
| |
|
| | const parts = email.split('@'); |
| | if (parts.length !== 2) { |
| | return parts.length > 2 ? Errors.code('MULTIPLE_AT_CHAR') : Errors.code('MISSING_AT_CHAR'); |
| | } |
| |
|
| | const [local, domain] = parts; |
| |
|
| | if (!local) { |
| | return Errors.code('EMPTY_LOCAL'); |
| | } |
| |
|
| | if (!options.ignoreLength) { |
| | if (email.length > 254) { |
| | return Errors.code('ADDRESS_TOO_LONG'); |
| | } |
| |
|
| | if (internals.encoder.encode(local).length > 64) { |
| | return Errors.code('LOCAL_TOO_LONG'); |
| | } |
| | } |
| |
|
| | |
| |
|
| | return internals.local(local, ascii) || Domain.analyze(domain, options); |
| | }; |
| |
|
| |
|
| | internals.local = function (local, ascii) { |
| |
|
| | const segments = local.split('.'); |
| | for (const segment of segments) { |
| | if (!segment.length) { |
| | return Errors.code('EMPTY_LOCAL_SEGMENT'); |
| | } |
| |
|
| | if (ascii) { |
| | if (!internals.atextRx.test(segment)) { |
| | return Errors.code('INVALID_LOCAL_CHARS'); |
| | } |
| |
|
| | continue; |
| | } |
| |
|
| | for (const char of segment) { |
| | if (internals.atextRx.test(char)) { |
| | continue; |
| | } |
| |
|
| | const binary = internals.binary(char); |
| | if (!internals.atomRx.test(binary)) { |
| | return Errors.code('INVALID_LOCAL_CHARS'); |
| | } |
| | } |
| | } |
| | }; |
| |
|
| |
|
| | internals.binary = function (char) { |
| |
|
| | return Array.from(internals.encoder.encode(char)).map((v) => String.fromCharCode(v)).join(''); |
| | }; |
| |
|
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| |
|
| | internals.atextRx = /^[\w!#\$%&'\*\+\-/=\?\^`\{\|\}~]+$/; |
| |
|
| |
|
| | internals.atomRx = new RegExp([ |
| |
|
| | |
| | '(?:[\\xc2-\\xdf][\\x80-\\xbf])', |
| |
|
| | |
| | '(?:\\xe0[\\xa0-\\xbf][\\x80-\\xbf])|(?:[\\xe1-\\xec][\\x80-\\xbf]{2})|(?:\\xed[\\x80-\\x9f][\\x80-\\xbf])|(?:[\\xee-\\xef][\\x80-\\xbf]{2})', |
| |
|
| | |
| | '(?:\\xf0[\\x90-\\xbf][\\x80-\\xbf]{2})|(?:[\\xf1-\\xf3][\\x80-\\xbf]{3})|(?:\\xf4[\\x80-\\x8f][\\x80-\\xbf]{2})' |
| |
|
| | ].join('|')); |
| |
|