Test Failed
Pull Request — 2.1.x (#287)
by Alies
10:45
created

DNSCheckValidation::validateDnsRecords()   B

Complexity

Conditions 6
Paths 22

Size

Total Lines 41

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 6.0493

Importance

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