You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
170 lines
4.8 KiB
170 lines
4.8 KiB
'use strict'; |
|
|
|
const Util = require('util'); |
|
|
|
const Domain = require('./domain'); |
|
const Errors = require('./errors'); |
|
|
|
|
|
const internals = { |
|
nonAsciiRx: /[^\x00-\x7f]/, |
|
encoder: new (Util.TextEncoder || TextEncoder)() // $lab:coverage:ignore$ |
|
}; |
|
|
|
|
|
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'); |
|
} |
|
|
|
// Unicode |
|
|
|
const ascii = !internals.nonAsciiRx.test(email); |
|
if (!ascii) { |
|
if (options.allowUnicode === false) { // Defaults to true |
|
return Errors.code('FORBIDDEN_UNICODE'); |
|
} |
|
|
|
email = email.normalize('NFC'); |
|
} |
|
|
|
// Basic structure |
|
|
|
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) { // http://tools.ietf.org/html/rfc5321#section-4.5.3.1.3 |
|
return Errors.code('ADDRESS_TOO_LONG'); |
|
} |
|
|
|
if (internals.encoder.encode(local).length > 64) { // http://tools.ietf.org/html/rfc5321#section-4.5.3.1.1 |
|
return Errors.code('LOCAL_TOO_LONG'); |
|
} |
|
} |
|
|
|
// Validate parts |
|
|
|
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(''); |
|
}; |
|
|
|
|
|
/* |
|
From RFC 5321: |
|
|
|
Mailbox = Local-part "@" ( Domain / address-literal ) |
|
|
|
Local-part = Dot-string / Quoted-string |
|
Dot-string = Atom *("." Atom) |
|
Atom = 1*atext |
|
atext = ALPHA / DIGIT / "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" / "/" / "=" / "?" / "^" / "_" / "`" / "{" / "|" / "}" / "~" |
|
|
|
Domain = sub-domain *("." sub-domain) |
|
sub-domain = Let-dig [Ldh-str] |
|
Let-dig = ALPHA / DIGIT |
|
Ldh-str = *( ALPHA / DIGIT / "-" ) Let-dig |
|
|
|
ALPHA = %x41-5A / %x61-7A ; a-z, A-Z |
|
DIGIT = %x30-39 ; 0-9 |
|
|
|
From RFC 6531: |
|
|
|
sub-domain =/ U-label |
|
atext =/ UTF8-non-ascii |
|
|
|
UTF8-non-ascii = UTF8-2 / UTF8-3 / UTF8-4 |
|
|
|
UTF8-2 = %xC2-DF UTF8-tail |
|
UTF8-3 = %xE0 %xA0-BF UTF8-tail / |
|
%xE1-EC 2( UTF8-tail ) / |
|
%xED %x80-9F UTF8-tail / |
|
%xEE-EF 2( UTF8-tail ) |
|
UTF8-4 = %xF0 %x90-BF 2( UTF8-tail ) / |
|
%xF1-F3 3( UTF8-tail ) / |
|
%xF4 %x80-8F 2( UTF8-tail ) |
|
|
|
UTF8-tail = %x80-BF |
|
|
|
Note: The following are not supported: |
|
|
|
RFC 5321: address-literal, Quoted-string |
|
RFC 5322: obs-*, CFWS |
|
*/ |
|
|
|
|
|
internals.atextRx = /^[\w!#\$%&'\*\+\-/=\?\^`\{\|\}~]+$/; // _ included in \w |
|
|
|
|
|
internals.atomRx = new RegExp([ |
|
|
|
// %xC2-DF UTF8-tail |
|
'(?:[\\xc2-\\xdf][\\x80-\\xbf])', |
|
|
|
// %xE0 %xA0-BF UTF8-tail %xE1-EC 2( UTF8-tail ) %xED %x80-9F UTF8-tail %xEE-EF 2( UTF8-tail ) |
|
'(?:\\xe0[\\xa0-\\xbf][\\x80-\\xbf])|(?:[\\xe1-\\xec][\\x80-\\xbf]{2})|(?:\\xed[\\x80-\\x9f][\\x80-\\xbf])|(?:[\\xee-\\xef][\\x80-\\xbf]{2})', |
|
|
|
// %xF0 %x90-BF 2( UTF8-tail ) %xF1-F3 3( UTF8-tail ) %xF4 %x80-8F 2( UTF8-tail ) |
|
'(?:\\xf0[\\x90-\\xbf][\\x80-\\xbf]{2})|(?:[\\xf1-\\xf3][\\x80-\\xbf]{3})|(?:\\xf4[\\x80-\\x8f][\\x80-\\xbf]{2})' |
|
|
|
].join('|'));
|
|
|