<?php

/**
	 * This is a forked library modified for use in Formea extension,
	 * Credit goes to original author(s) Eduardo Gulias Davis at https://github.com/egulias/EmailValidator.
	 * See LICENSE
	 *
	 * @version     1.0.0
	 * @package     Formea Email Validation
	 * @subpackage  Lexer
	 * @author      Feseur Sdn Bhd
	 * @copyright   Copyright (C) 2010-2023 Feseur Sdn Bhd. All rights reserved.
	 * @license     GNU General Public License version 2 or later; see LICENSE.txt
	 */

	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 LogicException;

	class DNSCheckValidation implements EmailValidation
	{
		/**
		 * @var int
		 */
		protected const DNS_RECORD_TYPES_TO_CHECK = DNS_MX + DNS_A + DNS_AAAA;

		/**
		 * 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)
		 */
		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 array
		 */
		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;
		}

		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), '.') . '.';

			return $this->validateDnsRecords($host);
		}


		/**
		 * 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, static::DNS_RECORD_TYPES_TO_CHECK);

			if ($dnsRecordsResult->withError())
			{
				$this->error = new InvalidEmail(new UnableToGetDNSRecord(), '');

				return false;
			}

			$dnsRecords = $dnsRecordsResult->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;
		}
	}
