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