Issues (28)

Security Analysis    no request data  

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

src/Rule/Validate/Email.php (3 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
/**
3
 *
4
 * This file is part of Aura for PHP.
5
 *
6
 * @license http://opensource.org/licenses/bsd-license.php BSD
7
 *
8
 */
9
namespace Aura\Filter\Rule\Validate;
10
11
use Aura\Filter\Exception;
12
13
/**
14
 *
15
 * Check that an email address conforms to RFCs 5321, 5322 and others.
16
 *
17
 * As of Version 3.0, we are now distinguishing clearly between a Mailbox
18
 * as defined by RFC 5321 and an addr-spec as defined by RFC 5322. Depending
19
 * on the context, either can be regarded as a valid email address. The
20
 * RFC 5321 Mailbox specification is more restrictive (comments, white space
21
 * and obsolete forms are not allowed).
22
 *
23
 * Read the following RFCs to understand the constraints:
24
 *
25
 * - <http://tools.ietf.org/html/rfc5321>
26
 * - <http://tools.ietf.org/html/rfc5322>
27
 * - <http://tools.ietf.org/html/rfc4291#section-2.2>
28
 * - <http://tools.ietf.org/html/rfc1123#section-2.1>
29
 * - <http://tools.ietf.org/html/rfc3696> (guidance only)
30
 *
31
 * Copyright © 2008-2011, Dominic Sayers
32
 * Test schema documentation Copyright © 2011, Daniel Marschall
33
 * All rights reserved.
34
 *
35
 * ---
36
 *
37
 * N.B.: Refactored by Paul M. Jones, from Dominic Sayers' is_email() function
38
 * to methods and properties. Errors and omissions should be presumed to be a
39
 * result of the refactoring, not of the original function.
40
 *
41
 * Further, this validation rule converts IDNs to ASCII, which is not required
42
 * per se by any of the email RFCs.
43
 *
44
 * ---
45
 *
46
 * @package Aura.Filter
47
 *
48
 * @author Dominic Sayers <[email protected]>
49
 *
50
 * @author Paul M. Jones <[email protected]>
51
 *
52
 * @copyright 2008-2011 Dominic Sayers
53
 *
54
 * @copyright 2015 Paul M. Jones
55
 *
56
 * @license http://www.opensource.org/licenses/bsd-license.php BSD License
57
 *
58
 * @link http://www.dominicsayers.com/isemail
59
 *
60
 * @version 3.04.1 - Changed my link to http://isemail.info throughout
61
 *
62
 */
63
class Email
64
{
65
    /*:diagnostic constants start:*/
66
67
    // Categories
68
    const VALID_CATEGORY = 1;
69
    const DNSWARN = 7;
70
    const RFC5321 = 15;
71
    const CFWS = 31;
72
    const DEPREC = 63;
73
    const RFC5322 = 127;
74
    const ERR = 255;
75
76
    // Diagnoses
77
    // Address is valid
78
    const VALID = 0;
79
    // Address is valid but a DNS check was not successful
80
    const DNSWARN_NO_MX_RECORD = 5;
81
    const DNSWARN_NO_RECORD = 6;
82
    // Address is valid for SMTP but has unusual elements
83
    const RFC5321_TLD = 9;
84
    const RFC5321_TLDNUMERIC = 10;
85
    const RFC5321_QUOTEDSTRING = 11;
86
    const RFC5321_ADDRESSLITERAL = 12;
87
    const RFC5321_IPV6DEPRECATED = 13;
88
    // Address is valid within the message but cannot be used unmodified for the envelope
89
    const CFWS_COMMENT = 17;
90
    const CFWS_FWS = 18;
91
    // Address contains deprecated elements but may still be valid in restricted contexts
92
    const DEPREC_LOCALPART = 33;
93
    const DEPREC_FWS = 34;
94
    const DEPREC_QTEXT = 35;
95
    const DEPREC_QP = 36;
96
    const DEPREC_COMMENT = 37;
97
    const DEPREC_CTEXT = 38;
98
    const DEPREC_CFWS_NEAR_AT = 49;
99
    // The address is only valid according to the broad definition of RFC 5322.
100
    // It is otherwise invalid.
101
    const RFC5322_DOMAIN = 65;
102
    const RFC5322_TOOLONG = 66;
103
    const RFC5322_LOCAL_TOOLONG = 67;
104
    const RFC5322_DOMAIN_TOOLONG = 68;
105
    const RFC5322_LABEL_TOOLONG = 69;
106
    const RFC5322_DOMAINLITERAL = 70;
107
    const RFC5322_DOMLIT_OBSDTEXT = 71;
108
    const RFC5322_IPV6_GRPCOUNT = 72;
109
    const RFC5322_IPV6_2X2XCOLON = 73;
110
    const RFC5322_IPV6_BADCHAR = 74;
111
    const RFC5322_IPV6_MAXGRPS = 75;
112
    const RFC5322_IPV6_COLONSTRT = 76;
113
    const RFC5322_IPV6_COLONEND = 77;
114
    // Address is invalid for any purpose
115
    const ERR_EXPECTING_DTEXT = 129;
116
    const ERR_NOLOCALPART = 130;
117
    const ERR_NODOMAIN = 131;
118
    const ERR_CONSECUTIVEDOTS = 132;
119
    const ERR_ATEXT_AFTER_CFWS = 133;
120
    const ERR_ATEXT_AFTER_QS = 134;
121
    const ERR_ATEXT_AFTER_DOMLIT = 135;
122
    const ERR_EXPECTING_QPAIR = 136;
123
    const ERR_EXPECTING_ATEXT = 137;
124
    const ERR_EXPECTING_QTEXT = 138;
125
    const ERR_EXPECTING_CTEXT = 139;
126
    const ERR_BACKSLASHEND = 140;
127
    const ERR_DOT_START = 141;
128
    const ERR_DOT_END = 142;
129
    const ERR_DOMAINHYPHENSTART = 143;
130
    const ERR_DOMAINHYPHENEND = 144;
131
    const ERR_UNCLOSEDQUOTEDSTR = 145;
132
    const ERR_UNCLOSEDCOMMENT = 146;
133
    const ERR_UNCLOSEDDOMLIT = 147;
134
    const ERR_FWS_CRLF_X2 = 148;
135
    const ERR_FWS_CRLF_END = 149;
136
    const ERR_CR_NO_LF = 150;
137
    /*:diagnostic constants end:*/
138
139
    // function control
140
    const THRESHOLD = 16;
141
142
    // Email parts
143
    const COMPONENT_LOCALPART = 0;
144
    const COMPONENT_DOMAIN = 1;
145
    const COMPONENT_LITERAL = 2;
146
    const CONTEXT_COMMENT = 3;
147
    const CONTEXT_FWS = 4;
148
    const CONTEXT_QUOTEDSTRING = 5;
149
    const CONTEXT_QUOTEDPAIR = 6;
150
151
    // Miscellaneous string constants
152
    const STRING_AT = '@';
153
    const STRING_BACKSLASH = '\\';
154
    const STRING_DOT = '.';
155
    const STRING_DQUOTE = '"';
156
    const STRING_OPENPARENTHESIS = '(';
157
    const STRING_CLOSEPARENTHESIS = ')';
158
    const STRING_OPENSQBRACKET = '[';
159
    const STRING_CLOSESQBRACKET = ']';
160
    const STRING_HYPHEN = '-';
161
    const STRING_COLON = ':';
162
    const STRING_DOUBLECOLON = '::';
163
    const STRING_SP = ' ';
164
    const STRING_HTAB = "\t";
165
    const STRING_CR = "\r";
166
    const STRING_LF = "\n";
167
    const STRING_IPV6TAG = 'IPv6:';
168
169
    // US-ASCII visible characters not valid for atext
170
    // <http://tools.ietf.org/html/rfc5322#section-3.2.3>
171
    const STRING_SPECIALS = '()<>[]:;@\\,."';
172
173
    /**
174
     *
175
     * The email address being checked.
176
     *
177
     * @var string
178
     *
179
     */
180
    protected $email;
181
182
    /**
183
     *
184
     * Check DNS as part of validation?
185
     *
186
     * @var bool
187
     *
188
     */
189
    protected $checkDns;
190
191
    /**
192
     *
193
     * The validation threshold level.
194
     *
195
     * @var int
196
     *
197
     */
198
    protected $threshold;
199
200
    /**
201
     *
202
     * Diagnose errors?
203
     *
204
     * @var bool
205
     *
206
     */
207
    protected $diagnose;
208
209
    /**
210
     *
211
     * Has DNS been checked?
212
     *
213
     * @var bool
214
     *
215
     */
216
    protected $dnsChecked;
217
218
    /**
219
     *
220
     * The return status.
221
     *
222
     * @var int
223
     *
224
     */
225
    protected $returnStatus;
226
227
    /**
228
     *
229
     * The length of the email address being checked.
230
     *
231
     * @var int
232
     *
233
     */
234
    protected $rawLength;
235
236
    /**
237
     *
238
     * The current parser context.
239
     *
240
     * @var int
241
     *
242
     */
243
    protected $context;
244
245
    /**
246
     *
247
     * Parser context stack.
248
     *
249
     * @var array
250
     *
251
     */
252
    protected $contextStack;
253
254
    /**
255
     *
256
     * The prior parser context.
257
     *
258
     * @var int
259
     *
260
     */
261
    protected $contextPrior;
262
263
    /**
264
     *
265
     * The current token being parsed.
266
     *
267
     * @var string
268
     *
269
     */
270
    protected $token;
271
272
    /**
273
     *
274
     * The previous token being parsed.
275
     *
276
     * @var string
277
     *
278
     */
279
    protected $tokenPrior;
280
281
    /**
282
     *
283
     * The components of the address.
284
     *
285
     * @var array
286
     *
287
     */
288
    protected $parseData;
289
290
    /**
291
     *
292
     * The dot-atom elements of the address.
293
     *
294
     * @var array
295
     *
296
     */
297
    protected $atomList;
298
299
    /**
300
     *
301
     * Element count.
302
     *
303
     * @var int
304
     *
305
     */
306
    protected $elementCount;
307
308
    /**
309
     *
310
     * Element length.
311
     *
312
     * @var int
313
     *
314
     */
315
    protected $elementLen;
316
317
    /**
318
     *
319
     * Is a hyphen allowed?
320
     *
321
     * @var bool
322
     *
323
     */
324
    protected $hyphenFlag;
325
326
    /**
327
     *
328
     * CFWS can only appear at the end of the element
329
     *
330
     * @var bool
331
     *
332
     */
333
    protected $endOrDie;
334
335
    /**
336
     *
337
     * Current position in the email string.
338
     *
339
     * @var int
340
     *
341
     */
342
    protected $pos;
343
344
    /**
345
     *
346
     * Count of CRLF occurrences.
347
     *
348
     * @var null|int
349
     *
350
     */
351
    protected $crlfCount;
352
353
    /**
354
     *
355
     * The final status of email validation.
356
     *
357
     * @var int
358
     *
359
     */
360
    protected $finalStatus;
361
362
    /**
363
     *
364
     * Validates that the value is an email address.
365
     *
366
     * @param object $subject The subject to be filtered.
367
     *
368
     * @param string $field The subject field name.
369
     *
370
     * @return bool True if valid, false if not.
371
     *
372
     */
373 165
    public function __invoke($subject, $field)
374
    {
375 165
        $email = $subject->$field;
376 165
        if ($this->intl()) {
377 165
            $email = $this->idnToAscii($email);
378
        }
379 165
        return $this->isEmail($email);
380
    }
381
382
    /**
383
     *
384
     * Is the intl extension loaded?
385
     *
386
     * @return bool
387
     *
388
     */
389 165
    protected function intl()
390
    {
391 165
        return extension_loaded('intl');
392
    }
393
394
    /**
395
     *
396
     * Converts an international domain in the email address to ASCII.
397
     *
398
     * @param string $email The email address to check.
399
     *
400
     * @return string The email with the IDN converted to ASCII (if possible).
401
     *
402
     */
403 165
    protected function idnToAscii($email)
404
    {
405 165
        $parts = explode('@', $email);
406 165
        $domain = array_pop($parts);
407 165
        if (! $parts) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $parts of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
408
            // no parts remaining, so no @ symbol, so not valid to begin with
409 4
            return $email;
410
        }
411
412
        // put the parts back together, with the domain part converted to ascii
413 161
        return implode('@', $parts) . '@' . idn_to_ascii($domain);
414
    }
415
416
    /**
417
     *
418
     * Checks that an email address conforms to RFCs 5321, 5322 and others,
419
     * allowing for international domain names when the intl extension is
420
     * loaded.
421
     *
422
     * @param string $email The email address to check.
423
     *
424
     * @param bool $checkDns Make a DNS check for MX records?
425
     *
426
     * @param mixed $errorlevel Determines the boundary between valid and
427
     * invalid addresses. Status codes above this number will be returned as-
428
     * is, status codes below will be returned as Email::VALID. Thus the
429
     * calling program can simply look for Email::VALID if it is only
430
     * interested in whether an address is valid or not. The errorlevel will
431
     * determine how "picky" is_email() is about the address. If omitted or
432
     * passed as false then isEmail() will return true or false rather than
433
     * an integer error or warning. N.B.: Note the difference between
434
     * $errorlevel = false and $errorlevel = 0.
435
     *
436
     */
437 165
    protected function isEmail($email, $checkDns = false, $errorlevel = false)
438
    {
439 165
        $this->reset($email, $checkDns, $errorlevel);
440 165
        $this->parse();
441 165
        $this->checkDns();
442 165
        $this->checkTld();
443 165
        $this->finalStatus();
444 165
        return ($this->diagnose)
445
            ? $this->finalStatus
446 165
            : ($this->finalStatus < Email::THRESHOLD);
447
    }
448
449
    /**
450
     *
451
     * Resets the validation rule for a new email address.
452
     *
453
     * @param string $email The email address to check.
454
     *
455
     * @param bool $checkDns Make a DNS check for MX records?
456
     *
457
     * @param mixed $errorlevel Determines the boundary between valid and
458
     * invalid addresses.
459
     *
460
     * @return null
461
     *
462
     */
463 165
    protected function reset($email, $checkDns, $errorlevel)
464
    {
465 165
        $this->email = $email;
466
467 165
        $this->checkDns = $checkDns;
468 165
        $this->dnsChecked = false;
469
470 165
        $this->setThresholdDiagnose($errorlevel);
471
472 165
        $this->returnStatus = array(Email::VALID);
0 ignored issues
show
Documentation Bug introduced by
It seems like array(\Aura\Filter\Rule\Validate\Email::VALID) of type array<integer,?> is incompatible with the declared type integer of property $returnStatus.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
473 165
        $this->rawLength = strlen($this->email);
474
475
        // Where we are
476 165
        $this->context = Email::COMPONENT_LOCALPART;
477
478
        // Where we have been
479 165
        $this->contextStack = array($this->context);
480
481
        // Where we just came from
482 165
        $this->contextPrior = Email::COMPONENT_LOCALPART;
483
484
        // The current character
485 165
        $this->token = '';
486
487
        // The previous character
488 165
        $this->tokenPrior = '';
489
490
        // For the components of the address
491 165
        $this->parseData = array(
492
            Email::COMPONENT_LOCALPART => '',
493
            Email::COMPONENT_DOMAIN => ''
494
        );
495
496
        // For the dot-atom elements of the address
497 165
        $this->atomList = array(
498
            Email::COMPONENT_LOCALPART => array(''),
499
            Email::COMPONENT_DOMAIN => array('')
500
        );
501
502 165
        $this->elementCount = 0;
503 165
        $this->elementLen = 0;
504
505
        // Hyphen cannot occur at the end of a subdomain
506 165
        $this->hyphenFlag = false;
507
508
        // CFWS can only appear at the end of the element
509 165
        $this->endOrDie = false;
510
511 165
        $this->finalStatus = null;
512
513 165
        $this->crlfCount = null;
514 165
    }
515
516
    /**
517
     *
518
     * Sets the $threshold and $diagnose properties.
519
     *
520
     * @param mixed $errorlevel Determines the boundary between valid and
521
     * invalid addresses.
522
     *
523
     * @return null
524
     *
525
     */
526 165
    protected function setThresholdDiagnose($errorlevel)
527
    {
528 165
        if (is_bool($errorlevel)) {
529 165
            $this->threshold = Email::VALID;
530 165
            $this->diagnose = (bool) $errorlevel;
531 165
            return;
532
        }
533
534
        $this->diagnose = true;
535
536
        switch ((int) $errorlevel) {
537
            case E_WARNING:
538
                // For backward compatibility
539
                $this->threshold = Email::THRESHOLD;
540
                break;
541
            case E_ERROR:
542
                // For backward compatibility
543
                $this->threshold = Email::VALID;
544
                break;
545
            default:
546
                $this->threshold = (int) $errorlevel;
547
        }
548
    }
549
550
    /**
551
     *
552
     * Parse the address into components, character by character.
553
     *
554
     * @return null
555
     *
556
     */
557 165
    protected function parse()
558
    {
559 165
        for ($this->pos = 0; $this->pos < $this->rawLength; $this->pos++) {
560 164
            $this->token = $this->email[$this->pos];
561 164
            $this->parseContext();
562 164
            if ((int) max($this->returnStatus) > Email::RFC5322) {
563
                // No point going on if we've got a fatal error
564 42
                break;
565
            }
566
        }
567 165
        $this->parseFinal();
568 165
    }
569
570
    /**
571
     *
572
     * Parse for the current context.
573
     *
574
     * @return null
575
     *
576
     */
577 164
    protected function parseContext()
578
    {
579 164
        switch ($this->context) {
580 164
            case Email::COMPONENT_LOCALPART:
581 164
                $this->parseComponentLocalPart();
582 164
                break;
583 143
            case Email::COMPONENT_DOMAIN:
584 119
                $this->parseComponentDomain();
585 119
                break;
586 102
            case Email::COMPONENT_LITERAL:
587 36
                $this->parseComponentLiteral();
588 36
                break;
589 73
            case Email::CONTEXT_QUOTEDSTRING:
590 27
                $this->parseContextQuotedString();
591 27
                break;
592 58
            case Email::CONTEXT_QUOTEDPAIR:
593 18
                $this->parseContextQuotedPair();
594 18
                break;
595 42
            case Email::CONTEXT_COMMENT:
596 20
                $this->parseContextComment();
597 20
                break;
598 23
            case Email::CONTEXT_FWS:
599 23
                $this->parseContextFws();
600 23
                break;
601
            default:
602
                throw new Exception("Unknown context: {$this->context}");
603
        }
604 164
    }
605
606
    /**
607
     *
608
     * Parse for the local part component.
609
     *
610
     * @return null
611
     *
612
     */
613 164
    protected function parseComponentLocalPart()
614
    {
615
        // http://tools.ietf.org/html/rfc5322#section-3.4.1
616
        //   local-part = dot-atom / quoted-string / obs-local-part
617
        //
618
        //   dot-atom = [CFWS] dot-atom-text [CFWS]
619
        //
620
        //   dot-atom-text = 1*atext *("." 1*atext)
621
        //
622
        //   quoted-string = [CFWS]
623
        //                       DQUOTE *([FWS] qcontent) [FWS] DQUOTE
624
        //                       [CFWS]
625
        //
626
        //   obs-local-part = word *("." word)
627
        //
628
        //   word = atom / quoted-string
629
        //
630
        //   atom = [CFWS] 1*atext [CFWS]
631 164
        switch ($this->token) {
632
633
            // Comment
634 164
            case Email::STRING_OPENPARENTHESIS:
635 13
                if ($this->elementLen === 0) {
636
                    // Comments are OK at the beginning of an element
637 12
                    $this->returnStatus[] = ($this->elementCount === 0)
638 11
                        ? Email::CFWS_COMMENT
639 12
                        : Email::DEPREC_COMMENT;
640
                } else {
641
                    // We can't start a comment in the middle of an element, so this better be the end
642 1
                    $this->returnStatus[] = Email::CFWS_COMMENT;
643 1
                    $this->endOrDie = true;
644
                }
645
646 13
                $this->contextStack[] = $this->context;
647 13
                $this->context = Email::CONTEXT_COMMENT;
648 13
                break;
649
650
            // Next dot-atom element
651 159
            case Email::STRING_DOT:
652 9
                if ($this->elementLen === 0) {
653
                    // Another dot, already?
654
                    // Fatal error
655 2
                    $this->returnStatus[] = ($this->elementCount === 0)
656 1
                        ? Email::ERR_DOT_START
657 2
                        : Email::ERR_CONSECUTIVEDOTS;
658
                } else {
659
                    // The entire local-part can be a quoted string for RFC 5321
660
                    // If it's just one atom that is quoted then it's an RFC 5322 obsolete form
661 8
                    if ($this->endOrDie) {
662 3
                        $this->returnStatus[] = Email::DEPREC_LOCALPART;
663
                    }
664
                }
665
666
                // CFWS & quoted strings are OK again now we're at the beginning of an element (although they are obsolete forms)
667 9
                $this->endOrDie = false;
668 9
                $this->elementLen = 0;
669 9
                $this->elementCount++;
670 9
                $this->parseData[Email::COMPONENT_LOCALPART] .= $this->token;
671 9
                $this->atomList[Email::COMPONENT_LOCALPART][$this->elementCount] = '';
672
673 9
                break;
674
675
            // Quoted string
676 158
            case Email::STRING_DQUOTE:
677 29
                if ($this->elementLen === 0) {
678
                    // The entire local-part can be a quoted string for RFC 5321
679
                    // If it's just one atom that is quoted then it's an RFC 5322 obsolete form
680 27
                    $this->returnStatus[] = ($this->elementCount === 0)
681 27
                        ? Email::RFC5321_QUOTEDSTRING
682 1
                        : Email::DEPREC_LOCALPART;
683
684 27
                    $this->parseData[Email::COMPONENT_LOCALPART] .= $this->token;
685 27
                    $this->atomList[Email::COMPONENT_LOCALPART][$this->elementCount] .= $this->token;
686 27
                    $this->elementLen++;
687
                    // Quoted string must be the entire element
688 27
                    $this->endOrDie = true;
689 27
                    $this->contextStack[] = $this->context;
690 27
                    $this->context = Email::CONTEXT_QUOTEDSTRING;
691
                } else {
692
                    // Fatal error
693 4
                    $this->returnStatus[] = Email::ERR_EXPECTING_ATEXT;
694
                }
695
696 29
                break;
697
698
            // Folding White Space
699 147
            case Email::STRING_CR:
700 144
            case Email::STRING_SP:
701 140
            case Email::STRING_HTAB:
702 13
                if (($this->token === Email::STRING_CR) && ((++$this->pos === $this->rawLength) || ($this->email[$this->pos] !== Email::STRING_LF))) {
703
                    // Fatal error
704 1
                    $this->returnStatus[] = Email::ERR_CR_NO_LF;
705 1
                    break;
706
                }
707
708 12
                if ($this->elementLen === 0) {
709 12
                    $this->returnStatus[] = ($this->elementCount === 0) ? Email::CFWS_FWS : Email::DEPREC_FWS;
710
                } else {
711
                    // We can't start FWS in the middle of an element, so this better be the end
712 2
                    $this->endOrDie = true;
713
                }
714
715 12
                $this->contextStack[] = $this->context;
716 12
                $this->context = Email::CONTEXT_FWS;
717 12
                $this->tokenPrior = $this->token;
718
719 12
                break;
720
721
            // @
722 140
            case Email::STRING_AT:
723
                // At this point we should have a valid local-part
724 129
                if (count($this->contextStack) !== 1) {
725
                    throw new Exception('Unexpected item on context stack');
726
                }
727
728 129
                if ($this->parseData[Email::COMPONENT_LOCALPART] === '') {
729
                    // Fatal error
730 3
                    $this->returnStatus[] = Email::ERR_NOLOCALPART;
731 126
                } elseif ($this->elementLen === 0) {
732
                    // Fatal error
733 1
                    $this->returnStatus[] = Email::ERR_DOT_END;
734 125
                } elseif (strlen($this->parseData[Email::COMPONENT_LOCALPART]) > 64) {
735
                    // http://tools.ietf.org/html/rfc5321#section-4.5.3.1.1
736
                    //   The maximum total length of a user name or other local-part is 64
737
                    //   octets.
738 3
                    $this->returnStatus[] = Email::RFC5322_LOCAL_TOOLONG;
739 122
                } elseif (($this->contextPrior === Email::CONTEXT_COMMENT) || ($this->contextPrior === Email::CONTEXT_FWS)) {
740
                    // http://tools.ietf.org/html/rfc5322#section-3.4.1
741
                    //   Comments and folding white space
742
                    //   SHOULD NOT be used around the "@" in the addr-spec.
743
                    //
744
                    // http://tools.ietf.org/html/rfc2119
745
                    // 4. SHOULD NOT   This phrase, or the phrase "NOT RECOMMENDED" mean that
746
                    //    there may exist valid reasons in particular circumstances when the
747
                    //    particular behavior is acceptable or even useful, but the full
748
                    //    implications should be understood and the case carefully weighed
749
                    //    before implementing any behavior described with this label.
750 1
                    $this->returnStatus[] = Email::DEPREC_CFWS_NEAR_AT;
751
                }
752
753
                // Clear everything down for the domain parsing
754 129
                $this->context = Email::COMPONENT_DOMAIN; // Where we are
755 129
                $this->contextStack = array($this->context); // Where we have been
756 129
                $this->elementCount = 0;
757 129
                $this->elementLen = 0;
758 129
                $this->endOrDie = false; // CFWS can only appear at the end of the element
759
760 129
                break;
761
762
            // atext
763
            default:
764
                // http://tools.ietf.org/html/rfc5322#section-3.2.3
765
                //    atext = ALPHA / DIGIT /    ; Printable US-ASCII
766
                //                        "!" / "#" /        ;  characters not including
767
                //                        "$" / "%" /        ;  specials.  Used for atoms.
768
                //                        "&" / "'" /
769
                //                        "*" / "+" /
770
                //                        "-" / "/" /
771
                //                        " = " / "?" /
772
                //                        "^" / "_" /
773
                //                        "`" / "{" /
774
                //                        "|" / "}" /
775
                //                        "~"
776 123
                if ($this->endOrDie) {
777
                    // We have encountered atext where it is no longer valid
778 2
                    switch ($this->contextPrior) {
779 2
                        case Email::CONTEXT_COMMENT:
780 1
                        case Email::CONTEXT_FWS:
781 1
                            $this->returnStatus[] = Email::ERR_ATEXT_AFTER_CFWS;
782 1
                            break;
783 1
                        case Email::CONTEXT_QUOTEDSTRING:
784 1
                            $this->returnStatus[] = Email::ERR_ATEXT_AFTER_QS;
785 1
                            break;
786
                        default:
787 2
                            throw new Exception("More atext found where none is allowed, but unrecognised prior context: {$this->contextPrior}");
788
                    }
789
                } else {
790 122
                    $this->contextPrior = $this->context;
791 122
                    $ord = ord($this->token);
792
793 122
                    if (($ord < 33) || ($ord > 126) || ($ord === 10) || (!is_bool(strpos(Email::STRING_SPECIALS, $this->token)))) {
794
                        // Fatal error
795 4
                        $this->returnStatus[] = Email::ERR_EXPECTING_ATEXT;
796
                    }
797
798 122
                    $this->parseData[Email::COMPONENT_LOCALPART] .= $this->token;
799 122
                    $this->atomList[Email::COMPONENT_LOCALPART][$this->elementCount] .= $this->token;
800 122
                    $this->elementLen++;
801
                }
802
        }
803 164
    }
804
805
    /**
806
     *
807
     * Parse for the domain component.
808
     *
809
     * @return null
810
     *
811
     */
812 119
    protected function parseComponentDomain()
813
    {
814
        // http://tools.ietf.org/html/rfc5322#section-3.4.1
815
        //   domain = dot-atom / domain-literal / obs-domain
816
        //
817
        //   dot-atom = [CFWS] dot-atom-text [CFWS]
818
        //
819
        //   dot-atom-text = 1*atext *("." 1*atext)
820
        //
821
        //   domain-literal = [CFWS] "[" *([FWS] dtext) [FWS] "]" [CFWS]
822
        //
823
        //   dtext = %d33-90 /          ; Printable US-ASCII
824
        //                       %d94-126 /         ;  characters not including
825
        //                       obs-dtext          ;  "[", "]", or "\"
826
        //
827
        //   obs-domain = atom *("." atom)
828
        //
829
        //   atom = [CFWS] 1*atext [CFWS]
830
831
        // http://tools.ietf.org/html/rfc5321#section-4.1.2
832
        //   Mailbox = Local-part "@" ( Domain / address-literal )
833
        //
834
        //   Domain = sub-domain *("." sub-domain)
835
        //
836
        //   address-literal = "[" ( IPv4-address-literal /
837
        //                    IPv6-address-literal /
838
        //                    General-address-literal ) "]"
839
        //                    ; See Section 4.1.3
840
841
        // http://tools.ietf.org/html/rfc5322#section-3.4.1
842
        //      Note: A liberal syntax for the domain portion of addr-spec is
843
        //      given here.  However, the domain portion contains addressing
844
        //      information specified by and used in other protocols (e.g.,
845
        //      [RFC1034], [RFC1035], [RFC1123], [RFC5321]).  It is therefore
846
        //      incumbent upon implementations to conform to the syntax of
847
        //      addresses for the context in which they are used.
848
        // is_email() author's note: it's not clear how to interpret this in
849
        // the context of a general email address validator. The conclusion I
850
        // have reached is this: "addressing information" must comply with
851
        // RFC 5321 (and in turn RFC 1035), anything that is "semantically
852
        // invisible" must comply only with RFC 5322.
853 119
        switch ($this->token) {
854
855
            // Comment
856 119
            case Email::STRING_OPENPARENTHESIS:
857 7
                if ($this->elementLen === 0) {
858
                    // Comments at the start of the domain are deprecated in the text
859
                    // Comments at the start of a subdomain are obs-domain
860
                    // (http://tools.ietf.org/html/rfc5322#section-3.4.1)
861 3
                    $this->returnStatus[] = ($this->elementCount === 0) ? Email::DEPREC_CFWS_NEAR_AT : Email::DEPREC_COMMENT;
862
                } else {
863 4
                    $this->returnStatus[] = Email::CFWS_COMMENT;
864
                    // We can't start a comment in the middle of an element, so this better be the end
865 4
                    $this->endOrDie = true;
866
                }
867
868 7
                $this->contextStack[] = $this->context;
869 7
                $this->context = Email::CONTEXT_COMMENT;
870 7
                break;
871
872
            // Next dot-atom element
873 118
            case Email::STRING_DOT:
874 76
                if ($this->elementLen === 0) {
875
                    // Another dot, already?
876
                    // Fatal error
877
                    $this->returnStatus[] = ($this->elementCount === 0) ? Email::ERR_DOT_START : Email::ERR_CONSECUTIVEDOTS;
878 76
                } elseif ($this->hyphenFlag) {
879
                    // Previous subdomain ended in a hyphen
880 1
                    $this->returnStatus[] = Email::ERR_DOMAINHYPHENEND;
881
                } else {
882
                    // Fatal error
883
                    //
884
                    // Nowhere in RFC 5321 does it say explicitly that the
885
                    // domain part of a Mailbox must be a valid domain according
886
                    // to the DNS standards set out in RFC 1035, but this *is*
887
                    // implied in several places. For instance, wherever the idea
888
                    // of host routing is discussed the RFC says that the domain
889
                    // must be looked up in the DNS. This would be nonsense unless
890
                    // the domain was designed to be a valid DNS domain. Hence we
891
                    // must conclude that the RFC 1035 restriction on label length
892
                    // also applies to RFC 5321 domains.
893
                    //
894
                    // http://tools.ietf.org/html/rfc1035#section-2.3.4
895
                    // labels          63 octets or less
896 75
                    if ($this->elementLen > 63) {
897
                        $this->returnStatus[] = Email::RFC5322_LABEL_TOOLONG;
898
                    }
899
                }
900
901
                // CFWS is OK again now we're at the beginning of an element (although it may be obsolete CFWS)
902 76
                $this->endOrDie = false;
903 76
                $this->elementLen = 0;
904 76
                $this->elementCount++;
905 76
                $this->atomList[Email::COMPONENT_DOMAIN][$this->elementCount] = '';
906 76
                $this->parseData[Email::COMPONENT_DOMAIN] .= $this->token;
907
908 76
                break;
909
910
            // Domain literal
911 118
            case Email::STRING_OPENSQBRACKET:
912 37
                if ($this->parseData[Email::COMPONENT_DOMAIN] === '') {
913
                    // Domain literal must be the only component
914 36
                    $this->endOrDie = true;
915 36
                    $this->elementLen++;
916 36
                    $this->contextStack[] = $this->context;
917 36
                    $this->context = Email::COMPONENT_LITERAL;
918 36
                    $this->parseData[Email::COMPONENT_DOMAIN] .= $this->token;
919 36
                    $this->atomList[Email::COMPONENT_DOMAIN][$this->elementCount] .= $this->token;
920 36
                    $this->parseData[Email::COMPONENT_LITERAL] = '';
921
                } else {
922
                    // Fatal error
923 1
                    $this->returnStatus[] = Email::ERR_EXPECTING_ATEXT;
924
                }
925
926 37
                break;
927
928
            // Folding White Space
929 84
            case Email::STRING_CR:
930 84
            case Email::STRING_SP:
931 83
            case Email::STRING_HTAB:
932 13
                if (($this->token === Email::STRING_CR) && ((++$this->pos === $this->rawLength) || ($this->email[$this->pos] !== Email::STRING_LF))) {
933
                    // Fatal error
934 1
                    $this->returnStatus[] = Email::ERR_CR_NO_LF;
935 1
                    break;
936
                }
937
938 12
                if ($this->elementLen === 0) {
939 1
                    $this->returnStatus[] = ($this->elementCount === 0) ? Email::DEPREC_CFWS_NEAR_AT : Email::DEPREC_FWS;
940
                } else {
941 12
                    $this->returnStatus[] = Email::CFWS_FWS;
942
                    // We can't start FWS in the middle of an element, so this better be the end
943 12
                    $this->endOrDie = true;
944
                }
945
946 12
                $this->contextStack[] = $this->context;
947 12
                $this->context = Email::CONTEXT_FWS;
948 12
                $this->tokenPrior = $this->token;
949 12
                break;
950
951
            // atext
952
            default:
953
                // RFC 5322 allows any atext...
954
                // http://tools.ietf.org/html/rfc5322#section-3.2.3
955
                //    atext = ALPHA / DIGIT /    ; Printable US-ASCII
956
                //                        "!" / "#" /        ;  characters not including
957
                //                        "$" / "%" /        ;  specials.  Used for atoms.
958
                //                        "&" / "'" /
959
                //                        "*" / "+" /
960
                //                        "-" / "/" /
961
                //                        " = " / "?" /
962
                //                        "^" / "_" /
963
                //                        "`" / "{" /
964
                //                        "|" / "}" /
965
                //                        "~"
966
967
                // But RFC 5321 only allows letter-digit-hyphen to comply with DNS rules (RFCs 1034 & 1123)
968
                // http://tools.ietf.org/html/rfc5321#section-4.1.2
969
                //   sub-domain = Let-dig [Ldh-str]
970
                //
971
                //   Let-dig = ALPHA / DIGIT
972
                //
973
                //   Ldh-str = *( ALPHA / DIGIT / "-" ) Let-dig
974
                //
975 83
                if ($this->endOrDie) {
976
                    // We have encountered atext where it is no longer valid
977 1
                    switch ($this->contextPrior) {
978 1
                        case Email::CONTEXT_COMMENT:
979 1
                        case Email::CONTEXT_FWS:
980
                            $this->returnStatus[] = Email::ERR_ATEXT_AFTER_CFWS;
981
                            break;
982 1
                        case Email::COMPONENT_LITERAL:
983 1
                            $this->returnStatus[] = Email::ERR_ATEXT_AFTER_DOMLIT;
984 1
                            break;
985
                        default:
986
                            throw new Exception("More atext found where none is allowed, but unrecognised prior context: {$this->contextPrior}");
987
                    }
988
                }
989
990 83
                $ord = ord($this->token);
991
992
                // Assume this token isn't a hyphen unless we discover it is
993 83
                $this->hyphenFlag = false;
994
995 83
                if (($ord < 33) || ($ord > 126) || (!is_bool(strpos(Email::STRING_SPECIALS, $this->token)))) {
996
                    // Fatal error
997 3
                    $this->returnStatus[] = Email::ERR_EXPECTING_ATEXT;
998 81
                } elseif ($this->token === Email::STRING_HYPHEN) {
999 9
                    if ($this->elementLen === 0) {
1000
                        // Hyphens can't be at the beginning of a subdomain
1001
                        // Fatal error
1002 1
                        $this->returnStatus[] = Email::ERR_DOMAINHYPHENSTART;
1003
                    }
1004 9
                    $this->hyphenFlag = true;
1005 79
                } elseif (!(($ord > 47 && $ord < 58) || ($ord > 64 && $ord < 91) || ($ord > 96 && $ord < 123))) {
1006
                    // Not an RFC 5321 subdomain, but still OK by RFC 5322
1007 1
                    $this->returnStatus[] = Email::RFC5322_DOMAIN;
1008
                }
1009
1010 83
                $this->parseData[Email::COMPONENT_DOMAIN] .= $this->token;
1011 83
                $this->atomList[Email::COMPONENT_DOMAIN][$this->elementCount] .= $this->token;
1012 83
                $this->elementLen++;
1013
        }
1014 119
    }
1015
1016
    /**
1017
     *
1018
     * Parse for a literal component.
1019
     *
1020
     * @return null
1021
     *
1022
     */
1023 36
    protected function parseComponentLiteral()
1024
    {
1025
        // http://tools.ietf.org/html/rfc5322#section-3.4.1
1026
        //   domain-literal = [CFWS] "[" *([FWS] dtext) [FWS] "]" [CFWS]
1027
        //
1028
        //   dtext = %d33-90 /          ; Printable US-ASCII
1029
        //                       %d94-126 /         ;  characters not including
1030
        //                       obs-dtext          ;  "[", "]", or "\"
1031
        //
1032
        //   obs-dtext = obs-NO-WS-CTL / quoted-pair
1033 36
        switch ($this->token) {
1034
1035
            // End of domain literal
1036 36
            case Email::STRING_CLOSESQBRACKET:
1037 32
                if ((int) max($this->returnStatus) < Email::DEPREC) {
1038
                    // Could be a valid RFC 5321 address literal, so let's check
1039
1040
                    // http://tools.ietf.org/html/rfc5321#section-4.1.2
1041
                    //   address-literal = "[" ( IPv4-address-literal /
1042
                    //                    IPv6-address-literal /
1043
                    //                    General-address-literal ) "]"
1044
                    //                    ; See Section 4.1.3
1045
                    //
1046
                    // http://tools.ietf.org/html/rfc5321#section-4.1.3
1047
                    //   IPv4-address-literal = Snum 3("."  Snum)
1048
                    //
1049
                    //   IPv6-address-literal = "IPv6:" IPv6-addr
1050
                    //
1051
                    //   General-address-literal = Standardized-tag ":" 1*dcontent
1052
                    //
1053
                    //   Standardized-tag = Ldh-str
1054
                    //                     ; Standardized-tag MUST be specified in a
1055
                    //                     ; Standards-Track RFC and registered with IANA
1056
                    //
1057
                    //   dcontent = %d33-90 / ; Printable US-ASCII
1058
                    //                  %d94-126 ; excl. "[", "\", "]"
1059
                    //
1060
                    //   Snum = 1*3DIGIT
1061
                    //                  ; representing a decimal integer
1062
                    //                  ; value in the range 0 through 255
1063
                    //
1064
                    //   IPv6-addr = IPv6-full / IPv6-comp / IPv6v4-full / IPv6v4-comp
1065
                    //
1066
                    //   IPv6-hex = 1*4HEXDIG
1067
                    //
1068
                    //   IPv6-full = IPv6-hex 7(":" IPv6-hex)
1069
                    //
1070
                    //   IPv6-comp = [IPv6-hex *5(":" IPv6-hex)] "::"
1071
                    //                  [IPv6-hex *5(":" IPv6-hex)]
1072
                    //                  ; The "::" represents at least 2 16-bit groups of
1073
                    //                  ; zeros.  No more than 6 groups in addition to the
1074
                    //                  ; "::" may be present.
1075
                    //
1076
                    //   IPv6v4-full = IPv6-hex 5(":" IPv6-hex) ":" IPv4-address-literal
1077
                    //
1078
                    //   IPv6v4-comp = [IPv6-hex *3(":" IPv6-hex)] "::"
1079
                    //                  [IPv6-hex *3(":" IPv6-hex) ":"]
1080
                    //                  IPv4-address-literal
1081
                    //                  ; The "::" represents at least 2 16-bit groups of
1082
                    //                  ; zeros.  No more than 4 groups in addition to the
1083
                    //                  ; "::" and IPv4-address-literal may be present.
1084
                    //
1085
                    // is_email() author's note: We can't use ip2long() to validate
1086
                    // IPv4 addresses because it accepts abbreviated addresses
1087
                    // (xxx.xxx.xxx), expanding the last group to complete the address.
1088
                    // filter_var() validates IPv6 address inconsistently (up to PHP 5.3.3
1089
                    // at least) -- see http://bugs.php.net/bug.php?id = 53236 for example
1090 29
                    $max_groups = 8;
1091 29
                    $matchesIP = array();
1092 29
                    $index = false;
1093 29
                    $addressliteral = $this->parseData[Email::COMPONENT_LITERAL];
1094
1095
                    // Extract IPv4 part from the end of the address-literal (if there is one)
1096 29
                    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) {
1097 10
                        $index = strrpos($addressliteral, $matchesIP[0]);
1098 10
                        if ($index !== 0) {
1099
                            // Convert IPv4 part to IPv6 format for further testing
1100 8
                            $addressliteral = substr($addressliteral, 0, $index) . '0:0';
1101
                        }
1102
                    }
1103
1104 29
                    if ($index === 0) {
1105
                        // Nothing there except a valid IPv4 address, so...
1106 2
                        $this->returnStatus[] = Email::RFC5321_ADDRESSLITERAL;
1107 27
                    } elseif (strncasecmp($addressliteral, Email::STRING_IPV6TAG, 5) !== 0) {
1108 8
                        $this->returnStatus[] = Email::RFC5322_DOMAINLITERAL;
1109
                    } else {
1110 19
                        $IPv6 = substr($addressliteral, 5);
1111
                        // Revision 2.7: Daniel Marschall's new IPv6 testing strategy
1112 19
                        $matchesIP = explode(Email::STRING_COLON, $IPv6);
1113 19
                        $groupCount = count($matchesIP);
1114 19
                        $index = strpos($IPv6, Email::STRING_DOUBLECOLON);
1115
1116 19
                        if ($index === false) {
1117
                            // We need exactly the right number of groups
1118 9
                            if ($groupCount !== $max_groups) {
1119 9
                                $this->returnStatus[] = Email::RFC5322_IPV6_GRPCOUNT;
1120
                            }
1121
                        } else {
1122 10
                            if ($index !== strrpos($IPv6, Email::STRING_DOUBLECOLON)) {
1123 2
                                $this->returnStatus[] = Email::RFC5322_IPV6_2X2XCOLON;
1124
                            } else {
1125 8
                                if ($index === 0 || $index === (strlen($IPv6) - 2)) {
1126
                                    // RFC 4291 allows :: at the start or end of an address with 7 other groups in addition
1127 2
                                    $max_groups++;
1128
                                }
1129
1130 8
                                if ($groupCount > $max_groups) {
1131 2
                                    $this->returnStatus[] = Email::RFC5322_IPV6_MAXGRPS;
1132 6
                                } elseif ($groupCount === $max_groups) {
1133
                                    // Eliding a single "::"
1134 1
                                    $this->returnStatus[] = Email::RFC5321_IPV6DEPRECATED;
1135
                                }
1136
                            }
1137
                        }
1138
1139
                        // Revision 2.7: Daniel Marschall's new IPv6 testing strategy
1140 19
                        if ((substr($IPv6, 0,  1) === Email::STRING_COLON) && (substr($IPv6, 1,  1) !== Email::STRING_COLON)) {
1141
                            // Address starts with a single colon
1142 2
                            $this->returnStatus[] = Email::RFC5322_IPV6_COLONSTRT;
1143 17
                        } elseif ((substr($IPv6, -1) === Email::STRING_COLON) && (substr($IPv6, -2, 1) !== Email::STRING_COLON)) {
1144
                            // Address ends with a single colon
1145 1
                            $this->returnStatus[] = Email::RFC5322_IPV6_COLONEND;
1146 16
                        } elseif (count(preg_grep('/^[0-9A-Fa-f]{0,4}$/', $matchesIP, PREG_GREP_INVERT)) !== 0) {
1147
                            // Check for unmatched characters
1148 1
                            $this->returnStatus[] = Email::RFC5322_IPV6_BADCHAR;
1149
                        } else {
1150 29
                            $this->returnStatus[] = Email::RFC5321_ADDRESSLITERAL;
1151
                        }
1152
                    }
1153
                } else {
1154 3
                    $this->returnStatus[] = Email::RFC5322_DOMAINLITERAL;
1155
                }
1156
1157 32
                $this->parseData[Email::COMPONENT_DOMAIN] .= $this->token;
1158 32
                $this->atomList[Email::COMPONENT_DOMAIN][$this->elementCount] .= $this->token;
1159 32
                $this->elementLen++;
1160 32
                $this->contextPrior = $this->context;
1161 32
                $this->context = (int) array_pop($this->contextStack);
1162 32
                break;
1163
1164 36
            case Email::STRING_BACKSLASH:
1165 5
                $this->returnStatus[] = Email::RFC5322_DOMLIT_OBSDTEXT;
1166 5
                $this->contextStack[] = $this->context;
1167 5
                $this->context = Email::CONTEXT_QUOTEDPAIR;
1168 5
                break;
1169
1170
            // Folding White Space
1171 36
            case Email::STRING_CR:
1172 36
            case Email::STRING_SP:
1173 36
            case Email::STRING_HTAB:
1174 1
                if (($this->token === Email::STRING_CR) && ((++$this->pos === $this->rawLength) || ($this->email[$this->pos] !== Email::STRING_LF))) {
1175
                    // Fatal error
1176
                    $this->returnStatus[] = Email::ERR_CR_NO_LF;
1177
                    break;
1178
                }
1179
1180 1
                $this->returnStatus[] = Email::CFWS_FWS;
1181
1182 1
                $this->contextStack[] = $this->context;
1183 1
                $this->context = Email::CONTEXT_FWS;
1184 1
                $this->tokenPrior = $this->token;
1185 1
                break;
1186
1187
            // dtext
1188
            default:
1189
                // http://tools.ietf.org/html/rfc5322#section-3.4.1
1190
                //   dtext = %d33-90 /          ; Printable US-ASCII
1191
                //                       %d94-126 /         ;  characters not including
1192
                //                       obs-dtext          ;  "[", "]", or "\"
1193
                //
1194
                //   obs-dtext = obs-NO-WS-CTL / quoted-pair
1195
                //
1196
                //   obs-NO-WS-CTL = %d1-8 /            ; US-ASCII control
1197
                //                       %d11 /             ;  characters that do not
1198
                //                       %d12 /             ;  include the carriage
1199
                //                       %d14-31 /          ;  return, line feed, and
1200
                //                       %d127              ;  white space characters
1201 36
                $ord = ord($this->token);
1202
1203
                // CR, LF, SP & HTAB have already been parsed above
1204 36
                if (($ord > 127) || ($ord === 0) || ($this->token === Email::STRING_OPENSQBRACKET)) {
1205 1
                    $this->returnStatus[] = Email::ERR_EXPECTING_DTEXT; // Fatal error
1206 1
                    break;
1207 36
                } elseif (($ord < 33) || ($ord === 127)) {
1208
                    $this->returnStatus[] = Email::RFC5322_DOMLIT_OBSDTEXT;
1209
                }
1210
1211 36
                $this->parseData[Email::COMPONENT_LITERAL] .= $this->token;
1212 36
                $this->parseData[Email::COMPONENT_DOMAIN] .= $this->token;
1213 36
                $this->atomList[Email::COMPONENT_DOMAIN][$this->elementCount] .= $this->token;
1214 36
                $this->elementLen++;
1215
        }
1216 36
    }
