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