index.js 63 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500
  1. 'use strict';
  2. // Load modules
  3. const Punycode = require('punycode');
  4. const Util = require('util');
  5. // Declare internals
  6. const internals = {
  7. hasOwn: Object.prototype.hasOwnProperty,
  8. indexOf: Array.prototype.indexOf,
  9. defaultThreshold: 16,
  10. maxIPv6Groups: 8,
  11. categories: {
  12. valid: 1,
  13. dnsWarn: 7,
  14. rfc5321: 15,
  15. cfws: 31,
  16. deprecated: 63,
  17. rfc5322: 127,
  18. error: 255
  19. },
  20. diagnoses: {
  21. // Address is valid
  22. valid: 0,
  23. // Address is valid for SMTP but has unusual elements
  24. rfc5321TLD: 9,
  25. rfc5321TLDNumeric: 10,
  26. rfc5321QuotedString: 11,
  27. rfc5321AddressLiteral: 12,
  28. // Address is valid for message, but must be modified for envelope
  29. cfwsComment: 17,
  30. cfwsFWS: 18,
  31. // Address contains non-ASCII when the allowUnicode option is false
  32. // Has to be > internals.defaultThreshold so that it's rejected
  33. // without an explicit errorLevel:
  34. undesiredNonAscii: 25,
  35. // Address contains deprecated elements, but may still be valid in some contexts
  36. deprecatedLocalPart: 33,
  37. deprecatedFWS: 34,
  38. deprecatedQTEXT: 35,
  39. deprecatedQP: 36,
  40. deprecatedComment: 37,
  41. deprecatedCTEXT: 38,
  42. deprecatedIPv6: 39,
  43. deprecatedCFWSNearAt: 49,
  44. // Address is only valid according to broad definition in RFC 5322, but is otherwise invalid
  45. rfc5322Domain: 65,
  46. rfc5322TooLong: 66,
  47. rfc5322LocalTooLong: 67,
  48. rfc5322DomainTooLong: 68,
  49. rfc5322LabelTooLong: 69,
  50. rfc5322DomainLiteral: 70,
  51. rfc5322DomainLiteralOBSDText: 71,
  52. rfc5322IPv6GroupCount: 72,
  53. rfc5322IPv62x2xColon: 73,
  54. rfc5322IPv6BadCharacter: 74,
  55. rfc5322IPv6MaxGroups: 75,
  56. rfc5322IPv6ColonStart: 76,
  57. rfc5322IPv6ColonEnd: 77,
  58. // Address is invalid for any purpose
  59. errExpectingDTEXT: 129,
  60. errNoLocalPart: 130,
  61. errNoDomain: 131,
  62. errConsecutiveDots: 132,
  63. errATEXTAfterCFWS: 133,
  64. errATEXTAfterQS: 134,
  65. errATEXTAfterDomainLiteral: 135,
  66. errExpectingQPair: 136,
  67. errExpectingATEXT: 137,
  68. errExpectingQTEXT: 138,
  69. errExpectingCTEXT: 139,
  70. errBackslashEnd: 140,
  71. errDotStart: 141,
  72. errDotEnd: 142,
  73. errDomainHyphenStart: 143,
  74. errDomainHyphenEnd: 144,
  75. errUnclosedQuotedString: 145,
  76. errUnclosedComment: 146,
  77. errUnclosedDomainLiteral: 147,
  78. errFWSCRLFx2: 148,
  79. errFWSCRLFEnd: 149,
  80. errCRNoLF: 150,
  81. errUnknownTLD: 160,
  82. errDomainTooShort: 161,
  83. errDotAfterDomainLiteral: 162
  84. },
  85. components: {
  86. localpart: 0,
  87. domain: 1,
  88. literal: 2,
  89. contextComment: 3,
  90. contextFWS: 4,
  91. contextQuotedString: 5,
  92. contextQuotedPair: 6
  93. }
  94. };
  95. internals.specials = function () {
  96. const specials = '()<>[]:;@\\,."'; // US-ASCII visible characters not valid for atext (http://tools.ietf.org/html/rfc5322#section-3.2.3)
  97. const lookup = new Array(0x100);
  98. lookup.fill(false);
  99. for (let i = 0; i < specials.length; ++i) {
  100. lookup[specials.codePointAt(i)] = true;
  101. }
  102. return function (code) {
  103. return lookup[code];
  104. };
  105. }();
  106. internals.c0Controls = function () {
  107. const lookup = new Array(0x100);
  108. lookup.fill(false);
  109. // add C0 control characters
  110. for (let i = 0; i < 33; ++i) {
  111. lookup[i] = true;
  112. }
  113. return function (code) {
  114. return lookup[code];
  115. };
  116. }();
  117. internals.c1Controls = function () {
  118. const lookup = new Array(0x100);
  119. lookup.fill(false);
  120. // add C1 control characters
  121. for (let i = 127; i < 160; ++i) {
  122. lookup[i] = true;
  123. }
  124. return function (code) {
  125. return lookup[code];
  126. };
  127. }();
  128. internals.regex = {
  129. ipV4: /\b(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)$/,
  130. ipV6: /^[a-fA-F\d]{0,4}$/
  131. };
  132. internals.normalizeSupportsNul = '\0'.normalize('NFC') === '\0';
  133. // $lab:coverage:off$
  134. internals.nulNormalize = function (email) {
  135. return email.split('\0').map((part) => part.normalize('NFC')).join('\0');
  136. };
  137. // $lab:coverage:on$
  138. internals.normalize = function (email) {
  139. return email.normalize('NFC');
  140. };
  141. // $lab:coverage:off$
  142. if (!internals.normalizeSupportsNul) {
  143. internals.normalize = function (email) {
  144. if (email.indexOf('\0') >= 0) {
  145. return internals.nulNormalize(email);
  146. }
  147. return email.normalize('NFC');
  148. };
  149. }
  150. // $lab:coverage:on$
  151. internals.checkIpV6 = function (items) {
  152. return items.every((value) => internals.regex.ipV6.test(value));
  153. };
  154. internals.isIterable = Array.isArray;
  155. /* $lab:coverage:off$ */
  156. if (typeof Symbol !== 'undefined') {
  157. internals.isIterable = (value) => Array.isArray(value) || (!!value && typeof value === 'object' && typeof value[Symbol.iterator] === 'function');
  158. }
  159. /* $lab:coverage:on$ */
  160. // Node 10 introduced isSet and isMap, which are useful for cross-context type
  161. // checking.
  162. // $lab:coverage:off$
  163. internals._isSet = (value) => value instanceof Set;
  164. internals._isMap = (value) => value instanceof Map;
  165. internals.isSet = Util.types && Util.types.isSet || internals._isSet;
  166. internals.isMap = Util.types && Util.types.isMap || internals._isMap;
  167. // $lab:coverage:on$
  168. /**
  169. * Normalize the given lookup "table" to an iterator. Outputs items in arrays
  170. * and sets, keys from maps (regardless of the corresponding value), and own
  171. * enumerable keys from all other objects (intended to be plain objects).
  172. *
  173. * @param {*} table The table to convert.
  174. * @returns {Iterable<*>} The converted table.
  175. */
  176. internals.normalizeTable = function (table) {
  177. if (internals.isSet(table) || Array.isArray(table)) {
  178. return table;
  179. }
  180. if (internals.isMap(table)) {
  181. return table.keys();
  182. }
  183. return Object.keys(table);
  184. };
  185. /**
  186. * Convert the given domain atom to its canonical form using Nameprep and string
  187. * lowercasing. Domain atoms that are all-ASCII will not undergo any changes via
  188. * Nameprep, and domain atoms that have already been canonicalized will not be
  189. * altered.
  190. *
  191. * @param {string} atom The atom to canonicalize.
  192. * @returns {string} The canonicalized atom.
  193. */
  194. internals.canonicalizeAtom = function (atom) {
  195. return Punycode.toASCII(atom).toLowerCase();
  196. };
  197. /**
  198. * Check whether any of the values in the given iterable, when passed through
  199. * the iteratee function, are equal to the given value.
  200. *
  201. * @param {Iterable<*>} iterable The iterable to check.
  202. * @param {function(*): *} iteratee The iteratee that receives each item from
  203. * the iterable.
  204. * @param {*} value The reference value.
  205. * @returns {boolean} Whether the given value matches any of the items in the
  206. * iterable per the iteratee.
  207. */
  208. internals.includesMapped = function (iterable, iteratee, value) {
  209. for (const item of iterable) {
  210. if (value === iteratee(item)) {
  211. return true;
  212. }
  213. }
  214. return false;
  215. };
  216. /**
  217. * Check whether the given top-level domain atom is valid based on the
  218. * configured blacklist/whitelist.
  219. *
  220. * @param {string} tldAtom The atom to check.
  221. * @param {Object} options
  222. * {*} tldBlacklist The set of domains to consider invalid.
  223. * {*} tldWhitelist The set of domains to consider valid.
  224. * @returns {boolean} Whether the given domain atom is valid per the blacklist/
  225. * whitelist.
  226. */
  227. internals.validDomain = function (tldAtom, options) {
  228. // Nameprep handles case-sensitive unicode stuff, but doesn't touch
  229. // uppercase ASCII characters.
  230. const canonicalTldAtom = internals.canonicalizeAtom(tldAtom);
  231. if (options.tldBlacklist) {
  232. return !internals.includesMapped(
  233. internals.normalizeTable(options.tldBlacklist),
  234. internals.canonicalizeAtom, canonicalTldAtom);
  235. }
  236. return internals.includesMapped(
  237. internals.normalizeTable(options.tldWhitelist),
  238. internals.canonicalizeAtom, canonicalTldAtom);
  239. };
  240. /**
  241. * Check whether the domain atoms has an address literal part followed by a
  242. * normal domain atom part. For example, [127.0.0.1].com.
  243. *
  244. * @param {string[]} domainAtoms The parsed domain atoms.
  245. * @returns {boolean} Whether there exists both a normal domain atom and an
  246. * address literal.
  247. */
  248. internals.hasDomainLiteralThenAtom = function (domainAtoms) {
  249. let hasDomainLiteral = false;
  250. for (let i = 0; i < domainAtoms.length; ++i) {
  251. if (domainAtoms[i][0] === '[') {
  252. hasDomainLiteral = true;
  253. }
  254. else if (hasDomainLiteral) {
  255. return true;
  256. }
  257. }
  258. return false;
  259. };
  260. /**
  261. * Check that an email address conforms to RFCs 5321, 5322, 6530 and others
  262. *
  263. * We distinguish clearly between a Mailbox as defined by RFC 5321 and an
  264. * addr-spec as defined by RFC 5322. Depending on the context, either can be
  265. * regarded as a valid email address. The RFC 5321 Mailbox specification is
  266. * more restrictive (comments, white space and obsolete forms are not allowed).
  267. *
  268. * @param {string} email The email address to check. See README for specifics.
  269. * @param {Object} options The (optional) options:
  270. * {*} errorLevel Determines the boundary between valid and invalid
  271. * addresses.
  272. * {*} tldBlacklist The set of domains to consider invalid.
  273. * {*} tldWhitelist The set of domains to consider valid.
  274. * {*} allowUnicode Whether to allow non-ASCII characters, defaults to true.
  275. * {*} minDomainAtoms The minimum number of domain atoms which must be present
  276. * for the address to be valid.
  277. * @param {function(number|boolean)} callback The (optional) callback handler.
  278. * @return {*}
  279. */
  280. exports.validate = internals.validate = function (email, options, callback) {
  281. options = options || {};
  282. if (typeof email !== 'string') {
  283. throw new TypeError('expected string email');
  284. }
  285. email = internals.normalize(email);
  286. // The callback function is deprecated.
  287. // $lab:coverage:off$
  288. if (typeof options === 'function') {
  289. callback = options;
  290. options = {};
  291. }
  292. if (typeof callback !== 'function') {
  293. callback = null;
  294. }
  295. // $lab:coverage:on$
  296. let diagnose;
  297. let threshold;
  298. if (typeof options.errorLevel === 'number') {
  299. diagnose = true;
  300. threshold = options.errorLevel;
  301. }
  302. else {
  303. diagnose = !!options.errorLevel;
  304. threshold = internals.diagnoses.valid;
  305. }
  306. if (options.tldWhitelist) {
  307. if (typeof options.tldWhitelist === 'string') {
  308. options.tldWhitelist = [options.tldWhitelist];
  309. }
  310. else if (typeof options.tldWhitelist !== 'object') {
  311. throw new TypeError('expected array or object tldWhitelist');
  312. }
  313. }
  314. if (options.tldBlacklist) {
  315. if (typeof options.tldBlacklist === 'string') {
  316. options.tldBlacklist = [options.tldBlacklist];
  317. }
  318. else if (typeof options.tldBlacklist !== 'object') {
  319. throw new TypeError('expected array or object tldBlacklist');
  320. }
  321. }
  322. if (options.minDomainAtoms && (options.minDomainAtoms !== ((+options.minDomainAtoms) | 0) || options.minDomainAtoms < 0)) {
  323. throw new TypeError('expected positive integer minDomainAtoms');
  324. }
  325. // Normalize the set of excluded diagnoses.
  326. if (options.excludeDiagnoses) {
  327. if (!internals.isIterable(options.excludeDiagnoses)) {
  328. throw new TypeError('expected iterable excludeDiagnoses');
  329. }
  330. // This won't catch cross-realm Sets pre-Node 10, but it will cast the
  331. // value to an in-realm Set representation.
  332. if (!internals.isSet(options.excludeDiagnoses)) {
  333. options.excludeDiagnoses = new Set(options.excludeDiagnoses);
  334. }
  335. }
  336. let maxResult = internals.diagnoses.valid;
  337. const updateResult = (value) => {
  338. if (value > maxResult && (!options.excludeDiagnoses || !options.excludeDiagnoses.has(value))) {
  339. maxResult = value;
  340. }
  341. };
  342. const allowUnicode = options.allowUnicode === undefined || !!options.allowUnicode;
  343. if (!allowUnicode && /[^\x00-\x7f]/.test(email)) {
  344. updateResult(internals.diagnoses.undesiredNonAscii);
  345. }
  346. const context = {
  347. now: internals.components.localpart,
  348. prev: internals.components.localpart,
  349. stack: [internals.components.localpart]
  350. };
  351. let prevToken = '';
  352. const parseData = {
  353. local: '',
  354. domain: ''
  355. };
  356. const atomData = {
  357. locals: [''],
  358. domains: ['']
  359. };
  360. let elementCount = 0;
  361. let elementLength = 0;
  362. let crlfCount = 0;
  363. let charCode;
  364. let hyphenFlag = false;
  365. let assertEnd = false;
  366. const emailLength = email.length;
  367. let token; // Token is used outside the loop, must declare similarly
  368. for (let i = 0; i < emailLength; i += token.length) {
  369. // Utilize codepoints to account for Unicode surrogate pairs
  370. token = String.fromCodePoint(email.codePointAt(i));
  371. switch (context.now) {
  372. // Local-part
  373. case internals.components.localpart:
  374. // http://tools.ietf.org/html/rfc5322#section-3.4.1
  375. // local-part = dot-atom / quoted-string / obs-local-part
  376. //
  377. // dot-atom = [CFWS] dot-atom-text [CFWS]
  378. //
  379. // dot-atom-text = 1*atext *("." 1*atext)
  380. //
  381. // quoted-string = [CFWS]
  382. // DQUOTE *([FWS] qcontent) [FWS] DQUOTE
  383. // [CFWS]
  384. //
  385. // obs-local-part = word *("." word)
  386. //
  387. // word = atom / quoted-string
  388. //
  389. // atom = [CFWS] 1*atext [CFWS]
  390. switch (token) {
  391. // Comment
  392. case '(':
  393. if (elementLength === 0) {
  394. // Comments are OK at the beginning of an element
  395. updateResult(elementCount === 0 ? internals.diagnoses.cfwsComment : internals.diagnoses.deprecatedComment);
  396. }
  397. else {
  398. updateResult(internals.diagnoses.cfwsComment);
  399. // Cannot start a comment in an element, should be end
  400. assertEnd = true;
  401. }
  402. context.stack.push(context.now);
  403. context.now = internals.components.contextComment;
  404. break;
  405. // Next dot-atom element
  406. case '.':
  407. if (elementLength === 0) {
  408. // Another dot, already?
  409. updateResult(elementCount === 0 ? internals.diagnoses.errDotStart : internals.diagnoses.errConsecutiveDots);
  410. }
  411. else {
  412. // The entire local-part can be a quoted string for RFC 5321; if one atom is quoted it's an RFC 5322 obsolete form
  413. if (assertEnd) {
  414. updateResult(internals.diagnoses.deprecatedLocalPart);
  415. }
  416. // CFWS & quoted strings are OK again now we're at the beginning of an element (although they are obsolete forms)
  417. assertEnd = false;
  418. elementLength = 0;
  419. ++elementCount;
  420. parseData.local += token;
  421. atomData.locals[elementCount] = '';
  422. }
  423. break;
  424. // Quoted string
  425. case '"':
  426. if (elementLength === 0) {
  427. // The entire local-part can be a quoted string for RFC 5321; if one atom is quoted it's an RFC 5322 obsolete form
  428. updateResult(elementCount === 0 ? internals.diagnoses.rfc5321QuotedString : internals.diagnoses.deprecatedLocalPart);
  429. parseData.local += token;
  430. atomData.locals[elementCount] += token;
  431. elementLength += Buffer.byteLength(token, 'utf8');
  432. // Quoted string must be the entire element
  433. assertEnd = true;
  434. context.stack.push(context.now);
  435. context.now = internals.components.contextQuotedString;
  436. }
  437. else {
  438. updateResult(internals.diagnoses.errExpectingATEXT);
  439. }
  440. break;
  441. // Folding white space
  442. case '\r':
  443. if (emailLength === ++i || email[i] !== '\n') {
  444. // Fatal error
  445. updateResult(internals.diagnoses.errCRNoLF);
  446. break;
  447. }
  448. // Fallthrough
  449. case ' ':
  450. case '\t':
  451. if (elementLength === 0) {
  452. updateResult(elementCount === 0 ? internals.diagnoses.cfwsFWS : internals.diagnoses.deprecatedFWS);
  453. }
  454. else {
  455. // We can't start FWS in the middle of an element, better be end
  456. assertEnd = true;
  457. }
  458. context.stack.push(context.now);
  459. context.now = internals.components.contextFWS;
  460. prevToken = token;
  461. break;
  462. case '@':
  463. // At this point we should have a valid local-part
  464. // $lab:coverage:off$
  465. if (context.stack.length !== 1) {
  466. throw new Error('unexpected item on context stack');
  467. }
  468. // $lab:coverage:on$
  469. if (parseData.local.length === 0) {
  470. // Fatal error
  471. updateResult(internals.diagnoses.errNoLocalPart);
  472. }
  473. else if (elementLength === 0) {
  474. // Fatal error
  475. updateResult(internals.diagnoses.errDotEnd);
  476. }
  477. // 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
  478. // octets
  479. else if (Buffer.byteLength(parseData.local, 'utf8') > 64) {
  480. updateResult(internals.diagnoses.rfc5322LocalTooLong);
  481. }
  482. // http://tools.ietf.org/html/rfc5322#section-3.4.1 comments and folding white space SHOULD NOT be used around "@" in the
  483. // addr-spec
  484. //
  485. // http://tools.ietf.org/html/rfc2119
  486. // 4. SHOULD NOT this phrase, or the phrase "NOT RECOMMENDED" mean that there may exist valid reasons in particular
  487. // circumstances when the particular behavior is acceptable or even useful, but the full implications should be understood
  488. // and the case carefully weighed before implementing any behavior described with this label.
  489. else if (context.prev === internals.components.contextComment || context.prev === internals.components.contextFWS) {
  490. updateResult(internals.diagnoses.deprecatedCFWSNearAt);
  491. }
  492. // Clear everything down for the domain parsing
  493. context.now = internals.components.domain;
  494. context.stack[0] = internals.components.domain;
  495. elementCount = 0;
  496. elementLength = 0;
  497. assertEnd = false; // CFWS can only appear at the end of the element
  498. break;
  499. // ATEXT
  500. default:
  501. // http://tools.ietf.org/html/rfc5322#section-3.2.3
  502. // atext = ALPHA / DIGIT / ; Printable US-ASCII
  503. // "!" / "#" / ; characters not including
  504. // "$" / "%" / ; specials. Used for atoms.
  505. // "&" / "'" /
  506. // "*" / "+" /
  507. // "-" / "/" /
  508. // "=" / "?" /
  509. // "^" / "_" /
  510. // "`" / "{" /
  511. // "|" / "}" /
  512. // "~"
  513. if (assertEnd) {
  514. // We have encountered atext where it is no longer valid
  515. switch (context.prev) {
  516. case internals.components.contextComment:
  517. case internals.components.contextFWS:
  518. updateResult(internals.diagnoses.errATEXTAfterCFWS);
  519. break;
  520. case internals.components.contextQuotedString:
  521. updateResult(internals.diagnoses.errATEXTAfterQS);
  522. break;
  523. // $lab:coverage:off$
  524. default:
  525. throw new Error('more atext found where none is allowed, but unrecognized prev context: ' + context.prev);
  526. // $lab:coverage:on$
  527. }
  528. }
  529. else {
  530. context.prev = context.now;
  531. charCode = token.codePointAt(0);
  532. // Especially if charCode == 10
  533. if (internals.specials(charCode) || internals.c0Controls(charCode) || internals.c1Controls(charCode)) {
  534. // Fatal error
  535. updateResult(internals.diagnoses.errExpectingATEXT);
  536. }
  537. parseData.local += token;
  538. atomData.locals[elementCount] += token;
  539. elementLength += Buffer.byteLength(token, 'utf8');
  540. }
  541. }
  542. break;
  543. case internals.components.domain:
  544. // http://tools.ietf.org/html/rfc5322#section-3.4.1
  545. // domain = dot-atom / domain-literal / obs-domain
  546. //
  547. // dot-atom = [CFWS] dot-atom-text [CFWS]
  548. //
  549. // dot-atom-text = 1*atext *("." 1*atext)
  550. //
  551. // domain-literal = [CFWS] "[" *([FWS] dtext) [FWS] "]" [CFWS]
  552. //
  553. // dtext = %d33-90 / ; Printable US-ASCII
  554. // %d94-126 / ; characters not including
  555. // obs-dtext ; "[", "]", or "\"
  556. //
  557. // obs-domain = atom *("." atom)
  558. //
  559. // atom = [CFWS] 1*atext [CFWS]
  560. // http://tools.ietf.org/html/rfc5321#section-4.1.2
  561. // Mailbox = Local-part "@" ( Domain / address-literal )
  562. //
  563. // Domain = sub-domain *("." sub-domain)
  564. //
  565. // address-literal = "[" ( IPv4-address-literal /
  566. // IPv6-address-literal /
  567. // General-address-literal ) "]"
  568. // ; See Section 4.1.3
  569. // http://tools.ietf.org/html/rfc5322#section-3.4.1
  570. // Note: A liberal syntax for the domain portion of addr-spec is
  571. // given here. However, the domain portion contains addressing
  572. // information specified by and used in other protocols (e.g.,
  573. // [RFC1034], [RFC1035], [RFC1123], [RFC5321]). It is therefore
  574. // incumbent upon implementations to conform to the syntax of
  575. // addresses for the context in which they are used.
  576. //
  577. // is_email() author's note: it's not clear how to interpret this in
  578. // he context of a general email address validator. The conclusion I
  579. // have reached is this: "addressing information" must comply with
  580. // RFC 5321 (and in turn RFC 1035), anything that is "semantically
  581. // invisible" must comply only with RFC 5322.
  582. switch (token) {
  583. // Comment
  584. case '(':
  585. if (elementLength === 0) {
  586. // Comments at the start of the domain are deprecated in the text, comments at the start of a subdomain are obs-domain
  587. // http://tools.ietf.org/html/rfc5322#section-3.4.1
  588. updateResult(elementCount === 0 ? internals.diagnoses.deprecatedCFWSNearAt : internals.diagnoses.deprecatedComment);
  589. }
  590. else {
  591. // We can't start a comment mid-element, better be at the end
  592. assertEnd = true;
  593. updateResult(internals.diagnoses.cfwsComment);
  594. }
  595. context.stack.push(context.now);
  596. context.now = internals.components.contextComment;
  597. break;
  598. // Next dot-atom element
  599. case '.':
  600. const punycodeLength = Punycode.toASCII(atomData.domains[elementCount]).length;
  601. if (elementLength === 0) {
  602. // Another dot, already? Fatal error.
  603. updateResult(elementCount === 0 ? internals.diagnoses.errDotStart : internals.diagnoses.errConsecutiveDots);
  604. }
  605. else if (hyphenFlag) {
  606. // Previous subdomain ended in a hyphen. Fatal error.
  607. updateResult(internals.diagnoses.errDomainHyphenEnd);
  608. }
  609. else if (punycodeLength > 63) {
  610. // RFC 5890 specifies that domain labels that are encoded using the Punycode algorithm
  611. // must adhere to the <= 63 octet requirement.
  612. // This includes string prefixes from the Punycode algorithm.
  613. //
  614. // https://tools.ietf.org/html/rfc5890#section-2.3.2.1
  615. // labels 63 octets or less
  616. updateResult(internals.diagnoses.rfc5322LabelTooLong);
  617. }
  618. // CFWS is OK again now we're at the beginning of an element (although
  619. // it may be obsolete CFWS)
  620. assertEnd = false;
  621. elementLength = 0;
  622. ++elementCount;
  623. atomData.domains[elementCount] = '';
  624. parseData.domain += token;
  625. break;
  626. // Domain literal
  627. case '[':
  628. if (atomData.domains[elementCount].length === 0) {
  629. if (parseData.domain.length) {
  630. // Domain literal interspersed with domain refs.
  631. updateResult(internals.diagnoses.errDotAfterDomainLiteral);
  632. }
  633. assertEnd = true;
  634. elementLength += Buffer.byteLength(token, 'utf8');
  635. context.stack.push(context.now);
  636. context.now = internals.components.literal;
  637. parseData.domain += token;
  638. atomData.domains[elementCount] += token;
  639. parseData.literal = '';
  640. }
  641. else {
  642. // Fatal error
  643. updateResult(internals.diagnoses.errExpectingATEXT);
  644. }
  645. break;
  646. // Folding white space
  647. case '\r':
  648. if (emailLength === ++i || email[i] !== '\n') {
  649. // Fatal error
  650. updateResult(internals.diagnoses.errCRNoLF);
  651. break;
  652. }
  653. // Fallthrough
  654. case ' ':
  655. case '\t':
  656. if (elementLength === 0) {
  657. updateResult(elementCount === 0 ? internals.diagnoses.deprecatedCFWSNearAt : internals.diagnoses.deprecatedFWS);
  658. }
  659. else {
  660. // We can't start FWS in the middle of an element, so this better be the end
  661. updateResult(internals.diagnoses.cfwsFWS);
  662. assertEnd = true;
  663. }
  664. context.stack.push(context.now);
  665. context.now = internals.components.contextFWS;
  666. prevToken = token;
  667. break;
  668. // This must be ATEXT
  669. default:
  670. // RFC 5322 allows any atext...
  671. // http://tools.ietf.org/html/rfc5322#section-3.2.3
  672. // atext = ALPHA / DIGIT / ; Printable US-ASCII
  673. // "!" / "#" / ; characters not including
  674. // "$" / "%" / ; specials. Used for atoms.
  675. // "&" / "'" /
  676. // "*" / "+" /
  677. // "-" / "/" /
  678. // "=" / "?" /
  679. // "^" / "_" /
  680. // "`" / "{" /
  681. // "|" / "}" /
  682. // "~"
  683. // But RFC 5321 only allows letter-digit-hyphen to comply with DNS rules
  684. // (RFCs 1034 & 1123)
  685. // http://tools.ietf.org/html/rfc5321#section-4.1.2
  686. // sub-domain = Let-dig [Ldh-str]
  687. //
  688. // Let-dig = ALPHA / DIGIT
  689. //
  690. // Ldh-str = *( ALPHA / DIGIT / "-" ) Let-dig
  691. //
  692. if (assertEnd) {
  693. // We have encountered ATEXT where it is no longer valid
  694. switch (context.prev) {
  695. case internals.components.contextComment:
  696. case internals.components.contextFWS:
  697. updateResult(internals.diagnoses.errATEXTAfterCFWS);
  698. break;
  699. case internals.components.literal:
  700. updateResult(internals.diagnoses.errATEXTAfterDomainLiteral);
  701. break;
  702. // $lab:coverage:off$
  703. default:
  704. throw new Error('more atext found where none is allowed, but unrecognized prev context: ' + context.prev);
  705. // $lab:coverage:on$
  706. }
  707. }
  708. charCode = token.codePointAt(0);
  709. // Assume this token isn't a hyphen unless we discover it is
  710. hyphenFlag = false;
  711. if (internals.specials(charCode) || internals.c0Controls(charCode) || internals.c1Controls(charCode)) {
  712. // Fatal error
  713. updateResult(internals.diagnoses.errExpectingATEXT);
  714. }
  715. else if (token === '-') {
  716. if (elementLength === 0) {
  717. // Hyphens cannot be at the beginning of a subdomain, fatal error
  718. updateResult(internals.diagnoses.errDomainHyphenStart);
  719. }
  720. hyphenFlag = true;
  721. }
  722. // Check if it's a neither a number nor a latin/unicode letter
  723. else if (charCode < 48 || (charCode > 122 && charCode < 192) || (charCode > 57 && charCode < 65) || (charCode > 90 && charCode < 97)) {
  724. // This is not an RFC 5321 subdomain, but still OK by RFC 5322
  725. updateResult(internals.diagnoses.rfc5322Domain);
  726. }
  727. parseData.domain += token;
  728. atomData.domains[elementCount] += token;
  729. elementLength += Buffer.byteLength(token, 'utf8');
  730. }
  731. break;
  732. // Domain literal
  733. case internals.components.literal:
  734. // http://tools.ietf.org/html/rfc5322#section-3.4.1
  735. // domain-literal = [CFWS] "[" *([FWS] dtext) [FWS] "]" [CFWS]
  736. //
  737. // dtext = %d33-90 / ; Printable US-ASCII
  738. // %d94-126 / ; characters not including
  739. // obs-dtext ; "[", "]", or "\"
  740. //
  741. // obs-dtext = obs-NO-WS-CTL / quoted-pair
  742. switch (token) {
  743. // End of domain literal
  744. case ']':
  745. if (maxResult < internals.categories.deprecated) {
  746. // Could be a valid RFC 5321 address literal, so let's check
  747. // http://tools.ietf.org/html/rfc5321#section-4.1.2
  748. // address-literal = "[" ( IPv4-address-literal /
  749. // IPv6-address-literal /
  750. // General-address-literal ) "]"
  751. // ; See Section 4.1.3
  752. //
  753. // http://tools.ietf.org/html/rfc5321#section-4.1.3
  754. // IPv4-address-literal = Snum 3("." Snum)
  755. //
  756. // IPv6-address-literal = "IPv6:" IPv6-addr
  757. //
  758. // General-address-literal = Standardized-tag ":" 1*dcontent
  759. //
  760. // Standardized-tag = Ldh-str
  761. // ; Standardized-tag MUST be specified in a
  762. // ; Standards-Track RFC and registered with IANA
  763. //
  764. // dcontent = %d33-90 / ; Printable US-ASCII
  765. // %d94-126 ; excl. "[", "\", "]"
  766. //
  767. // Snum = 1*3DIGIT
  768. // ; representing a decimal integer
  769. // ; value in the range 0 through 255
  770. //
  771. // IPv6-addr = IPv6-full / IPv6-comp / IPv6v4-full / IPv6v4-comp
  772. //
  773. // IPv6-hex = 1*4HEXDIG
  774. //
  775. // IPv6-full = IPv6-hex 7(":" IPv6-hex)
  776. //
  777. // IPv6-comp = [IPv6-hex *5(":" IPv6-hex)] "::"
  778. // [IPv6-hex *5(":" IPv6-hex)]
  779. // ; The "::" represents at least 2 16-bit groups of
  780. // ; zeros. No more than 6 groups in addition to the
  781. // ; "::" may be present.
  782. //
  783. // IPv6v4-full = IPv6-hex 5(":" IPv6-hex) ":" IPv4-address-literal
  784. //
  785. // IPv6v4-comp = [IPv6-hex *3(":" IPv6-hex)] "::"
  786. // [IPv6-hex *3(":" IPv6-hex) ":"]
  787. // IPv4-address-literal
  788. // ; The "::" represents at least 2 16-bit groups of
  789. // ; zeros. No more than 4 groups in addition to the
  790. // ; "::" and IPv4-address-literal may be present.
  791. let index = -1;
  792. let addressLiteral = parseData.literal;
  793. const matchesIP = internals.regex.ipV4.exec(addressLiteral);
  794. // Maybe extract IPv4 part from the end of the address-literal
  795. if (matchesIP) {
  796. index = matchesIP.index;
  797. if (index !== 0) {
  798. // Convert IPv4 part to IPv6 format for futher testing
  799. addressLiteral = addressLiteral.slice(0, index) + '0:0';
  800. }
  801. }
  802. if (index === 0) {
  803. // Nothing there except a valid IPv4 address, so...
  804. updateResult(internals.diagnoses.rfc5321AddressLiteral);
  805. }
  806. else if (addressLiteral.slice(0, 5).toLowerCase() !== 'ipv6:') {
  807. updateResult(internals.diagnoses.rfc5322DomainLiteral);
  808. }
  809. else {
  810. const match = addressLiteral.slice(5);
  811. let maxGroups = internals.maxIPv6Groups;
  812. const groups = match.split(':');
  813. index = match.indexOf('::');
  814. if (!~index) {
  815. // Need exactly the right number of groups
  816. if (groups.length !== maxGroups) {
  817. updateResult(internals.diagnoses.rfc5322IPv6GroupCount);
  818. }
  819. }
  820. else if (index !== match.lastIndexOf('::')) {
  821. updateResult(internals.diagnoses.rfc5322IPv62x2xColon);
  822. }
  823. else {
  824. if (index === 0 || index === match.length - 2) {
  825. // RFC 4291 allows :: at the start or end of an address with 7 other groups in addition
  826. ++maxGroups;
  827. }
  828. if (groups.length > maxGroups) {
  829. updateResult(internals.diagnoses.rfc5322IPv6MaxGroups);
  830. }
  831. else if (groups.length === maxGroups) {
  832. // Eliding a single "::"
  833. updateResult(internals.diagnoses.deprecatedIPv6);
  834. }
  835. }
  836. // IPv6 testing strategy
  837. if (match[0] === ':' && match[1] !== ':') {
  838. updateResult(internals.diagnoses.rfc5322IPv6ColonStart);
  839. }
  840. else if (match[match.length - 1] === ':' && match[match.length - 2] !== ':') {
  841. updateResult(internals.diagnoses.rfc5322IPv6ColonEnd);
  842. }
  843. else if (internals.checkIpV6(groups)) {
  844. updateResult(internals.diagnoses.rfc5321AddressLiteral);
  845. }
  846. else {
  847. updateResult(internals.diagnoses.rfc5322IPv6BadCharacter);
  848. }
  849. }
  850. }
  851. else {
  852. updateResult(internals.diagnoses.rfc5322DomainLiteral);
  853. }
  854. parseData.domain += token;
  855. atomData.domains[elementCount] += token;
  856. elementLength += Buffer.byteLength(token, 'utf8');
  857. context.prev = context.now;
  858. context.now = context.stack.pop();
  859. break;
  860. case '\\':
  861. updateResult(internals.diagnoses.rfc5322DomainLiteralOBSDText);
  862. context.stack.push(context.now);
  863. context.now = internals.components.contextQuotedPair;
  864. break;
  865. // Folding white space
  866. case '\r':
  867. if (emailLength === ++i || email[i] !== '\n') {
  868. updateResult(internals.diagnoses.errCRNoLF);
  869. break;
  870. }
  871. // Fallthrough
  872. case ' ':
  873. case '\t':
  874. updateResult(internals.diagnoses.cfwsFWS);
  875. context.stack.push(context.now);
  876. context.now = internals.components.contextFWS;
  877. prevToken = token;
  878. break;
  879. // DTEXT
  880. default:
  881. // http://tools.ietf.org/html/rfc5322#section-3.4.1
  882. // dtext = %d33-90 / ; Printable US-ASCII
  883. // %d94-126 / ; characters not including
  884. // obs-dtext ; "[", "]", or "\"
  885. //
  886. // obs-dtext = obs-NO-WS-CTL / quoted-pair
  887. //
  888. // obs-NO-WS-CTL = %d1-8 / ; US-ASCII control
  889. // %d11 / ; characters that do not
  890. // %d12 / ; include the carriage
  891. // %d14-31 / ; return, line feed, and
  892. // %d127 ; white space characters
  893. charCode = token.codePointAt(0);
  894. // '\r', '\n', ' ', and '\t' have already been parsed above
  895. if ((charCode !== 127 && internals.c1Controls(charCode)) || charCode === 0 || token === '[') {
  896. // Fatal error
  897. updateResult(internals.diagnoses.errExpectingDTEXT);
  898. break;
  899. }
  900. else if (internals.c0Controls(charCode) || charCode === 127) {
  901. updateResult(internals.diagnoses.rfc5322DomainLiteralOBSDText);
  902. }
  903. parseData.literal += token;
  904. parseData.domain += token;
  905. atomData.domains[elementCount] += token;
  906. elementLength += Buffer.byteLength(token, 'utf8');
  907. }
  908. break;
  909. // Quoted string
  910. case internals.components.contextQuotedString:
  911. // http://tools.ietf.org/html/rfc5322#section-3.2.4
  912. // quoted-string = [CFWS]
  913. // DQUOTE *([FWS] qcontent) [FWS] DQUOTE
  914. // [CFWS]
  915. //
  916. // qcontent = qtext / quoted-pair
  917. switch (token) {
  918. // Quoted pair
  919. case '\\':
  920. context.stack.push(context.now);
  921. context.now = internals.components.contextQuotedPair;
  922. break;
  923. // Folding white space. Spaces are allowed as regular characters inside a quoted string - it's only FWS if we include '\t' or '\r\n'
  924. case '\r':
  925. if (emailLength === ++i || email[i] !== '\n') {
  926. // Fatal error
  927. updateResult(internals.diagnoses.errCRNoLF);
  928. break;
  929. }
  930. // Fallthrough
  931. case '\t':
  932. // http://tools.ietf.org/html/rfc5322#section-3.2.2
  933. // Runs of FWS, comment, or CFWS that occur between lexical tokens in
  934. // a structured header field are semantically interpreted as a single
  935. // space character.
  936. // http://tools.ietf.org/html/rfc5322#section-3.2.4
  937. // the CRLF in any FWS/CFWS that appears within the quoted-string [is]
  938. // semantically "invisible" and therefore not part of the
  939. // quoted-string
  940. parseData.local += ' ';
  941. atomData.locals[elementCount] += ' ';
  942. elementLength += Buffer.byteLength(token, 'utf8');
  943. updateResult(internals.diagnoses.cfwsFWS);
  944. context.stack.push(context.now);
  945. context.now = internals.components.contextFWS;
  946. prevToken = token;
  947. break;
  948. // End of quoted string
  949. case '"':
  950. parseData.local += token;
  951. atomData.locals[elementCount] += token;
  952. elementLength += Buffer.byteLength(token, 'utf8');
  953. context.prev = context.now;
  954. context.now = context.stack.pop();
  955. break;
  956. // QTEXT
  957. default:
  958. // http://tools.ietf.org/html/rfc5322#section-3.2.4
  959. // qtext = %d33 / ; Printable US-ASCII
  960. // %d35-91 / ; characters not including
  961. // %d93-126 / ; "\" or the quote character
  962. // obs-qtext
  963. //
  964. // obs-qtext = obs-NO-WS-CTL
  965. //
  966. // obs-NO-WS-CTL = %d1-8 / ; US-ASCII control
  967. // %d11 / ; characters that do not
  968. // %d12 / ; include the carriage
  969. // %d14-31 / ; return, line feed, and
  970. // %d127 ; white space characters
  971. charCode = token.codePointAt(0);
  972. if ((charCode !== 127 && internals.c1Controls(charCode)) || charCode === 0 || charCode === 10) {
  973. updateResult(internals.diagnoses.errExpectingQTEXT);
  974. }
  975. else if (internals.c0Controls(charCode) || charCode === 127) {
  976. updateResult(internals.diagnoses.deprecatedQTEXT);
  977. }
  978. parseData.local += token;
  979. atomData.locals[elementCount] += token;
  980. elementLength += Buffer.byteLength(token, 'utf8');
  981. }
  982. // http://tools.ietf.org/html/rfc5322#section-3.4.1
  983. // If the string can be represented as a dot-atom (that is, it contains
  984. // no characters other than atext characters or "." surrounded by atext
  985. // characters), then the dot-atom form SHOULD be used and the quoted-
  986. // string form SHOULD NOT be used.
  987. break;
  988. // Quoted pair
  989. case internals.components.contextQuotedPair:
  990. // http://tools.ietf.org/html/rfc5322#section-3.2.1
  991. // quoted-pair = ("\" (VCHAR / WSP)) / obs-qp
  992. //
  993. // VCHAR = %d33-126 ; visible (printing) characters
  994. // WSP = SP / HTAB ; white space
  995. //
  996. // obs-qp = "\" (%d0 / obs-NO-WS-CTL / LF / CR)
  997. //
  998. // obs-NO-WS-CTL = %d1-8 / ; US-ASCII control
  999. // %d11 / ; characters that do not
  1000. // %d12 / ; include the carriage
  1001. // %d14-31 / ; return, line feed, and
  1002. // %d127 ; white space characters
  1003. //
  1004. // i.e. obs-qp = "\" (%d0-8, %d10-31 / %d127)
  1005. charCode = token.codePointAt(0);
  1006. if (charCode !== 127 && internals.c1Controls(charCode)) {
  1007. // Fatal error
  1008. updateResult(internals.diagnoses.errExpectingQPair);
  1009. }
  1010. else if ((charCode < 31 && charCode !== 9) || charCode === 127) {
  1011. // ' ' and '\t' are allowed
  1012. updateResult(internals.diagnoses.deprecatedQP);
  1013. }
  1014. // 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.
  1015. // http://tools.ietf.org/html/rfc5321#section-4.1.2
  1016. // the sending system SHOULD transmit the form that uses the minimum quoting possible.
  1017. context.prev = context.now;
  1018. // End of qpair
  1019. context.now = context.stack.pop();
  1020. const escapeToken = '\\' + token;
  1021. switch (context.now) {
  1022. case internals.components.contextComment:
  1023. break;
  1024. case internals.components.contextQuotedString:
  1025. parseData.local += escapeToken;
  1026. atomData.locals[elementCount] += escapeToken;
  1027. // The maximum sizes specified by RFC 5321 are octet counts, so we must include the backslash
  1028. elementLength += 2;
  1029. break;
  1030. case internals.components.literal:
  1031. parseData.domain += escapeToken;
  1032. atomData.domains[elementCount] += escapeToken;
  1033. // The maximum sizes specified by RFC 5321 are octet counts, so we must include the backslash
  1034. elementLength += 2;
  1035. break;
  1036. // $lab:coverage:off$
  1037. default:
  1038. throw new Error('quoted pair logic invoked in an invalid context: ' + context.now);
  1039. // $lab:coverage:on$
  1040. }
  1041. break;
  1042. // Comment
  1043. case internals.components.contextComment:
  1044. // http://tools.ietf.org/html/rfc5322#section-3.2.2
  1045. // comment = "(" *([FWS] ccontent) [FWS] ")"
  1046. //
  1047. // ccontent = ctext / quoted-pair / comment
  1048. switch (token) {
  1049. // Nested comment
  1050. case '(':
  1051. // Nested comments are ok
  1052. context.stack.push(context.now);
  1053. context.now = internals.components.contextComment;
  1054. break;
  1055. // End of comment
  1056. case ')':
  1057. context.prev = context.now;
  1058. context.now = context.stack.pop();
  1059. break;
  1060. // Quoted pair
  1061. case '\\':
  1062. context.stack.push(context.now);
  1063. context.now = internals.components.contextQuotedPair;
  1064. break;
  1065. // Folding white space
  1066. case '\r':
  1067. if (emailLength === ++i || email[i] !== '\n') {
  1068. // Fatal error
  1069. updateResult(internals.diagnoses.errCRNoLF);
  1070. break;
  1071. }
  1072. // Fallthrough
  1073. case ' ':
  1074. case '\t':
  1075. updateResult(internals.diagnoses.cfwsFWS);
  1076. context.stack.push(context.now);
  1077. context.now = internals.components.contextFWS;
  1078. prevToken = token;
  1079. break;
  1080. // CTEXT
  1081. default:
  1082. // http://tools.ietf.org/html/rfc5322#section-3.2.3
  1083. // ctext = %d33-39 / ; Printable US-ASCII
  1084. // %d42-91 / ; characters not including
  1085. // %d93-126 / ; "(", ")", or "\"
  1086. // obs-ctext
  1087. //
  1088. // obs-ctext = obs-NO-WS-CTL
  1089. //
  1090. // obs-NO-WS-CTL = %d1-8 / ; US-ASCII control
  1091. // %d11 / ; characters that do not
  1092. // %d12 / ; include the carriage
  1093. // %d14-31 / ; return, line feed, and
  1094. // %d127 ; white space characters
  1095. charCode = token.codePointAt(0);
  1096. if (charCode === 0 || charCode === 10 || (charCode !== 127 && internals.c1Controls(charCode))) {
  1097. // Fatal error
  1098. updateResult(internals.diagnoses.errExpectingCTEXT);
  1099. break;
  1100. }
  1101. else if (internals.c0Controls(charCode) || charCode === 127) {
  1102. updateResult(internals.diagnoses.deprecatedCTEXT);
  1103. }
  1104. }
  1105. break;
  1106. // Folding white space
  1107. case internals.components.contextFWS:
  1108. // http://tools.ietf.org/html/rfc5322#section-3.2.2
  1109. // FWS = ([*WSP CRLF] 1*WSP) / obs-FWS
  1110. // ; Folding white space
  1111. // But note the erratum:
  1112. // http://www.rfc-editor.org/errata_search.php?rfc=5322&eid=1908:
  1113. // In the obsolete syntax, any amount of folding white space MAY be
  1114. // inserted where the obs-FWS rule is allowed. This creates the
  1115. // possibility of having two consecutive "folds" in a line, and
  1116. // therefore the possibility that a line which makes up a folded header
  1117. // field could be composed entirely of white space.
  1118. //
  1119. // obs-FWS = 1*([CRLF] WSP)
  1120. if (prevToken === '\r') {
  1121. if (token === '\r') {
  1122. // Fatal error
  1123. updateResult(internals.diagnoses.errFWSCRLFx2);
  1124. break;
  1125. }
  1126. if (++crlfCount > 1) {
  1127. // Multiple folds => obsolete FWS
  1128. updateResult(internals.diagnoses.deprecatedFWS);
  1129. }
  1130. else {
  1131. crlfCount = 1;
  1132. }
  1133. }
  1134. switch (token) {
  1135. case '\r':
  1136. if (emailLength === ++i || email[i] !== '\n') {
  1137. // Fatal error
  1138. updateResult(internals.diagnoses.errCRNoLF);
  1139. }
  1140. break;
  1141. case ' ':
  1142. case '\t':
  1143. break;
  1144. default:
  1145. if (prevToken === '\r') {
  1146. // Fatal error
  1147. updateResult(internals.diagnoses.errFWSCRLFEnd);
  1148. }
  1149. crlfCount = 0;
  1150. // End of FWS
  1151. context.prev = context.now;
  1152. context.now = context.stack.pop();
  1153. // Look at this token again in the parent context
  1154. --i;
  1155. }
  1156. prevToken = token;
  1157. break;
  1158. // Unexpected context
  1159. // $lab:coverage:off$
  1160. default:
  1161. throw new Error('unknown context: ' + context.now);
  1162. // $lab:coverage:on$
  1163. } // Primary state machine
  1164. if (maxResult > internals.categories.rfc5322) {
  1165. // Fatal error, no point continuing
  1166. break;
  1167. }
  1168. } // Token loop
  1169. // Check for errors
  1170. if (maxResult < internals.categories.rfc5322) {
  1171. const punycodeLength = Punycode.toASCII(parseData.domain).length;
  1172. // Fatal errors
  1173. if (context.now === internals.components.contextQuotedString) {
  1174. updateResult(internals.diagnoses.errUnclosedQuotedString);
  1175. }
  1176. else if (context.now === internals.components.contextQuotedPair) {
  1177. updateResult(internals.diagnoses.errBackslashEnd);
  1178. }
  1179. else if (context.now === internals.components.contextComment) {
  1180. updateResult(internals.diagnoses.errUnclosedComment);
  1181. }
  1182. else if (context.now === internals.components.literal) {
  1183. updateResult(internals.diagnoses.errUnclosedDomainLiteral);
  1184. }
  1185. else if (token === '\r') {
  1186. updateResult(internals.diagnoses.errFWSCRLFEnd);
  1187. }
  1188. else if (parseData.domain.length === 0) {
  1189. updateResult(internals.diagnoses.errNoDomain);
  1190. }
  1191. else if (elementLength === 0) {
  1192. updateResult(internals.diagnoses.errDotEnd);
  1193. }
  1194. else if (hyphenFlag) {
  1195. updateResult(internals.diagnoses.errDomainHyphenEnd);
  1196. }
  1197. // Other errors
  1198. else if (punycodeLength > 255) {
  1199. // http://tools.ietf.org/html/rfc5321#section-4.5.3.1.2
  1200. // The maximum total length of a domain name or number is 255 octets.
  1201. updateResult(internals.diagnoses.rfc5322DomainTooLong);
  1202. }
  1203. else if (Buffer.byteLength(parseData.local, 'utf8') + punycodeLength + /* '@' */ 1 > 254) {
  1204. // http://tools.ietf.org/html/rfc5321#section-4.1.2
  1205. // Forward-path = Path
  1206. //
  1207. // Path = "<" [ A-d-l ":" ] Mailbox ">"
  1208. //
  1209. // http://tools.ietf.org/html/rfc5321#section-4.5.3.1.3
  1210. // The maximum total length of a reverse-path or forward-path is 256 octets (including the punctuation and element separators).
  1211. //
  1212. // Thus, even without (obsolete) routing information, the Mailbox can only be 254 characters long. This is confirmed by this verified
  1213. // erratum to RFC 3696:
  1214. //
  1215. // http://www.rfc-editor.org/errata_search.php?rfc=3696&eid=1690
  1216. // However, there is a restriction in RFC 2821 on the length of an address in MAIL and RCPT commands of 254 characters. Since
  1217. // addresses that do not fit in those fields are not normally useful, the upper limit on address lengths should normally be considered
  1218. // to be 254.
  1219. updateResult(internals.diagnoses.rfc5322TooLong);
  1220. }
  1221. else if (elementLength > 63) {
  1222. // http://tools.ietf.org/html/rfc1035#section-2.3.4
  1223. // labels 63 octets or less
  1224. updateResult(internals.diagnoses.rfc5322LabelTooLong);
  1225. }
  1226. else if (options.minDomainAtoms && atomData.domains.length < options.minDomainAtoms && (atomData.domains.length !== 1 || atomData.domains[0][0] !== '[')) {
  1227. updateResult(internals.diagnoses.errDomainTooShort);
  1228. }
  1229. else if (internals.hasDomainLiteralThenAtom(atomData.domains)) {
  1230. updateResult(internals.diagnoses.errDotAfterDomainLiteral);
  1231. }
  1232. else if (options.tldWhitelist || options.tldBlacklist) {
  1233. const tldAtom = atomData.domains[elementCount];
  1234. if (!internals.validDomain(tldAtom, options)) {
  1235. updateResult(internals.diagnoses.errUnknownTLD);
  1236. }
  1237. }
  1238. } // Check for errors
  1239. // Finish
  1240. if (maxResult < internals.categories.dnsWarn) {
  1241. // Per RFC 5321, domain atoms are limited to letter-digit-hyphen, so we only need to check code <= 57 to check for a digit
  1242. const code = atomData.domains[elementCount].codePointAt(0);
  1243. if (code <= 57) {
  1244. updateResult(internals.diagnoses.rfc5321TLDNumeric);
  1245. }
  1246. }
  1247. if (maxResult < threshold) {
  1248. maxResult = internals.diagnoses.valid;
  1249. }
  1250. const finishResult = diagnose ? maxResult : maxResult < internals.defaultThreshold;
  1251. // $lab:coverage:off$
  1252. if (callback) {
  1253. callback(finishResult);
  1254. }
  1255. // $lab:coverage:on$
  1256. return finishResult;
  1257. };
  1258. exports.diagnoses = internals.validate.diagnoses = (function () {
  1259. const diag = {};
  1260. const keys = Object.keys(internals.diagnoses);
  1261. for (let i = 0; i < keys.length; ++i) {
  1262. const key = keys[i];
  1263. diag[key] = internals.diagnoses[key];
  1264. }
  1265. return diag;
  1266. })();
  1267. exports.normalize = internals.normalize;