1217
1218
    /**
1219
     *
1220
     * Parse for a quoted-string context.
1221
     *
1222
     * @return null
1223
     *
1224
     */
1225 27
    protected function parseContextQuotedString()
1226
    {
1227
        // http://tools.ietf.org/html/rfc5322#section-3.2.4
1228
        //   quoted-string = [CFWS]
1229
        //                       DQUOTE *([FWS] qcontent) [FWS] DQUOTE
1230
        //                       [CFWS]
1231
        //
1232
        //   qcontent = qtext / quoted-pair
1233 27
        switch ($this->token) {
1234
1235
            // Quoted pair
1236 27
            case Email::STRING_BACKSLASH:
1237 12
                $this->contextStack[] = $this->context;
1238 12
                $this->context = Email::CONTEXT_QUOTEDPAIR;
1239 12
                break;
1240
1241
            // Folding White Space
1242
            // Inside a quoted string, spaces are allowed as regular characters.
1243
            // It's only FWS if we include HTAB or CRLF
1244 27
            case Email::STRING_CR:
1245 26
            case Email::STRING_HTAB:
1246 1
                if (($this->token === Email::STRING_CR) && ((++$this->pos === $this->rawLength) || ($this->email[$this->pos] !== Email::STRING_LF))) {
1247
                    // Fatal error
1248 1
                    $this->returnStatus[] = Email::ERR_CR_NO_LF;
1249 1
                    break;
1250
                }
1251
1252
                // http://tools.ietf.org/html/rfc5322#section-3.2.2
1253
                //   Runs of FWS, comment, or CFWS that occur between lexical tokens in a
1254
                //   structured header field are semantically interpreted as a single
1255
                //   space character.
1256
1257
                // http://tools.ietf.org/html/rfc5322#section-3.2.4
1258
                //   the CRLF in any FWS/CFWS that appears within the quoted-string [is]
1259
                //   semantically "invisible" and therefore not part of the quoted-string
1260
                $this->parseData[Email::COMPONENT_LOCALPART] .= Email::STRING_SP;
1261
                $this->atomList[Email::COMPONENT_LOCALPART][$this->elementCount] .= Email::STRING_SP;
1262
                $this->elementLen++;
1263
1264
                $this->returnStatus[] = Email::CFWS_FWS;
1265
                $this->contextStack[] = $this->context;
1266
                $this->context = Email::CONTEXT_FWS;
1267
                $this->tokenPrior = $this->token;
1268
                break;
1269
1270
            // End of quoted string
1271 26
            case Email::STRING_DQUOTE:
1272 18
                $this->parseData[Email::COMPONENT_LOCALPART] .= $this->token;
1273 18
                $this->atomList[Email::COMPONENT_LOCALPART][$this->elementCount] .= $this->token;
1274 18
                $this->elementLen++;
1275 18
                $this->contextPrior = $this->context;
1276 18
                $this->context = (int) array_pop($this->contextStack);
1277 18
                break;
1278
1279
            // qtext
1280
            default:
1281
                // http://tools.ietf.org/html/rfc5322#section-3.2.4
1282
                //   qtext = %d33 /             ; Printable US-ASCII
1283
                //                       %d35-91 /          ;  characters not including
1284
                //                       %d93-126 /         ;  "\" or the quote character
1285
                //                       obs-qtext
1286
                //
1287
                //   obs-qtext = obs-NO-WS-CTL
1288
                //
1289
                //   obs-NO-WS-CTL = %d1-8 /            ; US-ASCII control
1290
                //                       %d11 /             ;  characters that do not
1291
                //                       %d12 /             ;  include the carriage
1292
                //                       %d14-31 /          ;  return, line feed, and
1293
                //                       %d127              ;  white space characters
1294 18
                $ord = ord($this->token);
1295
1296 18
                if (($ord > 127) || ($ord === 0) || ($ord === 10)) {
1297
                    // Fatal error
1298 2
                    $this->returnStatus[] = Email::ERR_EXPECTING_QTEXT;
1299 17
                } elseif (($ord < 32) || ($ord === 127)) {
1300 2
                    $this->returnStatus[] = Email::DEPREC_QTEXT;
1301
                }
1302
1303 18
                $this->parseData[Email::COMPONENT_LOCALPART] .= $this->token;
1304 18
                $this->atomList[Email::COMPONENT_LOCALPART][$this->elementCount] .= $this->token;
1305 18
                $this->elementLen++;
1306
        }
1307
1308
        // http://tools.ietf.org/html/rfc5322#section-3.4.1
1309
        //   If the string can be represented as a dot-atom (that is, it contains
1310
        //   no characters other than atext characters or "." surrounded by atext
1311
        //   characters), then the dot-atom form SHOULD be used and the quoted-
1312
        //   string form SHOULD NOT be used.
1313
        //
1314
        // TODO
1315
        //
1316 27
    }
