| /** | |
| * Validates an email address according to RFCs 5321, 5322 and others. | |
| * | |
| * Authors: Dominic Sayers $(LT)dominic@sayers.cc$(GT), Jacob Carlborg | |
| * Copyright: Dominic Sayers, Jacob Carlborg 2008-. | |
| * Test schema documentation: Copyright © 2011, Daniel Marschall | |
| * License: $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost Software License 1.0) | |
| * Dominic Sayers graciously granted permission to use the Boost license via email on Feb 22, 2011. | |
| * Version: 3.0.13 - Version 3.0 of the original PHP implementation: $(LINK http://www.dominicsayers.com/isemail) | |
| * | |
| * Standards: | |
| * $(UL | |
| * $(LI RFC 5321) | |
| * $(LI RFC 5322) | |
| * ) | |
| * | |
| * References: | |
| * $(UL | |
| * $(LI $(LINK http://www.dominicsayers.com/isemail)) | |
| * $(LI $(LINK http://tools.ietf.org/html/rfc5321)) | |
| * $(LI $(LINK http://tools.ietf.org/html/rfc5322)) | |
| * ) | |
| * | |
| * Source: $(PHOBOSSRC std/net/isemail.d) | |
| */ | |
| module std.net.isemail; | |
| import std.range.primitives : back, front, ElementType, popFront, popBack; | |
| import std.traits; | |
| import std.typecons : Flag, Yes, No; | |
| /** | |
| * Check that an email address conforms to RFCs 5321, 5322 and others. | |
| * | |
| * Distinguishes between a Mailbox as defined by RFC 5321 and an addr-spec as | |
| * defined by RFC 5322. Depending on the context, either can be regarded as a | |
| * valid email address. | |
| * | |
| * Note: The DNS check is currently not implemented. | |
| * | |
| * Params: | |
| * email = The email address to check | |
| * checkDNS = If `Yes.checkDns` then a DNS check for MX records will be made | |
| * errorLevel = Determines the boundary between valid and invalid addresses. | |
| * Status codes above this number will be returned as-is, | |
| * status codes below will be returned as EmailStatusCode.valid. | |
| * Thus the calling program can simply look for EmailStatusCode.valid | |
| * if it is only interested in whether an address is valid or not. The | |
| * $(D_PARAM errorLevel) will determine how "picky" isEmail() is about | |
| * the address. | |
| * | |
| * If omitted or passed as EmailStatusCode.none then isEmail() will | |
| * not perform any finer grained error checking and an address is | |
| * either considered valid or not. Email status code will either be | |
| * EmailStatusCode.valid or EmailStatusCode.error. | |
| * | |
| * Returns: | |
| * An $(LREF EmailStatus), indicating the status of the email address. | |
| */ | |
| EmailStatus isEmail(Char)(const(Char)[] email, CheckDns checkDNS = No.checkDns, | |
| EmailStatusCode errorLevel = EmailStatusCode.none) | |
| if (isSomeChar!(Char)) | |
| { | |
| import std.algorithm.iteration : uniq, filter, map; | |
| import std.algorithm.searching : canFind, maxElement; | |
| import std.array : array, split; | |
| import std.conv : to; | |
| import std.exception : enforce; | |
| import std.string : indexOf, lastIndexOf; | |
| import std.uni : isNumber; | |
| alias tstring = const(Char)[]; | |
| alias Token = TokenImpl!(Char); | |
| enum defaultThreshold = 16; | |
| int threshold; | |
| bool diagnose; | |
| if (errorLevel == EmailStatusCode.any) | |
| { | |
| threshold = EmailStatusCode.valid; | |
| diagnose = true; | |
| } | |
| else if (errorLevel == EmailStatusCode.none) | |
| threshold = defaultThreshold; | |
| else | |
| { | |
| diagnose = true; | |
| switch (errorLevel) | |
| { | |
| case EmailStatusCode.warning: threshold = defaultThreshold; break; | |
| case EmailStatusCode.error: threshold = EmailStatusCode.valid; break; | |
| default: threshold = errorLevel; | |
| } | |
| } | |
| auto returnStatus = [EmailStatusCode.valid]; | |
| auto context = EmailPart.componentLocalPart; | |
| auto contextStack = [context]; | |
| auto contextPrior = context; | |
| tstring token = ""; | |
| tstring tokenPrior = ""; | |
| tstring[EmailPart] parseData = [EmailPart.componentLocalPart : "", EmailPart.componentDomain : ""]; | |
| tstring[][EmailPart] atomList = [EmailPart.componentLocalPart : [""], EmailPart.componentDomain : [""]]; | |
| auto elementCount = 0; | |
| auto elementLength = 0; | |
| auto hyphenFlag = false; | |
| auto endOrDie = false; | |
| auto crlfCount = int.min; // int.min == not defined | |
| for (size_t i; i < email.length; i++) | |
| { | |
| auto e = email[i]; | |
| token = email.get(i, e); | |
| switch (context) | |
| { | |
| case EmailPart.componentLocalPart: | |
| switch (token) | |
| { | |
| case Token.openParenthesis: | |
| if (elementLength == 0) | |
| returnStatus ~= elementCount == 0 ? EmailStatusCode.comment : | |
| EmailStatusCode.deprecatedComment; | |
| else | |
| { | |
| returnStatus ~= EmailStatusCode.comment; | |
| endOrDie = true; | |
| } | |
| contextStack ~= context; | |
| context = EmailPart.contextComment; | |
| break; | |
| case Token.dot: | |
| if (elementLength == 0) | |
| returnStatus ~= elementCount == 0 ? EmailStatusCode.errorDotStart : | |
| EmailStatusCode.errorConsecutiveDots; | |
| else | |
| { | |
| if (endOrDie) | |
| returnStatus ~= EmailStatusCode.deprecatedLocalPart; | |
| } | |
| endOrDie = false; | |
| elementLength = 0; | |
| elementCount++; | |
| parseData[EmailPart.componentLocalPart] ~= token; | |
| if (elementCount >= atomList[EmailPart.componentLocalPart].length) | |
| atomList[EmailPart.componentLocalPart] ~= ""; | |
| else | |
| atomList[EmailPart.componentLocalPart][elementCount] = ""; | |
| break; | |
| case Token.doubleQuote: | |
| if (elementLength == 0) | |
| { | |
| returnStatus ~= elementCount == 0 ? EmailStatusCode.rfc5321QuotedString : | |
| EmailStatusCode.deprecatedLocalPart; | |
| parseData[EmailPart.componentLocalPart] ~= token; | |
| atomList[EmailPart.componentLocalPart][elementCount] ~= token; | |
| elementLength++; | |
| endOrDie = true; | |
| contextStack ~= context; | |
| context = EmailPart.contextQuotedString; | |
| } | |
| else | |
| returnStatus ~= EmailStatusCode.errorExpectingText; | |
| break; | |
| case Token.cr: | |
| case Token.space: | |
| case Token.tab: | |
| if ((token == Token.cr) && ((++i == email.length) || (email.get(i, e) != Token.lf))) | |
| { | |
| returnStatus ~= EmailStatusCode.errorCrNoLf; | |
| break; | |
| } | |
| if (elementLength == 0) | |
| returnStatus ~= elementCount == 0 ? EmailStatusCode.foldingWhitespace : | |
| EmailStatusCode.deprecatedFoldingWhitespace; | |
| else | |
| endOrDie = true; | |
| contextStack ~= context; | |
| context = EmailPart.contextFoldingWhitespace; | |
| tokenPrior = token; | |
| break; | |
| case Token.at: | |
| enforce(contextStack.length == 1, "Unexpected item on context stack"); | |
| if (parseData[EmailPart.componentLocalPart] == "") | |
| returnStatus ~= EmailStatusCode.errorNoLocalPart; | |
| else if (elementLength == 0) | |
| returnStatus ~= EmailStatusCode.errorDotEnd; | |
| else if (parseData[EmailPart.componentLocalPart].length > 64) | |
| returnStatus ~= EmailStatusCode.rfc5322LocalTooLong; | |
| else if (contextPrior == EmailPart.contextComment || | |
| contextPrior == EmailPart.contextFoldingWhitespace) | |
| returnStatus ~= EmailStatusCode.deprecatedCommentFoldingWhitespaceNearAt; | |
| context = EmailPart.componentDomain; | |
| contextStack = [context]; | |
| elementCount = 0; | |
| elementLength = 0; | |
| endOrDie = false; | |
| break; | |
| default: | |
| if (endOrDie) | |
| { | |
| switch (contextPrior) | |
| { | |
| case EmailPart.contextComment: | |
| case EmailPart.contextFoldingWhitespace: | |
| returnStatus ~= EmailStatusCode.errorTextAfterCommentFoldingWhitespace; | |
| break; | |
| case EmailPart.contextQuotedString: | |
| returnStatus ~= EmailStatusCode.errorTextAfterQuotedString; | |
| break; | |
| default: | |
| throw new Exception("More text found where none is allowed, but " | |
| ~"unrecognised prior context: " ~ to!(string)(contextPrior)); | |
| } | |
| } | |
| else | |
| { | |
| contextPrior = context; | |
| immutable c = token.front; | |
| if (c < '!' || c > '~' || c == '\n' || Token.specials.canFind(token)) | |
| returnStatus ~= EmailStatusCode.errorExpectingText; | |
| parseData[EmailPart.componentLocalPart] ~= token; | |
| atomList[EmailPart.componentLocalPart][elementCount] ~= token; | |
| elementLength++; | |
| } | |
| } | |
| break; | |
| case EmailPart.componentDomain: | |
| switch (token) | |
| { | |
| case Token.openParenthesis: | |
| if (elementLength == 0) | |
| { | |
| returnStatus ~= elementCount == 0 ? | |
| EmailStatusCode.deprecatedCommentFoldingWhitespaceNearAt | |
| : EmailStatusCode.deprecatedComment; | |
| } | |
| else | |
| { | |
| returnStatus ~= EmailStatusCode.comment; | |
| endOrDie = true; | |
| } | |
| contextStack ~= context; | |
| context = EmailPart.contextComment; | |
| break; | |
| case Token.dot: | |
| if (elementLength == 0) | |
| returnStatus ~= elementCount == 0 ? EmailStatusCode.errorDotStart : | |
| EmailStatusCode.errorConsecutiveDots; | |
| else if (hyphenFlag) | |
| returnStatus ~= EmailStatusCode.errorDomainHyphenEnd; | |
| else | |
| { | |
| if (elementLength > 63) | |
| returnStatus ~= EmailStatusCode.rfc5322LabelTooLong; | |
| } | |
| endOrDie = false; | |
| elementLength = 0; | |
| elementCount++; | |
| //atomList[EmailPart.componentDomain][elementCount] = ""; | |
| atomList[EmailPart.componentDomain] ~= ""; | |
| parseData[EmailPart.componentDomain] ~= token; | |
| break; | |
| case Token.openBracket: | |
| if (parseData[EmailPart.componentDomain] == "") | |
| { | |
| endOrDie = true; | |
| elementLength++; | |
| contextStack ~= context; | |
| context = EmailPart.componentLiteral; | |
| parseData[EmailPart.componentDomain] ~= token; | |
| atomList[EmailPart.componentDomain][elementCount] ~= token; | |
| parseData[EmailPart.componentLiteral] = ""; | |
| } | |
| else | |
| returnStatus ~= EmailStatusCode.errorExpectingText; | |
| break; | |
| case Token.cr: | |
| case Token.space: | |
| case Token.tab: | |
| if (token == Token.cr && (++i == email.length || email.get(i, e) != Token.lf)) | |
| { | |
| returnStatus ~= EmailStatusCode.errorCrNoLf; | |
| break; | |
| } | |
| if (elementLength == 0) | |
| { | |
| returnStatus ~= elementCount == 0 ? | |
| EmailStatusCode.deprecatedCommentFoldingWhitespaceNearAt | |
| : EmailStatusCode.deprecatedFoldingWhitespace; | |
| } | |
| else | |
| { | |
| returnStatus ~= EmailStatusCode.foldingWhitespace; | |
| endOrDie = true; | |
| } | |
| contextStack ~= context; | |
| context = EmailPart.contextFoldingWhitespace; | |
| tokenPrior = token; | |
| break; | |
| default: | |
| if (endOrDie) | |
| { | |
| switch (contextPrior) | |
| { | |
| case EmailPart.contextComment: | |
| case EmailPart.contextFoldingWhitespace: | |
| returnStatus ~= EmailStatusCode.errorTextAfterCommentFoldingWhitespace; | |
| break; | |
| case EmailPart.componentLiteral: | |
| returnStatus ~= EmailStatusCode.errorTextAfterDomainLiteral; | |
| break; | |
| default: | |
| throw new Exception("More text found where none is allowed, but " | |
| ~"unrecognised prior context: " ~ to!(string)(contextPrior)); | |
| } | |
| } | |
| immutable c = token.front; | |
| hyphenFlag = false; | |
| if (c < '!' || c > '~' || Token.specials.canFind(token)) | |
| returnStatus ~= EmailStatusCode.errorExpectingText; | |
| else if (token == Token.hyphen) | |
| { | |
| if (elementLength == 0) | |
| returnStatus ~= EmailStatusCode.errorDomainHyphenStart; | |
| hyphenFlag = true; | |
| } | |
| else if (!((c > '/' && c < ':') || (c > '@' && c < '[') || (c > '`' && c < '{'))) | |
| returnStatus ~= EmailStatusCode.rfc5322Domain; | |
| parseData[EmailPart.componentDomain] ~= token; | |
| atomList[EmailPart.componentDomain][elementCount] ~= token; | |
| elementLength++; | |
| } | |
| break; | |
| case EmailPart.componentLiteral: | |
| switch (token) | |
| { | |
| case Token.closeBracket: | |
| if (returnStatus.maxElement() < EmailStatusCode.deprecated_) | |
| { | |
| auto maxGroups = 8; | |
| size_t index = -1; | |
| auto addressLiteral = parseData[EmailPart.componentLiteral]; | |
| const(Char)[] ipSuffix = matchIPSuffix(addressLiteral); | |
| if (ipSuffix.length) | |
| { | |
| index = addressLiteral.length - ipSuffix.length; | |
| if (index != 0) | |
| addressLiteral = addressLiteral[0 .. index] ~ "0:0"; | |
| } | |
| if (index == 0) | |
| returnStatus ~= EmailStatusCode.rfc5321AddressLiteral; | |
| else if (addressLiteral.compareFirstN(Token.ipV6Tag, 5)) | |
| returnStatus ~= EmailStatusCode.rfc5322DomainLiteral; | |
| else | |
| { | |
| auto ipV6 = addressLiteral[5 .. $]; | |
| auto matchesIp = ipV6.split(Token.colon); | |
| immutable groupCount = matchesIp.length; | |
| index = ipV6.indexOf(Token.doubleColon); | |
| if (index == -1) | |
| { | |
| if (groupCount != maxGroups) | |
| returnStatus ~= EmailStatusCode.rfc5322IpV6GroupCount; | |
| } | |
| else | |
| { | |
| if (index != ipV6.lastIndexOf(Token.doubleColon)) | |
| returnStatus ~= EmailStatusCode.rfc5322IpV6TooManyDoubleColons; | |
| else | |
| { | |
| if (index == 0 || index == (ipV6.length - 2)) | |
| maxGroups++; | |
| if (groupCount > maxGroups) | |
| returnStatus ~= EmailStatusCode.rfc5322IpV6MaxGroups; | |
| else if (groupCount == maxGroups) | |
| returnStatus ~= EmailStatusCode.rfc5321IpV6Deprecated; | |
| } | |
| } | |
| if (ipV6[0 .. 1] == Token.colon && ipV6[1 .. 2] != Token.colon) | |
| returnStatus ~= EmailStatusCode.rfc5322IpV6ColonStart; | |
| else if (ipV6[$ - 1 .. $] == Token.colon && ipV6[$ - 2 .. $ - 1] != Token.colon) | |
| returnStatus ~= EmailStatusCode.rfc5322IpV6ColonEnd; | |
| else if (!matchesIp | |
| .filter!(a => !isUpToFourHexChars(a)) | |
| .empty) | |
| returnStatus ~= EmailStatusCode.rfc5322IpV6BadChar; | |
| else | |
| returnStatus ~= EmailStatusCode.rfc5321AddressLiteral; | |
| } | |
| } | |
| else | |
| returnStatus ~= EmailStatusCode.rfc5322DomainLiteral; | |
| parseData[EmailPart.componentDomain] ~= token; | |
| atomList[EmailPart.componentDomain][elementCount] ~= token; | |
| elementLength++; | |
| contextPrior = context; | |
| context = contextStack.pop(); | |
| break; | |
| case Token.backslash: | |
| returnStatus ~= EmailStatusCode.rfc5322DomainLiteralObsoleteText; | |
| contextStack ~= context; | |
| context = EmailPart.contextQuotedPair; | |
| break; | |
| case Token.cr: | |
| case Token.space: | |
| case Token.tab: | |
| if (token == Token.cr && (++i == email.length || email.get(i, e) != Token.lf)) | |
| { | |
| returnStatus ~= EmailStatusCode.errorCrNoLf; | |
| break; | |
| } | |
| returnStatus ~= EmailStatusCode.foldingWhitespace; | |
| contextStack ~= context; | |
| context = EmailPart.contextFoldingWhitespace; | |
| tokenPrior = token; | |
| break; | |
| default: | |
| immutable c = token.front; | |
| if (c > AsciiToken.delete_ || c == '\0' || token == Token.openBracket) | |
| { | |
| returnStatus ~= EmailStatusCode.errorExpectingDomainText; | |
| break; | |
| } | |
| else if (c < '!' || c == AsciiToken.delete_ ) | |
| returnStatus ~= EmailStatusCode.rfc5322DomainLiteralObsoleteText; | |
| parseData[EmailPart.componentLiteral] ~= token; | |
| parseData[EmailPart.componentDomain] ~= token; | |
| atomList[EmailPart.componentDomain][elementCount] ~= token; | |
| elementLength++; | |
| } | |
| break; | |
| case EmailPart.contextQuotedString: | |
| switch (token) | |
| { | |
| case Token.backslash: | |
| contextStack ~= context; | |
| context = EmailPart.contextQuotedPair; | |
| break; | |
| case Token.cr: | |
| case Token.tab: | |
| if (token == Token.cr && (++i == email.length || email.get(i, e) != Token.lf)) | |
| { | |
| returnStatus ~= EmailStatusCode.errorCrNoLf; | |
| break; | |
| } | |
| parseData[EmailPart.componentLocalPart] ~= Token.space; | |
| atomList[EmailPart.componentLocalPart][elementCount] ~= Token.space; | |
| elementLength++; | |
| returnStatus ~= EmailStatusCode.foldingWhitespace; | |
| contextStack ~= context; | |
| context = EmailPart.contextFoldingWhitespace; | |
| tokenPrior = token; | |
| break; | |
| case Token.doubleQuote: | |
| parseData[EmailPart.componentLocalPart] ~= token; | |
| atomList[EmailPart.componentLocalPart][elementCount] ~= token; | |
| elementLength++; | |
| contextPrior = context; | |
| context = contextStack.pop(); | |
| break; | |
| default: | |
| immutable c = token.front; | |
| if (c > AsciiToken.delete_ || c == '\0' || c == '\n') | |
| returnStatus ~= EmailStatusCode.errorExpectingQuotedText; | |
| else if (c < ' ' || c == AsciiToken.delete_) | |
| returnStatus ~= EmailStatusCode.deprecatedQuotedText; | |
| parseData[EmailPart.componentLocalPart] ~= token; | |
| atomList[EmailPart.componentLocalPart][elementCount] ~= token; | |
| elementLength++; | |
| } | |
| break; | |
| case EmailPart.contextQuotedPair: | |
| immutable c = token.front; | |
| if (c > AsciiToken.delete_) | |
| returnStatus ~= EmailStatusCode.errorExpectingQuotedPair; | |
| else if (c < AsciiToken.unitSeparator && c != AsciiToken.horizontalTab || c == AsciiToken.delete_) | |
| returnStatus ~= EmailStatusCode.deprecatedQuotedPair; | |
| contextPrior = context; | |
| context = contextStack.pop(); | |
| token = Token.backslash ~ token; | |
| switch (context) | |
| { | |
| case EmailPart.contextComment: break; | |
| case EmailPart.contextQuotedString: | |
| parseData[EmailPart.componentLocalPart] ~= token; | |
| atomList[EmailPart.componentLocalPart][elementCount] ~= token; | |
| elementLength += 2; | |
| break; | |
| case EmailPart.componentLiteral: | |
| parseData[EmailPart.componentDomain] ~= token; | |
| atomList[EmailPart.componentDomain][elementCount] ~= token; | |
| elementLength += 2; | |
| break; | |
| default: | |
| throw new Exception("Quoted pair logic invoked in an invalid context: " ~ to!(string)(context)); | |
| } | |
| break; | |
| case EmailPart.contextComment: | |
| switch (token) | |
| { | |
| case Token.openParenthesis: | |
| contextStack ~= context; | |
| context = EmailPart.contextComment; | |
| break; | |
| case Token.closeParenthesis: | |
| contextPrior = context; | |
| context = contextStack.pop(); | |
| break; | |
| case Token.backslash: | |
| contextStack ~= context; | |
| context = EmailPart.contextQuotedPair; | |
| break; | |
| case Token.cr: | |
| case Token.space: | |
| case Token.tab: | |
| if (token == Token.cr && (++i == email.length || email.get(i, e) != Token.lf)) | |
| { | |
| returnStatus ~= EmailStatusCode.errorCrNoLf; | |
| break; | |
| } | |
| returnStatus ~= EmailStatusCode.foldingWhitespace; | |
| contextStack ~= context; | |
| context = EmailPart.contextFoldingWhitespace; | |
| tokenPrior = token; | |
| break; | |
| default: | |
| immutable c = token.front; | |
| if (c > AsciiToken.delete_ || c == '\0' || c == '\n') | |
| { | |
| returnStatus ~= EmailStatusCode.errorExpectingCommentText; | |
| break; | |
| } | |
| else if (c < ' ' || c == AsciiToken.delete_) | |
| returnStatus ~= EmailStatusCode.deprecatedCommentText; | |
| } | |
| break; | |
| case EmailPart.contextFoldingWhitespace: | |
| if (tokenPrior == Token.cr) | |
| { | |
| if (token == Token.cr) | |
| { | |
| returnStatus ~= EmailStatusCode.errorFoldingWhitespaceCrflX2; | |
| break; | |
| } | |
| if (crlfCount != int.min) // int.min == not defined | |
| { | |
| if (++crlfCount > 1) | |
| returnStatus ~= EmailStatusCode.deprecatedFoldingWhitespace; | |
| } | |
| else | |
| crlfCount = 1; | |
| } | |
| switch (token) | |
| { | |
| case Token.cr: | |
| if (++i == email.length || email.get(i, e) != Token.lf) | |
| returnStatus ~= EmailStatusCode.errorCrNoLf; | |
| break; | |
| case Token.space: | |
| case Token.tab: | |
| break; | |
| default: | |
| if (tokenPrior == Token.cr) | |
| { | |
| returnStatus ~= EmailStatusCode.errorFoldingWhitespaceCrLfEnd; | |
| break; | |
| } | |
| crlfCount = int.min; // int.min == not defined | |
| contextPrior = context; | |
| context = contextStack.pop(); | |
| i--; | |
| break; | |
| } | |
| tokenPrior = token; | |
| break; | |
| default: | |
| throw new Exception("Unkown context: " ~ to!(string)(context)); | |
| } | |
| if (returnStatus.maxElement() > EmailStatusCode.rfc5322) | |
| break; | |
| } | |
| if (returnStatus.maxElement() < EmailStatusCode.rfc5322) | |
| { | |
| if (context == EmailPart.contextQuotedString) | |
| returnStatus ~= EmailStatusCode.errorUnclosedQuotedString; | |
| else if (context == EmailPart.contextQuotedPair) | |
| returnStatus ~= EmailStatusCode.errorBackslashEnd; | |
| else if (context == EmailPart.contextComment) | |
| returnStatus ~= EmailStatusCode.errorUnclosedComment; | |
| else if (context == EmailPart.componentLiteral) | |
| returnStatus ~= EmailStatusCode.errorUnclosedDomainLiteral; | |
| else if (token == Token.cr) | |
| returnStatus ~= EmailStatusCode.errorFoldingWhitespaceCrLfEnd; | |
| else if (parseData[EmailPart.componentDomain] == "") | |
| returnStatus ~= EmailStatusCode.errorNoDomain; | |
| else if (elementLength == 0) | |
| returnStatus ~= EmailStatusCode.errorDotEnd; | |
| else if (hyphenFlag) | |
| returnStatus ~= EmailStatusCode.errorDomainHyphenEnd; | |
| else if (parseData[EmailPart.componentDomain].length > 255) | |
| returnStatus ~= EmailStatusCode.rfc5322DomainTooLong; | |
| else if ((parseData[EmailPart.componentLocalPart] ~ Token.at ~ parseData[EmailPart.componentDomain]).length > | |
| 254) | |
| returnStatus ~= EmailStatusCode.rfc5322TooLong; | |
| else if (elementLength > 63) | |
| returnStatus ~= EmailStatusCode.rfc5322LabelTooLong; | |
| } | |
| auto dnsChecked = false; | |
| if (checkDNS == Yes.checkDns && returnStatus.maxElement() < EmailStatusCode.dnsWarning) | |
| { | |
| assert(false, "DNS check is currently not implemented"); | |
| } | |
| if (!dnsChecked && returnStatus.maxElement() < EmailStatusCode.dnsWarning) | |
| { | |
| if (elementCount == 0) | |
| returnStatus ~= EmailStatusCode.rfc5321TopLevelDomain; | |
| if (isNumber(atomList[EmailPart.componentDomain][elementCount].front)) | |
| returnStatus ~= EmailStatusCode.rfc5321TopLevelDomainNumeric; | |
| } | |
| returnStatus = array(uniq(returnStatus)); | |
| auto finalStatus = returnStatus.maxElement(); | |
| if (returnStatus.length != 1) | |
| returnStatus.popFront(); | |
| parseData[EmailPart.status] = to!(tstring)(returnStatus); | |
| if (finalStatus < threshold) | |
| finalStatus = EmailStatusCode.valid; | |
| if (!diagnose) | |
| finalStatus = finalStatus < threshold ? EmailStatusCode.valid : EmailStatusCode.error; | |
| auto valid = finalStatus == EmailStatusCode.valid; | |
| tstring localPart = ""; | |
| tstring domainPart = ""; | |
| if (auto value = EmailPart.componentLocalPart in parseData) | |
| localPart = *value; | |
| if (auto value = EmailPart.componentDomain in parseData) | |
| domainPart = *value; | |
| return EmailStatus(valid, to!(string)(localPart), to!(string)(domainPart), finalStatus); | |
| } | |
| @safe unittest | |
| { | |
| assert(`test.test@iana.org`.isEmail(No.checkDns).statusCode == EmailStatusCode.valid); | |
| assert(`test.test@iana.org`.isEmail(No.checkDns, EmailStatusCode.none).statusCode == EmailStatusCode.valid); | |
| assert(`test@[IPv6:1111:2222:3333:4444:5555:6666::8888]`.isEmail(No.checkDns, | |
| EmailStatusCode.none).statusCode == EmailStatusCode.valid); | |
| assert(`test`.isEmail(No.checkDns, EmailStatusCode.none).statusCode == EmailStatusCode.error); | |
| assert(`(comment)test@iana.org`.isEmail(No.checkDns, EmailStatusCode.none).statusCode == EmailStatusCode.error); | |
| assert(``.isEmail(No.checkDns, EmailStatusCode.any).statusCode == EmailStatusCode.errorNoDomain); | |
| assert(`test`.isEmail(No.checkDns, EmailStatusCode.any).statusCode == EmailStatusCode.errorNoDomain); | |
| assert(`@`.isEmail(No.checkDns, EmailStatusCode.any).statusCode == EmailStatusCode.errorNoLocalPart); | |
| assert(`test@`.isEmail(No.checkDns, EmailStatusCode.any).statusCode == EmailStatusCode.errorNoDomain); | |
| // assert(`test@io`.isEmail(No.checkDns, EmailStatusCode.any).statusCode == EmailStatusCode.valid, | |
| // `io. currently has an MX-record (Feb 2011). Some DNS setups seem to find it, some don't.` | |
| // ` If you don't see the MX for io. then try setting your DNS server to 8.8.8.8 (the Google DNS server)`); | |
| assert(`@io`.isEmail(No.checkDns, EmailStatusCode.any).statusCode == EmailStatusCode.errorNoLocalPart, | |
| `io. currently has an MX-record (Feb 2011)`); | |
| assert(`@iana.org`.isEmail(No.checkDns, EmailStatusCode.any).statusCode == EmailStatusCode.errorNoLocalPart); | |
| assert(`test@iana.org`.isEmail(No.checkDns, EmailStatusCode.any).statusCode == EmailStatusCode.valid); | |
| assert(`test@nominet.org.uk`.isEmail(No.checkDns, EmailStatusCode.any).statusCode == EmailStatusCode.valid); | |
| assert(`test@about.museum`.isEmail(No.checkDns, EmailStatusCode.any).statusCode == EmailStatusCode.valid); | |
| assert(`a@iana.org`.isEmail(No.checkDns, EmailStatusCode.any).statusCode == EmailStatusCode.valid); | |
| //assert(`test@e.com`.isEmail(No.checkDns, EmailStatusCode.any).statusCode == EmailStatusCode.dnsWarningNoRecord); | |
| // DNS check is currently not implemented | |
| //assert(`test@iana.a`.isEmail(No.checkDns, EmailStatusCode.any).statusCode == EmailStatusCode.dnsWarningNoRecord); | |
| // DNS check is currently not implemented | |
| assert(`test.test@iana.org`.isEmail(No.checkDns, EmailStatusCode.any).statusCode == EmailStatusCode.valid); | |
| assert(`.test@iana.org`.isEmail(No.checkDns, EmailStatusCode.any).statusCode == EmailStatusCode.errorDotStart); | |
| assert(`test.@iana.org`.isEmail(No.checkDns, EmailStatusCode.any).statusCode == EmailStatusCode.errorDotEnd); | |
| assert(`test .. iana.org`.isEmail(No.checkDns, EmailStatusCode.any).statusCode == | |
| EmailStatusCode.errorConsecutiveDots); | |
| assert(`test_exa-mple.com`.isEmail(No.checkDns, EmailStatusCode.any).statusCode == EmailStatusCode.errorNoDomain); | |
| assert("!#$%&`*+/=?^`{|}~@iana.org".isEmail(No.checkDns, EmailStatusCode.any).statusCode == EmailStatusCode.valid); | |
| assert(`test\@test@iana.org`.isEmail(No.checkDns, EmailStatusCode.any).statusCode == | |
| EmailStatusCode.errorExpectingText); | |
| assert(`123@iana.org`.isEmail(No.checkDns, EmailStatusCode.any).statusCode == EmailStatusCode.valid); | |
| assert(`test@123.com`.isEmail(No.checkDns, EmailStatusCode.any).statusCode == EmailStatusCode.valid); | |
| assert(`test@iana.123`.isEmail(No.checkDns, EmailStatusCode.any).statusCode == | |
| EmailStatusCode.rfc5321TopLevelDomainNumeric); | |
| assert(`test@255.255.255.255`.isEmail(No.checkDns, EmailStatusCode.any).statusCode == | |
| EmailStatusCode.rfc5321TopLevelDomainNumeric); | |
| assert(`abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghiklm@iana.org`.isEmail(No.checkDns, | |
| EmailStatusCode.any).statusCode == EmailStatusCode.valid); | |
| assert(`abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghiklmn@iana.org`.isEmail(No.checkDns, | |
| EmailStatusCode.any).statusCode == EmailStatusCode.rfc5322LocalTooLong); | |
| // assert(`test@abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghikl.com`.isEmail(No.checkDns, | |
| // EmailStatusCode.any).statusCode == EmailStatusCode.dnsWarningNoRecord); | |
| // DNS check is currently not implemented | |
| assert(`test@abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghiklm.com`.isEmail(No.checkDns, | |
| EmailStatusCode.any).statusCode == EmailStatusCode.rfc5322LabelTooLong); | |
| assert(`test@mason-dixon.com`.isEmail(No.checkDns, EmailStatusCode.any).statusCode == EmailStatusCode.valid); | |
| assert(`test@-iana.org`.isEmail(No.checkDns, EmailStatusCode.any).statusCode == | |
| EmailStatusCode.errorDomainHyphenStart); | |
| assert(`test@iana-.com`.isEmail(No.checkDns, EmailStatusCode.any).statusCode == | |
| EmailStatusCode.errorDomainHyphenEnd); | |
| assert(`test@g--a.com`.isEmail(No.checkDns, EmailStatusCode.any).statusCode == EmailStatusCode.valid); | |
| //assert(`test@iana.co-uk`.isEmail(No.checkDns, EmailStatusCode.any).statusCode == | |
| //EmailStatusCode.dnsWarningNoRecord); // DNS check is currently not implemented | |
| assert(`test@.iana.org`.isEmail(No.checkDns, EmailStatusCode.any).statusCode == EmailStatusCode.errorDotStart); | |
| assert(`test@iana.org.`.isEmail(No.checkDns, EmailStatusCode.any).statusCode == EmailStatusCode.errorDotEnd); | |
| assert(`test@iana .. com`.isEmail(No.checkDns, EmailStatusCode.any).statusCode == | |
| EmailStatusCode.errorConsecutiveDots); | |
| //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` | |
| // `.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` | |
| // `.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 == | |
| // EmailStatusCode.dnsWarningNoRecord); // DNS check is currently not implemented | |
| // assert(`abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghiklm@abcdefghijklmnopqrstuvwxyz` | |
| // `abcdefghijklmnopqrstuvwxyzabcdefghikl.abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghikl.` | |
| // `abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghi`.isEmail(No.checkDns, | |
| // EmailStatusCode.any).statusCode == EmailStatusCode.dnsWarningNoRecord); | |
| // DNS check is currently not implemented | |
| assert((`abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghiklm@abcdefghijklmnopqrstuvwxyz`~ | |
| `abcdefghijklmnopqrstuvwxyzabcdefghikl.abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghikl.`~ | |
| `abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghij`).isEmail(No.checkDns, | |
| EmailStatusCode.any).statusCode == EmailStatusCode.rfc5322TooLong); | |
| assert((`a@abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghikl.abcdefghijklmnopqrstuvwxyz`~ | |
| `abcdefghijklmnopqrstuvwxyzabcdefghikl.abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghikl.`~ | |
| `abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefg.hij`).isEmail(No.checkDns, | |
| EmailStatusCode.any).statusCode == EmailStatusCode.rfc5322TooLong); | |
| assert((`a@abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghikl.abcdefghijklmnopqrstuvwxyz`~ | |
| `abcdefghijklmnopqrstuvwxyzabcdefghikl.abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghikl.`~ | |
| `abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefg.hijk`).isEmail(No.checkDns, | |
| EmailStatusCode.any).statusCode == EmailStatusCode.rfc5322DomainTooLong); | |
| assert(`"test"@iana.org`.isEmail(No.checkDns, EmailStatusCode.any).statusCode == | |
| EmailStatusCode.rfc5321QuotedString); | |
| assert(`""@iana.org`.isEmail(No.checkDns, EmailStatusCode.any).statusCode == EmailStatusCode.rfc5321QuotedString); | |
| assert(`"""@iana.org`.isEmail(No.checkDns, EmailStatusCode.any).statusCode == EmailStatusCode.errorExpectingText); | |
| assert(`"\a"@iana.org`.isEmail(No.checkDns, EmailStatusCode.any).statusCode == EmailStatusCode.rfc5321QuotedString); | |
| assert(`"\""@iana.org`.isEmail(No.checkDns, EmailStatusCode.any).statusCode == EmailStatusCode.rfc5321QuotedString); | |
| assert(`"\"@iana.org`.isEmail(No.checkDns, EmailStatusCode.any).statusCode == | |
| EmailStatusCode.errorUnclosedQuotedString); | |
| assert(`"\\"@iana.org`.isEmail(No.checkDns, EmailStatusCode.any).statusCode == EmailStatusCode.rfc5321QuotedString); | |
| assert(`test"@iana.org`.isEmail(No.checkDns, EmailStatusCode.any).statusCode == EmailStatusCode.errorExpectingText); | |
| assert(`"test@iana.org`.isEmail(No.checkDns, EmailStatusCode.any).statusCode == | |
| EmailStatusCode.errorUnclosedQuotedString); | |
| assert(`"test"test@iana.org`.isEmail(No.checkDns, EmailStatusCode.any).statusCode == | |
| EmailStatusCode.errorTextAfterQuotedString); | |
| assert(`test"text"@iana.org`.isEmail(No.checkDns, EmailStatusCode.any).statusCode == | |
| EmailStatusCode.errorExpectingText); | |
| assert(`"test""test"@iana.org`.isEmail(No.checkDns, EmailStatusCode.any).statusCode == | |
| EmailStatusCode.errorExpectingText); | |
| assert(`"test"."test"@iana.org`.isEmail(No.checkDns, EmailStatusCode.any).statusCode == | |
| EmailStatusCode.deprecatedLocalPart); | |
| assert(`"test\ test"@iana.org`.isEmail(No.checkDns, EmailStatusCode.any).statusCode == | |
| EmailStatusCode.rfc5321QuotedString); | |
| assert(`"test".test@iana.org`.isEmail(No.checkDns, EmailStatusCode.any).statusCode == | |
| EmailStatusCode.deprecatedLocalPart); | |
| assert("\"test\u0000\"@iana.org".isEmail(No.checkDns, EmailStatusCode.any).statusCode == | |
| EmailStatusCode.errorExpectingQuotedText); | |
| assert("\"test\\\u0000\"@iana.org".isEmail(No.checkDns, EmailStatusCode.any).statusCode == | |
| EmailStatusCode.deprecatedQuotedPair); | |
| assert(`"abcdefghijklmnopqrstuvwxyz abcdefghijklmnopqrstuvwxyz abcdefghj"@iana.org`.isEmail(No.checkDns, | |
| EmailStatusCode.any).statusCode == EmailStatusCode.rfc5322LocalTooLong, | |
| `Quotes are still part of the length restriction`); | |
| assert(`"abcdefghijklmnopqrstuvwxyz abcdefghijklmnopqrstuvwxyz abcdefg\h"@iana.org`.isEmail(No.checkDns, | |
| EmailStatusCode.any).statusCode == EmailStatusCode.rfc5322LocalTooLong, | |
| `Quoted pair is still part of the length restriction`); | |
| assert(`test@[255.255.255.255]`.isEmail(No.checkDns, EmailStatusCode.any).statusCode == | |
| EmailStatusCode.rfc5321AddressLiteral); | |
| assert(`test@a[255.255.255.255]`.isEmail(No.checkDns, EmailStatusCode.any).statusCode == | |
| EmailStatusCode.errorExpectingText); | |
| assert(`test@[255.255.255]`.isEmail(No.checkDns, EmailStatusCode.any).statusCode == | |
| EmailStatusCode.rfc5322DomainLiteral); | |
| assert(`test@[255.255.255.255.255]`.isEmail(No.checkDns, EmailStatusCode.any).statusCode == | |
| EmailStatusCode.rfc5322DomainLiteral); | |
| assert(`test@[255.255.255.256]`.isEmail(No.checkDns, EmailStatusCode.any).statusCode == | |
| EmailStatusCode.rfc5322DomainLiteral); | |
| assert(`test@[1111:2222:3333:4444:5555:6666:7777:8888]`.isEmail(No.checkDns, EmailStatusCode.any).statusCode == | |
| EmailStatusCode.rfc5322DomainLiteral); | |
| assert(`test@[IPv6:1111:2222:3333:4444:5555:6666:7777]`.isEmail(No.checkDns, EmailStatusCode.any).statusCode == | |
| EmailStatusCode.rfc5322IpV6GroupCount); | |
| assert(`test@[IPv6:1111:2222:3333:4444:5555:6666:7777:8888]`.isEmail(No.checkDns, EmailStatusCode.any).statusCode | |
| == EmailStatusCode.rfc5321AddressLiteral); | |
| assert(`test@[IPv6:1111:2222:3333:4444:5555:6666:7777:8888:9999]`.isEmail(No.checkDns, | |
| EmailStatusCode.any).statusCode == EmailStatusCode.rfc5322IpV6GroupCount); | |
| assert(`test@[IPv6:1111:2222:3333:4444:5555:6666:7777:888G]`.isEmail(No.checkDns, | |
| EmailStatusCode.any).statusCode == EmailStatusCode.rfc5322IpV6BadChar); | |
| assert(`test@[IPv6:1111:2222:3333:4444:5555:6666::8888]`.isEmail(No.checkDns, | |
| EmailStatusCode.any).statusCode == EmailStatusCode.rfc5321IpV6Deprecated); | |
| assert(`test@[IPv6:1111:2222:3333:4444:5555::8888]`.isEmail(No.checkDns, EmailStatusCode.any).statusCode == | |
| EmailStatusCode.rfc5321AddressLiteral); | |
| assert(`test@[IPv6:1111:2222:3333:4444:5555:6666::7777:8888]`.isEmail(No.checkDns, | |
| EmailStatusCode.any).statusCode == EmailStatusCode.rfc5322IpV6MaxGroups); | |
| assert(`test@[IPv6::3333:4444:5555:6666:7777:8888]`.isEmail(No.checkDns, EmailStatusCode.any).statusCode == | |
| EmailStatusCode.rfc5322IpV6ColonStart); | |
| assert(`test@[IPv6:::3333:4444:5555:6666:7777:8888]`.isEmail(No.checkDns, EmailStatusCode.any).statusCode == | |
| EmailStatusCode.rfc5321AddressLiteral); | |
| assert(`test@[IPv6:1111::4444:5555::8888]`.isEmail(No.checkDns, EmailStatusCode.any).statusCode == | |
| EmailStatusCode.rfc5322IpV6TooManyDoubleColons); | |
| assert(`test@[IPv6:::]`.isEmail(No.checkDns, EmailStatusCode.any).statusCode == | |
| EmailStatusCode.rfc5321AddressLiteral); | |
| assert(`test@[IPv6:1111:2222:3333:4444:5555:255.255.255.255]`.isEmail(No.checkDns, | |
| EmailStatusCode.any).statusCode == EmailStatusCode.rfc5322IpV6GroupCount); | |
| assert(`test@[IPv6:1111:2222:3333:4444:5555:6666:255.255.255.255]`.isEmail(No.checkDns, | |
| EmailStatusCode.any).statusCode == EmailStatusCode.rfc5321AddressLiteral); | |
| assert(`test@[IPv6:1111:2222:3333:4444:5555:6666:7777:255.255.255.255]`.isEmail(No.checkDns, | |
| EmailStatusCode.any).statusCode == EmailStatusCode.rfc5322IpV6GroupCount); | |
| assert(`test@[IPv6:1111:2222:3333:4444::255.255.255.255]`.isEmail(No.checkDns, | |
| EmailStatusCode.any).statusCode == EmailStatusCode.rfc5321AddressLiteral); | |
| assert(`test@[IPv6:1111:2222:3333:4444:5555:6666::255.255.255.255]`.isEmail(No.checkDns, | |
| EmailStatusCode.any).statusCode == EmailStatusCode.rfc5322IpV6MaxGroups); | |
| assert(`test@[IPv6:1111:2222:3333:4444:::255.255.255.255]`.isEmail(No.checkDns, EmailStatusCode.any).statusCode | |
| == EmailStatusCode.rfc5322IpV6TooManyDoubleColons); | |
| assert(`test@[IPv6::255.255.255.255]`.isEmail(No.checkDns, EmailStatusCode.any).statusCode == | |
| EmailStatusCode.rfc5322IpV6ColonStart); | |
| assert(` test @iana.org`.isEmail(No.checkDns, EmailStatusCode.any).statusCode == | |
| EmailStatusCode.deprecatedCommentFoldingWhitespaceNearAt); | |
| assert(`test@ iana .com`.isEmail(No.checkDns, EmailStatusCode.any).statusCode == | |
| EmailStatusCode.deprecatedCommentFoldingWhitespaceNearAt); | |
| assert(`test . test@iana.org`.isEmail(No.checkDns, EmailStatusCode.any).statusCode == | |
| EmailStatusCode.deprecatedFoldingWhitespace); | |
| assert("\u000D\u000A test@iana.org".isEmail(No.checkDns, EmailStatusCode.any).statusCode == | |
| EmailStatusCode.foldingWhitespace, `Folding whitespace`); | |
| assert("\u000D\u000A \u000D\u000A test@iana.org".isEmail(No.checkDns, EmailStatusCode.any).statusCode == | |
| EmailStatusCode.deprecatedFoldingWhitespace, `FWS with one line composed entirely of WSP`~ | |
| ` -- only allowed as obsolete FWS (someone might allow only non-obsolete FWS)`); | |
| assert(`(comment)test@iana.org`.isEmail(No.checkDns, EmailStatusCode.any).statusCode == EmailStatusCode.comment); | |
| assert(`((comment)test@iana.org`.isEmail(No.checkDns, EmailStatusCode.any).statusCode == | |
| EmailStatusCode.errorUnclosedComment); | |
| assert(`(comment(comment))test@iana.org`.isEmail(No.checkDns, EmailStatusCode.any).statusCode == | |
| EmailStatusCode.comment); | |
| assert(`test@(comment)iana.org`.isEmail(No.checkDns, EmailStatusCode.any).statusCode == | |
| EmailStatusCode.deprecatedCommentFoldingWhitespaceNearAt); | |
| assert(`test(comment)test@iana.org`.isEmail(No.checkDns, EmailStatusCode.any).statusCode == | |
| EmailStatusCode.errorTextAfterCommentFoldingWhitespace); | |
| assert(`test@(comment)[255.255.255.255]`.isEmail(No.checkDns, EmailStatusCode.any).statusCode == | |
| EmailStatusCode.deprecatedCommentFoldingWhitespaceNearAt); | |
| assert(`(comment)abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghiklm@iana.org`.isEmail(No.checkDns, | |
| EmailStatusCode.any).statusCode == EmailStatusCode.comment); | |
| assert(`test@(comment)abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghikl.com`.isEmail(No.checkDns, | |
| EmailStatusCode.any).statusCode == EmailStatusCode.deprecatedCommentFoldingWhitespaceNearAt); | |
| assert((`(comment)test@abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghik.abcdefghijklmnopqrstuvwxyz`~ | |
| `abcdefghijklmnopqrstuvwxyzabcdefghik.abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijk.`~ | |
| `abcdefghijklmnopqrstuvwxyzabcdefghijk.abcdefghijklmnopqrstu`).isEmail(No.checkDns, | |
| EmailStatusCode.any).statusCode == EmailStatusCode.comment); | |
| assert("test@iana.org\u000A".isEmail(No.checkDns, EmailStatusCode.any).statusCode == | |
| EmailStatusCode.errorExpectingText); | |
| assert(`test@xn--hxajbheg2az3al.xn--jxalpdlp`.isEmail(No.checkDns, EmailStatusCode.any).statusCode == | |
| EmailStatusCode.valid, `A valid IDN from ICANN's <a href="http://idn.icann.org/#The_example.test_names">`~ | |
| `IDN TLD evaluation gateway</a>`); | |
| assert(`xn--test@iana.org`.isEmail(No.checkDns, EmailStatusCode.any).statusCode == EmailStatusCode.valid, | |
| `RFC 3490: "unless the email standards are revised to invite the use of IDNA for local parts, a domain label`~ | |
| ` that holds the local part of an email address SHOULD NOT begin with the ACE prefix, and even if it does,`~ | |
| ` it is to be interpreted literally as a local part that happens to begin with the ACE prefix"`); | |
| assert(`test@iana.org-`.isEmail(No.checkDns, EmailStatusCode.any).statusCode == | |
| EmailStatusCode.errorDomainHyphenEnd); | |
| assert(`"test@iana.org`.isEmail(No.checkDns, EmailStatusCode.any).statusCode == | |
| EmailStatusCode.errorUnclosedQuotedString); | |
| assert(`(test@iana.org`.isEmail(No.checkDns, EmailStatusCode.any).statusCode == | |
| EmailStatusCode.errorUnclosedComment); | |
| assert(`test@(iana.org`.isEmail(No.checkDns, EmailStatusCode.any).statusCode == | |
| EmailStatusCode.errorUnclosedComment); | |
| assert(`test@[1.2.3.4`.isEmail(No.checkDns, EmailStatusCode.any).statusCode == | |
| EmailStatusCode.errorUnclosedDomainLiteral); | |
| assert(`"test\"@iana.org`.isEmail(No.checkDns, EmailStatusCode.any).statusCode == | |
| EmailStatusCode.errorUnclosedQuotedString); | |
| assert(`(comment\)test@iana.org`.isEmail(No.checkDns, EmailStatusCode.any).statusCode == | |
| EmailStatusCode.errorUnclosedComment); | |
| assert(`test@iana.org(comment\)`.isEmail(No.checkDns, EmailStatusCode.any).statusCode == | |
| EmailStatusCode.errorUnclosedComment); | |
| assert(`test@iana.org(comment\`.isEmail(No.checkDns, EmailStatusCode.any).statusCode == | |
| EmailStatusCode.errorBackslashEnd); | |
| assert(`test@[RFC-5322-domain-literal]`.isEmail(No.checkDns, EmailStatusCode.any).statusCode == | |
| EmailStatusCode.rfc5322DomainLiteral); | |
| assert(`test@[RFC-5322]-domain-literal]`.isEmail(No.checkDns, EmailStatusCode.any).statusCode == | |
| EmailStatusCode.errorTextAfterDomainLiteral); | |
| assert(`test@[RFC-5322-[domain-literal]`.isEmail(No.checkDns, EmailStatusCode.any).statusCode == | |
| EmailStatusCode.errorExpectingDomainText); | |
| assert("test@[RFC-5322-\\\u0007-domain-literal]".isEmail(No.checkDns, EmailStatusCode.any).statusCode == | |
| EmailStatusCode.rfc5322DomainLiteralObsoleteText, `obs-dtext <strong>and</strong> obs-qp`); | |
| assert("test@[RFC-5322-\\\u0009-domain-literal]".isEmail(No.checkDns, EmailStatusCode.any).statusCode == | |
| EmailStatusCode.rfc5322DomainLiteralObsoleteText); | |
| assert(`test@[RFC-5322-\]-domain-literal]`.isEmail(No.checkDns, EmailStatusCode.any).statusCode == | |
| EmailStatusCode.rfc5322DomainLiteralObsoleteText); | |
| assert(`test@[RFC-5322-domain-literal\]`.isEmail(No.checkDns, EmailStatusCode.any).statusCode == | |
| EmailStatusCode.errorUnclosedDomainLiteral); | |
| assert(`test@[RFC-5322-domain-literal\`.isEmail(No.checkDns, EmailStatusCode.any).statusCode == | |
| EmailStatusCode.errorBackslashEnd); | |
| assert(`test@[RFC 5322 domain literal]`.isEmail(No.checkDns, EmailStatusCode.any).statusCode == | |
| EmailStatusCode.rfc5322DomainLiteral, `Spaces are FWS in a domain literal`); | |
| assert(`test@[RFC-5322-domain-literal] (comment)`.isEmail(No.checkDns, EmailStatusCode.any).statusCode == | |
| EmailStatusCode.rfc5322DomainLiteral); | |
| assert("\u007F@iana.org".isEmail(No.checkDns, EmailStatusCode.any).statusCode == | |
| EmailStatusCode.errorExpectingText); | |
| assert("test@\u007F.org".isEmail(No.checkDns, EmailStatusCode.any).statusCode == | |
| EmailStatusCode.errorExpectingText); | |
| assert("\"\u007F\"@iana.org".isEmail(No.checkDns, EmailStatusCode.any).statusCode == | |
| EmailStatusCode.deprecatedQuotedText); | |
| assert("\"\\\u007F\"@iana.org".isEmail(No.checkDns, EmailStatusCode.any).statusCode == | |
| EmailStatusCode.deprecatedQuotedPair); | |
| assert("(\u007F)test@iana.org".isEmail(No.checkDns, EmailStatusCode.any).statusCode == | |
| EmailStatusCode.deprecatedCommentText); | |
| assert("test@iana.org\u000D".isEmail(No.checkDns, EmailStatusCode.any).statusCode == EmailStatusCode.errorCrNoLf, | |
| `No LF after the CR`); | |
| assert("\u000Dtest@iana.org".isEmail(No.checkDns, EmailStatusCode.any).statusCode == EmailStatusCode.errorCrNoLf, | |
| `No LF after the CR`); | |
| assert("\"\u000Dtest\"@iana.org".isEmail(No.checkDns, EmailStatusCode.any).statusCode == | |
| EmailStatusCode.errorCrNoLf, `No LF after the CR`); | |
| assert("(\u000D)test@iana.org".isEmail(No.checkDns, EmailStatusCode.any).statusCode == EmailStatusCode.errorCrNoLf, | |
| `No LF after the CR`); | |
| assert("(\u000D".isEmail(No.checkDns, EmailStatusCode.any).statusCode == EmailStatusCode.errorCrNoLf, | |
| `No LF after the CR`); | |
| assert("test@iana.org(\u000D)".isEmail(No.checkDns, EmailStatusCode.any).statusCode == EmailStatusCode.errorCrNoLf, | |
| `No LF after the CR`); | |
| assert("\u000Atest@iana.org".isEmail(No.checkDns, EmailStatusCode.any).statusCode == | |
| EmailStatusCode.errorExpectingText); | |
| assert("\"\u000A\"@iana.org".isEmail(No.checkDns, EmailStatusCode.any).statusCode == | |
| EmailStatusCode.errorExpectingQuotedText); | |
| assert("\"\\\u000A\"@iana.org".isEmail(No.checkDns, EmailStatusCode.any).statusCode == | |
| EmailStatusCode.deprecatedQuotedPair); | |
| assert("(\u000A)test@iana.org".isEmail(No.checkDns, EmailStatusCode.any).statusCode == | |
| EmailStatusCode.errorExpectingCommentText); | |
| assert("\u0007@iana.org".isEmail(No.checkDns, EmailStatusCode.any).statusCode == | |
| EmailStatusCode.errorExpectingText); | |
| assert("test@\u0007.org".isEmail(No.checkDns, EmailStatusCode.any).statusCode == | |
| EmailStatusCode.errorExpectingText); | |
| assert("\"\u0007\"@iana.org".isEmail(No.checkDns, EmailStatusCode.any).statusCode == | |
| EmailStatusCode.deprecatedQuotedText); | |
| assert("\"\\\u0007\"@iana.org".isEmail(No.checkDns, EmailStatusCode.any).statusCode == | |
| EmailStatusCode.deprecatedQuotedPair); | |
| assert("(\u0007)test@iana.org".isEmail(No.checkDns, EmailStatusCode.any).statusCode == | |
| EmailStatusCode.deprecatedCommentText); | |
| assert("\u000D\u000Atest@iana.org".isEmail(No.checkDns, EmailStatusCode.any).statusCode == | |
| EmailStatusCode.errorFoldingWhitespaceCrLfEnd, `Not FWS because no actual white space`); | |
| assert("\u000D\u000A \u000D\u000Atest@iana.org".isEmail(No.checkDns, EmailStatusCode.any).statusCode == | |
| EmailStatusCode.errorFoldingWhitespaceCrLfEnd, `Not obs-FWS because there must be white space on each "fold"`); | |
| assert(" \u000D\u000Atest@iana.org".isEmail(No.checkDns, EmailStatusCode.any).statusCode == | |
| EmailStatusCode.errorFoldingWhitespaceCrLfEnd, `Not FWS because no white space after the fold`); | |
| assert(" \u000D\u000A test@iana.org".isEmail(No.checkDns, EmailStatusCode.any).statusCode == | |
| EmailStatusCode.foldingWhitespace, `FWS`); | |
| assert(" \u000D\u000A \u000D\u000Atest@iana.org".isEmail(No.checkDns, EmailStatusCode.any).statusCode == | |
| EmailStatusCode.errorFoldingWhitespaceCrLfEnd, `Not FWS because no white space after the second fold`); | |
| assert(" \u000D\u000A\u000D\u000Atest@iana.org".isEmail(No.checkDns, EmailStatusCode.any).statusCode == | |
| EmailStatusCode.errorFoldingWhitespaceCrflX2, `Not FWS because no white space after either fold`); | |
| assert(" \u000D\u000A\u000D\u000A test@iana.org".isEmail(No.checkDns, EmailStatusCode.any).statusCode == | |
| EmailStatusCode.errorFoldingWhitespaceCrflX2, `Not FWS because no white space after the first fold`); | |
| assert("test@iana.org\u000D\u000A ".isEmail(No.checkDns, EmailStatusCode.any).statusCode == | |
| EmailStatusCode.foldingWhitespace, `FWS`); | |
| assert("test@iana.org\u000D\u000A \u000D\u000A ".isEmail(No.checkDns, EmailStatusCode.any).statusCode == | |
| EmailStatusCode.deprecatedFoldingWhitespace, `FWS with one line composed entirely of WSP -- `~ | |
| `only allowed as obsolete FWS (someone might allow only non-obsolete FWS)`); | |
| assert("test@iana.org\u000D\u000A".isEmail(No.checkDns, EmailStatusCode.any).statusCode == | |
| EmailStatusCode.errorFoldingWhitespaceCrLfEnd, `Not FWS because no actual white space`); | |
| assert("test@iana.org\u000D\u000A \u000D\u000A".isEmail(No.checkDns, EmailStatusCode.any).statusCode == | |
| EmailStatusCode.errorFoldingWhitespaceCrLfEnd, `Not obs-FWS because there must be white space on each "fold"`); | |
| assert("test@iana.org \u000D\u000A".isEmail(No.checkDns, EmailStatusCode.any).statusCode == | |
| EmailStatusCode.errorFoldingWhitespaceCrLfEnd, `Not FWS because no white space after the fold`); | |
| assert("test@iana.org \u000D\u000A ".isEmail(No.checkDns, EmailStatusCode.any).statusCode == | |
| EmailStatusCode.foldingWhitespace, `FWS`); | |
| assert("test@iana.org \u000D\u000A \u000D\u000A".isEmail(No.checkDns, EmailStatusCode.any).statusCode == | |
| EmailStatusCode.errorFoldingWhitespaceCrLfEnd, `Not FWS because no white space after the second fold`); | |
| assert("test@iana.org \u000D\u000A\u000D\u000A".isEmail(No.checkDns, EmailStatusCode.any).statusCode == | |
| EmailStatusCode.errorFoldingWhitespaceCrflX2, `Not FWS because no white space after either fold`); | |
| assert("test@iana.org \u000D\u000A\u000D\u000A ".isEmail(No.checkDns, EmailStatusCode.any).statusCode == | |
| EmailStatusCode.errorFoldingWhitespaceCrflX2, `Not FWS because no white space after the first fold`); | |
| assert(" test@iana.org".isEmail(No.checkDns, EmailStatusCode.any).statusCode == EmailStatusCode.foldingWhitespace); | |
| assert(`test@iana.org `.isEmail(No.checkDns, EmailStatusCode.any).statusCode == EmailStatusCode.foldingWhitespace); | |
| assert(`test@[IPv6:1::2:]`.isEmail(No.checkDns, EmailStatusCode.any).statusCode == | |
| EmailStatusCode.rfc5322IpV6ColonEnd); | |
| assert("\"test\\\u00A9\"@iana.org".isEmail(No.checkDns, EmailStatusCode.any).statusCode == | |
| EmailStatusCode.errorExpectingQuotedPair); | |
| assert(`test@iana/icann.org`.isEmail(No.checkDns, EmailStatusCode.any).statusCode == EmailStatusCode.rfc5322Domain); | |
| assert(`test.(comment)test@iana.org`.isEmail(No.checkDns, EmailStatusCode.any).statusCode == | |
| EmailStatusCode.deprecatedComment); | |
| assert(`test@org`.isEmail(No.checkDns, EmailStatusCode.any).statusCode == EmailStatusCode.rfc5321TopLevelDomain); | |
| // assert(`test@test.com`.isEmail(No.checkDns, EmailStatusCode.any).statusCode == | |
| //EmailStatusCode.dnsWarningNoMXRecord, `test.com has an A-record but not an MX-record`); | |
| // DNS check is currently not implemented | |
| // | |
| // assert(`test@nic.no`.isEmail(No.checkDns, EmailStatusCode.any).statusCode == EmailStatusCode.dnsWarningNoRecord, | |
| // `nic.no currently has no MX-records or A-records (Feb 2011). If you are seeing an A-record for nic.io then` | |
| // ` try setting your DNS server to 8.8.8.8 (the Google DNS server) - your DNS server may be faking an A-record` | |
| // ` (OpenDNS does this, for instance).`); // DNS check is currently not implemented | |
| } | |
| // https://issues.dlang.org/show_bug.cgi?id=17217 | |
| @safe unittest | |
| { | |
| wstring a = `test.test@iana.org`w; | |
| dstring b = `test.test@iana.org`d; | |
| const(wchar)[] c = `test.test@iana.org`w; | |
| const(dchar)[] d = `test.test@iana.org`d; | |
| assert(a.isEmail(No.checkDns).statusCode == EmailStatusCode.valid); | |
| assert(b.isEmail(No.checkDns).statusCode == EmailStatusCode.valid); | |
| assert(c.isEmail(No.checkDns).statusCode == EmailStatusCode.valid); | |
| assert(d.isEmail(No.checkDns).statusCode == EmailStatusCode.valid); | |
| } | |
| /** | |
| * Flag for indicating if the isEmail function should perform a DNS check or not. | |
| * | |
| * If set to `CheckDns.no`, isEmail does not perform DNS checking. | |
| * | |
| * Otherwise if set to `CheckDns.yes`, isEmail performs DNS checking. | |
| */ | |
| alias CheckDns = Flag!"checkDns"; | |
| /// Represents the status of an email address | |
| struct EmailStatus | |
| { | |
| private | |
| { | |
| bool valid_; | |
| string localPart_; | |
| string domainPart_; | |
| EmailStatusCode statusCode_; | |
| } | |
| /// Self aliases to a `bool` representing if the email is valid or not | |
| alias valid this; | |
| /* | |
| * Params: | |
| * valid = indicates if the email address is valid or not | |
| * localPart = the local part of the email address | |
| * domainPart = the domain part of the email address | |
| * statusCode = the status code | |
| */ | |
| private this (bool valid, string localPart, string domainPart, EmailStatusCode statusCode) @safe @nogc pure nothrow | |
| { | |
| this.valid_ = valid; | |
| this.localPart_ = localPart; | |
| this.domainPart_ = domainPart; | |
| this.statusCode_ = statusCode; | |
| } | |
| /// Returns: If the email address is valid or not. | |
| @property bool valid() const @safe @nogc pure nothrow scope | |
| { | |
| return valid_; | |
| } | |
| /// Returns: The local part of the email address, that is, the part before the @ sign. | |
| @property string localPart() const @safe @nogc pure nothrow return scope | |
| { | |
| return localPart_; | |
| } | |
| /// Returns: The domain part of the email address, that is, the part after the @ sign. | |
| @property string domainPart() const @safe @nogc pure nothrow return scope | |
| { | |
| return domainPart_; | |
| } | |
| /// Returns: The email status code | |
| @property EmailStatusCode statusCode() const @safe @nogc pure nothrow scope | |
| { | |
| return statusCode_; | |
| } | |
| /// Returns: A describing string of the status code | |
| @property string status() const @safe @nogc pure nothrow scope | |
| { | |
| return statusCodeDescription(statusCode_); | |
| } | |
| /// Returns: A textual representation of the email status | |
| string toString() const @safe pure scope | |
| { | |
| import std.format : format; | |
| return format("EmailStatus\n{\n\tvalid: %s\n\tlocalPart: %s\n\tdomainPart: %s\n\tstatusCode: %s\n}", valid, | |
| localPart, domainPart, statusCode); | |
| } | |
| } | |
| /** | |
| * Params: | |
| * statusCode = The $(LREF EmailStatusCode) to read | |
| * Returns: | |
| * A detailed string describing the given status code | |
| */ | |
| string statusCodeDescription(EmailStatusCode statusCode) @safe @nogc pure nothrow | |
| { | |
| final switch (statusCode) | |
| { | |
| // Categories | |
| case EmailStatusCode.validCategory: return "Address is valid"; | |
| case EmailStatusCode.dnsWarning: return "Address is valid but a DNS check was not successful"; | |
| case EmailStatusCode.rfc5321: return "Address is valid for SMTP but has unusual elements"; | |
| case EmailStatusCode.cFoldingWhitespace: return "Address is valid within the message but cannot be used"~ | |
| " unmodified for the envelope"; | |
| case EmailStatusCode.deprecated_: return "Address contains deprecated elements but may still be valid in"~ | |
| " restricted contexts"; | |
| case EmailStatusCode.rfc5322: return "The address is only valid according to the broad definition of RFC 5322."~ | |
| " It is otherwise invalid"; | |
| case EmailStatusCode.any: return ""; | |
| case EmailStatusCode.none: return ""; | |
| case EmailStatusCode.warning: return ""; | |
| case EmailStatusCode.error: return "Address is invalid for any purpose"; | |
| // Diagnoses | |
| case EmailStatusCode.valid: return "Address is valid"; | |
| // Address is valid but a DNS check was not successful | |
| case EmailStatusCode.dnsWarningNoMXRecord: return "Could not find an MX record for this domain but an A-record"~ | |
| " does exist"; | |
| case EmailStatusCode.dnsWarningNoRecord: return "Could not find an MX record or an A-record for this domain"; | |
| // Address is valid for SMTP but has unusual elements | |
| case EmailStatusCode.rfc5321TopLevelDomain: return "Address is valid but at a Top Level Domain"; | |
| case EmailStatusCode.rfc5321TopLevelDomainNumeric: return "Address is valid but the Top Level Domain begins"~ | |
| " with a number"; | |
| case EmailStatusCode.rfc5321QuotedString: return "Address is valid but contains a quoted string"; | |
| case EmailStatusCode.rfc5321AddressLiteral: return "Address is valid but at a literal address not a domain"; | |
| case EmailStatusCode.rfc5321IpV6Deprecated: return "Address is valid but contains a :: that only elides one"~ | |
| " zero group"; | |
| // Address is valid within the message but cannot be used unmodified for the envelope | |
| case EmailStatusCode.comment: return "Address contains comments"; | |
| case EmailStatusCode.foldingWhitespace: return "Address contains Folding White Space"; | |
| // Address contains deprecated elements but may still be valid in restricted contexts | |
| case EmailStatusCode.deprecatedLocalPart: return "The local part is in a deprecated form"; | |
| case EmailStatusCode.deprecatedFoldingWhitespace: return "Address contains an obsolete form of"~ | |
| " Folding White Space"; | |
| case EmailStatusCode.deprecatedQuotedText: return "A quoted string contains a deprecated character"; | |
| case EmailStatusCode.deprecatedQuotedPair: return "A quoted pair contains a deprecated character"; | |
| case EmailStatusCode.deprecatedComment: return "Address contains a comment in a position that is deprecated"; | |
| case EmailStatusCode.deprecatedCommentText: return "A comment contains a deprecated character"; | |
| case EmailStatusCode.deprecatedCommentFoldingWhitespaceNearAt: return "Address contains a comment or"~ | |
| " Folding White Space around the @ sign"; | |
| // The address is only valid according to the broad definition of RFC 5322 | |
| case EmailStatusCode.rfc5322Domain: return "Address is RFC 5322 compliant but contains domain characters that"~ | |
| " are not allowed by DNS"; | |
| case EmailStatusCode.rfc5322TooLong: return "Address is too long"; | |
| case EmailStatusCode.rfc5322LocalTooLong: return "The local part of the address is too long"; | |
| case EmailStatusCode.rfc5322DomainTooLong: return "The domain part is too long"; | |
| case EmailStatusCode.rfc5322LabelTooLong: return "The domain part contains an element that is too long"; | |
| case EmailStatusCode.rfc5322DomainLiteral: return "The domain literal is not a valid RFC 5321 address literal"; | |
| case EmailStatusCode.rfc5322DomainLiteralObsoleteText: return "The domain literal is not a valid RFC 5321"~ | |
| " address literal and it contains obsolete characters"; | |
| case EmailStatusCode.rfc5322IpV6GroupCount: | |
| return "The IPv6 literal address contains the wrong number of groups"; | |
| case EmailStatusCode.rfc5322IpV6TooManyDoubleColons: | |
| return "The IPv6 literal address contains too many :: sequences"; | |
| case EmailStatusCode.rfc5322IpV6BadChar: return "The IPv6 address contains an illegal group of characters"; | |
| case EmailStatusCode.rfc5322IpV6MaxGroups: return "The IPv6 address has too many groups"; | |
| case EmailStatusCode.rfc5322IpV6ColonStart: return "IPv6 address starts with a single colon"; | |
| case EmailStatusCode.rfc5322IpV6ColonEnd: return "IPv6 address ends with a single colon"; | |
| // Address is invalid for any purpose | |
| case EmailStatusCode.errorExpectingDomainText: | |
| return "A domain literal contains a character that is not allowed"; | |
| case EmailStatusCode.errorNoLocalPart: return "Address has no local part"; | |
| case EmailStatusCode.errorNoDomain: return "Address has no domain part"; | |
| case EmailStatusCode.errorConsecutiveDots: return "The address may not contain consecutive dots"; | |
| case EmailStatusCode.errorTextAfterCommentFoldingWhitespace: | |
| return "Address contains text after a comment or Folding White Space"; | |
| case EmailStatusCode.errorTextAfterQuotedString: return "Address contains text after a quoted string"; | |
| case EmailStatusCode.errorTextAfterDomainLiteral: return "Extra characters were found after the end of"~ | |
| " the domain literal"; | |
| case EmailStatusCode.errorExpectingQuotedPair: | |
| return "The address contains a character that is not allowed in a quoted pair"; | |
| case EmailStatusCode.errorExpectingText: return "Address contains a character that is not allowed"; | |
| case EmailStatusCode.errorExpectingQuotedText: | |
| return "A quoted string contains a character that is not allowed"; | |
| case EmailStatusCode.errorExpectingCommentText: return "A comment contains a character that is not allowed"; | |
| case EmailStatusCode.errorBackslashEnd: return "The address cannot end with a backslash"; | |
| case EmailStatusCode.errorDotStart: return "Neither part of the address may begin with a dot"; | |
| case EmailStatusCode.errorDotEnd: return "Neither part of the address may end with a dot"; | |
| case EmailStatusCode.errorDomainHyphenStart: return "A domain or subdomain cannot begin with a hyphen"; | |
| case EmailStatusCode.errorDomainHyphenEnd: return "A domain or subdomain cannot end with a hyphen"; | |
| case EmailStatusCode.errorUnclosedQuotedString: return "Unclosed quoted string"; | |
| case EmailStatusCode.errorUnclosedComment: return "Unclosed comment"; | |
| case EmailStatusCode.errorUnclosedDomainLiteral: return "Domain literal is missing its closing bracket"; | |
| case EmailStatusCode.errorFoldingWhitespaceCrflX2: | |
| return "Folding White Space contains consecutive CRLF sequences"; | |
| case EmailStatusCode.errorFoldingWhitespaceCrLfEnd: return "Folding White Space ends with a CRLF sequence"; | |
| case EmailStatusCode.errorCrNoLf: | |
| return "Address contains a carriage return that is not followed by a line feed"; | |
| } | |
| } | |
| /** | |
| * An email status code, indicating if an email address is valid or not. | |
| * If it is invalid it also indicates why. | |
| */ | |
| enum EmailStatusCode | |
| { | |
| // Categories | |
| /// Address is valid | |
| validCategory = 1, | |
| /// Address is valid but a DNS check was not successful | |
| dnsWarning = 7, | |
| /// Address is valid for SMTP but has unusual elements | |
| rfc5321 = 15, | |
| /// Address is valid within the message but cannot be used unmodified for the envelope | |
| cFoldingWhitespace = 31, | |
| /// Address contains deprecated elements but may still be valid in restricted contexts | |
| deprecated_ = 63, | |
| /// The address is only valid according to the broad definition of RFC 5322. It is otherwise invalid | |
| rfc5322 = 127, | |
| /** | |
| * All finer grained error checking is turned on. Address containing errors or | |
| * warnings is considered invalid. A specific email status code will be | |
| * returned indicating the error/warning of the address. | |
| */ | |
| any = 252, | |
| /** | |
| * Address is either considered valid or not, no finer grained error checking | |
| * is performed. Returned email status code will be either Error or Valid. | |
| */ | |
| none = 253, | |
| /** | |
| * Address containing warnings is considered valid, that is, | |
| * any status code below 16 is considered valid. | |
| */ | |
| warning = 254, | |
| /// Address is invalid for any purpose | |
| error = 255, | |
| // Diagnoses | |
| /// Address is valid | |
| valid = 0, | |
| // Address is valid but a DNS check was not successful | |
| /// Could not find an MX record for this domain but an A-record does exist | |
| dnsWarningNoMXRecord = 5, | |
| /// Could not find an MX record or an A-record for this domain | |
| dnsWarningNoRecord = 6, | |
| // Address is valid for SMTP but has unusual elements | |
| /// Address is valid but at a Top Level Domain | |
| rfc5321TopLevelDomain = 9, | |
| /// Address is valid but the Top Level Domain begins with a number | |
| rfc5321TopLevelDomainNumeric = 10, | |
| /// Address is valid but contains a quoted string | |
| rfc5321QuotedString = 11, | |
| /// Address is valid but at a literal address not a domain | |
| rfc5321AddressLiteral = 12, | |
| /// Address is valid but contains a :: that only elides one zero group | |
| rfc5321IpV6Deprecated = 13, | |
| // Address is valid within the message but cannot be used unmodified for the envelope | |
| /// Address contains comments | |
| comment = 17, | |
| /// Address contains Folding White Space | |
| foldingWhitespace = 18, | |
| // Address contains deprecated elements but may still be valid in restricted contexts | |
| /// The local part is in a deprecated form | |
| deprecatedLocalPart = 33, | |
| /// Address contains an obsolete form of Folding White Space | |
| deprecatedFoldingWhitespace = 34, | |
| /// A quoted string contains a deprecated character | |
| deprecatedQuotedText = 35, | |
| /// A quoted pair contains a deprecated character | |
| deprecatedQuotedPair = 36, | |
| /// Address contains a comment in a position that is deprecated | |
| deprecatedComment = 37, | |
| /// A comment contains a deprecated character | |
| deprecatedCommentText = 38, | |
| /// Address contains a comment or Folding White Space around the @ sign | |
| deprecatedCommentFoldingWhitespaceNearAt = 49, | |
| // The address is only valid according to the broad definition of RFC 5322 | |
| /// Address is RFC 5322 compliant but contains domain characters that are not allowed by DNS | |
| rfc5322Domain = 65, | |
| /// Address is too long | |
| rfc5322TooLong = 66, | |
| /// The local part of the address is too long | |
| rfc5322LocalTooLong = 67, | |
| /// The domain part is too long | |
| rfc5322DomainTooLong = 68, | |
| /// The domain part contains an element that is too long | |
| rfc5322LabelTooLong = 69, | |
| /// The domain literal is not a valid RFC 5321 address literal | |
| rfc5322DomainLiteral = 70, | |
| /// The domain literal is not a valid RFC 5321 address literal and it contains obsolete characters | |
| rfc5322DomainLiteralObsoleteText = 71, | |
| /// The IPv6 literal address contains the wrong number of groups | |
| rfc5322IpV6GroupCount = 72, | |
| /// The IPv6 literal address contains too many :: sequences | |
| rfc5322IpV6TooManyDoubleColons = 73, | |
| /// The IPv6 address contains an illegal group of characters | |
| rfc5322IpV6BadChar = 74, | |
| /// The IPv6 address has too many groups | |
| rfc5322IpV6MaxGroups = 75, | |
| /// IPv6 address starts with a single colon | |
| rfc5322IpV6ColonStart = 76, | |
| /// IPv6 address ends with a single colon | |
| rfc5322IpV6ColonEnd = 77, | |
| // Address is invalid for any purpose | |
| /// A domain literal contains a character that is not allowed | |
| errorExpectingDomainText = 129, | |
| /// Address has no local part | |
| errorNoLocalPart = 130, | |
| /// Address has no domain part | |
| errorNoDomain = 131, | |
| /// The address may not contain consecutive dots | |
| errorConsecutiveDots = 132, | |
| /// Address contains text after a comment or Folding White Space | |
| errorTextAfterCommentFoldingWhitespace = 133, | |
| /// Address contains text after a quoted string | |
| errorTextAfterQuotedString = 134, | |
| /// Extra characters were found after the end of the domain literal | |
| errorTextAfterDomainLiteral = 135, | |
| /// The address contains a character that is not allowed in a quoted pair | |
| errorExpectingQuotedPair = 136, | |
| /// Address contains a character that is not allowed | |
| errorExpectingText = 137, | |
| /// A quoted string contains a character that is not allowed | |
| errorExpectingQuotedText = 138, | |
| /// A comment contains a character that is not allowed | |
| errorExpectingCommentText = 139, | |
| /// The address cannot end with a backslash | |
| errorBackslashEnd = 140, | |
| /// Neither part of the address may begin with a dot | |
| errorDotStart = 141, | |
| /// Neither part of the address may end with a dot | |
| errorDotEnd = 142, | |
| /// A domain or subdomain cannot begin with a hyphen | |
| errorDomainHyphenStart = 143, | |
| /// A domain or subdomain cannot end with a hyphen | |
| errorDomainHyphenEnd = 144, | |
| /// Unclosed quoted string | |
| errorUnclosedQuotedString = 145, | |
| /// Unclosed comment | |
| errorUnclosedComment = 146, | |
| /// Domain literal is missing its closing bracket | |
| errorUnclosedDomainLiteral = 147, | |
| /// Folding White Space contains consecutive CRLF sequences | |
| errorFoldingWhitespaceCrflX2 = 148, | |
| /// Folding White Space ends with a CRLF sequence | |
| errorFoldingWhitespaceCrLfEnd = 149, | |
| /// Address contains a carriage return that is not followed by a line feed | |
| errorCrNoLf = 150, | |
| } | |
| private: | |
| // Email parts for the isEmail function | |
| enum EmailPart | |
| { | |
| // The local part of the email address, that is, the part before the @ sign | |
| componentLocalPart, | |
| // The domain part of the email address, that is, the part after the @ sign. | |
| componentDomain, | |
| componentLiteral, | |
| contextComment, | |
| contextFoldingWhitespace, | |
| contextQuotedString, | |
| contextQuotedPair, | |
| status | |
| } | |
| // Miscellaneous string constants | |
| struct TokenImpl(Char) | |
| { | |
| enum : const(Char)[] | |
| { | |
| at = "@", | |
| backslash = `\`, | |
| dot = ".", | |
| doubleQuote = `"`, | |
| openParenthesis = "(", | |
| closeParenthesis = ")", | |
| openBracket = "[", | |
| closeBracket = "]", | |
| hyphen = "-", | |
| colon = ":", | |
| doubleColon = "::", | |
| space = " ", | |
| tab = "\t", | |
| cr = "\r", | |
| lf = "\n", | |
| ipV6Tag = "IPV6:", | |
| // US-ASCII visible characters not valid for atext (http://tools.ietf.org/html/rfc5322#section-3.2.3) | |
| specials = `()<>[]:;@\\,."` | |
| } | |
| } | |
| enum AsciiToken | |
| { | |
| horizontalTab = 9, | |
| unitSeparator = 31, | |
| delete_ = 127 | |
| } | |
| /* | |
| * Compare the two given strings lexicographically. An upper limit of the number of | |
| * characters, that will be used in the comparison, can be specified. Supports both | |
| * case-sensitive and case-insensitive comparison. | |
| * | |
| * Params: | |
| * s1 = the first string to be compared | |
| * s2 = the second string to be compared | |
| * length = the length of strings to be used in the comparison. | |
| * caseInsensitive = if true, a case-insensitive comparison will be made, | |
| * otherwise a case-sensitive comparison will be made | |
| * | |
| * Returns: (for $(D pred = "a < b")): | |
| * | |
| * $(BOOKTABLE, | |
| * $(TR $(TD $(D < 0)) $(TD $(D s1 < s2) )) | |
| * $(TR $(TD $(D = 0)) $(TD $(D s1 == s2))) | |
| * $(TR $(TD $(D > 0)) $(TD $(D s1 > s2))) | |
| * ) | |
| */ | |
| int compareFirstN(alias pred = "a < b", S1, S2) (S1 s1, S2 s2, size_t length) | |
| if (is(immutable ElementType!(S1) == immutable dchar) && is(immutable ElementType!(S2) == immutable dchar)) | |
| { | |
| import std.uni : icmp; | |
| auto s1End = length <= s1.length ? length : s1.length; | |
| auto s2End = length <= s2.length ? length : s2.length; | |
| auto slice1 = s1[0 .. s1End]; | |
| auto slice2 = s2[0 .. s2End]; | |
| return slice1.icmp(slice2); | |
| } | |
| @safe unittest | |
| { | |
| assert("abc".compareFirstN("abcdef", 3) == 0); | |
| assert("abc".compareFirstN("Abc", 3) == 0); | |
| assert("abc".compareFirstN("abcdef", 6) < 0); | |
| assert("abcdef".compareFirstN("abc", 6) > 0); | |
| } | |
| /* | |
| * Pops the last element of the given range and returns the element. | |
| * | |
| * Params: | |
| * range = the range to pop the element from | |
| * | |
| * Returns: the popped element | |
| */ | |
| ElementType!(A) pop (A) (ref A a) | |
| if (isDynamicArray!(A) && !isNarrowString!(A) && isMutable!(A) && !is(A == void[])) | |
| { | |
| auto e = a.back; | |
| a.popBack(); | |
| return e; | |
| } | |
| @safe unittest | |
| { | |
| auto array = [0, 1, 2, 3]; | |
| auto result = array.pop(); | |
| assert(array == [0, 1, 2]); | |
| assert(result == 3); | |
| } | |
| /* | |
| * Returns the character at the given index as a string. The returned string will be a | |
| * slice of the original string. | |
| * | |
| * Params: | |
| * str = the string to get the character from | |
| * index = the index of the character to get | |
| * c = the character to return, or any other of the same length | |
| * | |
| * Returns: the character at the given index as a string | |
| */ | |
| const(T)[] get (T) (const(T)[] str, size_t index, dchar c) | |
| { | |
| import std.utf : codeLength; | |
| return str[index .. index + codeLength!(T)(c)]; | |
| } | |
| @safe unittest | |
| { | |
| assert("abc".get(1, 'b') == "b"); | |
| assert("löv".get(1, 'ö') == "ö"); | |
| } | |
| @safe unittest | |
| { | |
| assert("abc".get(1, 'b') == "b"); | |
| assert("löv".get(1, 'ö') == "ö"); | |
| } | |
| /+ | |
| Replacement for: | |
| --- | |
| static fourChars = ctRegex!(`^[0-9A-Fa-f]{0,4}$`.to!(const(Char)[])); | |
| ... | |
| a => a.matchFirst(fourChars).empty | |
| --- | |
| +/ | |
| bool isUpToFourHexChars(Char)(scope const(Char)[] s) | |
| { | |
| import std.ascii : isHexDigit; | |
| if (s.length > 4) return false; | |
| foreach (c; s) | |
| if (!isHexDigit(c)) return false; | |
| return true; | |
| } | |
| @nogc nothrow pure @safe unittest | |
| { | |
| assert(!isUpToFourHexChars("12345")); | |
| assert(!isUpToFourHexChars("defg")); | |
| assert(isUpToFourHexChars("1A0a")); | |
| } | |
| /+ | |
| Replacement for: | |
| --- | |
| static ipRegex = ctRegex!(`\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}`~ | |
| `(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$`.to!(const(Char)[])); | |
| ... | |
| auto matchesIp = addressLiteral.matchAll(ipRegex).map!(a => a.hit).array; | |
| ---- | |
| Note that only the first item of "matchAll" was ever used in practice | |
| so we can return `const(Char)[]` instead of `const(Char)[][]` using a | |
| zero-length string to indicate no match. | |
| +/ | |
| const(Char)[] matchIPSuffix(Char)(return scope const(Char)[] s) @nogc nothrow pure @safe | |
| { | |
| size_t end = s.length; | |
| if (end < 7) return null; | |
| // Check the first three `[.]\d{1,3}` | |
| foreach (_; 0 .. 3) | |
| { | |
| size_t start = void; | |
| if (end >= 2 && s[end-2] == '.') | |
| start = end - 2; | |
| else if (end >= 3 && s[end-3] == '.') | |
| start = end - 3; | |
| else if (end >= 4 && s[end-4] == '.') | |
| start = end - 4; | |
| else | |
| return null; | |
| uint x = 0; | |
| foreach (i; start + 1 .. end) | |
| { | |
| uint c = cast(uint) s[i] - '0'; | |
| if (c > 9) return null; | |
| x = x * 10 + c; | |
| } | |
| if (x > 255) return null; | |
| end = start; | |
| } | |
| // Check the final `\d{1,3}`. | |
| if (end < 1) return null; | |
| size_t start = end - 1; | |
| uint x = cast(uint) s[start] - '0'; | |
| if (x > 9) return null; | |
| if (start > 0 && cast(uint) s[start-1] - '0' <= 9) | |
| { | |
| --start; | |
| x += 10 * (cast(uint) s[start] - '0'); | |
| if (start > 0 && cast(uint) s[start-1] - '0' <= 9) | |
| { | |
| --start; | |
| x += 100 * (cast(uint) s[start] - '0'); | |
| } | |
| } | |
| if (x > 255) return null; | |
| // Must either be at start of string or preceded by a non-word character. | |
| // (TO DETERMINE: is the definition of "word character" ASCII only?) | |
| if (start == 0) return s; | |
| const b = s[start - 1]; | |
| import std.ascii : isAlphaNum; | |
| if (isAlphaNum(b) || b == '_') return null; | |
| return s[start .. $]; | |
| } | |
| @nogc nothrow pure @safe unittest | |
| { | |
| assert(matchIPSuffix("255.255.255.255") == "255.255.255.255"); | |
| assert(matchIPSuffix("babaev 176.16.0.1") == "176.16.0.1"); | |
| } |