diff options
author | Christoph Wurst <christoph@winzerhof-wurst.at> | 2020-12-30 11:35:38 +0300 |
---|---|---|
committer | Christoph Wurst <christoph@winzerhof-wurst.at> | 2020-12-30 11:37:26 +0300 |
commit | 3cf3dc9cf29a491cf191bbff9b791ae523c54c03 (patch) | |
tree | 3098bec3d3499cd5fb4347111644772cac0c9891 /egulias | |
parent | ff73290b41ae99ae9d601e77407b6a65a35a25e7 (diff) |
Require missing email validator
Signed-off-by: Christoph Wurst <christoph@winzerhof-wurst.at>
Diffstat (limited to 'egulias')
65 files changed, 2456 insertions, 0 deletions
diff --git a/egulias/email-validator/src/EmailLexer.php b/egulias/email-validator/src/EmailLexer.php new file mode 100644 index 00000000..59dcd587 --- /dev/null +++ b/egulias/email-validator/src/EmailLexer.php @@ -0,0 +1,283 @@ +<?php + +namespace Egulias\EmailValidator; + +use Doctrine\Common\Lexer\AbstractLexer; + +class EmailLexer extends AbstractLexer +{ + //ASCII values + const C_DEL = 127; + const C_NUL = 0; + const S_AT = 64; + const S_BACKSLASH = 92; + const S_DOT = 46; + const S_DQUOTE = 34; + const S_SQUOTE = 39; + const S_BACKTICK = 96; + const S_OPENPARENTHESIS = 49; + const S_CLOSEPARENTHESIS = 261; + const S_OPENBRACKET = 262; + const S_CLOSEBRACKET = 263; + const S_HYPHEN = 264; + const S_COLON = 265; + const S_DOUBLECOLON = 266; + const S_SP = 267; + const S_HTAB = 268; + const S_CR = 269; + const S_LF = 270; + const S_IPV6TAG = 271; + const S_LOWERTHAN = 272; + const S_GREATERTHAN = 273; + const S_COMMA = 274; + const S_SEMICOLON = 275; + const S_OPENQBRACKET = 276; + const S_CLOSEQBRACKET = 277; + const S_SLASH = 278; + const S_EMPTY = null; + const GENERIC = 300; + const CRLF = 301; + const INVALID = 302; + const ASCII_INVALID_FROM = 127; + const ASCII_INVALID_TO = 199; + + /** + * US-ASCII visible characters not valid for atext (@link http://tools.ietf.org/html/rfc5322#section-3.2.3) + * + * @var array + */ + protected $charValue = array( + '(' => self::S_OPENPARENTHESIS, + ')' => self::S_CLOSEPARENTHESIS, + '<' => self::S_LOWERTHAN, + '>' => self::S_GREATERTHAN, + '[' => self::S_OPENBRACKET, + ']' => self::S_CLOSEBRACKET, + ':' => self::S_COLON, + ';' => self::S_SEMICOLON, + '@' => self::S_AT, + '\\' => self::S_BACKSLASH, + '/' => self::S_SLASH, + ',' => self::S_COMMA, + '.' => self::S_DOT, + "'" => self::S_SQUOTE, + "`" => self::S_BACKTICK, + '"' => self::S_DQUOTE, + '-' => self::S_HYPHEN, + '::' => self::S_DOUBLECOLON, + ' ' => self::S_SP, + "\t" => self::S_HTAB, + "\r" => self::S_CR, + "\n" => self::S_LF, + "\r\n" => self::CRLF, + 'IPv6' => self::S_IPV6TAG, + '{' => self::S_OPENQBRACKET, + '}' => self::S_CLOSEQBRACKET, + '' => self::S_EMPTY, + '\0' => self::C_NUL, + ); + + /** + * @var bool + */ + protected $hasInvalidTokens = false; + + /** + * @var array + * + * @psalm-var array{value:string, type:null|int, position:int}|array<empty, empty> + */ + protected $previous = []; + + /** + * The last matched/seen token. + * + * @var array + * + * @psalm-var array{value:string, type:null|int, position:int} + */ + public $token; + + /** + * The next token in the input. + * + * @var array|null + */ + public $lookahead; + + /** + * @psalm-var array{value:'', type:null, position:0} + */ + private static $nullToken = [ + 'value' => '', + 'type' => null, + 'position' => 0, + ]; + + public function __construct() + { + $this->previous = $this->token = self::$nullToken; + $this->lookahead = null; + } + + /** + * @return void + */ + public function reset() + { + $this->hasInvalidTokens = false; + parent::reset(); + $this->previous = $this->token = self::$nullToken; + } + + /** + * @return bool + */ + public function hasInvalidTokens() + { + return $this->hasInvalidTokens; + } + + /** + * @param int $type + * @throws \UnexpectedValueException + * @return boolean + * + * @psalm-suppress InvalidScalarArgument + */ + public function find($type) + { + $search = clone $this; + $search->skipUntil($type); + + if (!$search->lookahead) { + throw new \UnexpectedValueException($type . ' not found'); + } + return true; + } + + /** + * getPrevious + * + * @return array + */ + public function getPrevious() + { + return $this->previous; + } + + /** + * moveNext + * + * @return boolean + */ + public function moveNext() + { + $this->previous = $this->token; + $hasNext = parent::moveNext(); + $this->token = $this->token ?: self::$nullToken; + + return $hasNext; + } + + /** + * Lexical catchable patterns. + * + * @return string[] + */ + protected function getCatchablePatterns() + { + return array( + '[a-zA-Z_]+[46]?', //ASCII and domain literal + '[^\x00-\x7F]', //UTF-8 + '[0-9]+', + '\r\n', + '::', + '\s+?', + '.', + ); + } + + /** + * Lexical non-catchable patterns. + * + * @return string[] + */ + protected function getNonCatchablePatterns() + { + return array('[\xA0-\xff]+'); + } + + /** + * Retrieve token type. Also processes the token value if necessary. + * + * @param string $value + * @throws \InvalidArgumentException + * @return integer + */ + protected function getType(&$value) + { + if ($this->isNullType($value)) { + return self::C_NUL; + } + + if ($this->isValid($value)) { + return $this->charValue[$value]; + } + + if ($this->isUTF8Invalid($value)) { + $this->hasInvalidTokens = true; + return self::INVALID; + } + + return self::GENERIC; + } + + /** + * @param string $value + * + * @return bool + */ + protected function isValid($value) + { + if (isset($this->charValue[$value])) { + return true; + } + + return false; + } + + /** + * @param string $value + * @return bool + */ + protected function isNullType($value) + { + if ($value === "\0") { + return true; + } + + return false; + } + + /** + * @param string $value + * @return bool + */ + protected function isUTF8Invalid($value) + { + if (preg_match('/\p{Cc}+/u', $value)) { + return true; + } + + return false; + } + + /** + * @return string + */ + protected function getModifiers() + { + return 'iu'; + } +} diff --git a/egulias/email-validator/src/EmailParser.php b/egulias/email-validator/src/EmailParser.php new file mode 100644 index 00000000..6b7bad66 --- /dev/null +++ b/egulias/email-validator/src/EmailParser.php @@ -0,0 +1,137 @@ +<?php + +namespace Egulias\EmailValidator; + +use Egulias\EmailValidator\Exception\ExpectingATEXT; +use Egulias\EmailValidator\Exception\NoLocalPart; +use Egulias\EmailValidator\Parser\DomainPart; +use Egulias\EmailValidator\Parser\LocalPart; +use Egulias\EmailValidator\Warning\EmailTooLong; + +/** + * EmailParser + * + * @author Eduardo Gulias Davis <me@egulias.com> + */ +class EmailParser +{ + const EMAIL_MAX_LENGTH = 254; + + /** + * @var array + */ + protected $warnings = []; + + /** + * @var string + */ + protected $domainPart = ''; + + /** + * @var string + */ + protected $localPart = ''; + /** + * @var EmailLexer + */ + protected $lexer; + + /** + * @var LocalPart + */ + protected $localPartParser; + + /** + * @var DomainPart + */ + protected $domainPartParser; + + public function __construct(EmailLexer $lexer) + { + $this->lexer = $lexer; + $this->localPartParser = new LocalPart($this->lexer); + $this->domainPartParser = new DomainPart($this->lexer); + } + + /** + * @param string $str + * @return array + */ + public function parse($str) + { + $this->lexer->setInput($str); + + if (!$this->hasAtToken()) { + throw new NoLocalPart(); + } + + + $this->localPartParser->parse($str); + $this->domainPartParser->parse($str); + + $this->setParts($str); + + if ($this->lexer->hasInvalidTokens()) { + throw new ExpectingATEXT(); + } + + return array('local' => $this->localPart, 'domain' => $this->domainPart); + } + + /** + * @return Warning\Warning[] + */ + public function getWarnings() + { + $localPartWarnings = $this->localPartParser->getWarnings(); + $domainPartWarnings = $this->domainPartParser->getWarnings(); + $this->warnings = array_merge($localPartWarnings, $domainPartWarnings); + + $this->addLongEmailWarning($this->localPart, $this->domainPart); + + return $this->warnings; + } + + /** + * @return string + */ + public function getParsedDomainPart() + { + return $this->domainPart; + } + + /** + * @param string $email + */ + protected function setParts($email) + { + $parts = explode('@', $email); + $this->domainPart = $this->domainPartParser->getDomainPart(); + $this->localPart = $parts[0]; + } + + /** + * @return bool + */ + protected function hasAtToken() + { + $this->lexer->moveNext(); + $this->lexer->moveNext(); + if ($this->lexer->token['type'] === EmailLexer::S_AT) { + return false; + } + + return true; + } + + /** + * @param string $localPart + * @param string $parsedDomainPart + */ + protected function addLongEmailWarning($localPart, $parsedDomainPart) + { + if (strlen($localPart . '@' . $parsedDomainPart) > self::EMAIL_MAX_LENGTH) { + $this->warnings[EmailTooLong::CODE] = new EmailTooLong(); + } + } +} diff --git a/egulias/email-validator/src/EmailValidator.php b/egulias/email-validator/src/EmailValidator.php new file mode 100644 index 00000000..a30f21dc --- /dev/null +++ b/egulias/email-validator/src/EmailValidator.php @@ -0,0 +1,67 @@ +<?php + +namespace Egulias\EmailValidator; + +use Egulias\EmailValidator\Exception\InvalidEmail; +use Egulias\EmailValidator\Validation\EmailValidation; + +class EmailValidator +{ + /** + * @var EmailLexer + */ + private $lexer; + + /** + * @var Warning\Warning[] + */ + protected $warnings = []; + + /** + * @var InvalidEmail|null + */ + protected $error; + + public function __construct() + { + $this->lexer = new EmailLexer(); + } + + /** + * @param string $email + * @param EmailValidation $emailValidation + * @return bool + */ + public function isValid($email, EmailValidation $emailValidation) + { + $isValid = $emailValidation->isValid($email, $this->lexer); + $this->warnings = $emailValidation->getWarnings(); + $this->error = $emailValidation->getError(); + + return $isValid; + } + + /** + * @return boolean + */ + public function hasWarnings() + { + return !empty($this->warnings); + } + + /** + * @return array + */ + public function getWarnings() + { + return $this->warnings; + } + + /** + * @return InvalidEmail|null + */ + public function getError() + { + return $this->error; + } +} diff --git a/egulias/email-validator/src/Exception/AtextAfterCFWS.php b/egulias/email-validator/src/Exception/AtextAfterCFWS.php new file mode 100644 index 00000000..97f41a2c --- /dev/null +++ b/egulias/email-validator/src/Exception/AtextAfterCFWS.php @@ -0,0 +1,9 @@ +<?php + +namespace Egulias\EmailValidator\Exception; + +class AtextAfterCFWS extends InvalidEmail +{ + const CODE = 133; + const REASON = "ATEXT found after CFWS"; +} diff --git a/egulias/email-validator/src/Exception/CRLFAtTheEnd.php b/egulias/email-validator/src/Exception/CRLFAtTheEnd.php new file mode 100644 index 00000000..ec23bc71 --- /dev/null +++ b/egulias/email-validator/src/Exception/CRLFAtTheEnd.php @@ -0,0 +1,9 @@ +<?php + +namespace Egulias\EmailValidator\Exception; + +class CRLFAtTheEnd extends InvalidEmail +{ + const CODE = 149; + const REASON = "CRLF at the end"; +} diff --git a/egulias/email-validator/src/Exception/CRLFX2.php b/egulias/email-validator/src/Exception/CRLFX2.php new file mode 100644 index 00000000..6bd377ee --- /dev/null +++ b/egulias/email-validator/src/Exception/CRLFX2.php @@ -0,0 +1,9 @@ +<?php + +namespace Egulias\EmailValidator\Exception; + +class CRLFX2 extends InvalidEmail +{ + const CODE = 148; + const REASON = "Folding whitespace CR LF found twice"; +} diff --git a/egulias/email-validator/src/Exception/CRNoLF.php b/egulias/email-validator/src/Exception/CRNoLF.php new file mode 100644 index 00000000..9c9f7394 --- /dev/null +++ b/egulias/email-validator/src/Exception/CRNoLF.php @@ -0,0 +1,9 @@ +<?php + +namespace Egulias\EmailValidator\Exception; + +class CRNoLF extends InvalidEmail +{ + const CODE = 150; + const REASON = "Missing LF after CR"; +} diff --git a/egulias/email-validator/src/Exception/CharNotAllowed.php b/egulias/email-validator/src/Exception/CharNotAllowed.php new file mode 100644 index 00000000..ea20ce59 --- /dev/null +++ b/egulias/email-validator/src/Exception/CharNotAllowed.php @@ -0,0 +1,9 @@ +<?php + +namespace Egulias\EmailValidator\Exception; + +class CharNotAllowed extends InvalidEmail +{ + const CODE = 201; + const REASON = "Non allowed character in domain"; +} diff --git a/egulias/email-validator/src/Exception/CommaInDomain.php b/egulias/email-validator/src/Exception/CommaInDomain.php new file mode 100644 index 00000000..e9245d96 --- /dev/null +++ b/egulias/email-validator/src/Exception/CommaInDomain.php @@ -0,0 +1,9 @@ +<?php + +namespace Egulias\EmailValidator\Exception; + +class CommaInDomain extends InvalidEmail +{ + const CODE = 200; + const REASON = "Comma ',' is not allowed in domain part"; +} diff --git a/egulias/email-validator/src/Exception/ConsecutiveAt.php b/egulias/email-validator/src/Exception/ConsecutiveAt.php new file mode 100644 index 00000000..165ff57a --- /dev/null +++ b/egulias/email-validator/src/Exception/ConsecutiveAt.php @@ -0,0 +1,9 @@ +<?php + +namespace Egulias\EmailValidator\Exception; + +class ConsecutiveAt extends InvalidEmail +{ + const CODE = 128; + const REASON = "Consecutive AT"; +} diff --git a/egulias/email-validator/src/Exception/ConsecutiveDot.php b/egulias/email-validator/src/Exception/ConsecutiveDot.php new file mode 100644 index 00000000..949af3b5 --- /dev/null +++ b/egulias/email-validator/src/Exception/ConsecutiveDot.php @@ -0,0 +1,9 @@ +<?php + +namespace Egulias\EmailValidator\Exception; + +class ConsecutiveDot extends InvalidEmail +{ + const CODE = 132; + const REASON = "Consecutive DOT"; +} diff --git a/egulias/email-validator/src/Exception/DomainAcceptsNoMail.php b/egulias/email-validator/src/Exception/DomainAcceptsNoMail.php new file mode 100644 index 00000000..40a99705 --- /dev/null +++ b/egulias/email-validator/src/Exception/DomainAcceptsNoMail.php @@ -0,0 +1,9 @@ +<?php + +namespace Egulias\EmailValidator\Exception; + +class DomainAcceptsNoMail extends InvalidEmail +{ + const CODE = 154; + const REASON = 'Domain accepts no mail (Null MX, RFC7505)'; +}
\ No newline at end of file diff --git a/egulias/email-validator/src/Exception/DomainHyphened.php b/egulias/email-validator/src/Exception/DomainHyphened.php new file mode 100644 index 00000000..6f586860 --- /dev/null +++ b/egulias/email-validator/src/Exception/DomainHyphened.php @@ -0,0 +1,9 @@ +<?php + +namespace Egulias\EmailValidator\Exception; + +class DomainHyphened extends InvalidEmail +{ + const CODE = 144; + const REASON = "Hyphen found in domain"; +} diff --git a/egulias/email-validator/src/Exception/DotAtEnd.php b/egulias/email-validator/src/Exception/DotAtEnd.php new file mode 100644 index 00000000..05ade77d --- /dev/null +++ b/egulias/email-validator/src/Exception/DotAtEnd.php @@ -0,0 +1,9 @@ +<?php + +namespace Egulias\EmailValidator\Exception; + +class DotAtEnd extends InvalidEmail +{ + const CODE = 142; + const REASON = "Dot at the end"; +} diff --git a/egulias/email-validator/src/Exception/DotAtStart.php b/egulias/email-validator/src/Exception/DotAtStart.php new file mode 100644 index 00000000..7772df7f --- /dev/null +++ b/egulias/email-validator/src/Exception/DotAtStart.php @@ -0,0 +1,9 @@ +<?php + +namespace Egulias\EmailValidator\Exception; + +class DotAtStart extends InvalidEmail +{ + const CODE = 141; + const REASON = "Found DOT at start"; +} diff --git a/egulias/email-validator/src/Exception/ExpectingAT.php b/egulias/email-validator/src/Exception/ExpectingAT.php new file mode 100644 index 00000000..36d633c1 --- /dev/null +++ b/egulias/email-validator/src/Exception/ExpectingAT.php @@ -0,0 +1,9 @@ +<?php + +namespace Egulias\EmailValidator\Exception; + +class ExpectingAT extends InvalidEmail +{ + const CODE = 202; + const REASON = "Expecting AT '@' "; +} diff --git a/egulias/email-validator/src/Exception/ExpectingATEXT.php b/egulias/email-validator/src/Exception/ExpectingATEXT.php new file mode 100644 index 00000000..095d9db7 --- /dev/null +++ b/egulias/email-validator/src/Exception/ExpectingATEXT.php @@ -0,0 +1,9 @@ +<?php + +namespace Egulias\EmailValidator\Exception; + +class ExpectingATEXT extends InvalidEmail +{ + const CODE = 137; + const REASON = "Expecting ATEXT"; +} diff --git a/egulias/email-validator/src/Exception/ExpectingCTEXT.php b/egulias/email-validator/src/Exception/ExpectingCTEXT.php new file mode 100644 index 00000000..63b870a4 --- /dev/null +++ b/egulias/email-validator/src/Exception/ExpectingCTEXT.php @@ -0,0 +1,9 @@ +<?php + +namespace Egulias\EmailValidator\Exception; + +class ExpectingCTEXT extends InvalidEmail +{ + const CODE = 139; + const REASON = "Expecting CTEXT"; +} diff --git a/egulias/email-validator/src/Exception/ExpectingDTEXT.php b/egulias/email-validator/src/Exception/ExpectingDTEXT.php new file mode 100644 index 00000000..6a5bb9bf --- /dev/null +++ b/egulias/email-validator/src/Exception/ExpectingDTEXT.php @@ -0,0 +1,9 @@ +<?php + +namespace Egulias\EmailValidator\Exception; + +class ExpectingDTEXT extends InvalidEmail +{ + const CODE = 129; + const REASON = "Expected DTEXT"; +} diff --git a/egulias/email-validator/src/Exception/ExpectingDomainLiteralClose.php b/egulias/email-validator/src/Exception/ExpectingDomainLiteralClose.php new file mode 100644 index 00000000..81aad427 --- /dev/null +++ b/egulias/email-validator/src/Exception/ExpectingDomainLiteralClose.php @@ -0,0 +1,9 @@ +<?php + +namespace Egulias\EmailValidator\Exception; + +class ExpectingDomainLiteralClose extends InvalidEmail +{ + const CODE = 137; + const REASON = "Closing bracket ']' for domain literal not found"; +} diff --git a/egulias/email-validator/src/Exception/ExpectingQPair.php b/egulias/email-validator/src/Exception/ExpectingQPair.php new file mode 100644 index 00000000..a738eeb6 --- /dev/null +++ b/egulias/email-validator/src/Exception/ExpectingQPair.php @@ -0,0 +1,9 @@ +<?php + +namespace Egulias\EmailValidator\Exception; + +class ExpectingQPair extends InvalidEmail +{ + const CODE = 136; + const REASON = "Expecting QPAIR"; +} diff --git a/egulias/email-validator/src/Exception/InvalidEmail.php b/egulias/email-validator/src/Exception/InvalidEmail.php new file mode 100644 index 00000000..1c0218e9 --- /dev/null +++ b/egulias/email-validator/src/Exception/InvalidEmail.php @@ -0,0 +1,14 @@ +<?php + +namespace Egulias\EmailValidator\Exception; + +abstract class InvalidEmail extends \InvalidArgumentException +{ + const REASON = "Invalid email"; + const CODE = 0; + + public function __construct() + { + parent::__construct(static::REASON, static::CODE); + } +} diff --git a/egulias/email-validator/src/Exception/LocalOrReservedDomain.php b/egulias/email-validator/src/Exception/LocalOrReservedDomain.php new file mode 100644 index 00000000..695b05a4 --- /dev/null +++ b/egulias/email-validator/src/Exception/LocalOrReservedDomain.php @@ -0,0 +1,9 @@ +<?php + +namespace Egulias\EmailValidator\Exception; + +class LocalOrReservedDomain extends InvalidEmail +{ + const CODE = 153; + const REASON = 'Local, mDNS or reserved domain (RFC2606, RFC6762)'; +}
\ No newline at end of file diff --git a/egulias/email-validator/src/Exception/NoDNSRecord.php b/egulias/email-validator/src/Exception/NoDNSRecord.php new file mode 100644 index 00000000..0aa5fa78 --- /dev/null +++ b/egulias/email-validator/src/Exception/NoDNSRecord.php @@ -0,0 +1,9 @@ +<?php + +namespace Egulias\EmailValidator\Exception; + +class NoDNSRecord extends InvalidEmail +{ + const CODE = 5; + const REASON = 'No MX or A DSN record was found for this email'; +} diff --git a/egulias/email-validator/src/Exception/NoDomainPart.php b/egulias/email-validator/src/Exception/NoDomainPart.php new file mode 100644 index 00000000..05a2604c --- /dev/null +++ b/egulias/email-validator/src/Exception/NoDomainPart.php @@ -0,0 +1,9 @@ +<?php + +namespace Egulias\EmailValidator\Exception; + +class NoDomainPart extends InvalidEmail +{ + const CODE = 131; + const REASON = "No Domain part"; +} diff --git a/egulias/email-validator/src/Exception/NoLocalPart.php b/egulias/email-validator/src/Exception/NoLocalPart.php new file mode 100644 index 00000000..07c14b84 --- /dev/null +++ b/egulias/email-validator/src/Exception/NoLocalPart.php @@ -0,0 +1,9 @@ +<?php + +namespace Egulias\EmailValidator\Exception; + +class NoLocalPart extends InvalidEmail +{ + const CODE = 130; + const REASON = "No local part"; +} diff --git a/egulias/email-validator/src/Exception/UnclosedComment.php b/egulias/email-validator/src/Exception/UnclosedComment.php new file mode 100644 index 00000000..86b2b096 --- /dev/null +++ b/egulias/email-validator/src/Exception/UnclosedComment.php @@ -0,0 +1,9 @@ +<?php + +namespace Egulias\EmailValidator\Exception; + +class UnclosedComment extends InvalidEmail +{ + const CODE = 146; + const REASON = "No closing comment token found"; +} diff --git a/egulias/email-validator/src/Exception/UnclosedQuotedString.php b/egulias/email-validator/src/Exception/UnclosedQuotedString.php new file mode 100644 index 00000000..730a39dd --- /dev/null +++ b/egulias/email-validator/src/Exception/UnclosedQuotedString.php @@ -0,0 +1,9 @@ +<?php + +namespace Egulias\EmailValidator\Exception; + +class UnclosedQuotedString extends InvalidEmail +{ + const CODE = 145; + const REASON = "Unclosed quoted string"; +} diff --git a/egulias/email-validator/src/Exception/UnopenedComment.php b/egulias/email-validator/src/Exception/UnopenedComment.php new file mode 100644 index 00000000..cff12d92 --- /dev/null +++ b/egulias/email-validator/src/Exception/UnopenedComment.php @@ -0,0 +1,9 @@ +<?php + +namespace Egulias\EmailValidator\Exception; + +class UnopenedComment extends InvalidEmail +{ + const CODE = 152; + const REASON = "No opening comment token found"; +} diff --git a/egulias/email-validator/src/Parser/DomainPart.php b/egulias/email-validator/src/Parser/DomainPart.php new file mode 100644 index 00000000..4dadba8a --- /dev/null +++ b/egulias/email-validator/src/Parser/DomainPart.php @@ -0,0 +1,443 @@ +<?php + +namespace Egulias\EmailValidator\Parser; + +use Egulias\EmailValidator\EmailLexer; +use Egulias\EmailValidator\Exception\CharNotAllowed; +use Egulias\EmailValidator\Exception\CommaInDomain; +use Egulias\EmailValidator\Exception\ConsecutiveAt; +use Egulias\EmailValidator\Exception\CRLFAtTheEnd; +use Egulias\EmailValidator\Exception\CRNoLF; +use Egulias\EmailValidator\Exception\DomainHyphened; +use Egulias\EmailValidator\Exception\DotAtEnd; +use Egulias\EmailValidator\Exception\DotAtStart; +use Egulias\EmailValidator\Exception\ExpectingATEXT; +use Egulias\EmailValidator\Exception\ExpectingDomainLiteralClose; +use Egulias\EmailValidator\Exception\ExpectingDTEXT; +use Egulias\EmailValidator\Exception\NoDomainPart; +use Egulias\EmailValidator\Exception\UnopenedComment; +use Egulias\EmailValidator\Warning\AddressLiteral; +use Egulias\EmailValidator\Warning\CFWSWithFWS; +use Egulias\EmailValidator\Warning\DeprecatedComment; +use Egulias\EmailValidator\Warning\DomainLiteral; +use Egulias\EmailValidator\Warning\DomainTooLong; +use Egulias\EmailValidator\Warning\IPV6BadChar; +use Egulias\EmailValidator\Warning\IPV6ColonEnd; +use Egulias\EmailValidator\Warning\IPV6ColonStart; +use Egulias\EmailValidator\Warning\IPV6Deprecated; +use Egulias\EmailValidator\Warning\IPV6DoubleColon; +use Egulias\EmailValidator\Warning\IPV6GroupCount; +use Egulias\EmailValidator\Warning\IPV6MaxGroups; +use Egulias\EmailValidator\Warning\LabelTooLong; +use Egulias\EmailValidator\Warning\ObsoleteDTEXT; +use Egulias\EmailValidator\Warning\TLD; + +class DomainPart extends Parser +{ + const DOMAIN_MAX_LENGTH = 254; + const LABEL_MAX_LENGTH = 63; + + /** + * @var string + */ + protected $domainPart = ''; + + public function parse($domainPart) + { + $this->lexer->moveNext(); + + $this->performDomainStartChecks(); + + $domain = $this->doParseDomainPart(); + + $prev = $this->lexer->getPrevious(); + $length = strlen($domain); + + if ($prev['type'] === EmailLexer::S_DOT) { + throw new DotAtEnd(); + } + if ($prev['type'] === EmailLexer::S_HYPHEN) { + throw new DomainHyphened(); + } + if ($length > self::DOMAIN_MAX_LENGTH) { + $this->warnings[DomainTooLong::CODE] = new DomainTooLong(); + } + if ($prev['type'] === EmailLexer::S_CR) { + throw new CRLFAtTheEnd(); + } + $this->domainPart = $domain; + } + + private function performDomainStartChecks() + { + $this->checkInvalidTokensAfterAT(); + $this->checkEmptyDomain(); + + if ($this->lexer->token['type'] === EmailLexer::S_OPENPARENTHESIS) { + $this->warnings[DeprecatedComment::CODE] = new DeprecatedComment(); + $this->parseDomainComments(); + } + } + + private function checkEmptyDomain() + { + $thereIsNoDomain = $this->lexer->token['type'] === EmailLexer::S_EMPTY || + ($this->lexer->token['type'] === EmailLexer::S_SP && + !$this->lexer->isNextToken(EmailLexer::GENERIC)); + + if ($thereIsNoDomain) { + throw new NoDomainPart(); + } + } + + private function checkInvalidTokensAfterAT() + { + if ($this->lexer->token['type'] === EmailLexer::S_DOT) { + throw new DotAtStart(); + } + if ($this->lexer->token['type'] === EmailLexer::S_HYPHEN) { + throw new DomainHyphened(); + } + } + + /** + * @return string + */ + public function getDomainPart() + { + return $this->domainPart; + } + + /** + * @param string $addressLiteral + * @param int $maxGroups + */ + public function checkIPV6Tag($addressLiteral, $maxGroups = 8) + { + $prev = $this->lexer->getPrevious(); + if ($prev['type'] === EmailLexer::S_COLON) { + $this->warnings[IPV6ColonEnd::CODE] = new IPV6ColonEnd(); + } + + $IPv6 = substr($addressLiteral, 5); + //Daniel Marschall's new IPv6 testing strategy + $matchesIP = explode(':', $IPv6); + $groupCount = count($matchesIP); + $colons = strpos($IPv6, '::'); + + if (count(preg_grep('/^[0-9A-Fa-f]{0,4}$/', $matchesIP, PREG_GREP_INVERT)) !== 0) { + $this->warnings[IPV6BadChar::CODE] = new IPV6BadChar(); + } + + if ($colons === false) { + // We need exactly the right number of groups + if ($groupCount !== $maxGroups) { + $this->warnings[IPV6GroupCount::CODE] = new IPV6GroupCount(); + } + return; + } + + if ($colons !== strrpos($IPv6, '::')) { + $this->warnings[IPV6DoubleColon::CODE] = new IPV6DoubleColon(); + return; + } + + if ($colons === 0 || $colons === (strlen($IPv6) - 2)) { + // RFC 4291 allows :: at the start or end of an address + //with 7 other groups in addition + ++$maxGroups; + } + + if ($groupCount > $maxGroups) { + $this->warnings[IPV6MaxGroups::CODE] = new IPV6MaxGroups(); + } elseif ($groupCount === $maxGroups) { + $this->warnings[IPV6Deprecated::CODE] = new IPV6Deprecated(); + } + } + + /** + * @return string + */ + protected function doParseDomainPart() + { + $domain = ''; + $label = ''; + $openedParenthesis = 0; + do { + $prev = $this->lexer->getPrevious(); + + $this->checkNotAllowedChars($this->lexer->token); + + if ($this->lexer->token['type'] === EmailLexer::S_OPENPARENTHESIS) { + $this->parseComments(); + $openedParenthesis += $this->getOpenedParenthesis(); + $this->lexer->moveNext(); + $tmpPrev = $this->lexer->getPrevious(); + if ($tmpPrev['type'] === EmailLexer::S_CLOSEPARENTHESIS) { + $openedParenthesis--; + } + } + if ($this->lexer->token['type'] === EmailLexer::S_CLOSEPARENTHESIS) { + if ($openedParenthesis === 0) { + throw new UnopenedComment(); + } else { + $openedParenthesis--; + } + } + + $this->checkConsecutiveDots(); + $this->checkDomainPartExceptions($prev); + + if ($this->hasBrackets()) { + $this->parseDomainLiteral(); + } + + if ($this->lexer->token['type'] === EmailLexer::S_DOT) { + $this->checkLabelLength($label); + $label = ''; + } else { + $label .= $this->lexer->token['value']; + } + + if ($this->isFWS()) { + $this->parseFWS(); + } + + $domain .= $this->lexer->token['value']; + $this->lexer->moveNext(); + if ($this->lexer->token['type'] === EmailLexer::S_SP) { + throw new CharNotAllowed(); + } + } while (null !== $this->lexer->token['type']); + + $this->checkLabelLength($label); + + return $domain; + } + + private function checkNotAllowedChars(array $token) + { + $notAllowed = [EmailLexer::S_BACKSLASH => true, EmailLexer::S_SLASH=> true]; + if (isset($notAllowed[$token['type']])) { + throw new CharNotAllowed(); + } + } + + /** + * @return string|false + */ + protected function parseDomainLiteral() + { + if ($this->lexer->isNextToken(EmailLexer::S_COLON)) { + $this->warnings[IPV6ColonStart::CODE] = new IPV6ColonStart(); + } + if ($this->lexer->isNextToken(EmailLexer::S_IPV6TAG)) { + $lexer = clone $this->lexer; + $lexer->moveNext(); + if ($lexer->isNextToken(EmailLexer::S_DOUBLECOLON)) { + $this->warnings[IPV6ColonStart::CODE] = new IPV6ColonStart(); + } + } + + return $this->doParseDomainLiteral(); + } + + /** + * @return string|false + */ + protected function doParseDomainLiteral() + { + $IPv6TAG = false; + $addressLiteral = ''; + do { + if ($this->lexer->token['type'] === EmailLexer::C_NUL) { + throw new ExpectingDTEXT(); + } + + if ($this->lexer->token['type'] === EmailLexer::INVALID || + $this->lexer->token['type'] === EmailLexer::C_DEL || + $this->lexer->token['type'] === EmailLexer::S_LF + ) { + $this->warnings[ObsoleteDTEXT::CODE] = new ObsoleteDTEXT(); + } + + if ($this->lexer->isNextTokenAny(array(EmailLexer::S_OPENQBRACKET, EmailLexer::S_OPENBRACKET))) { + throw new ExpectingDTEXT(); + } + + if ($this->lexer->isNextTokenAny( + array(EmailLexer::S_HTAB, EmailLexer::S_SP, $this->lexer->token['type'] === EmailLexer::CRLF) + )) { + $this->warnings[CFWSWithFWS::CODE] = new CFWSWithFWS(); + $this->parseFWS(); + } + + if ($this->lexer->isNextToken(EmailLexer::S_CR)) { + throw new CRNoLF(); + } + + if ($this->lexer->token['type'] === EmailLexer::S_BACKSLASH) { + $this->warnings[ObsoleteDTEXT::CODE] = new ObsoleteDTEXT(); + $addressLiteral .= $this->lexer->token['value']; + $this->lexer->moveNext(); + $this->validateQuotedPair(); + } + if ($this->lexer->token['type'] === EmailLexer::S_IPV6TAG) { + $IPv6TAG = true; + } + if ($this->lexer->token['type'] === EmailLexer::S_CLOSEQBRACKET) { + break; + } + + $addressLiteral .= $this->lexer->token['value']; + + } while ($this->lexer->moveNext()); + + $addressLiteral = str_replace('[', '', $addressLiteral); + $addressLiteral = $this->checkIPV4Tag($addressLiteral); + + if (false === $addressLiteral) { + return $addressLiteral; + } + + if (!$IPv6TAG) { + $this->warnings[DomainLiteral::CODE] = new DomainLiteral(); + return $addressLiteral; + } + + $this->warnings[AddressLiteral::CODE] = new AddressLiteral(); + + $this->checkIPV6Tag($addressLiteral); + + return $addressLiteral; + } + + /** + * @param string $addressLiteral + * + * @return string|false + */ + protected function checkIPV4Tag($addressLiteral) + { + $matchesIP = array(); + + // Extract IPv4 part from the end of the address-literal (if there is one) + if (preg_match( + '/\\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]?)$/', + $addressLiteral, + $matchesIP + ) > 0 + ) { + $index = strrpos($addressLiteral, $matchesIP[0]); + if ($index === 0) { + $this->warnings[AddressLiteral::CODE] = new AddressLiteral(); + return false; + } + // Convert IPv4 part to IPv6 format for further testing + $addressLiteral = substr($addressLiteral, 0, (int) $index) . '0:0'; + } + + return $addressLiteral; + } + + protected function checkDomainPartExceptions(array $prev) + { + $invalidDomainTokens = array( + EmailLexer::S_DQUOTE => true, + EmailLexer::S_SQUOTE => true, + EmailLexer::S_BACKTICK => true, + EmailLexer::S_SEMICOLON => true, + EmailLexer::S_GREATERTHAN => true, + EmailLexer::S_LOWERTHAN => true, + ); + + if (isset($invalidDomainTokens[$this->lexer->token['type']])) { + throw new ExpectingATEXT(); + } + + if ($this->lexer->token['type'] === EmailLexer::S_COMMA) { + throw new CommaInDomain(); + } + + if ($this->lexer->token['type'] === EmailLexer::S_AT) { + throw new ConsecutiveAt(); + } + + if ($this->lexer->token['type'] === EmailLexer::S_OPENQBRACKET && $prev['type'] !== EmailLexer::S_AT) { + throw new ExpectingATEXT(); + } + + if ($this->lexer->token['type'] === EmailLexer::S_HYPHEN && $this->lexer->isNextToken(EmailLexer::S_DOT)) { + throw new DomainHyphened(); + } + + if ($this->lexer->token['type'] === EmailLexer::S_BACKSLASH + && $this->lexer->isNextToken(EmailLexer::GENERIC)) { + throw new ExpectingATEXT(); + } + } + + /** + * @return bool + */ + protected function hasBrackets() + { + if ($this->lexer->token['type'] !== EmailLexer::S_OPENBRACKET) { + return false; + } + + try { + $this->lexer->find(EmailLexer::S_CLOSEBRACKET); + } catch (\RuntimeException $e) { + throw new ExpectingDomainLiteralClose(); + } + + return true; + } + + /** + * @param string $label + */ + protected function checkLabelLength($label) + { + if ($this->isLabelTooLong($label)) { + $this->warnings[LabelTooLong::CODE] = new LabelTooLong(); + } + } + + /** + * @param string $label + * @return bool + */ + private function isLabelTooLong($label) + { + if (preg_match('/[^\x00-\x7F]/', $label)) { + idn_to_ascii($label, IDNA_DEFAULT, INTL_IDNA_VARIANT_UTS46, $idnaInfo); + + return (bool) ($idnaInfo['errors'] & IDNA_ERROR_LABEL_TOO_LONG); + } + + return strlen($label) > self::LABEL_MAX_LENGTH; + } + + protected function parseDomainComments() + { + $this->isUnclosedComment(); + while (!$this->lexer->isNextToken(EmailLexer::S_CLOSEPARENTHESIS)) { + $this->warnEscaping(); + $this->lexer->moveNext(); + } + + $this->lexer->moveNext(); + if ($this->lexer->isNextToken(EmailLexer::S_DOT)) { + throw new ExpectingATEXT(); + } + } + + protected function addTLDWarnings() + { + if ($this->warnings[DomainLiteral::CODE]) { + $this->warnings[TLD::CODE] = new TLD(); + } + } +} diff --git a/egulias/email-validator/src/Parser/LocalPart.php b/egulias/email-validator/src/Parser/LocalPart.php new file mode 100644 index 00000000..3c21f34a --- /dev/null +++ b/egulias/email-validator/src/Parser/LocalPart.php @@ -0,0 +1,145 @@ +<?php + +namespace Egulias\EmailValidator\Parser; + +use Egulias\EmailValidator\Exception\DotAtEnd; +use Egulias\EmailValidator\Exception\DotAtStart; +use Egulias\EmailValidator\EmailLexer; +use Egulias\EmailValidator\Exception\ExpectingAT; +use Egulias\EmailValidator\Exception\ExpectingATEXT; +use Egulias\EmailValidator\Exception\UnclosedQuotedString; +use Egulias\EmailValidator\Exception\UnopenedComment; +use Egulias\EmailValidator\Warning\CFWSWithFWS; +use Egulias\EmailValidator\Warning\LocalTooLong; + +class LocalPart extends Parser +{ + public function parse($localPart) + { + $parseDQuote = true; + $closingQuote = false; + $openedParenthesis = 0; + $totalLength = 0; + + while ($this->lexer->token['type'] !== EmailLexer::S_AT && null !== $this->lexer->token['type']) { + if ($this->lexer->token['type'] === EmailLexer::S_DOT && null === $this->lexer->getPrevious()['type']) { + throw new DotAtStart(); + } + + $closingQuote = $this->checkDQUOTE($closingQuote); + if ($closingQuote && $parseDQuote) { + $parseDQuote = $this->parseDoubleQuote(); + } + + if ($this->lexer->token['type'] === EmailLexer::S_OPENPARENTHESIS) { + $this->parseComments(); + $openedParenthesis += $this->getOpenedParenthesis(); + } + + if ($this->lexer->token['type'] === EmailLexer::S_CLOSEPARENTHESIS) { + if ($openedParenthesis === 0) { + throw new UnopenedComment(); + } + + $openedParenthesis--; + } + + $this->checkConsecutiveDots(); + + if ($this->lexer->token['type'] === EmailLexer::S_DOT && + $this->lexer->isNextToken(EmailLexer::S_AT) + ) { + throw new DotAtEnd(); + } + + $this->warnEscaping(); + $this->isInvalidToken($this->lexer->token, $closingQuote); + + if ($this->isFWS()) { + $this->parseFWS(); + } + + $totalLength += strlen($this->lexer->token['value']); + $this->lexer->moveNext(); + } + + if ($totalLength > LocalTooLong::LOCAL_PART_LENGTH) { + $this->warnings[LocalTooLong::CODE] = new LocalTooLong(); + } + } + + /** + * @return bool + */ + protected function parseDoubleQuote() + { + $parseAgain = true; + $special = array( + EmailLexer::S_CR => true, + EmailLexer::S_HTAB => true, + EmailLexer::S_LF => true + ); + + $invalid = array( + EmailLexer::C_NUL => true, + EmailLexer::S_HTAB => true, + EmailLexer::S_CR => true, + EmailLexer::S_LF => true + ); + $setSpecialsWarning = true; + + $this->lexer->moveNext(); + + while ($this->lexer->token['type'] !== EmailLexer::S_DQUOTE && null !== $this->lexer->token['type']) { + $parseAgain = false; + if (isset($special[$this->lexer->token['type']]) && $setSpecialsWarning) { + $this->warnings[CFWSWithFWS::CODE] = new CFWSWithFWS(); + $setSpecialsWarning = false; + } + if ($this->lexer->token['type'] === EmailLexer::S_BACKSLASH && $this->lexer->isNextToken(EmailLexer::S_DQUOTE)) { + $this->lexer->moveNext(); + } + + $this->lexer->moveNext(); + + if (!$this->escaped() && isset($invalid[$this->lexer->token['type']])) { + throw new ExpectingATEXT(); + } + } + + $prev = $this->lexer->getPrevious(); + + if ($prev['type'] === EmailLexer::S_BACKSLASH) { + if (!$this->checkDQUOTE(false)) { + throw new UnclosedQuotedString(); + } + } + + if (!$this->lexer->isNextToken(EmailLexer::S_AT) && $prev['type'] !== EmailLexer::S_BACKSLASH) { + throw new ExpectingAT(); + } + + return $parseAgain; + } + + /** + * @param bool $closingQuote + */ + protected function isInvalidToken(array $token, $closingQuote) + { + $forbidden = array( + EmailLexer::S_COMMA, + EmailLexer::S_CLOSEBRACKET, + EmailLexer::S_OPENBRACKET, + EmailLexer::S_GREATERTHAN, + EmailLexer::S_LOWERTHAN, + EmailLexer::S_COLON, + EmailLexer::S_SEMICOLON, + EmailLexer::INVALID + ); + + if (in_array($token['type'], $forbidden) && !$closingQuote) { + throw new ExpectingATEXT(); + } + } +} diff --git a/egulias/email-validator/src/Parser/Parser.php b/egulias/email-validator/src/Parser/Parser.php new file mode 100644 index 00000000..ccdc9388 --- /dev/null +++ b/egulias/email-validator/src/Parser/Parser.php @@ -0,0 +1,249 @@ +<?php + +namespace Egulias\EmailValidator\Parser; + +use Egulias\EmailValidator\EmailLexer; +use Egulias\EmailValidator\Exception\AtextAfterCFWS; +use Egulias\EmailValidator\Exception\ConsecutiveDot; +use Egulias\EmailValidator\Exception\CRLFAtTheEnd; +use Egulias\EmailValidator\Exception\CRLFX2; +use Egulias\EmailValidator\Exception\CRNoLF; +use Egulias\EmailValidator\Exception\ExpectingQPair; +use Egulias\EmailValidator\Exception\ExpectingATEXT; +use Egulias\EmailValidator\Exception\ExpectingCTEXT; +use Egulias\EmailValidator\Exception\UnclosedComment; +use Egulias\EmailValidator\Exception\UnclosedQuotedString; +use Egulias\EmailValidator\Warning\CFWSNearAt; +use Egulias\EmailValidator\Warning\CFWSWithFWS; +use Egulias\EmailValidator\Warning\Comment; +use Egulias\EmailValidator\Warning\QuotedPart; +use Egulias\EmailValidator\Warning\QuotedString; + +abstract class Parser +{ + /** + * @var array + */ + protected $warnings = []; + + /** + * @var EmailLexer + */ + protected $lexer; + + /** + * @var int + */ + protected $openedParenthesis = 0; + + public function __construct(EmailLexer $lexer) + { + $this->lexer = $lexer; + } + + /** + * @return \Egulias\EmailValidator\Warning\Warning[] + */ + public function getWarnings() + { + return $this->warnings; + } + + /** + * @param string $str + */ + abstract public function parse($str); + + /** @return int */ + public function getOpenedParenthesis() + { + return $this->openedParenthesis; + } + + /** + * validateQuotedPair + */ + protected function validateQuotedPair() + { + if (!($this->lexer->token['type'] === EmailLexer::INVALID + || $this->lexer->token['type'] === EmailLexer::C_DEL)) { + throw new ExpectingQPair(); + } + + $this->warnings[QuotedPart::CODE] = + new QuotedPart($this->lexer->getPrevious()['type'], $this->lexer->token['type']); + } + + protected function parseComments() + { + $this->openedParenthesis = 1; + $this->isUnclosedComment(); + $this->warnings[Comment::CODE] = new Comment(); + while (!$this->lexer->isNextToken(EmailLexer::S_CLOSEPARENTHESIS)) { + if ($this->lexer->isNextToken(EmailLexer::S_OPENPARENTHESIS)) { + $this->openedParenthesis++; + } + $this->warnEscaping(); + $this->lexer->moveNext(); + } + + $this->lexer->moveNext(); + if ($this->lexer->isNextTokenAny(array(EmailLexer::GENERIC, EmailLexer::S_EMPTY))) { + throw new ExpectingATEXT(); + } + + if ($this->lexer->isNextToken(EmailLexer::S_AT)) { + $this->warnings[CFWSNearAt::CODE] = new CFWSNearAt(); + } + } + + /** + * @return bool + */ + protected function isUnclosedComment() + { + try { + $this->lexer->find(EmailLexer::S_CLOSEPARENTHESIS); + return true; + } catch (\RuntimeException $e) { + throw new UnclosedComment(); + } + } + + protected function parseFWS() + { + $previous = $this->lexer->getPrevious(); + + $this->checkCRLFInFWS(); + + if ($this->lexer->token['type'] === EmailLexer::S_CR) { + throw new CRNoLF(); + } + + if ($this->lexer->isNextToken(EmailLexer::GENERIC) && $previous['type'] !== EmailLexer::S_AT) { + throw new AtextAfterCFWS(); + } + + if ($this->lexer->token['type'] === EmailLexer::S_LF || $this->lexer->token['type'] === EmailLexer::C_NUL) { + throw new ExpectingCTEXT(); + } + + if ($this->lexer->isNextToken(EmailLexer::S_AT) || $previous['type'] === EmailLexer::S_AT) { + $this->warnings[CFWSNearAt::CODE] = new CFWSNearAt(); + } else { + $this->warnings[CFWSWithFWS::CODE] = new CFWSWithFWS(); + } + } + + protected function checkConsecutiveDots() + { + if ($this->lexer->token['type'] === EmailLexer::S_DOT && $this->lexer->isNextToken(EmailLexer::S_DOT)) { + throw new ConsecutiveDot(); + } + } + + /** + * @return bool + */ + protected function isFWS() + { + if ($this->escaped()) { + return false; + } + + if ($this->lexer->token['type'] === EmailLexer::S_SP || + $this->lexer->token['type'] === EmailLexer::S_HTAB || + $this->lexer->token['type'] === EmailLexer::S_CR || + $this->lexer->token['type'] === EmailLexer::S_LF || + $this->lexer->token['type'] === EmailLexer::CRLF + ) { + return true; + } + + return false; + } + + /** + * @return bool + */ + protected function escaped() + { + $previous = $this->lexer->getPrevious(); + + if ($previous && $previous['type'] === EmailLexer::S_BACKSLASH + && + $this->lexer->token['type'] !== EmailLexer::GENERIC + ) { + return true; + } + + return false; + } + + /** + * @return bool + */ + protected function warnEscaping() + { + if ($this->lexer->token['type'] !== EmailLexer::S_BACKSLASH) { + return false; + } + + if ($this->lexer->isNextToken(EmailLexer::GENERIC)) { + throw new ExpectingATEXT(); + } + + if (!$this->lexer->isNextTokenAny(array(EmailLexer::S_SP, EmailLexer::S_HTAB, EmailLexer::C_DEL))) { + return false; + } + + $this->warnings[QuotedPart::CODE] = + new QuotedPart($this->lexer->getPrevious()['type'], $this->lexer->token['type']); + return true; + + } + + /** + * @param bool $hasClosingQuote + * + * @return bool + */ + protected function checkDQUOTE($hasClosingQuote) + { + if ($this->lexer->token['type'] !== EmailLexer::S_DQUOTE) { + return $hasClosingQuote; + } + if ($hasClosingQuote) { + return $hasClosingQuote; + } + $previous = $this->lexer->getPrevious(); + if ($this->lexer->isNextToken(EmailLexer::GENERIC) && $previous['type'] === EmailLexer::GENERIC) { + throw new ExpectingATEXT(); + } + + try { + $this->lexer->find(EmailLexer::S_DQUOTE); + $hasClosingQuote = true; + } catch (\Exception $e) { + throw new UnclosedQuotedString(); + } + $this->warnings[QuotedString::CODE] = new QuotedString($previous['value'], $this->lexer->token['value']); + + return $hasClosingQuote; + } + + protected function checkCRLFInFWS() + { + if ($this->lexer->token['type'] !== EmailLexer::CRLF) { + return; + } + + if (!$this->lexer->isNextTokenAny(array(EmailLexer::S_SP, EmailLexer::S_HTAB))) { + throw new CRLFX2(); + } + + if (!$this->lexer->isNextTokenAny(array(EmailLexer::S_SP, EmailLexer::S_HTAB))) { + throw new CRLFAtTheEnd(); + } + } +} diff --git a/egulias/email-validator/src/Validation/DNSCheckValidation.php b/egulias/email-validator/src/Validation/DNSCheckValidation.php new file mode 100644 index 00000000..491082a5 --- /dev/null +++ b/egulias/email-validator/src/Validation/DNSCheckValidation.php @@ -0,0 +1,166 @@ +<?php + +namespace Egulias\EmailValidator\Validation; + +use Egulias\EmailValidator\EmailLexer; +use Egulias\EmailValidator\Exception\InvalidEmail; +use Egulias\EmailValidator\Exception\LocalOrReservedDomain; +use Egulias\EmailValidator\Exception\DomainAcceptsNoMail; +use Egulias\EmailValidator\Warning\NoDNSMXRecord; +use Egulias\EmailValidator\Exception\NoDNSRecord; + +class DNSCheckValidation implements EmailValidation +{ + /** + * @var array + */ + private $warnings = []; + + /** + * @var InvalidEmail|null + */ + private $error; + + /** + * @var array + */ + private $mxRecords = []; + + + public function __construct() + { + if (!function_exists('idn_to_ascii')) { + throw new \LogicException(sprintf('The %s class requires the Intl extension.', __CLASS__)); + } + } + + public function isValid($email, EmailLexer $emailLexer) + { + // use the input to check DNS if we cannot extract something similar to a domain + $host = $email; + + // Arguable pattern to extract the domain. Not aiming to validate the domain nor the email + if (false !== $lastAtPos = strrpos($email, '@')) { + $host = substr($email, $lastAtPos + 1); + } + + // Get the domain parts + $hostParts = explode('.', $host); + + // Reserved Top Level DNS Names (https://tools.ietf.org/html/rfc2606#section-2), + // mDNS and private DNS Namespaces (https://tools.ietf.org/html/rfc6762#appendix-G) + $reservedTopLevelDnsNames = [ + // Reserved Top Level DNS Names + 'test', + 'example', + 'invalid', + 'localhost', + + // mDNS + 'local', + + // Private DNS Namespaces + 'intranet', + 'internal', + 'private', + 'corp', + 'home', + 'lan', + ]; + + $isLocalDomain = count($hostParts) <= 1; + $isReservedTopLevel = in_array($hostParts[(count($hostParts) - 1)], $reservedTopLevelDnsNames, true); + + // Exclude reserved top level DNS names + if ($isLocalDomain || $isReservedTopLevel) { + $this->error = new LocalOrReservedDomain(); + return false; + } + + return $this->checkDns($host); + } + + public function getError() + { + return $this->error; + } + + public function getWarnings() + { + return $this->warnings; + } + + /** + * @param string $host + * + * @return bool + */ + protected function checkDns($host) + { + $variant = INTL_IDNA_VARIANT_UTS46; + + $host = rtrim(idn_to_ascii($host, IDNA_DEFAULT, $variant), '.') . '.'; + + return $this->validateDnsRecords($host); + } + + + /** + * Validate the DNS records for given host. + * + * @param string $host A set of DNS records in the format returned by dns_get_record. + * + * @return bool True on success. + */ + private function validateDnsRecords($host) + { + // Get all MX, A and AAAA DNS records for host + // Using @ as workaround to fix https://bugs.php.net/bug.php?id=73149 + $dnsRecords = @dns_get_record($host, DNS_MX + DNS_A + DNS_AAAA); + + + // No MX, A or AAAA DNS records + if (empty($dnsRecords)) { + $this->error = new NoDNSRecord(); + return false; + } + + // For each DNS record + foreach ($dnsRecords as $dnsRecord) { + if (!$this->validateMXRecord($dnsRecord)) { + return false; + } + } + + // No MX records (fallback to A or AAAA records) + if (empty($this->mxRecords)) { + $this->warnings[NoDNSMXRecord::CODE] = new NoDNSMXRecord(); + } + + return true; + } + + /** + * Validate an MX record + * + * @param array $dnsRecord Given DNS record. + * + * @return bool True if valid. + */ + private function validateMxRecord($dnsRecord) + { + if ($dnsRecord['type'] !== 'MX') { + return true; + } + + // "Null MX" record indicates the domain accepts no mail (https://tools.ietf.org/html/rfc7505) + if (empty($dnsRecord['target']) || $dnsRecord['target'] === '.') { + $this->error = new DomainAcceptsNoMail(); + return false; + } + + $this->mxRecords[] = $dnsRecord; + + return true; + } +} diff --git a/egulias/email-validator/src/Validation/EmailValidation.php b/egulias/email-validator/src/Validation/EmailValidation.php new file mode 100644 index 00000000..d5a015be --- /dev/null +++ b/egulias/email-validator/src/Validation/EmailValidation.php @@ -0,0 +1,34 @@ +<?php + +namespace Egulias\EmailValidator\Validation; + +use Egulias\EmailValidator\EmailLexer; +use Egulias\EmailValidator\Exception\InvalidEmail; +use Egulias\EmailValidator\Warning\Warning; + +interface EmailValidation +{ + /** + * Returns true if the given email is valid. + * + * @param string $email The email you want to validate. + * @param EmailLexer $emailLexer The email lexer. + * + * @return bool + */ + public function isValid($email, EmailLexer $emailLexer); + + /** + * Returns the validation error. + * + * @return InvalidEmail|null + */ + public function getError(); + + /** + * Returns the validation warnings. + * + * @return Warning[] + */ + public function getWarnings(); +} diff --git a/egulias/email-validator/src/Validation/Error/RFCWarnings.php b/egulias/email-validator/src/Validation/Error/RFCWarnings.php new file mode 100644 index 00000000..7f2256d6 --- /dev/null +++ b/egulias/email-validator/src/Validation/Error/RFCWarnings.php @@ -0,0 +1,11 @@ +<?php + +namespace Egulias\EmailValidator\Validation\Error; + +use Egulias\EmailValidator\Exception\InvalidEmail; + +class RFCWarnings extends InvalidEmail +{ + const CODE = 997; + const REASON = 'Warnings were found.'; +} diff --git a/egulias/email-validator/src/Validation/Error/SpoofEmail.php b/egulias/email-validator/src/Validation/Error/SpoofEmail.php new file mode 100644 index 00000000..8c92cb5a --- /dev/null +++ b/egulias/email-validator/src/Validation/Error/SpoofEmail.php @@ -0,0 +1,11 @@ +<?php + +namespace Egulias\EmailValidator\Validation\Error; + +use Egulias\EmailValidator\Exception\InvalidEmail; + +class SpoofEmail extends InvalidEmail +{ + const CODE = 998; + const REASON = "The email contains mixed UTF8 chars that makes it suspicious"; +} diff --git a/egulias/email-validator/src/Validation/Exception/EmptyValidationList.php b/egulias/email-validator/src/Validation/Exception/EmptyValidationList.php new file mode 100644 index 00000000..ee7c41aa --- /dev/null +++ b/egulias/email-validator/src/Validation/Exception/EmptyValidationList.php @@ -0,0 +1,16 @@ +<?php + +namespace Egulias\EmailValidator\Validation\Exception; + +use Exception; + +class EmptyValidationList extends \InvalidArgumentException +{ + /** + * @param int $code + */ + public function __construct($code = 0, Exception $previous = null) + { + parent::__construct("Empty validation list is not allowed", $code, $previous); + } +} diff --git a/egulias/email-validator/src/Validation/MultipleErrors.php b/egulias/email-validator/src/Validation/MultipleErrors.php new file mode 100644 index 00000000..3be59732 --- /dev/null +++ b/egulias/email-validator/src/Validation/MultipleErrors.php @@ -0,0 +1,32 @@ +<?php + +namespace Egulias\EmailValidator\Validation; + +use Egulias\EmailValidator\Exception\InvalidEmail; + +class MultipleErrors extends InvalidEmail +{ + const CODE = 999; + const REASON = "Accumulated errors for multiple validations"; + /** + * @var InvalidEmail[] + */ + private $errors = []; + + /** + * @param InvalidEmail[] $errors + */ + public function __construct(array $errors) + { + $this->errors = $errors; + parent::__construct(); + } + + /** + * @return InvalidEmail[] + */ + public function getErrors() + { + return $this->errors; + } +} diff --git a/egulias/email-validator/src/Validation/MultipleValidationWithAnd.php b/egulias/email-validator/src/Validation/MultipleValidationWithAnd.php new file mode 100644 index 00000000..feb22402 --- /dev/null +++ b/egulias/email-validator/src/Validation/MultipleValidationWithAnd.php @@ -0,0 +1,124 @@ +<?php + +namespace Egulias\EmailValidator\Validation; + +use Egulias\EmailValidator\EmailLexer; +use Egulias\EmailValidator\Validation\Exception\EmptyValidationList; + +class MultipleValidationWithAnd implements EmailValidation +{ + /** + * If one of validations gets failure skips all succeeding validation. + * This means MultipleErrors will only contain a single error which first found. + */ + const STOP_ON_ERROR = 0; + + /** + * All of validations will be invoked even if one of them got failure. + * So MultipleErrors will contain all causes. + */ + const ALLOW_ALL_ERRORS = 1; + + /** + * @var EmailValidation[] + */ + private $validations = []; + + /** + * @var array + */ + private $warnings = []; + + /** + * @var MultipleErrors|null + */ + private $error; + + /** + * @var int + */ + private $mode; + + /** + * @param EmailValidation[] $validations The validations. + * @param int $mode The validation mode (one of the constants). + */ + public function __construct(array $validations, $mode = self::ALLOW_ALL_ERRORS) + { + if (count($validations) == 0) { + throw new EmptyValidationList(); + } + + $this->validations = $validations; + $this->mode = $mode; + } + + /** + * {@inheritdoc} + */ + public function isValid($email, EmailLexer $emailLexer) + { + $result = true; + $errors = []; + foreach ($this->validations as $validation) { + $emailLexer->reset(); + $validationResult = $validation->isValid($email, $emailLexer); + $result = $result && $validationResult; + $this->warnings = array_merge($this->warnings, $validation->getWarnings()); + $errors = $this->addNewError($validation->getError(), $errors); + + if ($this->shouldStop($result)) { + break; + } + } + + if (!empty($errors)) { + $this->error = new MultipleErrors($errors); + } + + return $result; + } + + /** + * @param \Egulias\EmailValidator\Exception\InvalidEmail|null $possibleError + * @param \Egulias\EmailValidator\Exception\InvalidEmail[] $errors + * + * @return \Egulias\EmailValidator\Exception\InvalidEmail[] + */ + private function addNewError($possibleError, array $errors) + { + if (null !== $possibleError) { + $errors[] = $possibleError; + } + + return $errors; + } + + /** + * @param bool $result + * + * @return bool + */ + private function shouldStop($result) + { + return !$result && $this->mode === self::STOP_ON_ERROR; + } + + /** + * Returns the validation errors. + * + * @return MultipleErrors|null + */ + public function getError() + { + return $this->error; + } + + /** + * {@inheritdoc} + */ + public function getWarnings() + { + return $this->warnings; + } +} diff --git a/egulias/email-validator/src/Validation/NoRFCWarningsValidation.php b/egulias/email-validator/src/Validation/NoRFCWarningsValidation.php new file mode 100644 index 00000000..6b31e544 --- /dev/null +++ b/egulias/email-validator/src/Validation/NoRFCWarningsValidation.php @@ -0,0 +1,41 @@ +<?php + +namespace Egulias\EmailValidator\Validation; + +use Egulias\EmailValidator\EmailLexer; +use Egulias\EmailValidator\Exception\InvalidEmail; +use Egulias\EmailValidator\Validation\Error\RFCWarnings; + +class NoRFCWarningsValidation extends RFCValidation +{ + /** + * @var InvalidEmail|null + */ + private $error; + + /** + * {@inheritdoc} + */ + public function isValid($email, EmailLexer $emailLexer) + { + if (!parent::isValid($email, $emailLexer)) { + return false; + } + + if (empty($this->getWarnings())) { + return true; + } + + $this->error = new RFCWarnings(); + + return false; + } + + /** + * {@inheritdoc} + */ + public function getError() + { + return $this->error ?: parent::getError(); + } +} diff --git a/egulias/email-validator/src/Validation/RFCValidation.php b/egulias/email-validator/src/Validation/RFCValidation.php new file mode 100644 index 00000000..8781e0b6 --- /dev/null +++ b/egulias/email-validator/src/Validation/RFCValidation.php @@ -0,0 +1,49 @@ +<?php + +namespace Egulias\EmailValidator\Validation; + +use Egulias\EmailValidator\EmailLexer; +use Egulias\EmailValidator\EmailParser; +use Egulias\EmailValidator\Exception\InvalidEmail; + +class RFCValidation implements EmailValidation +{ + /** + * @var EmailParser|null + */ + private $parser; + + /** + * @var array + */ + private $warnings = []; + + /** + * @var InvalidEmail|null + */ + private $error; + + public function isValid($email, EmailLexer $emailLexer) + { + $this->parser = new EmailParser($emailLexer); + try { + $this->parser->parse((string)$email); + } catch (InvalidEmail $invalid) { + $this->error = $invalid; + return false; + } + + $this->warnings = $this->parser->getWarnings(); + return true; + } + + public function getError() + { + return $this->error; + } + + public function getWarnings() + { + return $this->warnings; + } +} diff --git a/egulias/email-validator/src/Validation/SpoofCheckValidation.php b/egulias/email-validator/src/Validation/SpoofCheckValidation.php new file mode 100644 index 00000000..e10bfabd --- /dev/null +++ b/egulias/email-validator/src/Validation/SpoofCheckValidation.php @@ -0,0 +1,51 @@ +<?php + +namespace Egulias\EmailValidator\Validation; + +use Egulias\EmailValidator\EmailLexer; +use Egulias\EmailValidator\Exception\InvalidEmail; +use Egulias\EmailValidator\Validation\Error\SpoofEmail; +use \Spoofchecker; + +class SpoofCheckValidation implements EmailValidation +{ + /** + * @var InvalidEmail|null + */ + private $error; + + public function __construct() + { + if (!extension_loaded('intl')) { + throw new \LogicException(sprintf('The %s class requires the Intl extension.', __CLASS__)); + } + } + + /** + * @psalm-suppress InvalidArgument + */ + public function isValid($email, EmailLexer $emailLexer) + { + $checker = new Spoofchecker(); + $checker->setChecks(Spoofchecker::SINGLE_SCRIPT); + + if ($checker->isSuspicious($email)) { + $this->error = new SpoofEmail(); + } + + return $this->error === null; + } + + /** + * @return InvalidEmail|null + */ + public function getError() + { + return $this->error; + } + + public function getWarnings() + { + return []; + } +} diff --git a/egulias/email-validator/src/Warning/AddressLiteral.php b/egulias/email-validator/src/Warning/AddressLiteral.php new file mode 100644 index 00000000..77e70f7f --- /dev/null +++ b/egulias/email-validator/src/Warning/AddressLiteral.php @@ -0,0 +1,14 @@ +<?php + +namespace Egulias\EmailValidator\Warning; + +class AddressLiteral extends Warning +{ + const CODE = 12; + + public function __construct() + { + $this->message = 'Address literal in domain part'; + $this->rfcNumber = 5321; + } +} diff --git a/egulias/email-validator/src/Warning/CFWSNearAt.php b/egulias/email-validator/src/Warning/CFWSNearAt.php new file mode 100644 index 00000000..be43bbe6 --- /dev/null +++ b/egulias/email-validator/src/Warning/CFWSNearAt.php @@ -0,0 +1,13 @@ +<?php + +namespace Egulias\EmailValidator\Warning; + +class CFWSNearAt extends Warning +{ + const CODE = 49; + + public function __construct() + { + $this->message = "Deprecated folding white space near @"; + } +} diff --git a/egulias/email-validator/src/Warning/CFWSWithFWS.php b/egulias/email-validator/src/Warning/CFWSWithFWS.php new file mode 100644 index 00000000..dea3450e --- /dev/null +++ b/egulias/email-validator/src/Warning/CFWSWithFWS.php @@ -0,0 +1,13 @@ +<?php + +namespace Egulias\EmailValidator\Warning; + +class CFWSWithFWS extends Warning +{ + const CODE = 18; + + public function __construct() + { + $this->message = 'Folding whites space followed by folding white space'; + } +} diff --git a/egulias/email-validator/src/Warning/Comment.php b/egulias/email-validator/src/Warning/Comment.php new file mode 100644 index 00000000..704c2908 --- /dev/null +++ b/egulias/email-validator/src/Warning/Comment.php @@ -0,0 +1,13 @@ +<?php + +namespace Egulias\EmailValidator\Warning; + +class Comment extends Warning +{ + const CODE = 17; + + public function __construct() + { + $this->message = "Comments found in this email"; + } +} diff --git a/egulias/email-validator/src/Warning/DeprecatedComment.php b/egulias/email-validator/src/Warning/DeprecatedComment.php new file mode 100644 index 00000000..ad43bd7c --- /dev/null +++ b/egulias/email-validator/src/Warning/DeprecatedComment.php @@ -0,0 +1,13 @@ +<?php + +namespace Egulias\EmailValidator\Warning; + +class DeprecatedComment extends Warning +{ + const CODE = 37; + + public function __construct() + { + $this->message = 'Deprecated comments'; + } +} diff --git a/egulias/email-validator/src/Warning/DomainLiteral.php b/egulias/email-validator/src/Warning/DomainLiteral.php new file mode 100644 index 00000000..6f36b5e2 --- /dev/null +++ b/egulias/email-validator/src/Warning/DomainLiteral.php @@ -0,0 +1,14 @@ +<?php + +namespace Egulias\EmailValidator\Warning; + +class DomainLiteral extends Warning +{ + const CODE = 70; + + public function __construct() + { + $this->message = 'Domain Literal'; + $this->rfcNumber = 5322; + } +} diff --git a/egulias/email-validator/src/Warning/DomainTooLong.php b/egulias/email-validator/src/Warning/DomainTooLong.php new file mode 100644 index 00000000..61ff17a7 --- /dev/null +++ b/egulias/email-validator/src/Warning/DomainTooLong.php @@ -0,0 +1,14 @@ +<?php + +namespace Egulias\EmailValidator\Warning; + +class DomainTooLong extends Warning +{ + const CODE = 255; + + public function __construct() + { + $this->message = 'Domain is too long, exceeds 255 chars'; + $this->rfcNumber = 5322; + } +} diff --git a/egulias/email-validator/src/Warning/EmailTooLong.php b/egulias/email-validator/src/Warning/EmailTooLong.php new file mode 100644 index 00000000..497309db --- /dev/null +++ b/egulias/email-validator/src/Warning/EmailTooLong.php @@ -0,0 +1,15 @@ +<?php + +namespace Egulias\EmailValidator\Warning; + +use Egulias\EmailValidator\EmailParser; + +class EmailTooLong extends Warning +{ + const CODE = 66; + + public function __construct() + { + $this->message = 'Email is too long, exceeds ' . EmailParser::EMAIL_MAX_LENGTH; + } +} diff --git a/egulias/email-validator/src/Warning/IPV6BadChar.php b/egulias/email-validator/src/Warning/IPV6BadChar.php new file mode 100644 index 00000000..ba2fcc01 --- /dev/null +++ b/egulias/email-validator/src/Warning/IPV6BadChar.php @@ -0,0 +1,14 @@ +<?php + +namespace Egulias\EmailValidator\Warning; + +class IPV6BadChar extends Warning +{ + const CODE = 74; + + public function __construct() + { + $this->message = 'Bad char in IPV6 domain literal'; + $this->rfcNumber = 5322; + } +} diff --git a/egulias/email-validator/src/Warning/IPV6ColonEnd.php b/egulias/email-validator/src/Warning/IPV6ColonEnd.php new file mode 100644 index 00000000..41afa78c --- /dev/null +++ b/egulias/email-validator/src/Warning/IPV6ColonEnd.php @@ -0,0 +1,14 @@ +<?php + +namespace Egulias\EmailValidator\Warning; + +class IPV6ColonEnd extends Warning +{ + const CODE = 77; + + public function __construct() + { + $this->message = ':: found at the end of the domain literal'; + $this->rfcNumber = 5322; + } +} diff --git a/egulias/email-validator/src/Warning/IPV6ColonStart.php b/egulias/email-validator/src/Warning/IPV6ColonStart.php new file mode 100644 index 00000000..1bf754e3 --- /dev/null +++ b/egulias/email-validator/src/Warning/IPV6ColonStart.php @@ -0,0 +1,14 @@ +<?php + +namespace Egulias\EmailValidator\Warning; + +class IPV6ColonStart extends Warning +{ + const CODE = 76; + + public function __construct() + { + $this->message = ':: found at the start of the domain literal'; + $this->rfcNumber = 5322; + } +} diff --git a/egulias/email-validator/src/Warning/IPV6Deprecated.php b/egulias/email-validator/src/Warning/IPV6Deprecated.php new file mode 100644 index 00000000..d752caaa --- /dev/null +++ b/egulias/email-validator/src/Warning/IPV6Deprecated.php @@ -0,0 +1,14 @@ +<?php + +namespace Egulias\EmailValidator\Warning; + +class IPV6Deprecated extends Warning +{ + const CODE = 13; + + public function __construct() + { + $this->message = 'Deprecated form of IPV6'; + $this->rfcNumber = 5321; + } +} diff --git a/egulias/email-validator/src/Warning/IPV6DoubleColon.php b/egulias/email-validator/src/Warning/IPV6DoubleColon.php new file mode 100644 index 00000000..4f823949 --- /dev/null +++ b/egulias/email-validator/src/Warning/IPV6DoubleColon.php @@ -0,0 +1,14 @@ +<?php + +namespace Egulias\EmailValidator\Warning; + +class IPV6DoubleColon extends Warning +{ + const CODE = 73; + + public function __construct() + { + $this->message = 'Double colon found after IPV6 tag'; + $this->rfcNumber = 5322; + } +} diff --git a/egulias/email-validator/src/Warning/IPV6GroupCount.php b/egulias/email-validator/src/Warning/IPV6GroupCount.php new file mode 100644 index 00000000..a59d317f --- /dev/null +++ b/egulias/email-validator/src/Warning/IPV6GroupCount.php @@ -0,0 +1,14 @@ +<?php + +namespace Egulias\EmailValidator\Warning; + +class IPV6GroupCount extends Warning +{ + const CODE = 72; + + public function __construct() + { + $this->message = 'Group count is not IPV6 valid'; + $this->rfcNumber = 5322; + } +} diff --git a/egulias/email-validator/src/Warning/IPV6MaxGroups.php b/egulias/email-validator/src/Warning/IPV6MaxGroups.php new file mode 100644 index 00000000..936274c1 --- /dev/null +++ b/egulias/email-validator/src/Warning/IPV6MaxGroups.php @@ -0,0 +1,14 @@ +<?php + +namespace Egulias\EmailValidator\Warning; + +class IPV6MaxGroups extends Warning +{ + const CODE = 75; + + public function __construct() + { + $this->message = 'Reached the maximum number of IPV6 groups allowed'; + $this->rfcNumber = 5321; + } +} diff --git a/egulias/email-validator/src/Warning/LabelTooLong.php b/egulias/email-validator/src/Warning/LabelTooLong.php new file mode 100644 index 00000000..daf07f40 --- /dev/null +++ b/egulias/email-validator/src/Warning/LabelTooLong.php @@ -0,0 +1,14 @@ +<?php + +namespace Egulias\EmailValidator\Warning; + +class LabelTooLong extends Warning +{ + const CODE = 63; + + public function __construct() + { + $this->message = 'Label too long'; + $this->rfcNumber = 5322; + } +} diff --git a/egulias/email-validator/src/Warning/LocalTooLong.php b/egulias/email-validator/src/Warning/LocalTooLong.php new file mode 100644 index 00000000..0d08d8b3 --- /dev/null +++ b/egulias/email-validator/src/Warning/LocalTooLong.php @@ -0,0 +1,15 @@ +<?php + +namespace Egulias\EmailValidator\Warning; + +class LocalTooLong extends Warning +{ + const CODE = 64; + const LOCAL_PART_LENGTH = 64; + + public function __construct() + { + $this->message = 'Local part is too long, exceeds 64 chars (octets)'; + $this->rfcNumber = 5322; + } +} diff --git a/egulias/email-validator/src/Warning/NoDNSMXRecord.php b/egulias/email-validator/src/Warning/NoDNSMXRecord.php new file mode 100644 index 00000000..b3c21a1f --- /dev/null +++ b/egulias/email-validator/src/Warning/NoDNSMXRecord.php @@ -0,0 +1,14 @@ +<?php + +namespace Egulias\EmailValidator\Warning; + +class NoDNSMXRecord extends Warning +{ + const CODE = 6; + + public function __construct() + { + $this->message = 'No MX DSN record was found for this email'; + $this->rfcNumber = 5321; + } +} diff --git a/egulias/email-validator/src/Warning/ObsoleteDTEXT.php b/egulias/email-validator/src/Warning/ObsoleteDTEXT.php new file mode 100644 index 00000000..10f19e33 --- /dev/null +++ b/egulias/email-validator/src/Warning/ObsoleteDTEXT.php @@ -0,0 +1,14 @@ +<?php + +namespace Egulias\EmailValidator\Warning; + +class ObsoleteDTEXT extends Warning +{ + const CODE = 71; + + public function __construct() + { + $this->rfcNumber = 5322; + $this->message = 'Obsolete DTEXT in domain literal'; + } +} diff --git a/egulias/email-validator/src/Warning/QuotedPart.php b/egulias/email-validator/src/Warning/QuotedPart.php new file mode 100644 index 00000000..36a4265a --- /dev/null +++ b/egulias/email-validator/src/Warning/QuotedPart.php @@ -0,0 +1,17 @@ +<?php + +namespace Egulias\EmailValidator\Warning; + +class QuotedPart extends Warning +{ + const CODE = 36; + + /** + * @param scalar $prevToken + * @param scalar $postToken + */ + public function __construct($prevToken, $postToken) + { + $this->message = "Deprecated Quoted String found between $prevToken and $postToken"; + } +} diff --git a/egulias/email-validator/src/Warning/QuotedString.php b/egulias/email-validator/src/Warning/QuotedString.php new file mode 100644 index 00000000..817e4e84 --- /dev/null +++ b/egulias/email-validator/src/Warning/QuotedString.php @@ -0,0 +1,17 @@ +<?php + +namespace Egulias\EmailValidator\Warning; + +class QuotedString extends Warning +{ + const CODE = 11; + + /** + * @param scalar $prevToken + * @param scalar $postToken + */ + public function __construct($prevToken, $postToken) + { + $this->message = "Quoted String found between $prevToken and $postToken"; + } +} diff --git a/egulias/email-validator/src/Warning/TLD.php b/egulias/email-validator/src/Warning/TLD.php new file mode 100644 index 00000000..2338b9f4 --- /dev/null +++ b/egulias/email-validator/src/Warning/TLD.php @@ -0,0 +1,13 @@ +<?php + +namespace Egulias\EmailValidator\Warning; + +class TLD extends Warning +{ + const CODE = 9; + + public function __construct() + { + $this->message = "RFC5321, TLD"; + } +} diff --git a/egulias/email-validator/src/Warning/Warning.php b/egulias/email-validator/src/Warning/Warning.php new file mode 100644 index 00000000..a2ee7b0d --- /dev/null +++ b/egulias/email-validator/src/Warning/Warning.php @@ -0,0 +1,47 @@ +<?php + +namespace Egulias\EmailValidator\Warning; + +abstract class Warning +{ + const CODE = 0; + + /** + * @var string + */ + protected $message = ''; + + /** + * @var int + */ + protected $rfcNumber = 0; + + /** + * @return string + */ + public function message() + { + return $this->message; + } + + /** + * @return int + */ + public function code() + { + return static::CODE; + } + + /** + * @return int + */ + public function RFCNumber() + { + return $this->rfcNumber; + } + + public function __toString() + { + return $this->message() . " rfc: " . $this->rfcNumber . "interal code: " . static::CODE; + } +} |