From 40c45e9de99186eda203a925c09424a3a8ec103c Mon Sep 17 00:00:00 2001
From: alecpl <alec@alec.pl>
Date: Wed, 07 Dec 2011 03:44:48 -0500
Subject: [PATCH] - Fixed issues with big memory allocation of IMAP results, improved a lot of rcube_imap class

---
 CHANGELOG                               |    1 
 program/include/rcube_result_thread.php |  670 +++++++++++++
 program/steps/mail/pagenav.inc          |   49 
 program/steps/mail/search.inc           |   10 
 program/include/rcube_imap.php          | 1175 ++++++-----------------
 program/include/rcube_imap_cache.php    |  264 +---
 program/steps/mail/func.inc             |    2 
 program/include/rcube_result_index.php  |  446 +++++++++
 program/steps/mail/sendmail.inc         |   12 
 program/include/rcube_imap_generic.php  |  308 ++---
 10 files changed, 1,686 insertions(+), 1,251 deletions(-)

diff --git a/CHANGELOG b/CHANGELOG
index 7c272dd..ac8bd9e 100644
--- a/CHANGELOG
+++ b/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)
diff --git a/program/include/rcube_imap.php b/program/include/rcube_imap.php
index c4175cb..455cc32 100644
--- a/program/include/rcube_imap.php
+++ b/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"));
     }
 
     /**
diff --git a/program/include/rcube_imap_cache.php b/program/include/rcube_imap_cache.php
index ee4e925..b3585b8 100644
--- a/program/include/rcube_imap_cache.php
+++ b/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;
     }
 }
diff --git a/program/include/rcube_imap_generic.php b/program/include/rcube_imap_generic.php
index a4e921f..ad147e5 100644
--- a/program/include/rcube_imap_generic.php
+++ b/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);
     }
 
     /**
diff --git a/program/include/rcube_result_index.php b/program/include/rcube_result_index.php
new file mode 100644
index 0000000..4decaf9
--- /dev/null
+++ b/program/include/rcube_result_index.php
@@ -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'];
+    }
+}
diff --git a/program/include/rcube_result_thread.php b/program/include/rcube_result_thread.php
new file mode 100644
index 0000000..f0f1356
--- /dev/null
+++ b/program/include/rcube_result_thread.php
@@ -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;
+    }
+}
diff --git a/program/steps/mail/func.inc b/program/steps/mail/func.inc
index a1db45e..7e3a86c 100644
--- a/program/steps/mail/func.inc
+++ b/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);
diff --git a/program/steps/mail/pagenav.inc b/program/steps/mail/pagenav.inc
index 974b3b4..16844f4 100644
--- a/program/steps/mail/pagenav.inc
+++ b/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();
diff --git a/program/steps/mail/search.inc b/program/steps/mail/search.inc
index 49f31e0..05ba9d2 100644
--- a/program/steps/mail/search.inc
+++ b/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);
diff --git a/program/steps/mail/sendmail.inc b/program/steps/mail/sendmail.inc
index 64deb73..0da1ee3 100644
--- a/program/steps/mail/sendmail.inc
+++ b/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;
 

--
Gitblit v1.9.1