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