1<?php
2
3namespace Egulias\EmailValidator\Validation;
4
5use Egulias\EmailValidator\EmailLexer;
6use Egulias\EmailValidator\Exception\InvalidEmail;
7use Egulias\EmailValidator\Exception\LocalOrReservedDomain;
8use Egulias\EmailValidator\Exception\DomainAcceptsNoMail;
9use Egulias\EmailValidator\Warning\NoDNSMXRecord;
10use Egulias\EmailValidator\Exception\NoDNSRecord;
11
12class 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)
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 LocalOrReservedDomain();
77            return false;
78        }
79
80        return $this->checkDns($host);
81    }
82
83    public function getError()
84    {
85        return $this->error;
86    }
87
88    public function getWarnings()
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)) {
126            $this->error = new NoDNSRecord();
127            return false;
128        }
129
130        // For each DNS record
131        foreach ($dnsRecords as $dnsRecord) {
132            if (!$this->validateMXRecord($dnsRecord)) {
133                return false;
134            }
135        }
136
137        // No MX records (fallback to A or AAAA records)
138        if (empty($this->mxRecords)) {
139            $this->warnings[NoDNSMXRecord::CODE] = new NoDNSMXRecord();
140        }
141
142        return true;
143    }
144
145    /**
146     * Validate an MX record
147     *
148     * @param array $dnsRecord Given DNS record.
149     *
150     * @return bool True if valid.
151     */
152    private function validateMxRecord($dnsRecord)
153    {
154        if ($dnsRecord['type'] !== 'MX') {
155            return true;
156        }
157
158        // "Null MX" record indicates the domain accepts no mail (https://tools.ietf.org/html/rfc7505)
159        if (empty($dnsRecord['target']) || $dnsRecord['target'] === '.') {
160            $this->error = new DomainAcceptsNoMail();
161            return false;
162        }
163
164        $this->mxRecords[] = $dnsRecord;
165
166        return true;
167    }
168}
169