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