--- /dev/null
+<?php
+
+require_once dirname(__FILE__) . '/Client/Base.php';
+require_once dirname(__FILE__) . '/Client/Exception.php';
+require_once dirname(__FILE__) . '/Client/Utf7imap.php';
+
+/**
+ * Horde_Imap_Client:: provides an abstracted API interface to various IMAP
+ * backends (RFC 3501).
+
+ * Required Parameters:
+ * password - (string) The IMAP user password.
+ * username - (string) The IMAP username.
+ *
+ * Optional Parameters:
+ * cache - (array) If set, caches data from fetch() calls. Requires
+ * Horde_Cache and Horde_Serialize to be installed. The array can
+ * contain the following keys (see Horde_Imap_Client_Cache:: for
+ * default values):
+ * <pre>
+ * '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:
+ * <pre>
+ * 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
+ * </pre>
+ * 'lifetime' - [OPTIONAL] (integer) The lifetime of the cache data (in secs).
+ * 'slicesize' - [OPTIONAL] (integer) The slicesize to use.
+ * </pre>
+ * 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 <tss@iki.fi>
+ *
+ * $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 <slusarz@curecanti.org>
+ * @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:
+ * <pre>
+ * 'nosort' - (boolean) Do not numerically sort the IDs before creating
+ * the range?
+ * DEFAULT: IDs are sorted
+ * </pre>
+ *
+ * @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:
+ * <pre>
+ * 'keepblob' - (boolean) Don't remove any "blob" information (i.e. text
+ * leading text between square brackets) from string.
+ * </pre>
+ *
+ * @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:
+ * <pre>
+ * '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.
+ * </pre>
+ */
+ 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://<iserver>[/] 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'));
--- /dev/null
+<?php
+/**
+ * Horde_Imap_Client_Base:: provides an abstracted API interface to various
+ * IMAP backends supporting the IMAP4rev1 protocol (RFC 3501).
+ *
+ * Required/Optional Parameters: See Horde_Imap_Client::.
+ *
+ * Copyright 2008 The Horde Project (http://www.horde.org/)
+ *
+ * $Horde: framework/Imap_Client/lib/Horde/Imap/Client/Base.php,v 1.81 2008/10/29 05:13:09 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 <slusarz@curecanti.org>
+ * @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:
+ * <pre>
+ * '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').
+ * </pre>
+ */
+ 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:
+ * <pre>
+ * 'utf8' - (boolean) True if 'mailbox' should be in UTF-8.
+ * DEFAULT: 'mailbox' returned in UTF7-IMAP.
+ * </pre>
+ *
+ * @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:
+ * <pre>
+ * '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: '.'
+ * </pre>
+ *
+ * @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:
+ * <pre>
+ * 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'.
+ * </pre>
+ *
+ * @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:
+ * <pre>
+ * '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.
+ * </pre>
+ * @param array $options Additonal options:
+ * <pre>
+ * 'create' - (boolean) Try to create $mailbox if it does not exist?
+ * DEFAULT: No.
+ * </pre>
+ *
+ * @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:
+ * <pre>
+ * 'expunge' - (boolean) Expunge all messages flagged as deleted?
+ * DEFAULT: No
+ * </pre>
+ */
+ 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:
+ * <pre>
+ * '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.
+ * </pre>
+ */
+ 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:
+ * <pre>
+ * 'results' - (array) The data to return. Consists of zero or more of the
+ * following flags:
+ * <pre>
+ * 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)
+ * </pre>
+ * '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:
+ * <pre>
+ * 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.
+ * </pre>
+ * 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)
+ * </pre>
+ *
+ * @return array An array with the following keys:
+ * <pre>
+ * '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.
+ * </pre>
+ */
+ 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:
+ * <pre>
+ * '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.
+ * </pre>
+ *
+ * @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:
+ * <pre>
+ * '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.
+ * </pre>
+ */
+ 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:
+ * <pre>
+ * 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.
+ * </pre>
+ * @param array $options Additional options:
+ * <pre>
+ * '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.
+ * </pre>
+ *
+ * @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:
+ * <pre>
+ * '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
+ * </pre>
+ *
+ * @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:
+ * <pre>
+ * '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.
+ * </pre>
+ *
+ * @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:
+ * <pre>
+ * '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.
+ * </pre>
+ */
+ 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:
+ * <pre>
+ * '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.
+ * </pre>
+ */
+ 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:
+ * <pre>
+ * 'lookup' - (array) If $ids is not null, the mapping of sequence
+ * numbers (keys) to UIDs (values).
+ * 'uids' - (array) The list of UIDs.
+ * </pre>
+ */
+ 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:
+ * <pre>
+ * '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.
+ * </pre>
+ */
+ 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:
+ * <pre>
+ * '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
+ * </pre>
+ */
+ 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:
+ * <pre>
+ * Horde_Imap_Client_Search_Query::DATE_BEFORE,
+ * Horde_Imap_Client_Search_Query::DATE_ON, or
+ * Horde_Imap_Client_Search_Query::DATE_SINCE.
+ * </pre>
+ * @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:
+ * <pre>
+ * Horde_Imap_Client_Search_Query::INTERVAL_OLDER, or
+ * Horde_Imap_Client_Search_Query::INTERVAL_YOUNGER
+ * </pre>
+ * @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;
+ }
+}
--- /dev/null
+<?php
+
+require_once 'Horde/Cache.php';
+require_once 'Horde/Serialize.php';
+
+/**
+ * Horde_Imap_Client_Cache:: provides an interface to cache various data
+ * retrieved from the IMAP server.
+ *
+ * Requires Horde_Cache and Horde_Serialize packages.
+ *
+ * REQUIRED Parameters:
+ * ====================
+ * 'driver' - (string) The Horde_Cache driver to use.
+ * 'driver_params' - (string) The params to pass to the Horde_Cache driver.
+ * 'hostspec' - (string) The IMAP hostspec.
+ * 'username' - (string) The IMAP username.
+ *
+ * Optional Parameters:
+ * ====================
+ * 'compress' - (string) Compression to use on the cached data.
+ * Either false, 'gzip' or 'lzf'.
+ * DEFAULT: No compression
+ * 'debug' - (resource) If set, will output debug information to the stream
+ * identified.
+ * DEFAULT: No debug output
+ * 'lifetime' - (integer) The lifetime of the cache data (in seconds).
+ * DEFAULT: 1 week (604800 secs)
+ * 'slicesize' - (integer) The slicesize to use.
+ * DEFAULT: 50
+ *
+ * $Horde: framework/Imap_Client/lib/Horde/Imap/Client/Cache.php,v 1.19 2008/10/28 21:54:40 slusarz Exp $
+ *
+ * Copyright 2005-2008 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (GPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/gpl.html.
+ *
+ * @author Michael Slusarz <slusarz@horde.org>
+ * @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:
+ * <pre>
+ * 'uidvalid' - (integer) The UIDVALIDITY of the mailbox.
+ * </pre>
+ */
+ 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()
+ );
+ }
+ }
+}
--- /dev/null
+<?php
+
+require_once dirname(__FILE__) . '/Cclient.php';
+
+/**
+ * Horde_Imap_Client_Cclient_pop3 provides an interface to a POP3 server (RFC
+ * 1939) via the PHP imap (c-client) module. This driver is an abstraction
+ * layer allowing POP3 commands to be used based on its IMAP equivalents.
+ *
+ * PHP IMAP module: http://www.php.net/imap
+ *
+ * No additional paramaters from those defined in Horde_Imap_Client_Cclient.
+ *
+ * Copyright 2008 The Horde Project (http://www.horde.org/)
+ *
+ * $Horde: framework/Imap_Client/lib/Horde/Imap/Client/Cclient-pop3.php,v 1.6 2008/10/09 21:06:22 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 <slusarz@curecanti.org>
+ * @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);
+ }
+
+}
--- /dev/null
+<?php
+/**
+ * Horde_Imap_Client_Cclient provides an interface to an IMAP server using the
+ * PHP imap (c-client) module.
+ *
+ * PHP IMAP module: http://www.php.net/imap
+ *
+ * Optional Parameters:
+ * retries - (integer) Connection retries.
+ * DEFAULT: 3
+ * timeout - (array) Timeout value (in seconds) for various actions. Unlinke
+ * the base Horde_Imap_Client class, this driver supports an
+ * array of timeout entries as follows:
+ * 'open', 'read', 'write', 'close'
+ * If timeout is a string, the same timeout will be used for all
+ * values.
+ * DEFAULT: C-client default values
+ * validate_cert - (boolean) If using tls or ssl connections, validate the
+ * certificate?
+ * DEFAULT: Don't validate
+ *
+ * Copyright 2008 The Horde Project (http://www.horde.org/)
+ *
+ * $Horde: framework/Imap_Client/lib/Horde/Imap/Client/Cclient.php,v 1.58 2008/10/28 21:54:40 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 <slusarz@curecanti.org>
+ * @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.
+ * <pre>
+ * For the 'attributes' option, this driver will return only these
+ * attributes:
+ * '\noinferiors', '\noselect', '\marked', '\unmarked', '\referral',
+ * '\haschildren', '\hasnochildren'
+ * </pre>
+ *
+ * @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;
+ }
+
+}
--- /dev/null
+<?php
+/**
+ * Exception handler for the Horde_Imap_Client class.
+ *
+ * Copyright 2008 The Horde Project (http://www.horde.org/)
+ *
+ * $Horde: framework/Imap_Client/lib/Horde/Imap/Client/Exception.php,v 1.16 2008/10/23 04:53:13 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 <slusarz@curecanti.org>
+ * @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;
+}
--- /dev/null
+<?php
+/**
+ * Horde_Imap_Client_Socket:: provides an interface to an IMAP4rev1 server
+ * (RFC 3501) using PHP functions.
+ *
+ * Optional Parameters: NONE
+ *
+ * This driver implements the following IMAP-related RFCs:
+ * RFC 2086/4314 - ACL
+ * RFC 2087 - QUOTA
+ * RFC 2088 - LITERAL+
+ * RFC 2195 - AUTH=CRAM-MD5
+ * RFC 2221 - LOGIN-REFERRALS
+ * RFC 2342 - NAMESPACE
+ * RFC 2595/4616 - AUTH=PLAIN
+ * RFC 2831 - DIGEST-MD5 authentication mechanism.
+ * RFC 2971 - ID
+ * RFC 3501 - IMAP4rev1 specification
+ * RFC 3502 - MULTIAPPEND
+ * RFC 3516 - BINARY
+ * RFC 3691 - UNSELECT
+ * RFC 4315 - UIDPLUS
+ * RFC 4422 - SASL Authentication (for DIGEST-MD5)
+ * RFC 4466 - Collected extensions (updates RFCs 2088, 3501, 3502, 3516)
+ * RFC 4551 - CONDSTORE
+ * RFC 4731 - ESEARCH
+ * RFC 4959 - SASL-IR
+ * RFC 5032 - WITHIN
+ * RFC 5161 - ENABLE
+ * RFC 5162 - QRESYNC
+ * RFC 5182 - SEARCHRES
+ * RFC 5255 - LANGUAGE/I18NLEVEL
+ * RFC 5256 - THREAD/SORT
+ * RFC 5267 - ESORT
+ *
+ * [NO RFC] - XIMAPPROXY
+ * + Requires imapproxy v1.2.7-rc1 or later
+ * + See http://lists.andrew.cmu.edu/pipermail/imapproxy-info/2008-October/000771.html and
+ * http://lists.andrew.cmu.edu/pipermail/imapproxy-info/2008-October/000772.html
+ *
+ * TODO (or not necessary?):
+ * RFC 2177 - IDLE (probably not necessary due to the limited connection
+ * time by each HTTP/PHP request)
+ * RFC 2193 - MAILBOX-REFERRALS
+ * RFC 4467/5092 - URLAUTH
+ * RFC 4469 - CATENATE
+ * RFC 4978 - COMPRESS=DEFLATE
+ * RFC 3348/5258 - LIST-EXTENDED
+ * RFC 5257 - ANNOTATE
+ * RFC 5259 - CONVERT
+ * RFC 5267 - CONTEXT
+ *
+ * Originally based on code from:
+ * + auth.php (1.49)
+ * + imap_general.php (1.212)
+ * + imap_messages.php (revision 13038)
+ * + strings.php (1.184.2.35)
+ * from the Squirrelmail project.
+ * Copyright (c) 1999-2007 The SquirrelMail Project Team
+ *
+ * Copyright 2005-2008 The Horde Project (http://www.horde.org/)
+ *
+ * $Horde: framework/Imap_Client/lib/Horde/Imap/Client/Socket.php,v 1.99 2008/10/29 05:13:00 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 <slusarz@curecanti.org>
+ * @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][<section #>]<<partial>> (RFC 3516)
+ * see BODY[] response
+ * BINARY.SIZE[<section #>] (RFC 3516)
+ * BODY
+ * BODY[.PEEK][<section>]<<partial>>
+ * <section> = HEADER, HEADER.FIELDS, HEADER.FIELDS.NOT, MIME,
+ * TEXT, empty
+ * <<partial>> = 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:
+ * <pre>
+ * '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
+ * </pre>
+ */
+ 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:
+ * <pre>
+ * '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'.
+ * </pre>
+ */
+ 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;
+ }
+ }
+}
--- /dev/null
+<?php
+/**
+ * Horde_Imap_Client_Sort:: provides a function to sort a list of IMAP
+ * mailboxes.
+ *
+ * $Horde: framework/Imap_Client/lib/Horde/Imap/Client/Sort.php,v 1.5 2008/10/17 05:56:16 slusarz Exp $
+ *
+ * Copyright 2004-2008 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (GPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/gpl.html.
+ *
+ * @author Michael Slusarz <slusarz@horde.org>
+ * @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:
+ * <pre>
+ * '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.
+ * </pre>
+ */
+ 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);
+ }
+}
--- /dev/null
+<?php
+/**
+ * Horde_Imap_Client_Utf7imap:: provides code to convert between UTF-8 and
+ * UTF7-IMAP (RFC 3501 [5.1.3]).
+ *
+ * Originally based on code:
+ * Copyright (C) 2000 Edmund Grimley Evans <edmundo@rano.org>
+ * Released under the GPL (version 2)
+ *
+ * Translated from C to PHP by Thomas Bruederli <roundcube@gmail.com>
+ * 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 <slusarz@curecanti.org>
+ * @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;
+ }
+}
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<package packagerversion="1.4.9" version="2.0" xmlns="http://pear.php.net/dtd/package-2.0" xmlns:tasks="http://pear.php.net/dtd/tasks-1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://pear.php.net/dtd/tasks-1.0
+http://pear.php.net/dtd/tasks-1.0.xsd
+http://pear.php.net/dtd/package-2.0
+http://pear.php.net/dtd/package-2.0.xsd">
+ <name>Horde_Imap_Client</name>
+ <channel>pear.horde.org</channel>
+ <summary>Horde IMAP abstraction interface</summary>
+ <description>This package provides an abstracted API interface to various
+ IMAP4rev1 (RFC 3501) backend drivers.
+ </description>
+ <lead>
+ <name>Michael Slusarz</name>
+ <user>slusarz</user>
+ <email>slusarz@horde.org</email>
+ <active>yes</active>
+ </lead>
+ <lead>
+ <name>Chuck Hagenbuch</name>
+ <user>chuck</user>
+ <email>chuck@horde.org</email>
+ <active>yes</active>
+ </lead>
+ <date>2008-02-25</date>
+ <version>
+ <release>0.0.1</release>
+ <api>0.0.1</api>
+ </version>
+ <stability>
+ <release>alpha</release>
+ <api>alpha</api>
+ </stability>
+ <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
+ <notes>
+ * Initial release
+ </notes>
+ <contents>
+ <dir name="/">
+ <dir name="lib">
+ <dir name="Horde">
+ <dir name="Imap">
+ <dir name="Client">
+ <file name="Base.php" role="php" />
+ <file name="Cache.php" role="php" />
+ <file name="Cclient.php" role="php" />
+ <file name="Cclient-pop3.php" role="php" />
+ <file name="Exception.php" role="php" />
+ <file name="Socket.php" role="php" />
+ <file name="Sort.php" role="php" />
+ <file name="Utf7imap.php" role="php" />
+ </dir> <!-- /lib/Horde/Imap/Client -->
+ <file name="Client.php" role="php" />
+ </dir> <!-- /lib/Horde/Imap -->
+ </dir> <!-- /lib/Horde -->
+ </dir> <!-- /lib -->
+ </dir> <!-- / -->
+ </contents>
+ <dependencies>
+ <required>
+ <php>
+ <min>5.2.0</min>
+ </php>
+ <pearinstaller>
+ <min>1.5.0</min>
+ </pearinstaller>
+ </required>
+ <optional>
+ <package>
+ <name>Auth_SASL</name>
+ <channel>pear.php.net</channel>
+ </package>
+ <package>
+ <name>Horde_Cache</name>
+ <channel>pear.horde.org</channel>
+ </package>
+ <package>
+ <name>Horde_Serialize</name>
+ <channel>pear.horde.org</channel>
+ </package>
+ <package>
+ <name>MIME</name>
+ <channel>pear.horde.org</channel>
+ </package>
+ <package>
+ <name>Secret</name>
+ <channel>pear.horde.org</channel>
+ </package>
+ <extension>
+ <name>imap</name>
+ </extension>
+ <extension>
+ <name>mbstring</name>
+ </extension>
+ </optional>
+ </dependencies>
+ <phprelease>
+ <filelist>
+ <install name="lib/Horde/Imap/Client/Base.php" as="Horde/Imap/Client/Base.php" />
+ <install name="lib/Horde/Imap/Client/Cache.php" as="Horde/Imap/Client/Cache.php" />
+ <install name="lib/Horde/Imap/Client/Cclient.php" as="Horde/Imap/Client/Cclient.php" />
+ <install name="lib/Horde/Imap/Client/Cclient-pop3.php" as="Horde/Imap/Client/Cclient-pop3.php" />
+ <install name="lib/Horde/Imap/Client/Exception.php" as="Horde/Imap/Client/Exception.php" />
+ <install name="lib/Horde/Imap/Client/Socket.php" as="Horde/Imap/Client/Socket.php" />
+ <install name="lib/Horde/Imap/Client/Sort.php" as="Horde/Imap/Client/Sort.php" />
+ <install name="lib/Horde/Imap/Client/Utf7imap.php" as="Horde/Imap/Client/Utf7imap.php" />
+ <install name="lib/Horde/Imap/Client.php" as="Horde/Imap/Client.php" />
+ </filelist>
+ </phprelease>
+</package>
--- /dev/null
+<?php
+/**
+ * Test script for the Horde_Imap_Client:: library.
+ *
+ * Usage:
+ * test_client.php [[username] [[password] [[IMAP URL]]]]
+ *
+ * Username/password/hostspec on the command line will override the $params
+ * values.
+ *
+ * TODO:
+ * + Test for 'charset' searching
+ * + setQuota(), getQuota(), getQuotaRoot()
+ * + setACL(), listACLRights(), getMyACLRights()
+ * + setLanguage()
+ * + setComparator()
+ * + RFC 4551 (CONDSTORE) related functions
+ *
+ * $Horde: framework/Imap_Client/test/Horde/Imap/test_client.php,v 1.47 2008/10/23 04:53:14 slusarz Exp $
+ *
+ * @author Michael Slusarz <slusarz@horde.org>
+ * @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 = <<<EOE
+Return-Path: <test@example.com>
+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 <foo@example.com>; Sat, 26 Jul 2008 20:09:03 -0600 (MDT)
+Message-ID: <abcd1234efgh5678@test1.example.com>
+Date: Sat, 26 Jul 2008 21:10:00 -0500 (CDT)
+From: Test <test@example.com>
+To: foo@example.com
+Subject: Test e-mail 1
+Mime-Version: 1.0
+Content-Type: text/plain
+
+Test.
+EOE;
+
+$test_email2 = <<<EOE
+Return-Path: <test@example.com>
+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 <foo@example.com>; 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 <test@example.com>
+To: foo@example.com
+Subject: Re: Test e-mail 1
+Mime-Version: 1.0
+Content-Type: text/plain
+In-Reply-To: <abcd1234efgh5678@test1.example.com>
+References: <abcd1234efgh5678@test1.example.com>
+ <originalreply123123123@test1.example.com>
+
+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";
--- /dev/null
+Return-Path: <test@example.com>
+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 <foo@example.com>; Sat, 26 Jul 2008 22:55:03 -0600 (MDT)
+Message-ID: <aaabbbcccddd111222333444@test1.example.com>
+Date: Sat, 26 Jul 2008 23:55:00 -0500 (CDT)
+From: Test <test@example.com>
+To: foo@example.com
+Subject: Test e-mail 2
+Mime-Version: 1.0
+Content-Type: text/plain
+
+Test.
--- /dev/null
+Return-Path: <test@example.com>
+Delivered-To: test@example.com
+Received: from localhost (localhost [127.0.0.1])
+ by foo.example.com (Postfix) with ESMTP id BBCC02F667
+ for <test@example.com>; 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" <test@example.com>
+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
+
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <title></title>
+ </head>
+ <body>
+ <p>
+ multipart/alternative test.<br />
+ </p>
+ </body>
+</html>
+--=_5otck98hqrbh--
+
+--=_5oyqt8yksw5p
+Content-Type: message/rfc822;
+ name="Forwarded Message: Test"
+MIME-Version: 1.0
+
+Return-Path: <test@example.com>
+Delivered-To: test@example.com
+Received: from localhost (localhost [127.0.0.1])
+ by foo.example.com (Postfix) with ESMTP id E57162F667
+ for <test@example.com>; 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 <test@example.com>
+To: Test User <test@example.com>
+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--
+