DNSCheckValidation.php 5.7 KB

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