2
0

EmailAddress.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565
  1. <?php
  2. /**
  3. * Zend Framework
  4. *
  5. * LICENSE
  6. *
  7. * This source file is subject to the new BSD license that is bundled
  8. * with this package in the file LICENSE.txt.
  9. * It is also available through the world-wide-web at this URL:
  10. * http://framework.zend.com/license/new-bsd
  11. * If you did not receive a copy of the license and are unable to
  12. * obtain it through the world-wide-web, please send an email
  13. * to license@zend.com so we can send you a copy immediately.
  14. *
  15. * @category Zend
  16. * @package Zend_Validate
  17. * @copyright Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
  18. * @license http://framework.zend.com/license/new-bsd New BSD License
  19. * @version $Id$
  20. */
  21. /**
  22. * @see Zend_Validate_Abstract
  23. */
  24. require_once 'Zend/Validate/Abstract.php';
  25. /**
  26. * @see Zend_Validate_Hostname
  27. */
  28. require_once 'Zend/Validate/Hostname.php';
  29. /**
  30. * @category Zend
  31. * @package Zend_Validate
  32. * @copyright Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
  33. * @license http://framework.zend.com/license/new-bsd New BSD License
  34. */
  35. class Zend_Validate_EmailAddress extends Zend_Validate_Abstract
  36. {
  37. const INVALID = 'emailAddressInvalid';
  38. const INVALID_FORMAT = 'emailAddressInvalidFormat';
  39. const INVALID_HOSTNAME = 'emailAddressInvalidHostname';
  40. const INVALID_MX_RECORD = 'emailAddressInvalidMxRecord';
  41. const INVALID_SEGMENT = 'emailAddressInvalidSegment';
  42. const DOT_ATOM = 'emailAddressDotAtom';
  43. const QUOTED_STRING = 'emailAddressQuotedString';
  44. const INVALID_LOCAL_PART = 'emailAddressInvalidLocalPart';
  45. const LENGTH_EXCEEDED = 'emailAddressLengthExceeded';
  46. /**
  47. * @var array
  48. */
  49. protected $_messageTemplates = array(
  50. self::INVALID => "Invalid type given, value should be a string",
  51. self::INVALID_FORMAT => "'%value%' is not a valid email address in the basic format local-part@hostname",
  52. self::INVALID_HOSTNAME => "'%hostname%' is not a valid hostname for email address '%value%'",
  53. self::INVALID_MX_RECORD => "'%hostname%' does not appear to have a valid MX record for the email address '%value%'",
  54. self::INVALID_SEGMENT => "'%hostname%' is not in a routable network segment. The email address '%value%' should not be resolved from public network.",
  55. self::DOT_ATOM => "'%localPart%' not matched against dot-atom format",
  56. self::QUOTED_STRING => "'%localPart%' not matched against quoted-string format",
  57. self::INVALID_LOCAL_PART => "'%localPart%' is not a valid local part for email address '%value%'",
  58. self::LENGTH_EXCEEDED => "'%value%' exceeds the allowed length"
  59. );
  60. /**
  61. * @see http://en.wikipedia.org/wiki/IPv4
  62. * @var array
  63. */
  64. protected $_invalidIp = array(
  65. '0' => '0.0.0.0/8',
  66. '10' => '10.0.0.0/8',
  67. '127' => '127.0.0.0/8',
  68. '128' => '128.0.0.0/16',
  69. '169' => '169.254.0.0/16',
  70. '172' => '172.16.0.0/12',
  71. '191' => '191.255.0.0/16',
  72. '192' => array(
  73. '192.0.0.0/24',
  74. '192.0.2.0/24',
  75. '192.88.99.0/24',
  76. '192.168.0.0/16'
  77. ),
  78. '198' => '198.18.0.0/15',
  79. '223' => '223.255.255.0/24',
  80. '224' => '224.0.0.0/4',
  81. '240' => '240.0.0.0/4'
  82. );
  83. /**
  84. * @var array
  85. */
  86. protected $_messageVariables = array(
  87. 'hostname' => '_hostname',
  88. 'localPart' => '_localPart'
  89. );
  90. /**
  91. * @var string
  92. */
  93. protected $_hostname;
  94. /**
  95. * @var string
  96. */
  97. protected $_localPart;
  98. /**
  99. * Internal options array
  100. */
  101. protected $_options = array(
  102. 'mx' => false,
  103. 'deep' => false,
  104. 'domain' => true,
  105. 'allow' => Zend_Validate_Hostname::ALLOW_DNS,
  106. 'hostname' => null
  107. );
  108. /**
  109. * Instantiates hostname validator for local use
  110. *
  111. * The following option keys are supported:
  112. * 'hostname' => A hostname validator, see Zend_Validate_Hostname
  113. * 'allow' => Options for the hostname validator, see Zend_Validate_Hostname::ALLOW_*
  114. * 'mx' => If MX check should be enabled, boolean
  115. * 'deep' => If a deep MX check should be done, boolean
  116. *
  117. * @param array|Zend_Config $options OPTIONAL
  118. * @return void
  119. */
  120. public function __construct($options = array())
  121. {
  122. if ($options instanceof Zend_Config) {
  123. $options = $options->toArray();
  124. } else if (!is_array($options)) {
  125. $count = func_num_args();
  126. if ($count > 1) {
  127. // @todo: Preperation for 2.0... needs to be cleared with the dev-team
  128. // trigger_error('Support for multiple arguments is deprecated in favor of a single options array', E_USER_NOTICE);
  129. }
  130. $options = func_get_args();
  131. $temp['allow'] = array_shift($options);
  132. if (!empty($options)) {
  133. $temp['mx'] = array_shift($options);
  134. }
  135. if (!empty($options)) {
  136. $temp['hostname'] = array_shift($options);
  137. }
  138. $options = $temp;
  139. }
  140. $options += $this->_options;
  141. $this->setOptions($options);
  142. }
  143. /**
  144. * Returns all set Options
  145. *
  146. * @return array
  147. */
  148. public function getOptions()
  149. {
  150. return $this->_options;
  151. }
  152. /**
  153. * Set options for the email validator
  154. *
  155. * @param array $options
  156. * @return Zend_Validate_EmailAddress fluid interface
  157. */
  158. public function setOptions(array $options = array())
  159. {
  160. if (array_key_exists('messages', $options)) {
  161. $this->setMessages($options['messages']);
  162. }
  163. if (array_key_exists('hostname', $options)) {
  164. if (array_key_exists('allow', $options)) {
  165. $this->setHostnameValidator($options['hostname'], $options['allow']);
  166. } else {
  167. $this->setHostnameValidator($options['hostname']);
  168. }
  169. }
  170. if (array_key_exists('mx', $options)) {
  171. $this->setValidateMx($options['mx']);
  172. }
  173. if (array_key_exists('deep', $options)) {
  174. $this->setDeepMxCheck($options['deep']);
  175. }
  176. if (array_key_exists('domain', $options)) {
  177. $this->setDomainCheck($options['domain']);
  178. }
  179. return $this;
  180. }
  181. /**
  182. * Sets the validation failure message template for a particular key
  183. * Adds the ability to set messages to the attached hostname validator
  184. *
  185. * @param string $messageString
  186. * @param string $messageKey OPTIONAL
  187. * @return Zend_Validate_Abstract Provides a fluent interface
  188. * @throws Zend_Validate_Exception
  189. */
  190. public function setMessage($messageString, $messageKey = null)
  191. {
  192. $messageKeys = $messageKey;
  193. if ($messageKey === null) {
  194. $keys = array_keys($this->_messageTemplates);
  195. $messageKeys = current($keys);
  196. }
  197. if (!isset($this->_messageTemplates[$messageKeys])) {
  198. $this->_options['hostname']->setMessage($messageString, $messageKey);
  199. }
  200. $this->_messageTemplates[$messageKeys] = $messageString;
  201. return $this;
  202. }
  203. /**
  204. * Returns the set hostname validator
  205. *
  206. * @return Zend_Validate_Hostname
  207. */
  208. public function getHostnameValidator()
  209. {
  210. return $this->_options['hostname'];
  211. }
  212. /**
  213. * @param Zend_Validate_Hostname $hostnameValidator OPTIONAL
  214. * @param int $allow OPTIONAL
  215. * @return void
  216. */
  217. public function setHostnameValidator(Zend_Validate_Hostname $hostnameValidator = null, $allow = Zend_Validate_Hostname::ALLOW_DNS)
  218. {
  219. if (!$hostnameValidator) {
  220. $hostnameValidator = new Zend_Validate_Hostname($allow);
  221. }
  222. $this->_options['hostname'] = $hostnameValidator;
  223. $this->_options['allow'] = $allow;
  224. return $this;
  225. }
  226. /**
  227. * Whether MX checking via getmxrr is supported or not
  228. *
  229. * This currently only works on UNIX systems
  230. *
  231. * @return boolean
  232. */
  233. public function validateMxSupported()
  234. {
  235. return function_exists('getmxrr');
  236. }
  237. /**
  238. * Returns the set validateMx option
  239. *
  240. * @return boolean
  241. */
  242. public function getValidateMx()
  243. {
  244. return $this->_options['mx'];
  245. }
  246. /**
  247. * Set whether we check for a valid MX record via DNS
  248. *
  249. * This only applies when DNS hostnames are validated
  250. *
  251. * @param boolean $mx Set allowed to true to validate for MX records, and false to not validate them
  252. * @return Zend_Validate_EmailAddress Fluid Interface
  253. */
  254. public function setValidateMx($mx)
  255. {
  256. if ((bool) $mx && !$this->validateMxSupported()) {
  257. require_once 'Zend/Validate/Exception.php';
  258. throw new Zend_Validate_Exception('MX checking not available on this system');
  259. }
  260. $this->_options['mx'] = (bool) $mx;
  261. return $this;
  262. }
  263. /**
  264. * Returns the set deepMxCheck option
  265. *
  266. * @return boolean
  267. */
  268. public function getDeepMxCheck()
  269. {
  270. return $this->_options['deep'];
  271. }
  272. /**
  273. * Set whether we check MX record should be a deep validation
  274. *
  275. * @param boolean $deep Set deep to true to perform a deep validation process for MX records
  276. * @return Zend_Validate_EmailAddress Fluid Interface
  277. */
  278. public function setDeepMxCheck($deep)
  279. {
  280. $this->_options['deep'] = (bool) $deep;
  281. return $this;
  282. }
  283. /**
  284. * Returns the set domainCheck option
  285. *
  286. * @return unknown
  287. */
  288. public function getDomainCheck()
  289. {
  290. return $this->_options['domain'];
  291. }
  292. /**
  293. * Sets if the domain should also be checked
  294. * or only the local part of the email address
  295. *
  296. * @param boolean $domain
  297. * @return Zend_Validate_EmailAddress Fluid Interface
  298. */
  299. public function setDomainCheck($domain = true)
  300. {
  301. $this->_options['domain'] = (boolean) $domain;
  302. return $this;
  303. }
  304. /**
  305. * Returns if the given host is reserved
  306. *
  307. * @param string $host
  308. * @return boolean
  309. */
  310. private function _isReserved($host){
  311. if (!preg_match('/^([0-9]{1,3}\.){3}[0-9]{1,3}$/', $host)) {
  312. $host = gethostbyname($host);
  313. }
  314. $octet = explode('.',$host);
  315. if ((int)$octet[0] >= 224) {
  316. return true;
  317. } else if (array_key_exists($octet[0], $this->_invalidIp)) {
  318. foreach ((array)$this->_invalidIp[$octet[0]] as $subnetData) {
  319. // we skip the first loop as we already know that octet matches
  320. for ($i = 1; $i < 4; $i++) {
  321. if (strpos($subnetData, $octet[$i]) !== $i * 4) {
  322. break;
  323. }
  324. }
  325. $host = explode("/", $subnetData);
  326. $binaryHost = "";
  327. $tmp = explode(".", $host[0]);
  328. for ($i = 0; $i < 4 ; $i++) {
  329. $binaryHost .= str_pad(decbin($tmp[$i]), 8, "0", STR_PAD_LEFT);
  330. }
  331. $segmentData = array(
  332. 'network' => (int)$this->_binaryToIp(str_pad(substr($binaryHost, 0, $host[1]), 32, 0)),
  333. 'broadcast' => (int)$this->_binaryToIp(str_pad(substr($binaryHost, 0, $host[1]), 32, 1))
  334. );
  335. for ($j = $i; $j < 4; $j++) {
  336. if ((int)$octet[$j] < $segmentData['network'][$j] ||
  337. (int)$octet[$j] > $segmentData['broadcast'][$j]) {
  338. return false;
  339. }
  340. }
  341. }
  342. return true;
  343. } else {
  344. return false;
  345. }
  346. }
  347. /**
  348. * Converts a binary string to an IP address
  349. *
  350. * @param string $binary
  351. * @return mixed
  352. */
  353. private function _toIp($binary)
  354. {
  355. $ip = array();
  356. $tmp = explode(".", chunk_split($binary, 8, "."));
  357. for ($i = 0; $i < 4 ; $i++) {
  358. $ip[$i] = bindec($tmp[$i]);
  359. }
  360. return $ip;
  361. }
  362. /**
  363. * Internal method to validate the local part of the email address
  364. *
  365. * @return boolean
  366. */
  367. private function _validateLocalPart()
  368. {
  369. // First try to match the local part on the common dot-atom format
  370. $result = false;
  371. // Dot-atom characters are: 1*atext *("." 1*atext)
  372. // atext: ALPHA / DIGIT / and "!", "#", "$", "%", "&", "'", "*",
  373. // "+", "-", "/", "=", "?", "^", "_", "`", "{", "|", "}", "~"
  374. $atext = 'a-zA-Z0-9\x21\x23\x24\x25\x26\x27\x2a\x2b\x2d\x2f\x3d\x3f\x5e\x5f\x60\x7b\x7c\x7d\x7e';
  375. if (preg_match('/^[' . $atext . ']+(\x2e+[' . $atext . ']+)*$/', $this->_localPart)) {
  376. $result = true;
  377. } else {
  378. // Try quoted string format
  379. // Quoted-string characters are: DQUOTE *([FWS] qtext/quoted-pair) [FWS] DQUOTE
  380. // qtext: Non white space controls, and the rest of the US-ASCII characters not
  381. // including "\" or the quote character
  382. $noWsCtl = '\x01-\x08\x0b\x0c\x0e-\x1f\x7f';
  383. $qtext = $noWsCtl . '\x21\x23-\x5b\x5d-\x7e';
  384. $ws = '\x20\x09';
  385. if (preg_match('/^\x22([' . $ws . $qtext . '])*[$ws]?\x22$/', $this->_localPart)) {
  386. $result = true;
  387. } else {
  388. $this->_error(self::DOT_ATOM);
  389. $this->_error(self::QUOTED_STRING);
  390. $this->_error(self::INVALID_LOCAL_PART);
  391. }
  392. }
  393. return $result;
  394. }
  395. /**
  396. * Internal method to validate the servers MX records
  397. *
  398. * @return boolean
  399. */
  400. private function _validateMXRecords()
  401. {
  402. $result = true;
  403. $mxHosts = array();
  404. getmxrr($this->_hostname, $mxHosts);
  405. if ($this->_options['deep'] && function_exists('checkdnsrr')) {
  406. $validAddress = false;
  407. $reserved = true;
  408. foreach ($mxHosts as $hostname) {
  409. $res = $this->_isReserved($hostname);
  410. if (!$res) {
  411. $reserved = false;
  412. }
  413. if (!$res
  414. && (checkdnsrr($hostname, "A")
  415. || checkdnsrr($hostname, "AAAA")
  416. || checkdnsrr($hostname, "A6"))) {
  417. $validAddress = true;
  418. break;
  419. }
  420. }
  421. if (!$validAddress) {
  422. $result = false;
  423. if ($reserved) {
  424. $this->_error(self::INVALID_NETWORK_SEGMENT);
  425. } else {
  426. $this->_error(self::INVALID_MX_RECORD);
  427. }
  428. }
  429. }
  430. return $result;
  431. }
  432. /**
  433. * Internal method to validate the hostname part of the email address
  434. *
  435. * @return boolean
  436. */
  437. private function _validateHostnamePart()
  438. {
  439. $hostname = $this->_options['hostname']->setTranslator($this->getTranslator())
  440. ->isValid($this->_hostname);
  441. if (!$hostname) {
  442. $this->_error(self::INVALID_HOSTNAME);
  443. // Get messages and errors from hostnameValidator
  444. foreach ($this->_options['hostname']->getMessages() as $code => $message) {
  445. $this->_messages[$code] = $message;
  446. }
  447. foreach ($this->_options['hostname']->getErrors() as $error) {
  448. $this->_errors[] = $error;
  449. }
  450. } else if ($this->_options['mx']) {
  451. // MX check on hostname
  452. $hostname = $this->_validateMXRecords();
  453. }
  454. return $hostname;
  455. }
  456. /**
  457. * Defined by Zend_Validate_Interface
  458. *
  459. * Returns true if and only if $value is a valid email address
  460. * according to RFC2822
  461. *
  462. * @link http://www.ietf.org/rfc/rfc2822.txt RFC2822
  463. * @link http://www.columbia.edu/kermit/ascii.html US-ASCII characters
  464. * @param string $value
  465. * @return boolean
  466. */
  467. public function isValid($value)
  468. {
  469. if (!is_string($value)) {
  470. $this->_error(self::INVALID);
  471. return false;
  472. }
  473. $matches = array();
  474. $length = true;
  475. $this->_setValue($value);
  476. // Split email address up and disallow '..'
  477. if ((strpos($value, '..') !== false) or
  478. (!preg_match('/^(.+)@([^@]+)$/', $value, $matches))) {
  479. $this->_error(self::INVALID_FORMAT);
  480. return false;
  481. }
  482. $this->_localPart = $matches[1];
  483. $this->_hostname = $matches[2];
  484. if ((strlen($this->_localPart) > 64) || (strlen($this->_hostname) > 255)) {
  485. $length = false;
  486. $this->_error(self::LENGTH_EXCEEDED);
  487. }
  488. // Match hostname part
  489. if ($this->_options['domain']) {
  490. $hostname = $this->_validateHostnamePart();
  491. }
  492. $local = $this->_validateLocalPart();
  493. // If both parts valid, return true
  494. if ($local && $length) {
  495. if (($this->_options['domain'] && $hostname) || !$this->_options['domain']) {
  496. return true;
  497. }
  498. }
  499. return false;
  500. }
  501. }