MDL-63422 lib: horde - review core loop / switch / case / continue
[moodle.git] / lib / horde / framework / Horde / Mail / Rfc822.php
1 <?php
2 /**
3  * Copyright (c) 2001-2010, Richard Heyes
4  * Copyright 2011-2017 Horde LLC (http://www.horde.org/)
5  * All rights reserved.
6  *
7  * Redistribution and use in source and binary forms, with or without
8  * modification, are permitted provided that the following conditions
9  * are met:
10  *
11  * o Redistributions of source code must retain the above copyright
12  *   notice, this list of conditions and the following disclaimer.
13  * o Redistributions in binary form must reproduce the above copyright
14  *   notice, this list of conditions and the following disclaimer in the
15  *   documentation and/or other materials provided with the distribution.
16  * o The names of the authors may not be used to endorse or promote
17  *   products derived from this software without specific prior written
18  *   permission.
19  *
20  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
23  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
24  * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
25  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
26  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
27  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
28  * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
29  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
30  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31  *
32  * RFC822 parsing code adapted from message-address.c and rfc822-parser.c
33  *   (Dovecot 2.1rc5)
34  *   Original code released under LGPL-2.1
35  *   Copyright (c) 2002-2011 Timo Sirainen <tss@iki.fi>
36  *
37  * @category  Horde
38  * @copyright 2001-2010 Richard Heyes
39  * @copyright 2002-2011 Timo Sirainen
40  * @copyright 2011-2017 Horde LLC
41  * @license   http://www.horde.org/licenses/bsd New BSD License
42  * @package   Mail
43  */
45 /**
46  * RFC 822/2822/3490/5322 Email parser/validator.
47  *
48  * @author    Richard Heyes <richard@phpguru.org>
49  * @author    Chuck Hagenbuch <chuck@horde.org>
50  * @author    Michael Slusarz <slusarz@horde.org>
51  * @author    Timo Sirainen <tss@iki.fi>
52  * @category  Horde
53  * @copyright 2001-2010 Richard Heyes
54  * @copyright 2002-2011 Timo Sirainen
55  * @copyright 2011-2017 Horde LLC
56  * @license   http://www.horde.org/licenses/bsd New BSD License
57  * @package   Mail
58  */
59 class Horde_Mail_Rfc822
60 {
61     /**
62      * Valid atext characters.
63      *
64      * @deprecated
65      * @since 2.0.3
66      */
67     const ATEXT = '!#$%&\'*+-./0123456789=?ABCDEFGHIJKLMNOPQRSTUVWXYZ^_`abcdefghijklmnopqrstuvwxyz{|}~';
69     /**
70      * Excluded (in ASCII decimal): 0-8, 10-31, 34, 40-41, 44, 58-60, 62, 64,
71      * 91-93, 127
72      *
73      * @since 2.0.3
74      */
75     const ENCODE_FILTER = "\0\1\2\3\4\5\6\7\10\12\13\14\15\16\17\20\21\22\23\24\25\26\27\30\31\32\33\34\35\36\37\"(),:;<>@[\\]\177";
77     /**
78      * The address string to parse.
79      *
80      * @var string
81      */
82     protected $_data;
84     /**
85      * Length of the address string.
86      *
87      * @var integer
88      */
89     protected $_datalen;
91     /**
92      * Comment cache.
93      *
94      * @var string
95      */
96     protected $_comments = array();
98     /**
99      * List object to return in parseAddressList().
100      *
101      * @var Horde_Mail_Rfc822_List
102      */
103     protected $_listob;
105     /**
106      * Configuration parameters.
107      *
108      * @var array
109      */
110     protected $_params = array();
112     /**
113      * Data pointer.
114      *
115      * @var integer
116      */
117     protected $_ptr;
119     /**
120      * Starts the whole process.
121      *
122      * @param mixed $address   The address(es) to validate. Either a string,
123      *                         a Horde_Mail_Rfc822_Object, or an array of
124      *                         strings and/or Horde_Mail_Rfc822_Objects.
125      * @param array $params    Optional parameters:
126      *   - default_domain: (string) Default domain/host.
127      *                     DEFAULT: None
128      *   - group: (boolean) Return a GroupList object instead of a List object?
129      *            DEFAULT: false
130      *   - limit: (integer) Stop processing after this many addresses.
131      *            DEFAULT: No limit (0)
132      *   - validate: (mixed) Strict validation of personal part data? If
133      *               false, attempts to allow non-ASCII characters and
134      *               non-quoted strings in the personal data, and will
135      *               silently abort if an unparseable address is found.
136      *               If true, does strict RFC 5322 (ASCII-only) parsing. If
137      *               'eai' (@since 2.5.0), allows RFC 6532 (EAI/UTF-8)
138      *               addresses.
139      *               DEFAULT: false
140      *
141      * @return Horde_Mail_Rfc822_List  A list object.
142      *
143      * @throws Horde_Mail_Exception
144      */
145     public function parseAddressList($address, array $params = array())
146     {
147         if ($address instanceof Horde_Mail_Rfc822_List) {
148             return $address;
149         }
151         if (empty($params['limit'])) {
152             $params['limit'] = -1;
153         }
155         $this->_params = array_merge(array(
156             'default_domain' => null,
157             'validate' => false
158         ), $params);
160         $this->_listob = empty($this->_params['group'])
161             ? new Horde_Mail_Rfc822_List()
162             : new Horde_Mail_Rfc822_GroupList();
164         if (!is_array($address)) {
165             $address = array($address);
166         }
168         $tmp = array();
169         foreach ($address as $val) {
170             if ($val instanceof Horde_Mail_Rfc822_Object) {
171                 $this->_listob->add($val);
172             } else {
173                 $tmp[] = rtrim(trim($val), ',');
174             }
175         }
177         if (!empty($tmp)) {
178             $this->_data = implode(',', $tmp);
179             $this->_datalen = strlen($this->_data);
180             $this->_ptr = 0;
182             $this->_parseAddressList();
183         }
185         $ret = $this->_listob;
186         unset($this->_listob);
188         return $ret;
189     }
191    /**
192      * Quotes and escapes the given string if necessary using rules contained
193      * in RFC 2822 [3.2.5].
194      *
195      * @param string $str   The string to be quoted and escaped.
196      * @param string $type  Either 'address', 'comment' (@since 2.6.0), or
197      *                      'personal'.
198      *
199      * @return string  The correctly quoted and escaped string.
200      */
201     public function encode($str, $type = 'address')
202     {
203         switch ($type) {
204         case 'comment':
205             // RFC 5322 [3.2.2]: Filter out non-printable US-ASCII and ( ) \
206             $filter = "\0\1\2\3\4\5\6\7\10\12\13\14\15\16\17\20\21\22\23\24\25\26\27\30\31\32\33\34\35\36\37\50\51\134\177";
207             break;
209         case 'personal':
210             // RFC 2822 [3.4]: Period not allowed in display name
211             $filter = self::ENCODE_FILTER . '.';
212             break;
214         case 'address':
215         default:
216             // RFC 2822 [3.4.1]: (HTAB, SPACE) not allowed in address
217             $filter = self::ENCODE_FILTER . "\11\40";
218             break;
219         }
221         // Strip double quotes if they are around the string already.
222         // If quoted, we know that the contents are already escaped, so
223         // unescape now.
224         $str = trim($str);
225         if ($str && ($str[0] === '"') && (substr($str, -1) === '"')) {
226             $str = stripslashes(substr($str, 1, -1));
227         }
229         return (strcspn($str, $filter) != strlen($str))
230             ? '"' . addcslashes($str, '\\"') . '"'
231             : $str;
232     }
234     /**
235      * If an email address has no personal information, get rid of any angle
236      * brackets (<>) around it.
237      *
238      * @param string $address  The address to trim.
239      *
240      * @return string  The trimmed address.
241      */
242     public function trimAddress($address)
243     {
244         $address = trim($address);
246         return (($address[0] == '<') && (substr($address, -1) == '>'))
247             ? substr($address, 1, -1)
248             : $address;
249     }
251     /* RFC 822 parsing methods. */
253     /**
254      * address-list = (address *("," address)) / obs-addr-list
255      */
256     protected function _parseAddressList()
257     {
258         $limit = $this->_params['limit'];
260         while (($this->_curr() !== false) && ($limit-- !== 0)) {
261             try {
262                 $this->_parseAddress();
263             } catch (Horde_Mail_Exception $e) {
264                if ($this->_params['validate']) {
265                    throw $e;
266                }
267                ++$this->_ptr;
268             }
270             switch ($this->_curr()) {
271             case ',':
272                 $this->_rfc822SkipLwsp(true);
273                 break;
275             case false:
276                 // No-op
277                 break;
279             default:
280                if ($this->_params['validate']) {
281                     throw new Horde_Mail_Exception('Error when parsing address list.');
282                }
283                break;
284             }
285         }
286     }
288     /**
289      * address = mailbox / group
290      */
291     protected function _parseAddress()
292     {
293         $start = $this->_ptr;
294         if (!$this->_parseGroup()) {
295             $this->_ptr = $start;
296             if ($mbox = $this->_parseMailbox()) {
297                 $this->_listob->add($mbox);
298             }
299         }
300     }
302     /**
303      * group           = display-name ":" [mailbox-list / CFWS] ";" [CFWS]
304      * display-name    = phrase
305      *
306      * @return boolean  True if a group was parsed.
307      *
308      * @throws Horde_Mail_Exception
309      */
310     protected function _parseGroup()
311     {
312         $this->_rfc822ParsePhrase($groupname);
314         if ($this->_curr(true) != ':') {
315             return false;
316         }
318         $addresses = new Horde_Mail_Rfc822_GroupList();
320         $this->_rfc822SkipLwsp();
322         while (($chr = $this->_curr()) !== false) {
323             if ($chr == ';') {
324                 ++$this->_ptr;
326                 if (count($addresses)) {
327                     $this->_listob->add(new Horde_Mail_Rfc822_Group($groupname, $addresses));
328                 }
330                 return true;
331             }
333             /* mailbox-list = (mailbox *("," mailbox)) / obs-mbox-list */
334             $addresses->add($this->_parseMailbox());
336             switch ($this->_curr()) {
337             case ',':
338                 $this->_rfc822SkipLwsp(true);
339                 break;
341             case ';':
342                 // No-op
343                 break;
345             default:
346                 break 2;
347             }
348         }
350         throw new Horde_Mail_Exception('Error when parsing group.');
351     }
353     /**
354      * mailbox = name-addr / addr-spec
355      *
356      * @return mixed  Mailbox object if mailbox was parsed, or false.
357      */
358     protected function _parseMailbox()
359     {
360         $this->_comments = array();
361         $start = $this->_ptr;
363         if (!($ob = $this->_parseNameAddr())) {
364             $this->_comments = array();
365             $this->_ptr = $start;
366             $ob = $this->_parseAddrSpec();
367         }
369         if ($ob) {
370             $ob->comment = $this->_comments;
371         }
373         return $ob;
374     }
376     /**
377      * name-addr    = [display-name] angle-addr
378      * display-name = phrase
379      *
380      * @return mixed  Mailbox object, or false.
381      */
382     protected function _parseNameAddr()
383     {
384         $this->_rfc822ParsePhrase($personal);
386         if ($ob = $this->_parseAngleAddr()) {
387             $ob->personal = $personal;
388             return $ob;
389         }
391         return false;
392     }
394     /**
395      * addr-spec = local-part "@" domain
396      *
397      * @return mixed  Mailbox object.
398      *
399      * @throws Horde_Mail_Exception
400      */
401     protected function _parseAddrSpec()
402     {
403         $ob = new Horde_Mail_Rfc822_Address();
404         $ob->mailbox = $this->_parseLocalPart();
406         if ($this->_curr() == '@') {
407             try {
408                 $this->_rfc822ParseDomain($host);
409                 if (strlen($host)) {
410                     $ob->host = $host;
411                 }
412             } catch (Horde_Mail_Exception $e) {
413                 if (!empty($this->_params['validate'])) {
414                     throw $e;
415                 }
416             }
417         }
419         if (is_null($ob->host)) {
420             if (!is_null($this->_params['default_domain'])) {
421                 $ob->host = $this->_params['default_domain'];
422             } elseif (!empty($this->_params['validate'])) {
423                 throw new Horde_Mail_Exception('Address is missing domain.');
424             }
425         }
427         return $ob;
428     }
430     /**
431      * local-part      = dot-atom / quoted-string / obs-local-part
432      * obs-local-part  = word *("." word)
433      *
434      * @return string  The local part.
435      *
436      * @throws Horde_Mail_Exception
437      */
438     protected function _parseLocalPart()
439     {
440         if (($curr = $this->_curr()) === false) {
441             throw new Horde_Mail_Exception('Error when parsing local part.');
442         }
444         if ($curr == '"') {
445             $this->_rfc822ParseQuotedString($str);
446         } else {
447             $this->_rfc822ParseDotAtom($str, ',;@');
448         }
450         return $str;
451     }
453     /**
454      * "<" [ "@" route ":" ] local-part "@" domain ">"
455      *
456      * @return mixed  Mailbox object, or false.
457      *
458      * @throws Horde_Mail_Exception
459      */
460     protected function _parseAngleAddr()
461     {
462         if ($this->_curr() != '<') {
463             return false;
464         }
466         $this->_rfc822SkipLwsp(true);
468         if ($this->_curr() == '@') {
469             // Route information is ignored.
470             $this->_parseDomainList();
471             if ($this->_curr() != ':') {
472                 throw new Horde_Mail_Exception('Invalid route.');
473             }
475             $this->_rfc822SkipLwsp(true);
476         }
478         $ob = $this->_parseAddrSpec();
480         if ($this->_curr() != '>') {
481             throw new Horde_Mail_Exception('Error when parsing angle address.');
482         }
484         $this->_rfc822SkipLwsp(true);
486         return $ob;
487     }
489     /**
490      * obs-domain-list = "@" domain *(*(CFWS / "," ) [CFWS] "@" domain)
491      *
492      * @return array  Routes.
493      *
494      * @throws Horde_Mail_Exception
495      */
496     protected function _parseDomainList()
497     {
498         $route = array();
500         while ($this->_curr() !== false) {
501             $this->_rfc822ParseDomain($str);
502             $route[] = '@' . $str;
504             $this->_rfc822SkipLwsp();
505             if ($this->_curr() != ',') {
506                 return $route;
507             }
508             ++$this->_ptr;
509         }
511         throw new Horde_Mail_Exception('Invalid domain list.');
512     }
514     /* RFC 822 parsing methods. */
516     /**
517      * phrase     = 1*word / obs-phrase
518      * word       = atom / quoted-string
519      * obs-phrase = word *(word / "." / CFWS)
520      *
521      * @param string &$phrase  The phrase data.
522      *
523      * @throws Horde_Mail_Exception
524      */
525     protected function _rfc822ParsePhrase(&$phrase)
526     {
527         $curr = $this->_curr();
528         if (($curr === false) || ($curr == '.')) {
529             throw new Horde_Mail_Exception('Error when parsing a group.');
530         }
532         do {
533             if ($curr == '"') {
534                 $this->_rfc822ParseQuotedString($phrase);
535             } else {
536                 $this->_rfc822ParseAtomOrDot($phrase);
537             }
539             $curr = $this->_curr();
540             if (($curr != '"') &&
541                 ($curr != '.') &&
542                 !$this->_rfc822IsAtext($curr)) {
543                 break;
544             }
546             $phrase .= ' ';
547         } while ($this->_ptr < $this->_datalen);
549         $this->_rfc822SkipLwsp();
550     }
552     /**
553      * @param string &$phrase  The quoted string data.
554      *
555      * @throws Horde_Mail_Exception
556      */
557     protected function _rfc822ParseQuotedString(&$str)
558     {
559         if ($this->_curr(true) != '"') {
560             throw new Horde_Mail_Exception('Error when parsing a quoted string.');
561         }
563         while (($chr = $this->_curr(true)) !== false) {
564             switch ($chr) {
565             case '"':
566                 $this->_rfc822SkipLwsp();
567                 return;
569             case "\n":
570                 /* Folding whitespace, remove the (CR)LF. */
571                 if (substr($str, -1) == "\r") {
572                     $str = substr($str, 0, -1);
573                 }
574                 break;
576             case '\\':
577                 if (($chr = $this->_curr(true)) === false) {
578                     break 2;
579                 }
580                 break;
581             }
583             $str .= $chr;
584         }
586         /* Missing trailing '"', or partial quoted character. */
587         throw new Horde_Mail_Exception('Error when parsing a quoted string.');
588     }
590     /**
591      * dot-atom        = [CFWS] dot-atom-text [CFWS]
592      * dot-atom-text   = 1*atext *("." 1*atext)
593      *
594      * atext           = ; Any character except controls, SP, and specials.
595      *
596      * For RFC-822 compatibility allow LWSP around '.'.
597      *
598      *
599      * @param string &$str      The atom/dot data.
600      * @param string $validate  Use these characters as delimiter.
601      *
602      * @throws Horde_Mail_Exception
603      */
604     protected function _rfc822ParseDotAtom(&$str, $validate = null)
605     {
606         $valid = false;
608         while ($this->_ptr < $this->_datalen) {
609             $chr = $this->_data[$this->_ptr];
611             /* TODO: Optimize by duplicating rfc822IsAtext code here */
612             if ($this->_rfc822IsAtext($chr, $validate)) {
613                 $str .= $chr;
614                 ++$this->_ptr;
615             } elseif (!$valid) {
616                 throw new Horde_Mail_Exception('Error when parsing dot-atom.');
617             } else {
618                 $this->_rfc822SkipLwsp();
620                 if ($this->_curr() != '.') {
621                     return;
622                 }
623                 $str .= $chr;
625                 $this->_rfc822SkipLwsp(true);
626             }
628             $valid = true;
629         }
630     }
632     /**
633      * atom  = [CFWS] 1*atext [CFWS]
634      * atext = ; Any character except controls, SP, and specials.
635      *
636      * This method doesn't just silently skip over WS.
637      *
638      * @param string &$str  The atom/dot data.
639      *
640      * @throws Horde_Mail_Exception
641      */
642     protected function _rfc822ParseAtomOrDot(&$str)
643     {
644         while ($this->_ptr < $this->_datalen) {
645             $chr = $this->_data[$this->_ptr];
646             if (($chr != '.') &&
647                 /* TODO: Optimize by duplicating rfc822IsAtext code here */
648                 !$this->_rfc822IsAtext($chr, ',<:')) {
649                 $this->_rfc822SkipLwsp();
650                 if (!$this->_params['validate']) {
651                     $str = trim($str);
652                 }
653                 return;
654             }
656             $str .= $chr;
657             ++$this->_ptr;
658         }
659     }
661     /**
662      * domain          = dot-atom / domain-literal / obs-domain
663      * domain-literal  = [CFWS] "[" *([FWS] dcontent) [FWS] "]" [CFWS]
664      * obs-domain      = atom *("." atom)
665      *
666      * @param string &$str  The domain string.
667      *
668      * @throws Horde_Mail_Exception
669      */
670     protected function _rfc822ParseDomain(&$str)
671     {
672         if ($this->_curr(true) != '@') {
673             throw new Horde_Mail_Exception('Error when parsing domain.');
674         }
676         $this->_rfc822SkipLwsp();
678         if ($this->_curr() == '[') {
679             $this->_rfc822ParseDomainLiteral($str);
680         } else {
681             $this->_rfc822ParseDotAtom($str, ';,> ');
682         }
683     }
685     /**
686      * domain-literal  = [CFWS] "[" *([FWS] dcontent) [FWS] "]" [CFWS]
687      * dcontent        = dtext / quoted-pair
688      * dtext           = NO-WS-CTL /     ; Non white space controls
689      *           %d33-90 /       ; The rest of the US-ASCII
690      *           %d94-126        ;  characters not including "[",
691      *                   ;  "]", or "\"
692      *
693      * @param string &$str  The domain string.
694      *
695      * @throws Horde_Mail_Exception
696      */
697     protected function _rfc822ParseDomainLiteral(&$str)
698     {
699         if ($this->_curr(true) != '[') {
700             throw new Horde_Mail_Exception('Error parsing domain literal.');
701         }
703         while (($chr = $this->_curr(true)) !== false) {
704             switch ($chr) {
705             case '\\':
706                 if (($chr = $this->_curr(true)) === false) {
707                     break 2;
708                 }
709                 break;
711             case ']':
712                 $this->_rfc822SkipLwsp();
713                 return;
714             }
716             $str .= $chr;
717         }
719         throw new Horde_Mail_Exception('Error parsing domain literal.');
720     }
722     /**
723      * @param boolean $advance  Advance cursor?
724      *
725      * @throws Horde_Mail_Exception
726      */
727     protected function _rfc822SkipLwsp($advance = false)
728     {
729         if ($advance) {
730             ++$this->_ptr;
731         }
733         while (($chr = $this->_curr()) !== false) {
734             switch ($chr) {
735             case ' ':
736             case "\n":
737             case "\r":
738             case "\t":
739                 ++$this->_ptr;
740                 break;
742             case '(':
743                 $this->_rfc822SkipComment();
744                 break;
746             default:
747                 return;
748             }
749         }
750     }
752     /**
753      * @throws Horde_Mail_Exception
754      */
755     protected function _rfc822SkipComment()
756     {
757         if ($this->_curr(true) != '(') {
758             throw new Horde_Mail_Exception('Error when parsing a comment.');
759         }
761         $comment = '';
762         $level = 1;
764         while (($chr = $this->_curr(true)) !== false) {
765             switch ($chr) {
766             case '(':
767                 ++$level;
768                 break;
770             case ')':
771                 if (--$level == 0) {
772                     $this->_comments[] = $comment;
773                     return;
774                 }
775                 break;
777             case '\\':
778                 if (($chr = $this->_curr(true)) === false) {
779                     break 2;
780                 }
781                 break;
782             }
784             $comment .= $chr;
785         }
787         throw new Horde_Mail_Exception('Error when parsing a comment.');
788     }
790     /**
791      * Check if data is an atom.
792      *
793      * @param string $chr       The character to check.
794      * @param string $validate  If in non-validate mode, use these characters
795      *                          as the non-atom delimiters.
796      *
797      * @return boolean  True if a valid atom.
798      */
799     protected function _rfc822IsAtext($chr, $validate = null)
800     {
801         if (!$this->_params['validate'] && !is_null($validate)) {
802             return strcspn($chr, $validate);
803         }
805         $ord = ord($chr);
807         /* UTF-8 characters check. */
808         if ($ord > 127) {
809             return ($this->_params['validate'] === 'eai');
810         }
812         /* Check for DISALLOWED characters under both RFCs 5322 and 6532. */
814         /* Unprintable characters && [SPACE] */
815         if ($ord <= 32) {
816             return false;
817         }
819         /* "(),:;<>@[\] [DEL] */
820         switch ($ord) {
821         case 34:
822         case 40:
823         case 41:
824         case 44:
825         case 58:
826         case 59:
827         case 60:
828         case 62:
829         case 64:
830         case 91:
831         case 92:
832         case 93:
833         case 127:
834             return false;
835         }
837         return true;
838     }
840     /* Helper methods. */
842     /**
843      * Return current character.
844      *
845      * @param boolean $advance  If true, advance the cursor.
846      *
847      * @return string  The current character (false if EOF reached).
848      */
849     protected function _curr($advance = false)
850     {
851         return ($this->_ptr >= $this->_datalen)
852             ? false
853             : $this->_data[$advance ? $this->_ptr++ : $this->_ptr];
854     }
856     /* Other public methods. */
858     /**
859      * Returns an approximate count of how many addresses are in the string.
860      * This is APPROXIMATE as it only splits based on a comma which has no
861      * preceding backslash.
862      *
863      * @param string $data  Addresses to count.
864      *
865      * @return integer  Approximate count.
866      */
867     public function approximateCount($data)
868     {
869         return count(preg_split('/(?<!\\\\),/', $data));
870     }
872     /**
873      * Validates whether an email is of the common internet form:
874      * <user>@<domain>. This can be sufficient for most people.
875      *
876      * Optional stricter mode can be utilized which restricts mailbox
877      * characters allowed to: alphanumeric, full stop, hyphen, and underscore.
878      *
879      * @param string $data     Address to check.
880      * @param boolean $strict  Strict check?
881      *
882      * @return mixed  False if it fails, an indexed array username/domain if
883      *                it matches.
884      */
885     public function isValidInetAddress($data, $strict = false)
886     {
887         $regex = $strict
888             ? '/^([.0-9a-z_+-]+)@(([0-9a-z-]+\.)+[0-9a-z]{2,})$/i'
889             : '/^([*+!.&#$|\'\\%\/0-9a-z^_`{}=?~:-]+)@(([0-9a-z-]+\.)+[0-9a-z]{2,})$/i';
891         return preg_match($regex, trim($data), $matches)
892             ? array($matches[1], $matches[2])
893             : false;
894     }