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