From: Michael M Slusarz Date: Thu, 6 Nov 2008 05:49:17 +0000 (-0700) Subject: Import Horde_Imap_Client lib. X-Git-Url: https://git.internetallee.de/?a=commitdiff_plain;h=6b86fff6d3931cb0db1a41b17d54fac3921e9833;p=horde.git Import Horde_Imap_Client lib. --- diff --git a/framework/Imap_Client/lib/Horde/Imap/Client.php b/framework/Imap_Client/lib/Horde/Imap/Client.php new file mode 100644 index 000000000..ab2f73ce7 --- /dev/null +++ b/framework/Imap_Client/lib/Horde/Imap/Client.php @@ -0,0 +1,548 @@ + + * 'compress' - [OPTIONAL] (string) Compression to use on the cached data. + * Either false, 'gzip' or 'lzf'. + * 'driver' - [REQUIRED] (string) The Horde_Cache driver to use. + * 'driver_params' - [REQUIRED] (array) The params to pass to the Horde_Cache + * driver. + * 'fields' - [OPTIONAL] (array) The fetch criteria to cache. If not defined, + * all cacheable data is cached. The following is a list of + * criteria that can be cached: + *
+ * Horde_Imap_Client::FETCH_STRUCTURE
+ * Horde_Imap_Client::FETCH_ENVELOPE
+ * Horde_Imap_Client::FETCH_FLAGS (only if server supports CONDSTORE IMAP
+ *                                 extension)
+ * Horde_Imap_Client::FETCH_DATE
+ * Horde_Imap_Client::FETCH_SIZE
+ * 
+ * 'lifetime' - [OPTIONAL] (integer) The lifetime of the cache data (in secs). + * 'slicesize' - [OPTIONAL] (integer) The slicesize to use. + * + * comparator - (string) The search comparator to use instead of the default + * IMAP server comparator. See setComparator() for the format. + * DEFAULT: Use the server default + * debug - (string) If set, will output debug information to the stream + * identified. The value can be any PHP supported wrapper that can + * be opened via fopen(). + * DEFAULT: No debug output + * hostspec - (string) The hostname or IP address of the server. + * DEFAULT: 'localhost' + * id - (array) Send ID information to the IMAP server (only if server + * supports the ID extension). An array with the keys being the fields + * to send and the values being the associated values. See RFC 2971 + * [3.3] for a list of defined field values. + * DEFAULT: No info sent to server + * lang - (array) A list of languages (in priority order) to be used to + * display human readable messages. + * DEFAULT: Messages output in IMAP server default language + * port - (integer) The server port to which we will connect. + * DEFAULT: 143 (imap or imap w/TLS) or 993 (imaps) + * secure - (string) Use SSL or TLS to connect. + * VALUES: false, 'ssl', 'tls'. + * DEFAULT: No encryption + * timeout - (integer) Connection timeout, in seconds. + * DEFAULT: 10 seconds + * + * Copyright 2008 The Horde Project (http://www.horde.org/) + * + * getBaseSubject() code adapted from imap-base-subject.c (Dovecot 1.2) + * Original code released under the LGPL v2.1 + * Copyright (c) 2002-2008 Timo Sirainen + * + * $Horde: framework/Imap_Client/lib/Horde/Imap/Client.php,v 1.40 2008/10/27 21:02:55 slusarz Exp $ + * + * See the enclosed file COPYING for license information (LGPL). If you + * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html. + * + * @author Michael Slusarz + * @category Horde + * @package Horde_Imap_Client + */ +class Horde_Imap_Client +{ + /* Global constants. */ + const USE_SEARCHRES = '*SEARCHRES*'; + + /* Constants for openMailbox() */ + const OPEN_READONLY = 1; + const OPEN_READWRITE = 2; + const OPEN_AUTO = 3; + + /* Constants for listMailboxes() */ + const MBOX_SUBSCRIBED = 1; + const MBOX_UNSUBSCRIBED = 2; + const MBOX_ALL = 3; + + /* Constants for status() */ + const STATUS_MESSAGES = 1; + const STATUS_RECENT = 2; + const STATUS_UIDNEXT = 4; + const STATUS_UIDVALIDITY = 8; + const STATUS_UNSEEN = 16; + const STATUS_ALL = 32; + const STATUS_FIRSTUNSEEN = 64; + const STATUS_FLAGS = 128; + const STATUS_PERMFLAGS = 256; + const STATUS_HIGHESTMODSEQ = 512; + const STATUS_UIDNOTSTICKY = 1024; + + /* Constants for search() */ + const SORT_ARRIVAL = 1; + const SORT_CC = 2; + const SORT_DATE = 3; + const SORT_FROM = 4; + const SORT_REVERSE = 5; + const SORT_SIZE = 6; + const SORT_SUBJECT = 7; + const SORT_TO = 8; + /* SORT_THREAD provided for completeness - it is not a valid sort criteria + * for search() (use thread() instead). */ + const SORT_THREAD = 9; + + const SORT_RESULTS_COUNT = 1; + const SORT_RESULTS_MATCH = 2; + const SORT_RESULTS_MAX = 3; + const SORT_RESULTS_MIN = 4; + const SORT_RESULTS_SAVE = 5; + + /* Constants for thread() */ + const THREAD_ORDEREDSUBJECT = 1; + const THREAD_REFERENCES = 2; + + /* Constants for fetch() */ + const FETCH_STRUCTURE = 1; + const FETCH_FULLMSG = 2; + const FETCH_HEADERTEXT = 3; + const FETCH_BODYTEXT = 4; + const FETCH_MIMEHEADER = 5; + const FETCH_BODYPART = 6; + const FETCH_BODYPARTSIZE = 7; + const FETCH_HEADERS = 8; + const FETCH_ENVELOPE = 9; + const FETCH_FLAGS = 10; + const FETCH_DATE = 11; + const FETCH_SIZE = 12; + const FETCH_UID = 13; + const FETCH_SEQ = 14; + const FETCH_MODSEQ = 15; + + /** + * The key used to encrypt the password when serializing. + * + * @var string + */ + public static $encryptKey = null; + + /** + * Autoload handler. + */ + public static function autoload($classname) + { + $res = false; + + $old_error = error_reporting(0); + switch ($classname) { + case 'Horde_MIME': + $res = require_once 'Horde/MIME.php'; + break; + + case 'Horde_MIME_Headers': + $res = require_once 'Horde/MIME/Headers.php'; + break; + + case 'Horde_MIME_Message': + $res = require_once 'Horde/MIME/Message.php'; + break; + + case 'Secret': + $res = require_once 'Horde/Secret.php'; + break; + } + error_reporting($old_error); + + return $res; + } + + /** + * Attempts to return a concrete Horde_Imap_Client instance based on + * $driver. + * Throws a Horde_Imap_Client_Exception on error. + * + * @param string $driver The type of concrete Horde_Imap_Client subclass + * to return. + * @param array $params A hash containing any additional configuration or + * connection parameters a subclass might need. + * + * @return mixed The newly created concrete Horde_Imap_Client instance. + */ + static final public function getInstance($driver, $params = array()) + { + $class = 'Horde_Imap_Client_' . strtr(basename($driver), '-', '_'); + if (!class_exists($class)) { + $fname = dirname(__FILE__) . '/Client/' . $driver . '.php'; + if (is_file($fname)) { + require_once $fname; + } + } + if (!class_exists($class)) { + throw new Horde_Imap_Client_Exception('Driver ' . $driver . ' not found', Horde_Imap_Client_Exception::DRIVER_NOT_FOUND); + } + return new $class($params); + } + + /** + * Create an IMAP message sequence string from a list of indices. + * Format: range_start:range_end,uid,uid2,range2_start:range2_end,... + * + * @param array $in An array of indices. + * @param array $options Additional options: + *
+     * 'nosort' - (boolean) Do not numerically sort the IDs before creating
+     *            the range?
+     *            DEFAULT: IDs are sorted
+     * 
+ * + * @return string The IMAP message sequence string. + */ + static final public function toSequenceString($ids, $options = array()) + { + if (empty($ids)) { + return ''; + } + + // Make sure IDs are unique + $ids = array_keys(array_flip($ids)); + + if (empty($options['nosort'])) { + sort($ids, SORT_NUMERIC); + } + + $first = $last = array_shift($ids); + $out = array(); + + foreach ($ids as $val) { + if ($last + 1 == $val) { + $last = $val; + } else { + $out[] = $first . ($last == $first ? '' : (':' . $last)); + $first = $last = $val; + } + } + $out[] = $first . ($last == $first ? '' : (':' . $last)); + + return implode(',', $out); + } + + /** + * Parse an IMAP message sequence string into a list of indices. + * Format: range_start:range_end,uid,uid2,range2_start:range2_end,... + * + * @param string $str The IMAP message sequence string. + * + * @return array An array of indices. + */ + static final public function fromSequenceString($str) + { + $ids = array(); + $str = trim($str); + + $idarray = explode(',', $str); + if (empty($idarray)) { + $idarray = array($str); + } + + foreach ($idarray as $val) { + $range = array_map('intval', explode(':', $val)); + if (count($range) == 1) { + $ids[] = $val; + } else { + list($low, $high) = ($range[0] < $range[1]) ? $range : array_reverse($range); + $ids = array_merge($ids, range($low, $high)); + } + } + + return $ids; + } + + /** + * Remove "bare newlines" from a string. + * + * @param string $str The original string. + * + * @return string The string with all bare newlines removed. + */ + static final public function removeBareNewlines($str) + { + return str_replace(array("\r\n", "\n"), array("\n", "\r\n"), $str); + } + + /** + * Escape IMAP output via a quoted string (see RFC 3501 [4.3]). + * + * @param string $str The unescaped string. + * + * @return string The escaped string. + */ + static final public function escape($str) + { + return '"' . addcslashes($str, '"\\') . '"'; + } + + /** + * Return the "base subject" defined in RFC 5256 [2.1]. + * + * @param string $str The original subject string. + * @param array $options Additional options: + *
+     * 'keepblob' - (boolean) Don't remove any "blob" information (i.e. text
+     *              leading text between square brackets) from string.
+     * 
+ * + * @return string The cleaned up subject string. + */ + static final public function getBaseSubject($str, $options = array()) + { + // Rule 1a: MIME decode to UTF-8 (if possible). + $str = Horde_MIME::decode($str, 'UTF-8'); + + // Rule 1b: Remove superfluous whitespace. + $str = preg_replace("/\s{2,}/", '', $str); + + do { + /* (2) Remove all trailing text of the subject that matches the + * the subj-trailer ABNF, repeat until no more matches are + * possible. */ + $str = preg_replace("/(?:\s*\(fwd\)\s*)+$/i", '', $str); + + do { + /* (3) Remove all prefix text of the subject that matches the + * subj-leader ABNF. */ + $found = self::_removeSubjLeader($str, !empty($options['keepblob'])); + + /* (4) If there is prefix text of the subject that matches + * the subj-blob ABNF, and removing that prefix leaves a + * non-empty subj-base, then remove the prefix text. */ + $found = (empty($options['keepblob']) && self::_removeBlobWhenNonempty($str)) || $found; + + /* (5) Repeat (3) and (4) until no matches remain. */ + } while ($found); + + /* (6) If the resulting text begins with the subj-fwd-hdr ABNF and + * ends with the subj-fwd-trl ABNF, remove the subj-fwd-hdr and + * subj-fwd-trl and repeat from step (2). */ + } while (self::_removeSubjFwdHdr($str)); + + return $str; + } + + /** + * Remove all prefix text of the subject that matches the subj-leader + * ABNF. + * + * @param string &$str The subject string. + * @param boolean $keepblob Remove blob information? + * + * @return boolean True if string was altered. + */ + static final protected function _removeSubjLeader(&$str, $keepblob = false) + { + $ret = false; + + if ($str[0] == ' ') { + $str = substr($str, 1); + $ret = true; + } + + $i = 0; + + if (!$keepblob) { + while ($str[$i] == '[') { + if (($i = self::_removeBlob($str, $i)) === false) { + return $ret; + } + } + } + + $cmp_str = substr($str, $i); + if (stripos($cmp_str, 're') === 0) { + $i += 2; + } elseif (stripos($cmp_str, 'fwd') === 0) { + $i += 3; + } elseif (stripos($cmp_str, 'fw') === 0) { + $i += 2; + } else { + return $ret; + } + + if ($str[$i] == ' ') { + ++$i; + } + + if (!$keepblob) { + while ($str[$i] == '[') { + if (($i = self::_removeBlob($str, $i)) === false) { + return $ret; + } + } + } + + if ($str[$i] != ':') { + return $ret; + } + + $str = substr($str, ++$i); + + return true; + } + + /** + * Remove "[...]" text. + * + * @param string &$str The subject string. + * + * @return boolean True if string was altered. + */ + static final protected function _removeBlob($str, $i) + { + if ($str[$i] != '[') { + return false; + } + + ++$i; + + for ($cnt = strlen($str); $i < $cnt; ++$i) { + if ($str[$i] == ']') { + break; + } + + if ($str[$i] == '[') { + return false; + } + } + + if ($i == ($cnt - 1)) { + return false; + } + + ++$i; + + if ($str[$i] == ' ') { + ++$i; + } + + return $i; + } + + /** + * Remove "[...]" text if it doesn't result in the subject becoming + * empty. + * + * @param string &$str The subject string. + * + * @return boolean True if string was altered. + */ + static final protected function _removeBlobWhenNonempty(&$str) + { + if (($str[0] == '[') && + (($i = self::_removeBlob($str, 0)) !== false) && + ($i != strlen($str))) { + $str = substr($str, $i); + return true; + } + + return false; + } + + /** + * Remove a "[fwd: ... ]" string. + * + * @param string &$str The subject string. + * + * @return boolean True if string was altered. + */ + static final protected function _removeSubjFwdHdr(&$str) + { + if ((stripos($str, '[fwd:') !== 0) || (substr($str, -1) != ']')) { + return false; + } + + $str = substr($str, 5, -1); + return true; + } + + /** + * Parse an IMAP URL (RFC 5092). + * + * @param string $url A IMAP URL string. + * + * @return mixed False if the URL is invalid. If valid, a URL with the + * following fields: + *
+     * 'auth' - (string) The authentication method to use.
+     * 'port' - (integer) The remote port
+     * 'hostspec' - (string) The remote server
+     * 'username' - (string) The username to use on the remote server.
+     * 
+ */ + static final public function parseImapURL($url) + { + $url = trim($url); + if (stripos($url, 'imap://') !== 0) { + return false; + } + $url = substr($url, 7); + + /* At present, only support imap://[/] style URLs. */ + if (($pos = strpos($url, '/')) !== false) { + $url = substr($url, 0, $pos); + } + + $ret_array = array(); + + /* Check for username/auth information. */ + if (($pos = strpos($url, '@')) !== false) { + if ((($apos = stripos($url, ';AUTH=')) !== false) && + ($apos < $pos)) { + $auth = substr($url, $apos + 6, $pos - $apos - 6); + if ($auth != '*') { + $ret_array['auth'] = $auth; + } + if ($apos) { + $ret_array['username'] = substr($url, 0, $apos); + } + } + $url = substr($url, $pos + 1); + } + + /* Check for port information. */ + if (($pos = strpos($url, ':')) !== false) { + $ret_array['port'] = substr($url, $pos + 1); + $url = substr($url, 0, $pos); + } + + $ret_array['hostspec'] = $url; + + return $ret_array; + } +} + +spl_autoload_register(array('Horde_Imap_Client_Base', 'autoload')); diff --git a/framework/Imap_Client/lib/Horde/Imap/Client/Base.php b/framework/Imap_Client/lib/Horde/Imap/Client/Base.php new file mode 100644 index 000000000..02e0a94cd --- /dev/null +++ b/framework/Imap_Client/lib/Horde/Imap/Client/Base.php @@ -0,0 +1,2983 @@ + + * @category Horde + * @package Horde_Imap_Client + */ +abstract class Horde_Imap_Client_Base extends Horde_Imap_Client +{ + /** + * Hash containing connection parameters. + * + * @var array + */ + protected $_params = array(); + + /** + * Is there an active authenticated connection to the IMAP Server? + * + * @var boolean + */ + protected $_isAuthenticated = false; + + /** + * Is there a secure connection to the IMAP Server? + * + * @var boolean + */ + protected $_isSecure = false; + + /** + * The currently selected mailbox. + * + * @var string + */ + protected $_selected = null; + + /** + * The current mailbox selection mode. + * + * @var integer + */ + protected $_mode = 0; + + /** + * Server data that will be cached when serialized. + * + * @var array + */ + protected $_init = array( + 'enabled' => array(), + 'namespace' => array() + ); + + /** + * The Horde_Imap_Client_Cache object. + * + * @var Horde_Cache + */ + protected $_cacheOb = null; + + /** + * The debug stream. + * + * @var resource + */ + protected $_debug = null; + + /** + * Constructs a new Horde_Imap_Client object. + * Throws a Horde_Imap_Client_Exception on error. + * + * @param array $params A hash containing configuration parameters. + */ + public function __construct($params = array()) + { + if (!isset($params['username']) || !isset($params['password'])) { + throw new Horde_Imap_Client_Exception('Horde_Imap_Client requires a username and password.'); + } + + // Default values. + if (empty($params['hostspec'])) { + $params['hostspec'] = 'localhost'; + } + + if (empty($params['port'])) { + $params['port'] = (isset($params['secure']) && ($params['secure'] == 'ssl')) ? 993 : 143; + } + + if (empty($params['timeout'])) { + $params['timeout'] = 10; + } + + if (empty($params['cache'])) { + $params['cache'] = array('fields' => array()); + } elseif (empty($params['cache']['fields'])) { + $params['cache']['fields'] = array( + self::FETCH_STRUCTURE => 1, + self::FETCH_ENVELOPE => 1, + self::FETCH_FLAGS => 1, + self::FETCH_DATE => 1, + self::FETCH_SIZE => 1 + ); + } else { + $params['cache']['fields'] = array_flip($params['cache']['fields']); + } + + $this->_params = $params; + + // This will initialize debugging, if needed. + $this->__wakeup(); + } + + /** + * Destructor. + */ + function __destruct() + { + $this->_closeDebug(); + } + + /** + * Do cleanup prior to serialization. + */ + function __sleep() + { + $this->_closeDebug(); + + // Don't store Horde_Imap_Client_Cache object. + $this->_cacheOb = null; + + // Encrypt password in serialized object. + if (!isset($this->_params['_passencrypt'])) { + $key = Horde_Imap_Client::$encryptKey; + if (!is_null($key)) { + $this->_params['_passencrypt'] = Secret::write($key, $this->_params['password']); + $this->_params['password'] = null; + } + } + } + + /** + * Do re-initialization on unserialize(). + */ + function __wakeup() + { + if (isset($this->_params['_passencrypt']) && + !is_null(Horde_Imap_Client::$encryptKey)) { + $this->_params['password'] = Secret::read(Horde_Imap_Client::$encryptKey, $this->_params['_passencrypt']); + } + + if (!empty($this->_params['debug'])) { + $this->_debug = fopen($this->_params['debug'], 'a'); + } + } + + /** + * Close debugging output. + */ + protected function _closeDebug() + { + if (is_resource($this->_debug)) { + fflush($this->_debug); + fclose($this->_debug); + $this->_debug = null; + } + } + + /** + * Initialize the Horde_Imap_Client_Cache object, if necessary. + * Throws a Horde_Imap_Client_Exception on error. + * + * @return boolean Returns true if caching is enabled. + */ + protected function _initCacheOb() + { + if (empty($this->_params['cache']['fields'])) { + return false; + } + + if (is_null($this->_cacheOb)) { + $p = $this->_params; + require_once dirname(__FILE__) . '/Cache.php'; + $this->_cacheOb = &Horde_Imap_Client_Cache::singleton(array_merge($p['cache'], array( + 'debug' => $this->_debug, + 'hostspec' => $p['hostspec'], + 'username' => $p['username'] + ))); + } + + return true; + } + + /** + * Returns the Horde_Imap_Client_Cache object used, if available. + * + * @return mixed Either the object or null. + */ + public function getCacheOb() + { + $this->_initCacheOb(); + return $this->_cacheOb; + } + + /** + * Returns whether the IMAP server supports the given capability + * (See RFC 3501 [6.1.1]). + * + * @param string $capability The capability string to query. + * + * @param mixed True if the server supports the queried capability, + * false if it doesn't, or an array if the capability can + * contain multiple values. + */ + public function queryCapability($capability) + { + if (!isset($this->_init['capability'])) { + try { + $this->capability(); + } catch (Horde_Imap_Client_Exception $e) { + return false; + } + } + $capability = strtoupper($capability); + return isset($this->_init['capability'][$capability]) ? $this->_init['capability'][$capability] : false; + } + + /** + * Get CAPABILITY information from the IMAP server. + * Throws a Horde_Imap_Client_Exception on error. + * + * @return array The capability array. + */ + public function capability() + { + if (!isset($this->_init['capability'])) { + $this->_init['capability'] = $this->_capability(); + } + + return $this->_init['capability']; + } + + /** + * Get CAPABILITY information from the IMAP server. + * Throws a Horde_Imap_Client_Exception on error. + * + * @return array The capability array. + */ + abstract protected function _capability(); + + /** + * Send a NOOP command (RFC 3501 [6.1.2]). + * Throws a Horde_Imap_Client_Exception on error. + */ + public function noop() + { + // NOOP only useful if we are already authenticated. + if ($this->_isAuthenticated) { + $this->_noop(); + } + } + + /** + * Send a NOOP command. + * Throws a Horde_Imap_Client_Exception on error. + */ + abstract protected function _noop(); + + /** + * Get the NAMESPACE information from the IMAP server (RFC 2342). + * Throws a Horde_Imap_Client_Exception on error. + * + * @param array $additional If the server supports namespaces, any + * additional namespaces to add to the + * namespace list that are not broadcast by + * the server. The namespaces must either be in + * UTF7-IMAP or UTF-8. + * + * @return array An array of namespace information with the name as the + * key and the following values: + *
+     * 'delimiter' - (string) The namespace delimiter.
+     * 'hidden' - (boolean) Is this a hidden namespace?
+     * 'name' - (string) The namespace name.
+     * 'translation' - OPTIONAL (string) This entry only present if the IMAP
+     *                 server supports RFC 5255 and the language has previous
+     *                 been set via setLanguage(). The translation will be in
+     *                 UTF7-IMAP.
+     * 'type' - (string) The namespace type (either 'personal', 'other' or
+     *          'shared').
+     * 
+ */ + public function getNamespaces($additional = array()) + { + $additional = array_map(array('Horde_Imap_Client_Utf7imap', 'Utf7ImapToUtf8'), $additional); + + $sig = md5(serialize($additional)); + + if (isset($this->_init['namespace'][$sig])) { + return $this->_init['namespace'][$sig]; + } + + $ns = $this->_getNamespaces(); + + foreach ($additional as $val) { + /* Skip namespaces if we have already auto-detected them. Also, + * hidden namespaces cannot be empty. */ + $val = trim($val); + if (empty($val) || isset($ns[$val])) { + continue; + } + + $mbox = $this->listMailboxes($val, self::MBOX_ALL, array('delimiter' => true)); + $first = reset($mbox); + + if ($first && ($first['mailbox'] == $val)) { + $ns[$val] = array( + 'name' => $val, + 'delimiter' => $first['delimiter'], + 'type' => 'shared', + 'hidden' => true + ); + } + } + + if (empty($ns)) { + /* This accurately determines the namespace information of the + * base namespace if the NAMESPACE command is not supported. + * See: RFC 3501 [6.3.8] */ + $mbox = $this->listMailboxes('', self::MBOX_ALL, array('delimiter' => true)); + $first = reset($mbox); + $ns[''] = array( + 'name' => '', + 'delimiter' => $first['delimiter'], + 'type' => 'personal', + 'hidden' => false + ); + } + + $this->_init['namespace'][$sig] = $ns; + + return $ns; + } + + /** + * Get the NAMESPACE information from the IMAP server. + * Throws a Horde_Imap_Client_Exception on error. + * + * @return array An array of namespace information. + */ + abstract protected function _getNamespaces(); + + /** + * Display if connection to the server has been secured via TLS or SSL. + * + * @return boolean True if the IMAP connection is secured. + */ + public function isSecureConnection() + { + return $this->_isSecure; + } + + /** + * Return a list of alerts that MUST be presented to the user (RFC 3501 + * [7.1]). + * + * @return array An array of alert messages. + */ + abstract public function alerts(); + + /** + * Login to the IMAP server. + * Throws a Horde_Imap_Client_Exception on error. + */ + public function login() + { + if ($this->_isAuthenticated) { + return; + } + + if ($this->_login()) { + if (!empty($this->_params['id'])) { + try { + $this->sendID(); + } catch (Horde_Imap_Client_Exception $e) { + // Ignore if server doesn't support ID + if ($e->getCode() != Horde_Imap_Client_Exception::NOSUPPORTIMAPEXT) { + throw $e; + } + } + } + + if (!empty($this->_params['comparator'])) { + try { + $this->setComparator(); + } catch (Horde_Imap_Client_Exception $e) { + // Ignore if server doesn't support I18NLEVEL=2 + if ($e->getCode() != Horde_Imap_Client_Exception::NOSUPPORTIMAPEXT) { + throw $e; + } + } + } + + /* Check for ability to cache flags here. */ + if (isset($this->_params['cache']['fields'][self::FETCH_FLAGS]) && + !isset($this->_init['enabled']['CONDSTORE'])) { + unset($this->_params['cache']['fields'][self::FETCH_FLAGS]); + } + } + + $this->_isAuthenticated = true; + } + + /** + * Login to the IMAP server. + * Throws a Horde_Imap_Client_Exception on error. + * + * @return boolean Return true if global login tasks should be run. + */ + abstract protected function _login(); + + /** + * Logout from the IMAP server (see RFC 3501 [6.1.3]). + */ + public function logout() + { + $this->_logout(); + $this->_isAuthenticated = false; + $this->_selected = null; + $this->_mode = 0; + } + + /** + * Logout from the IMAP server (see RFC 3501 [6.1.3]). + */ + abstract protected function _logout(); + + /** + * Send ID information to the IMAP server (RFC 2971). + * Throws a Horde_Imap_Client_Exception on error. + * + * @param array $info Overrides the value of the 'id' param and sends + * this information instead. + */ + public function sendID($info = null) + { + if (!$this->queryCapability('ID')) { + throw new Horde_Imap_Client_Exception('The IMAP server does not support the ID extension.', Horde_Imap_Client_Exception::NOSUPPORTIMAPEXT); + } + + $this->_sendID(is_null($info) ? (empty($this->_params['id']) ? array() : $this->_params['id']) : $info); + } + + /** + * Send ID information to the IMAP server (RFC 2971). + * Throws a Horde_Imap_Client_Exception on error. + * + * @param array $info The information to send to the server. + */ + abstract protected function _sendID($info); + + /** + * Return ID information from the IMAP server (RFC 2971). + * Throws a Horde_Imap_Client_Exception on error. + * + * @return array An array of information returned, with the keys as the + * 'field' and the values as the 'value'. + */ + public function getID() + { + if (!$this->queryCapability('ID')) { + throw new Horde_Imap_Client_Exception('The IMAP server does not support the ID extension.', Horde_Imap_Client_Exception::NOSUPPORTIMAPEXT); + } + + return $this->_getID(); + } + + /** + * Return ID information from the IMAP server (RFC 2971). + * Throws a Horde_Imap_Client_Exception on error. + * + * @return array An array of information returned, with the keys as the + * 'field' and the values as the 'value'. + */ + abstract protected function _getID(); + + /** + * Sets the preferred language for server response messages (RFC 5255). + * Throws a Horde_Imap_Client_Exception on error. + * + * @param array $info Overrides the value of the 'lang' param and sends + * this list of preferred languages instead. The + * special string 'i-default' can be used to restore + * the language to the server default. + * + * @return string The language accepted by the server, or null if the + * default language is used. + */ + public function setLanguage($langs = null) + { + if (!$this->queryCapability('LANGUAGE')) { + return null; + } + + $lang = is_null($langs) ? (empty($this->_params['lang']) ? null : $this->_params['lang']) : $langs; + if (is_null($lang)) { + return null; + } + + return $this->_setLanguage($lang); + } + + /** + * Sets the preferred language for server response messages (RFC 5255). + * Throws a Horde_Imap_Client_Exception on error. + * + * @param array $info The preferred list of languages. + * + * @return string The language accepted by the server, or null if the + * default language is used. + */ + abstract protected function _setLanguage($langs); + + /** + * Gets the preferred language for server response messages (RFC 5255). + * Throws a Horde_Imap_Client_Exception on error. + * + * @param array $list If true, return the list of available languages. + * + * @return mixed If $list is true, the list of languages available on the + * server (may be empty). If false, the language used by + * the server, or null if the default language is used. + */ + public function getLanguage($list = false) + { + if (!$this->queryCapability('LANGUAGE')) { + return $list ? array() : null; + } + + return $this->_getLanguage($list); + } + + /** + * Gets the preferred language for server response messages (RFC 5255). + * Throws a Horde_Imap_Client_Exception on error. + * + * @param array $list If true, return the list of available languages. + * + * @return mixed If $list is true, the list of languages available on the + * server (may be empty). If false, the language used by + * the server, or null if the default language is used. + */ + abstract protected function _getLanguage($list); + + /** + * Open a mailbox. + * Throws a Horde_Imap_Client_Exception on error. + * + * @param string $mailbox The mailbox to open. Either in UTF7-IMAP or + * UTF-8. + * @param integer $mode The access mode. Either + * Horde_Imap_Client::OPEN_READONLY, + * Horde_Imap_Client::OPEN_READWRITE, or + * Horde_Imap_Client::OPEN_AUTO. + */ + public function openMailbox($mailbox, $mode = self::OPEN_AUTO) + { + $change = false; + + $mailbox = Horde_Imap_Client_Utf7imap::Utf8ToUtf7Imap($mailbox); + + if ($mode == self::OPEN_AUTO) { + if (is_null($this->_selected) || ($this->_selected != $mailbox)) { + $mode = self::OPEN_READONLY; + $change = true; + } + } elseif (is_null($this->_selected) || + ($this->_selected != $mailbox) || + ($mode != $this->_mode)) { + $change = true; + } + + if ($change) { + $this->_openMailbox($mailbox, $mode); + $this->_selected = $mailbox; + $this->_mode = $mode; + } + } + + /** + * Open a mailbox. + * Throws a Horde_Imap_Client_Exception on error. + * + * @param string $mailbox The mailbox to open (UTF7-IMAP). + * @param integer $mode The access mode. + */ + abstract protected function _openMailbox($mailbox, $mode); + + /** + * Return the currently opened mailbox and access mode. + * Throws a Horde_Imap_Client_Exception on error. + * + * @param array $options Additional options: + *
+     * 'utf8' - (boolean) True if 'mailbox' should be in UTF-8.
+     *          DEFAULT: 'mailbox' returned in UTF7-IMAP.
+     * 
+ * + * @return mixed Either an array with two elements - 'mailbox' and + * 'mode' - or null if no mailbox selected. + */ + public function currentMailbox($options = array()) + { + return is_null($this->_selected) + ? null + : array( + 'mailbox' => empty($options['utf8']) ? $this->_selected : Horde_Imap_Client_Utf7imap::Utf7ImapToUtf8($this->_selected), + 'mode' => $this->_mode + ); + } + + /** + * Create a mailbox. + * Throws a Horde_Imap_Client_Exception on error. + * + * @param string $mailbox The mailbox to create. Either in UTF7-IMAP or + * UTF-8. + */ + public function createMailbox($mailbox) + { + $this->_createMailbox(Horde_Imap_Client_Utf7imap::Utf8ToUtf7Imap($mailbox)); + } + + /** + * Create a mailbox. + * Throws a Horde_Imap_Client_Exception on error. + * + * @param string $mailbox The mailbox to create (UTF7-IMAP). + */ + abstract protected function _createMailbox($mailbox); + + /** + * Delete a mailbox. + * Throws a Horde_Imap_Client_Exception on error. + * + * @param string $mailbox The mailbox to delete. Either in UTF7-IMAP or + * UTF-8. + */ + public function deleteMailbox($mailbox) + { + $mailbox = Horde_Imap_Client_Utf7imap::Utf8ToUtf7Imap($mailbox); + + $this->_deleteMailbox($mailbox); + + /* Delete mailbox cache. */ + if ($this->_initCacheOb()) { + $this->_cacheOb->deleteMailbox($mailbox); + } + + /* Unsubscribe from mailbox. */ + try { + $this->subscribeMailbox($mailbox, false); + } catch (Horde_Imap_Client_Exception $e) { + // Ignore failed unsubscribe request + } + } + + /** + * Delete a mailbox. + * Throws a Horde_Imap_Client_Exception on error. + * + * @param string $mailbox The mailbox to delete (UTF7-IMAP). + */ + abstract protected function _deleteMailbox($mailbox); + + /** + * Rename a mailbox. + * Throws a Horde_Imap_Client_Exception on error. + * + * @param string $old The old mailbox name. Either in UTF7-IMAP or + * UTF-8. + * @param string $new The new mailbox name. Either in UTF7-IMAP or + * UTF-8. + */ + public function renameMailbox($old, $new) + { + $old = Horde_Imap_Client_Utf7imap::Utf8ToUtf7Imap($old); + $new = Horde_Imap_Client_Utf7imap::Utf8ToUtf7Imap($new); + + /* Check if old mailbox was subscribed to. */ + $subscribed = $this->listMailboxes($old, self::MBOX_SUBSCRIBED, array('flat' => true)); + + $this->_renameMailbox($old, $new); + + /* Delete mailbox cache. */ + if ($this->_initCacheOb()) { + $this->_cacheOb->deleteMailbox($old); + } + + /* Clean up subscription information. */ + try { + $this->subscribeMailbox($old, false); + if (count($subscribed)) { + $this->subscribeMailbox($new, true); + } + } catch (Horde_Imap_Client_Exception $e) { + // Ignore failed unsubscribe request + } + } + + /** + * Rename a mailbox. + * Throws a Horde_Imap_Client_Exception on error. + * + * @param string $old The old mailbox name (UTF7-IMAP). + * @param string $new The new mailbox name (UTF7-IMAP). + */ + abstract protected function _renameMailbox($old, $new); + + /** + * Manage subscription status for a mailbox. + * Throws a Horde_Imap_Client_Exception on error. + * + * @param string $mailbox The mailbox to [un]subscribe to. Either in + * UTF7-IMAP or UTF-8. + * @param boolean $subscribe True to subscribe, false to unsubscribe. + */ + public function subscribeMailbox($mailbox, $subscribe = true) + { + $this->_subscribeMailbox(Horde_Imap_Client_Utf7imap::Utf8ToUtf7Imap($mailbox), (bool)$subscribe); + } + + /** + * Manage subscription status for a mailbox. + * Throws a Horde_Imap_Client_Exception on error. + * + * @param string $mailbox The mailbox to [un]subscribe to (UTF7-IMAP). + * @param boolean $subscribe True to subscribe, false to unsubscribe. + */ + abstract protected function _subscribeMailbox($mailbox, $subscribe); + + /** + * Obtain a list of mailboxes matching a pattern. + * Throws a Horde_Imap_Client_Exception on error. + * + * @todo RFC 5258 extensions + * + * @param string $pattern The mailbox search pattern (see RFC 3501 + * [6.3.8] for the format). Either in UTF7-IMAP or + * UTF-8. + * @param integer $mode Which mailboxes to return. Either + * Horde_Imap_Client::MBOX_SUBSCRIBED, + * Horde_Imap_Client::MBOX_UNSUBSCRIBED, or + * Horde_Imap_Client::MBOX_ALL. + * @param array $options Additional options: + *
+     * 'attributes' - (boolean) If true, return attribute information under
+     *                the 'attributes' key. The attributes will be returned
+     *                in an array with each attribute in lowercase.
+     *                DEFAULT: Do not return this information.
+     * 'utf8' - (boolean) True to return mailbox names in UTF-8.
+     *          DEFAULT: Names are returned in UTF7-IMAP.
+     * 'delimiter' - (boolean) If true, return delimiter information under
+     *               the 'delimiter' key.
+     *               DEFAULT: Do not return this information.
+     * 'flat' - (boolean) If true, return a flat list of mailbox names only.
+     *          Overrides both the 'attributes' and 'delimiter' options.
+     *          DEFAULT: Do not return flat list.
+     * 'sort' - (boolean) If true, return a sorted list of mailboxes?
+     *          DEFAULT: Do not sort the list.
+     * 'sort_delimiter' - (string) If 'sort' is true, this is the delimiter
+     *                    used to sort the mailboxes.
+     *                    DEFAULT: '.'
+     * 
+ * + * @return array If 'flat' option is true, the array values are the list + * of mailboxes. Otherwise, the array values are arrays + * with the following keys: 'mailbox', 'attributes' (only + * if 'attributes' option is true), and 'delimiter' (only + * if 'delimiter' option is true). + */ + public function listMailboxes($pattern, $mode = self::MBOX_ALL, + $options = array()) + { + $ret = $this->_listMailboxes(Horde_Imap_Client_Utf7imap::Utf8ToUtf7Imap($pattern), $mode, $options); + + if (!empty($options['sort'])) { + require_once dirname(__FILE__) . '/Sort.php'; + Horde_Imap_Client_Sort::sortMailboxes($ret, array('delimiter' => empty($options['sort_delimiter']) ? '.' : $options['sort_delimiter'], 'index' => false, 'keysort' => empty($options['flat']))); + } + + return $ret; + } + + /** + * Obtain a list of mailboxes matching a pattern. + * Throws a Horde_Imap_Client_Exception on error. + * + * @param string $pattern The mailbox search pattern (UTF7-IMAP). + * @param integer $mode Which mailboxes to return. + * @param array $options Additional options. + * + * @return array See self::listMailboxes(). + */ + abstract protected function _listMailboxes($pattern, $mode, $options); + + /** + * Obtain status information for a mailbox. + * Throws a Horde_Imap_Client_Exception on error. + * + * @param string $mailbox The mailbox to query. Either in UTF7-IMAP or + * or UTF-8. + * @param string $flags A bitmask of information requested from the + * server. Allowed flags: + *
+     * Flag: Horde_Imap_Client::STATUS_MESSAGES
+     *   Return key: 'messages'
+     *   Return format: (integer) The number of messages in the mailbox.
+     *
+     * Flag: Horde_Imap_Client::STATUS_RECENT
+     *   Return key: 'recent'
+     *   Return format: (integer) The number of messages with the '\Recent'
+     *                  flag set
+     *
+     * Flag: Horde_Imap_Client::STATUS_UIDNEXT
+     *   Return key: 'uidnext'
+     *   Return format: (integer) The next UID to be assigned in the mailbox.
+     *
+     * Flag: Horde_Imap_Client::STATUS_UIDVALIDITY
+     *   Return key: 'uidvalidity'
+     *   Return format: (integer) The unique identifier validity of the
+     *                  mailbox.
+     *
+     * Flag: Horde_Imap_Client::STATUS_UNSEEN
+     *   Return key: 'unseen'
+     *   Return format: (integer) The number of messages which do not have
+     *                  the '\Seen' flag set.
+     *
+     * Flag: Horde_Imap_Client::STATUS_FIRSTUNSEEN
+     *   Return key: 'firstunseen'
+     *   Return format: (integer) The sequence number of the first unseen
+     *                  message in the mailbox.
+     *
+     * Flag: Horde_Imap_Client::STATUS_FLAGS
+     *   Return key: 'flags'
+     *   Return format: (array) The list of defined flags in the mailbox (all
+     *                  flags are in lowercase).
+     *
+     * Flag: Horde_Imap_Client::STATUS_PERMFLAGS
+     *   Return key: 'permflags'
+     *   Return format: (array) The list of flags that a client can change
+     *                  permanently (all flags are in lowercase).
+     *
+     * Flag: Horde_Imap_Client::STATUS_HIGHESTMODSEQ
+     *   Return key: 'highestmodseq'
+     *   Return format: (mixed) If the server supports the CONDSTORE IMAP
+     *                  extension, this will be the highest mod-sequence value
+     *                  of all messages in the mailbox or null if the mailbox
+     *                  does not support mod-sequences. Else, this value will
+     *                  be undefined.
+     *
+     * Flag: Horde_Imap_Client::STATUS_UIDNOTSTICKY
+     *   Return key: 'uidnotsticky'
+     *   Return format: (boolean) If the server supports the UIDPLUS IMAP
+     *                  extension, and the queried mailbox does not support
+     *                  persistent UIDs, this value will be true. In all
+     *                  other cases, this value will be false.
+     *
+     * Flag: Horde_Imap_Client::STATUS_ALL (DEFAULT)
+     *   A shortcut to return 'messages', 'recent', 'uidnext', 'uidvalidity',
+     *   and 'unseen'.
+     * 
+ * + * @return array An array with the requested keys (see above). + */ + public function status($mailbox, $flags = self::STATUS_ALL) + { + if ($flags & self::STATUS_ALL) { + $flags |= self::STATUS_MESSAGES | self::STATUS_RECENT | self::STATUS_UNSEEN | self::STATUS_UIDNEXT | self::STATUS_UIDVALIDITY; + } + + return $this->_status(Horde_Imap_Client_Utf7imap::Utf8ToUtf7Imap($mailbox), $flags); + } + + /** + * Obtain status information for a mailbox. + * Throws a Horde_Imap_Client_Exception on error. + * + * @param string $mailbox The mailbox to query (UTF7-IMAP). + * @param string $flags A bitmask of information requested from the + * server. + * + * @return array See Horde_Imap_Client_Base::status(). + */ + abstract protected function _status($mailbox, $flags); + + /** + * Append message(s) to a mailbox. + * Throws a Horde_Imap_Client_Exception on error. + * + * @param string $mailbox The mailbox to append the message(s) to. Either + * in UTF7-IMAP or UTF-8. + * @param array $data The message data to append, along with + * additional options. An array of arrays with + * each embedded array having the following + * entries: + *
+     * 'data' - (mixed) The data to append. Either a string or a stream
+     *          resource.
+     *          DEFAULT: NONE (entry is MANDATORY)
+     * 'flags' - (array) An array of flags/keywords to set on the appended
+     *           message.
+     *           DEFAULT: Only the '\Recent' flag is set.
+     * 'internaldate' - (DateTime object) The internaldate to set for the
+     *                  appended message.
+     *                  DEFAULT: internaldate will be the same date as when
+     *                  the message was appended.
+     * 'messageid' - (string) For servers/drivers that support the UIDPLUS
+     *               IMAP extension, the UID of the appended message(s) can be
+     *               determined automatically. If this extension is not
+     *               available, the message-id of each message is needed to
+     *               determine the UID. If UIDPLUS is not available, and this
+     *               option is not defined, append() will return true only.
+     *               DEFAULT: If UIDPLUS is supported, or this string is
+     *               provided, appended ID is returned. Else, append() will
+     *               return true.
+     * 
+ * @param array $options Additonal options: + *
+     * 'create' - (boolean) Try to create $mailbox if it does not exist?
+     *             DEFAULT: No.
+     * 
+ * + * @return mixed An array of the UIDs of the appended messages (if server + * supports UIDPLUS extension or 'messageid' is defined) + * or true. + */ + public function append($mailbox, $data, $options = array()) + { + $mailbox = Horde_Imap_Client_Utf7imap::Utf8ToUtf7Imap($mailbox); + + $ret = $this->_append($mailbox, $data, $options); + if (is_array($ret)) { + return $ret; + } + + $msgid = false; + $uids = array(); + + while (list(,$val) = each($data)) { + if (empty($val['messageid'])) { + $uids[] = null; + } else { + $msgid = true; + $search_query = new Horde_Imap_Client_Search_Query(); + $search_query->headerText('Message-ID', $val['messageid']); + $uidsearch = $this->search($mailbox, $search_query); + $uids[] = reset($uidsearch['match']); + } + } + + return $msgid ? $uids : true; + } + + /** + * Append message(s) to a mailbox. + * Throws a Horde_Imap_Client_Exception on error. + * + * @param string $mailbox The mailbox to append the message(s) to + * (UTF7-IMAP). + * @param array $data The message data. + * @param array $options Additional options. + * + * @return mixed An array of the UIDs of the appended messages (if server + * supports UIDPLUS extension) or true. + */ + abstract protected function _append($mailbox, $data, $options); + + /** + * Request a checkpoint of the currently selected mailbox (RFC 3501 + * [6.4.1]). + * Throws a Horde_Imap_Client_Exception on error. + */ + public function check() + { + // CHECK only useful if we are already authenticated. + if ($this->_isAuthenticated) { + $this->_check(); + } + } + + /** + * Request a checkpoint of the currently selected mailbox. + * Throws a Horde_Imap_Client_Exception on error. + */ + abstract protected function _check(); + + /** + * Close the connection to the currently selected mailbox, optionally + * expunging all deleted messages (RFC 3501 [6.4.2]). + * Throws a Horde_Imap_Client_Exception on error. + * + * @param array $options Additional options: + *
+     * 'expunge' - (boolean) Expunge all messages flagged as deleted?
+     *             DEFAULT: No
+     * 
+ */ + public function close($options = array()) + { + if (is_null($this->_selected)) { + return; + } + + /* If we are caching, search for deleted messages. */ + if (!empty($options['expunge']) && $this->_initCacheOb()) { + $search_query = new Horde_Imap_Client_Search_Query(); + $search_query->flag('\\deleted', true); + $search_res = $this->search($this->_selected, $search_query); + } else { + $search_res = null; + } + + $this->_close($options); + $this->_selected = null; + $this->_mode = 0; + + if (!is_null($search_res)) { + $this->_cacheOb->deleteMsgs($this->_selected, $search_res['match']); + } + } + + /** + * Close the connection to the currently selected mailbox, optionally + * expunging all deleted messages (RFC 3501 [6.4.2]). + * Throws a Horde_Imap_Client_Exception on error. + * + * @param array $options Additional options. + */ + abstract protected function _close($options); + + /** + * Expunge deleted messages from the given mailbox. + * Throws a Horde_Imap_Client_Exception on error. + * + * @param string $mailbox The mailbox to expunge. Either in UTF7-IMAP + * or UTF-8. + * @param array $options Additional options: + *
+     * 'ids' - (array) A list of messages to expunge, but only if they
+     *         are also flagged as deleted. By default, this array is
+     *         assumed to contain UIDs (see 'sequence').
+     *         DEFAULT: All messages marked as deleted will be expunged.
+     * 'sequence' - (boolean) If true, 'ids' is an array of sequence numbers.
+     *              DEFAULT: 'sequence' is an array of UIDs.
+     * 
+ */ + public function expunge($mailbox, $options = array()) + { + $this->openMailbox($mailbox, self::OPEN_READWRITE); + $this->_expunge($options); + } + + /** + * Expunge all deleted messages from the given mailbox. + * Throws a Horde_Imap_Client_Exception on error. + * + * @param array $options Additional options. + */ + abstract protected function _expunge($options); + + /** + * Search a mailbox. + * Throws a Horde_Imap_Client_Exception on error. + * + * @param string $mailbox The mailbox to search. Either in UTF7-IMAP + * or UTF-8. + * @param object $query The search query (a + * Horde_Imap_Client_Search_Query object). + * Defaults to an ALL search. + * @param array $options Additional options: + *
+     * 'results' - (array) The data to return. Consists of zero or more of the
+     *                     following flags:
+     * 
+     * Horde_Imap_Client::SORT_RESULTS_COUNT
+     * Horde_Imap_Client::SORT_RESULTS_MATCH (DEFAULT)
+     * Horde_Imap_Client::SORT_RESULTS_MAX
+     * Horde_Imap_Client::SORT_RESULTS_MIN
+     * Horde_Imap_Client::SORT_RESULTS_SAVE - (This option is currently meant
+     *   for internal use only)
+     * 
+ * 'reverse' - (boolean) Sort the entire returned list of messages in + * reverse (i.e. descending) order. + * DEFAULT: Sorted in ascending order. + * 'sequence' - (boolean) If true, returns an array of sequence numbers. + * DEFAULT: Returns an array of UIDs + * 'sort' - (array) Sort the returned list of messages. Multiple sort + * criteria can be specified. The following sort criteria + * are available: + *
+     * Horde_Imap_Client::SORT_ARRIVAL
+     * Horde_Imap_Client::SORT_CC
+     * Horde_Imap_Client::SORT_DATE
+     * Horde_Imap_Client::SORT_FROM
+     * Horde_Imap_Client::SORT_SIZE
+     * Horde_Imap_Client::SORT_SUBJECT
+     * Horde_Imap_Client::SORT_TO.
+     * 
+ * Additionally, any sort criteria can be sorted in reverse order + * (instead of the default ascending order) by adding a + * Horde_Imap_Client::SORT_REVERSE element to the array directly + * before adding the sort element. Note that if you want the + * entire list to be sorted in reverse order, use the 'reverse' + * option instead. If this option is set, the 'results' option + * is ignored. + * DEFAULT: Arrival sort (Horde_Imap_Client::SORT_ARRIVAL) + *
+ * + * @return array An array with the following keys: + *
+     * 'count' - (integer) The number of messages that match the search
+     *           criteria.
+     *           Always returned.
+     * 'match' - OPTIONAL (array) The UIDs (default) or message sequence
+     *           numbers (if 'sequence' is true) that match $criteria.
+     *           Returned if 'sort' is false and
+     *           Horde_Imap_Client::SORT_RESULTS_MATCH is set.
+     * 'max' - (integer) The UID (default) or message sequence number (if
+     *         'sequence is true) of the highest message that satisifies
+     *         $criteria. Returns null if no matches found.
+     *         Returned if Horde_Imap_Client::SORT_RESULTS_MAX is set.
+     * 'min' - (integer) The UID (default) or message sequence number (if
+     *         'sequence is true) of the lowest message that satisifies
+     *         $criteria. Returns null if no matches found.
+     *         Returned if Horde_Imap_Client::SORT_RESULTS_MIN is set.
+     * 'modseq' - (integer) The highest mod-sequence for all messages being
+     *            returned.
+     *            Returned if 'sort' is false, the search query includes a
+     *            modseq command, and the server supports the CONDSTORE IMAP
+     *            extension.
+     * 'save' - (boolean) Whether the search results were saved. This value is
+     *          meant for internal use only. Returned if 'sort' is false and
+     *          Horde_Imap_Client::SORT_RESULTS_SAVE is set.
+     * 'sort' - (array) The sorted UIDs (default) or message sequence numbers
+     *          (if 'sequence' is true) that match $criteria.
+     *          Returned if 'sort' is true.
+     * 
+ */ + public function search($mailbox, $query = null, $options = array()) + { + $this->openMailbox($mailbox, self::OPEN_AUTO); + + if (empty($options['results'])) { + $options['results'] = array( + self::SORT_RESULTS_MATCH, + self::SORT_RESULTS_COUNT + ); + } + + // Default to an ALL search. + if (is_null($query)) { + $query = new Horde_Imap_Client_Search_Query(); + } + + $options['_query'] = $query->build(); + + /* Optimization - if query is just for a count of either RECENT or + * ALL messages, we can send status information instead. Can't + * optimize with unseen queries because we may cause an infinite loop + * between here and the status() call. */ + if ((count($options['results']) == 1) && + (reset($options['results']) == self::SORT_RESULTS_COUNT)) { + switch ($options['_query']['query']) { + case 'ALL': + $ret = $this->status($this->_selected, self::STATUS_MESSAGES); + return array('count' => $ret['messages']); + + case 'RECENT': + $ret = $this->status($this->_selected, self::STATUS_RECENT); + return array('count' => $ret['recent']); + } + } + + $ret = $this->_search($query, $options); + + if (!empty($options['reverse'])) { + if (empty($options['sort'])) { + $ret['match'] = array_reverse($ret['match']); + } else { + $ret['sort'] = array_reverse($ret['sort']); + } + } + + return $ret; + } + + /** + * Search a mailbox. + * Throws a Horde_Imap_Client_Exception on error. + * + * @param object $query The search query. + * @param array $options Additional options. The '_query' key contains + * the value of $query->build(). + * + * @return array An array of UIDs (default) or an array of message + * sequence numbers (if 'sequence' is true). + */ + abstract protected function _search($query, $options); + + /** + * Set the comparator to use for searching/sorting (RFC 5255). + * Throws a Horde_Imap_Client_Exception on error. + * + * @param string $comparator The comparator string (see RFC 4790 [3.1] - + * "collation-id" - for format). The reserved + * string 'default' can be used to select + * the default comparator. + */ + public function setComparator($comparator = null) + { + $comp = is_null($comparator) ? (empty($this->_params['comparator']) ? null : $this->_params['comparator']) : $comparator; + if (is_null($comp)) { + return; + } + + $i18n = $this->queryCapability('I18NLEVEL'); + if (empty($i18n) || (max($i18n) < 2)) { + throw new Horde_Imap_Client_Exception('The IMAP server does not support changing SEARCH/SORT comparators.', Horde_Imap_Client_Exception::NOSUPPORTIMAPEXT); + } + + $this->_setComparator($comp); + } + + /** + * Set the comparator to use for searching/sorting (RFC 5255). + * Throws a Horde_Imap_Client_Exception on error. + * + * @param string $comparator The comparator string (see RFC 4790 [3.1] - + * "collation-id" - for format). The reserved + * string 'default' can be used to select + * the default comparator. + */ + abstract protected function _setComparator($comparator); + + /** + * Get the comparator used for searching/sorting (RFC 5255). + * Throws a Horde_Imap_Client_Exception on error. + * + * @return mixed Null if the default comparator is being used, or an + * array of comparator information (see RFC 5255 [4.8]). + */ + public function getComparator() + { + $i18n = $this->queryCapability('I18NLEVEL'); + if (empty($i18n) || (max($i18n) < 2)) { + return null; + } + + return $this->_getComparator(); + } + + /** + * Get the comparator used for searching/sorting (RFC 5255). + * Throws a Horde_Imap_Client_Exception on error. + * + * @return mixed Null if the default comparator is being used, or an + * array of comparator information (see RFC 5255 [4.8]). + */ + abstract protected function _getComparator(); + + /** + * Thread sort a given list of messages (RFC 5256). + * Throws a Horde_Imap_Client_Exception on error. + * + * @param string $mailbox The mailbox to search. Either in UTF7-IMAP + * or UTF-8. + * @param array $options Additional options: + *
+     * 'criteria' - (mixed) The following thread criteria are available:
+     *              Horde_Imap_Client::THREAD_ORDEREDSUBJECT, and
+     *              Horde_Imap_Client::THREAD_REFERENCES. Additionally, other
+     *              algorithms can be explicitly specified by passing the IMAP
+     *              thread algorithm in as a string.
+     * 'search' - (object) The search query (a
+     *            Horde_Imap_Client_Search_Query object).
+     *            DEFAULT: All messages in mailbox included in thread sort.
+     * 'sequence' - (boolean) If true, each message is stored and referred to
+     *              by its message sequence number.
+     *              DEFAULT: Stored/referred to by UID.
+     * 
+ * + * @return Horde_Imap_Client_Thread A Horde_Imap_Client_Thread object. + */ + public function thread($mailbox, $options = array()) + { + $this->openMailbox($mailbox, self::OPEN_AUTO); + + $ret = $this->_thread($options); + return new Horde_Imap_Client_Thread($ret, empty($options['sequence']) ? 'uid' : 'sequence'); + } + + /** + * Thread sort a given list of messages (RFC 5256). + * Throws a Horde_Imap_Client_Exception on error. + * + * @param array $options Additional options. + * + * @return array An array with the following values, one per message, + * with the key being either the UID (default) or the + * message sequence number (if 'sequence' is true). Values + * of each entry: + *
+     * 'base' - (integer) The UID of the base message. Is null if this is the
+     *          only message in the thread.
+     * 'last' - (boolean) Is this the last message in a subthread?
+     * 'level' - (integer) The thread level of this message (1 = base).
+     * 'uid' - (integer) The UID of the message.
+     * 
+ */ + abstract protected function _thread($options); + + /** + * Fetch message data (see RFC 3501 [6.4.5]). + * Throws a Horde_Imap_Client_Exception on error. + * + * @param string $mailbox The mailbox to fetch messages from. Either in + * UTF7-IMAP or UTF-8. + * @param array $criteria The fetch criteria. Contains the following: + *
+     * Key: Horde_Imap_Client::FETCH_FULLMSG
+     *   Desc: Returns the full text of the message.
+     *         ONLY ONE of these entries should be defined.
+     *   Value: (array) The following options are available:
+     *     'length' - (integer) If 'start' is defined, the length of the
+     *                substring to return.
+     *                DEFAULT: The entire text is returned.
+     *     'peek' - (boolean) If set, does not set the '\Seen' flag on the
+     *              message.
+     *              DEFAULT: The seen flag is set.
+     *     'start' - (integer) If a portion of the full text is desired to be
+     *               returned, the starting position is identified here.
+     *               DEFAULT: The entire text is returned.
+     *   Return key: 'fullmsg'
+     *   Return format: (string) The full text of the entire message (or the
+     *                  portion of the text delineated by the 'start'/'length'
+     *                  parameters).
+     *
+     * Key: Horde_Imap_Client::FETCH_HEADERTEXT
+     *   Desc: Returns the header text. Header text is defined only for the
+     *         base RFC 2822 message or message/rfc822 parts. Attempting to
+     *         retireve the body text from other parts will result in a
+     *         thrown exception.
+     *         MORE THAN ONE of these entries can be defined. Each entry will
+     *         be a separate array contained in the value field.
+     *         Each entry should have a unique 'id' value.
+     *   Value: (array) One array for each request. Each array may contain
+     *          the following options:
+     *     'id' - (string) The MIME ID to obtain the header text for.
+     *            DEFAULT: The header text for the entire message (MIME ID: 0)
+     *            will be returned.
+     *     'length' - (integer) If 'start' is defined, the length of the
+     *                substring to return.
+     *                DEFAULT: The entire text is returned.
+     *     'parse' - (boolean) If true, and the Horde MIME library is
+     *               available, parse the header text into a MIME_Headers
+     *               object.
+     *               DEFAULT: The full header text is returned.
+     *     'peek' - (boolean) If set, does not set the '\Seen' flag on the
+     *              message.
+     *              DEFAULT: The seen flag is set.
+     *     'start' - (integer) If a portion of the full text is desired to be
+     *               returned, the starting position is identified here.
+     *               DEFAULT: The entire text is returned.
+     *   Return key: 'headertext'
+     *   Return format: (mixed) If 'parse' is true, a MIME_Headers object.
+     *                  Else, an array of header text entries. Keys are the
+     *                  the 'id', values are the message header text strings
+     *                  (or the portion of the text delineated by the
+     *                  'start'/'length' parameters).
+     *
+     * Key: Horde_Imap_Client::FETCH_BODYTEXT
+     *   Desc: Returns the body text. Body text is defined only for the
+     *         base RFC 2822 message or message/rfc822 parts. Attempting to
+     *         retireve the body text from other parts will result in a
+     *         thrown exception.
+     *         MORE THAN ONE of these entries can be defined. Each entry will
+     *         be a separate array contained in the value field.
+     *         Each entry should have a unique 'id' value.
+     *   Value: (array) One array for each request. Each array may contain
+     *          the following options:
+     *     'id' - (string) The MIME ID to obtain the body text for.
+     *            DEFAULT: The body text for the entire message (MIME ID: 0)
+     *            will be returned.
+     *     'length' - (integer) If 'start' is defined, the length of the
+     *                substring to return.
+     *                DEFAULT: The entire text is returned.
+     *     'peek' - (boolean) If set, does not set the '\Seen' flag on the
+     *              message.
+     *              DEFAULT: The seen flag is set.
+     *     'start' - (integer) If a portion of the full text is desired to be
+     *               returned, the starting position is identified here.
+     *               DEFAULT: The entire text is returned.
+     *   Return key: 'bodytext'
+     *   Return format: (array) An array of body text entries. Keys are the
+     *                  the 'id', values are the message body text strings
+     *                  (or the portion of the text delineated by the
+     *                  'start'/'length' parameters).
+     *
+     * Key: Horde_Imap_Client::FETCH_MIMEHEADER
+     *   Desc: Returns the MIME header text. MIME header text is defined only
+     *         for non RFC 2822 messages and non message/rfc822 parts.
+     *         Attempting to retrieve the MIME header from other parts will
+     *         result in a thrown exception.
+     *         MORE THAN ONE of these entries can be defined. Each entry will
+     *         be a separate array contained in the value field.
+     *         Each entry should have a unique 'id' value.
+     *   Value: (array) One array for each request. Each array may contain
+     *          the following options:
+     *     'id' - (string) The MIME ID to obtain the MIME header text for.
+     *            DEFAULT: NONE
+     *     'length' - (integer) If 'start' is defined, the length of the
+     *                substring to return.
+     *                DEFAULT: The entire text is returned.
+     *     'peek' - (boolean) If set, does not set the '\Seen' flag on the
+     *              message.
+     *              DEFAULT: The seen flag is set.
+     *     'start' - (integer) If a portion of the full text is desired to be
+     *               returned, the starting position is identified here.
+     *               DEFAULT: The entire text is returned.
+     *   Return key: 'mimeheader'
+     *   Return format: (array) An array of MIME header text entries. Keys are
+     *                  the 'id', values are the MIME header text strings
+     *                  (or the portion of the text delineated by the
+     *                  'start'/'length' parameters).
+     *
+     * Key: Horde_Imap_Client::FETCH_BODYPART
+     *   Desc: Returns the body part data for a given MIME ID.
+     *         MORE THAN ONE of these entries can be defined. Each entry will
+     *         be a separate array contained in the value field.
+     *         Each entry should have a unique 'id' value.
+     *   Value: (array) One array for each request. Each array may contain
+     *          the following options:
+     *     'decode' - (boolean) Attempt to server-side decode the bodypart
+     *                data if it is MIME transfer encoded. If it can be done,
+     *                the 'bodypartdecode' key will be set with one of two
+     *                values: '8bit' or 'binary'.
+     *                DEFAULT: The raw data.
+     *     'id' - (string) The MIME ID to obtain the body part text for.
+     *            DEFAULT: NONE
+     *     'length' - (integer) If 'start' is defined, the length of the
+     *                substring to return.
+     *                DEFAULT: The entire data is returned.
+     *     'peek' - (boolean) If set, does not set the '\Seen' flag on the
+     *              message.
+     *              DEFAULT: The seen flag is set.
+     *     'start' - (integer) If a portion of the full data is desired to be
+     *               returned, the starting position is identified here.
+     *               DEFAULT: The entire data is returned.
+     *   Return key: 'bodypart' (and possibly 'bodypartdecode')
+     *   Return format: (array) An array of body part data entries. Keys are
+     *                  the 'id', values are the body part data (or the
+     *                  portion of the data delineated by the 'start'/'length'
+     *                  parameters).
+     *
+     * Key: Horde_Imap_Client::FETCH_BODYPARTSIZE
+     *   Desc: Returns the decoded body part size for a given MIME ID.
+     *         MORE THAN ONE of these entries can be defined. Each entry will
+     *         be a separate array contained in the value field.
+     *         Each entry should have a unique 'id' value.
+     *   Value: (array) One array for each request. Each array may contain
+     *          the following options:
+     *     'id' - (string) The MIME ID to obtain the body part size for.
+     *            DEFAULT: NONE
+     *   Return key: 'bodypartsize' (if supported by server)
+     *   Return format: (integer) The body part size in bytes. If the server
+     *                  does not support the functionality, 'bodypartsize'
+     *                  will not be set.
+     *
+     * Key: Horde_Imap_Client::FETCH_HEADERS
+     *   Desc: Returns RFC 2822 header text that matches a search string.
+     *         This header search work only with the base RFC 2822 message or
+     *         message/rfc822 parts.
+     *         MORE THAN ONE of these entries can be defined. Each entry will
+     *         be a separate array contained in the value field.
+     *         Each entry should have a unique 'label' value.
+     *   Value: (array) One array for each request. Each array may contain
+     *          the following options:
+     *     'headers' - (array) The headers to search for (case-insensitive).
+     *                 DEFAULT: NONE (MANDATORY)
+     *     'id' - (string) The MIME ID to search.
+     *            DEFAULT: The base message part (MIME ID: 0)
+     *     'label' - (string) A unique label associated with this particular
+     *               search. This is how the results are stored.
+     *               DEFAULT: NONE (MANDATORY entry or exception will be
+     *               thrown)
+     *     'length' - (integer) If 'start' is defined, the length of the
+     *                substring to return.
+     *                DEFAULT: The entire text is returned.
+     *     'notsearch' - (boolean) Do a 'NOT' search on the headers.
+     *                   DEFAULT: false
+     *     'parse' - (boolean) If true, and the Horde MIME library is
+     *               available, parse the returned headers into a
+     *               MIME_Headers object.
+     *               DEFAULT: The full header text is returned.
+     *     'peek' - (boolean) If set, does not set the '\Seen' flag on the
+     *              message.
+     *              DEFAULT: The seen flag is set.
+     *     'start' - (integer) If a portion of the full text is desired to be
+     *               returned, the starting position is identified here.
+     *               DEFAULT: The entire text is returned.
+     *   Return key: 'headers'
+     *   Return format: (mixed) If parse is true, a MIME_Headers object.
+     *                  Else, an array of header search entries. Keys are
+     *                  the 'label'. If 'parse' is false, values are the
+     *                  matched header text. If 'parse' is true,
+     *                  values are an array with the header names as keys
+     *                  (case-insensitive) and the header values as the
+     *                  values. Both returns are subject to the search result
+     *                  being truncated due to the 'start'/'length'
+     *                  parameters.
+     *
+     * Key: Horde_Imap_Client::FETCH_STRUCTURE
+     *   Desc: Returns MIME structure information
+     *         ONLY ONE of these entries should be defined per fetch request.
+     *   Value: (array) The following options are available:
+     *     'noext' - (boolean) Don't return information on extensions
+     *               DEFAULT: Will return information on extensions
+     *     'parse' - (boolean) If true, and the Horde MIME library is
+     *               available, parse the returned structure into a
+     *               MIME_Message object.
+     *               DEFAULT: The array representation is returned.
+     *   Return key: 'structure' [CACHEABLE]
+     *   Return format: (mixed) If 'parse' is true, a MIME_Structure object.
+     *                          Else, an array with the following information:
+     *
+     *     'type' - (string) The MIME type
+     *     'subtype' - (string) The MIME subtype
+     *
+     *     The returned array MAY contain the following information:
+     *     'disposition' - (string) The disposition type of the part (e.g.
+     *                     'attachment', 'inline').
+     *     'dparameters' - (array) Attribute/value pairs from the part's
+     *                     Content-Disposition header.
+     *     'language' - (array) A list of body language values.
+     *     'location' - (string) The body content URI.
+     *
+     *     Depending on the MIME type of the part, the array will also contain
+     *     further information. If labeled as [OPTIONAL], the array MAY
+     *     contain this information, but only if 'noext' is false and the
+     *     server returned the requested information. Else, the value is not
+     *     set.
+     *
+     *     multipart/* parts:
+     *     ==================
+     *     'parts' - (array) An array of subparts (follows the same format as
+     *               the base structure array).
+     *     'parameters' - [OPTIONAL] (array) Attribute/value pairs from the
+     *                    part's Content-Type header.
+     *
+     *     All other parts:
+     *     ================
+     *     'parameters' - (array) Attribute/value pairs from the part's
+     *                    Content-Type header.
+     *     'id' - (string) The part's Content-ID value.
+     *     'description' - (string) The part's Content-Description value.
+     *     'encoding' - (string) The part's Content-Transfer-Encoding value.
+     *     'size' - (integer) - The part's size in bytes.
+     *     'envelope' - [ONLY message/rfc822] (array) See 'envelope' response.
+     *     'structure' - [ONLY message/rfc822] (array) See 'structure'
+     *                   response.
+     *     'lines' - [ONLY message/rfc822 and text/*] (integer) The size of
+     *               the body in text lines.
+     *     'md5' - [OPTIONAL] (string) The part's MD5 value.
+     *
+     * Key: Horde_Imap_Client::FETCH_ENVELOPE
+     *   Desc: Envelope header data
+     *         ONLY ONE of these entries should be defined per fetch request.
+     *   Value: NONE
+     *   Return key: 'envelope' [CACHEABLE]
+     *   Return format: (array) This array has 9 elements: 'date', 'subject',
+     *     'from', 'sender', 'reply-to', 'to', 'cc', 'bcc', 'in-reply-to', and
+     *     'message-id'. For 'date', 'subject', 'in-reply-to', and
+     *     'message-id', the values will be a string or null if it doesn't
+     *     exist. For the other keys, the value will be an array of arrays (or
+     *     an empty array if the header does not exist). Each of these
+     *     underlying arrays corresponds to a single address and contains 4
+     *     keys: 'personal', 'adl', 'mailbox', and 'host'. These keys will
+     *     only be set if the server returned information.
+     *
+     * Key: Horde_Imap_Client::FETCH_FLAGS
+     *   Desc: Flags set for the message
+     *         ONLY ONE of these entries should be defined per fetch request.
+     *   Value: NONE
+     *   Return key: 'flags' [CACHEABLE - if CONSTORE IMAP extension is
+     *                        supported on the server]
+     *   Return format: (array) Each flag will be in a separate array entry.
+     *     The flags will be entirely in lowercase.
+     *
+     * Key: Horde_Imap_Client::FETCH_DATE
+     *   Desc: The internal (IMAP) date of the message
+     *         ONLY ONE of these entries should be defined per fetch request.
+     *   Value: NONE
+     *   Return key: 'date' [CACHEABLE]
+     *   Return format: (DateTime object) Returns a PHP DateTime object.
+     *
+     * Key: Horde_Imap_Client::FETCH_SIZE
+     *   Desc: The size (in bytes) of the message
+     *         ONLY ONE of these entries should be defined per fetch request.
+     *   Value: NONE
+     *   Return key: 'size' [CACHEABLE]
+     *   Return format: (integer) The size of the message.
+     *
+     * Key: Horde_Imap_Client::FETCH_UID
+     *   Desc: The Unique ID of the message.
+     *         ONLY ONE of these entries should be defined per fetch request.
+     *   Value: NONE
+     *   Returned key: 'uid'
+     *   Return format: (integer) The unique ID of the message.
+     *
+     * Key: Horde_Imap_Client::FETCH_SEQ
+     *   Desc: The sequence number of the message.
+     *         ONLY ONE of these entries should be defined per fetch request.
+     *   Value: NONE
+     *   Return key: 'seq'
+     *   Return format: (integer) The sequence number of the message.
+     *
+     * Key: Horde_Imap_Client::FETCH_MODSEQ
+     *   Desc: The mod-sequence value for the message.
+     *         The server must support the CONDSTORE IMAP extension to use
+     *         this criteria. Additionally, the mailbox must support mod-
+     *         sequences or an exception will be thrown.
+     *         ONLY ONE of these entries should be defined per fetch request.
+     *   Value: NONE
+     *   Returned key: 'modseq'
+     *   Return format: (integer) The mod-sequence value of the message, or
+     *                  undefined if the server does not support CONDSTORE.
+     * 
+ * @param array $options Additional options: + *
+     * 'changedsince' - (integer) Only return messages that have a
+     *                  mod-sequence larger than this value. This option
+     *                  requires the CONDSTORE IMAP extension (if not present,
+     *                  this value is ignored). Additionally, the mailbox
+     *                  must support mod-sequences or an exception will be
+     *                  thrown. If valid, this option implicity adds the
+     *                  Horde_Imap_Client::FETCH_MODSEQ fetch criteria to
+     *                  the fetch command.
+     *                  DEFAULT: Mod-sequence values are ignored.
+     * 'ids' - (array) A list of messages to fetch data from.
+     *         DEFAULT: All messages in $mailbox will be fetched.
+     * 'sequence' - (boolean) If true, 'ids' is an array of sequence numbers.
+     *              DEFAULT: 'ids' is an array of UIDs.
+     * 'vanished' - (boolean) Only return messages from the UID set parameter
+     *              that have been expunged and whose associated mod-sequence
+     *              is larger than the specified mod-sequence. This option
+     *              requires the QRESYNC IMAP extension, requires
+     *              'changedsince' to be set, and requires 'sequence' to
+     *              be false.
+     *              DEFAULT: Vanished search ignored.
+     * 
+ * + * @return array An array of fetch results. The array consists of + * keys that correspond to 'ids', and values that + * contain the array of fetched information as requested + * in criteria. + */ + public function fetch($mailbox, $criteria, $options = array()) + { + $cache_array = $get_fields = $new_criteria = $ret = array(); + $cf = $this->_initCacheOb() ? $this->_params['cache']['fields'] : array(); + $qresync = isset($this->_init['enabled']['QRESYNC']); + $seq = !empty($options['sequence']); + + /* The 'vanished' modifier requires QRESYNC, 'changedsince', and + * !'sequence'. */ + if (!empty($options['vanished']) && + (!$qresync || + $seq || + empty($options['changedsince']))) { + throw new Horde_Imap_Client_Exception('The vanished FETCH modifier is missing a pre-requisite.'); + } + + /* The 'changedsince' modifier implicitly adds the MODSEQ FETCH item. + * (RFC 4551 [3.3.1]). A UID SEARCH will always return UID + * information (RFC 3501 [6.4.8]). Don't add to criteria because it + * simply creates a longer FETCH command. */ + + /* If using cache, we store by UID so we need to return UIDs. */ + if ($seq && !empty($cf)) { + $criteria[self::FETCH_UID] = true; + } + + $this->openMailbox($mailbox, self::OPEN_AUTO); + + /* We need the UIDVALIDITY for the current mailbox. */ + $status_res = $this->status($this->_selected, self::STATUS_HIGHESTMODSEQ | self::STATUS_UIDVALIDITY); + + /* Determine if caching is available and if anything in $criteria is + * cacheable. Do some sanity checking on criteria also. */ + foreach ($criteria as $k => $v) { + $cache_field = null; + + switch ($k) { + case self::FETCH_STRUCTURE: + /* Don't cache if 'noext' is present. It will probably be a + * rare event anyway. */ + if (empty($v['noext']) && isset($cf[$k])) { + /* Structure can be cached two ways - via MIME_Message or + * by internal array format. */ + $cache_field = empty($v['parse']) ? 'HICstructa' : 'HICstructm'; + $fetch_field = 'structure'; + } + break; + + case self::FETCH_BODYPARTSIZE: + if (!$this->queryCapability('BINARY')) { + unset($criteria[$k]); + } + break; + + case self::FETCH_ENVELOPE: + if (isset($cf[$k])) { + $cache_field = 'HICenv'; + $fetch_field = 'envelope'; + } + break; + + case self::FETCH_FLAGS: + if (isset($cf[$k])) { + /* QRESYNC would have already done syncing on mailbox + * open, so no need to do again. */ + if (!$qresync) { + /* Grab all flags updated since the cached modseq + * val. */ + $metadata = $this->_cacheOb->getMetaData($this->_selected, array('HICmodseq')); + if (isset($metadata['HICmodseq']) && + ($metadata['HICmodseq'] != $status_res['highestmodseq'])) { + $uids = $this->_cacheOb->get($this->_selected, array(), array(), $status_res['uidvalidity']); + if (!empty($uids)) { + $this->_fetch(array(self::FETCH_FLAGS => true), array('changedsince' => $metadata['HICmodseq'], 'ids' => $uids)); + } + $this->_cacheOb->setMetaData($mailbox, array('HICmodseq' => $status_res['highestmodseq'])); + } + } + + $cache_field = 'HICflags'; + $fetch_field = 'flags'; + } + break; + + case self::FETCH_DATE: + if (isset($cf[$k])) { + $cache_field = 'HICdate'; + $fetch_field = 'date'; + } + break; + + case self::FETCH_SIZE: + if (isset($cf[$k])) { + $cache_field = 'HICsize'; + $fetch_field = 'size'; + } + break; + + case self::FETCH_MODSEQ: + if (!isset($this->_init['enabled']['CONDSTORE'])) { + unset($criteria[$k]); + } + break; + } + + if (!is_null($cache_field)) { + $cache_array[$k] = array( + 'c' => $cache_field, + 'f' => $fetch_field + ); + $get_fields[] = $cache_field; + } + } + + /* If nothing is cacheable, we can do a straight search. */ + if (empty($cache_array)) { + return $this->_fetch($criteria, $options); + } + + /* If given sequence numbers, we need to switch to UIDs for caching + * purposes. Also, we need UID #'s now if searching the entire + * mailbox. */ + if ($seq || empty($options['ids'])) { + $res_seq = $this->_getSeqUIDLookup(empty($options['ids']) ? null : $options['ids'], $seq); + $uids = $res_seq['uids']; + } else { + $uids = $options['ids']; + } + + /* Get the cached values. */ + try { + $data = $this->_cacheOb->get($this->_selected, $uids, $get_fields, $status_res['uidvalidity']); + } catch (Horde_Imap_Client_Exception $e) { + if ($e->getCode() != Horde_Imap_Client_Exception::CACHEUIDINVALID) { + throw $e; + } + $data = array(); + } + + // Build a list of what we still need. + foreach ($uids as $val) { + $crit = $criteria; + $id = $seq ? $res_seq['lookup'][$val] : $val; + $ret[$id] = array('uid' => $id); + + foreach ($cache_array as $key => $cval) { + // Retrieved from cache so store in return array + if (isset($data[$val][$cval['c']])) { + $ret[$id][$cval['f']] = $data[$val][$cval['c']]; + unset($crit[$key]); + } + } + + if (!$seq) { + unset($crit[self::FETCH_UID]); + } + + if (!empty($crit)) { + $sig = md5(serialize(array_values($crit))); + if (isset($new_criteria[$sig])) { + $new_criteria[$sig]['i'][] = $id; + } else { + $new_criteria[$sig] = array('c' => $crit, 'i' => array($id)); + } + } + } + + if (!empty($new_criteria)) { + $opts = $options; + foreach ($new_criteria as $val) { + $opts['ids'] = $val['i']; + $fetch_res = $this->_fetch($val['c'], $opts); + reset($fetch_res); + while (list($k, $v) = each($fetch_res)) { + reset($v); + while (list($k2, $v2) = each($v)) { + $ret[$k][$k2] = $v2; + } + } + } + } + + return $ret; + } + + /** + * Fetch message data. + * Throws a Horde_Imap_Client_Exception on error. + * + * @param array $criteria The fetch criteria. + * @param array $options Additional options. + * + * @return array See self::fetch(). + */ + abstract protected function _fetch($criteria, $options); + + /** + * Store message flag data (see RFC 3501 [6.4.6]). + * Throws a Horde_Imap_Client_Exception on error. + * + * @param string $mailbox The mailbox containing the messages to modify. + * Either in UTF7-IMAP or UTF-8. + * @param array $options Additional options: + *
+     * 'add' - (array) An array of flags to add.
+     *         DEFAULT: No flags added.
+     * 'ids' - (array) The list of messages to modify.
+     *         DEFAULT: All messages in $mailbox will be modified.
+     * 'remove' - (array) An array of flags to remove.
+     *            DEFAULT: No flags removed.
+     * 'replace' - (array) Replace the current flags with this set
+     *             of flags. Overrides both the 'add' and 'remove' options.
+     *             DEFAULT: No replace is performed.
+     * 'sequence' - (boolean) If true, 'ids' is an array of sequence numbers.
+     *              DEFAULT: 'ids' is an array of UIDs.
+     * 'unchangedsince' - (integer) Only changes flags if the mod-sequence ID
+     *                    of the message is equal or less than this value.
+     *                    Requires the CONDSTORE IMAP extension on the server.
+     *                    Also requires the mailbox to support mod-sequences.
+     *                    Will throw an exception if either condition is not
+     *                    met.
+     *                    DEFAULT: mod-sequence is ignored when applying
+     *                             changes
+     * 
+ * + * @return array If 'unchangedsince' is set, this is a list of UIDs or + * sequence numbers (if 'sequence' is true) that failed + * the 'unchangedsince' test. Else, an empty array. + */ + public function store($mailbox, $options = array()) + { + $this->openMailbox($mailbox, self::OPEN_READWRITE); + + if (!empty($options['unchangedsince']) && + !isset($this->_init['enabled']['CONDSTORE'])) { + throw new Horde_Imap_Client_Exception('Server does not support the CONDSTORE extension.', Horde_Imap_Client_Exception::NOSUPPORTIMAPEXT); + } + + return $this->_store($options); + } + + /** + * Store message flag data. + * Throws a Horde_Imap_Client_Exception on error. + * + * @param array $options Additional options. + * + * @return array See Horde_Imap_Client::store(). + */ + abstract protected function _store($options); + + /** + * Copy messages to another mailbox. + * Throws a Horde_Imap_Client_Exception on error. + * + * @param string $source The source mailbox. Either in UTF7-IMAP + * or UTF-8. + * @param string $dest The destination mailbox. Either in UTF7-IMAP + * or UTF-8. + * @param array $options Additional options: + *
+     * 'create' - (boolean) Try to create $dest if it does not exist?
+     *            DEFAULT: No.
+     * 'ids' - (array) The list of messages to copy.
+     *         DEFAULT: All messages in $mailbox will be copied.
+     * 'move' - (boolean) If true, delete the original messages.
+     *          DEFAULT: Original messages are not deleted.
+     * 'sequence' - (boolean) If true, 'ids' is an array of sequence numbers.
+     *              DEFAULT: 'ids' is an array of UIDs.
+     * 
+ * + * @return mixed An array mapping old UIDs (keys) to new UIDs (values) on + * success (if the IMAP server and/or driver support the + * UIDPLUS extension) or true. + */ + public function copy($source, $dest, $options = array()) + { + $this->openMailbox($source, empty($options['move']) ? self::OPEN_AUTO : self::OPEN_READWRITE); + return $this->_copy(Horde_Imap_Client_Utf7imap::Utf8ToUtf7Imap($dest), $options); + } + + /** + * Copy messages to another mailbox. + * Throws a Horde_Imap_Client_Exception on error. + * + * @param string $dest The destination mailbox (UTF7-IMAP). + * @param array $options Additional options. + * + * @return mixed An array mapping old UIDs (keys) to new UIDs (values) on + * success (if the IMAP server and/or driver support the + * UIDPLUS extension) or true. + */ + abstract protected function _copy($dest, $options); + + /** + * Set quota limits. The server must support the IMAP QUOTA extension + * (RFC 2087). + * Throws a Horde_Imap_Client_Exception on error. + * + * @param string $root The quota root. Either in UTF7-IMAP or UTF-8. + * @param array $options Additional options: + *
+     * 'messages' - (integer) The limit to set on the number of messages
+     *              allowed.
+     *              DEFAULT: No limit set.
+     * 'storage' - (integer) The limit (in units of 1 KB) to set for the
+     *             storage size.
+     *             DEFAULT: No limit set.
+     * 
+ */ + public function setQuota($root, $options = array()) + { + if (!$this->queryCapability('QUOTA')) { + throw new Horde_Imap_Client_Exception('Server does not support the QUOTA extension.', Horde_Imap_Client_Exception::NOSUPPORTIMAPEXT); + } + + if (isset($options['messages']) || isset($options['storage'])) { + $this->_setQuota(Horde_Imap_Client_Utf7imap::Utf8ToUtf7Imap($root), $options); + } + } + + /** + * Set quota limits. + * Throws a Horde_Imap_Client_Exception on error. + * + * @param string $root The quota root (UTF7-IMAP). + * @param array $options Additional options. + * + * @return boolean True on success. + */ + abstract protected function _setQuota($root, $options); + + /** + * Get quota limits. The server must support the IMAP QUOTA extension + * (RFC 2087). + * Throws a Horde_Imap_Client_Exception on error. + * + * @param string $root The quota root. Either in UTF7-IMAP or UTF-8. + * + * @return mixed An array with these possible keys: 'messages' and + * 'storage'; each key holds an array with 2 values: + * 'limit' and 'usage'. + */ + public function getQuota($root) + { + if (!$this->queryCapability('QUOTA')) { + throw new Horde_Imap_Client_Exception('Server does not support the QUOTA extension.', Horde_Imap_Client_Exception::NOSUPPORTIMAPEXT); + } + + return $this->_getQuota(Horde_Imap_Client_Utf7imap::Utf8ToUtf7Imap($root)); + } + + /** + * Get quota limits. + * Throws a Horde_Imap_Client_Exception on error. + * + * @param string $root The quota root (UTF7-IMAP). + * + * @return mixed An array with these possible keys: 'messages' and + * 'storage'; each key holds an array with 2 values: + * 'limit' and 'usage'. + */ + abstract protected function _getQuota($root); + + /** + * Get quota limits for a mailbox. The server must support the IMAP QUOTA + * extension (RFC 2087). + * Throws a Horde_Imap_Client_Exception on error. + * + * @param string $mailbox A mailbox. Either in UTF7-IMAP or UTF-8. + * + * @return mixed An array with the keys being the quota roots. Each key + * holds an array with two possible keys: 'messages' and + * 'storage'; each of these keys holds an array with 2 + * values: 'limit' and 'usage'. + */ + public function getQuotaRoot($mailbox) + { + if (!$this->queryCapability('QUOTA')) { + throw new Horde_Imap_Client_Exception('Server does not support the QUOTA extension.', Horde_Imap_Client_Exception::NOSUPPORTIMAPEXT); + } + + return $this->_getQuotaRoot(Horde_Imap_Client_Utf7imap::Utf8ToUtf7Imap($mailbox)); + } + + /** + * Get quota limits for a mailbox. + * Throws a Horde_Imap_Client_Exception on error. + * + * @param string $mailbox A mailbox (UTF7-IMAP). + * + * @return mixed An array with the keys being the quota roots. Each key + * holds an array with two possible keys: 'messages' and + * 'storage'; each of these keys holds an array with 2 + * values: 'limit' and 'usage'. + */ + abstract protected function _getQuotaRoot($mailbox); + + /** + * Get the ACL rights for a given mailbox. The server must support the + * IMAP ACL extension (RFC 2086/4314). + * Throws a Horde_Imap_Client_Exception on error. + * + * @param string $mailbox A mailbox. Either in UTF7-IMAP or UTF-8. + * + * @return array An array with identifiers as the keys and an array of + * rights as the values. + */ + public function getACL($mailbox) + { + return $this->_getACL(Horde_Imap_Client_Utf7imap::Utf8ToUtf7Imap($mailbox)); + } + + /** + * Get ACL rights for a given mailbox. + * Throws a Horde_Imap_Client_Exception on error. + * + * @param string $mailbox A mailbox (UTF7-IMAP). + * + * @return array An array with identifiers as the keys and an array of + * rights as the values. + */ + abstract protected function _getACL($mailbox); + + /** + * Set ACL rights for a given mailbox/identifier. + * Throws a Horde_Imap_Client_Exception on error. + * + * @param string $mailbox A mailbox. Either in UTF7-IMAP or UTF-8. + * @param string $identifier The identifier to alter. Either in UTF7-IMAP + * or UTF-8. + * @param array $options Additional options: + *
+     * 'remove' - (boolean) If true, removes all rights for $identifier.
+     *            DEFAULT: Rights in 'rights' are added.
+     * 'rights' - (string) The rights to alter.
+     *            DEFAULT: No rights are altered.
+     * 
+ */ + public function setACL($mailbox, $identifier, $options) + { + if (!$this->queryCapability('ACL')) { + throw new Horde_Imap_Client_Exception('Server does not support the ACL extension.', Horde_Imap_Client_Exception::NOSUPPORTIMAPEXT); + } + + return $this->_setACL(Horde_Imap_Client_Utf7imap::Utf8ToUtf7Imap($mailbox), Horde_Imap_Client_Utf7imap::Utf8ToUtf7Imap($identifier), $options); + } + + /** + * Set ACL rights for a given mailbox/identifier. + * Throws a Horde_Imap_Client_Exception on error. + * + * @param string $mailbox A mailbox (UTF7-IMAP). + * @param string $identifier The identifier to alter (UTF7-IMAP). + * @param array $options Additional options. + */ + abstract protected function _setACL($mailbox, $identifier, $options); + + /** + * List the ACL rights for a given mailbox/identifier. The server must + * support the IMAP ACL extension (RFC 2086/4314). + * Throws a Horde_Imap_Client_Exception on error. + * + * @param string $mailbox A mailbox. Either in UTF7-IMAP or UTF-8. + * @param string $identifier The identifier to alter. Either in UTF7-IMAP + * or UTF-8. + * + * @return array An array with two elements: 'required' (a list of + * required rights) and 'optional' (a list of rights the + * identifier can be granted in the mailbox; these rights + * may be grouped together to indicate that they are tied + * to each other). + */ + public function listACLRights($mailbox, $identifier) + { + if (!$this->queryCapability('ACL')) { + throw new Horde_Imap_Client_Exception('Server does not support the ACL extension.', Horde_Imap_Client_Exception::NOSUPPORTIMAPEXT); + } + + return $this->_listACLRights(Horde_Imap_Client_Utf7imap::Utf8ToUtf7Imap($mailbox), Horde_Imap_Client_Utf7imap::Utf8ToUtf7Imap($identifier)); + } + + /** + * Get ACL rights for a given mailbox/identifier. + * Throws a Horde_Imap_Client_Exception on error. + * + * @param string $mailbox A mailbox (UTF7-IMAP). + * @param string $identifier The identifier to alter (UTF7-IMAP). + * + * @return array An array of rights (keys: 'required' and 'optional'). + */ + abstract protected function _listACLRights($mailbox, $identifier); + + /** + * Get the ACL rights for the current user for a given mailbox. The + * server must support the IMAP ACL extension (RFC 2086/4314). + * Throws a Horde_Imap_Client_Exception on error. + * + * @param string $mailbox A mailbox. Either in UTF7-IMAP or UTF-8. + * + * @return array An array of rights. + */ + public function getMyACLRights($mailbox) + { + if (!$this->queryCapability('ACL')) { + throw new Horde_Imap_Client_Exception('Server does not support the ACL extension.', Horde_Imap_Client_Exception::NOSUPPORTIMAPEXT); + } + + return $this->_getMyACLRights(Horde_Imap_Client_Utf7imap::Utf8ToUtf7Imap($mailbox)); + } + + /** + * Get the ACL rights for the current user for a given mailbox. + * Throws a Horde_Imap_Client_Exception on error. + * + * @param string $mailbox A mailbox (UTF7-IMAP). + * + * @return array An array of rights. + */ + abstract protected function _getMyACLRights($mailbox); + + /* Utility functions. */ + + /** + * Returns UIDs for an ALL search, or for a sequence number -> UID lookup. + * + * @param mixed $ids If null, return all UIDs for the mailbox. If an + * array, only look up these values. + * @param boolean $seq Are $ids sequence numbers? + * + * @return array An array with 2 possible entries: + *
+     * 'lookup' - (array) If $ids is not null, the mapping of sequence
+     *            numbers (keys) to UIDs (values).
+     * 'uids' - (array) The list of UIDs.
+     * 
+ */ + protected function _getSeqUIDLookup($ids, $seq) + { + $search = new Horde_Imap_Client_Search_Query(); + $search->sequence($ids, $seq); + $res = $this->search($this->_selected, $search, array('sort' => array(self::SORT_ARRIVAL))); + $ret = array('uids' => $res['sort']); + if ($seq) { + if (empty($ids)) { + $ids = range(1, count($ret['uids'])); + } else { + sort($ids, SORT_NUMERIC); + } + $ret['lookup'] = array_combine($ret['uids'], $ids); + } + + return $ret; + } + + /** + * Store FETCH data in cache. + * Throws a Horde_Imap_Client_Exception on error. + * + * @param array $data The data array. + * @param array $options Additional options: + *
+     * 'mailbox' - (string) The mailbox to update.
+     *             DEFAULT: The selected mailbox.
+     * 'seq' - (boolean) Is data stored with sequence numbers?
+     *             DEFAULT: Data stored with UIDs.
+     * 'uidvalid' - (integer) The UID Validity number.
+     *              DEFAULT: UIDVALIDITY discovered via a status() call.
+     * 
+ */ + protected function _updateCache($data, $options = array()) + { + if (!$this->_initCacheOb()) { + return; + } + + if (!empty($options['seq'])) { + $seq_res = $this->_getSeqUIDLookup(array_keys($data)); + } + + $cf = $this->_params['cache']['fields']; + $is_flags = false; + $highestmodseq = $tocache = array(); + $mailbox = empty($options['mailbox']) ? $this->_selected : $options['mailbox']; + + if (empty($options['uidvalid'])) { + $status_res = $this->status($mailbox, self::STATUS_HIGHESTMODSEQ | self::STATUS_UIDVALIDITY); + $uidvalid = $status_res['uidvalidity']; + if (isset($status_res['highestmodseq'])) { + $highestmodseq[] = $status_res['highestmodseq']; + } + } else { + $uidvalid = $options['uidvalid']; + } + + reset($data); + while (list($k, $v) = each($data)) { + $tmp = array(); + $id = empty($options['seq']) ? $k : $seq_res['lookup'][$k]; + + reset($v); + while (list($label, $val) = each($v)) { + switch ($label) { + case 'structure': + if (isset($cf[self::FETCH_STRUCTURE])) { + $tmp[is_array($val) ? 'HICstructa' : 'HICstructm'] = $val; + } + break; + + case 'envelope': + if (isset($cf[self::FETCH_ENVELOPE])) { + $tmp['HICenv'] = $val; + } + break; + + case 'flags': + if (isset($cf[self::FETCH_FLAGS])) { + /* A FLAGS FETCH can only occur if we are in the + * mailbox. So either HIGHESTMODSEQ has already been + * updated or the flag FETCHs will provide the new + * HIGHESTMODSEQ value. In either case, we are + * guaranteed that all cache information is correctly + * updated (in the former case, we reached here via + * an 'changedsince' FETCH and in the latter case, we + * are in EXAMINE/SELECT mode and will catch all flag + * changes). */ + if (isset($v['modseq'])) { + $highestmodseq[] = $v['modseq']; + } + $tmp['HICflags'] = $val; + $is_flags = true; + } + break; + + case 'date': + if (isset($cf[self::FETCH_DATE])) { + $tmp['HICdate'] = $val; + } + break; + + case 'size': + if (isset($cf[self::FETCH_SIZE])) { + $tmp['HICsize'] = $val; + } + break; + } + } + + if (!empty($tmp)) { + $tocache[$id] = $tmp; + } + } + + try { + $this->_cacheOb->set($mailbox, $tocache, $uidvalid); + if ($is_flags) { + $this->_cacheOb->setMetaData($mailbox, array('HICmodseq' => max($highestmodseq))); + } + } catch (Horde_Imap_Client_Exception $e) { + if ($e->getCode() != Horde_Imap_Client_Exception::CACHEUIDINVALID) { + throw $e; + } + } + } +} + +/* + * Abstraction of the IMAP4rev1 search criteria (see RFC 3501 [6.4.4]). This + * class allows translation between abstracted search criteria and a + * generated IMAP search criteria string suitable for sending to a remote + * IMAP server. + */ +class Horde_Imap_Client_Search_Query +{ + /* Constants for dateSearch() */ + const DATE_BEFORE = 'BEFORE'; + const DATE_ON = 'ON'; + const DATE_SINCE = 'SINCE'; + + /* Constants for intervalSearch() */ + const INTERVAL_OLDER = 'OLDER'; + const INTERVAL_YOUNGER = 'YOUNGER'; + + /** + * The charset of the search strings. All text strings must be in + * this charset. + * + * @var string + */ + protected $_charset = 'US-ASCII'; + + /** + * The list of defined system flags (see RFC 3501 [2.3.2]). + * + * @var array + */ + protected $_systemflags = array( + 'ANSWERED', 'DELETED', 'DRAFT', 'FLAGGED', 'RECENT', 'SEEN' + ); + + /** + * The list of 'system' headers that have a specific search query. + * + * @var array + */ + protected $_systemheaders = array( + 'BCC', 'CC', 'FROM', 'SUBJECT', 'TO' + ); + + /** + * The list of search params. + * + * @var array + */ + protected $_search = array(); + + /** + * List of extensions needed for advanced queries. + * + * @var array + */ + protected $_exts = array(); + + /** + * Sets the charset of the search text. + * + * @param string $charset The charset to use for the search. + */ + public function charset($charset) + { + $this->_charset = strtoupper($charset); + } + + /** + * Builds an IMAP4rev1 compliant search string. + * + * @return array An array with 3 elements: + *
+     * 'charset' - (string) The charset of the search string.
+     * 'imap4' - (boolean) True if the search uses IMAP4 criteria (as opposed
+     *           to IMAP2 search criteria)
+     * 'query' - (string) The IMAP search string
+     * 
+ */ + public function build() + { + $cmds = array(); + $imap4 = false; + $ptr = &$this->_search; + + if (isset($ptr['new'])) { + if ($ptr['new']) { + $cmds[] = 'NEW'; + unset($ptr['flag']['UNSEEN']); + } else { + $cmds[] = 'OLD'; + } + unset($ptr['flag']['RECENT']); + } + + if (!empty($ptr['flag'])) { + foreach ($ptr['flag'] as $key => $val) { + if ($key == 'draft') { + // DRAFT flag was not in IMAP2 + $imap4 = true; + } + + $tmp = ''; + if (!$val['set']) { + // This is a 'NOT' search. All system flags but \Recent + // have 'UN' equivalents. + if ($key == 'RECENT') { + $tmp = 'NOT '; + // NOT searches were not in IMAP2 + $imap4 = true; + } else { + $tmp = 'UN'; + } + } + + $cmds[] = $tmp . ($val['type'] == 'keyword' ? 'KEYWORD ' : '') . $key; + } + } + + if (!empty($ptr['header'])) { + foreach ($ptr['header'] as $val) { + $tmp = ''; + if ($val['not']) { + $tmp = 'NOT '; + // NOT searches were not in IMAP2 + $imap4 = true; + } + + if (!in_array($val['header'], $this->_systemheaders)) { + // HEADER searches were not in IMAP2 + $tmp .= 'HEADER '; + $imap4 = true; + } + $cmds[] = $tmp . $val['header'] . ' ' . Horde_Imap_Client::escape($val['text']); + } + } + + if (!empty($ptr['text'])) { + foreach ($ptr['text'] as $val) { + $tmp = ''; + if ($val['not']) { + $tmp = 'NOT '; + // NOT searches were not in IMAP2 + $imap4 = true; + } + $cmds[] = $tmp . $val['type'] . ' ' . Horde_Imap_Client::escape($val['text']); + } + } + + if (!empty($ptr['size'])) { + foreach ($ptr['size'] as $key => $val) { + $cmds[] = ($val['not'] ? 'NOT ' : '' ) . $key . ' ' . $val['size']; + // LARGER/SMALLER searches were not in IMAP2 + $imap4 = true; + } + } + + if (isset($ptr['sequence'])) { + $cmds[] = ($ptr['sequence']['not'] ? 'NOT ' : '') . ($ptr['sequence']['sequence'] ? '' : 'UID ') . $ptr['sequence']['ids']; + + // sequence searches were not in IMAP2 + $imap4 = true; + } + + if (!empty($ptr['date'])) { + foreach ($ptr['date'] as $key => $val) { + $tmp = ''; + if ($val['not']) { + $tmp = 'NOT '; + // NOT searches were not in IMAP2 + $imap4 = true; + } + + if ($key == 'header') { + $tmp .= 'SENT'; + // 'SENT*' searches were not in IMAP2 + $imap4 = true; + } + $cmds[] = $tmp . $val['range'] . ' ' . $val['date']; + } + } + + if (!empty($ptr['within'])) { + $imap4 = true; + $this->_exts['WITHIN'] = true; + + foreach ($ptr['within'] as $key => $val) { + $cmds[] = ($val['not'] ? 'NOT ' : '') . $key . ' ' . $val['interval']; + } + } + + if (!empty($ptr['modseq'])) { + $imap4 = true; + $this->_exts['CONDSTORE'] = true; + $cmds[] = ($ptr['modseq']['not'] ? 'NOT ' : '') . + 'MODSEQ ' . + (is_null($ptr['modseq']['name']) + ? '' + : Horde_Imap_Client::escape($ptr['modseq']['name']) . ' ' . $ptr['modseq']['type'] . ' ') . + $ptr['modseq']['value']; + } + + if (isset($ptr['prevsearch'])) { + $imap4 = true; + $this->_exts['SEARCHRES'] = true; + $cmds[] = ($ptr['prevsearch'] ? '' : 'NOT ') . '$'; + } + + $query = ''; + + // Add OR'ed queries + if (!empty($ptr['or'])) { + foreach ($ptr['or'] as $key => $val) { + // OR queries were not in IMAP 2 + $imap4 = true; + + if ($key == 0) { + $query = '(' . $query . ')'; + } + + $ret = $val->build(); + $query = 'OR (' . $ret['query'] . ') ' . $query; + } + } + + // Add AND'ed queries + if (!empty($ptr['and'])) { + foreach ($ptr['and'] as $key => $val) { + $ret = $val->build(); + $query .= ' ' . $ret['query']; + } + } + + // Default search is 'ALL' + if (empty($cmds)) { + $query .= empty($query) ? 'ALL' : ''; + } else { + $query .= implode(' ', $cmds); + } + + return array( + 'charset' => $this->_charset, + 'imap4' => $imap4, + 'query' => trim($query) + ); + } + + /** + * Return the list of any IMAP extensions needed to perform the query. + * + * @return array The list of extensions (CAPABILITY responses) needed to + * perform the query. + */ + public function extensionsNeeded() + { + return $this->_exts; + } + + /** + * Search for a flag/keywords. + * + * @param string $name The flag or keyword name. + * @param boolean $set If true, search for messages that have the flag + * set. If false, search for messages that do not + * have the flag set. + */ + public function flag($name, $set = true) + { + $name = strtoupper(ltrim($name, '\\')); + if (!isset($this->_search['flag'])) { + $this->_search['flag'] = array(); + } + $this->_search['flag'][$name] = array( + 'set' => $set, + 'type' => in_array($name, $this->_systemflags) ? 'flag' : 'keyword' + ); + } + + /** + * Search for either new messages (messages that have the '\Recent' flag + * but not the '\Seen' flag) or old messages (messages that do not have + * the '\Recent' flag). If new messages are searched, this will clear + * any '\Recent' or '\Unseen' flag searches. If old messages are searched, + * this will clear any '\Recent' flag search. + * + * @param boolean $newmsgs If true, searches for new messages. Else, + * search for old messages. + */ + public function newMsgs($newmsgs = true) + { + $this->_search['new'] = $newmsgs; + } + + /** + * Search for text in the header of a message. + * + * @param string $header The header field. + * @param string $text The search text. + * @param boolean $not If true, do a 'NOT' search of $text. + */ + public function headerText($header, $text, $not = false) + { + if (!isset($this->_search['header'])) { + $this->_search['header'] = array(); + } + $this->_search['header'][] = array( + 'header' => strtoupper($header), + 'text' => $text, + 'not' => $not + ); + } + + /** + * Search for text in either the entire message, or just the body. + * + * @param string $text The search text. + * @param string $bodyonly If true, only search in the body of the + * message. If false, also search in the headers. + * @param boolean $not If true, do a 'NOT' search of $text. + */ + public function text($text, $bodyonly = true, $not = false) + { + if (!isset($this->_search['text'])) { + $this->_search['text'] = array(); + } + $this->_search['text'][] = array( + 'text' => $text, + 'not' => $not, + 'type' => $bodyonly ? 'BODY' : 'TEXT' + ); + } + + /** + * Search for messages smaller/larger than a certain size. + * + * @param integer $size The size (in bytes). + * @param boolean $larger Search for messages larger than $size? + * @param boolean $not If true, do a 'NOT' search of $text. + */ + public function size($size, $larger = false, $not = false) + { + if (!isset($this->_search['size'])) { + $this->_search['size'] = array(); + } + $this->_search['size'][$larger ? 'LARGER' : 'SMALLER'] = array( + 'size' => (float)$size, + 'not' => $not + ); + } + + /** + * Search for messages within a given message range. Only one message + * range can be specified per query. + * + * @param array $ids The list of messages to search. + * @param boolean $sequence By default, $ids is assumed to be UIDs. If + * this param is true, $ids are taken to be + * message sequence numbers instead. + * @param boolean $not If true, do a 'NOT' search of the sequence. + */ + public function sequence($ids, $sequence = false, $not = false) + { + if (empty($ids)) { + $ids = '1:*'; + } else { + $ids = Horde_Imap_Client::toSequenceString($ids); + } + $this->_search['sequence'] = array( + 'ids' => $ids, + 'not' => $not, + 'sequence' => $sequence + ); + } + + /** + * Search for messages within a date range. Only one internal date and + * one RFC 2822 date can be specified per query. + * + * @param integer $month Month (from 1-12). + * @param integer $day Day of month (from 1-31). + * @param integer $year Year (4-digit year). + * @param string $range Either: + *
+     * Horde_Imap_Client_Search_Query::DATE_BEFORE,
+     * Horde_Imap_Client_Search_Query::DATE_ON, or
+     * Horde_Imap_Client_Search_Query::DATE_SINCE.
+     * 
+ * @param boolean $header If true, search using the date in the message + * headers. If false, search using the internal + * IMAP date (usually arrival time). + * @param boolean $not If true, do a 'NOT' search of the range. + */ + public function dateSearch($month, $day, $year, $range, $header = true, + $not = false) + { + $type = $header ? 'header' : 'internal'; + if (!isset($this->_search['date'])) { + $this->_search['date'] = array(); + } + $this->_search['date'][$header ? 'header' : 'internal'] = array( + 'date' => date("d-M-y", mktime(0, 0, 0, $month, $day, $year)), + 'range' => $range, + 'not' => $not + ); + } + + /** + * Search for messages within a given interval. Only one interval of each + * type can be specified per search query. The IMAP server must support + * the WITHIN extension (RFC 5032) for this query to be used. + * + * @param integer $interval Seconds from the present. + * @param string $range Either: + *
+     * Horde_Imap_Client_Search_Query::INTERVAL_OLDER, or
+     * Horde_Imap_Client_Search_Query::INTERVAL_YOUNGER
+     * 
+ * @param boolean $not If true, do a 'NOT' search. + */ + public function intervalSearch($interval, $range, $not = false) + { + if (!isset($this->_search['within'])) { + $this->_search['within'] = array(); + } + $this->_search['within'][$range] = array( + 'interval' => $interval, + 'not' => $not + ); + } + + /** + * AND queries - the contents of this query will be AND'ed (in its + * entirety) with the contents of each of the queries passed in. All + * AND'd queries must share the same charset as this query. + * + * @param array $queries An array of queries to AND with this one. Each + * query is a Horde_Imap_Client_Search_Query + * object. + */ + public function andSearch($queries) + { + if (!isset($this->_search['and'])) { + $this->_search['and'] = array(); + } + $this->_search['and'] = array_merge($this->_search['and'], $queries); + } + + /** + * OR a query - the contents of this query will be OR'ed (in its entirety) + * with the contents of each of the queries passed in. All OR'd queries + * must share the same charset as this query. All contents of any single + * query will be AND'ed together. + * + * @param array $queries An array of queries to OR with this one. Each + * query is a Horde_Imap_Client_Search_Query + * object. + */ + public function orSearch($queries) + { + if (!isset($this->_search['or'])) { + $this->_search['or'] = array(); + } + $this->_search['or'] = array_merge($this->_search['or'], $queries); + } + + /** + * Search for messages modified since a specific moment. The IMAP server + * must support the CONDSTORE extension (RFC 4551) for this query to be + * used. + * + * @param integer $value The mod-sequence value. + * @param string $name The entry-name string. + * @param string $type Either 'shared', 'priv', or 'all'. Defaults to + * 'all' + * @param boolean $not If true, do a 'NOT' search. + */ + public function modseq($value, $name = null, $type = null, $not = false) + { + if (!is_null($type)) { + $type = strtolower($type); + if (!in_array($type, array('shared', 'priv', 'all'))) { + $type = 'all'; + } + } + + $this->_search['modseq'] = array( + 'value' => $value, + 'name' => $name, + 'not' => $not, + 'type' => (!is_null($name) && is_null($type)) ? 'all' : $type + ); + } + + /** + * Use the results from the previous SEARCH command. The IMAP server must + * support the SEARCHRES extension (RFC 5032) for this query to be used. + * + * @param boolean $not If true, don't match the previous query. + */ + public function previousSearch($not = false) + { + $this->_search['prevsearch'] = $not; + } +} + +/* + * A class allowing easy access to threaded sort results from + * Horde_Imap_Client::thread(). + */ +class Horde_Imap_Client_Thread +{ + /** + * Internal thread data structure. + * + * @var array + */ + protected $_thread = array(); + + /** + * The index type. + * + * @var string + */ + protected $_type; + + /** + * Constructor. + * + * @param array $data The data as returned by + * Horde_Imap_Client_Base::_thread(). + * @param string $type Either 'uid' or 'sequence'. + */ + function __construct($data, $type) + { + $this->_thread = $data; + $this->_type = $type; + } + + /** + * Return the raw thread data array. + * + * @return array See Horde_Imap_Client_Base::_thread(). + */ + public function getRawData() + { + return $this->_thread; + } + + /** + * Gets the indention level for an index. + * + * @param integer $index The index. + * + * @return mixed Returns the thread indent level if $index found. + * Returns false on failure. + */ + public function getThreadIndent($index) + { + return isset($this->_thread[$index]['level']) + ? $this->_thread[$index]['level'] + : false; + } + + /** + * Gets the base thread index for an index. + * + * @param integer $index The index. + * + * @return mixed Returns the base index if $index is part of a thread. + * Returns false on failure. + */ + public function getThreadBase($index) + { + return !empty($this->_thread[$index]['base']) + ? $this->_thread[$index]['base'] + : false; + } + + /** + * Is this index the last in the current level? + * + * @param integer $index The index. + * + * @return boolean Returns true if $index is the last element in the + * current thread level. + * Returns false if not, or on failure. + */ + public function lastInLevel($index) + { + return !empty($this->_thread[$index]['last']) + ? $this->_thread[$index]['last'] + : false; + } + + /** + * Return the sorted list of messages indices. + * + * @param boolean $new True for newest first, false for oldest first. + * + * @return array The sorted list of messages. + */ + public function messageList($new) + { + return ($new) ? array_reverse(array_keys($this->_thread)) : array_keys($this->_thread); + } + + /** + * Returns the list of messages in the current thread. + * + * @param integer $index The index of the current message. + * + * @return array A list of message indices. + */ + public function getThread($index) + { + /* Find the beginning of the thread. */ + if (($begin = $this->getThreadBase($index)) === false) { + return array($index); + } + + /* Work forward from the first thread element to find the end of the + * thread. */ + $in_thread = false; + $thread_list = array(); + reset($this->_thread); + while (list($k, $v) = each($this->_thread)) { + if ($k == $begin) { + $in_thread = true; + } elseif ($in_thread && ($v['base'] != $begin)) { + break; + } + + if ($in_thread) { + $thread_list[] = $k; + } + } + + return $thread_list; + } +} diff --git a/framework/Imap_Client/lib/Horde/Imap/Client/Cache.php b/framework/Imap_Client/lib/Horde/Imap/Client/Cache.php new file mode 100644 index 000000000..1f719051c --- /dev/null +++ b/framework/Imap_Client/lib/Horde/Imap/Client/Cache.php @@ -0,0 +1,612 @@ + + * @category Horde + * @package Horde_Imap_Client + */ +class Horde_Imap_Client_Cache +{ + /** + * The configuration params. + * + * @var array + */ + protected $_params = array(); + + /** + * The Horde_Cache object. + * + * @var Horde_Cache + */ + protected $_cacheOb; + + /** + * The list of items to save on shutdown. + * + * @var array + */ + protected $_save = array(); + + /** + * The working data for the current pageload. All changes take place to + * this data. + * + * @var array + */ + protected $_data = array(); + + /** + * The list of cache slices loaded. + * + * @var array + */ + protected $_loaded = array(); + + /** + * The mapping of UIDs to slices. + * + * @var array + */ + protected $_slicemap = array(); + + /** + * Return a reference to a concrete Horde_Imap_Client_Cache instance. + * + * This method must be invoked as: + * $var = &IMP_MessageCache::singleton(); + * + * @param array $params The configuration parameters. + * + * @return Horde_Imap_Client_Cache The global instance. + */ + static public function &singleton($params = array()) + { + static $instance = array(); + + $sig = md5(serialize($params)); + + if (!isset($instance[$sig])) { + $instance[$sig] = new Horde_Imap_Client_Cache($params); + } + + return $instance[$sig]; + } + + /** + * Constructor. + * Throws a Horde_Imap_Client_Exception on error. + * + * @param array $params The configuration parameters. + */ + function __construct($params = array()) + { + if (empty($params['driver']) || + empty($params['driver_params']) || + empty($params['username']) || + empty($params['hostspec'])) { + throw new Horde_Imap_Client_Exception('Missing required parameters to Horde_Imap_Client_Cache.'); + } + + /* Initialize the Cache object. */ + $this->_cacheOb = &Horde_Cache::singleton($params['driver'], $params['driver_params']); + if (is_a($this->_cacheOb, 'PEAR_Error')) { + throw new Horde_Imap_Client_Exception($this->_cacheOb->getMessage()); + } + + $compress = null; + if (!empty($params['compress'])) { + switch ($params['compress']) { + case 'gzip': + if (Horde_Serialize::hasCapability(SERIALIZE_GZ_COMPRESS)) { + $compress = SERIALIZE_GZ_COMPRESS; + } + break; + + case 'lzf': + if (Horde_Serialize::hasCapability(SERIALIZE_LZF)) { + $compress = SERIALIZE_LZF; + } + break; + } + + if (is_null($compress)) { + throw new Horde_Imap_Client_Exception('Horde_Cache does not support the compression type given.'); + } + } + + $this->_params = array( + 'compress' => $compress, + 'debug' => empty($params['debug']) ? false : $params['debug'], + 'hostspec' => $params['hostspec'], + 'lifetime' => empty($params['lifetime']) ? 604800 : intval($params['lifetime']), + 'slicesize' => empty($params['slicesize']) ? 50 : intval($params['slicesize']), + 'username' => $params['username'] + ); + } + + /** + * Saves items to the cache at shutdown. + */ + function __destruct() + { + $compress = $this->_params['compress']; + $lifetime = $this->_params['lifetime']; + + foreach ($this->_save as $mbox => $uids) { + $dptr = &$this->_data[$mbox]; + $sptr = &$this->_slicemap[$mbox]; + + /* Get the list of slices to save. */ + foreach (array_intersect_key($sptr['slice'], array_flip($uids)) as $slice) { + $data = array(); + + /* Get the list of IDs to save. */ + foreach (array_keys($sptr['slice'], $slice) as $uid) { + /* Compress individual UID entries. We will worry about + * error checking when decompressing (cache data will + * automatically be invalidated then). */ + if (isset($dptr[$uid])) { + $data[$uid] = ($compress && is_array($dptr[$uid])) ? Horde_Serialize::serialize($dptr[$uid], array(SERIALIZE_BASIC, $compress)) : $dptr[$uid]; + } + } + + $cid = $this->_getCID($mbox, $slice); + if (empty($data)) { + // If empty, we can expire the cache. + $this->_cacheOb->expire($cid); + } else { + $this->_cacheOb->set($cid, Horde_Serialize::serialize($data, SERIALIZE_BASIC), $lifetime); + } + } + + // Save the slicemap + $this->_cacheOb->set($this->_getCID($mbox, 'slicemap'), Horde_Serialize::serialize($sptr, SERIALIZE_BASIC), $lifetime); + } + } + + /** + * Create the unique ID used to store the data in the cache. + * + * @param string $mailbox The mailbox to cache. + * @param string $slice The cache slice. + * + * @return string The cache ID (CID). + */ + protected function _getCID($mailbox, $slice) + { + /* Cache ID = "prefix | username | mailbox | hostspec | slice" */ + return 'horde_imap_client|' . $this->_params['username'] . '|' . $mailbox . '|' . $this->_params['hostspec'] . '|' . $slice; + } + + /** + * Get information from the cache. + * Throws a Horde_Imap_Cache_Exception on error. + * + * @param string $mailbox An IMAP mailbox string. + * @param array $uids The list of message UIDs to retrieve + * information for. If empty, returns the list + * of cached UIDs. + * @param array $fields An array of fields to retrieve. + * @param integer $uidvalid The IMAP uidvalidity value of the mailbox. + * + * @return array An array of arrays with the UID of the message as the + * key (if found) and the fields as values (will be + * undefined if not found). If $uids is empty, returns the + * full list of cached UIDs. + */ + public function get($mailbox, $uids = array(), $fields = array(), + $uidvalid = null) + { + if (empty($uids)) { + $this->_loadSliceMap($mailbox, $uidvalid); + return array_keys($this->_slicemap[$mailbox]['slice']); + } + + $ret_array = array(); + + $this->_loadUIDs($mailbox, $uids, $uidvalid); + if (!empty($this->_data[$mailbox])) { + $fields = array_flip($fields); + $ptr = &$this->_data[$mailbox]; + + foreach ($uids as $val) { + if (isset($ptr[$val])) { + $ret_array[$val] = array_intersect_key($ptr[$val], $fields); + } + } + + if ($this->_params['debug']) { + fwrite($this->_params['debug'], 'Horde_Imap_Client_Cache: Retrieved from cache (mailbox: ' . $mailbox . '; UIDs: ' . implode(',', array_keys($ret_array)) . ")\n"); + } + } + + return $ret_array; + } + + /** + * Store information in cache. + * + * @param string $mailbox An IMAP mailbox string. + * @param array $data The list of data to save. The keys are the + * UIDs, the values are an array of information + * to save. If empty, do a check to make sure + * the uidvalidity is still valid. + * @param integer $uidvalid The IMAP uidvalidity value of the mailbox. + */ + public function set($mailbox, $data, $uidvalid = null) + { + $save = array_keys($data); + if (empty($save)) { + $this->_loadSliceMap($mailbox, $uidvalid); + } else { + try { + $this->_loadUIDs($mailbox, $save, $uidvalid); + } catch (Horde_Imap_Client_Exception $e) { + // Ignore invalidity - just start building the new cache + } + + $d = &$this->_data[$mailbox]; + + reset($data); + while (list($k, $v) = each($data)) { + reset($v); + while (list($k2, $v2) = each($v)) { + $d[$k][$k2] = $v2; + } + } + + $this->_save[$mailbox] = isset($this->_save[$mailbox]) ? array_merge($this->_save[$mailbox], $save) : $save; + + /* Need to select slices now because we may need list of cached + * UIDs before we save. */ + $slices = $this->_getCacheSlices($mailbox, $save, true); + + if ($this->_params['debug']) { + fwrite($this->_params['debug'], 'Horde_Imap_Client_Cache: Stored in cache (mailbox: ' . $mailbox . '; UIDs: ' . implode(',', $save) . ")\n"); + } + } + } + + /** + * Get metadata information for a mailbox. + * + * @param string $mailbox An IMAP mailbox string. + * @param array $entries An array of entries to return. If empty, + * returns all metadata. + * + * @return array The requested metadata. Requested entries that do not + * exist will be undefined. The following entries are + * defaults and always present: + *
+     * 'uidvalid' - (integer) The UIDVALIDITY of the mailbox.
+     * 
+ */ + public function getMetaData($mailbox, $entries = array()) + { + $this->_loadSliceMap($mailbox); + return empty($entries) + ? $this->_slicemap[$mailbox]['data'] + : array_intersect_key($this->_slicemap[$mailbox]['data'], array_flip($entries)); + } + + /** + * Set metadata information for a mailbox. + * + * @param string $mailbox An IMAP mailbox string. + * @param array $data The list of data to save. The keys are the + * metadata IDs, the values are the associated + * data. The following labels are reserved: + * 'uidvalid'. + */ + public function setMetaData($mailbox, $data = array()) + { + if (!empty($data)) { + unset($data['uidvalid']); + $this->_loadSliceMap($mailbox); + $this->_slicemap[$mailbox]['data'] = array_merge($this->_slicemap[$mailbox]['data'], $data); + if (!isset($this->_save[$mailbox])) { + $this->_save[$mailbox] = array(); + } + } + } + + /** + * Delete messages in the cache. + * Throws a Horde_Imap_Client_Exception on error. + * + * @param string $mailbox An IMAP mailbox string. + * @param array $uids The list of message UIDs to delete. + */ + public function deleteMsgs($mailbox, $uids) + { + if (empty($uids)) { + return; + } + + $this->_loadSliceMap($mailbox); + + $save = array(); + $slicemap = &$this->_slicemap[$mailbox]; + $todelete = &$slicemap['delete']; + + foreach ($uids as $id) { + if (isset($slicemap['slice'][$id])) { + if (isset($this->_data[$mailbox][$id])) { + $save[] = $id; + unset($this->_data[$mailbox][$id]); + } else { + $slice = $slicemap['slice'][$id]; + if (!isset($todelete[$slice])) { + $todelete[$slice] = array(); + } + $todelete[$slice][] = $id; + } + unset($this->_save[$mailbox][$id], $slicemap['slice'][$id]); + } + } + + if (!empty($save)) { + if ($this->_params['debug']) { + fwrite($this->_params['debug'], 'Horde_Imap_Client_Cache: Deleted messages from cache (mailbox: ' . $mailbox . '; UIDs: ' . implode(',', $save) . ")\n"); + } + + $this->_save[$mailbox] = isset($this->_save[$mailbox]) ? array_merge($this->_save[$mailbox], $save) : $save; + } elseif (!isset($this->_save[$mailbox])) { + $this->_save[$mailbox] = array(); + } + } + + /** + * Delete a mailbox from the cache. + * + * @param string $mbox The mailbox to delete. + */ + public function deleteMailbox($mbox) + { + $this->_loadSliceMap($mbox); + foreach (array_keys(array_flip($this->_slicemap[$mbox]['slice'])) as $slice) { + $this->_cacheOb->expire($this->_getCID($mbox, $slice)); + } + $this->_cacheOb->expire($this->_getCID($mbox, 'slicemap')); + unset($this->_data[$mbox], $this->_loaded[$mbox], $this->_save[$mbox], $this->_slicemap[$mbox]); + + if ($this->_params['debug']) { + fwrite($this->_params['debug'], 'Horde_Imap_Client_Cache: Deleted mailbox from cache (mailbox: ' . $mbox . ")\n"); + } + } + + /** + * Load the given mailbox by regenerating from the cache. + * Throws a Horde_Imap_Client_Exception on error (only if $uidvalid is + * set). + * + * @param string $mailbox The mailbox to load. + * @param array $uids The UIDs to load. + * @param integer $uidvalid The IMAP uidvalidity value of the mailbox. + */ + protected function _loadMailbox($mailbox, $uids, $uidvalid = null) + { + if (!isset($this->_data[$mailbox])) { + $this->_data[$mailbox] = array(); + } + + $this->_loadSliceMap($mailbox, $uidvalid); + + foreach (array_keys(array_flip($this->_getCacheSlices($mailbox, $uids))) as $val) { + $this->_loadSlice($mailbox, $val); + } + } + + /** + * Load a cache slice into memory. + * + * @param string $mailbox The mailbox to load. + * @param integer $slice The slice to load. + */ + protected function _loadSlice($mailbox, $slice) + { + /* Get the unique cache identifier for this mailbox. */ + $cache_id = $this->_getCID($mailbox, $slice); + + if (!empty($this->_loaded[$cache_id])) { + return; + } + $this->_loaded[$cache_id] = true; + + /* Attempt to grab data from the cache. */ + if (($data = $this->_cacheOb->get($cache_id, $this->_params['lifetime'])) === false) { + return; + } + + $data = Horde_Serialize::unserialize($data, SERIALIZE_BASIC); + if (!is_array($data)) { + return; + } + + /* Remove old entries. */ + $ptr = &$this->_slicemap[$mailbox]; + if (isset($ptr['delete'][$slice])) { + $data = array_diff_key($data, $ptr['delete'][$slice]); + if ($this->_params['debug']) { + fwrite($this->_params['debug'], 'Horde_Imap_Client_Cache: Deleted messages from cache (mailbox: ' . $mailbox . '; UIDs: ' . implode(',', $ptr['delete'][$slice]) . ")\n"); + } + unset($ptr['delete'][$slice]); + + /* Check if slice has less than 5 entries. */ + $save = array(); + if ((count($data) < 5) && + ($slice != intval($ptr['count'] / $this->_params['slicesize']))) { + $save = array_keys($data); + $ptr['slice'] = array_diff_key($ptr['slice'], $save); + } + + if (!isset($this->_save[$mailbox])) { + $this->_save[$mailbox] = array(); + } + if (!empty($save)) { + $this->_save[$mailbox] = array_merge($this->_save[$mailbox], $save); + } + } + + $this->_data[$mailbox] += $data; + } + + /** + * Given a list of UIDs, determine the slices that need to be loaded. + * Throws a Horde_Imap_Client_Exception on error. + * + * @param string $mailbox The mailbox. + * @param array $uids A list of UIDs. + * @param boolean $set Set the slice information in $_slicemap? + * + * @return array UIDs as the keys, the slice number as the value. + */ + protected function _getCacheSlices($mailbox, $uids, $set = false) + { + $this->_loadSliceMap($mailbox); + + $lookup = array(); + $ptr = &$this->_slicemap[$mailbox]; + $slicesize = $this->_params['slicesize']; + + if (!empty($uids)) { + if ($set) { + $pcount = &$ptr['count']; + } else { + $pcount = $ptr['count']; + } + + foreach ($uids as $val) { + if (isset($ptr['slice'][$val])) { + $slice = $ptr['slice'][$val]; + } else { + $slice = intval($pcount++ / $slicesize); + if ($set) { + $ptr['slice'][$val] = $slice; + } + } + $lookup[$val] = $slice; + } + } + + return $lookup; + } + + /** + * Given a list of UIDs, unpacks the messages from stored cache data and + * returns the list of UIDs that exist in the cache. + * + * @param string $mailbox The mailbox. + * @param array $uids The list of UIDs to load. + * @param integer $uidvalid The IMAP uidvalidity value of the mailbox. + */ + protected function _loadUIDs($mailbox, $uids, $uidvalid) + { + $this->_loadMailbox($mailbox, $uids, $uidvalid); + if (empty($this->_data[$mailbox])) { + return; + } + + $compress = $this->_params['compress']; + $ptr = &$this->_data[$mailbox]['data']; + $todelete = array(); + + foreach ($uids as $val) { + if (isset($ptr[$val]) && !is_array($ptr[$val])) { + $success = false; + if (!is_null($compress)) { + $res = Horde_Serialize::unserialize($ptr[$val], array($compress, SERIALIZE_BASIC)); + if (!is_a($res, 'PEAR_Error')) { + $ptr[$val] = $res; + $success = true; + } + } + if (!$success) { + $todelete[] = $val; + } + } + } + + if (!empty($todelete)) { + $this->deleteMsgs($mailbox, $todelete); + } + } + + /** + * Load the slicemap for a given mailbox. The slicemap contains + * the uidvalidity information, the UIDs->slice lookup table, and any + * metadata that needs to be saved for the mailbox. + * + * @param string $mailbox The mailbox. + * @param integer $uidvalid The IMAP uidvalidity value of the mailbox. + */ + protected function _loadSliceMap($mailbox, $uidvalid = null) + { + if (!isset($this->_slicemap[$mailbox])) { + if (($data = $this->_cacheOb->get($this->_getCID($mailbox, 'slicemap'), $this->_params['lifetime'])) !== false) { + $slice = Horde_Serialize::unserialize($data, SERIALIZE_BASIC); + if (is_array($slice)) { + $this->_slicemap[$mailbox] = $slice; + } + } + } + + if (isset($this->_slicemap[$mailbox])) { + $ptr = &$this->_slicemap[$mailbox]['data']['uidvalid']; + if (is_null($ptr)) { + $ptr = $uidvalid; + } elseif (!is_null($uidvalid) && ($ptr != $uidvalid)) { + $this->deleteMailbox($mailbox); + throw new Horde_Imap_Client_Exception('UIDs have been invalidated', Horde_Imap_Client_Exception::CACHEUIDINVALID); + } + } else { + $this->_slicemap[$mailbox] = array( + // Tracking count for purposes of determining slices + 'count' => 0, + // Metadata storage + // By default includes UIDVALIDITY of mailbox. + 'data' => array('uidvalid' => $uidvalid), + // UIDs to delete + 'delete' => array(), + // The slice list. + 'slice' => array() + ); + } + } +} diff --git a/framework/Imap_Client/lib/Horde/Imap/Client/Cclient-pop3.php b/framework/Imap_Client/lib/Horde/Imap/Client/Cclient-pop3.php new file mode 100644 index 000000000..70f1c4a63 --- /dev/null +++ b/framework/Imap_Client/lib/Horde/Imap/Client/Cclient-pop3.php @@ -0,0 +1,439 @@ + + * @category Horde + * @package Horde_Imap_Client + */ +class Horde_Imap_Client_Cclient_pop3 extends Horde_Imap_Client_Cclient +{ + /** + * Constructs a new Horde_Imap_Client_Cclient object. + * + * @param array $params A hash containing configuration parameters. + */ + public function __construct($params) + { + $this->_service = 'pop3'; + if (!isset($params['port'])) { + $params['port'] = ($params['secure'] == 'ssl') ? 995 : 110; + } + parent::__construct($params); + } + + /** + * Get CAPABILITY info from the IMAP server. + * Throws a Horde_Imap_Client_Exception on error. + */ + protected function _capability() + { + throw new Horde_Imap_Client_Exception('IMAP CAPABILITY command not supported on POP3 servers.', Horde_Imap_Client_Exception::POP3_NOTSUPPORTED); + } + + /** + * Get the NAMESPACE information from the IMAP server. + * Throws a Horde_Imap_Client_Exception on error. + */ + protected function _getNamespaces() + { + throw new Horde_Imap_Client_Exception('IMAP namespaces not supported on POP3 servers.', Horde_Imap_Client_Exception::POP3_NOTSUPPORTED); + } + + /** + * Send ID information to the IMAP server (RFC 2971). + * Throws a Horde_Imap_Client_Exception on error. + * + * @param array $info The information to send to the server. + */ + protected function _sendID($info) + { + throw new Horde_Imap_Client_Exception('IMAP ID command not supported on POP3 servers.', Horde_Imap_Client_Exception::POP3_NOTSUPPORTED); + } + + /** + * Return ID information from the IMAP server (RFC 2971). + * Throws a Horde_Imap_Client_Exception on error. + */ + protected function _getID() + { + throw new Horde_Imap_Client_Exception('IMAP ID command not supported on POP3 servers.', Horde_Imap_Client_Exception::POP3_NOTSUPPORTED); + } + + /** + * Sets the preferred language for server response messages (RFC 5255). + * Throws a Horde_Imap_Client_Exception on error. + * + * @param array $info The preferred list of languages. + * + * @return string The language accepted by the server, or null if the + * default language is used. + */ + protected function _setLanguage($langs) + { + throw new Horde_Imap_Client_Exception('IMAP LANGUAGE extension not supported on POP3 servers.', Horde_Imap_Client_Exception::POP3_NOTSUPPORTED); + } + + /** + * Gets the preferred language for server response messages (RFC 5255). + * Throws a Horde_Imap_Client_Exception on error. + * + * @param array $list If true, return the list of available languages. + */ + protected function _getLanguage($list) + { + throw new Horde_Imap_Client_Exception('IMAP LANGUAGE extension not supported on POP3 servers.', Horde_Imap_Client_Exception::POP3_NOTSUPPORTED); + } + + /** + * Open a mailbox. + * Throws a Horde_Imap_Client_Exception on error. + * + * @param string $mailbox The mailbox to open (UTF7-IMAP). + * @param integer $mode The access mode. + */ + protected function _openMailbox($mailbox, $mode) + { + if (strcasecmp($mailbox, 'INBOX') !== 0) { + throw new Horde_Imap_Client_Exception('Mailboxes other than INBOX not supported on POP3 servers.', Horde_Imap_Client_Exception::POP3_NOTSUPPORTED); + } + } + + /** + * Create a mailbox. + * Throws a Horde_Imap_Client_Exception on error. + * + * @param string $mailbox The mailbox to create (UTF7-IMAP). + */ + protected function _createMailbox($mailbox) + { + throw new Horde_Imap_Client_Exception('Creating mailboxes not supported on POP3 servers.', Horde_Imap_Client_Exception::POP3_NOTSUPPORTED); + } + + /** + * Delete a mailbox. + * Throws a Horde_Imap_Client_Exception on error. + * + * @param string $mailbox The mailbox to delete (UTF7-IMAP). + */ + protected function _deleteMailbox($mailbox) + { + throw new Horde_Imap_Client_Exception('Deleting mailboxes not supported on POP3 servers.', Horde_Imap_Client_Exception::POP3_NOTSUPPORTED); + } + + /** + * Rename a mailbox. + * Throws a Horde_Imap_Client_Exception on error. + * + * @param string $old The old mailbox name (UTF7-IMAP). + * @param string $new The new mailbox name (UTF7-IMAP). + */ + protected function _renameMailbox($old, $new) + { + throw new Horde_Imap_Client_Exception('Renaming mailboxes not supported on POP3 servers.', Horde_Imap_Client_Exception::POP3_NOTSUPPORTED); + } + + /** + * Manage subscription status for a mailbox. + * Throws a Horde_Imap_Client_Exception on error. + * + * @param string $mailbox The mailbox to [un]subscribe to (UTF7-IMAP). + * @param boolean $subscribe True to subscribe, false to unsubscribe. + */ + protected function _subscribeMailbox($mailbox, $subscribe) + { + throw new Horde_Imap_Client_Exception('Mailboxes other than INBOX not supported on POP3 servers.', Horde_Imap_Client_Exception::POP3_NOTSUPPORTED); + } + + /** + * Unsubscribe to a mailbox. + * Throws a Horde_Imap_Client_Exception on error. + * + * @param string $mailbox The mailbox to unsubscribe to (UTF7-IMAP). + */ + protected function _unsubscribeMailbox($mailbox) + { + throw new Horde_Imap_Client_Exception('Mailboxes other than INBOX not supported on POP3 servers.', Horde_Imap_Client_Exception::POP3_NOTSUPPORTED); + } + + /** + * Obtain a list of mailboxes matching a pattern. + * Throws a Horde_Imap_Client_Exception on error. + * + * @param string $pattern The mailbox search pattern. + * @param integer $mode Which mailboxes to return. + * @param array $options Additional options. + * + * @return array See Horde_Imap_Client_Base::listMailboxes(). + */ + protected function _listMailboxes($pattern, $mode, $options) + { + $tmp = array('mailbox' => 'INBOX'); + + if (!empty($options['attributes'])) { + $tmp['attributes'] = array(); + } + if (!empty($options['delimiter'])) { + $tmp['delimiter'] = ''; + } + + return array('INBOX' => $tmp); + } + + /** + * Obtain status information for a mailbox. + * Throws a Horde_Imap_Client_Exception on error. + * + * @param string $mailbox The mailbox to query (UTF7-IMAP). + * @param string $flags A bitmask of information requested from the + * server. + * + * @return array See Horde_Imap_Client_Base::status(). + */ + protected function _status($mailbox, $flags) + { + if (strcasecmp($mailbox, 'INBOX') !== 0) { + throw new Horde_Imap_Client_Exception('Mailboxes other than INBOX not supported on POP3 servers.', Horde_Imap_Client_Exception::POP3_NOTSUPPORTED); + } + + // This driver only supports the base flags given by c-client. + if (($flags & Horde_Imap_Client::STATUS_FIRSTUNSEEN) || + ($flags & Horde_Imap_Client::STATUS_FLAGS) || + ($flags & Horde_Imap_Client::STATUS_PERMFLAGS)) { + throw new Horde_Imap_Client_Exception('Improper status request on POP3 server.', Horde_Imap_Client_Exception::POP3_NOTSUPPORTED); + } + + return parent::_status($mailbox, $flags); + } + + /** + * Search a mailbox. + * Throws a Horde_Imap_Client_Exception on error. + * + * @param object $query The search string. + * @param array $options Additional options. + * + * @return array An array of UIDs (default) or an array of message + * sequence numbers (if 'sequence' is true). + */ + protected function _search($query, $options) + { + // POP 3 supports c-client search criteria only. + $search_query = $query->build(); + + /* If more than 1 sort criteria given, or if SORT_REVERSE is given + * as a sort criteria, or search query uses IMAP4 criteria, use the + * Socket client instead. */ + if ($search_query['imap4'] || + (!empty($options['sort']) && + ((count($options['sort']) > 1) || + in_array(self::SORT_REVERSE, $options['sort'])))) { + throw new Horde_Imap_Client_Exception('Unsupported search criteria on POP3 server.', Horde_Imap_Client_Exception::POP3_NOTSUPPORTED); + } + + return parent::_search($query, $options); + } + + /** + * Set the comparator to use for searching/sorting (RFC 5255). + * Throws a Horde_Imap_Client_Exception on error. + * + * @param string $comparator The comparator string (see RFC 4790 [3.1] - + * "collation-id" - for format). The reserved + * string 'default' can be used to select + * the default comparator. + */ + protected function _setComparator($comparator) + { + throw new Horde_Imap_Client_Exception('Search comparators not supported on POP3 server.', Horde_Imap_Client_Exception::POP3_NOTSUPPORTED); + } + + /** + * Get the comparator used for searching/sorting (RFC 5255). + * Throws a Horde_Imap_Client_Exception on error. + */ + protected function _getComparator() + { + throw new Horde_Imap_Client_Exception('Search comparators not supported on POP3 server.', Horde_Imap_Client_Exception::POP3_NOTSUPPORTED); + } + + /** + * Thread sort a given list of messages (RFC 5256). + * Throws a Horde_Imap_Client_Exception on error. + * + * @param array $options Additional options. + * + * @return array See Horde_Imap_Client_Base::thread(). + */ + protected function _thread($options) + { + /* This driver only supports Horde_Imap_Client::THREAD_REFERENCES + * and does not support defining search criteria. */ + if (!empty($options['search']) || + (!empty($options['criteria']) && + $options['criteria'] != self::THREAD_REFERENCES)) { + throw new Horde_Imap_Client_Exception('Unsupported threading criteria on POP3 server.', Horde_Imap_Client_Exception::POP3_NOTSUPPORTED); + } + + return parent::_thread($options); + } + + /** + * Append a message to the mailbox. + * Throws a Horde_Imap_Client_Exception on error. + * + * @param array $mailbox The mailboxes to append the messages to + * (UTF7-IMAP). + * @param array $data The message data. + * @param array $options Additional options. + */ + protected function _append($mailbox, $data, $options) + { + throw new Horde_Imap_Client_Exception('Appending messages not supported on POP3 servers.', Horde_Imap_Client_Exception::POP3_NOTSUPPORTED); + } + + /** + * Fetch message data. + * Throws a Horde_Imap_Client_Exception on error. + * + * @param array $criteria The fetch criteria. + * @param array $options Additional options. + * + * @return array See self::fetch(). + */ + protected function _fetch($criteria, $options) + { + // No support for FETCH_MIMEHEADER or FETCH_HEADERS + $nosupport = array(self::FETCH_MIMEHEADER, self::FETCH_HEADERS); + + reset($criteria); + while (list($val,) = each($criteria)) { + if (in_array($val, $nosupport)) { + throw new Horde_Imap_Client_Exception('Fetch criteria provided not supported on POP3 servers.', Horde_Imap_Client_Exception::POP3_NOTSUPPORTED); + } + } + + return parent::_fetch($criteria, $options); + } + + /** + * Store message flag data. + * Throws a Horde_Imap_Client_Exception on error. + * + * @param array $options Additional options. + */ + protected function _store($options) + { + throw new Horde_Imap_Client_Exception('Flagging messages not supported on POP3 servers.', Horde_Imap_Client_Exception::POP3_NOTSUPPORTED); + } + + /** + * Copy messages to another mailbox. + * Throws a Horde_Imap_Client_Exception on error. + * + * @param string $dest The destination mailbox (UTF7-IMAP). + * @param array $options Additional options. + */ + protected function _copy($dest, $options) + { + throw new Horde_Imap_Client_Exception('Copying messages not supported on POP3 servers.', Horde_Imap_Client_Exception::POP3_NOTSUPPORTED); + } + + /** + * Set quota limits. + * Throws a Horde_Imap_Client_Exception on error. + * + * @param string $root The quota root (UTF7-IMAP). + * @param array $options Additional options. + */ + protected function _setQuota($root, $options) + { + throw new Horde_Imap_Client_Exception('IMAP quotas not supported on POP3 servers.', Horde_Imap_Client_Exception::POP3_NOTSUPPORTED); + } + + /** + * Get quota limits. + * Throws a Horde_Imap_Client_Exception on error. + * + * @param string $root The quota root (UTF7-IMAP). + */ + protected function _getQuota($root) + { + throw new Horde_Imap_Client_Exception('IMAP quotas not supported on POP3 servers.', Horde_Imap_Client_Exception::POP3_NOTSUPPORTED); + } + + /** + * Get quota limits for a mailbox. + * Throws a Horde_Imap_Client_Exception on error. + * + * @param string $mailbox A mailbox (UTF7-IMAP). + */ + protected function _getQuotaRoot($mailbox) + { + throw new Horde_Imap_Client_Exception('IMAP quotas not supported on POP3 servers.', Horde_Imap_Client_Exception::POP3_NOTSUPPORTED); + } + + /** + * Set ACL rights for a given mailbox/identifier. + * Throws a Horde_Imap_Client_Exception on error. + * + * @param string $mailbox A mailbox (UTF7-IMAP). + * @param string $identifier The identifier to alter (UTF7-IMAP). + * @param array $options Additional options. + */ + protected function _setACL($mailbox, $identifier, $options) + { + throw new Horde_Imap_Client_Exception('IMAP ACLs not supported on POP3 servers.', Horde_Imap_Client_Exception::POP3_NOTSUPPORTED); + } + + /** + * Get ACL rights for a given mailbox. + * Throws a Horde_Imap_Client_Exception on error. + * + * @param string $mailbox A mailbox (UTF7-IMAP). + */ + protected function _getACL($mailbox) + { + throw new Horde_Imap_Client_Exception('IMAP ACLs not supported on POP3 servers.', Horde_Imap_Client_Exception::POP3_NOTSUPPORTED); + } + + /** + * Get ACL rights for a given mailbox/identifier. + * Throws a Horde_Imap_Client_Exception on error. + * + * @param string $mailbox A mailbox (UTF7-IMAP). + * @param string $identifier The identifier (UTF7-IMAP). + */ + protected function _listACLRights($mailbox, $identifier) + { + throw new Horde_Imap_Client_Exception('IMAP ACLs not supported on POP3 servers.', Horde_Imap_Client_Exception::POP3_NOTSUPPORTED); + } + + /** + * Get the ACL rights for the current user for a given mailbox. + * Throws a Horde_Imap_Client_Exception on error. + * + * @param string $mailbox A mailbox (UTF7-IMAP). + */ + protected function _getMyACLRights($mailbox) + { + throw new Horde_Imap_Client_Exception('IMAP ACLs not supported on POP3 servers.', Horde_Imap_Client_Exception::POP3_NOTSUPPORTED); + } + +} diff --git a/framework/Imap_Client/lib/Horde/Imap/Client/Cclient.php b/framework/Imap_Client/lib/Horde/Imap/Client/Cclient.php new file mode 100644 index 000000000..e01a5d084 --- /dev/null +++ b/framework/Imap_Client/lib/Horde/Imap/Client/Cclient.php @@ -0,0 +1,1655 @@ + + * @category Horde + * @package Horde_Imap_Client + */ +class Horde_Imap_Client_Cclient extends Horde_Imap_Client_Base +{ + /** + * The Horde_Imap_Client_Socket object needed to obtain server info. + * + * @var Horde_Imap_Client_Socket + */ + protected $_socket; + + /** + * The IMAP resource stream. + * + * @var resource + */ + protected $_stream = null; + + /** + * The IMAP c-client connection string. + * + * @var string + */ + protected $_cstring; + + /** + * The service to connect to via c-client + * + * @var string + */ + protected $_service = 'imap'; + + /** + * The IMAP flags supported in this driver. + * + * @var array + */ + protected $_supportedFlags = array( + 'seen', 'answered', 'flagged', 'deleted', 'recent', 'draft' + ); + + /** + * The c-client code -> MIME type conversion table. + * + * @var array + */ + protected $_mimeTypes = array( + TYPETEXT => 'text', + TYPEMULTIPART => 'multipart', + TYPEMESSAGE => 'message', + TYPEAPPLICATION => 'application', + TYPEAUDIO => 'audio', + TYPEIMAGE => 'image', + TYPEVIDEO => 'video', + TYPEMODEL => 'model', + TYPEOTHER => 'other' + ); + + /** + * The c-client code -> MIME encodings conversion table. + * + * @var array + */ + protected $_mimeEncodings = array( + ENC7BIT => '7bit', + ENC8BIT => '8bit', + ENCBINARY => 'binary', + ENCBASE64 => 'base64', + ENCQUOTEDPRINTABLE => 'quoted-printable', + ENCOTHER => 'unknown' + ); + + /** + * Constructs a new Horde_Imap_Client_Cclient object. + * + * @param array $params A hash containing configuration parameters. + */ + public function __construct($params) + { + if (!isset($params['retries'])) { + $params['retries'] = 3; + } + parent::__construct($params); + } + + /** + * Do cleanup prior to serialization and provide a list of variables + * to serialize. + */ + function __sleep() + { + $this->logout(); + parent::__sleep(); + return array_diff(array_keys(get_class_vars(__CLASS__)), array('encryptKey')); + } + + /** + * Get CAPABILITY info from the IMAP server. + * Throws a Horde_Imap_Client_Exception on error. + * + * @return array The capability array. + */ + protected function _capability() + { + $cap = $this->_getSocket()->capability(); + + /* No need to support these extensions here - the wrapping required + * to make this work is probably just as resource intensive as what + * we are trying to avoid. */ + unset($cap['CONDSTORE'], $cap['QRESYNC']); + + return $cap; + } + + /** + * Send a NOOP command. + * Throws a Horde_Imap_Client_Exception on error. + */ + protected function _noop() + { + // Already guaranteed to be logged in here. + + $old_error = error_reporting(0); + $res = imap_ping($this->_stream); + error_reporting($old_error); + + if ($res === false) { + throw new Horde_Imap_Client_Exception('Received error from IMAP server when sending a NOOP command: ' . imap_last_error()); + } + } + + /** + * Get the NAMESPACE information from the IMAP server. + * Throws a Horde_Imap_Client_Exception on error. + * + * @return array An array of namespace information. + */ + protected function _getNamespaces() + { + return $this->_getSocket()->getNamespaces(); + } + + /** + * Return a list of alerts that MUST be presented to the user. + * + * @return array An array of alert messages. + */ + public function alerts() + { + // TODO: check for [ALERT]? + return imap_alerts(); + } + + /** + * Login to the IMAP server. + * Throws a Horde_Imap_Client_Exception on error. + * + * @return boolean Return true if global login tasks should be run. + */ + protected function _login() + { + $i = -1; + $res = false; + + if (!empty($this->_params['secure']) && !extension_loaded('openssl')) { + throw new Horde_Imap_Client_Exception('Secure connections require the PHP openssl extension: http://php.net/openssl.'); + } + + $mask = ($this->_service == 'pop3') ? 0 : OP_HALFOPEN; + + $old_error = error_reporting(0); + if (version_compare(PHP_VERSION, '5.2.1') != -1) { + $res = imap_open($this->_connString(), $this->_params['username'], $this->_params['password'], $mask, $this->_params['retries']); + } else { + while (($res === false) && + !strstr(strtolower(imap_last_error()), 'login failure') && + (++$i < $this->_params['retries'])) { + if ($i != 0) { + sleep(1); + } + $res = imap_open($this->_connString(), $this->_params['username'], $this->_params['password'], $mask); + } + } + error_reporting($old_error); + + if ($res === false) { + throw new Horde_Imap_Client_Exception('Could not authenticate to IMAP server: ' . imap_last_error()); + } + + $this->_stream = $res; + $this->_isSecure = !empty($this->_params['secure']); + + $this->setLanguage(); + + if (!empty($this->_params['timeout'])) { + $timeout = array( + 'open' => IMAP_OPENTIMEOUT, + 'read' => IMAP_READTIMEOUT, + 'write' => IMAP_WRITETIMEOUT, + 'close' => IMAP_CLOSETIMEOUT + ); + + foreach ($timeout as $key => $val) { + if (isset($this->_params['timeout'][$key])) { + imap_timeout($val, $this->_params['timeout'][$key]); + } + } + } + + return true; + } + + /** + * Log out of the IMAP session. + */ + protected function _logout() + { + if (!is_null($this->_stream)) { + imap_close($this->_stream); + $this->_stream = null; + if (isset($this->_socket)) { + $this->_socket->logout(); + } + } + } + + /** + * Send ID information to the IMAP server (RFC 2971). + * Throws a Horde_Imap_Client_Exception on error. + * + * @param array $info The information to send to the server. + */ + protected function _sendID($info) + { + $this->_getSocket()->sendID($info); + } + + /** + * Return ID information from the IMAP server (RFC 2971). + * Throws a Horde_Imap_Client_Exception on error. + * + * @return array An array of information returned, with the keys as the + * 'field' and the values as the 'value'. + */ + protected function _getID() + { + return $this->_getSocket()->getID(); + } + + /** + * Sets the preferred language for server response messages (RFC 5255). + * Throws a Horde_Imap_Client_Exception on error. + * + * @param array $info The preferred list of languages. + * + * @return string The language accepted by the server, or null if the + * default language is used. + */ + protected function _setLanguage($langs) + { + return $this->_getSocket()->setLanguage($langs); + } + + /** + * Gets the preferred language for server response messages (RFC 5255). + * Throws a Horde_Imap_Client_Exception on error. + * + * @param array $list If true, return the list of available languages. + * + * @return mixed If $list is true, the list of languages available on the + * server (may be empty). If false, the language used by + * the server, or null if the default language is used. + */ + protected function _getLanguage($list) + { + return $this->_getSocket()->getLanguage($list); + } + + /** + * Open a mailbox. + * Throws a Horde_Imap_Client_Exception on error. + * + * @param string $mailbox The mailbox to open (UTF7-IMAP). + * @param integer $mode The access mode. + */ + protected function _openMailbox($mailbox, $mode) + { + $this->login(); + $flag = ($mode == self::OPEN_READONLY) ? OP_READONLY : 0; + + $old_error = error_reporting(0); + if (version_compare(PHP_VERSION, '5.2.1') != -1) { + $res = imap_reopen($this->_stream, $this->_connString($mailbox), $flag, $this->_params['retries']); + } else { + $res = imap_reopen($this->_stream, $this->_connString($mailbox), $flag); + } + error_reporting($old_error); + + if ($res === false) { + throw new Horde_Imap_Client_Exception('Could not open mailbox "' . $mailbox . '": ' . imap_last_error()); + } + } + + /** + * Create a mailbox. + * Throws a Horde_Imap_Client_Exception on error. + * + * @param string $mailbox The mailbox to create (UTF7-IMAP). + */ + protected function _createMailbox($mailbox) + { + $this->login(); + + $old_error = error_reporting(0); + $res = imap_createmailbox($this->_stream, $this->_connString($mailbox)); + error_reporting($old_error); + + if ($res === false) { + throw new Horde_Imap_Client_Exception('Could not create mailbox "' . $mailbox . '": ' . imap_last_error()); + } + } + + /** + * Delete a mailbox. + * Throws a Horde_Imap_Client_Exception on error. + * + * @param string $mailbox The mailbox to delete (UTF7-IMAP). + */ + protected function _deleteMailbox($mailbox) + { + $this->login(); + + $old_error = error_reporting(0); + $res = imap_deletemailbox($this->_stream, $this->_connString($mailbox)); + error_reporting($old_error); + + if ($res === false) { + throw new Horde_Imap_Client_Exception('Could not delete mailbox "' . $mailbox . '": ' . imap_last_error()); + } + } + + /** + * Rename a mailbox. + * Throws a Horde_Imap_Client_Exception on error. + * + * @param string $old The old mailbox name (UTF7-IMAP). + * @param string $new The new mailbox name (UTF7-IMAP). + */ + protected function _renameMailbox($old, $new) + { + $this->login(); + + $old_error = error_reporting(0); + $res = imap_renamemailbox($this->_stream, $this->_connString($old), $this->_connString($new)); + error_reporting($old_error); + + if ($res === false) { + throw new Horde_Imap_Client_Exception('Could not rename mailbox "' . $old . '": ' . imap_last_error()); + } + } + + /** + * Manage subscription status for a mailbox. + * Throws a Horde_Imap_Client_Exception on error. + * + * @param string $mailbox The mailbox to [un]subscribe to (UTF7-IMAP). + * @param boolean $subscribe True to subscribe, false to unsubscribe. + */ + protected function _subscribeMailbox($mailbox, $subscribe) + { + $this->login(); + + $old_error = error_reporting(0); + if ($subscribe) { + $res = imap_subscribe($this->_stream, $this->_connString($mailbox)); + } else { + $res = imap_unsubscribe($this->_stream, $this->_connString($mailbox)); + } + error_reporting($old_error); + + if ($res === false) { + throw new Horde_Imap_Client_Exception('Could not ' . ($subscribe ? 'subscribe' : 'unsubscribe') . ' to mailbox "' . $mailbox . '": ' . imap_last_error()); + } + } + + /** + * Obtain a list of mailboxes matching a pattern. + * Throws a Horde_Imap_Client_Exception on error. + * + * @param string $pattern The mailbox search pattern. + * @param integer $mode Which mailboxes to return. + * @param array $options Additional options. + *
+     * For the 'attributes' option, this driver will return only these
+     * attributes:
+     *   '\noinferiors', '\noselect', '\marked', '\unmarked', '\referral',
+     *   '\haschildren', '\hasnochildren'
+     * 
+ * + * @return array See Horde_Imap_Client_Base::listMailboxes(). + */ + protected function _listMailboxes($pattern, $mode, $options) + { + $this->login(); + + switch ($mode) { + case self::MBOX_ALL: + if (!empty($options['flat'])) { + $mboxes = $this->_getMailboxList($pattern, $mode); + return (empty($options['utf8'])) ? $mboxes : array_map(array('Horde_Imap_Client_Utf7imap', 'Utf7ImapToUtf8'), $mboxes); + } + $check = false; + break; + + case self::MBOX_SUBSCRIBED: + case self::MBOX_UNSUBSCRIBED: + $sub = $this->_getMailboxList($pattern, self::MBOX_SUBSCRIBED); + if (!empty($options['flat'])) { + if (!empty($options['utf8'])) { + $sub = array_map(array('Horde_Imap_Client_Utf7imap', 'Utf7ImapToUtf8'), $sub); + } + if ($mode == self::MBOX_SUBSCRIBED) { + return $sub; + } + + $mboxes = $this->_getMailboxList($pattern, self::MBOX_ALL); + if (!empty($options['utf8'])) { + $sub = array_map(array('Horde_Imap_Client_Utf7imap', 'Utf7ImapToUtf8'), $sub); + } + return array_values(array_diff($mboxes, $sub)); + } + $sub = array_flip($sub); + $check = true; + } + + $attr = array( + LATT_NOINFERIORS => '\\noinferiors', + LATT_NOSELECT => '\\noselect', + LATT_MARKED => '\\marked', + LATT_UNMARKED => '\\unmarked', + LATT_REFERRAL => '\\referral', + LATT_HASCHILDREN => '\\haschildren', + LATT_HASNOCHILDREN => '\\hasnochildren' + ); + + $old_error = error_reporting(0); + $res = imap_getmailboxes($this->_stream, $this->_connString(), $pattern); + error_reporting($old_error); + + $mboxes = array(); + while (list(,$val) = each($res)) { + $mbox = substr($val->name, strpos($val->name, '}') + 1); + + if ($check && + ((($mode == self::MBOX_UNSUBSCRIBED) && + isset($sub[$mbox])) || + (($mode == self::MBOX_SUBSCRIBED) && + !isset($sub[$mbox])))) { + continue; + } + + if (!empty($options['utf8'])) { + $mbox = Horde_Imap_Client_Utf7imap::Utf7ImapToUtf8($mbox); + } + + $tmp = array('mailbox' => $mbox); + if (!empty($options['attributes'])) { + $tmp['attributes'] = array(); + foreach ($attr as $k => $a) { + if ($val->attributes & $k) { + $tmp['attributes'][] = $a; + } + } + } + if (!empty($options['delimiter'])) { + $tmp['delimiter'] = $val->delimiter; + } + $mboxes[$mbox] = $tmp; + } + + return $mboxes; + } + + /** + * Obtain a list of mailboxes matching a pattern. + * + * @param string $pattern The mailbox search pattern. + * @param integer $mode Which mailboxes to return. Either + * Horde_Imap_Client::MBOX_SUBSCRIBED or + * Horde_Imap_Client::MBOX_ALL. + * + * @return array A list of mailboxes in UTF7-IMAP format. + */ + protected function _getMailboxList($pattern, $mode) + { + $mboxes = array(); + + $old_error = error_reporting(0); + if ($mode != self::MBOX_ALL) { + $res = imap_list($this->_stream, $this->_connString(), $pattern); + } else { + $res = imap_lsub($this->_stream, $this->_connString(), $pattern); + } + error_reporting($old_error); + + if (is_array($res)) { + while (list(,$val) = each($res)) { + $mboxes[] = substr($val, strpos($val, '}') + 1); + } + } + + return $mboxes; + } + + /** + * Obtain status information for a mailbox. + * Throws a Horde_Imap_Client_Exception on error. + * + * @param string $mailbox The mailbox to query (UTF7-IMAP). + * @param string $flags A bitmask of information requested from the + * server. + * + * @return array See Horde_Imap_Client_Base::status(). + */ + protected function _status($mailbox, $flags) + { + $this->login(); + + /* If FLAGS/PERMFLAGS/HIGHESTMODSEQ/UIDNOTSTICKY are needed, we must + * use the Socket driver. */ + if (($flags & self::STATUS_FLAGS) || + ($flags & self::STATUS_PERMFLAGS) || + ($flags & self::STATUS_HIGHESTMODSEQ) || + ($flags & self::STATUS_UIDNOTSTICKY)) { + return $this->_getSocket()->status($mailbox, $flags); + } + + $items = array( + self::STATUS_MESSAGES => SA_MESSAGES, + self::STATUS_RECENT => SA_RECENT, + self::STATUS_UIDNEXT => SA_UIDNEXT, + self::STATUS_UIDVALIDITY => SA_UIDVALIDITY, + self::STATUS_UNSEEN => SA_UNSEEN + ); + + $c_flag = 0; + $res = null; + + foreach ($items as $key => $val) { + if ($key & $flags) { + $c_flag |= $val; + } + } + + if (!empty($c_flag)) { + $res = imap_status($this->_stream, $this->_connString($mailbox), $c_flag); + if (!is_object($res)) { + $res = null; + } + } + + if ($flags & self::STATUS_FIRSTUNSEEN) { + $search_query = new Horde_Imap_Client_Search_Query(); + $search_query->flag('\\unseen', false); + $search = $this->search($mailbox, $search_query, array('results' => array(self::SORT_RESULTS_MIN), 'sequence' => true)); + + if (is_null($res)) { + return array('firstunseen' => $search['min']); + } + $res->firstunseen = reset($search); + } + + if (is_null($res)) { + return array(); + } else { + unset($res->flags); + return (array)$res; + } + } + + /** + * Append message(s) to a mailbox. + * Throws a Horde_Imap_Client_Exception on error. + * + * @param string $mailbox The mailbox to append the message(s) to + * (UTF7-IMAP). + * @param array $data The message data. + * @param array $options Additional options. + * + * @return mixed Returns true. + */ + protected function _append($mailbox, $data, $options) + { + $this->login(); + + /* This driver does not support flags other than those defined in the + * IMAP4 spec, and does not support 'internaldate'. If either of these + * conditions exist, use the Socket driver instead. */ + while (list(,$val) = each($data)) { + if (isset($val['internaldate']) || + (!empty($val['flags']) && + $this->_nonSupportedFlags($val['flags']))) { + return $this->_getSocket()->append($mailbox, $data); + } + } + + while (list(,$val) = each($data)) { + $old_error = error_reporting(0); + $text = is_resource($val['data']) ? stream_get_contents($val['data']) : $val['data']; + $res = imap_append($this->_stream, $this->_connString($mailbox), $this->removeBareNewlines($text), empty($val['flags']) ? null : implode(' ', $val['flags'])); + error_reporting($old_error); + + if ($res === false) { + if (!empty($options['create'])) { + $this->createMailbox($mailbox); + unset($options['create']); + return $this->_append($mailbox, $data, $options); + } + throw new Horde_Imap_Client_Exception('Could not append message to IMAP server: ' . imap_last_error()); + } + } + + return true; + } + + /** + * Request a checkpoint of the currently selected mailbox. + * Throws a Horde_Imap_Client_Exception on error. + */ + protected function _check() + { + // Already guaranteed to be logged in here. + + $old_error = error_reporting(0); + $res = imap_check($this->_stream); + error_reporting($old_error); + + if ($res === false) { + throw new Horde_Imap_Client_Exception('Received error from IMAP server when sending a CHECK command: ' . imap_last_error()); + } + } + + /** + * Close the connection to the currently selected mailbox, optionally + * expunging all deleted messages (RFC 3501 [6.4.2]). + * Throws a Horde_Imap_Client_Exception on error. + * + * @param array $options Additional options. + */ + protected function _close($options) + { + if (!empty($options['expunge'])) { + $this->expunge($this->_selected); + } + $this->openMailbox($this->_selected, self::OPEN_READONLY); + } + + /** + * Expunge deleted messages from the given mailbox. + * Throws a Horde_Imap_Client_Exception on error. + * + * @param array $options Additional options. + */ + protected function _expunge($options) + { + // Already guaranteed to be logged in here. + + if (empty($options['ids'])) { + $old_error = error_reporting(0); + imap_expunge($this->_stream); + error_reporting($old_error); + return; + } + + $use_seq = !empty($options['sequence']); + + // Need to temporarily unflag all messages marked as deleted but not + // a part of requested UIDs to delete. + $search_query = new Horde_Imap_Client_Search_Query(); + $search_query->flag('\\deleted'); + $ids = $this->search($this->_selected, $search_query, array('sequence' => $use_seq)); + if (!empty($ids['match'])) { + $unflag = array_diff($ids['match'], $options['ids']); + if (!empty($unflag)) { + $this->store($this->_selected, array('ids' => $unflag, 'remove' => array('\\deleted'), 'sequence' => $use_seq)); + } + + /* If we are using a cache, we need to get the list of + * messages that will be expunged. */ + if ($this->_initCacheOb()) { + if ($use_seq) { + $res = $this->search($this->_selected, $search_query); + $expunged = $res['match']; + } else { + $expunged = array_intersect($ids['match'], $options['ids']); + } + + if (!empty($expunged)) { + $this->_cacheOb->deleteMsgs($this->_selected, $expunged); + } + } + } + + $old_error = error_reporting(0); + imap_expunge($this->_stream); + error_reporting($old_error); + + if (!empty($unflag)) { + $this->store($this->_selected, array('add' => array('\\deleted'), 'ids' => $unflag, 'sequence' => $use_seq)); + } + } + + /** + * Search a mailbox. + * Throws a Horde_Imap_Client_Exception on error. + * + * @param object $query The search string. + * @param array $options Additional options. The '_query' key contains + * the value of $query->build(). + * + * @return array An array of UIDs (default) or an array of message + * sequence numbers (if 'sequence' is true). + */ + protected function _search($query, $options) + { + // Already guaranteed to be logged in here. + + /* If more than 1 sort criteria given, or if SORT_REVERSE is given + * as a sort criteria, or search query uses IMAP4 criteria, use the + * Socket client instead. */ + if ($options['_query']['imap4'] || + (!empty($options['sort']) && + ((count($options['sort']) > 1) || + in_array(self::SORT_REVERSE, $options['sort'])))) { + return $this->_getSocket()->search($this->_selected, $query, $options); + } + + $old_error = error_reporting(0); + if (empty($options['sort'])) { + $res = imap_search($this->_stream, $options['_query']['query'], empty($options['sequence']) ? SE_UID : 0, $options['_query']['charset']); + } else { + $sort_criteria = array( + self::SORT_ARRIVAL => SORTARRIVAL, + self::SORT_CC => SORTCC, + self::SORT_DATE => SORTDATE, + self::SORT_FROM => SORTFROM, + self::SORT_SIZE => SORTSIZE, + self::SORT_SUBJECT => SORTSUBJECT, + self::SORT_TO => SORTTO + ); + + $res = imap_sort($this->_stream, $sort_criteria[reset($options['sort'])], 0, empty($options['sequence']) ? SE_UID : 0, $options['_query']['query'], $options['_query']['charset']); + } + $res = ($res === false) ? array() : $res; + error_reporting($old_error); + + $ret = array(); + foreach ($options['results'] as $val) { + switch ($val) { + case self::SORT_RESULTS_COUNT: + $ret['count'] = count($res); + break; + + case self::SORT_RESULTS_MATCH: + $ret[empty($options['sort']) ? 'match' : 'sort'] = $res; + break; + + case self::SORT_RESULTS_MAX: + $ret['max'] = empty($res) ? null : max($res); + break; + + case self::SORT_RESULTS_MIN: + $ret['min'] = empty($res) ? null : min($res); + break; + } + } + + return $ret; + } + + /** + * Set the comparator to use for searching/sorting (RFC 5255). + * Throws a Horde_Imap_Client_Exception on error. + * + * @param string $comparator The comparator string (see RFC 4790 [3.1] - + * "collation-id" - for format). The reserved + * string 'default' can be used to select + * the default comparator. + */ + protected function _setComparator($comparator) + { + return $this->_getSocket()->setComparator($comparator); + } + + /** + * Get the comparator used for searching/sorting (RFC 5255). + * Throws a Horde_Imap_Client_Exception on error. + * + * @return mixed Null if the default comparator is being used, or an + * array of comparator information (see RFC 5255 [4.8]). + */ + protected function _getComparator() + { + return $this->_getSocket()->getComparator(); + } + + /** + * Thread sort a given list of messages (RFC 5256). + * Throws a Horde_Imap_Client_Exception on error. + * + * @param array $options Additional options. + * + * @return array See Horde_Imap_Client_Base::_thread(). + */ + protected function _thread($options) + { + // Already guaranteed to be logged in here + + /* This driver only supports Horde_Imap_Client::THREAD_REFERENCES + * and does not support defining search criteria. */ + if (!empty($options['search']) || + (!empty($options['criteria']) && + $options['criteria'] != self::THREAD_REFERENCES)) { + return $this->_getSocket()->thread($this->_selected, $options); + } + + $use_seq = !empty($options['sequence']); + + $old_error = error_reporting(0); + $ob = imap_thread($this->_stream, $use_seq ? 0 : SE_UID); + error_reporting($old_error); + + if (empty($ob)) { + return array(); + } + + $container = $container_base = $last_index = $thread_base = $thread_base_idx = $uid = null; + $lookup = $ret = array(); + $i = $last_i = $level = 0; + + reset($ob); + while (list($key, $val) = each($ob)) { + $pos = strpos($key, '.'); + $index = substr($key, 0, $pos); + $type = substr($key, $pos + 1); + + switch ($type) { + case 'num': + if ($val === 0) { + $container = $index; + } else { + ++$i; + if (is_null($container) && empty($level)) { + $thread_base = $val; + $thread_base_idx = $index; + } + $lookup[$index] = $use_seq ? $index : $val; + $ret[$val] = array('uid' => $val); + } + break; + + case 'next': + if (!is_null($container) && ($container === $index)) { + $container_base = $val; + } else { + $ret[$lookup[$index]]['base'] = (!is_null($container)) + ? $lookup[$container_base] + : ((!empty($level) || ($val != 0)) ? $lookup[$thread_base_idx] : null); + ++$i; + ++$level; + } + break; + + case 'branch': + if ($container === $index) { + $container = $container_base = null; + $ret[$lookup[$last_index]]['last'] = true; + } else { + $ret[$lookup[$index]]['level'] = $level--; + $ret[$lookup[$index]]['last'] = !(!is_null($container) && empty($level)); + if ($index === $thread_base_idx) { + $index = null; + + } elseif (!empty($level) && + !is_null($last_index) && + isset($ret[$last_index])) { + $ret[$lookup[$last_index]]['last'] = ($last_i == ($i - 1)); + } + } + $last_index = $index; + $last_i = $i++; + break; + } + } + + return $ret; + } + + /** + * Fetch message data. + * Throws a Horde_Imap_Client_Exception on error. + * + * @param array $criteria The fetch criteria. + * @param array $options Additional options. + * + * @return array See self::fetch(). + */ + protected function _fetch($criteria, $options) + { + // Already guaranteed to be logged in here + + $err = false; + $hdrinfo = $overview = null; + + $old_error = error_reporting(0); + + // These options are not supported by this driver. + if (!empty($options['changedsince']) || + (reset($options['ids']) == self::USE_SEARCHRES)) { + return $this->_getSocket()->fetch($this->_selected, $criteria, $options); + } + + if (empty($options['ids'])) { + $seq = '1:*'; + $options['ids'] = range(1, imap_num_msg($this->_stream)); + } else { + $seq = $this->toSequenceString($options['ids']); + } + + $ret = array_combine($options['ids'], array_fill(0, count($options['ids']), array())); + + foreach ($criteria as $type => $c_val) { + if (!is_array($c_val)) { + $c_val = array(); + } + + switch ($type) { + case self::FETCH_STRUCTURE: + // 'noext' has no effect in this driver + foreach ($options['ids'] as $id) { + $structure = imap_fetchstructure($this->_stream, $id, empty($options['sequence']) ? FT_UID : 0); + if (!$structure) { + $err = true; + break 2; + } + $structure = $this->_parseStructure($structure); + $ret[$id]['structure'] = empty($c_val['parse']) ? $structure : Horde_MIME_Message::parseStructure($structure); + } + break; + + case self::FETCH_FULLMSG: + foreach ($options['ids'] as $id) { + $tmp = imap_fetchheader($this->_stream, $id, (empty($options['sequence']) ? FT_UID : 0) | FT_PREFETCHTEXT) . + imap_body($this->_stream, $id, (empty($options['sequence']) ? FT_UID : 0) | (empty($c_val['peek']) ? 0 : FT_PEEK)); + if (isset($c_val['start']) && !empty($c_val['length'])) { + $ret[$id]['fullmsg'] = substr($tmp, $c_val['start'], $c_val['length']); + } else { + $ret[$id]['fullmsg'] = $tmp; + } + } + break; + + case self::FETCH_HEADERTEXT: + case self::FETCH_BODYPART: + foreach ($c_val as $val) { + if ($type == self::FETCH_HEADERTEXT) { + $label = 'headertext'; + /* imap_fetchbody() can return header parts for a + * given MIME part by appending '.0' (or 0 for the + * main header) */ + if (empty($val['id'])) { + $val['id'] = 0; + $body_key = 0; + } else { + $body_key = $val['id'] . '.0'; + } + } else { + $label = 'bodypart'; + if (empty($val['id'])) { + throw new Horde_Imap_Client_Exception('Need a MIME ID when retrieving a MIME body part.'); + } + $body_key = $val['id']; + } + + foreach ($options['ids'] as $id) { + if (!isset($ret[$id][$label])) { + $ret[$id][$label] = array(); + } + $tmp = imap_fetchbody($this->_stream, $id, $header_key, empty($options['sequence']) ? FT_UID : 0); + + if (isset($val['start']) && !empty($val['length'])) { + $tmp = substr($tmp, $val['start'], $val['length']); + } + + if (!empty($val['parse'])) { + $tmp = Horde_MIME_Headers::parseHeaders($tmp); + } + + $ret[$id][$label][$val['id']] = $tmp; + } + } + break; + + case self::FETCH_BODYTEXT: + foreach ($c_val as $val) { + // This is the base body. This is easily obtained via + // imap_body(). + $use_imapbody = empty($val['id']); + + foreach ($options['ids'] as $id) { + if (!isset($ret[$id]['bodytext'])) { + $ret[$id]['bodytext'] = array(); + } + if ($use_imapbody) { + $tmp = imap_body($this->_stream, $id, (empty($options['sequence']) ? FT_UID : 0) | (empty($val['peek']) ? 0 : FT_PEEK)); + if (isset($val['start']) && !empty($val['length'])) { + $ret[$id]['bodytext'][0] = substr($tmp, $val['start'], $val['length']); + } else { + $ret[$id]['bodytext'][0] = $tmp; + } + } else { + /* OY! There is no way to download just the body + * of the message/rfc822 part. The best we can do + * is download the header of the part, determine + * the length, and then remove that info from the + * beginning of the imap_fetchbody() data. */ + $hdr_len = strlen(imap_fetchbody($this->_stream, $id, $val['id'] . '.0', (empty($options['sequence']) ? FT_UID : 0))); + $tmp = substr(imap_fetchbody($this->_stream, $id, $val['id'], (empty($options['sequence']) ? FT_UID : 0)), $hdr_len); + if (isset($val['start']) && !empty($val['length'])) { + $ret[$id]['bodytext'][$val['id']] = substr($tmp, $val['start'], $val['length']); + } else { + $ret[$id]['bodytext'][$val['id']] = $tmp; + } + } + } + } + break; + + case self::FETCH_MIMEHEADER: + case self::FETCH_HEADERS: + case self::FETCH_MODSEQ: + // Can't do it. Nope. Nada. Without heavy duty parsing of the + // full imap_body() object, it is impossible to retrieve the + // MIME headers for each individual part. Ship it off to + // the Socket driver. Adios. + // This goes for header field searches also. + // MODSEQ isn't available in c-client either. + switch ($type) { + case self::FETCH_MIMEHEADER: + $label = 'mimeheader'; + break; + + case self::FETCH_HEADERS: + $label = 'headers'; + break; + + case self::FETCH_MODSEQ: + $label = 'modseq'; + break; + } + $tmp = $this->_getSocket()->fetch($this->_selected, array($type => $c_val), $options); + foreach ($tmp as $id => $id_data) { + if (!isset($ret[$id][$label])) { + $ret[$id][$label] = array(); + } + $ret[$id][$label] = array_merge($ret[$id][$label], $id_data[$label]); + } + break; + + case self::FETCH_ENVELOPE: + if (is_null($hdrinfo)) { + $hdrinfo = array(); + foreach ($options['ids'] as $id) { + $hdrinfo[$id] = imap_headerinfo($this->_stream, empty($options['sequence']) ? imap_msgno($this->_stream, $id) : $id); + if (!$hdrinfo[$id]) { + $err = true; + break 2; + } + } + } + + $env_data = array( + 'date', 'subject', 'from', 'sender', 'reply_to', 'to', + 'cc', 'bcc', 'in_reply_to', 'message_id' + ); + + foreach ($options['ids'] as $id) { + $hptr = &$hdrinfo[$id]; + $ret[$id]['envelope'] = array(); + $ptr = &$ret[$id]['envelope']; + + foreach ($env_data as $e_val) { + $label = strtr($e_val, '_', '-'); + if (isset($hptr->$e_val)) { + if (is_array($hptr->$e_val)) { + $tmp = array(); + foreach ($hptr->$e_val as $a_val) { + $tmp[] = (array)$a_val; + } + $ptr[$label] = $tmp; + } else { + $ptr[$label] = $hptr->$e_val; + } + } else { + $ptr[$label] = null; + } + } + } + break; + + case self::FETCH_FLAGS: + if (is_null($overview)) { + $overview = imap_fetch_overview($this->_stream, $seq, empty($options['sequence']) ? FT_UID : 0); + if (!$overview) { + $err = true; + break 2; + } + } + + foreach ($options['ids'] as $id) { + $tmp = array(); + foreach ($this->_supportedFlags as $f_val) { + if ($overview[$id]->$f_val) { + $tmp[] = '\\' . $f_val; + } + } + $ret[$id]['flags'] = $tmp; + } + break; + + case self::FETCH_DATE: + if (is_null($hdrinfo)) { + $hdrinfo = array(); + foreach ($options['ids'] as $id) { + $hdrinfo[$id] = imap_headerinfo($this->_stream, empty($options['sequence']) ? imap_msgno($this->_stream, $id) : $id); + if (!$hdrinfo[$id]) { + $err = true; + break 2; + } + } + } + + foreach ($options['ids'] as $id) { + $ret[$id]['date'] = new DateTime($hdrinfo[$id]->MailDate); + } + break; + + case self::FETCH_SIZE: + if (!is_null($hdrinfo)) { + foreach ($options['ids'] as $id) { + $ret[$id]['size'] = $hdrinfo[$id]->Size; + } + } else { + if (is_null($overview)) { + $overview = imap_fetch_overview($this->_stream, $seq, empty($options['sequence']) ? FT_UID : 0); + if (!$overview) { + $err = true; + break; + } + } + foreach ($options['ids'] as $id) { + $ret[$id]['size'] = $overview[$id]->size; + } + } + break; + + case self::FETCH_UID: + if (empty($options['sequence'])) { + foreach ($options['ids'] as $id) { + $ret[$id]['uid'] = $id; + } + } else { + if (is_null($overview)) { + $overview = imap_fetch_overview($this->_stream, $seq, empty($options['sequence']) ? FT_UID : 0); + if (!$overview) { + $err = true; + break; + } + } + foreach ($options['ids'] as $id) { + $ret[$id]['uid'] = $overview[$id]->uid; + } + } + break; + + case self::FETCH_SEQ: + if (!empty($options['sequence'])) { + foreach ($options['ids'] as $id) { + $ret[$id]['seq'] = $id; + } + } else { + if (is_null($overview)) { + $overview = imap_fetch_overview($this->_stream, $seq, empty($options['sequence']) ? FT_UID : 0); + if (!$overview) { + $err = true; + break; + } + } + foreach ($options['ids'] as $id) { + $ret[$id]['uid'] = $overview[$id]->msgno; + } + } + } + } + error_reporting($old_error); + + if ($err) { + throw new Horde_Imap_Client_Exception('Error when fetching messages: ' . imap_last_error()); + } + + return $ret; + } + + /** + * Parse the output from imap_fetchstructure() in the format that + * this class returns structure data in. + * + * @param object $data Data from imap_fetchstructure(). + * + * @return array See self::fetch() for structure return format. + */ + protected function _parseStructure($data) + { + // Required entries + $ret = array( + 'type' => $this->_mimeTypes[$data->type], + 'subtype' => $data->ifsubtype ? strtolower($data->subtype) : 'x-unknown' + ); + + // Optional for multipart-parts, required for all others + if ($data->ifparameters) { + $ret['parameters'] = array(); + foreach ($data->parameters as $val) { + $ret['parameters'][$val->attribute] = $val->value; + } + } + + // Optional entries. 'location' and 'language' not supported + if ($data->ifdisposition) { + $ret['disposition'] = $data->disposition; + if ($data->ifdparameters) { + $ret['dparameters'] = array(); + foreach ($data->dparameters as $val) { + $ret['dparameters'][$val->attribute] = $val->value; + } + } + } + + if ($ret['type'] == 'multipart') { + // multipart/* specific entries + $ret['parts'] = array(); + foreach ($data->parts as $val) { + $ret['parts'][] = $this->_parseStructure($val); + } + } else { + // Required options + $ret['id'] = $data->ifid ? $data->id : null; + $ret['description'] = $data->ifdescription ? $data->description : null; + $ret['encoding'] = $this->_mimeEncodings[$data->encoding]; + $ret['size'] = $data->bytes; + + // Part specific options + if (($ret['type'] == 'message') && ($ret['subtype'] == 'rfc822')) { + // @todo - Doesn't seem to be an easy way to obtain the + // envelope information for this part. + $ret['envelope'] = array(); + $ret['structure'] = $this->_parseStructure(reset($data->parts)); + $ret['lines'] = $data->lines; + } elseif ($ret['type'] == 'text') { + $ret['lines'] = $data->lines; + } + + // No support for 'md5' option + } + + return $ret; + } + + /** + * Store message flag data. + * Throws a Horde_Imap_Client_Exception on error. + * + * @param array $options Additional options. + */ + protected function _store($options) + { + // Already guaranteed to be logged in here + + /* This driver does not support flags other than those defined in the + * IMAP4 spec. If other flags exist, need to use the Socket driver + * instead. */ + foreach (array('add', 'remove') as $val) { + if (!empty($options[$val]) && + $this->_nonSupportedFlags($options[$val])) { + return $this->_getSocket()->store($this->_selected, $options); + } + } + + // This driver does not support the 'unchangedsince' or 'replace' + // options, nor does it support using stored searches. + if (!empty($options['unchangedsince']) || + !empty($options['replace']) || + (reset($options['ids']) == self::USE_SEARCHRES)) { + // Requires Socket driver. + return $this->_getSocket()->store($this->_selected, $options); + } + + $seq = empty($options['ids']) + ? '1:*' + : $this->toSequenceString($options['ids']); + + $old_error = error_reporting(0); + + if (!empty($options['add'])) { + $res = imap_setflag_full($this->_stream, $seq, implode(' ', $options['add']), empty($options['sequence']) ? ST_UID : 0); + } + + if (($res === true) && !empty($options['remove'])) { + $res = imap_clearflag_full($this->_stream, $seq, implode(' ', $options['remove']), empty($options['sequence']) ? ST_UID : 0); + } + + error_reporting($old_error); + + if ($res === false) { + throw new Horde_Imap_Client_Exception('Error when flagging messages: ' . imap_last_error()); + } + + return array(); + } + + /** + * Copy messages to another mailbox. + * Throws a Horde_Imap_Client_Exception on error. + * + * @param string $dest The destination mailbox (UTF7-IMAP). + * @param array $options Additional options. + * + * @return boolean True on success (this driver does not support + * returning the UIDs). + */ + protected function _copy($dest, $options) + { + // Already guaranteed to be logged in here + + $opts = 0; + if (empty($options['sequence'])) { + $opts |= CP_UID; + } + if (!empty($options['move'])) { + $opts |= CP_MOVE; + } + + if (reset($options['ids']) == self::USE_SEARCHRES) { + // Requires Socket driver. + return $this->_getSocket()->copy($this->_selected, $options); + } + + $seq = empty($options['ids']) + ? '1:*' + : $this->toSequenceString($options['ids']); + + $old_error = error_reporting(0); + $res = imap_mail_copy($this->_stream, $seq, $this->_connString($dest), $opts); + error_reporting($old_error); + + if ($res === false) { + if (!empty($options['create'])) { + $this->createMailbox($dest); + unset($options['create']); + return $this->copy($dest, $options); + } + throw new Horde_Imap_Client_Exception('Error when copying/moving messages: ' . imap_last_error()); + } + + return true; + } + + /** + * Set quota limits. + * Throws a Horde_Imap_Client_Exception on error. + * + * @param string $root The quota root (UTF7-IMAP). + * @param array $options Additional options. + */ + protected function _setQuota($root, $options) + { + // This driver only supports setting the 'STORAGE' quota. + if (isset($options['messages'])) { + $this->_getSocket()->setQuota($root, $options); + return; + } + + $this->login(); + + $old_error = error_reporting(0); + $res = imap_set_quota($this->_stream, $root, $options['storage']); + error_reporting($old_error); + + if ($res === false) { + throw new Horde_Imap_Client_Exception('Error when setting quota: ' . imap_last_error()); + } + } + + /** + * Get quota limits. + * Throws a Horde_Imap_Client_Exception on error. + * + * @param string $root The quota root (UTF7-IMAP). + * + * @return mixed An array with these possible keys: 'messages' and + * 'storage'; each key holds an array with 2 values: + * 'limit' and 'usage'. + */ + protected function _getQuota($root) + { + $this->login(); + + $old_error = error_reporting(0); + $res = imap_get_quota($this->_stream, $root); + error_reporting($old_error); + + if ($res === false) { + throw new Horde_Imap_Client_Exception('Error when retrieving quota: ' . imap_last_error()); + } + } + + /** + * Get quota limits for a mailbox. + * Throws a Horde_Imap_Client_Exception on error. + * + * @param string $mailbox A mailbox (UTF7-IMAP). + * + * @return mixed An array with the keys being the quota roots. Each key + * holds an array with two possible keys: 'messages' and + * 'storage'; each of these keys holds an array with 2 + * values: 'limit' and 'usage'. + */ + protected function _getQuotaRoot($mailbox) + { + $this->login(); + + $old_error = error_reporting(0); + $res = imap_get_quotaroot($this->_stream, $mailbox); + error_reporting($old_error); + + if ($res === false) { + throw new Horde_Imap_Client_Exception('Error when retrieving quotaroot: ' . imap_last_error()); + } + + return array($mailbox => $ret); + } + + /** + * Set ACL rights for a given mailbox/identifier. + * Throws a Horde_Imap_Client_Exception on error. + * + * @param string $mailbox A mailbox (UTF7-IMAP). + * @param string $identifier The identifier to alter (UTF7-IMAP). + * @param array $options Additional options. + */ + protected function _setACL($mailbox, $identifier, $options) + { + $this->login(); + + if (empty($options['rights']) && !empty($options['remove'])) { + $acl = $this->listACLRights($mailbox, $identifier); + if (empty($acl['rights'])) { + return; + } + $options['rights'] = $acl['rights']; + $options['remove'] = true; + } + + if (empty($options['rights'])) { + return; + } + + $old_error = error_reporting(0); + $res = imap_setacl($this->_stream, $mailbox, $identifier, (empty($options['remove']) ? '+' : '-') . $implode('', $options['rights'])); + error_reporting($old_error); + + if ($res === false) { + throw new Horde_Imap_Client_Exception('Error when setting ACL: ' . imap_last_error()); + } + } + + /** + * Get ACL rights for a given mailbox. + * Throws a Horde_Imap_Client_Exception on error. + * + * @param string $mailbox A mailbox (UTF7-IMAP). + * + * @return array An array with identifiers as the keys and an array of + * rights as the values. + */ + protected function _getACL($mailbox) + { + $this->login(); + + $acl = array(); + + $old_error = error_reporting(0); + $res = imap_getacl($this->_stream, $mailbox); + error_reporting($old_error); + + if ($res === false) { + throw new Horde_Imap_Client_Exception('Error when retrieving ACLs: ' . imap_last_error()); + } + + foreach ($res as $id => $rights) { + $acl[$id] = array(); + for ($i = 0, $iMax = strlen($rights); $i < $iMax; ++$i) { + $acl[$id][] = $rights[$i]; + } + } + + return $acl; + } + + /** + * Get ACL rights for a given mailbox/identifier. + * Throws a Horde_Imap_Client_Exception on error. + * + * @param string $mailbox A mailbox (UTF7-IMAP). + * @param string $identifier The identifier (UTF-8). + * + * @return array An array of rights (keys: 'required' and 'optional'). + */ + protected function _listACLRights($mailbox, $identifier) + { + $acl = $this->getACL($mailbox); + // @todo - Does this return 'optional' information? + return isset($acl[$identifier]) ? $acl[$identifier] : array(); + } + + /** + * Get the ACL rights for the current user for a given mailbox. + * Throws a Horde_Imap_Client_Exception on error. + * + * @param string $mailbox A mailbox (UTF7-IMAP). + * + * @return array An array of rights. + */ + protected function _getMyACLRights($mailbox) + { + // No support in c-client for MYRIGHTS - need to call Socket driver + return $this->_getSocket()->getMyACLRights($mailbox); + } + + /* Internal functions */ + + /** + * Create a Horde_Imap_Client_Socket instance pre-filled with this client's + * parameters. + * + * @return Horde_Imap_Client_Socket The socket instance. + */ + protected function _getSocket() + { + if (!isset($this->_socket)) { + $this->_socket = $this->getInstance('Socket', $this->_params); + } + return $this->_socket; + } + + /** + * Generate the c-client connection string. + * + * @param string $mailbox The mailbox to add to the connection string. + * + * @return string The connection string. + */ + protected function _connString($mailbox = '') + { + if (isset($this->_cstring)) { + return $this->_cstring . $mailbox; + } + + $conn = '{' . $this->_params['hostspec'] . ':' . $this->_params['port'] . '/service=' . $this->_service; + + switch ($this->_params['secure']) { + case 'ssl': + $conn .= '/ssl'; + if (empty($this->_params['validate_cert'])) { + $conn .= '/novalidate-cert'; + } + break; + + case 'tls': + $conn .= '/tls'; + if (empty($this->_params['validate_cert'])) { + $conn .= '/novalidate-cert'; + } + break; + + default: + $conn .= '/notls'; + break; + } + $this->_cstring = $conn . '}'; + + return $this->_cstring . $mailbox; + } + + /** + * Checks a flag list for non-supported c-client flags. + * + * @param array $flags The list of flags. + * + * @return boolean True if there is a non-supported flag in $flags. + */ + protected function _nonSupportedFlags($flags) + { + // This driver does not support flags other than 'Seen', 'Answered', + // 'Flagged', 'Deleted', 'Recent', and 'Draft'. + foreach (array_map('strtolower', $flags) as $val) { + if (!in_array($val, $this->_supportedFlags)) { + return true; + } + } + + return false; + } + +} diff --git a/framework/Imap_Client/lib/Horde/Imap/Client/Exception.php b/framework/Imap_Client/lib/Horde/Imap/Client/Exception.php new file mode 100644 index 000000000..f7f1858e5 --- /dev/null +++ b/framework/Imap_Client/lib/Horde/Imap/Client/Exception.php @@ -0,0 +1,57 @@ + + * @category Horde + * @package Horde_Imap_Client + */ +class Horde_Imap_Client_Exception extends Exception +{ + /* Error message codes. */ + // Unspecified error (default) + const UNSPECIFIED = 0; + + // The given Horde_Imap_Client driver does not exist on the system. + const DRIVER_NOT_FOUND = 1; + + // The function called is not supported in POP3. + const POP3_NOTSUPPORTED = 2; + + // There was an unrecoverable error in UTF7IMAP -> UTF8 conversion. + const UTF7IMAP_CONVERSION = 3; + + // The IMAP server sent ended the connection. + const IMAP_DISCONNECT = 4; + + // The charset used in the search query is not supported on the server. + const BADCHARSET = 5; + + // There were errors parsing the MIME/RFC 2822 header of the part. + const PARSEERROR = 6; + + // The server could not decode the MIME part (see RFC 3516) + const UNKNOWNCTE = 7; + + // The server does not support the IMAP extensions needed for this + // operation + const NOSUPPORTIMAPEXT = 8; + + // The comparator specified by setComparator() was not recognized by the + // IMAP server + const BADCOMPARATOR = 9; + + // RFC 4551 [3.1.2] - All mailboxes are not required to support + // mod-sequences. + const MBOXNOMODSEQ = 10; + + // Thrown if the cache has become invalid. + const CACHEUIDINVALID = 11; +} diff --git a/framework/Imap_Client/lib/Horde/Imap/Client/Socket.php b/framework/Imap_Client/lib/Horde/Imap/Client/Socket.php new file mode 100644 index 000000000..91ce93231 --- /dev/null +++ b/framework/Imap_Client/lib/Horde/Imap/Client/Socket.php @@ -0,0 +1,3542 @@ + + * @category Horde + * @package Horde_Imap_Client + */ +class Horde_Imap_Client_Socket extends Horde_Imap_Client_Base +{ + /** + * The unique tag to use when making an IMAP query. + * + * @var integer + */ + protected $_tag = 0; + + /** + * The socket connection to the IMAP server. + * + * @var resource + */ + protected $_stream = null; + + /** + * Temp array (destroyed at end of process). + * + * @var array + */ + protected $_temp = array(); + + /** + * Destructor. + */ + public function __destruct() + { + $this->_temp['logout'] = 2; + $this->logout(); + parent::__destruct(); + } + + /** + * Do cleanup prior to serialization and provide a list of variables + * to serialize. + */ + function __sleep() + { + $this->_temp['logout'] = 2; + $this->logout(); + $this->_temp = array(); + $this->_tag = 0; + parent::__sleep(); + return array_diff(array_keys(get_class_vars(__CLASS__)), array('encryptKey')); + } + + /** + * Get CAPABILITY info from the IMAP server. + * Throws a Horde_Imap_Client_Exception on error. + * + * @return array The capability array. + */ + protected function _capability() + { + // Need to use connect call here or else we run into loop issues + // because _connect() can call capability() internally. + $this->_connect(); + + // It is possible the server provided capability information on + // connect, so check for it now. + if (!isset($this->_init['capability'])) { + $this->_sendLine('CAPABILITY'); + } + + return $this->_init['capability']; + } + + /** + * Parse a CAPABILITY Response (RFC 3501 [7.2.1]). + * + * @param array $data The CAPABILITY data. + */ + protected function _parseCapability($data) + { + $c = &$this->_init['capability']; + $c = array(); + + foreach ($data as $val) { + $cap_list = explode('=', $val); + $cap_list[0] = strtoupper($cap_list[0]); + if (isset($cap_list[1])) { + if (!isset($c[$cap_list[0]]) || !is_array($c[$cap_list[0]])) { + $c[$cap_list[0]] = array(); + } + $c[$cap_list[0]][] = $cap_list[1]; + } elseif (!isset($c[$cap_list[0]])) { + $c[$cap_list[0]] = true; + } + } + + /* RFC 5162 [1] - QRESYNC implies CONDSTORE, even if CONDSTORE is not + * listed as a capability. */ + if (isset($c['QRESYNC'])) { + $c['CONDSTORE'] = true; + } + + if (!empty($this->_temp['in_login'])) { + $this->_temp['logincapset'] = true; + } + } + + /** + * Send a NOOP command. + * Throws a Horde_Imap_Client_Exception on error. + */ + protected function _noop() + { + // NOOP doesn't return any specific response + $this->_sendLine('NOOP'); + } + + /** + * Get the NAMESPACE information from the IMAP server (RFC 2342). + * Throws a Horde_Imap_Client_Exception on error. + * + * @return array An array of namespace information. + */ + protected function _getNamespaces() + { + $this->login(); + + if ($this->queryCapability('NAMESPACE')) { + $this->_sendLine('NAMESPACE'); + return $this->_temp['namespace']; + } + + return array(); + } + + /** + * Parse a NAMESPACE response (RFC 2342 [5] & RFC 5255 [3.4]). + * + * @param array $data The NAMESPACE data. + */ + protected function _parseNamespace($data) + { + $namespace_array = array( + 0 => 'personal', + 1 => 'other', + 2 => 'shared' + ); + + $c = &$this->_temp['namespace']; + $c = array(); + $lang = $this->queryCapability('LANGUAGE'); + + // Per RFC 2342, response from NAMESPACE command is: + // (PERSONAL NAMESPACES) (OTHER_USERS NAMESPACE) (SHARED NAMESPACES) + foreach ($namespace_array as $i => $val) { + if (!is_array($data[$i]) && (strtoupper($data[$i]) == 'NIL')) { + continue; + } + reset($data[$i]); + while (list(,$v) = each($data[$i])) { + $c[$v[0]] = array( + 'name' => $v[0], + 'delimiter' => $v[1], + 'type' => $val, + 'hidden' => false + ); + // RFC 5255 [3.4] - TRANSLATION extension + if ($lang && (strtoupper($v[2] == 'TRANSLATION'))) { + $c[$v[0]]['translation'] = reset($v[3]); + } + } + } + } + + /** + * Return a list of alerts that MUST be presented to the user. + * + * @return array An array of alert messages. + */ + public function alerts() + { + return empty($this->_temp['alerts']) ? array() : $this->_temp['alerts']; + } + + /** + * Login to the IMAP server. + * Throws a Horde_Imap_Client_Exception on error. + * + * @return boolean Return true if global login tasks should be run. + */ + protected function _login() + { + if (!empty($this->_temp['preauth'])) { + return $this->_loginTasks(); + } + + $this->_connect(); + + $t = &$this->_temp; + + // Switch to secure channel if using TLS. + if (!$this->_isSecure && + ($this->_params['secure'] == 'tls')) { + if (!$this->queryCapability('STARTTLS')) { + // We should never hit this - STARTTLS is required pursuant + // to RFC 3501 [6.2.1]. + throw new Horde_Imap_Client_Exception('Server does not support TLS connections.', Horde_Imap_Client_Exception::NOSUPPORTIMAPEXT); + } + + // Switch over to a TLS connection. + // STARTTLS returns no untagged response. + $this->_sendLine('STARTTLS'); + + $old_error = error_reporting(0); + $res = stream_socket_enable_crypto($this->_stream, true, STREAM_CRYPTO_METHOD_TLS_CLIENT); + error_reporting($old_error); + + if (!$res) { + $this->logout(); + throw new Horde_Imap_Client_Exception('Could not open secure TLS connection to the IMAP server.'); + } + + // Expire cached CAPABILITY information (RFC 3501 [6.2.1]) + unset($this->_init['capability']); + + // Reset language (RFC 5255 [3.1]) + unset($this->_init['lang']); + + // Set language if not using imapproxy + if ($this->_init['imapproxy']) { + $this->setLanguage(); + } + + $this->_isSecure = true; + } + + if (empty($this->_init['authmethod'])) { + $first_login = true; + $imap_auth_mech = array(); + + $auth_methods = $this->queryCapability('AUTH'); + if (!empty($auth_methods)) { + // Add SASL methods. + $imap_auth_mech = array_intersect(array('DIGEST-MD5', 'CRAM-MD5'), $auth_methods); + + // Next, try 'PLAIN' authentication. + if (in_array('PLAIN', $auth_methods)) { + $imap_auth_mech[] = 'PLAIN'; + } + } + + // Fall back to 'LOGIN' if available. + if (!$this->queryCapability('LOGINDISABLED')) { + $imap_auth_mech[] = 'LOGIN'; + } + + if (empty($imap_auth_mech)) { + throw new Horde_Imap_Client_Exception('No supported IMAP authentication method could be found.'); + } + + /* Use MD5 authentication first, if available. But no need to use + * use special authentication if we are already using an + * encrypted connection. */ + if ($this->_isSecure) { + $imap_auth_mech = array_reverse($imap_auth_mech); + } + } else { + $first_login = false; + $imap_auth_mech = array($this->_init['authmethod']); + } + + foreach ($imap_auth_mech as $method) { + $t['referral'] = null; + + /* Set a flag indicating whether we have received a CAPABILITY + * response after we successfully login. Since capabilities may + * be different after login, this is the value we should end up + * caching if the object is eventually serialized. */ + $this->_temp['in_login'] = true; + + try { + $this->_tryLogin($method); + $success = true; + $this->_init['authmethod'] = $method; + unset($t['referralcount']); + } catch (Horde_Imap_Client_Exception $e) { + $success = false; + if (!empty($this->_init['authmethod'])) { + unset($this->_init['authmethod']); + return $this->login(); + } + } + + unset($this->_temp['in_login']); + + // Check for login referral (RFC 2221) response - can happen for + // an OK, NO, or BYE response. + if (!is_null($t['referral'])) { + foreach (array('hostspec', 'port', 'username') as $val) { + if (isset($t['referral'][$val])) { + $this->_params[$val] = $t['referral'][$val]; + } + } + + if (isset($t['referral']['auth'])) { + $this->_init['authmethod'] = $t['referral']['auth']; + } + + if (!isset($t['referralcount'])) { + $t['referralcount'] = 0; + } + + // RFC 2221 [3] - Don't follow more than 10 levels of referral + // without consulting the user. + if (++$t['referralcount'] < 10) { + $this->logout(); + unset($this->_init['capability']); + $this->_init['namespace'] = array(); + return $this->login(); + } + + unset($t['referralcount']); + } + + if ($success) { + return $this->_loginTasks($first_login); + } + } + + throw new Horde_Imap_Client_Exception('IMAP server denied authentication.'); + } + + /** + * Connects to the IMAP server. + * Throws a Horde_Imap_Client_Exception on error. + */ + protected function _connect() + { + if (!is_null($this->_stream)) { + return; + } + + if (!empty($this->_params['secure']) && !extension_loaded('openssl')) { + throw new Horde_Imap_Client_Exception('Secure connections require the PHP openssl extension.'); + } + + switch ($this->_params['secure']) { + case 'ssl': + $conn = 'ssl://'; + $this->_isSecure = true; + break; + + case 'tls': + default: + $conn = 'tcp://'; + break; + } + + $old_error = error_reporting(0); + $this->_stream = stream_socket_client($conn . $this->_params['hostspec'] . ':' . $this->_params['port'], $error_number, $error_string, $this->_params['timeout']); + error_reporting($old_error); + + if ($this->_stream === false) { + $this->_stream = null; + $this->_isSecure = false; + throw new Horde_Imap_Client_Exception('Error connecting to IMAP server: [' . $error_number . '] ' . $error_string); + } + + stream_set_timeout($this->_stream, $this->_params['timeout']); + + // Get greeting information. This is untagged so we need to specially + // deal with it here. A BYE response will be caught and thrown in + // _getLine(). + $ob = $this->_getLine(); + switch ($ob['response']) { + case 'BAD': + // Server is rejecting our connection. + throw new Horde_Imap_Client_Exception('Server rejected connection: ' . $ob['line']); + + case 'PREAUTH': + // The user was pre-authenticated. + $this->_temp['preauth'] = true; + break; + + default: + $this->_temp['preauth'] = false; + break; + } + $this->_parseServerResponse($ob); + + // Check for IMAP4rev1 support + if (!$this->queryCapability('IMAP4REV1')) { + throw new Horde_Imap_Client_Exception('This server does not support IMAP4rev1 (RFC 3501).'); + } + + // Set language if not using imapproxy + if (empty($this->_init['imapproxy'])) { + $this->_init['imapproxy'] = $this->queryCapability('XIMAPPROXY'); + if (!$this->_init['imapproxy']) { + $this->setLanguage(); + } + } + + // If pre-authenticated, we need to do all login tasks now. + if ($this->_temp['preauth']) { + $this->login(); + } + } + + /** + * Authenticate to the IMAP server. + * Throws a Horde_Imap_Client_Exception on error. + * + * @param string $method IMAP login method. + */ + protected function _tryLogin($method) + { + switch ($method) { + case 'CRAM-MD5': + case 'DIGEST-MD5': + $this->_sendLine('AUTHENTICATE ' . $method); + + switch ($method) { + case 'CRAM-MD5': + // RFC 2195 + $auth_sasl = Auth_SASL::factory('crammd5'); + $response = base64_encode($auth_sasl->getResponse($this->_params['username'], $this->_params['password'], base64_decode($ob['line']))); + $this->_sendLine($response, array('debug' => '[CRAM-MD5 Response]', 'notag' => true)); + break; + + case 'DIGEST-MD5': + $auth_sasl = Auth_SASL::factory('digestmd5'); + $response = base64_encode($auth_sasl->getResponse($this->_params['username'], $this->_params['password'], base64_decode($ob['line']), $this->_params['hostspec'], 'imap')); + $ob = $this->_sendLine($response, array('debug' => '[DIGEST-MD5 Response]', 'noparse' => true, 'notag' => true)); + $response = base64_decode($ob['line']); + if (strpos($response, 'rspauth=') === false) { + throw new Horde_Imap_Client_Exception('Unexpected response from server to Digest-MD5 response.'); + } + $this->_sendLine('', array('notag' => true)); + break; + } + break; + + case 'LOGIN': + $this->_sendLine('LOGIN ' . $this->escape($this->_params['username']) . ' ' . $this->escape($this->_params['password']), array('debug' => '[LOGIN Command]')); + break; + + case 'PLAIN': + // RFC 2595/4616 - PLAIN SASL mechanism + $auth = base64_encode(implode("\0", array($this->_params['username'], $this->_params['username'], $this->_params['password']))); + if ($this->queryCapability('SASL-IR')) { + // IMAP Extension for SASL Initial Client Response (RFC 4959) + $this->_sendLine('AUTHENTICATE PLAIN ' . $auth, array('debug' => '[SASL-IR AUTHENTICATE Command]')); + } else { + $this->_sendLine('AUTHENTICATE PLAIN'); + $this->_sendLine($auth, array('debug' => '[AUTHENTICATE Command]', 'notag' => true)); + } + break; + } + } + + /** + * Perform login tasks. + * + * @param boolean $firstlogin Is this the first login? + * + * @return boolean True if global login tasks should be performed. + */ + protected function _loginTasks($firstlogin = true) + { + /* If reusing an imapproxy connection, no need to do any of these + * login tasks again. */ + if (!$firstlogin && !empty($this->_temp['proxyreuse'])) { + // If we have not yet set the language, set it now. + if (!isset($this->_init['lang'])) { + $this->setLanguage(); + } + return false; + } + + $this->_init['enabled'] = array(); + + /* If we logged in for first time, and server did not return + * capability information, we need to grab it now. */ + if ($firstlogin && empty($this->_temp['logincapset'])) { + unset($this->_init['capability']); + } + $this->setLanguage(); + + /* Only active QRESYNC/CONDSTORE if caching is enabled. */ + if ($this->_initCacheOb()) { + if ($this->queryCapability('QRESYNC')) { + /* QRESYNC REQUIRES ENABLE, so we just need to send one ENABLE + * QRESYNC call to enable both QRESYNC && CONDSTORE. */ + $this->_enable(array('QRESYNC')); + $this->_init['enabled']['CONDSTORE'] = true; + } elseif ($this->queryCapability('CONDSTORE')) { + /* CONDSTORE may be available, but ENABLE may not be. */ + if ($this->queryCapability('ENABLE')) { + $this->_enable(array('CONDSTORE')); + } + } + } + + return true; + } + + /** + * Log out of the IMAP session. + */ + protected function _logout() + { + if (!is_null($this->_stream)) { + /* $_temp['logout'] = 1 -- do explicit LOGOUT + * $_temp['logout'] = 2 -- immediately close connection. */ + if (empty($this->_temp['logout']) || + ($this->_temp['logout'] != 2)) { + $this->_temp['logout'] = 1; + try { + $this->_sendLine('LOGOUT'); + } catch (Horde_Imap_Client_Exception $e) {} + } + unset($this->_temp['logout']); + fclose($this->_stream); + $this->_stream = null; + } + } + + /** + * Send ID information to the IMAP server (RFC 2971). + * Throws a Horde_Imap_Client_Exception on error. + * + * @param array $info The information to send to the server. + */ + protected function _sendID($info) + { + if (empty($info)) { + $cmd = 'NIL'; + } else { + $cmd = '('; + foreach ($info as $key => $val) { + $cmd .= $this->escape(strtolower($key)) . ' ' . $this->escape($val); + } + $cmd .= ')'; + } + + $this->_sendLine('ID ' . $cmd); + } + + /** + * Parse an ID response (RFC 2971 [3.2]) + * + * @param array $data The server response. + */ + protected function _parseID($data) + { + $this->_temp['id'] = array(); + $d = reset($data); + if (is_array($d)) { + for ($i = 0, $cnt = count($d); $i < $cnt; $i += 2) { + $this->_temp['id'][$d[$i]] = $d[$i + 1]; + } + } + } + + /** + * Return ID information from the IMAP server (RFC 2971). + * Throws a Horde_Imap_Client_Exception on error. + * + * @return array An array of information returned, with the keys as the + * 'field' and the values as the 'value'. + */ + protected function _getID() + { + if (!isset($this->_temp['id'])) { + $this->sendID(); + } + return $this->_temp['id']; + } + + /** + * Sets the preferred language for server response messages (RFC 5255). + * Throws a Horde_Imap_Client_Exception on error. + * + * @param array $info The preferred list of languages. + * + * @return string The language accepted by the server, or null if the + * default language is used. + */ + protected function _setLanguage($langs) + { + $cmd = array(); + foreach ($langs as $val) { + $cmd[] = $this->escape($val); + } + + try { + $this->_sendLine('LANGUAGE ' . implode(' ', $cmd)); + } catch (Horde_Imap_Client_Exception $e) { + $this->_init['lang'] = null; + return null; + } + + return $this->_init['lang']; + } + + /** + * Gets the preferred language for server response messages (RFC 5255). + * Throws a Horde_Imap_Client_Exception on error. + * + * @param array $list If true, return the list of available languages. + * + * @return mixed If $list is true, the list of languages available on the + * server (may be empty). If false, the language used by + * the server, or null if the default language is used. + */ + protected function _getLanguage($list) + { + if (!$list) { + return empty($this->_init['lang']) ? null : $this->_init['lang']; + } + + if (!isset($this->_init['langavail'])) { + try { + $this->_sendLine('LANGUAGE'); + } catch (Horde_Imap_Client_Exception $e) { + $this->_init['langavail'] = array(); + } + } + + return $this->_init['langavail']; + } + + /** + * Parse a LANGUAGE response (RFC 5255 [3.3]) + * + * @param array $data The server response. + */ + protected function _parseLanguage($data) + { + // Store data in $_params because it mustbe saved across page accesses + if (count($data[0]) == 1) { + // This is the language that was set. + $this->_init['lang'] = reset($data[0]); + } else { + // These are the languages that are available. + $this->_init['langavail'] = $data[0]; + } + } + + /** + * Enable an IMAP extension (see RFC 5161). + * Throws a Horde_Imap_Client_Exception on error. + * + * @param array $exts The extensions to enable. + */ + protected function _enable($exts) + { + // Only enable non-enabled extensions + $exts = array_diff($exts, array_keys($this->_init['enabled'])); + if (!empty($exts)) { + $this->_sendLine('ENABLE ' . implode(' ', array_map('strtoupper', $exts))); + } + } + + /** + * Parse an ENABLED response (RFC 5161 [3.2]) + * + * @param array $data The server response. + */ + protected function _parseEnabled($data) + { + $this->_init['enabled'] = array_merge($this->_init['enabled'], array_flip($data)); + } + + /** + * Open a mailbox. + * Throws a Horde_Imap_Client_Exception on error. + * + * @param string $mailbox The mailbox to open (UTF7-IMAP). + * @param integer $mode The access mode. + */ + protected function _openMailbox($mailbox, $mode) + { + $this->login(); + + $condstore = false; + $qresync = isset($this->_init['enabled']['QRESYNC']); + + /* Let the 'CLOSE' response code handle mailbox switching if QRESYNC + * is active. */ + if (empty($this->_temp['mailbox']['name']) || + (!$qresync && ($mailbox != $this->_temp['mailbox']['name']))) { + $this->_temp['mailbox'] = array('name' => $mailbox); + $this->_selected = $mailbox; + } elseif ($qresync) { + $this->_temp['qresyncmbox'] = $mailbox; + } + + $cmd = (($mode == self::OPEN_READONLY) ? 'EXAMINE' : 'SELECT') . ' ' . $this->escape($mailbox); + + /* If QRESYNC is available, synchronize the mailbox. */ + if ($qresync) { + $metadata = $this->_cacheOb->getMetaData($mailbox, array('HICmodseq', 'uidvalid')); + if (isset($metadata['HICmodseq'])) { + $uids = $this->_cacheOb->get($mailbox); + if (!empty($uids)) { + /* This command may cause several things to happen. + * 1. UIDVALIDITY may have changed. If so, we need + * to expire the cache immediately (done below). + * 2. NOMODSEQ may have been returned. If so, we also + * need to expire the cache immediately (done below). + * 3. VANISHED/FETCH information was returned. These + * responses will have already been handled by those + * response handlers. + * TODO: Use 4th parameter (useful if we keep a sequence + * number->UID lookup in the future). */ + $cmd .= ' (QRESYNC (' . $metadata['uidvalid'] . ' ' . $metadata['HICmodseq'] . ' ' . $this->toSequenceString($uids) . '))'; + } + } + } elseif (!isset($this->_init['enabled']['CONDSTORE']) && + $this->_initCacheOb() && + $this->queryCapability('CONDSTORE')) { + /* Activate CONDSTORE now if ENABLE is not available. */ + $cmd .= ' (CONDSTORE)'; + $condstore = true; + } + + try { + $this->_sendLine($cmd); + } catch (Horde_Imap_Client_Exception $e) { + // An EXAMINE/SELECT failure with a return of 'NO' will cause the + // current mailbox to be unselected. + if ($this->_temp['parseresperr']['response'] == 'NO') { + $this->_selected = null; + $this->_mode = 0; + } + throw $e; + } + + if ($qresync && isset($metadata['uidvalid'])) { + if (is_null($this->_temp['mailbox']['highestmodseq']) || + ($this->_temp['mailbox']['uidvalidity'] != $metadata['uidvalid'])) { + $this->_cacheOb->deleteMailbox($mailbox); + } else { + /* We know the mailbox has been updated, so update the + * highestmodseq metadata in the cache. */ + $this->_cacheOb->setMetaData($mailbox, array('HICmodseq' => $this->_temp['mailbox']['highestmodseq'])); + } + } elseif ($condstore) { + $this->_init['enabled']['CONDSTORE'] = true; + } + } + + /** + * Create a mailbox. + * Throws a Horde_Imap_Client_Exception on error. + * + * @param string $mailbox The mailbox to create (UTF7-IMAP). + */ + protected function _createMailbox($mailbox) + { + $this->login(); + + // CREATE returns no untagged information (RFC 3501 [6.3.3]) + $this->_sendLine('CREATE ' . $this->escape($mailbox)); + } + + /** + * Delete a mailbox. + * Throws a Horde_Imap_Client_Exception on error. + * + * @param string $mailbox The mailbox to delete (UTF7-IMAP). + */ + protected function _deleteMailbox($mailbox) + { + $this->login(); + + // Some IMAP servers will not allow a delete of a currently open + // mailbox. + if ($this->_selected == $mailbox) { + $this->close(); + } + + try { + // DELETE returns no untagged information (RFC 3501 [6.3.4]) + $this->_sendLine('DELETE ' . $this->escape($mailbox)); + } catch (Horde_Imap_Client_Exception $e) { + // Some IMAP servers won't allow a mailbox delete unless all + // messages in that mailbox are deleted. + if (!empty($this->_temp['deleteretry'])) { + unset($this->_temp['deleteretry']); + throw $e; + } + + $this->store($mailbox, array('add' => array('\\deleted'))); + $this->expunge($mailbox); + + $this->_temp['deleteretry'] = true; + $this->deleteMailbox($mailbox); + } + + unset($this->_temp['deleteretry']); + } + + /** + * Rename a mailbox. + * Throws a Horde_Imap_Client_Exception on error. + * + * @param string $old The old mailbox name (UTF7-IMAP). + * @param string $new The new mailbox name (UTF7-IMAP). + */ + protected function _renameMailbox($old, $new) + { + $this->login(); + + // RENAME returns no untagged information (RFC 3501 [6.3.5]) + $this->_sendLine('RENAME ' . $this->escape($old) . ' ' . $this->escape($new)); + } + + /** + * Manage subscription status for a mailbox. + * Throws a Horde_Imap_Client_Exception on error. + * + * @param string $mailbox The mailbox to [un]subscribe to (UTF7-IMAP). + * @param boolean $subscribe True to subscribe, false to unsubscribe. + */ + protected function _subscribeMailbox($mailbox, $subscribe) + { + $this->login(); + + // SUBSCRIBE/UNSUBSCRIBE returns no untagged information (RFC 3501 + // [6.3.6 & 6.3.7]) + $this->_sendLine(($subscribe ? '' : 'UN') . 'SUBSCRIBE ' . $this->escape($mailbox)); + } + + /** + * Obtain a list of mailboxes matching a pattern. + * Throws a Horde_Imap_Client_Exception on error. + * + * @param string $pattern The mailbox search pattern. + * @param integer $mode Which mailboxes to return. + * @param array $options Additional options. + * + * @return array See self::listMailboxes(). + */ + protected function _listMailboxes($pattern, $mode, $options) + { + $this->login(); + + // 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 != self::MBOX_ALL) { + $subscribed = $this->_getMailboxList($pattern, self::MBOX_SUBSCRIBED, array('flat' => true)); + // If mode is subscribed, and 'flat' option is true, we can + // return now. + if (($mode == self::MBOX_SUBSCRIBED) && !empty($options['flat'])) { + return $subscribed; + } + } else { + $subscribed = null; + } + + return $this->_getMailboxList($pattern, $mode, $options, $subscribed); + } + + /** + * Obtain a list of mailboxes matching a pattern. + * Throws a Horde_Imap_Client_Exception on error. + * + * @param string $pattern The mailbox search pattern. + * @param integer $mode Which mailboxes to return. + * @param array $options Additional options. + * @param array $subscribed A list of subscribed mailboxes. + * + * @return array See self::listMailboxes((). + */ + protected function _getMailboxList($pattern, $mode, $options, + $subscribed = null) + { + $check = (($mode != self::MBOX_ALL) && !is_null($subscribed)); + + // Setup cache entry for use in _parseList() + $t = &$this->_temp; + $t['mailboxlist'] = array( + 'check' => $check, + 'subscribed' => $check ? array_flip($subscribed) : null, + 'options' => $options + ); + $t['listresponse'] = array(); + + $this->_sendLine((($mode == self::MBOX_SUBSCRIBED) ? 'LSUB' : 'LIST') . ' "" ' . $this->escape($pattern)); + + return (empty($options['flat'])) ? $t['listresponse'] : array_values($t['listresponse']); + } + + /** + * Parse a LIST/LSUB response (RFC 3501 [7.2.2 & 7.2.3]). + * Throws a Horde_Imap_Client_Exception on error. + * + * @param array $data The server response (includes type as first + * element). + */ + protected function _parseList($data) + { + $ml = $this->_temp['mailboxlist']; + $mlo = $ml['options']; + $lr = &$this->_temp['listresponse']; + + $mode = strtoupper($data[0]); + $mbox = $data[3]; + + /* If dealing with [un]subscribed mailboxes, check to make sure + * this mailbox is in the correct category. */ + if ($ml['check'] && + ((($mode == 'LIST') && isset($ml['subscribed'][$mbox])) || + (($mode == 'LSUB') && !isset($ml['subscribed'][$mbox])))) { + return; + } + + if (!empty($mlo['utf8'])) { + $mbox = Horde_Imap_Client_Utf7imap::Utf7ImapToUtf8($mbox); + } + + if (empty($mlo['flat'])) { + $tmp = array('mailbox' => $mbox); + if (!empty($mlo['attributes'])) { + $tmp['attributes'] = array_map('strtolower', $data[1]); + } + if (!empty($mlo['delimiter'])) { + $tmp['delimiter'] = $data[2]; + } + $lr[$mbox] = $tmp; + } else { + $lr[] = $mbox; + } + } + + /** + * Obtain status information for a mailbox. + * Throws a Horde_Imap_Client_Exception on error. + * + * @param string $mailbox The mailbox to query (UTF7-IMAP). + * @param string $flags A bitmask of information requested from the + * server. + * + * @return array See Horde_Imap_Client_Base::status(). + */ + protected function _status($mailbox, $flags) + { + $data = $query = array(); + $search = null; + + $items = array( + self::STATUS_MESSAGES => 'messages', + self::STATUS_RECENT => 'recent', + self::STATUS_UIDNEXT => 'uidnext', + self::STATUS_UIDVALIDITY => 'uidvalidity', + self::STATUS_UNSEEN => 'unseen', + self::STATUS_FIRSTUNSEEN => 'firstunseen', + self::STATUS_FLAGS => 'flags', + self::STATUS_PERMFLAGS => 'permflags', + self::STATUS_UIDNOTSTICKY => 'uidnotsticky', + ); + + /* Don't include 'highestmodseq' return if server does not support it. + * OK to use queryCapability('CONDSTORE') here because we may not have + * yet sent an enabling command. */ + if ($this->queryCapability('CONDSTORE')) { + $items[self::STATUS_HIGHESTMODSEQ] = 'highestmodseq'; + } + + /* If FLAGS/PERMFLAGS/UIDNOTSTICKY/FIRSTUNSEEN are needed, we must do + * a SELECT/EXAMINE to get this information (data will be caught in + * the code below). */ + if (($flags & self::STATUS_FIRSTUNSEEN) || + ($flags & self::STATUS_FLAGS) || + ($flags & self::STATUS_PERMFLAGS) || + ($flags & self::STATUS_UIDNOTSTICKY)) { + $this->openMailbox($mailbox); + } else { + $this->login(); + } + + foreach ($items as $key => $val) { + if ($key & $flags) { + if ($mailbox == $this->_selected) { + if (isset($this->_temp['mailbox'][$val])) { + $data[$val] = $this->_temp['mailbox'][$val]; + } else { + if ($key == self::STATUS_UIDNOTSTICKY) { + /* In the absence of uidnotsticky information, or + * if UIDPLUS is not supported, we assume the UIDs + * are sticky. */ + $data[$val] = false; + } elseif (in_array($key, array(self::STATUS_FIRSTUNSEEN, self::STATUS_UNSEEN))) { + /* If we already know there are no messages in the + * current mailbox, we know there is no + * firstunseen and unseen info also. */ + if (empty($this->_temp['mailbox']['messages'])) { + $data[$val] = ($key == self::STATUS_FIRSTUNSEEN) ? null : 0; + } else { + /* RFC 3501 [6.3.1] - FIRSTUNSEEN information + * is not mandatory. If missing EXAMINE/SELECT + * we need to do a search. An UNSEEN count + * also requires a search. */ + if (is_null($search)) { + $search_query = new Horde_Imap_Client_Search_Query(); + $search_query->flag('\\seen', false); + $search = $this->search($mailbox, $search_query, array('results' => array(($key == self::STATUS_FIRSTUNSEEN) ? self::SORT_RESULTS_MIN : self::SORT_RESULTS_COUNT), 'sequence' => true)); + } + + $data[$val] = $search[($key == self::STATUS_FIRSTUNSEEN) ? 'min' : 'count']; + } + } + } + } else { + $query[] = $val; + } + } + } + + if (empty($query)) { + return $data; + } + + $this->_temp['status'] = array(); + $this->_sendLine('STATUS ' . $this->escape($mailbox) . ' (' . implode(' ', array_map('strtoupper', $query)) . ')'); + + return $this->_temp['status']; + } + + /** + * Parse a STATUS response (RFC 3501 [7.2.4], RFC 4551 [3.6]) + * + * @param array $data The server response. + */ + protected function _parseStatus($data) + { + for ($i = 0, $len = count($data); $i < $len; $i += 2) { + $item = strtolower($data[$i]); + $val = $data[$i + 1]; + if (!$val && ($item == 'highestmodseq')) { + $val = null; + } + $this->_temp['status'][$item] = $val; + } + } + + /** + * Append message(s) to a mailbox. + * Throws a Horde_Imap_Client_Exception on error. + * + * @param string $mailbox The mailbox to append the message(s) to + * (UTF7-IMAP). + * @param array $data The message data. + * @param array $options Additional options. + * + * @return mixed An array of the UIDs of the appended messages (if server + * supports UIDPLUS extension) or true. + */ + protected function _append($mailbox, $data, $options) + { + $this->login(); + + // If the mailbox is currently selected read-only, we need to close + // because some IMAP implementations won't allow an append. + if (($this->_selected == $mailbox) && + ($this->_mode == Horde_Imap_Client::OPEN_READONLY)) { + $this->close(); + } + + // Check for MULTIAPPEND extension (RFC 3502) + $multiappend = $this->queryCapability('MULTIAPPEND'); + + $t = &$this->_temp; + $t['appenduid'] = array(); + $t['trycreate'] = null; + $t['uidplusmbox'] = $mailbox; + $cnt = count($data); + $i = 0; + $notag = false; + $literaldata = true; + + reset($data); + while (list(,$m_data) = each($data)) { + if (!$i++ || !$multiappend) { + $cmd = 'APPEND ' . $this->escape($mailbox); + } else { + $cmd = ''; + $notag = true; + } + + if (!empty($m_data['flags'])) { + $cmd .= ' (' . implode(' ', $m_data['flags']) . ')'; + } + + if (!empty($m_data['internaldate'])) { + $cmd .= ' ' . $this->escape($m_data['internaldate']->format('j-M-Y H:i:s O')); + } + + /* @todo There is no way I am aware of to determine the length of + * a stream. Having a user pass in the length of a stream is + * cumbersome, and they would most likely have to do just as much + * work to get the length of the stream as we have to do here. So + * for now, simply grab the contents of the stream and do a + * strlen() call to determine the literal size to send to the + * IMAP server. */ + $text = $this->removeBareNewlines(is_resource($m_data['data']) ? stream_get_contents($m_data['data']) : $m_data['data']); + $datalength = strlen($text); + + /* RFC 3516/4466 says we should be able to append binary data + * using literal8 "~{#} format", but it doesn't seem to work in + * all servers tried (UW-IMAP/Cyrus). However, there is no other + * way to append null data, so try anyway. */ + $binary = (strpos($text, null) !== false); + + /* Need to add 2 additional characters (we send CRLF at the end of + * a line) to literal count for multiappend messages to ensure the + * server will accept the next line of information, which contains + * the next append request. */ + if ($multiappend) { + if ($i == $cnt) { + $literaldata = false; + } else { + $datalength += 2; + } + } else { + $literaldata = false; + } + + try { + $this->_sendLine($cmd, array('binary' => $binary, 'literal' => $datalength, 'notag' => $notag)); + } catch (Horde_Imap_Client_Exception $e) { + if (!empty($options['create']) && $this->_temp['trycreate']) { + $this->createMailbox($mailbox); + unset($options['create']); + return $this->_append($mailbox, $data, $options); + } + throw $e; + } + + // Send data. + $this->_sendLine($text, array('literaldata' => $literaldata, 'notag' => true)); + } + + /* If we reach this point and have data in $_temp['appenduid'], + * UIDPLUS (RFC 4315) has done the dirty work for us. */ + return empty($t['appenduid']) ? true : $t['appenduid']; + } + + /** + * Request a checkpoint of the currently selected mailbox. + * Throws a Horde_Imap_Client_Exception on error. + */ + protected function _check() + { + // CHECK returns no untagged information (RFC 3501 [6.4.1]) + $this->_sendLine('CHECK'); + } + + /** + * Close the connection to the currently selected mailbox, optionally + * expunging all deleted messages (RFC 3501 [6.4.2]). + * Throws a Horde_Imap_Client_Exception on error. + * + * @param array $options Additional options. + */ + protected function _close($options) + { + if (empty($options['expunge'])) { + if ($this->queryCapability('UNSELECT')) { + // RFC 3691 defines 'UNSELECT' for precisely this purpose + $this->_sendLine('UNSELECT'); + } else { + // RFC 3501 [6.4.2]: to close a mailbox without expunge, + // select a non-existent mailbox. Selecting a null mailbox + // should do the trick. + try { + $this->_sendLine('SELECT ""'); + } catch (Horde_Imap_Client_Exception $e) { + // Ignore - we are expecting a NO return. + } + } + } else { + // If caching, we need to know the UIDs being deleted, so call + // expunge() before calling close(). + if ($this->_initCacheOb()) { + $this->expunge($this->_selected); + } + + // CLOSE returns no untagged information (RFC 3501 [6.4.2]) + $this->_sendLine('CLOSE'); + + /* Ignore HIGHESTMODSEQ information (RFC 5162 [3.4]) since the + * expunge() call would have already caught it. */ + } + + // Need to clear status cache since we are no longer in mailbox. + $this->_temp['mailbox'] = array(); + } + + /** + * Expunge deleted messages from the given mailbox. + * Throws a Horde_Imap_Client_Exception on error. + * + * @param array $options Additional options. + */ + protected function _expunge($options) + { + $unflag = array(); + $mailbox = $this->_selected; + $seq = !empty($options['sequence']); + $s_res = null; + $uidplus = $this->queryCapability('UIDPLUS'); + $use_cache = $this->_initCacheOb(); + + if (empty($options['ids'])) { + $uid_string = '1:*'; + } elseif ($uidplus) { + /* UID EXPUNGE command needs UIDs. */ + if (reset($options['ids']) === self::USE_SEARCHRES) { + $uid_string = '$'; + } elseif ($seq) { + $results = array(self::SORT_RESULTS_MATCH); + if ($this->queryCapability('SEARCHRES')) { + $results[] = self::SORT_RESULTS_SAVE; + } + $s_res = $this->search($mailbox, null, array('results' => $results)); + $uid_string = (in_array(self::SORT_RESULTS_SAVE, $results) && !empty($s_res['save'])) + ? '$' + : $this->toSequenceString($s_res['match']); + } else { + $uid_string = $this->toSequenceString($options['ids']); + } + } else { + /* Without UIDPLUS, need to temporarily unflag all messages marked + * as deleted but not a part of requested IDs to delete. Use NOT + * searches to accomplish this goal. */ + $search_query = new Horde_Imap_Client_Search_Query(); + $search_query->flag('\\deleted', true); + if (reset($options['ids']) === self::USE_SEARCHRES) { + $search_query->previousSearch(true); + } else { + $search_query->sequence($options['ids'], $seq, true); + } + + $res = $this->search($mailbox, $search_query); + $unflag = $res['match']; + + $this->store($mailbox, array('ids' => $unflag, 'remove' => array('\\deleted'))); + } + + /* We need to get Msgno -> UID lookup table if we are caching. + * Apparently, there is no guarantee that if we are using QRESYNC that + * we will get VANISHED responses, so we need to do this. */ + if ($use_cache && is_null($s_res)) { + /* Keys in $s_res['sort'] start at 0, not 1. */ + $s_res = $this->search($mailbox, null, array('sort' => array(self::SORT_ARRIVAL))); + } + + $tmp = &$this->_temp; + $tmp['expunge'] = $tmp['vanished'] = array(); + + /* Always use UID EXPUNGE if available. */ + if ($uidplus) { + $this->_sendLine('UID EXPUNGE ' . $uid_string); + } elseif ($use_cache) { + $this->_sendLine('EXPUNGE'); + } else { + /* This is faster than an EXPUNGE because the server will not + * return untagged EXPUNGE responses. We can only do this if + * we are not updating cache information. */ + $this->close(array('expunge' => true)); + } + + if (!empty($unflag)) { + $this->store($mailbox, array('add' => array('\\deleted'), 'ids' => $unflag)); + } + + if ($use_cache) { + if (!empty($tmp['vanished'])) { + $i = count($tmp['vanished']); + $expunged = $tmp['vanished']; + } elseif (!empty($tmp['expunge'])) { + $expunged = array(); + $i = 0; + $t = $s_res['sort']; + + foreach ($tmp['expunge'] as $val) { + $expunged[] = $t[$val - 1 + $i++]; + } + } + + if (!empty($expunged)) { + $this->_cacheOb->deleteMsgs($mailbox, $expunged); + $tmp['mailbox']['messages'] -= $i; + } + + if (isset($this->_init['enabled']['QRESYNC'])) { + $this->_cacheOb->setMetaData($mailbox, array('HICmodseq' => $this->_temp['mailbox']['highestmodseq'])); + } + } + } + + /** + * Parse an EXPUNGE response (RFC 3501 [7.4.1]). + * + * @param integer $seq The message sequence number. + */ + protected function _parseExpunge($seq) + { + $this->_temp['expunge'][] = $seq; + } + + /** + * Parse a VANISHED response (RFC 5162 [3.6]). + * + * @param array $data The response data. + */ + protected function _parseVanished($data) + { + /* There are two forms of VANISHED. VANISHED (EARLIER) will be sent + * be sent in a FETCH (VANISHED) or SELECT/EXAMINE (QRESYNC) call. + * If this is the case, we can go ahead and update the cache + * immediately (we know we are caching or else QRESYNC would not be + * enabled). HIGHESTMODSEQ information will be grabbed at the end in + * the tagged response. */ + if (is_array($data[0])) { + if (strtoupper(reset($data[0])) == 'EARLIER') { + $this->_cacheOb->deleteMsgs($this->_temp['mailbox']['name'], $this->fromSequenceString($data[1])); + } + } else { + /* The second form is just VANISHED. This is returned from an + * EXPUNGE command and will be processed in _expunge() (since + * we need to adjust message counts in the current mailbox). */ + $this->_temp['vanished'] = $this->fromSequenceString($data[0]); + } + } + + /** + * Search a mailbox. This driver supports all IMAP4rev1 search criteria + * as defined in RFC 3501. + * Throws a Horde_Imap_Client_Exception on error. + * + * @param object $query The search query. + * @param array $options Additional options. The '_query' key contains + * the value of $query->build(). + * + * @return array An array of UIDs (default) or an array of message + * sequence numbers (if 'sequence' is true). + */ + protected function _search($query, $options) + { + // Check for IMAP extensions needed + foreach ($query->extensionsNeeded() as $val) { + if (!$this->queryCapability($val)) { + throw new Horde_Imap_Client_Exception('IMAP Server does not support sorting extension ' . $val . '.', Horde_Imap_Client_Exception::NOSUPPORTIMAPEXT); + } + + /* RFC 4551 [3.1] - trying to do a MODSEQ SEARCH on a mailbox that + * doesn't support it will return BAD. Catch that here and thrown + * an exception. */ + if (($val == 'CONDSTORE') && + is_null($this->_temp['mailbox']['highestmodseq']) && + (strpos($options['_query']['query'], 'MODSEQ ') !== false)) { + throw new Horde_Imap_Client_Exception('Mailbox does not support mod-sequences.', Horde_Imap_Client_Exception::MBOXNOMODSEQ); + } + } + + $cmd = ''; + if (empty($options['sequence'])) { + $cmd = 'UID '; + } + + $sort_criteria = array( + self::SORT_ARRIVAL => 'ARRIVAL', + self::SORT_CC => 'CC', + self::SORT_DATE => 'DATE', + self::SORT_FROM => 'FROM', + self::SORT_REVERSE => 'REVERSE', + self::SORT_SIZE => 'SIZE', + self::SORT_SUBJECT => 'SUBJECT', + self::SORT_TO => 'TO' + ); + + $results_criteria = array( + self::SORT_RESULTS_COUNT => 'COUNT', + self::SORT_RESULTS_MATCH => 'ALL', + self::SORT_RESULTS_MAX => 'MAX', + self::SORT_RESULTS_MIN => 'MIN', + self::SORT_RESULTS_SAVE => 'SAVE' + ); + + // Check if the server supports server-side sorting (RFC 5256). + $esearch = $server_sort = $return_sort = false; + if (!empty($options['sort'])) { + $return_sort = true; + $server_sort = $this->queryCapability('SORT'); + + /* Make sure sort options are correct. If not, default to ARRIVAL + * sort. */ + if (count(array_intersect($options['sort'], array_keys($sort_criteria))) === 0) { + $options['sort'] = array(self::SORT_ARRIVAL); + } + } + + if ($server_sort) { + // Check for ESORT capability (RFC 5267) + if ($this->queryCapability('ESORT')) { + $results = array(); + foreach ($options['results'] as $val) { + if (isset($results_criteria[$val]) && + ($val != self::SORT_RESULTS_SAVE)) { + $results[] = $results_criteria[$val]; + } + } + $cmd .= 'SORT RETURN ( ' . implode(' ', $results) . ') ('; + } else { + $cmd .= 'SORT ('; + } + + foreach ($options['sort'] as $val) { + if (isset($sort_criteria[$val])) { + $cmd .= $sort_criteria[$val] . ' '; + } + } + $cmd = rtrim($cmd) . ') '; + } else { + // Check if the server supports ESEARCH (RFC 4731). + $esearch = $this->queryCapability('ESEARCH'); + + if ($esearch) { + // Always use ESEARCH if available because it returns results + // in a more compact sequence-set list + $results = array(); + foreach ($options['results'] as $val) { + if (isset($results_criteria[$val])) { + $results[] = $results_criteria[$val]; + } + } + $cmd .= 'SEARCH RETURN (' . implode(' ', $results) . ') CHARSET '; + } else { + $cmd .= 'SEARCH CHARSET '; + } + + // SEARCHRES requires ESEARCH + unset($this->_temp['searchnotsaved']); + } + + $er = &$this->_temp['esearchresp']; + $sr = &$this->_temp['searchresp']; + $er = $sr = array(); + + $this->_sendLine($cmd . $options['_query']['charset'] . ' ' . $options['_query']['query']); + + if ($return_sort && !$server_sort) { + $sr = array_values($this->_clientSort($sr, $options)); + } + + $ret = array(); + foreach ($options['results'] as $val) { + switch ($val) { + case self::SORT_RESULTS_COUNT: + $ret['count'] = $esearch ? $er['count'] : count($sr); + break; + + case self::SORT_RESULTS_MATCH: + $ret[$return_sort ? 'sort' : 'match'] = $sr; + break; + + case self::SORT_RESULTS_MAX: + $ret['max'] = $esearch ? (isset($er['max']) ? $er['max'] : null) : (empty($sr) ? null : max($sr)); + break; + + case self::SORT_RESULTS_MIN: + $ret['min'] = $esearch ? (isset($er['min']) ? $er['min'] : null) : (empty($sr) ? null : min($sr)); + break; + + case self::SORT_RESULTS_SAVE: + $ret['save'] = $esearch ? empty($this->_temp['searchnotsaved']) : false; + } + } + + // Add modseq data, if needed. + if (!empty($er['modseq'])) { + $ret['modseq'] = $er['modseq']; + } + + return $ret; + } + + /** + * Parse a SEARCH/SORT response (RFC 3501 [7.2.5]; RFC 4466 [3]; + * RFC 5256 [4]; RFC 5267 [3]). + * + * @param array $data The server response. + */ + protected function _parseSearch($data) + { + // The extended search response will have a (NAME VAL) entry(s) at + // the end of the returned data. Do a check for this data. + if (is_array(end($data))) { + $this->_parseEsearch(array_pop($data)); + } + + $this->_temp['searchresp'] = $data; + } + + /** + * Parse an ESEARCH response (RFC 4466 [2.6.2]) + * Format: (TAG "a567") UID COUNT 5 ALL 4:19,21,28 + * + * @param array $data The server response. + */ + protected function _parseEsearch($data) + { + $i = 0; + $len = count($data); + + // Ignore search correlator information + if (is_array($data[$i])) { + ++$i; + } + + // Ignore UID tag + if (($i != $len) && (strtoupper($data[$i]) == 'UID')) { + ++$i; + } + + // This catches the case of an '(ALL)' esearch with no results + if ($i == $len) { + return; + } + + for (; $i < $len; $i += 2) { + $val = $data[$i + 1]; + $tag = strtoupper($data[$i]); + switch ($tag) { + case 'ALL': + $this->_temp['searchresp'] = $this->fromSequenceString($val); + break; + + case 'COUNT': + case 'MAX': + case 'MIN': + case 'MODSEQ': + $this->_temp['esearchresp'][strtolower($tag)] = $val; + break; + } + } + } + + /** + * If server does not support the SORT IMAP extension (RFC 5256), we need + * to do sorting on the client side. + * Throws a Horde_Imap_Client_Exception on error. + * + * @param array $res The search results. + * @param array $opts The options to search(). + * + * @return array The sort results. + */ + protected function _clientSort($res, $opts) + { + if (empty($res)) { + return $res; + } + + /* Generate the FETCH command needed. */ + $criteria = array(); + foreach ($opts['sort'] as $val) { + switch ($val) { + case self::SORT_DATE: + $criteria[self::FETCH_DATE] = true; + // Fall through + + case self::SORT_CC: + case self::SORT_FROM: + case self::SORT_SUBJECT: + case self::SORT_TO: + $criteria[self::FETCH_ENVELOPE] = true; + break; + + case self::SORT_SIZE: + $criteria[self::FETCH_SIZE] = true; + break; + } + } + + /* Get the FETCH results now. */ + if (!empty($criteria)) { + $fetch_res = $this->fetch($this->_selected, $criteria, array('ids' => $res, 'sequence' => $opts['sequence'])); + } + + /* The initial sort is on the entire set. */ + $slices = array(0 => $res); + + $reverse = false; + foreach ($opts['sort'] as $val) { + if ($val == self::SORT_REVERSE) { + $reverse = true; + continue; + } + + $slices_list = $slices; + $slices = array(); + + foreach ($slices_list as $slice_start => $slice) { + $sorted = array(); + + if ($reverse) { + $slice = array_reverse($slice); + } + + switch ($val) { + case self::SORT_ARRIVAL: + /* There is no requirement that IDs be returned in + * sequence order (see RFC 4549 [4.3.1]). So we must sort + * ourselves. */ + $sorted = $slice; + sort($sorted, SORT_NUMERIC); + break; + + case self::SORT_SIZE: + foreach ($slice as $num) { + $sorted[$num] = $fetch_res[$num]['size']; + } + asort($sorted, SORT_NUMERIC); + break; + + case self::SORT_CC: + case self::SORT_FROM: + case self::SORT_TO: + if ($val == self::SORT_CC) { + $field = 'cc'; + } elseif ($val = self::SORT_FROM) { + $field = 'from'; + } else { + $field = 'to'; + } + + foreach ($slice as $num) { + $sorted[$num] = empty($fetch_res[$num]['envelope'][$field]) + ? null + : $fetch_res[$num]['envelope'][$field][0]['mailbox']; + } + asort($sorted, SORT_LOCALE_STRING); + break; + + case self::SORT_DATE: + // Date sorting rules in RFC 5256 [2.2] + $sorted = $this->_getSentDates($fetch_res, $slice); + asort($sorted, SORT_NUMERIC); + break; + + case self::SORT_SUBJECT: + // Subject sorting rules in RFC 5256 [2.1] + foreach ($slice as $num) { + $sorted[$num] = empty($fetch_res[$num]['envelope']['subject']) + ? '' + : $this->getBaseSubject($fetch_res[$num]['envelope']['subject']); + } + asort($sorted, SORT_LOCALE_STRING); + break; + } + + // At this point, keys of $sorted are sequence/UID and values + // are the sort strings + if (!empty($sorted)) { + if (count($sorted) == count($res)) { + $res = array_keys($sorted); + } else { + array_splice($res, $slice_start, count($slice), array_keys($sorted)); + } + + // Check for ties. + $last = $start = null; + $i = 0; + reset($sorted); + while (list($k, $v) = each($sorted)) { + if (is_null($last) || ($last != $v)) { + if ($i) { + $slices[array_search($res, $start)] = array_slice($sorted, array_search($sorted, $start), $i + 1); + $i = 0; + } + $last = $v; + $start = $k; + } else { + ++$i; + } + } + if ($i) { + $slices[array_search($res, $start)] = array_slice($sorted, array_search($sorted, $start), $i + 1); + } + } + } + + $reverse = false; + } + + return $res; + } + + /** + * Get the sent dates for purposes of SORT/THREAD sorting under RFC 5256 + * [2.2]. + * + * @param array $data Data returned from fetch() that includes both the + * 'envelope' and 'date' keys. + * @param array $ids The IDs to process. + * + * @return array A mapping of IDs -> UNIX timestamps. + */ + protected function _getSentDates($data, $ids) + { + $dates = array(); + + $tz = new DateTimeZone('UTC'); + foreach ($ids as $num) { + if (empty($data[$num]['envelope']['date'])) { + $dt = $data[$num]['date']; + $dt->setTimezone($tz); + } else { + $dt = new DateTime($data[$num]['envelope']['date'], $tz); + } + $dates[$num] = $dt->format('U'); + } + + return $dates; + } + + /** + * Set the comparator to use for searching/sorting (RFC 5255). + * Throws a Horde_Imap_Client_Exception on error. + * + * @param string $comparator The comparator string (see RFC 4790 [3.1] - + * "collation-id" - for format). The reserved + * string 'default' can be used to select + * the default comparator. + */ + protected function _setComparator($comparator) + { + $this->_login(); + + $cmd = array(); + foreach (explode(' ', $comparator) as $val) { + $cmd[] = $this->escape($val); + } + + $this->_sendLine('COMPARATOR ' . implode(' ', $cmd)); + } + + /** + * Get the comparator used for searching/sorting (RFC 5255). + * Throws a Horde_Imap_Client_Exception on error. + * + * @return mixed Null if the default comparator is being used, or an + * array of comparator information (see RFC 5255 [4.8]). + */ + protected function _getComparator() + { + $this->_login(); + + $this->_sendLine('COMPARATOR'); + + return isset($this->_temp['comparator']) ? $this->_temp['comparator'] : null; + } + + /** + * Parse a COMPARATOR response (RFC 5255 [4.8]) + * + * @param array $data The server response. + */ + protected function _parseComparator($data) + { + $this->_temp['comparator'] = $data; + } + + /** + * Thread sort a given list of messages (RFC 5256). + * Throws a Horde_Imap_Client_Exception on error. + * + * @param array $options Additional options. + * + * @return array See Horde_Imap_Client_Base::_thread(). + */ + protected function _thread($options) + { + $thread_criteria = array( + self::THREAD_ORDEREDSUBJECT => 'ORDEREDSUBJECT', + self::THREAD_REFERENCES => 'REFERENCES' + ); + + $tsort = (isset($options['criteria'])) + ? (is_string($options['criteria']) ? strtoupper($options['criteria']) : $thread_criteria[$options['criteria']]) + : 'REFERENCES'; + + $cap = $this->queryCapability('THREAD'); + if (!$cap || !in_array($tsort, $cap)) { + if ($tsort == 'ORDEREDSUBJECT') { + if (empty($options['search'])) { + $ids = array(); + } else { + $search_res = $this->search($this->_selected, $options['search'], array('sequence' => !empty($options['sequence']))); + $ids = $search_res['match']; + } + + /* Do client-side ORDEREDSUBJECT threading. */ + $fetch_res = $this->fetch($this->_selected, array(self::FETCH_ENVELOPE => true, self::FETCH_DATE => true), array('ids' => $ids, 'sequence' => !empty($options['sequence']))); + return $this->_clientThreadOrderedsubject($fetch_res); + } else { + throw new Horde_Imap_Client_Exception('Server does not support REFERENCES thread sort.', Horde_Imap_Client_Exception::NOSUPPORTIMAPEXT); + } + } + + if (empty($options['search'])) { + $charset = 'US-ASCII'; + $search = 'ALL'; + } else { + $search_query = $options['search']->build(); + $charset = $search_query['charset']; + $search = $search_query['query']; + } + + $this->_temp['threadresp'] = array(); + $this->_sendLine((empty($options['sequence']) ? 'UID ' : '') . 'THREAD ' . $tsort . ' ' . $charset . ' ' . $search); + + return $this->_temp['threadresp']; + } + + /** + * Parse a THREAD response (RFC 5256 [4]). + * + * @param array $data An array of thread token data. + * @param boolean $islast Is this the last item in the level? + * @param integer $level The current tree level. + */ + protected function _parseThread($data, $level = 0, $islast = true) + { + $tb = &$this->_temp['threadbase']; + $tr = &$this->_temp['threadresp']; + + if (!$level) { + $tb = null; + } + $cnt = count($data) - 1; + + reset($data); + while (list($key, $val) = each($data)) { + if (is_array($val)) { + $this->_parseThread($val, $level, $cnt == $key); + } else { + if (is_null($tb) && ($level || $cnt)) { + $tb = $val; + } + $tr[$val] = array( + 'base' => $tb, + 'last' => $islast, + 'level' => $level++, + 'id' => $val + ); + } + } + } + + /** + * If server does not support the THREAD IMAP extension (RFC 5256), do + * ORDEREDSUBJECT threading on the client side. + * + * @param array $res The search results. + * @param array $opts The options to search(). + * + * @return array The sort results. + */ + protected function _clientThreadOrderedsubject($data) + { + $dates = $this->_getSentDates($data, array_keys($data)); + $level = $sorted = $tsort = array(); + $this->_temp['threadresp'] = array(); + + reset($data); + while(list($k, $v) = each($data)) { + $subject = empty($v['envelope']['subject']) + ? '' + : $this->getBaseSubject($v['envelope']['subject']); + if (!isset($sorted[$subject])) { + $sorted[$subject] = array(); + } + $sorted[$subject][$k] = $dates[$k]; + } + + /* Step 1: Sort by base subject (already done). + * Step 2: Sort by sent date within each thread. */ + foreach (array_keys($sorted) as $key) { + asort($sorted[$key], SORT_NUMERIC); + $tsort[$key] = reset($sorted[$key]); + } + + /* Step 3: Sort by the sent date of the first message in the + * thread. */ + asort($tsort, SORT_NUMERIC); + + /* Now, $tsort contains the order of the threads, and each thread + * is sorted in $sorted. */ + foreach (array_keys($tsort) as $key) { + $keys = array_keys($sorted[$key]); + $tmp = array($keys[0]); + if (count($keys) > 1) { + $tmp[] = array_slice($keys, 1); + } + $this->_parseThread($tmp); + } + + return $this->_temp['threadresp']; + } + + /** + * Fetch message data. + * Throws a Horde_Imap_Client_Exception on error. + * + * @todo Provide a function that would allow streaming of large data + * items like bodytext. + * + * @param array $criteria The fetch criteria. + * @param array $options Additional options. + * + * @return array See self::fetch(). + */ + protected function _fetch($criteria, $options) + { + $t = &$this->_temp; + $t['fetchparams'] = array(); + $fp = &$t['fetchparams']; + $fetch = array(); + + /* Build an IMAP4rev1 compliant FETCH query. We handle the following + * criteria: + * BINARY[.PEEK][
]<> (RFC 3516) + * see BODY[] response + * BINARY.SIZE[
] (RFC 3516) + * BODY + * BODY[.PEEK][
]<> + *
= HEADER, HEADER.FIELDS, HEADER.FIELDS.NOT, MIME, + * TEXT, empty + * <> = 0.# (# of bytes) + * BODYSTRUCTURE + * ENVELOPE + * FLAGS + * INTERNALDATE + * MODSEQ (RFC 4551) + * RFC822.SIZE + * UID + * + * No need to support these (can be built from other queries): + * =========================================================== + * ALL macro => (FLAGS INTERNALDATE RFC822.SIZE ENVELOPE) + * FAST macro => (FLAGS INTERNALDATE RFC822.SIZE) + * FULL macro => (FLAGS INTERNALDATE RFC822.SIZE ENVELOPE BODY) + * RFC822 => BODY[] + * RFC822.HEADER => BODY[HEADER] + * RFC822.TEXT => BODY[TEXT] + */ + reset($criteria); + while (list($type, $c_val) = each($criteria)) { + if (!is_array($c_val)) { + $c_val = array(); + } + + switch ($type) { + case self::FETCH_STRUCTURE: + $fp['parsestructure'] = !empty($c_val['parse']); + $fetch[] = !empty($c_val['noext']) ? 'BODY' : 'BODYSTRUCTURE'; + break; + + case self::FETCH_FULLMSG: + if (empty($c_val['peek'])) { + $this->openMailbox($this->_selected, self::OPEN_READWRITE); + } + $fetch[] = 'BODY' . + (!empty($c_val['peek']) ? '.PEEK' : '') . + '[]' . + (isset($c_val['start']) && !empty($c_val['length']) ? ('<' . $c_val['start'] . '.' . $c_val['length'] . '>') : ''); + break; + + case self::FETCH_HEADERTEXT: + case self::FETCH_BODYTEXT: + case self::FETCH_MIMEHEADER: + case self::FETCH_BODYPART: + case self::FETCH_HEADERS: + foreach ($c_val as $val) { + $main_cmd = 'BODY'; + + if (empty($val['id'])) { + $cmd = ''; + } else { + $cmd = $val['id'] . '.'; + } + + switch ($type) { + case self::FETCH_HEADERTEXT: + $fp['parseheadertext'] = !empty($val['parse']); + $cmd .= 'HEADER'; + break; + + case self::FETCH_BODYTEXT: + $cmd .= 'TEXT'; + break; + + case self::FETCH_MIMEHEADER: + if (empty($val['id'])) { + throw new Horde_Imap_Client_Exception('Need a MIME ID when retrieving a MIME header.'); + } + $cmd .= 'MIME'; + break; + + case self::FETCH_BODYPART: + if (empty($val['id'])) { + throw new Horde_Imap_Client_Exception('Need a MIME ID when retrieving a MIME body part.'); + } + // Remove the last dot from the string. + $cmd = substr($cmd, 0, -1); + + if (!empty($val['decode']) && + $this->queryCapability('BINARY')) { + $main_cmd = 'BINARY'; + } + break; + + case self::FETCH_HEADERS: + if (empty($val['label'])) { + throw new Horde_Imap_Client_Exception('Need a unique label when doing a headers field search.'); + } + if (empty($val['headers'])) { + throw new Horde_Imap_Client_Exception('Need headers to query when doing a headers field search.'); + } + $fp['parseheaders'] = !empty($val['parse']); + + $cmd .= 'HEADER.FIELDS'; + if (!empty($val['notsearch'])) { + $cmd .= '.NOT'; + } + $cmd .= ' (' . implode(' ', array_map('strtoupper', $val['headers'])) . ')'; + + // Maintain a command -> label lookup so we can put + // the results in the proper location. + if (!isset($fp['hdrfields'])) { + $fp['hdrfields'] = array(); + } + $fp['hdrfields'][$cmd] = $val['label']; + } + + if (empty($c_val['peek'])) { + $this->openMailbox($this->_selected, self::OPEN_READWRITE); + } + + $fetch[] = $main_cmd . + (!empty($c_val['peek']) ? '.PEEK' : '') . + '[' . $cmd . ']' . + (isset($c_val['start']) && !empty($c_val['length']) ? ('<' . $c_val['start'] . '.' . $c_val['length'] . '>') : ''); + } + break; + + case self::FETCH_BODYPARTSIZE: + foreach ($c_val as $val) { + if (empty($val['id'])) { + throw new Horde_Imap_Client_Exception('Need a MIME ID when retrieving unencoded MIME body part size.'); + } + $fetch[] = 'BINARY.SIZE[' . $val['id'] . ']'; + } + break; + + case self::FETCH_ENVELOPE: + $fetch[] = 'ENVELOPE'; + break; + + case self::FETCH_FLAGS: + $fetch[] = 'FLAGS'; + break; + + case self::FETCH_DATE: + $fetch[] = 'INTERNALDATE'; + break; + + case self::FETCH_SIZE: + $fetch[] = 'RFC822.SIZE'; + break; + + case self::FETCH_UID: + $fetch[] = 'UID'; + break; + + case self::FETCH_SEQ: + // Nothing we need to add to fetch criteria. + break; + + case self::FETCH_MODSEQ: + /* RFC 4551 [3.1] - trying to do a FETCH of MODSEQ on a + * mailbox that doesn't support it will return BAD. Catch that + * here and throw an exception. */ + if (is_null($this->_temp['mailbox']['highestmodseq'])) { + throw new Horde_Imap_Client_Exception('Mailbox does not support mod-sequences.', Horde_Imap_Client_Exception::MBOXNOMODSEQ); + } + $fetch[] = 'MODSEQ'; + break; + } + } + + $seq = empty($options['ids']) + ? '1:*' + : ((reset($options['ids']) === self::USE_SEARCHRES) + ? '$' + : $this->toSequenceString($options['ids'])); + $use_seq = !empty($options['sequence']); + + $cmd = ($use_seq ? '' : 'UID ') . 'FETCH ' . $seq . ' (' . implode(' ', $fetch) . ')'; + + if (!empty($options['changedsince'])) { + if (is_null($this->_temp['mailbox']['highestmodseq'])) { + throw new Horde_Imap_Client_Exception('Mailbox does not support mod-sequences.', Horde_Imap_Client_Exception::MBOXNOMODSEQ); + } + $cmd .= ' (CHANGEDSINCE ' . intval($options['changedsince']) . ')'; + } + + $this->_sendLine($cmd); + + return $t['fetchresp'][$use_seq ? 'seq' : 'uid']; + } + + /** + * Parse a FETCH response (RFC 3501 [7.4.2]). A FETCH response may occur + * due to a FETCH command, or due to a change in a message's state (i.e. + * the flags change). + * + * @param integer $id The message sequence number. + * @param array $data The server response. + */ + protected function _parseFetch($id, $data) + { + $section_storage = array( + 'HEADER' => 'headertext', + 'TEXT' => 'bodytext', + 'MIME' => 'mimeheader' + ); + + $i = 0; + $cnt = count($data); + + if (isset($this->_temp['fetchresp']['seq'][$id])) { + $tmp = $this->_temp['fetchresp']['seq'][$id]; + $uid = isset($tmp['uid']) ? $tmp['uid'] : null; + } else { + $tmp = array('seq' => $id); + $uid = null; + } + + while ($i < $cnt) { + $tag = strtoupper($data[$i]); + switch ($tag) { + case 'BODY': + case 'BODYSTRUCTURE': + // Only care about these if doing a FETCH command. + $tmp['structure'] = empty($this->_temp['fetchparams']['parsestructure']) + ? $this->_parseBodystructure($data[++$i]) + : Horde_MIME_Message::parseStructure($this->_parseBodystructure($data[++$i])); + break; + + case 'ENVELOPE': + $tmp['envelope'] = $this->_parseEnvelope($data[++$i]); + break; + + case 'FLAGS': + $tmp['flags'] = array_map('strtolower', $data[++$i]); + break; + + case 'INTERNALDATE': + $tmp['date'] = new DateTime($data[++$i]); + break; + + case 'RFC822.SIZE': + $tmp['size'] = $data[++$i]; + break; + + case 'UID': + $uid = $tmp['uid'] = $data[++$i]; + break; + + case 'MODSEQ': + $tmp['modseq'] = reset($data[++$i]); + break; + + default: + // Catch BODY[*]<#> responses + if (strpos($tag, 'BODY[') === 0) { + // Remove the beginning 'BODY[' + $tag = substr($tag, 5); + + // BODY[HEADER.FIELDS] request + if (!empty($this->_temp['fetchparams']['hdrfields']) && + (strpos($tag, 'HEADER.FIELDS') !== false)) { + if (!isset($tmp['headers'])) { + $tmp['headers'] = array(); + } + + // A HEADER.FIELDS entry will be tokenized thusly: + // [0] => BODY[#.HEADER.FIELDS.NOT + // [1] => Array + // ( + // [0] => MESSAGE-ID + // ) + // [2] => ]<0> + // [3] => **Header search text** + $sig = $tag . ' (' . implode(' ', array_map('strtoupper', $data[++$i])) . ')'; + + // Ignore the trailing bracket + ++$i; + + $tmp['headers'][$this->_temp['fetchparams']['hdrfields'][$sig]] = empty($this->_temp['fetchparams']['parseheaders']) + ? $data[++$i] + : Horde_MIME_Headers::parseHeaders($data[++$i]); + } else { + // Remove trailing bracket and octet start info + $tag = substr($tag, 0, strrpos($tag, ']')); + + if (!strlen($tag)) { + // BODY[] request + $tmp['fullmsg'] = $data[++$i]; + } elseif (is_numeric(substr($tag, -1))) { + // BODY[MIMEID] request + if (!isset($tmp['bodypart'])) { + $tmp['bodypart'] = array(); + } + $tmp['bodypart'][$tag] = $data[++$i]; + } else { + // BODY[HEADER|TEXT|MIME] request + if (($last_dot = strrpos($tag, '.')) === false) { + $mime_id = 0; + } else { + $mime_id = substr($tag, 0, $last_dot); + $tag = substr($tag, $last_dot + 1); + } + + $label = $section_storage[$tag]; + + if (!isset($tmp[$label])) { + $tmp[$label] = array(); + } + $tmp[$label][$mime_id] = empty($this->_temp['fetchparams']['parseheadertext']) + ? $data[++$i] + : Horde_MIME_Headers::parseHeaders($data[++$i]); + } + } + } elseif (strpos($tag, 'BINARY[') === 0) { + // Catch BINARY[*]<#> responses + // Remove the beginning 'BINARY[' and the trailing bracket + // and octet start info + $tag = substr($tag, 7, strrpos($tag, ']') - 7); + if (!isset($tmp['bodypart'])) { + $tmp['bodypart'] = $tmp['bodypartdecode'] = array(); + } + $tmp['bodypart'][$tag] = $data[++$i]; + $tmp['bodypartdecode'][$tag] = !empty($this->_temp['literal8']) ? 'binary': '8bit'; + } elseif (strpos($tag, 'BINARY.SIZE[') === 0) { + // Catch BINARY.SIZE[*] responses + // Remove the beginning 'BINARY.SIZE[' and the trailing + // bracket and octet start info + $tag = substr($tag, 12, strrpos($tag, ']') - 12); + if (!isset($tmp['bodypartsize'])) { + $tmp['bodypartsize'] = array(); + } + $tmp['bodypartsize'][$tag] = $data[++$i]; + } + break; + } + + ++$i; + } + + $this->_temp['fetchresp']['seq'][$id] = $tmp; + if (!is_null($uid)) { + $this->_temp['fetchresp']['uid'][$uid] = $tmp; + } + } + + /** + * Recursively parse BODYSTRUCTURE data from a FETCH return (see + * RFC 3501 [7.4.2]). + * + * @param array $data The tokenized information from the server. + * + * @return array The array of bodystructure information. + */ + protected function _parseBodystructure($data) + { + // If index 0 is an array, this is a multipart part. + if (is_array($data[0])) { + $ret = array( + 'parts' => array(), + 'type' => 'multipart' + ); + + // Keep going through array values until we find a non-array. + for ($i = 0, $cnt = count($data); $i < $cnt; ++$i) { + if (!is_array($data[$i])) { + break; + } + $ret['parts'][] = $this->_parseBodystructure($data[$i]); + } + + // The first string entry after an array entry gives us the + // subpart type. + $ret['subtype'] = strtolower($data[$i]); + + // After the subtype is further extension information. This + // information won't be present if this is a BODY request, and + // MAY not appear for BODYSTRUCTURE requests. + + // This is parameter information. + if (isset($data[++$i]) && is_array($data[$i])) { + $ret['parameters'] = $this->_parseStructureParams($data[$i]); + } + + // This is disposition information. + if (isset($data[++$i]) && is_array($data[$i])) { + $ret['disposition'] = strtolower($data[$i][0]); + $ret['dparameters'] = $this->_parseStructureParams($data[$i][1]); + } + + // This is body language information. + if (isset($data[++$i])) { + if (is_array($data[$i])) { + $ret['language'] = $data[$i]; + } elseif ($data[$i] != 'NIL') { + $ret['language'] = array($data[$i]); + } + } + + // This is body location information + if (isset($data[++$i]) && ($data[$i] != 'NIL')) { + $ret['location'] = $data[$i]; + } + + // There can be further information returned in the future, but + // for now we are done. + } else { + $ret = array( + 'type' => strtolower($data[0]), + 'subtype' => strtolower($data[1]), + 'parameters' => $this->_parseStructureParams($data[2]), + 'id' => ($data[3] == 'NIL') ? null : $data[3], + 'description' => ($data[4] == 'NIL') ? null : $data[4], + 'encoding' => ($data[5] == 'NIL') ? null : strtolower($data[5]), + 'size' => ($data[6] == 'NIL') ? null : $data[6] + ); + + // If the type is 'message/rfc822' or 'text/*', several extra + // fields are included + switch ($ret['type']) { + case 'message': + if ($ret['subtype'] == 'rfc822') { + $ret['envelope'] = $this->_parseEnvelope($data[7]); + $ret['structure'] = $this->_parseBodystructure($data[8]); + $ret['lines'] = $data[9]; + $i = 10; + } else { + $i = 7; + } + break; + + case 'text': + $ret['lines'] = $data[7]; + $i = 8; + break; + + default: + $i = 7; + break; + } + + // After the subtype is further extension information. This + // information won't be present if this is a BODY request, and + // MAY not appear for BODYSTRUCTURE requests. + + // This is MD5 information + if (isset($data[$i]) && ($data[$i] != 'NIL')) { + $ret['md5'] = $data[$i]; + } + + // This is disposition information + if (isset($data[++$i]) && is_array($data[$i])) { + $ret['disposition'] = strtolower($data[$i][0]); + $ret['dparameters'] = $this->_parseStructureParams($data[$i][1]); + } + + // This is body language information. + if (isset($data[++$i])) { + if (is_array($data[$i])) { + $ret['language'] = $data[$i]; + } elseif ($data[$i] != 'NIL') { + $ret['language'] = array($data[$i]); + } + } + + // This is body location information + if (isset($data[++$i]) && ($data[$i] != 'NIL')) { + $ret['location'] = $data[$i]; + } + } + + return $ret; + } + + /** + * Helper function to parse a parameters-like tokenized array. + * + * @param array $data The tokenized data. + * + * @return array The parameter array. + */ + protected function _parseStructureParams($data) + { + $ret = array(); + + if (is_array($data)) { + for ($i = 0, $cnt = count($data); $i < $cnt; ++$i) { + $ret[strtolower($data[$i])] = $data[++$i]; + } + } + + return $ret; + } + + /** + * Parse ENVELOPE data from a FETCH return (see RFC 3501 [7.4.2]). + * + * @param array $data The tokenized information from the server. + * + * @return array The array of envelope information. + */ + protected function _parseEnvelope($data) + { + $addr_structure = array( + 'personal', 'adl', 'mailbox', 'host' + ); + $env_data = array( + 0 => 'date', + 1 => 'subject', + 8 => 'in-reply-to', + 9 => 'message-id' + ); + $env_data_array = array( + 2 => 'from', + 3 => 'sender', + 4 => 'reply-to', + 5 => 'to', + 6 => 'cc', + 7 => 'bcc' + ); + + $ret = array(); + + foreach ($env_data as $key => $val) { + $ret[$val] = (strtoupper($data[$key]) == 'NIL') ? null : $data[$key]; + } + + // These entries are address structures. + foreach ($env_data_array as $key => $val) { + $ret[$val] = array(); + // Check for 'NIL' value here. + if (is_array($data[$key])) { + reset($data[$key]); + while (list(,$a_val) = each($data[$key])) { + $tmp_addr = array(); + foreach ($addr_structure as $add_key => $add_val) { + if (strtoupper($a_val[$add_key]) != 'NIL') { + $tmp_addr[$add_val] = $a_val[$add_key]; + } + } + $ret[$val][] = $tmp_addr; + } + } + } + + return $ret; + } + + /** + * Store message flag data. + * Throws a Horde_Imap_Client_Exception on error. + * + * @param array $options Additional options. + * + * @return array See Horde_Imap_Client::store(). + */ + protected function _store($options) + { + $seq = empty($options['ids']) + ? '1:*' + : ((reset($options['ids']) === self::USE_SEARCHRES) + ? '$' + : $this->toSequenceString($options['ids'])); + + $cmd_prefix = (empty($options['sequence']) ? 'UID ' : '') . + 'STORE ' . $seq . ' '; + $ucsince = !empty($options['unchangedsince']); + + if ($ucsince) { + /* RFC 4551 [3.1] - trying to do a UNCHANGEDSINCE STORE on a + * mailbox that doesn't support it will return BAD. Catch that + * here and throw an exception. */ + if (is_null($this->_temp['mailbox']['highestmodseq'])) { + throw new Horde_Imap_Client_Exception('Mailbox does not support mod-sequences.', Horde_Imap_Client_Exception::MBOXNOMODSEQ); + } + + $cmd .= '(UNCHANGEDSINCE ' . intval($options['unchangedsince']) . ') '; + } + + $this->_temp['modified'] = array(); + + if (!empty($options['replace'])) { + $this->_sendLine($cmd_prefix . 'FLAGS' . ($this->_debug ? '.SILENT' : '') . ' (' . implode(' ', $options['replace']) . ')'); + } else { + foreach (array('add' => '+', 'remove' => '-') as $k => $v) { + if (!empty($options[$k])) { + $this->_sendLine($cmd_prefix . $v . 'FLAGS' . ($this->_debug ? '.SILENT' : '') . ' (' . implode(' ', $options[$k]) . ')'); + } + } + } + + /* Update the flags in the cache. Only update if store was successful + * and flag information was not returned. */ + if (!empty($this->_temp['fetchresp']) && + isset($this->_init['enabled']['CONDSTORE'])) { + $fr = $this->_temp['fetchresp']; + $out = $uids = array(); + + if (empty($fr['uid'])) { + $res = $fr['seq']; + $seq_res = $this->_getSeqUIDLookup(array_keys($res), true); + } else { + $res = $fr['uid']; + $seq_res = null; + } + + foreach (array_keys($res) as $key) { + if (!isset($res[$key]['flags'])) { + $uids[is_null($seq_res) ? $key : $seq_res['lookup'][$key]] = $res[$key]['modseq']; + } + } + + /* Get the list of flags from the cache. */ + if (empty($options['replace'])) { + $data = $this->_cacheOb->get($this->_selected, array_keys($uids), array('HICflags'), $this->_temp['mailbox']['uidvalidity']); + + foreach ($uids as $uid => $modseq) { + $flags = isset($data[$uid]['HICflags']) ? $data[$uid]['HICflags'] : array(); + if (!empty($options['add'])) { + $flags = array_merge($flags, $options['add']); + } + if (!empty($options['remove'])) { + $flags = array_diff($flags, $options['remove']); + } + $out[$uid] = array('modseq' => $uids[$uid], 'flags' => array_keys(array_flip($flags))); + } + } else { + foreach ($uids as $uid => $modseq) { + $out[$uid] = array('modseq' => $uids[$uid], 'flags' => $options['replace']); + } + } + + $this->_updateCache($out); + } + + return $this->_temp['modified']; + } + + /** + * Copy messages to another mailbox. + * Throws a Horde_Imap_Client_Exception on error. + * + * @param string $dest The destination mailbox (UTF7-IMAP). + * @param array $options Additional options. + * + * @return mixed An array mapping old UIDs (keys) to new UIDs (values) on + * success (if the IMAP server and/or driver support the + * UIDPLUS extension) or true. + */ + protected function _copy($dest, $options) + { + $this->_temp['copyuid'] = $this->_temp['trycreate'] = null; + $this->_temp['uidplusmbox'] = $dest; + + $seq = empty($options['ids']) + ? '1:*' + : ((reset($options['ids']) === self::USE_SEARCHRES) + ? '$' + : $this->toSequenceString($options['ids'])); + + // COPY returns no untagged information (RFC 3501 [6.4.7]) + try { + $this->_sendLine((empty($options['sequence']) ? 'UID ' : '') . 'COPY ' . $seq . ' ' . $this->escape($dest)); + } catch (Horde_Imap_Client_Exception $e) { + if (!empty($options['create']) && $this->_temp['trycreate']) { + $this->createMailbox($dest); + unset($options['create']); + return $this->_copy($dest, $options); + } + throw $e; + } + + // If moving, delete the old messages now. + if (!empty($options['move'])) { + $opts = array('ids' => empty($options['ids']) ? array() : $options['ids'], 'sequence' => !empty($options['sequence'])); + $this->store($this->_selected, array_merge(array('add' => array('\\deleted')), $opts)); + $this->expunge($this->_selected, $opts); + } + + /* UIDPLUS (RFC 4315) allows easy determination of the UID of the + * copied messages. If UID not returned, then destination mailbox + * does not support persistent UIDs. + * @todo Use UIDPLUS information to move cached data to new + * mailbox (see RFC 4549 [4.2.2.1]). */ + return is_null($this->_temp['copyuid']) + ? true + : $this->_temp['copyuid']; + } + + /** + * Set quota limits. + * Throws a Horde_Imap_Client_Exception on error. + * + * @param string $root The quota root (UTF7-IMAP). + * @param array $options Additional options. + */ + protected function _setQuota($root, $options) + { + $this->login(); + + $limits = array(); + if (isset($options['messages'])) { + $limits[] = 'MESSAGE ' . $options['messages']; + } + if (isset($options['storage'])) { + $limits[] = 'STORAGE ' . $options['storage']; + } + + $this->_sendLine('SETQUOTA ' . $this->escape($root) . ' (' . implode(' ', $limits) . ')'); + } + + /** + * Get quota limits. + * Throws a Horde_Imap_Client_Exception on error. + * + * @param string $root The quota root (UTF7-IMAP). + * + * @return mixed An array with these possible keys: 'messages' and + * 'storage'; each key holds an array with 2 values: + * 'limit' and 'usage'. + */ + protected function _getQuota($root) + { + $this->login(); + + $this->_temp['quotaresp'] = array(); + $this->_sendLine('GETQUOTA ' . $this->escape($root)); + return reset($this->_temp['quotaresp']); + } + + /** + * Parse a QUOTA response (RFC 2087 [5.1]). + * + * @param array $data The server response. + */ + protected function _parseQuota($data) + { + $c = &$this->_temp['quotaresp']; + + $root = $data[0]; + $c[$root] = array(); + + for ($i = 0, $len = count($data[1]); $i < $len; $i += 3) { + if (in_array($data[1][$i], array('MESSAGE', 'STORAGE'))) { + $c[$root][strtolower($data[1][$i])] = array('limit' => $data[1][$i + 2], 'usage' => $data[1][$i + 1]); + + } + } + } + + /** + * Get quota limits for a mailbox. + * Throws a Horde_Imap_Client_Exception on error. + * + * @param string $mailbox A mailbox (UTF7-IMAP). + * + * @return mixed An array with the keys being the quota roots. Each key + * holds an array with two possible keys: 'messages' and + * 'storage'; each of these keys holds an array with 2 + * values: 'limit' and 'usage'. + */ + protected function _getQuotaRoot($mailbox) + { + $this->login(); + + $this->_temp['quotaresp'] = array(); + $this->_sendLine('GETQUOTAROOT ' . $this->escape($mailbox)); + return $this->_temp['quotaresp']; + } + + /** + * Set ACL rights for a given mailbox/identifier. + * Throws a Horde_Imap_Client_Exception on error. + * + * @param string $mailbox A mailbox (UTF7-IMAP). + * @param string $identifier The identifier to alter (UTF7-IMAP). + * @param array $options Additional options. + */ + protected function _setACL($mailbox, $identifier, $options) + { + $this->login(); + + // SETACL/DELETEACL returns no untagged information (RFC 4314 [3.1 & + // 3.2]). + if (empty($options['rights']) && !empty($options['remove'])) { + $this->_sendLine('DELETEACL ' . $this->escape($mailbox) . ' ' . $identifier); + } else { + $this->_sendLine('SETACL ' . $this->escape($mailbox) . ' ' . $identifier . ' ' . $options['rights']); + } + } + + /** + * Get ACL rights for a given mailbox. + * Throws a Horde_Imap_Client_Exception on error. + * + * @param string $mailbox A mailbox (UTF7-IMAP). + * + * @return array An array with identifiers as the keys and an array of + * rights as the values. + */ + protected function _getACL($mailbox) + { + $this->login(); + + $this->_temp['getacl'] = array(); + $this->_sendLine('GETACL ' . $this->escape($mailbox)); + return $this->_temp['getacl']; + } + + /** + * Parse an ACL response (RFC 4314 [3.6]). + * + * @param array $data The server response. + */ + protected function _parseACL($data) + { + $acl = &$this->_temp['getacl']; + + // Ignore mailbox argument -> index 1 + for ($i = 1, $len = count($data); $i < $len; $i += 2) { + $acl[$data[$i]] = str_split($data[$i + 1]); + } + } + + /** + * Get ACL rights for a given mailbox/identifier. + * Throws a Horde_Imap_Client_Exception on error. + * + * @param string $mailbox A mailbox (UTF7-IMAP). + * @param string $identifier The identifier (US-ASCII). + * + * @return array An array of rights (keys: 'required' and 'optional'). + */ + protected function _listACLRights($mailbox, $identifier) + { + $this->login(); + + $this->_temp['listaclrights'] = array(); + $this->_sendLine('LISTRIGHTS ' . $this->escape($mailbox) . ' ' . $identifier); + return $this->_temp['listaclrights']; + } + + /** + * Parse a LISTRIGHTS response (RFC 4314 [3.7]). + * + * @param array $data The server response. + */ + protected function _parseListRights($data) + { + // Ignore mailbox and identifier arguments + $this->_temp['myrights'] = array( + 'required' => str_split($data[2]), + 'optional' => array_slice($data, 3) + ); + } + + /** + * Get the ACL rights for the current user for a given mailbox. + * Throws a Horde_Imap_Client_Exception on error. + * + * @param string $mailbox A mailbox (UTF7-IMAP). + * + * @return array An array of rights. + */ + protected function _getMyACLRights($mailbox) + { + $this->login(); + + $this->_temp['myrights'] = array(); + $this->_sendLine('MYRIGHTS ' . $this->escape($mailbox)); + return $this->_temp['myrights']; + } + + /** + * Parse a MYRIGHTS response (RFC 4314 [3.8]). + * + * @param array $data The server response. + */ + protected function _parseMyRights($data) + { + $this->_temp['myrights'] = $data[1]; + } + + /* Internal functions. */ + + /** + * Perform a command on the IMAP server. A connection to the server must + * have already been made. + * Throws a Horde_Imap_Client_Exception on error. + * + * @todo RFC 3501 allows the sending of multiple commands at once. For + * simplicity of implementation at this time, we will execute + * commands one at a time. This allows us to easily determine data + * meant for a command while scanning for untagged responses + * unilaterally sent by the server. + * + * @param string $query The IMAP command to execute. + * @param array $options Additional options: + *
+     * 'binary' - (boolean) Does $query contain binary data?  If so, and the
+     *            'BINARY' extension is available on the server, the data
+     *            will be sent in literal8 format. If not available, an
+     *            exception will be returned. 'binary' requires literal to
+     *            be defined.
+     *            DEFAULT: Sends literals in a non-binary compliant method.
+     * 'debug' - (string) When debugging, send this string instead of the
+     *           actual command/data sent.
+     *           DEFAULT: Raw data output to debug stream.
+     * 'literal' - (integer) Send the command followed by a literal. The value
+     *             of 'literal' is the length of the literal data.
+     *             Will attempt to use LITERAL+ capability if possible.
+     *             DEFAULT: Do not send literal
+     * 'literaldata' - (boolean) Is this literal data?  If so, will parse the
+     *                 server response based on the existence of LITERAL+.
+     *                 DEFAULT: Server specific.
+     * 'noparse' - (boolean) Don't parse the response and instead return the
+     *             server response.
+     *             DEFAULT: Parses the response
+     * 'notag' - (boolean) Don't prepend an IMAP tag (i.e. for a continuation
+     *           response).
+     *           DEFAULT: false
+     * 
+ */ + protected function _sendLine($query, $options = array()) + { + if (empty($options['notag'])) { + $query = ++$this->_tag . ' ' . $query; + + /* Catch all FETCH responses until a tagged response. */ + $this->_temp['fetchresp'] = array('seq' => array(), 'uid' => array()); + } + + $continuation = $literalplus = false; + + if (!empty($options['literal']) || !empty($options['literaldata'])) { + if ($this->queryCapability('LITERAL+')) { + /* RFC 2088 - If LITERAL+ is available, saves a roundtrip + * from the server. */ + $literalplus = true; + } else { + $continuation = true; + } + + if (!empty($options['literal'])) { + $query .= ' '; + + // RFC 3516 - Send literal8 if we have binary data. + if (!empty($options['binary'])) { + if (!$this->queryCapability('BINARY')) { + throw new Horde_Imap_Client_Exception('Can not send binary data to server that does not support it.', Horde_Imap_Client_Exception::NOSUPPORTIMAPEXT); + } + $query .= '~'; + } + + $query .= '{' . $options['literal'] . ($literalplus ? '+' : '') . '}'; + } + } + + if ($this->_debug) { + fwrite($this->_debug, 'C: ' . (empty($options['debug']) ? $query : $options['debug']) . "\n"); + } + + fwrite($this->_stream, $query . "\r\n"); + + if ($literalplus) { + return; + } + + if ($continuation) { + $ob = $this->_getLine(); + if ($ob['type'] != 'continuation') { + throw new Horde_Imap_Client_Exception('Unexpected response from IMAP server while waiting for a continuation request: ' . $ob['line']); + } + } elseif (empty($options['noparse'])) { + $this->_parseResponse($this->_tag); + } else { + return $this->_getLine(); + } + } + + /** + * Gets a line from the IMAP stream and parses it. + * Throws a Horde_Imap_Client_Exception on error. + * + * @return array An array with the following keys: + *
+     * 'line' - (string) The server response text (set for all but an untagged
+     *          response with no response code).
+     * 'response' - (string) Either 'OK', 'NO', 'BAD', 'PREAUTH', or ''.
+     * 'tag' - (string) If tagged response, the tag string.
+     * 'token' - (array) The tokenized response (set if an untagged response
+     *           with no response code).
+     * 'type' - (string) Either 'tagged', 'untagged', or 'continuation'.
+     * 
+ */ + protected function _getLine() + { + $ob = array('line' => '', 'response' => '', 'tag' => '', 'token' => ''); + + if (feof($this->_stream)) { + $this->_temp['logout'] = 2; + $this->logout(); + throw new Horde_Imap_Client_Exception('IMAP Server closed the connection unexpectedly.', Horde_Imap_Client_Exception::IMAP_DISCONNECT); + } + + $read = rtrim(fgets($this->_stream)); + if (empty($read)) { + return; + } + + if ($this->_debug) { + fwrite($this->_debug, 'S: ' . $read . "\n"); + } + + $read = explode(' ', $read, 3); + + switch ($read[0]) { + /* Continuation response. */ + case '+': + $ob['line'] = implode(' ', array_slice($read, 1)); + $ob['type'] = 'continuation'; + break; + + /* Untagged response. */ + case '*': + $ob['type'] = 'untagged'; + + $read[1] = strtoupper($read[1]); + if ($read[1] == 'BYE') { + if (!empty($this->_temp['logout']) && + ($this->_temp['logout'] == 1)) { + /* A BYE response received as part of a logout cmd should + * be treated like a regular command. A client MUST + * process the entire command until logging out. RFC 3501 + * [3.4]. */ + $ob['response'] = $read[1]; + $ob['line'] = implode(' ', array_slice($read, 2)); + } else { + $this->_temp['logout'] = 2; + $this->logout(); + throw new Horde_Imap_Client_Exception('IMAP Server closed the connection: ' . implode(' ', array_slice($read, 1)), Horde_Imap_Client_Exception::IMAP_DISCONNECT); + } + } + + if (in_array($read[1], array('OK', 'NO', 'BAD', 'PREAUTH'))) { + $ob['response'] = $read[1]; + $ob['line'] = implode(' ', array_slice($read, 2)); + } else { + /* Tokenize response. */ + $line = implode(' ', array_slice($read, 1)); + $binary = $literal = false; + $this->_temp['token'] = null; + $this->_temp['literal8'] = array(); + + do { + $literal_len = null; + + if (!$literal && (substr($line, -1) == '}')) { + $pos = strrpos($line, '{'); + $literal_len = substr($line, $pos + 1, -1); + if (is_numeric($literal_len)) { + + // Check for literal8 response + if ($line[$pos - 1] == '~') { + $binary = true; + $line = substr($line, 0, $pos - 1); + $this->_temp['literal8'][substr($line, strrpos($line, ' '))] = true; + } else { + $line = substr($line, 0, $pos); + } + } else { + $literal_len = null; + } + } + + if ($literal) { + $this->_temp['token']['ptr'][$this->_temp['token']['paren']][] = $line; + } else { + $this->_tokenizeData($line); + } + + if (is_null($literal_len)) { + if (!$literal) { + break; + } + $binary = $literal = false; + $line = rtrim(fgets($this->_stream)); + } else { + $literal = true; + $line = ''; + while ($literal_len) { + $data_read = fread($this->_stream, min($literal_len, 8192)); + $literal_len -= strlen($data_read); + $line .= rtrim($data_read); + } + } + + if ($this->_debug) { + $debug_line = $binary + ? "[BINARY DATA - $literal_len bytes]" + : $line; + fwrite($this->_debug, 'S: ' . $debug_line . "\n"); + } + } while (true); + + $ob['token'] = $this->_temp['token']['out']; + } + break; + + /* Tagged response. */ + default: + $ob['type'] = 'tagged'; + $ob['line'] = implode(' ', array_slice($read, 2)); + $ob['tag'] = $read[0]; + $ob['response'] = $read[1]; + break; + } + + return $ob; + } + + /** + * Tokenize IMAP data. Handles quoted strings and parantheses. + * + * @param string $line The raw IMAP data. + */ + protected function _tokenizeData($line) + { + if (is_null($this->_temp['token'])) { + $this->_temp['token'] = array( + 'in_quote' => false, + 'paren' => 0, + 'out' => array(), + 'ptr' => array() + ); + $this->_temp['token']['ptr'][0] = &$this->_temp['token']['out']; + } + + $c = &$this->_temp['token']; + $tmp = ''; + + for ($i = 0, $len = strlen($line); $i < $len; ++$i) { + $char = $line[$i]; + switch ($char) { + case '"': + if ($c['in_quote']) { + if ($i && ($line[$i - 1] != '//')) { + $c['in_quote'] = false; + $c['ptr'][$c['paren']][] = stripcslashes($tmp); + $tmp = ''; + } else { + $tmp .= $char; + } + } else { + $c['in_quote'] = true; + } + break; + + default: + if ($c['in_quote']) { + $tmp .= $char; + break; + } + + switch ($char) { + case '(': + $c['ptr'][$c['paren']][] = array(); + $c['ptr'][$c['paren'] + 1] = &$c['ptr'][$c['paren']][count($c['ptr'][$c['paren']]) - 1]; + ++$c['paren']; + break; + + case ')': + if (strlen($tmp)) { + $c['ptr'][$c['paren']][] = $tmp; + $tmp = ''; + } + --$c['paren']; + break; + + case ' ': + if (strlen($tmp)) { + $c['ptr'][$c['paren']][] = $tmp; + $tmp = ''; + } + break; + + default: + $tmp .= $char; + break; + } + break; + } + } + + if (strlen($tmp)) { + $c['ptr'][$c['paren']][] = $tmp; + } + } + + /** + * Parse all untagged and tagged responses for a given command. + * Throws a Horde_Imap_Client_Exception on error. + * + * @param string $tag The IMAP tag of the current command. + */ + protected function _parseResponse($tag) + { + while ($ob = $this->_getLine()) { + if (($ob['type'] == 'tagged') && ($ob['tag'] == $tag)) { + // Here we know there isn't an untagged response, so directly + // call _parseStatusResponse(). + $this->_parseStatusResponse($ob); + + // Now that any status response has been processed, we can + // throw errors if appropriate. + switch ($ob['response']) { + case 'BAD': + case 'NO': + if (empty($this->_temp['parsestatuserr'])) { + $errcode = 0; + $errstr = empty($ob['line']) ? '[No error message returned by server.]' : $ob['line']; + } else { + list($errcode, $errstr) = $this->_temp['parsestatuserr']; + } + $this->_temp['parseresperr'] = $ob; + + if ($ob['response'] == 'BAD') { + throw new Horde_Imap_Client_Exception('Bad IMAP request: ' . $errstr, $errcode); + } else { + throw new Horde_Imap_Client_Exception('IMAP error: ' . $errstr, $errcode); + } + } + + /* Update the cache, if needed. */ + $tmp = $this->_temp['fetchresp']; + if (!empty($tmp['uid'])) { + $this->_updateCache($tmp['uid']); + } elseif (!empty($tmp['seq'])) { + $this->_updateCache($tmp['seq'], array('seq' => true)); + } + + break; + } + $this->_parseServerResponse($ob); + } + } + + /** + * Handle unilateral server responses - untagged data not returned from an + * explicit server call (see RFC 3501 [2.2.2]). + * + * @param array An array returned from self::_getLine(). + */ + protected function _parseServerResponse($ob) + { + if (!empty($ob['response'])) { + $this->_parseStatusResponse($ob); + } else { + // First, catch all untagged responses where the name appears + // first on the line. + switch (strtoupper($ob['token'][0])) { + case 'CAPABILITY': + $this->_parseCapability(array_slice($ob['token'], 1)); + break; + + case 'LIST': + case 'LSUB': + $this->_parseList($ob['token'], 1); + break; + + case 'STATUS': + // Parse a STATUS response (RFC 3501 [7.2.4]). + $this->_parseStatus($ob['token'][2]); + break; + + case 'SEARCH': + case 'SORT': + // Parse a SEARCH/SORT response (RFC 3501 [7.2.5] & + // RFC 5256 [4]). + $this->_parseSearch(array_slice($ob['token'], 1)); + break; + + case 'ESEARCH': + // Parse an ESEARCH response (RFC 4466 [2.6.2]). + $this->_parseEsearch(array_slice($ob['token'], 1)); + break; + + case 'FLAGS': + $this->_temp['mailbox']['flags'] = array_map('strtolower', $ob['token'][1]); + break; + + case 'QUOTA': + $this->_parseQuota(array_slice($ob['token'], 1)); + break; + + case 'QUOTAROOT': + // Ignore this line - we can get this information from + // the untagged QUOTA responses. + break; + + case 'NAMESPACE': + $this->_parseNamespace(array_slice($ob['token'], 1)); + break; + + case 'THREAD': + foreach (array_slice($ob['token'], 1) as $val) { + $this->_parseThread($val); + } + break; + + case 'ACL': + $this->_parseACL(array_slice($ob['token'], 1)); + break; + + case 'LISTRIGHTS': + $this->_parseListRights(array_slice($ob['token'], 1)); + break; + + case 'MYRIGHTS': + $this->_parseMyRights(array_slice($ob['token'], 1)); + break; + + case 'ID': + // ID extension (RFC 2971) + $this->_parseID(array_slice($ob['token'], 1)); + break; + + case 'ENABLED': + // ENABLE extension (RFC 5161) + $this->_parseEnabled(array_slice($ob['token'], 1)); + break; + + case 'LANGUAGE': + // LANGUAGE extension (RFC 5255 [3.2]) + $this->_parseLanguage(array_slice($ob['token'], 1)); + break; + + case 'COMPARATOR': + // I18NLEVEL=2 extension (RFC 5255 [4.7]) + $this->_parseComparator(array_slice($ob['token'], 1)); + break; + + case 'VANISHED': + // QRESYNC extension (RFC 5162 [3.6]) + $this->_parseVanished(array_slice($ob['token'], 1)); + break; + + default: + // Next, look for responses where the keywords occur second. + $type = strtoupper($ob['token'][1]); + switch ($type) { + case 'EXISTS': + case 'RECENT': + // RECENT response - RFC 3501 [7.3.1] + // EXISTS response - RFC 3501 [7.3.2] + $this->_temp['mailbox'][$type == 'RECENT' ? 'recent' : 'messages'] = $ob['token'][0]; + break; + + case 'EXPUNGE': + // EXPUNGE response - RFC 3501 [7.4.1] + $this->_parseExpunge($ob['token'][0]); + break; + + case 'FETCH': + // FETCH response - RFC 3501 [7.4.2] + $this->_parseFetch($ob['token'][0], reset(array_slice($ob['token'], 2))); + break; + } + break; + } + } + } + + /** + * Handle status responses (see RFC 3501 [7.1]). + * + * @param array An array returned from self::_getLine(). + */ + protected function _parseStatusResponse($ob) + { + if ($ob['line'][0] != '[') { + return; + } + + $pos = strpos($ob['line'], ' ', 2); + $end_pos = strpos($ob['line'], ']', 2); + if ($pos > $end_pos) { + $code = strtoupper(substr($ob['line'], 1, $end_pos - 1)); + $data = null; + } else { + $code = strtoupper(substr($ob['line'], 1, $pos - 1)); + $data = substr($ob['line'], $pos + 1, $end_pos - $pos - 1); + } + + $this->_temp['parsestatuserr'] = null; + + switch ($code) { + case 'ALERT': + if (!isset($this->_temp['alerts'])) { + $this->_temp['alerts'] = array(); + } + $this->_temp['alerts'][] = $data; + break; + + case 'BADCHARSET': + /* @todo Store the list of search charsets supported by the server + * (this is a MAY response, not a MUST response) */ + $this->_temp['parsestatuserr'] = array( + Horde_Imap_Client_Exception::BADCHARSET, + substr($ob['line'], $end_pos + 2) + ); + break; + + case 'CAPABILITY': + $this->_temp['token'] = null; + $this->_tokenizeData($data); + $this->_parseCapability($this->_temp['token']['out']); + break; + + case 'PARSE': + $this->_temp['parsestatuserr'] = array( + Horde_Imap_Client_Exception::PARSEERROR, + substr($ob['line'], $end_pos + 2) + ); + break; + + case 'READ-ONLY': + case 'READ-WRITE': + // Ignore - openMailbox() takes care of this for us + break; + + case 'TRYCREATE': + // RFC 3501 [7.1] + $this->_temp['trycreate'] = true; + break; + + case 'PERMANENTFLAGS': + $this->_temp['token'] = null; + $this->_tokenizeData($data); + $this->_temp['mailbox']['permflags'] = array_map('strtolower', reset($this->_temp['token']['out'])); + break; + + case 'UIDNEXT': + case 'UIDVALIDITY': + $this->_temp['mailbox'][strtolower($code)] = $data; + break; + + case 'UNSEEN': + /* This is different from the STATUS UNSEEN response - this item, + * if defined, returns the first UNSEEN message in the mailbox. */ + $this->_temp['mailbox']['firstunseen'] = $data; + break; + + case 'REFERRAL': + // Defined by RFC 2221 + $this->_temp['referral'] = $this->parseImapURL($data); + break; + + case 'UNKNOWN-CTE': + // Defined by RFC 3516 + $this->_temp['parsestatuserr'] = array( + Horde_Imap_Client_Exception::UNKNOWNCTE, + substr($ob['line'], $end_pos + 2) + ); + break; + + case 'APPENDUID': + case 'COPYUID': + // Defined by RFC 4315 + // APPENDUID: [0] = UIDVALIDITY, [1] = UID(s) + // COPYUID: [0] = UIDVALIDITY, [1] = UIDFROM, [2] = UIDTO + $parts = explode(' ', $data); + + if (($this->_selected == $this->_temp['uidplusmbox']) && + ($this->_temp['mailbox']['uidvalidity'] != $parts[0])) { + $this->_temp['mailbox'] = array('uidvalidity' => $parts[0]); + $this->_temp['searchnotsaved'] = true; + } + + /* Check for cache expiration (see RFC 4549 [4.1]). */ + $this->_updateCache(array(), array('mailbox' => $this->_temp['uidplusmbox'], 'uidvalid' => $parts[0])); + + if ($code == 'APPENDUID') { + $this->_temp['appenduid'] = array_merge($this->_temp['appenduid'], $this->fromSequenceString($parts[1])); + } else { + $this->_temp['copyuid'] = array_combine($this->fromSequenceString($parts[1]), $this->fromSequenceString($parts[2])); + } + break; + + case 'UIDNOTSTICKY': + // Defined by RFC 4315 [3] + $this->_temp['mailbox']['uidnotsticky'] = true; + break; + + case 'HIGHESTMODSEQ': + case 'NOMODSEQ': + // Defined by RFC 4551 [3.1.1 & 3.1.2] + $this->_temp['mailbox']['highestmodseq'] = ($code == 'HIGHESTMODSEQ') ? $data : null; + break; + + case 'MODIFIED': + // Defined by RFC 4551 [3.2] + $this->_temp['modified'] = $this->fromSequenceString($data); + break; + + case 'CLOSED': + // Defined by RFC 5162 [3.7] + if (isset($this->_temp['qresyncmbox'])) { + $this->_temp['mailbox'] = array('name' => $this->_temp['qresyncmbox']); + $this->_selected = $this->_temp['qresyncmbox']; + } + break; + + case 'NOTSAVED': + // Defined by RFC 5182 [2.5] + $this->_temp['searchnotsaved'] = true; + break; + + case 'BADCOMPARATOR': + // Defined by RFC 5255 [4.9] + $this->_temp['parsestatuserr'] = array( + Horde_Imap_Client_Exception::BADCOMPARATOR, + substr($ob['line'], $end_pos + 2) + ); + break; + + case 'XPROXYREUSE': + // The proxy connection was reused, so no need to do login tasks. + $this->_temp['proxyreuse'] = true; + break; + + default: + // Unknown response codes SHOULD be ignored - RFC 3501 [7.1] + break; + } + } +} diff --git a/framework/Imap_Client/lib/Horde/Imap/Client/Sort.php b/framework/Imap_Client/lib/Horde/Imap/Client/Sort.php new file mode 100644 index 000000000..d431312e8 --- /dev/null +++ b/framework/Imap_Client/lib/Horde/Imap/Client/Sort.php @@ -0,0 +1,116 @@ + + * @category Horde + * @package Horde_Imap_Client + */ +class Horde_Imap_Client_Sort +{ + /** + * The delimiter character to use. + * + * @var string + */ + private static $_delimiter = '.'; + + /** + * Should we sort with 'INBOX' at the front of the list? + * + * @var boolean + */ + private static $_sortinbox = true; + + /** + * Sort a list of mailboxes. + * $mbox will be sorted after running this function. + * + * @param array &$mbox The list of mailboxes to sort. + * @param array $options Additional options: + *
+     * 'delimiter' - (string) The delimiter to use.
+     *               DEFAULT: '.'
+     * 'inbox' - (boolean) Always put 'INBOX' at the head of the list?
+     *           DEFAULT: Yes
+     * 'index' - (boolean) If sorting by value ('keysort' is false), maintain
+     *           key index association?
+     *           DEFAULT: No
+     * 'keysort' - (boolean) Sort by $mbox's keys?
+     *             DEFAULT: Sort by $mbox values.
+     * 
+ */ + public static final function sortMailboxes(&$mbox, $options) + { + if (isset($options['delimiter'])) { + self::$_delimiter = $options['delimiter']; + } + + if (empty($options['inbox'])) { + self::$_sortinbox = false; + } + + $cmp = array('Horde_Imap_Client_Sort', 'mboxCompare'); + if (!empty($options['keysort'])) { + uksort($mbox, $cmp); + } elseif (!empty($options['index'])) { + uasort($mbox, $cmp); + } else { + usort($mbox, $cmp); + } + } + + /** + * Hierarchical folder sorting function (used with usort()). + * + * @param string $a Comparison item 1. + * @param string $b Comparison item 2. + * + * @return integer See usort(). + */ + public static final function mboxCompare($a, $b) + { + /* Always return INBOX as "smaller". */ + if (self::$_sortinbox) { + if (strcasecmp($a, 'INBOX') == 0) { + return -1; + } elseif (strcasecmp($b, 'INBOX') == 0) { + return 1; + } + } + + $a_parts = explode(self::$_delimiter, $a); + $b_parts = explode(self::$_delimiter, $b); + + $a_count = count($a_parts); + $b_count = count($b_parts); + + for ($i = 0, $iMax = min($a_count, $b_count); $i < $iMax; ++$i) { + if ($a_parts[$i] != $b_parts[$i]) { + /* If only one of the folders is under INBOX, return it as + * "smaller". */ + if (self::$_sortinbox && ($i == 0)) { + $a_base = (strcasecmp($a_parts[0], 'INBOX') == 0); + $b_base = (strcasecmp($b_parts[0], 'INBOX') == 0); + if ($a_base && !$b_base) { + return -1; + } elseif (!$a_base && $b_base) { + return 1; + } + } + $cmp = strnatcasecmp($a_parts[$i], $b_parts[$i]); + return ($cmp == 0) ? strcmp($a_parts[$i], $b_parts[$i]) : $cmp; + } + } + + return ($a_count - $b_count); + } +} diff --git a/framework/Imap_Client/lib/Horde/Imap/Client/Utf7imap.php b/framework/Imap_Client/lib/Horde/Imap/Client/Utf7imap.php new file mode 100644 index 000000000..46ce7e8d6 --- /dev/null +++ b/framework/Imap_Client/lib/Horde/Imap/Client/Utf7imap.php @@ -0,0 +1,287 @@ + + * Released under the GPL (version 2) + * + * Translated from C to PHP by Thomas Bruederli + * Code extracted from the RoundCube Webmail (http://roundcube.net) project, + * SVN revision 1757 + * The RoundCube project is released under the GPL (version 2) + * + * Copyright 2008 The Horde Project (http://www.horde.org/) + * + * $Horde: framework/Imap_Client/lib/Horde/Imap/Client/Utf7imap.php,v 1.4 2008/10/09 04:43:26 slusarz Exp $ + * + * See the enclosed file COPYING for license information (LGPL). If you + * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html. + * + * @author Michael Slusarz + * @category Horde + * @package Horde_Imap_Client + */ +class Horde_Imap_Client_Utf7imap +{ + /** + * Lookup table for conversion. + * + * @var array + */ + private static $_index64 = array( + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, 63, -1, -1, -1, + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, + -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, + -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, + 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1 + ); + + /** + * Lookup table for conversion. + * + * @var array + */ + private static $_base64 = array( + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', + 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', + 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', + 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', + '4', '5', '6', '7', '8', '9', '+', ',' + ); + + /** + * Is mbstring extension available? + * + * @var array + */ + private static $_mbstring = null; + + /** + * Convert a string from UTF7-IMAP to UTF-8. + * Throws a Horde_Imap_Client_Exception on error. + * + * @param string The UTF7-IMAP string. + * + * @return string The converted UTF-8 string. + */ + public static function Utf7ImapToUtf8($str) + { + /* Try mbstring, if available, which should be faster. Don't use the + * IMAP utf7_* functions because they are known to be buggy. */ + if (is_null(self::$_mbstring)) { + self::$_mbstring = extension_loaded('mbstring'); + } + if (self::$_mbstring) { + $old_error = error_reporting(0); + $output = mb_convert_encoding($str, 'UTF-8', 'UTF7-IMAP'); + error_reporting($old_error); + return $output; + } + + $str = strval($str); + $p = ''; + $ptr = &self::$_index64; + + for ($i = 0, $u7len = strlen($str); $u7len > 0; ++$i, --$u7len) { + $u7 = $str[$i]; + if ($u7 == '&') { + $u7 = $str[++$i]; + if (--$u7len && ($u7 == '-')) { + $p .= '&'; + continue; + } + + $ch = 0; + $k = 10; + for (; $u7len > 0; ++$i, --$u7len) { + $u7 = $str[$i]; + + if ((ord($u7) & 0x80) || ($b = $ptr[ord($u7)]) == -1) { + break; + } + + if ($k > 0) { + $ch |= $b << $k; + $k -= 6; + } else { + $ch |= $b >> (-$k); + if ($ch < 0x80) { + /* Printable US-ASCII */ + if ((0x20 <= $ch) && ($ch < 0x7f)) { + throw new Horde_Imap_Client_Exception('Error converting string.', Horde_Imap_Client_Exception::UTF7IMAP_CONVERSION); + } + $p .= chr($ch); + } else if ($ch < 0x800) { + $p .= chr(0xc0 | ($ch >> 6)) . + chr(0x80 | ($ch & 0x3f)); + } else { + $p .= chr(0xe0 | ($ch >> 12)) . + chr(0x80 | (($ch >> 6) & 0x3f)) . + chr(0x80 | ($ch & 0x3f)); + } + + $ch = ($b << (16 + $k)) & 0xffff; + $k += 10; + } + } + + /* Non-zero or too many extra bits. */ + if ($ch || ($k < 6)) { + throw new Horde_Imap_Client_Exception('Error converting string.', Horde_Imap_Client_Exception::UTF7IMAP_CONVERSION); + } + + /* Base64 not properly terminated. */ + if (!$u7len || $u7 != '-') { + throw new Horde_Imap_Client_Exception('Error converting string.', Horde_Imap_Client_Exception::UTF7IMAP_CONVERSION); + } + + /* Adjacent Base64 sections. */ + if (($u7len > 2) && + ($str[$i + 1] == '&') && + ($str[$i + 2] != '-')) { + throw new Horde_Imap_Client_Exception('Error converting string.', Horde_Imap_Client_Exception::UTF7IMAP_CONVERSION); + } + } elseif ((ord($u7) < 0x20) || (ord($u7) >= 0x7f)) { + /* Not printable US-ASCII */ + throw new Horde_Imap_Client_Exception('Error converting string.', Horde_Imap_Client_Exception::UTF7IMAP_CONVERSION); + } else { + $p .= $u7; + } + } + + return $p; + } + + /** + * Convert a string from UTF-8 to UTF7-IMAP. + * Throws a Horde_Imap_Client_Exception on error. + * + * @param string The UTF-8 string. + * + * @return string The converted UTF7-IMAP string. + */ + public static function Utf8ToUtf7Imap($str) + { + /* No need to do conversion if all chars are in US-ASCII range. */ + if (!preg_match('/[\x80-\xff]/', $str)) { + return $str; + } + + /* Try mbstring, if available, which should be faster. Don't use the + * IMAP utf7_* functions because they are known to be buggy. */ + if (is_null(self::$_mbstring)) { + self::$_mbstring = extension_loaded('mbstring'); + } + if (self::$_mbstring) { + $old_error = error_reporting(0); + $output = mb_convert_encoding($str, 'UTF7-IMAP', 'UTF-8'); + error_reporting($old_error); + return $output; + } + + $u8len = strlen($str); + $i = 0; + $base64 = false; + $p = ''; + $ptr = &self::$_base64; + + while ($u8len) { + $u8 = $str[$i]; + $c = ord($u8); + + if ($c < 0x80) { + $ch = $c; + $n = 0; + } elseif ($c < 0xc2) { + throw new Horde_Imap_Client_Exception('Error converting string.', Horde_Imap_Client_Exception::UTF7IMAP_CONVERSION); + } elseif ($c < 0xe0) { + $ch = $c & 0x1f; + $n = 1; + } elseif ($c < 0xf0) { + $ch = $c & 0x0f; + $n = 2; + } elseif ($c < 0xf8) { + $ch = $c & 0x07; + $n = 3; + } elseif ($c < 0xfc) { + $ch = $c & 0x03; + $n = 4; + } elseif ($c < 0xfe) { + $ch = $c & 0x01; + $n = 5; + } else { + throw new Horde_Imap_Client_Exception('Error converting string.', Horde_Imap_Client_Exception::UTF7IMAP_CONVERSION); + } + + if ($n > --$u8len) { + throw new Horde_Imap_Client_Exception('Error converting string.', Horde_Imap_Client_Exception::UTF7IMAP_CONVERSION); + } + + ++$i; + + for ($j = 0; $j < $n; ++$j) { + $o = ord($str[$i + $j]); + if (($o & 0xc0) != 0x80) { + throw new Horde_Imap_Client_Exception('Error converting string.', Horde_Imap_Client_Exception::UTF7IMAP_CONVERSION); + } + $ch = ($ch << 6) | ($o & 0x3f); + } + + if (($n > 1) && !($ch >> ($n * 5 + 1))) { + throw new Horde_Imap_Client_Exception('Error converting string.', Horde_Imap_Client_Exception::UTF7IMAP_CONVERSION); + } + + $i += $n; + $u8len -= $n; + + if (($ch < 0x20) || ($ch >= 0x7f)) { + if (!$base64) { + $p .= '&'; + $base64 = true; + $b = 0; + $k = 10; + } + + if ($ch & ~0xffff) { + $ch = 0xfffe; + } + + $p .= $ptr[($b | $ch >> $k)]; + $k -= 6; + for (; $k >= 0; $k -= 6) { + $p .= $ptr[(($ch >> $k) & 0x3f)]; + } + + $b = ($ch << (-$k)) & 0x3f; + $k += 16; + } else { + if ($base64) { + if ($k > 10) { + $p .= $ptr[$b]; + } + $p .= '-'; + $base64 = false; + } + + $p .= chr($ch); + if (chr($ch) == '&') { + $p .= '-'; + } + } + } + + if ($base64) { + if ($k > 10) { + $p .= $ptr[$b]; + } + $p .= '-'; + } + + return $p; + } +} diff --git a/framework/Imap_Client/package.xml b/framework/Imap_Client/package.xml new file mode 100644 index 000000000..a88d2e8d7 --- /dev/null +++ b/framework/Imap_Client/package.xml @@ -0,0 +1,109 @@ + + + Horde_Imap_Client + pear.horde.org + Horde IMAP abstraction interface + This package provides an abstracted API interface to various + IMAP4rev1 (RFC 3501) backend drivers. + + + Michael Slusarz + slusarz + slusarz@horde.org + yes + + + Chuck Hagenbuch + chuck + chuck@horde.org + yes + + 2008-02-25 + + 0.0.1 + 0.0.1 + + + alpha + alpha + + LGPL + + * Initial release + + + + + + + + + + + + + + + + + + + + + + + + + + 5.2.0 + + + 1.5.0 + + + + + Auth_SASL + pear.php.net + + + Horde_Cache + pear.horde.org + + + Horde_Serialize + pear.horde.org + + + MIME + pear.horde.org + + + Secret + pear.horde.org + + + imap + + + mbstring + + + + + + + + + + + + + + + + + diff --git a/framework/Imap_Client/test/Horde/Imap/test_client.php b/framework/Imap_Client/test/Horde/Imap/test_client.php new file mode 100644 index 000000000..1f0df9af4 --- /dev/null +++ b/framework/Imap_Client/test/Horde/Imap/test_client.php @@ -0,0 +1,910 @@ + + * @category Horde + * @package Horde_Imap_Client + */ + +/** Configuration **/ +$driver = 'Socket'; // 'Socket', 'Cclient', or 'Cclient-pop3' +$params = array( + 'username' => '', + 'password' => '', + 'hostspec' => '', + 'port' => '', + 'secure' => '', // empty, 'ssl', or 'tls' + 'debug' => 'php://output' +); + +$cache_params = array( + 'driver' => 'file', // REQUIRED - Horde_Cache driver. + 'driver_params' => array( // REQUIRED + 'dir' => '/tmp', + 'prefix' => 'iclient' + ), + 'compress' => null, // false, 'gzip', or 'lzf' + 'lifetime' => null, // (integer) Lifetime, in seconds + 'slicesize' => null // (integer) Slicesize +); + +// Test mailbox names (without namespace information) +$test_mbox = 'TestMailboxTest'; +$test_mbox_utf8 = 'TestMailboxTest1è'; +/** End Configuration **/ + + +$currdir = dirname(__FILE__); +$dir = dirname(dirname(dirname($currdir))) . '/lib/Horde/Imap/'; +require_once $dir . 'Client.php'; +require_once $dir . '/Client/Sort.php'; + +/* Check for Horde_Cache::. */ +if (@require_once 'Horde/Cache.php') { + $horde_cache = true; + print "Using Horde_Imap_Client_Cache (driver: " . $cache_params['driver'] . ").\n\n"; + $params['cache'] = $cache_params; +} else { + $horde_cache = false; +} + +if (!empty($argv[1])) { + $params['username'] = $argv[1]; +} +if (empty($params['username'])) { + exit("Need username. Exiting.\n"); +} + +if (empty($params['password'])) { + $params['password'] = $argv[2]; +} +if (empty($argv[2])) { + exit("Need password. Exiting.\n"); +} + +if (!empty($argv[3])) { + $params = array_merge($params, Horde_Imap_Client::parseImapURL($argv[3])); +} + +function error_handler($exception) { + print "\n=====================================\n" . + 'ERROR EXCEPTION: ' . $exception->getMessage() . + "\n=====================================\n"; +} +set_exception_handler('error_handler'); + +function exception_handler($exception) { + print "\n=====================================\n" . + 'UNCAUGHT EXCEPTION: ' . $exception->getMessage() . + "\n=====================================\n"; +} +set_exception_handler('exception_handler'); + +if (@require_once 'Benchmark/Timer.php') { + $timer = new Benchmark_Timer(); + $timer->start(); +} + +// Add an ID field to send to server (ID extension) +$params['id'] = array('name' => 'Horde_Imap_Client test program'); + +$imap_client = Horde_Imap_Client::getInstance($driver, $params); +if ($driver == 'Cclient-pop3') { + $test_mbox = $test_mbox_utf8 = 'INBOX'; +} + +$use_imapproxy = false; +print "CAPABILITY listing:\n"; +try { + print_r($imap_client->capability()); + $use_imapproxy = $imap_client->queryCapability('XIMAPPROXY'); +} catch (Horde_Imap_Client_Exception $e) { + print 'ERROR: ' . $e->getMessage() . "\n"; +} + +print "\nLOGIN:\n"; +try { + $imap_client->login(); + print "Login: OK.\n"; +} catch (Horde_Imap_Client_Exception $e) { + print 'ERROR: ' . $e->getMessage() . "\n"; + exit("Login: FAILED. EXITING.\n"); +} + +print "\nUsing a secure connection: " . ($imap_client->isSecureConnection() ? 'YES' : 'NO') . "\n"; + +print "\nID information from server:\n"; +try { + print_r($imap_client->getID()); + print "ID information: OK.\n"; +} catch (Horde_Imap_Client_Exception $e) { + print 'ERROR: ' . $e->getMessage() . "\n"; + print "ID information: FAILED.\n"; +} + +print "\nLanguage information from server:\n"; +try { + print_r($imap_client->getLanguage(true)); + $lang = $imap_client->getLanguage(); + print "Language information (" . ($lang ? $lang : 'NONE') . "): OK.\n"; +} catch (Horde_Imap_Client_Exception $e) { + print 'ERROR: ' . $e->getMessage() . "\n"; + print "Language information: FAILED.\n"; +} + +print "\nComparator information from server:\n"; +try { + print_r($imap_client->getComparator()); + print "Comparator information: OK.\n"; +} catch (Horde_Imap_Client_Exception $e) { + print 'ERROR: ' . $e->getMessage() . "\n"; + print "Comparator information: FAILED.\n"; +} + +print "\nNAMESPACES:\n"; +try { + $namespaces = $imap_client->getNamespaces(); + print_r($namespaces); + print "Namespaces: OK.\n"; +} catch (Horde_Imap_Client_Exception $e) { + print 'ERROR: ' . $e->getMessage() . "\n"; + print "Namespaces: FAILED.\n"; + $namespaces = array(); +} + +// Tack on namespace information to folder names. +$base_ns = reset($namespaces); +if (empty($base_ns['name'])) { + $ns_prefix = ''; +} else { + $ns_prefix = rtrim($base_ns['name'], $base_ns['delimiter']) . $base_ns['delimiter']; +} +$test_mbox = $ns_prefix . $test_mbox; +$test_mbox_utf8 = $ns_prefix . $test_mbox_utf8; + +print "\nOpen INBOX read-only, read-write, and auto.\n"; +try { + $imap_client->openMailbox('INBOX', Horde_Imap_Client::OPEN_READONLY); + print "Read-only: OK\n"; +} catch (Horde_Imap_Client_Exception $e) { + print 'ERROR: ' . $e->getMessage() . "\n"; + print "Read-only: FAILED\n"; +} + +try { + $imap_client->openMailbox('INBOX', Horde_Imap_Client::OPEN_READWRITE); + print "Read-write: OK\n"; +} catch (Horde_Imap_Client_Exception $e) { + print 'ERROR: ' . $e->getMessage() . "\n"; + print "Read-write: FAILED\n"; +} + +try { + $imap_client->openMailbox('INBOX', Horde_Imap_Client::OPEN_AUTO); + print "Auto: OK\n"; +} catch (Horde_Imap_Client_Exception $e) { + print 'ERROR: ' . $e->getMessage() . "\n"; + print "Auto: FAILED\n"; +} + +print "\nCurrent mailbox information:\n"; +print_r($imap_client->currentMailbox()); + +print "\nCreating mailbox " . $test_mbox . ".\n"; +try { + $imap_client->createMailbox($test_mbox); + print "Creating: OK\n"; +} catch (Horde_Imap_Client_Exception $e) { + print 'ERROR: ' . $e->getMessage() . "\n"; + print "Creating: FAILED\n"; +} + +print "\nSubscribing to mailbox " . $test_mbox . ".\n"; +try { + $imap_client->subscribeMailbox($test_mbox, true); + print "Subscribing: OK\n"; +} catch (Horde_Imap_Client_Exception $e) { + print 'ERROR: ' . $e->getMessage() . "\n"; + print "Subscribing: FAILED\n"; +} + +print "\nUnsubscribing to mailbox " . $test_mbox . ".\n"; +try { + $imap_client->subscribeMailbox($test_mbox, false); + print "Unsubscribing: OK\n"; +} catch (Horde_Imap_Client_Exception $e) { + print 'ERROR: ' . $e->getMessage() . "\n"; + print "Unsubscribing: FAILED\n"; +} + +print "\nRenaming mailbox " . $test_mbox . " to " . $test_mbox_utf8 . ".\n"; +try { + $imap_client->renameMailbox($test_mbox, $test_mbox_utf8); + print "Renaming: OK\n"; +} catch (Horde_Imap_Client_Exception $e) { + print 'ERROR: ' . $e->getMessage() . "\n"; + print "Renaming: FAILED\n"; +} + +print "\nDeleting mailbox " . $test_mbox_utf8 . ".\n"; +try { + $imap_client->deleteMailbox($test_mbox_utf8); + print "Deleting: OK\n"; +} catch (Horde_Imap_Client_Exception $e) { + print 'ERROR: ' . $e->getMessage() . "\n"; + print "Deleting: FAILED\n"; +} + +print "\nDeleting (non-existent) mailbox " . $test_mbox_utf8 . ".\n"; +try { + $imap_client->deleteMailbox($test_mbox_utf8); + print "Failed deletion: FAILED\n"; +} catch (Horde_Imap_Client_Exception $e) { + print "Deleting: OK\n"; + print 'Error returned from IMAP server: ' . $e->getMessage() . "\n"; +} + +print "\nListing all mailboxes in base level (flat format).\n"; +print_r($imap_client->listMailboxes('%', Horde_Imap_Client::MBOX_ALL, array('flat' => true))); + +print "\nListing all mailboxes (flat format).\n"; +print_r($imap_client->listMailboxes('*', Horde_Imap_Client::MBOX_ALL, array('flat' => true))); + +print "\nListing subscribed mailboxes (flat format, in UTF-8 encoding).\n"; +print_r($imap_client->listMailboxes('*', Horde_Imap_Client::MBOX_SUBSCRIBED, array('flat' => true, 'utf8' => true))); + +print "\nListing unsubscribed mailboxes in base level (with attribute and delimiter information).\n"; +print_r($imap_client->listMailboxes('%', Horde_Imap_Client::MBOX_UNSUBSCRIBED, array('attributes' => true, 'delimiter' => true))); + +print "\nAll status information for INBOX.\n"; +print_r($imap_client->status('INBOX', Horde_Imap_Client::STATUS_ALL)); + +print "\nOnly UIDNEXT status information for INBOX.\n"; +print_r($imap_client->status('INBOX', Horde_Imap_Client::STATUS_UIDNEXT)); + +print "\nOnly FIRSTUNSEEN, FLAGS, PERMFLAGS, HIGHESTMODSEQ, and UIDNOTSTICKY status information for INBOX.\n"; +try { + print_r($imap_client->status('INBOX', Horde_Imap_Client::STATUS_FIRSTUNSEEN | Horde_Imap_Client::STATUS_FLAGS | Horde_Imap_Client::STATUS_PERMFLAGS | Horde_Imap_Client::STATUS_HIGHESTMODSEQ | Horde_Imap_Client::STATUS_UIDNOTSTICKY)); + print "Status: OK.\n"; +} catch (Horde_Imap_Client_Exception $e) { + print 'ERROR: ' . $e->getMessage() . "\n"; + print "Status: FAILED.\n"; +} + +print "\nCreating mailbox " . $test_mbox . " for message tests.\n"; +try { + $imap_client->createMailbox($test_mbox); + print "Created " . $test_mbox . " OK.\n"; +} catch (Horde_Imap_Client_Exception $e) { + print 'ERROR: ' . $e->getMessage() . "\n"; + print "Creation: FAILED.\n"; +} + +print "\nCreating mailbox " . $test_mbox_utf8 . " for message tests.\n"; +try { + $imap_client->createMailbox($test_mbox_utf8); + print "Created " . $test_mbox_utf8 . " OK.\n"; +} catch (Horde_Imap_Client_Exception $e) { + print 'ERROR: ' . $e->getMessage() . "\n"; + print "Creation: FAILED.\n"; +} + +print "\nAll status information for " . $test_mbox . ", including 'firstunseen' and 'highestmodseq'.\n"; +try { + print_r($imap_client->status($test_mbox, Horde_Imap_Client::STATUS_ALL | Horde_Imap_Client::STATUS_FIRSTUNSEEN | Horde_Imap_Client::STATUS_HIGHESTMODSEQ)); + print "Status: OK.\n"; +} catch (Horde_Imap_Client_Exception $e) { + print 'ERROR: ' . $e->getMessage() . "\n"; + print "Status: FAILED.\n"; +} + +$test_email = << +Delivered-To: foo@example.com +Received: from test1.example.com (test1.example.com [192.168.10.10]) + by test2.example.com (Postfix) with ESMTP id E6F7890AF + for ; Sat, 26 Jul 2008 20:09:03 -0600 (MDT) +Message-ID: +Date: Sat, 26 Jul 2008 21:10:00 -0500 (CDT) +From: Test +To: foo@example.com +Subject: Test e-mail 1 +Mime-Version: 1.0 +Content-Type: text/plain + +Test. +EOE; + +$test_email2 = << +Delivered-To: foo@example.com +Received: from test1.example.com (test1.example.com [192.168.10.10]) + by test2.example.com (Postfix) with ESMTP id E8796FBA + for ; Sat, 26 Jul 2008 21:19:13 -0600 (MDT) +Message-ID: <98761234@test1.example.com> +Date: Sat, 26 Jul 2008 21:19:00 -0500 (CDT) +From: Test +To: foo@example.com +Subject: Re: Test e-mail 1 +Mime-Version: 1.0 +Content-Type: text/plain +In-Reply-To: +References: + + +Test reply. +EOE; + +$uid1 = $uid2 = $uid3 = $uid4 = null; + +print "\nAppending test e-mail 1 (with \\Flagged), 2 via a stream (with \\Seen), 3 via a stream, and 4 (with internaldate):\n"; +try { + $handle = fopen($currdir . '/test_email.txt', 'r'); + $handle2 = fopen($currdir . '/test_email2.txt', 'r'); + $uid = $imap_client->append($test_mbox, array( + array('data' => $test_email, 'flags' => array('\\flagged'), 'messageid' => 'abcd1234efgh5678@test1.example.com'), + array('data' => $handle, 'flags' => array('\\seen'), 'messageid' => 'aaabbbcccddd111222333444@test1.example.com'), + array('data' => $handle2, 'messageid' => '2008yhnujm@foo.example.com'), + array('data' => $test_email2, 'internaldate' => new DateTime('17 August 2003'), 'messageid' => '98761234@test1.example.com') + )); + if ($uid === true) { + throw new Horde_Imap_Client_Exception('Append successful but UIDs not properly returned.'); + } + list($uid1, $uid2, $uid3, $uid4) = $uid; + print "Append test-email 1 OK [UID: $uid1]\n"; + print "Append test-email 2 OK [UID: $uid2]\n"; + print "Append test-email 3 OK [UID: $uid3]\n"; + print "Append test-email 4 OK [UID: $uid4]\n"; +} catch (Horde_Imap_Client_Exception $e) { + print 'ERROR: ' . $e->getMessage() . "\n"; + print "Appending: FAILED.\n"; +} +fclose($handle); +fclose($handle2); + +if (!is_null($uid1)) { + print "\nCopying test e-mail 1 to " . $test_mbox_utf8 . ".\n"; + try { + $uid5 = $imap_client->copy($test_mbox, $test_mbox_utf8, array('ids' => array($uid1))); + if ($uid5 === true) { + print "Copy: OK\n"; + $uid5 = null; + } elseif (is_array($uid5)) { + print_r($uid5); + reset($uid5); + print "Copy: OK [From UID " . key($uid5) . " to UID " . current($uid5) . "]\n"; + } + } catch (Horde_Imap_Client_Exception $e) { + print 'ERROR: ' . $e->getMessage() . "\n"; + print "Copy: FAILED\n"; + $uid5 = null; + } +} else { + $uid5 = null; +} + +if (!is_null($uid2)) { + print "\nFlagging test e-mail 2 with the Deleted flag.\n"; + try { + $imap_client->store($test_mbox, array('add' => array('\\deleted'), 'ids' => array($uid2))); + print "Flagging: OK\n"; + } catch (Horde_Imap_Client_Exception $e) { + print 'ERROR: ' . $e->getMessage() . "\n"; + print "Flagging: FAILED\n"; + } +} + +if (!is_null($uid1)) { + print "\nExpunging mailbox by specifying non-deleted UID.\n"; + try { + $imap_client->expunge($test_mbox, array('ids' => array($uid1))); + print "Expunging: OK\n"; + } catch (Horde_Imap_Client_Exception $e) { + print 'ERROR: ' . $e->getMessage() . "\n"; + print "Expunging: FAILED\n"; + } +} + +print "\nGet status of " . $test_mbox . " (should have 4 messages).\n"; +try { + print_r($imap_client->status($test_mbox, Horde_Imap_Client::STATUS_ALL)); + print "Status: OK\n"; +} catch (Horde_Imap_Client_Exception $e) { + print 'ERROR: ' . $e->getMessage() . "\n"; + print "Status: FAILED\n"; +} + +print "\nExpunging mailbox (should remove test e-mail 2).\n"; +try { + $imap_client->expunge($test_mbox); + print "Expunging: OK\n"; +} catch (Horde_Imap_Client_Exception $e) { + print 'ERROR: ' . $e->getMessage() . "\n"; + print "Expunging: FAILED\n"; +} + +print "\nGet status of " . $test_mbox . " (should have 3 messages).\n"; +print_r($imap_client->status($test_mbox, Horde_Imap_Client::STATUS_ALL)); + +if (!is_null($uid5)) { + print "\nMove test e-mail 1 from " . $test_mbox_utf8 . " to " . $test_mbox . ".\n"; + try { + $uid6 = $imap_client->copy($test_mbox_utf8, $test_mbox, array('ids' => array(current($uid5)), 'move' => true)); + if ($uid6 === true) { + print "Move: OK\n"; + } elseif (is_array($uid6)) { + print_r($uid6); + reset($uid6); + print "Move: OK [From UID " . key($uid6) . " to UID " . current($uid6) . "]\n"; + } + } catch (Horde_Imap_Client_Exception $e) { + print 'ERROR: ' . $e->getMessage() . "\n"; + print "Move: FAILED\n"; + } +} + +print "\nDeleting mailbox " . $test_mbox_utf8 . ".\n"; +try { + $imap_client->deleteMailbox($test_mbox_utf8); + print "Deleting: OK\n"; +} catch (Horde_Imap_Client_Exception $e) { + print 'ERROR: ' . $e->getMessage() . "\n"; + print "Deleting: FAILED\n"; +} + +print "\nFlagging test e-mail 3 with the Deleted flag.\n"; +if (!is_null($uid3)) { + try { + $imap_client->store($test_mbox, array('add' => array('\\deleted'), 'ids' => array($uid3))); + print "Flagging: OK\n"; + } catch (Horde_Imap_Client_Exception $e) { + print 'ERROR: ' . $e->getMessage() . "\n"; + print "Flagging: FAILED\n"; + } +} + +print "\nClosing " . $test_mbox . " without expunging.\n"; +try { + $imap_client->close(); + print "Closing: OK\n"; +} catch (Horde_Imap_Client_Exception $e) { + print 'ERROR: ' . $e->getMessage() . "\n"; + print "Closing: FAILED\n"; +} + +print "\nGet status of " . $test_mbox . " (should have 4 messages).\n"; +print_r($imap_client->status($test_mbox, Horde_Imap_Client::STATUS_ALL)); + +// Create a simple 'ALL' search query +$all_query = new Horde_Imap_Client_Search_Query(); + +print "\nSearching " . $test_mbox . " for all messages (returning UIDs).\n"; +try { + print_r($imap_client->search($test_mbox, $all_query)); + print "Search: OK\n"; +} catch (Horde_Imap_Client_Exception $e) { + print 'ERROR: ' . $e->getMessage() . "\n"; + print "Search: FAILED\n"; +} + +print "\nSearching " . $test_mbox . " for all messages (returning message sequence numbers).\n"; +try { + print_r($imap_client->search($test_mbox, $all_query, array('results' => array(Horde_Imap_Client::SORT_RESULTS_COUNT, Horde_Imap_Client::SORT_RESULTS_MATCH, Horde_Imap_Client::SORT_RESULTS_MAX, Horde_Imap_Client::SORT_RESULTS_MIN), 'sequence' => true))); + print "Search: OK\n"; +} catch (Horde_Imap_Client_Exception $e) { + print 'ERROR: ' . $e->getMessage() . "\n"; + print "Search: FAILED\n"; +} + +print "\nSearching " . $test_mbox . " (should be optimized by using internal status instead).\n"; +try { + $query1 = $query2 = $all_query; + print_r($imap_client->search($test_mbox, $all_query, array('results' => array(Horde_Imap_Client::SORT_RESULTS_COUNT)))); + $query1->flag('\\recent'); + print_r($imap_client->search($test_mbox, $query1, array('results' => array(Horde_Imap_Client::SORT_RESULTS_COUNT)))); + $query2->flag('\\seen', false); + print_r($imap_client->search($test_mbox, $query2, array('results' => array(Horde_Imap_Client::SORT_RESULTS_COUNT)))); + print_r($imap_client->search($test_mbox, $query2, array('results' => array(Horde_Imap_Client::SORT_RESULTS_MIN)))); + print "Search: OK\n"; +} catch (Horde_Imap_Client_Exception $e) { + print 'ERROR: ' . $e->getMessage() . "\n"; + print "Search: FAILED\n"; +} + +print "\nSort " . $test_mbox . " by from and reverse date for all messages (returning UIDs).\n"; +try { + print_r($imap_client->search($test_mbox, $all_query, array('sort' => array(Horde_Imap_Client::SORT_FROM, Horde_Imap_Client::SORT_REVERSE, Horde_Imap_Client::SORT_DATE)))); + print "Search: OK\n"; +} catch (Horde_Imap_Client_Exception $e) { + print 'ERROR: ' . $e->getMessage() . "\n"; + print "Search: FAILED\n"; +} + +print "\nSort " . $test_mbox . " by thread - references algorithm (UIDs).\n"; +try { + print_r($imap_client->thread($test_mbox, array('criteria' => Horde_Imap_Client::THREAD_REFERENCES))); + print "Thread search: OK\n"; +} catch (Horde_Imap_Client_Exception $e) { + print 'ERROR: ' . $e->getMessage() . "\n"; + print "Thread search: FAILED\n"; +} + +print "\nSort 1st 5 messages in " . $test_mbox . " by thread - references algorithm (UIDs).\n"; +try { + $ten_query = new Horde_Imap_Client_Search_Query(); + $ten_query->sequence(Horde_Imap_Client::fromSequenceString('1:5'), true); + print_r($imap_client->thread($test_mbox, array('search' => $ten_query, 'criteria' => Horde_Imap_Client::THREAD_REFERENCES))); + print "Thread search: OK\n"; +} catch (Horde_Imap_Client_Exception $e) { + print 'ERROR: ' . $e->getMessage() . "\n"; + print "Thread search: FAILED\n"; +} + +print "\nSort " . $test_mbox . " by thread - orderedsubject algorithm (sequence numbers).\n"; +try { + print_r($imap_client->thread($test_mbox, array('criteria' => Horde_Imap_Client::THREAD_ORDEREDSUBJECT, 'sequence' => true))); + print "Thread search: OK\n"; +} catch (Horde_Imap_Client_Exception $e) { + print 'ERROR: ' . $e->getMessage() . "\n"; + print "Thread search: FAILED\n"; +} + +$simple_fetch = array( + Horde_Imap_Client::FETCH_STRUCTURE => true, + Horde_Imap_Client::FETCH_ENVELOPE => true, + Horde_Imap_Client::FETCH_FLAGS => true, + Horde_Imap_Client::FETCH_DATE => true, + Horde_Imap_Client::FETCH_SIZE => true +); + +print "\nSimple fetch example:\n"; +try { + print_r($imap_client->fetch($test_mbox, $simple_fetch)); + print "Fetch: OK\n"; +} catch (Horde_Imap_Client_Exception $e) { + print 'ERROR: ' . $e->getMessage() . "\n"; + print "Fetch: FAILED\n"; +} + +if ($horde_cache) { + print "\nRepeat simple fetch example - should retrieve data from cache:\n"; + try { + print_r($imap_client->fetch($test_mbox, $simple_fetch)); + print "Fetch: OK\n"; + } catch (Horde_Imap_Client_Exception $e) { + print 'ERROR: ' . $e->getMessage() . "\n"; + print "Fetch: FAILED\n"; + } +} + +print "\nFetching message information from complex MIME message:\n"; +try { + $fetch_res = $imap_client->fetch($test_mbox, array( + Horde_Imap_Client::FETCH_FULLMSG => array( + 'length' => 100, + 'peek' => true, + 'start' => 0 + ), + + Horde_Imap_Client::FETCH_HEADERTEXT => array( + // Header of entire message + array( + 'length' => 100, + 'peek' => true, + 'start' => 0 + ), + // Header of message/rfc822 part + array( + 'id' => 2, + 'length' => 100, + 'peek' => true, + 'start' => 0 + ) + ), + + Horde_Imap_Client::FETCH_BODYTEXT => array( + // Body text of entire message + array( + 'length' => 100, + 'peek' => true, + 'start' => 0 + ), + // Body text of message/rfc822 part + array( + 'id' => 2, + 'length' => 100, + 'peek' => true, + 'start' => 0 + ) + ), + + Horde_Imap_Client::FETCH_MIMEHEADER => array( + // MIME Header of multipart/alternative part + array( + 'id' => 1, + 'length' => 100, + 'peek' => true, + 'start' => 0 + ), + // MIME Header of text/plain part embedded in message/rfc822 part + array( + 'id' => '2.1', + 'length' => 100, + 'peek' => true, + 'start' => 0 + ) + ), + + Horde_Imap_Client::FETCH_BODYPART => array( + // Body text of multipart/alternative part + array( + 'id' => 1, + 'length' => 100, + 'peek' => true, + 'start' => 0 + ), + // Body text of image/png part embedded in message/rfc822 part + // Try to do server-side decoding, if available + array( + 'decode' => true, + 'id' => '2.2', + 'length' => 100, + 'peek' => true, + 'start' => 0 + ) + ), + + // If supported, return decoded body part size + Horde_Imap_Client::FETCH_BODYPARTSIZE => array( + array('id' => '2.2') + ), + + Horde_Imap_Client::FETCH_HEADERS => array( + // Select message-id header from base message header + array( + 'headers' => array('message-id'), + 'label' => 'headersearch1', + 'length' => 100, + 'peek' => true, + 'start' => 0 + ), + // Select everything but message-id header from message/rfc822 + // header + array( + 'id' => 2, + 'headers' => array('message-id'), + 'label' => 'headersearch2', + 'length' => 100, + 'notsearch' => true, + 'peek' => true, + 'start' => 0 + ) + ), + + Horde_Imap_Client::FETCH_STRUCTURE => true, + Horde_Imap_Client::FETCH_ENVELOPE => true, + Horde_Imap_Client::FETCH_FLAGS => true, + Horde_Imap_Client::FETCH_DATE => true, + Horde_Imap_Client::FETCH_SIZE => true, + Horde_Imap_Client::FETCH_UID => true, + Horde_Imap_Client::FETCH_MODSEQ => true + ), array('ids' => array($uid3))); + print_r($fetch_res); + print "Fetch: OK\n"; +} catch (Horde_Imap_Client_Exception $e) { + print 'ERROR: ' . $e->getMessage() . "\n"; + print "Fetch: FAILED\n"; + + // If POP3, try easier fetch criteria + if ($driver == 'Cclient-pop3') { + try { + print_r($imap_client->fetch('INBOX', array( + Horde_Imap_Client::FETCH_FULLMSG => array( + 'length' => 100, + 'peek' => true, + 'start' => 0) + ), array('ids' => array(1)))); + print "Fetch: OK\n"; + } catch (Horde_Imap_Client_Exception $e) { + print 'ERROR: ' . $e->getMessage() . "\n"; + print "Fetch (POP3): FAILED\n"; + } + } +} + +print "\nFetching parsed header information (requires Horde MIME library):\n"; +try { + print_r($imap_client->fetch($test_mbox, array( + Horde_Imap_Client::FETCH_HEADERS => array( + array( + 'headers' => array('message-id'), + 'label' => 'headersearch1', + 'parse' => true, + 'peek' => true) + ) + ), array('ids' => array($uid3)))); + print "Fetch: OK\n"; +} catch (Horde_Imap_Client_Exception $e) { + print 'ERROR: ' . $e->getMessage() . "\n"; + print "Fetch: FAILED\n"; +} + +print "\nRe-open " . $test_mbox . " READ-WRITE.\n"; +try { + $imap_client->openMailbox($test_mbox, Horde_Imap_Client::OPEN_READWRITE); + print "Read-write: OK\n"; +} catch (Horde_Imap_Client_Exception $e) { + print 'ERROR: ' . $e->getMessage() . "\n"; + print "Read-write: FAILED\n"; +} + +print "\nClosing " . $test_mbox . " while expunging.\n"; +try { + $imap_client->close(array('expunge' => true)); + print "Closing: OK\n"; +} catch (Horde_Imap_Client_Exception $e) { + print 'ERROR: ' . $e->getMessage() . "\n"; + print "Closing: FAILED\n"; +} + +print "\nGet status of " . $test_mbox . " (should have 3 messages).\n"; +print_r($imap_client->status($test_mbox, Horde_Imap_Client::STATUS_ALL)); + +print "\nDeleting mailbox " . $test_mbox . ".\n"; +try { + $imap_client->deleteMailbox($test_mbox); + print "Deleting " . $test_mbox . ": OK\n"; +} catch (Horde_Imap_Client_Exception $e) { + print 'ERROR: ' . $e->getMessage() . "\n"; + print "Deleting: FAILED\n"; +} + +print "\nTesting a complex search query built using Horde_Imap_Client_Search_Query:\n"; +$query = new Horde_Imap_Client_Search_Query(); +$query->flag('\\Answered'); +$query->flag('\\Deleted', true); +$query->flag('\\Recent'); +$query->flag('\\Unseen'); +$query->flag('TestKeyword'); +// This second flag request for '\Answered' should overrule the first request +$query->flag('\\Answered', true); +// Querying for new should clear both '\Recent' and '\Unseen' +$query->newMsgs(); +$query->headerText('cc', 'Testing'); +$query->headerText('message-id', 'abcdefg1234567', true); +$query->headerText('subject', '8bit char 1è.'); +$query->charset('UTF-8'); +$query->text('Test1'); +$query->text('Test2', false); +$query->size('1024', true); +$query->size('4096', false); +$query->sequence(array(1, 5, 50, 51, 52, 55, 54, 53, 55, 55, 100, 500, 501)); +$query->dateSearch(6, 15, 2008, Horde_Imap_Client_Search_Query::DATE_BEFORE, true, true); +$query->dateSearch(6, 20, 2008, Horde_Imap_Client_Search_Query::DATE_ON, false); +// Add 2 simple OR queries +$query2 = new Horde_Imap_Client_Search_Query(); +$query2->text('Test3', false, true); +$query3 = new Horde_Imap_Client_Search_Query(); +$query3->newMsgs(false); +$query3->intervalSearch(100000, Horde_Imap_Client_Search_Query::INTERVAL_YOUNGER); +$query3->modseq(1234, '/flags/\deleted', 'all'); +$query->orSearch(array($query2, $query3)); +print_r($query->build()); + +print "\nTesting mailbox sorting:\n"; +$test_sort = array( + 'A', + 'Testing.JJ', + 'A1', + 'INBOX', + 'a', + 'A1.INBOX', + '2.A', + 'a.A.1.2', + $test_mbox, + $test_mbox_utf8 +); +print_r($test_sort); +Horde_Imap_Client_Sort::sortMailboxes($test_sort, array('delimiter' => '.', 'inbox' => true)); +print_r($test_sort); + +print "Testing serialization of object. Will automatically logout.\n"; +$old_error = error_reporting(0); +if (require_once 'Horde/Secret.php') { + Horde_Imap_Client::$encryptKey = uniqid(); +} +error_reporting($old_error); +$serialized_data = serialize($imap_client); +print "\nSerialized object:\n"; +print_r($serialized_data); + +// Unset $encryptKey so password is not output in cleartext +Horde_Imap_Client::$encryptKey = null; +$unserialized_data = unserialize($serialized_data); +print "\n\nUnserialized object:\n"; +print_r($unserialized_data); + +if ($use_imapproxy) { + print "\nTesting reuse of imapproxy connection.\n"; + try { + $unserialized_data->status('INBOX', Horde_Imap_Client::STATUS_MESSAGES); + print "OK.\n"; + } catch (Horde_Imap_Client_Exception $e) { + print 'ERROR: ' . $e->getMessage() . "\n"; + } + + $unserialized_data->logout(); + print "\nLogging out: OK\n"; +} + +$subject_lines = array( + 'Re: Test', + 're: Test', + 'Fwd: Test', + 'fwd: Test', + 'Fwd: Re: Test', + 'Fwd: Re: Test (fwd)', + "Re: re:re: fwd:[fwd: \t Test] (fwd) (fwd)(fwd) " +); + +print "\nBase subject parsing:\n"; +foreach ($subject_lines as $val) { + print " ORIGINAL: \"" . $val . "\"\n"; + print " BASE: \"" . Horde_Imap_Client::getBaseSubject($val) . "\"\n\n"; +} + +$imap_urls = array( + 'NOT A VALID URL', + 'imap://test.example.com/', + 'imap://test.example.com:143/', + 'imap://testuser@test.example.com/', + 'imap://testuser@test.example.com:143/', + 'imap://;AUTH=PLAIN@test.example.com/', + 'imap://;AUTH=PLAIN@test.example.com:143/', + 'imap://;AUTH=*@test.example.com:143/', + 'imap://testuser;AUTH=*@test.example.com:143/', + 'imap://testuser;AUTH=PLAIN@test.example.com:143/' +); + +print "\nRFC 5092 URL parsing:\n"; +foreach ($imap_urls as $val) { + print "URL: " . $val . "\n"; + print "PARSED:\n"; + print_r(Horde_Imap_Client::parseImapURL($val)); + print "\n"; +} + +if (isset($fetch_res) && + @require_once 'Horde/MIME/Message.php') { + print "\nTesting MIME_Message::parseStructure() on complex MIME message:\n"; + $parse_res = MIME_Message::parseStructure($fetch_res[$uid3]['structure']); + print_r($parse_res); + + print "\nTesting MIME_Message::parseMessage() on complex MIME message:\n"; + $parse_text_res = MIME_Message::parseMessage(file_get_contents($currdir . '/test_email2.txt')); + print_r($parse_text_res); +} + +if (isset($timer)) { + $timer->stop(); + print "\nTime elapsed: " . $timer->timeElapsed() . " seconds"; +} + +print "\nMemory used: " . memory_get_usage() . " (Peak: " . memory_get_peak_usage() . ")\n"; diff --git a/framework/Imap_Client/test/Horde/Imap/test_email.txt b/framework/Imap_Client/test/Horde/Imap/test_email.txt new file mode 100644 index 000000000..ad5f2a893 --- /dev/null +++ b/framework/Imap_Client/test/Horde/Imap/test_email.txt @@ -0,0 +1,14 @@ +Return-Path: +Delivered-To: foo@example.com +Received: from test1.example.com (test1.example.com [192.168.10.10]) + by test2.example.com (Postfix) with ESMTP id E6F7610184CDD + for ; Sat, 26 Jul 2008 22:55:03 -0600 (MDT) +Message-ID: +Date: Sat, 26 Jul 2008 23:55:00 -0500 (CDT) +From: Test +To: foo@example.com +Subject: Test e-mail 2 +Mime-Version: 1.0 +Content-Type: text/plain + +Test. diff --git a/framework/Imap_Client/test/Horde/Imap/test_email2.txt b/framework/Imap_Client/test/Horde/Imap/test_email2.txt new file mode 100644 index 000000000..33f1e8f6d --- /dev/null +++ b/framework/Imap_Client/test/Horde/Imap/test_email2.txt @@ -0,0 +1,112 @@ +Return-Path: +Delivered-To: test@example.com +Received: from localhost (localhost [127.0.0.1]) + by foo.example.com (Postfix) with ESMTP id BBCC02F667 + for ; Thu, 2 Oct 2008 13:15:25 -0600 (MDT) +Received: from foo2.example.com (foo2.example.com [192.168.100.100]) by + foo.example.com (Horde Framework) with HTTP; + Thu, 02 Oct 2008 13:15:25 -0600 +Message-ID: <2008yhnujm@foo.example.com> +Date: Thu, 02 Oct 2008 13:15:25 -0600 +From: "A. Test User" +To: test@example.com +Subject: Fwd: Test +MIME-Version: 1.0 +Content-Type: multipart/mixed; boundary="=_5oyqt8yksw5p" +Content-Transfer-Encoding: 7bit +User-Agent: Test e-mail program + +This message is in MIME format. + +--=_5oyqt8yksw5p +Content-Type: multipart/alternative; boundary="=_5otck98hqrbh" +Content-Transfer-Encoding: 7bit + +This message is in MIME format. + +--=_5otck98hqrbh +Content-Type: text/plain; charset=ISO-8859-1 +Content-Description: Plaintext Version of Message +Content-Disposition: inline +Content-Transfer-Encoding: 7bit + +multipart/alternative test. + +--=_5otck98hqrbh +Content-Type: text/html; charset=ISO-8859-1 +Content-Description: HTML Version of Message +Content-Disposition: inline +Content-Transfer-Encoding: 7bit + + + + + + + +

+ multipart/alternative test.
+

+ + +--=_5otck98hqrbh-- + +--=_5oyqt8yksw5p +Content-Type: message/rfc822; + name="Forwarded Message: Test" +MIME-Version: 1.0 + +Return-Path: +Delivered-To: test@example.com +Received: from localhost (localhost [127.0.0.1]) + by foo.example.com (Postfix) with ESMTP id E57162F667 + for ; Thu, 2 Oct 2008 13:12:11 -0600 (MDT) +Received: from foo2.example.com (foo2.example.com [192.168.100.100]) by + foo.example.com (Horde Framework) with HTTP; + Thu, 02 Oct 2008 13:12:11 -0600 +Message-ID: <20081002131211.2404701yx4vklbez@test.example.com> +Date: Thu, 02 Oct 2008 13:12:11 -0600 +From: Test User +To: Test User +Subject: Test +MIME-Version: 1.0 +Content-Type: multipart/mixed; boundary="=_22nxtd2snnyj" +Content-Transfer-Encoding: 7bit +User-Agent: Test Agent + +This message is in MIME format. + +--=_22nxtd2snnyj +Content-Type: text/plain; charset=ISO-8859-1; DelSp="Yes"; format="flowed" +Content-Disposition: inline +Content-Transfer-Encoding: 7bit + +Test w/attachment + +--=_22nxtd2snnyj +Content-Type: image/png; name="info_icon.png" +Content-Disposition: attachment; filename="info_icon.png" +Content-Transfer-Encoding: base64 + +iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAwFBMVEUAAAAMExoUGyMVGyMcIysj +OE0yRlwvS2cwS2czT2w0T2w1UnA5VnREWGw9WnhAW3hKWm1CXnxGYoBKZoRLZoNOaohVbYdTb41U +b4xXc5Fbd5VldolfeJJgeJFfe5ljf51nf5pog6Fsh6Vuh6F3hpZ9ipiBlq2AmLOHm66WqL2Yqr2Z +rMCcrL2issOltMamtciwvc7///////////////////////////////////////////////////// +//////+mdyt7AAAAQHRSTlP///////////////////////////////////////////////////// +//////////////////////////////8AwnuxRAAAAIxJREFUeNptj1cOwkAMRIdesoQQSkjooYc6 +J+D+t8L2CpAi5u89z2ptvErxApafwK64nA89fET9ccw3q2U2rXmBpzCZpckIKjDTOZkm42ETKgrr +K8d9E3dhksKD0MRN56SyM3HVPqncNbHfyntSOGjAvj2thUkXtOEXqy7msc5ble/q0SR0Hen/P66U +N4YNJcNOYmoYAAAAAElFTkSuQmCC + +--=_22nxtd2snnyj-- + + +--=_5oyqt8yksw5p +Content-Type: text/plain; charset=UTF-8; name="test_txt.txt" +Content-Disposition: attachment; filename="test_txt.txt" +Content-Transfer-Encoding: 7bit + +Test text attachment. +--=_5oyqt8yksw5p-- +