<?php
/**
* Copyright 2008-2017 Horde LLC (http://www.horde.org/)
*
* See the enclosed file LICENSE for license information (LGPL). If you
* did not receive this file, see http://www.horde.org/licenses/lgpl21.
*
* @category Horde
* @copyright 2008-2017 Horde LLC
* @license http://www.horde.org/licenses/lgpl21 LGPL 2.1
* @package Imap_Client
*/
/**
* An abstracted API interface to IMAP backends supporting the IMAP4rev1
* protocol (RFC 3501).
*
* @author Michael Slusarz <slusarz@horde.org>
* @category Horde
* @copyright 2008-2017 Horde LLC
* @license http://www.horde.org/licenses/lgpl21 LGPL 2.1
* @package Imap_Client
*
* @property-read Horde_Imap_Client_Base_Alert $alerts_ob
The alert reporting object (@since 2.26.0)
* @property-read Horde_Imap_Client_Data_Capability $capability
* A capability object. (@since 2.24.0)
* @property-read Horde_Imap_Client_Data_SearchCharset $search_charset
* A search charset object. (@since 2.24.0)
* @property-read Horde_Imap_Client_Url $url The URL object for the current
* connection parameters (@since 2.24.0)
*/
abstract class Horde_Imap_Client_Base
implements Serializable, SplObserver
{
/** Serialized version. */
const VERSION = 3;
/** Cache names for miscellaneous data. */
const CACHE_MODSEQ = '_m';
const CACHE_SEARCH = '_s';
/* @since 2.9.0 */
const CACHE_SEARCHID = '_i';
/** Cache names used exclusively within this class. @since 2.11.0 */
const CACHE_DOWNGRADED = 'HICdg';
/**
* The list of fetch fields that can be cached, and their cache names.
*
* @var array
*/
public $cacheFields = array(
Horde_Imap_Client::FETCH_ENVELOPE => 'HICenv',
Horde_Imap_Client::FETCH_FLAGS => 'HICflags',
Horde_Imap_Client::FETCH_HEADERS => 'HIChdrs',
Horde_Imap_Client::FETCH_IMAPDATE => 'HICdate',
Horde_Imap_Client::FETCH_SIZE => 'HICsize',
Horde_Imap_Client::FETCH_STRUCTURE => 'HICstruct'
);
/**
* Has the internal configuration changed?
*
* @var boolean
*/
public $changed = false;
/**
* Horde_Imap_Client is optimized for short (i.e. 1 seconds) scripts. It
* makes heavy use of mailbox caching to save on server accesses. This
* property should be set to false for long-running scripts, or else
* status() data may not reflect the current state of the mailbox on the
* server.
*
* @since 2.14.0
*
* @var boolean
*/
public $statuscache = true;
/**
* Alerts reporting object.
*
* @var Horde_Imap_Client_Base_Alerts
*/
protected $_alerts;
/**
* The Horde_Imap_Client_Cache object.
*
* @var Horde_Imap_Client_Cache
*/
protected $_cache = null;
/**
* Connection to the IMAP server.
*
* @var Horde\Socket\Client
*/
protected $_connection = null;
/**
* The debug object.
*
* @var Horde_Imap_Client_Base_Debug
*/
protected $_debug = null;
/**
* The default ports to use for a connection.
* First element is non-secure, second is SSL.
*
* @var array
*/
protected $_defaultPorts = array();
/**
* The fetch data object type to return.
*
* @var string
*/
protected $_fetchDataClass = 'Horde_Imap_Client_Data_Fetch';
/**
* Cached server data.
*
* @var array
*/
protected $_init;
/**
* Is there an active authenticated connection to the IMAP Server?
*
* @var boolean
*/
protected $_isAuthenticated = false;
/**
* The current mailbox selection mode.
*
* @var integer
*/
protected $_mode = 0;
/**
* Hash containing connection parameters.
* This hash never changes.
*
* @var array
*/
protected $_params = array();
/**
* The currently selected mailbox.
*
* @var Horde_Imap_Client_Mailbox
*/
protected $_selected = null;
/**
* Temp array (destroyed at end of process).
*
* @var array
*/
protected $_temp = array();
/**
* Constructor.
*
* @param array $params Configuration parameters:
* <pre>
* - cache: (array) If set, caches data from fetch(), search(), and
* thread() calls. Requires the horde/Cache package to be
* installed. The array can contain the following keys (see
* Horde_Imap_Client_Cache for default values):
* - backend: [REQUIRED (or cacheob)] (Horde_Imap_Client_Cache_Backend)
* Backend cache driver [@since 2.9.0].
* - fetch_ignore: (array) A list of mailboxes to ignore when storing
* fetch data.
* - fields: (array) The fetch criteria to cache. If not defined, all
* cacheable data is cached. The following is a list of
* criteria that can be cached:
* - Horde_Imap_Client::FETCH_ENVELOPE
* - Horde_Imap_Client::FETCH_FLAGS
* Only if server supports CONDSTORE extension
* - Horde_Imap_Client::FETCH_HEADERS
* Only for queries that specifically request caching
* - Horde_Imap_Client::FETCH_IMAPDATE
* - Horde_Imap_Client::FETCH_SIZE
* - Horde_Imap_Client::FETCH_STRUCTURE
* - capability_ignore: (array) A list of IMAP capabilites to ignore, even
* if they are supported on the server.
* DEFAULT: No supported capabilities are ignored.
* - comparator: (string) The search comparator to use instead of the
* default server comparator. See setComparator() for
* format.
* DEFAULT: Use the server default
* - context: (array) Any context parameters passed to
* stream_create_context(). @since 2.27.0
* - debug: (string) If set, will output debug information to the stream
* provided. The value can be any PHP supported wrapper that can
* be opened via PHP's fopen() function.
* DEFAULT: No debug output
* - hostspec: (string) The hostname or IP address of the server.
* DEFAULT: 'localhost'
* - id: (array) Send ID information to the server (only if server
* supports the ID extension). An array with the keys as the fields
* to send and the values being the associated values. See RFC 2971
* [3.3] for a list of standard 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
* - password: (mixed) The user password. Either a string or a
* Horde_Imap_Client_Base_Password object [@since 2.14.0].
* - 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 (No encryption)
* - 'ssl' (Auto-detect SSL version)
* - 'sslv2' (Force SSL version 3)
* - 'sslv3' (Force SSL version 2)
* - 'tls' (TLS; started via protocol-level negotation over
* unencrypted channel; RECOMMENDED way of initiating secure
* connection)
* - 'tlsv1' (TLS direct version 1.x connection to server) [@since
* 2.16.0]
* - true (TLS if available/necessary) [@since 2.15.0]
* DEFAULT: false
* - timeout: (integer) Connection timeout, in seconds.
* DEFAULT: 30 seconds
* - username: (string) [REQUIRED] The username.
* - authusername (string) The username used for SASL authentication.
* If specified this is the user name whose password is used
* (e.g. administrator).
* Only valid for RFC 2595/4616 - PLAIN SASL mechanism.
* DEFAULT: the same value provided in the username parameter.
* </pre>
*/
public function __construct(array $params = array())
{
if (!isset($params['username'])) {
throw new InvalidArgumentException('Horde_Imap_Client requires a username.');
}
$this->_setInit();
// Default values.
$params = array_merge(array(
'context' => array(),
'hostspec' => 'localhost',
'secure' => false,
'timeout' => 30
), array_filter($params));
if (!isset($params['port']) && strpos($params['hostspec'], 'unix://') !== 0) {
$params['port'] = (!empty($params['secure']) && in_array($params['secure'], array('ssl', 'sslv2', 'sslv3'), true))
? $this->_defaultPorts[1]
: $this->_defaultPorts[0];
}
if (empty($params['cache'])) {
$params['cache'] = array('fields' => array());
} elseif (empty($params['cache']['fields'])) {
$params['cache']['fields'] = $this->cacheFields;
} else {
$params['cache']['fields'] = array_flip($params['cache']['fields']);
}
if (empty($params['cache']['fetch_ignore'])) {
$params['cache']['fetch_ignore'] = array();
}
$this->_params = $params;
if (isset($params['password'])) {
$this->setParam('password', $params['password']);
}
$this->changed = true;
$this->_initOb();
}
/**
* Get encryption key.
*
* @deprecated Pass callable into 'password' parameter instead.
*
* @return string The encryption key.
*/
protected function _getEncryptKey()
{
if (is_callable($ekey = $this->getParam('encryptKey'))) {
return call_user_func($ekey);
}
throw new InvalidArgumentException('encryptKey parameter is not a valid callback.');
}
/**
* Do initialization tasks.
*/
protected function _initOb()
{
register_shutdown_function(array($this, 'shutdown'));
$this->_alerts = new Horde_Imap_Client_Base_Alerts();
// @todo: Remove (BC)
$this->_alerts->attach($this);
$this->_debug = ($debug = $this->getParam('debug'))
? new Horde_Imap_Client_Base_Debug($debug)
: new Horde_Support_Stub();
// @todo: Remove (BC purposes)
if (isset($this->_init['capability']) &&
!is_object($this->_init['capability'])) {
$this->_setInit('capability');
}
foreach (array('capability', 'search_charset') as $val) {
if (isset($this->_init[$val])) {
$this->_init[$val]->attach($this);
}
}
}
/**
* Shutdown actions.
*/
public function shutdown()
{
try {
$this->logout();
} catch (Horde_Imap_Client_Exception $e) {
}
}
/**
* This object can not be cloned.
*/
public function __clone()
{
throw new LogicException('Object cannot be cloned.');
}
/**
*/
> #[ReturnTypeWillChange]
public function update(SplSubject $subject)
{
if (($subject instanceof Horde_Imap_Client_Data_Capability) ||
($subject instanceof Horde_Imap_Client_Data_SearchCharset)) {
$this->changed = true;
}
/* @todo: BC - remove */
if ($subject instanceof Horde_Imap_Client_Base_Alerts) {
$this->_temp['alerts'][] = $subject->getLast()->alert;
}
}
/**
*/
public function serialize()
{
< return serialize(array(
< 'i' => $this->_init,
< 'p' => $this->_params,
< 'v' => self::VERSION
< ));
> return serialize($this->__serialize());
}
/**
*/
public function unserialize($data)
{
$data = @unserialize($data);
< if (!is_array($data) ||
< !isset($data['v']) ||
< ($data['v'] != self::VERSION)) {
> if (!is_array($data)) {
> throw new Exception('Cache version change');
> }
> $this->__unserialize($data);
> }
>
> /**
> * @return array
> */
> public function __serialize()
> {
> return array(
> 'i' => $this->_init,
> 'p' => $this->_params,
> 'v' => self::VERSION
> );
> }
>
> public function __unserialize(array $data)
> {
> if (empty($data['v']) || $data['v'] != self::VERSION) {
throw new Exception('Cache version change');
}
$this->_init = $data['i'];
$this->_params = $data['p'];
$this->_initOb();
}
/**
*/
public function __get($name)
{
switch ($name) {
case 'alerts_ob':
return $this->_alerts;
case 'capability':
return $this->_capability();
case 'search_charset':
if (!isset($this->_init['search_charset'])) {
$this->_init['search_charset'] = new Horde_Imap_Client_Data_SearchCharset();
$this->_init['search_charset']->attach($this);
}
$this->_init['search_charset']->setBaseOb($this);
return $this->_init['search_charset'];
case 'url':
$url = new Horde_Imap_Client_Url();
$url->hostspec = $this->getParam('hostspec');
$url->port = $this->getParam('port');
$url->protocol = 'imap';
return $url;
}
}
/**
* Set an initialization value.
*
* @param string $key The initialization key. If null, resets all keys.
* @param mixed $val The cached value. If null, removes the key.
*/
public function _setInit($key = null, $val = null)
{
if (is_null($key)) {
$this->_init = array();
} elseif (is_null($val)) {
unset($this->_init[$key]);
} else {
switch ($key) {
case 'capability':
if ($ci = $this->getParam('capability_ignore')) {
$ignored = array();
foreach ($ci as $val2) {
$c = explode('=', $val2);
if ($val->query($c[0], isset($c[1]) ? $c[1] : null)) {
$ignored[] = $val2;
$val->remove($c[0], isset($c[1]) ? $c[1] : null);
}
}
if ($this->_debug->debug && !empty($ignored)) {
$this->_debug->info(sprintf(
'CONFIG: IGNORING these IMAP capabilities: %s',
implode(', ', $ignored)
));
}
}
$val->attach($this);
break;
}
/* Nothing has changed. */
if (isset($this->_init[$key]) && ($this->_init[$key] === $val)) {
return;
}
$this->_init[$key] = $val;
}
$this->changed = true;
}
/**
* Initialize the Horde_Imap_Client_Cache object, if necessary.
*
* @param boolean $current If true, we are going to update the currently
* selected mailbox. Add an additional check to
* see if caching is available in current
* mailbox.
*
* @return boolean Returns true if caching is enabled.
*/
protected function _initCache($current = false)
{
$c = $this->getParam('cache');
if (empty($c['fields'])) {
return false;
}
if (is_null($this->_cache)) {
if (isset($c['backend'])) {
$backend = $c['backend'];
} elseif (isset($c['cacheob'])) {
/* Deprecated */
$backend = new Horde_Imap_Client_Cache_Backend_Cache($c);
} else {
return false;
}
$this->_cache = new Horde_Imap_Client_Cache(array(
'backend' => $backend,
'baseob' => $this,
'debug' => $this->_debug
));
}
return $current
/* If UIDs are labeled as not sticky, don't cache since UIDs will
* change on every access. */
? !($this->_mailboxOb()->getStatus(Horde_Imap_Client::STATUS_UIDNOTSTICKY))
: true;
}
/**
* Returns a value from the internal params array.
*
* @param string $key The param key.
*
* @return mixed The param value, or null if not found.
*/
public function getParam($key)
{
/* Passwords may be stored encrypted. */
switch ($key) {
case 'password':
if (isset($this->_params[$key]) &&
($this->_params[$key] instanceof Horde_Imap_Client_Base_Password)) {
return $this->_params[$key]->getPassword();
}
// DEPRECATED
if (!empty($this->_params['_passencrypt'])) {
try {
$secret = new Horde_Secret();
return $secret->read($this->_getEncryptKey(), $this->_params['password']);
} catch (Exception $e) {
return null;
}
}
break;
}
return isset($this->_params[$key])
? $this->_params[$key]
: null;
}
/**
* Sets a configuration parameter value.
*
* @param string $key The param key.
* @param mixed $val The param value.
*/
public function setParam($key, $val)
{
switch ($key) {
case 'password':
if ($val instanceof Horde_Imap_Client_Base_Password) {
break;
}
// DEPRECATED: Encrypt password.
try {
$encrypt_key = $this->_getEncryptKey();
if (strlen($encrypt_key)) {
$secret = new Horde_Secret();
$val = $secret->write($encrypt_key, $val);
$this->_params['_passencrypt'] = true;
}
} catch (Exception $e) {}
break;
}
$this->_params[$key] = $val;
$this->changed = true;
}
/**
* Returns the Horde_Imap_Client_Cache object used, if available.
*
* @return mixed Either the cache object or null.
*/
public function getCache()
{
$this->_initCache();
return $this->_cache;
}
/**
* Returns the correct IDs object for use with this driver.
*
* @param mixed $ids Either self::ALL, self::SEARCH_RES, self::LARGEST,
* Horde_Imap_Client_Ids object, array, or sequence
* string.
* @param boolean $sequence Are $ids message sequence numbers?
*
* @return Horde_Imap_Client_Ids The IDs object.
*/
public function getIdsOb($ids = null, $sequence = false)
{
return new Horde_Imap_Client_Ids($ids, $sequence);
}
/**
* Returns whether the IMAP server supports the given capability
* (See RFC 3501 [6.1.1]).
*
* @deprecated Use $capability property instead.
*
* @param string $capability The capability string to query.
*
* @return 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)
{
try {
$c = $this->_capability();
return ($out = $c->getParams($capability))
? $out
: $c->query($capability);
} catch (Horde_Imap_Client_Exception $e) {
return false;
}
}
/**
* Get CAPABILITY information from the IMAP server.
*
* @deprecated Use $capability property instead.
*
* @return array The capability array.
*
* @throws Horde_Imap_Client_Exception
*/
public function capability()
{
return $this->_capability()->toArray();
}
/**
* Query server capability.
*
* Required because internal code can't call capability via magic method
* directly - it may not exist yet, the creation code may call capability
* recursively, and __get() doesn't allow recursive calls to the same
* property (chicken/egg issue).
*
* @return mixed The capability object if no arguments provided. If
* arguments are provided, they are passed to the query()
* method and this value is returned.
* @throws Horde_Imap_Client_Exception
*/
protected function _capability()
{
if (!isset($this->_init['capability'])) {
$this->_initCapability();
}
return ($args = func_num_args())
? $this->_init['capability']->query(func_get_arg(0), ($args > 1) ? func_get_arg(1) : null)
: $this->_init['capability'];
}
/**
* Retrieve capability information from the IMAP server.
*
* @throws Horde_Imap_Client_Exception
*/
abstract protected function _initCapability();
/**
* Send a NOOP command (RFC 3501 [6.1.2]).
*
* @throws Horde_Imap_Client_Exception
*/
public function noop()
{
if (!$this->_connection) {
// NOOP can be called in the unauthenticated state.
$this->_connect();
}
$this->_noop();
}
/**
* Send a NOOP command.
*
* @throws Horde_Imap_Client_Exception
*/
abstract protected function _noop();
/**
* Get the NAMESPACE information from the IMAP server (RFC 2342).
*
* @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 be UTF-8
* strings.
* @param array $opts Additional options:
* - ob_return: (boolean) If true, returns a
* Horde_Imap_Client_Namespace_List object instead of an
* array.
*
* @return mixed A Horde_Imap_Client_Namespace_List object if
* 'ob_return', is true. Otherwise, an array of namespace
* objects (@deprecated) with the name as the key (UTF-8)
* and the following values:
* <pre>
* - delimiter: (string) The namespace delimiter.
* - hidden: (boolean) Is this a hidden namespace?
* - name: (string) The namespace name (UTF-8).
* - translation: (string) Returns the translated name of the namespace
* (UTF-8). Requires RFC 5255 and a previous call to
* setLanguage().
* - type: (integer) The namespace type. Either:
* - Horde_Imap_Client::NS_PERSONAL
* - Horde_Imap_Client::NS_OTHER
* - Horde_Imap_Client::NS_SHARED
* </pre>
*
* @throws Horde_Imap_Client_Exception
*/
public function getNamespaces(
array $additional = array(), array $opts = array()
)
{
$additional = array_map('strval', $additional);
$sig = hash(
'md5',
json_encode($additional) . intval(empty($opts['ob_return']))
);
if (isset($this->_init['namespace'][$sig])) {
$ns = $this->_init['namespace'][$sig];
} else {
$this->login();
$ns = $this->_getNamespaces();
/* Skip namespaces if we have already auto-detected them. Also,
* hidden namespaces cannot be empty. */
$to_process = array_diff(array_filter($additional, 'strlen'), array_map('strlen', iterator_to_array($ns)));
if (!empty($to_process)) {
foreach ($this->listMailboxes($to_process, Horde_Imap_Client::MBOX_ALL, array('delimiter' => true)) as $key => $val) {
$ob = new Horde_Imap_Client_Data_Namespace();
$ob->delimiter = $val['delimiter'];
$ob->hidden = true;
$ob->name = $key;
$ob->type = $ob::NS_SHARED;
$ns[$val] = $ob;
}
}
if (!count($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('', Horde_Imap_Client::MBOX_ALL, array('delimiter' => true));
$first = reset($mbox);
$ob = new Horde_Imap_Client_Data_Namespace();
$ob->delimiter = $first['delimiter'];
$ns[''] = $ob;
}
$this->_init['namespace'][$sig] = $ns;
$this->_setInit('namespace', $this->_init['namespace']);
}
if (!empty($opts['ob_return'])) {
return $ns;
}
/* @todo Remove for 3.0 */
$out = array();
foreach ($ns as $key => $val) {
$out[$key] = array(
'delimiter' => $val->delimiter,
'hidden' => $val->hidden,
'name' => $val->name,
'translation' => $val->translation,
'type' => $val->type
);
}
return $out;
}
/**
* Get the NAMESPACE information from the IMAP server.
*
* @return Horde_Imap_Client_Namespace_List Namespace list object.
*
* @throws Horde_Imap_Client_Exception
*/
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->_connection && $this->_connection->secure);
}
/**
* Connect to the remote server.
*
* @throws Horde_Imap_Client_Exception
*/
abstract protected function _connect();
/**
* Return a list of alerts that MUST be presented to the user (RFC 3501
* [7.1]).
*
* @deprecated Add an observer to the $alerts_ob property instead.
*
* @return array An array of alert messages.
*/
public function alerts()
{
$alerts = isset($this->_temp['alerts'])
? $this->_temp['alerts']
: array();
unset($this->_temp['alerts']);
return $alerts;
}
/**
* Login to the IMAP server.
*
* @throws Horde_Imap_Client_Exception
*/
public function login()
{
if (!$this->_isAuthenticated && $this->_login()) {
if ($this->getParam('id')) {
try {
$this->sendID();
/* ID is queued - force sending the queued command. */
$this->_sendCmd($this->_pipeline());
} catch (Horde_Imap_Client_Exception_NoSupportExtension $e) {
// Ignore if server doesn't support ID extension.
}
}
if ($this->getParam('comparator')) {
try {
$this->setComparator();
} catch (Horde_Imap_Client_Exception_NoSupportExtension $e) {
// Ignore if server doesn't support I18NLEVEL=2
}
}
}
$this->_isAuthenticated = true;
}
/**
* Login to the IMAP server.
*
* @return boolean Return true if global login tasks should be run.
*
* @throws Horde_Imap_Client_Exception
*/
abstract protected function _login();
/**
* Logout from the IMAP server (see RFC 3501 [6.1.3]).
*/
public function logout()
{
if ($this->_isAuthenticated && $this->_connection->connected) {
$this->_logout();
$this->_connection->close();
}
$this->_connection = $this->_selected = null;
$this->_isAuthenticated = false;
$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).
*
* @param array $info Overrides the value of the 'id' param and sends
* this information instead.
*
* @throws Horde_Imap_Client_Exception
* @throws Horde_Imap_Client_Exception_NoSupportExtension
*/
public function sendID($info = null)
{
if (!$this->_capability('ID')) {
throw new Horde_Imap_Client_Exception_NoSupportExtension('ID');
}
$this->_sendID(is_null($info) ? ($this->getParam('id') ?: array()) : $info);
}
/**
* Send ID information to the IMAP server (RFC 2971).
*
* @param array $info The information to send to the server.
*
* @throws Horde_Imap_Client_Exception
*/
abstract protected function _sendID($info);
/**
* Return ID information from the IMAP server (RFC 2971).
*
* @return array An array of information returned, with the keys as the
* 'field' and the values as the 'value'.
*
* @throws Horde_Imap_Client_Exception
* @throws Horde_Imap_Client_Exception_NoSupportExtension
*/
public function getID()
{
if (!$this->_capability('ID')) {
throw new Horde_Imap_Client_Exception_NoSupportExtension('ID');
}
return $this->_getID();
}
/**
* Return ID information from the IMAP server (RFC 2971).
*
* @return array An array of information returned, with the keys as the
* 'field' and the values as the 'value'.
*
* @throws Horde_Imap_Client_Exception
*/
abstract protected function _getID();
/**
* Sets the preferred language for server response messages (RFC 5255).
*
* @param array $langs 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.
*
* @throws Horde_Imap_Client_Exception
*/
public function setLanguage($langs = null)
{
$lang = null;
if ($this->_capability('LANGUAGE')) {
$lang = is_null($langs)
? $this->getParam('lang')
: $langs;
}
return is_null($lang)
? null
: $this->_setLanguage($lang);
}
/**
* Sets the preferred language for server response messages (RFC 5255).
*
* @param array $langs The preferred list of languages.
*
* @return string The language accepted by the server, or null if the
* default language is used.
*
* @throws Horde_Imap_Client_Exception
*/
abstract protected function _setLanguage($langs);
/**
* Gets the preferred language for server response messages (RFC 5255).
*
* @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.
*
* @throws Horde_Imap_Client_Exception
*/
public function getLanguage($list = false)
{
if (!$this->_capability('LANGUAGE')) {
return $list ? array() : null;
}
return $this->_getLanguage($list);
}
/**
* Gets the preferred language for server response messages (RFC 5255).
*
* @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.
*
* @throws Horde_Imap_Client_Exception
*/
abstract protected function _getLanguage($list);
/**
* Open a mailbox.
*
* @param mixed $mailbox The mailbox to open. Either a
* Horde_Imap_Client_Mailbox object or a string
* (UTF-8).
* @param integer $mode The access mode. Either
* - Horde_Imap_Client::OPEN_READONLY
* - Horde_Imap_Client::OPEN_READWRITE
* - Horde_Imap_Client::OPEN_AUTO
*
* @throws Horde_Imap_Client_Exception
*/
public function openMailbox($mailbox, $mode = Horde_Imap_Client::OPEN_AUTO)
{
$this->login();
$change = false;
$mailbox = Horde_Imap_Client_Mailbox::get($mailbox);
if ($mode == Horde_Imap_Client::OPEN_AUTO) {
if (is_null($this->_selected) ||
!$mailbox->equals($this->_selected)) {
$mode = Horde_Imap_Client::OPEN_READONLY;
$change = true;
}
} else {
$change = (is_null($this->_selected) ||
!$mailbox->equals($this->_selected) ||
($mode != $this->_mode));
}
if ($change) {
$this->_openMailbox($mailbox, $mode);
$this->_mailboxOb()->open = true;
if ($this->_initCache(true)) {
$this->_condstoreSync();
}
}
}
/**
* Open a mailbox.
*
* @param Horde_Imap_Client_Mailbox $mailbox The mailbox to open.
* @param integer $mode The access mode.
*
* @throws Horde_Imap_Client_Exception
*/
abstract protected function _openMailbox(Horde_Imap_Client_Mailbox $mailbox,
$mode);
/**
* Called when the selected mailbox is changed.
*
* @param mixed $mailbox The selected mailbox or null.
* @param integer $mode The access mode.
*/
protected function _changeSelected($mailbox = null, $mode = null)
{
$this->_mode = $mode;
if (is_null($mailbox)) {
$this->_selected = null;
} else {
$this->_selected = clone $mailbox;
$this->_mailboxOb()->reset();
}
}
/**
* Return the Horde_Imap_Client_Base_Mailbox object.
*
* @param string $mailbox The mailbox name. Defaults to currently
* selected mailbox.
*
* @return Horde_Imap_Client_Base_Mailbox Mailbox object.
*/
protected function _mailboxOb($mailbox = null)
{
$name = is_null($mailbox)
? strval($this->_selected)
: strval($mailbox);
if (!isset($this->_temp['mailbox_ob'][$name])) {
$this->_temp['mailbox_ob'][$name] = new Horde_Imap_Client_Base_Mailbox();
}
return $this->_temp['mailbox_ob'][$name];
}
/**
* Return the currently opened mailbox and access mode.
*
* @return mixed Null if no mailbox selected, or an array with two
* elements:
* - mailbox: (Horde_Imap_Client_Mailbox) The mailbox object.
* - mode: (integer) Current mode.
*
* @throws Horde_Imap_Client_Exception
*/
public function currentMailbox()
{
return is_null($this->_selected)
? null
: array(
'mailbox' => clone $this->_selected,
'mode' => $this->_mode
);
}
/**
* Create a mailbox.
*
* @param mixed $mailbox The mailbox to create. Either a
* Horde_Imap_Client_Mailbox object or a string
* (UTF-8).
* @param array $opts Additional options:
* - special_use: (array) An array of special-use flags to mark the
* mailbox with. The server MUST support RFC 6154.
*
* @throws Horde_Imap_Client_Exception
*/
public function createMailbox($mailbox, array $opts = array())
{
$this->login();
if (!$this->_capability('CREATE-SPECIAL-USE')) {
unset($opts['special_use']);
}
$this->_createMailbox(Horde_Imap_Client_Mailbox::get($mailbox), $opts);
}
/**
* Create a mailbox.
*
* @param Horde_Imap_Client_Mailbox $mailbox The mailbox to create.
* @param array $opts Additional options. See
* createMailbox().
*
* @throws Horde_Imap_Client_Exception
*/
abstract protected function _createMailbox(Horde_Imap_Client_Mailbox $mailbox,
$opts);
/**
* Delete a mailbox.
*
* @param mixed $mailbox The mailbox to delete. Either a
* Horde_Imap_Client_Mailbox object or a string
* (UTF-8).
*
* @throws Horde_Imap_Client_Exception
*/
public function deleteMailbox($mailbox)
{
$this->login();
$mailbox = Horde_Imap_Client_Mailbox::get($mailbox);
$this->_deleteMailbox($mailbox);
$this->_deleteMailboxPost($mailbox);
}
/**
* Delete a mailbox.
*
* @param Horde_Imap_Client_Mailbox $mailbox The mailbox to delete.
*
* @throws Horde_Imap_Client_Exception
*/
abstract protected function _deleteMailbox(Horde_Imap_Client_Mailbox $mailbox);
/**
* Actions to perform after a mailbox delete.
*
* @param Horde_Imap_Client_Mailbox $mailbox The deleted mailbox.
*/
protected function _deleteMailboxPost(Horde_Imap_Client_Mailbox $mailbox)
{
/* Delete mailbox caches. */
if ($this->_initCache()) {
$this->_cache->deleteMailbox($mailbox);
}
unset($this->_temp['mailbox_ob'][strval($mailbox)]);
/* Unsubscribe from mailbox. */
try {
$this->subscribeMailbox($mailbox, false);
} catch (Horde_Imap_Client_Exception $e) {
// Ignore failed unsubscribe request
}
}
/**
* Rename a mailbox.
*
* @param mixed $old The old mailbox name. Either a
* Horde_Imap_Client_Mailbox object or a string (UTF-8).
* @param mixed $new The new mailbox name. Either a
* Horde_Imap_Client_Mailbox object or a string (UTF-8).
*
* @throws Horde_Imap_Client_Exception
*/
public function renameMailbox($old, $new)
{
// Login will be handled by first listMailboxes() call.
$old = Horde_Imap_Client_Mailbox::get($old);
$new = Horde_Imap_Client_Mailbox::get($new);
/* Check if old mailbox(es) were subscribed to. */
$base = $this->listMailboxes($old, Horde_Imap_Client::MBOX_SUBSCRIBED, array('delimiter' => true));
if (empty($base)) {
$base = $this->listMailboxes($old, Horde_Imap_Client::MBOX_ALL, array('delimiter' => true));
$base = reset($base);
$subscribed = array();
} else {
$base = reset($base);
$subscribed = array($base['mailbox']);
}
$all_mboxes = array($base['mailbox']);
if (strlen($base['delimiter'])) {
$search = $old->list_escape . $base['delimiter'] . '*';
$all_mboxes = array_merge($all_mboxes, $this->listMailboxes($search, Horde_Imap_Client::MBOX_ALL, array('flat' => true)));
$subscribed = array_merge($subscribed, $this->listMailboxes($search, Horde_Imap_Client::MBOX_SUBSCRIBED, array('flat' => true)));
}
$this->_renameMailbox($old, $new);
/* Delete mailbox actions. */
foreach ($all_mboxes as $val) {
$this->_deleteMailboxPost($val);
}
foreach ($subscribed as $val) {
try {
$this->subscribeMailbox(new Horde_Imap_Client_Mailbox(substr_replace($val, $new, 0, strlen($old))));
} catch (Horde_Imap_Client_Exception $e) {
// Ignore failed subscription requests
}
}
}
/**
* Rename a mailbox.
*
* @param Horde_Imap_Client_Mailbox $old The old mailbox name.
* @param Horde_Imap_Client_Mailbox $new The new mailbox name.
*
* @throws Horde_Imap_Client_Exception
*/
abstract protected function _renameMailbox(Horde_Imap_Client_Mailbox $old,
Horde_Imap_Client_Mailbox $new);
/**
* Manage subscription status for a mailbox.
*
* @param mixed $mailbox The mailbox to [un]subscribe to. Either a
* Horde_Imap_Client_Mailbox object or a string
* (UTF-8).
* @param boolean $subscribe True to subscribe, false to unsubscribe.
*
* @throws Horde_Imap_Client_Exception
*/
public function subscribeMailbox($mailbox, $subscribe = true)
{
$this->login();
$this->_subscribeMailbox(Horde_Imap_Client_Mailbox::get($mailbox), (bool)$subscribe);
}
/**
* Manage subscription status for a mailbox.
*
* @param Horde_Imap_Client_Mailbox $mailbox The mailbox to [un]subscribe
* to.
* @param boolean $subscribe True to subscribe, false to
* unsubscribe.
*
* @throws Horde_Imap_Client_Exception
*/
abstract protected function _subscribeMailbox(Horde_Imap_Client_Mailbox $mailbox,
$subscribe);
/**
* Obtain a list of mailboxes matching a pattern.
*
* @param mixed $pattern The mailbox search pattern(s) (see RFC 3501
* [6.3.8] for the format). A UTF-8 string or an
* array of strings. If a Horde_Imap_Client_Mailbox
* object is given, it is escaped (i.e. wildcard
* patterns are converted to return the miminal
* number of matches possible).
* @param integer $mode Which mailboxes to return. Either:
* - Horde_Imap_Client::MBOX_SUBSCRIBED
* Return subscribed mailboxes.
* - Horde_Imap_Client::MBOX_SUBSCRIBED_EXISTS
* Return subscribed mailboxes that exist on the server.
* - Horde_Imap_Client::MBOX_UNSUBSCRIBED
* Return unsubscribed mailboxes.
* - Horde_Imap_Client::MBOX_ALL
* Return all mailboxes regardless of subscription status.
* - Horde_Imap_Client::MBOX_ALL_SUBSCRIBED (@since 2.23.0)
* Return all mailboxes regardless of subscription status, and ensure
* the '\subscribed' attribute is set if mailbox is subscribed
* (implies 'attributes' option is true).
* @param array $options Additional options:
* <pre>
* - attributes: (boolean) If true, return attribute information under
* the 'attributes' key.
* DEFAULT: Do not return this information.
* - children: (boolean) Tell server to return children attribute
* information (\HasChildren, \HasNoChildren). Requires the
* LIST-EXTENDED extension to guarantee this information is
* returned. Server MAY return this attribute without this
* option, or if the CHILDREN extension is available, but it
* is not guaranteed.
* DEFAULT: false
* - flat: (boolean) If true, return a flat list of mailbox names only.
* Overrides the 'attributes' option.
* DEFAULT: Do not return flat list.
* - recursivematch: (boolean) Force the server to return information
* about parent mailboxes that don't match other
* selection options, but have some sub-mailboxes that
* do. Information about children is returned in the
* CHILDINFO extended data item ('extended'). Requires
* the LIST-EXTENDED extension.
* DEFAULT: false
* - remote: (boolean) Tell server to return mailboxes that reside on
* another server. Requires the LIST-EXTENDED extension.
* DEFAULT: false
* - special_use: (boolean) Tell server to return special-use attribute
* information (see Horde_Imap_Client SPECIALUSE_*
* constants). Server must support the SPECIAL-USE return
* option for this setting to have any effect.
* DEFAULT: false
* - status: (integer) Tell server to return status information. The
* value is a bitmask that may contain any of:
* - Horde_Imap_Client::STATUS_MESSAGES
* - Horde_Imap_Client::STATUS_RECENT
* - Horde_Imap_Client::STATUS_UIDNEXT
* - Horde_Imap_Client::STATUS_UIDVALIDITY
* - Horde_Imap_Client::STATUS_UNSEEN
* - Horde_Imap_Client::STATUS_HIGHESTMODSEQ
* DEFAULT: 0
* - 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 a list
* of Horde_Imap_Client_Mailbox objects. Otherwise, the
* keys are UTF-8 mailbox names and the values are arrays
* with these keys:
* - attributes: (array) List of lower-cased attributes [only if
* 'attributes' option is true].
* - delimiter: (string) The delimiter for the mailbox.
* - extended: (TODO) TODO [only if 'recursivematch' option is true and
* LIST-EXTENDED extension is supported on the server].
* - mailbox: (Horde_Imap_Client_Mailbox) The mailbox object.
* - status: (array) See status() [only if 'status' option is true].
*
* @throws Horde_Imap_Client_Exception
*/
public function listMailboxes($pattern,
$mode = Horde_Imap_Client::MBOX_ALL,
array $options = array())
{
$this->login();
$pattern = is_array($pattern)
? array_unique($pattern)
: array($pattern);
/* Prepare patterns. */
$plist = array();
foreach ($pattern as $val) {
if ($val instanceof Horde_Imap_Client_Mailbox) {
$val = $val->list_escape;
}
$plist[] = Horde_Imap_Client_Mailbox::get(preg_replace(
array("/\*{2,}/", "/\%{2,}/"),
array('*', '%'),
Horde_Imap_Client_Utf7imap::Utf8ToUtf7Imap($val)
), true);
}
if (isset($options['special_use']) &&
!$this->_capability('SPECIAL-USE')) {
unset($options['special_use']);
}
$ret = $this->_listMailboxes($plist, $mode, $options);
if (!empty($options['status']) &&
!$this->_capability('LIST-STATUS')) {
foreach ($this->status(array_keys($ret), $options['status']) as $key => $val) {
$ret[$key]['status'] = $val;
}
}
if (empty($options['sort'])) {
return $ret;
}
$list_ob = new Horde_Imap_Client_Mailbox_List(empty($options['flat']) ? array_keys($ret) : $ret);
$sorted = $list_ob->sort(array(
'delimiter' => empty($options['sort_delimiter']) ? '.' : $options['sort_delimiter']
));
if (!empty($options['flat'])) {
return $sorted;
}
$out = array();
foreach ($sorted as $val) {
$out[$val] = $ret[$val];
}
return $out;
}
/**
* Obtain a list of mailboxes matching a pattern.
*
* @param array $pattern The mailbox search patterns
* (Horde_Imap_Client_Mailbox objects).
* @param integer $mode Which mailboxes to return.
* @param array $options Additional options.
*
* @return array See listMailboxes().
*
* @throws Horde_Imap_Client_Exception
*/
abstract protected function _listMailboxes($pattern, $mode, $options);
/**
* Obtain status information for a mailbox.
*
* @param mixed $mailbox The mailbox(es) to query. Either a
* Horde_Imap_Client_Mailbox object, a string
* (UTF-8), or an array of objects/strings (since
* 2.10.0).
* @param integer $flags A bitmask of information requested from the
* server. Allowed flags:
* <pre>
* - Horde_Imap_Client::STATUS_MESSAGES
* Return key: messages
* Return format: (integer) The number of messages in the mailbox.
*
* - Horde_Imap_Client::STATUS_RECENT
* Return key: recent
* Return format: (integer) The number of messages with the \Recent
* flag set as currently reported in the mailbox
*
* - Horde_Imap_Client::STATUS_RECENT_TOTAL
* Return key: recent_total
* Return format: (integer) The number of messages with the \Recent
* flag set. This returns the total number of messages
* that have been marked as recent in this mailbox
* since the PHP process began. (since 2.12.0)
*
* - Horde_Imap_Client::STATUS_UIDNEXT
* Return key: uidnext
* Return format: (integer) The next UID to be assigned in the
* mailbox. Only returned if the server automatically
* provides the data.
*
* - Horde_Imap_Client::STATUS_UIDNEXT_FORCE
* Return key: uidnext
* Return format: (integer) The next UID to be assigned in the
* mailbox. This option will always determine this
* value, even if the server does not automatically
* provide this data.
*
* - Horde_Imap_Client::STATUS_UIDVALIDITY
* Return key: uidvalidity
* Return format: (integer) The unique identifier validity of the
* mailbox.
*
* - Horde_Imap_Client::STATUS_UNSEEN
* Return key: unseen
* Return format: (integer) The number of messages which do not have
* the \Seen flag set.
*
* - Horde_Imap_Client::STATUS_FIRSTUNSEEN
* Return key: firstunseen
* Return format: (integer) The sequence number of the first unseen
* message in the mailbox.
*
* - Horde_Imap_Client::STATUS_FLAGS
* Return key: flags
* Return format: (array) The list of defined flags in the mailbox
* (all flags are in lowercase).
*
* - 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).
*
* - Horde_Imap_Client::STATUS_HIGHESTMODSEQ
* Return key: highestmodseq
* Return format: (integer) If the server supports the CONDSTORE
* IMAP extension, this will be the highest
* mod-sequence value of all messages in the mailbox.
* Else 0 if CONDSTORE not available or the mailbox
* does not support mod-sequences.
*
* - Horde_Imap_Client::STATUS_SYNCMODSEQ
* Return key: syncmodseq
* Return format: (integer) If caching, and the server supports the
* CONDSTORE IMAP extension, this is the cached
* mod-sequence value of the mailbox when it was opened
* for the first time in this access. Will be null if
* not caching, CONDSTORE not available, or the mailbox
* does not support mod-sequences.
*
* - Horde_Imap_Client::STATUS_SYNCFLAGUIDS
* Return key: syncflaguids
* Return format: (Horde_Imap_Client_Ids) If caching, the server
* supports the CONDSTORE IMAP extension, and the
* mailbox contained cached data when opened for the
* first time in this access, this is the list of UIDs
* in which flags have changed since STATUS_SYNCMODSEQ.
*
* - Horde_Imap_Client::STATUS_SYNCVANISHED
* Return key: syncvanished
* Return format: (Horde_Imap_Client_Ids) If caching, the server
* supports the CONDSTORE IMAP extension, and the
* mailbox contained cached data when opened for the
* first time in this access, this is the list of UIDs
* which have been deleted since STATUS_SYNCMODSEQ.
*
* - 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.
*
* - Horde_Imap_Client::STATUS_FORCE_REFRESH
* Normally, the status information will be cached for a given
* mailbox. Since most PHP requests are generally less than a second,
* this is fine. However, if your script is long running, the status
* information may not be up-to-date. Specifying this flag will ensure
* that the server is always polled for the current mailbox status
* before results are returned. (since 2.14.0)
*
* - Horde_Imap_Client::STATUS_ALL (DEFAULT)
* Shortcut to return 'messages', 'recent', 'uidnext', 'uidvalidity',
* and 'unseen' values.
* </ul>
* @param array $opts Additional options:
* <pre>
* - sort: (boolean) If true, sort the list of mailboxes? (since 2.10.0)
* DEFAULT: Do not sort the list.
* - sort_delimiter: (string) If 'sort' is true, this is the delimiter
* used to sort the mailboxes. (since 2.10.0)
* DEFAULT: '.'
* </pre>
*
* @return array If $mailbox contains multiple mailboxes, an array with
* keys being the UTF-8 mailbox name and values as arrays
* containing the requested keys (see above).
* Otherwise, an array with keys as the requested keys (see
* above) and values as the key data.
*
* @throws Horde_Imap_Client_Exception
*/
public function status($mailbox, $flags = Horde_Imap_Client::STATUS_ALL,
array $opts = array())
{
$opts = array_merge(array(
'sort' => false,
'sort_delimiter' => '.'
), $opts);
$this->login();
if (is_array($mailbox)) {
if (empty($mailbox)) {
return array();
}
$ret_array = true;
} else {
$mailbox = array($mailbox);
$ret_array = false;
}
$mlist = array_map(array('Horde_Imap_Client_Mailbox', 'get'), $mailbox);
$unselected_flags = array(
'messages' => Horde_Imap_Client::STATUS_MESSAGES,
'recent' => Horde_Imap_Client::STATUS_RECENT,
'uidnext' => Horde_Imap_Client::STATUS_UIDNEXT,
'uidvalidity' => Horde_Imap_Client::STATUS_UIDVALIDITY,
'unseen' => Horde_Imap_Client::STATUS_UNSEEN
);
if (!$this->statuscache) {
$flags |= Horde_Imap_Client::STATUS_FORCE_REFRESH;
}
if ($flags & Horde_Imap_Client::STATUS_ALL) {
foreach ($unselected_flags as $val) {
$flags |= $val;
}
}
$master = $ret = array();
/* Catch flags that are not supported. */
if (($flags & Horde_Imap_Client::STATUS_HIGHESTMODSEQ) &&
!$this->_capability()->isEnabled('CONDSTORE')) {
$master['highestmodseq'] = 0;
$flags &= ~Horde_Imap_Client::STATUS_HIGHESTMODSEQ;
}
if (($flags & Horde_Imap_Client::STATUS_UIDNOTSTICKY) &&
!$this->_capability('UIDPLUS')) {
$master['uidnotsticky'] = false;
$flags &= ~Horde_Imap_Client::STATUS_UIDNOTSTICKY;
}
/* UIDNEXT return options. */
if ($flags & Horde_Imap_Client::STATUS_UIDNEXT_FORCE) {
$flags |= Horde_Imap_Client::STATUS_UIDNEXT;
}
foreach ($mlist as $val) {
$name = strval($val);
$tmp_flags = $flags;
if ($val->equals($this->_selected)) {
/* Check if already in mailbox. */
$opened = true;
if ($flags & Horde_Imap_Client::STATUS_FORCE_REFRESH) {
$this->noop();
}
} else {
/* A list of STATUS options (other than those handled directly
* below) that require the mailbox to be explicitly opened. */
$opened = ($flags & Horde_Imap_Client::STATUS_FIRSTUNSEEN) ||
($flags & Horde_Imap_Client::STATUS_FLAGS) ||
($flags & Horde_Imap_Client::STATUS_PERMFLAGS) ||
($flags & Horde_Imap_Client::STATUS_UIDNOTSTICKY) ||
/* Force mailboxes containing wildcards to be accessed via
* STATUS so that wildcards do not return a bunch of
* mailboxes in the LIST-STATUS response. */
(strpbrk($name, '*%') !== false);
}
$ret[$name] = $master;
$ptr = &$ret[$name];
/* STATUS_PERMFLAGS requires a read/write mailbox. */
if ($flags & Horde_Imap_Client::STATUS_PERMFLAGS) {
$this->openMailbox($val, Horde_Imap_Client::OPEN_READWRITE);
$opened = true;
}
/* Handle SYNC related return options. These require the mailbox
* to be opened at least once. */
if ($flags & Horde_Imap_Client::STATUS_SYNCMODSEQ) {
$this->openMailbox($val);
$ptr['syncmodseq'] = $this->_mailboxOb($val)->getStatus(Horde_Imap_Client::STATUS_SYNCMODSEQ);
$tmp_flags &= ~Horde_Imap_Client::STATUS_SYNCMODSEQ;
$opened = true;
}
if ($flags & Horde_Imap_Client::STATUS_SYNCFLAGUIDS) {
$this->openMailbox($val);
$ptr['syncflaguids'] = $this->getIdsOb($this->_mailboxOb($val)->getStatus(Horde_Imap_Client::STATUS_SYNCFLAGUIDS));
$tmp_flags &= ~Horde_Imap_Client::STATUS_SYNCFLAGUIDS;
$opened = true;
}
if ($flags & Horde_Imap_Client::STATUS_SYNCVANISHED) {
$this->openMailbox($val);
$ptr['syncvanished'] = $this->getIdsOb($this->_mailboxOb($val)->getStatus(Horde_Imap_Client::STATUS_SYNCVANISHED));
$tmp_flags &= ~Horde_Imap_Client::STATUS_SYNCVANISHED;
$opened = true;
}
/* Handle RECENT_TOTAL option. */
if ($flags & Horde_Imap_Client::STATUS_RECENT_TOTAL) {
$this->openMailbox($val);
$ptr['recent_total'] = $this->_mailboxOb($val)->getStatus(Horde_Imap_Client::STATUS_RECENT_TOTAL);
$tmp_flags &= ~Horde_Imap_Client::STATUS_RECENT_TOTAL;
$opened = true;
}
if ($opened) {
if ($tmp_flags) {
$tmp = $this->_status(array($val), $tmp_flags);
$ptr += reset($tmp);
}
} else {
$to_process[] = $val;
}
}
if ($flags && !empty($to_process)) {
if ((count($to_process) > 1) &&
$this->_capability('LIST-STATUS')) {
foreach ($this->listMailboxes($to_process, Horde_Imap_Client::MBOX_ALL, array('status' => $flags)) as $key => $val) {
if (isset($val['status'])) {
$ret[$key] += $val['status'];
}
}
} else {
foreach ($this->_status($to_process, $flags) as $key => $val) {
$ret[$key] += $val;
}
}
}
if (!$opts['sort'] || (count($ret) === 1)) {
return $ret_array
? $ret
: reset($ret);
}
$list_ob = new Horde_Imap_Client_Mailbox_List(array_keys($ret));
$sorted = $list_ob->sort(array(
'delimiter' => $opts['sort_delimiter']
));
$out = array();
foreach ($sorted as $val) {
$out[$val] = $ret[$val];
}
return $out;
}
/**
* Obtain status information for mailboxes.
*
* @param array $mboxes The list of mailbox objects to query.
* @param integer $flags A bitmask of information requested from the
* server.
*
* @return array See array return for status().
*
* @throws Horde_Imap_Client_Exception
*/
abstract protected function _status($mboxes, $flags);
/**
* Perform a STATUS call on multiple mailboxes at the same time.
*
* This method leverages the LIST-EXTENDED and LIST-STATUS extensions on
* the IMAP server to improve the efficiency of this operation.
*
* @deprecated Use status() instead.
*
* @param array $mailboxes The mailboxes to query. Either
* Horde_Imap_Client_Mailbox objects, strings
* (UTF-8), or a combination of the two.
* @param integer $flags See status().
* @param array $opts Additional options:
* - sort: (boolean) If true, sort the list of mailboxes?
* DEFAULT: Do not sort the list.
* - sort_delimiter: (string) If 'sort' is true, this is the delimiter
* used to sort the mailboxes.
* DEFAULT: '.'
*
* @return array An array with the keys as the mailbox names (UTF-8) and
* the values as arrays with the requested keys (from the
* mask given in $flags).
*/
public function statusMultiple($mailboxes,
$flags = Horde_Imap_Client::STATUS_ALL,
array $opts = array())
{
return $this->status($mailboxes, $flags, $opts);
}
/**
* Append message(s) to a mailbox.
*
* @param mixed $mailbox The mailbox to append the message(s) to. Either
* a Horde_Imap_Client_Mailbox object or a string
* (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. If a string or a stream resource,
* this will be used as the entire contents of a single message.
* If an array, will catenate all given parts into a single
* message. This array contains one or more arrays with
* two keys:
* - t: (string) Either 'url' or 'text'.
* - v: (mixed) If 't' is 'url', this is the IMAP URL to the message
* part to append. If 't' is 'text', this is either a string or
* resource representation of the message part data.
* 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) The internaldate to set for the appended
* message.
* DEFAULT: internaldate will be the same date as when
* the message was appended.
* </pre>
* @param array $options Additonal options:
* <pre>
* - create: (boolean) Try to create $mailbox if it does not exist?
* DEFAULT: No.
* </pre>
*
* @return Horde_Imap_Client_Ids The UIDs of the appended messages.
*
* @throws Horde_Imap_Client_Exception
*/
public function append($mailbox, $data, array $options = array())
{
$this->login();
$mailbox = Horde_Imap_Client_Mailbox::get($mailbox);
$ret = $this->_append($mailbox, $data, $options);
if ($ret instanceof Horde_Imap_Client_Ids) {
return $ret;
}
$uids = $this->getIdsOb();
foreach ($data as $val) {
if (is_resource($val['data'])) {
rewind($val['data']);
}
$uids->add($this->_getUidByMessageId(
$mailbox,
Horde_Mime_Headers::parseHeaders($val['data'])->getHeader('Message-ID')
));
}
return $uids;
}
/**
* Append message(s) to a mailbox.
*
* @param Horde_Imap_Client_Mailbox $mailbox The mailbox to append the
* message(s) to.
* @param array $data The message data.
* @param array $options Additional options.
*
* @return mixed A Horde_Imap_Client_Ids object containing the UIDs of
* the appended messages (if server supports UIDPLUS
* extension) or true.
*
* @throws Horde_Imap_Client_Exception
*/
abstract protected function _append(Horde_Imap_Client_Mailbox $mailbox,
$data, $options);
/**
* Request a checkpoint of the currently selected mailbox (RFC 3501
* [6.4.1]).
*
* @throws Horde_Imap_Client_Exception
*/
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 Horde_Imap_Client_Exception
*/
abstract protected function _check();
/**
* Close the connection to the currently selected mailbox, optionally
* expunging all deleted messages (RFC 3501 [6.4.2]).
*
* @param array $options Additional options:
* - expunge: (boolean) Expunge all messages flagged as deleted?
* DEFAULT: No
*
* @throws Horde_Imap_Client_Exception
*/
public function close(array $options = array())
{
// This check catches the non-logged in case.
if (is_null($this->_selected)) {
return;
}
/* If we are caching, search for deleted messages. */
if (!empty($options['expunge']) && $this->_initCache(true)) {
/* Make sure mailbox is read-write to expunge. */
$this->openMailbox($this->_selected, Horde_Imap_Client::OPEN_READWRITE);
if ($this->_mode == Horde_Imap_Client::OPEN_READONLY) {
throw new Horde_Imap_Client_Exception(
Horde_Imap_Client_Translation::r("Cannot expunge read-only mailbox."),
Horde_Imap_Client_Exception::MAILBOX_READONLY
);
}
$search_query = new Horde_Imap_Client_Search_Query();
$search_query->flag(Horde_Imap_Client::FLAG_DELETED, true);
$search_res = $this->search($this->_selected, $search_query);
$mbox = $this->_selected;
} else {
$search_res = null;
}
$this->_close($options);
$this->_selected = null;
$this->_mode = 0;
if (!is_null($search_res)) {
$this->_deleteMsgs($mbox, $search_res['match']);
}
}
/**
* Close the connection to the currently selected mailbox, optionally
* expunging all deleted messages (RFC 3501 [6.4.2]).
*
* @param array $options Additional options.
*
* @throws Horde_Imap_Client_Exception
*/
abstract protected function _close($options);
/**
* Expunge deleted messages from the given mailbox.
*
* @param mixed $mailbox The mailbox to expunge. Either a
* Horde_Imap_Client_Mailbox object or a string
* (UTF-8).
* @param array $options Additional options:
* - delete: (boolean) If true, will flag all messages in 'ids' as
* deleted (since 2.10.0).
* DEFAULT: false
* - ids: (Horde_Imap_Client_Ids) A list of messages to expunge. These
* messages must already be flagged as deleted (unless 'delete'
* is true).
* DEFAULT: All messages marked as deleted will be expunged.
* - list: (boolean) If true, returns the list of expunged messages
* (UIDs only).
* DEFAULT: false
*
* @return Horde_Imap_Client_Ids If 'list' option is true, returns the
* UID list of expunged messages.
*
* @throws Horde_Imap_Client_Exception
*/
public function expunge($mailbox, array $options = array())
{
// Open mailbox call will handle the login.
$this->openMailbox($mailbox, Horde_Imap_Client::OPEN_READWRITE);
/* Don't expunge if the mailbox is readonly. */
if ($this->_mode == Horde_Imap_Client::OPEN_READONLY) {
throw new Horde_Imap_Client_Exception(
Horde_Imap_Client_Translation::r("Cannot expunge read-only mailbox."),
Horde_Imap_Client_Exception::MAILBOX_READONLY
);
}
if (empty($options['ids'])) {
$options['ids'] = $this->getIdsOb(Horde_Imap_Client_Ids::ALL);
} elseif ($options['ids']->isEmpty()) {
return $this->getIdsOb();
}
return $this->_expunge($options);
}
/**
* Expunge all deleted messages from the given mailbox.
*
* @param array $options Additional options.
*
* @return Horde_Imap_Client_Ids If 'list' option is true, returns the
* list of expunged messages.
*
* @throws Horde_Imap_Client_Exception
*/
abstract protected function _expunge($options);
/**
* Search a mailbox.
*
* @param mixed $mailbox The mailbox to search.
* Either a
* Horde_Imap_Client_Mailbox
* object or a string
* (UTF-8).
* @param Horde_Imap_Client_Search_Query $query The search query.
* Defaults to an ALL
* search.
* @param array $options Additional options:
* <pre>
* - nocache: (boolean) Don't cache the results.
* DEFAULT: false (results cached, if possible)
* - partial: (mixed) The range of results to return (message sequence
* numbers) Only a single range is supported (represented by
* the minimum and maximum values contained in the range
* given).
* DEFAULT: All messages are returned.
* - results: (array) The data to return. Consists of zero or more of
* the following flags:
* - Horde_Imap_Client::SEARCH_RESULTS_COUNT
* - Horde_Imap_Client::SEARCH_RESULTS_MATCH (DEFAULT)
* - Horde_Imap_Client::SEARCH_RESULTS_MAX
* - Horde_Imap_Client::SEARCH_RESULTS_MIN
* - Horde_Imap_Client::SEARCH_RESULTS_SAVE
* - Horde_Imap_Client::SEARCH_RESULTS_RELEVANCY
* - 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. 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. The following sort
* criteria are available:
* - Horde_Imap_Client::SORT_ARRIVAL
* - Horde_Imap_Client::SORT_CC
* - Horde_Imap_Client::SORT_DATE
* - Horde_Imap_Client::SORT_DISPLAYFROM
* On servers that don't support SORT=DISPLAY, this criteria will
* fallback to doing client-side sorting.
* - Horde_Imap_Client::SORT_DISPLAYFROM_FALLBACK
* On servers that don't support SORT=DISPLAY, this criteria will
* fallback to Horde_Imap_Client::SORT_FROM [since 2.4.0].
* - Horde_Imap_Client::SORT_DISPLAYTO
* On servers that don't support SORT=DISPLAY, this criteria will
* fallback to doing client-side sorting.
* - Horde_Imap_Client::SORT_DISPLAYTO_FALLBACK
* On servers that don't support SORT=DISPLAY, this criteria will
* fallback to Horde_Imap_Client::SORT_TO [since 2.4.0].
* - Horde_Imap_Client::SORT_FROM
* - Horde_Imap_Client::SORT_SEQUENCE
* - Horde_Imap_Client::SORT_SIZE
* - Horde_Imap_Client::SORT_SUBJECT
* - Horde_Imap_Client::SORT_TO
*
* [On servers that support SEARCH=FUZZY, this criteria is also
* available:]
* - Horde_Imap_Client::SORT_RELEVANCY
* </pre>
*
* @return array An array with the following keys:
* <pre>
* - count: (integer) The number of messages that match the search
* criteria. Always returned.
* - match: (Horde_Imap_Client_Ids) The IDs that match $criteria, sorted
* if the 'sort' modifier was set. Returned if
* Horde_Imap_Client::SEARCH_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::SEARCH_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::SEARCH_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.
* - relevancy: (array) The list of relevancy scores. Returned if
* Horde_Imap_Client::SEARCH_RESULTS_RELEVANCY is set and
* the server supports FUZZY search matching.
* - save: (boolean) Whether the search results were saved. Returned if
* Horde_Imap_Client::SEARCH_RESULTS_SAVE is set.
* </pre>
*
* @throws Horde_Imap_Client_Exception
*/
public function search($mailbox, $query = null, array $options = array())
{
$this->login();
if (empty($options['results'])) {
$options['results'] = array(
Horde_Imap_Client::SEARCH_RESULTS_MATCH,
Horde_Imap_Client::SEARCH_RESULTS_COUNT
);
} elseif (!in_array(Horde_Imap_Client::SEARCH_RESULTS_COUNT, $options['results'])) {
$options['results'][] = Horde_Imap_Client::SEARCH_RESULTS_COUNT;
}
// Default to an ALL search.
if (is_null($query)) {
$query = new Horde_Imap_Client_Search_Query();
}
// Check for SEARCHRES support.
if ((($pos = array_search(Horde_Imap_Client::SEARCH_RESULTS_SAVE, $options['results'])) !== false) &&
!$this->_capability('SEARCHRES')) {
unset($options['results'][$pos]);
}
// Check for SORT-related options.
if (!empty($options['sort'])) {
foreach ($options['sort'] as $key => $val) {
switch ($val) {
case Horde_Imap_Client::SORT_DISPLAYFROM_FALLBACK:
$options['sort'][$key] = $this->_capability('SORT', 'DISPLAY')
? Horde_Imap_Client::SORT_DISPLAYFROM
: Horde_Imap_Client::SORT_FROM;
break;
case Horde_Imap_Client::SORT_DISPLAYTO_FALLBACK:
$options['sort'][$key] = $this->_capability('SORT', 'DISPLAY')
? Horde_Imap_Client::SORT_DISPLAYTO
: Horde_Imap_Client::SORT_TO;
break;
}
}
}
/* Default search results. */
$default_ret = array(
'count' => 0,
'match' => $this->getIdsOb(),
'max' => null,
'min' => null,
'relevancy' => array()
);
/* Build search query. */
$squery = $query->build($this);
/* Check for query contents. If empty, this means that the query
* object has identified that this query can NEVER return any results.
* Immediately return now. */
if (!count($squery['query'])) {
return $default_ret;
}
// Check for supported charset.
if (!is_null($squery['charset']) &&
($this->search_charset->query($squery['charset'], true) === false)) {
foreach ($this->search_charset->charsets as $val) {
try {
$new_query = clone $query;
$new_query->charset($val);
break;
} catch (Horde_Imap_Client_Exception_SearchCharset $e) {
unset($new_query);
}
}
if (!isset($new_query)) {
throw $e;
}
$query = $new_query;
$squery = $query->build($this);
}
// Store query in $options array to pass to child method.
$options['_query'] = $squery;
/* RFC 6203: MUST NOT request relevancy results if we are not using
* FUZZY searching. */
if (in_array(Horde_Imap_Client::SEARCH_RESULTS_RELEVANCY, $options['results']) &&
!in_array('SEARCH=FUZZY', $squery['exts_used'])) {
throw new InvalidArgumentException('Cannot specify RELEVANCY results if not doing a FUZZY search.');
}
/* Check for partial matching. */
if (!empty($options['partial'])) {
$pids = $this->getIdsOb($options['partial'], true)->range_string;
if (!strlen($pids)) {
throw new InvalidArgumentException('Cannot specify empty sequence range for a PARTIAL search.');
}
if (strpos($pids, ':') === false) {
$pids .= ':' . $pids;
}
$options['partial'] = $pids;
}
/* 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']) == Horde_Imap_Client::SEARCH_RESULTS_COUNT)) {
switch ($squery['query']) {
case 'ALL':
$ret = $this->status($mailbox, Horde_Imap_Client::STATUS_MESSAGES);
return array('count' => $ret['messages']);
case 'RECENT':
$ret = $this->status($mailbox, Horde_Imap_Client::STATUS_RECENT);
return array('count' => $ret['recent']);
}
}
$this->openMailbox($mailbox, Horde_Imap_Client::OPEN_AUTO);
/* Take advantage of search result caching. If CONDSTORE available,
* we can cache all queries and invalidate the cache when the MODSEQ
* changes. If CONDSTORE not available, we can only store queries
* that don't involve flags. We store results by hashing the options
* array. */
$cache = null;
if (empty($options['nocache']) &&
$this->_initCache(true) &&
($this->_capability()->isEnabled('CONDSTORE') ||
!$query->flagSearch())) {
$cache = $this->_getSearchCache('search', $options);
if (isset($cache['data'])) {
if (isset($cache['data']['match'])) {
$cache['data']['match'] = $this->getIdsOb($cache['data']['match']);
}
return $cache['data'];
}
}
/* Optimization: Catch when there are no messages in a mailbox. */
$status_res = $this->status($this->_selected, Horde_Imap_Client::STATUS_MESSAGES | Horde_Imap_Client::STATUS_HIGHESTMODSEQ);
if ($status_res['messages'] ||
in_array(Horde_Imap_Client::SEARCH_RESULTS_SAVE, $options['results'])) {
/* RFC 7162 [3.1.2.2] - trying to do a MODSEQ SEARCH on a mailbox
* that doesn't support it will return BAD. */
if (in_array('CONDSTORE', $squery['exts']) &&
!$this->_mailboxOb()->getStatus(Horde_Imap_Client::STATUS_HIGHESTMODSEQ)) {
throw new Horde_Imap_Client_Exception(
Horde_Imap_Client_Translation::r("Mailbox does not support mod-sequences."),
Horde_Imap_Client_Exception::MBOXNOMODSEQ
);
}
$ret = $this->_search($query, $options);
} else {
$ret = $default_ret;
if (isset($status_res['highestmodseq'])) {
$ret['modseq'] = $status_res['highestmodseq'];
}
}
if ($cache) {
$save = $ret;
if (isset($save['match'])) {
$save['match'] = strval($ret['match']);
}
$this->_setSearchCache($save, $cache);
}
return $ret;
}
/**
* Search a mailbox.
*
* @param object $query The search query.
* @param array $options Additional options. The '_query' key contains
* the value of $query->build().
*
* @return Horde_Imap_Client_Ids An array of IDs.
*
* @throws Horde_Imap_Client_Exception
*/
abstract protected function _search($query, $options);
/**
* Set the comparator to use for searching/sorting (RFC 5255).
*
* @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.
*
* @throws Horde_Imap_Client_Exception
* @throws Horde_Imap_Client_Exception_NoSupportExtension
*/
public function setComparator($comparator = null)
{
$comp = is_null($comparator)
? $this->getParam('comparator')
: $comparator;
if (is_null($comp)) {
return;
}
$this->login();
if (!$this->_capability('I18NLEVEL', '2')) {
throw new Horde_Imap_Client_Exception_NoSupportExtension(
'I18NLEVEL',
'The IMAP server does not support changing SEARCH/SORT comparators.'
);
}
$this->_setComparator($comp);
}
/**
* Set the comparator to use for searching/sorting (RFC 5255).
*
* @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.
*
* @throws Horde_Imap_Client_Exception
*/
abstract protected function _setComparator($comparator);
/**
* Get the comparator used for searching/sorting (RFC 5255).
*
* @return mixed Null if the default comparator is being used, or an
* array of comparator information (see RFC 5255 [4.8]).
*
* @throws Horde_Imap_Client_Exception
*/
public function getComparator()
{
$this->login();
return $this->_capability('I18NLEVEL', '2')
? $this->_getComparator()
: null;
}
/**
* Get the comparator used for searching/sorting (RFC 5255).
*
* @return mixed Null if the default comparator is being used, or an
* array of comparator information (see RFC 5255 [4.8]).
*
* @throws Horde_Imap_Client_Exception
*/
abstract protected function _getComparator();
/**
* Thread sort a given list of messages (RFC 5256).
*
* @param mixed $mailbox The mailbox to query. Either a
* Horde_Imap_Client_Mailbox object or a string
* (UTF-8).
* @param array $options Additional options:
* <pre>
* - criteria: (mixed) The following thread criteria are available:
* - Horde_Imap_Client::THREAD_ORDEREDSUBJECT
* - Horde_Imap_Client::THREAD_REFERENCES
* - Horde_Imap_Client::THREAD_REFS
* Other algorithms can be explicitly specified by passing the IMAP
* thread algorithm in as a string value.
* DEFAULT: Horde_Imap_Client::THREAD_ORDEREDSUBJECT
* - search: (Horde_Imap_Client_Search_Query) The search query.
* 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_Data_Thread A thread data object.
*
* @throws Horde_Imap_Client_Exception
*/
public function thread($mailbox, array $options = array())
{
// Open mailbox call will handle the login.
$this->openMailbox($mailbox, Horde_Imap_Client::OPEN_AUTO);
/* Take advantage of search result caching. If CONDSTORE available,
* we can cache all queries and invalidate the cache when the MODSEQ
* changes. If CONDSTORE not available, we can only store queries
* that don't involve flags. See search() for similar caching. */
$cache = null;
if ($this->_initCache(true) &&
($this->_capability()->isEnabled('CONDSTORE') ||
empty($options['search']) ||
!$options['search']->flagSearch())) {
$cache = $this->_getSearchCache('thread', $options);
if (isset($cache['data']) &&
($cache['data'] instanceof Horde_Imap_Client_Data_Thread)) {
return $cache['data'];
}
}
$status_res = $this->status($this->_selected, Horde_Imap_Client::STATUS_MESSAGES);
$ob = $status_res['messages']
? $this->_thread($options)
: new Horde_Imap_Client_Data_Thread(array(), empty($options['sequence']) ? 'uid' : 'sequence');
if ($cache) {
$this->_setSearchCache($ob, $cache);
}
return $ob;
}
/**
* Thread sort a given list of messages (RFC 5256).
*
* @param array $options Additional options. See thread().
*
* @return Horde_Imap_Client_Data_Thread A thread data object.
*
* @throws Horde_Imap_Client_Exception
*/
abstract protected function _thread($options);
/**
* Fetch message data (see RFC 3501 [6.4.5]).
*
* @param mixed $mailbox The mailbox to search.
* Either a
* Horde_Imap_Client_Mailbox
* object or a string (UTF-8).
* @param Horde_Imap_Client_Fetch_Query $query Fetch query object.
* @param array $options Additional options:
* - changedsince: (integer) Only return messages that have a
* mod-sequence larger than this value. This option
* requires the CONDSTORE IMAP extension (if not present,
* this value is ignored). Additionally, the mailbox
* must support mod-sequences or an exception will be
* thrown. If valid, this option implicity adds the
* mod-sequence fetch criteria to the fetch command.
* DEFAULT: Mod-sequence values are ignored.
* - exists: (boolean) Ensure that all ids returned exist on the server.
* If false, the list of ids returned in the results object
* is not guaranteed to reflect the current state of the
* remote mailbox.
* DEFAULT: false
* - ids: (Horde_Imap_Client_Ids) A list of messages to fetch data from.
* DEFAULT: All messages in $mailbox will be fetched.
* - nocache: (boolean) If true, will not cache the results (previously
* cached data will still be used to generate results) [since
* 2.8.0].
* DEFAULT: false
*
* @return Horde_Imap_Client_Fetch_Results A results object.
*
* @throws Horde_Imap_Client_Exception
* @throws Horde_Imap_Client_Exception_NoSupportExtension
*/
public function fetch($mailbox, $query, array $options = array())
{
try {
$ret = $this->_fetchWrapper($mailbox, $query, $options);
unset($this->_temp['fetch_nocache']);
return $ret;
} catch (Exception $e) {
unset($this->_temp['fetch_nocache']);
throw $e;
}
}
/**
* Wrapper for fetch() to allow internal state to be reset on exception.
*
* @internal
* @see fetch()
*/
private function _fetchWrapper($mailbox, $query, $options)
{
$this->login();
$query = clone $query;
$cache_array = $header_cache = $new_query = array();
if (empty($options['ids'])) {
$options['ids'] = $this->getIdsOb(Horde_Imap_Client_Ids::ALL);
} elseif ($options['ids']->isEmpty()) {
return new Horde_Imap_Client_Fetch_Results($this->_fetchDataClass);
} elseif ($options['ids']->search_res &&
!$this->_capability('SEARCHRES')) {
/* SEARCHRES requires server support. */
throw new Horde_Imap_Client_Exception_NoSupportExtension('SEARCHRES');
}
$this->openMailbox($mailbox, Horde_Imap_Client::OPEN_AUTO);
$mbox_ob = $this->_mailboxOb();
if (!empty($options['nocache'])) {
$this->_temp['fetch_nocache'] = true;
}
$cf = $this->_initCache(true)
? $this->_cacheFields()
: array();
if (!empty($cf)) {
/* If using cache, we store by UID so we need to return UIDs. */
$query->uid();
}
$modseq_check = !empty($options['changedsince']);
if ($query->contains(Horde_Imap_Client::FETCH_MODSEQ)) {
if (!$this->_capability()->isEnabled('CONDSTORE')) {
unset($query[Horde_Imap_Client::FETCH_MODSEQ]);
} elseif (empty($options['changedsince'])) {
$modseq_check = true;
}
}
if ($modseq_check &&
!$mbox_ob->getStatus(Horde_Imap_Client::STATUS_HIGHESTMODSEQ)) {
/* RFC 7162 [3.1.2.2] - trying to do a MODSEQ FETCH on a mailbox
* that doesn't support it will return BAD. */
throw new Horde_Imap_Client_Exception(
Horde_Imap_Client_Translation::r("Mailbox does not support mod-sequences."),
Horde_Imap_Client_Exception::MBOXNOMODSEQ
);
}
/* Determine if caching is available and if anything in $query is
* cacheable. */
foreach ($cf as $k => $v) {
if (isset($query[$k])) {
switch ($k) {
case Horde_Imap_Client::FETCH_ENVELOPE:
case Horde_Imap_Client::FETCH_FLAGS:
case Horde_Imap_Client::FETCH_IMAPDATE:
case Horde_Imap_Client::FETCH_SIZE:
case Horde_Imap_Client::FETCH_STRUCTURE:
$cache_array[$k] = $v;
break;
case Horde_Imap_Client::FETCH_HEADERS:
$this->_temp['headers_caching'] = array();
foreach ($query[$k] as $key => $val) {
/* Only cache if directly requested. Iterate through
* requests to ensure at least one can be cached. */
if (!empty($val['cache']) && !empty($val['peek'])) {
$cache_array[$k] = $v;
ksort($val);
$header_cache[$key] = hash('md5', serialize($val));
}
}
break;
}
}
}
$ret = new Horde_Imap_Client_Fetch_Results(
$this->_fetchDataClass,
$options['ids']->sequence ? Horde_Imap_Client_Fetch_Results::SEQUENCE : Horde_Imap_Client_Fetch_Results::UID
);
/* If nothing is cacheable, we can do a straight search. */
if (empty($cache_array)) {
$options['_query'] = $query;
$this->_fetch($ret, array($options));
return $ret;
}
$cs_ret = empty($options['changedsince'])
? null
: clone $ret;
/* Convert special searches to UID lists and create mapping. */
$ids = $this->resolveIds(
$this->_selected,
$options['ids'],
empty($options['exists']) ? 1 : 2
);
/* Add non-user settable cache fields. */
$cache_array[Horde_Imap_Client::FETCH_DOWNGRADED] = self::CACHE_DOWNGRADED;
/* Get the cached values. */
$data = $this->_cache->get(
$this->_selected,
$ids->ids,
array_values($cache_array),
$mbox_ob->getStatus(Horde_Imap_Client::STATUS_UIDVALIDITY)
);
/* Build a list of what we still need. */
$map = array_flip($mbox_ob->map->map);
$sequence = $options['ids']->sequence;
foreach ($ids as $uid) {
$crit = clone $query;
if ($sequence) {
if (!isset($map[$uid])) {
continue;
}
$entry_idx = $map[$uid];
} else {
$entry_idx = $uid;
unset($crit[Horde_Imap_Client::FETCH_UID]);
}
$entry = $ret->get($entry_idx);
if (isset($map[$uid])) {
$entry->setSeq($map[$uid]);
unset($crit[Horde_Imap_Client::FETCH_SEQ]);
}
$entry->setUid($uid);
foreach ($cache_array as $key => $cid) {
switch ($key) {
case Horde_Imap_Client::FETCH_DOWNGRADED:
if (!empty($data[$uid][$cid])) {
$entry->setDowngraded(true);
}
break;
case Horde_Imap_Client::FETCH_ENVELOPE:
if (isset($data[$uid][$cid]) &&
($data[$uid][$cid] instanceof Horde_Imap_Client_Data_Envelope)) {
$entry->setEnvelope($data[$uid][$cid]);
unset($crit[$key]);
}
break;
case Horde_Imap_Client::FETCH_FLAGS:
if (isset($data[$uid][$cid]) &&
is_array($data[$uid][$cid])) {
$entry->setFlags($data[$uid][$cid]);
unset($crit[$key]);
}
break;
case Horde_Imap_Client::FETCH_HEADERS:
foreach ($header_cache as $hkey => $hval) {
if (isset($data[$uid][$cid][$hval])) {
/* We have found a cached entry with the same
* MD5 sum. */
$entry->setHeaders($hkey, $data[$uid][$cid][$hval]);
$crit->remove($key, $hkey);
} else {
$this->_temp['headers_caching'][$hkey] = $hval;
}
}
break;
case Horde_Imap_Client::FETCH_IMAPDATE:
if (isset($data[$uid][$cid]) &&
($data[$uid][$cid] instanceof Horde_Imap_Client_DateTime)) {
$entry->setImapDate($data[$uid][$cid]);
unset($crit[$key]);
}
break;
case Horde_Imap_Client::FETCH_SIZE:
if (isset($data[$uid][$cid])) {
$entry->setSize($data[$uid][$cid]);
unset($crit[$key]);
}
break;
case Horde_Imap_Client::FETCH_STRUCTURE:
if (isset($data[$uid][$cid]) &&
($data[$uid][$cid] instanceof Horde_Mime_Part)) {
$entry->setStructure($data[$uid][$cid]);
unset($crit[$key]);
}
break;
}
}
if (count($crit)) {
$sig = $crit->hash();
if (isset($new_query[$sig])) {
$new_query[$sig]['i'][] = $entry_idx;
} else {
$new_query[$sig] = array(
'c' => $crit,
'i' => array($entry_idx)
);
}
}
}
$to_fetch = array();
foreach ($new_query as $val) {
$ids_ob = $this->getIdsOb(null, $sequence);
$ids_ob->duplicates = true;
$ids_ob->add($val['i']);
$to_fetch[] = array_merge($options, array(
'_query' => $val['c'],
'ids' => $ids_ob
));
}
if (!empty($to_fetch)) {
$this->_fetch(is_null($cs_ret) ? $ret : $cs_ret, $to_fetch);
}
if (is_null($cs_ret)) {
return $ret;
}
/* If doing changedsince query, and all other data is cached, we still
* need to hit IMAP server to determine proper results set. */
if (empty($new_query)) {
$squery = new Horde_Imap_Client_Search_Query();
$squery->modseq($options['changedsince'] + 1);
$squery->ids($options['ids']);
$cs = $this->search($this->_selected, $squery, array(
'sequence' => $sequence
));
foreach ($cs['match'] as $val) {
$entry = $ret->get($val);
if ($sequence) {
$entry->setSeq($val);
} else {
$entry->setUid($val);
}
$cs_ret[$val] = $entry;
}
} else {
foreach ($cs_ret as $key => $val) {
$val->merge($ret->get($key));
}
}
return $cs_ret;
}
/**
* Fetch message data.
*
* Fetch queries should be grouped in the $queries argument. Each value
* is an array of fetch options, with the fetch query stored in the
* '_query' parameter. IMPORTANT: All queries must have the same ID
* type (either sequence or UID).
*
* @param Horde_Imap_Client_Fetch_Results $results Fetch results.
* @param array $queries The list of queries.
*
* @throws Horde_Imap_Client_Exception
*/
abstract protected function _fetch(Horde_Imap_Client_Fetch_Results $results,
$queries);
/**
* Get the list of vanished messages (UIDs that have been expunged since a
* given mod-sequence value).
*
* @param mixed $mailbox The mailbox to query. Either a
* Horde_Imap_Client_Mailbox object or a string
* (UTF-8).
* @param integer $modseq Search for expunged messages after this
* mod-sequence value.
* @param array $opts Additional options:
* - ids: (Horde_Imap_Client_Ids) Restrict to these UIDs.
* DEFAULT: Returns full list of UIDs vanished (QRESYNC only).
* This option is REQUIRED for non-QRESYNC servers or
* else an empty list will be returned.
*
* @return Horde_Imap_Client_Ids List of UIDs that have vanished.
*
* @throws Horde_Imap_Client_NoSupportExtension
*/
public function vanished($mailbox, $modseq, array $opts = array())
{
$this->login();
if (empty($opts['ids'])) {
if (!$this->_capability()->isEnabled('QRESYNC')) {
return $this->getIdsOb();
}
$opts['ids'] = $this->getIdsOb(Horde_Imap_Client_Ids::ALL);
} elseif ($opts['ids']->isEmpty()) {
return $this->getIdsOb();
} elseif ($opts['ids']->sequence) {
throw new InvalidArgumentException('Vanished requires UIDs.');
}
$this->openMailbox($mailbox, Horde_Imap_Client::OPEN_AUTO);
if ($this->_capability()->isEnabled('QRESYNC')) {
if (!$this->_mailboxOb()->getStatus(Horde_Imap_Client::STATUS_HIGHESTMODSEQ)) {
throw new Horde_Imap_Client_Exception(
Horde_Imap_Client_Translation::r("Mailbox does not support mod-sequences."),
Horde_Imap_Client_Exception::MBOXNOMODSEQ
);
}
return $this->_vanished(max(1, $modseq), $opts['ids']);
}
$ids = $this->resolveIds($mailbox, $opts['ids']);
$squery = new Horde_Imap_Client_Search_Query();
$squery->ids($ids);
$search = $this->search($mailbox, $squery, array(
'nocache' => true
));
return $this->getIdsOb(array_diff($ids->ids, $search['match']->ids));
}
/**
* Get the list of vanished messages.
*
* @param integer $modseq Mod-sequence value.
* @param Horde_Imap_Client_Ids $ids UIDs.
*
* @return Horde_Imap_Client_Ids List of UIDs that have vanished.
*/
abstract protected function _vanished($modseq, Horde_Imap_Client_Ids $ids);
/**
* Store message flag data (see RFC 3501 [6.4.6]).
*
* @param mixed $mailbox The mailbox containing the messages to modify.
* Either a Horde_Imap_Client_Mailbox object or a
* string (UTF-8).
* @param array $options Additional options:
* - add: (array) An array of flags to add.
* DEFAULT: No flags added.
* - ids: (Horde_Imap_Client_Ids) 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.
* - unchangedsince: (integer) Only changes flags if the mod-sequence ID
* of the message is equal or less than this value.
* Requires the CONDSTORE IMAP extension on the server.
* Also requires the mailbox to support mod-sequences.
* Will throw an exception if either condition is not
* met.
* DEFAULT: mod-sequence is ignored when applying
* changes
*
* @return Horde_Imap_Client_Ids A Horde_Imap_Client_Ids object
* containing the list of IDs that failed
* the 'unchangedsince' test.
*
* @throws Horde_Imap_Client_Exception
* @throws Horde_Imap_Client_Exception_NoSupportExtension
*/
public function store($mailbox, array $options = array())
{
// Open mailbox call will handle the login.
$this->openMailbox($mailbox, Horde_Imap_Client::OPEN_READWRITE);
/* SEARCHRES requires server support. */
if (empty($options['ids'])) {
$options['ids'] = $this->getIdsOb(Horde_Imap_Client_Ids::ALL);
} elseif ($options['ids']->isEmpty()) {
return $this->getIdsOb();
} elseif ($options['ids']->search_res &&
!$this->_capability('SEARCHRES')) {
throw new Horde_Imap_Client_Exception_NoSupportExtension('SEARCHRES');
}
if (!empty($options['unchangedsince'])) {
if (!$this->_capability()->isEnabled('CONDSTORE')) {
throw new Horde_Imap_Client_Exception_NoSupportExtension('CONDSTORE');
}
/* RFC 7162 [3.1.2.2] - trying to do a UNCHANGEDSINCE STORE on a
* mailbox that doesn't support it will return BAD. */
if (!$this->_mailboxOb()->getStatus(Horde_Imap_Client::STATUS_HIGHESTMODSEQ)) {
throw new Horde_Imap_Client_Exception(
Horde_Imap_Client_Translation::r("Mailbox does not support mod-sequences."),
Horde_Imap_Client_Exception::MBOXNOMODSEQ
);
}
}
return $this->_store($options);
}
/**
* Store message flag data.
*
* @param array $options Additional options.
*
* @return Horde_Imap_Client_Ids A Horde_Imap_Client_Ids object
* containing the list of IDs that failed
* the 'unchangedsince' test.
*
* @throws Horde_Imap_Client_Exception
*/
abstract protected function _store($options);
/**
* Copy messages to another mailbox.
*
* @param mixed $source The source mailbox. Either a
* Horde_Imap_Client_Mailbox object or a string
* (UTF-8).
* @param mixed $dest The destination mailbox. Either a
* Horde_Imap_Client_Mailbox object or a string
* (UTF-8).
* @param array $options Additional options:
* - create: (boolean) Try to create $dest if it does not exist?
* DEFAULT: No.
* - force_map: (boolean) Forces the array mapping to always be
* returned. [@since 2.19.0]
* - ids: (Horde_Imap_Client_Ids) 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.
*
* @return mixed An array mapping old UIDs (keys) to new UIDs (values) on
* success (only guaranteed if 'force_map' is true) or
* true.
*
* @throws Horde_Imap_Client_Exception
* @throws Horde_Imap_Client_Exception_NoSupportExtension
*/
public function copy($source, $dest, array $options = array())
{
// Open mailbox call will handle the login.
$this->openMailbox($source, empty($options['move']) ? Horde_Imap_Client::OPEN_AUTO : Horde_Imap_Client::OPEN_READWRITE);
/* SEARCHRES requires server support. */
if (empty($options['ids'])) {
$options['ids'] = $this->getIdsOb(Horde_Imap_Client_Ids::ALL);
} elseif ($options['ids']->isEmpty()) {
return array();
} elseif ($options['ids']->search_res &&
!$this->_capability('SEARCHRES')) {
throw new Horde_Imap_Client_Exception_NoSupportExtension('SEARCHRES');
}
$dest = Horde_Imap_Client_Mailbox::get($dest);
$res = $this->_copy($dest, $options);
if (($res === true) && !empty($options['force_map'])) {
/* Need to manually create mapping from Message-ID data. */
$query = new Horde_Imap_Client_Fetch_Query();
$query->envelope();
$fetch = $this->fetch($source, $query, array(
'ids' => $options['ids']
));
$res = array();
foreach ($fetch as $val) {
if ($uid = $this->_getUidByMessageId($dest, $val->getEnvelope()->message_id)) {
$res[$val->getUid()] = $uid;
}
}
}
return $res;
}
/**
* Copy messages to another mailbox.
*
* @param Horde_Imap_Client_Mailbox $dest The destination mailbox.
* @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.
*
* @throws Horde_Imap_Client_Exception
*/
abstract protected function _copy(Horde_Imap_Client_Mailbox $dest,
$options);
/**
* Set quota limits. The server must support the IMAP QUOTA extension
* (RFC 2087).
*
* @param mixed $root The quota root. Either a
* Horde_Imap_Client_Mailbox object or a string
* (UTF-8).
* @param array $resources The resource values to set. Keys are the
* resource atom name; value is the resource
* value.
*
* @throws Horde_Imap_Client_Exception
* @throws Horde_Imap_Client_Exception_NoSupportExtension
*/
public function setQuota($root, array $resources = array())
{
$this->login();
if (!$this->_capability('QUOTA')) {
throw new Horde_Imap_Client_Exception_NoSupportExtension('QUOTA');
}
if (!empty($resources)) {
$this->_setQuota(Horde_Imap_Client_Mailbox::get($root), $resources);
}
}
/**
* Set quota limits.
*
* @param Horde_Imap_Client_Mailbox $root The quota root.
* @param array $resources The resource values to set.
*
* @return boolean True on success.
*
* @throws Horde_Imap_Client_Exception
*/
abstract protected function _setQuota(Horde_Imap_Client_Mailbox $root,
$resources);
/**
* Get quota limits. The server must support the IMAP QUOTA extension
* (RFC 2087).
*
* @param mixed $root The quota root. Either a Horde_Imap_Client_Mailbox
* object or a string (UTF-8).
*
* @return mixed An array with resource keys. Each key holds an array
* with 2 values: 'limit' and 'usage'.
*
* @throws Horde_Imap_Client_Exception
* @throws Horde_Imap_Client_Exception_NoSupportExtension
*/
public function getQuota($root)
{
$this->login();
if (!$this->_capability('QUOTA')) {
throw new Horde_Imap_Client_Exception_NoSupportExtension('QUOTA');
}
return $this->_getQuota(Horde_Imap_Client_Mailbox::get($root));
}
/**
* Get quota limits.
*
* @param Horde_Imap_Client_Mailbox $root The quota root.
*
* @return mixed An array with resource keys. Each key holds an array
* with 2 values: 'limit' and 'usage'.
*
* @throws Horde_Imap_Client_Exception
*/
abstract protected function _getQuota(Horde_Imap_Client_Mailbox $root);
/**
* Get quota limits for a mailbox. The server must support the IMAP QUOTA
* extension (RFC 2087).
*
* @param mixed $mailbox A mailbox. Either a Horde_Imap_Client_Mailbox
* object or a string (UTF-8).
*
* @return mixed An array with the keys being the quota roots. Each key
* holds an array with resource keys: each of these keys
* holds an array with 2 values: 'limit' and 'usage'.
*
* @throws Horde_Imap_Client_Exception
* @throws Horde_Imap_Client_Exception_NoSupportExtension
*/
public function getQuotaRoot($mailbox)
{
$this->login();
if (!$this->_capability('QUOTA')) {
throw new Horde_Imap_Client_Exception_NoSupportExtension('QUOTA');
}
return $this->_getQuotaRoot(Horde_Imap_Client_Mailbox::get($mailbox));
}
/**
* Get quota limits for a mailbox.
*
* @param Horde_Imap_Client_Mailbox $mailbox A mailbox.
*
* @return mixed An array with the keys being the quota roots. Each key
* holds an array with resource keys: each of these keys
* holds an array with 2 values: 'limit' and 'usage'.
*
* @throws Horde_Imap_Client_Exception
*/
abstract protected function _getQuotaRoot(Horde_Imap_Client_Mailbox $mailbox);
/**
* Get the ACL rights for a given mailbox. The server must support the
* IMAP ACL extension (RFC 2086/4314).
*
* @param mixed $mailbox A mailbox. Either a Horde_Imap_Client_Mailbox
* object or a string (UTF-8).
*
* @return array An array with identifiers as the keys and
* Horde_Imap_Client_Data_Acl objects as the values.
*
* @throws Horde_Imap_Client_Exception
*/
public function getACL($mailbox)
{
$this->login();
return $this->_getACL(Horde_Imap_Client_Mailbox::get($mailbox));
}
/**
* Get ACL rights for a given mailbox.
*
* @param Horde_Imap_Client_Mailbox $mailbox A mailbox.
*
* @return array An array with identifiers as the keys and
* Horde_Imap_Client_Data_Acl objects as the values.
*
* @throws Horde_Imap_Client_Exception
*/
abstract protected function _getACL(Horde_Imap_Client_Mailbox $mailbox);
/**
* Set ACL rights for a given mailbox/identifier.
*
* @param mixed $mailbox A mailbox. Either a Horde_Imap_Client_Mailbox
* object or a string (UTF-8).
* @param string $identifier The identifier to alter (UTF-8).
* @param array $options Additional options:
* - rights: (string) The rights to alter or set.
* - action: (string, optional) If 'add' or 'remove', adds or removes the
* specified rights. Sets the rights otherwise.
*
* @throws Horde_Imap_Client_Exception
* @throws Horde_Imap_Client_Exception_NoSupportExtension
*/
public function setACL($mailbox, $identifier, $options)
{
$this->login();
if (!$this->_capability('ACL')) {
throw new Horde_Imap_Client_Exception_NoSupportExtension('ACL');
}
if (empty($options['rights'])) {
if (!isset($options['action']) ||
(($options['action'] != 'add') &&
$options['action'] != 'remove')) {
$this->_deleteACL(
Horde_Imap_Client_Mailbox::get($mailbox),
Horde_Imap_Client_Utf7imap::Utf8ToUtf7Imap($identifier)
);
}
return;
}
$acl = ($options['rights'] instanceof Horde_Imap_Client_Data_Acl)
? $options['rights']
: new Horde_Imap_Client_Data_Acl(strval($options['rights']));
$options['rights'] = $acl->getString(
$this->_capability('RIGHTS')
? Horde_Imap_Client_Data_AclCommon::RFC_4314
: Horde_Imap_Client_Data_AclCommon::RFC_2086
);
if (isset($options['action'])) {
switch ($options['action']) {
case 'add':
$options['rights'] = '+' . $options['rights'];
break;
case 'remove':
$options['rights'] = '-' . $options['rights'];
break;
}
}
$this->_setACL(
Horde_Imap_Client_Mailbox::get($mailbox),
Horde_Imap_Client_Utf7imap::Utf8ToUtf7Imap($identifier),
$options
);
}
/**
* Set ACL rights for a given mailbox/identifier.
*
* @param Horde_Imap_Client_Mailbox $mailbox A mailbox.
* @param string $identifier The identifier to alter
* (UTF7-IMAP).
* @param array $options Additional options. 'rights'
* contains the string of
* rights to set on the server.
*
* @throws Horde_Imap_Client_Exception
*/
abstract protected function _setACL(Horde_Imap_Client_Mailbox $mailbox,
$identifier, $options);
/**
* Deletes ACL rights for a given mailbox/identifier.
*
* @param mixed $mailbox A mailbox. Either a Horde_Imap_Client_Mailbox
* object or a string (UTF-8).
* @param string $identifier The identifier to delete (UTF-8).
*
* @throws Horde_Imap_Client_Exception
* @throws Horde_Imap_Client_Exception_NoSupportExtension
*/
public function deleteACL($mailbox, $identifier)
{
$this->login();
if (!$this->_capability('ACL')) {
throw new Horde_Imap_Client_Exception_NoSupportExtension('ACL');
}
$this->_deleteACL(
Horde_Imap_Client_Mailbox::get($mailbox),
Horde_Imap_Client_Utf7imap::Utf8ToUtf7Imap($identifier)
);
}
/**
* Deletes ACL rights for a given mailbox/identifier.
*
* @param Horde_Imap_Client_Mailbox $mailbox A mailbox.
* @param string $identifier The identifier to delete
* (UTF7-IMAP).
*
* @throws Horde_Imap_Client_Exception
*/
abstract protected function _deleteACL(Horde_Imap_Client_Mailbox $mailbox,
$identifier);
/**
* List the ACL rights for a given mailbox/identifier. The server must
* support the IMAP ACL extension (RFC 2086/4314).
*
* @param mixed $mailbox A mailbox. Either a Horde_Imap_Client_Mailbox
* object or a string (UTF-8).
* @param string $identifier The identifier to query (UTF-8).
*
* @return Horde_Imap_Client_Data_AclRights An ACL data rights object.
*
* @throws Horde_Imap_Client_Exception
* @throws Horde_Imap_Client_Exception_NoSupportExtension
*/
public function listACLRights($mailbox, $identifier)
{
$this->login();
if (!$this->_capability('ACL')) {
throw new Horde_Imap_Client_Exception_NoSupportExtension('ACL');
}
return $this->_listACLRights(
Horde_Imap_Client_Mailbox::get($mailbox),
Horde_Imap_Client_Utf7imap::Utf8ToUtf7Imap($identifier)
);
}
/**
* Get ACL rights for a given mailbox/identifier.
*
* @param Horde_Imap_Client_Mailbox $mailbox A mailbox.
* @param string $identifier The identifier to query
* (UTF7-IMAP).
*
* @return Horde_Imap_Client_Data_AclRights An ACL data rights object.
*
* @throws Horde_Imap_Client_Exception
*/
abstract protected function _listACLRights(Horde_Imap_Client_Mailbox $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).
*
* @param mixed $mailbox A mailbox. Either a Horde_Imap_Client_Mailbox
* object or a string (UTF-8).
*
* @return Horde_Imap_Client_Data_Acl An ACL data object.
*
* @throws Horde_Imap_Client_Exception
* @throws Horde_Imap_Client_Exception_NoSupportExtension
*/
public function getMyACLRights($mailbox)
{
$this->login();
if (!$this->_capability('ACL')) {
throw new Horde_Imap_Client_Exception_NoSupportExtension('ACL');
}
return $this->_getMyACLRights(Horde_Imap_Client_Mailbox::get($mailbox));
}
/**
* Get the ACL rights for the current user for a given mailbox.
*
* @param Horde_Imap_Client_Mailbox $mailbox A mailbox.
*
* @return Horde_Imap_Client_Data_Acl An ACL data object.
*
* @throws Horde_Imap_Client_Exception
*/
abstract protected function _getMyACLRights(Horde_Imap_Client_Mailbox $mailbox);
/**
* Return master list of ACL rights available on the server.
*
* @return array A list of ACL rights.
*/
public function allAclRights()
{
$this->login();
$rights = array(
Horde_Imap_Client::ACL_LOOKUP,
Horde_Imap_Client::ACL_READ,
Horde_Imap_Client::ACL_SEEN,
Horde_Imap_Client::ACL_WRITE,
Horde_Imap_Client::ACL_INSERT,
Horde_Imap_Client::ACL_POST,
Horde_Imap_Client::ACL_ADMINISTER
);
if ($capability = $this->_capability()->getParams('RIGHTS')) {
// Add rights defined in CAPABILITY string (RFC 4314).
return array_merge($rights, str_split(reset($capability)));
}
// Add RFC 2086 rights (deprecated by RFC 4314, but need to keep for
// compatibility with old servers).
return array_merge($rights, array(
Horde_Imap_Client::ACL_CREATE,
Horde_Imap_Client::ACL_DELETE
));
}
/**
* Get metadata for a given mailbox. The server must support either the
* IMAP METADATA extension (RFC 5464) or the ANNOTATEMORE extension
* (http://ietfreport.isoc.org/idref/draft-daboo-imap-annotatemore/).
*
* @param mixed $mailbox A mailbox. Either a Horde_Imap_Client_Mailbox
* object or a string (UTF-8).
* @param array $entries The entries to fetch (UTF-8 strings).
* @param array $options Additional options:
* - depth: (string) Either "0", "1" or "infinity". Returns only the
* given value (0), only values one level below the specified
* value (1) or all entries below the specified value
* (infinity).
* - maxsize: (integer) The maximal size the returned values may have.
* DEFAULT: No maximal size.
*
* @return array An array with metadata names as the keys and metadata
* values as the values. If 'maxsize' is set, and entries
* exist on the server larger than this size, the size will
* be returned in the key '*longentries'.
*
* @throws Horde_Imap_Client_Exception
*/
public function getMetadata($mailbox, $entries, array $options = array())
{
$this->login();
if (!is_array($entries)) {
$entries = array($entries);
}
return $this->_getMetadata(Horde_Imap_Client_Mailbox::get($mailbox), array_map(array('Horde_Imap_Client_Utf7imap', 'Utf8ToUtf7Imap'), $entries), $options);
}
/**
* Get metadata for a given mailbox.
*
* @param Horde_Imap_Client_Mailbox $mailbox A mailbox.
* @param array $entries The entries to fetch
* (UTF7-IMAP strings).
* @param array $options Additional options.
*
* @return array An array with metadata names as the keys and metadata
* values as the values.
*
* @throws Horde_Imap_Client_Exception
*/
abstract protected function _getMetadata(Horde_Imap_Client_Mailbox $mailbox,
$entries, $options);
/**
* Set metadata for a given mailbox/identifier.
*
* @param mixed $mailbox A mailbox. Either a Horde_Imap_Client_Mailbox
* object or a string (UTF-8). If empty, sets a
* server annotation.
* @param array $data A set of data values. The metadata values
* corresponding to the keys of the array will
* be set to the values in the array.
*
* @throws Horde_Imap_Client_Exception
*/
public function setMetadata($mailbox, $data)
{
$this->login();
$this->_setMetadata(Horde_Imap_Client_Mailbox::get($mailbox), $data);
}
/**
* Set metadata for a given mailbox/identifier.
*
* @param Horde_Imap_Client_Mailbox $mailbox A mailbox.
* @param array $data A set of data values. See
* setMetadata() for format.
*
* @throws Horde_Imap_Client_Exception
*/
abstract protected function _setMetadata(Horde_Imap_Client_Mailbox $mailbox,
$data);
/* Public utility functions. */
/**
* Returns a unique identifier for the current mailbox status.
*
* @deprecated
*
* @param mixed $mailbox A mailbox. Either a Horde_Imap_Client_Mailbox
* object or a string (UTF-8).
* @param array $addl Additional cache info to add to the cache ID
* string.
*
* @return string The cache ID string, which will change when the
* composition of the mailbox changes. The uidvalidity
* will always be the first element, and will be delimited
* by the '|' character.
*
* @throws Horde_Imap_Client_Exception
*/
public function getCacheId($mailbox, array $addl = array())
{
return Horde_Imap_Client_Base_Deprecated::getCacheId($this, $mailbox, $this->_capability()->isEnabled('CONDSTORE'), $addl);
}
/**
* Parses a cacheID created by getCacheId().
*
* @deprecated
*
* @param string $id The cache ID.
*
* @return array An array with the following information:
* - highestmodseq: (integer)
* - messages: (integer)
* - uidnext: (integer)
* - uidvalidity: (integer) Always present
*/
public function parseCacheId($id)
{
return Horde_Imap_Client_Base_Deprecated::parseCacheId($id);
}
/**
* Resolves an IDs object into a list of IDs.
*
* @param Horde_Imap_Client_Mailbox $mailbox The mailbox.
* @param Horde_Imap_Client_Ids $ids The Ids object.
* @param integer $convert Convert to UIDs?
* - 0: No
* - 1: Only if $ids is not already a UIDs object
* - 2: Always
*
* @return Horde_Imap_Client_Ids The list of IDs.
*/
public function resolveIds(Horde_Imap_Client_Mailbox $mailbox,
Horde_Imap_Client_Ids $ids, $convert = 0)
{
$map = $this->_mailboxOb($mailbox)->map;
if ($ids->special) {
/* Optimization for ALL sequence searches. */
if (!$convert && $ids->all && $ids->sequence) {
$res = $this->status($mailbox, Horde_Imap_Client::STATUS_MESSAGES);
return $this->getIdsOb($res['messages'] ? ('1:' . $res['messages']) : array(), true);
}
$convert = 2;
} elseif (!$convert ||
(!$ids->sequence && ($convert == 1)) ||
$ids->isEmpty()) {
return clone $ids;
} else {
/* Do an all or nothing: either we have all the numbers/UIDs in
* memory and can return, or just send the whole ID query to the
* server. Any advantage we would get by a partial search are
* outweighed by the complexities needed to make the search and
* then merge back into the original results. */
$lookup = $map->lookup($ids);
if (count($lookup) === count($ids)) {
return $this->getIdsOb(array_values($lookup));
}
}
$query = new Horde_Imap_Client_Search_Query();
$query->ids($ids);
$res = $this->search($mailbox, $query, array(
'results' => array(
Horde_Imap_Client::SEARCH_RESULTS_MATCH,
Horde_Imap_Client::SEARCH_RESULTS_SAVE
),
'sequence' => (!$convert && $ids->sequence),
'sort' => array(Horde_Imap_Client::SORT_SEQUENCE)
));
/* Update mapping. */
if ($convert) {
if ($ids->all) {
$ids = $this->getIdsOb('1:' . count($res['match']));
} elseif ($ids->special) {
return $res['match'];
}
/* Sanity checking (Bug #12911). */
$list1 = array_slice($ids->ids, 0, count($res['match']));
$list2 = $res['match']->ids;
if (!empty($list1) &&
!empty($list2) &&
(count($list1) === count($list2))) {
$map->update(array_combine($list1, $list2));
}
}
return $res['match'];
}
/**
* Determines if the given charset is valid for search-related queries.
* This check pertains just to the basic IMAP SEARCH command.
*
* @deprecated Use $search_charset property instead.
*
* @param string $charset The query charset.
*
* @return boolean True if server supports this charset.
*/
public function validSearchCharset($charset)
{
return $this->search_charset->query($charset);
}
/* Mailbox syncing functions. */
/**
* Returns a unique token for the current mailbox synchronization status.
*
* @since 2.2.0
*
* @param mixed $mailbox A mailbox. Either a Horde_Imap_Client_Mailbox
* object or a string (UTF-8).
*
* @return string The sync token.
*
* @throws Horde_Imap_Client_Exception
*/
public function getSyncToken($mailbox)
{
$out = array();
foreach ($this->_syncStatus($mailbox) as $key => $val) {
$out[] = $key . $val;
}
return base64_encode(implode(',', $out));
}
/**
* Synchronize a mailbox from a sync token.
*
* @since 2.2.0
*
* @param mixed $mailbox A mailbox. Either a Horde_Imap_Client_Mailbox
* object or a string (UTF-8).
* @param string $token A sync token generated by getSyncToken().
* @param array $opts Additional options:
* - criteria: (integer) Mask of Horde_Imap_Client::SYNC_* criteria to
* return. Defaults to SYNC_ALL.
* - ids: (Horde_Imap_Client_Ids) A cached list of UIDs. Unless QRESYNC
* is available on the server, failure to specify this option
* means SYNC_VANISHEDUIDS information cannot be returned.
*
* @return Horde_Imap_Client_Data_Sync A sync object.
*
* @throws Horde_Imap_Client_Exception
* @throws Horde_Imap_Client_Exception_Sync
*/
public function sync($mailbox, $token, array $opts = array())
{
if (($token = base64_decode($token, true)) === false) {
throw new Horde_Imap_Client_Exception_Sync('Bad token.', Horde_Imap_Client_Exception_Sync::BAD_TOKEN);
}
$sync = array();
foreach (explode(',', $token) as $val) {
$sync[substr($val, 0, 1)] = substr($val, 1);
}
return new Horde_Imap_Client_Data_Sync(
$this,
$mailbox,
$sync,
$this->_syncStatus($mailbox),
(isset($opts['criteria']) ? $opts['criteria'] : Horde_Imap_Client::SYNC_ALL),
(isset($opts['ids']) ? $opts['ids'] : null)
);
}
/* Private utility functions. */
/**
* Store FETCH data in cache.
*
* @param Horde_Imap_Client_Fetch_Results $data The fetch results.
*
* @throws Horde_Imap_Client_Exception
*/
protected function _updateCache(Horde_Imap_Client_Fetch_Results $data)
{
if (!empty($this->_temp['fetch_nocache']) ||
empty($this->_selected) ||
!count($data) ||
!$this->_initCache(true)) {
return;
}
$c = $this->getParam('cache');
if (in_array(strval($this->_selected), $c['fetch_ignore'])) {
$this->_debug->info(sprintf(
'CACHE: Ignoring FETCH data [%s]',
$this->_selected
));
return;
}
/* Optimization: we can directly use getStatus() here since we know
* these values are initialized. */
$mbox_ob = $this->_mailboxOb();
$highestmodseq = $mbox_ob->getStatus(Horde_Imap_Client::STATUS_HIGHESTMODSEQ);
$uidvalidity = $mbox_ob->getStatus(Horde_Imap_Client::STATUS_UIDVALIDITY);
$mapping = $modseq = $tocache = array();
if (count($data)) {
$cf = $this->_cacheFields();
}
foreach ($data as $v) {
/* It is possible that we received FETCH information that doesn't
* contain UID data. This is uncacheable so don't process. */
if (!($uid = $v->getUid())) {
return;
}
$tmp = array();
if ($v->isDowngraded()) {
$tmp[self::CACHE_DOWNGRADED] = true;
}
foreach ($cf as $key => $val) {
if ($v->exists($key)) {
switch ($key) {
case Horde_Imap_Client::FETCH_ENVELOPE:
$tmp[$val] = $v->getEnvelope();
break;
case Horde_Imap_Client::FETCH_FLAGS:
if ($highestmodseq) {
$modseq[$uid] = $v->getModSeq();
$tmp[$val] = $v->getFlags();
}
break;
case Horde_Imap_Client::FETCH_HEADERS:
foreach ($this->_temp['headers_caching'] as $label => $hash) {
if ($hdr = $v->getHeaders($label)) {
$tmp[$val][$hash] = $hdr;
}
}
break;
case Horde_Imap_Client::FETCH_IMAPDATE:
$tmp[$val] = $v->getImapDate();
break;
case Horde_Imap_Client::FETCH_SIZE:
$tmp[$val] = $v->getSize();
break;
case Horde_Imap_Client::FETCH_STRUCTURE:
$tmp[$val] = clone $v->getStructure();
break;
}
}
}
if (!empty($tmp)) {
$tocache[$uid] = $tmp;
}
$mapping[$v->getSeq()] = $uid;
}
if (!empty($mapping)) {
if (!empty($tocache)) {
$this->_cache->set($this->_selected, $tocache, $uidvalidity);
}
$this->_mailboxOb()->map->update($mapping);
}
if (!empty($modseq)) {
$this->_updateModSeq(max(array_merge($modseq, array($highestmodseq))));
$mbox_ob->setStatus(Horde_Imap_Client::STATUS_SYNCFLAGUIDS, array_keys($modseq));
}
}
/**
* Moves cache entries from the current mailbox to another mailbox.
*
* @param Horde_Imap_Client_Mailbox $to The destination mailbox.
* @param array $map Mapping of source UIDs (keys) to
* destination UIDs (values).
* @param string $uidvalid UIDVALIDITY of destination
* mailbox.
*
* @throws Horde_Imap_Client_Exception
*/
protected function _moveCache(Horde_Imap_Client_Mailbox $to, $map,
$uidvalid)
{
if (!$this->_initCache()) {
return;
}
$c = $this->getParam('cache');
if (in_array(strval($to), $c['fetch_ignore'])) {
$this->_debug->info(sprintf(
'CACHE: Ignoring moving FETCH data (%s => %s)',
$this->_selected,
$to
));
return;
}
$old = $this->_cache->get($this->_selected, array_keys($map), null);
$new = array();
foreach ($map as $key => $val) {
if (!empty($old[$key])) {
$new[$val] = $old[$key];
}
}
if (!empty($new)) {
$this->_cache->set($to, $new, $uidvalid);
}
}
/**
* Delete messages in the cache.
*
* @param Horde_Imap_Client_Mailbox $mailbox The mailbox.
* @param Horde_Imap_Client_Ids $ids The list of IDs to delete in
* $mailbox.
* @param array $opts Additional options (not used
* in base class).
*
* @return Horde_Imap_Client_Ids UIDs that were deleted.
* @throws Horde_Imap_Client_Exception
*/
protected function _deleteMsgs(Horde_Imap_Client_Mailbox $mailbox,
Horde_Imap_Client_Ids $ids,
array $opts = array())
{
if (!$this->_initCache()) {
return $ids;
}
$mbox_ob = $this->_mailboxOb();
$ids_ob = $ids->sequence
? $this->getIdsOb($mbox_ob->map->lookup($ids))
: $ids;
$this->_cache->deleteMsgs($mailbox, $ids_ob->ids);
$mbox_ob->setStatus(Horde_Imap_Client::STATUS_SYNCVANISHED, $ids_ob->ids);
$mbox_ob->map->remove($ids);
return $ids_ob;
}
/**
* Retrieve data from the search cache.
*
* @param string $type The cache type ('search' or 'thread').
* @param array $options The options array of the calling function.
*
* @return mixed Returns search cache metadata. If search was retrieved,
* data is in key 'data'.
* Returns null if caching is not available.
*/
protected function _getSearchCache($type, $options)
{
$status = $this->status($this->_selected, Horde_Imap_Client::STATUS_HIGHESTMODSEQ | Horde_Imap_Client::STATUS_UIDVALIDITY);
/* Search caching requires MODSEQ, which may not be active for a
* mailbox. */
if (empty($status['highestmodseq'])) {
return null;
}
ksort($options);
$cache = hash('md5', $type . serialize($options));
$cacheid = $this->getSyncToken($this->_selected);
$ret = array();
$md = $this->_cache->getMetaData(
$this->_selected,
$status['uidvalidity'],
array(self::CACHE_SEARCH, self::CACHE_SEARCHID)
);
if (!isset($md[self::CACHE_SEARCHID]) ||
($md[self::CACHE_SEARCHID] != $cacheid)) {
$md[self::CACHE_SEARCH] = array();
$md[self::CACHE_SEARCHID] = $cacheid;
if ($this->_debug->debug &&
!isset($this->_temp['searchcacheexpire'][strval($this->_selected)])) {
$this->_debug->info(sprintf(
'SEARCH: Expired from cache [%s]',
$this->_selected
));
$this->_temp['searchcacheexpire'][strval($this->_selected)] = true;
}
} elseif (isset($md[self::CACHE_SEARCH][$cache])) {
$this->_debug->info(sprintf(
'SEARCH: Retrieved %s from cache (%s [%s])',
$type,
$cache,
$this->_selected
));
$ret['data'] = $md[self::CACHE_SEARCH][$cache];
unset($md[self::CACHE_SEARCHID]);
}
return array_merge($ret, array(
'id' => $cache,
'metadata' => $md,
'type' => $type
));
}
/**
* Set data in the search cache.
*
* @param mixed $data The cache data to store.
* @param string $sdata The search data returned from _getSearchCache().
*/
protected function _setSearchCache($data, $sdata)
{
$sdata['metadata'][self::CACHE_SEARCH][$sdata['id']] = $data;
$this->_cache->setMetaData($this->_selected, null, $sdata['metadata']);
if ($this->_debug->debug) {
$this->_debug->info(sprintf(
'SEARCH: Saved %s to cache (%s [%s])',
$sdata['type'],
$sdata['id'],
$this->_selected
));
unset($this->_temp['searchcacheexpire'][strval($this->_selected)]);
}
}
/**
* Updates the cached MODSEQ value.
*
* @param integer $modseq MODSEQ value to store.
*
* @return mixed The MODSEQ of the old value if it was replaced (or false
* if it didn't exist or is the same).
*/
protected function _updateModSeq($modseq)
{
if (!$this->_initCache(true)) {
return false;
}
$mbox_ob = $this->_mailboxOb();
$uidvalid = $mbox_ob->getStatus(Horde_Imap_Client::STATUS_UIDVALIDITY);
$md = $this->_cache->getMetaData($this->_selected, $uidvalid, array(self::CACHE_MODSEQ));
if (isset($md[self::CACHE_MODSEQ])) {
if ($md[self::CACHE_MODSEQ] < $modseq) {
$set = true;
$sync = $md[self::CACHE_MODSEQ];
} else {
$set = false;
$sync = 0;
}
$mbox_ob->setStatus(Horde_Imap_Client::STATUS_SYNCMODSEQ, $md[self::CACHE_MODSEQ]);
} else {
$set = true;
$sync = 0;
}
/* $modseq can be 0 - NOMODSEQ - so don't store in that case. */
if ($set && $modseq) {
$this->_cache->setMetaData($this->_selected, $uidvalid, array(
self::CACHE_MODSEQ => $modseq
));
}
return $sync;
}
/**
* Synchronizes the current mailbox cache with the server (using CONDSTORE
* or QRESYNC).
*/
protected function _condstoreSync()
{
$mbox_ob = $this->_mailboxOb();
/* Check that modseqs are available in mailbox. */
if (!($highestmodseq = $mbox_ob->getStatus(Horde_Imap_Client::STATUS_HIGHESTMODSEQ)) ||
!($modseq = $this->_updateModSeq($highestmodseq))) {
$mbox_ob->sync = true;
}
if ($mbox_ob->sync) {
return;
}
$uids_ob = $this->getIdsOb($this->_cache->get(
$this->_selected,
array(),
array(),
$mbox_ob->getStatus(Horde_Imap_Client::STATUS_UIDVALIDITY)
));
if (!count($uids_ob)) {
$mbox_ob->sync = true;
return;
}
/* Are we caching flags? */
if (array_key_exists(Horde_Imap_Client::FETCH_FLAGS, $this->_cacheFields())) {
$fquery = new Horde_Imap_Client_Fetch_Query();
$fquery->flags();
/* Update flags in cache. Cache will be updated in _fetch(). */
$this->_fetch(new Horde_Imap_Client_Fetch_Results(), array(
array(
'_query' => $fquery,
'changedsince' => $modseq,
'ids' => $uids_ob
)
));
}
/* Search for deleted messages, and remove from cache. */
$vanished = $this->vanished($this->_selected, $modseq, array(
'ids' => $uids_ob
));
< $disappear = array_diff($uids_ob->ids, $vanished->ids);
< if (!empty($disappear)) {
< $this->_deleteMsgs($this->_selected, $this->getIdsOb($disappear));
> if (!empty($vanished->ids)) {
> $this->_deleteMsgs($this->_selected, $this->getIdsOb($vanished->ids));
}
$mbox_ob->sync = true;
}
/**
* Provide the list of available caching fields.
*
* @return array The list of available caching fields (fields are in the
* key).
*/
protected function _cacheFields()
{
$c = $this->getParam('cache');
$out = $c['fields'];
if (!$this->_capability()->isEnabled('CONDSTORE')) {
unset($out[Horde_Imap_Client::FETCH_FLAGS]);
}
return $out;
}
/**
* Return the current mailbox synchronization status.
*
* @param mixed $mailbox A mailbox. Either a Horde_Imap_Client_Mailbox
* object or a string (UTF-8).
*
* @return array An array with status data. (This data is not guaranteed
* to have any specific format).
*/
protected function _syncStatus($mailbox)
{
$status = $this->status(
$mailbox,
Horde_Imap_Client::STATUS_HIGHESTMODSEQ |
Horde_Imap_Client::STATUS_MESSAGES |
Horde_Imap_Client::STATUS_UIDNEXT_FORCE |
Horde_Imap_Client::STATUS_UIDVALIDITY
);
$fields = array('uidnext', 'uidvalidity');
if (empty($status['highestmodseq'])) {
$fields[] = 'messages';
} else {
$fields[] = 'highestmodseq';
}
$out = array();
$sync_map = array_flip(Horde_Imap_Client_Data_Sync::$map);
foreach ($fields as $val) {
$out[$sync_map[$val]] = $status[$val];
}
return array_filter($out);
}
/**
* Get a message UID by the Message-ID. Returns the last message in a
* mailbox that matches.
*
* @param Horde_Imap_Client_Mailbox $mailbox The mailbox to search
* @param string $msgid Message-ID.
*
* @return string UID (null if not found).
*/
protected function _getUidByMessageId($mailbox, $msgid)
{
if (!$msgid) {
return null;
}
$query = new Horde_Imap_Client_Search_Query();
$query->headerText('Message-ID', $msgid);
$res = $this->search($mailbox, $query, array(
'results' => array(Horde_Imap_Client::SEARCH_RESULTS_MAX)
));
return $res['max'];
}
}