| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211 |
- <?php
- namespace Egulias\EmailValidator\Validation;
- use Egulias\EmailValidator\EmailLexer;
- use Egulias\EmailValidator\Result\InvalidEmail;
- use Egulias\EmailValidator\Result\Reason\DomainAcceptsNoMail;
- use Egulias\EmailValidator\Result\Reason\LocalOrReservedDomain;
- use Egulias\EmailValidator\Result\Reason\NoDNSRecord as ReasonNoDNSRecord;
- use Egulias\EmailValidator\Result\Reason\UnableToGetDNSRecord;
- use Egulias\EmailValidator\Warning\NoDNSMXRecord;
- use Egulias\EmailValidator\Warning\Warning;
- class DNSCheckValidation implements EmailValidation
- {
- /**
- * Reserved Top Level DNS Names (https://tools.ietf.org/html/rfc2606#section-2),
- * mDNS and private DNS Namespaces (https://tools.ietf.org/html/rfc6762#appendix-G)
- *
- * @var string[]
- */
- public const RESERVED_DNS_TOP_LEVEL_NAMES = [
- // Reserved Top Level DNS Names
- 'test',
- 'example',
- 'invalid',
- 'localhost',
- // mDNS
- 'local',
- // Private DNS Namespaces
- 'intranet',
- 'internal',
- 'private',
- 'corp',
- 'home',
- 'lan',
- ];
- /**
- * @var Warning[]
- */
- private $warnings = [];
- /**
- * @var InvalidEmail|null
- */
- private $error;
- /**
- * @var array
- */
- private $mxRecords = [];
- /**
- * @var DNSGetRecordWrapper
- */
- private $dnsGetRecord;
- public function __construct(?DNSGetRecordWrapper $dnsGetRecord = null)
- {
- if (!function_exists('idn_to_ascii')) {
- throw new \LogicException(sprintf('The %s class requires the Intl extension.', __CLASS__));
- }
- if ($dnsGetRecord == null) {
- $dnsGetRecord = new DNSGetRecordWrapper();
- }
- $this->dnsGetRecord = $dnsGetRecord;
- }
- public function isValid(string $email, EmailLexer $emailLexer): bool
- {
- // use the input to check DNS if we cannot extract something similar to a domain
- $host = $email;
- // Arguable pattern to extract the domain. Not aiming to validate the domain nor the email
- if (false !== $lastAtPos = strrpos($email, '@')) {
- $host = substr($email, $lastAtPos + 1);
- }
- // Get the domain parts
- $hostParts = explode('.', $host);
- $isLocalDomain = count($hostParts) <= 1;
- $isReservedTopLevel = in_array($hostParts[(count($hostParts) - 1)], self::RESERVED_DNS_TOP_LEVEL_NAMES, true);
- // Exclude reserved top level DNS names
- if ($isLocalDomain || $isReservedTopLevel) {
- $this->error = new InvalidEmail(new LocalOrReservedDomain(), $host);
- return false;
- }
- return $this->checkDns($host);
- }
- public function getError(): ?InvalidEmail
- {
- return $this->error;
- }
- /**
- * @return Warning[]
- */
- public function getWarnings(): array
- {
- return $this->warnings;
- }
- /**
- * @param string $host
- *
- * @return bool
- */
- protected function checkDns($host)
- {
- $variant = INTL_IDNA_VARIANT_UTS46;
- $host = rtrim(idn_to_ascii($host, IDNA_DEFAULT, $variant), '.');
- $hostParts = explode('.', $host);
- $host = array_pop($hostParts);
- while (count($hostParts) > 0) {
- $host = array_pop($hostParts) . '.' . $host;
- if ($this->validateDnsRecords($host)) {
- return true;
- }
- }
- return false;
- }
- /**
- * Validate the DNS records for given host.
- *
- * @param string $host A set of DNS records in the format returned by dns_get_record.
- *
- * @return bool True on success.
- */
- private function validateDnsRecords($host): bool
- {
- $dnsRecordsResult = $this->dnsGetRecord->getRecords($host, DNS_A + DNS_MX);
- if ($dnsRecordsResult->withError()) {
- $this->error = new InvalidEmail(new UnableToGetDNSRecord(), '');
- return false;
- }
- $dnsRecords = $dnsRecordsResult->getRecords();
- // Combined check for A+MX+AAAA can fail with SERVFAIL, even in the presence of valid A/MX records
- $aaaaRecordsResult = $this->dnsGetRecord->getRecords($host, DNS_AAAA);
- if (! $aaaaRecordsResult->withError()) {
- $dnsRecords = array_merge($dnsRecords, $aaaaRecordsResult->getRecords());
- }
- // No MX, A or AAAA DNS records
- if ($dnsRecords === []) {
- $this->error = new InvalidEmail(new ReasonNoDNSRecord(), '');
- return false;
- }
- // For each DNS record
- foreach ($dnsRecords as $dnsRecord) {
- if (!$this->validateMXRecord($dnsRecord)) {
- // No MX records (fallback to A or AAAA records)
- if (empty($this->mxRecords)) {
- $this->warnings[NoDNSMXRecord::CODE] = new NoDNSMXRecord();
- }
- return false;
- }
- }
- return true;
- }
- /**
- * Validate an MX record
- *
- * @param array $dnsRecord Given DNS record.
- *
- * @return bool True if valid.
- */
- private function validateMxRecord($dnsRecord): bool
- {
- if (!isset($dnsRecord['type'])) {
- $this->error = new InvalidEmail(new ReasonNoDNSRecord(), '');
- return false;
- }
- if ($dnsRecord['type'] !== 'MX') {
- return true;
- }
- // "Null MX" record indicates the domain accepts no mail (https://tools.ietf.org/html/rfc7505)
- if (empty($dnsRecord['target']) || $dnsRecord['target'] === '.') {
- $this->error = new InvalidEmail(new DomainAcceptsNoMail(), "");
- return false;
- }
- $this->mxRecords[] = $dnsRecord;
- return true;
- }
- }
|