Message-id validator (#290)

* scafolding for MessageID validation

* Improve domain valid tokens

* Improved EmailParser to remove leaked logic. User of lexer recorder within parsers.

* MessageIDParser passing tests.

* change left for right, which is the right one

* psaml errors

* Better naming

* comments are not allowed in IDLeft for message-id

* Suppress psalm inheritance over tokens and dependencies

* Update src/Parser.php

Co-authored-by: Alexander M. Turek <me@derrabus.de>

* Update src/Parser.php

Co-authored-by: Alexander M. Turek <me@derrabus.de>

* improve parser from comments

Co-authored-by: Alexander M. Turek <me@derrabus.de>
This commit is contained in:
Eduardo Gulias Davis
2021-03-06 18:29:10 +01:00
committed by GitHub
parent 451b438902
commit 8e526a57c1
17 changed files with 559 additions and 242 deletions
+1 -1
View File
@@ -11,7 +11,7 @@ use Egulias\EmailValidator\Result\Reason\UnclosedComment;
use Egulias\EmailValidator\Result\Reason\UnOpenedComment;
use Egulias\EmailValidator\Warning\Comment as WarningComment;
class Comment extends Parser
class Comment extends PartParser
{
/**
* @var int
+1 -1
View File
@@ -20,7 +20,7 @@ use Egulias\EmailValidator\Result\Reason\ExpectingDTEXT;
use Egulias\EmailValidator\Result\Reason\UnusualElements;
use Egulias\EmailValidator\Warning\DomainLiteral as WarningDomainLiteral;
class DomainLiteral extends Parser
class DomainLiteral extends PartParser
{
public function parse() : Result
{
+33 -31
View File
@@ -22,7 +22,7 @@ use Egulias\EmailValidator\Parser\CommentStrategy\DomainComment;
use Egulias\EmailValidator\Result\Reason\ExpectingDomainLiteralClose;
use Egulias\EmailValidator\Parser\DomainLiteral as DomainLiteralParser;
class DomainPart extends Parser
class DomainPart extends PartParser
{
const DOMAIN_MAX_LENGTH = 253;
const LABEL_MAX_LENGTH = 63;
@@ -39,6 +39,9 @@ class DomainPart extends Parser
public function parse() : Result
{
$this->lexer->clearRecorded();
$this->lexer->startRecording();
$this->lexer->moveNext();
$domainChecks = $this->performDomainStartChecks();
@@ -49,20 +52,22 @@ class DomainPart extends Parser
if ($this->lexer->token['type'] === EmailLexer::S_AT) {
return new InvalidEmail(new ConsecutiveAt(), $this->lexer->token['value']);
}
$domain = $this->doParseDomainPart();
if ($domain->isInvalid()) {
return $domain;
}
$length = strlen($this->domainPart);
$result = $this->doParseDomainPart();
if ($result->isInvalid()) {
return $result;
}
$end = $this->checkEndOfDomain();
if ($end->isInvalid()) {
return $end;
}
$this->lexer->stopRecording();
$this->domainPart = $this->lexer->getAccumulatedValues();
$length = strlen($this->domainPart);
if ($length > self::DOMAIN_MAX_LENGTH) {
//$this->warnings[DomainTooLong::CODE] = new DomainTooLong();
return new InvalidEmail(new DomainTooLong(), $this->lexer->token['value']);
}
@@ -128,14 +133,6 @@ class DomainPart extends Parser
return new ValidEmail();
}
/**
* @return string
*/
public function getDomainPart()
{
return $this->domainPart;
}
protected function parseComments(): Result
{
$commentParser = new Comment($this->lexer, new DomainComment());
@@ -229,7 +226,6 @@ class DomainPart extends Parser
*/
protected function parseDomainLiteral() : Result
{
try {
$this->lexer->find(EmailLexer::S_CLOSEBRACKET);
} catch (\RuntimeException $e) {
@@ -242,22 +238,8 @@ class DomainPart extends Parser
return $result;
}
/**
* @return InvalidEmail|ValidEmail
*/
protected function checkDomainPartExceptions(array $prev, bool $hasComments) : Result
{
$validDomainTokens = array(
EmailLexer::GENERIC => true,
EmailLexer::S_HYPHEN => true,
EmailLexer::S_DOT => true,
);
if ($hasComments) {
$validDomainTokens[EmailLexer::S_OPENPARENTHESIS] = true;
$validDomainTokens[EmailLexer::S_CLOSEPARENTHESIS] = true;
}
if ($this->lexer->token['type'] === EmailLexer::S_OPENBRACKET && $prev['type'] !== EmailLexer::S_AT) {
return new InvalidEmail(new ExpectingATEXT('OPENBRACKET not after AT'), $this->lexer->token['value']);
}
@@ -271,6 +253,22 @@ class DomainPart extends Parser
return new InvalidEmail(new ExpectingATEXT('Escaping following "ATOM"'), $this->lexer->token['value']);
}
return $this->validateTokens($hasComments);
}
protected function validateTokens(bool $hasComments) : Result
{
$validDomainTokens = array(
EmailLexer::GENERIC => true,
EmailLexer::S_HYPHEN => true,
EmailLexer::S_DOT => true,
);
if ($hasComments) {
$validDomainTokens[EmailLexer::S_OPENPARENTHESIS] = true;
$validDomainTokens[EmailLexer::S_CLOSEPARENTHESIS] = true;
}
if (!isset($validDomainTokens[$this->lexer->token['type']])) {
return new InvalidEmail(new ExpectingATEXT('Invalid token in domain: ' . $this->lexer->token['value']), $this->lexer->token['value']);
}
@@ -278,7 +276,6 @@ class DomainPart extends Parser
return new ValidEmail();
}
private function checkLabelLength(bool $isEndOfDomain = false) : Result
{
if ($this->lexer->token['type'] === EmailLexer::S_DOT || $isEndOfDomain) {
@@ -307,4 +304,9 @@ class DomainPart extends Parser
$this->warnings[TLD::CODE] = new TLD();
}
}
public function domainPart() : string
{
return $this->domainPart;
}
}
+1 -1
View File
@@ -11,7 +11,7 @@ use Egulias\EmailValidator\Result\Reason\ExpectingATEXT;
use Egulias\EmailValidator\Result\Reason\UnclosedQuotedString;
use Egulias\EmailValidator\Result\Result;
class DoubleQuote extends Parser
class DoubleQuote extends PartParser
{
public function parse() : Result
{
+9 -10
View File
@@ -13,7 +13,7 @@ use Egulias\EmailValidator\Result\Reason\ExpectingCTEXT;
use Egulias\EmailValidator\Result\Result;
use Egulias\EmailValidator\Result\ValidEmail;
class FoldingWhiteSpace extends Parser
class FoldingWhiteSpace extends PartParser
{
public function parse() : Result
{
@@ -23,7 +23,10 @@ class FoldingWhiteSpace extends Parser
$previous = $this->lexer->getPrevious();
$this->checkCRLFInFWS();
$resultCRLF = $this->checkCRLFInFWS();
if ($resultCRLF->isInvalid()) {
return $resultCRLF;
}
if ($this->lexer->token['type'] === EmailLexer::S_CR) {
return new InvalidEmail(new CRNoLF(), $this->lexer->token['value']);
@@ -46,10 +49,7 @@ class FoldingWhiteSpace extends Parser
return new ValidEmail();
}
/**
* @return InvalidEmail|ValidEmail|null
*/
protected function checkCRLFInFWS()
protected function checkCRLFInFWS() : Result
{
if ($this->lexer->token['type'] !== EmailLexer::CRLF) {
return new ValidEmail();
@@ -63,12 +63,11 @@ class FoldingWhiteSpace extends Parser
if (!$this->lexer->isNextTokenAny(array(EmailLexer::S_SP, EmailLexer::S_HTAB))) {
return new InvalidEmail(new CRLFAtTheEnd(), $this->lexer->token['value']);
}
return new ValidEmail();
}
/**
* @return bool
*/
protected function isFWS()
protected function isFWS() : bool
{
if ($this->escaped()) {
return false;
+16
View File
@@ -0,0 +1,16 @@
<?php
namespace Egulias\EmailValidator\Parser;
use Egulias\EmailValidator\Result\Result;
use Egulias\EmailValidator\Parser\LocalPart;
use Egulias\EmailValidator\Result\InvalidEmail;
use Egulias\EmailValidator\Result\Reason\CommentsInIDRight;
class IDLeftPart extends LocalPart
{
protected function parseComments(): Result
{
return new InvalidEmail(new CommentsInIDRight(), $this->lexer->token['value']);
}
}
+29
View File
@@ -0,0 +1,29 @@
<?php
namespace Egulias\EmailValidator\Parser;
use Egulias\EmailValidator\EmailLexer;
use Egulias\EmailValidator\Result\Result;
use Egulias\EmailValidator\Result\ValidEmail;
use Egulias\EmailValidator\Result\InvalidEmail;
use Egulias\EmailValidator\Result\Reason\ExpectingATEXT;
class IDRightPart extends DomainPart
{
protected function validateTokens(bool $hasComments) : Result
{
$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']])) {
return new InvalidEmail(new ExpectingATEXT('Invalid token in domain: ' . $this->lexer->token['value']), $this->lexer->token['value']);
}
return new ValidEmail();
}
}
+35 -20
View File
@@ -13,27 +13,17 @@ use Egulias\EmailValidator\Result\Reason\ConsecutiveDot;
use Egulias\EmailValidator\Result\Reason\ExpectingATEXT;
use Egulias\EmailValidator\Parser\CommentStrategy\LocalComment;
class LocalPart extends Parser
class LocalPart extends PartParser
{
/**
* Invalid lexer tokens for local part
* @var array
* @var string
*/
private $invalidTokens = array(
EmailLexer::S_COMMA => EmailLexer::S_COMMA,
EmailLexer::S_CLOSEBRACKET => EmailLexer::S_CLOSEBRACKET,
EmailLexer::S_OPENBRACKET => EmailLexer::S_OPENBRACKET,
EmailLexer::S_GREATERTHAN => EmailLexer::S_GREATERTHAN,
EmailLexer::S_LOWERTHAN => EmailLexer::S_LOWERTHAN,
EmailLexer::S_COLON => EmailLexer::S_COLON,
EmailLexer::S_SEMICOLON => EmailLexer::S_SEMICOLON,
EmailLexer::INVALID => EmailLexer::INVALID
);
private $localPart = '';
public function parse() : Result
{
$totalLength = 0;
$this->lexer->startRecording();
while ($this->lexer->token['type'] !== EmailLexer::S_AT && null !== $this->lexer->token['type']) {
if ($this->hasDotAtStart()) {
@@ -74,8 +64,9 @@ class LocalPart extends Parser
return $resultEscaping;
}
if (isset($this->invalidTokens[$this->lexer->token['type']])) {
return new InvalidEmail(new ExpectingATEXT('Invalid token found'), $this->lexer->token['value']);
$resultToken = $this->validateTokens(false);
if ($resultToken->isInvalid()) {
return $resultToken;
}
$resultFWS = $this->parseLocalFWS();
@@ -83,17 +74,41 @@ class LocalPart extends Parser
return $resultFWS;
}
$totalLength += strlen($this->lexer->token['value']);
$this->lexer->moveNext();
}
if ($totalLength > LocalTooLong::LOCAL_PART_LENGTH) {
$this->lexer->stopRecording();
$this->localPart = rtrim($this->lexer->getAccumulatedValues(), '@');
if (strlen($this->localPart) > LocalTooLong::LOCAL_PART_LENGTH) {
$this->warnings[LocalTooLong::CODE] = new LocalTooLong();
}
return new ValidEmail();
}
protected function validateTokens(bool $hasComments) : Result
{
$invalidTokens = array(
EmailLexer::S_COMMA => EmailLexer::S_COMMA,
EmailLexer::S_CLOSEBRACKET => EmailLexer::S_CLOSEBRACKET,
EmailLexer::S_OPENBRACKET => EmailLexer::S_OPENBRACKET,
EmailLexer::S_GREATERTHAN => EmailLexer::S_GREATERTHAN,
EmailLexer::S_LOWERTHAN => EmailLexer::S_LOWERTHAN,
EmailLexer::S_COLON => EmailLexer::S_COLON,
EmailLexer::S_SEMICOLON => EmailLexer::S_SEMICOLON,
EmailLexer::INVALID => EmailLexer::INVALID
);
if (isset($invalidTokens[$this->lexer->token['type']])) {
return new InvalidEmail(new ExpectingATEXT('Invalid token found'), $this->lexer->token['value']);
}
return new ValidEmail();
}
public function localPart() : string
{
return $this->localPart;
}
private function parseLocalFWS() : Result
{
$foldingWS = new FoldingWhiteSpace($this->lexer);
@@ -118,7 +133,7 @@ class LocalPart extends Parser
return $parseAgain;
}
private function parseComments(): Result
protected function parseComments(): Result
{
$commentParser = new Comment($this->lexer, new LocalComment());
$result = $commentParser->parse();
@@ -8,7 +8,7 @@ use Egulias\EmailValidator\Result\Reason\ConsecutiveDot;
use Egulias\EmailValidator\Result\Result;
use Egulias\EmailValidator\Result\ValidEmail;
abstract class Parser
abstract class PartParser
{
/**
* @var array
@@ -25,6 +25,8 @@ abstract class Parser
$this->lexer = $lexer;
}
abstract public function parse() : Result;
/**
* @return \Egulias\EmailValidator\Warning\Warning[]
*/
@@ -33,8 +35,6 @@ abstract class Parser
return $this->warnings;
}
abstract public function parse() : Result;
protected function parseFWS() : Result
{
$foldingWS = new FoldingWhiteSpace($this->lexer);
@@ -52,9 +52,6 @@ abstract class Parser
return new ValidEmail();
}
/**
* @return bool
*/
protected function escaped() : bool
{
$previous = $this->lexer->getPrevious();