alecpl
2011-12-07 40c45e9de99186eda203a925c09424a3a8ec103c
- Fixed issues with big memory allocation of IMAP results, improved a lot of rcube_imap class


2 files added
8 files modified
2937 ■■■■■ changed files
CHANGELOG 1 ●●●● patch | view | raw | blame | history
program/include/rcube_imap.php 1175 ●●●● patch | view | raw | blame | history
program/include/rcube_imap_cache.php 264 ●●●● patch | view | raw | blame | history
program/include/rcube_imap_generic.php 308 ●●●●● patch | view | raw | blame | history
program/include/rcube_result_index.php 446 ●●●●● patch | view | raw | blame | history
program/include/rcube_result_thread.php 670 ●●●●● patch | view | raw | blame | history
program/steps/mail/func.inc 2 ●●● patch | view | raw | blame | history
program/steps/mail/pagenav.inc 49 ●●●● patch | view | raw | blame | history
program/steps/mail/search.inc 10 ●●●●● patch | view | raw | blame | history
program/steps/mail/sendmail.inc 12 ●●●● patch | view | raw | blame | history
CHANGELOG
@@ -1,6 +1,7 @@
CHANGELOG Roundcube Webmail
===========================
- Fix issues with big memory allocation of IMAP results
- Replace prompt() with jQuery UI dialog (#1485135)
- Fix navigation in messages search results
- Improved handling of some malformed values encoded with quoted-printable (#1488232)
program/include/rcube_imap.php
@@ -431,40 +431,39 @@
     * Save a set of message ids for future message listing methods
     *
     * @param  string  IMAP Search query
     * @param  array   List of message ids or NULL if empty
     * @param  rcube_result_index|rcube_result_thread  Result set
     * @param  string  Charset of search string
     * @param  string  Sorting field
     * @param  string  True if set is sorted (SORT was used for searching)
     */
    function set_search_set($str=null, $msgs=null, $charset=null, $sort_field=null, $threads=false, $sorted=false)
    function set_search_set($str=null, $msgs=null, $charset=null, $sort_field=null, $sorted=false)
    {
        if (is_array($str) && $msgs == null)
            list($str, $msgs, $charset, $sort_field, $threads, $sorted) = $str;
        if ($msgs === false)
            $msgs = array();
        else if ($msgs != null && !is_array($msgs))
            $msgs = explode(',', $msgs);
        if (is_array($str) && $msgs === null)
            list($str, $msgs, $charset, $sort_field, $sorted) = $str;
        $this->search_string     = $str;
        $this->search_set        = $msgs;
        $this->search_charset    = $charset;
        $this->search_sort_field = $sort_field;
        $this->search_threads    = $threads;
        $this->search_sorted     = $sorted;
        $this->search_threads    = is_a($this->search_set, 'rcube_result_thread');
    }
    /**
     * Return the saved search set as hash array
     *
     * @param bool $clone Clone result object
     *
     * @return array Search set
     */
    function get_search_set()
    {
        return array($this->search_string,
        return array(
            $this->search_string,
            $this->search_set,
            $this->search_charset,
            $this->search_sort_field,
            $this->search_threads,
            $this->search_sorted,
        );
    }
@@ -689,10 +688,10 @@
        // count search set
        if ($this->search_string && $mailbox == $this->mailbox && ($mode == 'ALL' || $mode == 'THREADS') && !$force) {
            if ($this->search_threads)
                return $mode == 'ALL' ? count((array)$this->search_set['depth']) : count((array)$this->search_set['tree']);
            if ($mode == 'ALL')
                return $this->search_set->countMessages();
            else
                return count((array)$this->search_set);
                return $this->search_set->count();
        }
        $a_mailbox_cache = $this->get_cache('messagecount');
@@ -705,12 +704,13 @@
            $a_mailbox_cache[$mailbox] = array();
        if ($mode == 'THREADS') {
            $res   = $this->_threadcount($mailbox, $msg_count);
            $count = $res['count'];
            $res   = $this->fetch_threads($mailbox, $force);
            $count = $res->count();
            if ($status) {
                $this->set_folder_stats($mailbox, 'cnt', $res['msgcount']);
                $this->set_folder_stats($mailbox, 'maxuid', $res['maxuid'] ? $this->id2uid($res['maxuid'], $mailbox) : 0);
                $msg_count = $res->countMessages();
                $this->set_folder_stats($mailbox, 'cnt', $msg_count);
                $this->set_folder_stats($mailbox, 'maxuid', $msg_count ? $this->id2uid($msg_count, $mailbox) : 0);
            }
        }
        // RECENT count is fetched a bit different
@@ -721,7 +721,6 @@
        else if ($this->skip_deleted) {
            $search_str = "ALL UNDELETED";
            $keys       = array('COUNT');
            $need_uid   = false;
            if ($mode == 'UNSEEN') {
                $search_str .= " UNSEEN";
@@ -732,24 +731,23 @@
                }
                if ($status) {
                    $keys[]   = 'MAX';
                    $need_uid = true;
                }
            }
            // @TODO: if $force==false && $mode == 'ALL' we could try to use cache index here
            // get message count using (E)SEARCH
            // not very performant but more precise (using UNDELETED)
            $index = $this->conn->search($mailbox, $search_str, $need_uid, $keys);
            $count = is_array($index) ? $index['COUNT'] : 0;
            $index = $this->conn->search($mailbox, $search_str, true, $keys);
            $count = $index->count();
            if ($mode == 'ALL') {
                if ($this->messages_caching) {
                    // Save additional info required by cache status check
                    $this->icache['undeleted_idx'] = array($mailbox, $index['ALL'], $index['COUNT']);
                }
                // Cache index data, will be used in message_index_direct()
                $this->icache['undeleted_idx'] = $index;
                if ($status) {
                    $this->set_folder_stats($mailbox, 'cnt', $count);
                    $this->set_folder_stats($mailbox, 'maxuid', is_array($index) ? $index['MAX'] : 0);
                    $this->set_folder_stats($mailbox, 'maxuid', $index->max());
                }
            }
        }
@@ -775,40 +773,6 @@
    /**
     * Private method for getting nr of threads
     *
     * @param string $mailbox   Folder name
     *
     * @returns array Array containing items: 'count' - threads count,
     *                'msgcount' = messages count, 'maxuid' = max. UID in the set
     * @access  private
     */
    private function _threadcount($mailbox)
    {
        $result = array();
        if (!empty($this->icache['threads'])) {
            $dcount = count($this->icache['threads']['depth']);
            $result = array(
                'count'    => count($this->icache['threads']['tree']),
                'msgcount' => $dcount,
                'maxuid'   => $dcount ? max(array_keys($this->icache['threads']['depth'])) : 0,
            );
        }
        else if (is_array($result = $this->fetch_threads($mailbox))) {
            $dcount = count($result[1]);
            $result = array(
                'count'    => count($result[0]),
                'msgcount' => $dcount,
                'maxuid'   => $dcount ? max(array_keys($result[1])) : 0,
            );
        }
        return $result;
    }
    /**
     * Public method for listing headers
     * convert mailbox name with root dir first
     *
@@ -817,10 +781,11 @@
     * @param   string   $sort_field Header field to sort by
     * @param   string   $sort_order Sort order [ASC|DESC]
     * @param   int      $slice      Number of slice items to extract from result array
     *
     * @return  array    Indexed array with message header objects
     * @access  public
     */
    function list_headers($mailbox='', $page=NULL, $sort_field=NULL, $sort_order=NULL, $slice=0)
    public function list_headers($mailbox='', $page=NULL, $sort_field=NULL, $sort_order=NULL, $slice=0)
    {
        if (!strlen($mailbox)) {
            $mailbox = $this->mailbox;
@@ -844,110 +809,40 @@
     */
    private function _list_headers($mailbox='', $page=NULL, $sort_field=NULL, $sort_order=NULL, $slice=0)
    {
        if (!strlen($mailbox))
        if (!strlen($mailbox)) {
            return array();
        }
        // use saved message set
        if ($this->search_string && $mailbox == $this->mailbox)
            return $this->_list_header_set($mailbox, $page, $sort_field, $sort_order, $slice);
        if ($this->threading)
            return $this->_list_thread_headers($mailbox, $page, $sort_field, $sort_order, $slice);
        $this->_set_sort_order($sort_field, $sort_order);
        $this->set_sort_order($sort_field, $sort_order);
        $page = $page ? $page : $this->list_page;
        // Use messages cache
        if ($mcache = $this->get_mcache_engine()) {
            $msg_index = $mcache->get_index($mailbox, $this->sort_field, $this->sort_order);
            if (empty($msg_index))
                return array();
            $from      = ($page-1) * $this->page_size;
            $to        = $from + $this->page_size;
            $msg_index = array_values($msg_index); // UIDs
            $is_uid    = true;
            $sorted    = true;
            if ($from || $to)
                $msg_index = array_slice($msg_index, $from, $to - $from);
            if ($slice)
                $msg_index = array_slice($msg_index, -$slice, $slice);
            $a_msg_headers = $mcache->get_messages($mailbox, $msg_index);
        }
        // retrieve headers from IMAP
        // use message index sort as default sorting (for better performance)
        else if (!$this->sort_field) {
            if ($this->skip_deleted) {
                // @TODO: this could be cached
                if ($msg_index = $this->_search_index($mailbox, 'ALL UNDELETED')) {
                    list($begin, $end) = $this->_get_message_range(count($msg_index), $page);
                    $msg_index = array_slice($msg_index, $begin, $end-$begin);
                }
            }
            else if ($max = $this->conn->countMessages($mailbox)) {
                list($begin, $end) = $this->_get_message_range($max, $page);
                $msg_index = range($begin+1, $end);
            }
            else
                $msg_index = array();
            if ($slice && $msg_index)
                $msg_index = array_slice($msg_index, ($this->sort_order == 'DESC' ? 0 : -$slice), $slice);
            // fetch reqested headers from server
            if ($msg_index)
                $a_msg_headers = $this->fetch_headers($mailbox, $msg_index);
        }
        // use SORT command
        else if ($this->get_capability('SORT') &&
            // Courier-IMAP provides SORT capability but allows to disable it by admin (#1486959)
            ($msg_index = $this->conn->sort($mailbox, $this->sort_field,
                $this->skip_deleted ? 'UNDELETED' : '', true)) !== false
        ) {
            if (!empty($msg_index)) {
                list($begin, $end) = $this->_get_message_range(count($msg_index), $page);
                $msg_index = array_slice($msg_index, $begin, $end-$begin);
                $is_uid    = true;
                if ($slice)
                    $msg_index = array_slice($msg_index, ($this->sort_order == 'DESC' ? 0 : -$slice), $slice);
                // fetch reqested headers from server
                $a_msg_headers = $this->fetch_headers($mailbox, $msg_index, true);
            }
        }
        // fetch specified header for all messages and sort
        else if ($msg_index = $this->conn->fetchHeaderIndex($mailbox, "1:*",
            $this->sort_field, $this->skip_deleted)
        ) {
            asort($msg_index); // ASC
            $msg_index = array_keys($msg_index);
            list($begin, $end) = $this->_get_message_range(count($msg_index), $page);
            $msg_index = array_slice($msg_index, $begin, $end-$begin);
            if ($slice)
                $msg_index = array_slice($msg_index, ($this->sort_order == 'DESC' ? 0 : -$slice), $slice);
            // fetch reqested headers from server
            $a_msg_headers = $this->fetch_headers($mailbox, $msg_index);
        // use saved message set
        if ($this->search_string && $mailbox == $this->mailbox) {
            return $this->_list_header_set($mailbox, $page, $slice);
        }
        // return empty array if no messages found
        if (!is_array($a_msg_headers) || empty($a_msg_headers))
        if ($this->threading) {
            return $this->_list_thread_headers($mailbox, $page, $slice);
        }
        // get UIDs of all messages in the folder, sorted
        $index = $this->message_index($mailbox, $this->sort_field, $this->sort_order);
        if ($index->isEmpty()) {
            return array();
        }
        // use this class for message sorting
        $sorter = new rcube_header_sorter();
        $sorter->set_index($msg_index, $is_uid);
        $sorter->sort_headers($a_msg_headers);
        $from = ($page-1) * $this->page_size;
        $to   = $from + $this->page_size;
        if ($this->sort_order == 'DESC' && !$sorted)
            $a_msg_headers = array_reverse($a_msg_headers);
        $index->slice($from, $to - $from);
        if ($slice)
            $index->slice(-$slice, $slice);
        // fetch reqested messages headers
        $a_index = $index->get();
        $a_msg_headers = $this->fetch_headers($mailbox, $a_index);
        return array_values($a_msg_headers);
    }
@@ -958,33 +853,20 @@
     *
     * @param   string   $mailbox    Mailbox/folder name
     * @param   int      $page       Current page to list
     * @param   string   $sort_field Header field to sort by
     * @param   string   $sort_order Sort order [ASC|DESC]
     * @param   int      $slice      Number of slice items to extract from result array
     *
     * @return  array    Indexed array with message header objects
     * @see     rcube_imap::list_headers
     */
    private function _list_thread_headers($mailbox, $page=NULL, $sort_field=NULL, $sort_order=NULL, $slice=0)
    private function _list_thread_headers($mailbox, $page, $slice=0)
    {
        $this->_set_sort_order($sort_field, $sort_order);
        $page   = $page ? $page : $this->list_page;
        $mcache = $this->get_mcache_engine();
        // get all threads (not sorted)
        if ($mcache)
            list ($thread_tree, $msg_depth, $has_children) = $mcache->get_thread($mailbox);
        if ($mcache = $this->get_mcache_engine())
            $threads = $mcache->get_thread($mailbox);
        else
            list ($thread_tree, $msg_depth, $has_children) = $this->fetch_threads($mailbox);
            $threads = $this->fetch_threads($mailbox);
        if (empty($thread_tree))
            return array();
        $msg_index = $this->sort_threads($mailbox, $thread_tree);
        return $this->_fetch_thread_headers($mailbox,
            $thread_tree, $msg_depth, $has_children, $msg_index, $page, $slice);
        return $this->_fetch_thread_headers($mailbox, $threads, $page, $slice);
    }
@@ -994,7 +876,7 @@
     * @param  string $mailbox  Folder name
     * @param  bool   $force    Use IMAP server, no cache
     *
     * @return  array    Array with thread data
     * @return rcube_imap_thread Thread data object
     */
    function fetch_threads($mailbox, $force = false)
    {
@@ -1006,72 +888,50 @@
        if (empty($this->icache['threads'])) {
            // get all threads
            $result = $this->conn->thread($mailbox, $this->threading,
                $this->skip_deleted ? 'UNDELETED' : '');
                $this->skip_deleted ? 'UNDELETED' : '', true);
            // add to internal (fast) cache
            $this->icache['threads'] = array();
            $this->icache['threads']['tree'] = is_array($result) ? $result[0] : array();
            $this->icache['threads']['depth'] = is_array($result) ? $result[1] : array();
            $this->icache['threads']['has_children'] = is_array($result) ? $result[2] : array();
            $this->icache['threads'] = $result;
        }
        return array(
            $this->icache['threads']['tree'],
            $this->icache['threads']['depth'],
            $this->icache['threads']['has_children'],
        );
        return $this->icache['threads'];
    }
    /**
     * Private method for fetching threaded messages headers
     *
     * @param string  $mailbox      Mailbox name
     * @param array   $thread_tree  Thread tree data
     * @param array   $msg_depth    Thread depth data
     * @param array   $has_children Thread children data
     * @param array   $msg_index    Messages index
     * @param int     $page         List page number
     * @param int     $slice        Number of threads to slice
     * @param string              $mailbox    Mailbox name
     * @param rcube_result_thread $threads    Threads data object
     * @param int                 $page       List page number
     * @param int                 $slice      Number of threads to slice
     *
     * @return array  Messages headers
     * @access  private
     */
    private function _fetch_thread_headers($mailbox, $thread_tree, $msg_depth, $has_children, $msg_index, $page, $slice=0)
    private function _fetch_thread_headers($mailbox, $threads, $page, $slice=0)
    {
        // now get IDs for current page
        list($begin, $end) = $this->_get_message_range(count($msg_index), $page);
        $msg_index = array_slice($msg_index, $begin, $end-$begin);
        // Sort thread structure
        $this->sort_threads($threads);
        $from = ($page-1) * $this->page_size;
        $to   = $from + $this->page_size;
        $threads->slice($from, $to - $from);
        if ($slice)
            $msg_index = array_slice($msg_index, ($this->sort_order == 'DESC' ? 0 : -$slice), $slice);
            $threads->slice(-$slice, $slice);
        if ($this->sort_order == 'DESC')
            $msg_index = array_reverse($msg_index);
        // flatten threads array
        // @TODO: fetch children only in expanded mode (?)
        $all_ids = array();
        foreach ($msg_index as $root) {
            $all_ids[] = $root;
            if (!empty($thread_tree[$root]))
                $all_ids = array_merge($all_ids, array_keys_recursive($thread_tree[$root]));
        }
        // Get UIDs of all messages in all threads
        $a_index = $threads->get();
        // fetch reqested headers from server
        $a_msg_headers = $this->fetch_headers($mailbox, $all_ids);
        $a_msg_headers = $this->fetch_headers($mailbox, $a_index);
        // return empty array if no messages found
        if (!is_array($a_msg_headers) || empty($a_msg_headers))
            return array();
        // use this class for message sorting
        $sorter = new rcube_header_sorter();
        $sorter->set_index($all_ids);
        $sorter->sort_headers($a_msg_headers);
        unset($a_index);
        // Set depth, has_children and unread_children fields in headers
        $this->_set_thread_flags($a_msg_headers, $msg_depth, $has_children);
        $this->_set_thread_flags($a_msg_headers, $threads);
        return array_values($a_msg_headers);
    }
@@ -1081,30 +941,31 @@
     * Private method for setting threaded messages flags:
     * depth, has_children and unread_children
     *
     * @param  array  $headers      Reference to headers array indexed by message ID
     * @param  array  $msg_depth    Array of messages depth indexed by message ID
     * @param  array  $msg_children Array of messages children flags indexed by message ID
     * @return array   Message headers array indexed by message ID
     * @param  array             $headers Reference to headers array indexed by message UID
     * @param  rcube_imap_result $threads Threads data object
     *
     * @return array Message headers array indexed by message UID
     * @access private
     */
    private function _set_thread_flags(&$headers, $msg_depth, $msg_children)
    private function _set_thread_flags(&$headers, $threads)
    {
        $parents = array();
        foreach ($headers as $idx => $header) {
            $id = $header->id;
            $depth = $msg_depth[$id];
        list ($msg_depth, $msg_children) = $threads->getThreadData();
        foreach ($headers as $uid => $header) {
            $depth = $msg_depth[$uid];
            $parents = array_slice($parents, 0, $depth);
            if (!empty($parents)) {
                $headers[$idx]->parent_uid = end($parents);
                $headers[$uid]->parent_uid = end($parents);
                if (empty($header->flags['SEEN']))
                    $headers[$parents[0]]->unread_children++;
            }
            array_push($parents, $header->uid);
            array_push($parents, $uid);
            $headers[$idx]->depth = $depth;
            $headers[$idx]->has_children = $msg_children[$id];
            $headers[$uid]->depth = $depth;
            $headers[$uid]->has_children = $msg_children[$uid];
        }
    }
@@ -1112,133 +973,114 @@
    /**
     * Private method for listing a set of message headers (search results)
     *
     * @param   string   $mailbox    Mailbox/folder name
     * @param   int      $page       Current page to list
     * @param   string   $sort_field Header field to sort by
     * @param   string   $sort_order Sort order [ASC|DESC]
     * @param   int  $slice      Number of slice items to extract from result array
     * @param   string   $mailbox  Mailbox/folder name
     * @param   int      $page     Current page to list
     * @param   int      $slice    Number of slice items to extract from result array
     *
     * @return  array    Indexed array with message header objects
     * @access  private
     * @see     rcube_imap::list_header_set()
     */
    private function _list_header_set($mailbox, $page=NULL, $sort_field=NULL, $sort_order=NULL, $slice=0)
    private function _list_header_set($mailbox, $page, $slice=0)
    {
        if (!strlen($mailbox) || empty($this->search_set))
        if (!strlen($mailbox) || empty($this->search_set) || $this->search_set->isEmpty()) {
            return array();
        }
        // use saved messages from searching
        if ($this->threading)
            return $this->_list_thread_header_set($mailbox, $page, $sort_field, $sort_order, $slice);
        if ($this->threading) {
            return $this->_list_thread_header_set($mailbox, $page, $slice);
        }
        // search set is threaded, we need a new one
        if ($this->search_threads) {
            if (empty($this->search_set['tree']))
                return array();
            $this->search('', $this->search_string, $this->search_charset, $sort_field);
            $this->search('', $this->search_string, $this->search_charset, $this->sort_field);
        }
        $msgs = $this->search_set;
        $a_msg_headers = array();
        $page = $page ? $page : $this->list_page;
        $start_msg = ($page-1) * $this->page_size;
        $index = clone $this->search_set;
        $from  = ($page-1) * $this->page_size;
        $to    = $from + $this->page_size;
        $this->_set_sort_order($sort_field, $sort_order);
        // return empty array if no messages found
        if ($index->isEmpty())
            return array();
        // quickest method (default sorting)
        if (!$this->search_sort_field && !$this->sort_field) {
            if ($sort_order == 'DESC')
                $msgs = array_reverse($msgs);
            $got_index = true;
        }
        // sorted messages, so we can first slice array and then fetch only wanted headers
        else if ($this->search_sorted) { // SORT searching result
            $got_index = true;
            // reset search set if sorting field has been changed
            if ($this->sort_field && $this->search_sort_field != $this->sort_field) {
                $this->search('', $this->search_string, $this->search_charset, $this->sort_field);
                $index = clone $this->search_set;
                // return empty array if no messages found
                if ($index->isEmpty())
                    return array();
            }
        }
        if ($got_index) {
            if ($this->sort_order != $index->getParameters('ORDER')) {
                $index->revert();
            }
            // get messages uids for one page
            $msgs = array_slice(array_values($msgs), $start_msg, min(count($msgs)-$start_msg, $this->page_size));
            $index->slice($from, $to-$from);
            if ($slice)
                $msgs = array_slice($msgs, -$slice, $slice);
                $index->slice(-$slice, $slice);
            // fetch headers
            $a_msg_headers = $this->fetch_headers($mailbox, $msgs);
            // I didn't found in RFC that FETCH always returns messages sorted by index
            $sorter = new rcube_header_sorter();
            $sorter->set_index($msgs);
            $sorter->sort_headers($a_msg_headers);
            $a_index       = $index->get();
            $a_msg_headers = $this->fetch_headers($mailbox, $a_index);
            return array_values($a_msg_headers);
        }
        // sorted messages, so we can first slice array and then fetch only wanted headers
        if ($this->search_sorted) { // SORT searching result
            // reset search set if sorting field has been changed
            if ($this->sort_field && $this->search_sort_field != $this->sort_field)
                $msgs = $this->search('', $this->search_string, $this->search_charset, $this->sort_field);
        // SEARCH result, need sorting
        $cnt = $index->count();
        // 300: experimantal value for best result
        if (($cnt > 300 && $cnt > $this->page_size) || !$this->sort_field) {
            // use memory less expensive (and quick) method for big result set
            $index = clone $this->message_index('', $this->sort_field, $this->sort_order);
            // get messages uids for one page...
            $index->slice($start_msg, min($cnt-$from, $this->page_size));
            if ($slice)
                $index->slice(-$slice, $slice);
            // ...and fetch headers
            $a_index       = $index->get();
            $a_msg_headers = $this->fetch_headers($mailbox, $a_index);
            return array_values($a_msg_headers);
        }
        else {
            // for small result set we can fetch all messages headers
            $a_index       = $index->get();
            $a_msg_headers = $this->fetch_headers($mailbox, $a_index, false);
            // return empty array if no messages found
            if (empty($msgs))
            if (!is_array($a_msg_headers) || empty($a_msg_headers))
                return array();
            if ($sort_order == 'DESC')
                $msgs = array_reverse($msgs);
            // if not already sorted
            $a_msg_headers = $this->conn->sortHeaders(
                $a_msg_headers, $this->sort_field, $this->sort_order);
            // get messages uids for one page
            $msgs = array_slice(array_values($msgs), $start_msg, min(count($msgs)-$start_msg, $this->page_size));
            // only return the requested part of the set
            $a_msg_headers = array_slice(array_values($a_msg_headers),
                $from, min($cnt-$to, $this->page_size));
            if ($slice)
                $msgs = array_slice($msgs, -$slice, $slice);
                $a_msg_headers = array_slice($a_msg_headers, -$slice, $slice);
            // fetch headers
            $a_msg_headers = $this->fetch_headers($mailbox, $msgs);
            $sorter = new rcube_header_sorter();
            $sorter->set_index($msgs);
            $sorter->sort_headers($a_msg_headers);
            return array_values($a_msg_headers);
        }
        else { // SEARCH result, need sorting
            $cnt = count($msgs);
            // 300: experimantal value for best result
            if (($cnt > 300 && $cnt > $this->page_size) || !$this->sort_field) {
                // use memory less expensive (and quick) method for big result set
                $a_index = $this->message_index('', $this->sort_field, $this->sort_order);
                // get messages uids for one page...
                $msgs = array_slice($a_index, $start_msg, min($cnt-$start_msg, $this->page_size));
                if ($slice)
                    $msgs = array_slice($msgs, -$slice, $slice);
                // ...and fetch headers
                $a_msg_headers = $this->fetch_headers($mailbox, $msgs);
                // return empty array if no messages found
                if (!is_array($a_msg_headers) || empty($a_msg_headers))
                    return array();
                $sorter = new rcube_header_sorter();
                $sorter->set_index($msgs);
                $sorter->sort_headers($a_msg_headers);
                return array_values($a_msg_headers);
            }
            else {
                // for small result set we can fetch all messages headers
                $a_msg_headers = $this->fetch_headers($mailbox, $msgs);
                // return empty array if no messages found
                if (!is_array($a_msg_headers) || empty($a_msg_headers))
                    return array();
                // if not already sorted
                $a_msg_headers = $this->conn->sortHeaders(
                    $a_msg_headers, $this->sort_field, $this->sort_order);
                // only return the requested part of the set
                $a_msg_headers = array_slice(array_values($a_msg_headers),
                    $start_msg, min($cnt-$start_msg, $this->page_size));
                if ($slice)
                    $a_msg_headers = array_slice($a_msg_headers, -$slice, $slice);
                return $a_msg_headers;
            }
            return $a_msg_headers;
        }
    }
@@ -1248,105 +1090,62 @@
     *
     * @param   string   $mailbox    Mailbox/folder name
     * @param   int      $page       Current page to list
     * @param   string   $sort_field Header field to sort by
     * @param   string   $sort_order Sort order [ASC|DESC]
     * @param   int      $slice      Number of slice items to extract from result array
     *
     * @return  array    Indexed array with message header objects
     * @access  private
     * @see     rcube_imap::list_header_set()
     */
    private function _list_thread_header_set($mailbox, $page=NULL, $sort_field=NULL, $sort_order=NULL, $slice=0)
    private function _list_thread_header_set($mailbox, $page, $slice=0)
    {
        // update search_set if previous data was fetched with disabled threading
        if (!$this->search_threads) {
            if (empty($this->search_set))
            if ($this->search_set->isEmpty())
                return array();
            $this->search('', $this->search_string, $this->search_charset, $sort_field);
            $this->search('', $this->search_string, $this->search_charset, $this->sort_field);
        }
        // empty result
        if (empty($this->search_set['tree']))
            return array();
        $thread_tree = $this->search_set['tree'];
        $msg_depth = $this->search_set['depth'];
        $has_children = $this->search_set['children'];
        $a_msg_headers = array();
        $page = $page ? $page : $this->list_page;
        $start_msg = ($page-1) * $this->page_size;
        $this->_set_sort_order($sort_field, $sort_order);
        $msg_index = $this->sort_threads($mailbox, $thread_tree, array_keys($msg_depth));
        return $this->_fetch_thread_headers($mailbox,
            $thread_tree, $msg_depth, $has_children, $msg_index, $page, $slice=0);
        return $this->_fetch_thread_headers($mailbox, clone $this->search_set, $page, $slice);
    }
    /**
     * Helper function to get first and last index of the requested set
     *
     * @param  int     $max  Messages count
     * @param  mixed   $page Page number to show, or string 'all'
     * @return array   Array with two values: first index, last index
     * @access private
     */
    private function _get_message_range($max, $page)
    {
        $start_msg = ($page-1) * $this->page_size;
        if ($page=='all') {
            $begin  = 0;
            $end    = $max;
        }
        else if ($this->sort_order=='DESC') {
            $begin  = $max - $this->page_size - $start_msg;
            $end    = $max - $start_msg;
        }
        else {
            $begin  = $start_msg;
            $end    = $start_msg + $this->page_size;
        }
        if ($begin < 0) $begin = 0;
        if ($end < 0) $end = $max;
        if ($end > $max) $end = $max;
        return array($begin, $end);
    }
    /**
     * Fetches messages headers
     * Fetches messages headers (by UID)
     *
     * @param  string  $mailbox  Mailbox name
     * @param  array   $msgs     Messages sequence numbers
     * @param  bool    $is_uid   Enable if $msgs numbers are UIDs
     * @param  array   $msgs     Message UIDs
     * @param  bool    $sort     Enables result sorting by $msgs
     * @param  bool    $force    Disables cache use
     *
     * @return array Messages headers indexed by UID
     * @access private
     */
    function fetch_headers($mailbox, $msgs, $is_uid = false, $force = false)
    function fetch_headers($mailbox, $msgs, $sort = true, $force = false)
    {
        if (empty($msgs))
            return array();
        if (!$force && ($mcache = $this->get_mcache_engine())) {
            return $mcache->get_messages($mailbox, $msgs, $is_uid);
            $headers = $mcache->get_messages($mailbox, $msgs);
        }
        else {
            // fetch reqested headers from server
            $headers = $this->conn->fetchHeaders(
                $mailbox, $msgs, true, false, $this->get_fetch_headers());
        }
        // fetch reqested headers from server
        $index = $this->conn->fetchHeaders(
            $mailbox, $msgs, $is_uid, false, $this->get_fetch_headers());
        if (empty($index))
        if (empty($headers))
            return array();
        foreach ($index as $headers) {
            $a_msg_headers[$headers->uid] = $headers;
        foreach ($headers as $h) {
            $a_msg_headers[$h->uid] = $h;
        }
        if ($sort) {
            // use this class for message sorting
            $sorter = new rcube_header_sorter();
            $sorter->set_index($msgs);
            $sorter->sort_headers($a_msg_headers);
        }
        return $a_msg_headers;
@@ -1362,7 +1161,7 @@
     * @param string $mailbox Mailbox/folder name
     * @return int   Folder status
     */
    function mailbox_status($mailbox = null)
    public function mailbox_status($mailbox = null)
    {
        if (!strlen($mailbox)) {
            $mailbox = $this->mailbox;
@@ -1425,205 +1224,171 @@
    /**
     * Return sorted array of message IDs (not UIDs)
     * Return sorted list of message UIDs
     *
     * @param string $mailbox    Mailbox to get index from
     * @param string $sort_field Sort column
     * @param string $sort_order Sort order [ASC, DESC]
     * @return array Indexed array with message IDs
     *
     * @return rcube_result_index|rcube_result_thread List of messages (UIDs)
     */
    function message_index($mailbox='', $sort_field=NULL, $sort_order=NULL)
    public function message_index($mailbox='', $sort_field=NULL, $sort_order=NULL)
    {
        if ($this->threading)
            return $this->thread_index($mailbox, $sort_field, $sort_order);
        $this->_set_sort_order($sort_field, $sort_order);
        $this->set_sort_order($sort_field, $sort_order);
        if (!strlen($mailbox)) {
            $mailbox = $this->mailbox;
        }
        $key = "{$mailbox}:{$this->sort_field}:{$this->sort_order}:{$this->search_string}.msgi";
        // we have a saved search result, get index from there
        if (!isset($this->icache[$key]) && $this->search_string
            && !$this->search_threads && $mailbox == $this->mailbox) {
            // use message index sort as default sorting
            if (!$this->sort_field) {
                $msgs = $this->search_set;
                if ($this->search_sort_field != 'date')
                    sort($msgs);
                if ($this->sort_order == 'DESC')
                    $this->icache[$key] = array_reverse($msgs);
                else
                    $this->icache[$key] = $msgs;
        if ($this->search_string) {
            if ($this->search_threads) {
                $this->search($mailbox, $this->search_string, $this->search_charset, $this->sort_field);
            }
            // sort with SORT command
            else if ($this->search_sorted) {
                if ($this->sort_field && $this->search_sort_field != $this->sort_field)
                    $this->search('', $this->search_string, $this->search_charset, $this->sort_field);
                if ($this->sort_order == 'DESC')
                    $this->icache[$key] = array_reverse($this->search_set);
                else
                    $this->icache[$key] = $this->search_set;
            // use message index sort as default sorting
            if (!$this->sort_field || $this->search_sorted) {
                if ($this->sort_field && $this->search_sort_field != $this->sort_field) {
                    $this->search($mailbox, $this->search_string, $this->search_charset, $this->sort_field);
                }
                $index = $this->search_set;
            }
            else {
                $a_index = $this->conn->fetchHeaderIndex($mailbox,
                    join(',', $this->search_set), $this->sort_field, $this->skip_deleted);
                if (is_array($a_index)) {
                    if ($this->sort_order=="ASC")
                        asort($a_index);
                    else if ($this->sort_order=="DESC")
                        arsort($a_index);
                    $this->icache[$key] = array_keys($a_index);
                }
                else {
                    $this->icache[$key] = array();
                }
                $index = $this->conn->index($mailbox, $this->search_set->get(),
                    $this->sort_field, $this->skip_deleted, true, true);
            }
        }
        // have stored it in RAM
        if (isset($this->icache[$key]))
            return $this->icache[$key];
            if ($this->sort_order != $index->getParameters('ORDER')) {
                $index->revert();
            }
            return $index;
        }
        // check local cache
        if ($mcache = $this->get_mcache_engine()) {
            $a_index = $mcache->get_index($mailbox, $this->sort_field, $this->sort_order);
            $this->icache[$key] = array_keys($a_index);
            $index = $mcache->get_index($mailbox, $this->sort_field, $this->sort_order);
        }
        // fetch from IMAP server
        else {
            $this->icache[$key] = $this->message_index_direct(
            $index = $this->message_index_direct(
                $mailbox, $this->sort_field, $this->sort_order);
        }
        return $this->icache[$key];
        return $index;
    }
    /**
     * Return sorted array of message IDs (not UIDs) directly from IMAP server.
     * Doesn't use cache and ignores current search settings.
     * Return sorted list of message UIDs ignoring current search settings.
     * Doesn't uses cache by default.
     *
     * @param string $mailbox    Mailbox to get index from
     * @param string $sort_field Sort column
     * @param string $sort_order Sort order [ASC, DESC]
     * @param bool   $skip_cache Disables cache usage
     *
     * @return array Indexed array with message IDs
     * @return rcube_result_index Sorted list of message UIDs
     */
    function message_index_direct($mailbox, $sort_field = null, $sort_order = null)
    public function message_index_direct($mailbox, $sort_field = null, $sort_order = null, $skip_cache = true)
    {
        if (!$skip_cache && ($mcache = $this->get_mcache_engine())) {
            $index = $mcache->get_index($mailbox, $sort_field, $sort_order);
        }
        // use message index sort as default sorting
        if (!$sort_field) {
            if ($this->skip_deleted) {
                $a_index = $this->conn->search($mailbox, 'ALL UNDELETED');
                // I didn't found that SEARCH should return sorted IDs
                if (is_array($a_index))
                    sort($a_index);
            } else if ($max = $this->_messagecount($mailbox, 'ALL', true, false)) {
                $a_index = range(1, $max);
        else if (!$sort_field) {
            if ($this->skip_deleted && !empty($this->icache['undeleted_idx'])
                && $this->icache['undeleted_idx']->getParameters('MAILBOX') == $mailbox
            ) {
                $index = $this->icache['undeleted_idx'];
            }
            if ($a_index !== false && $sort_order == 'DESC')
                $a_index = array_reverse($a_index);
            else {
                $index = $this->conn->search($mailbox,
                    'ALL' .($this->skip_deleted ? ' UNDELETED' : ''), true);
            }
        }
        // fetch complete message index
        else if ($this->get_capability('SORT') &&
            ($a_index = $this->conn->sort($mailbox,
                $sort_field, $this->skip_deleted ? 'UNDELETED' : '')) !== false
        ) {
            if ($sort_order == 'DESC')
                $a_index = array_reverse($a_index);
        }
        else if ($a_index = $this->conn->fetchHeaderIndex(
            $mailbox, "1:*", $sort_field, $skip_deleted)) {
            if ($sort_order=="ASC")
                asort($a_index);
            else if ($sort_order=="DESC")
                arsort($a_index);
        else {
            if ($this->get_capability('SORT')) {
                $index = $this->conn->sort($mailbox, $sort_field,
                    $this->skip_deleted ? 'UNDELETED' : '', true);
            }
            $a_index = array_keys($a_index);
            if (empty($index) || $index->isError()) {
                $index = $this->conn->index($mailbox, "1:*", $sort_field,
                    $this->skip_deleted, false, true);
            }
        }
        return $a_index !== false ? $a_index : array();
        if ($sort_order != $index->getParameters('ORDER')) {
            $index->revert();
        }
        return $index;
    }
    /**
     * Return sorted array of threaded message IDs (not UIDs)
     * Return index of threaded message UIDs
     *
     * @param string $mailbox    Mailbox to get index from
     * @param string $sort_field Sort column
     * @param string $sort_order Sort order [ASC, DESC]
     * @return array Indexed array with message IDs
     *
     * @return rcube_result_thread Message UIDs
     */
    function thread_index($mailbox='', $sort_field=NULL, $sort_order=NULL)
    {
        $this->_set_sort_order($sort_field, $sort_order);
        if (!strlen($mailbox)) {
            $mailbox = $this->mailbox;
        }
        $key = "{$mailbox}:{$this->sort_field}:{$this->sort_order}:{$this->search_string}.thi";
        // we have a saved search result, get index from there
        if (!isset($this->icache[$key]) && $this->search_string
            && $this->search_threads && $mailbox == $this->mailbox) {
            // use message IDs for better performance
            $ids = array_keys_recursive($this->search_set['tree']);
            $this->icache[$key] = $this->_flatten_threads($mailbox, $this->search_set['tree'], $ids);
        if ($this->search_string && $this->search_threads && $mailbox == $this->mailbox) {
            $threads = $this->search_set;
        }
        else {
            // get all threads (default sort order)
            $threads = $this->fetch_threads($mailbox);
        }
        // have stored it in RAM
        if (isset($this->icache[$key]))
            return $this->icache[$key];
        $this->set_sort_order($sort_field, $sort_order);
        $this->sort_threads($threads);
        // get all threads (default sort order)
        list ($thread_tree) = $this->fetch_threads($mailbox);
        $this->icache[$key] = $this->_flatten_threads($mailbox, $thread_tree);
        return $this->icache[$key];
        return $threads;
    }
    /**
     * Return array of threaded messages (all, not only roots)
     * Sort threaded result, using THREAD=REFS method
     *
     * @param string $mailbox     Mailbox to get index from
     * @param array  $thread_tree Threaded messages array (see fetch_threads())
     * @param array  $ids         Message IDs if we know what we need (e.g. search result)
     *                            for better performance
     * @return array Indexed array with message IDs
     *
     * @access private
     * @param rcube_result_thread $threads  Threads result set
     */
    private function _flatten_threads($mailbox, $thread_tree, $ids=null)
    private function sort_threads($threads)
    {
        if (empty($thread_tree))
            return array();
        $msg_index = $this->sort_threads($mailbox, $thread_tree, $ids);
        if ($this->sort_order == 'DESC')
            $msg_index = array_reverse($msg_index);
        // flatten threads array
        $all_ids = array();
        foreach ($msg_index as $root) {
            $all_ids[] = $root;
            if (!empty($thread_tree[$root])) {
                foreach (array_keys_recursive($thread_tree[$root]) as $val)
                    $all_ids[] = $val;
            }
        if ($threads->isEmpty()) {
            return;
        }
        return $all_ids;
        // THREAD=ORDEREDSUBJECT: sorting by sent date of root message
        // THREAD=REFERENCES:     sorting by sent date of root message
        // THREAD=REFS:           sorting by the most recent date in each thread
        if ($this->sort_field && ($this->sort_field != 'date' || $this->get_capability('THREAD') != 'REFS')) {
            $index = $this->message_index_direct($this->mailbox, $this->sort_field, $this->sort_order, false);
            if (!$index->isEmpty()) {
                $threads->sort($index);
            }
        }
        else {
            if ($this->sort_order != $threads->getParameters('ORDER')) {
                $threads->revert();
            }
        }
    }
@@ -1634,13 +1399,12 @@
     * @param  string  $str        Search criteria
     * @param  string  $charset    Search charset
     * @param  string  $sort_field Header field to sort by
     * @return array   search results as list of message IDs
     * @access public
     */
    function search($mailbox='', $str=NULL, $charset=NULL, $sort_field=NULL)
    function search($mailbox='', $str='ALL', $charset=NULL, $sort_field=NULL)
    {
        if (!$str)
            return false;
            $str = 'ALL';
        if (!strlen($mailbox)) {
            $mailbox = $this->mailbox;
@@ -1648,10 +1412,8 @@
        $results = $this->_search_index($mailbox, $str, $charset, $sort_field);
        $this->set_search_set($str, $results, $charset, $sort_field, (bool)$this->threading,
        $this->set_search_set($str, $results, $charset, $sort_field,
            $this->threading || $this->search_sorted ? true : false);
        return $results;
    }
@@ -1663,7 +1425,7 @@
     * @param string $charset    Charset
     * @param string $sort_field Sorting field
     *
     * @return array   search results as list of message ids
     * @return rcube_result_index|rcube_result_thread  Search results (UIDs)
     * @see rcube_imap::search()
     */
    private function _search_index($mailbox, $criteria='ALL', $charset=NULL, $sort_field=NULL)
@@ -1674,63 +1436,44 @@
            $criteria = 'UNDELETED '.$criteria;
        if ($this->threading) {
            $a_messages = $this->conn->thread($mailbox, $this->threading, $criteria, $charset);
            $threads = $this->conn->thread($mailbox, $this->threading, $criteria, true, $charset);
            // Error, try with US-ASCII (RFC5256: SORT/THREAD must support US-ASCII and UTF-8,
            // but I've seen that Courier doesn't support UTF-8)
            if ($a_messages === false && $charset && $charset != 'US-ASCII')
                $a_messages = $this->conn->thread($mailbox, $this->threading,
                    $this->convert_criteria($criteria, $charset), 'US-ASCII');
            if ($threads->isError() && $charset && $charset != 'US-ASCII')
                $threads = $this->conn->thread($mailbox, $this->threading,
                    $this->convert_criteria($criteria, $charset), true, 'US-ASCII');
            if ($a_messages !== false) {
                list ($thread_tree, $msg_depth, $has_children) = $a_messages;
                $a_messages = array(
                    'tree' => $thread_tree,
                    'depth'=> $msg_depth,
                    'children' => $has_children
                );
            }
            return $a_messages;
            return $threads;
        }
        if ($sort_field && $this->get_capability('SORT')) {
            $charset = $charset ? $charset : $this->default_charset;
            $a_messages = $this->conn->sort($mailbox, $sort_field, $criteria, false, $charset);
            $charset  = $charset ? $charset : $this->default_charset;
            $messages = $this->conn->sort($mailbox, $sort_field, $criteria, true, $charset);
            // Error, try with US-ASCII (RFC5256: SORT/THREAD must support US-ASCII and UTF-8,
            // but I've seen Courier with disabled UTF-8 support)
            if ($a_messages === false && $charset && $charset != 'US-ASCII')
                $a_messages = $this->conn->sort($mailbox, $sort_field,
                    $this->convert_criteria($criteria, $charset), false, 'US-ASCII');
            if ($messages->isError() && $charset && $charset != 'US-ASCII')
                $messages = $this->conn->sort($mailbox, $sort_field,
                    $this->convert_criteria($criteria, $charset), true, 'US-ASCII');
            if ($a_messages !== false) {
            if (!$messages->isError()) {
                $this->search_sorted = true;
                return $a_messages;
                return $messages;
            }
        }
        if ($orig_criteria == 'ALL') {
            $max = $this->_messagecount($mailbox, 'ALL', true, false);
            $a_messages = $max ? range(1, $max) : array();
        }
        else {
            $a_messages = $this->conn->search($mailbox,
                ($charset ? "CHARSET $charset " : '') . $criteria);
        $messages = $this->conn->search($mailbox,
            ($charset ? "CHARSET $charset " : '') . $criteria, true);
            // Error, try with US-ASCII (some servers may support only US-ASCII)
            if ($a_messages === false && $charset && $charset != 'US-ASCII')
                $a_messages = $this->conn->search($mailbox,
                    'CHARSET US-ASCII ' . $this->convert_criteria($criteria, $charset));
            // I didn't found that SEARCH should return sorted IDs
            if (is_array($a_messages) && !$this->sort_field)
                sort($a_messages);
        }
        // Error, try with US-ASCII (some servers may support only US-ASCII)
        if ($messages->isError() && $charset && $charset != 'US-ASCII')
            $messages = $this->conn->search($mailbox,
                'CHARSET US-ASCII ' . $this->convert_criteria($criteria, $charset), true);
        $this->search_sorted = false;
        return $a_messages;
        return $messages;
    }
@@ -1742,18 +1485,20 @@
     * @param  string  $str     Search string
     * @param  boolean $ret_uid True if UIDs should be returned
     *
     * @return array   Search results as list of message IDs or UIDs
     * @return rcube_result_index  Search result (UIDs)
     */
    function search_once($mailbox='', $str=NULL, $ret_uid=false)
    function search_once($mailbox='', $str='ALL')
    {
        if (!$str)
            return false;
            return 'ALL';
        if (!strlen($mailbox)) {
            $mailbox = $this->mailbox;
        }
        return $this->conn->search($mailbox, $str, $ret_uid);
        $index = $this->conn->search($mailbox, $str, true);
        return $index;
    }
@@ -1791,139 +1536,33 @@
    /**
     * Sort thread
     *
     * @param string $mailbox     Mailbox name
     * @param  array $thread_tree Unsorted thread tree (rcube_imap_generic::thread() result)
     * @param  array $ids         Message IDs if we know what we need (e.g. search result)
     *
     * @return array Sorted roots IDs
     */
    function sort_threads($mailbox, $thread_tree, $ids = null)
    {
        // THREAD=ORDEREDSUBJECT: sorting by sent date of root message
        // THREAD=REFERENCES:     sorting by sent date of root message
        // THREAD=REFS:           sorting by the most recent date in each thread
        // default sorting
        if (!$this->sort_field || ($this->sort_field == 'date' && $this->threading == 'REFS')) {
            return array_keys((array)$thread_tree);
        }
        // here we'll implement REFS sorting
        else {
            if ($mcache = $this->get_mcache_engine()) {
                $a_index = $mcache->get_index($mailbox, $this->sort_field, 'ASC');
                if (is_array($a_index)) {
                    $a_index = array_keys($a_index);
                    // now we must remove IDs that doesn't exist in $ids
                    if (!empty($ids))
                        $a_index = array_intersect($a_index, $ids);
                }
            }
            // use SORT command
            else if ($this->get_capability('SORT') &&
                ($a_index = $this->conn->sort($mailbox, $this->sort_field,
                    !empty($ids) ? $ids : ($this->skip_deleted ? 'UNDELETED' : ''))) !== false
            ) {
                // do nothing
            }
            else {
                // fetch specified headers for all messages and sort them
                $a_index = $this->conn->fetchHeaderIndex($mailbox, !empty($ids) ? $ids : "1:*",
                    $this->sort_field, $this->skip_deleted);
                // return unsorted tree if we've got no index data
                if (!empty($a_index)) {
                    asort($a_index); // ASC
                    $a_index = array_values($a_index);
                }
            }
            if (empty($a_index))
                return array_keys((array)$thread_tree);
            return $this->_sort_thread_refs($thread_tree, $a_index);
        }
    }
    /**
     * THREAD=REFS sorting implementation
     *
     * @param  array $tree   Thread tree array (message identifiers as keys)
     * @param  array $index  Array of sorted message identifiers
     *
     * @return array   Array of sorted roots messages
     */
    private function _sort_thread_refs($tree, $index)
    {
        if (empty($tree))
            return array();
        $index = array_combine(array_values($index), $index);
        // assign roots
        foreach ($tree as $idx => $val) {
            $index[$idx] = $idx;
            if (!empty($val)) {
                $idx_arr = array_keys_recursive($tree[$idx]);
                foreach ($idx_arr as $subidx)
                    $index[$subidx] = $idx;
            }
        }
        $index = array_values($index);
        // create sorted array of roots
        $msg_index = array();
        if ($this->sort_order != 'DESC') {
            foreach ($index as $idx)
                if (!isset($msg_index[$idx]))
                    $msg_index[$idx] = $idx;
            $msg_index = array_values($msg_index);
        }
        else {
            for ($x=count($index)-1; $x>=0; $x--)
                if (!isset($msg_index[$index[$x]]))
                    $msg_index[$index[$x]] = $index[$x];
            $msg_index = array_reverse($msg_index);
        }
        return $msg_index;
    }
    /**
     * Refresh saved search set
     *
     * @return array Current search set
     */
    function refresh_search()
    {
        if (!empty($this->search_string))
            $this->search_set = $this->search('', $this->search_string, $this->search_charset,
                $this->search_sort_field, $this->search_threads, $this->search_sorted);
        if (!empty($this->search_string)) {
            $this->search('', $this->search_string, $this->search_charset, $this->search_sort_field);
        }
        return $this->get_search_set();
    }
    /**
     * Check if the given message ID is part of the current search set
     * Check if the given message UID is part of the current search set
     *
     * @param string $msgid Message id
     * @param string $msgid Message UID
     *
     * @return boolean True on match or if no search request is stored
     */
    function in_searchset($msgid)
    function in_searchset($uid)
    {
        if (!empty($this->search_string)) {
            if ($this->search_threads)
                return isset($this->search_set['depth']["$msgid"]);
            else
                return in_array("$msgid", (array)$this->search_set, true);
            return $this->search_set->exists($uid);
        }
        else
            return true;
        return true;
    }
@@ -1981,7 +1620,7 @@
        // message doesn't exist?
        if (empty($headers))
            return null;
            return null;
        // structure might be cached
        if (!empty($headers->structure))
@@ -2656,14 +2295,8 @@
                // threads are too complicated to just remove messages from set
                if ($this->search_threads || $all_mode)
                    $this->refresh_search();
                else {
                    $a_uids = explode(',', $uids);
                    foreach ($a_uids as $uid)
                        $a_mids[] = $this->uid2id($uid, $from_mbox);
                    $this->search_set = array_diff($this->search_set, $a_mids);
                }
                unset($a_mids);
                unset($a_uids);
                else
                    $this->search_set->filter(explode(',', $uids));
            }
            // remove cached messages
@@ -2756,14 +2389,8 @@
                // threads are too complicated to just remove messages from set
                if ($this->search_threads || $all_mode)
                    $this->refresh_search();
                else {
                    $a_uids = explode(',', $uids);
                    foreach ($a_uids as $uid)
                        $a_mids[] = $this->uid2id($uid, $mailbox);
                    $this->search_set = array_diff($this->search_set, $a_mids);
                    unset($a_uids);
                    unset($a_mids);
                }
                else
                    $this->search_set->filter(explode(',', $uids));
            }
            // remove cached messages
@@ -2882,19 +2509,8 @@
                $all = true;
            }
            // get UIDs from current search set
            // @TODO: skip fetchUIDs() and work with IDs instead of UIDs (?)
            else {
                if ($this->search_threads)
                    $uids = $this->conn->fetchUIDs($mailbox, array_keys($this->search_set['depth']));
                else
                    $uids = $this->conn->fetchUIDs($mailbox, $this->search_set);
                // save ID-to-UID mapping in local cache
                if (is_array($uids))
                    foreach ($uids as $id => $uid)
                        $this->uid_id_map[$mailbox][$uid] = $id;
                $uids = join(',', $uids);
                $uids = join(',', $this->search_set->get());
            }
        }
        else {
@@ -2907,43 +2523,6 @@
        return array($uids, (bool) $all);
    }
    /**
     * Translate UID to message ID
     *
     * @param int    $uid     Message UID
     * @param string $mailbox Mailbox name
     *
     * @return int   Message ID
     */
    function get_id($uid, $mailbox=null)
    {
        if (!strlen($mailbox)) {
            $mailbox = $this->mailbox;
        }
        return $this->uid2id($uid, $mailbox);
    }
    /**
     * Translate message number to UID
     *
     * @param int    $id      Message ID
     * @param string $mailbox Mailbox name
     *
     * @return int   Message UID
     */
    function get_uid($id, $mailbox=null)
    {
        if (!strlen($mailbox)) {
            $mailbox = $this->mailbox;
        }
        return $this->id2uid($id, $mailbox);
    }
    /* --------------------------------
@@ -3592,9 +3171,10 @@
        $data = $this->conn->data;
        // add (E)SEARCH result for ALL UNDELETED query
        if (!empty($this->icache['undeleted_idx']) && $this->icache['undeleted_idx'][0] == $mailbox) {
            $data['ALL_UNDELETED']   = $this->icache['undeleted_idx'][1];
            $data['COUNT_UNDELETED'] = $this->icache['undeleted_idx'][2];
        if (!empty($this->icache['undeleted_idx'])
            && $this->icache['undeleted_idx']->getParameters('MAILBOX') == $mailbox
        ) {
            $data['UNDELETED'] = $this->icache['undeleted_idx'];
        }
        return $data;
@@ -4268,23 +3848,6 @@
    }
    /**
     * Convert body charset to RCMAIL_CHARSET according to the ctype_parameters
     *
     * @param string $body        Part body to decode
     * @param string $ctype_param Charset to convert from
     * @return string Content converted to internal charset
     */
    function charset_decode($body, $ctype_param)
    {
        if (is_array($ctype_param) && !empty($ctype_param['charset']))
            return rcube_charset_convert($body, $ctype_param['charset']);
        // defaults to what is specified in the class header
        return rcube_charset_convert($body,  $this->default_charset);
    }
    /* --------------------------------
     *         private methods
     * --------------------------------*/
@@ -4296,7 +3859,7 @@
     * @param string $sort_order Sort order
     * @access private
     */
    private function _set_sort_order($sort_field, $sort_order)
    private function set_sort_order($sort_field, $sort_order)
    {
        if ($sort_field != null)
            $this->sort_field = asciiwords($sort_field);
@@ -4366,46 +3929,14 @@
    /**
     * Finds message sequence ID for specified UID
     *
     * @param int    $uid      Message UID
     * @param string $mailbox  Mailbox name
     * @param bool   $force    True to skip cache
     *
     * @return int Message (sequence) ID
     */
    function uid2id($uid, $mailbox = null, $force = false)
    {
        if (!strlen($mailbox)) {
            $mailbox = $this->mailbox;
        }
        if (!empty($this->uid_id_map[$mailbox][$uid])) {
            return $this->uid_id_map[$mailbox][$uid];
        }
        if (!$force && ($mcache = $this->get_mcache_engine()))
            $id = $mcache->uid2id($mailbox, $uid);
        if (empty($id))
            $id = $this->conn->UID2ID($mailbox, $uid);
        $this->uid_id_map[$mailbox][$uid] = $id;
        return $id;
    }
    /**
     * Find UID of the specified message sequence ID
     *
     * @param int    $id       Message (sequence) ID
     * @param string $mailbox  Mailbox name
     * @param bool   $force    True to skip cache
     *
     * @return int Message UID
     */
    function id2uid($id, $mailbox = null, $force = false)
    function id2uid($id, $mailbox = null)
    {
        if (!strlen($mailbox)) {
            $mailbox = $this->mailbox;
@@ -4415,11 +3946,7 @@
            return $uid;
        }
        if (!$force && ($mcache = $this->get_mcache_engine()))
            $uid = $mcache->id2uid($mailbox, $id);
        if (empty($uid))
            $uid = $this->conn->ID2UID($mailbox, $id);
        $uid = $this->conn->ID2UID($mailbox, $id);
        $this->uid_id_map[$mailbox][$uid] = $id;
@@ -4705,24 +4232,19 @@
 */
class rcube_header_sorter
{
    private $seqs = array();
    private $uids = array();
    /**
     * Set the predetermined sort order.
     *
     * @param array $index  Numerically indexed array of IMAP ID or UIDs
     * @param bool  $is_uid Set to true if $index contains UIDs
     * @param array $index  Numerically indexed array of IMAP UIDs
     */
    function set_index($index, $is_uid = false)
    function set_index($index)
    {
        $index = array_flip($index);
        if ($is_uid)
            $this->uids = $index;
        else
            $this->seqs = $index;
        $this->uids = $index;
    }
    /**
@@ -4732,30 +4254,7 @@
     */
    function sort_headers(&$headers)
    {
        if (!empty($this->uids))
            uksort($headers, array($this, "compare_uids"));
        else
            uasort($headers, array($this, "compare_seqnums"));
    }
    /**
     * Sort method called by uasort()
     *
     * @param rcube_mail_header $a
     * @param rcube_mail_header $b
     */
    function compare_seqnums($a, $b)
    {
        // First get the sequence number from the header object (the 'id' field).
        $seqa = $a->id;
        $seqb = $b->id;
        // then find each sequence number in my ordered list
        $posa = isset($this->seqs[$seqa]) ? intval($this->seqs[$seqa]) : -1;
        $posb = isset($this->seqs[$seqb]) ? intval($this->seqs[$seqb]) : -1;
        // return the relative position as the comparison value
        return $posa - $posb;
        uksort($headers, array($this, "compare_uids"));
    }
    /**
program/include/rcube_imap_cache.php
@@ -108,7 +108,7 @@
    /**
     * Return (sorted) messages index.
     * Return (sorted) messages index (UIDs).
     * If index doesn't exist or is invalid, will be updated.
     *
     * @param string  $mailbox     Folder name
@@ -122,22 +122,22 @@
    {
        if (empty($this->icache[$mailbox]))
            $this->icache[$mailbox] = array();
console('cache::get_index');
        $sort_order = strtoupper($sort_order) == 'ASC' ? 'ASC' : 'DESC';
        // 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('result', $this->icache[$mailbox]['index'])) {
            if (!array_key_exists('object', $this->icache[$mailbox]['index'])) {
                $index = $this->icache[$mailbox]['index'];
            }
            // We've got a valid index
            else if ($sort_field == 'ANY' || $this->icache[$mailbox]['index']['sort_field'] == $sort_field
            ) {
                if ($this->icache[$mailbox]['index']['sort_order'] == $sort_order)
                    return $this->icache[$mailbox]['index']['result'];
                else
                    return array_reverse($this->icache[$mailbox]['index']['result'], true);
            else if ($sort_field == 'ANY' || $this->icache[$mailbox]['index']['sort_field'] == $sort_field) {
                $result = $this->icache[$mailbox]['index']['object'];
                if ($result->getParameters('ORDER') != $sort_order) {
                    $result->revert();
                }
                return $result;
            }
        }
@@ -173,13 +173,13 @@
            else {
                $is_valid = $this->validate($mailbox, $index, $exists);
            }
console("valid:".$is_valid);
            if ($is_valid) {
                // build index, assign sequence IDs to unique IDs
                $data = array_combine($index['seq'], $index['uid']);
                $data = $index['object'];
                // revert the order if needed
                if ($index['sort_order'] != $sort_order)
                    $data = array_reverse($data, true);
                if ($data->getParameters('ORDER') != $sort_order) {
                    $data->revert();
                }
            }
        }
        else {
@@ -201,14 +201,12 @@
            $data      = $this->get_index_data($mailbox, $sort_field, $sort_order, $mbox_data);
            // insert/update
            $this->add_index_row($mailbox, $sort_field, $sort_order, $data, $mbox_data,
                $exists, $index['modseq']);
            $this->add_index_row($mailbox, $sort_field, $data, $mbox_data, $exists, $index['modseq']);
        }
        $this->icache[$mailbox]['index'] = array(
            'result'     => $data,
            'object'     => $data,
            'sort_field' => $sort_field,
            'sort_order' => $sort_order,
            'modseq'     => !empty($index['modseq']) ? $index['modseq'] : $mbox_data['HIGHESTMODSEQ']
        );
@@ -233,11 +231,7 @@
        // Seek in internal cache
        if (array_key_exists('thread', $this->icache[$mailbox])) {
            return array(
                $this->icache[$mailbox]['thread']['tree'],
                $this->icache[$mailbox]['thread']['depth'],
                $this->icache[$mailbox]['thread']['children'],
            );
            return $this->icache[$mailbox]['thread']['object'];
        }
        // Get thread from DB (if DB wasn't already queried)
@@ -249,8 +243,6 @@
            // get_thread() is called more than once or after clear()
            $this->icache[$mailbox]['thread_queried'] = true;
        }
        $data = null;
        // Entry exist, check cache status
        if (!empty($index)) {
@@ -269,22 +261,21 @@
            if ($mbox_data['EXISTS']) {
                // get all threads (default sort order)
                list ($thread_tree, $msg_depth, $has_children) = $this->imap->fetch_threads($mailbox, true);
                $threads = $this->imap->fetch_threads($mailbox, true);
            }
            else {
                $threads = new rcube_imap_result($mailbox, '');
            }
            $index = array(
                'tree'     => !empty($thread_tree) ? $thread_tree : array(),
                'depth'    => !empty($msg_depth) ? $msg_depth : array(),
                'children' => !empty($has_children) ? $has_children : array(),
            );
            $index['object'] = $threads;
            // insert/update
            $this->add_thread_row($mailbox, $index, $mbox_data, $exists);
            $this->add_thread_row($mailbox, $threads, $mbox_data, $exists);
        }
        $this->icache[$mailbox]['thread'] = $index;
        return array($index['tree'], $index['depth'], $index['children']);
        return $index['object'];
    }
@@ -292,27 +283,14 @@
     * Returns list of messages (headers). See rcube_imap::fetch_headers().
     *
     * @param string $mailbox  Folder name
     * @param array  $msgs     Message sequence numbers
     * @param bool   $is_uid   True if $msgs contains message UIDs
     * @param array  $msgs     Message UIDs
     *
     * @return array The list of messages (rcube_mail_header) indexed by UID
     */
    function get_messages($mailbox, $msgs = array(), $is_uid = true)
    function get_messages($mailbox, $msgs = array())
    {
        if (empty($msgs)) {
            return array();
        }
        // @TODO: it would be nice if we could work with UIDs only
        // then index would be not needed. For now we need it to
        // map id to uid here and to update message id for cached message
        // Convert IDs to UIDs
        $index = $this->get_index($mailbox, 'ANY');
        if (!$is_uid) {
            foreach ($msgs as $idx => $msgid)
                if ($uid = $index[$msgid])
                    $msgs[$idx] = $uid;
        }
        // Fetch messages from cache
@@ -334,10 +312,6 @@
            // save memory, we don't need message body here (?)
            $result[$uid]->body = null;
            // update message ID according to index data
            if (!empty($index) && ($id = array_search($uid, $index)))
                $result[$uid]->id = $id;
            if (!empty($result[$uid])) {
                unset($msgs[$uid]);
            }
@@ -345,7 +319,7 @@
        // Fetch not found messages from IMAP server
        if (!empty($msgs)) {
            $messages = $this->imap->fetch_headers($mailbox, array_keys($msgs), true, true);
            $messages = $this->imap->fetch_headers($mailbox, array_keys($msgs), false, true);
            // Insert to DB and add to result list
            if (!empty($messages)) {
@@ -391,11 +365,6 @@
        if ($sql_arr = $this->db->fetch_assoc($sql_result)) {
            $message = $this->build_message($sql_arr);
            $found   = true;
            // update message ID according to index data
            $index = $this->get_index($mailbox, 'ANY');
            if (!empty($index) && ($id = array_search($uid, $index)))
                $message->id = $id;
        }
        // Get the message from IMAP server
@@ -617,45 +586,11 @@
    /**
     * @param string $mailbox Folder name
     * @param int    $id      Message (sequence) ID
     *
     * @return int Message UID
     */
    function id2uid($mailbox, $id)
    {
        if (!empty($this->icache['pending_index_update']))
            return null;
        // get index if it exists
        $index = $this->get_index($mailbox, 'ANY', null, true);
        return $index[$id];
    }
    /**
     * @param string $mailbox Folder name
     * @param int    $uid     Message UID
     *
     * @return int Message (sequence) ID
     */
    function uid2id($mailbox, $uid)
    {
        if (!empty($this->icache['pending_index_update']))
            return null;
        // get index if it exists
        $index = $this->get_index($mailbox, 'ANY', null, true);
        return array_search($uid, (array)$index);
    }
    /**
     * Fetches index data from database
     */
    private function get_index_row($mailbox)
    {
console('cache::get_index_row');
        // Get index from DB
        $sql_result = $this->db->query(
            "SELECT data, valid"
@@ -665,18 +600,22 @@
            $this->userid, $mailbox);
        if ($sql_arr = $this->db->fetch_assoc($sql_result)) {
            $data = explode('@', $sql_arr['data']);
            $data  = explode('@', $sql_arr['data']);
            $index = @unserialize($data[0]);
            unset($data[0]);
            if (empty($index)) {
                $index = new rcube_result_index($mailbox);
            }
            return array(
                'valid'      => $sql_arr['valid'],
                'seq'        => explode(',', $data[0]),
                'uid'        => explode(',', $data[1]),
                'sort_field' => $data[2],
                'sort_order' => $data[3],
                'deleted'    => $data[4],
                'validity'   => $data[5],
                'uidnext'    => $data[6],
                'modseq'     => $data[7],
                'object'     => $index,
                'sort_field' => $data[1],
                'deleted'    => $data[2],
                'validity'   => $data[3],
                'uidnext'    => $data[4],
                'modseq'     => $data[5],
            );
        }
@@ -698,20 +637,16 @@
            $this->userid, $mailbox);
        if ($sql_arr = $this->db->fetch_assoc($sql_result)) {
            $data = explode('@', $sql_arr['data']);
            $data   = explode('@', $sql_arr['data']);
            $thread = @unserialize($data[0]);
            unset($data[0]);
            // Uncompress data, see add_thread_row()
  //          $data[0] = str_replace(array('*', '^', '#'), array(';a:0:{}', 'i:', ';a:1:'), $data[0]);
            $data[0] = unserialize($data[0]);
            // build 'depth' and 'children' arrays
            $depth = $children = array();
            $this->build_thread_data($data[0], $depth, $children);
            if (empty($thread)) {
                $thread = new rcube_imap_result($mailbox);
            }
            return array(
                'tree'     => $data[0],
                'depth'    => $depth,
                'children' => $children,
                'object'   => $thread,
                'deleted'  => $data[1],
                'validity' => $data[2],
                'uidnext'  => $data[3],
@@ -725,14 +660,12 @@
    /**
     * Saves index data into database
     */
    private function add_index_row($mailbox, $sort_field, $sort_order,
        $data = array(), $mbox_data = array(), $exists = false, $modseq = null)
    private function add_index_row($mailbox, $sort_field,
        $data, $mbox_data = array(), $exists = false, $modseq = null)
    {
        $data = array(
            implode(',', array_keys($data)),
            implode(',', array_values($data)),
            serialize($data),
            $sort_field,
            $sort_order,
            (int) $this->skip_deleted,
            (int) $mbox_data['UIDVALIDITY'],
            (int) $mbox_data['UIDNEXT'],
@@ -759,14 +692,10 @@
    /**
     * Saves thread data into database
     */
    private function add_thread_row($mailbox, $data = array(), $mbox_data = array(), $exists = false)
    private function add_thread_row($mailbox, $data, $mbox_data = array(), $exists = false)
    {
        $tree = serialize($data['tree']);
        // This significantly reduces data length
//        $tree = str_replace(array(';a:0:{}', 'i:', ';a:1:'), array('*', '^', '#'), $tree);
        $data = array(
            $tree,
            serialize($data),
            (int) $this->skip_deleted,
            (int) $mbox_data['UIDVALIDITY'],
            (int) $mbox_data['UIDNEXT'],
@@ -794,7 +723,8 @@
     */
    private function validate($mailbox, $index, &$exists = true)
    {
        $is_thread = isset($index['tree']);
        $object    = $index['object'];
        $is_thread = is_a($object, 'rcube_result_thread');
        // Get mailbox data (UIDVALIDITY, counters, etc.) for status check
        $mbox_data = $this->imap->mailbox_data($mailbox);
@@ -814,21 +744,21 @@
        // Folder is empty but cache isn't
        if (empty($mbox_data['EXISTS'])) {
            if (!empty($index['seq']) || !empty($index['tree'])) {
            if (!$object->isEmpty()) {
                $this->clear($mailbox);
                $exists = false;
                return false;
            }
        }
        // Folder is not empty but cache is
        else if (empty($index['seq']) && empty($index['tree'])) {
        else if ($object->isEmpty()) {
            unset($this->icache[$mailbox][$is_thread ? 'thread' : 'index']);
            return false;
        }
        // Validation flag
        if (!$is_thread && empty($index['valid'])) {
            unset($this->icache[$mailbox][$is_thread ? 'thread' : 'index']);
            unset($this->icache[$mailbox]['index']);
            return false;
        }
@@ -853,7 +783,7 @@
        // @TODO: find better validity check for threaded index
        if ($is_thread) {
            // check messages number...
            if (!$this->skip_deleted && $mbox_data['EXISTS'] != @max(array_keys($index['depth']))) {
            if (!$this->skip_deleted && $mbox_data['EXISTS'] != $object->countMessages()) {
                return false;
            }
            return true;
@@ -862,14 +792,15 @@
        // The rest of checks, more expensive
        if (!empty($this->skip_deleted)) {
            // compare counts if available
            if ($mbox_data['COUNT_UNDELETED'] != null
                && $mbox_data['COUNT_UNDELETED'] != count($index['uid'])) {
            if (!empty($mbox_data['UNDELETED'])
                && $mbox_data['UNDELETED']->count() != $object->count()
            ) {
                return false;
            }
            // compare UID sets
            if ($mbox_data['ALL_UNDELETED'] != null) {
                $uids_new = rcube_imap_generic::uncompressMessageSet($mbox_data['ALL_UNDELETED']);
                $uids_old = $index['uid'];
            if (!empty($mbox_data['UNDELETED'])) {
                $uids_new = $mbox_data['UNDELETED']->get();
                $uids_old = $object->get();
                if (count($uids_new) != count($uids_old)) {
                    return false;
@@ -884,20 +815,20 @@
            else {
                // get all undeleted messages excluding cached UIDs
                $ids = $this->imap->search_once($mailbox, 'ALL UNDELETED NOT UID '.
                    rcube_imap_generic::compressMessageSet($index['uid']));
                    rcube_imap_generic::compressMessageSet($object->get()));
                if (!empty($ids)) {
                if (!$ids->isEmpty()) {
                    return false;
                }
            }
        }
        else {
            // check messages number...
            if ($mbox_data['EXISTS'] != max($index['seq'])) {
            if ($mbox_data['EXISTS'] != $object->count()) {
                return false;
            }
            // ... and max UID
            if (max($index['uid']) != $this->imap->id2uid($mbox_data['EXISTS'], $mailbox, true)) {
            if ($object->max() != $this->imap->id2uid($mbox_data['EXISTS'], $mailbox, true)) {
                return false;
            }
        }
@@ -1060,7 +991,7 @@
        }
        $sort_field = $index['sort_field'];
        $sort_order = $index['sort_order'];
        $sort_order = $index['object']->getParameters('ORDER');
        $exists     = true;
        // Validate index
@@ -1069,14 +1000,14 @@
            $data = $this->get_index_data($mailbox, $sort_field, $sort_order, $mbox_data);
        }
        else {
            $data = array_combine($index['seq'], $index['uid']);
            $data = $index['object'];
        }
        // update index and/or HIGHESTMODSEQ value
        $this->add_index_row($mailbox, $sort_field, $sort_order, $data, $mbox_data, $exists);
        $this->add_index_row($mailbox, $sort_field, $data, $mbox_data, $exists);
        // update internal cache for get_index()
        $this->icache[$mailbox]['index']['result'] = $data;
        $this->icache[$mailbox]['index']['object'] = $data;
    }
@@ -1099,20 +1030,6 @@
        }
        return $message;
    }
    /**
     * Creates 'depth' and 'children' arrays from stored thread 'tree' data.
     */
    private function build_thread_data($data, &$depth, &$children, $level = 0)
    {
        foreach ((array)$data as $key => $val) {
            $children[$key] = !empty($val);
            $depth[$key] = $level;
            if (!empty($val))
                $this->build_thread_data($val, $depth, $children, $level + 1);
        }
    }
@@ -1170,43 +1087,18 @@
     */
    private function get_index_data($mailbox, $sort_field, $sort_order, $mbox_data = array())
    {
        $data = array();
        if (empty($mbox_data)) {
            $mbox_data = $this->imap->mailbox_data($mailbox);
        }
        // Prevent infinite loop.
        // It happens when rcube_imap::message_index_direct() is called.
        // There id2uid() is called which will again call get_index() and so on.
        if (!$sort_field && !$this->skip_deleted)
            $this->icache['pending_index_update'] = true;
        if ($mbox_data['EXISTS']) {
            // fetch sorted sequence numbers
            $data_seq = $this->imap->message_index_direct($mailbox, $sort_field, $sort_order);
            // fetch UIDs
            if (!empty($data_seq)) {
                // Seek in internal cache
                if (array_key_exists('index', (array)$this->icache[$mailbox])
                    && array_key_exists('result', (array)$this->icache[$mailbox]['index'])
                )
                    $data_uid = $this->icache[$mailbox]['index']['result'];
                else
                    $data_uid = $this->imap->conn->fetchUIDs($mailbox, $data_seq);
                // build index
                if (!empty($data_uid)) {
                    foreach ($data_seq as $seq)
                        if ($uid = $data_uid[$seq])
                            $data[$seq] = $uid;
                }
            }
            $index = $this->imap->message_index_direct($mailbox, $sort_field, $sort_order);
        }
        else {
            $index = new rcube_result_index($mailbox, '* SORT');
        }
        // Reset internal flags
        $this->icache['pending_index_update'] = false;
        return $data;
        return $index;
    }
}
program/include/rcube_imap_generic.php
@@ -26,7 +26,6 @@
*/
/**
 * Struct representing an e-mail message header
 *
@@ -1218,8 +1217,8 @@
        // Invoke SEARCH as a fallback
        $index = $this->search($mailbox, 'ALL UNSEEN', false, array('COUNT'));
        if (is_array($index)) {
            return (int) $index['COUNT'];
        if (!$index->isError()) {
            return $index->count();
        }
        return false;
@@ -1293,8 +1292,21 @@
        return false;
    }
    function sort($mailbox, $field, $add='', $is_uid=FALSE, $encoding = 'US-ASCII')
    /**
     * Executes SORT command
     *
     * @param string $mailbox    Mailbox name
     * @param string $field      Field to sort by (ARRIVAL, CC, DATE, FROM, SIZE, SUBJECT, TO)
     * @param string $add        Searching criteria
     * @param bool   $return_uid Enables UID SORT usage
     * @param string $encoding   Character set
     *
     * @return rcube_result_index Response data
     */
    function sort($mailbox, $field, $add='', $return_uid=false, $encoding = 'US-ASCII')
    {
        require_once dirname(__FILE__) . '/rcube_result_index.php';
        $field = strtoupper($field);
        if ($field == 'INTERNALDATE') {
            $field = 'ARRIVAL';
@@ -1304,33 +1316,61 @@
            'FROM' => 1, 'SIZE' => 1, 'SUBJECT' => 1, 'TO' => 1);
        if (!$fields[$field]) {
            return false;
            return new rcube_result_index($mailbox);
        }
        if (!$this->select($mailbox)) {
            return false;
            return new rcube_result_index($mailbox);
        }
        // message IDs
        if (!empty($add))
            $add = $this->compressMessageSet($add);
        list($code, $response) = $this->execute($is_uid ? 'UID SORT' : 'SORT',
        list($code, $response) = $this->execute($return_uid ? 'UID SORT' : 'SORT',
            array("($field)", $encoding, 'ALL' . (!empty($add) ? ' '.$add : '')));
        if ($code == self::ERROR_OK) {
            // remove prefix and unilateral untagged server responses
            $response = substr($response, stripos($response, '* SORT') + 7);
            if ($pos = strpos($response, '*')) {
                $response = substr($response, 0, $pos);
            }
            return preg_split('/[\s\r\n]+/', $response, -1, PREG_SPLIT_NO_EMPTY);
        if ($code != self::ERROR_OK) {
            $response = null;
        }
        return false;
        return new rcube_result_index($mailbox, $response);
    }
    function fetchHeaderIndex($mailbox, $message_set, $index_field='', $skip_deleted=true, $uidfetch=false)
    /**
     * Simulates SORT command by using FETCH and sorting.
     *
     * @param string       $mailbox      Mailbox name
     * @param string|array $message_set  Searching criteria (list of messages to return)
     * @param string       $index_field  Field to sort by (ARRIVAL, CC, DATE, FROM, SIZE, SUBJECT, TO)
     * @param bool         $skip_deleted Makes that DELETED messages will be skipped
     * @param bool         $uidfetch     Enables UID FETCH usage
     * @param bool         $return_uid   Enables returning UIDs instead of IDs
     *
     * @return rcube_result_index Response data
     */
    function index($mailbox, $message_set, $index_field='', $skip_deleted=true,
        $uidfetch=false, $return_uid=false)
    {
        require_once dirname(__FILE__) . '/rcube_result_index.php';
        $msg_index = $this->fetchHeaderIndex($mailbox, $message_set,
            $index_field, $skip_deleted, $uidfetch, $return_uid);
        if (!empty($msg_index)) {
            asort($msg_index); // ASC
            $msg_index = array_keys($msg_index);
            $msg_index = '* SEARCH ' . implode(' ', $msg_index);
        }
        else {
            $msg_index = is_array($msg_index) ? '* SEARCH' : null;
        }
        return new rcube_result_index($mailbox, $msg_index);
    }
    function fetchHeaderIndex($mailbox, $message_set, $index_field='', $skip_deleted=true,
        $uidfetch=false, $return_uid=false)
    {
        if (is_array($message_set)) {
            if (!($message_set = $this->compressMessageSet($message_set)))
@@ -1370,25 +1410,32 @@
        }
        // build FETCH command string
        $key     = $this->nextTag();
        $cmd     = $uidfetch ? 'UID FETCH' : 'FETCH';
        $deleted = $skip_deleted ? ' FLAGS' : '';
        $key    = $this->nextTag();
        $cmd    = $uidfetch ? 'UID FETCH' : 'FETCH';
        $fields = array();
        if ($mode == 1 && $index_field == 'DATE')
            $request = " $cmd $message_set (INTERNALDATE BODY.PEEK[HEADER.FIELDS (DATE)]$deleted)";
        else if ($mode == 1)
            $request = " $cmd $message_set (BODY.PEEK[HEADER.FIELDS ($index_field)]$deleted)";
        if ($return_uid)
            $fields[] = 'UID';
        if ($skip_deleted)
            $fields[] = 'FLAGS';
        if ($mode == 1) {
            if ($index_field == 'DATE')
                $fields[] = 'INTERNALDATE';
            $fields[] = "BODY.PEEK[HEADER.FIELDS ($index_field)]";
        }
        else if ($mode == 2) {
            if ($index_field == 'SIZE')
                $request = " $cmd $message_set (RFC822.SIZE$deleted)";
            else
                $request = " $cmd $message_set ($index_field$deleted)";
        } else if ($mode == 3)
            $request = " $cmd $message_set (FLAGS)";
        else // 4
            $request = " $cmd $message_set (INTERNALDATE$deleted)";
                $fields[] = 'RFC822.SIZE';
            else if (!$return_uid || $index_field != 'UID')
                $fields[] = $index_field;
        }
        else if ($mode == 3 && !$skip_deleted)
            $fields[] = 'FLAGS';
        else if ($mode == 4)
            $fields[] = 'INTERNALDATE';
        $request = $key . $request;
        $request = "$key $cmd $message_set (" . implode(' ', $fields) . ")";
        if (!$this->putLine($request)) {
            $this->setError(self::ERROR_COMMAND, "Unable to send command: $request");
@@ -1405,6 +1452,12 @@
                $id     = $m[1];
                $flags  = NULL;
                if ($return_uid) {
                    if (preg_match('/UID ([0-9]+)/', $line, $matches))
                        $id = (int) $matches[1];
                    else
                        continue;
                }
                if ($skip_deleted && preg_match('/FLAGS \(([^)]+)\)/', $line, $matches)) {
                    $flags = explode(' ', strtoupper($matches[1]));
                    if (in_array('\\DELETED', $flags)) {
@@ -1534,9 +1587,11 @@
    function UID2ID($mailbox, $uid)
    {
        if ($uid > 0) {
            $id_a = $this->search($mailbox, "UID $uid");
            if (is_array($id_a) && count($id_a) == 1) {
                return (int) $id_a[0];
            $index = $this->search($mailbox, "UID $uid");
            if ($index->count() == 1) {
                $arr = $index->get();
                return (int) $arr[0];
            }
        }
        return null;
@@ -1560,21 +1615,14 @@
            return null;
        }
        list($code, $response) = $this->execute('FETCH', array($id, '(UID)'));
        $index = $this->search($mailbox, $id, true);
        if ($code == self::ERROR_OK && preg_match("/^\* $id FETCH \(UID (.*)\)/i", $response, $m)) {
            return (int) $m[1];
        if ($index->count() == 1) {
            $arr = $index->get();
            return (int) $arr[0];
        }
        return null;
    }
    function fetchUIDs($mailbox, $message_set=null)
    {
        if (empty($message_set))
            $message_set = '1:*';
        return $this->fetchHeaderIndex($mailbox, $message_set, 'UID', false);
    }
    /**
@@ -1970,68 +2018,30 @@
        return $r;
    }
    // Don't be tempted to change $str to pass by reference to speed this up - it will slow it down by about
    // 7 times instead :-) See comments on http://uk2.php.net/references and this article:
    // http://derickrethans.nl/files/phparch-php-variables-article.pdf
    private function parseThread($str, $begin, $end, $root, $parent, $depth, &$depthmap, &$haschildren)
    /**
     * Executes THREAD command
     *
     * @param string $mailbox    Mailbox name
     * @param string $algorithm  Threading algorithm (ORDEREDSUBJECT, REFERENCES, REFS)
     * @param string $criteria   Searching criteria
     * @param bool   $return_uid Enables UIDs in result instead of sequence numbers
     * @param string $encoding   Character set
     *
     * @return rcube_result_thread Thread data
     */
    function thread($mailbox, $algorithm='REFERENCES', $criteria='', $return_uid=false, $encoding='US-ASCII')
    {
        $node = array();
        if ($str[$begin] != '(') {
            $stop = $begin + strspn($str, '1234567890', $begin, $end - $begin);
            $msg = substr($str, $begin, $stop - $begin);
            if ($msg == 0)
                return $node;
            if (is_null($root))
                $root = $msg;
            $depthmap[$msg] = $depth;
            $haschildren[$msg] = false;
            if (!is_null($parent))
                $haschildren[$parent] = true;
            if ($stop + 1 < $end)
                $node[$msg] = $this->parseThread($str, $stop + 1, $end, $root, $msg, $depth + 1, $depthmap, $haschildren);
            else
                $node[$msg] = array();
        } else {
            $off = $begin;
            while ($off < $end) {
                $start = $off;
                $off++;
                $n = 1;
                while ($n > 0) {
                    $p = strpos($str, ')', $off);
                    if ($p === false) {
                        error_log("Mismatched brackets parsing IMAP THREAD response:");
                        error_log(substr($str, ($begin < 10) ? 0 : ($begin - 10), $end - $begin + 20));
                        error_log(str_repeat(' ', $off - (($begin < 10) ? 0 : ($begin - 10))));
                        return $node;
                    }
                    $p1 = strpos($str, '(', $off);
                    if ($p1 !== false && $p1 < $p) {
                        $off = $p1 + 1;
                        $n++;
                    } else {
                        $off = $p + 1;
                        $n--;
                    }
                }
                $node += $this->parseThread($str, $start + 1, $off - 1, $root, $parent, $depth, $depthmap, $haschildren);
            }
        }
        require_once dirname(__FILE__) . '/rcube_result_thread.php';
        return $node;
    }
    function thread($mailbox, $algorithm='REFERENCES', $criteria='', $encoding='US-ASCII')
    {
        $old_sel = $this->selected;
        if (!$this->select($mailbox)) {
            return false;
            return new rcube_result_thread($mailbox);
        }
        // return empty result when folder is empty and we're just after SELECT
        if ($old_sel != $mailbox && !$this->data['EXISTS']) {
            return array(array(), array(), array());
            return new rcube_result_thread($mailbox);
        }
        $encoding  = $encoding ? trim($encoding) : 'US-ASCII';
@@ -2039,28 +2049,14 @@
        $criteria  = $criteria ? 'ALL '.trim($criteria) : 'ALL';
        $data      = '';
        list($code, $response) = $this->execute('THREAD', array(
            $algorithm, $encoding, $criteria));
        list($code, $response) = $this->execute($return_uid ? 'UID THREAD' : 'THREAD',
            array($algorithm, $encoding, $criteria));
        if ($code == self::ERROR_OK) {
            // remove prefix...
            $response = substr($response, stripos($response, '* THREAD') + 9);
            // ...unilateral untagged server responses
            if ($pos = strpos($response, '*')) {
                $response = substr($response, 0, $pos);
            }
            $response    = str_replace("\r\n", '', $response);
            $depthmap    = array();
            $haschildren = array();
            $tree = $this->parseThread($response, 0, strlen($response),
                null, null, 0, $depthmap, $haschildren);
            return array($tree, $depthmap, $haschildren);
        if ($code != self::ERROR_OK) {
            $response = null;
        }
        return false;
        return new rcube_result_thread($mailbox, $response);
    }
    /**
@@ -2071,22 +2067,27 @@
     * @param bool   $return_uid Enable UID in result instead of sequence ID
     * @param array  $items      Return items (MIN, MAX, COUNT, ALL)
     *
     * @return array Message identifiers or item-value hash
     * @return rcube_result_index Result data
     */
    function search($mailbox, $criteria, $return_uid=false, $items=array())
    {
        require_once dirname(__FILE__) . '/rcube_result_index.php';
        $old_sel = $this->selected;
        if (!$this->select($mailbox)) {
            return false;
            return new rcube_result_index($mailbox);
        }
        // return empty result when folder is empty and we're just after SELECT
        if ($old_sel != $mailbox && !$this->data['EXISTS']) {
            if (!empty($items))
                return array_combine($items, array_fill(0, count($items), 0));
            else
                return array();
            return new rcube_result_index($mailbox, '* SEARCH');
        }
        // If ESEARCH is supported always use ALL
        // but not when items are specified or using simple id2uid search
        if (empty($items) && ((int) $criteria != $criteria)) {
            $items = array('ALL');
        }
        $esearch  = empty($items) ? false : $this->getCapability('ESEARCH');
@@ -2097,6 +2098,7 @@
        if (!empty($items) && $esearch) {
            $params .= 'RETURN (' . implode(' ', $items) . ')';
        }
        if (!empty($criteria)) {
            $modseq = stripos($criteria, 'MODSEQ') !== false;
            $params .= ($params ? ' ' : '') . $criteria;
@@ -2108,65 +2110,11 @@
        list($code, $response) = $this->execute($return_uid ? 'UID SEARCH' : 'SEARCH',
            array($params));
        if ($code == self::ERROR_OK) {
            // remove prefix...
            $response = substr($response, stripos($response,
                $esearch ? '* ESEARCH' : '* SEARCH') + ($esearch ? 10 : 9));
            // ...and unilateral untagged server responses
            if ($pos = strpos($response, '*')) {
                $response = rtrim(substr($response, 0, $pos));
            }
            // remove MODSEQ response
            if ($modseq) {
                if (preg_match('/\(MODSEQ ([0-9]+)\)$/', $response, $m)) {
                    $response = substr($response, 0, -strlen($m[0]));
                }
            }
            if ($esearch) {
                // Skip prefix: ... (TAG "A285") UID ...
                $this->tokenizeResponse($response, $return_uid ? 2 : 1);
                $result = array();
                for ($i=0; $i<count($items); $i++) {
                    // If the SEARCH returns no matches, the server MUST NOT
                    // include the item result option in the ESEARCH response
                    if ($ret = $this->tokenizeResponse($response, 2)) {
                        list ($name, $value) = $ret;
                        $result[$name] = $value;
                    }
                }
                return $result;
            }
            else {
                $response = preg_split('/[\s\r\n]+/', $response, -1, PREG_SPLIT_NO_EMPTY);
                if (!empty($items)) {
                    $result = array();
                    if (in_array('COUNT', $items)) {
                        $result['COUNT'] = count($response);
                    }
                    if (in_array('MIN', $items)) {
                        $result['MIN'] = !empty($response) ? min($response) : 0;
                    }
                    if (in_array('MAX', $items)) {
                        $result['MAX'] = !empty($response) ? max($response) : 0;
                    }
                    if (in_array('ALL', $items)) {
                        $result['ALL'] = $this->compressMessageSet($response, true);
                    }
                    return $result;
                }
                else {
                    return $response;
                }
            }
        if ($code != self::ERROR_OK) {
            $response = null;
        }
        return false;
        return new rcube_result_index($mailbox, $response);
    }
    /**
program/include/rcube_result_index.php
New file
@@ -0,0 +1,446 @@
<?php
/*
 +-----------------------------------------------------------------------+
 | program/include/rcube_result_index.php                                |
 |                                                                       |
 | This file is part of the Roundcube Webmail client                     |
 | Copyright (C) 2005-2011, The Roundcube Dev Team                       |
 | Copyright (C) 2011, Kolab Systems AG                                  |
 | Licensed under the GNU GPL                                            |
 |                                                                       |
 | PURPOSE:                                                              |
 |   SORT/SEARCH/ESEARCH response handler                                |
 |                                                                       |
 +-----------------------------------------------------------------------+
 | Author: Thomas Bruederli <roundcube@gmail.com>                        |
 | Author: Aleksander Machniak <alec@alec.pl>                            |
 +-----------------------------------------------------------------------+
 $Id: rcube_imap.php 5347 2011-10-19 06:35:29Z alec $
*/
/**
 * Class for accessing IMAP's SORT/SEARCH/ESEARCH result
 */
class rcube_result_index
{
    private $raw_data;
    private $mailbox;
    private $meta = array();
    private $params = array();
    private $order = 'ASC';
    const SEPARATOR_ELEMENT = ' ';
    /**
     * Object constructor.
     */
    public function __construct($mailbox = null, $data = null)
    {
        $this->mailbox = $mailbox;
        $this->init($data);
    }
    /**
     * Initializes object with SORT command response
     *
     * @param string $data IMAP response string
     */
    public function init($data = null)
    {
        $this->meta = array();
        $data = explode('*', (string)$data);
        // ...skip unilateral untagged server responses
        for ($i=0, $len=count($data); $i<$len; $i++) {
            $data_item = &$data[$i];
            if (preg_match('/^ SORT/i', $data_item)) {
                $data_item = substr($data_item, 5);
                break;
            }
            else if (preg_match('/^ (E?SEARCH)/i', $data_item, $m)) {
                $data_item = substr($data_item, strlen($m[0]));
                if (strtoupper($m[1]) == 'ESEARCH') {
                    $data_item = trim($data_item);
                    // remove MODSEQ response
                    if (preg_match('/\(MODSEQ ([0-9]+)\)$/i', $data_item, $m)) {
                        $data_item = substr($data_item, 0, -strlen($m[0]));
                        $this->params['MODSEQ'] = $m[1];
                    }
                    // remove TAG response part
                    if (preg_match('/^\(TAG ["a-z0-9]+\)\s*/i', $data_item, $m)) {
                        $data_item = substr($data_item, strlen($m[0]));
                    }
                    // remove UID
                    $data_item = preg_replace('/^UID\s*/i', '', $data_item);
                    // ESEARCH parameters
                    while (preg_match('/^([a-z]+) ([0-9:,]+)\s*/i', $data_item, $m)) {
                        $param = strtoupper($m[1]);
                        $value = $m[2];
                        $this->params[strtoupper($m[1])] = $value;
                        $data_item = substr($data_item, strlen($m[0]));
                        if (in_array($param, array('COUNT', 'MIN', 'MAX'))) {
                            $this->meta[strtolower($param)] = (int) $m[2];
                        }
                    }
// @TODO: Implement compression using compressMessageSet() in __sleep() and __wakeup() ?
// @TODO: work with compressed result?!
                    if (isset($this->params['ALL'])) {
                        $data[$idx] = implode(self::SEPARATOR_ELEMENT,
                            rcube_imap_generic::uncompressMessageSet($this->params['ALL']));
                    }
                }
                break;
            }
            unset($data[$i]);
        }
        if (empty($data)) {
            return;
        }
        $data = array_shift($data);
        $data = trim($data);
        $data = preg_replace('/[\r\n]/', '', $data);
        $data = preg_replace('/\s+/', ' ', $data);
        $this->raw_data = $data;
    }
    /**
     * Checks the result from IMAP command
     *
     * @return bool True if the result is an error, False otherwise
     */
    public function isError()
    {
        return $this->raw_data === null ? true : false;
    }
    /**
     * Checks if the result is empty
     *
     * @return bool True if the result is empty, False otherwise
     */
    public function isEmpty()
    {
        return empty($this->raw_data) ? true : false;
    }
    /**
     * Returns number of elements in the result
     *
     * @return int Number of elements
     */
    public function count()
    {
        if ($this->meta['count'] !== null)
            return $this->meta['count'];
        if (empty($this->raw_data)) {
            $this->meta['count']  = 0;
            $this->meta['length'] = 0;
        }
        else
            // @TODO: check performance substr_count() vs. explode()
            $this->meta['count'] = 1 + substr_count($this->raw_data, self::SEPARATOR_ELEMENT);
        return $this->meta['count'];
    }
    /**
     * Returns number of elements in the result.
     * Alias for count() for compatibility with rcube_result_thread
     *
     * @return int Number of elements
     */
    public function countMessages()
    {
        return $this->count();
    }
    /**
     * Returns maximal message identifier in the result
     *
     * @return int Maximal message identifier
     */
    public function max()
    {
        if (!isset($this->meta['max'])) {
            // @TODO: do it by parsing raw_data?
            $this->meta['max'] = (int) @max($this->get());
        }
        return $this->meta['max'];
    }
    /**
     * Returns minimal message identifier in the result
     *
     * @return int Minimal message identifier
     */
    public function min()
    {
        if (!isset($this->meta['min'])) {
            // @TODO: do it by parsing raw_data?
            $this->meta['min'] = (int) @min($this->get());
        }
        return $this->meta['min'];
    }
    /**
     * Slices data set.
     *
     * @param $offset Offset (as for PHP's array_slice())
     * @param $length Number of elements (as for PHP's array_slice())
     *
     */
    public function slice($offset, $length)
    {
        $data = $this->get();
        $data = array_slice($data, $offset, $length);
        $this->meta          = array();
        $this->meta['count'] = count($data);
        $this->raw_data      = implode(self::SEPARATOR_ELEMENT, $data);
    }
    /**
     * Filters data set. Removes elements listed in $ids list.
     *
     * @param array $ids List of IDs to remove.
     */
    public function filter($ids = array())
    {
        $data = $this->get();
        $data = array_diff($data, $ids);
        $this->meta          = array();
        $this->meta['count'] = count($data);
        $this->raw_data      = implode(self::SEPARATOR_ELEMENT, $data);
    }
    /**
     * Filters data set. Removes elements not listed in $ids list.
     *
     * @param array $ids List of IDs to keep.
     */
    public function intersect($ids = array())
    {
        $data = $this->get();
        $data = array_intersect($data, $ids);
        $this->meta          = array();
        $this->meta['count'] = count($data);
        $this->raw_data      = implode(self::SEPARATOR_ELEMENT, $data);
    }
    /**
     * Reverts order of elements in the result
     */
    public function revert()
    {
        $this->order = $this->order == 'ASC' ? 'DESC' : 'ASC';
        if (empty($this->raw_data)) {
            return;
        }
        // @TODO: maybe do this in chunks
        $data = $this->get();
        $data = array_reverse($data);
        $this->raw_data = implode(self::SEPARATOR_ELEMENT, $data);
        $this->meta['pos'] = array();
    }
    /**
     * Check if the given message ID exists in the object
     *
     * @param int  $msgid     Message ID
     * @param bool $get_index When enabled element's index will be returned.
     *                        Elements are indexed starting with 0
     *
     * @return mixed False if message ID doesn't exist, True if exists or
     *               index of the element if $get_index=true
     */
    public function exists($msgid, $get_index = false)
    {
        if (empty($this->raw_data)) {
            return false;
        }
        $msgid = (int) $msgid;
        $begin = implode('|', array('^', preg_quote(self::SEPARATOR_ELEMENT, '/')));
        $end   = implode('|', array('$', preg_quote(self::SEPARATOR_ELEMENT, '/')));
        if (preg_match("/($begin)$msgid($end)/", $this->raw_data, $m,
            $get_index ? PREG_OFFSET_CAPTURE : null)
        ) {
            if ($get_index) {
                $idx = 0;
                if ($m[0][1]) {
                    $idx = 1 + substr_count($this->raw_data, self::SEPARATOR_ELEMENT, 0, $m[0][1]);
                }
                // cache position of this element, so we can use it in getElement()
                $this->meta['pos'][$idx] = (int)$m[0][1];
                return $idx;
            }
            return true;
        }
        return false;
    }
    /**
     * Return all messages in the result.
     *
     * @return array List of message IDs
     */
    public function get()
    {
        if (empty($this->raw_data)) {
            return array();
        }
        return explode(self::SEPARATOR_ELEMENT, $this->raw_data);
    }
    /**
     * Return all messages in the result.
     *
     * @return array List of message IDs
     */
    public function getCompressed()
    {
        if (empty($this->raw_data)) {
            return '';
        }
        return rcube_imap_generic::compressMessageSet($this->get());
    }
    /**
     * Return result element at specified index
     *
     * @param int|string  $index  Element's index or "FIRST" or "LAST"
     *
     * @return int Element value
     */
    public function getElement($index)
    {
        $count = $this->count();
        if (!$count) {
            return null;
        }
        // first element
        if ($index === 0 || $index === '0' || $index === 'FIRST') {
            $pos = strpos($this->raw_data, self::SEPARATOR_ELEMENT);
            if ($pos === false)
                $result = (int) $this->raw_data;
            else
                $result = (int) substr($this->raw_data, 0, $pos);
            return $result;
        }
        // last element
        if ($index === 'LAST' || $index == $count-1) {
            $pos = strrpos($this->raw_data, self::SEPARATOR_ELEMENT);
            if ($pos === false)
                $result = (int) $this->raw_data;
            else
                $result = (int) substr($this->raw_data, $pos);
            return $result;
        }
        // do we know the position of the element or the neighbour of it?
        if (!empty($this->meta['pos'])) {
            if (isset($this->meta['pos'][$index]))
                $pos = $this->meta['pos'][$index];
            else if (isset($this->meta['pos'][$index-1]))
                $pos = strpos($this->raw_data, self::SEPARATOR_ELEMENT,
                    $this->meta['pos'][$index-1] + 1);
            else if (isset($this->meta['pos'][$index+1]))
                $pos = strrpos($this->raw_data, self::SEPARATOR_ELEMENT,
                    $this->meta['pos'][$index+1] - $this->length() - 1);
            if (isset($pos) && preg_match('/([0-9]+)/', $this->raw_data, $m, null, $pos)) {
                return (int) $m[1];
            }
        }
        // Finally use less effective method
        $data = explode(self::SEPARATOR_ELEMENT, $this->raw_data);
        return $data[$index];
    }
    /**
     * Returns response parameters, e.g. ESEARCH's MIN/MAX/COUNT/ALL/MODSEQ
     * or internal data e.g. MAILBOX, ORDER
     *
     * @param string $param  Parameter name
     *
     * @return array|string Response parameters or parameter value
     */
    public function getParameters($param=null)
    {
        $params = $this->params;
        $params['MAILBOX'] = $this->mailbox;
        $params['ORDER']   = $this->order;
        if ($param !== null) {
            return $params[$param];
        }
        return $params;
    }
    /**
     * Returns length of internal data representation
     *
     * @return int Data length
     */
    private function length()
    {
        if (!isset($this->meta['length'])) {
            $this->meta['length'] = strlen($this->raw_data);
        }
        return $this->meta['length'];
    }
}
program/include/rcube_result_thread.php
New file
@@ -0,0 +1,670 @@
<?php
/*
 +-----------------------------------------------------------------------+
 | program/include/rcube_result_thread.php                               |
 |                                                                       |
 | This file is part of the Roundcube Webmail client                     |
 | Copyright (C) 2005-2011, The Roundcube Dev Team                       |
 | Copyright (C) 2011, Kolab Systems AG                                  |
 | Licensed under the GNU GPL                                            |
 |                                                                       |
 | PURPOSE:                                                              |
 |   THREAD response handler                                             |
 |                                                                       |
 +-----------------------------------------------------------------------+
 | Author: Thomas Bruederli <roundcube@gmail.com>                        |
 | Author: Aleksander Machniak <alec@alec.pl>                            |
 +-----------------------------------------------------------------------+
 $Id: rcube_imap.php 5347 2011-10-19 06:35:29Z alec $
*/
/**
 * Class for accessing IMAP's THREAD result
 */
class rcube_result_thread
{
    private $raw_data;
    private $mailbox;
    private $meta = array();
    private $order = 'ASC';
    const SEPARATOR_ELEMENT = ' ';
    const SEPARATOR_ITEM    = '~';
    const SEPARATOR_LEVEL   = ':';
    /**
     * Object constructor.
     */
    public function __construct($mailbox = null, $data = null)
    {
        $this->mailbox = $mailbox;
        $this->init($data);
    }
    /**
     * Initializes object with IMAP command response
     *
     * @param string $data IMAP response string
     */
    public function init($data = null)
    {
        $this->meta = array();
        $data = explode('*', (string)$data);
        // ...skip unilateral untagged server responses
        for ($i=0, $len=count($data); $i<$len; $i++) {
            if (preg_match('/^ THREAD/i', $data[$i])) {
                $data[$i] = substr($data[$i], 7);
                break;
            }
            unset($data[$i]);
        }
        if (empty($data)) {
            return;
        }
        $data = array_shift($data);
        $data = trim($data);
        $data = preg_replace('/[\r\n]/', '', $data);
        $data = preg_replace('/\s+/', ' ', $data);
        $this->raw_data = $this->parseThread($data);
    }
    /**
     * Checks the result from IMAP command
     *
     * @return bool True if the result is an error, False otherwise
     */
    public function isError()
    {
        return $this->raw_data === null ? true : false;
    }
    /**
     * Checks if the result is empty
     *
     * @return bool True if the result is empty, False otherwise
     */
    public function isEmpty()
    {
        return empty($this->raw_data) ? true : false;
    }
    /**
     * Returns number of elements (threads) in the result
     *
     * @return int Number of elements
     */
    public function count()
    {
        if ($this->meta['count'] !== null)
            return $this->meta['count'];
        if (empty($this->raw_data)) {
            $this->meta['count'] = 0;
        }
        else
            $this->meta['count'] = 1 + substr_count($this->raw_data, self::SEPARATOR_ELEMENT);
        if (!$this->meta['count'])
            $this->meta['messages'] = 0;
        return $this->meta['count'];
    }
    /**
     * Returns number of all messages in the result
     *
     * @return int Number of elements
     */
    public function countMessages()
    {
        if ($this->meta['messages'] !== null)
            return $this->meta['messages'];
        if (empty($this->raw_data)) {
            $this->meta['messages'] = 0;
        }
        else {
            $regexp = '/((^|' . preg_quote(self::SEPARATOR_ELEMENT, '/')
                . '|' . preg_quote(self::SEPARATOR_ITEM, '/') . ')[0-9]+)/';
            // @TODO: can we do this in a better way?
            $this->meta['messages'] = preg_match_all($regexp, $this->raw_data, $m);
        }
        if ($this->meta['messages'] == 0 || $this->meta['messages'] == 1)
            $this->meta['count'] = $this->meta['messages'];
        return $this->meta['messages'];
    }
    /**
     * Returns maximum message identifier in the result
     *
     * @return int Maximum message identifier
     */
    public function max()
    {
        if (!isset($this->meta['max'])) {
            // @TODO: do it by parsing raw_data?
            $this->meta['max'] = (int) @max($this->get());
        }
        return $this->meta['max'];
    }
    /**
     * Returns minimum message identifier in the result
     *
     * @return int Minimum message identifier
     */
    public function min()
    {
        if (!isset($this->meta['min'])) {
            // @TODO: do it by parsing raw_data?
            $this->meta['min'] = (int) @min($this->get());
        }
        return $this->meta['min'];
    }
    /**
     * Slices data set.
     *
     * @param $offset Offset (as for PHP's array_slice())
     * @param $length Number of elements (as for PHP's array_slice())
     */
    public function slice($offset, $length)
    {
        $data = explode(self::SEPARATOR_ELEMENT, $this->raw_data);
        $data = array_slice($data, $offset, $length);
        $this->meta          = array();
        $this->meta['count'] = count($data);
        $this->raw_data      = implode(self::SEPARATOR_ELEMENT, $data);
    }
    /**
     * Filters data set. Removes threads not listed in $roots list.
     *
     * @param array $roots List of IDs of thread roots.
     */
    public function filter($roots)
    {
        $datalen = strlen($this->raw_data);
        $roots   = array_flip($roots);
        $result  = '';
        $start   = 0;
        $this->meta          = array();
        $this->meta['count'] = 0;
        while (($pos = @strpos($this->raw_data, self::SEPARATOR_ELEMENT, $start))
            || ($start < $datalen && ($pos = $datalen))
        ) {
            $len   = $pos - $start;
            $elem  = substr($this->raw_data, $start, $len);
            $start = $pos + 1;
            // extract root message ID
            if ($npos = strpos($elem, self::SEPARATOR_ITEM)) {
                $root = (int) substr($elem, 0, $npos);
            }
            else {
                $root = $elem;
            }
            if (isset($roots[$root])) {
                $this->meta['count']++;
                $result .= self::SEPARATOR_ELEMENT . $elem;
            }
        }
        $this->raw_data = ltrim($result, self::SEPARATOR_ELEMENT);
    }
    /**
     * Reverts order of elements in the result
     */
    public function revert()
    {
        $this->order = $this->order == 'ASC' ? 'DESC' : 'ASC';
        if (empty($this->raw_data)) {
            return;
        }
        $this->meta['pos'] = array();
        $datalen = strlen($this->raw_data);
        $result  = '';
        $start   = 0;
        while (($pos = @strpos($this->raw_data, self::SEPARATOR_ELEMENT, $start))
            || ($start < $datalen && ($pos = $datalen))
        ) {
            $len   = $pos - $start;
            $elem  = substr($this->raw_data, $start, $len);
            $start = $pos + 1;
            $result = $elem . self::SEPARATOR_ELEMENT . $result;
        }
        $this->raw_data = rtrim($result, self::SEPARATOR_ELEMENT);
    }
    /**
     * Check if the given message ID exists in the object
     *
     * @param int $msgid Message ID
     * @param bool $get_index When enabled element's index will be returned.
     *                        Elements are indexed starting with 0
     *
     * @return boolean True on success, False if message ID doesn't exist
     */
    public function exists($msgid, $get_index = false)
    {
        $msgid = (int) $msgid;
        $begin = implode('|', array(
            '^',
            preg_quote(self::SEPARATOR_ELEMENT, '/'),
            preg_quote(self::SEPARATOR_LEVEL, '/'),
        ));
        $end = implode('|', array(
            '$',
            preg_quote(self::SEPARATOR_ELEMENT, '/'),
            preg_quote(self::SEPARATOR_ITEM, '/'),
        ));
        if (preg_match("/($begin)$msgid($end)/", $this->raw_data, $m,
            $get_index ? PREG_OFFSET_CAPTURE : null)
        ) {
            if ($get_index) {
                $idx = 0;
                if ($m[0][1]) {
                    $idx = substr_count($this->raw_data, self::SEPARATOR_ELEMENT, 0, $m[0][1]+1)
                        + substr_count($this->raw_data, self::SEPARATOR_ITEM, 0, $m[0][1]+1);
                }
                // cache position of this element, so we can use it in getElement()
                $this->meta['pos'][$idx] = (int)$m[0][1];
                return $idx;
            }
            return true;
        }
        return false;
    }
    /**
     * Return IDs of all messages in the result. Threaded data will be flattened.
     *
     * @return array List of message identifiers
     */
    public function get()
    {
        if (empty($this->raw_data)) {
            return array();
        }
        $regexp = '/(' . preg_quote(self::SEPARATOR_ELEMENT, '/')
            . '|' . preg_quote(self::SEPARATOR_ITEM, '/') . '[0-9]+' . preg_quote(self::SEPARATOR_LEVEL, '/')
            .')/';
        return preg_split($regexp, $this->raw_data);
    }
    /**
     * Return all messages in the result.
     *
     * @return array List of message identifiers
     */
    public function getCompressed()
    {
        if (empty($this->raw_data)) {
            return '';
        }
        return rcube_imap_generic::compressMessageSet($this->get());
    }
    /**
     * Return result element at specified index (all messages, not roots)
     *
     * @param int|string  $index  Element's index or "FIRST" or "LAST"
     *
     * @return int Element value
     */
    public function getElement($index)
    {
        $count = $this->count();
        if (!$count) {
            return null;
        }
        // first element
        if ($index === 0 || $index === '0' || $index === 'FIRST') {
            preg_match('/^([0-9]+)/', $this->raw_data, $m);
            $result = (int) $m[1];
            return $result;
        }
        // last element
        if ($index === 'LAST' || $index == $count-1) {
            preg_match('/([0-9]+)$/', $this->raw_data, $m);
            $result = (int) $m[1];
            return $result;
        }
        // do we know the position of the element or the neighbour of it?
        if (!empty($this->meta['pos'])) {
            $element = preg_quote(self::SEPARATOR_ELEMENT, '/');
            $item    = preg_quote(self::SEPARATOR_ITEM, '/') . '[0-9]+' . preg_quote(self::SEPARATOR_LEVEL, '/') .'?';
            $regexp  = '(' . $element . '|' . $item . ')';
            if (isset($this->meta['pos'][$index])) {
                if (preg_match('/([0-9]+)/', $this->raw_data, $m, null, $this->meta['pos'][$index]))
                    $result = $m[1];
            }
            else if (isset($this->meta['pos'][$index-1])) {
                // get chunk of data after previous element
                $data = substr($this->raw_data, $this->meta['pos'][$index-1]+1, 50);
                $data = preg_replace('/^[0-9]+/', '', $data); // remove UID at $index position
                $data = preg_replace("/^$regexp/", '', $data); // remove separator
                if (preg_match('/^([0-9]+)/', $data, $m))
                    $result = $m[1];
            }
            else if (isset($this->meta['pos'][$index+1])) {
                // get chunk of data before next element
                $pos  = max(0, $this->meta['pos'][$index+1] - 50);
                $len  = min(50, $this->meta['pos'][$index+1]);
                $data = substr($this->raw_data, $pos, $len);
                $data = preg_replace("/$regexp\$/", '', $data); // remove separator
                if (preg_match('/([0-9]+)$/', $data, $m))
                    $result = $m[1];
            }
            if (isset($result)) {
                return (int) $result;
            }
        }
        // Finally use less effective method
        $data = $this->get();
        return $data[$index];
    }
    /**
     * Returns response parameters e.g. MAILBOX, ORDER
     *
     * @param string $param  Parameter name
     *
     * @return array|string Response parameters or parameter value
     */
    public function getParameters($param=null)
    {
        $params = $this->params;
        $params['MAILBOX'] = $this->mailbox;
        $params['ORDER']   = $this->order;
        if ($param !== null) {
            return $params[$param];
        }
        return $params;
    }
    /**
     * THREAD=REFS sorting implementation (based on provided index)
     *
     * @param rcube_result_index $index  Sorted message identifiers
     */
    public function sort($index)
    {
        $this->sort_order = $index->getParameters('ORDER');
        if (empty($this->raw_data)) {
            return;
        }
        // when sorting search result it's good to make the index smaller
        if ($index->count() != $this->countMessages()) {
            $index->intersect($this->get());
        }
        $result  = array_fill_keys($index->get(), null);
        $datalen = strlen($this->raw_data);
        $start   = 0;
        // Here we're parsing raw_data twice, we want only one big array
        // in memory at a time
        // Assign roots
        while (($pos = @strpos($this->raw_data, self::SEPARATOR_ELEMENT, $start))
            || ($start < $datalen && ($pos = $datalen))
        ) {
            $len   = $pos - $start;
            $elem  = substr($this->raw_data, $start, $len);
            $start = $pos + 1;
            $items = explode(self::SEPARATOR_ITEM, $elem);
            $root  = (int) array_shift($items);
            $result[$elem] = $elem;
            foreach ($items as $item) {
                list($lv, $id) = explode(self::SEPARATOR_LEVEL, $item);
                    $result[$id] = $root;
            }
        }
        // get only unique roots
        $result = array_filter($result); // make sure there are no nulls
        $result = array_unique($result, SORT_NUMERIC);
        // Re-sort raw data
        $result = array_fill_keys($result, null);
        $start = 0;
        while (($pos = @strpos($this->raw_data, self::SEPARATOR_ELEMENT, $start))
            || ($start < $datalen && ($pos = $datalen))
        ) {
            $len   = $pos - $start;
            $elem  = substr($this->raw_data, $start, $len);
            $start = $pos + 1;
            $npos = strpos($elem, self::SEPARATOR_ITEM);
            $root = (int) ($npos ? substr($elem, 0, $npos) : $elem);
            $result[$root] = $elem;
        }
        $this->raw_data = implode(self::SEPARATOR_ELEMENT, $result);
    }
    /**
     * Returns data as tree
     *
     * @return array Data tree
     */
    public function getTree()
    {
        $datalen = strlen($this->raw_data);
        $result  = array();
        $start   = 0;
        while (($pos = @strpos($this->raw_data, self::SEPARATOR_ELEMENT, $start))
            || ($start < $datalen && ($pos = $datalen))
        ) {
            $len   = $pos - $start;
            $elem  = substr($this->raw_data, $start, $len);
            $items = explode(self::SEPARATOR_ITEM, $elem);
            $result[array_shift($items)] = $this->buildThread($items);
            $start = $pos + 1;
        }
        return $result;
    }
    /**
     * Returns thread depth and children data
     *
     * @return array Thread data
     */
    public function getThreadData()
    {
        $data     = $this->getTree();
        $depth    = array();
        $children = array();
        $this->buildThreadData($data, $depth, $children);
        return array($depth, $children);
    }
    /**
     * Creates 'depth' and 'children' arrays from stored thread 'tree' data.
     */
    private function buildThreadData($data, &$depth, &$children, $level = 0)
    {
        foreach ((array)$data as $key => $val) {
            $children[$key] = !empty($val);
            $depth[$key] = $level;
            if (!empty($val))
                $this->buildThreadData($val, $depth, $children, $level + 1);
        }
    }
    /**
     * Converts part of the raw thread into an array
     */
    private function buildThread($items, $level = 1, &$pos = 0)
    {
        $result = array();
        for ($len=count($items); $pos < $len; $pos++) {
            list($lv, $id) = explode(self::SEPARATOR_LEVEL, $items[$pos]);
            if ($level == $lv) {
                $pos++;
                $result[$id] = $this->buildThread($items, $level+1, $pos);
            }
            else {
                $pos--;
                break;
            }
        }
        return $result;
    }
    /**
     * IMAP THREAD response parser
     */
    private function parseThread($str, $begin = 0, $end = 0, $depth = 0)
    {
        // Don't be tempted to change $str to pass by reference to speed this up - it will slow it down by about
        // 7 times instead :-) See comments on http://uk2.php.net/references and this article:
        // http://derickrethans.nl/files/phparch-php-variables-article.pdf
        $node = '';
        if (!$end) {
            $end = strlen($str);
        }
        // Let's try to store data in max. compacted stracture as a string,
        // arrays handling is much more expensive
        // For the following structure: THREAD (2)(3 6 (4 23)(44 7 96))
        // -- 2
        //
        // -- 3
        //     \-- 6
        //         |-- 4
        //         |    \-- 23
        //         |
        //         \-- 44
        //              \-- 7
        //                   \-- 96
        //
        // The output will be: 2,3^1:6^2:4^3:23^2:44^3:7^4:96
        if ($str[$begin] != '(') {
            $stop = $begin + strspn($str, '1234567890', $begin, $end - $begin);
            $msg  = substr($str, $begin, $stop - $begin);
            if (!$msg) {
                return $node;
            }
            $this->meta['messages']++;
            $node .= ($depth ? self::SEPARATOR_ITEM.$depth.self::SEPARATOR_LEVEL : '').$msg;
            if ($stop + 1 < $end) {
                $node .= $this->parseThread($str, $stop + 1, $end, $depth + 1);
            }
        } else {
            $off = $begin;
            while ($off < $end) {
                $start = $off;
                $off++;
                $n = 1;
                while ($n > 0) {
                    $p = strpos($str, ')', $off);
                    if ($p === false) {
                        // error, wrong structure, mismatched brackets in IMAP THREAD response
                        // @TODO: write error to the log or maybe set $this->raw_data = null;
                        return $node;
                    }
                    $p1 = strpos($str, '(', $off);
                    if ($p1 !== false && $p1 < $p) {
                        $off = $p1 + 1;
                        $n++;
                    } else {
                        $off = $p + 1;
                        $n--;
                    }
                }
                $thread = $this->parseThread($str, $start + 1, $off - 1, $depth);
                if ($thread) {
                    if (!$depth) {
                        if ($node) {
                            $node .= self::SEPARATOR_ELEMENT;
                        }
                    }
                    $node .= $thread;
                }
            }
        }
        return $node;
    }
}
program/steps/mail/func.inc
@@ -93,7 +93,7 @@
      $_SESSION['search'] = $IMAP->get_search_set();
      $_SESSION['search_request'] = $search_request;
      $OUTPUT->set_env('search_request', $search_request);
      }
    }
      $search_mods = $RCMAIL->config->get('search_mods', $SEARCH_MODS_DEFAULT);
      $OUTPUT->set_env('search_mods', $search_mods);
program/steps/mail/pagenav.inc
@@ -19,52 +19,29 @@
*/
$uid = get_input_value('_uid', RCUBE_INPUT_GET);
$uid   = get_input_value('_uid', RCUBE_INPUT_GET);
$index = $IMAP->message_index(null, $_SESSION['sort_col'], $_SESSION['sort_order']);
$cnt   = $index->countMessages();
// Select mailbox first, for better performance
$mbox_name = $IMAP->get_mailbox_name();
$IMAP->select_mailbox($mbox_name);
// Get messages count (only messages, no threads here)
$cnt  = $IMAP->messagecount(NULL, 'ALL');
if ($_SESSION['sort_col'] == 'date' && $_SESSION['sort_order'] == 'DESC'
    && empty($_REQUEST['_search']) && !$CONFIG['skip_deleted'] && !$IMAP->threading
) {
    // this assumes that we are sorted by date_DESC
    $seq   = $IMAP->get_id($uid);
    $index = $cnt - $seq;
    $prev  = $IMAP->get_uid($seq + 1);
    $first = $IMAP->get_uid($cnt);
    $next  = $IMAP->get_uid($seq - 1);
    $last  = $IMAP->get_uid(1);
}
else {
    // Only if we use custom sorting
    $a_msg_index = $IMAP->message_index(NULL, $_SESSION['sort_col'], $_SESSION['sort_order']);
    $index = array_search($IMAP->get_id($uid), $a_msg_index);
    $count = count($a_msg_index);
    $prev  = isset($a_msg_index[$index-1]) ? $IMAP->get_uid($a_msg_index[$index-1]) : -1;
    $first = $count > 1 ? $IMAP->get_uid($a_msg_index[0]) : -1;
    $next  = isset($a_msg_index[$index+1]) ? $IMAP->get_uid($a_msg_index[$index+1]) : -1;
    $last  = $count > 1 ? $IMAP->get_uid($a_msg_index[$count-1]) : -1;
if ($cnt && ($pos = $index->exists($uid, true)) !== false) {
    $prev  = $pos ? $index->getElement($pos-1) : 0;
    $first = $pos ? $index->getElement('FIRST') : 0;
    $next  = $pos < $cnt-1 ? $index->getElement($pos+1) : 0;
    $last  = $pos < $cnt-1 ? $index->getElement('LAST') : 0;
}
// Set UIDs and activate navigation buttons
if ($prev > 0) {
if ($prev) {
    $OUTPUT->set_env('prev_uid', $prev);
    $OUTPUT->command('enable_command', 'previousmessage', 'firstmessage', true);
}
if ($next > 0) {
if ($next) {
    $OUTPUT->set_env('next_uid', $next);
    $OUTPUT->command('enable_command', 'nextmessage', 'lastmessage', true);
}
if ($first > 0)
if ($first)
    $OUTPUT->set_env('first_uid', $first);
if ($last > 0)
if ($last)
    $OUTPUT->set_env('last_uid', $last);
// Don't need a real messages count value
@@ -73,7 +50,7 @@
// Set rowcount text
$OUTPUT->command('set_rowcount', rcube_label(array(
    'name' => 'messagenrof',
    'vars' => array('nr'  => $index+1, 'count' => $cnt)
    'vars' => array('nr'  => $pos+1, 'count' => $cnt)
)));
$OUTPUT->send();
program/steps/mail/search.inc
@@ -109,10 +109,6 @@
if ($search_str)
  $IMAP->search($mbox, $search_str, $imap_charset, $_SESSION['sort_col']);
// Get the headers
$result_h = $IMAP->list_headers($mbox, 1, $_SESSION['sort_col'], $_SESSION['sort_order']);
$count = $IMAP->messagecount(NULL, $IMAP->threading ? 'THREADS' : 'ALL');
// save search results in session
if (!is_array($_SESSION['search']))
  $_SESSION['search'] = array();
@@ -123,6 +119,12 @@
}
$_SESSION['search_request'] = $search_request;
// Get the headers
$result_h = $IMAP->list_headers($mbox, 1, $_SESSION['sort_col'], $_SESSION['sort_order']);
$count = $IMAP->messagecount($mbox, $IMAP->threading ? 'THREADS' : 'ALL');
// Make sure we got the headers
if (!empty($result_h)) {
  rcmail_js_message_list($result_h);
program/steps/mail/sendmail.inc
@@ -701,11 +701,11 @@
  if ($olddraftmessageid) {
    // delete previous saved draft
    // @TODO: use message UID (remember to check UIDVALIDITY) to skip this SEARCH
    $a_deleteid = $IMAP->search_once($CONFIG['drafts_mbox'],
        'HEADER Message-ID '.$olddraftmessageid, true);
    $delete_idx = $IMAP->search_once($CONFIG['drafts_mbox'],
        'HEADER Message-ID '.$olddraftmessageid);
    if (!empty($a_deleteid)) {
      $deleted = $IMAP->delete_message($a_deleteid, $CONFIG['drafts_mbox']);
    if ($del_uid = $delete_idx->getElement('FIRST')) {
      $deleted = $IMAP->delete_message($del_uid, $CONFIG['drafts_mbox']);
      // raise error if deletion of old draft failed
      if (!$deleted)
@@ -726,8 +726,8 @@
  // remember new draft-uid ($saved could be an UID or TRUE here)
  if (is_bool($saved)) {
    $draftuids = $IMAP->search_once($CONFIG['drafts_mbox'], 'HEADER Message-ID '.$msgid, true);
    $saved     = $draftuids[0];
    $draft_idx = $IMAP->search_once($CONFIG['drafts_mbox'], 'HEADER Message-ID '.$msgid);
    $saved     = $draft_idx->getElement('FIRST');
  }
  $COMPOSE['param']['draft_uid'] = $saved;