1317
1318
    /**
1319
     *
1320
     * Parse for a quoted-pair context.
1321
     *
1322
     * @return null
1323
     *
1324
     */
1325 18
    protected function parseContextQuotedPair()
1326
    {
1327
        // http://tools.ietf.org/html/rfc5322#section-3.2.1
1328
        //   quoted-pair = ("\" (VCHAR / WSP)) / obs-qp
1329
        //
1330
        //   VCHAR = %d33-126            ; visible (printing) characters
1331
        //   WSP = SP / HTAB           ; white space
1332
        //
1333
        //   obs-qp = "\" (%d0 / obs-NO-WS-CTL / LF / CR)
1334
        //
1335
        //   obs-NO-WS-CTL = %d1-8 /            ; US-ASCII control
1336
        //                       %d11 /             ;  characters that do not
1337
        //                       %d12 /             ;  include the carriage
1338
        //                       %d14-31 /          ;  return, line feed, and
1339
        //                       %d127              ;  white space characters
1340
        //
1341
        // i.e. obs-qp = "\" (%d0-8, %d10-31 / %d127)
1342 18
        $ord = ord($this->token);
1343
1344 18
        if ($ord > 127) {
1345 2
            $this->returnStatus[] = Email::ERR_EXPECTING_QPAIR;
1346 16
        } elseif ((($ord < 31) && ($ord !== 9)) || ($ord === 127)) {
1347
            // SP & HTAB are allowed
1348
            // Fatal error
1349 4
            $this->returnStatus[] = Email::DEPREC_QP;
1350
        }
1351
1352
        // At this point we know where this qpair occurred so
1353
        // we could check to see if the character actually
1354
        // needed to be quoted at all.
1355
        // http://tools.ietf.org/html/rfc5321#section-4.1.2
1356
        //   the sending system SHOULD transmit the
1357
        //   form that uses the minimum quoting possible.
1358
        //
1359
        // TODO: check whether the character needs to be quoted (escaped) in this context
1360
        //
1361 18
        $this->contextPrior = $this->context;
1362 18
        $this->context = (int) array_pop($this->contextStack); // End of qpair
1363 18
        $this->token = Email::STRING_BACKSLASH . $this->token;
1364
1365 18
        switch ($this->context) {
1366 18
            case Email::CONTEXT_COMMENT:
1367 2
                break;
1368 16
            case Email::CONTEXT_QUOTEDSTRING:
1369 12
                $this->parseData[Email::COMPONENT_LOCALPART] .= $this->token;
1370 12
                $this->atomList[Email::COMPONENT_LOCALPART][$this->elementCount] .= $this->token;
1371
                // The maximum sizes specified by RFC 5321 are octet counts, so we must include the backslash
1372 12
                $this->elementLen += 2;
1373 12
                break;
1374 4
            case Email::COMPONENT_LITERAL:
1375 4
                $this->parseData[Email::COMPONENT_DOMAIN] .= $this->token;
1376 4
                $this->atomList[Email::COMPONENT_DOMAIN][$this->elementCount] .= $this->token;
1377
                // The maximum sizes specified by RFC 5321 are octet counts, so we must include the backslash
1378 4
                $this->elementLen += 2;
1379 4
                break;
1380
            default:
1381
                throw new Exception("Quoted pair logic invoked in an invalid context: {$this->context}");
1382
        }
1383 18
    }
