1<?php
2
3namespace Egulias\EmailValidator\Parser;
4
5use Egulias\EmailValidator\EmailLexer;
6use Egulias\EmailValidator\Result\Result;
7use Egulias\EmailValidator\Result\ValidEmail;
8use Egulias\EmailValidator\Result\InvalidEmail;
9use Egulias\EmailValidator\Warning\LocalTooLong;
10use Egulias\EmailValidator\Result\Reason\DotAtEnd;
11use Egulias\EmailValidator\Result\Reason\DotAtStart;
12use Egulias\EmailValidator\Result\Reason\ConsecutiveDot;
13use Egulias\EmailValidator\Result\Reason\ExpectingATEXT;
14use Egulias\EmailValidator\Parser\CommentStrategy\LocalComment;
15
16class LocalPart extends PartParser
17{
18    /**
19     * @var string
20     */
21    private $localPart = '';
22
23
24    public function parse() : Result
25    {
26        $this->lexer->startRecording();
27
28        while ($this->lexer->token['type'] !== EmailLexer::S_AT && null !== $this->lexer->token['type']) {
29            if ($this->hasDotAtStart()) {
30                return new InvalidEmail(new DotAtStart(), $this->lexer->token['value']);
31            }
32
33            if ($this->lexer->token['type'] === EmailLexer::S_DQUOTE) {
34                $dquoteParsingResult = $this->parseDoubleQuote();
35
36                //Invalid double quote parsing
37                if($dquoteParsingResult->isInvalid()) {
38                    return $dquoteParsingResult;
39                }
40            }
41
42            if ($this->lexer->token['type'] === EmailLexer::S_OPENPARENTHESIS ||
43                $this->lexer->token['type'] === EmailLexer::S_CLOSEPARENTHESIS ) {
44                $commentsResult = $this->parseComments();
45
46                //Invalid comment parsing
47                if($commentsResult->isInvalid()) {
48                    return $commentsResult;
49                }
50            }
51
52            if ($this->lexer->token['type'] === EmailLexer::S_DOT && $this->lexer->isNextToken(EmailLexer::S_DOT)) {
53                return new InvalidEmail(new ConsecutiveDot(), $this->lexer->token['value']);
54            }
55
56            if ($this->lexer->token['type'] === EmailLexer::S_DOT &&
57                $this->lexer->isNextToken(EmailLexer::S_AT)
58            ) {
59                return new InvalidEmail(new DotAtEnd(), $this->lexer->token['value']);
60            }
61
62            $resultEscaping = $this->validateEscaping();
63            if ($resultEscaping->isInvalid()) {
64                return $resultEscaping;
65            }
66
67            $resultToken = $this->validateTokens(false);
68            if ($resultToken->isInvalid()) {
69                return $resultToken;
70            }
71
72            $resultFWS = $this->parseLocalFWS();
73            if($resultFWS->isInvalid()) {
74                return $resultFWS;
75            }
76
77            $this->lexer->moveNext();
78        }
79
80        $this->lexer->stopRecording();
81        $this->localPart = rtrim($this->lexer->getAccumulatedValues(), '@');
82        if (strlen($this->localPart) > LocalTooLong::LOCAL_PART_LENGTH) {
83            $this->warnings[LocalTooLong::CODE] = new LocalTooLong();
84        }
85
86        return new ValidEmail();
87    }
88
89    protected function validateTokens(bool $hasComments) : Result
90    {
91        $invalidTokens = array(
92            EmailLexer::S_COMMA => EmailLexer::S_COMMA,
93            EmailLexer::S_CLOSEBRACKET => EmailLexer::S_CLOSEBRACKET,
94            EmailLexer::S_OPENBRACKET => EmailLexer::S_OPENBRACKET,
95            EmailLexer::S_GREATERTHAN => EmailLexer::S_GREATERTHAN,
96            EmailLexer::S_LOWERTHAN => EmailLexer::S_LOWERTHAN,
97            EmailLexer::S_COLON => EmailLexer::S_COLON,
98            EmailLexer::S_SEMICOLON => EmailLexer::S_SEMICOLON,
99            EmailLexer::INVALID => EmailLexer::INVALID
100        );
101        if (isset($invalidTokens[$this->lexer->token['type']])) {
102            return new InvalidEmail(new ExpectingATEXT('Invalid token found'), $this->lexer->token['value']);
103        }
104        return new ValidEmail();
105    }
106
107    public function localPart() : string
108    {
109        return $this->localPart;
110    }
111
112    private function parseLocalFWS() : Result
113    {
114        $foldingWS = new FoldingWhiteSpace($this->lexer);
115        $resultFWS = $foldingWS->parse();
116        if ($resultFWS->isValid()) {
117            $this->warnings = array_merge($this->warnings, $foldingWS->getWarnings());
118        }
119        return $resultFWS;
120    }
121
122    private function hasDotAtStart() : bool
123    {
124            return $this->lexer->token['type'] === EmailLexer::S_DOT && null === $this->lexer->getPrevious()['type'];
125    }
126
127    private function parseDoubleQuote() : Result
128    {
129        $dquoteParser = new DoubleQuote($this->lexer);
130        $parseAgain = $dquoteParser->parse();
131        $this->warnings = array_merge($this->warnings, $dquoteParser->getWarnings());
132
133        return $parseAgain;
134    }
135
136    protected function parseComments(): Result
137    {
138        $commentParser = new Comment($this->lexer, new LocalComment());
139        $result = $commentParser->parse();
140        $this->warnings = array_merge($this->warnings, $commentParser->getWarnings());
141        if($result->isInvalid()) {
142            return $result;
143        }
144        return $result;
145    }
146
147    private function validateEscaping() : Result
148    {
149        //Backslash found
150        if ($this->lexer->token['type'] !== EmailLexer::S_BACKSLASH) {
151            return new ValidEmail();
152        }
153
154        if ($this->lexer->isNextToken(EmailLexer::GENERIC)) {
155            return new InvalidEmail(new ExpectingATEXT('Found ATOM after escaping'), $this->lexer->token['value']);
156        }
157
158        if (!$this->lexer->isNextTokenAny(array(EmailLexer::S_SP, EmailLexer::S_HTAB, EmailLexer::C_DEL))) {
159            return new ValidEmail();
160        }
161
162        return new ValidEmail();
163    }
164}