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