1<?php 2/** 3 * RFC 822 Email address list validation Utility 4 * 5 * PHP version 5 6 * 7 * LICENSE: 8 * 9 * Copyright (c) 2001-2017, Chuck Hagenbuch & Richard Heyes 10 * All rights reserved. 11 * 12 * Redistribution and use in source and binary forms, with or without 13 * modification, are permitted provided that the following conditions 14 * are met: 15 * 16 * 1. Redistributions of source code must retain the above copyright 17 * notice, this list of conditions and the following disclaimer. 18 * 19 * 2. Redistributions in binary form must reproduce the above copyright 20 * notice, this list of conditions and the following disclaimer in the 21 * documentation and/or other materials provided with the distribution. 22 * 23 * 3. Neither the name of the copyright holder nor the names of its 24 * contributors may be used to endorse or promote products derived from 25 * this software without specific prior written permission. 26 * 27 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 28 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 29 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 30 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 31 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 32 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 33 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 34 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 35 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 36 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 37 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 38 * 39 * @category Mail 40 * @package Mail 41 * @author Richard Heyes <richard@phpguru.org> 42 * @author Chuck Hagenbuch <chuck@horde.org 43 * @copyright 2001-2017 Richard Heyes 44 * @license http://opensource.org/licenses/BSD-3-Clause New BSD License 45 * @version CVS: $Id$ 46 * @link http://pear.php.net/package/Mail/ 47 */ 48 49/** 50 * RFC 822 Email address list validation Utility 51 * 52 * What is it? 53 * 54 * This class will take an address string, and parse it into it's consituent 55 * parts, be that either addresses, groups, or combinations. Nested groups 56 * are not supported. The structure it returns is pretty straight forward, 57 * and is similar to that provided by the imap_rfc822_parse_adrlist(). Use 58 * print_r() to view the structure. 59 * 60 * How do I use it? 61 * 62 * $address_string = 'My Group: "Richard" <richard@localhost> (A comment), ted@example.com (Ted Bloggs), Barney;'; 63 * $structure = Mail_RFC822::parseAddressList($address_string, 'example.com', true) 64 * print_r($structure); 65 * 66 * @author Richard Heyes <richard@phpguru.org> 67 * @author Chuck Hagenbuch <chuck@horde.org> 68 * @version $Revision$ 69 * @license BSD 70 * @package Mail 71 */ 72class Mail_RFC822 { 73 74 /** 75 * The address being parsed by the RFC822 object. 76 * @var string $address 77 */ 78 var $address = ''; 79 80 /** 81 * The default domain to use for unqualified addresses. 82 * @var string $default_domain 83 */ 84 var $default_domain = 'localhost'; 85 86 /** 87 * Should we return a nested array showing groups, or flatten everything? 88 * @var boolean $nestGroups 89 */ 90 var $nestGroups = true; 91 92 /** 93 * Whether or not to validate atoms for non-ascii characters. 94 * @var boolean $validate 95 */ 96 var $validate = true; 97 98 /** 99 * The array of raw addresses built up as we parse. 100 * @var array $addresses 101 */ 102 var $addresses = array(); 103 104 /** 105 * The final array of parsed address information that we build up. 106 * @var array $structure 107 */ 108 var $structure = array(); 109 110 /** 111 * The current error message, if any. 112 * @var string $error 113 */ 114 var $error = null; 115 116 /** 117 * An internal counter/pointer. 118 * @var integer $index 119 */ 120 var $index = null; 121 122 /** 123 * The number of groups that have been found in the address list. 124 * @var integer $num_groups 125 * @access public 126 */ 127 var $num_groups = 0; 128 129 /** 130 * A variable so that we can tell whether or not we're inside a 131 * Mail_RFC822 object. 132 * @var boolean $mailRFC822 133 */ 134 var $mailRFC822 = true; 135 136 /** 137 * A limit after which processing stops 138 * @var int $limit 139 */ 140 var $limit = null; 141 142 /** 143 * Sets up the object. The address must either be set here or when 144 * calling parseAddressList(). One or the other. 145 * 146 * @param string $address The address(es) to validate. 147 * @param string $default_domain Default domain/host etc. If not supplied, will be set to localhost. 148 * @param boolean $nest_groups Whether to return the structure with groups nested for easier viewing. 149 * @param boolean $validate Whether to validate atoms. Turn this off if you need to run addresses through before encoding the personal names, for instance. 150 * 151 * @return object Mail_RFC822 A new Mail_RFC822 object. 152 */ 153 public function __construct($address = null, $default_domain = null, $nest_groups = null, $validate = null, $limit = null) 154 { 155 if (isset($address)) $this->address = $address; 156 if (isset($default_domain)) $this->default_domain = $default_domain; 157 if (isset($nest_groups)) $this->nestGroups = $nest_groups; 158 if (isset($validate)) $this->validate = $validate; 159 if (isset($limit)) $this->limit = $limit; 160 } 161 162 /** 163 * Starts the whole process. The address must either be set here 164 * or when creating the object. One or the other. 165 * 166 * @param string $address The address(es) to validate. 167 * @param string $default_domain Default domain/host etc. 168 * @param boolean $nest_groups Whether to return the structure with groups nested for easier viewing. 169 * @param boolean $validate Whether to validate atoms. Turn this off if you need to run addresses through before encoding the personal names, for instance. 170 * 171 * @return array A structured array of addresses. 172 */ 173 public function parseAddressList($address = null, $default_domain = null, $nest_groups = null, $validate = null, $limit = null) 174 { 175 if (!isset($this) || !isset($this->mailRFC822)) { 176 $obj = new Mail_RFC822($address, $default_domain, $nest_groups, $validate, $limit); 177 return $obj->parseAddressList(); 178 } 179 180 if (isset($address)) $this->address = $address; 181 if (isset($default_domain)) $this->default_domain = $default_domain; 182 if (isset($nest_groups)) $this->nestGroups = $nest_groups; 183 if (isset($validate)) $this->validate = $validate; 184 if (isset($limit)) $this->limit = $limit; 185 186 $this->structure = array(); 187 $this->addresses = array(); 188 $this->error = null; 189 $this->index = null; 190 191 // Unfold any long lines in $this->address. 192 $this->address = preg_replace('/\r?\n/', "\r\n", $this->address); 193 $this->address = preg_replace('/\r\n(\t| )+/', ' ', $this->address); 194 195 while ($this->address = $this->_splitAddresses($this->address)); 196 197 if ($this->address === false || isset($this->error)) { 198 require_once 'PEAR.php'; 199 return PEAR::raiseError($this->error); 200 } 201 202 // Validate each address individually. If we encounter an invalid 203 // address, stop iterating and return an error immediately. 204 foreach ($this->addresses as $address) { 205 $valid = $this->_validateAddress($address); 206 207 if ($valid === false || isset($this->error)) { 208 require_once 'PEAR.php'; 209 return PEAR::raiseError($this->error); 210 } 211 212 if (!$this->nestGroups) { 213 $this->structure = array_merge($this->structure, $valid); 214 } else { 215 $this->structure[] = $valid; 216 } 217 } 218 219 return $this->structure; 220 } 221 222 /** 223 * Splits an address into separate addresses. 224 * 225 * @param string $address The addresses to split. 226 * @return boolean Success or failure. 227 */ 228 protected function _splitAddresses($address) 229 { 230 if (!empty($this->limit) && count($this->addresses) == $this->limit) { 231 return ''; 232 } 233 234 if ($this->_isGroup($address) && !isset($this->error)) { 235 $split_char = ';'; 236 $is_group = true; 237 } elseif (!isset($this->error)) { 238 $split_char = ','; 239 $is_group = false; 240 } elseif (isset($this->error)) { 241 return false; 242 } 243 244 // Split the string based on the above ten or so lines. 245 $parts = explode($split_char, $address); 246 $string = $this->_splitCheck($parts, $split_char); 247 248 // If a group... 249 if ($is_group) { 250 // If $string does not contain a colon outside of 251 // brackets/quotes etc then something's fubar. 252 253 // First check there's a colon at all: 254 if (strpos($string, ':') === false) { 255 $this->error = 'Invalid address: ' . $string; 256 return false; 257 } 258 259 // Now check it's outside of brackets/quotes: 260 if (!$this->_splitCheck(explode(':', $string), ':')) { 261 return false; 262 } 263 264 // We must have a group at this point, so increase the counter: 265 $this->num_groups++; 266 } 267 268 // $string now contains the first full address/group. 269 // Add to the addresses array. 270 $this->addresses[] = array( 271 'address' => trim($string), 272 'group' => $is_group 273 ); 274 275 // Remove the now stored address from the initial line, the +1 276 // is to account for the explode character. 277 $address = trim(substr($address, strlen($string) + 1)); 278 279 // If the next char is a comma and this was a group, then 280 // there are more addresses, otherwise, if there are any more 281 // chars, then there is another address. 282 if ($is_group && substr($address, 0, 1) == ','){ 283 $address = trim(substr($address, 1)); 284 return $address; 285 286 } elseif (strlen($address) > 0) { 287 return $address; 288 289 } else { 290 return ''; 291 } 292 293 // If you got here then something's off 294 return false; 295 } 296 297 /** 298 * Checks for a group at the start of the string. 299 * 300 * @param string $address The address to check. 301 * @return boolean Whether or not there is a group at the start of the string. 302 */ 303 protected function _isGroup($address) 304 { 305 // First comma not in quotes, angles or escaped: 306 $parts = explode(',', $address); 307 $string = $this->_splitCheck($parts, ','); 308 309 // Now we have the first address, we can reliably check for a 310 // group by searching for a colon that's not escaped or in 311 // quotes or angle brackets. 312 if (count($parts = explode(':', $string)) > 1) { 313 $string2 = $this->_splitCheck($parts, ':'); 314 return ($string2 !== $string); 315 } else { 316 return false; 317 } 318 } 319 320 /** 321 * A common function that will check an exploded string. 322 * 323 * @param array $parts The exloded string. 324 * @param string $char The char that was exploded on. 325 * @return mixed False if the string contains unclosed quotes/brackets, or the string on success. 326 */ 327 protected function _splitCheck($parts, $char) 328 { 329 $string = $parts[0]; 330 331 for ($i = 0; $i < count($parts); $i++) { 332 if ($this->_hasUnclosedQuotes($string) 333 || $this->_hasUnclosedBrackets($string, '<>') 334 || $this->_hasUnclosedBrackets($string, '[]') 335 || $this->_hasUnclosedBrackets($string, '()') 336 || substr($string, -1) == '\\') { 337 if (isset($parts[$i + 1])) { 338 $string = $string . $char . $parts[$i + 1]; 339 } else { 340 $this->error = 'Invalid address spec. Unclosed bracket or quotes'; 341 return false; 342 } 343 } else { 344 $this->index = $i; 345 break; 346 } 347 } 348 349 return $string; 350 } 351 352 /** 353 * Checks if a string has unclosed quotes or not. 354 * 355 * @param string $string The string to check. 356 * @return boolean True if there are unclosed quotes inside the string, 357 * false otherwise. 358 */ 359 protected function _hasUnclosedQuotes($string) 360 { 361 $string = trim($string); 362 $iMax = strlen($string); 363 $in_quote = false; 364 $i = $slashes = 0; 365 366 for (; $i < $iMax; ++$i) { 367 switch ($string[$i]) { 368 case '\\': 369 ++$slashes; 370 break; 371 372 case '"': 373 if ($slashes % 2 == 0) { 374 $in_quote = !$in_quote; 375 } 376 // Fall through to default action below. 377 378 default: 379 $slashes = 0; 380 break; 381 } 382 } 383 384 return $in_quote; 385 } 386 387 /** 388 * Checks if a string has an unclosed brackets or not. IMPORTANT: 389 * This function handles both angle brackets and square brackets; 390 * 391 * @param string $string The string to check. 392 * @param string $chars The characters to check for. 393 * @return boolean True if there are unclosed brackets inside the string, false otherwise. 394 */ 395 protected function _hasUnclosedBrackets($string, $chars) 396 { 397 $num_angle_start = substr_count($string, $chars[0]); 398 $num_angle_end = substr_count($string, $chars[1]); 399 400 $this->_hasUnclosedBracketsSub($string, $num_angle_start, $chars[0]); 401 $this->_hasUnclosedBracketsSub($string, $num_angle_end, $chars[1]); 402 403 if ($num_angle_start < $num_angle_end) { 404 $this->error = 'Invalid address spec. Unmatched quote or bracket (' . $chars . ')'; 405 return false; 406 } else { 407 return ($num_angle_start > $num_angle_end); 408 } 409 } 410 411 /** 412 * Sub function that is used only by hasUnclosedBrackets(). 413 * 414 * @param string $string The string to check. 415 * @param integer &$num The number of occurences. 416 * @param string $char The character to count. 417 * @return integer The number of occurences of $char in $string, adjusted for backslashes. 418 */ 419 protected function _hasUnclosedBracketsSub($string, &$num, $char) 420 { 421 $parts = explode($char, $string); 422 for ($i = 0; $i < count($parts); $i++){ 423 if (substr($parts[$i], -1) == '\\' || $this->_hasUnclosedQuotes($parts[$i])) 424 $num--; 425 if (isset($parts[$i + 1])) 426 $parts[$i + 1] = $parts[$i] . $char . $parts[$i + 1]; 427 } 428 429 return $num; 430 } 431 432 /** 433 * Function to begin checking the address. 434 * 435 * @param string $address The address to validate. 436 * @return mixed False on failure, or a structured array of address information on success. 437 */ 438 protected function _validateAddress($address) 439 { 440 $is_group = false; 441 $addresses = array(); 442 443 if ($address['group']) { 444 $is_group = true; 445 446 // Get the group part of the name 447 $parts = explode(':', $address['address']); 448 $groupname = $this->_splitCheck($parts, ':'); 449 $structure = array(); 450 451 // And validate the group part of the name. 452 if (!$this->_validatePhrase($groupname)){ 453 $this->error = 'Group name did not validate.'; 454 return false; 455 } else { 456 // Don't include groups if we are not nesting 457 // them. This avoids returning invalid addresses. 458 if ($this->nestGroups) { 459 $structure = new stdClass; 460 $structure->groupname = $groupname; 461 } 462 } 463 464 $address['address'] = ltrim(substr($address['address'], strlen($groupname . ':'))); 465 } 466 467 // If a group then split on comma and put into an array. 468 // Otherwise, Just put the whole address in an array. 469 if ($is_group) { 470 while (strlen($address['address']) > 0) { 471 $parts = explode(',', $address['address']); 472 $addresses[] = $this->_splitCheck($parts, ','); 473 $address['address'] = trim(substr($address['address'], strlen(end($addresses) . ','))); 474 } 475 } else { 476 $addresses[] = $address['address']; 477 } 478 479 // Trim the whitespace from all of the address strings. 480 array_map('trim', $addresses); 481 482 // Validate each mailbox. 483 // Format could be one of: name <geezer@domain.com> 484 // geezer@domain.com 485 // geezer 486 // ... or any other format valid by RFC 822. 487 for ($i = 0; $i < count($addresses); $i++) { 488 if (!$this->validateMailbox($addresses[$i])) { 489 if (empty($this->error)) { 490 $this->error = 'Validation failed for: ' . $addresses[$i]; 491 } 492 return false; 493 } 494 } 495 496 // Nested format 497 if ($this->nestGroups) { 498 if ($is_group) { 499 $structure->addresses = $addresses; 500 } else { 501 $structure = $addresses[0]; 502 } 503 504 // Flat format 505 } else { 506 if ($is_group) { 507 $structure = array_merge($structure, $addresses); 508 } else { 509 $structure = $addresses; 510 } 511 } 512 513 return $structure; 514 } 515 516 /** 517 * Function to validate a phrase. 518 * 519 * @param string $phrase The phrase to check. 520 * @return boolean Success or failure. 521 */ 522 protected function _validatePhrase($phrase) 523 { 524 // Splits on one or more Tab or space. 525 $parts = preg_split('/[ \\x09]+/', $phrase, -1, PREG_SPLIT_NO_EMPTY); 526 527 $phrase_parts = array(); 528 while (count($parts) > 0){ 529 $phrase_parts[] = $this->_splitCheck($parts, ' '); 530 for ($i = 0; $i < $this->index + 1; $i++) 531 array_shift($parts); 532 } 533 534 foreach ($phrase_parts as $part) { 535 // If quoted string: 536 if (substr($part, 0, 1) == '"') { 537 if (!$this->_validateQuotedString($part)) { 538 return false; 539 } 540 continue; 541 } 542 543 // Otherwise it's an atom: 544 if (!$this->_validateAtom($part)) return false; 545 } 546 547 return true; 548 } 549 550 /** 551 * Function to validate an atom which from rfc822 is: 552 * atom = 1*<any CHAR except specials, SPACE and CTLs> 553 * 554 * If validation ($this->validate) has been turned off, then 555 * validateAtom() doesn't actually check anything. This is so that you 556 * can split a list of addresses up before encoding personal names 557 * (umlauts, etc.), for example. 558 * 559 * @param string $atom The string to check. 560 * @return boolean Success or failure. 561 */ 562 protected function _validateAtom($atom) 563 { 564 if (!$this->validate) { 565 // Validation has been turned off; assume the atom is okay. 566 return true; 567 } 568 569 // Check for any char from ASCII 0 - ASCII 127 570 if (!preg_match('/^[\\x00-\\x7E]+$/i', $atom, $matches)) { 571 return false; 572 } 573 574 // Check for specials: 575 if (preg_match('/[][()<>@,;\\:". ]/', $atom)) { 576 return false; 577 } 578 579 // Check for control characters (ASCII 0-31): 580 if (preg_match('/[\\x00-\\x1F]+/', $atom)) { 581 return false; 582 } 583 584 return true; 585 } 586 587 /** 588 * Function to validate quoted string, which is: 589 * quoted-string = <"> *(qtext/quoted-pair) <"> 590 * 591 * @param string $qstring The string to check 592 * @return boolean Success or failure. 593 */ 594 protected function _validateQuotedString($qstring) 595 { 596 // Leading and trailing " 597 $qstring = substr($qstring, 1, -1); 598 599 // Perform check, removing quoted characters first. 600 return !preg_match('/[\x0D\\\\"]/', preg_replace('/\\\\./', '', $qstring)); 601 } 602 603 /** 604 * Function to validate a mailbox, which is: 605 * mailbox = addr-spec ; simple address 606 * / phrase route-addr ; name and route-addr 607 * 608 * @param string &$mailbox The string to check. 609 * @return boolean Success or failure. 610 */ 611 public function validateMailbox(&$mailbox) 612 { 613 // A couple of defaults. 614 $phrase = ''; 615 $comment = ''; 616 $comments = array(); 617 618 // Catch any RFC822 comments and store them separately. 619 $_mailbox = $mailbox; 620 while (strlen(trim($_mailbox)) > 0) { 621 $parts = explode('(', $_mailbox); 622 $before_comment = $this->_splitCheck($parts, '('); 623 if ($before_comment != $_mailbox) { 624 // First char should be a (. 625 $comment = substr(str_replace($before_comment, '', $_mailbox), 1); 626 $parts = explode(')', $comment); 627 $comment = $this->_splitCheck($parts, ')'); 628 $comments[] = $comment; 629 630 // +2 is for the brackets 631 $_mailbox = substr($_mailbox, strpos($_mailbox, '('.$comment)+strlen($comment)+2); 632 } else { 633 break; 634 } 635 } 636 637 foreach ($comments as $comment) { 638 $mailbox = str_replace("($comment)", '', $mailbox); 639 } 640 641 $mailbox = trim($mailbox); 642 643 // Check for name + route-addr 644 if (substr($mailbox, -1) == '>' && substr($mailbox, 0, 1) != '<') { 645 $parts = explode('<', $mailbox); 646 $name = $this->_splitCheck($parts, '<'); 647 648 $phrase = trim($name); 649 $route_addr = trim(substr($mailbox, strlen($name.'<'), -1)); 650 651 if ($this->_validatePhrase($phrase) === false || ($route_addr = $this->_validateRouteAddr($route_addr)) === false) { 652 return false; 653 } 654 655 // Only got addr-spec 656 } else { 657 // First snip angle brackets if present. 658 if (substr($mailbox, 0, 1) == '<' && substr($mailbox, -1) == '>') { 659 $addr_spec = substr($mailbox, 1, -1); 660 } else { 661 $addr_spec = $mailbox; 662 } 663 664 if (($addr_spec = $this->_validateAddrSpec($addr_spec)) === false) { 665 return false; 666 } 667 } 668 669 // Construct the object that will be returned. 670 $mbox = new stdClass(); 671 672 // Add the phrase (even if empty) and comments 673 $mbox->personal = $phrase; 674 $mbox->comment = isset($comments) ? $comments : array(); 675 676 if (isset($route_addr)) { 677 $mbox->mailbox = $route_addr['local_part']; 678 $mbox->host = $route_addr['domain']; 679 $route_addr['adl'] !== '' ? $mbox->adl = $route_addr['adl'] : ''; 680 } else { 681 $mbox->mailbox = $addr_spec['local_part']; 682 $mbox->host = $addr_spec['domain']; 683 } 684 685 $mailbox = $mbox; 686 return true; 687 } 688 689 /** 690 * This function validates a route-addr which is: 691 * route-addr = "<" [route] addr-spec ">" 692 * 693 * Angle brackets have already been removed at the point of 694 * getting to this function. 695 * 696 * @param string $route_addr The string to check. 697 * @return mixed False on failure, or an array containing validated address/route information on success. 698 */ 699 protected function _validateRouteAddr($route_addr) 700 { 701 // Check for colon. 702 if (strpos($route_addr, ':') !== false) { 703 $parts = explode(':', $route_addr); 704 $route = $this->_splitCheck($parts, ':'); 705 } else { 706 $route = $route_addr; 707 } 708 709 // If $route is same as $route_addr then the colon was in 710 // quotes or brackets or, of course, non existent. 711 if ($route === $route_addr){ 712 unset($route); 713 $addr_spec = $route_addr; 714 if (($addr_spec = $this->_validateAddrSpec($addr_spec)) === false) { 715 return false; 716 } 717 } else { 718 // Validate route part. 719 if (($route = $this->_validateRoute($route)) === false) { 720 return false; 721 } 722 723 $addr_spec = substr($route_addr, strlen($route . ':')); 724 725 // Validate addr-spec part. 726 if (($addr_spec = $this->_validateAddrSpec($addr_spec)) === false) { 727 return false; 728 } 729 } 730 731 if (isset($route)) { 732 $return['adl'] = $route; 733 } else { 734 $return['adl'] = ''; 735 } 736 737 $return = array_merge($return, $addr_spec); 738 return $return; 739 } 740 741 /** 742 * Function to validate a route, which is: 743 * route = 1#("@" domain) ":" 744 * 745 * @param string $route The string to check. 746 * @return mixed False on failure, or the validated $route on success. 747 */ 748 protected function _validateRoute($route) 749 { 750 // Split on comma. 751 $domains = explode(',', trim($route)); 752 753 foreach ($domains as $domain) { 754 $domain = str_replace('@', '', trim($domain)); 755 if (!$this->_validateDomain($domain)) return false; 756 } 757 758 return $route; 759 } 760 761 /** 762 * Function to validate a domain, though this is not quite what 763 * you expect of a strict internet domain. 764 * 765 * domain = sub-domain *("." sub-domain) 766 * 767 * @param string $domain The string to check. 768 * @return mixed False on failure, or the validated domain on success. 769 */ 770 protected function _validateDomain($domain) 771 { 772 // Note the different use of $subdomains and $sub_domains 773 $subdomains = explode('.', $domain); 774 775 while (count($subdomains) > 0) { 776 $sub_domains[] = $this->_splitCheck($subdomains, '.'); 777 for ($i = 0; $i < $this->index + 1; $i++) 778 array_shift($subdomains); 779 } 780 781 foreach ($sub_domains as $sub_domain) { 782 if (!$this->_validateSubdomain(trim($sub_domain))) 783 return false; 784 } 785 786 // Managed to get here, so return input. 787 return $domain; 788 } 789 790 /** 791 * Function to validate a subdomain: 792 * subdomain = domain-ref / domain-literal 793 * 794 * @param string $subdomain The string to check. 795 * @return boolean Success or failure. 796 */ 797 protected function _validateSubdomain($subdomain) 798 { 799 if (preg_match('|^\[(.*)]$|', $subdomain, $arr)){ 800 if (!$this->_validateDliteral($arr[1])) return false; 801 } else { 802 if (!$this->_validateAtom($subdomain)) return false; 803 } 804 805 // Got here, so return successful. 806 return true; 807 } 808 809 /** 810 * Function to validate a domain literal: 811 * domain-literal = "[" *(dtext / quoted-pair) "]" 812 * 813 * @param string $dliteral The string to check. 814 * @return boolean Success or failure. 815 */ 816 protected function _validateDliteral($dliteral) 817 { 818 return !preg_match('/(.)[][\x0D\\\\]/', $dliteral, $matches) && ((! isset($matches[1])) || $matches[1] != '\\'); 819 } 820 821 /** 822 * Function to validate an addr-spec. 823 * 824 * addr-spec = local-part "@" domain 825 * 826 * @param string $addr_spec The string to check. 827 * @return mixed False on failure, or the validated addr-spec on success. 828 */ 829 protected function _validateAddrSpec($addr_spec) 830 { 831 $addr_spec = trim($addr_spec); 832 833 // Split on @ sign if there is one. 834 if (strpos($addr_spec, '@') !== false) { 835 $parts = explode('@', $addr_spec); 836 $local_part = $this->_splitCheck($parts, '@'); 837 $domain = substr($addr_spec, strlen($local_part . '@')); 838 839 // No @ sign so assume the default domain. 840 } else { 841 $local_part = $addr_spec; 842 $domain = $this->default_domain; 843 } 844 845 if (($local_part = $this->_validateLocalPart($local_part)) === false) return false; 846 if (($domain = $this->_validateDomain($domain)) === false) return false; 847 848 // Got here so return successful. 849 return array('local_part' => $local_part, 'domain' => $domain); 850 } 851 852 /** 853 * Function to validate the local part of an address: 854 * local-part = word *("." word) 855 * 856 * @param string $local_part 857 * @return mixed False on failure, or the validated local part on success. 858 */ 859 protected function _validateLocalPart($local_part) 860 { 861 $parts = explode('.', $local_part); 862 $words = array(); 863 864 // Split the local_part into words. 865 while (count($parts) > 0) { 866 $words[] = $this->_splitCheck($parts, '.'); 867 for ($i = 0; $i < $this->index + 1; $i++) { 868 array_shift($parts); 869 } 870 } 871 872 // Validate each word. 873 foreach ($words as $word) { 874 // word cannot be empty (#17317) 875 if ($word === '') { 876 return false; 877 } 878 // If this word contains an unquoted space, it is invalid. (6.2.4) 879 if (strpos($word, ' ') && $word[0] !== '"') 880 { 881 return false; 882 } 883 884 if ($this->_validatePhrase(trim($word)) === false) return false; 885 } 886 887 // Managed to get here, so return the input. 888 return $local_part; 889 } 890 891 /** 892 * Returns an approximate count of how many addresses are in the 893 * given string. This is APPROXIMATE as it only splits based on a 894 * comma which has no preceding backslash. Could be useful as 895 * large amounts of addresses will end up producing *large* 896 * structures when used with parseAddressList(). 897 * 898 * @param string $data Addresses to count 899 * @return int Approximate count 900 */ 901 public function approximateCount($data) 902 { 903 return count(preg_split('/(?<!\\\\),/', $data)); 904 } 905 906 /** 907 * This is a email validating function separate to the rest of the 908 * class. It simply validates whether an email is of the common 909 * internet form: <user>@<domain>. This can be sufficient for most 910 * people. Optional stricter mode can be utilised which restricts 911 * mailbox characters allowed to alphanumeric, full stop, hyphen 912 * and underscore. 913 * 914 * @param string $data Address to check 915 * @param boolean $strict Optional stricter mode 916 * @return mixed False if it fails, an indexed array 917 * username/domain if it matches 918 */ 919 public function isValidInetAddress($data, $strict = false) 920 { 921 $regex = $strict ? '/^([.0-9a-z_+-]+)@(([0-9a-z-]+\.)+[0-9a-z]{2,})$/i' : '/^([*+!.&#$|\'\\%\/0-9a-z^_`{}=?~:-]+)@(([0-9a-z-]+\.)+[0-9a-z]{2,})$/i'; 922 if (preg_match($regex, trim($data), $matches)) { 923 return array($matches[1], $matches[2]); 924 } else { 925 return false; 926 } 927 } 928 929} 930