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