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