1379 lines
59 KiB
JavaScript
1379 lines
59 KiB
JavaScript
|
'use strict';
|
||
|
|
||
|
// Load modules
|
||
|
|
||
|
const Dns = require('dns');
|
||
|
|
||
|
|
||
|
// Declare internals
|
||
|
|
||
|
const internals = {
|
||
|
hasOwn: Object.prototype.hasOwnProperty,
|
||
|
indexOf: Array.prototype.indexOf,
|
||
|
defaultThreshold: 16,
|
||
|
maxIPv6Groups: 8,
|
||
|
|
||
|
categories: {
|
||
|
valid: 1,
|
||
|
dnsWarn: 7,
|
||
|
rfc5321: 15,
|
||
|
cfws: 31,
|
||
|
deprecated: 63,
|
||
|
rfc5322: 127,
|
||
|
error: 255
|
||
|
},
|
||
|
|
||
|
diagnoses: {
|
||
|
|
||
|
// Address is valid
|
||
|
|
||
|
valid: 0,
|
||
|
|
||
|
// Address is valid, but the DNS check failed
|
||
|
|
||
|
dnsWarnNoMXRecord: 5,
|
||
|
dnsWarnNoRecord: 6,
|
||
|
|
||
|
// Address is valid for SMTP but has unusual elements
|
||
|
|
||
|
rfc5321TLD: 9,
|
||
|
rfc5321TLDNumeric: 10,
|
||
|
rfc5321QuotedString: 11,
|
||
|
rfc5321AddressLiteral: 12,
|
||
|
|
||
|
// Address is valid for message, but must be modified for envelope
|
||
|
|
||
|
cfwsComment: 17,
|
||
|
cfwsFWS: 18,
|
||
|
|
||
|
// Address contains deprecated elements, but may still be valid in some contexts
|
||
|
|
||
|
deprecatedLocalPart: 33,
|
||
|
deprecatedFWS: 34,
|
||
|
deprecatedQTEXT: 35,
|
||
|
deprecatedQP: 36,
|
||
|
deprecatedComment: 37,
|
||
|
deprecatedCTEXT: 38,
|
||
|
deprecatedIPv6: 39,
|
||
|
deprecatedCFWSNearAt: 49,
|
||
|
|
||
|
// Address is only valid according to broad definition in RFC 5322, but is otherwise invalid
|
||
|
|
||
|
rfc5322Domain: 65,
|
||
|
rfc5322TooLong: 66,
|
||
|
rfc5322LocalTooLong: 67,
|
||
|
rfc5322DomainTooLong: 68,
|
||
|
rfc5322LabelTooLong: 69,
|
||
|
rfc5322DomainLiteral: 70,
|
||
|
rfc5322DomainLiteralOBSDText: 71,
|
||
|
rfc5322IPv6GroupCount: 72,
|
||
|
rfc5322IPv62x2xColon: 73,
|
||
|
rfc5322IPv6BadCharacter: 74,
|
||
|
rfc5322IPv6MaxGroups: 75,
|
||
|
rfc5322IPv6ColonStart: 76,
|
||
|
rfc5322IPv6ColonEnd: 77,
|
||
|
|
||
|
// Address is invalid for any purpose
|
||
|
|
||
|
errExpectingDTEXT: 129,
|
||
|
errNoLocalPart: 130,
|
||
|
errNoDomain: 131,
|
||
|
errConsecutiveDots: 132,
|
||
|
errATEXTAfterCFWS: 133,
|
||
|
errATEXTAfterQS: 134,
|
||
|
errATEXTAfterDomainLiteral: 135,
|
||
|
errExpectingQPair: 136,
|
||
|
errExpectingATEXT: 137,
|
||
|
errExpectingQTEXT: 138,
|
||
|
errExpectingCTEXT: 139,
|
||
|
errBackslashEnd: 140,
|
||
|
errDotStart: 141,
|
||
|
errDotEnd: 142,
|
||
|
errDomainHyphenStart: 143,
|
||
|
errDomainHyphenEnd: 144,
|
||
|
errUnclosedQuotedString: 145,
|
||
|
errUnclosedComment: 146,
|
||
|
errUnclosedDomainLiteral: 147,
|
||
|
errFWSCRLFx2: 148,
|
||
|
errFWSCRLFEnd: 149,
|
||
|
errCRNoLF: 150,
|
||
|
errUnknownTLD: 160,
|
||
|
errDomainTooShort: 161
|
||
|
},
|
||
|
|
||
|
components: {
|
||
|
localpart: 0,
|
||
|
domain: 1,
|
||
|
literal: 2,
|
||
|
contextComment: 3,
|
||
|
contextFWS: 4,
|
||
|
contextQuotedString: 5,
|
||
|
contextQuotedPair: 6
|
||
|
}
|
||
|
};
|
||
|
|
||
|
|
||
|
// $lab:coverage:off$
|
||
|
internals.defer = typeof process !== 'undefined' && process && typeof process.nextTick === 'function' ?
|
||
|
process.nextTick.bind(process) :
|
||
|
function (callback) {
|
||
|
|
||
|
return setTimeout(callback, 0);
|
||
|
};
|
||
|
// $lab:coverage:on$
|
||
|
|
||
|
|
||
|
internals.specials = function () {
|
||
|
|
||
|
const specials = '()<>[]:;@\\,."'; // US-ASCII visible characters not valid for atext (http://tools.ietf.org/html/rfc5322#section-3.2.3)
|
||
|
const lookup = new Array(0x100);
|
||
|
for (let i = 0xff; i >= 0; --i) {
|
||
|
lookup[i] = false;
|
||
|
}
|
||
|
|
||
|
for (let i = 0; i < specials.length; ++i) {
|
||
|
lookup[specials.charCodeAt(i)] = true;
|
||
|
}
|
||
|
|
||
|
return function (code) {
|
||
|
|
||
|
return lookup[code];
|
||
|
};
|
||
|
}();
|
||
|
|
||
|
|
||
|
internals.regex = {
|
||
|
ipV4: /\b(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)$/,
|
||
|
ipV6: /^[a-fA-F\d]{0,4}$/
|
||
|
};
|
||
|
|
||
|
|
||
|
internals.checkIpV6 = function (items) {
|
||
|
|
||
|
return items.every((value) => internals.regex.ipV6.test(value));
|
||
|
};
|
||
|
|
||
|
|
||
|
internals.validDomain = function (tldAtom, options) {
|
||
|
|
||
|
if (options.tldBlacklist) {
|
||
|
if (Array.isArray(options.tldBlacklist)) {
|
||
|
return internals.indexOf.call(options.tldBlacklist, tldAtom) === -1;
|
||
|
}
|
||
|
|
||
|
return !internals.hasOwn.call(options.tldBlacklist, tldAtom);
|
||
|
}
|
||
|
|
||
|
if (Array.isArray(options.tldWhitelist)) {
|
||
|
return internals.indexOf.call(options.tldWhitelist, tldAtom) !== -1;
|
||
|
}
|
||
|
|
||
|
return internals.hasOwn.call(options.tldWhitelist, tldAtom);
|
||
|
};
|
||
|
|
||
|
|
||
|
/**
|
||
|
* Check that an email address conforms to RFCs 5321, 5322 and others
|
||
|
*
|
||
|
* We distinguish clearly between a Mailbox as defined by RFC 5321 and an
|
||
|
* addr-spec as defined by RFC 5322. Depending on the context, either can be
|
||
|
* regarded as a valid email address. The RFC 5321 Mailbox specification is
|
||
|
* more restrictive (comments, white space and obsolete forms are not allowed).
|
||
|
*
|
||
|
* @param {string} email The email address to check. See README for specifics.
|
||
|
* @param {Object} options The (optional) options:
|
||
|
* {boolean} checkDNS If true then will check DNS for MX records. If
|
||
|
* true this call to isEmail _will_ be asynchronous.
|
||
|
* {*} errorLevel Determines the boundary between valid and invalid
|
||
|
* addresses.
|
||
|
* {*} tldBlacklist The set of domains to consider invalid.
|
||
|
* {*} tldWhitelist The set of domains to consider valid.
|
||
|
* {*} minDomainAtoms The minimum number of domain atoms which must be present
|
||
|
* for the address to be valid.
|
||
|
* @param {function(number|boolean)} callback The (optional) callback handler.
|
||
|
* @return {*}
|
||
|
*/
|
||
|
|
||
|
exports.validate = internals.validate = function (email, options, callback) {
|
||
|
|
||
|
options = options || {};
|
||
|
|
||
|
if (typeof options === 'function') {
|
||
|
callback = options;
|
||
|
options = {};
|
||
|
}
|
||
|
|
||
|
if (typeof callback !== 'function') {
|
||
|
if (options.checkDNS) {
|
||
|
throw new TypeError('expected callback function for checkDNS option');
|
||
|
}
|
||
|
|
||
|
callback = null;
|
||
|
}
|
||
|
|
||
|
let diagnose;
|
||
|
let threshold;
|
||
|
|
||
|
if (typeof options.errorLevel === 'number') {
|
||
|
diagnose = true;
|
||
|
threshold = options.errorLevel;
|
||
|
}
|
||
|
else {
|
||
|
diagnose = !!options.errorLevel;
|
||
|
threshold = internals.diagnoses.valid;
|
||
|
}
|
||
|
|
||
|
if (options.tldWhitelist) {
|
||
|
if (typeof options.tldWhitelist === 'string') {
|
||
|
options.tldWhitelist = [options.tldWhitelist];
|
||
|
}
|
||
|
else if (typeof options.tldWhitelist !== 'object') {
|
||
|
throw new TypeError('expected array or object tldWhitelist');
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (options.tldBlacklist) {
|
||
|
if (typeof options.tldBlacklist === 'string') {
|
||
|
options.tldBlacklist = [options.tldBlacklist];
|
||
|
}
|
||
|
else if (typeof options.tldBlacklist !== 'object') {
|
||
|
throw new TypeError('expected array or object tldBlacklist');
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (options.minDomainAtoms && (options.minDomainAtoms !== ((+options.minDomainAtoms) | 0) || options.minDomainAtoms < 0)) {
|
||
|
throw new TypeError('expected positive integer minDomainAtoms');
|
||
|
}
|
||
|
|
||
|
let maxResult = internals.diagnoses.valid;
|
||
|
const updateResult = (value) => {
|
||
|
|
||
|
if (value > maxResult) {
|
||
|
maxResult = value;
|
||
|
}
|
||
|
};
|
||
|
|
||
|
const context = {
|
||
|
now: internals.components.localpart,
|
||
|
prev: internals.components.localpart,
|
||
|
stack: [internals.components.localpart]
|
||
|
};
|
||
|
|
||
|
let prevToken = '';
|
||
|
|
||
|
const parseData = {
|
||
|
local: '',
|
||
|
domain: ''
|
||
|
};
|
||
|
const atomData = {
|
||
|
locals: [''],
|
||
|
domains: ['']
|
||
|
};
|
||
|
|
||
|
let elementCount = 0;
|
||
|
let elementLength = 0;
|
||
|
let crlfCount = 0;
|
||
|
let charCode;
|
||
|
|
||
|
let hyphenFlag = false;
|
||
|
let assertEnd = false;
|
||
|
|
||
|
const emailLength = email.length;
|
||
|
|
||
|
let token; // Token is used outside the loop, must declare similarly
|
||
|
for (let i = 0; i < emailLength; ++i) {
|
||
|
token = email[i];
|
||
|
|
||
|
switch (context.now) {
|
||
|
// Local-part
|
||
|
case internals.components.localpart:
|
||
|
// http://tools.ietf.org/html/rfc5322#section-3.4.1
|
||
|
// local-part = dot-atom / quoted-string / obs-local-part
|
||
|
//
|
||
|
// dot-atom = [CFWS] dot-atom-text [CFWS]
|
||
|
//
|
||
|
// dot-atom-text = 1*atext *("." 1*atext)
|
||
|
//
|
||
|
// quoted-string = [CFWS]
|
||
|
// DQUOTE *([FWS] qcontent) [FWS] DQUOTE
|
||
|
// [CFWS]
|
||
|
//
|
||
|
// obs-local-part = word *("." word)
|
||
|
//
|
||
|
// word = atom / quoted-string
|
||
|
//
|
||
|
// atom = [CFWS] 1*atext [CFWS]
|
||
|
switch (token) {
|
||
|
// Comment
|
||
|
case '(':
|
||
|
if (elementLength === 0) {
|
||
|
// Comments are OK at the beginning of an element
|
||
|
updateResult(elementCount === 0 ? internals.diagnoses.cfwsComment : internals.diagnoses.deprecatedComment);
|
||
|
}
|
||
|
else {
|
||
|
updateResult(internals.diagnoses.cfwsComment);
|
||
|
// Cannot start a comment in an element, should be end
|
||
|
assertEnd = true;
|
||
|
}
|
||
|
|
||
|
context.stack.push(context.now);
|
||
|
context.now = internals.components.contextComment;
|
||
|
break;
|
||
|
|
||
|
// Next dot-atom element
|
||
|
case '.':
|
||
|
if (elementLength === 0) {
|
||
|
// Another dot, already?
|
||
|
updateResult(elementCount === 0 ? internals.diagnoses.errDotStart : internals.diagnoses.errConsecutiveDots);
|
||
|
}
|
||
|
else {
|
||
|
// The entire local-part can be a quoted string for RFC 5321; if one atom is quoted it's an RFC 5322 obsolete form
|
||
|
if (assertEnd) {
|
||
|
updateResult(internals.diagnoses.deprecatedLocalPart);
|
||
|
}
|
||
|
|
||
|
// CFWS & quoted strings are OK again now we're at the beginning of an element (although they are obsolete forms)
|
||
|
assertEnd = false;
|
||
|
elementLength = 0;
|
||
|
++elementCount;
|
||
|
parseData.local += token;
|
||
|
atomData.locals[elementCount] = '';
|
||
|
}
|
||
|
|
||
|
break;
|
||
|
|
||
|
// Quoted string
|
||
|
case '"':
|
||
|
if (elementLength === 0) {
|
||
|
// The entire local-part can be a quoted string for RFC 5321; if one atom is quoted it's an RFC 5322 obsolete form
|
||
|
updateResult(elementCount === 0 ? internals.diagnoses.rfc5321QuotedString : internals.diagnoses.deprecatedLocalPart);
|
||
|
|
||
|
parseData.local += token;
|
||
|
atomData.locals[elementCount] += token;
|
||
|
++elementLength;
|
||
|
|
||
|
// Quoted string must be the entire element
|
||
|
assertEnd = true;
|
||
|
context.stack.push(context.now);
|
||
|
context.now = internals.components.contextQuotedString;
|
||
|
}
|
||
|
else {
|
||
|
updateResult(internals.diagnoses.errExpectingATEXT);
|
||
|
}
|
||
|
|
||
|
break;
|
||
|
|
||
|
// Folding white space
|
||
|
case '\r':
|
||
|
if (emailLength === ++i || email[i] !== '\n') {
|
||
|
// Fatal error
|
||
|
updateResult(internals.diagnoses.errCRNoLF);
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
// Fallthrough
|
||
|
|
||
|
case ' ':
|
||
|
case '\t':
|
||
|
if (elementLength === 0) {
|
||
|
updateResult(elementCount === 0 ? internals.diagnoses.cfwsFWS : internals.diagnoses.deprecatedFWS);
|
||
|
}
|
||
|
else {
|
||
|
// We can't start FWS in the middle of an element, better be end
|
||
|
assertEnd = true;
|
||
|
}
|
||
|
|
||
|
context.stack.push(context.now);
|
||
|
context.now = internals.components.contextFWS;
|
||
|
prevToken = token;
|
||
|
break;
|
||
|
|
||
|
case '@':
|
||
|
// At this point we should have a valid local-part
|
||
|
// $lab:coverage:off$
|
||
|
if (context.stack.length !== 1) {
|
||
|
throw new Error('unexpected item on context stack');
|
||
|
}
|
||
|
// $lab:coverage:on$
|
||
|
|
||
|
if (parseData.local.length === 0) {
|
||
|
// Fatal error
|
||
|
updateResult(internals.diagnoses.errNoLocalPart);
|
||
|
}
|
||
|
else if (elementLength === 0) {
|
||
|
// Fatal error
|
||
|
updateResult(internals.diagnoses.errDotEnd);
|
||
|
}
|
||
|
// http://tools.ietf.org/html/rfc5321#section-4.5.3.1.1 the maximum total length of a user name or other local-part is 64
|
||
|
// octets
|
||
|
else if (parseData.local.length > 64) {
|
||
|
updateResult(internals.diagnoses.rfc5322LocalTooLong);
|
||
|
}
|
||
|
// http://tools.ietf.org/html/rfc5322#section-3.4.1 comments and folding white space SHOULD NOT be used around "@" in the
|
||
|
// addr-spec
|
||
|
//
|
||
|
// http://tools.ietf.org/html/rfc2119
|
||
|
// 4. SHOULD NOT this phrase, or the phrase "NOT RECOMMENDED" mean that there may exist valid reasons in particular
|
||
|
// circumstances when the particular behavior is acceptable or even useful, but the full implications should be understood
|
||
|
// and the case carefully weighed before implementing any behavior described with this label.
|
||
|
else if (context.prev === internals.components.contextComment || context.prev === internals.components.contextFWS) {
|
||
|
updateResult(internals.diagnoses.deprecatedCFWSNearAt);
|
||
|
}
|
||
|
|
||
|
// Clear everything down for the domain parsing
|
||
|
context.now = internals.components.domain;
|
||
|
context.stack[0] = internals.components.domain;
|
||
|
elementCount = 0;
|
||
|
elementLength = 0;
|
||
|
assertEnd = false; // CFWS can only appear at the end of the element
|
||
|
break;
|
||
|
|
||
|
// ATEXT
|
||
|
default:
|
||
|
// http://tools.ietf.org/html/rfc5322#section-3.2.3
|
||
|
// atext = ALPHA / DIGIT / ; Printable US-ASCII
|
||
|
// "!" / "#" / ; characters not including
|
||
|
// "$" / "%" / ; specials. Used for atoms.
|
||
|
// "&" / "'" /
|
||
|
// "*" / "+" /
|
||
|
// "-" / "/" /
|
||
|
// "=" / "?" /
|
||
|
// "^" / "_" /
|
||
|
// "`" / "{" /
|
||
|
// "|" / "}" /
|
||
|
// "~"
|
||
|
if (assertEnd) {
|
||
|
// We have encountered atext where it is no longer valid
|
||
|
switch (context.prev) {
|
||
|
case internals.components.contextComment:
|
||
|
case internals.components.contextFWS:
|
||
|
updateResult(internals.diagnoses.errATEXTAfterCFWS);
|
||
|
break;
|
||
|
|
||
|
case internals.components.contextQuotedString:
|
||
|
updateResult(internals.diagnoses.errATEXTAfterQS);
|
||
|
break;
|
||
|
|
||
|
// $lab:coverage:off$
|
||
|
default:
|
||
|
throw new Error('more atext found where none is allowed, but unrecognized prev context: ' + context.prev);
|
||
|
// $lab:coverage:on$
|
||
|
}
|
||
|
}
|
||
|
else {
|
||
|
context.prev = context.now;
|
||
|
charCode = token.charCodeAt(0);
|
||
|
|
||
|
// Especially if charCode == 10
|
||
|
if (charCode < 33 || charCode > 126 || internals.specials(charCode)) {
|
||
|
|
||
|
// Fatal error
|
||
|
updateResult(internals.diagnoses.errExpectingATEXT);
|
||
|
}
|
||
|
|
||
|
parseData.local += token;
|
||
|
atomData.locals[elementCount] += token;
|
||
|
++elementLength;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
break;
|
||
|
|
||
|
case internals.components.domain:
|
||
|
// http://tools.ietf.org/html/rfc5322#section-3.4.1
|
||
|
// domain = dot-atom / domain-literal / obs-domain
|
||
|
//
|
||
|
// dot-atom = [CFWS] dot-atom-text [CFWS]
|
||
|
//
|
||
|
// dot-atom-text = 1*atext *("." 1*atext)
|
||
|
//
|
||
|
// domain-literal = [CFWS] "[" *([FWS] dtext) [FWS] "]" [CFWS]
|
||
|
//
|
||
|
// dtext = %d33-90 / ; Printable US-ASCII
|
||
|
// %d94-126 / ; characters not including
|
||
|
// obs-dtext ; "[", "]", or "\"
|
||
|
//
|
||
|
// obs-domain = atom *("." atom)
|
||
|
//
|
||
|
// atom = [CFWS] 1*atext [CFWS]
|
||
|
|
||
|
// http://tools.ietf.org/html/rfc5321#section-4.1.2
|
||
|
// Mailbox = Local-part "@" ( Domain / address-literal )
|
||
|
//
|
||
|
// Domain = sub-domain *("." sub-domain)
|
||
|
//
|
||
|
// address-literal = "[" ( IPv4-address-literal /
|
||
|
// IPv6-address-literal /
|
||
|
// General-address-literal ) "]"
|
||
|
// ; See Section 4.1.3
|
||
|
|
||
|
// http://tools.ietf.org/html/rfc5322#section-3.4.1
|
||
|
// Note: A liberal syntax for the domain portion of addr-spec is
|
||
|
// given here. However, the domain portion contains addressing
|
||
|
// information specified by and used in other protocols (e.g.,
|
||
|
// [RFC1034], [RFC1035], [RFC1123], [RFC5321]). It is therefore
|
||
|
// incumbent upon implementations to conform to the syntax of
|
||
|
// addresses for the context in which they are used.
|
||
|
//
|
||
|
// is_email() author's note: it's not clear how to interpret this in
|
||
|
// he context of a general email address validator. The conclusion I
|
||
|
// have reached is this: "addressing information" must comply with
|
||
|
// RFC 5321 (and in turn RFC 1035), anything that is "semantically
|
||
|
// invisible" must comply only with RFC 5322.
|
||
|
switch (token) {
|
||
|
// Comment
|
||
|
case '(':
|
||
|
if (elementLength === 0) {
|
||
|
// Comments at the start of the domain are deprecated in the text, comments at the start of a subdomain are obs-domain
|
||
|
// http://tools.ietf.org/html/rfc5322#section-3.4.1
|
||
|
updateResult(elementCount === 0 ? internals.diagnoses.deprecatedCFWSNearAt : internals.diagnoses.deprecatedComment);
|
||
|
}
|
||
|
else {
|
||
|
// We can't start a comment mid-element, better be at the end
|
||
|
assertEnd = true;
|
||
|
updateResult(internals.diagnoses.cfwsComment);
|
||
|
}
|
||
|
|
||
|
context.stack.push(context.now);
|
||
|
context.now = internals.components.contextComment;
|
||
|
break;
|
||
|
|
||
|
// Next dot-atom element
|
||
|
case '.':
|
||
|
if (elementLength === 0) {
|
||
|
// Another dot, already? Fatal error.
|
||
|
updateResult(elementCount === 0 ? internals.diagnoses.errDotStart : internals.diagnoses.errConsecutiveDots);
|
||
|
}
|
||
|
else if (hyphenFlag) {
|
||
|
// Previous subdomain ended in a hyphen. Fatal error.
|
||
|
updateResult(internals.diagnoses.errDomainHyphenEnd);
|
||
|
}
|
||
|
else if (elementLength > 63) {
|
||
|
// Nowhere in RFC 5321 does it say explicitly that the domain part of a Mailbox must be a valid domain according to the
|
||
|
// DNS standards set out in RFC 1035, but this *is* implied in several places. For instance, wherever the idea of host
|
||
|
// routing is discussed the RFC says that the domain must be looked up in the DNS. This would be nonsense unless the
|
||
|
// domain was designed to be a valid DNS domain. Hence we must conclude that the RFC 1035 restriction on label length
|
||
|
// also applies to RFC 5321 domains.
|
||
|
//
|
||
|
// http://tools.ietf.org/html/rfc1035#section-2.3.4
|
||
|
// labels 63 octets or less
|
||
|
|
||
|
updateResult(internals.diagnoses.rfc5322LabelTooLong);
|
||
|
}
|
||
|
|
||
|
// CFWS is OK again now we're at the beginning of an element (although
|
||
|
// it may be obsolete CFWS)
|
||
|
assertEnd = false;
|
||
|
elementLength = 0;
|
||
|
++elementCount;
|
||
|
atomData.domains[elementCount] = '';
|
||
|
parseData.domain += token;
|
||
|
|
||
|
break;
|
||
|
|
||
|
// Domain literal
|
||
|
case '[':
|
||
|
if (parseData.domain.length === 0) {
|
||
|
// Domain literal must be the only component
|
||
|
assertEnd = true;
|
||
|
++elementLength;
|
||
|
context.stack.push(context.now);
|
||
|
context.now = internals.components.literal;
|
||
|
parseData.domain += token;
|
||
|
atomData.domains[elementCount] += token;
|
||
|
parseData.literal = '';
|
||
|
}
|
||
|
else {
|
||
|
// Fatal error
|
||
|
updateResult(internals.diagnoses.errExpectingATEXT);
|
||
|
}
|
||
|
|
||
|
break;
|
||
|
|
||
|
// Folding white space
|
||
|
case '\r':
|
||
|
if (emailLength === ++i || email[i] !== '\n') {
|
||
|
// Fatal error
|
||
|
updateResult(internals.diagnoses.errCRNoLF);
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
// Fallthrough
|
||
|
|
||
|
case ' ':
|
||
|
case '\t':
|
||
|
if (elementLength === 0) {
|
||
|
updateResult(elementCount === 0 ? internals.diagnoses.deprecatedCFWSNearAt : internals.diagnoses.deprecatedFWS);
|
||
|
}
|
||
|
else {
|
||
|
// We can't start FWS in the middle of an element, so this better be the end
|
||
|
updateResult(internals.diagnoses.cfwsFWS);
|
||
|
assertEnd = true;
|
||
|
}
|
||
|
|
||
|
context.stack.push(context.now);
|
||
|
context.now = internals.components.contextFWS;
|
||
|
prevToken = token;
|
||
|
break;
|
||
|
|
||
|
// This must be ATEXT
|
||
|
default:
|
||
|
// RFC 5322 allows any atext...
|
||
|
// http://tools.ietf.org/html/rfc5322#section-3.2.3
|
||
|
// atext = ALPHA / DIGIT / ; Printable US-ASCII
|
||
|
// "!" / "#" / ; characters not including
|
||
|
// "$" / "%" / ; specials. Used for atoms.
|
||
|
// "&" / "'" /
|
||
|
// "*" / "+" /
|
||
|
// "-" / "/" /
|
||
|
// "=" / "?" /
|
||
|
// "^" / "_" /
|
||
|
// "`" / "{" /
|
||
|
// "|" / "}" /
|
||
|
// "~"
|
||
|
|
||
|
// But RFC 5321 only allows letter-digit-hyphen to comply with DNS rules
|
||
|
// (RFCs 1034 & 1123)
|
||
|
// http://tools.ietf.org/html/rfc5321#section-4.1.2
|
||
|
// sub-domain = Let-dig [Ldh-str]
|
||
|
//
|
||
|
// Let-dig = ALPHA / DIGIT
|
||
|
//
|
||
|
// Ldh-str = *( ALPHA / DIGIT / "-" ) Let-dig
|
||
|
//
|
||
|
if (assertEnd) {
|
||
|
// We have encountered ATEXT where it is no longer valid
|
||
|
switch (context.prev) {
|
||
|
case internals.components.contextComment:
|
||
|
case internals.components.contextFWS:
|
||
|
updateResult(internals.diagnoses.errATEXTAfterCFWS);
|
||
|
break;
|
||
|
|
||
|
case internals.components.literal:
|
||
|
updateResult(internals.diagnoses.errATEXTAfterDomainLiteral);
|
||
|
break;
|
||
|
|
||
|
// $lab:coverage:off$
|
||
|
default:
|
||
|
throw new Error('more atext found where none is allowed, but unrecognized prev context: ' + context.prev);
|
||
|
// $lab:coverage:on$
|
||
|
}
|
||
|
}
|
||
|
|
||
|
charCode = token.charCodeAt(0);
|
||
|
// Assume this token isn't a hyphen unless we discover it is
|
||
|
hyphenFlag = false;
|
||
|
|
||
|
if (charCode < 33 || charCode > 126 || internals.specials(charCode)) {
|
||
|
// Fatal error
|
||
|
updateResult(internals.diagnoses.errExpectingATEXT);
|
||
|
}
|
||
|
else if (token === '-') {
|
||
|
if (elementLength === 0) {
|
||
|
// Hyphens cannot be at the beginning of a subdomain, fatal error
|
||
|
updateResult(internals.diagnoses.errDomainHyphenStart);
|
||
|
}
|
||
|
|
||
|
hyphenFlag = true;
|
||
|
}
|
||
|
// Check if it's a neither a number nor a latin letter
|
||
|
else if (charCode < 48 || charCode > 122 || (charCode > 57 && charCode < 65) || (charCode > 90 && charCode < 97)) {
|
||
|
// This is not an RFC 5321 subdomain, but still OK by RFC 5322
|
||
|
updateResult(internals.diagnoses.rfc5322Domain);
|
||
|
}
|
||
|
|
||
|
parseData.domain += token;
|
||
|
atomData.domains[elementCount] += token;
|
||
|
++elementLength;
|
||
|
}
|
||
|
|
||
|
break;
|
||
|
|
||
|
// Domain literal
|
||
|
case internals.components.literal:
|
||
|
// http://tools.ietf.org/html/rfc5322#section-3.4.1
|
||
|
// domain-literal = [CFWS] "[" *([FWS] dtext) [FWS] "]" [CFWS]
|
||
|
//
|
||
|
// dtext = %d33-90 / ; Printable US-ASCII
|
||
|
// %d94-126 / ; characters not including
|
||
|
// obs-dtext ; "[", "]", or "\"
|
||
|
//
|
||
|
// obs-dtext = obs-NO-WS-CTL / quoted-pair
|
||
|
switch (token) {
|
||
|
// End of domain literal
|
||
|
case ']':
|
||
|
if (maxResult < internals.categories.deprecated) {
|
||
|
// Could be a valid RFC 5321 address literal, so let's check
|
||
|
|
||
|
// http://tools.ietf.org/html/rfc5321#section-4.1.2
|
||
|
// address-literal = "[" ( IPv4-address-literal /
|
||
|
// IPv6-address-literal /
|
||
|
// General-address-literal ) "]"
|
||
|
// ; See Section 4.1.3
|
||
|
//
|
||
|
// http://tools.ietf.org/html/rfc5321#section-4.1.3
|
||
|
// IPv4-address-literal = Snum 3("." Snum)
|
||
|
//
|
||
|
// IPv6-address-literal = "IPv6:" IPv6-addr
|
||
|
//
|
||
|
// General-address-literal = Standardized-tag ":" 1*dcontent
|
||
|
//
|
||
|
// Standardized-tag = Ldh-str
|
||
|
// ; Standardized-tag MUST be specified in a
|
||
|
// ; Standards-Track RFC and registered with IANA
|
||
|
//
|
||
|
// dcontent = %d33-90 / ; Printable US-ASCII
|
||
|
// %d94-126 ; excl. "[", "\", "]"
|
||
|
//
|
||
|
// Snum = 1*3DIGIT
|
||
|
// ; representing a decimal integer
|
||
|
// ; value in the range 0 through 255
|
||
|
//
|
||
|
// IPv6-addr = IPv6-full / IPv6-comp / IPv6v4-full / IPv6v4-comp
|
||
|
//
|
||
|
// IPv6-hex = 1*4HEXDIG
|
||
|
//
|
||
|
// IPv6-full = IPv6-hex 7(":" IPv6-hex)
|
||
|
//
|
||
|
// IPv6-comp = [IPv6-hex *5(":" IPv6-hex)] "::"
|
||
|
// [IPv6-hex *5(":" IPv6-hex)]
|
||
|
// ; The "::" represents at least 2 16-bit groups of
|
||
|
// ; zeros. No more than 6 groups in addition to the
|
||
|
// ; "::" may be present.
|
||
|
//
|
||
|
// IPv6v4-full = IPv6-hex 5(":" IPv6-hex) ":" IPv4-address-literal
|
||
|
//
|
||
|
// IPv6v4-comp = [IPv6-hex *3(":" IPv6-hex)] "::"
|
||
|
// [IPv6-hex *3(":" IPv6-hex) ":"]
|
||
|
// IPv4-address-literal
|
||
|
// ; The "::" represents at least 2 16-bit groups of
|
||
|
// ; zeros. No more than 4 groups in addition to the
|
||
|
// ; "::" and IPv4-address-literal may be present.
|
||
|
|
||
|
let index = -1;
|
||
|
let addressLiteral = parseData.literal;
|
||
|
const matchesIP = internals.regex.ipV4.exec(addressLiteral);
|
||
|
|
||
|
// Maybe extract IPv4 part from the end of the address-literal
|
||
|
if (matchesIP) {
|
||
|
index = matchesIP.index;
|
||
|
if (index !== 0) {
|
||
|
// Convert IPv4 part to IPv6 format for futher testing
|
||
|
addressLiteral = addressLiteral.slice(0, index) + '0:0';
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (index === 0) {
|
||
|
// Nothing there except a valid IPv4 address, so...
|
||
|
updateResult(internals.diagnoses.rfc5321AddressLiteral);
|
||
|
}
|
||
|
else if (addressLiteral.slice(0, 5).toLowerCase() !== 'ipv6:') {
|
||
|
updateResult(internals.diagnoses.rfc5322DomainLiteral);
|
||
|
}
|
||
|
else {
|
||
|
const match = addressLiteral.slice(5);
|
||
|
let maxGroups = internals.maxIPv6Groups;
|
||
|
const groups = match.split(':');
|
||
|
index = match.indexOf('::');
|
||
|
|
||
|
if (!~index) {
|
||
|
// Need exactly the right number of groups
|
||
|
if (groups.length !== maxGroups) {
|
||
|
updateResult(internals.diagnoses.rfc5322IPv6GroupCount);
|
||
|
}
|
||
|
}
|
||
|
else if (index !== match.lastIndexOf('::')) {
|
||
|
updateResult(internals.diagnoses.rfc5322IPv62x2xColon);
|
||
|
}
|
||
|
else {
|
||
|
if (index === 0 || index === match.length - 2) {
|
||
|
// RFC 4291 allows :: at the start or end of an address with 7 other groups in addition
|
||
|
++maxGroups;
|
||
|
}
|
||
|
|
||
|
if (groups.length > maxGroups) {
|
||
|
updateResult(internals.diagnoses.rfc5322IPv6MaxGroups);
|
||
|
}
|
||
|
else if (groups.length === maxGroups) {
|
||
|
// Eliding a single "::"
|
||
|
updateResult(internals.diagnoses.deprecatedIPv6);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// IPv6 testing strategy
|
||
|
if (match[0] === ':' && match[1] !== ':') {
|
||
|
updateResult(internals.diagnoses.rfc5322IPv6ColonStart);
|
||
|
}
|
||
|
else if (match[match.length - 1] === ':' && match[match.length - 2] !== ':') {
|
||
|
updateResult(internals.diagnoses.rfc5322IPv6ColonEnd);
|
||
|
}
|
||
|
else if (internals.checkIpV6(groups)) {
|
||
|
updateResult(internals.diagnoses.rfc5321AddressLiteral);
|
||
|
}
|
||
|
else {
|
||
|
updateResult(internals.diagnoses.rfc5322IPv6BadCharacter);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
else {
|
||
|
updateResult(internals.diagnoses.rfc5322DomainLiteral);
|
||
|
}
|
||
|
|
||
|
parseData.domain += token;
|
||
|
atomData.domains[elementCount] += token;
|
||
|
++elementLength;
|
||
|
context.prev = context.now;
|
||
|
context.now = context.stack.pop();
|
||
|
break;
|
||
|
|
||
|
case '\\':
|
||
|
updateResult(internals.diagnoses.rfc5322DomainLiteralOBSDText);
|
||
|
context.stack.push(context.now);
|
||
|
context.now = internals.components.contextQuotedPair;
|
||
|
break;
|
||
|
|
||
|
// Folding white space
|
||
|
case '\r':
|
||
|
if (emailLength === ++i || email[i] !== '\n') {
|
||
|
updateResult(internals.diagnoses.errCRNoLF);
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
// Fallthrough
|
||
|
|
||
|
case ' ':
|
||
|
case '\t':
|
||
|
updateResult(internals.diagnoses.cfwsFWS);
|
||
|
|
||
|
context.stack.push(context.now);
|
||
|
context.now = internals.components.contextFWS;
|
||
|
prevToken = token;
|
||
|
break;
|
||
|
|
||
|
// DTEXT
|
||
|
default:
|
||
|
// http://tools.ietf.org/html/rfc5322#section-3.4.1
|
||
|
// dtext = %d33-90 / ; Printable US-ASCII
|
||
|
// %d94-126 / ; characters not including
|
||
|
// obs-dtext ; "[", "]", or "\"
|
||
|
//
|
||
|
// obs-dtext = obs-NO-WS-CTL / quoted-pair
|
||
|
//
|
||
|
// obs-NO-WS-CTL = %d1-8 / ; US-ASCII control
|
||
|
// %d11 / ; characters that do not
|
||
|
// %d12 / ; include the carriage
|
||
|
// %d14-31 / ; return, line feed, and
|
||
|
// %d127 ; white space characters
|
||
|
charCode = token.charCodeAt(0);
|
||
|
|
||
|
// '\r', '\n', ' ', and '\t' have already been parsed above
|
||
|
if (charCode > 127 || charCode === 0 || token === '[') {
|
||
|
// Fatal error
|
||
|
updateResult(internals.diagnoses.errExpectingDTEXT);
|
||
|
break;
|
||
|
}
|
||
|
else if (charCode < 33 || charCode === 127) {
|
||
|
updateResult(internals.diagnoses.rfc5322DomainLiteralOBSDText);
|
||
|
}
|
||
|
|
||
|
parseData.literal += token;
|
||
|
parseData.domain += token;
|
||
|
atomData.domains[elementCount] += token;
|
||
|
++elementLength;
|
||
|
}
|
||
|
|
||
|
break;
|
||
|
|
||
|
// Quoted string
|
||
|
case internals.components.contextQuotedString:
|
||
|
// http://tools.ietf.org/html/rfc5322#section-3.2.4
|
||
|
// quoted-string = [CFWS]
|
||
|
// DQUOTE *([FWS] qcontent) [FWS] DQUOTE
|
||
|
// [CFWS]
|
||
|
//
|
||
|
// qcontent = qtext / quoted-pair
|
||
|
switch (token) {
|
||
|
// Quoted pair
|
||
|
case '\\':
|
||
|
context.stack.push(context.now);
|
||
|
context.now = internals.components.contextQuotedPair;
|
||
|
break;
|
||
|
|
||
|
// Folding white space. Spaces are allowed as regular characters inside a quoted string - it's only FWS if we include '\t' or '\r\n'
|
||
|
case '\r':
|
||
|
if (emailLength === ++i || email[i] !== '\n') {
|
||
|
// Fatal error
|
||
|
updateResult(internals.diagnoses.errCRNoLF);
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
// Fallthrough
|
||
|
|
||
|
case '\t':
|
||
|
// http://tools.ietf.org/html/rfc5322#section-3.2.2
|
||
|
// Runs of FWS, comment, or CFWS that occur between lexical tokens in
|
||
|
// a structured header field are semantically interpreted as a single
|
||
|
// space character.
|
||
|
|
||
|
// http://tools.ietf.org/html/rfc5322#section-3.2.4
|
||
|
// the CRLF in any FWS/CFWS that appears within the quoted-string [is]
|
||
|
// semantically "invisible" and therefore not part of the
|
||
|
// quoted-string
|
||
|
|
||
|
parseData.local += ' ';
|
||
|
atomData.locals[elementCount] += ' ';
|
||
|
++elementLength;
|
||
|
|
||
|
updateResult(internals.diagnoses.cfwsFWS);
|
||
|
context.stack.push(context.now);
|
||
|
context.now = internals.components.contextFWS;
|
||
|
prevToken = token;
|
||
|
break;
|
||
|
|
||
|
// End of quoted string
|
||
|
case '"':
|
||
|
parseData.local += token;
|
||
|
atomData.locals[elementCount] += token;
|
||
|
++elementLength;
|
||
|
context.prev = context.now;
|
||
|
context.now = context.stack.pop();
|
||
|
break;
|
||
|
|
||
|
// QTEXT
|
||
|
default:
|
||
|
// http://tools.ietf.org/html/rfc5322#section-3.2.4
|
||
|
// qtext = %d33 / ; Printable US-ASCII
|
||
|
// %d35-91 / ; characters not including
|
||
|
// %d93-126 / ; "\" or the quote character
|
||
|
// obs-qtext
|
||
|
//
|
||
|
// obs-qtext = obs-NO-WS-CTL
|
||
|
//
|
||
|
// obs-NO-WS-CTL = %d1-8 / ; US-ASCII control
|
||
|
// %d11 / ; characters that do not
|
||
|
// %d12 / ; include the carriage
|
||
|
// %d14-31 / ; return, line feed, and
|
||
|
// %d127 ; white space characters
|
||
|
charCode = token.charCodeAt(0);
|
||
|
|
||
|
if (charCode > 127 || charCode === 0 || charCode === 10) {
|
||
|
updateResult(internals.diagnoses.errExpectingQTEXT);
|
||
|
}
|
||
|
else if (charCode < 32 || charCode === 127) {
|
||
|
updateResult(internals.diagnoses.deprecatedQTEXT);
|
||
|
}
|
||
|
|
||
|
parseData.local += token;
|
||
|
atomData.locals[elementCount] += token;
|
||
|
++elementLength;
|
||
|
}
|
||
|
|
||
|
// http://tools.ietf.org/html/rfc5322#section-3.4.1
|
||
|
// If the string can be represented as a dot-atom (that is, it contains
|
||
|
// no characters other than atext characters or "." surrounded by atext
|
||
|
// characters), then the dot-atom form SHOULD be used and the quoted-
|
||
|
// string form SHOULD NOT be used.
|
||
|
|
||
|
break;
|
||
|
// Quoted pair
|
||
|
case internals.components.contextQuotedPair:
|
||
|
// http://tools.ietf.org/html/rfc5322#section-3.2.1
|
||
|
// quoted-pair = ("\" (VCHAR / WSP)) / obs-qp
|
||
|
//
|
||
|
// VCHAR = %d33-126 ; visible (printing) characters
|
||
|
// WSP = SP / HTAB ; white space
|
||
|
//
|
||
|
// obs-qp = "\" (%d0 / obs-NO-WS-CTL / LF / CR)
|
||
|
//
|
||
|
// obs-NO-WS-CTL = %d1-8 / ; US-ASCII control
|
||
|
// %d11 / ; characters that do not
|
||
|
// %d12 / ; include the carriage
|
||
|
// %d14-31 / ; return, line feed, and
|
||
|
// %d127 ; white space characters
|
||
|
//
|
||
|
// i.e. obs-qp = "\" (%d0-8, %d10-31 / %d127)
|
||
|
charCode = token.charCodeAt(0);
|
||
|
|
||
|
if (charCode > 127) {
|
||
|
// Fatal error
|
||
|
updateResult(internals.diagnoses.errExpectingQPair);
|
||
|
}
|
||
|
else if ((charCode < 31 && charCode !== 9) || charCode === 127) {
|
||
|
// ' ' and '\t' are allowed
|
||
|
updateResult(internals.diagnoses.deprecatedQP);
|
||
|
}
|
||
|
|
||
|
// At this point we know where this qpair occurred so we could check to see if the character actually needed to be quoted at all.
|
||
|
// http://tools.ietf.org/html/rfc5321#section-4.1.2
|
||
|
// the sending system SHOULD transmit the form that uses the minimum quoting possible.
|
||
|
|
||
|
context.prev = context.now;
|
||
|
// End of qpair
|
||
|
context.now = context.stack.pop();
|
||
|
token = '\\' + token;
|
||
|
|
||
|
switch (context.now) {
|
||
|
case internals.components.contextComment:
|
||
|
break;
|
||
|
|
||
|
case internals.components.contextQuotedString:
|
||
|
parseData.local += token;
|
||
|
atomData.locals[elementCount] += token;
|
||
|
|
||
|
// The maximum sizes specified by RFC 5321 are octet counts, so we must include the backslash
|
||
|
elementLength += 2;
|
||
|
break;
|
||
|
|
||
|
case internals.components.literal:
|
||
|
parseData.domain += token;
|
||
|
atomData.domains[elementCount] += token;
|
||
|
|
||
|
// The maximum sizes specified by RFC 5321 are octet counts, so we must include the backslash
|
||
|
elementLength += 2;
|
||
|
break;
|
||
|
|
||
|
// $lab:coverage:off$
|
||
|
default:
|
||
|
throw new Error('quoted pair logic invoked in an invalid context: ' + context.now);
|
||
|
// $lab:coverage:on$
|
||
|
}
|
||
|
break;
|
||
|
|
||
|
// Comment
|
||
|
case internals.components.contextComment:
|
||
|
// http://tools.ietf.org/html/rfc5322#section-3.2.2
|
||
|
// comment = "(" *([FWS] ccontent) [FWS] ")"
|
||
|
//
|
||
|
// ccontent = ctext / quoted-pair / comment
|
||
|
switch (token) {
|
||
|
// Nested comment
|
||
|
case '(':
|
||
|
// Nested comments are ok
|
||
|
context.stack.push(context.now);
|
||
|
context.now = internals.components.contextComment;
|
||
|
break;
|
||
|
|
||
|
// End of comment
|
||
|
case ')':
|
||
|
context.prev = context.now;
|
||
|
context.now = context.stack.pop();
|
||
|
break;
|
||
|
|
||
|
// Quoted pair
|
||
|
case '\\':
|
||
|
context.stack.push(context.now);
|
||
|
context.now = internals.components.contextQuotedPair;
|
||
|
break;
|
||
|
|
||
|
// Folding white space
|
||
|
case '\r':
|
||
|
if (emailLength === ++i || email[i] !== '\n') {
|
||
|
// Fatal error
|
||
|
updateResult(internals.diagnoses.errCRNoLF);
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
// Fallthrough
|
||
|
|
||
|
case ' ':
|
||
|
case '\t':
|
||
|
updateResult(internals.diagnoses.cfwsFWS);
|
||
|
|
||
|
context.stack.push(context.now);
|
||
|
context.now = internals.components.contextFWS;
|
||
|
prevToken = token;
|
||
|
break;
|
||
|
|
||
|
// CTEXT
|
||
|
default:
|
||
|
// http://tools.ietf.org/html/rfc5322#section-3.2.3
|
||
|
// ctext = %d33-39 / ; Printable US-ASCII
|
||
|
// %d42-91 / ; characters not including
|
||
|
// %d93-126 / ; "(", ")", or "\"
|
||
|
// obs-ctext
|
||
|
//
|
||
|
// obs-ctext = obs-NO-WS-CTL
|
||
|
//
|
||
|
// obs-NO-WS-CTL = %d1-8 / ; US-ASCII control
|
||
|
// %d11 / ; characters that do not
|
||
|
// %d12 / ; include the carriage
|
||
|
// %d14-31 / ; return, line feed, and
|
||
|
// %d127 ; white space characters
|
||
|
charCode = token.charCodeAt(0);
|
||
|
|
||
|
if (charCode > 127 || charCode === 0 || charCode === 10) {
|
||
|
// Fatal error
|
||
|
updateResult(internals.diagnoses.errExpectingCTEXT);
|
||
|
break;
|
||
|
}
|
||
|
else if (charCode < 32 || charCode === 127) {
|
||
|
updateResult(internals.diagnoses.deprecatedCTEXT);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
break;
|
||
|
|
||
|
// Folding white space
|
||
|
case internals.components.contextFWS:
|
||
|
// http://tools.ietf.org/html/rfc5322#section-3.2.2
|
||
|
// FWS = ([*WSP CRLF] 1*WSP) / obs-FWS
|
||
|
// ; Folding white space
|
||
|
|
||
|
// But note the erratum:
|
||
|
// http://www.rfc-editor.org/errata_search.php?rfc=5322&eid=1908:
|
||
|
// In the obsolete syntax, any amount of folding white space MAY be
|
||
|
// inserted where the obs-FWS rule is allowed. This creates the
|
||
|
// possibility of having two consecutive "folds" in a line, and
|
||
|
// therefore the possibility that a line which makes up a folded header
|
||
|
// field could be composed entirely of white space.
|
||
|
//
|
||
|
// obs-FWS = 1*([CRLF] WSP)
|
||
|
|
||
|
if (prevToken === '\r') {
|
||
|
if (token === '\r') {
|
||
|
// Fatal error
|
||
|
updateResult(internals.diagnoses.errFWSCRLFx2);
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
if (++crlfCount > 1) {
|
||
|
// Multiple folds => obsolete FWS
|
||
|
updateResult(internals.diagnoses.deprecatedFWS);
|
||
|
}
|
||
|
else {
|
||
|
crlfCount = 1;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
switch (token) {
|
||
|
case '\r':
|
||
|
if (emailLength === ++i || email[i] !== '\n') {
|
||
|
// Fatal error
|
||
|
updateResult(internals.diagnoses.errCRNoLF);
|
||
|
}
|
||
|
|
||
|
break;
|
||
|
|
||
|
case ' ':
|
||
|
case '\t':
|
||
|
break;
|
||
|
|
||
|
default:
|
||
|
if (prevToken === '\r') {
|
||
|
// Fatal error
|
||
|
updateResult(internals.diagnoses.errFWSCRLFEnd);
|
||
|
}
|
||
|
|
||
|
crlfCount = 0;
|
||
|
|
||
|
// End of FWS
|
||
|
context.prev = context.now;
|
||
|
context.now = context.stack.pop();
|
||
|
|
||
|
// Look at this token again in the parent context
|
||
|
--i;
|
||
|
}
|
||
|
|
||
|
prevToken = token;
|
||
|
break;
|
||
|
|
||
|
// Unexpected context
|
||
|
// $lab:coverage:off$
|
||
|
default:
|
||
|
throw new Error('unknown context: ' + context.now);
|
||
|
// $lab:coverage:on$
|
||
|
} // Primary state machine
|
||
|
|
||
|
if (maxResult > internals.categories.rfc5322) {
|
||
|
// Fatal error, no point continuing
|
||
|
break;
|
||
|
}
|
||
|
} // Token loop
|
||
|
|
||
|
// Check for errors
|
||
|
if (maxResult < internals.categories.rfc5322) {
|
||
|
// Fatal errors
|
||
|
if (context.now === internals.components.contextQuotedString) {
|
||
|
updateResult(internals.diagnoses.errUnclosedQuotedString);
|
||
|
}
|
||
|
else if (context.now === internals.components.contextQuotedPair) {
|
||
|
updateResult(internals.diagnoses.errBackslashEnd);
|
||
|
}
|
||
|
else if (context.now === internals.components.contextComment) {
|
||
|
updateResult(internals.diagnoses.errUnclosedComment);
|
||
|
}
|
||
|
else if (context.now === internals.components.literal) {
|
||
|
updateResult(internals.diagnoses.errUnclosedDomainLiteral);
|
||
|
}
|
||
|
else if (token === '\r') {
|
||
|
updateResult(internals.diagnoses.errFWSCRLFEnd);
|
||
|
}
|
||
|
else if (parseData.domain.length === 0) {
|
||
|
updateResult(internals.diagnoses.errNoDomain);
|
||
|
}
|
||
|
else if (elementLength === 0) {
|
||
|
updateResult(internals.diagnoses.errDotEnd);
|
||
|
}
|
||
|
else if (hyphenFlag) {
|
||
|
updateResult(internals.diagnoses.errDomainHyphenEnd);
|
||
|
}
|
||
|
|
||
|
// Other errors
|
||
|
else if (parseData.domain.length > 255) {
|
||
|
// http://tools.ietf.org/html/rfc5321#section-4.5.3.1.2
|
||
|
// The maximum total length of a domain name or number is 255 octets.
|
||
|
updateResult(internals.diagnoses.rfc5322DomainTooLong);
|
||
|
}
|
||
|
else if (parseData.local.length + parseData.domain.length + /* '@' */ 1 > 254) {
|
||
|
// http://tools.ietf.org/html/rfc5321#section-4.1.2
|
||
|
// Forward-path = Path
|
||
|
//
|
||
|
// Path = "<" [ A-d-l ":" ] Mailbox ">"
|
||
|
//
|
||
|
// http://tools.ietf.org/html/rfc5321#section-4.5.3.1.3
|
||
|
// The maximum total length of a reverse-path or forward-path is 256 octets (including the punctuation and element separators).
|
||
|
//
|
||
|
// Thus, even without (obsolete) routing information, the Mailbox can only be 254 characters long. This is confirmed by this verified
|
||
|
// erratum to RFC 3696:
|
||
|
//
|
||
|
// http://www.rfc-editor.org/errata_search.php?rfc=3696&eid=1690
|
||
|
// However, there is a restriction in RFC 2821 on the length of an address in MAIL and RCPT commands of 254 characters. Since
|
||
|
// addresses that do not fit in those fields are not normally useful, the upper limit on address lengths should normally be considered
|
||
|
// to be 254.
|
||
|
updateResult(internals.diagnoses.rfc5322TooLong);
|
||
|
}
|
||
|
else if (elementLength > 63) {
|
||
|
// http://tools.ietf.org/html/rfc1035#section-2.3.4
|
||
|
// labels 63 octets or less
|
||
|
updateResult(internals.diagnoses.rfc5322LabelTooLong);
|
||
|
}
|
||
|
else if (options.minDomainAtoms && atomData.domains.length < options.minDomainAtoms) {
|
||
|
updateResult(internals.diagnoses.errDomainTooShort);
|
||
|
}
|
||
|
else if (options.tldWhitelist || options.tldBlacklist) {
|
||
|
const tldAtom = atomData.domains[elementCount];
|
||
|
|
||
|
if (!internals.validDomain(tldAtom, options)) {
|
||
|
updateResult(internals.diagnoses.errUnknownTLD);
|
||
|
}
|
||
|
}
|
||
|
} // Check for errors
|
||
|
|
||
|
let dnsPositive = false;
|
||
|
let finishImmediately = false;
|
||
|
|
||
|
const finish = () => {
|
||
|
|
||
|
if (!dnsPositive && maxResult < internals.categories.dnsWarn) {
|
||
|
// Per RFC 5321, domain atoms are limited to letter-digit-hyphen, so we only need to check code <= 57 to check for a digit
|
||
|
const code = atomData.domains[elementCount].charCodeAt(0);
|
||
|
if (code <= 57) {
|
||
|
updateResult(internals.diagnoses.rfc5321TLDNumeric);
|
||
|
}
|
||
|
else if (elementCount === 0) {
|
||
|
updateResult(internals.diagnoses.rfc5321TLD);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (maxResult < threshold) {
|
||
|
maxResult = internals.diagnoses.valid;
|
||
|
}
|
||
|
|
||
|
const finishResult = diagnose ? maxResult : maxResult < internals.defaultThreshold;
|
||
|
|
||
|
if (callback) {
|
||
|
if (finishImmediately) {
|
||
|
callback(finishResult);
|
||
|
}
|
||
|
else {
|
||
|
internals.defer(callback.bind(null, finishResult));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return finishResult;
|
||
|
}; // Finish
|
||
|
|
||
|
if (options.checkDNS && maxResult < internals.categories.dnsWarn) {
|
||
|
// http://tools.ietf.org/html/rfc5321#section-2.3.5
|
||
|
// Names that can be resolved to MX RRs or address (i.e., A or AAAA) RRs (as discussed in Section 5) are permitted, as are CNAME RRs whose
|
||
|
// targets can be resolved, in turn, to MX or address RRs.
|
||
|
//
|
||
|
// http://tools.ietf.org/html/rfc5321#section-5.1
|
||
|
// The lookup first attempts to locate an MX record associated with the name. If a CNAME record is found, the resulting name is processed
|
||
|
// as if it were the initial name. ... If an empty list of MXs is returned, the address is treated as if it was associated with an implicit
|
||
|
// MX RR, with a preference of 0, pointing to that host.
|
||
|
//
|
||
|
// isEmail() author's note: We will regard the existence of a CNAME to be sufficient evidence of the domain's existence. For performance
|
||
|
// reasons we will not repeat the DNS lookup for the CNAME's target, but we will raise a warning because we didn't immediately find an MX
|
||
|
// record.
|
||
|
if (elementCount === 0) {
|
||
|
// Checking TLD DNS only works if you explicitly check from the root
|
||
|
parseData.domain += '.';
|
||
|
}
|
||
|
|
||
|
const dnsDomain = parseData.domain;
|
||
|
Dns.resolveMx(dnsDomain, (err, mxRecords) => {
|
||
|
|
||
|
// If we have a fatal error, then we must assume that there are no records
|
||
|
if (err && err.code !== Dns.NODATA) {
|
||
|
updateResult(internals.diagnoses.dnsWarnNoRecord);
|
||
|
return finish();
|
||
|
}
|
||
|
|
||
|
if (mxRecords && mxRecords.length) {
|
||
|
dnsPositive = true;
|
||
|
return finish();
|
||
|
}
|
||
|
|
||
|
let count = 3;
|
||
|
let done = false;
|
||
|
updateResult(internals.diagnoses.dnsWarnNoMXRecord);
|
||
|
|
||
|
const handleRecords = (ignoreError, records) => {
|
||
|
|
||
|
if (done) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
--count;
|
||
|
|
||
|
if (records && records.length) {
|
||
|
done = true;
|
||
|
return finish();
|
||
|
}
|
||
|
|
||
|
if (count === 0) {
|
||
|
// No usable records for the domain can be found
|
||
|
updateResult(internals.diagnoses.dnsWarnNoRecord);
|
||
|
done = true;
|
||
|
finish();
|
||
|
}
|
||
|
};
|
||
|
|
||
|
Dns.resolveCname(dnsDomain, handleRecords);
|
||
|
Dns.resolve4(dnsDomain, handleRecords);
|
||
|
Dns.resolve6(dnsDomain, handleRecords);
|
||
|
});
|
||
|
|
||
|
finishImmediately = true;
|
||
|
}
|
||
|
else {
|
||
|
const result = finish();
|
||
|
finishImmediately = true;
|
||
|
return result;
|
||
|
} // CheckDNS
|
||
|
};
|
||
|
|
||
|
|
||
|
exports.diagnoses = internals.validate.diagnoses = (function () {
|
||
|
|
||
|
const diag = {};
|
||
|
const keys = Object.keys(internals.diagnoses);
|
||
|
for (let i = 0; i < keys.length; ++i) {
|
||
|
const key = keys[i];
|
||
|
diag[key] = internals.diagnoses[key];
|
||
|
}
|
||
|
|
||
|
return diag;
|
||
|
})();
|