Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.
   1  <?php
   2  /**
   3   * Copyright 2009-2017 Horde LLC (http://www.horde.org/)
   4   *
   5   * See the enclosed file LICENSE for license information (LGPL). If you
   6   * did not receive this file, see http://www.horde.org/licenses/lgpl21.
   7   *
   8   * ---------------------------------------------------------------------------
   9   *
  10   * Based on the PEAR Net_POP3 package (version 1.3.6) by:
  11   *     Richard Heyes <richard@phpguru.org>
  12   *     Damian Fernandez Sosa <damlists@cnba.uba.ar>
  13   *
  14   * Copyright (c) 2002, Richard Heyes
  15   * All rights reserved.
  16   *
  17   * Redistribution and use in source and binary forms, with or without
  18   * modification, are permitted provided that the following conditions
  19   * are met:
  20   *
  21   * o Redistributions of source code must retain the above copyright
  22   *   notice, this list of conditions and the following disclaimer.
  23   * o Redistributions in binary form must reproduce the above copyright
  24   *   notice, this list of conditions and the following disclaimer in the
  25   *   documentation and/or other materials provided with the distribution.
  26   * o The names of the authors may not be used to endorse or promote
  27   *   products derived from this software without specific prior written
  28   *   permission.
  29   *
  30   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
  31   * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
  32   * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
  33   * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
  34   * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
  35   * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
  36   * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
  37   * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
  38   * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  39   * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
  40   * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  41   *
  42   * ---------------------------------------------------------------------------
  43   *
  44   * @category  Horde
  45   * @copyright 2002 Richard Heyes
  46   * @copyright 2009-2017 Horde LLC
  47   * @license   http://www.horde.org/licenses/lgpl21 LGPL 2.1
  48   * @package   Imap_Client
  49   */
  50  
  51  /**
  52   * An interface to a POP3 server using PHP functions.
  53   *
  54   * It is an abstraction layer allowing POP3 commands to be used based on
  55   * IMAP equivalents.
  56   *
  57   * This driver implements the following POP3-related RFCs:
  58   * <pre>
  59   *   - STD 53/RFC 1939: POP3 specification
  60   *   - RFC 2195: CRAM-MD5 authentication
  61   *   - RFC 2449: POP3 extension mechanism
  62   *   - RFC 2595/4616: PLAIN authentication
  63   *   - RFC 2831: DIGEST-MD5 SASL Authentication (obsoleted by RFC 6331)
  64   *   - RFC 3206: AUTH/SYS response codes
  65   *   - RFC 4616: AUTH=PLAIN
  66   *   - RFC 5034: POP3 SASL
  67   *   - RFC 5802: AUTH=SCRAM-SHA-1
  68   *   - RFC 6856: UTF8, LANG
  69   * </pre>
  70   *
  71   * @author    Richard Heyes <richard@phpguru.org>
  72   * @author    Michael Slusarz <slusarz@horde.org>
  73   * @category  Horde
  74   * @copyright 2002 Richard Heyes
  75   * @copyright 2009-2017 Horde LLC
  76   * @license   http://www.horde.org/licenses/lgpl21 LGPL 2.1
  77   * @package   Imap_Client
  78   */
  79  class Horde_Imap_Client_Socket_Pop3 extends Horde_Imap_Client_Base
  80  {
  81      /* Internal key used to store mailbox level cache data. \1 is not a valid
  82       * ID in POP3, so it should be safe to use. */
  83      const MBOX_CACHE = "\1mbox";
  84  
  85      /**
  86       * The default ports to use for a connection.
  87       *
  88       * @var array
  89       */
  90      protected $_defaultPorts = array(110, 995);
  91  
  92      /**
  93       * The list of deleted messages.
  94       *
  95       * @var array
  96       */
  97      protected $_deleted = array();
  98  
  99      /**
 100       * This object returns POP3 Fetch data objects.
 101       *
 102       * @var string
 103       */
 104      protected $_fetchDataClass = 'Horde_Imap_Client_Data_Fetch_Pop3';
 105  
 106      /**
 107       */
 108      public function __get($name)
 109      {
 110          $out = parent::__get($name);
 111  
 112          switch ($name) {
 113          case 'url':
 114              $out->protocol = 'pop3';
 115              break;
 116          }
 117  
 118          return $out;
 119      }
 120  
 121      /**
 122       */
 123      protected function _initCache($current = false)
 124      {
 125          return parent::_initCache($current) &&
 126                 $this->_capability('UIDL');
 127      }
 128  
 129      /**
 130       */
 131      public function getIdsOb($ids = null, $sequence = false)
 132      {
 133          return new Horde_Imap_Client_Ids_Pop3($ids, $sequence);
 134      }
 135  
 136      /**
 137       */
 138      protected function _initCapability()
 139      {
 140          $this->_connect();
 141  
 142          $c = new Horde_Imap_Client_Data_Capability();
 143  
 144          try {
 145              $res = $this->_sendLine('CAPA', array(
 146                  'multiline' => 'array'
 147              ));
 148  
 149              foreach ($res['data'] as $val) {
 150                  $prefix = explode(' ', $val);
 151                  $c->add($prefix[0], array_slice($prefix, 1));
 152              }
 153          } catch (Horde_Imap_Client_Exception $e) {
 154              $this->_temp['no_capa'] = true;
 155  
 156              /* Need to probe for capabilities if CAPA command is not
 157               * available. */
 158              $c->add('USER');
 159  
 160              /* Capability sniffing only guaranteed after authentication is
 161               * completed (if any). */
 162              if (!empty($this->_init['authmethod'])) {
 163                  $this->_pop3Cache('uidl');
 164                  if (empty($this->_temp['no_uidl'])) {
 165                      $c->add('UIDL');
 166                  }
 167  
 168                  $this->_pop3Cache('top', 1);
 169                  if (empty($this->_temp['no_top'])) {
 170                      $c->add('TOP');
 171                  }
 172              }
 173          }
 174  
 175          $this->_setInit('capability', $c);
 176      }
 177  
 178      /**
 179       */
 180      protected function _noop()
 181      {
 182          $this->_sendLine('NOOP');
 183      }
 184  
 185      /**
 186       * @throws Horde_Imap_Client_Exception_NoSupportPop3
 187       */
 188      protected function _getNamespaces()
 189      {
 190          throw new Horde_Imap_Client_Exception_NoSupportPop3('Namespaces');
 191      }
 192  
 193      /**
 194       */
 195      protected function _login()
 196      {
 197          /* Blank passwords are not allowed, so no need to even try
 198           * authentication to determine this. */
 199          if (!strlen($this->getParam('password'))) {
 200              throw new Horde_Imap_Client_Exception(
 201                  Horde_Imap_Client_Translation::r("No password provided."),
 202                  Horde_Imap_Client_Exception::LOGIN_AUTHENTICATIONFAILED
 203              );
 204          }
 205  
 206          $this->_connect();
 207  
 208          $first_login = empty($this->_init['authmethod']);
 209  
 210          // Switch to secure channel if using TLS.
 211          if (!$this->isSecureConnection()) {
 212              $secure = $this->getParam('secure');
 213  
 214              if (($secure === 'tls') || $secure === true) {
 215                  // Switch over to a TLS connection.
 216                  if ($first_login && !$this->_capability('STLS')) {
 217                      if ($secure === 'tls') {
 218                          throw new Horde_Imap_Client_Exception(
 219                              Horde_Imap_Client_Translation::r("Could not open secure connection to the POP3 server.") . ' ' . Horde_Imap_Client_Translation::r("Server does not support secure connections."),
 220                              Horde_Imap_Client_Exception::LOGIN_TLSFAILURE
 221                          );
 222                      } else {
 223                          $this->setParam('secure', false);
 224                      }
 225                  } else {
 226                      $this->_sendLine('STLS');
 227  
 228                      $this->setParam('secure', 'tls');
 229  
 230                      if (!$this->_connection->startTls()) {
 231                          $this->logout();
 232                          throw new Horde_Imap_Client_Exception(
 233                              Horde_Imap_Client_Translation::r("Could not open secure connection to the POP3 server."),
 234                              Horde_Imap_Client_Exception::LOGIN_TLSFAILURE
 235                          );
 236                      }
 237                      $this->_debug->info('Successfully completed TLS negotiation.');
 238                  }
 239  
 240                  // Expire cached CAPABILITY information
 241                  $this->_setInit('capability');
 242              } else {
 243                  $this->setParam('secure', false);
 244              }
 245          }
 246  
 247          if ($first_login) {
 248              /* At least one server (Dovecot 1.x) may return SASL capability
 249               * with no arguments. */
 250              $auth_mech = $this->_capability()->getParams('SASL');
 251  
 252              if (isset($this->_temp['pop3timestamp'])) {
 253                  $auth_mech[] = 'APOP';
 254              }
 255  
 256              $auth_mech[] = 'USER';
 257  
 258              /* Enable UTF-8 mode (RFC 6856). MUST occur after STLS is
 259               * issued. */
 260              if ($this->_capability('UTF8')) {
 261                  try {
 262                      $this->_sendLine('UTF8');
 263                      $this->_temp['utf8'] = true;
 264                  } catch (Horde_Imap_Client_Exception $e) {
 265                      /* If server responds to UTF8 command with error,
 266                       * fallback to legacy non-UTF8 behavior. */
 267                  }
 268              }
 269          } else {
 270              $auth_mech = array($this->_init['authmethod']);
 271          }
 272  
 273          foreach ($auth_mech as $method) {
 274              try {
 275                  $this->_tryLogin($method);
 276                  $this->_setInit('authmethod', $method);
 277  
 278                  if (!empty($this->_temp['no_capa']) ||
 279                      !$this->_capability('UIDL')) {
 280                      $this->_setInit('capability');
 281                  }
 282  
 283                  return true;
 284              } catch (Horde_Imap_Client_Exception $e) {
 285                  if (!empty($this->_init['authmethod']) &&
 286                      ($e->getCode() != $e::LOGIN_UNAVAILABLE) &&
 287                      ($e->getCode() != $e::POP3_TEMP_ERROR)) {
 288                      $this->_setInit();
 289                      return $this->login();
 290                  }
 291              }
 292          }
 293  
 294          throw new Horde_Imap_Client_Exception(
 295              Horde_Imap_Client_Translation::r("POP3 server denied authentication."),
 296              $e->getCode() ?: $e::LOGIN_AUTHENTICATIONFAILED
 297          );
 298      }
 299  
 300      /**
 301       * Connects to the server.
 302       *
 303       * @throws Horde_Imap_Client_Exception
 304       */
 305      protected function _connect()
 306      {
 307          if (!is_null($this->_connection)) {
 308              return;
 309          }
 310  
 311          try {
 312              $this->_connection = new Horde_Imap_Client_Socket_Connection_Pop3(
 313                  $this->getParam('hostspec'),
 314                  $this->getParam('port'),
 315                  $this->getParam('timeout'),
 316                  $this->getParam('secure'),
 317                  $this->getParam('context'),
 318                  array(
 319                      'debug' => $this->_debug
 320                  )
 321              );
 322          } catch (Horde\Socket\Client\Exception $e) {
 323              $e2 = new Horde_Imap_Client_Exception(
 324                  Horde_Imap_Client_Translation::r("Error connecting to mail server."),
 325                  Horde_Imap_Client_Exception::SERVER_CONNECT
 326              );
 327              $e2->details = $e->details;
 328              throw $e2;
 329          }
 330  
 331          $line = $this->_getResponse();
 332  
 333          // Check for string matching APOP timestamp
 334          if (preg_match('/<.+@.+>/U', $line['resp'], $matches)) {
 335              $this->_temp['pop3timestamp'] = $matches[0];
 336          }
 337      }
 338  
 339      /**
 340       * Authenticate to the POP3 server.
 341       *
 342       * @param string $method  POP3 login method.
 343       *
 344       * @throws Horde_Imap_Client_Exception
 345       */
 346      protected function _tryLogin($method)
 347      {
 348          $username = $this->getParam('username');
 349          $password = $this->getParam('password');
 350  
 351          switch ($method) {
 352          case 'CRAM-MD5':
 353          case 'CRAM-SHA1':
 354          case 'CRAM-SHA256':
 355              // RFC 5034: CRAM-MD5
 356              // CRAM-SHA1 & CRAM-SHA256 supported by Courier SASL library
 357              $challenge = $this->_sendLine('AUTH ' . $method);
 358              $response = base64_encode($username . ' ' . hash_hmac(Horde_String::lower(substr($method, 5)), base64_decode(substr($challenge['resp'], 2)), $password, true));
 359              $this->_sendLine($response, array(
 360                  'debug' => sprintf('[AUTH Response (username: %s)]', $username)
 361              ));
 362              break;
 363  
 364          case 'DIGEST-MD5':
 365              // RFC 2831; Obsoleted by RFC 6331
 366              $challenge = $this->_sendLine('AUTH DIGEST-MD5');
 367              $response = base64_encode(new Horde_Imap_Client_Auth_DigestMD5(
 368                  $username,
 369                  $password,
 370                  base64_decode(substr($challenge['resp'], 2)),
 371                  $this->getParam('hostspec'),
 372                  'pop3'
 373              ));
 374              $sresponse = $this->_sendLine($response, array(
 375                  'debug' => sprintf('[AUTH Response (username: %s)]', $username)
 376              ));
 377              if (stripos(base64_decode(substr($sresponse['resp'], 2)), 'rspauth=') === false) {
 378                  throw new Horde_Imap_Client_Exception(
 379                      Horde_Imap_Client_Translation::r("Unexpected response from server when authenticating."),
 380                      Horde_Imap_Client_Exception::SERVER_CONNECT
 381                  );
 382              }
 383  
 384              /* POP3 doesn't use protocol's third step. */
 385              $this->_sendLine('');
 386              break;
 387  
 388          case 'LOGIN':
 389              // RFC 4616 (AUTH=PLAIN) & 5034 (POP3 SASL)
 390              $this->_sendLine('AUTH LOGIN');
 391              $this->_sendLine(base64_encode($username));
 392              $this->_sendLine(base64_encode($password), array(
 393                  'debug' => sprintf('[AUTH Password (username: %s)]', $username)
 394              ));
 395              break;
 396  
 397          case 'PLAIN':
 398              // RFC 5034
 399              $this->_sendLine('AUTH PLAIN ' . base64_encode(implode("\0", array(
 400                  $username,
 401                  $username,
 402                  $password
 403              ))), array(
 404                  'debug' => sprintf('AUTH PLAIN [Auth Response (username: %s)]', $username)
 405              ));
 406              break;
 407  
 408          case 'APOP':
 409              /* If UTF8 (+ USER) is active, and non-ASCII exists, need to apply
 410               * SASLprep to username/password. RFC 6856[2.2]. Reject if
 411               * UTF8 (+ USER) is not supported and 8-bit characters exist. */
 412              if (Horde_Mime::is8bit($username) ||
 413                  Horde_Mime::is8bit($password)) {
 414                  if (empty($this->_temp['utf8']) ||
 415                      !$this->_capability('UTF8', 'USER') ||
 416                      !class_exists('Horde_Stringprep')) {
 417                      $error = true;
 418                  } else {
 419                      Horde_Stringprep::autoload();
 420                      $saslprep = new Znerol\Component\Stringprep\Profile\SASLprep();
 421  
 422                      try {
 423                          $username = $saslprep->apply(
 424                              $username,
 425                              'UTF-8',
 426                              Znerol\Compnonent\Stringprep\Profile::MODE_QUERY
 427                          );
 428                          $password = $saslprep->apply(
 429                              $password,
 430                              'UTF-8',
 431                              Znerol\Compnonent\Stringprep\Profile::MODE_STORE
 432                          );
 433                          $error = false;
 434                      } catch (Znerol\Component\Stringprep\ProfileException $e) {
 435                          $error = true;
 436                      }
 437                  }
 438  
 439                  if ($error) {
 440                      throw new Horde_Imap_Client_Exception(
 441                          Horde_Imap_Client_Translation::r("Authentication failed."),
 442                          Horde_Imap_Client_Exception::LOGIN_AUTHENTICATIONFAILED
 443                      );
 444                  }
 445              }
 446  
 447              // RFC 1939 [7]
 448              $this->_sendLine('APOP ' . $username . ' ' .
 449                  hash('md5', $this->_temp['pop3timestamp'] . $password));
 450              break;
 451  
 452          case 'USER':
 453              /* POP3 servers without UTF8 (+ USER) does not accept non-ASCII
 454               * in USER/PASS. RFC 6856[2.2] */
 455              if ((empty($this->_temp['utf8']) ||
 456                   !$this->_capability('UTF8', 'USER')) &&
 457                  (Horde_Mime::is8bit($username) ||
 458                   Horde_Mime::is8bit($password))) {
 459                  throw new Horde_Imap_Client_Exception(
 460                      Horde_Imap_Client_Translation::r("Authentication failed."),
 461                      Horde_Imap_Client_Exception::LOGIN_AUTHENTICATIONFAILED
 462                  );
 463              }
 464  
 465              // RFC 1939 [7]
 466              $this->_sendLine('USER ' . $username);
 467              $this->_sendLine('PASS ' . $password, array(
 468                  'debug' => 'PASS [Password]'
 469              ));
 470              break;
 471  
 472          case 'SCRAM-SHA-1':
 473              $scram = new Horde_Imap_Client_Auth_Scram(
 474                  $username,
 475                  $password,
 476                  'SHA1'
 477              );
 478  
 479              $c1 = $this->_sendLine(
 480                  'AUTH ' . $method . ' ' . base64_encode($scram->getClientFirstMessage())
 481              );
 482  
 483              $sr1 = base64_decode(substr($c1['resp'], 2));
 484              if (!$scram->parseServerFirstMessage($sr1)) {
 485                  throw new Horde_Imap_Client_Exception(
 486                      Horde_Imap_Client_Translation::r("Authentication failed."),
 487                      Horde_Imap_Client_Exception::LOGIN_AUTHENTICATIONFAILED
 488                  );
 489              }
 490  
 491              $c2 = $this->_sendLine(
 492                  base64_encode($scram->getClientFinalMessage())
 493              );
 494  
 495              $sr2 = base64_decode(substr($c2['resp'], 2));
 496              if (!$scram->parseServerFirstMessage($sr)) {
 497                  throw new Horde_Imap_Client_Exception(
 498                      Horde_Imap_Client_Translation::r("Authentication failed."),
 499                      Horde_Imap_Client_Exception::LOGIN_AUTHENTICATIONFAILED
 500                  );
 501  
 502                  /* This means authentication passed, according to the server,
 503                   * but the server signature is incorrect. This indicates that
 504                   * server verification has failed. Immediately disconnect from
 505                   * the server, since this is a possible security issue. */
 506                  $this->logout();
 507                  throw new Horde_Imap_Client_Exception(
 508                      Horde_Imap_Client_Translation::r("Server failed verification check."),
 509                      Horde_Imap_Client_Exception::LOGIN_SERVER_VERIFICATION_FAILED
 510                  );
 511              }
 512  
 513              $this->_sendLine('');
 514              break;
 515  
 516          default:
 517              $e = new Horde_Imap_Client_Exception(
 518                  Horde_Imap_Client_Translation::r("Unknown authentication method: %s"),
 519                  Horde_Imap_Client_Exception::SERVER_CONNECT
 520              );
 521              $e->messagePrintf(array($method));
 522              throw $e;
 523          }
 524      }
 525  
 526      /**
 527       */
 528      protected function _logout()
 529      {
 530          try {
 531              $this->_sendLine('QUIT');
 532          } catch (Horde_Imap_Client_Exception $e) {}
 533          $this->_deleted = array();
 534      }
 535  
 536      /**
 537       * @throws Horde_Imap_Client_Exception_NoSupportPop3
 538       */
 539      protected function _sendID($info)
 540      {
 541          throw new Horde_Imap_Client_Exception_NoSupportPop3('ID command');
 542      }
 543  
 544      /**
 545       * Return implementation information from the POP3 server (RFC 2449 [6.9]).
 546       */
 547      protected function _getID()
 548      {
 549          return ($id = $this->_capability()->getParams('IMPLEMENTATION'))
 550              ? array('implementation' => reset($id))
 551              : array();
 552      }
 553  
 554      /**
 555       * @throws Horde_Imap_Client_Exception_NoSupportPop3
 556       */
 557      protected function _setLanguage($langs)
 558      {
 559          // RFC 6856 [3]
 560          if (!$this->_capability('LANG')) {
 561              throw new Horde_Imap_Client_Exception_NoSupportPop3('LANGUAGE extension');
 562          }
 563  
 564          foreach ($langs as $val) {
 565              try {
 566                  $this->_sendLine('LANG ' . $val);
 567                  $this->_temp['lang'] = $val;
 568              } catch (Horde_Imap_Client_Exception $e) {
 569                  // Setting language failed - move on to next one.
 570              }
 571          }
 572  
 573          return $this->_getLanguage(false);
 574      }
 575  
 576      /**
 577       * @throws Horde_Imap_Client_Exception_NoSupportPop3
 578       */
 579      protected function _getLanguage($list)
 580      {
 581          // RFC 6856 [3]
 582          if (!$this->_capability('LANG')) {
 583              throw new Horde_Imap_Client_Exception_NoSupportPop3('LANGUAGE extension');
 584          }
 585  
 586          if (!$list) {
 587              return isset($this->_temp['lang'])
 588                  ? $this->_temp['lang']
 589                  : null;
 590          }
 591  
 592          $langs = array();
 593  
 594          try {
 595              $res = $this->_sendLine('LANG', array(
 596                  'multiline' => 'array'
 597              ));
 598  
 599              foreach ($res['data'] as $val) {
 600                  $parts = explode(' ', $val);
 601                  $langs[] = $parts[0];
 602                  // $parts[1] - lanuage description (not used)
 603              }
 604          } catch (Horde_Imap_Client_Exception $e) {
 605              // Ignore: language listing might fail. RFC 6856 [3.3]
 606          }
 607  
 608          return $langs;
 609      }
 610  
 611      /**
 612       * @throws Horde_Imap_Client_Exception_NoSupportPop3
 613       */
 614      protected function _openMailbox(Horde_Imap_Client_Mailbox $mailbox, $mode)
 615      {
 616          if ($mailbox != 'INBOX') {
 617              throw new Horde_Imap_Client_Exception_NoSupportPop3('Mailboxes other than INBOX');
 618          }
 619          $this->_changeSelected($mailbox, $mode);
 620      }
 621  
 622      /**
 623       * @throws Horde_Imap_Client_Exception_NoSupportPop3
 624       */
 625      protected function _createMailbox(Horde_Imap_Client_Mailbox $mailbox, $opts)
 626      {
 627          throw new Horde_Imap_Client_Exception_NoSupportPop3('Creating mailboxes');
 628      }
 629  
 630      /**
 631       * @throws Horde_Imap_Client_Exception_NoSupportPop3
 632       */
 633      protected function _deleteMailbox(Horde_Imap_Client_Mailbox $mailbox)
 634      {
 635          throw new Horde_Imap_Client_Exception_NoSupportPop3('Deleting mailboxes');
 636      }
 637  
 638      /**
 639       * @throws Horde_Imap_Client_Exception_NoSupportPop3
 640       */
 641      protected function _renameMailbox(Horde_Imap_Client_Mailbox $old,
 642                                        Horde_Imap_Client_Mailbox $new)
 643      {
 644          throw new Horde_Imap_Client_Exception_NoSupportPop3('Renaming mailboxes');
 645      }
 646  
 647      /**
 648       * @throws Horde_Imap_Client_Exception_NoSupportPop3
 649       */
 650      protected function _subscribeMailbox(Horde_Imap_Client_Mailbox $mailbox,
 651                                           $subscribe)
 652      {
 653          throw new Horde_Imap_Client_Exception_NoSupportPop3('Mailboxes other than INBOX');
 654      }
 655  
 656      /**
 657       */
 658      protected function _listMailboxes($pattern, $mode, $options)
 659      {
 660          if (empty($options['flat'])) {
 661              return array(
 662                  'INBOX' => array(
 663                      'attributes' => array(),
 664                      'delimiter' => '',
 665                      'mailbox' => Horde_Imap_Client_Mailbox::get('INBOX')
 666                  )
 667              );
 668          }
 669  
 670          return array('INBOX' => Horde_Imap_Client_Mailbox::get('INBOX'));
 671      }
 672  
 673      /**
 674       * @param integer $flags   This driver only supports the options listed
 675       *                         under Horde_Imap_Client::STATUS_ALL.
 676       * @throws Horde_Imap_Client_Exception_NoSupportPop3
 677       */
 678      protected function _status($mboxes, $flags)
 679      {
 680          if ((count($mboxes) > 1) || (reset($mboxes) != 'INBOX')) {
 681              throw new Horde_Imap_Client_Exception_NoSupportPop3('Mailboxes other than INBOX');
 682          }
 683  
 684          $this->openMailbox('INBOX');
 685  
 686          $ret = array();
 687  
 688          if ($flags & Horde_Imap_Client::STATUS_MESSAGES) {
 689              $res = $this->_pop3Cache('stat');
 690              $ret['messages'] = $res['msgs'];
 691          }
 692  
 693          if ($flags & Horde_Imap_Client::STATUS_RECENT) {
 694              $res = $this->_pop3Cache('stat');
 695              $ret['recent'] = $res['msgs'];
 696          }
 697  
 698          // No need for STATUS_UIDNEXT_FORCE handling since STATUS_UIDNEXT will
 699          // always return a value.
 700          $uidl = $this->_capability('UIDL');
 701          if ($flags & Horde_Imap_Client::STATUS_UIDNEXT) {
 702              if ($uidl) {
 703                  $ctx = hash_init('md5');
 704                  foreach ($this->_pop3Cache('uidl') as $key => $val) {
 705                      hash_update($ctx, '|' . $key . '|' . $val);
 706                  }
 707                  $ret['uidnext'] = hash_final($ctx);
 708              } else {
 709                  $res = $this->_pop3Cache('stat');
 710                  $ret['uidnext'] = $res['msgs'] + 1;
 711              }
 712          }
 713  
 714          if ($flags & Horde_Imap_Client::STATUS_UIDVALIDITY) {
 715              $ret['uidvalidity'] = $uidl
 716                  ? 1
 717                  : microtime(true);
 718          }
 719  
 720          if ($flags & Horde_Imap_Client::STATUS_UNSEEN) {
 721              $ret['unseen'] = 0;
 722          }
 723  
 724          return array('INBOX' => $ret);
 725      }
 726  
 727      /**
 728       * @throws Horde_Imap_Client_Exception_NoSupportPop3
 729       */
 730      protected function _append(Horde_Imap_Client_Mailbox $mailbox, $data,
 731                                 $options)
 732      {
 733          throw new Horde_Imap_Client_Exception_NoSupportPop3('Appending messages');
 734      }
 735  
 736      /**
 737       */
 738      protected function _check()
 739      {
 740          $this->noop();
 741      }
 742  
 743      /**
 744       */
 745      protected function _close($options)
 746      {
 747          if (!empty($options['expunge'])) {
 748              $this->logout();
 749          }
 750      }
 751  
 752      /**
 753       * @param array $options  Additional options. 'ids' has no effect in this
 754       *                        driver.
 755       */
 756      protected function _expunge($options)
 757      {
 758          $msg_list = $this->_deleted;
 759          $this->logout();
 760          return empty($options['list'])
 761              ? null
 762              : $msg_list;
 763      }
 764  
 765      /**
 766       * @throws Horde_Imap_Client_Exception_NoSupportPop3
 767       */
 768      protected function _search($query, $options)
 769      {
 770          $sort = empty($options['sort'])
 771              ? null
 772              : reset($options['sort']);
 773  
 774          // Only support a single query: an ALL search sorted by sequence.
 775          if ((strval($options['_query']['query']) != 'ALL') ||
 776              ($sort &&
 777               ((count($options['sort']) > 1) ||
 778                ($sort != Horde_Imap_Client::SORT_SEQUENCE)))) {
 779              throw new Horde_Imap_Client_Exception_NoSupportPop3('Server search');
 780          }
 781  
 782          $status = $this->status($this->_selected, Horde_Imap_Client::STATUS_MESSAGES);
 783          $res = range(1, $status['messages']);
 784  
 785          if (empty($options['sequence'])) {
 786              $tmp = array();
 787              $uidllist = $this->_pop3Cache('uidl');
 788              foreach ($res as $val) {
 789                  $tmp[] = $uidllist[$val];
 790              }
 791              $res = $tmp;
 792          }
 793  
 794          if (!empty($options['partial'])) {
 795              $partial = $this->getIdsOb($options['partial'], true);
 796              $min = $partial->min - 1;
 797              $res = array_slice($res, $min, $partial->max - $min);
 798          }
 799  
 800          $ret = array();
 801          foreach ($options['results'] as $val) {
 802              switch ($val) {
 803              case Horde_Imap_Client::SEARCH_RESULTS_COUNT:
 804                  $ret['count'] = count($res);
 805                  break;
 806  
 807              case Horde_Imap_Client::SEARCH_RESULTS_MATCH:
 808                  $ret['match'] = $this->getIdsOb($res);
 809                  break;
 810  
 811              case Horde_Imap_Client::SEARCH_RESULTS_MAX:
 812                  $ret['max'] = empty($res) ? null : max($res);
 813                  break;
 814  
 815              case Horde_Imap_Client::SEARCH_RESULTS_MIN:
 816                  $ret['min'] = empty($res) ? null : min($res);
 817                  break;
 818              }
 819          }
 820  
 821          return $ret;
 822      }
 823  
 824      /**
 825       * @throws Horde_Imap_Client_Exception_NoSupportPop3
 826       */
 827      protected function _setComparator($comparator)
 828      {
 829          throw new Horde_Imap_Client_Exception_NoSupportPop3('Search comparators');
 830      }
 831  
 832      /**
 833       * @throws Horde_Imap_Client_Exception_NoSupportPop3
 834       */
 835      protected function _getComparator()
 836      {
 837          throw new Horde_Imap_Client_Exception_NoSupportPop3('Search comparators');
 838      }
 839  
 840      /**
 841       * @throws Horde_Imap_Client_Exception_NoSupportPop3
 842       */
 843      protected function _thread($options)
 844      {
 845          throw new Horde_Imap_Client_Exception_NoSupportPop3('Server threading');
 846      }
 847  
 848      /**
 849       */
 850      protected function _fetch(Horde_Imap_Client_Fetch_Results $results,
 851                                $queries)
 852      {
 853          foreach ($queries as $options) {
 854              $this->_fetchCmd($results, $options);
 855          }
 856  
 857          $this->_updateCache($results);
 858      }
 859  
 860       /**
 861       * Fetch data for a given fetch query.
 862       *
 863       * @param Horde_Imap_Client_Fetch_Results $results  Fetch results.
 864       * @param array $options                            Fetch query options.
 865       */
 866      protected function _fetchCmd(Horde_Imap_Client_Fetch_Results $results,
 867                                   $options)
 868      {
 869          // Grab sequence IDs - IDs will always be the message number for
 870          // POP3 fetch commands.
 871          $seq_ids = $this->_getSeqIds($options['ids']);
 872          if (empty($seq_ids)) {
 873              return;
 874          }
 875  
 876          $lookup = $options['ids']->sequence
 877              ? array_combine($seq_ids, $seq_ids)
 878              : $this->_pop3Cache('uidl');
 879  
 880          foreach ($options['_query'] as $type => $c_val) {
 881              switch ($type) {
 882              case Horde_Imap_Client::FETCH_FULLMSG:
 883                  foreach ($seq_ids as $id) {
 884                      $tmp = $this->_pop3Cache('msg', $id);
 885  
 886                      if (empty($c_val['start']) && empty($c_val['length'])) {
 887                          $tmp2 = fopen('php://temp', 'r+');
 888                          stream_copy_to_stream($tmp, $tmp2, empty($c_val['length']) ? -1 : $c_val['length'], empty($c_val['start']) ? 0 : $c_val['start']);
 889                          $results->get($lookup[$id])->setFullMsg($tmp2);
 890                      } else {
 891                          $results->get($lookup[$id])->setFullMsg($tmp);
 892                      }
 893                  }
 894                  break;
 895  
 896              case Horde_Imap_Client::FETCH_HEADERTEXT:
 897                  // Ignore 'peek' option
 898                  foreach ($c_val as $key => $val) {
 899                      foreach ($seq_ids as $id) {
 900                          /* Message header can be retrieved via TOP, if the
 901                           * command is available. */
 902                          try {
 903                              $tmp = ($key == 0)
 904                                  ? $this->_pop3Cache('hdr', $id)
 905                                  : Horde_Mime_Part::getRawPartText(stream_get_contents($this->_pop3Cache('msg', $id)), 'header', $key);
 906                              $results->get($lookup[$id])->setHeaderText($key, $this->_processString($tmp, $c_val));
 907                          } catch (Horde_Mime_Exception $e) {}
 908                      }
 909                  }
 910                  break;
 911  
 912              case Horde_Imap_Client::FETCH_BODYTEXT:
 913                  // Ignore 'peek' option
 914                  foreach ($c_val as $key => $val) {
 915                      foreach ($seq_ids as $id) {
 916                          try {
 917                              $results->get($lookup[$id])->setBodyText($key, $this->_processString(Horde_Mime_Part::getRawPartText(stream_get_contents($this->_pop3Cache('msg', $id)), 'body', $key), $val));
 918                          } catch (Horde_Mime_Exception $e) {}
 919                      }
 920                  }
 921                  break;
 922  
 923              case Horde_Imap_Client::FETCH_MIMEHEADER:
 924                  // Ignore 'peek' option
 925                  foreach ($c_val as $key => $val) {
 926                      foreach ($seq_ids as $id) {
 927                          try {
 928                              $results->get($lookup[$id])->setMimeHeader($key, $this->_processString(Horde_Mime_Part::getRawPartText(stream_get_contents($this->_pop3Cache('msg', $id)), 'header', $key), $val));
 929                          } catch (Horde_Mime_Exception $e) {}
 930                      }
 931                  }
 932                  break;
 933  
 934              case Horde_Imap_Client::FETCH_BODYPART:
 935                  // Ignore 'decode', 'peek'
 936                  foreach ($c_val as $key => $val) {
 937                      foreach ($seq_ids as $id) {
 938                          try {
 939                              $results->get($lookup[$id])->setBodyPart($key, $this->_processString(Horde_Mime_Part::getRawPartText(stream_get_contents($this->_pop3Cache('msg', $id)), 'body', $key), $val));
 940                          } catch (Horde_Mime_Exception $e) {}
 941                      }
 942                  }
 943                  break;
 944  
 945              case Horde_Imap_Client::FETCH_HEADERS:
 946                  // Ignore 'length', 'peek'
 947                  foreach ($seq_ids as $id) {
 948                      $ob = $this->_pop3Cache('hdrob', $id);
 949                      foreach ($c_val as $key => $val) {
 950                          $tmp = $ob;
 951  
 952                          if (empty($val['notsearch'])) {
 953                              $tmp2 = $tmp->toArray(array('nowrap' => true));
 954                              foreach (array_keys($tmp2) as $hdr) {
 955                                  if (!in_array($hdr, $val['headers'])) {
 956                                      unset($tmp[$hdr]);
 957                                  }
 958                              }
 959                          } else {
 960                              foreach ($val['headers'] as $hdr) {
 961                                  unset($tmp[$hdr]);
 962                              }
 963                          }
 964  
 965                          $results->get($lookup[$id])->setHeaders($key, $tmp);
 966                      }
 967                  }
 968                  break;
 969  
 970              case Horde_Imap_Client::FETCH_STRUCTURE:
 971                  foreach ($seq_ids as $id) {
 972                      if ($ptr = $this->_pop3Cache('msg', $id)) {
 973                          try {
 974                              $results->get($lookup[$id])->setStructure(Horde_Mime_Part::parseMessage(stream_get_contents($ptr), array('no_body' => true)));
 975                          } catch (Horde_Exception $e) {}
 976                      }
 977                  }
 978                  break;
 979  
 980              case Horde_Imap_Client::FETCH_ENVELOPE:
 981                  foreach ($seq_ids as $id) {
 982                      $tmp = $this->_pop3Cache('hdrob', $id);
 983                      $results->get($lookup[$id])->setEnvelope(array(
 984                          'date' => $tmp['Date'],
 985                          'subject' => $tmp['Subject'],
 986                          'from' => ($h = $tmp['From']) ? $h->getAddressList(true) : null,
 987                          'sender' => ($h = $tmp['Sender']) ? $h->getAddressList(true) : null,
 988                          'reply_to' => ($h = $tmp['Reply-to']) ? $h->getAddressList(true) : null,
 989                          'to' => ($h = $tmp['To']) ? $h->getAddressList(true) : null,
 990                          'cc' => ($h = $tmp['Cc']) ? $h->getAddressList(true) : null,
 991                          'bcc' => ($h = $tmp['Bcc']) ? $h->getAddressList(true) : null,
 992                          'in_reply_to' => $tmp['In-Reply-To'],
 993                          'message_id' => $tmp['Message-ID']
 994                      ));
 995                  }
 996                  break;
 997  
 998              case Horde_Imap_Client::FETCH_IMAPDATE:
 999                  foreach ($seq_ids as $id) {
1000                      $tmp = $this->_pop3Cache('hdrob', $id);
1001                      $results->get($lookup[$id])->setImapDate($tmp['Date']);
1002                  }
1003                  break;
1004  
1005              case Horde_Imap_Client::FETCH_SIZE:
1006                  $sizelist = $this->_pop3Cache('size');
1007                  foreach ($seq_ids as $id) {
1008                      $results->get($lookup[$id])->setSize($sizelist[$id]);
1009                  }
1010                  break;
1011  
1012              case Horde_Imap_Client::FETCH_SEQ:
1013                  foreach ($seq_ids as $id) {
1014                      $results->get($lookup[$id])->setSeq($id);
1015                  }
1016                  break;
1017  
1018              case Horde_Imap_Client::FETCH_UID:
1019                  $uidllist = $this->_pop3Cache('uidl');
1020                  foreach ($seq_ids as $id) {
1021                      if (isset($uidllist[$id])) {
1022                          $results->get($lookup[$id])->setUid($uidllist[$id]);
1023                      }
1024                  }
1025                  break;
1026              }
1027          }
1028      }
1029  
1030      /**
1031       * Retrieve locally cached message data.
1032       *
1033       * @param string $type    Either 'hdr', 'hdrob', 'msg', 'size', 'stat',
1034       *                        'top', or 'uidl'.
1035       * @param integer $index  The message index.
1036       * @param mixed $data     Additional information needed.
1037       *
1038       * @return mixed  The cached data. 'msg' returns a stream resource. All
1039       *                other types return strings.
1040       *
1041       * @throws Horde_Imap_Client_Exception
1042       */
1043      protected function _pop3Cache(
1044          $type, $index = self::MBOX_CACHE, $data = null
1045      )
1046      {
1047          if (isset($this->_temp['pop3cache'][$index][$type])) {
1048              if ($type == 'msg') {
1049                  rewind($this->_temp['pop3cache'][$index][$type]);
1050              }
1051              return $this->_temp['pop3cache'][$index][$type];
1052          }
1053  
1054          switch ($type) {
1055          case 'hdr':
1056          case 'top':
1057              $data = null;
1058              if (($type == 'top') || $this->_capability('TOP')) {
1059                  try {
1060                      $res = $this->_sendLine('TOP ' . $index . ' 0', array(
1061                          'multiline' => 'stream'
1062                      ));
1063                      rewind($res['data']);
1064                      $data = stream_get_contents($res['data']);
1065                      fclose($res['data']);
1066                  } catch (Horde_Imap_Client_Exception $e) {
1067                      $this->_temp['no_top'] = true;
1068                      if ($type == 'top') {
1069                          return null;
1070                      }
1071                  }
1072              }
1073  
1074              if (is_null($data)) {
1075                  $data = Horde_Mime_Part::getRawPartText(stream_get_contents($this->_pop3Cache('msg', $index)), 'header', 0);
1076              }
1077              break;
1078  
1079          case 'hdrob':
1080              $data = Horde_Mime_Headers::parseHeaders($this->_pop3Cache('hdr', $index));
1081              break;
1082  
1083          case 'msg':
1084              $res = $this->_sendLine('RETR ' . $index, array(
1085                  'multiline' => 'stream'
1086              ));
1087              $data = $res['data'];
1088              rewind($data);
1089              break;
1090  
1091          case 'size':
1092          case 'uidl':
1093              $data = array();
1094              try {
1095                  $res = $this->_sendLine(($type == 'size') ? 'LIST' : 'UIDL', array(
1096                      'multiline' => 'array'
1097                  ));
1098                  foreach ($res['data'] as $val) {
1099                      $resp_data = explode(' ', $val, 2);
1100                      $data[$resp_data[0]] = $resp_data[1];
1101                  }
1102              } catch (Horde_Imap_Client_Exception $e) {
1103                  if ($type == 'uidl') {
1104                      $this->_temp['no_uidl'] = true;
1105                  }
1106              }
1107              break;
1108  
1109          case 'stat':
1110              $resp = $this->_sendLine('STAT');
1111              $resp_data = explode(' ', $resp['resp'], 2);
1112              $data = array('msgs' => $resp_data[0], 'size' => $resp_data[1]);
1113              break;
1114          }
1115  
1116          $this->_temp['pop3cache'][$index][$type] = $data;
1117  
1118          return $data;
1119      }
1120  
1121      /**
1122       * Process a string response based on criteria options.
1123       *
1124       * @param string $str  The original string.
1125       * @param array $opts  The criteria options.
1126       *
1127       * @return string  The requested string.
1128       */
1129      protected function _processString($str, $opts)
1130      {
1131          if (!empty($opts['length'])) {
1132              return substr($str, empty($opts['start']) ? 0 : $opts['start'], $opts['length']);
1133          } elseif (!empty($opts['start'])) {
1134              return substr($str, $opts['start']);
1135          }
1136  
1137          return $str;
1138      }
1139  
1140      /**
1141       * @throws Horde_Imap_Client_Exception_NoSupportPop3
1142       */
1143      protected function _vanished($modseq, Horde_Imap_Client_Ids $ids)
1144      {
1145          throw new Horde_Imap_Client_Exception_NoSupportPop3('QRESYNC commands');
1146      }
1147  
1148      /**
1149       * @param array $options  Additional options. This driver does not support
1150       *                        'unchangedsince'.
1151       */
1152      protected function _store($options)
1153      {
1154          $delete = $reset = false;
1155  
1156          /* Only support deleting/undeleting messages. */
1157          if (isset($options['replace'])) {
1158              $delete = (bool)(count(array_intersect($options['replace'], array(
1159                  Horde_Imap_Client::FLAG_DELETED
1160              ))));
1161              $reset = !$delete;
1162          } else {
1163              if (!empty($options['add'])) {
1164                  $delete = (bool)(count(array_intersect($options['add'], array(
1165                      Horde_Imap_Client::FLAG_DELETED
1166                  ))));
1167              }
1168  
1169              if (!empty($options['remove'])) {
1170                  $reset = !(bool)(count(array_intersect($options['remove'], array(
1171                      Horde_Imap_Client::FLAG_DELETED
1172                  ))));
1173              }
1174          }
1175  
1176          if ($reset) {
1177              $this->_sendLine('RSET');
1178          } elseif ($delete) {
1179              foreach ($this->_getSeqIds($options['ids']) as $id) {
1180                  try {
1181                      $this->_sendLine('DELE ' . $id);
1182                      $this->_deleted[] = $id;
1183  
1184                      unset(
1185                          $this->_temp['pop3cache'][self::MBOX_CACHE],
1186                          $this->_temp['pop3cache'][$id]
1187                      );
1188                  } catch (Horde_Imap_Client_Exception $e) {}
1189              }
1190          }
1191  
1192          return $this->getIdsOb();
1193      }
1194  
1195      /**
1196       * @throws Horde_Imap_Client_Exception_NoSupportPop3
1197       */
1198      protected function _copy(Horde_Imap_Client_Mailbox $dest, $options)
1199      {
1200          throw new Horde_Imap_Client_Exception_NoSupportPop3('Copying messages');
1201      }
1202  
1203      /**
1204       * @throws Horde_Imap_Client_Exception_NoSupportPop3
1205       */
1206      protected function _setQuota(Horde_Imap_Client_Mailbox $root, $options)
1207      {
1208          throw new Horde_Imap_Client_Exception_NoSupportPop3('Quotas');
1209      }
1210  
1211      /**
1212       * @throws Horde_Imap_Client_Exception_NoSupportPop3
1213       */
1214      protected function _getQuota(Horde_Imap_Client_Mailbox $root)
1215      {
1216          throw new Horde_Imap_Client_Exception_NoSupportPop3('Quotas');
1217      }
1218  
1219      /**
1220       * @throws Horde_Imap_Client_Exception_NoSupportPop3
1221       */
1222      protected function _getQuotaRoot(Horde_Imap_Client_Mailbox $mailbox)
1223      {
1224          throw new Horde_Imap_Client_Exception_NoSupportPop3('Quotas');
1225      }
1226  
1227      /**
1228       * @throws Horde_Imap_Client_Exception_NoSupportPop3
1229       */
1230      protected function _setACL(Horde_Imap_Client_Mailbox $mailbox, $identifier,
1231                                 $options)
1232      {
1233          throw new Horde_Imap_Client_Exception_NoSupportPop3('ACLs');
1234      }
1235  
1236      /**
1237       * @throws Horde_Imap_Client_Exception_NoSupportPop3
1238       */
1239      protected function _deleteACL(Horde_Imap_Client_Mailbox $mailbox, $identifier)
1240      {
1241          throw new Horde_Imap_Client_Exception_NoSupportPop3('ACLs');
1242      }
1243  
1244      /**
1245       * @throws Horde_Imap_Client_Exception_NoSupportPop3
1246       */
1247      protected function _getACL(Horde_Imap_Client_Mailbox $mailbox)
1248      {
1249          throw new Horde_Imap_Client_Exception_NoSupportPop3('ACLs');
1250      }
1251  
1252      /**
1253       * @throws Horde_Imap_Client_Exception_NoSupportPop3
1254       */
1255      protected function _listACLRights(Horde_Imap_Client_Mailbox $mailbox,
1256                                        $identifier)
1257      {
1258          throw new Horde_Imap_Client_Exception_NoSupportPop3('ACLs');
1259      }
1260  
1261      /**
1262       * @throws Horde_Imap_Client_Exception_NoSupportPop3
1263       */
1264      protected function _getMyACLRights(Horde_Imap_Client_Mailbox $mailbox)
1265      {
1266          throw new Horde_Imap_Client_Exception_NoSupportPop3('ACLs');
1267      }
1268  
1269      /**
1270       * @throws Horde_Imap_Client_Exception_NoSupportPop3
1271       */
1272      protected function _getMetadata(Horde_Imap_Client_Mailbox $mailbox,
1273                                      $entries, $options)
1274      {
1275          throw new Horde_Imap_Client_Exception_NoSupportPop3('Metadata');
1276      }
1277  
1278      /**
1279       * @throws Horde_Imap_Client_Exception_NoSupportPop3
1280       */
1281      protected function _setMetadata(Horde_Imap_Client_Mailbox $mailbox, $data)
1282      {
1283          throw new Horde_Imap_Client_Exception_NoSupportPop3('Metadata');
1284      }
1285  
1286      /**
1287       */
1288      protected function _getSearchCache($type, $options)
1289      {
1290          /* POP3 does not support search caching. */
1291          return null;
1292      }
1293  
1294      /**
1295       */
1296      public function resolveIds(Horde_Imap_Client_Mailbox $mailbox,
1297                                 Horde_Imap_Client_Ids $ids, $convert = 0)
1298      {
1299          if (!$ids->special &&
1300              (!$convert ||
1301               (!$ids->sequence && ($convert == 1)) ||
1302               $ids->isEmpty())) {
1303              return clone $ids;
1304          }
1305  
1306          $uids = $this->_pop3Cache('uidl');
1307  
1308          return $this->getIdsOb(
1309              $ids->all ? array_values($uids) : array_intersect_keys($uids, $ids->ids)
1310          );
1311      }
1312  
1313      /* Internal functions. */
1314  
1315      /**
1316       * Perform a command on the server. A connection to the server must have
1317       * already been made.
1318       *
1319       * @param string $cmd     The command to execute.
1320       * @param array $options  Additional options:
1321       * <pre>
1322       *   - debug: (string) When debugging, send this string instead of the
1323       *            actual command/data sent.
1324       *            DEFAULT: Raw data output to debug stream.
1325       *   - multiline: (mixed) 'array', 'none', or 'stream'.
1326       * </pre>
1327       *
1328       * @return array  See _getResponse().
1329       *
1330       * @throws Horde_Imap_Client_Exception
1331       */
1332      protected function _sendLine($cmd, $options = array())
1333      {
1334          if (!empty($options['debug'])) {
1335              $this->_debug->client($options['debug']);
1336          }
1337  
1338          if ($this->_debug->debug) {
1339              $timer = new Horde_Support_Timer();
1340              $timer->push();
1341          }
1342  
1343          try {
1344              $this->_connection->write($cmd, empty($options['debug']));
1345          } catch (Horde_Imap_Client_Exception $e) {
1346              throw $e;
1347          }
1348  
1349          $resp = $this->_getResponse(
1350              empty($options['multiline']) ? false : $options['multiline']
1351          );
1352  
1353          if ($this->_debug->debug) {
1354              $this->_debug->info(sprintf(
1355                  'Command took %s seconds.',
1356                  round($timer->pop(), 4)
1357              ));
1358          }
1359  
1360          return $resp;
1361      }
1362  
1363      /**
1364       * Gets a line from the stream and parses it.
1365       *
1366       * @param mixed $multiline  'array', 'none', 'stream', or null.
1367       *
1368       * @return array  An array with the following keys:
1369       *   - data: (mixed) Stream, array, or null.
1370       *   - resp: (string) The server response text.
1371       *
1372       * @throws Horde_Imap_Client_Exception
1373       */
1374      protected function _getResponse($multiline = false)
1375      {
1376          $ob = array('resp' => '');
1377  
1378          $read = explode(' ', rtrim($this->_connection->read(), "\r\n"), 2);
1379          if (!in_array($read[0], array('+OK', '-ERR', '+'))) {
1380              $this->_debug->info('ERROR: IMAP read/timeout error.');
1381              throw new Horde_Imap_Client_Exception(
1382                  Horde_Imap_Client_Translation::r("Error when communicating with the mail server."),
1383                  Horde_Imap_Client_Exception::SERVER_READERROR
1384              );
1385          }
1386  
1387          $respcode = null;
1388          if (isset($read[1]) &&
1389              isset($this->_init['capability']) &&
1390              $this->_capability('RESP-CODES')) {
1391              $respcode = $this->_parseResponseCode($read[1]);
1392          }
1393  
1394          switch ($read[0]) {
1395          case '+OK':
1396          case '+':
1397              if ($respcode) {
1398                  $ob['resp'] = $respcode->text;
1399              } elseif (isset($read[1])) {
1400                  $ob['resp'] = $read[1];
1401              }
1402              break;
1403  
1404          case '-ERR':
1405              $errcode = 0;
1406              if ($respcode) {
1407                  $errtext = $respcode->text;
1408  
1409                  if (isset($respcode->code)) {
1410                      switch ($respcode->code) {
1411                      // RFC 2449 [8.1.1]
1412                      case 'IN-USE':
1413                      // RFC 2449 [8.1.2]
1414                      case 'LOGIN-DELAY':
1415                          $errcode = Horde_Imap_Client_Exception::LOGIN_UNAVAILABLE;
1416                          break;
1417  
1418                      // RFC 3206 [4]
1419                      case 'SYS/TEMP':
1420                          $errcode = Horde_Imap_Client_Exception::POP3_TEMP_ERROR;
1421                          break;
1422  
1423                      // RFC 3206 [4]
1424                      case 'SYS/PERM':
1425                          $errcode = Horde_Imap_Client_Exception::POP3_PERM_ERROR;
1426                          break;
1427  
1428                      // RFC 3206 [5]
1429                      case 'AUTH':
1430                          $errcode = Horde_Imap_Client_Exception::LOGIN_AUTHENTICATIONFAILED;
1431                          break;
1432  
1433                      // RFC 6856 [5]
1434                      case 'UTF8':
1435                          /* This code can only be issued if we (as client) are
1436                           * broken, so no need to handle since we should never
1437                           * be broken. */
1438                          break;
1439                      }
1440                  }
1441              } elseif (isset($read[1])) {
1442                  $errtext = $read[1];
1443              } else {
1444                  $errtext = '[No error message provided by server]';
1445              }
1446  
1447              $e = new Horde_Imap_Client_Exception(
1448                  Horde_Imap_Client_Translation::r("POP3 error reported by server."),
1449                  $errcode
1450              );
1451              $e->details = $errtext;
1452              throw $e;
1453          }
1454  
1455          switch ($multiline) {
1456          case 'array':
1457              $ob['data'] = array();
1458              break;
1459  
1460          case 'none':
1461              $ob['data'] = null;
1462              break;
1463  
1464          case 'stream':
1465              $ob['data'] = fopen('php://temp', 'r+');
1466              break;
1467  
1468          default:
1469              return $ob;
1470          }
1471  
1472          do {
1473              $orig_read = $this->_connection->read();
1474              $read = rtrim($orig_read, "\r\n");
1475  
1476              if ($read === '.') {
1477                  break;
1478              } elseif (substr($read, 0, 2) === '..') {
1479                  $read = substr($read, 1);
1480              }
1481  
1482              if (is_array($ob['data'])) {
1483                  $ob['data'][] = $read;
1484              } elseif (!is_null($ob['data'])) {
1485                  fwrite($ob['data'], $orig_read);
1486              }
1487          } while (true);
1488  
1489          return $ob;
1490      }
1491  
1492      /**
1493       * Returns a list of sequence IDs.
1494       *
1495       * @param Horde_Imap_Client_Ids $ids  The ID list.
1496       *
1497       * @return array  A list of sequence IDs.
1498       */
1499      protected function _getSeqIds(Horde_Imap_Client_Ids $ids)
1500      {
1501          if (!count($ids)) {
1502              $status = $this->status($this->_selected, Horde_Imap_Client::STATUS_MESSAGES);
1503              return range(1, $status['messages']);
1504          } elseif ($ids->sequence) {
1505              return $ids->ids;
1506          }
1507  
1508          return array_keys(array_intersect($this->_pop3Cache('uidl'), $ids->ids));
1509      }
1510  
1511      /**
1512       * Parses response text for response codes (RFC 2449 [8]).
1513       *
1514       * @param string $text  The response text.
1515       *
1516       * @return object  An object with the following properties:
1517       *   - code: (string) The response code, if it exists.
1518       *   - data: (string) The response code data, if it exists.
1519       *   - text: (string) The human-readable response text.
1520       */
1521      protected function _parseResponseCode($text)
1522      {
1523          $ret = new stdClass;
1524  
1525          $text = trim($text);
1526          if ($text[0] === '[') {
1527              $pos = strpos($text, ' ', 2);
1528              $end_pos = strpos($text, ']', 2);
1529              if ($pos > $end_pos) {
1530                  $ret->code = Horde_String::upper(substr($text, 1, $end_pos - 1));
1531              } else {
1532                  $ret->code = Horde_String::upper(substr($text, 1, $pos - 1));
1533                  $ret->data = substr($text, $pos + 1, $end_pos - $pos - 1);
1534              }
1535              $ret->text = trim(substr($text, $end_pos + 1));
1536          } else {
1537              $ret->text = $text;
1538          }
1539  
1540          return $ret;
1541      }
1542  
1543  }