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