1384
1385
    /**
1386
     *
1387
     * Parse for a comment context.
1388
     *
1389
     * @return null
1390
     *
1391
     */
1392 20
    protected function parseContextComment()
1393
    {
1394
        // http://tools.ietf.org/html/rfc5322#section-3.2.2
1395
        //   comment = "(" *([FWS] ccontent) [FWS] ")"
1396
        //
1397
        //   ccontent = ctext / quoted-pair / comment
1398 20
        switch ($this->token) {
1399
1400
            // Nested comment
1401 20
            case Email::STRING_OPENPARENTHESIS:
1402
                // Nested comments are OK
1403 2
                $this->contextStack[] = $this->context;
1404 2
                $this->context = Email::CONTEXT_COMMENT;
1405 2
                break;
1406
1407
            // End of comment
1408 20
            case Email::STRING_CLOSEPARENTHESIS:
1409 12
                $this->contextPrior = $this->context;
1410 12
                $this->context = (int) array_pop($this->contextStack);
1411
1412
                // http://tools.ietf.org/html/rfc5322#section-3.2.2
1413
                //   Runs of FWS, comment, or CFWS that occur between lexical tokens in a
1414
                //   structured header field are semantically interpreted as a single
1415
                //   space character.
1416
                //
1417
                // is_email() author's note: This *cannot* mean that we must add a
1418
                // space to the address wherever CFWS appears. This would result in
1419
                // any addr-spec that had CFWS outside a quoted string being invalid
1420
                // for RFC 5321.
1421
                //
1422
                // if (($this->context === Email::COMPONENT_LOCALPART) || ($this->context === Email::COMPONENT_DOMAIN)) {
1423
                //     $this->parseData[$this->context] .= Email::STRING_SP;
1424
                //     $this->atomList[$this->context][$this->elementCount] .= Email::STRING_SP;
1425
                //     $this->elementLen++;
1426
                // }
1427
1428 12
                break;
1429
1430
            // Quoted pair
1431 20
            case Email::STRING_BACKSLASH:
1432 3
                $this->contextStack[] = $this->context;
1433 3
                $this->context = Email::CONTEXT_QUOTEDPAIR;
1434 3
                break;
1435
1436
            // Folding White Space
1437 20
            case Email::STRING_CR:
1438 18
            case Email::STRING_SP:
1439 18
            case Email::STRING_HTAB:
1440 2
                if (($this->token === Email::STRING_CR) && ((++$this->pos === $this->rawLength) || ($this->email[$this->pos] !== Email::STRING_LF))) {
1441
                    // Fatal error
1442 2
                    $this->returnStatus[] = Email::ERR_CR_NO_LF;
1443 2
                    break;
1444
                }
1445
1446
                $this->returnStatus[] = Email::CFWS_FWS;
1447
1448
                $this->contextStack[] = $this->context;
1449
                $this->context = Email::CONTEXT_FWS;
1450
                $this->tokenPrior = $this->token;
1451
                break;
1452
1453
            // ctext
1454
            default:
1455
                // http://tools.ietf.org/html/rfc5322#section-3.2.3
1456
                //   ctext = %d33-39 /          ; Printable US-ASCII
1457
                //                       %d42-91 /          ;  characters not including
1458
                //                       %d93-126 /         ;  "(", ")", or "\"
1459
                //                       obs-ctext
1460
                //
1461
                //   obs-ctext = obs-NO-WS-CTL
1462
                //
1463
                //   obs-NO-WS-CTL = %d1-8 /            ; US-ASCII control
1464
                //                       %d11 /             ;  characters that do not
1465
                //                       %d12 /             ;  include the carriage
1466
                //                       %d14-31 /          ;  return, line feed, and
1467
                //                       %d127              ;  white space characters
1468 18
                $ord = ord($this->token);
1469
1470 18
                if (($ord > 127) || ($ord === 0) || ($ord === 10)) {
1471 1
                    $this->returnStatus[] = Email::ERR_EXPECTING_CTEXT; // Fatal error
1472 1
                    break;
1473 17
                } elseif (($ord < 32) || ($ord === 127)) {
1474 2
                    $this->returnStatus[] = Email::DEPREC_CTEXT;
1475
                }
1476
        }
1477 20
    }
