* @category Horde * @copyright 2008-2017 Horde LLC * @license http://www.horde.org/licenses/gpl GPL * @package IMP * * @property-read boolean $changed If true, this object has changed. * @property-read Horde_Imap_Client_Base $client_ob The IMAP client object. * @property-read IMP_Imap_Config $config Base backend config settings. * @property-read boolean $init Has the IMAP object been initialized? * @property-read integer $max_compose_bodysize The maximum size (in bytes) * of the compose message body. * @property-read integer $max_compose_recipients The maximum number of * recipients to send to per * compose message. * @property-read integer $max_compose_timelimit The maximum number of * recipients to send to in the * configured timelimit. * @property-read integer $max_create_mboxes The maximum number of mailboxes * a user can create. * @property-read string $server_key Server key used to login. * @property-read string $thread_algo The threading algorithm to use. * @property-read Horde_Imap_Client_Url $url A URL object. */ class IMP_Imap implements Serializable { /* Access constants. */ const ACCESS_FOLDERS = 1; const ACCESS_SEARCH = 2; const ACCESS_FLAGS = 3; const ACCESS_UNSEEN = 4; const ACCESS_TRASH = 5; const ACCESS_CREATEMBOX = 6; const ACCESS_CREATEMBOX_MAX = 7; const ACCESS_COMPOSE_BODYSIZE = 13; const ACCESS_COMPOSE_RECIPIENTS = 8; const ACCESS_COMPOSE_TIMELIMIT = 9; const ACCESS_ACL = 10; const ACCESS_DRAFTS = 11; const ACCESS_REMOTE = 12; const ACCESS_IMPORT = 14; const ACCESS_SORT = 15; /* Default namespace. */ const NS_DEFAULT = "\0default"; /** * Cached backend configuration. * * @var array */ static protected $_backends = array(); /** * Has this object changed? * * @var boolean */ protected $_changed = false; /** * Backend config. * * @var IMP_Imap_Config */ protected $_config; /** * Object identifier. * * @var string */ protected $_id; /** * The IMAP client object. * * @var Horde_Imap_Client_Base */ protected $_ob; /** * Temporary data cache (destroyed at end of request). * * @var array */ protected $_temp = array(); /** * Constructor. * * @param string $id Object identifier. */ public function __construct($id) { $this->_id = strval($id); } /** */ public function __get($key) { switch ($key) { case 'changed': return $this->_changed; case 'client_ob': return $this->init ? $this->_ob : null; case 'config': return isset($this->_config) ? $this->_config : new Horde_Support_Stub(); case 'init': return isset($this->_ob); case 'max_compose_bodysize': case 'max_compose_recipients': case 'max_compose_timelimit': $perm = $GLOBALS['injector']->getInstance('Horde_Perms')->getPermissions('imp:' . str_replace('max_compose', 'max', $key), $GLOBALS['registry']->getAuth()); return intval($perm[0]); case 'max_create_mboxes': $perm = $GLOBALS['injector']->getInstance('Horde_Perms')->getPermissions('imp:' . $this->_getPerm($key), $GLOBALS['registry']->getAuth()); return intval($perm[0]); case 'server_key': return $this->init ? $this->_ob->getParam('imp:backend') : null; case 'thread_algo': if (!$this->init) { return 'ORDEREDSUBJECT'; } if ($thread = $this->_ob->getParam('imp:thread_algo')) { return $thread; } $thread = $this->config->thread; $thread_cap = $this->queryCapability('THREAD'); if (!in_array($thread, is_array($thread_cap) ? $thread_cap : array())) { $thread = 'ORDEREDSUBJECT'; } $this->_ob->setParam('imp:thread_algo', $thread); $this->_changed = true; return $thread; case 'url': $url = new Horde_Imap_Client_Url(); if ($this->init) { $url->hostspec = $this->getParam('hostspec'); $url->port = $this->getParam('port'); $url->protocol = $this->isImap() ? 'imap' : 'pop'; } return $url; } } /** */ public function __toString() { return $this->_id; } /** * Get the full permission name for a permission. * * @param string $perm The permission. * * @return string The full (backend-specific) permission name. */ protected function _getPerm($perm) { return 'backends:' . ($this->init ? $this->server_key . ':' : '') . $perm; } /** * Determine if this is a connection to an IMAP server. * * @return boolean True if connected to IMAP server. */ public function isImap() { return ($this->init && ($this->_ob instanceof Horde_Imap_Client_Socket)); } /** * Determine if this is a connection to an IMAP server. * * @return boolean True if connected to IMAP server. */ public function isPop3() { return ($this->init && ($this->_ob instanceof Horde_Imap_Client_Socket_Pop3)); } /** * Create the base Horde_Imap_Client object (from an entry in * backends.php). * * @param string $username The username to authenticate with. * @param string $password The password to authenticate with. * @param string $skey Create a new object using this server key. * * @return Horde_Imap_Client_Base Client object. * @throws IMP_Imap_Exception */ public function createBaseImapObject($username, $password, $skey) { if ($this->init) { return $this->client_ob; } if (($config = $this->loadServerConfig($skey)) === false) { $error = new IMP_Imap_Exception('Could not load server configuration.'); Horde::log($error); throw $error; } $imap_config = array( 'hostspec' => $config->hostspec, 'id' => $config->id, 'password' => new IMP_Imap_Password($password), 'port' => $config->port, 'secure' => (($secure = $config->secure) ? $secure : false), 'username' => $username, // IMP specific config 'imp:backend' => $skey ); /* Needed here to set config information in createImapObject(). */ $this->_config = $config; try { return $this->createImapObject($imap_config, ($config->protocol == 'imap')); } catch (IMP_Imap_Exception $e) { unset($this->_config); throw $e; } } /** * Create a Horde_Imap_Client object. * * @param array $config The IMAP configuration. * @param boolean $imap True if IMAP connection, false if POP3. * * @return Horde_Imap_Client_Base Client object. * @throws IMP_Imap_Exception */ public function createImapObject($config, $imap = true) { if ($this->init) { return $this->_ob; } $sconfig = $this->config; $config = array_merge(array( 'cache' => $sconfig->cache_params, 'capability_ignore' => $sconfig->capability_ignore, 'comparator' => $sconfig->comparator, 'debug' => $sconfig->debug, 'debug_literal' => $sconfig->debug_raw, 'lang' => $sconfig->lang, 'timeout' => $sconfig->timeout, // 'imp:login' - Set in __call() ), $config); try { $this->_ob = $imap ? new Horde_Imap_Client_Socket($config) : new Horde_Imap_Client_Socket_Pop3($config); return $this->_ob; } catch (Horde_Imap_Client_Exception $e) { Horde::log($e->raw_msg); throw new IMP_Imap_Exception($e); } } /** * Perform post-login tasks. */ public function doPostLoginTasks() { global $prefs; switch ($this->_config->protocol) { case 'imap': /* Overwrite default special mailbox names. */ foreach ($this->_config->special_mboxes as $key => $val) { if ($key != IMP_Mailbox::MBOX_USERSPECIAL) { $prefs->setValue($key, $val, array( 'force' => true, 'nosave' => true )); } } break; case 'pop': /* Turn some options off if we are working with POP3. */ foreach (array('newmail_notify', 'save_sent_mail') as $val) { $prefs->setValue($val, false, array( 'force' => true, 'nosave' => true )); $prefs->setLocked($val, true); } $prefs->setLocked(IMP_Mailbox::MBOX_DRAFTS, true); $prefs->setLocked(IMP_Mailbox::MBOX_SENT, true); $prefs->setLocked(IMP_Mailbox::MBOX_SPAM, true); $prefs->setLocked(IMP_Mailbox::MBOX_TEMPLATES, true); $prefs->setLocked(IMP_Mailbox::MBOX_TRASH, true); break; } $this->updateFetchIgnore(); } /** * Update the list of mailboxes to ignore when caching FETCH data in the * IMAP client object. */ public function updateFetchIgnore() { if ($this->isImap()) { $special = IMP_Mailbox::getSpecialMailboxes(); $cache = $this->_ob->getParam('cache'); $cache['fetch_ignore'] = array_filter(array( strval($special[IMP_Mailbox::SPECIAL_SPAM]), strval($special[IMP_Mailbox::SPECIAL_TRASH]) )); $this->_ob->setParam('cache', $cache); } } /** * Checks access rights for a server. * * @param integer $right Access right. * * @return boolean Does the access right exist? */ public function access($right) { global $injector; if (!$this->init) { return false; } switch ($right) { case self::ACCESS_ACL: return ($this->config->acl && $this->queryCapability('ACL')); case self::ACCESS_CREATEMBOX: return ($this->isImap() && $injector->getInstance('Horde_Core_Perms')->hasAppPermission($this->_getPerm('create_mboxes'))); case self::ACCESS_CREATEMBOX_MAX: return ($this->isImap() && $injector->getInstance('Horde_Core_Perms')->hasAppPermission($this->_getPerm('max_create_mboxes'))); case self::ACCESS_DRAFTS: case self::ACCESS_FLAGS: case self::ACCESS_IMPORT: case self::ACCESS_SEARCH: case self::ACCESS_UNSEEN: return $this->isImap(); case self::ACCESS_FOLDERS: case self::ACCESS_TRASH: return ($this->isImap() && $injector->getInstance('Horde_Core_Perms')->hasAppPermission($this->_getPerm('allow_folders'))); case self::ACCESS_REMOTE: return $injector->getInstance('Horde_Core_Perms')->hasAppPermission($this->_getPerm('allow_remote')); case self::ACCESS_SORT: return ($this->isImap() && ($this->config->sort_force || $this->_ob->queryCapability('SORT'))); } return false; } /** * Checks compose access rights for a server. * * @param integer $right Access right. * @param integer $data Data required to check the rights: *
* - ACCESS_COMPOSE_BODYSIZE
* The size of the body data.
*
* - ACCESS_COMPOSE_RECIPIENTS
* - ACCESS_COMPOSE_TIMELIMIT
* The number of e-mail recipients.
*
*
* @return boolean Is the access allowed?
*/
public function accessCompose($right, $data)
{
switch ($right) {
case self::ACCESS_COMPOSE_BODYSIZE:
$perm_name = 'max_bodysize';
break;
case self::ACCESS_COMPOSE_RECIPIENTS:
$perm_name = 'max_recipients';
break;
case self::ACCESS_COMPOSE_TIMELIMIT:
$perm_name = 'max_timelimit';
break;
default:
return false;
}
return $GLOBALS['injector']->getInstance('Horde_Core_Perms')->hasAppPermission(
$perm_name,
array(
'opts' => array(
'value' => $data
)
)
);
}
/**
* Get namespace info for a full mailbox path.
*
* @param string $mailbox The mailbox path. (self:NS_DEFAULT will
* return the default personal namespace.)
* @param boolean $personal If true, will return empty namespace only
* if it is a personal namespace.
*
* @return mixed The namespace info for the mailbox path or null if the
* path doesn't exist.
*/
public function getNamespace($mailbox, $personal = false)
{
if ($this->isImap()) {
$ns = $this->getNamespaces();
if ($mailbox !== self::NS_DEFAULT) {
return $ns->getNamespace($mailbox, $personal);
}
foreach ($ns as $val) {
if ($val->type === $val::NS_PERSONAL) {
return $val;
}
}
}
return null;
}
/**
* Return the cache ID for this mailbox.
*
* @param string $mailbox The mailbox name (UTF-8).
* @param array $addl Local IMP metadata to add to the cache ID.
*
* @return string The cache ID.
*/
public function getCacheId($mailbox, array $addl = array())
{
return $this->getSyncToken($mailbox) .
(empty($addl) ? '' : ('|' . implode('|', $addl)));
}
/**
* Parses the cache ID for this mailbox.
*
* @param string $id Cache ID generated by getCacheId().
*
* @return array Two element array:
* - date: (integer) Date information (day of year), if embedded in
* cache ID.
* - token: (string) Mailbox sync token.
*/
public function parseCacheId($id)
{
$out = array('date' => null);
if ((($pos = strrpos($id, '|')) !== false) &&
(substr($id, $pos + 1, 1) == 'D')) {
$out['date'] = substr($id, $pos + 2);
}
$out['token'] = (($pos = strpos($id, '|')) === false)
? $id
: substr($id, 0, $pos);
return $out;
}
/**
* Returns a list of messages, split into slices based on the total
* message size.
*
* @param string $mbox IMAP mailbox.
* @param Horde_Imap_Client_Ids $ids ID list.
* @param integer $size Maximum size of a slice.
*
* @return array An array of Horde_Imap_Client_Ids objects.
*/
public function getSlices(
$mbox, Horde_Imap_Client_Ids $ids, $size = 5242880
)
{
$imp_imap = IMP_Mailbox::get($mbox)->imp_imap;
$query = new Horde_Imap_Client_Fetch_Query();
$query->size();
try {
$res = $imp_imap->fetch($mbox, $query, array(
'ids' => $ids,
'nocache' => true
));
} catch (IMP_Imap_Exception $e) {
return array();
}
$curr = $slices = array();
$curr_size = 0;
foreach ($res as $key => $val) {
$curr_size += $val->getSize();
if ($curr_size > $size) {
$slices[] = $imp_imap->getIdsOb($curr, $ids->sequence);
$curr = array();
}
$curr[] = $key;
}
$slices[] = $imp_imap->getIdsOb($curr, $ids->sequence);
return $slices;
}
/**
* Handle status() calls. This call may hit multiple servers.
*
* @see Horde_Imap_Client_Base#status()
*/
protected function _status($args)
{
global $injector;
$accounts = $mboxes = $out = array();
$imap_factory = $injector->getInstance('IMP_Factory_Imap');
foreach (IMP_Mailbox::get($args[0]) as $val) {
if ($raccount = $val->remote_account) {
$accounts[strval($raccount)] = $raccount;
}
$mboxes[strval($raccount)][] = $val;
}
foreach ($mboxes as $key => $val) {
$imap = $imap_factory->create($key);
if ($imap->init) {
foreach (call_user_func_array(array($imap, 'impStatus'), array($val) + $args) as $key2 => $val2) {
$out[isset($accounts[$key]) ? $accounts[$key]->mailbox($key2) : $key2] = $val2;
}
}
}
return $out;
}
/**
* All other calls to this class are routed to the underlying
* Horde_Imap_Client_Base object.
*
* @param string $method Method name.
* @param array $params Method parameters.
*
* @return mixed The return from the requested method.
* @throws BadMethodCallException
* @throws IMP_Imap_Exception
*/
public function __call($method, $params)
{
global $injector;
if (!$this->init) {
/* Fallback for these methods. */
switch ($method) {
case 'getIdsOb':
$ob = new Horde_Imap_Client_Ids();
call_user_func_array(array($ob, 'add'), $params);
return $ob;
}
throw new Horde_Exception_AuthenticationFailure(
'IMP is marked as authenticated, but no credentials can be found in the session.',
Horde_Auth::REASON_SESSION
);
}
switch ($method) {
case 'append':
case 'createMailbox':
case 'deleteMailbox':
case 'expunge':
case 'fetch':
case 'getACL':
case 'getMetadata':
case 'getMyACLRights':
case 'getQuota':
case 'getQuotaRoot':
case 'getSyncToken':
case 'setMetadata':
case 'setQuota':
case 'store':
case 'subscribeMailbox':
case 'sync':
case 'thread':
// Horde_Imap_Client_Mailbox: these calls all have the mailbox as
// their first parameter.
$params[0] = IMP_Mailbox::getImapMboxOb($params[0]);
break;
case 'copy':
case 'renameMailbox':
// These calls may hit multiple servers.
$source = IMP_Mailbox::get($params[0]);
$dest = IMP_Mailbox::get($params[1]);
if ($source->remote_account != $dest->remote_account) {
return call_user_func_array(array($this, '_' . $method), $params);
}
// Horde_Imap_Client_Mailbox: these calls all have the mailbox as
// their first two parameters.
$params[0] = $source->imap_mbox_ob;
$params[1] = $dest->imap_mbox_ob;
break;
case 'getNamespaces':
if (isset($this->_temp['ns'])) {
return $this->_temp['ns'];
}
$nsconfig = $this->config->namespace;
$params[0] = is_null($nsconfig) ? array() : $nsconfig;
$params[1] = array('ob_return' => true);
break;
case 'impStatus':
/* Internal method: allows status call with array of mailboxes,
* guaranteeing they are all on this server. */
$params[0] = IMP_Mailbox::getImapMboxOb($params[0]);
$method = 'status';
break;
case 'openMailbox':
$mbox = IMP_Mailbox::get($params[0]);
if ($mbox->search) {
/* Can't open a search mailbox. */
return;
}
$params[0] = $mbox->imap_mbox_ob;
break;
case 'search':
$params = call_user_func_array(array($this, '_search'), $params);
break;
case 'status':
if (is_array($params[0])) {
return $this->_status($params);
}
$params[0] = IMP_Mailbox::getImapMboxOb($params[0]);
break;
default:
if (!method_exists($this->_ob, $method)) {
throw new BadMethodCallException(
sprintf('%s: Invalid method call "%s".', __CLASS__, $method)
);
}
break;
}
try {
$result = call_user_func_array(array($this->_ob, $method), $params);
} catch (Horde_Imap_Client_Exception $e) {
$error = new IMP_Imap_Exception($e);
if (!$error->authError()) {
switch ($method) {
case 'getNamespaces':
return new Horde_Imap_Client_Namespace_List();
}
}
Horde::log(
new Exception(
sprintf('[%s] %s', $method, $e->raw_msg),
$e->getCode(),
$e
),
'WARN'
);
throw $error;
}
/* Special handling for various methods. */
switch ($method) {
case 'createMailbox':
case 'deleteMailbox':
case 'renameMailbox':
$injector->getInstance('IMP_Mailbox_SessionCache')->expire(
null,
// Mailbox is first parameter.
IMP_Mailbox::get($params[0])
);
break;
case 'getNamespaces':
$this->_temp['ns'] = $result;
break;
case 'login':
if (!$this->_ob->getParam('imp:login')) {
/* Check for POP3 UIDL support. */
if ($this->isPop3() && !$this->queryCapability('UIDL')) {
Horde::log(
sprintf(
'The POP3 server does not support the REQUIRED UIDL capability. [server key: %s]',
$this->server_key
),
'CRIT'
);
throw new Horde_Exception_AuthenticationFailure(
_("The mail server is not currently avaliable."),
Horde_Auth::REASON_MESSAGE
);
}
$this->_ob->setParam('imp:login', true);
$this->_changed = true;
}
break;
case 'setACL':
$injector->getInstance('IMP_Mailbox_SessionCache')->expire(
IMP_Mailbox_SessionCache::CACHE_ACL,
IMP_Mailbox::get($params[0])
);
break;
}
return $result;
}
/**
* Prepares an IMAP search query. Needed because certain configuration
* parameters may need to be dynamically altered before passed to the
* Imap_Client object.
*
* @param string $mailbox The mailbox to search.
* @param Horde_Imap_Client_Search_Query $query The search query object.
* @param array $opts Additional options.
*
* @return array Parameters to use in the search() call.
*/
protected function _search($mailbox, $query = null, array $opts = array())
{
$mailbox = IMP_Mailbox::get($mailbox);
if (!empty($opts['sort']) && $mailbox->access_sort) {
/* If doing a from/to search, use display sorting if possible.
* Although there is a fallback to a PHP-based display sort, for
* performance reasons only do a display sort if it is supported
* on the server. */
foreach ($opts['sort'] as $key => $val) {
switch ($val) {
case Horde_Imap_Client::SORT_FROM:
$opts['sort'][$key] = Horde_Imap_Client::SORT_DISPLAYFROM_FALLBACK;
break;
case Horde_Imap_Client::SORT_TO:
$opts['sort'][$key] = Horde_Imap_Client::SORT_DISPLAYTO_FALLBACK;
break;
}
}
}
if (!is_null($query)) {
$query->charset('UTF-8', false);
}
return array($mailbox->imap_mbox_ob, $query, $opts);
}
/**
* Handle copy() calls that hit multiple servers.
*
* @see Horde_Imap_Client_Base#copy()
*/
protected function _copy()
{
global $injector;
$args = func_get_args();
$imap_factory = $injector->getInstance('IMP_Factory_Imap');
$source_imap = $imap_factory->create($args[0]);
$dest_imap = $imap_factory->create($args[1]);
$create = !empty($args[2]['create']);
$ids = isset($args[2]['ids'])
? $args[2]['ids']
: $source_imap->getIdsOb(Horde_Imap_Client_Ids::ALL);
$move = !empty($args[2]['move']);
$retval = true;
$query = new Horde_Imap_Client_Fetch_Query();
$query->fullText(array(
'peek' => true
));
foreach ($this->getSlices($args[0], $ids) as $val) {
try {
$res = $source_imap->fetch($args[0], $query, array(
'ids' => $val,
'nocache' => true
));
$append = array();
foreach ($res as $msg) {
$append[] = array(
'data' => $msg->getFullMsg(true)
);
}
$dest_imap->append($args[1], $append, array(
'create' => $create
));
if ($move) {
$source_imap->expunge($args[0], array(
'delete' => true,
'ids' => $val
));
}
} catch (IMP_Imap_Exception $e) {
$retval = false;
}
}
return $retval;
}
/**
* Handle copy() calls. This call may hit multiple servers, so
* need to handle separately from other IMAP calls.
*
* @see Horde_Imap_Client_Base#renameMailbox()
*/
protected function _renameMailbox()
{
$args = func_get_args();
$source = IMP_Mailbox::get($args[0]);
if ($source->create() && $this->copy($source, $args[1])) {
$source->delete();
} else {
throw new IMP_Imap_Exception(_("Could not move all messages between mailboxes, so the original mailbox was not removed."));
}
}
/* Static methods. */
/**
* Loads the IMP server configuration from backends.php.
*
* @param string $server Returns this labeled entry only.
*
* @return mixed If $server is set return this entry; else, return the
* entire servers array. Returns false on error.
*/
static public function loadServerConfig($server = null)
{
global $registry;
if (empty(self::$_backends)) {
try {
$s = $registry->loadConfigFile('backends.php', 'servers', 'imp')->config['servers'];
} catch (Horde_Exception $e) {
Horde::log($e, 'ERR');
return false;
}
foreach ($s as $key => $val) {
if (empty($val['disabled'])) {
self::$_backends[$key] = new IMP_Imap_Config($val);
}
}
}
return is_null($server)
? self::$_backends
: (isset(self::$_backends[$server]) ? self::$_backends[$server] : false);
}
/* Serializable methods. */
/**
*/
public function serialize()
{
return $GLOBALS['injector']->getInstance('Horde_Pack')->pack(
array(
$this->_ob,
$this->_id,
$this->_config
),
array(
'compression' => false,
'phpob' => true
)
);
}
/**
*/
public function unserialize($data)
{
list(
$this->_ob,
$this->_id,
$this->_config
) = $GLOBALS['injector']->getInstance('Horde_Pack')->unpack($data);
}
}