PageRenderTime 174ms CodeModel.GetById 4ms app.highlight 156ms RepoModel.GetById 1ms app.codeStats 1ms

/std/net/isemail.d

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