Passed
Pull Request — 3.x (#339)
by
unknown
01:57
created

DNSCheckValidation::validateDnsRecords()   A

Complexity

Conditions 6
Paths 6

Size

Total Lines 28
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 6.0852

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 6
eloc 14
c 2
b 0
f 0
nc 6
nop 1
dl 0
loc 28
ccs 13
cts 15
cp 0.8667
crap 6.0852
rs 9.2222
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
    public 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
    /**
59
     * @var DNSGetRecordWrapper
60
     */
61
    private $dnsGetRecord;
62
63 24
    public function __construct(?DNSGetRecordWrapper $dnsGetRecord = null)
64
    {
65 24
        if (!function_exists('idn_to_ascii')) {
66
            throw new \LogicException(sprintf('The %s class requires the Intl extension.', __CLASS__));
67
        }
68
69 24
        if ($dnsGetRecord == null) {
70 23
            $dnsGetRecord = new DNSGetRecordWrapper();
71
        }
72
73 24
        $this->dnsGetRecord = $dnsGetRecord;
74
    }
75
76 24
    public function isValid(string $email, EmailLexer $emailLexer): bool
77
    {
78
        // use the input to check DNS if we cannot extract something similar to a domain
79 24
        $host = $email;
80
81
        // Arguable pattern to extract the domain. Not aiming to validate the domain nor the email
82 24
        if (false !== $lastAtPos = strrpos($email, '@')) {
83 12
            $host = substr($email, $lastAtPos + 1);
84
        }
85
86
        // Get the domain parts
87 24
        $hostParts = explode('.', $host);
88
89 24
        $isLocalDomain = count($hostParts) <= 1;
90 24
        $isReservedTopLevel = in_array($hostParts[(count($hostParts) - 1)], self::RESERVED_DNS_TOP_LEVEL_NAMES, true);
91
92
        // Exclude reserved top level DNS names
93 24
        if ($isLocalDomain || $isReservedTopLevel) {
94 11
            $this->error = new InvalidEmail(new LocalOrReservedDomain(), $host);
95 11
            return false;
96
        }
97
98 13
        return $this->checkDns($host);
99
    }
100
101 14
    public function getError(): ?InvalidEmail
102
    {
103 14
        return $this->error;
104
    }
105
106
    public function getWarnings(): array
107
    {
108
        return $this->warnings;
109
    }
110
111
    /**
112
     * @param string $host
113
     *
114
     * @return bool
115
     */
116 13
    protected function checkDns($host)
117
    {
118 13
        $variant = INTL_IDNA_VARIANT_UTS46;
119
120 13
        $host = rtrim(idn_to_ascii($host, IDNA_DEFAULT, $variant), '.') . '.';
121
122 13
        return $this->validateDnsRecords($host);
123
    }
124
125
126
    /**
127
     * Validate the DNS records for given host.
128
     *
129
     * @param string $host A set of DNS records in the format returned by dns_get_record.
130
     *
131
     * @return bool True on success.
132
     */
133 13
    private function validateDnsRecords($host): bool
134
    {
135 13
        $dnsRecordsResult = $this->dnsGetRecord->getRecords($host, static::DNS_RECORD_TYPES_TO_CHECK);
136
137 13
        if ($dnsRecordsResult->withError()) {
138
            $this->error = new InvalidEmail(new UnableToGetDNSRecord(), '');
139
            return false;
140
        }
141
142 13
        $dnsRecords = $dnsRecordsResult->getRecords();
143
144
        // No MX, A or AAAA DNS records
145 13
        if ($dnsRecords === []) {
146 2
            $this->error = new InvalidEmail(new ReasonNoDNSRecord(), '');
147 2
            return false;
148
        }
149
150
        // For each DNS record
151 11
        foreach ($dnsRecords as $dnsRecord) {
152 11
            if (!$this->validateMXRecord($dnsRecord)) {
153
                // No MX records (fallback to A or AAAA records)
154 2
                if (empty($this->mxRecords)) {
155 2
                    $this->warnings[NoDNSMXRecord::CODE] = new NoDNSMXRecord();
156
                }
157 2
                return false;
158
            }
159
        }
160 9
        return true;
161
    }
162
163
    /**
164
     * Validate an MX record
165
     *
166
     * @param array $dnsRecord Given DNS record.
167
     *
168
     * @return bool True if valid.
169
     */
170 11
    private function validateMxRecord($dnsRecord): bool
171
    {
172 11
        if (!isset($dnsRecord['type'])) {
173 1
            $this->error = new InvalidEmail(new ReasonNoDNSRecord(), '');
174 1
            return false;
175
        }
176
177 10
        if ($dnsRecord['type'] !== 'MX') {
178 10
            return true;
179
        }
180
181
        // "Null MX" record indicates the domain accepts no mail (https://tools.ietf.org/html/rfc7505)
182 10
        if (empty($dnsRecord['target']) || $dnsRecord['target'] === '.') {
183 1
            $this->error = new InvalidEmail(new DomainAcceptsNoMail(), "");
184 1
            return false;
185
        }
186
187 9
        $this->mxRecords[] = $dnsRecord;
188
189 9
        return true;
190
    }
191
}
192