Passed
Pull Request — 3.x (#346)
by Alexander M.
02:30
created

DNSCheckValidation::validateDnsRecords()   B

Complexity

Conditions 7
Paths 24

Size

Total Lines 38
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 7.392

Importance

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