Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/nextcloud/3rdparty.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChristoph Wurst <christoph@winzerhof-wurst.at>2020-12-30 11:35:38 +0300
committerChristoph Wurst <christoph@winzerhof-wurst.at>2020-12-30 11:37:26 +0300
commit3cf3dc9cf29a491cf191bbff9b791ae523c54c03 (patch)
tree3098bec3d3499cd5fb4347111644772cac0c9891 /egulias
parentff73290b41ae99ae9d601e77407b6a65a35a25e7 (diff)
Require missing email validator
Signed-off-by: Christoph Wurst <christoph@winzerhof-wurst.at>
Diffstat (limited to 'egulias')
-rw-r--r--egulias/email-validator/src/EmailLexer.php283
-rw-r--r--egulias/email-validator/src/EmailParser.php137
-rw-r--r--egulias/email-validator/src/EmailValidator.php67
-rw-r--r--egulias/email-validator/src/Exception/AtextAfterCFWS.php9
-rw-r--r--egulias/email-validator/src/Exception/CRLFAtTheEnd.php9
-rw-r--r--egulias/email-validator/src/Exception/CRLFX2.php9
-rw-r--r--egulias/email-validator/src/Exception/CRNoLF.php9
-rw-r--r--egulias/email-validator/src/Exception/CharNotAllowed.php9
-rw-r--r--egulias/email-validator/src/Exception/CommaInDomain.php9
-rw-r--r--egulias/email-validator/src/Exception/ConsecutiveAt.php9
-rw-r--r--egulias/email-validator/src/Exception/ConsecutiveDot.php9
-rw-r--r--egulias/email-validator/src/Exception/DomainAcceptsNoMail.php9
-rw-r--r--egulias/email-validator/src/Exception/DomainHyphened.php9
-rw-r--r--egulias/email-validator/src/Exception/DotAtEnd.php9
-rw-r--r--egulias/email-validator/src/Exception/DotAtStart.php9
-rw-r--r--egulias/email-validator/src/Exception/ExpectingAT.php9
-rw-r--r--egulias/email-validator/src/Exception/ExpectingATEXT.php9
-rw-r--r--egulias/email-validator/src/Exception/ExpectingCTEXT.php9
-rw-r--r--egulias/email-validator/src/Exception/ExpectingDTEXT.php9
-rw-r--r--egulias/email-validator/src/Exception/ExpectingDomainLiteralClose.php9
-rw-r--r--egulias/email-validator/src/Exception/ExpectingQPair.php9
-rw-r--r--egulias/email-validator/src/Exception/InvalidEmail.php14
-rw-r--r--egulias/email-validator/src/Exception/LocalOrReservedDomain.php9
-rw-r--r--egulias/email-validator/src/Exception/NoDNSRecord.php9
-rw-r--r--egulias/email-validator/src/Exception/NoDomainPart.php9
-rw-r--r--egulias/email-validator/src/Exception/NoLocalPart.php9
-rw-r--r--egulias/email-validator/src/Exception/UnclosedComment.php9
-rw-r--r--egulias/email-validator/src/Exception/UnclosedQuotedString.php9
-rw-r--r--egulias/email-validator/src/Exception/UnopenedComment.php9
-rw-r--r--egulias/email-validator/src/Parser/DomainPart.php443
-rw-r--r--egulias/email-validator/src/Parser/LocalPart.php145
-rw-r--r--egulias/email-validator/src/Parser/Parser.php249
-rw-r--r--egulias/email-validator/src/Validation/DNSCheckValidation.php166
-rw-r--r--egulias/email-validator/src/Validation/EmailValidation.php34
-rw-r--r--egulias/email-validator/src/Validation/Error/RFCWarnings.php11
-rw-r--r--egulias/email-validator/src/Validation/Error/SpoofEmail.php11
-rw-r--r--egulias/email-validator/src/Validation/Exception/EmptyValidationList.php16
-rw-r--r--egulias/email-validator/src/Validation/MultipleErrors.php32
-rw-r--r--egulias/email-validator/src/Validation/MultipleValidationWithAnd.php124
-rw-r--r--egulias/email-validator/src/Validation/NoRFCWarningsValidation.php41
-rw-r--r--egulias/email-validator/src/Validation/RFCValidation.php49
-rw-r--r--egulias/email-validator/src/Validation/SpoofCheckValidation.php51
-rw-r--r--egulias/email-validator/src/Warning/AddressLiteral.php14
-rw-r--r--egulias/email-validator/src/Warning/CFWSNearAt.php13
-rw-r--r--egulias/email-validator/src/Warning/CFWSWithFWS.php13
-rw-r--r--egulias/email-validator/src/Warning/Comment.php13
-rw-r--r--egulias/email-validator/src/Warning/DeprecatedComment.php13
-rw-r--r--egulias/email-validator/src/Warning/DomainLiteral.php14
-rw-r--r--egulias/email-validator/src/Warning/DomainTooLong.php14
-rw-r--r--egulias/email-validator/src/Warning/EmailTooLong.php15
-rw-r--r--egulias/email-validator/src/Warning/IPV6BadChar.php14
-rw-r--r--egulias/email-validator/src/Warning/IPV6ColonEnd.php14
-rw-r--r--egulias/email-validator/src/Warning/IPV6ColonStart.php14
-rw-r--r--egulias/email-validator/src/Warning/IPV6Deprecated.php14
-rw-r--r--egulias/email-validator/src/Warning/IPV6DoubleColon.php14
-rw-r--r--egulias/email-validator/src/Warning/IPV6GroupCount.php14
-rw-r--r--egulias/email-validator/src/Warning/IPV6MaxGroups.php14
-rw-r--r--egulias/email-validator/src/Warning/LabelTooLong.php14
-rw-r--r--egulias/email-validator/src/Warning/LocalTooLong.php15
-rw-r--r--egulias/email-validator/src/Warning/NoDNSMXRecord.php14
-rw-r--r--egulias/email-validator/src/Warning/ObsoleteDTEXT.php14
-rw-r--r--egulias/email-validator/src/Warning/QuotedPart.php17
-rw-r--r--egulias/email-validator/src/Warning/QuotedString.php17
-rw-r--r--egulias/email-validator/src/Warning/TLD.php13
-rw-r--r--egulias/email-validator/src/Warning/Warning.php47
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;
+ }
+}