Passed
Pull Request — 3.x (#323)
by Eduardo Gulias
09:09 queued 07:19
created

DNSCheckValidation::getWarnings()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
ccs 0
cts 2
cp 0
crap 2
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace Egulias\EmailValidator\Validation;
4
5
use Egulias\EmailValidator\Validation\DNSGetRecordWrapper;
6
use Egulias\EmailValidator\EmailLexer;
7
use Egulias\EmailValidator\Result\InvalidEmail;
8
use Egulias\EmailValidator\Result\Reason\DomainAcceptsNoMail;
9
use Egulias\EmailValidator\Result\Reason\LocalOrReservedDomain;
10
use Egulias\EmailValidator\Result\Reason\NoDNSRecord as ReasonNoDNSRecord;
11
use Egulias\EmailValidator\Result\Reason\UnableToGetDNSRecord;
12
use Egulias\EmailValidator\Warning\NoDNSMXRecord;
13
14
class DNSCheckValidation implements EmailValidation
15
{
16
    /**
17
     * @var int
18
     */
19
    protected const DNS_RECORD_TYPES_TO_CHECK = DNS_MX + DNS_A + DNS_AAAA;
20
21
    /**
22
     * Reserved Top Level DNS Names (https://tools.ietf.org/html/rfc2606#section-2),
23
     * mDNS and private DNS Namespaces (https://tools.ietf.org/html/rfc6762#appendix-G)
24
     */
25
    const RESERVED_DNS_TOP_LEVEL_NAMES = [
26
        // Reserved Top Level DNS Names
27
        'test',
28
        'example',
29
        'invalid',
30
        'localhost',
31
32
        // mDNS
33
        'local',
34
35
        // Private DNS Namespaces
36
        'intranet',
37
        'internal',
38
        'private',
39
        'corp',
40
        'home',
41
        'lan',
42
    ];
43
    
44
    /**
45
     * @var array
46
     */
47
    private $warnings = [];
48
49
    /**
50
     * @var InvalidEmail|null
51
     */
52
    private $error;
53
54
    /**
55
     * @var array
56
     */
57
    private $mxRecords = [];
58
59
    /**
60
     * @var DNSGetRecordWrapper
61
     */
62
    private $dnsGetRecord;
63
64 24
    public function __construct(DNSGetRecordWrapper $dnsGetRecord = null)
65
    {
66 24
        if (!function_exists('idn_to_ascii')) {
67
            throw new \LogicException(sprintf('The %s class requires the Intl extension.', __CLASS__));
68
        }
69
70 24
        if ($dnsGetRecord == null) {
71 23
            $dnsGetRecord = new DNSGetRecordWrapper();
72
        }
73
74 24
        $this->dnsGetRecord = $dnsGetRecord;
75
    }
76
77 24
    public function isValid(string $email, EmailLexer $emailLexer) : bool
78
    {
79
        // use the input to check DNS if we cannot extract something similar to a domain
80 24
        $host = $email;
81
82
        // Arguable pattern to extract the domain. Not aiming to validate the domain nor the email
83 24
        if (false !== $lastAtPos = strrpos($email, '@')) {
84 12
            $host = substr($email, $lastAtPos + 1);
85
        }
86
87
        // Get the domain parts
88 24
        $hostParts = explode('.', $host);
89
90 24
        $isLocalDomain = count($hostParts) <= 1;
91 24
        $isReservedTopLevel = in_array($hostParts[(count($hostParts) - 1)], self::RESERVED_DNS_TOP_LEVEL_NAMES, true);
92
93
        // Exclude reserved top level DNS names
94 24
        if ($isLocalDomain || $isReservedTopLevel) {
95 11
            $this->error = new InvalidEmail(new LocalOrReservedDomain(), $host);
96 11
            return false;
97
        }
98
99 13
        return $this->checkDns($host);
100
    }
101
102 14
    public function getError() : ?InvalidEmail
103
    {
104 14
        return $this->error;
105
    }
106
107
    public function getWarnings() : array
108
    {
109
        return $this->warnings;
110
    }
111
112
    /**
113
     * @param string $host
114
     *
115
     * @return bool
116
     */
117 13
    protected function checkDns($host)
118
    {
119 13
        $variant = INTL_IDNA_VARIANT_UTS46;
120
121 13
        $host = rtrim(idn_to_ascii($host, IDNA_DEFAULT, $variant), '.') . '.';
122
123 13
        return $this->validateDnsRecords($host);
124
    }
125
126
127
    /**
128
     * Validate the DNS records for given host.
129
     *
130
     * @param string $host A set of DNS records in the format returned by dns_get_record.
131
     *
132
     * @return bool True on success.
133
     */
134 13
    private function validateDnsRecords($host) : bool
135
    {
136 13
        $dnsRecordsResult = $this->dnsGetRecord->getRecords($host, static::DNS_RECORD_TYPES_TO_CHECK);
137
138 13
        if ($dnsRecordsResult->withError()) {
139
            $this->error = new InvalidEmail(new UnableToGetDNSRecord(), '');
140
            return false;
141
        }
142
143 13
        $dnsRecords = $dnsRecordsResult->getRecords();
144
145
        // No MX, A or AAAA DNS records
146 13
        if ($dnsRecords === []) {
147 2
            $this->error = new InvalidEmail(new ReasonNoDNSRecord(), '');
148 2
            return false;
149
        }
150
151
        // For each DNS record
152 11
        foreach ($dnsRecords as $dnsRecord) {
153 11
            if (!$this->validateMXRecord($dnsRecord)) {
154
                // No MX records (fallback to A or AAAA records)
155 2
                if (empty($this->mxRecords)) {
156 2
                    $this->warnings[NoDNSMXRecord::CODE] = new NoDNSMXRecord();
157
                }
158 2
                return false;
159
            }
160
        }
161 9
        return true;
162
    }
163
164
    /**
165
     * Validate an MX record
166
     *
167
     * @param array $dnsRecord Given DNS record.
168
     *
169
     * @return bool True if valid.
170
     */
171 11
    private function validateMxRecord($dnsRecord) : bool
172
    {
173 11
        if (!isset($dnsRecord['type'])) {
174 1
            $this->error = new InvalidEmail(new ReasonNoDNSRecord(), '');
175 1
            return false;
176
        }
177
178 10
        if ($dnsRecord['type'] !== 'MX') {
179 10
            return true;
180
        }
181
182
        // "Null MX" record indicates the domain accepts no mail (https://tools.ietf.org/html/rfc7505)
183 10
        if (empty($dnsRecord['target']) || $dnsRecord['target'] === '.') {
184 1
            $this->error = new InvalidEmail(new DomainAcceptsNoMail(), "");
185 1
            return false;
186
        }
187
188 9
        $this->mxRecords[] = $dnsRecord;
189
190 9
        return true;
191
    }
192
}