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