Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.3.x will end 7 October 2024 (12 months).
  • Bug fixes for security issues in 4.3.x will end 21 April 2025 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.2.x is supported too.

Differences Between: [Versions 310 and 403] [Versions 311 and 403] [Versions 39 and 403] [Versions 400 and 403]

   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   */
  44  
  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{|}~';
  68  
  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";
  76  
  77      /**
  78       * The address string to parse.
  79       *
  80       * @var string
  81       */
  82      protected $_data;
  83  
  84      /**
  85       * Length of the address string.
  86       *
  87       * @var integer
  88       */
  89      protected $_datalen;
  90  
  91      /**
  92       * Comment cache.
  93       *
  94       * @var string
  95       */
  96      protected $_comments = array();
  97  
  98      /**
  99       * List object to return in parseAddressList().
 100       *
 101       * @var Horde_Mail_Rfc822_List
 102       */
 103      protected $_listob;
 104  
 105      /**
 106       * Configuration parameters.
 107       *
 108       * @var array
 109       */
 110      protected $_params = array();
 111  
 112      /**
 113       * Data pointer.
 114       *
 115       * @var integer
 116       */
 117      protected $_ptr;
 118  
 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          }
 150  
 151          if (empty($params['limit'])) {
 152              $params['limit'] = -1;
 153          }
 154  
 155          $this->_params = array_merge(array(
 156              'default_domain' => null,
 157              'validate' => false
 158          ), $params);
 159  
 160          $this->_listob = empty($this->_params['group'])
 161              ? new Horde_Mail_Rfc822_List()
 162              : new Horde_Mail_Rfc822_GroupList();
 163  
 164          if (!is_array($address)) {
 165              $address = array($address);
 166          }
 167  
 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          }
 176  
 177          if (!empty($tmp)) {
 178              $this->_data = implode(',', $tmp);
 179              $this->_datalen = strlen($this->_data);
 180              $this->_ptr = 0;
 181  
 182              $this->_parseAddressList();
 183          }
 184  
 185          $ret = $this->_listob;
 186          unset($this->_listob);
 187  
 188          return $ret;
 189      }
 190  
 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;
 208  
 209          case 'personal':
 210              // RFC 2822 [3.4]: Period not allowed in display name
 211              $filter = self::ENCODE_FILTER . '.';
 212              break;
 213  
 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          }
 220  
 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          }
 228  
 229          return (strcspn($str, $filter) != strlen($str))
 230              ? '"' . addcslashes($str, '\\"') . '"'
 231              : $str;
 232      }
 233  
 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);
 245  
 246          return (($address[0] == '<') && (substr($address, -1) == '>'))
 247              ? substr($address, 1, -1)
 248              : $address;
 249      }
 250  
 251      /* RFC 822 parsing methods. */
 252  
 253      /**
 254       * address-list = (address *("," address)) / obs-addr-list
 255       */
 256      protected function _parseAddressList()
 257      {
 258          $limit = $this->_params['limit'];
 259  
 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              }
 269  
 270              switch ($this->_curr()) {
 271              case ',':
 272                  $this->_rfc822SkipLwsp(true);
 273                  break;
 274  
 275              case false:
 276                  // No-op
 277                  break;
 278  
 279              default:
 280                 if ($this->_params['validate']) {
 281                      throw new Horde_Mail_Exception('Error when parsing address list.');
 282                 }
 283                 break;
 284              }
 285          }
 286      }
 287  
 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      }
 301  
 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);
 313  
 314          if ($this->_curr(true) != ':') {
 315              return false;
 316          }
 317  
 318          $addresses = new Horde_Mail_Rfc822_GroupList();
 319  
 320          $this->_rfc822SkipLwsp();
 321  
 322          while (($chr = $this->_curr()) !== false) {
 323              if ($chr == ';') {
 324                  ++$this->_ptr;
 325  
 326                  if (count($addresses)) {
 327                      $this->_listob->add(new Horde_Mail_Rfc822_Group($groupname, $addresses));
 328                  }
 329  
 330                  return true;
 331              }
 332  
 333              /* mailbox-list = (mailbox *("," mailbox)) / obs-mbox-list */
 334              $addresses->add($this->_parseMailbox());
 335  
 336              switch ($this->_curr()) {
 337              case ',':
 338                  $this->_rfc822SkipLwsp(true);
 339                  break;
 340  
 341              case ';':
 342                  // No-op
 343                  break;
 344  
 345              default:
 346                  break 2;
 347              }
 348          }
 349  
 350          throw new Horde_Mail_Exception('Error when parsing group.');
 351      }
 352  
 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;
 362  
 363          if (!($ob = $this->_parseNameAddr())) {
 364              $this->_comments = array();
 365              $this->_ptr = $start;
 366              $ob = $this->_parseAddrSpec();
 367          }
 368  
 369          if ($ob) {
 370              $ob->comment = $this->_comments;
 371          }
 372  
 373          return $ob;
 374      }
 375  
 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);
 385  
 386          if ($ob = $this->_parseAngleAddr()) {
 387              $ob->personal = $personal;
 388              return $ob;
 389          }
 390  
 391          return false;
 392      }
 393  
 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();
 405  
 406          if ($this->_curr() == '@') {
 407              try {
 408                  $this->_rfc822ParseDomain($host);
 409                  if (!empty($host)) {
 410                      $ob->host = $host;
 411                  }
 412              } catch (Horde_Mail_Exception $e) {
 413                  if (!empty($this->_params['validate'])) {
 414                      throw $e;
 415                  }
 416              }
 417          }
 418  
 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          }
 426  
 427          return $ob;
 428      }
 429  
 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          }
 443  
 444          if ($curr == '"') {
 445              $this->_rfc822ParseQuotedString($str);
 446          } else {
 447              $this->_rfc822ParseDotAtom($str, ',;@');
 448          }
 449  
 450          return $str;
 451      }
 452  
 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          }
 465  
 466          $this->_rfc822SkipLwsp(true);
 467  
 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              }
 474  
 475              $this->_rfc822SkipLwsp(true);
 476          }
 477  
 478          $ob = $this->_parseAddrSpec();
 479  
 480          if ($this->_curr() != '>') {
 481              throw new Horde_Mail_Exception('Error when parsing angle address.');
 482          }
 483  
 484          $this->_rfc822SkipLwsp(true);
 485  
 486          return $ob;
 487      }
 488  
 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();
 499  
 500          while ($this->_curr() !== false) {
 501              $this->_rfc822ParseDomain($str);
 502              $route[] = '@' . $str;
 503  
 504              $this->_rfc822SkipLwsp();
 505              if ($this->_curr() != ',') {
 506                  return $route;
 507              }
 508              ++$this->_ptr;
 509          }
 510  
 511          throw new Horde_Mail_Exception('Invalid domain list.');
 512      }
 513  
 514      /* RFC 822 parsing methods. */
 515  
 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          }
 531  
 532          do {
 533              if ($curr == '"') {
 534                  $this->_rfc822ParseQuotedString($phrase);
 535              } else {
 536                  $this->_rfc822ParseAtomOrDot($phrase);
 537              }
 538  
 539              $curr = $this->_curr();
 540              if (($curr != '"') &&
 541                  ($curr != '.') &&
 542                  !$this->_rfc822IsAtext($curr)) {
 543                  break;
 544              }
 545  
 546              $phrase .= ' ';
 547          } while ($this->_ptr < $this->_datalen);
 548  
 549          $this->_rfc822SkipLwsp();
 550      }
 551  
 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          }
 562  
 563          while (($chr = $this->_curr(true)) !== false) {
 564              switch ($chr) {
 565              case '"':
 566                  $this->_rfc822SkipLwsp();
 567                  return;
 568  
 569              case "\n":
 570                  /* Folding whitespace, remove the (CR)LF. */
 571                  if (substr($str, -1) == "\r") {
 572                      $str = substr($str, 0, -1);
 573                  }
 574                  continue 2;
 575  
 576              case '\\':
 577                  if (($chr = $this->_curr(true)) === false) {
 578                      break 2;
 579                  }
 580                  break;
 581              }
 582  
 583              $str .= $chr;
 584          }
 585  
 586          /* Missing trailing '"', or partial quoted character. */
 587          throw new Horde_Mail_Exception('Error when parsing a quoted string.');
 588      }
 589  
 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;
 607  
 608          while ($this->_ptr < $this->_datalen) {
 609              $chr = $this->_data[$this->_ptr];
 610  
 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();
 619  
 620                  if ($this->_curr() != '.') {
 621                      return;
 622                  }
 623                  $str .= $chr;
 624  
 625                  $this->_rfc822SkipLwsp(true);
 626              }
 627  
 628              $valid = true;
 629          }
 630      }
 631  
 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'] && $str !== null) {
 651                      $str = trim($str);
 652                  }
 653                  return;
 654              }
 655  
 656              $str .= $chr;
 657              ++$this->_ptr;
 658          }
 659      }
 660  
 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          }
 675  
 676          $this->_rfc822SkipLwsp();
 677  
 678          if ($this->_curr() == '[') {
 679              $this->_rfc822ParseDomainLiteral($str);
 680          } else {
 681              $this->_rfc822ParseDotAtom($str, ';,> ');
 682          }
 683      }
 684  
 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          }
 702  
 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;
 710  
 711              case ']':
 712                  $this->_rfc822SkipLwsp();
 713                  return;
 714              }
 715  
 716              $str .= $chr;
 717          }
 718  
 719          throw new Horde_Mail_Exception('Error parsing domain literal.');
 720      }
 721  
 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          }
 732  
 733          while (($chr = $this->_curr()) !== false) {
 734              switch ($chr) {
 735              case ' ':
 736              case "\n":
 737              case "\r":
 738              case "\t":
 739                  ++$this->_ptr;
 740                  continue 2;
 741  
 742              case '(':
 743                  $this->_rfc822SkipComment();
 744                  break;
 745  
 746              default:
 747                  return;
 748              }
 749          }
 750      }
 751  
 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          }
 760  
 761          $comment = '';
 762          $level = 1;
 763  
 764          while (($chr = $this->_curr(true)) !== false) {
 765              switch ($chr) {
 766              case '(':
 767                  ++$level;
 768                  continue 2;
 769  
 770              case ')':
 771                  if (--$level == 0) {
 772                      $this->_comments[] = $comment;
 773                      return;
 774                  }
 775                  break;
 776  
 777              case '\\':
 778                  if (($chr = $this->_curr(true)) === false) {
 779                      break 2;
 780                  }
 781                  break;
 782              }
 783  
 784              $comment .= $chr;
 785          }
 786  
 787          throw new Horde_Mail_Exception('Error when parsing a comment.');
 788      }
 789  
 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          }
 804  
 805          $ord = ord($chr);
 806  
 807          /* UTF-8 characters check. */
 808          if ($ord > 127) {
 809              return ($this->_params['validate'] === 'eai');
 810          }
 811  
 812          /* Check for DISALLOWED characters under both RFCs 5322 and 6532. */
 813  
 814          /* Unprintable characters && [SPACE] */
 815          if ($ord <= 32) {
 816              return false;
 817          }
 818  
 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          }
 836  
 837          return true;
 838      }
 839  
 840      /* Helper methods. */
 841  
 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      }
 855  
 856      /* Other public methods. */
 857  
 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      }
 871  
 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';
 890  
 891          return preg_match($regex, trim($data), $matches)
 892              ? array($matches[1], $matches[2])
 893              : false;
 894      }
 895  
 896  }