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