diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-13 11:30:08 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-13 11:30:08 +0000 |
commit | 4ce65d59ca91871cfd126497158200a818720bce (patch) | |
tree | e277def01fc7eba7dbc21c4a4ae5576e8aa2cf1f /vendor/ipl/validator/src/EmailAddressValidator.php | |
parent | Initial commit. (diff) | |
download | icinga-php-library-upstream/0.13.1.tar.xz icinga-php-library-upstream/0.13.1.zip |
Adding upstream version 0.13.1.upstream/0.13.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'vendor/ipl/validator/src/EmailAddressValidator.php')
-rw-r--r-- | vendor/ipl/validator/src/EmailAddressValidator.php | 341 |
1 files changed, 341 insertions, 0 deletions
diff --git a/vendor/ipl/validator/src/EmailAddressValidator.php b/vendor/ipl/validator/src/EmailAddressValidator.php new file mode 100644 index 0000000..52c3697 --- /dev/null +++ b/vendor/ipl/validator/src/EmailAddressValidator.php @@ -0,0 +1,341 @@ +<?php + +namespace ipl\Validator; + +use Exception; +use ipl\I18n\Translation; + +/** + * Validates an email address + * + * Email Address syntax: (<local part>@<domain-literal part>) + * + * We currently do not support dot-atom syntax (refer RFC 2822 [https://www.ietf.org/rfc/rfc2822.txt] + * documentation for more details) for domain-literal part of an email address + * + */ +class EmailAddressValidator extends BaseValidator +{ + use Translation; + + /** + * If MX check should be enabled + * + * @var bool + */ + protected $mx = false; + + /** + * If a deep MX check should be enabled + * + * @var bool + */ + protected $deep = false; + + /** + * Create a new E-mail address validator with optional options + * + * Optional options: + * + * 'mx' => If an MX check should be enabled, boolean + * 'deep' => If a deep MX check should be enabled, boolean + * + * @param array $options + * + * @throws Exception + */ + public function __construct(array $options = []) + { + if (array_key_exists('mx', $options)) { + $this->setEnableMxCheck($options['mx']); + } + + if (array_key_exists('deep', $options)) { + $this->setEnableDeepMxCheck($options['deep']); + } + } + + /** + * Set MX check + * + * To validate if the hostname is a DNS mail exchange (MX) record set it to true + * + * @param bool $mx if MX check should be enabled + * + * @return $this + */ + public function setEnableMxCheck(bool $mx = true): self + { + $this->mx = $mx; + + return $this; + } + + /** + * Set Deep MX check + * + * To validate if the hostname is a DNS mail exchange (MX) record, and it points to an A record (for IPv4) or + * an AAAA / A6 record (for IPv6) set it to true + * + * @param bool $deep if deep MX check should be enabled + * + * @return $this + * + * @throws Exception in case MX check has not been enabled + */ + public function setEnableDeepMxCheck(bool $deep = true): self + { + if (! $this->mx) { + throw new Exception("MX record check has to be enabled to enable deep MX record check"); + } + + $this->deep = $deep; + + return $this; + } + + /** + * Validate the local part (username / the part before '@') of the email address + * + * @param string $localPart + * @param string $email + * + * @return bool + */ + private function validateLocalPart(string $localPart, string $email): bool + { + // First try to match the local part on the common dot-atom format + $result = false; + + // Dot-atom characters are: 1*atext *("." 1*atext) + // atext: ALPHA / DIGIT / and "!", "#", "$", "%", "&", "'", "*", + // "+", "-", "/", "=", "?", "^", "_", "`", "{", "|", "}", "~" + $atext = 'a-zA-Z0-9\x21\x23\x24\x25\x26\x27\x2a\x2b\x2d\x2f\x3d\x3f\x5e\x5f\x60\x7b\x7c\x7d\x7e'; + if (preg_match('/^[' . $atext . ']+(\x2e+[' . $atext . ']+)*$/', $localPart)) { + $result = true; + } else { + // Try quoted string format (RFC 5321 Chapter 4.1.2) + + // Quoted-string characters are: DQUOTE *(qtext/quoted-pair) DQUOTE + $qtext = '\x20-\x21\x23-\x5b\x5d-\x7e'; // %d32-33 / %d35-91 / %d93-126 + $quotedPair = '\x20-\x7e'; // %d92 %d32-126 + if (preg_match('/^"([' . $qtext . ']|\x5c[' . $quotedPair . '])*"$/', $localPart)) { + $result = true; + } else { + $this->addMessage(sprintf( + $this->translate( + "'%s' can not be matched against dot-atom format or quoted-string format" + ), + $localPart + )); + $this->addMessage(sprintf( + $this->translate("Hence '%s' is not a valid local part for email address '%s'"), + $localPart, + $email + )); + } + } + + return $result; + } + + /** + * Validate the hostname part of the email address + * + * @param string $hostname + * @param string $email + * + * @return bool + */ + private function validateHostnamePart(string $hostname, string $email): bool + { + $hostValidator = new HostnameValidator(); + + if ($this->validateIp($hostname)) { + return true; + } + + if (preg_match('/^\[([^\]]*)\]$/i', $hostname, $matches)) { + $validHostname = $matches[1]; + if (! $this->validateIp($validHostname)) { + $this->addMessage(sprintf( + $this->translate("host name %s is a domain literal and is invalid"), + $hostname + )); + + return false; + } + + return true; + } + + if (! $hostValidator->isValid($hostname)) { + $this->addMessage(sprintf( + $this->translate('%s is not a valid domain name for email address %s.'), + $hostname, + $email + )); + + return false; + } elseif ($this->mx) { + // MX check on hostname + return $this->validateMXRecords($hostname, $email); + } + + return true; + } + + /** + * Check if the given IP address is valid + * + * @param string $value + * + * @return bool + */ + private function validateIp(string $value): bool + { + if (! filter_var($value, FILTER_VALIDATE_IP)) { + return false; + } + + return true; + } + + /** + * Returns true if and only if $value is a valid email address + * according to RFC2822 + * + * @param string $value + * + * @return bool + */ + public function isValid($value): bool + { + $this->clearMessages(); + + $matches = []; + $length = true; + + // Split email address up and disallow '..' + if ( + (strpos($value, '..') !== false) + || (! preg_match('/^(.+)@([^@]+)$/', $value, $matches)) + ) { + $this->addMessage(sprintf( + $this->translate("'%s' is not a valid email address in the basic format local-part@hostname"), + $value + )); + return false; + } + + $localPart = $matches[1]; + $hostname = $matches[2]; + + if ((strlen($localPart) > 64) || (strlen($hostname) > 255)) { + $length = false; + $this->addMessage(sprintf( + $this->translate("'%s' exceeds the allowed length"), + $value + )); + } + + $local = $this->validateLocalPart($localPart, $value); + + // If both parts valid, return true + if (($local && $this->validateHostnamePart($hostname, $value)) && $length) { + return true; + } + + return false; + } + + /** + * Perform deep MX record validation + * + * Check if the hostname is a valid DNS mail exchange (MX) record in case deep MX record check is enabled, + * also checks if the corresponding MX record points to an A record (for IPv4) or an AAAA / A6 record (for IPv6) + * + * @param string $hostname + * @param string $email + * + * @return bool + */ + private function validateMXRecords(string $hostname, string $email): bool + { + $mxHosts = []; + //decode IDN domain name + $decodedHostname = idn_to_ascii($hostname, 0, INTL_IDNA_VARIANT_UTS46); + + $result = getmxrr($decodedHostname, $mxHosts); + if (! $result) { + $this->addMessage(sprintf( + $this->translate("'%s' does not appear to have a valid MX record for the email address '%s'"), + $hostname, + $email + )); + } elseif ($this->deep) { + $validAddress = false; + $reserved = true; + foreach ($mxHosts as $decodedHostname) { + $res = $this->isReserved($decodedHostname); + if (! $res) { + $reserved = false; + } + + if ( + ! $res + && ( + checkdnsrr($decodedHostname, "A") + || checkdnsrr($decodedHostname, "AAAA") + || checkdnsrr($decodedHostname, "A6") + ) + ) { + $validAddress = true; + break; + } + } + + if (! $validAddress) { + $result = false; + if ($reserved) { + $this->addMessage(sprintf( + $this->translate( + "'%s' is not in a routable network segment." . + " The email address '%s' should not be resolved from public network" + ), + $hostname, + $email + )); + } else { + $this->addMessage(sprintf( + $this->translate("'%s' does not appear to have a valid MX record for the email address '%s'"), + $hostname, + $email + )); + } + } + } + + return $result; + } + + /** + * Validate whether the given host is reserved + * + * @param string $host host name or ip address + * + * @return bool + */ + private function isReserved(string $host): bool + { + if (! preg_match('/^([0-9]{1,3}\.){3}[0-9]{1,3}$/', $host)) { + $host = gethostbyname($host); + } + + if (! filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_NO_RES_RANGE | FILTER_FLAG_NO_PRIV_RANGE)) { + return true; + } + + return false; + } +} |