Overview

Packages

  • Mail

Classes

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