Test Setup Failed
Push — 3.0.0-dev ( e2e234...b76c11 )
by Eduardo Gulias
01:56
created

DNSCheckValidation::validateDnsRecords()   B

Complexity

Conditions 6
Paths 5

Size

Total Lines 24

Duplication

Lines 0
Ratio 0 %

Importance

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