I have started using Zend_Validate_EmailAddress
with the mx
and deep
options set to true. I think I am getting some false negatives when it comes to MX records that have IP addresses in the reserved IP ranges.
A good example is the MX records for harn.ufl.edu. It appear that it's failing because of IP addresses in the 128.0.0.0/16 range. It does however have one record that uses 8.6.245.30, which is not in the reserved range.
Another example is the MX record for martinhealth.org. It's MX record domain uses 198.136.38.2.
Is this a case of something that's technically incorrect but actually works?
As the comments on my post allude, there is a bug in Zend_Validate_EmailAddress::_isReserved
. Not only is it buggy, but it's difficult to understand the logic flow. It was a private
function, so I changed it to protected
so I could override it in my sub-class. There were also some incorrect ranges in the $_invalidIp
array.
For my logic check, I decided that the simplest (clearest?) way to compare IP addresses was to convert them to their decimal integer equivalents.
Here's my sub-class:
class My_Validate_EmailAddressDeep extends Zend_Validate_EmailAddress
{
/**
* @var array
*/
protected $_messageTemplates = array(
self::INVALID => "Invalid type given. String expected",
self::INVALID_FORMAT => "'%value%' is not a valid email address in the basic [user]@[hostname] format",
self::INVALID_HOSTNAME => "The '%hostname%' part of '%value%' is not a valid hostname",
self::INVALID_MX_RECORD => "'%hostname%' does not appear to be configured to accept email",
self::INVALID_SEGMENT => "'%hostname%' does not appear to be configured to accept external email",
self::DOT_ATOM => null,
self::QUOTED_STRING => null,
self::INVALID_LOCAL_PART => "The '%localPart%' part of '%value%' is not valid",
self::LENGTH_EXCEEDED => "'%value%' is longer than the allowed length for an email address",
);
/**
* Internal options array
* @var array
*/
protected $_options = array(
'allow' => Zend_Validate_Hostname::ALLOW_DNS,
'deep' => true,
'domain' => true,
'hostname' => null,
'mx' => true,
);
/**
* @see http://en.wikipedia.org/wiki/Reserved_IP_addresses#Reserved_IPv4_addresses
* @var array [first octet] => [[CIDR] => [[range start], [range end]]]
*/
protected $_reservedIps = array(
'0' => array('0.0.0.0/8' => array('0.0.0.0', '0.255.255.255',),),
'10' => array('10.0.0.0/8' => array('10.0.0.0', '10.255.255.255',),),
'127' => array('127.0.0.0/8' => array('127.0.0.0', '127.255.255.255',),),
'169' => array('169.254.0.0/16' => array('169.254.0.0', '169.254.255.255',),),
'172' => array('172.16.0.0/12' => array('172.16.0.0', '172.31.255.255',),),
'192' => array(
'192.0.2.0/24' => array('192.0.2.0', '192.0.2.255',),
'192.88.99.0/24' => array('192.88.99.0', '192.88.99.255',),
'192.168.0.0/16' => array('192.168.0.0', '192.168.255.255',),
),
'198' => array(
'198.18.0.0/15' => array('198.18.0.0', '198.19.255.255',),
'198.51.100.0/24' => array('198.51.100.0', '198.51.100.255',),
),
'203' => array('203.0.113.0/24' => array('203.0.113.0', '203.0.113.255',),),
'224' => array('224.0.0.0/4' => array('224.0.0.0', '239.255.255.255',),),
'240' => array('240.0.0.0/4' => array('240.0.0.0', '255.255.255.255',),),
);
/**
* Returns if the given host is reserved
*
* @param string $host
* @return boolean
*/
protected function _isReserved($host)
{
if (!preg_match('/^([0-9]{1,3}\.){3}[0-9]{1,3}$/', $host)) {
$host = gethostbyname($host);
}
$octets = explode('.', $host);
if (224 <= (int) $octets[0]) {
// IP Addresses beginning with 224 or greater are all reserved, short-circuit range checks
return true;
} elseif (array_key_exists($octets[0], $this->_reservedIps)) {
// for integer comparisons
$intIp = $this->_ipToInt($host);
// loop over reserved IP addresses
foreach ($this->_reservedIps as $ranges) {
foreach ($ranges as $range) {
if (($this->_ipToInt($range[0]) <= $intIp)
&& ($this->_ipToInt($range[1]) >= $intIp)) {
// the IP address falls in a reserved range
return true;
}
}
}
// the IP address did not fall in a reserved range
return false;
} else {
return false;
}
}
/**
* Convert a dot-decimal IP address to it's decimal integer equivalent
*
* @param string $ip
* @return integer
*/
protected function _ipToInt($ip)
{
$octets = explode('.', $ip);
foreach ($octets as $key => $octet) {
$octets[$key] = str_pad(decbin($octet), 8, '0', STR_PAD_LEFT);
}
$bin = implode('', $octets);
return bindec($bin);
}
}