56d77880f37b598c9a88169ac705f3019e8260dd
[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 * @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 * @access public
158 * @param string $address The address(es) to validate.
159 * @param string $default_domain Default domain/host etc.
160 * @param bool $validate Whether to validate atoms. Turn this off if you need to run addresses through before encoding the personal names, for instance.
161 * @param int $limit
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 // 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 * @access private
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 } else {
365 return $num_angle_start > $num_angle_end;
366 }
367 }
368
369 /**
370 * Sub function that is used only by hasUnclosedBrackets().
371 *
372 * @access private
373 * @param string $string The string to check.
374 * @param int &$num The number of occurrences.
375 * @param string $char The character to count.
376 * @return int The number of occurrences of $char in $string, adjusted for backslashes.
377 */
378 protected function _hasUnclosedBracketsSub($string, &$num, $char)
379 {
380 $parts = explode($char, $string);
381 $partsCounter = count($parts);
382 for ($i = 0; $i < $partsCounter; $i++) {
383 if (substr($parts[$i], -1) === '\\' || $this->_hasUnclosedQuotes($parts[$i])) {
384 $num--;
385 }
386 if (isset($parts[$i + 1])) {
387 $parts[$i + 1] = $parts[$i] . $char . $parts[$i + 1];
388 }
389 }
390 return $num;
391 }
392
393 /**
394 * Function to begin checking the address.
395 *
396 * @access private
397 * @param string $address The address to validate.
398 * @return mixed False on failure, or a structured array of address information on success.
399 */
400 protected function _validateAddress($address)
401 {
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 * @access private
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 * @access private
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 * @access private
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 = substr($qstring, 1, -1);
539 // Perform check, removing quoted characters first.
540 return !preg_match('/[\\x0D\\\\"]/', 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 * @access public
549 * @param string &$mailbox The string to check.
550 * @return bool Success or failure.
551 */
552 protected function validateMailbox(&$mailbox)
553 {
554 // A couple of defaults.
555 $phrase = '';
556 $comment = '';
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 = isset($comments) ? $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 * @access private
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 // Check for colon.
630 if (strpos($route_addr, ':') !== false) {
631 $parts = explode(':', $route_addr);
632 $route = $this->_splitCheck($parts, ':');
633 } else {
634 $route = $route_addr;
635 }
636 // If $route is same as $route_addr then the colon was in
637 // quotes or brackets or, of course, non existent.
638 if ($route === $route_addr) {
639 unset($route);
640 $addr_spec = $route_addr;
641 if (($addr_spec = $this->_validateAddrSpec($addr_spec)) === false) {
642 return false;
643 }
644 } else {
645 // Validate route part.
646 if (($route = $this->_validateRoute($route)) === false) {
647 return false;
648 }
649 $addr_spec = substr($route_addr, strlen($route . ':'));
650 // Validate addr-spec part.
651 if (($addr_spec = $this->_validateAddrSpec($addr_spec)) === false) {
652 return false;
653 }
654 }
655 if (isset($route)) {
656 $return['adl'] = $route;
657 } else {
658 $return['adl'] = '';
659 }
660 $return = array_merge($return, $addr_spec);
661 return $return;
662 }
663
664 /**
665 * Function to validate a route, which is:
666 * route = 1#("@" domain) ":"
667 *
668 * @access private
669 * @param string $route The string to check.
670 * @return mixed False on failure, or the validated $route on success.
671 */
672 protected function _validateRoute($route)
673 {
674 // Split on comma.
675 $domains = explode(',', trim($route));
676 foreach ($domains as $domain) {
677 $domain = str_replace('@', '', trim($domain));
678 if (!$this->_validateDomain($domain)) {
679 return false;
680 }
681 }
682 return $route;
683 }
684
685 /**
686 * Function to validate a domain, though this is not quite what
687 * you expect of a strict internet domain.
688 *
689 * domain = sub-domain *("." sub-domain)
690 *
691 * @access private
692 * @param string $domain The string to check.
693 * @return mixed False on failure, or the validated domain on success.
694 */
695 protected function _validateDomain($domain)
696 {
697 // Note the different use of $subdomains and $sub_domains
698 $subdomains = explode('.', $domain);
699 while (!empty($subdomains)) {
700 $sub_domains[] = $this->_splitCheck($subdomains, '.');
701 for ($i = 0; $i < $this->index + 1; $i++) {
702 array_shift($subdomains);
703 }
704 }
705 foreach ($sub_domains as $sub_domain) {
706 if (!$this->_validateSubdomain(trim($sub_domain))) {
707 return false;
708 }
709 }
710 // Managed to get here, so return input.
711 return $domain;
712 }
713
714 /**
715 * Function to validate a subdomain:
716 * subdomain = domain-ref / domain-literal
717 *
718 * @access private
719 * @param string $subdomain The string to check.
720 * @return bool Success or failure.
721 */
722 protected function _validateSubdomain($subdomain)
723 {
724 if (preg_match('|^\\[(.*)]$|', $subdomain, $arr)) {
725 if (!$this->_validateDliteral($arr[1])) {
726 return false;
727 }
728 } else {
729 if (!$this->_validateAtom($subdomain)) {
730 return false;
731 }
732 }
733 // Got here, so return successful.
734 return true;
735 }
736
737 /**
738 * Function to validate a domain literal:
739 * domain-literal = "[" *(dtext / quoted-pair) "]"
740 *
741 * @access private
742 * @param string $dliteral The string to check.
743 * @return bool Success or failure.
744 */
745 protected function _validateDliteral($dliteral)
746 {
747 return !preg_match('/(.)[][\\x0D\\\\]/', $dliteral, $matches) && $matches[1] !== '\\';
748 }
749
750 /**
751 * Function to validate an addr-spec.
752 *
753 * addr-spec = local-part "@" domain
754 *
755 * @access private
756 * @param string $addr_spec The string to check.
757 * @return mixed False on failure, or the validated addr-spec on success.
758 */
759 protected function _validateAddrSpec($addr_spec)
760 {
761 $addr_spec = trim($addr_spec);
762 // Split on @ sign if there is one.
763 if (strpos($addr_spec, '@') !== false) {
764 $parts = explode('@', $addr_spec);
765 $local_part = $this->_splitCheck($parts, '@');
766 $domain = substr($addr_spec, strlen($local_part . '@'));
767 } else {
768 $local_part = $addr_spec;
769 $domain = $this->default_domain;
770 }
771 if (($local_part = $this->_validateLocalPart($local_part)) === false) {
772 return false;
773 }
774 if (($domain = $this->_validateDomain($domain)) === false) {
775 return false;
776 }
777 // Got here so return successful.
778 return ['local_part' => $local_part, 'domain' => $domain];
779 }
780
781 /**
782 * Function to validate the local part of an address:
783 * local-part = word *("." word)
784 *
785 * @access private
786 * @param string $local_part
787 * @return mixed False on failure, or the validated local part on success.
788 */
789 protected function _validateLocalPart($local_part)
790 {
791 $parts = explode('.', $local_part);
792 $words = [];
793 // Split the local_part into words.
794 while (!empty($parts)) {
795 $words[] = $this->_splitCheck($parts, '.');
796 for ($i = 0; $i < $this->index + 1; $i++) {
797 array_shift($parts);
798 }
799 }
800 // Validate each word.
801 foreach ($words as $word) {
802 // If this word contains an unquoted space, it is invalid. (6.2.4)
803 if (strpos($word, ' ') && $word[0] !== '"') {
804 return false;
805 }
806 if ($this->_validatePhrase(trim($word)) === false) {
807 return false;
808 }
809 }
810 // Managed to get here, so return the input.
811 return $local_part;
812 }
813 }