Aleksander Machniak
2016-03-28 46f7b7096450939fe03c95aa81ce06ae4bfca89d
program/lib/Roundcube/rcube_imap_cache.php
@@ -1,6 +1,6 @@
<?php
/*
/**
 +-----------------------------------------------------------------------+
 | This file is part of the Roundcube Webmail client                     |
 | Copyright (C) 2005-2012, The Roundcube Dev Team                       |
@@ -27,6 +27,9 @@
 */
class rcube_imap_cache
{
    const MODE_INDEX   = 1;
    const MODE_MESSAGE = 2;
    /**
     * Instance of rcube_imap
     *
@@ -49,6 +52,20 @@
    private $userid;
    /**
     * Expiration time in seconds
     *
     * @var int
     */
    private $ttl;
    /**
     * Maximum cached message size
     *
     * @var int
     */
    private $threshold;
    /**
     * Internal (in-memory) cache
     *
     * @var array
@@ -56,6 +73,7 @@
    private $icache = array();
    private $skip_deleted = false;
    private $mode;
    /**
     * List of known flags. Thanks to this we can handle flag changes
@@ -83,15 +101,35 @@
    /**
     * Object constructor.
     *
     * @param rcube_db   $db           DB handler
     * @param rcube_imap $imap         IMAP handler
     * @param int        $userid       User identifier
     * @param bool       $skip_deleted skip_deleted flag
     * @param string     $ttl          Expiration time of memcache/apc items
     * @param int        $threshold    Maximum cached message size
     */
    function __construct($db, $imap, $userid, $skip_deleted)
    function __construct($db, $imap, $userid, $skip_deleted, $ttl=0, $threshold=0)
    {
        // convert ttl string to seconds
        $ttl = get_offset_sec($ttl);
        if ($ttl > 2592000) $ttl = 2592000;
        $this->db           = $db;
        $this->imap         = $imap;
        $this->userid       = $userid;
        $this->skip_deleted = $skip_deleted;
    }
        $this->ttl          = $ttl;
        $this->threshold    = $threshold;
        // cache all possible information by default
        $this->mode = self::MODE_INDEX | self::MODE_MESSAGE;
        // database tables
        $this->index_table    = $db->table_name('cache_index', true);
        $this->thread_table   = $db->table_name('cache_thread', true);
        $this->messages_table = $db->table_name('cache_messages', true);
    }
    /**
     * Cleanup actions (on shutdown).
@@ -102,6 +140,15 @@
        $this->icache = null;
    }
    /**
     * Set cache mode
     *
     * @param int $mode Cache mode
     */
    public function set_mode($mode)
    {
        $this->mode = $mode;
    }
    /**
     * Return (sorted) messages index (UIDs).
@@ -125,7 +172,7 @@
        // Seek in internal cache
        if (array_key_exists('index', $this->icache[$mailbox])) {
            // The index was fetched from database already, but not validated yet
            if (!array_key_exists('object', $this->icache[$mailbox]['index'])) {
            if (empty($this->icache[$mailbox]['index']['validated'])) {
                $index = $this->icache[$mailbox]['index'];
            }
            // We've got a valid index
@@ -202,6 +249,7 @@
        }
        $this->icache[$mailbox]['index'] = array(
            'validated'  => true,
            'object'     => $data,
            'sort_field' => $sort_field,
            'modseq'     => !empty($index['modseq']) ? $index['modseq'] : $mbox_data['HIGHESTMODSEQ']
@@ -210,14 +258,11 @@
        return $data;
    }
    /**
     * Return messages thread.
     * If threaded index doesn't exist or is invalid, will be updated.
     *
     * @param string  $mailbox     Folder name
     * @param string  $sort_field  Sorting column
     * @param string  $sort_order  Sorting order (ASC|DESC)
     * @param string $mailbox Folder name
     *
     * @return array Messages threaded index
     */