1478
1479
    /**
1480
     *
1481
     * Parse for a folding-white-space context.
1482
     *
1483
     * @return null
1484
     *
1485
     */
1486 23
    protected function parseContextFws()
1487
    {
1488
        // http://tools.ietf.org/html/rfc5322#section-3.2.2
1489
        //   FWS = ([*WSP CRLF] 1*WSP) /  obs-FWS
1490
        //                                          ; Folding white space
1491
1492
        // But note the erratum:
1493
        // http://www.rfc-editor.org/errata_search.php?rfc = 5322&eid = 1908:
1494
        //   In the obsolete syntax, any amount of folding white space MAY be
1495
        //   inserted where the obs-FWS rule is allowed.  This creates the
1496
        //   possibility of having two consecutive "folds" in a line, and
1497
        //   therefore the possibility that a line which makes up a folded header
1498
        //   field could be composed entirely of white space.
1499
        //
1500
        //   obs-FWS = 1*([CRLF] WSP)
1501 23
        if ($this->tokenPrior === Email::STRING_CR) {
1502 16
            if ($this->token === Email::STRING_CR) {
1503
                // Fatal error
1504 4
                $this->returnStatus[] = Email::ERR_FWS_CRLF_X2;
1505 4
                return;
1506
            }
1507
1508 12
            if (isset($this->crlfCount)) {
1509 4
                if (++$this->crlfCount > 1) {
1510 4
                    $this->returnStatus[] = Email::DEPREC_FWS;
1511
                } // Multiple folds = obsolete FWS
1512
            } else {
1513 12
                $this->crlfCount = 1;
1514
            }
1515
        }
1516
1517 23
        switch ($this->token) {
1518 23
            case Email::STRING_CR:
1519 14
                if ((++$this->pos === $this->rawLength) || ($this->email[$this->pos] !== Email::STRING_LF)) {
1520
                     // Fatal error
1521
                    $this->returnStatus[] = Email::ERR_CR_NO_LF;
1522
                }
1523 14
                break;
1524
1525 18
            case Email::STRING_SP:
1526 13
            case Email::STRING_HTAB:
1527 10
                break;
1528
1529
            default:
1530 13
                if ($this->tokenPrior === Email::STRING_CR) {
1531
                    // Fatal error
1532 4
                    $this->returnStatus[] = Email::ERR_FWS_CRLF_END;
1533 4
                    break;
1534
                }
1535
1536 9
                if (isset($this->crlfCount)) {
1537 3
                    unset($this->crlfCount);
1538
                }
1539
1540 9
                $this->contextPrior = $this->context;
1541 9
                $this->context = (int) array_pop($this->contextStack); // End of FWS
1542
1543
                // http://tools.ietf.org/html/rfc5322#section-3.2.2
1544
                //   Runs of FWS, comment, or CFWS that occur between lexical tokens in a
1545
                //   structured header field are semantically interpreted as a single
1546
                //   space character.
1547
                //
1548
                // is_email() author's note: This *cannot* mean that we must add a
1549
                // space to the address wherever CFWS appears. This would result in
1550
                // any addr-spec that had CFWS outside a quoted string being invalid
1551
                // for RFC 5321.
1552
                //
1553
                // if (($this->context === Email::COMPONENT_LOCALPART) || ($this->context === Email::COMPONENT_DOMAIN)) {
1554
                //     $this->parseData[$this->context] .= Email::STRING_SP;
1555
                //     $this->atomList[$this->context][$this->elementCount] .= Email::STRING_SP;
1556
                //     $this->elementLen++;
1557
                // }
1558
1559 9
                $this->pos--; // Look at this token again in the parent context
1560
        }
1561
1562 23
        $this->tokenPrior = $this->token;
1563 23
    }
