Changeset 7688 for trunk/library/Mail/Mail/RFC822.php
- Timestamp:
- 12/26/12 16:15:59 (11 years ago)
- File:
-
- 1 edited
Legend:
- Unmodified
- Added
- Removed
-
trunk/library/Mail/Mail/RFC822.php
r7673 r7688 68 68 * @package Mail 69 69 */ 70 class Mail_RFC822 { 71 72 /** 73 * The address being parsed by the RFC822 object. 74 * @var string $address 75 */ 76 var $address = ''; 77 78 /** 79 * The default domain to use for unqualified addresses. 80 * @var string $default_domain 81 */ 82 var $default_domain = 'localhost'; 83 84 /** 85 * Should we return a nested array showing groups, or flatten everything? 86 * @var boolean $nestGroups 87 */ 88 var $nestGroups = true; 89 90 /** 91 * Whether or not to validate atoms for non-ascii characters. 92 * @var boolean $validate 93 */ 94 var $validate = true; 95 96 /** 97 * The array of raw addresses built up as we parse. 98 * @var array $addresses 99 */ 100 var $addresses = array(); 101 102 /** 103 * The final array of parsed address information that we build up. 104 * @var array $structure 105 */ 106 var $structure = array(); 107 108 /** 109 * The current error message, if any. 110 * @var string $error 111 */ 112 var $error = null; 113 114 /** 115 * An internal counter/pointer. 116 * @var integer $index 117 */ 118 var $index = null; 119 120 /** 121 * The number of groups that have been found in the address list. 122 * @var integer $num_groups 123 * @access public 124 */ 125 var $num_groups = 0; 126 127 /** 128 * A variable so that we can tell whether or not we're inside a 129 * Mail_RFC822 object. 130 * @var boolean $mailRFC822 131 */ 132 var $mailRFC822 = true; 133 134 /** 135 * A limit after which processing stops 136 * @var int $limit 137 */ 138 var $limit = null; 139 140 /** 141 * Sets up the object. The address must either be set here or when 142 * calling parseAddressList(). One or the other. 143 * 144 * @access public 145 * @param string $address The address(es) to validate. 146 * @param string $default_domain Default domain/host etc. If not supplied, will be set to localhost. 147 * @param boolean $nest_groups Whether to return the structure with groups nested for easier viewing. 148 * @param boolean $validate Whether to validate atoms. Turn this off if you need to run addresses through before encoding the personal names, for instance. 149 * 150 * @return object Mail_RFC822 A new Mail_RFC822 object. 151 */ 152 function Mail_RFC822($address = null, $default_domain = null, $nest_groups = null, $validate = null, $limit = null) 153 { 154 if (isset($address)) $this->address = $address; 155 if (isset($default_domain)) $this->default_domain = $default_domain; 156 if (isset($nest_groups)) $this->nestGroups = $nest_groups; 157 if (isset($validate)) $this->validate = $validate; 158 if (isset($limit)) $this->limit = $limit; 70 if(!class_exists('Mail_RFC822')) 71 { 72 class 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 * @access public 147 * @param string $address The address(es) to validate. 148 * @param string $default_domain Default domain/host etc. If not supplied, will be set to localhost. 149 * @param boolean $nest_groups Whether to return the structure with groups nested for easier viewing. 150 * @param boolean $validate Whether to validate atoms. Turn this off if you need to run addresses through before encoding the personal names, for instance. 151 * 152 * @return object Mail_RFC822 A new Mail_RFC822 object. 153 */ 154 function Mail_RFC822($address = null, $default_domain = null, $nest_groups = null, $validate = null, $limit = null) 155 { 156 if (isset($address)) $this->address = $address; 157 if (isset($default_domain)) $this->default_domain = $default_domain; 158 if (isset($nest_groups)) $this->nestGroups = $nest_groups; 159 if (isset($validate)) $this->validate = $validate; 160 if (isset($limit)) $this->limit = $limit; 161 } 162 163 /** 164 * Starts the whole process. The address must either be set here 165 * or when creating the object. One or the other. 166 * 167 * @access public 168 * @param string $address The address(es) to validate. 169 * @param string $default_domain Default domain/host etc. 170 * @param boolean $nest_groups Whether to return the structure with groups nested for easier viewing. 171 * @param boolean $validate Whether to validate atoms. Turn this off if you need to run addresses through before encoding the personal names, for instance. 172 * 173 * @return array A structured array of addresses. 174 */ 175 function parseAddressList($address = null, $default_domain = null, $nest_groups = null, $validate = null, $limit = null) 176 { 177 if (!isset($this) || !isset($this->mailRFC822)) { 178 $obj = new Mail_RFC822($address, $default_domain, $nest_groups, $validate, $limit); 179 return $obj->parseAddressList(); 180 } 181 182 if (isset($address)) $this->address = $address; 183 if (isset($default_domain)) $this->default_domain = $default_domain; 184 if (isset($nest_groups)) $this->nestGroups = $nest_groups; 185 if (isset($validate)) $this->validate = $validate; 186 if (isset($limit)) $this->limit = $limit; 187 188 $this->structure = array(); 189 $this->addresses = array(); 190 $this->error = null; 191 $this->index = null; 192 193 // Unfold any long lines in $this->address. 194 $this->address = preg_replace('/\r?\n/', "\r\n", $this->address); 195 $this->address = preg_replace('/\r\n(\t| )+/', ' ', $this->address); 196 197 while ($this->address = $this->_splitAddresses($this->address)); 198 199 if ($this->address === false || isset($this->error)) { 200 require_once dirname(__FILE__).'/../../PEAR/PEAR.php'; 201 return PEAR::raiseError($this->error); 202 } 203 204 // Validate each address individually. If we encounter an invalid 205 // address, stop iterating and return an error immediately. 206 foreach ($this->addresses as $address) { 207 $valid = $this->_validateAddress($address); 208 209 if ($valid === false || isset($this->error)) { 210 require_once 'PEAR.php'; 211 return PEAR::raiseError($this->error); 212 } 213 214 if (!$this->nestGroups) { 215 $this->structure = array_merge($this->structure, $valid); 216 } else { 217 $this->structure[] = $valid; 218 } 219 } 220 221 return $this->structure; 222 } 223 224 /** 225 * Splits an address into separate addresses. 226 * 227 * @access private 228 * @param string $address The addresses to split. 229 * @return boolean Success or failure. 230 */ 231 function _splitAddresses($address) 232 { 233 if (!empty($this->limit) && count($this->addresses) == $this->limit) { 234 return ''; 235 } 236 237 if ($this->_isGroup($address) && !isset($this->error)) { 238 $split_char = ';'; 239 $is_group = true; 240 } elseif (!isset($this->error)) { 241 $split_char = ','; 242 $is_group = false; 243 } elseif (isset($this->error)) { 244 return false; 245 } 246 247 // Split the string based on the above ten or so lines. 248 $parts = explode($split_char, $address); 249 $string = $this->_splitCheck($parts, $split_char); 250 251 // If a group... 252 if ($is_group) { 253 // If $string does not contain a colon outside of 254 // brackets/quotes etc then something's fubar. 255 256 // First check there's a colon at all: 257 if (strpos($string, ':') === false) { 258 $this->error = 'Invalid address: ' . $string; 259 return false; 260 } 261 262 // Now check it's outside of brackets/quotes: 263 if (!$this->_splitCheck(explode(':', $string), ':')) { 264 return false; 265 } 266 267 // We must have a group at this point, so increase the counter: 268 $this->num_groups++; 269 } 270 271 // $string now contains the first full address/group. 272 // Add to the addresses array. 273 $this->addresses[] = array( 274 'address' => trim($string), 275 'group' => $is_group 276 ); 277 278 // Remove the now stored address from the initial line, the +1 279 // is to account for the explode character. 280 $address = trim(substr($address, strlen($string) + 1)); 281 282 // If the next char is a comma and this was a group, then 283 // there are more addresses, otherwise, if there are any more 284 // chars, then there is another address. 285 if ($is_group && substr($address, 0, 1) == ','){ 286 $address = trim(substr($address, 1)); 287 return $address; 288 289 } elseif (strlen($address) > 0) { 290 return $address; 291 292 } else { 293 return ''; 294 } 295 296 // If you got here then something's off 297 return false; 298 } 299 300 /** 301 * Checks for a group at the start of the string. 302 * 303 * @access private 304 * @param string $address The address to check. 305 * @return boolean Whether or not there is a group at the start of the string. 306 */ 307 function _isGroup($address) 308 { 309 // First comma not in quotes, angles or escaped: 310 $parts = explode(',', $address); 311 $string = $this->_splitCheck($parts, ','); 312 313 // Now we have the first address, we can reliably check for a 314 // group by searching for a colon that's not escaped or in 315 // quotes or angle brackets. 316 if (count($parts = explode(':', $string)) > 1) { 317 $string2 = $this->_splitCheck($parts, ':'); 318 return ($string2 !== $string); 319 } else { 320 return false; 321 } 322 } 323 324 /** 325 * A common function that will check an exploded string. 326 * 327 * @access private 328 * @param array $parts The exloded string. 329 * @param string $char The char that was exploded on. 330 * @return mixed False if the string contains unclosed quotes/brackets, or the string on success. 331 */ 332 function _splitCheck($parts, $char) 333 { 334 $string = $parts[0]; 335 336 $parts_count = count($parts); 337 for ($i = 0; $i < $parts_count; ++$i) { 338 if ($this->_hasUnclosedQuotes($string) 339 || $this->_hasUnclosedBrackets($string, '<>') 340 || $this->_hasUnclosedBrackets($string, '[]') 341 || $this->_hasUnclosedBrackets($string, '()') 342 || substr($string, -1) == '\\') { 343 if (isset($parts[$i + 1])) { 344 $string = $string . $char . $parts[$i + 1]; 345 } else { 346 $this->error = 'Invalid address spec. Unclosed bracket or quotes'; 347 return false; 348 } 349 } else { 350 $this->index = $i; 351 break; 352 } 353 } 354 355 return $string; 356 } 357 358 /** 359 * Checks if a string has unclosed quotes or not. 360 * 361 * @access private 362 * @param string $string The string to check. 363 * @return boolean True if there are unclosed quotes inside the string, 364 * false otherwise. 365 */ 366 function _hasUnclosedQuotes($string) 367 { 368 $string = trim($string); 369 $iMax = strlen($string); 370 $in_quote = false; 371 $i = $slashes = 0; 372 373 for (; $i < $iMax; ++$i) { 374 switch ($string[$i]) { 375 case '\\': 376 ++$slashes; 377 break; 378 379 case '"': 380 if ($slashes % 2 == 0) { 381 $in_quote = !$in_quote; 382 } 383 // Fall through to default action below. 384 385 default: 386 $slashes = 0; 387 break; 388 } 389 } 390 391 return $in_quote; 392 } 393 394 /** 395 * Checks if a string has an unclosed brackets or not. IMPORTANT: 396 * This function handles both angle brackets and square brackets; 397 * 398 * @access private 399 * @param string $string The string to check. 400 * @param string $chars The characters to check for. 401 * @return boolean True if there are unclosed brackets inside the string, false otherwise. 402 */ 403 function _hasUnclosedBrackets($string, $chars) 404 { 405 $num_angle_start = substr_count($string, $chars[0]); 406 $num_angle_end = substr_count($string, $chars[1]); 407 408 $this->_hasUnclosedBracketsSub($string, $num_angle_start, $chars[0]); 409 $this->_hasUnclosedBracketsSub($string, $num_angle_end, $chars[1]); 410 411 if ($num_angle_start < $num_angle_end) { 412 $this->error = 'Invalid address spec. Unmatched quote or bracket (' . $chars . ')'; 413 return false; 414 } else { 415 return ($num_angle_start > $num_angle_end); 416 } 417 } 418 419 /** 420 * Sub function that is used only by hasUnclosedBrackets(). 421 * 422 * @access private 423 * @param string $string The string to check. 424 * @param integer &$num The number of occurences. 425 * @param string $char The character to count. 426 * @return integer The number of occurences of $char in $string, adjusted for backslashes. 427 */ 428 function _hasUnclosedBracketsSub($string, &$num, $char) 429 { 430 $parts = explode($char, $string); 431 $parts_count = count($parts); 432 for ($i = 0; $i < $parts_count; ++$i){ 433 if (substr($parts[$i], -1) == '\\' || $this->_hasUnclosedQuotes($parts[$i])) 434 $num--; 435 if (isset($parts[$i + 1])) 436 $parts[$i + 1] = $parts[$i] . $char . $parts[$i + 1]; 437 } 438 439 return $num; 440 } 441 442 /** 443 * Function to begin checking the address. 444 * 445 * @access private 446 * @param string $address The address to validate. 447 * @return mixed False on failure, or a structured array of address information on success. 448 */ 449 function _validateAddress($address) 450 { 451 $is_group = false; 452 $addresses = array(); 453 454 if ($address['group']) { 455 $is_group = true; 456 457 // Get the group part of the name 458 $parts = explode(':', $address['address']); 459 $groupname = $this->_splitCheck($parts, ':'); 460 $structure = array(); 461 462 // And validate the group part of the name. 463 if (!$this->_validatePhrase($groupname)){ 464 $this->error = 'Group name did not validate.'; 465 return false; 466 } else { 467 // Don't include groups if we are not nesting 468 // them. This avoids returning invalid addresses. 469 if ($this->nestGroups) { 470 $structure = new stdClass; 471 $structure->groupname = $groupname; 472 } 473 } 474 475 $address['address'] = ltrim(substr($address['address'], strlen($groupname . ':'))); 476 } 477 478 // If a group then split on comma and put into an array. 479 // Otherwise, Just put the whole address in an array. 480 if ($is_group) { 481 while (strlen($address['address']) > 0) { 482 $parts = explode(',', $address['address']); 483 $addresses[] = $this->_splitCheck($parts, ','); 484 $address['address'] = trim(substr($address['address'], strlen(end($addresses) . ','))); 485 } 486 } else { 487 $addresses[] = $address['address']; 488 } 489 490 // Check that $addresses is set, if address like this: 491 // Groupname:; 492 // Then errors were appearing. 493 if (!count($addresses)){ 494 $this->error = 'Empty group.'; 495 return false; 496 } 497 498 // Trim the whitespace from all of the address strings. 499 array_map('trim', $addresses); 500 501 // Validate each mailbox. 502 // Format could be one of: name <geezer@domain.com> 503 // geezer@domain.com 504 // geezer 505 // ... or any other format valid by RFC 822. 506 $addresses_count = count($addresses); 507 for ($i = 0; $i < $addresses_count; ++$i) { 508 if (!$this->validateMailbox($addresses[$i])) { 509 if (empty($this->error)) { 510 $this->error = 'Validation failed for: ' . $addresses[$i]; 511 } 512 return false; 513 } 514 } 515 516 // Nested format 517 if ($this->nestGroups) { 518 if ($is_group) { 519 $structure->addresses = $addresses; 520 } else { 521 $structure = $addresses[0]; 522 } 523 524 // Flat format 525 } else { 526 if ($is_group) { 527 $structure = array_merge($structure, $addresses); 528 } else { 529 $structure = $addresses; 530 } 531 } 532 533 return $structure; 534 } 535 536 /** 537 * Function to validate a phrase. 538 * 539 * @access private 540 * @param string $phrase The phrase to check. 541 * @return boolean Success or failure. 542 */ 543 function _validatePhrase($phrase) 544 { 545 // Splits on one or more Tab or space. 546 $parts = preg_split('/[ \\x09]+/', $phrase, -1, PREG_SPLIT_NO_EMPTY); 547 548 $phrase_parts = array(); 549 while (count($parts) > 0){ 550 $phrase_parts[] = $this->_splitCheck($parts, ' '); 551 for ($i = 0; $i < $this->index + 1; ++$i) 552 array_shift($parts); 553 } 554 555 foreach ($phrase_parts as $part) { 556 // If quoted string: 557 if (substr($part, 0, 1) == '"') { 558 if (!$this->_validateQuotedString($part)) { 559 return false; 560 } 561 continue; 562 } 563 564 // Otherwise it's an atom: 565 if (!$this->_validateAtom($part)) return false; 566 } 567 568 return true; 569 } 570 571 /** 572 * Function to validate an atom which from rfc822 is: 573 * atom = 1*<any CHAR except specials, SPACE and CTLs> 574 * 575 * If validation ($this->validate) has been turned off, then 576 * validateAtom() doesn't actually check anything. This is so that you 577 * can split a list of addresses up before encoding personal names 578 * (umlauts, etc.), for example. 579 * 580 * @access private 581 * @param string $atom The string to check. 582 * @return boolean Success or failure. 583 */ 584 function _validateAtom($atom) 585 { 586 if (!$this->validate) { 587 // Validation has been turned off; assume the atom is okay. 588 return true; 589 } 590 591 // Check for any char from ASCII 0 - ASCII 127 592 if (!preg_match('/^[\\x00-\\x7E]+$/i', $atom, $matches)) { 593 return false; 594 } 595 596 // Check for specials: 597 if (preg_match('/[][()<>@,;\\:". ]/', $atom)) { 598 return false; 599 } 600 601 // Check for control characters (ASCII 0-31): 602 if (preg_match('/[\\x00-\\x1F]+/', $atom)) { 603 return false; 604 } 605 606 return true; 607 } 608 609 /** 610 * Function to validate quoted string, which is: 611 * quoted-string = <"> *(qtext/quoted-pair) <"> 612 * 613 * @access private 614 * @param string $qstring The string to check 615 * @return boolean Success or failure. 616 */ 617 function _validateQuotedString($qstring) 618 { 619 // Leading and trailing " 620 $qstring = substr($qstring, 1, -1); 621 622 // Perform check, removing quoted characters first. 623 return !preg_match('/[\x0D\\\\"]/', preg_replace('/\\\\./', '', $qstring)); 624 } 625 626 /** 627 * Function to validate a mailbox, which is: 628 * mailbox = addr-spec ; simple address 629 * / phrase route-addr ; name and route-addr 630 * 631 * @access public 632 * @param string &$mailbox The string to check. 633 * @return boolean Success or failure. 634 */ 635 function validateMailbox(&$mailbox) 636 { 637 // A couple of defaults. 638 $phrase = ''; 639 $comment = ''; 640 $comments = array(); 641 642 // Catch any RFC822 comments and store them separately. 643 $_mailbox = $mailbox; 644 while (strlen(trim($_mailbox)) > 0) { 645 $parts = explode('(', $_mailbox); 646 $before_comment = $this->_splitCheck($parts, '('); 647 if ($before_comment != $_mailbox) { 648 // First char should be a (. 649 $comment = substr(str_replace($before_comment, '', $_mailbox), 1); 650 $parts = explode(')', $comment); 651 $comment = $this->_splitCheck($parts, ')'); 652 $comments[] = $comment; 653 654 // +2 is for the brackets 655 $_mailbox = substr($_mailbox, strpos($_mailbox, '('.$comment)+strlen($comment)+2); 656 } else { 657 break; 658 } 659 } 660 661 foreach ($comments as $comment) { 662 $mailbox = str_replace("($comment)", '', $mailbox); 663 } 664 665 $mailbox = trim($mailbox); 666 667 // Check for name + route-addr 668 if (substr($mailbox, -1) == '>' && substr($mailbox, 0, 1) != '<') { 669 $parts = explode('<', $mailbox); 670 $name = $this->_splitCheck($parts, '<'); 671 672 $phrase = trim($name); 673 $route_addr = trim(substr($mailbox, strlen($name.'<'), -1)); 674 675 if ($this->_validatePhrase($phrase) === false || ($route_addr = $this->_validateRouteAddr($route_addr)) === false) { 676 return false; 677 } 678 679 // Only got addr-spec 680 } else { 681 // First snip angle brackets if present. 682 if (substr($mailbox, 0, 1) == '<' && substr($mailbox, -1) == '>') { 683 $addr_spec = substr($mailbox, 1, -1); 684 } else { 685 $addr_spec = $mailbox; 686 } 687 688 if (($addr_spec = $this->_validateAddrSpec($addr_spec)) === false) { 689 return false; 690 } 691 } 692 693 // Construct the object that will be returned. 694 $mbox = new stdClass(); 695 696 // Add the phrase (even if empty) and comments 697 $mbox->personal = $phrase; 698 $mbox->comment = isset($comments) ? $comments : array(); 699 700 if (isset($route_addr)) { 701 $mbox->mailbox = $route_addr['local_part']; 702 $mbox->host = $route_addr['domain']; 703 $route_addr['adl'] !== '' ? $mbox->adl = $route_addr['adl'] : ''; 704 } else { 705 $mbox->mailbox = $addr_spec['local_part']; 706 $mbox->host = $addr_spec['domain']; 707 } 708 709 $mailbox = $mbox; 710 return true; 711 } 712 713 /** 714 * This function validates a route-addr which is: 715 * route-addr = "<" [route] addr-spec ">" 716 * 717 * Angle brackets have already been removed at the point of 718 * getting to this function. 719 * 720 * @access private 721 * @param string $route_addr The string to check. 722 * @return mixed False on failure, or an array containing validated address/route information on success. 723 */ 724 function _validateRouteAddr($route_addr) 725 { 726 // Check for colon. 727 if (strpos($route_addr, ':') !== false) { 728 $parts = explode(':', $route_addr); 729 $route = $this->_splitCheck($parts, ':'); 730 } else { 731 $route = $route_addr; 732 } 733 734 // If $route is same as $route_addr then the colon was in 735 // quotes or brackets or, of course, non existent. 736 if ($route === $route_addr){ 737 unset($route); 738 $addr_spec = $route_addr; 739 if (($addr_spec = $this->_validateAddrSpec($addr_spec)) === false) { 740 return false; 741 } 742 } else { 743 // Validate route part. 744 if (($route = $this->_validateRoute($route)) === false) { 745 return false; 746 } 747 748 $addr_spec = substr($route_addr, strlen($route . ':')); 749 750 // Validate addr-spec part. 751 if (($addr_spec = $this->_validateAddrSpec($addr_spec)) === false) { 752 return false; 753 } 754 } 755 756 if (isset($route)) { 757 $return['adl'] = $route; 758 } else { 759 $return['adl'] = ''; 760 } 761 762 $return = array_merge($return, $addr_spec); 763 return $return; 764 } 765 766 /** 767 * Function to validate a route, which is: 768 * route = 1#("@" domain) ":" 769 * 770 * @access private 771 * @param string $route The string to check. 772 * @return mixed False on failure, or the validated $route on success. 773 */ 774 function _validateRoute($route) 775 { 776 // Split on comma. 777 $domains = explode(',', trim($route)); 778 779 foreach ($domains as $domain) { 780 $domain = str_replace('@', '', trim($domain)); 781 if (!$this->_validateDomain($domain)) return false; 782 } 783 784 return $route; 785 } 786 787 /** 788 * Function to validate a domain, though this is not quite what 789 * you expect of a strict internet domain. 790 * 791 * domain = sub-domain *("." sub-domain) 792 * 793 * @access private 794 * @param string $domain The string to check. 795 * @return mixed False on failure, or the validated domain on success. 796 */ 797 function _validateDomain($domain) 798 { 799 // Note the different use of $subdomains and $sub_domains 800 $subdomains = explode('.', $domain); 801 802 while (count($subdomains) > 0) { 803 $sub_domains[] = $this->_splitCheck($subdomains, '.'); 804 for ($i = 0; $i < $this->index + 1; ++$i) 805 array_shift($subdomains); 806 } 807 808 foreach ($sub_domains as $sub_domain) { 809 if (!$this->_validateSubdomain(trim($sub_domain))) 810 return false; 811 } 812 813 // Managed to get here, so return input. 814 return $domain; 815 } 816 817 /** 818 * Function to validate a subdomain: 819 * subdomain = domain-ref / domain-literal 820 * 821 * @access private 822 * @param string $subdomain The string to check. 823 * @return boolean Success or failure. 824 */ 825 function _validateSubdomain($subdomain) 826 { 827 if (preg_match('|^\[(.*)]$|', $subdomain, $arr)){ 828 if (!$this->_validateDliteral($arr[1])) return false; 829 } else { 830 if (!$this->_validateAtom($subdomain)) return false; 831 } 832 833 // Got here, so return successful. 834 return true; 835 } 836 837 /** 838 * Function to validate a domain literal: 839 * domain-literal = "[" *(dtext / quoted-pair) "]" 840 * 841 * @access private 842 * @param string $dliteral The string to check. 843 * @return boolean Success or failure. 844 */ 845 function _validateDliteral($dliteral) 846 { 847 return !preg_match('/(.)[][\x0D\\\\]/', $dliteral, $matches) && $matches[1] != '\\'; 848 } 849 850 /** 851 * Function to validate an addr-spec. 852 * 853 * addr-spec = local-part "@" domain 854 * 855 * @access private 856 * @param string $addr_spec The string to check. 857 * @return mixed False on failure, or the validated addr-spec on success. 858 */ 859 function _validateAddrSpec($addr_spec) 860 { 861 $addr_spec = trim($addr_spec); 862 863 // Split on @ sign if there is one. 864 if (strpos($addr_spec, '@') !== false) { 865 $parts = explode('@', $addr_spec); 866 $local_part = $this->_splitCheck($parts, '@'); 867 $domain = substr($addr_spec, strlen($local_part . '@')); 868 869 // No @ sign so assume the default domain. 870 } else { 871 $local_part = $addr_spec; 872 $domain = $this->default_domain; 873 } 874 875 if (($local_part = $this->_validateLocalPart($local_part)) === false) return false; 876 if (($domain = $this->_validateDomain($domain)) === false) return false; 877 878 // Got here so return successful. 879 return array('local_part' => $local_part, 'domain' => $domain); 880 } 881 882 /** 883 * Function to validate the local part of an address: 884 * local-part = word *("." word) 885 * 886 * @access private 887 * @param string $local_part 888 * @return mixed False on failure, or the validated local part on success. 889 */ 890 function _validateLocalPart($local_part) 891 { 892 $parts = explode('.', $local_part); 893 $words = array(); 894 895 // Split the local_part into words. 896 while (count($parts) > 0){ 897 $words[] = $this->_splitCheck($parts, '.'); 898 for ($i = 0; $i < $this->index + 1; ++$i) { 899 array_shift($parts); 900 } 901 } 902 903 // Validate each word. 904 foreach ($words as $word) { 905 // If this word contains an unquoted space, it is invalid. (6.2.4) 906 if (strpos($word, ' ') && $word[0] !== '"') 907 { 908 return false; 909 } 910 911 if ($this->_validatePhrase(trim($word)) === false) return false; 912 } 913 914 // Managed to get here, so return the input. 915 return $local_part; 916 } 917 918 /** 919 * Returns an approximate count of how many addresses are in the 920 * given string. This is APPROXIMATE as it only splits based on a 921 * comma which has no preceding backslash. Could be useful as 922 * large amounts of addresses will end up producing *large* 923 * structures when used with parseAddressList(). 924 * 925 * @param string $data Addresses to count 926 * @return int Approximate count 927 */ 928 function approximateCount($data) 929 { 930 return count(preg_split('/(?<!\\\\),/', $data)); 931 } 932 933 /** 934 * This is a email validating function separate to the rest of the 935 * class. It simply validates whether an email is of the common 936 * internet form: <user>@<domain>. This can be sufficient for most 937 * people. Optional stricter mode can be utilised which restricts 938 * mailbox characters allowed to alphanumeric, full stop, hyphen 939 * and underscore. 940 * 941 * @param string $data Address to check 942 * @param boolean $strict Optional stricter mode 943 * @return mixed False if it fails, an indexed array 944 * username/domain if it matches 945 */ 946 function isValidInetAddress($data, $strict = false) 947 { 948 $regex = $strict ? '/^([.0-9a-z_+-]+)@(([0-9a-z-]+\.)+[0-9a-z]{2,})$/i' : '/^([*+!.&#$|\'\\%\/0-9a-z^_`{}=?~:-]+)@(([0-9a-z-]+\.)+[0-9a-z]{2,})$/i'; 949 if (preg_match($regex, trim($data), $matches)) { 950 return array($matches[1], $matches[2]); 951 } else { 952 return false; 953 } 954 } 955 159 956 } 160 161 /**162 * Starts the whole process. The address must either be set here163 * or when creating the object. One or the other.164 *165 * @access public166 * @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 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 dirname(__FILE__).'/../../PEAR/PEAR.php';199 return PEAR::raiseError($this->error);200 }201 202 // Validate each address individually. If we encounter an invalid203 // 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 * @access private226 * @param string $address The addresses to split.227 * @return boolean Success or failure.228 */229 function _splitAddresses($address)230 {231 if (!empty($this->limit) && count($this->addresses) == $this->limit) {232 return '';233 }234 235 if ($this->_isGroup($address) && !isset($this->error)) {236 $split_char = ';';237 $is_group = true;238 } elseif (!isset($this->error)) {239 $split_char = ',';240 $is_group = false;241 } elseif (isset($this->error)) {242 return false;243 }244 245 // Split the string based on the above ten or so lines.246 $parts = explode($split_char, $address);247 $string = $this->_splitCheck($parts, $split_char);248 249 // If a group...250 if ($is_group) {251 // If $string does not contain a colon outside of252 // brackets/quotes etc then something's fubar.253 254 // First check there's a colon at all:255 if (strpos($string, ':') === false) {256 $this->error = 'Invalid address: ' . $string;257 return false;258 }259 260 // Now check it's outside of brackets/quotes:261 if (!$this->_splitCheck(explode(':', $string), ':')) {262 return false;263 }264 265 // We must have a group at this point, so increase the counter:266 $this->num_groups++;267 }268 269 // $string now contains the first full address/group.270 // Add to the addresses array.271 $this->addresses[] = array(272 'address' => trim($string),273 'group' => $is_group274 );275 276 // Remove the now stored address from the initial line, the +1277 // is to account for the explode character.278 $address = trim(substr($address, strlen($string) + 1));279 280 // If the next char is a comma and this was a group, then281 // there are more addresses, otherwise, if there are any more282 // chars, then there is another address.283 if ($is_group && substr($address, 0, 1) == ','){284 $address = trim(substr($address, 1));285 return $address;286 287 } elseif (strlen($address) > 0) {288 return $address;289 290 } else {291 return '';292 }293 294 // If you got here then something's off295 return false;296 }297 298 /**299 * Checks for a group at the start of the string.300 *301 * @access private302 * @param string $address The address to check.303 * @return boolean Whether or not there is a group at the start of the string.304 */305 function _isGroup($address)306 {307 // First comma not in quotes, angles or escaped:308 $parts = explode(',', $address);309 $string = $this->_splitCheck($parts, ',');310 311 // Now we have the first address, we can reliably check for a312 // group by searching for a colon that's not escaped or in313 // quotes or angle brackets.314 if (count($parts = explode(':', $string)) > 1) {315 $string2 = $this->_splitCheck($parts, ':');316 return ($string2 !== $string);317 } else {318 return false;319 }320 }321 322 /**323 * A common function that will check an exploded string.324 *325 * @access private326 * @param array $parts The exloded string.327 * @param string $char The char that was exploded on.328 * @return mixed False if the string contains unclosed quotes/brackets, or the string on success.329 */330 function _splitCheck($parts, $char)331 {332 $string = $parts[0];333 334 $parts_count = count($parts);335 for ($i = 0; $i < $parts_count; ++$i) {336 if ($this->_hasUnclosedQuotes($string)337 || $this->_hasUnclosedBrackets($string, '<>')338 || $this->_hasUnclosedBrackets($string, '[]')339 || $this->_hasUnclosedBrackets($string, '()')340 || substr($string, -1) == '\\') {341 if (isset($parts[$i + 1])) {342 $string = $string . $char . $parts[$i + 1];343 } else {344 $this->error = 'Invalid address spec. Unclosed bracket or quotes';345 return false;346 }347 } else {348 $this->index = $i;349 break;350 }351 }352 353 return $string;354 }355 356 /**357 * Checks if a string has unclosed quotes or not.358 *359 * @access private360 * @param string $string The string to check.361 * @return boolean True if there are unclosed quotes inside the string,362 * false otherwise.363 */364 function _hasUnclosedQuotes($string)365 {366 $string = trim($string);367 $iMax = strlen($string);368 $in_quote = false;369 $i = $slashes = 0;370 371 for (; $i < $iMax; ++$i) {372 switch ($string[$i]) {373 case '\\':374 ++$slashes;375 break;376 377 case '"':378 if ($slashes % 2 == 0) {379 $in_quote = !$in_quote;380 }381 // Fall through to default action below.382 383 default:384 $slashes = 0;385 break;386 }387 }388 389 return $in_quote;390 }391 392 /**393 * Checks if a string has an unclosed brackets or not. IMPORTANT:394 * This function handles both angle brackets and square brackets;395 *396 * @access private397 * @param string $string The string to check.398 * @param string $chars The characters to check for.399 * @return boolean True if there are unclosed brackets inside the string, false otherwise.400 */401 function _hasUnclosedBrackets($string, $chars)402 {403 $num_angle_start = substr_count($string, $chars[0]);404 $num_angle_end = substr_count($string, $chars[1]);405 406 $this->_hasUnclosedBracketsSub($string, $num_angle_start, $chars[0]);407 $this->_hasUnclosedBracketsSub($string, $num_angle_end, $chars[1]);408 409 if ($num_angle_start < $num_angle_end) {410 $this->error = 'Invalid address spec. Unmatched quote or bracket (' . $chars . ')';411 return false;412 } else {413 return ($num_angle_start > $num_angle_end);414 }415 }416 417 /**418 * Sub function that is used only by hasUnclosedBrackets().419 *420 * @access private421 * @param string $string The string to check.422 * @param integer &$num The number of occurences.423 * @param string $char The character to count.424 * @return integer The number of occurences of $char in $string, adjusted for backslashes.425 */426 function _hasUnclosedBracketsSub($string, &$num, $char)427 {428 $parts = explode($char, $string);429 $parts_count = count($parts);430 for ($i = 0; $i < $parts_count; ++$i){431 if (substr($parts[$i], -1) == '\\' || $this->_hasUnclosedQuotes($parts[$i]))432 $num--;433 if (isset($parts[$i + 1]))434 $parts[$i + 1] = $parts[$i] . $char . $parts[$i + 1];435 }436 437 return $num;438 }439 440 /**441 * Function to begin checking the address.442 *443 * @access private444 * @param string $address The address to validate.445 * @return mixed False on failure, or a structured array of address information on success.446 */447 function _validateAddress($address)448 {449 $is_group = false;450 $addresses = array();451 452 if ($address['group']) {453 $is_group = true;454 455 // Get the group part of the name456 $parts = explode(':', $address['address']);457 $groupname = $this->_splitCheck($parts, ':');458 $structure = array();459 460 // And validate the group part of the name.461 if (!$this->_validatePhrase($groupname)){462 $this->error = 'Group name did not validate.';463 return false;464 } else {465 // Don't include groups if we are not nesting466 // them. This avoids returning invalid addresses.467 if ($this->nestGroups) {468 $structure = new stdClass;469 $structure->groupname = $groupname;470 }471 }472 473 $address['address'] = ltrim(substr($address['address'], strlen($groupname . ':')));474 }475 476 // If a group then split on comma and put into an array.477 // Otherwise, Just put the whole address in an array.478 if ($is_group) {479 while (strlen($address['address']) > 0) {480 $parts = explode(',', $address['address']);481 $addresses[] = $this->_splitCheck($parts, ',');482 $address['address'] = trim(substr($address['address'], strlen(end($addresses) . ',')));483 }484 } else {485 $addresses[] = $address['address'];486 }487 488 // Check that $addresses is set, if address like this:489 // Groupname:;490 // Then errors were appearing.491 if (!count($addresses)){492 $this->error = 'Empty group.';493 return false;494 }495 496 // Trim the whitespace from all of the address strings.497 array_map('trim', $addresses);498 499 // Validate each mailbox.500 // Format could be one of: name <geezer@domain.com>501 // geezer@domain.com502 // geezer503 // ... or any other format valid by RFC 822.504 $addresses_count = count($addresses);505 for ($i = 0; $i < $addresses_count; ++$i) {506 if (!$this->validateMailbox($addresses[$i])) {507 if (empty($this->error)) {508 $this->error = 'Validation failed for: ' . $addresses[$i];509 }510 return false;511 }512 }513 514 // Nested format515 if ($this->nestGroups) {516 if ($is_group) {517 $structure->addresses = $addresses;518 } else {519 $structure = $addresses[0];520 }521 522 // Flat format523 } else {524 if ($is_group) {525 $structure = array_merge($structure, $addresses);526 } else {527 $structure = $addresses;528 }529 }530 531 return $structure;532 }533 534 /**535 * Function to validate a phrase.536 *537 * @access private538 * @param string $phrase The phrase to check.539 * @return boolean Success or failure.540 */541 function _validatePhrase($phrase)542 {543 // Splits on one or more Tab or space.544 $parts = preg_split('/[ \\x09]+/', $phrase, -1, PREG_SPLIT_NO_EMPTY);545 546 $phrase_parts = array();547 while (count($parts) > 0){548 $phrase_parts[] = $this->_splitCheck($parts, ' ');549 for ($i = 0; $i < $this->index + 1; ++$i)550 array_shift($parts);551 }552 553 foreach ($phrase_parts as $part) {554 // If quoted string:555 if (substr($part, 0, 1) == '"') {556 if (!$this->_validateQuotedString($part)) {557 return false;558 }559 continue;560 }561 562 // Otherwise it's an atom:563 if (!$this->_validateAtom($part)) return false;564 }565 566 return true;567 }568 569 /**570 * Function to validate an atom which from rfc822 is:571 * atom = 1*<any CHAR except specials, SPACE and CTLs>572 *573 * If validation ($this->validate) has been turned off, then574 * validateAtom() doesn't actually check anything. This is so that you575 * can split a list of addresses up before encoding personal names576 * (umlauts, etc.), for example.577 *578 * @access private579 * @param string $atom The string to check.580 * @return boolean Success or failure.581 */582 function _validateAtom($atom)583 {584 if (!$this->validate) {585 // Validation has been turned off; assume the atom is okay.586 return true;587 }588 589 // Check for any char from ASCII 0 - ASCII 127590 if (!preg_match('/^[\\x00-\\x7E]+$/i', $atom, $matches)) {591 return false;592 }593 594 // Check for specials:595 if (preg_match('/[][()<>@,;\\:". ]/', $atom)) {596 return false;597 }598 599 // Check for control characters (ASCII 0-31):600 if (preg_match('/[\\x00-\\x1F]+/', $atom)) {601 return false;602 }603 604 return true;605 }606 607 /**608 * Function to validate quoted string, which is:609 * quoted-string = <"> *(qtext/quoted-pair) <">610 *611 * @access private612 * @param string $qstring The string to check613 * @return boolean Success or failure.614 */615 function _validateQuotedString($qstring)616 {617 // Leading and trailing "618 $qstring = substr($qstring, 1, -1);619 620 // Perform check, removing quoted characters first.621 return !preg_match('/[\x0D\\\\"]/', preg_replace('/\\\\./', '', $qstring));622 }623 624 /**625 * Function to validate a mailbox, which is:626 * mailbox = addr-spec ; simple address627 * / phrase route-addr ; name and route-addr628 *629 * @access public630 * @param string &$mailbox The string to check.631 * @return boolean Success or failure.632 */633 function validateMailbox(&$mailbox)634 {635 // A couple of defaults.636 $phrase = '';637 $comment = '';638 $comments = array();639 640 // Catch any RFC822 comments and store them separately.641 $_mailbox = $mailbox;642 while (strlen(trim($_mailbox)) > 0) {643 $parts = explode('(', $_mailbox);644 $before_comment = $this->_splitCheck($parts, '(');645 if ($before_comment != $_mailbox) {646 // First char should be a (.647 $comment = substr(str_replace($before_comment, '', $_mailbox), 1);648 $parts = explode(')', $comment);649 $comment = $this->_splitCheck($parts, ')');650 $comments[] = $comment;651 652 // +2 is for the brackets653 $_mailbox = substr($_mailbox, strpos($_mailbox, '('.$comment)+strlen($comment)+2);654 } else {655 break;656 }657 }658 659 foreach ($comments as $comment) {660 $mailbox = str_replace("($comment)", '', $mailbox);661 }662 663 $mailbox = trim($mailbox);664 665 // Check for name + route-addr666 if (substr($mailbox, -1) == '>' && substr($mailbox, 0, 1) != '<') {667 $parts = explode('<', $mailbox);668 $name = $this->_splitCheck($parts, '<');669 670 $phrase = trim($name);671 $route_addr = trim(substr($mailbox, strlen($name.'<'), -1));672 673 if ($this->_validatePhrase($phrase) === false || ($route_addr = $this->_validateRouteAddr($route_addr)) === false) {674 return false;675 }676 677 // Only got addr-spec678 } else {679 // First snip angle brackets if present.680 if (substr($mailbox, 0, 1) == '<' && substr($mailbox, -1) == '>') {681 $addr_spec = substr($mailbox, 1, -1);682 } else {683 $addr_spec = $mailbox;684 }685 686 if (($addr_spec = $this->_validateAddrSpec($addr_spec)) === false) {687 return false;688 }689 }690 691 // Construct the object that will be returned.692 $mbox = new stdClass();693 694 // Add the phrase (even if empty) and comments695 $mbox->personal = $phrase;696 $mbox->comment = isset($comments) ? $comments : array();697 698 if (isset($route_addr)) {699 $mbox->mailbox = $route_addr['local_part'];700 $mbox->host = $route_addr['domain'];701 $route_addr['adl'] !== '' ? $mbox->adl = $route_addr['adl'] : '';702 } else {703 $mbox->mailbox = $addr_spec['local_part'];704 $mbox->host = $addr_spec['domain'];705 }706 707 $mailbox = $mbox;708 return true;709 }710 711 /**712 * This function validates a route-addr which is:713 * route-addr = "<" [route] addr-spec ">"714 *715 * Angle brackets have already been removed at the point of716 * getting to this function.717 *718 * @access private719 * @param string $route_addr The string to check.720 * @return mixed False on failure, or an array containing validated address/route information on success.721 */722 function _validateRouteAddr($route_addr)723 {724 // Check for colon.725 if (strpos($route_addr, ':') !== false) {726 $parts = explode(':', $route_addr);727 $route = $this->_splitCheck($parts, ':');728 } else {729 $route = $route_addr;730 }731 732 // If $route is same as $route_addr then the colon was in733 // quotes or brackets or, of course, non existent.734 if ($route === $route_addr){735 unset($route);736 $addr_spec = $route_addr;737 if (($addr_spec = $this->_validateAddrSpec($addr_spec)) === false) {738 return false;739 }740 } else {741 // Validate route part.742 if (($route = $this->_validateRoute($route)) === false) {743 return false;744 }745 746 $addr_spec = substr($route_addr, strlen($route . ':'));747 748 // Validate addr-spec part.749 if (($addr_spec = $this->_validateAddrSpec($addr_spec)) === false) {750 return false;751 }752 }753 754 if (isset($route)) {755 $return['adl'] = $route;756 } else {757 $return['adl'] = '';758 }759 760 $return = array_merge($return, $addr_spec);761 return $return;762 }763 764 /**765 * Function to validate a route, which is:766 * route = 1#("@" domain) ":"767 *768 * @access private769 * @param string $route The string to check.770 * @return mixed False on failure, or the validated $route on success.771 */772 function _validateRoute($route)773 {774 // Split on comma.775 $domains = explode(',', trim($route));776 777 foreach ($domains as $domain) {778 $domain = str_replace('@', '', trim($domain));779 if (!$this->_validateDomain($domain)) return false;780 }781 782 return $route;783 }784 785 /**786 * Function to validate a domain, though this is not quite what787 * you expect of a strict internet domain.788 *789 * domain = sub-domain *("." sub-domain)790 *791 * @access private792 * @param string $domain The string to check.793 * @return mixed False on failure, or the validated domain on success.794 */795 function _validateDomain($domain)796 {797 // Note the different use of $subdomains and $sub_domains798 $subdomains = explode('.', $domain);799 800 while (count($subdomains) > 0) {801 $sub_domains[] = $this->_splitCheck($subdomains, '.');802 for ($i = 0; $i < $this->index + 1; ++$i)803 array_shift($subdomains);804 }805 806 foreach ($sub_domains as $sub_domain) {807 if (!$this->_validateSubdomain(trim($sub_domain)))808 return false;809 }810 811 // Managed to get here, so return input.812 return $domain;813 }814 815 /**816 * Function to validate a subdomain:817 * subdomain = domain-ref / domain-literal818 *819 * @access private820 * @param string $subdomain The string to check.821 * @return boolean Success or failure.822 */823 function _validateSubdomain($subdomain)824 {825 if (preg_match('|^\[(.*)]$|', $subdomain, $arr)){826 if (!$this->_validateDliteral($arr[1])) return false;827 } else {828 if (!$this->_validateAtom($subdomain)) return false;829 }830 831 // Got here, so return successful.832 return true;833 }834 835 /**836 * Function to validate a domain literal:837 * domain-literal = "[" *(dtext / quoted-pair) "]"838 *839 * @access private840 * @param string $dliteral The string to check.841 * @return boolean Success or failure.842 */843 function _validateDliteral($dliteral)844 {845 return !preg_match('/(.)[][\x0D\\\\]/', $dliteral, $matches) && $matches[1] != '\\';846 }847 848 /**849 * Function to validate an addr-spec.850 *851 * addr-spec = local-part "@" domain852 *853 * @access private854 * @param string $addr_spec The string to check.855 * @return mixed False on failure, or the validated addr-spec on success.856 */857 function _validateAddrSpec($addr_spec)858 {859 $addr_spec = trim($addr_spec);860 861 // Split on @ sign if there is one.862 if (strpos($addr_spec, '@') !== false) {863 $parts = explode('@', $addr_spec);864 $local_part = $this->_splitCheck($parts, '@');865 $domain = substr($addr_spec, strlen($local_part . '@'));866 867 // No @ sign so assume the default domain.868 } else {869 $local_part = $addr_spec;870 $domain = $this->default_domain;871 }872 873 if (($local_part = $this->_validateLocalPart($local_part)) === false) return false;874 if (($domain = $this->_validateDomain($domain)) === false) return false;875 876 // Got here so return successful.877 return array('local_part' => $local_part, 'domain' => $domain);878 }879 880 /**881 * Function to validate the local part of an address:882 * local-part = word *("." word)883 *884 * @access private885 * @param string $local_part886 * @return mixed False on failure, or the validated local part on success.887 */888 function _validateLocalPart($local_part)889 {890 $parts = explode('.', $local_part);891 $words = array();892 893 // Split the local_part into words.894 while (count($parts) > 0){895 $words[] = $this->_splitCheck($parts, '.');896 for ($i = 0; $i < $this->index + 1; ++$i) {897 array_shift($parts);898 }899 }900 901 // Validate each word.902 foreach ($words as $word) {903 // If this word contains an unquoted space, it is invalid. (6.2.4)904 if (strpos($word, ' ') && $word[0] !== '"')905 {906 return false;907 }908 909 if ($this->_validatePhrase(trim($word)) === false) return false;910 }911 912 // Managed to get here, so return the input.913 return $local_part;914 }915 916 /**917 * Returns an approximate count of how many addresses are in the918 * given string. This is APPROXIMATE as it only splits based on a919 * comma which has no preceding backslash. Could be useful as920 * large amounts of addresses will end up producing *large*921 * structures when used with parseAddressList().922 *923 * @param string $data Addresses to count924 * @return int Approximate count925 */926 function approximateCount($data)927 {928 return count(preg_split('/(?<!\\\\),/', $data));929 }930 931 /**932 * This is a email validating function separate to the rest of the933 * class. It simply validates whether an email is of the common934 * internet form: <user>@<domain>. This can be sufficient for most935 * people. Optional stricter mode can be utilised which restricts936 * mailbox characters allowed to alphanumeric, full stop, hyphen937 * and underscore.938 *939 * @param string $data Address to check940 * @param boolean $strict Optional stricter mode941 * @return mixed False if it fails, an indexed array942 * username/domain if it matches943 */944 function isValidInetAddress($data, $strict = false)945 {946 $regex = $strict ? '/^([.0-9a-z_+-]+)@(([0-9a-z-]+\.)+[0-9a-z]{2,})$/i' : '/^([*+!.&#$|\'\\%\/0-9a-z^_`{}=?~:-]+)@(([0-9a-z-]+\.)+[0-9a-z]{2,})$/i';947 if (preg_match($regex, trim($data), $matches)) {948 return array($matches[1], $matches[2]);949 } else {950 return false;951 }952 }953 954 957 } 958
Note: See TracChangeset
for help on using the changeset viewer.