@@ -256,26 +301,17 @@
        if ($index === null) {
            // Get mailbox data (UIDVALIDITY, counters, etc.) for status check
            $mbox_data = $this->imap->folder_data($mailbox);
            if ($mbox_data['EXISTS']) {
                // get all threads (default sort order)
                $threads = $this->imap->fetch_threads($mailbox, true);
            }
            else {
                $threads = new rcube_result_thread($mailbox, '* THREAD');
            }
            $index['object'] = $threads;
            // Get THREADS result
            $index['object'] = $this->get_thread_data($mailbox, $mbox_data);
            // insert/update
            $this->add_thread_row($mailbox, $threads, $mbox_data, $exists);
            $this->add_thread_row($mailbox, $index['object'], $mbox_data, $exists);
        }
        $this->icache[$mailbox]['thread'] = $index;
        return $index['object'];
    }
    /**
     * Returns list of messages (headers). See rcube_imap::fetch_headers().
@@ -291,38 +327,46 @@
            return array();
        }
        // Fetch messages from cache
        $sql_result = $this->db->query(
            "SELECT uid, data, flags"
            ." FROM ".$this->db->table_name('cache_messages')
            ." WHERE user_id = ?"
                ." AND mailbox = ?"
                ." AND uid IN (".$this->db->array2list($msgs, 'integer').")",
            $this->userid, $mailbox);
        $msgs   = array_flip($msgs);
        $result = array();
        while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
            $uid          = intval($sql_arr['uid']);
            $result[$uid] = $this->build_message($sql_arr);
        if ($this->mode & self::MODE_MESSAGE) {
            // Fetch messages from cache
            $sql_result = $this->db->query(
                "SELECT `uid`, `data`, `flags`"
                ." FROM {$this->messages_table}"
                ." WHERE `user_id` = ?"
                    ." AND `mailbox` = ?"
                    ." AND `uid` IN (".$this->db->array2list($msgs, 'integer').")",
                $this->userid, $mailbox);
            if (!empty($result[$uid])) {
                // save memory, we don't need message body here (?)
                $result[$uid]->body = null;
            $msgs = array_flip($msgs);
                unset($msgs[$uid]);
            while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
                $uid          = intval($sql_arr['uid']);
                $result[$uid] = $this->build_message($sql_arr);
                if (!empty($result[$uid])) {
                    // save memory, we don't need message body here (?)
                    $result[$uid]->body = null;
                    unset($msgs[$uid]);
                }
            }
            $msgs = array_flip($msgs);
        }
        // Fetch not found messages from IMAP server
        if (!empty($msgs)) {
            $messages = $this->imap->fetch_headers($mailbox, array_keys($msgs), false, true);
            $messages = $this->imap->fetch_headers($mailbox, $msgs, false, true);
            // Insert to DB and add to result list
            if (!empty($messages)) {
                foreach ($messages as $msg) {
                    $this->add_message($mailbox, $msg, !array_key_exists($msg->uid, $result));
                    if ($this->mode & self::MODE_MESSAGE) {
                        $this->add_message($mailbox, $msg, !array_key_exists($msg->uid, $result));
                    }
                    $result[$msg->uid] = $msg;
                }
            }
@@ -330,7 +374,6 @@
        return $result;
    }
    /**
     * Returns message data.
@@ -353,23 +396,29 @@
            return $this->icache['__message']['object'];
        }
        $sql_result = $this->db->query(
            "SELECT flags, data"
            ." FROM ".$this->db->table_name('cache_messages')
            ." WHERE user_id = ?"
                ." AND mailbox = ?"
                ." AND uid = ?",
                $this->userid, $mailbox, (int)$uid);
        if ($this->mode & self::MODE_MESSAGE) {
            $sql_result = $this->db->query(
                "SELECT `flags`, `data`"
                ." FROM {$this->messages_table}"
                ." WHERE `user_id` = ?"
                    ." AND `mailbox` = ?"
                    ." AND `uid` = ?",
                    $this->userid, $mailbox, (int)$uid);
        if ($sql_arr = $this->db->fetch_assoc($sql_result)) {
            $message = $this->build_message($sql_arr);
            $found   = true;
            if ($sql_arr = $this->db->fetch_assoc($sql_result)) {
                $message = $this->build_message($sql_arr);
                $found   = true;
            }
        }
        // Get the message from IMAP server
        if (empty($message) && $update) {
            $message = $this->imap->get_message_headers($uid, $mailbox, true);
            // cache will be updated in close(), see below
        }
        if (!($this->mode & self::MODE_MESSAGE)) {
            return $message;
        }
        // Save the message in internal cache, will be written to DB in close()
@@ -393,7 +442,6 @@
        return $message;
    }
    /**
     * Saves the message in cache.
     *
@@ -407,8 +455,12 @@
            return;
        }
        $msg   = serialize($this->db->encode(clone $message));
        if (!($this->mode & self::MODE_MESSAGE)) {
            return;
        }
        $flags = 0;
        $msg   = clone $message;
        if (!empty($message->flags)) {
            foreach ($this->flags as $idx => $flag) {
@@ -417,32 +469,50 @@
                }
            }
        }
        unset($msg->flags);
        $msg = $this->db->encode($msg, true);
        // update cache record (even if it exists, the update
        // here will work as select, assume row exist if affected_rows=0)
        if (!$force) {
            $res = $this->db->query(
                "UPDATE ".$this->db->table_name('cache_messages')
                ." SET flags = ?, data = ?, changed = ".$this->db->now()
                ." WHERE user_id = ?"
                    ." AND mailbox = ?"
                    ." AND uid = ?",
                "UPDATE {$this->messages_table}"
                ." SET `flags` = ?, `data` = ?, `expires` = " . ($this->ttl ? $this->db->now($this->ttl) : 'NULL')
                ." WHERE `user_id` = ?"
                    ." AND `mailbox` = ?"
                    ." AND `uid` = ?",
                $flags, $msg, $this->userid, $mailbox, (int) $message->uid);
            if ($this->db->affected_rows()) {
            if ($this->db->affected_rows($res)) {
                return;
            }
        }
        // insert new record
        $this->db->query(
            "INSERT INTO ".$this->db->table_name('cache_messages')
            ." (user_id, mailbox, uid, flags, changed, data)"
            ." VALUES (?, ?, ?, ?, ".$this->db->now().", ?)",
            $this->userid, $mailbox, (int) $message->uid, $flags, $msg);
    }
        $this->db->set_option('ignore_key_errors', true);
        // insert new record
        $res = $this->db->query(
            "INSERT INTO {$this->messages_table}"
            ." (`user_id`, `mailbox`, `uid`, `flags`, `expires`, `data`)"
            ." VALUES (?, ?, ?, ?, ". ($this->ttl ? $this->db->now($this->ttl) : 'NULL') . ", ?)",
            $this->userid, $mailbox, (int) $message->uid, $flags, $msg);
        // race-condition, insert failed so try update (#1489146)
        // thanks to ignore_key_errors "duplicate row" errors will be ignored
        if ($force && !$res && !$this->db->is_error($res)) {
            $this->db->query(
                "UPDATE {$this->messages_table}"
                ." SET `expires` = " . ($this->ttl ? $this->db->now($this->ttl) : 'NULL')
                    .", `flags` = ?, `data` = ?"
                ." WHERE `user_id` = ?"
                    ." AND `mailbox` = ?"
                    ." AND `uid` = ?",
                $flags, $msg, $this->userid, $mailbox, (int) $message->uid);
        }
        $this->db->set_option('ignore_key_errors', false);
    }
    /**
     * Sets the flag for specified message.
@@ -456,6 +526,10 @@
    function change_flag($mailbox, $uids, $flag, $enabled = false)
    {
        if (empty($uids)) {
            return;
        }
        if (!($this->mode & self::MODE_MESSAGE)) {
            return;
        }
@@ -479,17 +553,18 @@
            }
        }
        $binary_check = $this->db->db_provider == 'oracle' ? "BITAND(`flags`, %d)" : "(`flags` & %d)";
        $this->db->query(
            "UPDATE ".$this->db->table_name('cache_messages')
            ." SET changed = ".$this->db->now()
            .", flags = flags ".($enabled ? "+ $idx" : "- $idx")
            ." WHERE user_id = ?"
                ." AND mailbox = ?"
                .(!empty($uids) ? " AND uid IN (".$this->db->array2list($uids, 'integer').")" : "")
                ." AND (flags & $idx) ".($enabled ? "= 0" : "= $idx"),
            "UPDATE {$this->messages_table}"
            ." SET `expires` = ". ($this->ttl ? $this->db->now($this->ttl) : 'NULL')
            .", `flags` = `flags` ".($enabled ? "+ $idx" : "- $idx")
            ." WHERE `user_id` = ?"
                ." AND `mailbox` = ?"
                .(!empty($uids) ? " AND `uid` IN (".$this->db->array2list($uids, 'integer').")" : "")
                ." AND " . sprintf($binary_check, $idx) . ($enabled ? " = 0" : " = $idx"),
            $this->userid, $mailbox);
    }
    /**
     * Removes message(s) from cache.
@@ -499,10 +574,14 @@
     */
    function remove_message($mailbox = null, $uids = null)
    {
        if (!($this->mode & self::MODE_MESSAGE)) {
            return;
        }
        if (!strlen($mailbox)) {
            $this->db->query(
                "DELETE FROM ".$this->db->table_name('cache_messages')
                ." WHERE user_id = ?",
                "DELETE FROM {$this->messages_table}"
                ." WHERE `user_id` = ?",
                $this->userid);
        }
        else {
@@ -515,14 +594,13 @@
            }
            $this->db->query(
                "DELETE FROM ".$this->db->table_name('cache_messages')
                ." WHERE user_id = ?"
                    ." AND mailbox = ?"
                    .($uids !== null ? " AND uid IN (".$this->db->array2list((array)$uids, 'integer').")" : ""),
                "DELETE FROM {$this->messages_table}"
                ." WHERE `user_id` = ?"
                    ." AND `mailbox` = ?"
                    .($uids !== null ? " AND `uid` IN (".$this->db->array2list((array)$uids, 'integer').")" : ""),
                $this->userid, $mailbox);
        }
    }
    /**
     * Clears index cache.
@@ -537,18 +615,18 @@
        // otherwise use 'valid' flag to not loose HIGHESTMODSEQ value
        if ($remove) {
            $this->db->query(
                "DELETE FROM ".$this->db->table_name('cache_index')
                ." WHERE user_id = ?"
                    .(strlen($mailbox) ? " AND mailbox = ".$this->db->quote($mailbox) : ""),
                "DELETE FROM {$this->index_table}"
                ." WHERE `user_id` = ?"
                    .(strlen($mailbox) ? " AND `mailbox` = ".$this->db->quote($mailbox) : ""),
                $this->userid
            );
        }
        else {
            $this->db->query(
                "UPDATE ".$this->db->table_name('cache_index')
                ." SET valid = 0"
                ." WHERE user_id = ?"
                    .(strlen($mailbox) ? " AND mailbox = ".$this->db->quote($mailbox) : ""),
                "UPDATE {$this->index_table}"
                ." SET `valid` = 0"
                ." WHERE `user_id` = ?"
                    .(strlen($mailbox) ? " AND `mailbox` = ".$this->db->quote($mailbox) : ""),
                $this->userid
            );
        }
@@ -563,7 +641,6 @@
        }
    }
    /**
     * Clears thread cache.
     *
@@ -572,9 +649,9 @@
    function remove_thread($mailbox = null)
    {
        $this->db->query(
            "DELETE FROM ".$this->db->table_name('cache_thread')
            ." WHERE user_id = ?"
                .(strlen($mailbox) ? " AND mailbox = ".$this->db->quote($mailbox) : ""),
            "DELETE FROM {$this->thread_table}"
            ." WHERE `user_id` = ?"
                .(strlen($mailbox) ? " AND `mailbox` = ".$this->db->quote($mailbox) : ""),
            $this->userid
        );
@@ -587,7 +664,6 @@
            $this->icache = array();
        }
    }
    /**
     * Clears the cache.
@@ -602,27 +678,24 @@
        $this->remove_message($mailbox, $uids);
    }
    /**
     * Delete cache entries older than TTL
     *
     * @param string $ttl  Lifetime of message cache entries
     * Delete expired cache entries
     */
    function expunge($ttl)
    static function gc()
    {
        // get expiration timestamp
        $ts = get_offset_time($ttl, -1);
        $rcube = rcube::get_instance();
        $db    = $rcube->get_dbh();
        $now   = $db->now();
        $this->db->query("DELETE FROM ".$this->db->table_name('cache_messages')
              ." WHERE changed < " . $this->db->fromunixtime($ts));
        $db->query("DELETE FROM " . $db->table_name('cache_messages', true)
              ." WHERE `expires` < $now");
        $this->db->query("DELETE FROM ".$this->db->table_name('cache_index')
              ." WHERE changed < " . $this->db->fromunixtime($ts));
        $db->query("DELETE FROM " . $db->table_name('cache_index', true)
              ." WHERE `expires` < $now");
        $this->db->query("DELETE FROM ".$this->db->table_name('cache_thread')
              ." WHERE changed < " . $this->db->fromunixtime($ts));
        $db->query("DELETE FROM ".$db->table_name('cache_thread', true)
              ." WHERE `expires` < $now");
    }
    /**
     * Fetches index data from database
@@ -631,15 +704,15 @@
    {
        // Get index from DB
        $sql_result = $this->db->query(
            "SELECT data, valid"
            ." FROM ".$this->db->table_name('cache_index')
            ." WHERE user_id = ?"
                ." AND mailbox = ?",
            "SELECT `data`, `valid`"
            ." FROM {$this->index_table}"
            ." WHERE `user_id` = ?"
                ." AND `mailbox` = ?",
            $this->userid, $mailbox);
        if ($sql_arr = $this->db->fetch_assoc($sql_result)) {
            $data  = explode('@', $sql_arr['data']);
            $index = @unserialize($data[0]);
            $index = $this->db->decode($data[0], true);
            unset($data[0]);
            if (empty($index)) {
@@ -660,7 +733,6 @@
        return null;
    }
    /**
     * Fetches thread data from database
     */