1564
1565
    /**
1566
     *
1567
     * Final wrap-up parsing.
1568
     *
1569
     * @return null
1570
     *
1571
     */
1572 165
    protected function parseFinal()
1573
    {
1574
        // Some simple final tests
1575 165
        if ((int) max($this->returnStatus) < Email::RFC5322) {
1576 123
            if ($this->context === Email::CONTEXT_QUOTEDSTRING) {
1577
                // Fatal error
1578 4
                $this->returnStatus[] = Email::ERR_UNCLOSEDQUOTEDSTR;
1579 119
            } elseif ($this->context === Email::CONTEXT_QUOTEDPAIR) {
1580
                // Fatal error
1581 2
                $this->returnStatus[] = Email::ERR_BACKSLASHEND;
1582 117
            } elseif ($this->context === Email::CONTEXT_COMMENT) {
1583
                // Fatal error
1584 5
                $this->returnStatus[] = Email::ERR_UNCLOSEDCOMMENT;
1585 112
            } elseif ($this->context === Email::COMPONENT_LITERAL) {
1586
                // Fatal error
1587 2
                $this->returnStatus[] = Email::ERR_UNCLOSEDDOMLIT;
1588 110
            } elseif ($this->token === Email::STRING_CR) {
1589
                // Fatal error
1590 4
                $this->returnStatus[] = Email::ERR_FWS_CRLF_END;
1591 106
            } elseif ($this->parseData[Email::COMPONENT_DOMAIN] === '') {
1592
                // Fatal error
1593 9
                $this->returnStatus[] = Email::ERR_NODOMAIN;
1594 97
            } elseif ($this->elementLen === 0) {
1595
                // Fatal error
1596 1
                $this->returnStatus[] = Email::ERR_DOT_END;
1597 96
            } elseif ($this->hyphenFlag) {
1598
                // Fatal error
1599 1
                $this->returnStatus[] = Email::ERR_DOMAINHYPHENEND;
1600 95
            } elseif (strlen($this->parseData[Email::COMPONENT_DOMAIN]) > 255) {
1601
                // http://tools.ietf.org/html/rfc5321#section-4.5.3.1.2
1602
                //   The maximum total length of a domain name or number is 255 octets.
1603
                $this->returnStatus[] = Email::RFC5322_DOMAIN_TOOLONG;
1604 95
            } elseif (strlen($this->parseData[Email::COMPONENT_LOCALPART] . Email::STRING_AT . $this->parseData[Email::COMPONENT_DOMAIN]) > 254) {
1605
                // http://tools.ietf.org/html/rfc5321#section-4.1.2
1606
                //   Forward-path = Path
1607
                //
1608
                //   Path = "<" [ A-d-l ":" ] Mailbox ">"
1609
                //
1610
                // http://tools.ietf.org/html/rfc5321#section-4.5.3.1.3
1611
                //   The maximum total length of a reverse-path or forward-path is 256
1612
                //   octets (including the punctuation and element separators).
1613
                //
1614
                // Thus, even without (obsolete) routing information, the Mailbox can
1615
                // only be 254 characters long. This is confirmed by this verified
1616
                // erratum to RFC 3696:
1617
                //
1618
                // http://www.rfc-editor.org/errata_search.php?rfc = 3696&eid = 1690
1619
                //   However, there is a restriction in RFC 2821 on the length of an
1620
                //   address in MAIL and RCPT commands of 254 characters.  Since addresses
1621
                //   that do not fit in those fields are not normally useful, the upper
1622
                //   limit on address lengths should normally be considered to be 254.
1623 2
                $this->returnStatus[] = Email::RFC5322_TOOLONG;
1624 93
            } elseif ($this->elementLen > 63) {
1625
                // http://tools.ietf.org/html/rfc1035#section-2.3.4
1626
                // labels          63 octets or less
1627
                $this->returnStatus[] = Email::RFC5322_LABEL_TOOLONG;
1628
            }
1629
        }
1630 165
    }
