PageRenderTime 58ms CodeModel.GetById 21ms RepoModel.GetById 0ms app.codeStats 0ms

/std/net/isemail.d

http://github.com/jcd/phobos
D | 2073 lines | 1336 code | 402 blank | 335 comment | 341 complexity | a1982edccdc56ec3ff1ce8370f71a4cd MD5 | raw file
  1. /**
  2. * Validates an email address according to RFCs 5321, 5322 and others.
  3. *
  4. * Authors: Dominic Sayers <dominic@sayers.cc>, Jacob Carlborg
  5. * Copyright: Dominic Sayers, Jacob Carlborg 2008-.
  6. * Test schema documentation: Copyright © 2011, Daniel Marschall
  7. * License: $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost Software License 1.0)
  8. * Version: 3.0.13 - Version 3.0 of the original PHP implementation: $(LINK http://www.dominicsayers.com/isemail)
  9. *
  10. * Standards:
  11. * $(UL
  12. * $(LI RFC 5321)
  13. * $(LI RFC 5322)
  14. * )
  15. *
  16. * References:
  17. * $(UL
  18. * $(LI $(LINK http://www.dominicsayers.com/isemail))
  19. * $(LI $(LINK http://tools.ietf.org/html/rfc5321))
  20. * $(LI $(LINK http://tools.ietf.org/html/rfc5322))
  21. * )
  22. *
  23. * Source: $(PHOBOSSRC std/net/_isemail.d)
  24. */
  25. module std.net.isemail;
  26. import std.algorithm : cmp, equal, uniq, filter, contains = canFind;
  27. import std.range : ElementType;
  28. import std.array;
  29. import std.ascii;
  30. import std.conv;
  31. import std.exception : enforce;
  32. import std.regex;
  33. import std.string;
  34. import std.traits;
  35. import std.utf;
  36. import std.uni;
  37. /**
  38. * Check that an email address conforms to RFCs 5321, 5322 and others.
  39. *
  40. * As of Version 3.0, we are now distinguishing clearly between a Mailbox as defined
  41. * by RFC 5321 and an addr-spec as defined by RFC 5322. Depending on the context,
  42. * either can be regarded as a valid email address. The RFC 5321 Mailbox specification
  43. * is more restrictive (comments, white space and obsolete forms are not allowed).
  44. *
  45. * Note: The DNS check is currently not implemented.
  46. *
  47. * Params:
  48. * email = The email address to check
  49. * checkDNS = If CheckDns.yes then a DNS check for MX records will be made
  50. * errorLevel = Determines the boundary between valid and invalid addresses.
  51. * Status codes above this number will be returned as-is,
  52. * status codes below will be returned as EmailStatusCode.valid.
  53. * Thus the calling program can simply look for EmailStatusCode.valid
  54. * if it is only interested in whether an address is valid or not. The
  55. * $(D_PARAM errorLevel) will determine how "picky" isEmail() is about
  56. * the address.
  57. *
  58. * If omitted or passed as EmailStatusCode.none then isEmail() will
  59. * not perform any finer grained error checking and an address is
  60. * either considered valid or not. Email status code will either be
  61. * EmailStatusCode.valid or EmailStatusCode.error.
  62. *
  63. * Returns: an EmailStatus, indicating the status of the email address.
  64. */
  65. EmailStatus isEmail (Char) (const(Char)[] email, CheckDns checkDNS = CheckDns.no,
  66. EmailStatusCode errorLevel = EmailStatusCode.none) if (isSomeChar!(Char))
  67. {
  68. alias const(Char)[] tstring;
  69. enum defaultThreshold = 16;
  70. int threshold;
  71. bool diagnose;
  72. if (errorLevel == EmailStatusCode.any || errorLevel == EmailStatusCode.none)
  73. {
  74. threshold = EmailStatusCode.valid;
  75. diagnose = errorLevel == EmailStatusCode.any;
  76. }
  77. else
  78. {
  79. diagnose = true;
  80. switch (errorLevel)
  81. {
  82. case EmailStatusCode.warning: threshold = defaultThreshold; break;
  83. case EmailStatusCode.error: threshold = EmailStatusCode.valid; break;
  84. default: threshold = errorLevel;
  85. }
  86. }
  87. auto returnStatus = [EmailStatusCode.valid];
  88. auto context = EmailPart.componentLocalPart;
  89. auto contextStack = [context];
  90. auto contextPrior = context;
  91. tstring token = "";
  92. tstring tokenPrior = "";
  93. tstring[EmailPart] parseData = [EmailPart.componentLocalPart : "", EmailPart.componentDomain : ""];
  94. tstring[][EmailPart] atomList = [EmailPart.componentLocalPart : [""], EmailPart.componentDomain : [""]];
  95. auto elementCount = 0;
  96. auto elementLength = 0;
  97. auto hyphenFlag = false;
  98. auto endOrDie = false;
  99. auto crlfCount = int.min; // int.min == not defined
  100. foreach (ref i, e ; email)
  101. {
  102. token = email.get(i, e);
  103. switch (context)
  104. {
  105. case EmailPart.componentLocalPart:
  106. switch (token)
  107. {
  108. case Token.openParenthesis:
  109. if (elementLength == 0)
  110. returnStatus ~= elementCount == 0 ? EmailStatusCode.comment :
  111. EmailStatusCode.deprecatedComment;
  112. else
  113. {
  114. returnStatus ~= EmailStatusCode.comment;
  115. endOrDie = true;
  116. }
  117. contextStack ~= context;
  118. context = EmailPart.contextComment;
  119. break;
  120. case Token.dot:
  121. if (elementLength == 0)
  122. returnStatus ~= elementCount == 0 ? EmailStatusCode.errorDotStart :
  123. EmailStatusCode.errorConsecutiveDots;
  124. else
  125. {
  126. if (endOrDie)
  127. returnStatus ~= EmailStatusCode.deprecatedLocalPart;
  128. }
  129. endOrDie = false;
  130. elementLength = 0;
  131. elementCount++;
  132. parseData[EmailPart.componentLocalPart] ~= token;
  133. if (elementCount >= atomList[EmailPart.componentLocalPart].length)
  134. atomList[EmailPart.componentLocalPart] ~= "";
  135. else
  136. atomList[EmailPart.componentLocalPart][elementCount] = "";
  137. break;
  138. case Token.doubleQuote:
  139. if (elementLength == 0)
  140. {
  141. returnStatus ~= elementCount == 0 ? EmailStatusCode.rfc5321QuotedString :
  142. EmailStatusCode.deprecatedLocalPart;
  143. parseData[EmailPart.componentLocalPart] ~= token;
  144. atomList[EmailPart.componentLocalPart][elementCount] ~= token;
  145. elementLength++;
  146. endOrDie = true;
  147. contextStack ~= context;
  148. context = EmailPart.contextQuotedString;
  149. }
  150. else
  151. returnStatus ~= EmailStatusCode.errorExpectingText;
  152. break;
  153. case Token.cr:
  154. case Token.space:
  155. case Token.tab:
  156. if ((token == Token.cr) && ((++i == email.length) || (email.get(i, e) != Token.lf)))
  157. {
  158. returnStatus ~= EmailStatusCode.errorCrNoLf;
  159. break;
  160. }
  161. if (elementLength == 0)
  162. returnStatus ~= elementCount == 0 ? EmailStatusCode.foldingWhitespace :
  163. EmailStatusCode.deprecatedFoldingWhitespace;
  164. else
  165. endOrDie = true;
  166. contextStack ~= context;
  167. context = EmailPart.contextFoldingWhitespace;
  168. tokenPrior = token;
  169. break;
  170. case Token.at:
  171. enforce(contextStack.length == 1, "Unexpected item on context stack");
  172. if (parseData[EmailPart.componentLocalPart] == "")
  173. returnStatus ~= EmailStatusCode.errorNoLocalPart;
  174. else if (elementLength == 0)
  175. returnStatus ~= EmailStatusCode.errorDotEnd;
  176. else if (parseData[EmailPart.componentLocalPart].length > 64)
  177. returnStatus ~= EmailStatusCode.rfc5322LocalTooLong;
  178. else if (contextPrior == EmailPart.contextComment ||
  179. contextPrior == EmailPart.contextFoldingWhitespace)
  180. returnStatus ~= EmailStatusCode.deprecatedCommentFoldingWhitespaceNearAt;
  181. context = EmailPart.componentDomain;
  182. contextStack = [context];
  183. elementCount = 0;
  184. elementLength = 0;
  185. endOrDie = false;
  186. break;
  187. default:
  188. if (endOrDie)
  189. {
  190. switch (contextPrior)
  191. {
  192. case EmailPart.contextComment:
  193. case EmailPart.contextFoldingWhitespace:
  194. returnStatus ~= EmailStatusCode.errorTextAfterCommentFoldingWhitespace;
  195. break;
  196. case EmailPart.contextQuotedString:
  197. returnStatus ~= EmailStatusCode.errorTextAfterQuotedString;
  198. break;
  199. default:
  200. throw new Exception("More text found where none is allowed, but unrecognised prior "
  201. "context: " ~ to!(string)(contextPrior));
  202. }
  203. }
  204. else
  205. {
  206. contextPrior = context;
  207. auto c = token.front;
  208. if (c < '!' || c > '~' || c == '\n' || Token.specials.contains(token))
  209. returnStatus ~= EmailStatusCode.errorExpectingText;
  210. parseData[EmailPart.componentLocalPart] ~= token;
  211. atomList[EmailPart.componentLocalPart][elementCount] ~= token;
  212. elementLength++;
  213. }
  214. }
  215. break;
  216. case EmailPart.componentDomain:
  217. switch (token)
  218. {
  219. case Token.openParenthesis:
  220. if (elementLength == 0)
  221. returnStatus ~= elementCount == 0 ? EmailStatusCode.deprecatedCommentFoldingWhitespaceNearAt
  222. : EmailStatusCode.deprecatedComment;
  223. else
  224. {
  225. returnStatus ~= EmailStatusCode.comment;
  226. endOrDie = true;
  227. }
  228. contextStack ~= context;
  229. context = EmailPart.contextComment;
  230. break;
  231. case Token.dot:
  232. if (elementLength == 0)
  233. returnStatus ~= elementCount == 0 ? EmailStatusCode.errorDotStart :
  234. EmailStatusCode.errorConsecutiveDots;
  235. else if (hyphenFlag)
  236. returnStatus ~= EmailStatusCode.errorDomainHyphenEnd;
  237. else
  238. {
  239. if (elementLength > 63)
  240. returnStatus ~= EmailStatusCode.rfc5322LabelTooLong;
  241. }
  242. endOrDie = false;
  243. elementLength = 0,
  244. elementCount++;
  245. //atomList[EmailPart.componentDomain][elementCount] = "";
  246. atomList[EmailPart.componentDomain] ~= "";
  247. parseData[EmailPart.componentDomain] ~= token;
  248. break;
  249. case Token.openBracket:
  250. if (parseData[EmailPart.componentDomain] == "")
  251. {
  252. endOrDie = true;
  253. elementLength++;
  254. contextStack ~= context;
  255. context = EmailPart.componentLiteral;
  256. parseData[EmailPart.componentDomain] ~= token;
  257. atomList[EmailPart.componentDomain][elementCount] ~= token;
  258. parseData[EmailPart.componentLiteral] = "";
  259. }
  260. else
  261. returnStatus ~= EmailStatusCode.errorExpectingText;
  262. break;
  263. case Token.cr:
  264. case Token.space:
  265. case Token.tab:
  266. if (token == Token.cr && (++i == email.length || email.get(i, e) != Token.lf))
  267. {
  268. returnStatus ~= EmailStatusCode.errorCrNoLf;
  269. break;
  270. }
  271. if (elementLength == 0)
  272. returnStatus ~= elementCount == 0 ? EmailStatusCode.deprecatedCommentFoldingWhitespaceNearAt
  273. : EmailStatusCode.deprecatedFoldingWhitespace;
  274. else
  275. {
  276. returnStatus ~= EmailStatusCode.foldingWhitespace;
  277. endOrDie = true;
  278. }
  279. contextStack ~= context;
  280. context = EmailPart.contextFoldingWhitespace;
  281. tokenPrior = token;
  282. break;
  283. default:
  284. if (endOrDie)
  285. {
  286. switch (contextPrior)
  287. {
  288. case EmailPart.contextComment:
  289. case EmailPart.contextFoldingWhitespace:
  290. returnStatus ~= EmailStatusCode.errorTextAfterCommentFoldingWhitespace;
  291. break;
  292. case EmailPart.componentLiteral:
  293. returnStatus ~= EmailStatusCode.errorTextAfterDomainLiteral;
  294. break;
  295. default:
  296. throw new Exception("More text found where none is allowed, but unrecognised prior "
  297. "context: " ~ to!(string)(contextPrior));
  298. }
  299. }
  300. auto c = token.front;
  301. hyphenFlag = false;
  302. if (c < '!' || c > '~' || Token.specials.contains(token))
  303. returnStatus ~= EmailStatusCode.errorExpectingText;
  304. else if (token == Token.hyphen)
  305. {
  306. if (elementLength == 0)
  307. returnStatus ~= EmailStatusCode.errorDomainHyphenStart;
  308. hyphenFlag = true;
  309. }
  310. else if (!((c > '/' && c < ':') || (c > '@' && c < '[') || (c > '`' && c < '{')))
  311. returnStatus ~= EmailStatusCode.rfc5322Domain;
  312. parseData[EmailPart.componentDomain] ~= token;
  313. atomList[EmailPart.componentDomain][elementCount] ~= token;
  314. elementLength++;
  315. }
  316. break;
  317. case EmailPart.componentLiteral:
  318. switch (token)
  319. {
  320. case Token.closeBracket:
  321. if (returnStatus.max() < EmailStatusCode.deprecated_)
  322. {
  323. auto maxGroups = 8;
  324. size_t index = -1;
  325. auto addressLiteral = parseData[EmailPart.componentLiteral];
  326. enum regexStr = `\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}`~
  327. `(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$`;
  328. auto matchesIp = array(addressLiteral.match(regex!tstring(regexStr)).captures);
  329. if (!matchesIp.empty)
  330. {
  331. index = addressLiteral.lastIndexOf(matchesIp.front);
  332. if (index != 0)
  333. addressLiteral = addressLiteral.substr(0, index) ~ "0:0";
  334. }
  335. if (index == 0)
  336. returnStatus ~= EmailStatusCode.rfc5321AddressLiteral;
  337. else if (addressLiteral.compareFirstN(Token.ipV6Tag, 5, true))
  338. returnStatus ~= EmailStatusCode.rfc5322DomainLiteral;
  339. else
  340. {
  341. auto ipV6 = addressLiteral.substr(5);
  342. matchesIp = ipV6.split(Token.colon);
  343. auto groupCount = matchesIp.length;
  344. index = ipV6.indexOf(Token.doubleColon);
  345. if (index == -1)
  346. {
  347. if (groupCount != maxGroups)
  348. returnStatus ~= EmailStatusCode.rfc5322IpV6GroupCount;
  349. }
  350. else
  351. {
  352. if (index != ipV6.lastIndexOf(Token.doubleColon))
  353. returnStatus ~= EmailStatusCode.rfc5322IpV6TooManyDoubleColons;
  354. else
  355. {
  356. if (index == 0 || index == (ipV6.length - 2))
  357. maxGroups++;
  358. if (groupCount > maxGroups)
  359. returnStatus ~= EmailStatusCode.rfc5322IpV6MaxGroups;
  360. else if (groupCount == maxGroups)
  361. returnStatus ~= EmailStatusCode.rfc5321IpV6Deprecated;
  362. }
  363. }
  364. if (ipV6.substr(0, 1) == Token.colon && ipV6.substr(1, 1) != Token.colon)
  365. returnStatus ~= EmailStatusCode.rfc5322IpV6ColonStart;
  366. else if (ipV6.substr(-1) == Token.colon && ipV6.substr(-2, -1) != Token.colon)
  367. returnStatus ~= EmailStatusCode.rfc5322IpV6ColonEnd;
  368. else if (!matchesIp.grep(regex!(tstring)(`^[0-9A-Fa-f]{0,4}$`), true).empty)
  369. returnStatus ~= EmailStatusCode.rfc5322IpV6BadChar;
  370. else
  371. returnStatus ~= EmailStatusCode.rfc5321AddressLiteral;
  372. }
  373. }
  374. else
  375. returnStatus ~= EmailStatusCode.rfc5322DomainLiteral;
  376. parseData[EmailPart.componentDomain] ~= token;
  377. atomList[EmailPart.componentDomain][elementCount] ~= token;
  378. elementLength++;
  379. contextPrior = context;
  380. context = contextStack.pop();
  381. break;
  382. case Token.backslash:
  383. returnStatus ~= EmailStatusCode.rfc5322DomainLiteralObsoleteText;
  384. contextStack ~= context;
  385. context = EmailPart.contextQuotedPair;
  386. break;
  387. case Token.cr:
  388. case Token.space:
  389. case Token.tab:
  390. if (token == Token.cr && (++i == email.length || email.get(i, e) != Token.lf))
  391. {
  392. returnStatus ~= EmailStatusCode.errorCrNoLf;
  393. break;
  394. }
  395. returnStatus ~= EmailStatusCode.foldingWhitespace;
  396. contextStack ~= context;
  397. context = EmailPart.contextFoldingWhitespace;
  398. tokenPrior = token;
  399. break;
  400. default:
  401. auto c = token.front;
  402. if (c > AsciiToken.delete_ || c == '\0' || token == Token.openBracket)
  403. {
  404. returnStatus ~= EmailStatusCode.errorExpectingDomainText;
  405. break;
  406. }
  407. else if (c < '!' || c == AsciiToken.delete_ )
  408. returnStatus ~= EmailStatusCode.rfc5322DomainLiteralObsoleteText;
  409. parseData[EmailPart.componentLiteral] ~= token;
  410. parseData[EmailPart.componentDomain] ~= token;
  411. atomList[EmailPart.componentDomain][elementCount] ~= token;
  412. elementLength++;
  413. }
  414. break;
  415. case EmailPart.contextQuotedString:
  416. switch (token)
  417. {
  418. case Token.backslash:
  419. contextStack ~= context;
  420. context = EmailPart.contextQuotedPair;
  421. break;
  422. case Token.cr:
  423. case Token.tab:
  424. if (token == Token.cr && (++i == email.length || email.get(i, e) != Token.lf))
  425. {
  426. returnStatus ~= EmailStatusCode.errorCrNoLf;
  427. break;
  428. }
  429. parseData[EmailPart.componentLocalPart] ~= Token.space;
  430. atomList[EmailPart.componentLocalPart][elementCount] ~= Token.space;
  431. elementLength++;
  432. returnStatus ~= EmailStatusCode.foldingWhitespace;
  433. contextStack ~= context;
  434. context = EmailPart.contextFoldingWhitespace;
  435. tokenPrior = token;
  436. break;
  437. case Token.doubleQuote:
  438. parseData[EmailPart.componentLocalPart] ~= token;
  439. atomList[EmailPart.componentLocalPart][elementCount] ~= token;
  440. elementLength++;
  441. contextPrior = context;
  442. context = contextStack.pop();
  443. break;
  444. default:
  445. auto c = token.front;
  446. if (c > AsciiToken.delete_ || c == '\0' || c == '\n')
  447. returnStatus ~= EmailStatusCode.errorExpectingQuotedText;
  448. else if (c < ' ' || c == AsciiToken.delete_)
  449. returnStatus ~= EmailStatusCode.deprecatedQuotedText;
  450. parseData[EmailPart.componentLocalPart] ~= token;
  451. atomList[EmailPart.componentLocalPart][elementCount] ~= token;
  452. elementLength++;
  453. }
  454. break;
  455. case EmailPart.contextQuotedPair:
  456. auto c = token.front;
  457. if (c > AsciiToken.delete_)
  458. returnStatus ~= EmailStatusCode.errorExpectingQuotedPair;
  459. else if (c < AsciiToken.unitSeparator && c != AsciiToken.horizontalTab || c == AsciiToken.delete_)
  460. returnStatus ~= EmailStatusCode.deprecatedQuotedPair;
  461. contextPrior = context;
  462. context = contextStack.pop();
  463. token = Token.backslash ~ token;
  464. switch (context)
  465. {
  466. case EmailPart.contextComment: break;
  467. case EmailPart.contextQuotedString:
  468. parseData[EmailPart.componentLocalPart] ~= token;
  469. atomList[EmailPart.componentLocalPart][elementCount] ~= token;
  470. elementLength += 2;
  471. break;
  472. case EmailPart.componentLiteral:
  473. parseData[EmailPart.componentDomain] ~= token;
  474. atomList[EmailPart.componentDomain][elementCount] ~= token;
  475. elementLength += 2;
  476. break;
  477. default:
  478. throw new Exception("Quoted pair logic invoked in an invalid context: " ~ to!(string)(context));
  479. }
  480. break;
  481. case EmailPart.contextComment:
  482. switch (token)
  483. {
  484. case Token.openParenthesis:
  485. contextStack ~= context;
  486. context = EmailPart.contextComment;
  487. break;
  488. case Token.closeParenthesis:
  489. contextPrior = context;
  490. context = contextStack.pop();
  491. break;
  492. case Token.backslash:
  493. contextStack ~= context;
  494. context = EmailPart.contextQuotedPair;
  495. break;
  496. case Token.cr:
  497. case Token.space:
  498. case Token.tab:
  499. if (token == Token.cr && (++i == email.length || email.get(i, e) != Token.lf))
  500. {
  501. returnStatus ~= EmailStatusCode.errorCrNoLf;
  502. break;
  503. }
  504. returnStatus ~= EmailStatusCode.foldingWhitespace;
  505. contextStack ~= context;
  506. context = EmailPart.contextFoldingWhitespace;
  507. tokenPrior = token;
  508. break;
  509. default:
  510. auto c = token.front;
  511. if (c > AsciiToken.delete_ || c == '\0' || c == '\n')
  512. {
  513. returnStatus ~= EmailStatusCode.errorExpectingCommentText;
  514. break;
  515. }
  516. else if (c < ' ' || c == AsciiToken.delete_)
  517. returnStatus ~= EmailStatusCode.deprecatedCommentText;
  518. }
  519. break;
  520. case EmailPart.contextFoldingWhitespace:
  521. if (tokenPrior == Token.cr)
  522. {
  523. if (token == Token.cr)
  524. {
  525. returnStatus ~= EmailStatusCode.errorFoldingWhitespaceCrflX2;
  526. break;
  527. }
  528. if (crlfCount != int.min) // int.min == not defined
  529. {
  530. if (++crlfCount > 1)
  531. returnStatus ~= EmailStatusCode.deprecatedFoldingWhitespace;
  532. }
  533. else
  534. crlfCount = 1;
  535. }
  536. switch (token)
  537. {
  538. case Token.cr:
  539. if (++i == email.length || email.get(i, e) != Token.lf)
  540. returnStatus ~= EmailStatusCode.errorCrNoLf;
  541. break;
  542. case Token.space:
  543. case Token.tab:
  544. break;
  545. default:
  546. if (tokenPrior == Token.cr)
  547. {
  548. returnStatus ~= EmailStatusCode.errorFoldingWhitespaceCrLfEnd;
  549. break;
  550. }
  551. crlfCount = int.min; // int.min == not defined
  552. contextPrior = context;
  553. context = contextStack.pop();
  554. i--;
  555. break;
  556. }
  557. tokenPrior = token;
  558. break;
  559. default:
  560. throw new Exception("Unkown context: " ~ to!(string)(context));
  561. }
  562. if (returnStatus.max() > EmailStatusCode.rfc5322)
  563. break;
  564. }
  565. if (returnStatus.max() < EmailStatusCode.rfc5322)
  566. {
  567. if (context == EmailPart.contextQuotedString)
  568. returnStatus ~= EmailStatusCode.errorUnclosedQuotedString;
  569. else if (context == EmailPart.contextQuotedPair)
  570. returnStatus ~= EmailStatusCode.errorBackslashEnd;
  571. else if (context == EmailPart.contextComment)
  572. returnStatus ~= EmailStatusCode.errorUnclosedComment;
  573. else if (context == EmailPart.componentLiteral)
  574. returnStatus ~= EmailStatusCode.errorUnclosedDomainLiteral;
  575. else if (token == Token.cr)
  576. returnStatus ~= EmailStatusCode.errorFoldingWhitespaceCrLfEnd;
  577. else if (parseData[EmailPart.componentDomain] == "")
  578. returnStatus ~= EmailStatusCode.errorNoDomain;
  579. else if (elementLength == 0)
  580. returnStatus ~= EmailStatusCode.errorDotEnd;
  581. else if (hyphenFlag)
  582. returnStatus ~= EmailStatusCode.errorDomainHyphenEnd;
  583. else if (parseData[EmailPart.componentDomain].length > 255)
  584. returnStatus ~= EmailStatusCode.rfc5322DomainTooLong;
  585. else if ((parseData[EmailPart.componentLocalPart] ~ Token.at ~ parseData[EmailPart.componentDomain]).length >
  586. 254)
  587. returnStatus ~= EmailStatusCode.rfc5322TooLong;
  588. else if (elementLength > 63)
  589. returnStatus ~= EmailStatusCode.rfc5322LabelTooLong;
  590. }
  591. auto dnsChecked = false;
  592. if (checkDNS == CheckDns.yes && returnStatus.max() < EmailStatusCode.dnsWarning)
  593. {
  594. assert(false, "DNS check is currently not implemented");
  595. }
  596. if (!dnsChecked && returnStatus.max() < EmailStatusCode.dnsWarning)
  597. {
  598. if (elementCount == 0)
  599. returnStatus ~= EmailStatusCode.rfc5321TopLevelDomain;
  600. if (isNumeric(atomList[EmailPart.componentDomain][elementCount].front))
  601. returnStatus ~= EmailStatusCode.rfc5321TopLevelDomainNumeric;
  602. }
  603. returnStatus = array(uniq(returnStatus));
  604. auto finalStatus = returnStatus.max();
  605. if (returnStatus.length != 1)
  606. returnStatus.popFront();
  607. parseData[EmailPart.status] = to!(tstring)(returnStatus);
  608. if (finalStatus < threshold)
  609. finalStatus = EmailStatusCode.valid;
  610. if (!diagnose)
  611. finalStatus = finalStatus < threshold ? EmailStatusCode.valid : EmailStatusCode.error;
  612. auto valid = finalStatus == EmailStatusCode.valid;
  613. tstring localPart = "";
  614. tstring domainPart = "";
  615. if (auto value = EmailPart.componentLocalPart in parseData)
  616. localPart = *value;
  617. if (auto value = EmailPart.componentDomain in parseData)
  618. domainPart = *value;
  619. return EmailStatus(valid, to!(string)(localPart), to!(string)(domainPart), finalStatus);
  620. }
  621. unittest
  622. {
  623. assert(``.isEmail(CheckDns.no, EmailStatusCode.any).statusCode == EmailStatusCode.errorNoDomain);
  624. assert(`test`.isEmail(CheckDns.no, EmailStatusCode.any).statusCode == EmailStatusCode.errorNoDomain);
  625. assert(`@`.isEmail(CheckDns.no, EmailStatusCode.any).statusCode == EmailStatusCode.errorNoLocalPart);
  626. assert(`test@`.isEmail(CheckDns.no, EmailStatusCode.any).statusCode == EmailStatusCode.errorNoDomain);
  627. // assert(`test@io`.isEmail(CheckDns.no, EmailStatusCode.any).statusCode == EmailStatusCode.valid,
  628. // `io. currently has an MX-record (Feb 2011). Some DNS setups seem to find it, some don't.`
  629. // ` If you don't see the MX for io. then try setting your DNS server to 8.8.8.8 (the Google DNS server)`);
  630. assert(`@io`.isEmail(CheckDns.no, EmailStatusCode.any).statusCode == EmailStatusCode.errorNoLocalPart,
  631. `io. currently has an MX-record (Feb 2011)`);
  632. assert(`@iana.org`.isEmail(CheckDns.no, EmailStatusCode.any).statusCode == EmailStatusCode.errorNoLocalPart);
  633. assert(`test@iana.org`.isEmail(CheckDns.no, EmailStatusCode.any).statusCode == EmailStatusCode.valid);
  634. assert(`test@nominet.org.uk`.isEmail(CheckDns.no, EmailStatusCode.any).statusCode == EmailStatusCode.valid);
  635. assert(`test@about.museum`.isEmail(CheckDns.no, EmailStatusCode.any).statusCode == EmailStatusCode.valid);
  636. assert(`a@iana.org`.isEmail(CheckDns.no, EmailStatusCode.any).statusCode == EmailStatusCode.valid);
  637. //assert(`test@e.com`.isEmail(CheckDns.no, EmailStatusCode.any).statusCode == EmailStatusCode.dnsWarningNoRecord);
  638. // DNS check is currently not implemented
  639. //assert(`test@iana.a`.isEmail(CheckDns.no, EmailStatusCode.any).statusCode == EmailStatusCode.dnsWarningNoRecord);
  640. // DNS check is currently not implemented
  641. assert(`test.test@iana.org`.isEmail(CheckDns.no, EmailStatusCode.any).statusCode == EmailStatusCode.valid);
  642. assert(`.test@iana.org`.isEmail(CheckDns.no, EmailStatusCode.any).statusCode == EmailStatusCode.errorDotStart);
  643. assert(`test.@iana.org`.isEmail(CheckDns.no, EmailStatusCode.any).statusCode == EmailStatusCode.errorDotEnd);
  644. assert(`test..iana.org`.isEmail(CheckDns.no, EmailStatusCode.any).statusCode ==
  645. EmailStatusCode.errorConsecutiveDots);
  646. assert(`test_exa-mple.com`.isEmail(CheckDns.no, EmailStatusCode.any).statusCode == EmailStatusCode.errorNoDomain);
  647. assert("!#$%&`*+/=?^`{|}~@iana.org".isEmail(CheckDns.no, EmailStatusCode.any).statusCode == EmailStatusCode.valid);
  648. assert(`test\@test@iana.org`.isEmail(CheckDns.no, EmailStatusCode.any).statusCode ==
  649. EmailStatusCode.errorExpectingText);
  650. assert(`123@iana.org`.isEmail(CheckDns.no, EmailStatusCode.any).statusCode == EmailStatusCode.valid);
  651. assert(`test@123.com`.isEmail(CheckDns.no, EmailStatusCode.any).statusCode == EmailStatusCode.valid);
  652. assert(`test@iana.123`.isEmail(CheckDns.no, EmailStatusCode.any).statusCode ==
  653. EmailStatusCode.rfc5321TopLevelDomainNumeric);
  654. assert(`test@255.255.255.255`.isEmail(CheckDns.no, EmailStatusCode.any).statusCode ==
  655. EmailStatusCode.rfc5321TopLevelDomainNumeric);
  656. assert(`abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghiklm@iana.org`.isEmail(CheckDns.no,
  657. EmailStatusCode.any).statusCode == EmailStatusCode.valid);
  658. assert(`abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghiklmn@iana.org`.isEmail(CheckDns.no,
  659. EmailStatusCode.any).statusCode == EmailStatusCode.rfc5322LocalTooLong);
  660. // assert(`test@abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghikl.com`.isEmail(CheckDns.no,
  661. // EmailStatusCode.any).statusCode == EmailStatusCode.dnsWarningNoRecord);
  662. // DNS check is currently not implemented
  663. assert(`test@abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghiklm.com`.isEmail(CheckDns.no,
  664. EmailStatusCode.any).statusCode == EmailStatusCode.rfc5322LabelTooLong);
  665. assert(`test@mason-dixon.com`.isEmail(CheckDns.no, EmailStatusCode.any).statusCode == EmailStatusCode.valid);
  666. assert(`test@-iana.org`.isEmail(CheckDns.no, EmailStatusCode.any).statusCode ==
  667. EmailStatusCode.errorDomainHyphenStart);
  668. assert(`test@iana-.com`.isEmail(CheckDns.no, EmailStatusCode.any).statusCode ==
  669. EmailStatusCode.errorDomainHyphenEnd);
  670. assert(`test@g--a.com`.isEmail(CheckDns.no, EmailStatusCode.any).statusCode == EmailStatusCode.valid);
  671. //assert(`test@iana.co-uk`.isEmail(CheckDns.no, EmailStatusCode.any).statusCode ==
  672. //EmailStatusCode.dnsWarningNoRecord); // DNS check is currently not implemented
  673. assert(`test@.iana.org`.isEmail(CheckDns.no, EmailStatusCode.any).statusCode == EmailStatusCode.errorDotStart);
  674. assert(`test@iana.org.`.isEmail(CheckDns.no, EmailStatusCode.any).statusCode == EmailStatusCode.errorDotEnd);
  675. assert(`test@iana..com`.isEmail(CheckDns.no, EmailStatusCode.any).statusCode ==
  676. EmailStatusCode.errorConsecutiveDots);
  677. //assert(`a@a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z`
  678. // `.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z`
  679. // `.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v`.isEmail(CheckDns.no, EmailStatusCode.any).statusCode ==
  680. // EmailStatusCode.dnsWarningNoRecord); // DNS check is currently not implemented
  681. // assert(`abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghiklm@abcdefghijklmnopqrstuvwxyz`
  682. // `abcdefghijklmnopqrstuvwxyzabcdefghikl.abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghikl.`
  683. // `abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghi`.isEmail(CheckDns.no,
  684. // EmailStatusCode.any).statusCode == EmailStatusCode.dnsWarningNoRecord);
  685. // DNS check is currently not implemented
  686. assert(`abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghiklm@abcdefghijklmnopqrstuvwxyz`
  687. `abcdefghijklmnopqrstuvwxyzabcdefghikl.abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghikl.`
  688. `abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghij`.isEmail(CheckDns.no,
  689. EmailStatusCode.any).statusCode == EmailStatusCode.rfc5322TooLong);
  690. assert(`a@abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghikl.abcdefghijklmnopqrstuvwxyz`
  691. `abcdefghijklmnopqrstuvwxyzabcdefghikl.abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghikl.`
  692. `abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefg.hij`.isEmail(CheckDns.no,
  693. EmailStatusCode.any).statusCode == EmailStatusCode.rfc5322TooLong);
  694. assert(`a@abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghikl.abcdefghijklmnopqrstuvwxyz`
  695. `abcdefghijklmnopqrstuvwxyzabcdefghikl.abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghikl.`
  696. `abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefg.hijk`.isEmail(CheckDns.no,
  697. EmailStatusCode.any).statusCode == EmailStatusCode.rfc5322DomainTooLong);
  698. assert(`"test"@iana.org`.isEmail(CheckDns.no, EmailStatusCode.any).statusCode ==
  699. EmailStatusCode.rfc5321QuotedString);
  700. assert(`""@iana.org`.isEmail(CheckDns.no, EmailStatusCode.any).statusCode == EmailStatusCode.rfc5321QuotedString);
  701. assert(`"""@iana.org`.isEmail(CheckDns.no, EmailStatusCode.any).statusCode == EmailStatusCode.errorExpectingText);
  702. assert(`"\a"@iana.org`.isEmail(CheckDns.no, EmailStatusCode.any).statusCode == EmailStatusCode.rfc5321QuotedString);
  703. assert(`"\""@iana.org`.isEmail(CheckDns.no, EmailStatusCode.any).statusCode == EmailStatusCode.rfc5321QuotedString);
  704. assert(`"\"@iana.org`.isEmail(CheckDns.no, EmailStatusCode.any).statusCode ==
  705. EmailStatusCode.errorUnclosedQuotedString);
  706. assert(`"\\"@iana.org`.isEmail(CheckDns.no, EmailStatusCode.any).statusCode == EmailStatusCode.rfc5321QuotedString);
  707. assert(`test"@iana.org`.isEmail(CheckDns.no, EmailStatusCode.any).statusCode == EmailStatusCode.errorExpectingText);
  708. assert(`"test@iana.org`.isEmail(CheckDns.no, EmailStatusCode.any).statusCode ==
  709. EmailStatusCode.errorUnclosedQuotedString);
  710. assert(`"test"test@iana.org`.isEmail(CheckDns.no, EmailStatusCode.any).statusCode ==
  711. EmailStatusCode.errorTextAfterQuotedString);
  712. assert(`test"text"@iana.org`.isEmail(CheckDns.no, EmailStatusCode.any).statusCode ==
  713. EmailStatusCode.errorExpectingText);
  714. assert(`"test""test"@iana.org`.isEmail(CheckDns.no, EmailStatusCode.any).statusCode ==
  715. EmailStatusCode.errorExpectingText);
  716. assert(`"test"."test"@iana.org`.isEmail(CheckDns.no, EmailStatusCode.any).statusCode ==
  717. EmailStatusCode.deprecatedLocalPart);
  718. assert(`"test\ test"@iana.org`.isEmail(CheckDns.no, EmailStatusCode.any).statusCode ==
  719. EmailStatusCode.rfc5321QuotedString);
  720. assert(`"test".test@iana.org`.isEmail(CheckDns.no, EmailStatusCode.any).statusCode ==
  721. EmailStatusCode.deprecatedLocalPart);
  722. assert("\"test\u0000\"@iana.org".isEmail(CheckDns.no, EmailStatusCode.any).statusCode ==
  723. EmailStatusCode.errorExpectingQuotedText);
  724. assert("\"test\\\u0000\"@iana.org".isEmail(CheckDns.no, EmailStatusCode.any).statusCode ==
  725. EmailStatusCode.deprecatedQuotedPair);
  726. assert(`"abcdefghijklmnopqrstuvwxyz abcdefghijklmnopqrstuvwxyz abcdefghj"@iana.org`.isEmail(CheckDns.no,
  727. EmailStatusCode.any).statusCode == EmailStatusCode.rfc5322LocalTooLong,
  728. `Quotes are still part of the length restriction`);
  729. assert(`"abcdefghijklmnopqrstuvwxyz abcdefghijklmnopqrstuvwxyz abcdefg\h"@iana.org`.isEmail(CheckDns.no,
  730. EmailStatusCode.any).statusCode == EmailStatusCode.rfc5322LocalTooLong,
  731. `Quoted pair is still part of the length restriction`);
  732. assert(`test@[255.255.255.255]`.isEmail(CheckDns.no, EmailStatusCode.any).statusCode ==
  733. EmailStatusCode.rfc5321AddressLiteral);
  734. assert(`test@a[255.255.255.255]`.isEmail(CheckDns.no, EmailStatusCode.any).statusCode ==
  735. EmailStatusCode.errorExpectingText);
  736. assert(`test@[255.255.255]`.isEmail(CheckDns.no, EmailStatusCode.any).statusCode ==
  737. EmailStatusCode.rfc5322DomainLiteral);
  738. assert(`test@[255.255.255.255.255]`.isEmail(CheckDns.no, EmailStatusCode.any).statusCode ==
  739. EmailStatusCode.rfc5322DomainLiteral);
  740. assert(`test@[255.255.255.256]`.isEmail(CheckDns.no, EmailStatusCode.any).statusCode ==
  741. EmailStatusCode.rfc5322DomainLiteral);
  742. assert(`test@[1111:2222:3333:4444:5555:6666:7777:8888]`.isEmail(CheckDns.no, EmailStatusCode.any).statusCode ==
  743. EmailStatusCode.rfc5322DomainLiteral);
  744. assert(`test@[IPv6:1111:2222:3333:4444:5555:6666:7777]`.isEmail(CheckDns.no, EmailStatusCode.any).statusCode ==
  745. EmailStatusCode.rfc5322IpV6GroupCount);
  746. assert(`test@[IPv6:1111:2222:3333:4444:5555:6666:7777:8888]`.isEmail(CheckDns.no, EmailStatusCode.any).statusCode
  747. == EmailStatusCode.rfc5321AddressLiteral);
  748. assert(`test@[IPv6:1111:2222:3333:4444:5555:6666:7777:8888:9999]`.isEmail(CheckDns.no,
  749. EmailStatusCode.any).statusCode == EmailStatusCode.rfc5322IpV6GroupCount);
  750. assert(`test@[IPv6:1111:2222:3333:4444:5555:6666:7777:888G]`.isEmail(CheckDns.no,
  751. EmailStatusCode.any).statusCode == EmailStatusCode.rfc5322IpV6BadChar);
  752. assert(`test@[IPv6:1111:2222:3333:4444:5555:6666::8888]`.isEmail(CheckDns.no,
  753. EmailStatusCode.any).statusCode == EmailStatusCode.rfc5321IpV6Deprecated);
  754. assert(`test@[IPv6:1111:2222:3333:4444:5555::8888]`.isEmail(CheckDns.no, EmailStatusCode.any).statusCode ==
  755. EmailStatusCode.rfc5321AddressLiteral);
  756. assert(`test@[IPv6:1111:2222:3333:4444:5555:6666::7777:8888]`.isEmail(CheckDns.no,
  757. EmailStatusCode.any).statusCode == EmailStatusCode.rfc5322IpV6MaxGroups);
  758. assert(`test@[IPv6::3333:4444:5555:6666:7777:8888]`.isEmail(CheckDns.no, EmailStatusCode.any).statusCode ==
  759. EmailStatusCode.rfc5322IpV6ColonStart);
  760. assert(`test@[IPv6:::3333:4444:5555:6666:7777:8888]`.isEmail(CheckDns.no, EmailStatusCode.any).statusCode ==
  761. EmailStatusCode.rfc5321AddressLiteral);
  762. assert(`test@[IPv6:1111::4444:5555::8888]`.isEmail(CheckDns.no, EmailStatusCode.any).statusCode ==
  763. EmailStatusCode.rfc5322IpV6TooManyDoubleColons);
  764. assert(`test@[IPv6:::]`.isEmail(CheckDns.no, EmailStatusCode.any).statusCode ==
  765. EmailStatusCode.rfc5321AddressLiteral);
  766. assert(`test@[IPv6:1111:2222:3333:4444:5555:255.255.255.255]`.isEmail(CheckDns.no,
  767. EmailStatusCode.any).statusCode == EmailStatusCode.rfc5322IpV6GroupCount);
  768. assert(`test@[IPv6:1111:2222:3333:4444:5555:6666:255.255.255.255]`.isEmail(CheckDns.no,
  769. EmailStatusCode.any).statusCode == EmailStatusCode.rfc5321AddressLiteral);
  770. assert(`test@[IPv6:1111:2222:3333:4444:5555:6666:7777:255.255.255.255]`.isEmail(CheckDns.no,
  771. EmailStatusCode.any).statusCode == EmailStatusCode.rfc5322IpV6GroupCount);
  772. assert(`test@[IPv6:1111:2222:3333:4444::255.255.255.255]`.isEmail(CheckDns.no,
  773. EmailStatusCode.any).statusCode == EmailStatusCode.rfc5321AddressLiteral);
  774. assert(`test@[IPv6:1111:2222:3333:4444:5555:6666::255.255.255.255]`.isEmail(CheckDns.no,
  775. EmailStatusCode.any).statusCode == EmailStatusCode.rfc5322IpV6MaxGroups);
  776. assert(`test@[IPv6:1111:2222:3333:4444:::255.255.255.255]`.isEmail(CheckDns.no, EmailStatusCode.any).statusCode
  777. == EmailStatusCode.rfc5322IpV6TooManyDoubleColons);
  778. assert(`test@[IPv6::255.255.255.255]`.isEmail(CheckDns.no, EmailStatusCode.any).statusCode ==
  779. EmailStatusCode.rfc5322IpV6ColonStart);
  780. assert(` test @iana.org`.isEmail(CheckDns.no, EmailStatusCode.any).statusCode ==
  781. EmailStatusCode.deprecatedCommentFoldingWhitespaceNearAt);
  782. assert(`test@ iana .com`.isEmail(CheckDns.no, EmailStatusCode.any).statusCode ==
  783. EmailStatusCode.deprecatedCommentFoldingWhitespaceNearAt);
  784. assert(`test . test@iana.org`.isEmail(CheckDns.no, EmailStatusCode.any).statusCode ==
  785. EmailStatusCode.deprecatedFoldingWhitespace);
  786. assert("\u000D\u000A test@iana.org".isEmail(CheckDns.no, EmailStatusCode.any).statusCode ==
  787. EmailStatusCode.foldingWhitespace, `Folding whitespace`);
  788. assert("\u000D\u000A \u000D\u000A test@iana.org".isEmail(CheckDns.no, EmailStatusCode.any).statusCode ==
  789. EmailStatusCode.deprecatedFoldingWhitespace, `FWS with one line composed entirely of WSP`
  790. ` -- only allowed as obsolete FWS (someone might allow only non-obsolete FWS)`);
  791. assert(`(comment)test@iana.org`.isEmail(CheckDns.no, EmailStatusCode.any).statusCode == EmailStatusCode.comment);
  792. assert(`((comment)test@iana.org`.isEmail(CheckDns.no, EmailStatusCode.any).statusCode ==
  793. EmailStatusCode.errorUnclosedComment);
  794. assert(`(comment(comment))test@iana.org`.isEmail(CheckDns.no, EmailStatusCode.any).statusCode ==
  795. EmailStatusCode.comment);
  796. assert(`test@(comment)iana.org`.isEmail(CheckDns.no, EmailStatusCode.any).statusCode ==
  797. EmailStatusCode.deprecatedCommentFoldingWhitespaceNearAt);
  798. assert(`test(comment)test@iana.org`.isEmail(CheckDns.no, EmailStatusCode.any).statusCode ==
  799. EmailStatusCode.errorTextAfterCommentFoldingWhitespace);
  800. assert(`test@(comment)[255.255.255.255]`.isEmail(CheckDns.no, EmailStatusCode.any).statusCode ==
  801. EmailStatusCode.deprecatedCommentFoldingWhitespaceNearAt);
  802. assert(`(comment)abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghiklm@iana.org`.isEmail(CheckDns.no,
  803. EmailStatusCode.any).statusCode == EmailStatusCode.comment);
  804. assert(`test@(comment)abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghikl.com`.isEmail(CheckDns.no,
  805. EmailStatusCode.any).statusCode == EmailStatusCode.deprecatedCommentFoldingWhitespaceNearAt);
  806. assert(`(comment)test@abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghik.abcdefghijklmnopqrstuvwxyz`
  807. `abcdefghijklmnopqrstuvwxyzabcdefghik.abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijk.`
  808. `abcdefghijklmnopqrstuvwxyzabcdefghijk.abcdefghijklmnopqrstu`.isEmail(CheckDns.no,
  809. EmailStatusCode.any).statusCode == EmailStatusCode.comment);
  810. assert("test@iana.org\u000A".isEmail(CheckDns.no, EmailStatusCode.any).statusCode ==
  811. EmailStatusCode.errorExpectingText);
  812. assert(`test@xn--hxajbheg2az3al.xn--jxalpdlp`.isEmail(CheckDns.no, EmailStatusCode.any).statusCode ==
  813. EmailStatusCode.valid, `A valid IDN from ICANN's <a href="http://idn.icann.org/#The_example.test_names">`
  814. `IDN TLD evaluation gateway</a>`);
  815. assert(`xn--test@iana.org`.isEmail(CheckDns.no, EmailStatusCode.any).statusCode == EmailStatusCode.valid,
  816. `RFC 3490: "unless the email standards are revised to invite the use of IDNA for local parts, a domain label`
  817. ` that holds the local part of an email address SHOULD NOT begin with the ACE prefix, and even if it does,`
  818. ` it is to be interpreted literally as a local part that happens to begin with the ACE prefix"`);
  819. assert(`test@iana.org-`.isEmail(CheckDns.no, EmailStatusCode.any).statusCode ==
  820. EmailStatusCode.errorDomainHyphenEnd);
  821. assert(`"test@iana.org`.isEmail(CheckDns.no, EmailStatusCode.any).statusCode ==
  822. EmailStatusCode.errorUnclosedQuotedString);
  823. assert(`(test@iana.org`.isEmail(CheckDns.no, EmailStatusCode.any).statusCode ==
  824. EmailStatusCode.errorUnclosedComment);
  825. assert(`test@(iana.org`.isEmail(CheckDns.no, EmailStatusCode.any).statusCode ==
  826. EmailStatusCode.errorUnclosedComment);
  827. assert(`test@[1.2.3.4`.isEmail(CheckDns.no, EmailStatusCode.any).statusCode ==
  828. EmailStatusCode.errorUnclosedDomainLiteral);
  829. assert(`"test\"@iana.org`.isEmail(CheckDns.no, EmailStatusCode.any).statusCode ==
  830. EmailStatusCode.errorUnclosedQuotedString);
  831. assert(`(comment\)test@iana.org`.isEmail(CheckDns.no, EmailStatusCode.any).statusCode ==
  832. EmailStatusCode.errorUnclosedComment);
  833. assert(`test@iana.org(comment\)`.isEmail(CheckDns.no, EmailStatusCode.any).statusCode ==
  834. EmailStatusCode.errorUnclosedComment);
  835. assert(`test@iana.org(comment\`.isEmail(CheckDns.no, EmailStatusCode.any).statusCode ==
  836. EmailStatusCode.errorBackslashEnd);
  837. assert(`test@[RFC-5322-domain-literal]`.isEmail(CheckDns.no, EmailStatusCode.any).statusCode ==
  838. EmailStatusCode.rfc5322DomainLiteral);
  839. assert(`test@[RFC-5322]-domain-literal]`.isEmail(CheckDns.no, EmailStatusCode.any).statusCode ==
  840. EmailStatusCode.errorTextAfterDomainLiteral);
  841. assert(`test@[RFC-5322-[domain-literal]`.isEmail(CheckDns.no, EmailStatusCode.any).statusCode ==
  842. EmailStatusCode.errorExpectingDomainText);
  843. assert("test@[RFC-5322-\\\u0007-domain-literal]".isEmail(CheckDns.no, EmailStatusCode.any).statusCode ==
  844. EmailStatusCode.rfc5322DomainLiteralObsoleteText, `obs-dtext <strong>and</strong> obs-qp`);
  845. assert("test@[RFC-5322-\\\u0009-domain-literal]".isEmail(CheckDns.no, EmailStatusCode.any).statusCode ==
  846. EmailStatusCode.rfc5322DomainLiteralObsoleteText);
  847. assert(`test@[RFC-5322-\]-domain-literal]`.isEmail(CheckDns.no, EmailStatusCode.any).statusCode ==
  848. EmailStatusCode.rfc5322DomainLiteralObsoleteText);
  849. assert(`test@[RFC-5322-domain-literal\]`.isEmail(CheckDns.no, EmailStatusCode.any).statusCode ==
  850. EmailStatusCode.errorUnclosedDomainLiteral);
  851. assert(`test@[RFC-5322-domain-literal\`.isEmail(CheckDns.no, EmailStatusCode.any).statusCode ==
  852. EmailStatusCode.errorBackslashEnd);
  853. assert(`test@[RFC 5322 domain literal]`.isEmail(CheckDns.no, EmailStatusCode.any).statusCode ==
  854. EmailStatusCode.rfc5322DomainLiteral, `Spaces are FWS in a domain literal`);
  855. assert(`test@[RFC-5322-domain-literal] (comment)`.isEmail(CheckDns.no, EmailStatusCode.any).statusCode ==
  856. EmailStatusCode.rfc5322DomainLiteral);
  857. assert("\u007F@iana.org".isEmail(CheckDns.no, EmailStatusCode.any).statusCode == EmailStatusCode.errorExpectingText);
  858. assert("test@\u007F.org".isEmail(CheckDns.no, EmailStatusCode.any).statusCode == EmailStatusCode.errorExpectingText);
  859. assert("\"\u007F\"@iana.org".isEmail(CheckDns.no, EmailStatusCode.any).statusCode == EmailStatusCode.deprecatedQuotedText);
  860. assert("\"\\\u007F\"@iana.org".isEmail(CheckDns.no, EmailStatusCode.any).statusCode ==
  861. EmailStatusCode.deprecatedQuotedPair);
  862. assert("(\u007F)test@iana.org".isEmail(CheckDns.no, EmailStatusCode.any).statusCode ==
  863. EmailStatusCode.deprecatedCommentText);
  864. assert("test@iana.org\u000D".isEmail(CheckDns.no, EmailStatusCode.any).statusCode == EmailStatusCode.errorCrNoLf,
  865. `No LF after the CR`);
  866. assert("\u000Dtest@iana.org".isEmail(CheckDns.no, EmailStatusCode.any).statusCode == EmailStatusCode.errorCrNoLf,
  867. `No LF after the CR`);
  868. assert("\"\u000Dtest\"@iana.org".isEmail(CheckDns.no, EmailStatusCode.any).statusCode == EmailStatusCode.errorCrNoLf
  869. ,`No LF after the CR`);
  870. assert("(\u000D)test@iana.org".isEmail(CheckDns.no, EmailStatusCode.any).statusCode == EmailStatusCode.errorCrNoLf,
  871. `No LF after the CR`);
  872. assert("(\u000D".isEmail(CheckDns.no, EmailStatusCode.any).statusCode == EmailStatusCode.errorCrNoLf,
  873. `No LF after the CR`);
  874. assert("test@iana.org(\u000D)".isEmail(CheckDns.no, EmailStatusCode.any).statusCode == EmailStatusCode.errorCrNoLf,
  875. `No LF after the CR`);
  876. assert("\u000Atest@iana.org".isEmail(CheckDns.no, EmailStatusCode.any).statusCode ==
  877. EmailStatusCode.errorExpectingText);
  878. assert("\"\u000A\"@iana.org".isEmail(CheckDns.no, EmailStatusCode.any).statusCode ==
  879. EmailStatusCode.errorExpectingQuotedText);
  880. assert("\"\\\u000A\"@iana.org".isEmail(CheckDns.no, EmailStatusCode.any).statusCode ==
  881. EmailStatusCode.deprecatedQuotedPair);
  882. assert("(\u000A)test@iana.org".isEmail(CheckDns.no, EmailStatusCode.any).statusCode ==
  883. EmailStatusCode.errorExpectingCommentText);
  884. assert("\u0007@iana.org".isEmail(CheckDns.no, EmailStatusCode.any).statusCode ==
  885. EmailStatusCode.errorExpectingText);
  886. assert("test@\u0007.org".isEmail(CheckDns.no, EmailStatusCode.any).statusCode ==
  887. EmailStatusCode.errorExpectingText);
  888. assert("\"\u0007\"@iana.org".isEmail(CheckDns.no, EmailStatusCode.any).statusCode ==
  889. EmailStatusCode.deprecatedQuotedText);
  890. assert("\"\\\u0007\"@iana.org".isEmail(CheckDns.no, EmailStatusCode.any).statusCode ==
  891. EmailStatusCode.deprecatedQuotedPair);
  892. assert("(\u0007)test@iana.org".isEmail(CheckDns.no, EmailStatusCode.any).statusCode ==
  893. EmailStatusCode.deprecatedCommentText);
  894. assert("\u000D\u000Atest@iana.org".isEmail(CheckDns.no, EmailStatusCode.any).statusCode ==
  895. EmailStatusCode.errorFoldingWhitespaceCrLfEnd, `Not FWS because no actual white space`);
  896. assert("\u000D\u000A \u000D\u000Atest@iana.org".isEmail(CheckDns.no, EmailStatusCode.any).statusCode ==
  897. EmailStatusCode.errorFoldingWhitespaceCrLfEnd, `Not obs-FWS because there must be white space on each "fold"`);
  898. assert(" \u000D\u000Atest@iana.org".isEmail(CheckDns.no, EmailStatusCode.any).statusCode ==
  899. EmailStatusCode.errorFoldingWhitespaceCrLfEnd, `Not FWS because no white space after the fold`);
  900. assert(" \u000D\u000A test@iana.org".isEmail(CheckDns.no, EmailStatusCode.any).statusCode ==
  901. EmailStatusCode.foldingWhitespace, `FWS`);
  902. assert(" \u000D\u000A \u000D\u000Atest@iana.org".isEmail(CheckDns.no, EmailStatusCode.any).statusCode ==
  903. EmailStatusCode.errorFoldingWhitespaceCrLfEnd, `Not FWS because no white space after the second fold`);
  904. assert(" \u000D\u000A\u000D\u000Atest@iana.org".isEmail(CheckDns.no, EmailStatusCode.any).statusCode ==
  905. EmailStatusCode.errorFoldingWhitespaceCrflX2, `Not FWS because no white space after either fold`);
  906. assert(" \u000D\u000A\u000D\u000A test@iana.org".isEmail(CheckDns.no, EmailStatusCode.any).statusCode ==
  907. EmailStatusCode.errorFoldingWhitespaceCrflX2, `Not FWS because no white space after the first fold`);
  908. assert("test@iana.org\u000D\u000A ".isEmail(CheckDns.no, EmailStatusCode.any).statusCode ==
  909. EmailStatusCode.foldingWhitespace, `FWS`);
  910. assert("test@iana.org\u000D\u000A \u000D\u000A ".isEmail(CheckDns.no, EmailStatusCode.any).statusCode ==
  911. EmailStatusCode.deprecatedFoldingWhitespace, `FWS with one line composed entirely of WSP -- `
  912. `only allowed as obsolete FWS (someone might allow only non-obsolete FWS)`);
  913. assert("test@iana.org\u000D\u000A".isEmail(CheckDns.no, EmailStatusCode.any).statusCode ==
  914. EmailStatusCode.errorFoldingWhitespaceCrLfEnd, `Not FWS because no actual white space`);
  915. assert("test@iana.org\u000D\u000A \u000D\u000A".isEmail(CheckDns.no, EmailStatusCode.any).statusCode ==
  916. EmailStatusCode.errorFoldingWhitespaceCrLfEnd, `Not obs-FWS because there must be white space on each "fold"`);
  917. assert("test@iana.org \u000D\u000A".isEmail(CheckDns.no, EmailStatusCode.any).statusCode ==
  918. EmailStatusCode.errorFoldingWhitespaceCrLfEnd, `Not FWS because no white space after the fold`);
  919. assert("test@iana.org \u000D\u000A ".isEmail(CheckDns.no, EmailStatusCode.any).statusCode ==
  920. EmailStatusCode.foldingWhitespace, `FWS`);
  921. assert("test@iana.org \u000D\u000A \u000D\u000A".isEmail(CheckDns.no, EmailStatusCode.any).statusCode ==
  922. EmailStatusCode.errorFoldingWhitespaceCrLfEnd, `Not FWS because no white space after the second fold`);
  923. assert("test@iana.org \u000D\u000A\u000D\u000A".isEmail(CheckDns.no, EmailStatusCode.any).statusCode ==
  924. EmailStatusCode.errorFoldingWhitespaceCrflX2, `Not FWS because no white space after either fold`);
  925. assert("test@iana.org \u000D\u000A\u000D\u000A ".isEmail(CheckDns.no, EmailStatusCode.any).statusCode ==
  926. EmailStatusCode.errorFoldingWhitespaceCrflX2, `Not FWS because no white space after the first fold`);
  927. assert(" test@iana.org".isEmail(CheckDns.no, EmailStatusCode.any).statusCode == EmailStatusCode.foldingWhitespace);
  928. assert(`test@iana.org `.isEmail(CheckDns.no, EmailStatusCode.any).statusCode == EmailStatusCode.foldingWhitespace);
  929. assert(`test@[IPv6:1::2:]`.isEmail(CheckDns.no, EmailStatusCode.any).statusCode ==
  930. EmailStatusCode.rfc5322IpV6ColonEnd);
  931. assert("\"test\\\u00A9\"@iana.org".isEmail(CheckDns.no, EmailStatusCode.any).statusCode ==
  932. EmailStatusCode.errorExpectingQuotedPair);
  933. assert(`test@iana/icann.org`.isEmail(CheckDns.no, EmailStatusCode.any).statusCode == EmailStatusCode.rfc5322Domain);
  934. assert(`test.(comment)test@iana.org`.isEmail(CheckDns.no, EmailStatusCode.any).statusCode ==
  935. EmailStatusCode.deprecatedComment);
  936. assert(`test@org`.isEmail(CheckDns.no, EmailStatusCode.any).statusCode == EmailStatusCode.rfc5321TopLevelDomain);
  937. // assert(`test@test.com`.isEmail(CheckDns.no, EmailStatusCode.any).statusCode ==
  938. //EmailStatusCode.dnsWarningNoMXRecord, `test.com has an A-record but not an MX-record`);
  939. // DNS check is currently not implemented
  940. //
  941. // assert(`test@nic.no`.isEmail(CheckDns.no, EmailStatusCode.any).statusCode == EmailStatusCode.dnsWarningNoRecord,
  942. // `nic.no currently has no MX-records or A-records (Feb 2011). If you are seeing an A-record for nic.io then`
  943. // ` try setting your DNS server to 8.8.8.8 (the Google DNS server) - your DNS server may be faking an A-record`
  944. // ` (OpenDNS does this, for instance).`); // DNS check is currently not implemented
  945. }
  946. /// Enum for indicating if the isEmail function should perform a DNS check or not.
  947. enum CheckDns
  948. {
  949. /// Does not perform DNS checking
  950. no,
  951. /// Performs DNS checking
  952. yes
  953. }
  954. /// This struct represents the status of an email address
  955. struct EmailStatus
  956. {
  957. private
  958. {
  959. bool valid_;
  960. string localPart_;
  961. string domainPart_;
  962. EmailStatusCode statusCode_;
  963. }
  964. ///
  965. alias valid this;
  966. /*
  967. * Params:
  968. * valid = indicates if the email address is valid or not
  969. * localPart = the local part of the email address
  970. * domainPart = the domain part of the email address
  971. * statusCode = the status code
  972. */
  973. private this (bool valid, string localPart, string domainPart, EmailStatusCode statusCode)
  974. {
  975. this.valid_ = valid;
  976. this.localPart_ = localPart;
  977. this.domainPart_ = domainPart;
  978. this.statusCode_ = statusCode;
  979. }
  980. /// Indicates if the email address is valid or not.
  981. @property bool valid ()
  982. {
  983. return valid_;
  984. }
  985. /// The local part of the email address, that is, the part before the @ sign.
  986. @property string localPart ()
  987. {
  988. return localPart_;
  989. }
  990. /// The domain part of the email address, that is, the part after the @ sign.
  991. @property string domainPart ()
  992. {
  993. return domainPart_;
  994. }
  995. /// The email status code
  996. @property EmailStatusCode statusCode ()
  997. {
  998. return statusCode_;
  999. }
  1000. /// Returns a describing string of the status code
  1001. @property string status ()
  1002. {
  1003. return statusCodeDescription(statusCode_);
  1004. }
  1005. /// Returns a textual representation of the email status
  1006. string toString ()
  1007. {
  1008. return format("EmailStatus\n{\n\tvalid: %s\n\tlocalPart: %s\n\tdomainPart: %s\n\tstatusCode: %s\n}", valid,
  1009. localPart, domainPart, statusCode);
  1010. }
  1011. }
  1012. /// Returns a describing string of the given status code
  1013. string statusCodeDescription (EmailStatusCode statusCode)
  1014. {
  1015. final switch (statusCode)
  1016. {
  1017. // Categories
  1018. case EmailStatusCode.validCategory: return "Address is valid";
  1019. case EmailStatusCode.dnsWarning: return "Address is valid but a DNS check was not successful";
  1020. case EmailStatusCode.rfc5321: return "Address is valid for SMTP but has unusual elements";
  1021. case EmailStatusCode.cFoldingWhitespace: return "Address is valid within the message but cannot be used"
  1022. " unmodified for the envelope";
  1023. case EmailStatusCode.deprecated_: return "Address contains deprecated elements but may still be valid in"
  1024. " restricted contexts";
  1025. case EmailStatusCode.rfc5322: return "The address is only valid according to the broad definition of RFC 5322."
  1026. " It is otherwise invalid";
  1027. case EmailStatusCode.any: return "";
  1028. case EmailStatusCode.none: return "";
  1029. case EmailStatusCode.warning: return "";
  1030. case EmailStatusCode.error: return "Address is invalid for any purpose";
  1031. // Diagnoses
  1032. case EmailStatusCode.valid: return "Address is valid";
  1033. // Address is valid but a DNS check was not successful
  1034. case EmailStatusCode.dnsWarningNoMXRecord: return "Could not find an MX record for this domain but an A-record"
  1035. " does exist";
  1036. case EmailStatusCode.dnsWarningNoRecord: return "Could not find an MX record or an A-record for this domain";
  1037. // Address is valid for SMTP but has unusual elements
  1038. case EmailStatusCode.rfc5321TopLevelDomain: return "Address is valid but at a Top Level Domain";
  1039. case EmailStatusCode.rfc5321TopLevelDomainNumeric: return "Address is valid but the Top Level Domain begins"
  1040. " with a number";
  1041. case EmailStatusCode.rfc5321QuotedString: return "Address is valid but contains a quoted string";
  1042. case EmailStatusCode.rfc5321AddressLiteral: return "Address is valid but at a literal address not a domain";
  1043. case EmailStatusCode.rfc5321IpV6Deprecated: return "Address is valid but contains a :: that only elides one"
  1044. " zero group";
  1045. // Address is valid within the message but cannot be used unmodified for the envelope
  1046. case EmailStatusCode.comment: return "Address contains comments";
  1047. case EmailStatusCode.foldingWhitespace: return "Address contains Folding White Space";
  1048. // Address contains deprecated elements but may still be valid in restricted contexts
  1049. case EmailStatusCode.deprecatedLocalPart: return "The local part is in a deprecated form";
  1050. case EmailStatusCode.deprecatedFoldingWhitespace: return "Address contains an obsolete form of"
  1051. " Folding White Space";
  1052. case EmailStatusCode.deprecatedQuotedText: return "A quoted string contains a deprecated character";
  1053. case EmailStatusCode.deprecatedQuotedPair: return "A quoted pair contains a deprecated character";
  1054. case EmailStatusCode.deprecatedComment: return "Address contains a comment in a position that is deprecated";
  1055. case EmailStatusCode.deprecatedCommentText: return "A comment contains a deprecated character";
  1056. case EmailStatusCode.deprecatedCommentFoldingWhitespaceNearAt: return "Address contains a comment or"
  1057. " Folding White Space around the @ sign";
  1058. // The address is only valid according to the broad definition of RFC 5322
  1059. case EmailStatusCode.rfc5322Domain: return "Address is RFC 5322 compliant but contains domain characters that"
  1060. " are not allowed by DNS";
  1061. case EmailStatusCode.rfc5322TooLong: return "Address is too long";
  1062. case EmailStatusCode.rfc5322LocalTooLong: return "The local part of the address is too long";
  1063. case EmailStatusCode.rfc5322DomainTooLong: return "The domain part is too long";
  1064. case EmailStatusCode.rfc5322LabelTooLong: return "The domain part contains an element that is too long";
  1065. case EmailStatusCode.rfc5322DomainLiteral: return "The domain literal is not a valid RFC 5321 address literal";
  1066. case EmailStatusCode.rfc5322DomainLiteralObsoleteText: return "The domain literal is not a valid RFC 5321"
  1067. " address literal and it contains obsolete characters";
  1068. case EmailStatusCode.rfc5322IpV6GroupCount:
  1069. return "The IPv6 literal address contains the wrong number of groups";
  1070. case EmailStatusCode.rfc5322IpV6TooManyDoubleColons:
  1071. return "The IPv6 literal address contains too many :: sequences";
  1072. case EmailStatusCode.rfc5322IpV6BadChar: return "The IPv6 address contains an illegal group of characters";
  1073. case EmailStatusCode.rfc5322IpV6MaxGroups: return "The IPv6 address has too many groups";
  1074. case EmailStatusCode.rfc5322IpV6ColonStart: return "IPv6 address starts with a single colon";
  1075. case EmailStatusCode.rfc5322IpV6ColonEnd: return "IPv6 address ends with a single colon";
  1076. // Address is invalid for any purpose
  1077. case EmailStatusCode.errorExpectingDomainText:
  1078. return "A domain literal contains a character that is not allowed";
  1079. case EmailStatusCode.errorNoLocalPart: return "Address has no local part";
  1080. case EmailStatusCode.errorNoDomain: return "Address has no domain part";
  1081. case EmailStatusCode.errorConsecutiveDots: return "The address may not contain consecutive dots";
  1082. case EmailStatusCode.errorTextAfterCommentFoldingWhitespace:
  1083. return "Address contains text after a comment or Folding White Space";
  1084. case EmailStatusCode.errorTextAfterQuotedString: return "Address contains text after a quoted string";
  1085. case EmailStatusCode.errorTextAfterDomainLiteral: return "Extra characters were found after the end of"
  1086. " the domain literal";
  1087. case EmailStatusCode.errorExpectingQuotedPair:
  1088. return "The address contains a character that is not allowed in a quoted pair";
  1089. case EmailStatusCode.errorExpectingText: return "Address contains a character that is not allowed";
  1090. case EmailStatusCode.errorExpectingQuotedText:
  1091. return "A quoted string contains a character that is not allowed";
  1092. case EmailStatusCode.errorExpectingCommentText: return "A comment contains a character that is not allowed";
  1093. case EmailStatusCode.errorBackslashEnd: return "The address cannot end with a backslash";
  1094. case EmailStatusCode.errorDotStart: return "Neither part of the address may begin with a dot";
  1095. case EmailStatusCode.errorDotEnd: return "Neither part of the address may end with a dot";
  1096. case EmailStatusCode.errorDomainHyphenStart: return "A domain or subdomain cannot begin with a hyphen";
  1097. case EmailStatusCode.errorDomainHyphenEnd: return "A domain or subdomain cannot end with a hyphen";
  1098. case EmailStatusCode.errorUnclosedQuotedString: return "Unclosed quoted string";
  1099. case EmailStatusCode.errorUnclosedComment: return "Unclosed comment";
  1100. case EmailStatusCode.errorUnclosedDomainLiteral: return "Domain literal is missing its closing bracket";
  1101. case EmailStatusCode.errorFoldingWhitespaceCrflX2:
  1102. return "Folding White Space contains consecutive CRLF sequences";
  1103. case EmailStatusCode.errorFoldingWhitespaceCrLfEnd: return "Folding White Space ends with a CRLF sequence";
  1104. case EmailStatusCode.errorCrNoLf:
  1105. return "Address contains a carriage return that is not followed by a line feed";
  1106. }
  1107. }
  1108. /**
  1109. * An email status code, indicating if an email address is valid or not.
  1110. * If it is invalid it also indicates why.
  1111. */
  1112. enum EmailStatusCode
  1113. {
  1114. // Categories
  1115. /// Address is valid
  1116. validCategory = 1,
  1117. /// Address is valid but a DNS check was not successful
  1118. dnsWarning = 7,
  1119. /// Address is valid for SMTP but has unusual elements
  1120. rfc5321 = 15,
  1121. /// Address is valid within the message but cannot be used unmodified for the envelope
  1122. cFoldingWhitespace = 31,
  1123. /// Address contains deprecated elements but may still be valid in restricted contexts
  1124. deprecated_ = 63,
  1125. /// The address is only valid according to the broad definition of RFC 5322. It is otherwise invalid
  1126. rfc5322 = 127,
  1127. /**
  1128. * All finer grained error checking is turned on. Address containing errors or
  1129. * warnings is considered invalid. A specific email status code will be
  1130. * returned indicating the error/warning of the address.
  1131. */
  1132. any = 252,
  1133. /**
  1134. * Address is either considered valid or not, no finer grained error checking
  1135. * is performed. Returned email status code will be either Error or Valid.
  1136. */
  1137. none = 253,
  1138. /**
  1139. * Address containing warnings is considered valid, that is,
  1140. * any status code below 16 is considered valid.
  1141. */
  1142. warning = 254,
  1143. /// Address is invalid for any purpose
  1144. error = 255,
  1145. // Diagnoses
  1146. /// Address is valid
  1147. valid = 0,
  1148. // Address is valid but a DNS check was not successful
  1149. /// Could not find an MX record for this domain but an A-record does exist
  1150. dnsWarningNoMXRecord = 5,
  1151. /// Could not find an MX record or an A-record for this domain
  1152. dnsWarningNoRecord = 6,
  1153. // Address is valid for SMTP but has unusual elements
  1154. /// Address is valid but at a Top Level Domain
  1155. rfc5321TopLevelDomain = 9,
  1156. /// Address is valid but the Top Level Domain begins with a number
  1157. rfc5321TopLevelDomainNumeric = 10,
  1158. /// Address is valid but contains a quoted string
  1159. rfc5321QuotedString = 11,
  1160. /// Address is valid but at a literal address not a domain
  1161. rfc5321AddressLiteral = 12,
  1162. /// Address is valid but contains a :: that only elides one zero group
  1163. rfc5321IpV6Deprecated = 13,
  1164. // Address is valid within the message but cannot be used unmodified for the envelope
  1165. /// Address contains comments
  1166. comment = 17,
  1167. /// Address contains Folding White Space
  1168. foldingWhitespace = 18,
  1169. // Address contains deprecated elements but may still be valid in restricted contexts
  1170. /// The local part is in a deprecated form
  1171. deprecatedLocalPart = 33,
  1172. /// Address contains an obsolete form of Folding White Space
  1173. deprecatedFoldingWhitespace = 34,
  1174. /// A quoted string contains a deprecated character
  1175. deprecatedQuotedText = 35,
  1176. /// A quoted pair contains a deprecated character
  1177. deprecatedQuotedPair = 36,
  1178. /// Address contains a comment in a position that is deprecated
  1179. deprecatedComment = 37,
  1180. /// A comment contains a deprecated character
  1181. deprecatedCommentText = 38,
  1182. /// Address contains a comment or Folding White Space around the @ sign
  1183. deprecatedCommentFoldingWhitespaceNearAt = 49,
  1184. // The address is only valid according to the broad definition of RFC 5322
  1185. /// Address is RFC 5322 compliant but contains domain characters that are not allowed by DNS
  1186. rfc5322Domain = 65,
  1187. /// Address is too long
  1188. rfc5322TooLong = 66,
  1189. /// The local part of the address is too long
  1190. rfc5322LocalTooLong = 67,
  1191. /// The domain part is too long
  1192. rfc5322DomainTooLong = 68,
  1193. /// The domain part contains an element that is too long
  1194. rfc5322LabelTooLong = 69,
  1195. /// The domain literal is not a valid RFC 5321 address literal
  1196. rfc5322DomainLiteral = 70,
  1197. /// The domain literal is not a valid RFC 5321 address literal and it contains obsolete characters
  1198. rfc5322DomainLiteralObsoleteText = 71,
  1199. /// The IPv6 literal address contains the wrong number of groups
  1200. rfc5322IpV6GroupCount = 72,
  1201. /// The IPv6 literal address contains too many :: sequences
  1202. rfc5322IpV6TooManyDoubleColons = 73,
  1203. /// The IPv6 address contains an illegal group of characters
  1204. rfc5322IpV6BadChar = 74,
  1205. /// The IPv6 address has too many groups
  1206. rfc5322IpV6MaxGroups = 75,
  1207. /// IPv6 address starts with a single colon
  1208. rfc5322IpV6ColonStart = 76,
  1209. /// IPv6 address ends with a single colon
  1210. rfc5322IpV6ColonEnd = 77,
  1211. // Address is invalid for any purpose
  1212. /// A domain literal contains a character that is not allowed
  1213. errorExpectingDomainText = 129,
  1214. /// Address has no local part
  1215. errorNoLocalPart = 130,
  1216. /// Address has no domain part
  1217. errorNoDomain = 131,
  1218. /// The address may not contain consecutive dots
  1219. errorConsecutiveDots = 132,
  1220. /// Address contains text after a comment or Folding White Space
  1221. errorTextAfterCommentFoldingWhitespace = 133,
  1222. /// Address contains text after a quoted string
  1223. errorTextAfterQuotedString = 134,
  1224. /// Extra characters were found after the end of the domain literal
  1225. errorTextAfterDomainLiteral = 135,
  1226. /// The address contains a character that is not allowed in a quoted pair
  1227. errorExpectingQuotedPair = 136,
  1228. /// Address contains a character that is not allowed
  1229. errorExpectingText = 137,
  1230. /// A quoted string contains a character that is not allowed
  1231. errorExpectingQuotedText = 138,
  1232. /// A comment contains a character that is not allowed
  1233. errorExpectingCommentText = 139,
  1234. /// The address cannot end with a backslash
  1235. errorBackslashEnd = 140,
  1236. /// Neither part of the address may begin with a dot
  1237. errorDotStart = 141,
  1238. /// Neither part of the address may end with a dot
  1239. errorDotEnd = 142,
  1240. /// A domain or subdomain cannot begin with a hyphen
  1241. errorDomainHyphenStart = 143,
  1242. /// A domain or subdomain cannot end with a hyphen
  1243. errorDomainHyphenEnd = 144,
  1244. /// Unclosed quoted string
  1245. errorUnclosedQuotedString = 145,
  1246. /// Unclosed comment
  1247. errorUnclosedComment = 146,
  1248. /// Domain literal is missing its closing bracket
  1249. errorUnclosedDomainLiteral = 147,
  1250. /// Folding White Space contains consecutive CRLF sequences
  1251. errorFoldingWhitespaceCrflX2 = 148,
  1252. /// Folding White Space ends with a CRLF sequence
  1253. errorFoldingWhitespaceCrLfEnd = 149,
  1254. /// Address contains a carriage return that is not followed by a line feed
  1255. errorCrNoLf = 150,
  1256. }
  1257. private:
  1258. // Email parts for the isEmail function
  1259. enum EmailPart
  1260. {
  1261. // The local part of the email address, that is, the part before the @ sign
  1262. componentLocalPart,
  1263. // The domain part of the email address, that is, the part after the @ sign.
  1264. componentDomain,
  1265. componentLiteral,
  1266. contextComment,
  1267. contextFoldingWhitespace,
  1268. contextQuotedString,
  1269. contextQuotedPair,
  1270. status
  1271. }
  1272. // Miscellaneous string constants
  1273. struct Token
  1274. {
  1275. enum
  1276. {
  1277. at = "@",
  1278. backslash = `\`,
  1279. dot = ".",
  1280. doubleQuote = `"`,
  1281. openParenthesis = "(",
  1282. closeParenthesis = ")",
  1283. openBracket = "[",
  1284. closeBracket = "]",
  1285. hyphen = "-",
  1286. colon = ":",
  1287. doubleColon = "::",
  1288. space = " ",
  1289. tab = "\t",
  1290. cr = "\r",
  1291. lf = "\n",
  1292. ipV6Tag = "IPV6:",
  1293. // US-ASCII visible characters not valid for atext (http://tools.ietf.org/html/rfc5322#section-3.2.3)
  1294. specials = `()<>[]:;@\\,."`
  1295. }
  1296. }
  1297. enum AsciiToken
  1298. {
  1299. horizontalTab = 9,
  1300. unitSeparator = 31,
  1301. delete_ = 127
  1302. }
  1303. /*
  1304. * Returns the maximum of the values in the given array.
  1305. *
  1306. * Examples:
  1307. * ---
  1308. * assert([1, 2, 3, 4].max == 4);
  1309. * assert([3, 5, 9, 2, 5].max == 9);
  1310. * assert([7, 13, 9, 12, 0].max == 13);
  1311. * ---
  1312. *
  1313. * Params:
  1314. * arr = the array containing the values to return the maximum of
  1315. *
  1316. * Returns: the maximum value
  1317. */
  1318. T max (T) (T[] arr)
  1319. {
  1320. import std.algorithm/* : max*/;
  1321. auto max = arr.front;
  1322. foreach (i ; 0 .. arr.length - 1)
  1323. max = std.algorithm.max(max, arr[i + 1]);
  1324. return max;
  1325. }
  1326. unittest
  1327. {
  1328. assert([1, 2, 3, 4].max() == 4);
  1329. assert([3, 5, 9, 2, 5].max() == 9);
  1330. assert([7, 13, 9, 12, 0].max() == 13);
  1331. }
  1332. /*
  1333. * Returns the portion of string specified by the $(D_PARAM start) and
  1334. * $(D_PARAM length) parameters.
  1335. *
  1336. * Examples:
  1337. * ---
  1338. * assert("abcdef".substr(-1) == "f");
  1339. * assert("abcdef".substr(-2) == "ef");
  1340. * assert("abcdef".substr(-3, 1) == "d");
  1341. * ---
  1342. *
  1343. * Params:
  1344. * str = the input string. Must be one character or longer.
  1345. * start = if $(D_PARAM start) is non-negative, the returned string will start at the
  1346. * $(D_PARAM start)'th position in $(D_PARAM str), counting from zero.
  1347. * For instance, in the string "abcdef", the character at position 0 is 'a',
  1348. * the character at position 2 is 'c', and so forth.
  1349. *
  1350. * If $(D_PARAM start) is negative, the returned string will start at the
  1351. * $(D_PARAM start)'th character from the end of $(D_PARAM str).
  1352. *
  1353. * If $(D_PARAM str) is less than or equal to $(D_PARAM start) characters long,
  1354. * $(D_KEYWORD true) will be returned.
  1355. *
  1356. * length = if $(D_PARAM length) is given and is positive, the string returned will
  1357. * contain at most $(D_PARAM length) characters beginning from $(D_PARAM start)
  1358. * (depending on the length of string).
  1359. *
  1360. * If $(D_PARAM length) is given and is negative, then that many characters
  1361. * will be omitted from the end of string (after the start position has been
  1362. * calculated when a $(D_PARAM start) is negative). If $(D_PARAM start)
  1363. * denotes the position of this truncation or beyond, $(D_KEYWORD false)
  1364. * will be returned.
  1365. *
  1366. * If $(D_PARAM length) is given and is 0, an empty string will be returned.
  1367. *
  1368. * If $(D_PARAM length) is omitted, the substring starting from $(D_PARAM start)
  1369. * until the end of the string will be returned.
  1370. *
  1371. * Returns: the extracted part of string, or an empty string.
  1372. */
  1373. T[] substr (T) (T[] str, ptrdiff_t start = 0, ptrdiff_t length = ptrdiff_t.min)
  1374. {
  1375. ptrdiff_t end = length;
  1376. if (start < 0)
  1377. {
  1378. start = str.length + start;
  1379. if (end < 0)
  1380. {
  1381. if (end == ptrdiff_t.min)
  1382. end = 0;
  1383. end = str.length + end;
  1384. }
  1385. else
  1386. end = start + end;
  1387. }
  1388. else
  1389. {
  1390. if (end == ptrdiff_t.min)
  1391. end = str.length;
  1392. if (end < 0)
  1393. end = str.length + end;
  1394. else
  1395. end = start + end;
  1396. }
  1397. if (start > end)
  1398. end = start;
  1399. if (end > str.length)
  1400. end = str.length;
  1401. return str[start .. end];
  1402. }
  1403. unittest
  1404. {
  1405. assert("abcdef".substr(-1) == "f");
  1406. assert("abcdef".substr(-2) == "ef");
  1407. assert("abcdef".substr(-3, 1) == "d");
  1408. assert("abcdef".substr(0, -1) == "abcde");
  1409. assert("abcdef".substr(2, -1) == "cde");
  1410. assert("abcdef".substr(4, -4) == []);
  1411. assert("abcdef".substr(-3, -1) == "de");
  1412. assert("abcdef".substr(1, 1) == "b");
  1413. assert("abcdef".substr(-1, -1) == []);
  1414. }
  1415. /*
  1416. * Compare the two given strings lexicographically. An upper limit of the number of
  1417. * characters, that will be used in the comparison, can be specified. Supports both
  1418. * case-sensitive and case-insensitive comparison.
  1419. *
  1420. * Examples:
  1421. * ---
  1422. * assert("abc".compareFirstN("abcdef", 3) == 0);
  1423. * assert("abc".compareFirstN("Abc", 3, true) == 0);
  1424. * assert("abc".compareFirstN("abcdef", 6) < 0);
  1425. * assert("abcdef".compareFirstN("abc", 6) > 0);
  1426. * ---
  1427. *
  1428. * Params:
  1429. * s1 = the first string to be compared
  1430. * s2 = the second string to be compared
  1431. * length = the length of strings to be used in the comparison.
  1432. * caseInsensitive = if true, a case-insensitive comparison will be made,
  1433. * otherwise a case-sensitive comparison will be made
  1434. *
  1435. * Returns: (for $(D pred = "a < b")):
  1436. *
  1437. * $(BOOKTABLE,
  1438. * $(TR $(TD $(D < 0)) $(TD $(D s1 < s2) ))
  1439. * $(TR $(TD $(D = 0)) $(TD $(D s1 == s2)))
  1440. * $(TR $(TD $(D > 0)) $(TD $(D s1 > s2)))
  1441. * )
  1442. */
  1443. int compareFirstN (alias pred = "a < b", S1, S2) (S1 s1, S2 s2, size_t length, bool caseInsensitive = false)
  1444. if (is(Unqual!(ElementType!(S1)) == dchar) && is(Unqual!(ElementType!(S2)) == dchar))
  1445. {
  1446. auto s1End = length <= s1.length ? length : s1.length;
  1447. auto s2End = length <= s2.length ? length : s2.length;
  1448. auto slice1 = s1[0 .. s1End];
  1449. auto slice2 = s2[0 .. s2End];
  1450. return caseInsensitive ? slice1.icmp(slice2) : slice1.cmp(slice2);
  1451. }
  1452. unittest
  1453. {
  1454. assert("abc".compareFirstN("abcdef", 3) == 0);
  1455. assert("abc".compareFirstN("Abc", 3, true) == 0);
  1456. assert("abc".compareFirstN("abcdef", 6) < 0);
  1457. assert("abcdef".compareFirstN("abc", 6) > 0);
  1458. }
  1459. /*
  1460. * Returns a range consisting of the elements of the $(D_PARAM input) range that
  1461. * matches the given $(D_PARAM pattern).
  1462. *
  1463. * Examples:
  1464. * ---
  1465. * assert(equal(["ab", "0a", "cd", "1b"].grep(regex(`\d\w`)), ["0a", "1b"]));
  1466. * assert(equal(["abc", "0123", "defg", "4567"].grep(regex(`(\w+)`), true), ["0123", "4567"]));
  1467. * ---
  1468. *
  1469. * Params:
  1470. * input = the input range
  1471. * pattern = the regular expression pattern to search for
  1472. * invert = if $(D_KEYWORD true), this function returns the elements of the
  1473. * input range that do $(B not) match the given $(D_PARAM pattern).
  1474. *
  1475. * Returns: a range containing the matched elements
  1476. */
  1477. auto grep (Range, Regex) (Range input, Regex pattern, bool invert = false)
  1478. {
  1479. auto dg = invert ? (ElementType!(Range) e) { return e.match(pattern).empty; } :
  1480. (ElementType!(Range) e) { return !e.match(pattern).empty; };
  1481. return filter!(dg)(input);
  1482. }
  1483. unittest
  1484. {
  1485. assert(equal(["ab", "0a", "cd", "1b"].grep(regex(`\d\w`)), ["0a", "1b"]));
  1486. assert(equal(["abc", "0123", "defg", "4567"].grep(regex(`4567`), true), ["abc", "0123", "defg"]));
  1487. }
  1488. /*
  1489. * Pops the last element of the given range and returns the element.
  1490. *
  1491. * Examples:
  1492. * ---
  1493. * auto array = [0, 1, 2, 3];
  1494. * auto result = array.pop();
  1495. *
  1496. * assert(array == [0, 1, 2]);
  1497. * assert(result == 3);
  1498. * ---
  1499. *
  1500. * Params:
  1501. * range = the range to pop the element from
  1502. *
  1503. * Returns: the popped element
  1504. */
  1505. ElementType!(A) pop (A) (ref A a) if (isDynamicArray!(A) && !isNarrowString!(A) && isMutable!(A) && !is(A == void[]))
  1506. {
  1507. auto e = a.back;
  1508. a.popBack();
  1509. return e;
  1510. }
  1511. unittest
  1512. {
  1513. auto array = [0, 1, 2, 3];
  1514. auto result = array.pop();
  1515. assert(array == [0, 1, 2]);
  1516. assert(result == 3);
  1517. }
  1518. /*
  1519. * Returns the character at the given index as a string. The returned string will be a
  1520. * slice of the original string.
  1521. *
  1522. * Examples:
  1523. * ---
  1524. * assert("abc".get(1, 'b') == "b");
  1525. * assert("löv".get(1, 'ö') == "ö");
  1526. * ---
  1527. *
  1528. * Params:
  1529. * str = the string to get the character from
  1530. * index = the index of the character to get
  1531. * c = the character to return, or any other of the same length
  1532. *
  1533. * Returns: the character at the given index as a string
  1534. */
  1535. const(T)[] get (T) (const(T)[] str, size_t index, dchar c)
  1536. {
  1537. return str[index .. index + codeLength!(T)(c)];
  1538. }
  1539. unittest
  1540. {
  1541. assert("abc".get(1, 'b') == "b");
  1542. assert("löv".get(1, 'ö') == "ö");
  1543. }
  1544. // issue 4673
  1545. bool isNumeric (dchar c)
  1546. {
  1547. switch (c)
  1548. {
  1549. case 'i':
  1550. case '.':
  1551. case '-':
  1552. case '+':
  1553. case 'u':
  1554. case 'l':
  1555. case 'L':
  1556. case 'U':
  1557. case 'I':
  1558. return false;
  1559. default:
  1560. }
  1561. return std.uni.isNumber(c);
  1562. }
  1563. // Issue 5744
  1564. import core.stdc.string : memcmp;
  1565. ptrdiff_t lastIndexOf(Char1, Char2)(in Char1[] s, const(Char2)[] sub,
  1566. CaseSensitive cs = CaseSensitive.yes) if (isSomeChar!Char1 && isSomeChar!Char2)
  1567. {
  1568. if (cs == CaseSensitive.yes)
  1569. {
  1570. Char2 c;
  1571. if (sub.length == 0)
  1572. return s.length;
  1573. c = sub[0];
  1574. if (sub.length == 1)
  1575. return std.string.lastIndexOf(s, c);
  1576. for (ptrdiff_t i = s.length - sub.length; i >= 0; i--)
  1577. {
  1578. if (s[i] == c)
  1579. {
  1580. if (memcmp(&s[i + 1], &sub[1], sub.length - 1) == 0)
  1581. return i;
  1582. }
  1583. }
  1584. return -1;
  1585. }
  1586. else
  1587. {
  1588. dchar c;
  1589. if (sub.length == 0)
  1590. return s.length;
  1591. c = sub[0];
  1592. if (sub.length == 1)
  1593. return std.string.lastIndexOf(s, c, cs);
  1594. if (c <= 0x7F)
  1595. {
  1596. c = std.ascii.toLower(c);
  1597. for (ptrdiff_t i = s.length - sub.length; i >= 0; i--)
  1598. {
  1599. if (std.ascii.toLower(s[i]) == c)
  1600. {
  1601. if (icmp(s[i + 1 .. i + sub.length], sub[1 .. sub.length]) == 0)
  1602. return i;
  1603. }
  1604. }
  1605. }
  1606. else
  1607. {
  1608. for (ptrdiff_t i = s.length - sub.length; i >= 0; i--)
  1609. {
  1610. if (icmp(s[i .. i + sub.length], sub) == 0)
  1611. return i;
  1612. }
  1613. }
  1614. return -1;
  1615. }
  1616. }