[TASK] Use strict comparison for strings
[Packages/TYPO3.CMS.git] / typo3 / sysext / core / Classes / Mail / Rfc822AddressesParser.php
1 <?php
2 namespace 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 */
63 class 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 = null;
106
107 /**
108 * An internal counter/pointer.
109 *
110 * @var int $index
111 */
112 private $index = null;
113
114 /**
115 * The number of groups that have been found in the address list.
116 *
117 * @var int $num_groups
118 * @access public
119 */
120 private $num_groups = 0;
121
122 /**
123 * A limit after which processing stops
124 *
125 * @var int $limit
126 */
127 private $limit = null;
128
129 /**
130 * Sets up the object.
131 *
132 * @access public
133 * @param string $address The address(es) to validate.
134 * @param string $default_domain Default domain/host etc. If not supplied, will be set to localhost.
135 * @param bool $validate Whether to validate atoms. Turn this off if you need to run addresses through before encoding the personal names, for instance.
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 * @access public
158 * @param string $address The address(es) to validate.
159 * @param string $default_domain Default domain/host etc.
160 * @param bool $nest_groups Whether to return the structure with groups nested for easier viewing.
161 * @param bool $validate Whether to validate atoms. Turn this off if you need to run addresses through before encoding the personal names, for instance.
162 * @return array A structured array of addresses.
163 */
164 public function parseAddressList($address = null, $default_domain = null, $validate = null, $limit = null)
165 {
166 if (isset($address)) {
167 $this->address = $address;
168 }
169 if (isset($default_domain)) {
170 $this->default_domain = $default_domain;
171 }
172 if (isset($validate)) {
173 $this->validate = $validate;
174 }
175 if (isset($limit)) {
176 $this->limit = $limit;
177 }
178 $this->structure = [];
179 $this->addresses = [];
180 $this->error = null;
181 $this->index = null;
182 // Unfold any long lines in $this->address.
183 $this->address = preg_replace('/\\r?\\n/', '
184 ', $this->address);
185 $this->address = preg_replace('/\\r\\n(\\t| )+/', ' ', $this->address);
186 while ($this->address = $this->_splitAddresses($this->address)) {
187 }
188 if ($this->address === false || isset($this->error)) {
189 throw new \InvalidArgumentException($this->error, 1294681466);
190 }
191 // Validate each address individually. If we encounter an invalid
192 // address, stop iterating and return an error immediately.
193 foreach ($this->addresses as $address) {
194 $valid = $this->_validateAddress($address);
195 if ($valid === false || isset($this->error)) {
196 throw new \InvalidArgumentException($this->error, 1294681467);
197 }
198 $this->structure = array_merge($this->structure, $valid);
199 }
200 return $this->structure;
201 }
202
203 /**
204 * Splits an address into separate addresses.
205 *
206 * @access private
207 * @param string $address The addresses to split.
208 * @return bool Success or failure.
209 */
210 protected function _splitAddresses($address)
211 {
212 if (!empty($this->limit) && count($this->addresses) == $this->limit) {
213 return '';
214 }
215 if ($this->_isGroup($address) && !isset($this->error)) {
216 $split_char = ';';
217 $is_group = true;
218 } elseif (!isset($this->error)) {
219 $split_char = ',';
220 $is_group = false;
221 } elseif (isset($this->error)) {
222 return false;
223 }
224 // Split the string based on the above ten or so lines.
225 $parts = explode($split_char, $address);
226 $string = $this->_splitCheck($parts, $split_char);
227 // If a group...
228 if ($is_group) {
229 // If $string does not contain a colon outside of
230 // brackets/quotes etc then something's fubar.
231 // First check there's a colon at all:
232 if (strpos($string, ':') === false) {
233 $this->error = 'Invalid address: ' . $string;
234 return false;
235 }
236 // Now check it's outside of brackets/quotes:
237 if (!$this->_splitCheck(explode(':', $string), ':')) {
238 return false;
239 }
240 // We must have a group at this point, so increase the counter:
241 $this->num_groups++;
242 }
243 // $string now contains the first full address/group.
244 // Add to the addresses array.
245 $this->addresses[] = [
246 'address' => trim($string),
247 'group' => $is_group
248 ];
249 // Remove the now stored address from the initial line, the +1
250 // is to account for the explode character.
251 $address = trim(substr($address, strlen($string) + 1));
252 // If the next char is a comma and this was a group, then
253 // there are more addresses, otherwise, if there are any more
254 // chars, then there is another address.
255 if ($is_group && $address[0] === ',') {
256 $address = trim(substr($address, 1));
257 return $address;
258 } elseif ($address !== '') {
259 return $address;
260 } else {
261 return '';
262 }
263 }
264
265 /**
266 * Checks for a group at the start of the string.
267 *
268 * @access private
269 * @param string $address The address to check.
270 * @return bool Whether or not there is a group at the start of the string.
271 */
272 protected function _isGroup($address)
273 {
274 // First comma not in quotes, angles or escaped:
275 $parts = explode(',', $address);
276 $string = $this->_splitCheck($parts, ',');
277 // Now we have the first address, we can reliably check for a
278 // group by searching for a colon that's not escaped or in
279 // quotes or angle brackets.
280 if (count(($parts = explode(':', $string))) > 1) {
281 $string2 = $this->_splitCheck($parts, ':');
282 return $string2 !== $string;
283 } else {
284 return false;
285 }
286 }
287
288 /**
289 * A common function that will check an exploded string.
290 *
291 * @access private
292 * @param array $parts The exloded 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 * @access private
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 default:
339 $slashes = 0;
340 }
341 }
342 return $in_quote;
343 }
344
345 /**
346 * Checks if a string has an unclosed brackets or not. IMPORTANT:
347 * This function handles both angle brackets and square brackets;
348 *
349 * @access private
350 * @param string $string The string to check.
351 * @param string $chars The characters to check for.
352 * @return bool TRUE if there are unclosed brackets inside the string, FALSE otherwise.
353 */
354 protected function _hasUnclosedBrackets($string, $chars)
355 {
356 $num_angle_start = substr_count($string, $chars[0]);
357 $num_angle_end = substr_count($string, $chars[1]);
358 $this->_hasUnclosedBracketsSub($string, $num_angle_start, $chars[0]);
359 $this->_hasUnclosedBracketsSub($string, $num_angle_end, $chars[1]);
360 if ($num_angle_start < $num_angle_end) {
361 $this->error = 'Invalid address spec. Unmatched quote or bracket (' . $chars . ')';
362 return false;
363 } else {
364 return $num_angle_start > $num_angle_end;
365 }
366 }
367
368 /**
369 * Sub function that is used only by hasUnclosedBrackets().
370 *
371 * @access private
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 * @access private
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 $is_group = false;
402 $addresses = [];
403 if ($address['group']) {
404 $is_group = true;
405 // Get the group part of the name
406 $parts = explode(':', $address['address']);
407 $groupname = $this->_splitCheck($parts, ':');
408 $structure = [];
409 // And validate the group part of the name.
410 if (!$this->_validatePhrase($groupname)) {
411 $this->error = 'Group name did not validate.';
412 return false;
413 }
414 $address['address'] = ltrim(substr($address['address'], strlen($groupname . ':')));
415 }
416 // If a group then split on comma and put into an array.
417 // Otherwise, Just put the whole address in an array.
418 if ($is_group) {
419 while ($address['address'] !== '') {
420 $parts = explode(',', $address['address']);
421 $addresses[] = $this->_splitCheck($parts, ',');
422 $address['address'] = trim(substr($address['address'], strlen(end($addresses) . ',')));
423 }
424 } else {
425 $addresses[] = $address['address'];
426 }
427 // Check that $addresses is set, if address like this:
428 // Groupname:;
429 // Then errors were appearing.
430 if (empty($addresses)) {
431 $this->error = 'Empty group.';
432 return false;
433 }
434 // Trim the whitespace from all of the address strings.
435 array_map('trim', $addresses);
436 // Validate each mailbox.
437 // Format could be one of: name <geezer@domain.com>
438 // geezer@domain.com
439 // geezer
440 // ... or any other format valid by RFC 822.
441 $addressesCount = count($addresses);
442 for ($i = 0; $i < $addressesCount; $i++) {
443 if (!$this->validateMailbox($addresses[$i])) {
444 if (empty($this->error)) {
445 $this->error = 'Validation failed for: ' . $addresses[$i];
446 }
447 return false;
448 }
449 }
450 if ($is_group) {
451 $structure = array_merge($structure, $addresses);
452 } else {
453 $structure = $addresses;
454 }
455 return $structure;
456 }
457
458 /**
459 * Function to validate a phrase.
460 *
461 * @access private
462 * @param string $phrase The phrase to check.
463 * @return bool Success or failure.
464 */
465 protected function _validatePhrase($phrase)
466 {
467 // Splits on one or more Tab or space.
468 $parts = preg_split('/[ \\x09]+/', $phrase, -1, PREG_SPLIT_NO_EMPTY);
469 $phrase_parts = [];
470 while (!empty($parts)) {
471 $phrase_parts[] = $this->_splitCheck($parts, ' ');
472 for ($i = 0; $i < $this->index + 1; $i++) {
473 array_shift($parts);
474 }
475 }
476 foreach ($phrase_parts as $part) {
477 // If quoted string:
478 if ($part[0] === '"') {
479 if (!$this->_validateQuotedString($part)) {
480 return false;
481 }
482 continue;
483 }
484 // Otherwise it's an atom:
485 if (!$this->_validateAtom($part)) {
486 return false;
487 }
488 }
489 return true;
490 }
491
492 /**
493 * Function to validate an atom which from rfc822 is:
494 * atom = 1*<any CHAR except specials, SPACE and CTLs>
495 *
496 * If validation ($this->validate) has been turned off, then
497 * validateAtom() doesn't actually check anything. This is so that you
498 * can split a list of addresses up before encoding personal names
499 * (umlauts, etc.), for example.
500 *
501 * @access private
502 * @param string $atom The string to check.
503 * @return bool Success or failure.
504 */
505 protected function _validateAtom($atom)
506 {
507 if (!$this->validate) {
508 // Validation has been turned off; assume the atom is okay.
509 return true;
510 }
511 // Check for any char from ASCII 0 - ASCII 127
512 if (!preg_match('/^[\\x00-\\x7E]+$/i', $atom, $matches)) {
513 return false;
514 }
515 // Check for specials:
516 if (preg_match('/[][()<>@,;\\:". ]/', $atom)) {
517 return false;
518 }
519 // Check for control characters (ASCII 0-31):
520 if (preg_match('/[\\x00-\\x1F]+/', $atom)) {
521 return false;
522 }
523 return true;
524 }
525
526 /**
527 * Function to validate quoted string, which is:
528 * quoted-string = <"> *(qtext/quoted-pair) <">
529 *
530 * @access private
531 * @param string $qstring The string to check
532 * @return bool Success or failure.
533 */
534 protected function _validateQuotedString($qstring)
535 {
536 // Leading and trailing "
537 $qstring = substr($qstring, 1, -1);
538 // Perform check, removing quoted characters first.
539 return !preg_match('/[\\x0D\\\\"]/', preg_replace('/\\\\./', '', $qstring));
540 }
541
542 /**
543 * Function to validate a mailbox, which is:
544 * mailbox = addr-spec ; simple address
545 * phrase route-addr ; name and route-addr
546 *
547 * @access public
548 * @param string &$mailbox The string to check.
549 * @return bool Success or failure.
550 */
551 protected function validateMailbox(&$mailbox)
552 {
553 // A couple of defaults.
554 $phrase = '';
555 $comment = '';
556 $comments = [];
557 // Catch any RFC822 comments and store them separately.
558 $_mailbox = $mailbox;
559 while (trim($_mailbox) !== '') {
560 $parts = explode('(', $_mailbox);
561 $before_comment = $this->_splitCheck($parts, '(');
562 if ($before_comment != $_mailbox) {
563 // First char should be a (.
564 $comment = substr(str_replace($before_comment, '', $_mailbox), 1);
565 $parts = explode(')', $comment);
566 $comment = $this->_splitCheck($parts, ')');
567 $comments[] = $comment;
568 // +2 is for the brackets
569 $_mailbox = substr($_mailbox, strpos($_mailbox, ('(' . $comment)) + strlen($comment) + 2);
570 } else {
571 break;
572 }
573 }
574 foreach ($comments as $comment) {
575 $mailbox = str_replace('(' . $comment . ')', '', $mailbox);
576 }
577 $mailbox = trim($mailbox);
578 // Check for name + route-addr
579 if (substr($mailbox, -1) === '>' && $mailbox[0] !== '<') {
580 $parts = explode('<', $mailbox);
581 $name = $this->_splitCheck($parts, '<');
582 $phrase = trim($name);
583 $route_addr = trim(substr($mailbox, strlen($name . '<'), -1));
584 if ($this->_validatePhrase($phrase) === false || ($route_addr = $this->_validateRouteAddr($route_addr)) === false) {
585 return false;
586 }
587 } else {
588 // First snip angle brackets if present.
589 if ($mailbox[0] === '<' && substr($mailbox, -1) === '>') {
590 $addr_spec = substr($mailbox, 1, -1);
591 } else {
592 $addr_spec = $mailbox;
593 }
594 if (($addr_spec = $this->_validateAddrSpec($addr_spec)) === false) {
595 return false;
596 }
597 }
598 // Construct the object that will be returned.
599 $mbox = new \stdClass();
600 // Add the phrase (even if empty) and comments
601 $mbox->personal = $phrase;
602 $mbox->comment = isset($comments) ? $comments : [];
603 if (isset($route_addr)) {
604 $mbox->mailbox = $route_addr['local_part'];
605 $mbox->host = $route_addr['domain'];
606 $route_addr['adl'] !== '' ? ($mbox->adl = $route_addr['adl']) : '';
607 } else {
608 $mbox->mailbox = $addr_spec['local_part'];
609 $mbox->host = $addr_spec['domain'];
610 }
611 $mailbox = $mbox;
612 return true;
613 }
614
615 /**
616 * This function validates a route-addr which is:
617 * route-addr = "<" [route] addr-spec ">"
618 *
619 * Angle brackets have already been removed at the point of
620 * getting to this function.
621 *
622 * @access private
623 * @param string $route_addr The string to check.
624 * @return mixed False on failure, or an array containing validated address/route information on success.
625 */
626 protected function _validateRouteAddr($route_addr)
627 {
628 // Check for colon.
629 if (strpos($route_addr, ':') !== false) {
630 $parts = explode(':', $route_addr);
631 $route = $this->_splitCheck($parts, ':');
632 } else {
633 $route = $route_addr;
634 }
635 // If $route is same as $route_addr then the colon was in
636 // quotes or brackets or, of course, non existent.
637 if ($route === $route_addr) {
638 unset($route);
639 $addr_spec = $route_addr;
640 if (($addr_spec = $this->_validateAddrSpec($addr_spec)) === false) {
641 return false;
642 }
643 } else {
644 // Validate route part.
645 if (($route = $this->_validateRoute($route)) === false) {
646 return false;
647 }
648 $addr_spec = substr($route_addr, strlen($route . ':'));
649 // Validate addr-spec part.
650 if (($addr_spec = $this->_validateAddrSpec($addr_spec)) === false) {
651 return false;
652 }
653 }
654 if (isset($route)) {
655 $return['adl'] = $route;
656 } else {
657 $return['adl'] = '';
658 }
659 $return = array_merge($return, $addr_spec);
660 return $return;
661 }
662
663 /**
664 * Function to validate a route, which is:
665 * route = 1#("@" domain) ":"
666 *
667 * @access private
668 * @param string $route The string to check.
669 * @return mixed False on failure, or the validated $route on success.
670 */
671 protected function _validateRoute($route)
672 {
673 // Split on comma.
674 $domains = explode(',', trim($route));
675 foreach ($domains as $domain) {
676 $domain = str_replace('@', '', trim($domain));
677 if (!$this->_validateDomain($domain)) {
678 return false;
679 }
680 }
681 return $route;
682 }
683
684 /**
685 * Function to validate a domain, though this is not quite what
686 * you expect of a strict internet domain.
687 *
688 * domain = sub-domain *("." sub-domain)
689 *
690 * @access private
691 * @param string $domain The string to check.
692 * @return mixed False on failure, or the validated domain on success.
693 */
694 protected function _validateDomain($domain)
695 {
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 * @access private
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 * @access private
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 * @access private
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 * @access private
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 }