1631
1632
    /**
1633
     *
1634
     * Make a DNS check on the MX record, if requested.
1635
     *
1636
     * @return null
1637
     *
1638
     */
1639 165
    protected function checkDns()
1640
    {
1641
        // Check DNS?
1642 165
        if ($this->checkDns && ((int) max($this->returnStatus) < Email::DNSWARN) && function_exists('dns_get_record')) {
1643
            // http://tools.ietf.org/html/rfc5321#section-2.3.5
1644
            //   Names that can
1645
            //   be resolved to MX RRs or address (i.e., A or AAAA) RRs (as discussed
1646
            //   in Section 5) are permitted, as are CNAME RRs whose targets can be
1647
            //   resolved, in turn, to MX or address RRs.
1648
            //
1649
            // http://tools.ietf.org/html/rfc5321#section-5.1
1650
            //   The lookup first attempts to locate an MX record associated with the
1651
            //   name.  If a CNAME record is found, the resulting name is processed as
1652
            //   if it were the initial name. ... If an empty list of MXs is returned,
1653
            //   the address is treated as if it was associated with an implicit MX
1654
            //   RR, with a preference of 0, pointing to that host.
1655
            //
1656
            // is_email() author's note: We will regard the existence of a CNAME to be
1657
            // sufficient evidence of the domain's existence. For performance reasons
1658
            // we will not repeat the DNS lookup for the CNAME's target, but we will
1659
            // raise a warning because we didn't immediately find an MX record.
1660
            if ($this->elementCount === 0) {
1661
                // Checking TLD DNS seems to work only if you explicitly check from the root
1662
                $this->parseData[Email::COMPONENT_DOMAIN] .= '.';
1663
            }
1664
1665
            $result = @dns_get_record($this->parseData[Email::COMPONENT_DOMAIN], DNS_MX); // Not using checkdnsrr because of a suspected bug in PHP 5.3 (http://bugs.php.net/bug.php?id = 51844)
1666
1667
            if ((is_bool($result) && !(bool) $result)) {
1668
                // Domain can't be found in DNS
1669
                $this->returnStatus[] = Email::DNSWARN_NO_RECORD;
1670
            } else {
1671
                if (count($result) === 0) {
1672
                    // MX-record for domain can't be found
1673
                    $this->returnStatus[] = Email::DNSWARN_NO_MX_RECORD;
1674
                    $result = @dns_get_record($this->parseData[Email::COMPONENT_DOMAIN], DNS_A + DNS_CNAME);
1675
                    if (count($result) === 0) {
1676
                        // No usable records for the domain can be found
1677
                        $this->returnStatus[] = Email::DNSWARN_NO_RECORD;
1678
                    }
1679
                } else {
1680
                    $this->dnsChecked = true;
1681
                }
1682
            }
1683
        }
1684 165
    }
