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