/lib/horde/framework/Horde/Imap/Client/Socket.php
PHP | 5169 lines | 3392 code | 640 blank | 1137 comment | 471 complexity | cac63893e689657c347c9f55d02fb112 MD5 | raw file
Possible License(s): MIT, AGPL-3.0, MPL-2.0-no-copyleft-exception, LGPL-3.0, GPL-3.0, Apache-2.0, LGPL-2.1, BSD-3-Clause
Large files files are truncated, but you can click here to view the full file
- <?php
- /**
- * Copyright 2005-2017 Horde LLC (http://www.horde.org/)
- *
- * See the enclosed file LICENSE for license information (LGPL). If you
- * did not receive this file, see http://www.horde.org/licenses/lgpl21.
- *
- * Originally based on code from:
- * - auth.php (1.49)
- * - imap_general.php (1.212)
- * - imap_messages.php (revision 13038)
- * - strings.php (1.184.2.35)
- * from the Squirrelmail project.
- * Copyright (c) 1999-2007 The SquirrelMail Project Team
- *
- * @category Horde
- * @copyright 1999-2007 The SquirrelMail Project Team
- * @copyright 2005-2017 Horde LLC
- * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1
- * @package Imap_Client
- */
- /**
- * An interface to an IMAP4rev1 server (RFC 3501) using standard PHP code.
- *
- * Implements the following IMAP-related RFCs (see
- * http://www.iana.org/assignments/imap4-capabilities):
- * <pre>
- * - RFC 2086/4314: ACL
- * - RFC 2087: QUOTA
- * - RFC 2088: LITERAL+
- * - RFC 2195: AUTH=CRAM-MD5
- * - RFC 2221: LOGIN-REFERRALS
- * - RFC 2342: NAMESPACE
- * - RFC 2595/4616: TLS & AUTH=PLAIN
- * - RFC 2831: DIGEST-MD5 authentication mechanism (obsoleted by RFC 6331)
- * - RFC 2971: ID
- * - RFC 3348: CHILDREN
- * - RFC 3501: IMAP4rev1 specification
- * - RFC 3502: MULTIAPPEND
- * - RFC 3516: BINARY
- * - RFC 3691: UNSELECT
- * - RFC 4315: UIDPLUS
- * - RFC 4422: SASL Authentication (for DIGEST-MD5)
- * - RFC 4466: Collected extensions (updates RFCs 2088, 3501, 3502, 3516)
- * - RFC 4469/5550: CATENATE
- * - RFC 4731: ESEARCH
- * - RFC 4959: SASL-IR
- * - RFC 5032: WITHIN
- * - RFC 5161: ENABLE
- * - RFC 5182: SEARCHRES
- * - RFC 5255: LANGUAGE/I18NLEVEL
- * - RFC 5256: THREAD/SORT
- * - RFC 5258: LIST-EXTENDED
- * - RFC 5267: ESORT; PARTIAL search return option
- * - RFC 5464: METADATA
- * - RFC 5530: IMAP Response Codes
- * - RFC 5802: AUTH=SCRAM-SHA-1
- * - RFC 5819: LIST-STATUS
- * - RFC 5957: SORT=DISPLAY
- * - RFC 6154: SPECIAL-USE/CREATE-SPECIAL-USE
- * - RFC 6203: SEARCH=FUZZY
- * - RFC 6851: MOVE
- * - RFC 6855: UTF8=ACCEPT/UTF8=ONLY
- * - RFC 6858: DOWNGRADED response code
- * - RFC 7162: CONDSTORE/QRESYNC
- * </pre>
- *
- * Implements the following non-RFC extensions:
- * <pre>
- * - draft-ietf-morg-inthread-01: THREAD=REFS
- * - draft-daboo-imap-annotatemore-07: ANNOTATEMORE
- * - draft-daboo-imap-annotatemore-08: ANNOTATEMORE2
- * - XIMAPPROXY
- * Requires imapproxy v1.2.7-rc1 or later
- * See https://squirrelmail.svn.sourceforge.net/svnroot/squirrelmail/trunk/imap_proxy/README
- * - AUTH=XOAUTH2
- * https://developers.google.com/gmail/xoauth2_protocol
- * </pre>
- *
- * TODO (or not necessary?):
- * <pre>
- * - RFC 2177: IDLE
- * - RFC 2193: MAILBOX-REFERRALS
- * - RFC 4467/5092/5524/5550/5593: URLAUTH, URLAUTH=BINARY, URL-PARTIAL
- * - RFC 4978: COMPRESS=DEFLATE
- * See: http://bugs.php.net/bug.php?id=48725
- * - RFC 5257: ANNOTATE (Experimental)
- * - RFC 5259: CONVERT
- * - RFC 5267: CONTEXT=SEARCH; CONTEXT=SORT
- * - RFC 5465: NOTIFY
- * - RFC 5466: FILTERS
- * - RFC 6785: IMAPSIEVE
- * - RFC 7377: MULTISEARCH
- * </pre>
- *
- * @author Michael Slusarz <slusarz@horde.org>
- * @category Horde
- * @copyright 1999-2007 The SquirrelMail Project Team
- * @copyright 2005-2017 Horde LLC
- * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1
- * @package Imap_Client
- */
- class Horde_Imap_Client_Socket extends Horde_Imap_Client_Base
- {
- /**
- * Cache names used exclusively within this class.
- */
- const CACHE_FLAGS = 'HICflags';
- /**
- * Queued commands to send to the server.
- *
- * @var array
- */
- protected $_cmdQueue = array();
- /**
- * The default ports to use for a connection.
- *
- * @var array
- */
- protected $_defaultPorts = array(143, 993);
- /**
- * Mapping of status fields to IMAP names.
- *
- * @var array
- */
- protected $_statusFields = array(
- 'messages' => Horde_Imap_Client::STATUS_MESSAGES,
- 'recent' => Horde_Imap_Client::STATUS_RECENT,
- 'uidnext' => Horde_Imap_Client::STATUS_UIDNEXT,
- 'uidvalidity' => Horde_Imap_Client::STATUS_UIDVALIDITY,
- 'unseen' => Horde_Imap_Client::STATUS_UNSEEN,
- 'firstunseen' => Horde_Imap_Client::STATUS_FIRSTUNSEEN,
- 'flags' => Horde_Imap_Client::STATUS_FLAGS,
- 'permflags' => Horde_Imap_Client::STATUS_PERMFLAGS,
- 'uidnotsticky' => Horde_Imap_Client::STATUS_UIDNOTSTICKY,
- 'highestmodseq' => Horde_Imap_Client::STATUS_HIGHESTMODSEQ
- );
- /**
- * The unique tag to use when making an IMAP query.
- *
- * @var integer
- */
- protected $_tag = 0;
- /**
- * @param array $params A hash containing configuration parameters.
- * Additional parameters to base driver:
- * - debug_literal: (boolean) If true, will output the raw text of
- * literal responses to the debug stream. Otherwise,
- * outputs a summary of the literal response.
- * - envelope_addrs: (integer) The maximum number of address entries to
- * read for FETCH ENVELOPE address fields.
- * DEFAULT: 1000
- * - envelope_string: (integer) The maximum length of string fields
- * returned by the FETCH ENVELOPE command.
- * DEFAULT: 2048
- * - xoauth2_token: (mixed) If set, will authenticate via the XOAUTH2
- * mechanism (if available) with this token. Either a
- * string (since 2.13.0) or a
- * Horde_Imap_Client_Base_Password object (since
- * 2.14.0).
- */
- public function __construct(array $params = array())
- {
- parent::__construct(array_merge(array(
- 'debug_literal' => false,
- 'envelope_addrs' => 1000,
- 'envelope_string' => 2048
- ), $params));
- }
- /**
- */
- public function __get($name)
- {
- switch ($name) {
- case 'search_charset':
- if (!isset($this->_init['search_charset']) &&
- $this->_capability()->isEnabled('UTF8=ACCEPT')) {
- $this->_init['search_charset'] = new Horde_Imap_Client_Data_SearchCharset_Utf8();
- }
- break;
- }
- return parent::__get($name);
- }
- /**
- */
- public function getParam($key)
- {
- switch ($key) {
- case 'xoauth2_token':
- if (isset($this->_params[$key]) &&
- ($this->_params[$key] instanceof Horde_Imap_Client_Base_Password)) {
- return $this->_params[$key]->getPassword();
- }
- break;
- }
- return parent::getParam($key);
- }
- /**
- */
- public function update(SplSubject $subject)
- {
- if (!empty($this->_init['imapproxy']) &&
- ($subject instanceof Horde_Imap_Client_Data_Capability_Imap)) {
- $this->_setInit('enabled', $subject->isEnabled());
- }
- return parent::update($subject);
- }
- /**
- */
- protected function _initCapability()
- {
- // Need to use connect call here or else we run into loop issues
- // because _connect() can generate the capability object internally.
- $this->_connect();
- // It is possible the server provided capability information on
- // connect, so check for it now.
- if (!isset($this->_init['capability'])) {
- $this->_sendCmd($this->_command('CAPABILITY'));
- }
- }
- /**
- * Parse a CAPABILITY Response (RFC 3501 [7.2.1]).
- *
- * @param Horde_Imap_Client_Interaction_Pipeline $pipeline Pipeline
- * object.
- * @param array $data An array of CAPABILITY strings.
- */
- protected function _parseCapability(
- Horde_Imap_Client_Interaction_Pipeline $pipeline,
- $data
- )
- {
- if (!empty($this->_temp['no_cap'])) {
- return;
- }
- $pipeline->data['capability_set'] = true;
- $c = new Horde_Imap_Client_Data_Capability_Imap();
- foreach ($data as $val) {
- $cap_list = explode('=', $val);
- $c->add(
- $cap_list[0],
- isset($cap_list[1]) ? array($cap_list[1]) : null
- );
- }
- $this->_setInit('capability', $c);
- }
- /**
- */
- protected function _noop()
- {
- // NOOP doesn't return any specific response
- $this->_sendCmd($this->_command('NOOP'));
- }
- /**
- */
- protected function _getNamespaces()
- {
- if ($this->_capability('NAMESPACE')) {
- $data = $this->_sendCmd($this->_command('NAMESPACE'))->data;
- if (isset($data['namespace'])) {
- return $data['namespace'];
- }
- }
- return new Horde_Imap_Client_Namespace_List();
- }
- /**
- * Parse a NAMESPACE response (RFC 2342 [5] & RFC 5255 [3.4]).
- *
- * @param Horde_Imap_Client_Interaction_Pipeline $pipeline Pipeline
- * object.
- * @param Horde_Imap_Client_Tokenize $data The NAMESPACE data.
- */
- protected function _parseNamespace(
- Horde_Imap_Client_Interaction_Pipeline $pipeline,
- Horde_Imap_Client_Tokenize $data
- )
- {
- $namespace_array = array(
- Horde_Imap_Client_Data_Namespace::NS_PERSONAL,
- Horde_Imap_Client_Data_Namespace::NS_OTHER,
- Horde_Imap_Client_Data_Namespace::NS_SHARED
- );
- $c = array();
- // Per RFC 2342, response from NAMESPACE command is:
- // (PERSONAL NAMESPACES) (OTHER_USERS NAMESPACE) (SHARED NAMESPACES)
- foreach ($namespace_array as $val) {
- $entry = $data->next();
- if (is_null($entry)) {
- continue;
- }
- while ($data->next() !== false) {
- $ob = Horde_Imap_Client_Mailbox::get($data->next(), true);
- $ns = new Horde_Imap_Client_Data_Namespace();
- $ns->delimiter = $data->next();
- $ns->name = strval($ob);
- $ns->type = $val;
- $c[strval($ob)] = $ns;
- // RFC 4466: NAMESPACE extensions
- while (($ext = $data->next()) !== false) {
- switch (Horde_String::upper($ext)) {
- case 'TRANSLATION':
- // RFC 5255 [3.4] - TRANSLATION extension
- $data->next();
- $ns->translation = $data->next();
- $data->next();
- break;
- }
- }
- }
- }
- $pipeline->data['namespace'] = new Horde_Imap_Client_Namespace_List($c);
- }
- /**
- */
- protected function _login()
- {
- $secure = $this->getParam('secure');
- if (!empty($this->_temp['preauth'])) {
- unset($this->_temp['preauth']);
- /* Don't allow PREAUTH if we are requring secure access, since
- * PREAUTH cannot provide secure access. */
- if (!$this->isSecureConnection() && ($secure !== false)) {
- $this->logout();
- throw new Horde_Imap_Client_Exception(
- Horde_Imap_Client_Translation::r("Could not open secure TLS connection to the IMAP server."),
- Horde_Imap_Client_Exception::LOGIN_TLSFAILURE
- );
- }
- return $this->_loginTasks();
- }
- /* Blank passwords are not allowed, so no need to even try
- * authentication to determine this. */
- if (!strlen($this->getParam('password'))) {
- throw new Horde_Imap_Client_Exception(
- Horde_Imap_Client_Translation::r("No password provided."),
- Horde_Imap_Client_Exception::LOGIN_AUTHENTICATIONFAILED
- );
- }
- $this->_connect();
- $first_login = empty($this->_init['authmethod']);
- // Switch to secure channel if using TLS.
- if (!$this->isSecureConnection() &&
- (($secure === 'tls') ||
- (($secure === true) &&
- $this->_capability('LOGINDISABLED')))) {
- if ($first_login && !$this->_capability('STARTTLS')) {
- /* We should never hit this - STARTTLS is required pursuant to
- * RFC 3501 [6.2.1]. */
- throw new Horde_Imap_Client_Exception(
- Horde_Imap_Client_Translation::r("Server does not support TLS connections."),
- Horde_Imap_Client_Exception::LOGIN_TLSFAILURE
- );
- }
- // Switch over to a TLS connection.
- // STARTTLS returns no untagged response.
- $this->_sendCmd($this->_command('STARTTLS'));
- if (!$this->_connection->startTls()) {
- $this->logout();
- throw new Horde_Imap_Client_Exception(
- Horde_Imap_Client_Translation::r("Could not open secure TLS connection to the IMAP server."),
- Horde_Imap_Client_Exception::LOGIN_TLSFAILURE
- );
- }
- $this->_debug->info('Successfully completed TLS negotiation.');
- $this->setParam('secure', 'tls');
- $secure = 'tls';
- if ($first_login) {
- // Expire cached CAPABILITY information (RFC 3501 [6.2.1])
- $this->_setInit('capability');
- // Reset language (RFC 5255 [3.1])
- $this->_setInit('lang');
- }
- // Set language if using imapproxy
- if (!empty($this->_init['imapproxy'])) {
- $this->setLanguage();
- }
- }
- /* If we reached this point and don't have a secure connection, then
- * a secure connections is not available. */
- if (($secure === true) && !$this->isSecureConnection()) {
- $this->setParam('secure', false);
- $secure = false;
- }
- if ($first_login) {
- // Add authentication methods.
- $auth_mech = array();
- $auth = array_flip($this->_capability()->getParams('AUTH'));
- // XOAUTH2
- if (isset($auth['XOAUTH2']) && $this->getParam('xoauth2_token')) {
- $auth_mech[] = 'XOAUTH2';
- }
- unset($auth['XOAUTH2']);
- /* 'AUTH=PLAIN' authentication always exists if under TLS (RFC 3501
- * [7.2.1]; RFC 2595), even though we might get here with a
- * non-TLS secure connection too. Use it over all other
- * authentication methods, although we need to do sanity checking
- * since broken IMAP servers may not support as required -
- * fallback to LOGIN instead, if not explicitly disabled. */
- if ($secure) {
- if (isset($auth['PLAIN'])) {
- $auth_mech[] = 'PLAIN';
- unset($auth['PLAIN']);
- } elseif (!$this->_capability('LOGINDISABLED')) {
- $auth_mech[] = 'LOGIN';
- }
- }
- // Check for supported SCRAM AUTH mechanisms. Preferred because it
- // provides verification of server authenticity.
- foreach (array_keys($auth) as $key) {
- switch ($key) {
- case 'SCRAM-SHA-1':
- $auth_mech[] = $key;
- unset($auth[$key]);
- break;
- }
- }
- // Check for supported CRAM AUTH mechanisms.
- foreach (array_keys($auth) as $key) {
- switch ($key) {
- case 'CRAM-SHA1':
- case 'CRAM-SHA256':
- $auth_mech[] = $key;
- unset($auth[$key]);
- break;
- }
- }
- // Prefer CRAM-MD5 over DIGEST-MD5, as the latter has been
- // obsoleted (RFC 6331).
- if (isset($auth['CRAM-MD5'])) {
- $auth_mech[] = 'CRAM-MD5';
- } elseif (isset($auth['DIGEST-MD5'])) {
- $auth_mech[] = 'DIGEST-MD5';
- }
- unset($auth['CRAM-MD5'], $auth['DIGEST-MD5']);
- // Add other auth mechanisms.
- $auth_mech = array_merge($auth_mech, array_keys($auth));
- // Fall back to 'LOGIN' if available.
- if (!$secure && !$this->_capability('LOGINDISABLED')) {
- $auth_mech[] = 'LOGIN';
- }
- if (empty($auth_mech)) {
- throw new Horde_Imap_Client_Exception(
- Horde_Imap_Client_Translation::r("No supported IMAP authentication method could be found."),
- Horde_Imap_Client_Exception::LOGIN_NOAUTHMETHOD
- );
- }
- $auth_mech = array_unique($auth_mech);
- } else {
- $auth_mech = array($this->_init['authmethod']);
- }
- $login_err = null;
- foreach ($auth_mech as $method) {
- try {
- $resp = $this->_tryLogin($method);
- $data = $resp->data;
- $this->_setInit('authmethod', $method);
- unset($this->_temp['referralcount']);
- } catch (Horde_Imap_Client_Exception_ServerResponse $e) {
- $data = $e->resp_data;
- if (isset($data['loginerr'])) {
- $login_err = $data['loginerr'];
- }
- $resp = false;
- } catch (Horde_Imap_Client_Exception $e) {
- $resp = false;
- }
- // Check for login referral (RFC 2221) response - can happen for
- // an OK, NO, or BYE response.
- if (isset($data['referral'])) {
- foreach (array('host', 'port', 'username') as $val) {
- if (!is_null($data['referral']->$val)) {
- $this->setParam($val, $data['referral']->$val);
- }
- }
- if (!is_null($data['referral']->auth)) {
- $this->_setInit('authmethod', $data['referral']->auth);
- }
- if (!isset($this->_temp['referralcount'])) {
- $this->_temp['referralcount'] = 0;
- }
- // RFC 2221 [3] - Don't follow more than 10 levels of referral
- // without consulting the user.
- if (++$this->_temp['referralcount'] < 10) {
- $this->logout();
- $this->_setInit('capability');
- $this->_setInit('namespace');
- return $this->login();
- }
- unset($this->_temp['referralcount']);
- }
- if ($resp) {
- return $this->_loginTasks($first_login, $resp->data);
- }
- }
- /* Try again from scratch if authentication failed in an established,
- * previously-authenticated object. */
- if (!empty($this->_init['authmethod'])) {
- $this->_setInit();
- unset($this->_temp['no_cap']);
- try {
- return $this->_login();
- } catch (Horde_Imap_Client_Exception $e) {}
- }
- /* Default to AUTHENTICATIONFAILED error (see RFC 5530[3]). */
- if (is_null($login_err)) {
- throw new Horde_Imap_Client_Exception(
- Horde_Imap_Client_Translation::r("Mail server denied authentication."),
- Horde_Imap_Client_Exception::LOGIN_AUTHENTICATIONFAILED
- );
- }
- throw $login_err;
- }
- /**
- * Connects to the IMAP server.
- *
- * @throws Horde_Imap_Client_Exception
- */
- protected function _connect()
- {
- if (!is_null($this->_connection)) {
- return;
- }
- try {
- $this->_connection = new Horde_Imap_Client_Socket_Connection_Socket(
- $this->getParam('hostspec'),
- $this->getParam('port'),
- $this->getParam('timeout'),
- $this->getParam('secure'),
- $this->getParam('context'),
- array(
- 'debug' => $this->_debug,
- 'debugliteral' => $this->getParam('debug_literal')
- )
- );
- } catch (Horde\Socket\Client\Exception $e) {
- $e2 = new Horde_Imap_Client_Exception(
- Horde_Imap_Client_Translation::r("Error connecting to mail server."),
- Horde_Imap_Client_Exception::SERVER_CONNECT
- );
- $e2->details = $e->details;
- throw $e2;
- }
- // If we already have capability information, don't re-set with
- // (possibly) limited information sent in the initial banner.
- if (isset($this->_init['capability'])) {
- $this->_temp['no_cap'] = true;
- }
- /* Get greeting information (untagged response). */
- try {
- $this->_getLine($this->_pipeline());
- } catch (Horde_Imap_Client_Exception_ServerResponse $e) {
- if ($e->status === Horde_Imap_Client_Interaction_Server::BYE) {
- /* Server is explicitly rejecting our connection (RFC 3501
- * [7.1.5]). */
- $e->setMessage(Horde_Imap_Client_Translation::r("Server rejected connection."));
- $e->setCode(Horde_Imap_Client_Exception::SERVER_CONNECT);
- }
- throw $e;
- }
- // Check for IMAP4rev1 support
- if (!$this->_capability('IMAP4REV1')) {
- throw new Horde_Imap_Client_Exception(
- Horde_Imap_Client_Translation::r("The mail server does not support IMAP4rev1 (RFC 3501)."),
- Horde_Imap_Client_Exception::SERVER_CONNECT
- );
- }
- // Set language if NOT using imapproxy
- if (empty($this->_init['imapproxy'])) {
- if ($this->_capability('XIMAPPROXY')) {
- $this->_setInit('imapproxy', true);
- } else {
- $this->setLanguage();
- }
- }
- // If pre-authenticated, we need to do all login tasks now.
- if (!empty($this->_temp['preauth'])) {
- $this->login();
- }
- }
- /**
- * Authenticate to the IMAP server.
- *
- * @param string $method IMAP login method.
- *
- * @return Horde_Imap_Client_Interaction_Pipeline Pipeline object.
- *
- * @throws Horde_Imap_Client_Exception
- */
- protected function _tryLogin($method)
- {
- $username = $this->getParam('username');
- if (is_null($authusername = $this->getParam('authusername'))) {
- $authusername = $username;
- }
- $password = $this->getParam('password');
- switch ($method) {
- case 'CRAM-MD5':
- case 'CRAM-SHA1':
- case 'CRAM-SHA256':
- // RFC 2195: CRAM-MD5
- // CRAM-SHA1 & CRAM-SHA256 supported by Courier SASL library
- $args = array(
- $username,
- Horde_String::lower(substr($method, 5)),
- $password
- );
- $cmd = $this->_command('AUTHENTICATE')->add(array(
- $method,
- new Horde_Imap_Client_Interaction_Command_Continuation(function($ob) use ($args) {
- return new Horde_Imap_Client_Data_Format_List(
- base64_encode($args[0] . ' ' . hash_hmac($args[1], base64_decode($ob->token->current()), $args[2], false))
- );
- })
- ));
- $cmd->debug = array(
- null,
- sprintf('[AUTHENTICATE response (username: %s)]', $username)
- );
- break;
- case 'DIGEST-MD5':
- // RFC 2831/4422; obsoleted by RFC 6331
- // Need $args because PHP 5.3 doesn't allow access to $this in
- // anonymous functions.
- $args = array(
- $username,
- $password,
- $this->getParam('hostspec')
- );
- $cmd = $this->_command('AUTHENTICATE')->add(array(
- $method,
- new Horde_Imap_Client_Interaction_Command_Continuation(function($ob) use ($args) {
- return new Horde_Imap_Client_Data_Format_List(
- base64_encode(new Horde_Imap_Client_Auth_DigestMD5(
- $args[0],
- $args[1],
- base64_decode($ob->token->current()),
- $args[2],
- 'imap'
- ))
- );
- }),
- new Horde_Imap_Client_Interaction_Command_Continuation(function($ob) {
- if (strpos(base64_decode($ob->token->current()), 'rspauth=') === false) {
- throw new Horde_Imap_Client_Exception(
- Horde_Imap_Client_Translation::r("Unexpected response from server when authenticating."),
- Horde_Imap_Client_Exception::SERVER_CONNECT
- );
- }
- return new Horde_Imap_Client_Data_Format_List();
- })
- ));
- $cmd->debug = array(
- null,
- sprintf('[AUTHENTICATE Response (username: %s)]', $username),
- null
- );
- break;
- case 'LOGIN':
- /* See, e.g., RFC 6855 [5] - LOGIN command does not support
- * non-ASCII characters. If we reach this point, treat as an
- * authentication failure. */
- try {
- $username = new Horde_Imap_Client_Data_Format_Astring($username);
- $password = new Horde_Imap_Client_Data_Format_Astring($password);
- } catch (Horde_Imap_Client_Data_Format_Exception $e) {
- throw new Horde_Imap_Client_Exception(
- Horde_Imap_Client_Translation::r("Authentication failed."),
- Horde_Imap_Client_Exception::LOGIN_AUTHENTICATIONFAILED
- );
- }
- $cmd = $this->_command('LOGIN')->add(array(
- $username,
- $password
- ));
- $cmd->debug = array(
- sprintf('LOGIN %s [PASSWORD]', $username)
- );
- break;
- case 'PLAIN':
- // RFC 2595/4616 - PLAIN SASL mechanism
- $cmd = $this->_authInitialResponse(
- $method,
- base64_encode(implode("\0", array(
- $username,
- $authusername,
- $password
- ))),
- $username
- );
- break;
- case 'SCRAM-SHA-1':
- $scram = new Horde_Imap_Client_Auth_Scram(
- $username,
- $password,
- 'SHA1'
- );
- $cmd = $this->_authInitialResponse(
- $method,
- base64_encode($scram->getClientFirstMessage())
- );
- $cmd->add(
- new Horde_Imap_Client_Interaction_Command_Continuation(function($ob) use ($scram) {
- $sr1 = base64_decode($ob->token->current());
- return new Horde_Imap_Client_Data_Format_List(
- $scram->parseServerFirstMessage($sr1)
- ? base64_encode($scram->getClientFinalMessage())
- : '*'
- );
- })
- );
- $self = $this;
- $cmd->add(
- new Horde_Imap_Client_Interaction_Command_Continuation(function($ob) use ($scram, $self) {
- $sr2 = base64_decode($ob->token->current());
- if (!$scram->parseServerFinalMessage($sr2)) {
- /* This means authentication passed, according to the
- * server, but the server signature is incorrect.
- * This indicates that server verification has failed.
- * Immediately disconnect from the server, since this
- * is a possible security issue. */
- $self->logout();
- throw new Horde_Imap_Client_Exception(
- Horde_Imap_Client_Translation::r("Server failed verification check."),
- Horde_Imap_Client_Exception::LOGIN_SERVER_VERIFICATION_FAILED
- );
- }
- return new Horde_Imap_Client_Data_Format_List();
- })
- );
- break;
- case 'XOAUTH2':
- // Google XOAUTH2
- $cmd = $this->_authInitialResponse(
- $method,
- $this->getParam('xoauth2_token')
- );
- /* This is an optional command continuation. XOAUTH2 will return
- * error information in continuation response. */
- $error_continuation = new Horde_Imap_Client_Interaction_Command_Continuation(
- function($ob) {
- return new Horde_Imap_Client_Data_Format_List();
- }
- );
- $error_continuation->optional = true;
- $cmd->add($error_continuation);
- break;
- default:
- $e = new Horde_Imap_Client_Exception(
- Horde_Imap_Client_Translation::r("Unknown authentication method: %s"),
- Horde_Imap_Client_Exception::SERVER_CONNECT
- );
- $e->messagePrintf(array($method));
- throw $e;
- }
- return $this->_sendCmd($this->_pipeline($cmd));
- }
- /**
- * Create the AUTHENTICATE command for the initial client response.
- *
- * @param string $method AUTHENTICATE SASL method.
- * @param string $ir Initial client response.
- * @param string $username If set, log a username message in debug log
- * instead of raw data.
- *
- * @return Horde_Imap_Client_Interaction_Command A command object.
- */
- protected function _authInitialResponse($method, $ir, $username = null)
- {
- $cmd = $this->_command('AUTHENTICATE')->add($method);
- if ($this->_capability('SASL-IR')) {
- // IMAP Extension for SASL Initial Client Response (RFC 4959)
- $cmd->add($ir);
- if ($username) {
- $cmd->debug = array(
- sprintf('AUTHENTICATE %s [INITIAL CLIENT RESPONSE (username: %s)]', $method, $username)
- );
- }
- } else {
- $cmd->add(
- new Horde_Imap_Client_Interaction_Command_Continuation(function($ob) use ($ir) {
- return new Horde_Imap_Client_Data_Format_List($ir);
- })
- );
- if ($username) {
- $cmd->debug = array(
- null,
- sprintf('[INITIAL CLIENT RESPONSE (username: %s)]', $username)
- );
- }
- }
- return $cmd;
- }
- /**
- * Perform login tasks.
- *
- * @param boolean $firstlogin Is this the first login?
- * @param array $resp The data response from the login command.
- * May include:
- * - capability_set: (boolean) True if CAPABILITY was set after login.
- * - proxyreuse: (boolean) True if re-used connection via imapproxy.
- *
- * @return boolean True if global login tasks should be performed.
- */
- protected function _loginTasks($firstlogin = true, array $resp = array())
- {
- /* If reusing an imapproxy connection, no need to do any of these
- * login tasks again. */
- if (!$firstlogin && !empty($resp['proxyreuse'])) {
- if (isset($this->_init['enabled'])) {
- foreach ($this->_init['enabled'] as $val) {
- $this->_capability()->enable($val);
- }
- }
- // If we have not yet set the language, set it now.
- if (!isset($this->_init['lang'])) {
- $this->_temp['lang_queue'] = true;
- $this->setLanguage();
- unset($this->_temp['lang_queue']);
- }
- return false;
- }
- /* If we logged in for first time, and server did not return
- * capability information, we need to mark for retrieval. */
- if ($firstlogin && empty($resp['capability_set'])) {
- $this->_setInit('capability');
- }
- $this->_temp['lang_queue'] = true;
- $this->setLanguage();
- unset($this->_temp['lang_queue']);
- /* Only active QRESYNC/CONDSTORE if caching is enabled. */
- $enable = array();
- if ($this->_initCache()) {
- if ($this->_capability('QRESYNC')) {
- $enable[] = 'QRESYNC';
- } elseif ($this->_capability('CONDSTORE')) {
- $enable[] = 'CONDSTORE';
- }
- }
- /* Use UTF8=ACCEPT, if available. */
- if ($this->_capability('UTF8', 'ACCEPT')) {
- $enable[] = 'UTF8=ACCEPT';
- }
- $this->_enable($enable);
- return true;
- }
- /**
- */
- protected function _logout()
- {
- if (empty($this->_temp['logout'])) {
- /* If using imapproxy, force sending these commands, since they
- * may not be sent again if they are (likely) initialization
- * commands. */
- if (!empty($this->_cmdQueue) &&
- !empty($this->_init['imapproxy'])) {
- $this->_sendCmd($this->_pipeline());
- }
- $this->_temp['logout'] = true;
- try {
- $this->_sendCmd($this->_command('LOGOUT'));
- } catch (Horde_Imap_Client_Exception_ServerResponse $e) {
- // Ignore server errors
- }
- unset($this->_temp['logout']);
- }
- }
- /**
- */
- protected function _sendID($info)
- {
- $cmd = $this->_command('ID');
- if (empty($info)) {
- $cmd->add(new Horde_Imap_Client_Data_Format_Nil());
- } else {
- $tmp = new Horde_Imap_Client_Data_Format_List();
- foreach ($info as $key => $val) {
- $tmp->add(array(
- new Horde_Imap_Client_Data_Format_String(Horde_String::lower($key)),
- new Horde_Imap_Client_Data_Format_Nstring($val)
- ));
- }
- $cmd->add($tmp);
- }
- $temp = &$this->_temp;
- /* Add to queue - this doesn't need to be sent immediately. */
- $cmd->on_error = function() use (&$temp) {
- /* Ignore server errors. E.g. Cyrus returns this:
- * 001 NO Only one Id allowed in non-authenticated state
- * even though NO is not allowed in RFC 2971[3.1]. */
- $temp['id'] = array();
- return true;
- };
- $cmd->on_success = function() use ($cmd, &$temp) {
- $temp['id'] = $cmd->pipeline->data['id'];
- };
- $this->_cmdQueue[] = $cmd;
- }
- /**
- * Parse an ID response (RFC 2971 [3.2]).
- *
- * @param Horde_Imap_Client_Interaction_Pipeline $pipeline Pipeline
- * object.
- * @param Horde_Imap_Client_Tokenize $data The server response.
- */
- protected function _parseID(
- Horde_Imap_Client_Interaction_Pipeline $pipeline,
- Horde_Imap_Client_Tokenize $data
- )
- {
- if (!isset($pipeline->data['id'])) {
- $pipeline->data['id'] = array();
- }
- if (!is_null($data->next())) {
- while (($curr = $data->next()) !== false) {
- if (!is_null($id = $data->next())) {
- $pipeline->data['id'][$curr] = $id;
- }
- }
- }
- }
- /**
- */
- protected function _getID()
- {
- if (!isset($this->_temp['id'])) {
- $this->sendID();
- /* ID is queued - force sending the queued command. */
- $this->_sendCmd($this->_pipeline());
- }
- return $this->_temp['id'];
- }
- /**
- */
- protected function _setLanguage($langs)
- {
- $cmd = $this->_command('LANGUAGE');
- foreach ($langs as $lang) {
- $cmd->add(new Horde_Imap_Client_Data_Format_Astring($lang));
- }
- if (!empty($this->_temp['lang_queue'])) {
- $this->_cmdQueue[] = $cmd;
- return array();
- }
- try {
- $this->_sendCmd($cmd);
- } catch (Horde_Imap_Client_Exception $e) {
- $this->_setInit('lang', false);
- return null;
- }
- return $this->_init['lang'];
- }
- /**
- */
- protected function _getLanguage($list)
- {
- if (!$list) {
- return empty($this->_init['lang'])
- ? null
- : $this->_init['lang'];
- }
- if (!isset($this->_init['langavail'])) {
- try {
- $this->_sendCmd($this->_command('LANGUAGE'));
- } catch (Horde_Imap_Client_Exception $e) {
- $this->_setInit('langavail', array());
- }
- }
- return $this->_init['langavail'];
- }
- /**
- * Parse a LANGUAGE response (RFC 5255 [3.3]).
- *
- * @param Horde_Imap_Client_Tokenize $data The server response.
- */
- protected function _parseLanguage(Horde_Imap_Client_Tokenize $data)
- {
- $lang_list = $data->flushIterator();
- if (count($lang_list) === 1) {
- // This is the language that was set.
- $this->_setInit('lang', reset($lang_list));
- } else {
- // These are the languages that are available.
- $this->_setInit('langavail', $lang_list);
- }
- }
- /**
- * Enable an IMAP extension (see RFC 5161).
- *
- * @param array $exts The extensions to enable.
- *
- * @throws Horde_Imap_Client_Exception
- */
- protected function _enable($exts)
- {
- if (!empty($exts) && $this->_capability('ENABLE')) {
- $c = $this->_capability();
- $todo = array();
- // Only enable non-enabled extensions.
- foreach ($exts as $val) {
- if (!$c->isEnabled($val)) {
- $c->enable($val);
- $todo[] = $val;
- }
- }
- if (!empty($todo)) {
- $cmd = $this->_command('ENABLE')->add($todo);
- $cmd->on_error = function() use ($todo, $c) {
- /* Something went wrong... disable the extensions. */
- foreach ($todo as $val) {
- $c->enable($val, false);
- }
- };
- $this->_cmdQueue[] = $cmd;
- }
- }
- }
- /**
- * Parse an ENABLED response (RFC 5161 [3.2]).
- *
- * @param Horde_Imap_Client_Tokenize $data The server response.
- */
- protected function _parseEnabled(Horde_Imap_Client_Tokenize $data)
- {
- $c = $this->_capability();
- foreach ($data->flushIterator() as $val) {
- $c->enable($val);
- }
- }
- /**
- */
- protected function _openMailbox(Horde_Imap_Client_Mailbox $mailbox, $mode)
- {
- $c = $this->_capability();
- $qresync = $c->isEnabled('QRESYNC');
- $cmd = $this->_command(
- ($mode == Horde_Imap_Client::OPEN_READONLY) ? 'EXAMINE' : 'SELECT'
- )->add(
- $this->_getMboxFormatOb($mailbox)
- );
- $pipeline = $this->_pipeline($cmd);
- /* If QRESYNC is available, synchronize the mailbox. */
- if ($qresync) {
- $this->_initCache();
- $md = $this->_cache->getMetaData($mailbox, null, array(self::CACHE_MODSEQ, 'uidvalid'));
- /* CACHE_MODSEQ can be set but 0 (NOMODSEQ was returned). */
- if (!empty($md[self::CACHE_MODSEQ])) {
- if ($uids = $this->_cache->get($mailbox)) {
- $uids = $this->getIdsOb($uids);
- /* Check for extra long UID string. Assume that any
- * server that can handle QRESYNC can also handle long
- * input strings (at least 8 KB), so 7 KB is as good as
- * any guess as to an upper limit. If this occurs, provide
- * a range string (min -> max) instead. */
- if (strlen($uid_str = $uids->tostring_sort) > 7000) {
- $uid_str = $uids->range_string;
- }
- } else {
- $uid_str = null;
- }
- /* Several things can happen with a QRESYNC:
- * 1. UIDVALIDITY may have changed. If so, we need to expire
- * the cache immediately (done below).
- * 2. NOMODSEQ may have been returned. We can keep current
- * message cache data but won't be able to do flag caching.
- * 3. VANISHED/FETCH information was returned. These responses
- * will have already been handled by those response handlers.
- * 4. We are already synced with the local server in which
- * case it acts like a normal EXAMINE/SELECT. */
- $cmd->add(new Horde_Imap_Client_Data_Format_List(array(
- 'QRESYNC',
- new Horde_Imap_Client_Data_Format_List(array_filter(array(
- $md['uidvalid'],
- $md[self::CACHE_MODSEQ],
- $uid_str
- )))
- )));
- }
- /* Let the 'CLOSED' response code handle mailbox switching if
- * QRESYNC is active. */
- if ($this->_selected) {
- $pipeline->data['qresyncmbox'] = array($mailbox, $mode);
- } else {
- $this->_changeSelected($mailbox, $mode);
- }
- } else {
- if (!$c->isEnabled('CONDSTORE') &&
- $this->_initCache() &&
- $c->query('CONDSTORE')) {
- /* Activate CONDSTORE now if ENABLE is not available. */
- $cmd->add(new Horde_Imap_Client_Data_Format_List('CONDSTORE'));
- $c->enable('CONDSTORE');
- }
- $this->_changeSelected($mailbox, $mode);
- }
- try {
- $this->_sendCmd($pipeline);
- } catch (Horde_Imap_Client_Exception_ServerResponse $e) {
- // An EXAMINE/SELECT failure with a return of 'NO' will cause the
- // current mailbox to be unselected.
- if ($e->status === Horde_Imap_Client_Interaction_Server::NO) {
- $this->_changeSelected(null);
- $this->_mode = 0;
- if (!$e->getCode()) {
- $e = new Horde_Imap_Client_Exception(
- Horde_Imap_Client_Translation::r("Could not open mailbox \"%s\"."),
- Horde_Imap_Client_Exception::MAILBOX_NOOPEN
- );
- $e->messagePrintf(array($mailbox));
- }
- }
- throw $e;
- }
- if ($qresync) {
- /* Mailbox is fully sync'd. */
- $this->_mailboxOb()->sync = true;
- }
- }
- /**
- */
- protected function _createMailbox(Horde_Imap_Client_Mailbox $mailbox, $opts)
- {
- $cmd = $this->_command('CREATE')->add(
- $this->_getMboxFormatOb($mailbox)
- );
- // RFC 6154 Sec. 3
- if (!empty($opts['special_use'])) {
- $use = new Horde_Imap_Client_Data_Format_List('USE');
- $use->add(
- new Horde_Imap_Client_Data_Format_List($opts['special_use'])
- );
- $cmd->add($use);
- }
- // CREATE returns no untagged information (RFC 3501 [6.3.3])
- $this->_sendCmd($cmd);
- }
- /**
- */
- protected function _deleteMailbox(Horde_Imap_Client_Mailbox $mailbox)
- {
- // Some IMAP servers will not allow a delete of a currently open
- // mailbox.
- if ($mailbox->equals($this->_selected)) {
- $this->close();
- }
- $cmd = $this->_command('DELETE')->add(
- $this->_getMboxFormatOb($mailbox)
- );
- try {
- // DELETE returns no untagged information (RFC 3501 [6.3.4])
- $this->_sendCmd($cmd);
- } catch (Horde_Imap_Client_Exception $e) {
- // Some IMAP servers won't allow a mailbox delete unless all
- // messages in that mailbox are deleted.
- $this->expunge($mailbox, array(
- 'delete' => true
- ));
- $this->_sendCmd($cmd);
- }
- }
- /**
- */
- protected function _renameMailbox(Horde_Imap_Client_Mailbox $old,
- Horde_Imap_Client_Mailbox $new)
- {
- // Some IMAP servers will not allow a rename of a currently open
- // mailbox.
- if ($old->equals($this->_selected)) {
- $this->close();
- }
- // RENAME returns no untagged information (RFC 3501 [6.3.5])
- $this->_sendCmd(
- $this->_command('RENAME')->add(array(
- $this->_getMboxFormatOb($old),
- $this->_getMboxFormatOb($new)
- ))
- );
- }
- /**
- */
- protected function _subscribeMailbox(Horde_Imap_Client_Mailbox $mailbox,
- $subscribe)
- {
- // SUBSCRIBE/UNSUBSCRIBE returns no untagged information (RFC 3501
- // [6.3.6 & 6.3.7])
- $this->_sendCmd(
- $this->_command(
- $subscribe ? 'SUBSCRIBE' : 'UNSUBSCRIBE'
- )->add(
- $this->_getMboxFormatOb($mailbox)
- )
- );
- }
- /**
- */
- protected function _listMailboxes($pattern, $mode, $options)
- {
- // RFC 5258 [3.1]: Use LSUB for MBOX_SUBSCRIBED if no other server
- // return options are specified.
- if (($mode == Horde_Imap_Client::MBOX_SUBSCRIBED) &&
- !array_intersect(array_keys($options), array('attributes', 'children', 'recursivematch', 'remote', 'special_use', 'status'))) {
- return $this->_getMailboxList(
- $pattern,
- Horde_Imap_Client::MBOX_SUBSCRIBED,
- array(
- 'flat' => !empty($options['flat']),
- 'no_listext' => true
- )
- );
- }
- // Get the list of subscribed/unsubscribed mailboxes. Since LSUB is
- // not guaranteed to have correct attributes, we must use LIST to
- // ensure we receive the correct information.
- if (($mode != Horde_Imap_Client::MBOX_ALL) &&
- !$this->_capability('LIST-EXTENDED')) {
- $subscribed = $this->_getMailboxList(
- $pattern,
- Horde_Imap_Client::MBOX_SUBSCRIBED,
- array('flat' => true)
- );
- // If mode is subscribed, and 'flat' option is true, we can
- // return now.
- if (($mode == Horde_Imap_Client::MBOX_SUBSCRIBED) &&
- !empty($options['flat'])) {
- return $subscribed;
- }
- } else {
- $subscribed = null;
- }
- return $this->_getMailboxList($pattern, $mode, $options, $subscribed);
- }
- /**
- * Obtain a list of mailboxes.
- *
- * @param array $pattern The mailbox search pattern(s).
- * @param integer $mode Which mailboxes to return.
- * @param array $options Additional options. 'no_listext' will skip
- * using the LIST-EXTENDED capability.
- * @param array $subscribed A list of subscribed mailboxes.
- *
- * @return array See listMailboxes(().
- *
- * @throws Horde_Imap_Client_Exception
- */
- protected function _getMailboxList($pattern, $mode, $options,
- $subscribed = null)
- {
- // Setup entry for use in _parseList().
- $pipeline = $this->_pipeline();
- $pipeline->data['mailboxlist'] = array(
- 'ext' => false,
- 'mode' => $mode,
- 'opts' => $options,
- /* Can't use array_merge here because it will destroy any mailbox
- * name (key) that is "numeric". */
- 'sub' => (is_null($subscribed) ? null : array_flip(array_map('strval', $subscribed)) + array('INBOX' => true))
- );
- $pipeline->data['listresponse'] = array();
- $cmds = array();
- $return_opts = new Horde_Imap_Client_Data_Format_List();
- if ($this->_capability('LIST-EXTENDED') &&
- empty($options['no_listext'])) {
- $cmd = $this->_command('LIST');
- $pipeline->data['mailboxlist']['ext'] = true;
- $select_opts = new Horde_Imap_Client_Data_Format_List();
- $subscribed = false;
- switch ($mode) {
- case Horde_Imap_Client::MBOX_ALL_SUBSCRIBED:
- case Horde_Imap_Client::MBOX_UNSUBSCRIBED:
- $return_opts->add('SUBSCRIBED');
- break;
- case Horde_Imap_Client::MBOX_SUBSCRIBED:
- case Horde_Imap_Client::MBOX_SUBSCRIBED_EXISTS:
- $select_opts->add('SUBSCRIBED');
- $return_opts->add('SUBSCRIBED');
- $subscribed = true;
- break;
- }
- if (!empty($options['remote'])) {
- $select_opts->add('REMOTE');
- }
- if (!empty($options['recursivematch'])) {
- $selec…
Large files files are truncated, but you can click here to view the full file