1685
1686
    /**
1687
     *
1688
     * Check the top-level domain of the address.
1689
     *
1690
     * @return null
1691
     *
1692
     */
1693 165
    protected function checkTld()
1694
    {
1695
        // Check for TLD addresses
1696
        // -----------------------
1697
        // TLD addresses are specifically allowed in RFC 5321 but they are
1698
        // unusual to say the least. We will allocate a separate
1699
        // status to these addresses on the basis that they are more likely
1700
        // to be typos than genuine addresses (unless we've already
1701
        // established that the domain does have an MX record)
1702
        //
1703
        // http://tools.ietf.org/html/rfc5321#section-2.3.5
1704
        //   In the case
1705
        //   of a top-level domain used by itself in an email address, a single
1706
        //   string is used without any dots.  This makes the requirement,
1707
        //   described in more detail below, that only fully-qualified domain
1708
        //   names appear in SMTP transactions on the public Internet,
1709
        //   particularly important where top-level domains are involved.
1710
        //
1711
        // TLD format
1712
        // ----------
1713
        // The format of TLDs has changed a number of times. The standards
1714
        // used by IANA have been largely ignored by ICANN, leading to
1715
        // confusion over the standards being followed. These are not defined
1716
        // anywhere, except as a general component of a DNS host name (a label).
1717
        // However, this could potentially lead to 123.123.123.123 being a
1718
        // valid DNS name (rather than an IP address) and thereby creating
1719
        // an ambiguity. The most authoritative statement on TLD formats that
1720
        // the author can find is in a (rejected!) erratum to RFC 1123
1721
        // submitted by John Klensin, the author of RFC 5321:
1722
        //
1723
        // http://www.rfc-editor.org/errata_search.php?rfc = 1123&eid = 1353
1724
        //   However, a valid host name can never have the dotted-decimal
1725
        //   form #.#.#.#, since this change does not permit the highest-level
1726
        //   component label to start with a digit even if it is not all-numeric.
1727 165
        if (!$this->dnsChecked && ((int) max($this->returnStatus) < Email::DNSWARN)) {
1728 26
            if ($this->elementCount === 0) {
1729 2
                $this->returnStatus[] = Email::RFC5321_TLD;
1730
            }
1731
1732 26
            if (is_numeric($this->atomList[Email::COMPONENT_DOMAIN][$this->elementCount][0])) {
1733 2
                $this->returnStatus[] = Email::RFC5321_TLDNUMERIC;
1734
            }
1735
        }
1736 165
    }
1737
1738
    /**
1739
     *
1740
     * Sets the final status and return status.
1741
     *
1742
     * @return null
1743
     *
1744
     */
1745 165
    protected function finalStatus()
1746
    {
1747 165
        $this->returnStatus = array_unique($this->returnStatus);
0 ignored issues
show
Documentation Bug introduced by
It seems like array_unique($this->returnStatus) of type array is incompatible with the declared type integer of property $returnStatus.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
1748 165
        $this->finalStatus = (int) max($this->returnStatus);
1749
1750 165
        if (count($this->returnStatus) !== 1) {
1751
            // remove redundant Email::VALID
1752 143
            array_shift($this->returnStatus);
1753
        }
1754
1755 165
        $this->parseData['status'] = $this->returnStatus;
1756
1757 165
        if ($this->finalStatus < $this->threshold) {
1758
            $this->finalStatus = Email::VALID;
1759
        }
1760 165
    }
1761
}
1762