@@ -668,15 +740,15 @@
    {
        // Get thread from DB
        $sql_result = $this->db->query(
            "SELECT data"
            ." FROM ".$this->db->table_name('cache_thread')
            ." WHERE user_id = ?"
                ." AND mailbox = ?",
            "SELECT `data`"
            ." FROM {$this->thread_table}"
            ." WHERE `user_id` = ?"
                ." AND `mailbox` = ?",
            $this->userid, $mailbox);
        if ($sql_arr = $this->db->fetch_assoc($sql_result)) {
            $data   = explode('@', $sql_arr['data']);
            $thread = @unserialize($data[0]);
            $thread = $this->db->decode($data[0], true);
            unset($data[0]);
            if (empty($thread)) {
@@ -694,7 +766,6 @@
        return null;
    }
    /**
     * Saves index data into database
     */
@@ -702,32 +773,51 @@
        $data, $mbox_data = array(), $exists = false, $modseq = null)
    {
        $data = array(
            serialize($data),
            $this->db->encode($data, true),
            $sort_field,
            (int) $this->skip_deleted,
            (int) $mbox_data['UIDVALIDITY'],
            (int) $mbox_data['UIDNEXT'],
            $modseq ? $modseq : $mbox_data['HIGHESTMODSEQ'],
        );
        $data = implode('@', $data);
        $data    = implode('@', $data);
        $expires = $this->ttl ? $this->db->now($this->ttl) : 'NULL';
        if ($exists) {
            $sql_result = $this->db->query(
                "UPDATE ".$this->db->table_name('cache_index')
                ." SET data = ?, valid = 1, changed = ".$this->db->now()
                ." WHERE user_id = ?"
                    ." AND mailbox = ?",
            $res = $this->db->query(
                "UPDATE {$this->index_table}"
                ." SET `data` = ?, `valid` = 1, `expires` = $expires"
                ." WHERE `user_id` = ?"
                    ." AND `mailbox` = ?",
                $data, $this->userid, $mailbox);
            if ($this->db->affected_rows($res)) {
                return;
            }
        }
        $this->db->set_option('ignore_key_errors', true);
        $res = $this->db->query(
            "INSERT INTO {$this->index_table}"
            ." (`user_id`, `mailbox`, `valid`, `expires`, `data`)"
            ." VALUES (?, ?, 1, $expires, ?)",
            $this->userid, $mailbox, $data);
        // race-condition, insert failed so try update (#1489146)
        // thanks to ignore_key_errors "duplicate row" errors will be ignored
        if (!$exists && !$res && !$this->db->is_error($res)) {
            $res = $this->db->query(
                "UPDATE {$this->index_table}"
                ." SET `data` = ?, `valid` = 1, `expires` = $expires"
                ." WHERE `user_id` = ?"
                    ." AND `mailbox` = ?",
                $data, $this->userid, $mailbox);
        }
        else {
            $sql_result = $this->db->query(
                "INSERT INTO ".$this->db->table_name('cache_index')
                ." (user_id, mailbox, data, valid, changed)"
                ." VALUES (?, ?, ?, 1, ".$this->db->now().")",
                $this->userid, $mailbox, $data);
        }
    }
        $this->db->set_option('ignore_key_errors', false);
    }
    /**
     * Saves thread data into database
@@ -735,30 +825,49 @@
    private function add_thread_row($mailbox, $data, $mbox_data = array(), $exists = false)
    {
        $data = array(
            serialize($data),
            $this->db->encode($data, true),
            (int) $this->skip_deleted,
            (int) $mbox_data['UIDVALIDITY'],
            (int) $mbox_data['UIDNEXT'],
        );
        $data = implode('@', $data);
        $data    = implode('@', $data);
        $expires = $this->ttl ? $this->db->now($this->ttl) : 'NULL';
        if ($exists) {
            $sql_result = $this->db->query(
                "UPDATE ".$this->db->table_name('cache_thread')
                ." SET data = ?, changed = ".$this->db->now()
                ." WHERE user_id = ?"
                    ." AND mailbox = ?",
            $res = $this->db->query(
                "UPDATE {$this->thread_table}"
                ." SET `data` = ?, `expires` = $expires"
                ." WHERE `user_id` = ?"
                    ." AND `mailbox` = ?",
                $data, $this->userid, $mailbox);
            if ($this->db->affected_rows($res)) {
                return;
            }
        }
        $this->db->set_option('ignore_key_errors', true);
        $res = $this->db->query(
            "INSERT INTO {$this->thread_table}"
            ." (`user_id`, `mailbox`, `expires`, `data`)"
            ." VALUES (?, ?, $expires, ?)",
            $this->userid, $mailbox, $data);
        // race-condition, insert failed so try update (#1489146)
        // thanks to ignore_key_errors "duplicate row" errors will be ignored
        if (!$exists && !$res && !$this->db->is_error($res)) {
            $this->db->query(
                "UPDATE {$this->thread_table}"
                ." SET `expires` = $expires, `data` = ?"
                ." WHERE `user_id` = ?"
                    ." AND `mailbox` = ?",
                $data, $this->userid, $mailbox);
        }
        else {
            $sql_result = $this->db->query(
                "INSERT INTO ".$this->db->table_name('cache_thread')
                ." (user_id, mailbox, data, changed)"
                ." VALUES (?, ?, ?, ".$this->db->now().")",
                $this->userid, $mailbox, $data);
        }
    }
        $this->db->set_option('ignore_key_errors', false);
    }
    /**
     * Checks index/thread validity
@@ -772,6 +881,8 @@
        if (empty($object)) {
            return false;
        }
        $index['validated'] = true;
        // Get mailbox data (UIDVALIDITY, counters, etc.) for status check
        $mbox_data = $this->imap->folder_data($mailbox);
@@ -875,14 +986,13 @@
                return false;
            }
            // ... and max UID
            if ($object->max() != $this->imap->id2uid($mbox_data['EXISTS'], $mailbox, true)) {
            if ($object->max() != $this->imap->id2uid($mbox_data['EXISTS'], $mailbox)) {
                return false;
            }
        }
        return true;
    }
    /**
     * Synchronizes the mailbox.
@@ -964,15 +1074,17 @@
        $removed = array();
        // Get known UIDs
        $sql_result = $this->db->query(
            "SELECT uid"
            ." FROM ".$this->db->table_name('cache_messages')
            ." WHERE user_id = ?"
                ." AND mailbox = ?",
            $this->userid, $mailbox);
        if ($this->mode & self::MODE_MESSAGE) {
            $sql_result = $this->db->query(
                "SELECT `uid`"
                ." FROM {$this->messages_table}"
                ." WHERE `user_id` = ?"
                    ." AND `mailbox` = ?",
                $this->userid, $mailbox);
        while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
            $uids[] = $sql_arr['uid'];
            while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
                $uids[] = $sql_arr['uid'];
            }
        }
        // Synchronize messages data
@@ -983,7 +1095,7 @@
                $uids, true, array('FLAGS'), $index['modseq'], $qresync);
            if (!empty($result)) {
                foreach ($result as $id => $msg) {
                foreach ($result as $msg) {
                    $uid = $msg->uid;
                    // Remove deleted message
                    if ($this->skip_deleted && !empty($msg->flags['DELETED'])) {
@@ -1003,12 +1115,12 @@
                    }
                    $this->db->query(
                        "UPDATE ".$this->db->table_name('cache_messages')
                        ." SET flags = ?, changed = ".$this->db->now()
                        ." WHERE user_id = ?"
                            ." AND mailbox = ?"
                            ." AND uid = ?"
                            ." AND flags <> ?",
                        "UPDATE {$this->messages_table}"
                        ." SET `flags` = ?, `expires` = " . ($this->ttl ? $this->db->now($this->ttl) : 'NULL')
                        ." WHERE `user_id` = ?"
                            ." AND `mailbox` = ?"
                            ." AND `uid` = ?"
                            ." AND `flags` <> ?",
                        $flags, $this->userid, $mailbox, $uid, $flags);
                }
            }
@@ -1032,17 +1144,18 @@
            }
        }
        // Invalidate thread index (?)
        if (!$index['valid']) {
            $this->remove_thread($mailbox);
        }
        $sort_field = $index['sort_field'];
        $sort_order = $index['object']->get_parameters('ORDER');
        $exists     = true;
        // Validate index
        if (!$this->validate($mailbox, $index, $exists)) {
            // Invalidate (remove) thread index
            // if $exists=false it was already removed in validate()
            if ($exists) {
                $this->remove_thread($mailbox);
            }
            // Update index
            $data = $this->get_index_data($mailbox, $sort_field, $sort_order, $mbox_data);
        }
@@ -1057,7 +1170,6 @@
        $this->icache[$mailbox]['index']['object'] = $data;
    }
    /**
     * Converts cache row into message object.
     *
@@ -1067,7 +1179,7 @@
     */
    private function build_message($sql_arr)
    {
        $message = $this->db->decode(unserialize($sql_arr['data']));
        $message = $this->db->decode($sql_arr['data'], true);
        if ($message) {
            $message->flags = array();
@@ -1081,7 +1193,6 @@
        return $message;
    }
    /**
     * Saves message stored in internal cache
     */
@@ -1090,28 +1201,36 @@
        // Save current message from internal cache
        if ($message = $this->icache['__message']) {
            // clean up some object's data
            $object = $this->message_object_prepare($message['object']);
            $this->message_object_prepare($message['object']);
            // calculate current md5 sum
            $md5sum = md5(serialize($object));
            $md5sum = md5(serialize($message['object']));
            if ($message['md5sum'] != $md5sum) {
                $this->add_message($message['mailbox'], $object, !$message['exists']);
                $this->add_message($message['mailbox'], $message['object'], !$message['exists']);
            }
            $this->icache['__message']['md5sum'] = $md5sum;
        }
    }
    /**
     * Prepares message object to be stored in database.
     *
     * @param rcube_message_header|rcube_message_part
     */
    private function message_object_prepare($msg)
    private function message_object_prepare(&$msg, &$size = 0)
    {
        // Remove body too big (>25kB)
        if ($msg->body && strlen($msg->body) > 25 * 1024) {
            unset($msg->body);
        // Remove body too big
        if (isset($msg->body)) {
            $length = strlen($msg->body);
            if ($msg->body_modified || $size + $length > $this->threshold * 1024) {
                unset($msg->body);
            }
            else {
                $size += $length;
            }
        }
        // Fix mimetype which might be broken by some code when message is displayed
@@ -1121,15 +1240,18 @@
            list($msg->ctype_primary, $msg->ctype_secondary) = explode('/', $msg->mimetype);
        }
        if (is_array($msg->structure->parts)) {
            foreach ($msg->structure->parts as $idx => $part) {
                $msg->structure->parts[$idx] = $this->message_object_prepare($part);
            }
        unset($msg->replaces);
        if (is_object($msg->structure)) {
            $this->message_object_prepare($msg->structure, $size);
        }
        return $msg;
        if (is_array($msg->parts)) {
            foreach ($msg->parts as $part) {
                $this->message_object_prepare($part, $size);
            }
        }
    }
    /**
     * Fetches index data from IMAP server
@@ -1150,6 +1272,23 @@
        return $index;
    }
    /**
     * Fetches thread data from IMAP server
     */
    private function get_thread_data($mailbox, $mbox_data = array())
    {
        if (empty($mbox_data)) {
            $mbox_data = $this->imap->folder_data($mailbox);
        }
        if ($mbox_data['EXISTS']) {
            // get all threads (default sort order)
            return $this->imap->threads_direct($mailbox);
        }
        return new rcube_result_thread($mailbox, '* THREAD');
    }
}
// for backward compat.