DNSCheckValidation::validateMxRecord()   A
last analyzed

Complexity

Conditions 5
Paths 4

Size

Total Lines 20
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 5

Importance

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