File indexing completed on 2024-05-12 06:03:13

0001 <?php
0002 /**
0003  * Zend Framework
0004  *
0005  * LICENSE
0006  *
0007  * This source file is subject to the new BSD license that is bundled
0008  * with this package in the file LICENSE.txt.
0009  * It is also available through the world-wide-web at this URL:
0010  * http://framework.zend.com/license/new-bsd
0011  * If you did not receive a copy of the license and are unable to
0012  * obtain it through the world-wide-web, please send an email
0013  * to license@zend.com so we can send you a copy immediately.
0014  *
0015  * @category   Zend
0016  * @package    Zend_Validate
0017  * @copyright  Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
0018  * @license    http://framework.zend.com/license/new-bsd     New BSD License
0019  * @version    $Id$
0020  */
0021 
0022 /**
0023  * @see Zend_Validate_Abstract
0024  */
0025 // require_once 'Zend/Validate/Abstract.php';
0026 
0027 /**
0028  * @see Zend_Validate_Hostname
0029  */
0030 // require_once 'Zend/Validate/Hostname.php';
0031 
0032 /**
0033  * @category   Zend
0034  * @package    Zend_Validate
0035  * @copyright  Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
0036  * @license    http://framework.zend.com/license/new-bsd     New BSD License
0037  */
0038 class Zend_Validate_EmailAddress extends Zend_Validate_Abstract
0039 {
0040     const INVALID            = 'emailAddressInvalid';
0041     const INVALID_FORMAT     = 'emailAddressInvalidFormat';
0042     const INVALID_HOSTNAME   = 'emailAddressInvalidHostname';
0043     const INVALID_MX_RECORD  = 'emailAddressInvalidMxRecord';
0044     const INVALID_SEGMENT    = 'emailAddressInvalidSegment';
0045     const DOT_ATOM           = 'emailAddressDotAtom';
0046     const QUOTED_STRING      = 'emailAddressQuotedString';
0047     const INVALID_LOCAL_PART = 'emailAddressInvalidLocalPart';
0048     const LENGTH_EXCEEDED    = 'emailAddressLengthExceeded';
0049 
0050     /**
0051      * @var array
0052      */
0053     protected $_messageTemplates = array(
0054         self::INVALID            => "Invalid type given. String expected",
0055         self::INVALID_FORMAT     => "'%value%' is not a valid email address in the basic format local-part@hostname",
0056         self::INVALID_HOSTNAME   => "'%hostname%' is not a valid hostname for email address '%value%'",
0057         self::INVALID_MX_RECORD  => "'%hostname%' does not appear to have a valid MX record for the email address '%value%'",
0058         self::INVALID_SEGMENT    => "'%hostname%' is not in a routable network segment. The email address '%value%' should not be resolved from public network",
0059         self::DOT_ATOM           => "'%localPart%' can not be matched against dot-atom format",
0060         self::QUOTED_STRING      => "'%localPart%' can not be matched against quoted-string format",
0061         self::INVALID_LOCAL_PART => "'%localPart%' is not a valid local part for email address '%value%'",
0062         self::LENGTH_EXCEEDED    => "'%value%' exceeds the allowed length",
0063     );
0064 
0065     /**
0066      * As of RFC5753 (JAN 2010), the following blocks are no longer reserved:
0067      *   - 128.0.0.0/16
0068      *   - 191.255.0.0/16
0069      *   - 223.255.255.0/24
0070      * @see http://tools.ietf.org/html/rfc5735#page-6
0071      *
0072      * As of RFC6598 (APR 2012), the following blocks are now reserved:
0073      *   - 100.64.0.0/10
0074      * @see http://tools.ietf.org/html/rfc6598#section-7
0075      *
0076      * @see http://en.wikipedia.org/wiki/IPv4
0077      * @var array
0078      */
0079     protected $_invalidIp = array(
0080         '0'   => '0.0.0.0/8',
0081         '10'  => '10.0.0.0/8',
0082         '100' => '100.64.0.0/10',
0083         '127' => '127.0.0.0/8',
0084         '169' => '169.254.0.0/16',
0085         '172' => '172.16.0.0/12',
0086         '192' => array(
0087             '192.0.0.0/24',
0088             '192.0.2.0/24',
0089             '192.88.99.0/24',
0090             '192.168.0.0/16'
0091         ),
0092         '198' => '198.18.0.0/15',
0093         '224' => '224.0.0.0/4',
0094         '240' => '240.0.0.0/4'
0095     );
0096 
0097     /**
0098      * @var array
0099      */
0100     protected $_messageVariables = array(
0101         'hostname'  => '_hostname',
0102         'localPart' => '_localPart'
0103     );
0104 
0105     /**
0106      * @var string
0107      */
0108     protected $_hostname;
0109 
0110     /**
0111      * @var string
0112      */
0113     protected $_localPart;
0114 
0115     /**
0116      * Internal options array
0117      */
0118     protected $_options = array(
0119         'mx'       => false,
0120         'deep'     => false,
0121         'domain'   => true,
0122         'allow'    => Zend_Validate_Hostname::ALLOW_DNS,
0123         'hostname' => null
0124     );
0125 
0126     /**
0127      * Instantiates hostname validator for local use
0128      *
0129      * The following option keys are supported:
0130      * 'hostname' => A hostname validator, see Zend_Validate_Hostname
0131      * 'allow'    => Options for the hostname validator, see Zend_Validate_Hostname::ALLOW_*
0132      * 'mx'       => If MX check should be enabled, boolean
0133      * 'deep'     => If a deep MX check should be done, boolean
0134      *
0135      * @param array|string|Zend_Config $options OPTIONAL
0136      */
0137     public function __construct($options = array())
0138     {
0139         if ($options instanceof Zend_Config) {
0140             $options = $options->toArray();
0141         } else if (!is_array($options)) {
0142             $options = func_get_args();
0143             $temp['allow'] = array_shift($options);
0144             if (!empty($options)) {
0145                 $temp['mx'] = array_shift($options);
0146             }
0147 
0148             if (!empty($options)) {
0149                 $temp['hostname'] = array_shift($options);
0150             }
0151 
0152             $options = $temp;
0153         }
0154 
0155         $options += $this->_options;
0156         $this->setOptions($options);
0157     }
0158 
0159     /**
0160      * Returns all set Options
0161      *
0162      * @return array
0163      */
0164     public function getOptions()
0165     {
0166         return $this->_options;
0167     }
0168 
0169     /**
0170      * Set options for the email validator
0171      *
0172      * @param array $options
0173      * @return Zend_Validate_EmailAddress Provides a fluent inteface
0174      */
0175     public function setOptions(array $options = array())
0176     {
0177         if (array_key_exists('messages', $options)) {
0178             $this->setMessages($options['messages']);
0179         }
0180 
0181         if (array_key_exists('hostname', $options)) {
0182             if (array_key_exists('allow', $options)) {
0183                 $this->setHostnameValidator($options['hostname'], $options['allow']);
0184             } else {
0185                 $this->setHostnameValidator($options['hostname']);
0186             }
0187         } elseif ($this->_options['hostname'] == null) {
0188             $this->setHostnameValidator();
0189         }
0190 
0191         if (array_key_exists('mx', $options)) {
0192             $this->setValidateMx($options['mx']);
0193         }
0194 
0195         if (array_key_exists('deep', $options)) {
0196             $this->setDeepMxCheck($options['deep']);
0197         }
0198 
0199         if (array_key_exists('domain', $options)) {
0200             $this->setDomainCheck($options['domain']);
0201         }
0202 
0203         return $this;
0204     }
0205 
0206     /**
0207      * Sets the validation failure message template for a particular key
0208      * Adds the ability to set messages to the attached hostname validator
0209      *
0210      * @param  string $messageString
0211      * @param  string $messageKey     OPTIONAL
0212      * @return Zend_Validate_Abstract Provides a fluent interface
0213      * @throws Zend_Validate_Exception
0214      */
0215     public function setMessage($messageString, $messageKey = null)
0216     {
0217         if ($messageKey === null) {
0218             $this->_options['hostname']->setMessage($messageString);
0219             parent::setMessage($messageString);
0220             return $this;
0221         }
0222 
0223         if (!isset($this->_messageTemplates[$messageKey])) {
0224             $this->_options['hostname']->setMessage($messageString, $messageKey);
0225         }
0226 
0227         $this->_messageTemplates[$messageKey] = $messageString;
0228         return $this;
0229     }
0230 
0231     /**
0232      * Returns the set hostname validator
0233      *
0234      * @return Zend_Validate_Hostname
0235      */
0236     public function getHostnameValidator()
0237     {
0238         return $this->_options['hostname'];
0239     }
0240 
0241     /**
0242      * @param Zend_Validate_Hostname $hostnameValidator OPTIONAL
0243      * @param int                    $allow             OPTIONAL
0244      * @return $this
0245      */
0246     public function setHostnameValidator(Zend_Validate_Hostname $hostnameValidator = null, $allow = Zend_Validate_Hostname::ALLOW_DNS)
0247     {
0248         if (!$hostnameValidator) {
0249             $hostnameValidator = new Zend_Validate_Hostname($allow);
0250         }
0251 
0252         $this->_options['hostname'] = $hostnameValidator;
0253         $this->_options['allow']    = $allow;
0254         return $this;
0255     }
0256 
0257     /**
0258      * Whether MX checking via getmxrr is supported or not
0259      *
0260      * This currently only works on UNIX systems
0261      *
0262      * @return boolean
0263      */
0264     public function validateMxSupported()
0265     {
0266         return function_exists('getmxrr');
0267     }
0268 
0269     /**
0270      * Returns the set validateMx option
0271      *
0272      * @return boolean
0273      */
0274     public function getValidateMx()
0275     {
0276         return $this->_options['mx'];
0277     }
0278 
0279     /**
0280      * Set whether we check for a valid MX record via DNS
0281      *
0282      * This only applies when DNS hostnames are validated
0283      *
0284      * @param boolean $mx Set allowed to true to validate for MX records, and false to not validate them
0285      * @throws Zend_Validate_Exception
0286      * @return Zend_Validate_EmailAddress Provides a fluent inteface
0287      */
0288     public function setValidateMx($mx)
0289     {
0290         if ((bool) $mx && !$this->validateMxSupported()) {
0291             // require_once 'Zend/Validate/Exception.php';
0292             throw new Zend_Validate_Exception('MX checking not available on this system');
0293         }
0294 
0295         $this->_options['mx'] = (bool) $mx;
0296         return $this;
0297     }
0298 
0299     /**
0300      * Returns the set deepMxCheck option
0301      *
0302      * @return boolean
0303      */
0304     public function getDeepMxCheck()
0305     {
0306         return $this->_options['deep'];
0307     }
0308 
0309     /**
0310      * Set whether we check MX record should be a deep validation
0311      *
0312      * @param boolean $deep Set deep to true to perform a deep validation process for MX records
0313      * @return Zend_Validate_EmailAddress Provides a fluent inteface
0314      */
0315     public function setDeepMxCheck($deep)
0316     {
0317         $this->_options['deep'] = (bool) $deep;
0318         return $this;
0319     }
0320 
0321     /**
0322      * Returns the set domainCheck option
0323      *
0324      * @return unknown
0325      */
0326     public function getDomainCheck()
0327     {
0328         return $this->_options['domain'];
0329     }
0330 
0331     /**
0332      * Sets if the domain should also be checked
0333      * or only the local part of the email address
0334      *
0335      * @param boolean $domain
0336      * @return Zend_Validate_EmailAddress Provides a fluent inteface
0337      */
0338     public function setDomainCheck($domain = true)
0339     {
0340         $this->_options['domain'] = (boolean) $domain;
0341         return $this;
0342     }
0343 
0344     /**
0345      * Returns if the given host is reserved
0346      *
0347      * @param string $host
0348      * @return boolean
0349      */
0350     private function _isReserved($host){
0351         if (!preg_match('/^([0-9]{1,3}\.){3}[0-9]{1,3}$/', $host)) {
0352             $host = gethostbyname($host);
0353         }
0354 
0355         $octet = explode('.',$host);
0356         if ((int)$octet[0] >= 224) {
0357             return true;
0358         } else if (array_key_exists($octet[0], $this->_invalidIp)) {
0359             foreach ((array)$this->_invalidIp[$octet[0]] as $subnetData) {
0360                 // we skip the first loop as we already know that octet matches
0361                 for ($i = 1; $i < 4; $i++) {
0362                     if (strpos($subnetData, $octet[$i]) !== $i * 4) {
0363                         break;
0364                     }
0365                 }
0366 
0367                 $host       = explode("/", $subnetData);
0368                 $binaryHost = "";
0369                 $tmp        = explode(".", $host[0]);
0370                 for ($i = 0; $i < 4 ; $i++) {
0371                     $binaryHost .= str_pad(decbin($tmp[$i]), 8, "0", STR_PAD_LEFT);
0372                 }
0373 
0374                 $segmentData = array(
0375                     'network'   => (int)$this->_toIp(str_pad(substr($binaryHost, 0, $host[1]), 32, 0)),
0376                     'broadcast' => (int)$this->_toIp(str_pad(substr($binaryHost, 0, $host[1]), 32, 1))
0377                 );
0378 
0379                 for ($j = $i; $j < 4; $j++) {
0380                     if ((int)$octet[$j] < $segmentData['network'][$j] ||
0381                         (int)$octet[$j] > $segmentData['broadcast'][$j]) {
0382                         return false;
0383                     }
0384                 }
0385             }
0386 
0387             return true;
0388         } else {
0389             return false;
0390         }
0391     }
0392 
0393     /**
0394      * Converts a binary string to an IP address
0395      *
0396      * @param string $binary
0397      * @return mixed
0398      */
0399     private function _toIp($binary)
0400     {
0401         $ip  = array();
0402         $tmp = explode(".", chunk_split($binary, 8, "."));
0403         for ($i = 0; $i < 4 ; $i++) {
0404             $ip[$i] = bindec($tmp[$i]);
0405         }
0406 
0407         return $ip;
0408     }
0409 
0410     /**
0411      * Internal method to validate the local part of the email address
0412      *
0413      * @return boolean
0414      */
0415     private function _validateLocalPart()
0416     {
0417         // First try to match the local part on the common dot-atom format
0418         $result = false;
0419 
0420         // Dot-atom characters are: 1*atext *("." 1*atext)
0421         // atext: ALPHA / DIGIT / and "!", "#", "$", "%", "&", "'", "*",
0422         //        "+", "-", "/", "=", "?", "^", "_", "`", "{", "|", "}", "~"
0423         $atext = 'a-zA-Z0-9\x21\x23\x24\x25\x26\x27\x2a\x2b\x2d\x2f\x3d\x3f\x5e\x5f\x60\x7b\x7c\x7d\x7e';
0424         if (preg_match('/^[' . $atext . ']+(\x2e+[' . $atext . ']+)*$/', $this->_localPart)) {
0425             $result = true;
0426         } else {
0427             // Try quoted string format (RFC 5321 Chapter 4.1.2)
0428 
0429             // Quoted-string characters are: DQUOTE *(qtext/quoted-pair) DQUOTE
0430             $qtext      = '\x20-\x21\x23-\x5b\x5d-\x7e'; // %d32-33 / %d35-91 / %d93-126
0431             $quotedPair = '\x20-\x7e'; // %d92 %d32-126
0432             if (preg_match('/^"(['. $qtext .']|\x5c[' . $quotedPair . '])*"$/', $this->localPart)) {
0433                 $result = true;
0434             } else {
0435                 $this->_error(self::DOT_ATOM);
0436                 $this->_error(self::QUOTED_STRING);
0437                 $this->_error(self::INVALID_LOCAL_PART);
0438             }
0439         }
0440 
0441         return $result;
0442     }
0443 
0444     /**
0445      * Internal method to validate the servers MX records
0446      *
0447      * @return boolean
0448      */
0449     private function _validateMXRecords()
0450     {
0451         $mxHosts = array();
0452         $hostname = $this->_hostname;
0453 
0454         //decode IDN domain name if possible
0455         if (function_exists('idn_to_ascii')) {
0456             $hostname = idn_to_ascii($this->_hostname);
0457         }
0458 
0459         $result = getmxrr($hostname, $mxHosts);
0460         if (!$result) {
0461             $this->_error(self::INVALID_MX_RECORD);
0462         } else if ($this->_options['deep'] && function_exists('checkdnsrr')) {
0463             $validAddress = false;
0464             $reserved     = true;
0465             foreach ($mxHosts as $hostname) {
0466                 $res = $this->_isReserved($hostname);
0467                 if (!$res) {
0468                     $reserved = false;
0469                 }
0470 
0471                 if (!$res
0472                     && (checkdnsrr($hostname, "A")
0473                     || checkdnsrr($hostname, "AAAA")
0474                     || checkdnsrr($hostname, "A6"))) {
0475                     $validAddress = true;
0476                     break;
0477                 }
0478             }
0479 
0480             if (!$validAddress) {
0481                 $result = false;
0482                 if ($reserved) {
0483                     $this->_error(self::INVALID_SEGMENT);
0484                 } else {
0485                     $this->_error(self::INVALID_MX_RECORD);
0486                 }
0487             }
0488         }
0489 
0490         return $result;
0491     }
0492 
0493     /**
0494      * Internal method to validate the hostname part of the email address
0495      *
0496      * @return boolean
0497      */
0498     private function _validateHostnamePart()
0499     {
0500         $hostname = $this->_options['hostname']->setTranslator($this->getTranslator())
0501                          ->isValid($this->_hostname);
0502         if (!$hostname) {
0503             $this->_error(self::INVALID_HOSTNAME);
0504 
0505             // Get messages and errors from hostnameValidator
0506             foreach ($this->_options['hostname']->getMessages() as $code => $message) {
0507                 $this->_messages[$code] = $message;
0508             }
0509 
0510             foreach ($this->_options['hostname']->getErrors() as $error) {
0511                 $this->_errors[] = $error;
0512             }
0513         } else if ($this->_options['mx']) {
0514             // MX check on hostname
0515             $hostname = $this->_validateMXRecords();
0516         }
0517 
0518         return $hostname;
0519     }
0520 
0521     /**
0522      * Defined by Zend_Validate_Interface
0523      *
0524      * Returns true if and only if $value is a valid email address
0525      * according to RFC2822
0526      *
0527      * @link   http://www.ietf.org/rfc/rfc2822.txt RFC2822
0528      * @link   http://www.columbia.edu/kermit/ascii.html US-ASCII characters
0529      * @param  string $value
0530      * @return boolean
0531      */
0532     public function isValid($value)
0533     {
0534         if (!is_string($value)) {
0535             $this->_error(self::INVALID);
0536             return false;
0537         }
0538 
0539         $matches = array();
0540         $length  = true;
0541         $this->_setValue($value);
0542 
0543         // Split email address up and disallow '..'
0544         if ((strpos($value, '..') !== false) or
0545             (!preg_match('/^(.+)@([^@]+)$/', $value, $matches))) {
0546             $this->_error(self::INVALID_FORMAT);
0547             return false;
0548         }
0549 
0550         $this->_localPart = $matches[1];
0551         $this->_hostname  = $matches[2];
0552 
0553         if ((strlen($this->_localPart) > 64) || (strlen($this->_hostname) > 255)) {
0554             $length = false;
0555             $this->_error(self::LENGTH_EXCEEDED);
0556         }
0557 
0558         // Match hostname part
0559         if ($this->_options['domain']) {
0560             $hostname = $this->_validateHostnamePart();
0561         }
0562 
0563         $local = $this->_validateLocalPart();
0564 
0565         // If both parts valid, return true
0566         if ($local && $length) {
0567             if (($this->_options['domain'] && $hostname) || !$this->_options['domain']) {
0568                 return true;
0569             }
0570         }
0571 
0572         return false;
0573     }
0574 }