The previous behaviour did not work in the addressbook if more than 500 results were provided, because nothing turned the uid only fetch into the full data, resulting in "empty" result records. This solution seems to work fine for the cases tested and avoids having to completely different codepaths based on a number of records found.
1473 lines
48 KiB
PHP
1473 lines
48 KiB
PHP
<?php
|
|
|
|
/**
|
|
* Kolab storage cache class providing a local caching layer for Kolab groupware objects.
|
|
*
|
|
* @version @package_version@
|
|
* @author Thomas Bruederli <bruederli@kolabsys.com>
|
|
*
|
|
* Copyright (C) 2012-2013, Kolab Systems AG <contact@kolabsys.com>
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU Affero General Public License as
|
|
* published by the Free Software Foundation, either version 3 of the
|
|
* License, or (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU Affero General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Affero General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
class kolab_storage_cache
|
|
{
|
|
const DB_DATE_FORMAT = 'Y-m-d H:i:s';
|
|
|
|
protected $db;
|
|
protected $imap;
|
|
protected $folder;
|
|
protected $uid2msg;
|
|
protected $objects;
|
|
protected $metadata = array();
|
|
protected $folder_id;
|
|
protected $resource_uri;
|
|
protected $enabled = true;
|
|
protected $synched = false;
|
|
protected $synclock = false;
|
|
protected $ready = false;
|
|
protected $cache_table;
|
|
protected $folders_table;
|
|
protected $max_sql_packet;
|
|
protected $max_sync_lock_time = 600;
|
|
protected $extra_cols = array();
|
|
protected $data_props = array();
|
|
protected $order_by = null;
|
|
protected $limit = null;
|
|
protected $error = 0;
|
|
protected $server_timezone;
|
|
protected $sync_start;
|
|
protected $cache_bypassed = 0;
|
|
|
|
|
|
/**
|
|
* Factory constructor
|
|
*/
|
|
public static function factory(kolab_storage_folder $storage_folder)
|
|
{
|
|
$subclass = 'kolab_storage_cache_' . $storage_folder->type;
|
|
if (class_exists($subclass)) {
|
|
return new $subclass($storage_folder);
|
|
}
|
|
else {
|
|
rcube::raise_error(array(
|
|
'code' => 900,
|
|
'type' => 'php',
|
|
'message' => "No kolab_storage_cache class found for folder '$storage_folder->name' of type '$storage_folder->type'"
|
|
), true);
|
|
|
|
return new kolab_storage_cache($storage_folder);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Default constructor
|
|
*/
|
|
public function __construct(kolab_storage_folder $storage_folder = null)
|
|
{
|
|
$rcmail = rcube::get_instance();
|
|
$this->db = $rcmail->get_dbh();
|
|
$this->imap = $rcmail->get_storage();
|
|
$this->enabled = $rcmail->config->get('kolab_cache', false);
|
|
$this->folders_table = $this->db->table_name('kolab_folders');
|
|
$this->server_timezone = new DateTimeZone(date_default_timezone_get());
|
|
|
|
if ($this->enabled) {
|
|
// always read folder cache and lock state from DB master
|
|
$this->db->set_table_dsn('kolab_folders', 'w');
|
|
// remove sync-lock on script termination
|
|
$rcmail->add_shutdown_function(array($this, '_sync_unlock'));
|
|
}
|
|
|
|
if ($storage_folder) {
|
|
$this->set_folder($storage_folder);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Direct access to cache by folder_id
|
|
* (only for internal use)
|
|
*/
|
|
public function select_by_id($folder_id)
|
|
{
|
|
$query = $this->db->query("SELECT * FROM `{$this->folders_table}` WHERE `folder_id` = ?", $folder_id);
|
|
if ($sql_arr = $this->db->fetch_assoc($query)) {
|
|
$this->metadata = $sql_arr;
|
|
$this->folder_id = $sql_arr['folder_id'];
|
|
$this->folder = new StdClass;
|
|
$this->folder->type = $sql_arr['type'];
|
|
$this->resource_uri = $sql_arr['resource'];
|
|
$this->cache_table = $this->db->table_name('kolab_cache_' . $sql_arr['type']);
|
|
$this->ready = true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Connect cache with a storage folder
|
|
*
|
|
* @param kolab_storage_folder The storage folder instance to connect with
|
|
*/
|
|
public function set_folder(kolab_storage_folder $storage_folder)
|
|
{
|
|
$this->folder = $storage_folder;
|
|
|
|
if (empty($this->folder->name) || !$this->folder->valid) {
|
|
$this->ready = false;
|
|
return;
|
|
}
|
|
|
|
// compose fully qualified ressource uri for this instance
|
|
$this->resource_uri = $this->folder->get_resource_uri();
|
|
$this->cache_table = $this->db->table_name('kolab_cache_' . $this->folder->type);
|
|
$this->ready = $this->enabled && !empty($this->folder->type);
|
|
$this->folder_id = null;
|
|
}
|
|
|
|
/**
|
|
* Returns true if this cache supports query by type
|
|
*/
|
|
public function has_type_col()
|
|
{
|
|
return in_array('type', $this->extra_cols);
|
|
}
|
|
|
|
/**
|
|
* Getter for the numeric ID used in cache tables
|
|
*/
|
|
public function get_folder_id()
|
|
{
|
|
$this->_read_folder_data();
|
|
return $this->folder_id;
|
|
}
|
|
|
|
/**
|
|
* Returns code of last error
|
|
*
|
|
* @return int Error code
|
|
*/
|
|
public function get_error()
|
|
{
|
|
return $this->error;
|
|
}
|
|
|
|
/**
|
|
* Synchronize local cache data with remote
|
|
*/
|
|
public function synchronize()
|
|
{
|
|
// only sync once per request cycle
|
|
if ($this->synched)
|
|
return;
|
|
|
|
if (!$this->ready) {
|
|
// kolab cache is disabled, synchronize IMAP mailbox cache only
|
|
$this->imap_mode(true);
|
|
$this->imap->folder_sync($this->folder->name);
|
|
$this->imap_mode(false);
|
|
}
|
|
else {
|
|
$this->sync_start = time();
|
|
|
|
// read cached folder metadata
|
|
$this->_read_folder_data();
|
|
|
|
// Read folder data from IMAP
|
|
$ctag = $this->folder->get_ctag();
|
|
|
|
// Validate current ctag
|
|
list($uidvalidity, $highestmodseq, $uidnext) = explode('-', $ctag);
|
|
|
|
if (empty($uidvalidity) || empty($highestmodseq)) {
|
|
rcube::raise_error(array(
|
|
'code' => 900,
|
|
'message' => "Failed to sync the kolab cache (Invalid ctag)"
|
|
), true);
|
|
}
|
|
// check cache status ($this->metadata is set in _read_folder_data())
|
|
else if (
|
|
empty($this->metadata['ctag'])
|
|
|| empty($this->metadata['changed'])
|
|
|| $this->metadata['ctag'] !== $ctag
|
|
) {
|
|
// lock synchronization for this folder or wait if locked
|
|
$this->_sync_lock();
|
|
|
|
// Run a full-sync (initial sync or continue the aborted sync)
|
|
if (empty($this->metadata['changed']) || empty($this->metadata['ctag'])) {
|
|
$result = $this->synchronize_full();
|
|
}
|
|
// Synchronize only the changes since last sync
|
|
else {
|
|
$result = $this->synchronize_update($ctag);
|
|
}
|
|
|
|
// update ctag value (will be written to database in _sync_unlock())
|
|
if ($result) {
|
|
$this->metadata['ctag'] = $ctag;
|
|
$this->metadata['changed'] = date(self::DB_DATE_FORMAT, time());
|
|
}
|
|
|
|
// remove lock
|
|
$this->_sync_unlock();
|
|
}
|
|
}
|
|
|
|
$this->check_error();
|
|
$this->synched = time();
|
|
}
|
|
|
|
/**
|
|
* Perform full cache synchronization
|
|
*/
|
|
protected function synchronize_full()
|
|
{
|
|
// get effective time limit we have for synchronization (~70% of the execution time)
|
|
$time_limit = $this->_max_sync_lock_time() * 0.7;
|
|
|
|
if (time() - $this->sync_start > $time_limit) {
|
|
return false;
|
|
}
|
|
|
|
// disable messages cache if configured to do so
|
|
$this->imap_mode(true);
|
|
|
|
// synchronize IMAP mailbox cache, does nothing if messages cache is disabled
|
|
$this->imap->folder_sync($this->folder->name);
|
|
|
|
// compare IMAP index with object cache index
|
|
$imap_index = $this->imap->index($this->folder->name, null, null, true, true);
|
|
|
|
$this->imap_mode(false);
|
|
|
|
if ($imap_index->is_error()) {
|
|
rcube::raise_error(array(
|
|
'code' => 900,
|
|
'message' => "Failed to sync the kolab cache (SEARCH failed)"
|
|
), true);
|
|
return false;
|
|
}
|
|
|
|
// determine objects to fetch or to invalidate
|
|
$imap_index = $imap_index->get();
|
|
$del_index = array();
|
|
$old_index = $this->current_index($del_index);
|
|
|
|
// Fetch objects and store in DB
|
|
$result = $this->synchronize_fetch($imap_index, $old_index, $del_index);
|
|
|
|
if ($result) {
|
|
// Remove redundant entries from IMAP and cache
|
|
$rem_index = array_intersect($del_index, $imap_index);
|
|
$del_index = array_merge(array_unique($del_index), array_diff($old_index, $imap_index));
|
|
|
|
$this->synchronize_delete($rem_index, $del_index);
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Perform partial cache synchronization, based on QRESYNC
|
|
*/
|
|
protected function synchronize_update()
|
|
{
|
|
if (!$this->imap->get_capability('QRESYNC')) {
|
|
rcube::raise_error(array(
|
|
'code' => 900,
|
|
'message' => "Failed to sync the kolab cache (no QRESYNC capability)"
|
|
), true);
|
|
|
|
return $this->synchronize_full();
|
|
}
|
|
|
|
// Handle the previous ctag
|
|
list($uidvalidity, $highestmodseq, $uidnext) = explode('-', $this->metadata['ctag']);
|
|
|
|
if (empty($uidvalidity) || empty($highestmodseq)) {
|
|
rcube::raise_error(array(
|
|
'code' => 900,
|
|
'message' => "Failed to sync the kolab cache (Invalid old ctag)"
|
|
), true);
|
|
return false;
|
|
}
|
|
|
|
// Enable QRESYNC
|
|
$res = $this->imap->conn->enable('QRESYNC');
|
|
if ($res === false) {
|
|
rcube::raise_error(array(
|
|
'code' => 900,
|
|
'message' => "Failed to sync the kolab cache (failed to enable QRESYNC/CONDSTORE)"
|
|
), true);
|
|
|
|
return false;
|
|
}
|
|
|
|
$mbox_data = $this->imap->folder_data($this->folder->name);
|
|
if (empty($mbox_data)) {
|
|
rcube::raise_error(array(
|
|
'code' => 900,
|
|
'message' => "Failed to sync the kolab cache (failed to get folder state)"
|
|
), true);
|
|
|
|
return false;
|
|
}
|
|
|
|
// Check UIDVALIDITY
|
|
if ($uidvalidity != $mbox_data['UIDVALIDITY']) {
|
|
return $this->synchronize_full();
|
|
}
|
|
|
|
// QRESYNC not supported on specified mailbox
|
|
if (!empty($mbox_data['NOMODSEQ']) || empty($mbox_data['HIGHESTMODSEQ'])) {
|
|
rcube::raise_error(array(
|
|
'code' => 900,
|
|
'message' => "Failed to sync the kolab cache (QRESYNC not supported on the folder)"
|
|
), true);
|
|
|
|
return $this->synchronize_full();
|
|
}
|
|
|
|
// Get modified flags and vanished messages
|
|
// UID FETCH 1:* (FLAGS) (CHANGEDSINCE 0123456789 VANISHED)
|
|
$result = $this->imap->conn->fetch(
|
|
$this->folder->name, '1:*', true, array('FLAGS'), $highestmodseq, true
|
|
);
|
|
|
|
$removed = array();
|
|
$modified = array();
|
|
$existing = $this->current_index($removed);
|
|
|
|
if (!empty($result)) {
|
|
foreach ($result as $msg) {
|
|
$uid = $msg->uid;
|
|
|
|
// Message marked as deleted
|
|
if (!empty($msg->flags['DELETED'])) {
|
|
$removed[] = $uid;
|
|
continue;
|
|
}
|
|
|
|
// Flags changed or new
|
|
$modified[] = $uid;
|
|
}
|
|
}
|
|
|
|
$new = array_diff($modified, $existing, $removed);
|
|
$result = true;
|
|
|
|
if (!empty($new)) {
|
|
$result = $this->synchronize_fetch($new, $existing, $removed);
|
|
|
|
if (!$result) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// VANISHED found?
|
|
$mbox_data = $this->imap->folder_data($this->folder->name);
|
|
|
|
// Removed vanished messages from the database
|
|
$vanished = (array) rcube_imap_generic::uncompressMessageSet($mbox_data['VANISHED'] ?? null);
|
|
|
|
// Remove redundant entries from IMAP and DB
|
|
$vanished = array_merge($removed, array_intersect($vanished, $existing));
|
|
$this->synchronize_delete($removed, $vanished);
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Fetch objects from IMAP and save into the database
|
|
*/
|
|
protected function synchronize_fetch($new_index, &$old_index, &$del_index)
|
|
{
|
|
// get effective time limit we have for synchronization (~70% of the execution time)
|
|
$time_limit = $this->_max_sync_lock_time() * 0.7;
|
|
|
|
if (time() - $this->sync_start > $time_limit) {
|
|
return false;
|
|
}
|
|
|
|
$i = 0;
|
|
$aborted = false;
|
|
|
|
// fetch new objects from imap
|
|
foreach (array_diff($new_index, $old_index) as $msguid) {
|
|
// Note: We'll store only objects matching the folder type
|
|
// anything else will be silently ignored
|
|
if ($object = $this->folder->read_object($msguid)) {
|
|
// Deduplication: remove older objects with the same UID
|
|
// Here we do not resolve conflicts, we just make sure
|
|
// the most recent version of the object will be used
|
|
if ($old_msguid = ($old_index[$object['uid']] ?? null)) {
|
|
if ($old_msguid < $msguid) {
|
|
$del_index[] = $old_msguid;
|
|
}
|
|
else {
|
|
$del_index[] = $msguid;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
$old_index[$object['uid']] = $msguid;
|
|
|
|
$this->_extended_insert($msguid, $object);
|
|
|
|
// check time limit and abort sync if running too long
|
|
if (++$i % 50 == 0 && time() - $this->sync_start > $time_limit) {
|
|
$aborted = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
$this->_extended_insert(0, null);
|
|
|
|
return $aborted === false;
|
|
}
|
|
|
|
/**
|
|
* Remove specified objects from the database and IMAP
|
|
*/
|
|
protected function synchronize_delete($imap_delete, $db_delete)
|
|
{
|
|
if (!empty($imap_delete)) {
|
|
$this->imap_mode(true);
|
|
$this->imap->delete_message($imap_delete, $this->folder->name);
|
|
$this->imap_mode(false);
|
|
}
|
|
|
|
if (!empty($db_delete)) {
|
|
$quoted_ids = join(',', array_map(array($this->db, 'quote'), $db_delete));
|
|
$this->db->query(
|
|
"DELETE FROM `{$this->cache_table}` WHERE `folder_id` = ? AND `msguid` IN ($quoted_ids)",
|
|
$this->folder_id
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return current use->msguid index
|
|
*/
|
|
protected function current_index(&$duplicates = array())
|
|
{
|
|
// read cache index
|
|
$sql_result = $this->db->query(
|
|
"SELECT `msguid`, `uid` FROM `{$this->cache_table}` WHERE `folder_id` = ?"
|
|
. " ORDER BY `msguid` DESC", $this->folder_id
|
|
);
|
|
|
|
$index = $del_index = array();
|
|
|
|
while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
|
|
// Mark all duplicates for removal (note sorting order above)
|
|
// Duplicates here should not happen, but they do sometimes
|
|
if (isset($index[$sql_arr['uid']])) {
|
|
$duplicates[] = $sql_arr['msguid'];
|
|
}
|
|
else {
|
|
$index[$sql_arr['uid']] = $sql_arr['msguid'];
|
|
}
|
|
}
|
|
|
|
return $index;
|
|
}
|
|
|
|
/**
|
|
* Read a single entry from cache or from IMAP directly
|
|
*
|
|
* @param string Related IMAP message UID
|
|
* @param string Object type to read
|
|
* @param string IMAP folder name the entry relates to
|
|
* @param array Hash array with object properties or null if not found
|
|
*/
|
|
public function get($msguid, $type = null, $foldername = null)
|
|
{
|
|
// delegate to another cache instance
|
|
if ($foldername && $foldername != $this->folder->name) {
|
|
$success = false;
|
|
if ($targetfolder = kolab_storage::get_folder($foldername)) {
|
|
$success = $targetfolder->cache->get($msguid, $type);
|
|
$this->error = $targetfolder->cache->get_error();
|
|
}
|
|
return $success;
|
|
}
|
|
|
|
// load object if not in memory
|
|
if (!isset($this->objects[$msguid])) {
|
|
if ($this->ready) {
|
|
$this->_read_folder_data();
|
|
|
|
$sql_result = $this->db->query(
|
|
"SELECT * FROM `{$this->cache_table}` ".
|
|
"WHERE `folder_id` = ? AND `msguid` = ?",
|
|
$this->folder_id,
|
|
$msguid
|
|
);
|
|
|
|
if ($sql_arr = $this->db->fetch_assoc($sql_result)) {
|
|
$this->objects = array($msguid => $this->_unserialize($sql_arr)); // store only this object in memory (#2827)
|
|
}
|
|
}
|
|
|
|
// fetch from IMAP if not present in cache
|
|
if (empty($this->objects[$msguid])) {
|
|
if ($object = $this->folder->read_object($msguid, $type ?: '*', $foldername)) {
|
|
$this->objects = array($msguid => $object);
|
|
$this->set($msguid, $object);
|
|
}
|
|
}
|
|
}
|
|
|
|
$this->check_error();
|
|
return $this->objects[$msguid];
|
|
}
|
|
|
|
/**
|
|
* Getter for a single Kolab object identified by its UID
|
|
*
|
|
* @param string $uid Object UID
|
|
*
|
|
* @return array The Kolab object represented as hash array
|
|
*/
|
|
public function get_by_uid($uid)
|
|
{
|
|
$old_order_by = $this->order_by;
|
|
$old_limit = $this->limit;
|
|
|
|
// set order to make sure we get most recent object version
|
|
// set limit to skip count query
|
|
$this->order_by = '`msguid` DESC';
|
|
$this->limit = array(1, 0);
|
|
|
|
$list = $this->select(array(array('uid', '=', $uid)));
|
|
|
|
// set the order/limit back to defined value
|
|
$this->order_by = $old_order_by;
|
|
$this->limit = $old_limit;
|
|
|
|
if (!empty($list) && !empty($list[0])) {
|
|
return $list[0];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Insert/Update a cache entry
|
|
*
|
|
* @param string Related IMAP message UID
|
|
* @param mixed Hash array with object properties to save or false to delete the cache entry
|
|
* @param string IMAP folder name the entry relates to
|
|
*/
|
|
public function set($msguid, $object, $foldername = null)
|
|
{
|
|
if (!$msguid) {
|
|
return;
|
|
}
|
|
|
|
// delegate to another cache instance
|
|
if ($foldername && $foldername != $this->folder->name) {
|
|
if ($targetfolder = kolab_storage::get_folder($foldername)) {
|
|
$targetfolder->cache->set($msguid, $object);
|
|
$this->error = $targetfolder->cache->get_error();
|
|
}
|
|
return;
|
|
}
|
|
|
|
// remove old entry
|
|
if ($this->ready) {
|
|
$this->_read_folder_data();
|
|
$this->db->query("DELETE FROM `{$this->cache_table}` WHERE `folder_id` = ? AND `msguid` = ?",
|
|
$this->folder_id, $msguid);
|
|
}
|
|
|
|
if ($object) {
|
|
// insert new object data...
|
|
$this->save($msguid, $object);
|
|
}
|
|
else {
|
|
// ...or set in-memory cache to false
|
|
$this->objects[$msguid] = $object;
|
|
}
|
|
|
|
$this->check_error();
|
|
}
|
|
|
|
|
|
/**
|
|
* Insert (or update) a cache entry
|
|
*
|
|
* @param int Related IMAP message UID
|
|
* @param mixed Hash array with object properties to save or false to delete the cache entry
|
|
* @param int Optional old message UID (for update)
|
|
*/
|
|
public function save($msguid, $object, $olduid = null)
|
|
{
|
|
// write to cache
|
|
if ($this->ready) {
|
|
$this->_read_folder_data();
|
|
|
|
$sql_data = $this->_serialize($object);
|
|
$sql_data['folder_id'] = $this->folder_id;
|
|
$sql_data['msguid'] = $msguid;
|
|
$sql_data['uid'] = $object['uid'];
|
|
|
|
$args = array();
|
|
$cols = array('folder_id', 'msguid', 'uid', 'changed', 'data', 'tags', 'words');
|
|
$cols = array_merge($cols, $this->extra_cols);
|
|
|
|
foreach ($cols as $idx => $col) {
|
|
$cols[$idx] = $this->db->quote_identifier($col);
|
|
$args[] = $sql_data[$col];
|
|
}
|
|
|
|
if ($olduid) {
|
|
foreach ($cols as $idx => $col) {
|
|
$cols[$idx] = "$col = ?";
|
|
}
|
|
|
|
$query = "UPDATE `{$this->cache_table}` SET " . implode(', ', $cols)
|
|
. " WHERE `folder_id` = ? AND `msguid` = ?";
|
|
$args[] = $this->folder_id;
|
|
$args[] = $olduid;
|
|
}
|
|
else {
|
|
$query = "INSERT INTO `{$this->cache_table}` (`created`, " . implode(', ', $cols)
|
|
. ") VALUES (" . $this->db->now() . str_repeat(', ?', count($cols)) . ")";
|
|
}
|
|
|
|
$result = $this->db->query($query, $args);
|
|
|
|
if (!$this->db->affected_rows($result)) {
|
|
rcube::raise_error(array(
|
|
'code' => 900, 'type' => 'php',
|
|
'message' => "Failed to write to kolab cache"
|
|
), true);
|
|
}
|
|
}
|
|
|
|
// keep a copy in memory for fast access
|
|
$this->objects = array($msguid => $object);
|
|
$this->uid2msg = array($object['uid'] => $msguid);
|
|
|
|
$this->check_error();
|
|
}
|
|
|
|
|
|
/**
|
|
* Move an existing cache entry to a new resource
|
|
*
|
|
* @param string Entry's IMAP message UID
|
|
* @param string Entry's Object UID
|
|
* @param kolab_storage_folder Target storage folder instance
|
|
* @param string Target entry's IMAP message UID
|
|
*/
|
|
public function move($msguid, $uid, $target, $new_msguid = null)
|
|
{
|
|
if ($this->ready && $target) {
|
|
// clear cached uid mapping and force new lookup
|
|
unset($target->cache->uid2msg[$uid]);
|
|
|
|
// resolve new message UID in target folder
|
|
if (!$new_msguid) {
|
|
$new_msguid = $target->cache->uid2msguid($uid);
|
|
}
|
|
|
|
if ($new_msguid) {
|
|
$this->_read_folder_data();
|
|
|
|
$this->db->query(
|
|
"UPDATE `{$this->cache_table}` SET `folder_id` = ?, `msguid` = ? ".
|
|
"WHERE `folder_id` = ? AND `msguid` = ?",
|
|
$target->cache->get_folder_id(),
|
|
$new_msguid,
|
|
$this->folder_id,
|
|
$msguid
|
|
);
|
|
|
|
$result = $this->db->affected_rows();
|
|
}
|
|
}
|
|
|
|
if (empty($result)) {
|
|
// just clear cache entry
|
|
$this->set($msguid, false);
|
|
}
|
|
|
|
unset($this->uid2msg[$uid]);
|
|
$this->check_error();
|
|
}
|
|
|
|
|
|
/**
|
|
* Remove all objects from local cache
|
|
*/
|
|
public function purge()
|
|
{
|
|
if (!$this->ready) {
|
|
return true;
|
|
}
|
|
|
|
$this->_read_folder_data();
|
|
|
|
$result = $this->db->query(
|
|
"DELETE FROM `{$this->cache_table}` WHERE `folder_id` = ?",
|
|
$this->folder_id
|
|
);
|
|
|
|
return $this->db->affected_rows($result);
|
|
}
|
|
|
|
/**
|
|
* Update resource URI for existing cache entries
|
|
*
|
|
* @param string Target IMAP folder to move it to
|
|
*/
|
|
public function rename($new_folder)
|
|
{
|
|
if (!$this->ready) {
|
|
return;
|
|
}
|
|
|
|
if ($target = kolab_storage::get_folder($new_folder)) {
|
|
// resolve new message UID in target folder
|
|
$this->db->query(
|
|
"UPDATE `{$this->folders_table}` SET `resource` = ? ".
|
|
"WHERE `resource` = ?",
|
|
$target->get_resource_uri(),
|
|
$this->resource_uri
|
|
);
|
|
|
|
$this->check_error();
|
|
}
|
|
else {
|
|
$this->error = kolab_storage::ERROR_IMAP_CONN;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Select Kolab objects filtered by the given query
|
|
*
|
|
* @param array Pseudo-SQL query as list of filter parameter triplets
|
|
* triplet: array('<colname>', '<comparator>', '<value>')
|
|
* @param boolean Set true to only return UIDs instead of complete objects
|
|
* @param boolean Use fast mode to fetch only minimal set of information
|
|
* (no xml fetching and parsing, etc.)
|
|
*
|
|
* @return array List of Kolab data objects (each represented as hash array) or UIDs
|
|
*/
|
|
public function select($query = array(), $uids = false, $fast = false)
|
|
{
|
|
$result = $uids ? array() : new kolab_storage_dataset($this);
|
|
$count = null;
|
|
|
|
// read from local cache DB (assume it to be synchronized)
|
|
if ($this->ready) {
|
|
$this->_read_folder_data();
|
|
|
|
// fetch full object data unless only uids are requested
|
|
$fetchall = !$uids;
|
|
|
|
// skip SELECT if we know it will return nothing
|
|
if ($count === 0) {
|
|
return $result;
|
|
}
|
|
|
|
$sql_query = "SELECT " . ($fetchall ? '*' : "`msguid` AS `_msguid`, `uid`")
|
|
. " FROM `{$this->cache_table}` WHERE `folder_id` = ?"
|
|
. $this->_sql_where($query)
|
|
. (!empty($this->order_by) ? " ORDER BY " . $this->order_by : '');
|
|
|
|
$sql_result = $this->limit ?
|
|
$this->db->limitquery($sql_query, $this->limit[1], $this->limit[0], $this->folder_id) :
|
|
$this->db->query($sql_query, $this->folder_id);
|
|
|
|
if ($this->db->is_error($sql_result)) {
|
|
if ($uids) {
|
|
return null;
|
|
}
|
|
$result->set_error(true);
|
|
return $result;
|
|
}
|
|
|
|
while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
|
|
if ($fast) {
|
|
$sql_arr['fast-mode'] = true;
|
|
}
|
|
if ($uids) {
|
|
$this->uid2msg[$sql_arr['uid']] = $sql_arr['_msguid'];
|
|
$result[] = $sql_arr['uid'];
|
|
}
|
|
else if ($fetchall && ($object = $this->_unserialize($sql_arr))) {
|
|
$result[] = $object;
|
|
}
|
|
else if (!$fetchall) {
|
|
// only add msguid to dataset index
|
|
$result[] = $sql_arr;
|
|
}
|
|
}
|
|
}
|
|
// use IMAP
|
|
else {
|
|
$filter = $this->_query2assoc($query);
|
|
|
|
$this->imap_mode(true);
|
|
|
|
if ($filter['type']) {
|
|
$search = 'UNDELETED HEADER X-Kolab-Type ' . kolab_format::KTYPE_PREFIX . $filter['type'];
|
|
$index = $this->imap->search_once($this->folder->name, $search);
|
|
}
|
|
else {
|
|
$index = $this->imap->index($this->folder->name, null, null, true, true);
|
|
}
|
|
|
|
$this->imap_mode(false);
|
|
|
|
if ($index->is_error()) {
|
|
$this->check_error();
|
|
if ($uids) {
|
|
return null;
|
|
}
|
|
$result->set_error(true);
|
|
return $result;
|
|
}
|
|
|
|
$index = $index->get();
|
|
$result = $uids ? $index : $this->_fetch($index, $filter['type']);
|
|
|
|
// TODO: post-filter result according to query
|
|
}
|
|
|
|
// We don't want to cache big results in-memory, however
|
|
// if we select only one object here, there's a big chance we will need it later
|
|
if (!$uids && count($result) == 1) {
|
|
if ($msguid = $result[0]['_msguid']) {
|
|
$this->uid2msg[$result[0]['uid']] = $msguid;
|
|
$this->objects = array($msguid => $result[0]);
|
|
}
|
|
}
|
|
|
|
$this->check_error();
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Get number of objects mathing the given query
|
|
*
|
|
* @param array $query Pseudo-SQL query as list of filter parameter triplets
|
|
* @return integer The number of objects of the given type
|
|
*/
|
|
public function count($query = array())
|
|
{
|
|
// read from local cache DB (assume it to be synchronized)
|
|
if ($this->ready) {
|
|
$this->_read_folder_data();
|
|
|
|
$sql_result = $this->db->query(
|
|
"SELECT COUNT(*) AS `numrows` FROM `{$this->cache_table}` ".
|
|
"WHERE `folder_id` = ?" . $this->_sql_where($query),
|
|
$this->folder_id
|
|
);
|
|
|
|
if ($this->db->is_error($sql_result)) {
|
|
return null;
|
|
}
|
|
|
|
$sql_arr = $this->db->fetch_assoc($sql_result);
|
|
$count = intval($sql_arr['numrows']);
|
|
}
|
|
// use IMAP
|
|
else {
|
|
$filter = $this->_query2assoc($query);
|
|
|
|
$this->imap_mode(true);
|
|
|
|
if ($filter['type']) {
|
|
$search = 'UNDELETED HEADER X-Kolab-Type ' . kolab_format::KTYPE_PREFIX . $filter['type'];
|
|
$index = $this->imap->search_once($this->folder->name, $search);
|
|
}
|
|
else {
|
|
$index = $this->imap->index($this->folder->name, null, null, true, true);
|
|
}
|
|
|
|
$this->imap_mode(false);
|
|
|
|
if ($index->is_error()) {
|
|
$this->check_error();
|
|
return null;
|
|
}
|
|
|
|
// TODO: post-filter result according to query
|
|
|
|
$count = $index->count();
|
|
}
|
|
|
|
$this->check_error();
|
|
return $count;
|
|
}
|
|
|
|
/**
|
|
* Reset the sync state, i.e. force sync when synchronize() is called again
|
|
*/
|
|
public function reset()
|
|
{
|
|
$this->synched = null;
|
|
}
|
|
|
|
/**
|
|
* Define ORDER BY clause for cache queries
|
|
*/
|
|
public function set_order_by($sortcols)
|
|
{
|
|
if (!empty($sortcols)) {
|
|
$sortcols = array_map(function($v) {
|
|
$v = trim($v);
|
|
if (strpos($v, ' ')) {
|
|
list($column, $order) = explode(' ', $v, 2);
|
|
return "`{$column}` {$order}";
|
|
}
|
|
return "`{$v}`";
|
|
}, (array) $sortcols);
|
|
|
|
$this->order_by = join(', ', $sortcols);
|
|
}
|
|
else {
|
|
$this->order_by = null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Define LIMIT clause for cache queries
|
|
*/
|
|
public function set_limit($length, $offset = 0)
|
|
{
|
|
$this->limit = array($length, $offset);
|
|
}
|
|
|
|
/**
|
|
* Helper method to compose a valid SQL query from pseudo filter triplets
|
|
*/
|
|
protected function _sql_where($query)
|
|
{
|
|
$sql_where = '';
|
|
foreach ((array) $query as $param) {
|
|
if (is_array($param[0])) {
|
|
$subq = array();
|
|
foreach ($param[0] as $q) {
|
|
$subq[] = preg_replace('/^\s*AND\s+/i', '', $this->_sql_where(array($q)));
|
|
}
|
|
if (!empty($subq)) {
|
|
$sql_where .= ' AND (' . implode($param[1] == 'OR' ? ' OR ' : ' AND ', $subq) . ')';
|
|
}
|
|
continue;
|
|
}
|
|
else if ($param[1] == '=' && is_array($param[2])) {
|
|
$qvalue = '(' . join(',', array_map(array($this->db, 'quote'), $param[2])) . ')';
|
|
$param[1] = 'IN';
|
|
}
|
|
else if ($param[1] == '~' || $param[1] == 'LIKE' || $param[1] == '!~' || $param[1] == '!LIKE') {
|
|
$not = ($param[1] == '!~' || $param[1] == '!LIKE') ? 'NOT ' : '';
|
|
$param[1] = $not . 'LIKE';
|
|
$qvalue = $this->db->quote('%'.preg_replace('/(^\^|\$$)/', ' ', $param[2]).'%');
|
|
}
|
|
else if ($param[1] == '~*' || $param[1] == '!~*') {
|
|
$not = $param[1][1] == '!' ? 'NOT ' : '';
|
|
$param[1] = $not . 'LIKE';
|
|
$qvalue = $this->db->quote(preg_replace('/(^\^|\$$)/', ' ', $param[2]).'%');
|
|
}
|
|
else if ($param[0] == 'tags') {
|
|
$param[1] = ($param[1] == '!=' ? 'NOT ' : '' ) . 'LIKE';
|
|
$qvalue = $this->db->quote('% '.$param[2].' %');
|
|
}
|
|
else {
|
|
$qvalue = $this->db->quote($param[2]);
|
|
}
|
|
|
|
$sql_where .= sprintf(' AND %s %s %s',
|
|
$this->db->quote_identifier($param[0]),
|
|
$param[1],
|
|
$qvalue
|
|
);
|
|
}
|
|
|
|
return $sql_where;
|
|
}
|
|
|
|
/**
|
|
* Helper method to convert the given pseudo-query triplets into
|
|
* an associative filter array with 'equals' values only
|
|
*/
|
|
protected function _query2assoc($query)
|
|
{
|
|
// extract object type from query parameter
|
|
$filter = array();
|
|
foreach ($query as $param) {
|
|
if ($param[1] == '=')
|
|
$filter[$param[0]] = $param[2];
|
|
}
|
|
return $filter;
|
|
}
|
|
|
|
/**
|
|
* Fetch messages from IMAP
|
|
*
|
|
* @param array List of message UIDs to fetch
|
|
* @param string Requested object type or * for all
|
|
* @param string IMAP folder to read from
|
|
* @return array List of parsed Kolab objects
|
|
*/
|
|
protected function _fetch($index, $type = null, $folder = null)
|
|
{
|
|
$results = new kolab_storage_dataset($this);
|
|
foreach ((array)$index as $msguid) {
|
|
if ($object = $this->folder->read_object($msguid, $type, $folder)) {
|
|
$results[] = $object;
|
|
$this->set($msguid, $object);
|
|
}
|
|
}
|
|
|
|
return $results;
|
|
}
|
|
|
|
/**
|
|
* Helper method to convert the given Kolab object into a dataset to be written to cache
|
|
*/
|
|
protected function _serialize($object)
|
|
{
|
|
$data = array();
|
|
$sql_data = array('changed' => null, 'tags' => '', 'words' => '');
|
|
|
|
if ($object['changed']) {
|
|
$sql_data['changed'] = date(self::DB_DATE_FORMAT, is_object($object['changed']) ? $object['changed']->format('U') : $object['changed']);
|
|
}
|
|
|
|
if ($object['_formatobj']) {
|
|
$xml = (string) $object['_formatobj']->write(3.0);
|
|
|
|
$data['_size'] = strlen($xml);
|
|
$sql_data['tags'] = ' ' . join(' ', $object['_formatobj']->get_tags()) . ' '; // pad with spaces for strict/prefix search
|
|
$sql_data['words'] = ' ' . join(' ', $object['_formatobj']->get_words()) . ' ';
|
|
}
|
|
|
|
// Store only minimal set of object properties
|
|
foreach ($this->data_props as $prop) {
|
|
if (isset($object[$prop])) {
|
|
$data[$prop] = $object[$prop];
|
|
if ($data[$prop] instanceof DateTimeInterface) {
|
|
$data[$prop] = array(
|
|
'cl' => 'DateTime',
|
|
'dt' => $data[$prop]->format('Y-m-d H:i:s'),
|
|
'tz' => $data[$prop]->getTimezone()->getName(),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
$sql_data['data'] = json_encode(rcube_charset::clean($data));
|
|
|
|
return $sql_data;
|
|
}
|
|
|
|
/**
|
|
* Helper method to turn stored cache data into a valid storage object
|
|
*/
|
|
protected function _unserialize($sql_arr)
|
|
{
|
|
if (!empty($sql_arr['fast-mode']) && !empty($sql_arr['data']) && ($object = json_decode($sql_arr['data'], true))) {
|
|
$object['uid'] = $sql_arr['uid'];
|
|
|
|
foreach ($this->data_props as $prop) {
|
|
if (!empty($object[$prop]['cl']) && $object[$prop]['cl'] == 'DateTime') {
|
|
$object[$prop] = new DateTime($object[$prop]['dt'], new DateTimeZone($object[$prop]['tz']));
|
|
}
|
|
else if (!isset($object[$prop]) && isset($sql_arr[$prop])) {
|
|
$object[$prop] = $sql_arr[$prop];
|
|
}
|
|
}
|
|
|
|
if ($sql_arr['created'] && empty($object['created'])) {
|
|
$object['created'] = new DateTime($sql_arr['created']);
|
|
}
|
|
|
|
if ($sql_arr['changed'] && empty($object['changed'])) {
|
|
$object['changed'] = new DateTime($sql_arr['changed']);
|
|
}
|
|
|
|
$object['_type'] = !empty($sql_arr['type']) ? $sql_arr['type'] : $this->folder->type;
|
|
$object['_msguid'] = $sql_arr['msguid'];
|
|
$object['_mailbox'] = $this->folder->name;
|
|
}
|
|
// Fetch object xml
|
|
else {
|
|
// FIXME: Because old cache solution allowed storing objects that
|
|
// do not match folder type we may end up with invalid objects.
|
|
// 2nd argument of read_object() here makes sure they are still
|
|
// usable. However, not allowing them here might be also an intended
|
|
// solution in future.
|
|
$object = $this->folder->read_object($sql_arr['msguid'], '*');
|
|
}
|
|
|
|
return $object;
|
|
}
|
|
|
|
/**
|
|
* Write records into cache using extended inserts to reduce the number of queries to be executed
|
|
*
|
|
* @param int Message UID. Set 0 to commit buffered inserts
|
|
* @param array Kolab object to cache
|
|
*/
|
|
protected function _extended_insert($msguid, $object)
|
|
{
|
|
static $buffer = '';
|
|
|
|
$line = '';
|
|
$cols = array('folder_id', 'msguid', 'uid', 'created', 'changed', 'data', 'tags', 'words');
|
|
if ($this->extra_cols) {
|
|
$cols = array_merge($cols, $this->extra_cols);
|
|
}
|
|
|
|
if ($object) {
|
|
$sql_data = $this->_serialize($object);
|
|
|
|
// Skip multi-folder insert for all databases but MySQL
|
|
// In Oracle we can't put long data inline, others we don't support yet
|
|
if (strpos($this->db->db_provider, 'mysql') !== 0) {
|
|
$extra_args = array();
|
|
$params = array($this->folder_id, $msguid, $object['uid'], $sql_data['changed'],
|
|
$sql_data['data'], $sql_data['tags'], $sql_data['words']);
|
|
|
|
foreach ($this->extra_cols as $col) {
|
|
$params[] = $sql_data[$col];
|
|
$extra_args[] = '?';
|
|
}
|
|
|
|
$cols = implode(', ', array_map(function($n) { return "`{$n}`"; }, $cols));
|
|
$extra_args = count($extra_args) ? ', ' . implode(', ', $extra_args) : '';
|
|
|
|
$result = $this->db->query(
|
|
"INSERT INTO `{$this->cache_table}` ($cols)"
|
|
. " VALUES (?, ?, ?, " . $this->db->now() . ", ?, ?, ?, ?$extra_args)",
|
|
$params
|
|
);
|
|
|
|
if (!$this->db->affected_rows($result)) {
|
|
rcube::raise_error(array(
|
|
'code' => 900, 'message' => "Failed to write to kolab cache"
|
|
), true);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
$values = array(
|
|
$this->db->quote($this->folder_id),
|
|
$this->db->quote($msguid),
|
|
$this->db->quote($object['uid']),
|
|
$this->db->now(),
|
|
$this->db->quote($sql_data['changed']),
|
|
$this->db->quote($sql_data['data']),
|
|
$this->db->quote($sql_data['tags']),
|
|
$this->db->quote($sql_data['words']),
|
|
);
|
|
foreach ($this->extra_cols as $col) {
|
|
$values[] = $this->db->quote($sql_data[$col]);
|
|
}
|
|
$line = '(' . join(',', $values) . ')';
|
|
}
|
|
|
|
if ($buffer && (!$msguid || (strlen($buffer) + strlen($line) > $this->max_sql_packet()))) {
|
|
$columns = implode(', ', array_map(function($n) { return "`{$n}`"; }, $cols));
|
|
$update = implode(', ', array_map(function($i) { return "`{$i}` = VALUES(`{$i}`)"; }, array_slice($cols, 2)));
|
|
|
|
$result = $this->db->query(
|
|
"INSERT INTO `{$this->cache_table}` ($columns) VALUES $buffer"
|
|
. " ON DUPLICATE KEY UPDATE $update"
|
|
);
|
|
|
|
if (!$this->db->affected_rows($result)) {
|
|
rcube::raise_error(array(
|
|
'code' => 900, 'message' => "Failed to write to kolab cache"
|
|
), true);
|
|
}
|
|
|
|
$buffer = '';
|
|
}
|
|
|
|
$buffer .= ($buffer ? ',' : '') . $line;
|
|
}
|
|
|
|
/**
|
|
* Returns max_allowed_packet from mysql config
|
|
*/
|
|
protected function max_sql_packet()
|
|
{
|
|
if (!$this->max_sql_packet) {
|
|
// mysql limit or max 4 MB
|
|
$value = $this->db->get_variable('max_allowed_packet', 1048500);
|
|
$this->max_sql_packet = min($value, 4*1024*1024) - 2000;
|
|
}
|
|
|
|
return $this->max_sql_packet;
|
|
}
|
|
|
|
/**
|
|
* Read this folder's ID and cache metadata
|
|
*/
|
|
protected function _read_folder_data()
|
|
{
|
|
// already done
|
|
if (!empty($this->folder_id) || !$this->ready)
|
|
return;
|
|
|
|
$sql_arr = $this->db->fetch_assoc($this->db->query(
|
|
"SELECT `folder_id`, `synclock`, `ctag`, `changed`"
|
|
. " FROM `{$this->folders_table}` WHERE `resource` = ?",
|
|
$this->resource_uri
|
|
));
|
|
|
|
if ($sql_arr) {
|
|
$this->metadata = $sql_arr;
|
|
$this->folder_id = $sql_arr['folder_id'];
|
|
}
|
|
else {
|
|
$this->db->query("INSERT INTO `{$this->folders_table}` (`resource`, `type`)"
|
|
. " VALUES (?, ?)", $this->resource_uri, $this->folder->type);
|
|
|
|
$this->folder_id = $this->db->insert_id('kolab_folders');
|
|
$this->metadata = array();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check lock record for this folder and wait if locked or set lock
|
|
*/
|
|
protected function _sync_lock()
|
|
{
|
|
if (!$this->ready)
|
|
return;
|
|
|
|
$this->_read_folder_data();
|
|
|
|
// abort if database is not set-up
|
|
if ($this->db->is_error()) {
|
|
$this->check_error();
|
|
$this->ready = false;
|
|
return;
|
|
}
|
|
|
|
$read_query = "SELECT `synclock`, `ctag` FROM `{$this->folders_table}` WHERE `folder_id` = ?";
|
|
$write_query = "UPDATE `{$this->folders_table}` SET `synclock` = ? WHERE `folder_id` = ? AND `synclock` = ?";
|
|
$max_lock_time = $this->_max_sync_lock_time();
|
|
|
|
// wait if locked (expire locks after 10 minutes) ...
|
|
// ... or if setting lock fails (another process meanwhile set it)
|
|
while (
|
|
(intval($this->metadata['synclock'] ?? 0) + $max_lock_time > time()) ||
|
|
(($res = $this->db->query($write_query, time(), $this->folder_id, intval($this->metadata['synclock'] ?? 0)))
|
|
&& !($affected = $this->db->affected_rows($res))
|
|
)
|
|
) {
|
|
usleep(500000);
|
|
$this->metadata = $this->db->fetch_assoc($this->db->query($read_query, $this->folder_id));
|
|
}
|
|
|
|
$this->synclock = $affected > 0;
|
|
}
|
|
|
|
/**
|
|
* Remove lock for this folder
|
|
*/
|
|
public function _sync_unlock()
|
|
{
|
|
if (!$this->ready || !$this->synclock)
|
|
return;
|
|
|
|
$this->db->query(
|
|
"UPDATE `{$this->folders_table}` SET `synclock` = 0, `ctag` = ?, `changed` = ? WHERE `folder_id` = ?",
|
|
$this->metadata['ctag'],
|
|
$this->metadata['changed'],
|
|
$this->folder_id
|
|
);
|
|
|
|
$this->synclock = false;
|
|
}
|
|
|
|
protected function _max_sync_lock_time()
|
|
{
|
|
$limit = get_offset_sec(ini_get('max_execution_time'));
|
|
|
|
if ($limit <= 0 || $limit > $this->max_sync_lock_time) {
|
|
$limit = $this->max_sync_lock_time;
|
|
}
|
|
|
|
return $limit;
|
|
}
|
|
|
|
/**
|
|
* Check IMAP connection error state
|
|
*/
|
|
protected function check_error()
|
|
{
|
|
if (($err_code = $this->imap->get_error_code()) < 0) {
|
|
$this->error = kolab_storage::ERROR_IMAP_CONN;
|
|
if (($res_code = $this->imap->get_response_code()) !== 0 && in_array($res_code, array(rcube_storage::NOPERM, rcube_storage::READONLY))) {
|
|
$this->error = kolab_storage::ERROR_NO_PERMISSION;
|
|
}
|
|
}
|
|
else if ($this->db->is_error()) {
|
|
$this->error = kolab_storage::ERROR_CACHE_DB;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Resolve an object UID into an IMAP message UID
|
|
*
|
|
* @param string Kolab object UID
|
|
* @param boolean Include deleted objects
|
|
* @return int The resolved IMAP message UID
|
|
*/
|
|
public function uid2msguid($uid, $deleted = false)
|
|
{
|
|
// query local database if available
|
|
if (!isset($this->uid2msg[$uid]) && $this->ready) {
|
|
$this->_read_folder_data();
|
|
|
|
$sql_result = $this->db->query(
|
|
"SELECT `msguid` FROM `{$this->cache_table}` ".
|
|
"WHERE `folder_id` = ? AND `uid` = ? ORDER BY `msguid` DESC",
|
|
$this->folder_id,
|
|
$uid
|
|
);
|
|
|
|
if ($sql_arr = $this->db->fetch_assoc($sql_result)) {
|
|
$this->uid2msg[$uid] = $sql_arr['msguid'];
|
|
}
|
|
}
|
|
|
|
if (!isset($this->uid2msg[$uid])) {
|
|
// use IMAP SEARCH to get the right message
|
|
$index = $this->imap->search_once($this->folder->name, ($deleted ? '' : 'UNDELETED ') .
|
|
'HEADER SUBJECT ' . rcube_imap_generic::escape($uid));
|
|
$results = $index->get();
|
|
$this->uid2msg[$uid] = end($results);
|
|
}
|
|
|
|
return $this->uid2msg[$uid];
|
|
}
|
|
|
|
/**
|
|
* Getter for protected member variables
|
|
*/
|
|
public function __get($name)
|
|
{
|
|
if ($name == 'folder_id') {
|
|
$this->_read_folder_data();
|
|
}
|
|
|
|
return $this->$name;
|
|
}
|
|
|
|
/**
|
|
* Set Roundcube storage options and bypass messages/indexes cache.
|
|
*
|
|
* We use skip_deleted and threading settings specific to Kolab,
|
|
* we have to change these global settings only temporarily.
|
|
* Roundcube cache duplicates information already stored in kolab_cache,
|
|
* that's why we can disable it for better performance.
|
|
*
|
|
* @param bool $force True to start Kolab mode, False to stop it.
|
|
*/
|
|
public function imap_mode($force = false)
|
|
{
|
|
// remember current IMAP settings
|
|
if ($force) {
|
|
$this->imap_options = array(
|
|
'skip_deleted' => $this->imap->get_option('skip_deleted'),
|
|
'threading' => $this->imap->get_threading(),
|
|
);
|
|
}
|
|
|
|
// re-set IMAP settings
|
|
$this->imap->set_threading($force ? false : $this->imap_options['threading']);
|
|
$this->imap->set_options(array(
|
|
'skip_deleted' => $force ? true : $this->imap_options['skip_deleted'],
|
|
));
|
|
|
|
// if kolab cache is disabled do nothing
|
|
if (!$this->enabled) {
|
|
return;
|
|
}
|
|
|
|
static $messages_cache, $cache_bypass;
|
|
|
|
if ($messages_cache === null) {
|
|
$rcmail = rcube::get_instance();
|
|
$messages_cache = (bool) $rcmail->config->get('messages_cache');
|
|
$cache_bypass = (int) $rcmail->config->get('kolab_messages_cache_bypass');
|
|
}
|
|
|
|
if ($messages_cache) {
|
|
// handle recurrent (multilevel) bypass() calls
|
|
if ($force) {
|
|
$this->cache_bypassed += 1;
|
|
if ($this->cache_bypassed > 1) {
|
|
return;
|
|
}
|
|
}
|
|
else {
|
|
$this->cache_bypassed -= 1;
|
|
if ($this->cache_bypassed > 0) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
switch ($cache_bypass) {
|
|
case 2:
|
|
// Disable messages and index cache completely
|
|
$this->imap->set_messages_caching(!$force);
|
|
break;
|
|
|
|
case 3:
|
|
case 1:
|
|
// We'll disable messages cache, but keep index cache (1) or vice-versa (3)
|
|
// Default mode is both (MODE_INDEX | MODE_MESSAGE)
|
|
$mode = $cache_bypass == 3 ? rcube_imap_cache::MODE_MESSAGE : rcube_imap_cache::MODE_INDEX;
|
|
|
|
if (!$force) {
|
|
$mode |= $cache_bypass == 3 ? rcube_imap_cache::MODE_INDEX : rcube_imap_cache::MODE_MESSAGE;
|
|
}
|
|
|
|
$this->imap->set_messages_caching(true, $mode);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Converts DateTime or unix timestamp into sql date format
|
|
* using server timezone.
|
|
*/
|
|
protected function _convert_datetime($datetime)
|
|
{
|
|
if (is_object($datetime)) {
|
|
$dt = clone $datetime;
|
|
$dt->setTimeZone($this->server_timezone);
|
|
return $dt->format(self::DB_DATE_FORMAT);
|
|
}
|
|
else if ($datetime) {
|
|
return date(self::DB_DATE_FORMAT, $datetime);
|
|
}
|
|
}
|
|
}
|