index.js 59 KB

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