Passed
Push — message-id-validator ( ab2232...8edbe4 )
by Eduardo Gulias
04:06 queued 02:03
created

DNSCheckValidation   A

Complexity

Total Complexity 20

Size/Duplication

Total Lines 172
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 6

Test Coverage

Coverage 86.27%

Importance

Changes 0
Metric Value
wmc 20
lcom 1
cbo 6
dl 0
loc 172
ccs 44
cts 51
cp 0.8627
rs 10
c 0
b 0
f 0

7 Methods

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