From f52c936f4d451a5d3a87d2501aa5a1701cdafde5 Mon Sep 17 00:00:00 2001
From: thomascube <thomas@roundcube.net>
Date: Wed, 17 Mar 2010 08:24:09 -0400
Subject: [PATCH] Merged devel-threads branch (r3066:3364) back into trunk

---
 skins/default/common.css                       |   10 
 program/js/common.js                           |    2 
 THREADS                                        |   43 
 skins/default/mail.css                         |  158 ++
 program/steps/mail/show.inc                    |    4 
 program/lib/imap.inc                           |  441 +-----
 program/localization/de_CH/labels.inc          |   24 
 skins/default/images/messageactions.gif        |    0 
 program/steps/settings/manage_folders.inc      |   84 +
 skins/default/iehacks.css                      |   24 
 index.php                                      |    2 
 program/include/rcube_shared.inc               |   20 
 skins/default/images/messageactions.png        |    0 
 skins/default/templates/mail.html              |   59 
 skins/default/images/mail_footer.png           |    0 
 program/steps/mail/check_recent.inc            |   33 
 program/steps/mail/func.inc                    |  358 ++---
 program/steps/settings/func.inc                |   24 
 skins/default/images/icons/columnpicker.gif    |    0 
 program/localization/pl_PL/labels.inc          |    2 
 program/steps/mail/mark.inc                    |    9 
 program/js/list.js                             |  274 ++++
 CHANGELOG                                      |    4 
 program/include/html.php                       |    5 
 program/steps/mail/viewsource.inc              |    2 
 skins/default/images/icons/unread_children.png |    0 
 program/include/rcube_user.php                 |    7 
 config/main.inc.php.dist                       |   13 
 skins/default/includes/messagemenu.html        |    2 
 program/steps/settings/save_prefs.inc          |    2 
 program/steps/mail/move_del.inc                |   10 
 program/steps/mail/list.inc                    |   21 
 program/steps/mail/search.inc                  |    4 
 skins/default/ie6hacks.css                     |   15 
 program/include/rcube_imap.php                 |  646 +++++++++-
 program/include/rcmail.php                     |    1 
 program/localization/en_US/labels.inc          |   25 
 skins/default/functions.js                     |   87 +
 program/js/app.js                              | 1035 +++++++++++-----
 bin/msgexport.sh                               |    4 
 40 files changed, 2,322 insertions(+), 1,132 deletions(-)

diff --git a/CHANGELOG b/CHANGELOG
index cc11e45..101d1f4 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,6 +1,10 @@
 CHANGELOG RoundCube Webmail
 ===========================
 
+- Threaded message listing now available
+- Added sorting by ARRIVAL and CC
+- Message list columns configurable by the user
+- Removed 'index_sort' option, now we're using empty 'message_sort_col' for this
 - virtuser_query: support other identity data (#1486148)
 - Options virtuser_* replaced with virtuser_* plugins
 - Plugin API: Implemented 'email2user' and 'user2email' hooks
diff --git a/THREADS b/THREADS
new file mode 100644
index 0000000..022b7fa
--- /dev/null
+++ b/THREADS
@@ -0,0 +1,43 @@
+CHANGES IN RELATION TO ORIGINAL PATCH
+    - don't add nested messages into selection on collapse if parent message
+      is in selection
+    - some changes in messages highlighting
+    - re-written all changes in rcube_imap.php
+    - temporary removed threads caching (see TODO)
+    - use depth=0 for roots
+    - thread expand state is not stored anywhere
+    - removed imap_thread_algorithm option, we're using the best algorithm
+      supported by server and implement REFS sorting in Roundcube
+    - use underlined subject for root with unread children (icon is still supported)
+    - on deleting messages the whole list isn't refreshed
+    - added 'expand unread' button
+
+TODO (must have):
+    - threads caching
+    - updating threaded message list on message delete
+    - don't reload messages list on check_recent
+
+TODO (other):
+    - performance: fetching all messages for list in "expand all" state only,
+      if "expand all" is disabled we should fetch only root messages and fetch
+      children on-demand (on expand button click),
+      Notice: this is not so simple, because we need to fetch children
+    	      to set "unread_children", but we can fetch only flags instead of
+    	      all headers for each child
+    - button in #listcontrols to mark all messages in current thread (with selected
+      root or child message),
+    + thread tree icons
+    + thread css: message row height, thread/status icon alignment
+      (change size of all list icons to 14x14)
+    - remove 'indexsort' label from localization files
+
+TODO (by the way):
+    - use jQuery.inArray instead of find_in_array() (common.js)
+    + use only one function (js) to generate messages list
+
+KNOWN ISSUES:
+    - on new message (check_recent) the whole list is reloaded
+    + table header replacement doesn't work on IE
+    - css issues on IE6
+    + css issues on IE7
+
diff --git a/bin/msgexport.sh b/bin/msgexport.sh
index 7dd56e9..005e2ca 100755
--- a/bin/msgexport.sh
+++ b/bin/msgexport.sh
@@ -89,8 +89,8 @@
 		$from = current($IMAP->decode_address_list($headers->from, 1, false));
 		
 		fwrite($out, sprintf("From %s %s UID %d\n", $from['mailto'], $headers->date, $headers->uid));
-		fwrite($out, iil_C_FetchPartHeader($IMAP->conn, $IMAP->mailbox, $i, null));
-		fwrite($out, iil_C_HandlePartBody($IMAP->conn, $IMAP->mailbox, $i, null, 1));
+		fwrite($out, iil_C_FetchPartHeader($IMAP->conn, $mbox, $i, null));
+		fwrite($out, iil_C_HandlePartBody($IMAP->conn, $mbox, $i, null, 1));
 		fwrite($out, "\n\n\n");
 		
 		progress_update($i, $count);
diff --git a/config/main.inc.php.dist b/config/main.inc.php.dist
index 5709a60..299d834 100644
--- a/config/main.inc.php.dist
+++ b/config/main.inc.php.dist
@@ -232,10 +232,11 @@
 // USER INTERFACE
 // ----------------------------------
 
-// default sort col
-$rcmail_config['message_sort_col'] = 'date';
+// default messages sort column. Use empty value for default server's sorting, 
+// or 'arrival', 'date', 'subject', 'from', 'to', 'size', 'cc'
+$rcmail_config['message_sort_col'] = '';
 
-// default sort order
+// default messages sort order
 $rcmail_config['message_sort_order'] = 'DESC';
 
 // These cols are shown in the message list. Available cols are:
@@ -461,8 +462,10 @@
 // If true, after message delete/move, the next message will be displayed
 $rcmail_config['display_next'] = false;
 
-// If true, messages list will be sorted by message index instead of message date
-$rcmail_config['index_sort'] = true;
+// 0 - Do not expand threads 
+// 1 - Expand all threads automatically 
+// 2 - Expand only threads with unread messages 
+$rcmail_config['autoexpand_threads'] = 0;
 
 // When replying place cursor above original message (top posting)
 $rcmail_config['top_posting'] = false;
diff --git a/index.php b/index.php
index fe7f6b5..ee4aa45 100644
--- a/index.php
+++ b/index.php
@@ -226,6 +226,8 @@
     'delete-folder' => 'manage_folders.inc',
     'subscribe'     => 'manage_folders.inc',
     'unsubscribe'   => 'manage_folders.inc',
+    'enable-threading'  => 'manage_folders.inc',
+    'disable-threading' => 'manage_folders.inc',
     'add-identity'  => 'edit_identity.inc',
   )
 );
diff --git a/program/include/html.php b/program/include/html.php
index 99132a1..0930f58 100644
--- a/program/include/html.php
+++ b/program/include/html.php
@@ -5,7 +5,7 @@
  | program/include/html.php                                              |
  |                                                                       |
  | This file is part of the RoundCube Webmail client                     |
- | Copyright (C) 2005-2009, RoundCube Dev, - Switzerland                 |
+ | Copyright (C) 2005-2010, RoundCube Dev, - Switzerland                 |
  | Licensed under the GNU GPL                                            |
  |                                                                       |
  | PURPOSE:                                                              |
@@ -34,8 +34,7 @@
 
     public static $lc_tags = true;
     public static $common_attrib = array('id','class','style','title','align');
-    public static $containers = array('iframe','div','span','p','h1','h2','h3',
-	'form','textarea','table','tr','th','td','style','script');
+    public static $containers = array('iframe','div','span','p','h1','h2','h3','form','textarea','table','thead','tbody','tr','th','td','style','script');
 
     /**
      * Constructor
diff --git a/program/include/rcmail.php b/program/include/rcmail.php
index 81f6137..3ea4180 100644
--- a/program/include/rcmail.php
+++ b/program/include/rcmail.php
@@ -410,7 +410,6 @@
     $this->imap = new rcube_imap($this->db);
     $this->imap->debug_level = $this->config->get('debug_level');
     $this->imap->skip_deleted = $this->config->get('skip_deleted');
-    $this->imap->index_sort = $this->config->get('index_sort', true);
 
     // enable caching of imap data
     if ($this->config->get('enable_caching')) {
diff --git a/program/include/rcube_imap.php b/program/include/rcube_imap.php
index 91d36c2..332d530 100644
--- a/program/include/rcube_imap.php
+++ b/program/include/rcube_imap.php
@@ -5,7 +5,7 @@
  | program/include/rcube_imap.php                                        |
  |                                                                       |
  | This file is part of the RoundCube Webmail client                     |
- | Copyright (C) 2005-2009, RoundCube Dev. - Switzerland                 |
+ | Copyright (C) 2005-2010, RoundCube Dev. - Switzerland                 |
  | Licensed under the GNU GPL                                            |
  |                                                                       |
  | PURPOSE:                                                              |
@@ -41,35 +41,40 @@
  */
 class rcube_imap
 {
-  var $db;
-  var $conn;
-  var $root_dir = '';
-  var $mailbox = 'INBOX';
-  var $list_page = 1;
-  var $page_size = 10;
-  var $sort_field = 'date';
-  var $sort_order = 'DESC';
-  var $index_sort = true;
-  var $delimiter = NULL;
-  var $caching_enabled = FALSE;
-  var $default_charset = 'ISO-8859-1';
-  var $struct_charset = NULL;
-  var $default_folders = array('INBOX');
-  var $fetch_add_headers = '';
-  var $cache = array();
-  var $cache_keys = array();  
-  var $cache_changes = array();
-  var $uid_id_map = array();
-  var $msg_headers = array();
-  var $skip_deleted = FALSE;
-  var $search_set = NULL;
-  var $search_string = '';
-  var $search_charset = '';
-  var $search_sort_field = '';  
-  var $debug_level = 1;
-  var $error_code = 0;
-  var $options = array('auth_method' => 'check');
-  
+  public $debug_level = 1;
+  public $error_code = 0;
+  public $skip_deleted = false;
+  public $root_dir = '';
+  public $page_size = 10;
+  public $list_page = 1;
+  public $delimiter = NULL;
+  public $threading = false;
+  public $fetch_add_headers = '';
+  public $conn;
+
+  private $db;
+  private $root_ns = '';
+  private $mailbox = 'INBOX';
+  private $sort_field = '';
+  private $sort_order = 'DESC';
+  private $caching_enabled = false;
+  private $default_charset = 'ISO-8859-1';
+  private $struct_charset = NULL;
+  private $default_folders = array('INBOX');
+  private $default_folders_lc = array('inbox');
+  private $icache = array();
+  private $cache = array();
+  private $cache_keys = array();  
+  private $cache_changes = array();
+  private $uid_id_map = array();
+  private $msg_headers = array();
+  public  $search_set = NULL;
+  public  $search_string = '';
+  private $search_charset = '';
+  private $search_sort_field = '';
+  private $search_threads = false;
+  private $db_header_fields = array('idx', 'uid', 'subject', 'from', 'to', 'cc', 'date', 'size');
+  private $options = array('auth_method' => 'check');
   private $host, $user, $pass, $port, $ssl;
 
 
@@ -299,17 +304,18 @@
    * @param  string  Charset of search string
    * @param  string  Sorting field
    */
-  function set_search_set($str=null, $msgs=null, $charset=null, $sort_field=null)
+  function set_search_set($str=null, $msgs=null, $charset=null, $sort_field=null, $threads=false)
     {
     if (is_array($str) && $msgs == null)
-      list($str, $msgs, $charset, $sort_field) = $str;
+      list($str, $msgs, $charset, $sort_field, $threads) = $str;
     if ($msgs != null && !is_array($msgs))
       $msgs = explode(',', $msgs);
-      
+
     $this->search_string = $str;
     $this->search_set = $msgs;
     $this->search_charset = $charset;
     $this->search_sort_field = $sort_field;
+    $this->search_threads = $threads;
     }
 
 
@@ -319,7 +325,12 @@
    */
   function get_search_set()
     {
-    return array($this->search_string, $this->search_set, $this->search_charset, $this->search_sort_field);
+    return array($this->search_string,
+	$this->search_set,
+	$this->search_charset,
+	$this->search_sort_field,
+	$this->search_threads,
+	);
     }
 
 
@@ -345,6 +356,30 @@
   function get_capability($cap)
     {
     return iil_C_GetCapability($this->conn, strtoupper($cap));
+    }
+
+
+  /**
+   * Sets threading flag to the best supported THREAD algorithm
+   *
+   * @param  boolean  TRUE to enable and FALSE
+   * @return string   Algorithm or false if THREAD is not supported
+   * @access public
+   */
+  function set_threading($enable=false)
+    {
+    $this->threading = false;
+    
+    if ($enable) {
+      if ($this->get_capability('THREAD=REFS'))
+        $this->threading = 'REFS';
+      else if ($this->get_capability('THREAD=REFERENCES'))
+        $this->threading = 'REFERENCES';
+      else if ($this->get_capability('THREAD=ORDEREDSUBJECT'))
+        $this->threading = 'ORDEREDSUBJECT';
+      }
+      
+    return $this->threading;
     }
 
 
@@ -480,11 +515,15 @@
 
     if (empty($mailbox))
       $mailbox = $this->mailbox;
-      
-    // count search set
-    if ($this->search_string && $mailbox == $this->mailbox && $mode == 'ALL' && !$force)
-      return count((array)$this->search_set);
 
+    // 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']);
+      else
+        return count((array)$this->search_set);
+      }
+    
     $a_mailbox_cache = $this->get_cache('messagecount');
     
     // return cached value
@@ -494,8 +533,11 @@
     if (!is_array($a_mailbox_cache[$mailbox]))
       $a_mailbox_cache[$mailbox] = array();
 
+    if ($mode == 'THREADS')
+      $count = $this->_threadcount($mailbox);
+
     // RECENT count is fetched a bit different
-    if ($mode == 'RECENT')
+    else if ($mode == 'RECENT')
        $count = iil_C_CheckForRecent($this->conn, $mailbox);
 
     // use SEARCH for message counting
@@ -534,6 +576,24 @@
 
 
   /**
+   * Private method for getting nr of threads
+   *
+   * @access  private
+   * @see     rcube_imap::messagecount()
+   */
+  private function _threadcount($mailbox)
+    {
+    if (!empty($this->icache['threads']))
+      return count($this->icache['threads']['tree']);
+    
+    list ($thread_tree, $msg_depth, $has_children) = $this->_fetch_threads($mailbox);
+
+//    $this->update_thread_cache($mailbox, $thread_tree, $msg_depth, $has_children);
+    return count($thread_tree);  
+    }
+
+
+  /**
    * Public method for listing headers
    * convert mailbox name with root dir first
    *
@@ -567,6 +627,9 @@
     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, $recursive, $slice);
+
     $this->_set_sort_order($sort_field, $sort_order);
 
     $page = $page ? $page : $this->list_page;
@@ -593,8 +656,8 @@
     // retrieve headers from IMAP
     $a_msg_headers = array();
 
-    // use message index sort for sorting by Date (for better performance)
-    if ($this->index_sort && $this->sort_field == 'date')
+    // use message index sort as default sorting (for better performance)
+    if (!$this->sort_field)
       {
         if ($this->skip_deleted) {
           // @TODO: this could be cached
@@ -671,6 +734,140 @@
 
 
   /**
+   * Private method for listing message headers using threads
+   *
+   * @access  private
+   * @see     rcube_imap::list_headers
+   */
+  private function _list_thread_headers($mailbox, $page=NULL, $sort_field=NULL, $sort_order=NULL, $recursive=FALSE, $slice=0)
+    {
+    $this->_set_sort_order($sort_field, $sort_order);
+
+    $page = $page ? $page : $this->list_page;
+//    $cache_key = $mailbox.'.msg';
+//    $cache_status = $this->check_cache_status($mailbox, $cache_key);
+
+    // get all threads (default sort order)
+    list ($thread_tree, $msg_depth, $has_children) = $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);
+    }
+
+
+  /**
+   * Private method for fetching threads data
+   *
+   * @param   string   Mailbox/folder name
+   * @return  array    Array with thread data
+   * @access  private
+   */
+  private function _fetch_threads($mailbox)
+    {
+    if (empty($this->icache['threads'])) {
+      // get all threads
+      list ($thread_tree, $msg_depth, $has_children) = iil_C_Thread($this->conn,
+        $mailbox, $this->threading, $this->skip_deleted ? 'UNDELETED' : '');
+    
+      // add to internal (fast) cache
+      $this->icache['threads'] = array();
+      $this->icache['threads']['tree'] = $thread_tree;
+      $this->icache['threads']['depth'] = $msg_depth;
+      $this->icache['threads']['has_children'] = $has_children;
+      }
+
+    return array(
+      $this->icache['threads']['tree'],
+      $this->icache['threads']['depth'],
+      $this->icache['threads']['has_children'],
+      );
+    }
+
+
+  /**
+   * Private method for fetching threaded messages headers
+   *
+   * @access  private
+   */
+  private function _fetch_thread_headers($mailbox, $thread_tree, $msg_depth, $has_children, $msg_index, $page, $slice=0)
+    {
+    $cache_key = $mailbox.'.msg';
+    // 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);
+
+    if ($slice)
+      $msg_index = array_slice($msg_index, ($this->sort_order == 'DESC' ? 0 : -$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]));
+      }
+
+    // fetch reqested headers from server
+    $this->_fetch_headers($mailbox, $all_ids, $a_msg_headers, $cache_key);
+
+    // 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_sequence_numbers($all_ids);
+    $sorter->sort_headers($a_msg_headers);
+
+    // Set depth, has_children and unread_children fields in headers
+    $this->_set_thread_flags($a_msg_headers, $msg_depth, $has_children);
+
+    return array_values($a_msg_headers);
+    }
+
+
+  /**
+   * Private method for setting threaded messages flags:
+   * depth, has_children and unread_children
+   *
+   * @param  array   Reference to headers array indexed by message ID
+   * @param  array   Array of messages depth indexed by message ID
+   * @param  array   Array of messages children flags indexed by message ID
+   * @return array   Message headers array indexed by message ID
+   * @access private
+   */
+  private function _set_thread_flags(&$headers, $msg_depth, $msg_children)
+    {
+    $parents = array();
+
+    foreach ($headers as $idx => $header) {
+      $id = $header->id;
+      $depth = $msg_depth[$id];
+      $parents = array_slice($parents, 0, $depth);
+
+      if (!empty($parents)) {
+        $headers[$idx]->parent_uid = end($parents);
+        if (!$header->seen)
+          $headers[$parents[0]]->unread_children++;
+        }
+      array_push($parents, $header->uid);
+
+      $headers[$idx]->depth = $depth;
+      $headers[$idx]->has_children = $msg_children[$id];
+      }
+    }
+
+
+  /**
    * Private method for listing a set of message headers (search results)
    *
    * @param   string   Mailbox/folder name
@@ -687,6 +884,14 @@
     if (!strlen($mailbox) || empty($this->search_set))
       return array();
 
+    // use saved messages from searching
+    if ($this->threading)
+      return $this->_list_thread_header_set($mailbox, $page, $sort_field, $sort_order, $slice);
+
+    // search set is threaded, we need a new one
+    if ($this->search_threads)
+      $this->search('', $this->search_string, $this->search_charset, $sort_field);
+    
     $msgs = $this->search_set;
     $a_msg_headers = array();
     $page = $page ? $page : $this->list_page;
@@ -694,8 +899,8 @@
 
     $this->_set_sort_order($sort_field, $sort_order);
 
-    // quickest method
-    if ($this->index_sort && $this->search_sort_field == 'date' && $this->sort_field == 'date')
+    // quickest method (default sorting)
+    if (!$this->search_sort_field && !$this->sort_field)
       {
       if ($sort_order == 'DESC')
         $msgs = array_reverse($msgs);
@@ -716,8 +921,9 @@
 
       return array_values($a_msg_headers);
       }
+
     // sorted messages, so we can first slice array and then fetch only wanted headers
-    if ($this->get_capability('sort') && (!$this->index_sort || $this->sort_field != 'date')) // SORT searching result
+    if ($this->get_capability('sort')) // SORT searching result
       {
       // reset search set if sorting field has been changed
       if ($this->sort_field && $this->search_sort_field != $this->sort_field)
@@ -745,10 +951,10 @@
 
       return array_values($a_msg_headers);
       }
-    else { // SEARCH searching result, need sorting
+    else { // SEARCH result, need sorting
       $cnt = count($msgs);
       // 300: experimantal value for best result
-      if (($cnt > 300 && $cnt > $this->page_size) || ($this->index_sort && $this->sort_field == 'date')) {
+      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...
@@ -787,6 +993,40 @@
         return $a_msg_headers;
         }
       }
+    }
+
+
+  /**
+   * Private method for listing a set of threaded message headers (search results)
+   *
+   * @param   string   Mailbox/folder name
+   * @param   int      Current page to list
+   * @param   string   Header field to sort by
+   * @param   string   Sort order [ASC|DESC]
+   * @param   boolean  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)
+    {
+    // update search_set if previous data was fetched with disabled threading
+    if (!$this->search_threads)
+      $this->search('', $this->search_string, $this->search_charset, $sort_field);
+
+    $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);
     }
 
 
@@ -901,18 +1141,22 @@
    */
   function message_index($mbox_name='', $sort_field=NULL, $sort_order=NULL)
     {
+    if ($this->threading)
+      return $this->thread_index($mbox_name, $sort_field, $sort_order);
+
     $this->_set_sort_order($sort_field, $sort_order);
 
     $mailbox = $mbox_name ? $this->mod_mailbox($mbox_name) : $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->cache[$key]) && $this->search_string && $mailbox == $this->mailbox)
+    if (!isset($this->cache[$key]) && $this->search_string
+      && !$this->search_threads && $mailbox == $this->mailbox)
     {
       $this->cache[$key] = array();
       
-      // use message index sort for sorting by Date
-      if ($this->index_sort && $this->sort_field == 'date')
+      // use message index sort as default sorting
+      if (!$this->sort_field)
       {
         $msgs = $this->search_set;
 
@@ -937,7 +1181,8 @@
       }
       else
       {
-        $a_index = iil_C_FetchHeaderIndex($this->conn, $mailbox, join(',', $this->search_set), $this->sort_field, $this->skip_deleted);
+        $a_index = iil_C_FetchHeaderIndex($this->conn, $mailbox,
+	  join(',', $this->search_set), $this->sort_field, $this->skip_deleted);
 
         if ($this->sort_order=="ASC")
           asort($a_index);
@@ -963,8 +1208,8 @@
       return array_keys($a_index);
       }
 
-    // use message index sort for sorting by Date
-    if ($this->index_sort && $this->sort_field == 'date')
+    // use message index sort as default sorting
+    if (!$this->sort_field)
       {
       if ($this->skip_deleted) {
         $a_index = $this->_search_index($mailbox, 'ALL');
@@ -1002,9 +1247,90 @@
 
 
   /**
+   * Return sorted array of threaded message IDs (not UIDs)
+   *
+   * @param string Mailbox to get index from
+   * @param string Sort column
+   * @param string Sort order [ASC, DESC]
+   * @return array Indexed array with message IDs
+   */
+  function thread_index($mbox_name='', $sort_field=NULL, $sort_order=NULL)
+    {
+    $this->_set_sort_order($sort_field, $sort_order);
+
+    $mailbox = $mbox_name ? $this->mod_mailbox($mbox_name) : $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->cache[$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->cache[$key] = $this->_flatten_threads($mailbox, $this->search_set['tree'], $ids);
+    }
+
+    // have stored it in RAM
+    if (isset($this->cache[$key]))
+      return $this->cache[$key];
+/*
+    // check local cache
+    $cache_key = $mailbox.'.msg';
+    $cache_status = $this->check_cache_status($mailbox, $cache_key);
+
+    // cache is OK
+    if ($cache_status>0)
+      {
+      $a_index = $this->get_message_cache_index($cache_key, TRUE, $this->sort_field, $this->sort_order);
+      return array_keys($a_index);
+      }
+*/
+    // get all threads (default sort order)
+    list ($thread_tree) = $this->_fetch_threads($mailbox);
+
+    $this->cache[$key] = $this->_flatten_threads($mailbox, $thread_tree);
+    
+    return $this->cache[$key];
+    }
+
+
+  /**
+   * Return array of threaded messages (all, not only roots)
+   *
+   * @param string Mailbox to get index from
+   * @param array  Threaded messages array (see _fetch_threads())
+   * @param array  Message IDs if we know what we need (e.g. search result)
+   *               for better performance
+   * @return array Indexed array with message IDs
+   *
    * @access private
    */
-  function sync_header_index($mailbox)
+  private function _flatten_threads($mailbox, $thread_tree, $ids=null)
+    {
+    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]))
+        $all_ids = array_merge($all_ids, array_keys_recursive($thread_tree[$root]));
+      }
+
+    return $all_ids;
+    }
+
+
+  /**
+   * @access private
+   */
+  private function sync_header_index($mailbox)
     {
     $cache_key = $mailbox.'.msg';
     $cache_index = $this->get_message_cache_index($cache_key);
@@ -1102,7 +1428,7 @@
       $results = $this->search($mbox_name, $res, NULL, $sort_field);
     }
 
-    $this->set_search_set($str, $results, $charset, $sort_field);
+    $this->set_search_set($str, $results, $charset, $sort_field, (bool)$this->threading);
 
     return $results;
     }
@@ -1122,7 +1448,17 @@
     if ($this->skip_deleted && !preg_match('/UNDELETED/', $criteria))
       $criteria = 'UNDELETED '.$criteria;
 
-    if ($sort_field && $this->get_capability('sort') && (!$this->index_sort || $sort_field != 'date')) {
+    if ($this->threading) {
+      list ($thread_tree, $msg_depth, $has_children) = iil_C_Thread($this->conn,
+            $mailbox, $this->threading, $criteria, $charset);
+
+      $a_messages = array(
+        'tree' 	=> $thread_tree,
+	'depth'	=> $msg_depth,
+	'children' => $has_children
+        );
+      }
+    else if ($sort_field && $this->get_capability('sort')) {
       $charset = $charset ? $charset : $this->default_charset;
       $a_messages = iil_C_Sort($this->conn, $mailbox, $sort_field, $criteria, FALSE, $charset);
       }
@@ -1135,7 +1471,7 @@
         $a_messages = iil_C_Search($this->conn, $mailbox, ($charset ? "CHARSET $charset " : '') . $criteria);
     
         // I didn't found that SEARCH always returns sorted IDs
-        if ($this->index_sort && $this->sort_field == 'date')
+        if (!$this->sort_field)
           sort($a_messages);
         }
       }
@@ -1149,6 +1485,90 @@
     
   
   /**
+   * Sort thread
+   *
+   * @param string Mailbox name
+   * @param  array Unsorted thread tree (iil_C_Thread() result)
+   * @param  array Message IDs if we know what we need (e.g. search result)
+   * @return array Sorted roots IDs
+   * @access private
+   */
+  private 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($thread_tree);
+      }
+    // here we'll implement REFS sorting, for performance reason
+    else { // ($sort_field == 'date' && $this->threading != 'REFS')
+      // use SORT command
+      if ($this->get_capability('sort')) {
+        $a_index = iil_C_Sort($this->conn, $mailbox, $this->sort_field,
+	    !empty($ids) ? $ids : ($this->skip_deleted ? 'UNDELETED' : ''));
+        }
+      else {
+        // fetch specified headers for all messages and sort them
+        $a_index = iil_C_FetchHeaderIndex($this->conn, $mailbox, !empty($ids) ? $ids : "1:*",
+	    $this->sort_field, $this->skip_deleted);
+        asort($a_index); // ASC
+	$a_index = array_values($a_index);
+        }
+
+	return $this->_sort_thread_refs($thread_tree, $a_index);
+      }
+    }
+
+
+  /**
+   * THREAD=REFS sorting implementation
+   *
+   * @param  array   Thread tree array (message identifiers as keys)
+   * @param  array   Array of sorted message identifiers
+   * @return array   Array of sorted roots messages
+   * @access private
+   */
+  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
@@ -1156,7 +1576,8 @@
   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_set = $this->search('', $this->search_string, $this->search_charset,
+    	    $this->search_sort_field, $this->search_threads);
       
     return $this->get_search_set();
     }
@@ -1760,7 +2181,7 @@
    * @return boolean True on success, False on error
    */
   function move_message($uids, $to_mbox, $from_mbox='')
-    {
+  {
     $fbox = $from_mbox;
     $tbox = $to_mbox;
     $to_mbox = $this->mod_mailbox($to_mbox);
@@ -1802,25 +2223,35 @@
     }
     // moving failed
     else if ($config->get('delete_always', false) && $tbox == $config->get('trash_mbox')) {
-      return $this->delete_message($a_uids, $fbox);
+      $moved = $this->delete_message($a_uids, $fbox);
     }
 
-    // remove message ids from search set
-    if ($moved && $this->search_set && $from_mbox == $this->mailbox) {
-      foreach ($a_uids as $uid)
-        $a_mids[] = $this->_uid2id($uid, $from_mbox);
-      $this->search_set = array_diff($this->search_set, $a_mids);
-    }
+    if ($moved) {
+      // unset threads internal cache
+      unset($this->icache['threads']);
 
-    // update cached message headers
-    $cache_key = $from_mbox.'.msg';
-    if ($moved && $start_index = $this->get_message_cache_index_min($cache_key, $a_uids)) {
-      // clear cache from the lowest index on
-      $this->clear_message_cache($cache_key, $start_index);
-      }
+      // remove message ids from search set
+      if ($this->search_set && $from_mbox == $this->mailbox) {
+        // threads are too complicated to just remove messages from set
+        if ($this->search_threads)
+          $this->refresh_search();
+        else {
+          foreach ($a_uids as $uid)
+            $a_mids[] = $this->_uid2id($uid, $from_mbox);
+          $this->search_set = array_diff($this->search_set, $a_mids);
+          }
+        }
+
+      // update cached message headers
+      $cache_key = $from_mbox.'.msg';
+      if ($start_index = $this->get_message_cache_index_min($cache_key, $a_uids)) {
+        // clear cache from the lowest index on
+        $this->clear_message_cache($cache_key, $start_index);
+       }
+    }
 
     return $moved;
-    }
+  }
 
 
   /**
@@ -1831,7 +2262,7 @@
    * @return boolean True on success, False on error
    */
   function delete_message($uids, $mbox_name='')
-    {
+  {
     $mailbox = $mbox_name ? $this->mod_mailbox($mbox_name) : $this->mailbox;
 
     // convert the list of uids to array
@@ -1843,31 +2274,38 @@
 
     $deleted = iil_C_Delete($this->conn, $mailbox, join(',', $a_uids));
 
-    // send expunge command in order to have the deleted message
-    // really deleted from the mailbox
-    if ($deleted)
-      {
+    if ($deleted) {
+      // send expunge command in order to have the deleted message
+      // really deleted from the mailbox
       $this->_expunge($mailbox, FALSE, $a_uids);
       $this->_clear_messagecount($mailbox);
       unset($this->uid_id_map[$mailbox]);
-      }
 
-    // remove message ids from search set
-    if ($deleted && $this->search_set && $mailbox == $this->mailbox) {
-      foreach ($a_uids as $uid)
-        $a_mids[] = $this->_uid2id($uid, $mailbox);
-      $this->search_set = array_diff($this->search_set, $a_mids);
+      // unset threads internal cache
+      unset($this->icache['threads']);
+      
+      // remove message ids from search set
+      if ($this->search_set && $mailbox == $this->mailbox) {
+        // threads are too complicated to just remove messages from set
+        if ($this->search_threads)
+          $this->refresh_search();
+        else {
+          foreach ($a_uids as $uid)
+            $a_mids[] = $this->_uid2id($uid, $mailbox);
+          $this->search_set = array_diff($this->search_set, $a_mids);
+          }
+        }
+
+      // remove deleted messages from cache
+      $cache_key = $mailbox.'.msg';
+      if ($start_index = $this->get_message_cache_index_min($cache_key, $a_uids)) {
+        // clear cache from the lowest index on
+        $this->clear_message_cache($cache_key, $start_index);
+      }
     }
-
-    // remove deleted messages from cache
-    $cache_key = $mailbox.'.msg';
-    if ($deleted && $start_index = $this->get_message_cache_index_min($cache_key, $a_uids)) {
-      // clear cache from the lowest index on
-      $this->clear_message_cache($cache_key, $start_index);
-      }
 
     return $deleted;
-    }
+  }
 
 
   /**
@@ -2422,13 +2860,11 @@
   private function get_message_cache($key, $from, $to, $sort_field, $sort_order)
     {
     $cache_key = "$key:$from:$to:$sort_field:$sort_order";
-    $db_header_fields = array('idx', 'uid', 'subject', 'from', 'to', 'cc', 'date', 'size');
     
     $config = rcmail::get_instance()->config;
 
-    // use idx sort for sorting by Date with index_sort=true or for unknown field
-    if (($sort_field == 'date' && $this->index_sort)
-      || !in_array($sort_field, $db_header_fields)) {
+    // use idx sort as default sorting
+    if (!$sort_field || !in_array($sort_field, $this->db_header_fields)) {
       $sort_field = 'idx';
       }
     
@@ -2465,9 +2901,9 @@
    */
   private function &get_cached_message($key, $uid)
     {
-    $internal_key = '__single_msg';
+    $internal_key = 'message';
     
-    if ($this->caching_enabled && !isset($this->cache[$internal_key][$uid]))
+    if ($this->caching_enabled && !isset($this->icache[$internal_key][$uid]))
       {
       $sql_result = $this->db->query(
         "SELECT idx, headers, structure
@@ -2482,13 +2918,13 @@
       if ($sql_arr = $this->db->fetch_assoc($sql_result))
         {
 	$this->uid_id_map[preg_replace('/\.msg$/', '', $key)][$uid] = $sql_arr['idx'];
-        $this->cache[$internal_key][$uid] = $this->db->decode(unserialize($sql_arr['headers']));
-        if (is_object($this->cache[$internal_key][$uid]) && !empty($sql_arr['structure']))
-          $this->cache[$internal_key][$uid]->structure = $this->db->decode(unserialize($sql_arr['structure']));
+        $this->icache[$internal_key][$uid] = $this->db->decode(unserialize($sql_arr['headers']));
+        if (is_object($this->icache[$internal_key][$uid]) && !empty($sql_arr['structure']))
+          $this->icache[$internal_key][$uid]->structure = $this->db->decode(unserialize($sql_arr['structure']));
         }
       }
 
-    return $this->cache[$internal_key][$uid];
+    return $this->icache[$internal_key][$uid];
     }
 
   /**
@@ -2505,8 +2941,8 @@
     if (!empty($sa_message_index[$key]) && !$force)
       return $sa_message_index[$key];
 
-    // use idx sort for sorting by Date with index_sort=true
-    if ($sort_field == 'date' && $this->index_sort)
+    // use idx sort as default
+    if (!$sort_field || !in_array($sort_field, $this->db_header_fields))
       $sort_field = 'idx';
     
     $sa_message_index[$key] = array();
@@ -2534,8 +2970,8 @@
         return;
 
     // add to internal (fast) cache
-    $this->cache['__single_msg'][$headers->uid] = clone $headers;
-    $this->cache['__single_msg'][$headers->uid]->structure = $struct;
+    $this->icache['message'][$headers->uid] = clone $headers;
+    $this->icache['message'][$headers->uid]->structure = $struct;
 
     // no further caching
     if (!$this->caching_enabled)
diff --git a/program/include/rcube_shared.inc b/program/include/rcube_shared.inc
index f4f23a2..a130391 100644
--- a/program/include/rcube_shared.inc
+++ b/program/include/rcube_shared.inc
@@ -608,6 +608,26 @@
 
 
 /**
+ * Get all keys from array (recursive)
+ * 
+ * @param array Input array
+ * @return array
+ */
+function array_keys_recursive($array)
+{
+  $keys = array();
+  
+  if (!empty($array))
+    foreach ($array as $key => $child) {
+      $keys[] = $key;
+      if ($children = array_keys_recursive($child))
+        $keys = array_merge($keys, $children);
+    }
+  return $keys;
+}
+
+
+/**
  * mbstring replacement functions
  */
 
diff --git a/program/include/rcube_user.php b/program/include/rcube_user.php
index 835ffe9..6ed16db 100644
--- a/program/include/rcube_user.php
+++ b/program/include/rcube_user.php
@@ -111,19 +111,22 @@
       if (!isset($old_prefs[$key]) && ($value == $config->get($key)))
         unset($save_prefs[$key]);
     }
-    
+
+    $save_prefs = serialize($save_prefs);
+
     $this->db->query(
       "UPDATE ".get_table_name('users')."
        SET    preferences=?,
               language=?
        WHERE  user_id=?",
-      serialize($save_prefs),
+      $save_prefs,
       $_SESSION['language'],
       $this->ID);
 
     $this->language = $_SESSION['language'];
     if ($this->db->affected_rows()) {
       $config->set_user_prefs($a_user_prefs);
+      $this->data['preferences'] = $save_prefs;
       return true;
     }
 
diff --git a/program/js/app.js b/program/js/app.js
index abad6ec..a5114fa 100644
--- a/program/js/app.js
+++ b/program/js/app.js
@@ -3,11 +3,12 @@
  | RoundCube Webmail Client Script                                       |
  |                                                                       |
  | This file is part of the RoundCube Webmail client                     |
- | Copyright (C) 2005-2009, RoundCube Dev, - Switzerland                 |
+ | Copyright (C) 2005-2010, RoundCube Dev, - Switzerland                 |
  | Licensed under the GNU GPL                                            |
  |                                                                       |
  +-----------------------------------------------------------------------+
  | Authors: Thomas Bruederli <roundcube@gmail.com>                       |
+ |          Aleksander 'A.L.E.C' Machniak <alec@alec.pl>                 |
  |          Charles McNulty <charles@charlesmcnulty.com>                 |
  +-----------------------------------------------------------------------+
  | Requires: jquery.js, common.js, list.js                               |
@@ -160,9 +161,13 @@
     switch (this.task)
       {
       case 'mail':
+        // enable mail commands
+        this.enable_command('list', 'checkmail', 'compose', 'add-contact', 'search', 'reset-search', 'collapse-folder', true);
+      
         if (this.gui_objects.messagelist)
           {
-          this.message_list = new rcube_list_widget(this.gui_objects.messagelist, {multiselect:true, draggable:true, keyboard:true, dblclick_time:this.dblclick_time});
+          this.message_list = new rcube_list_widget(this.gui_objects.messagelist,
+            {multiselect:true, multiexpand:true, draggable:true, keyboard:true, dblclick_time:this.dblclick_time});
           this.message_list.row_init = function(o){ p.init_message_row(o); };
           this.message_list.addEventListener('dblclick', function(o){ p.msglist_dbl_click(o); });
           this.message_list.addEventListener('keypress', function(o){ p.msglist_keypress(o); });
@@ -170,22 +175,22 @@
           this.message_list.addEventListener('dragstart', function(o){ p.drag_start(o); });
           this.message_list.addEventListener('dragmove', function(e){ p.drag_move(e); });
           this.message_list.addEventListener('dragend', function(e){ p.drag_end(e); });
+          this.message_list.addEventListener('expandcollapse', function(e){ p.msglist_expand(e); });
           document.onmouseup = function(e){ return p.doc_mouse_up(e); };
 
+          this.set_message_coltypes(this.env.coltypes);
           this.message_list.init();
-          this.enable_command('toggle_status', 'toggle_flag', true);
+          this.enable_command('toggle_status', 'toggle_flag', 'menu-open', 'menu-save', true);
           
           if (this.gui_objects.mailcontframe)
             this.gui_objects.mailcontframe.onmousedown = function(e){ return p.click_on_list(e); };
           else
             this.message_list.focus();
-          }
           
-        if (this.env.coltypes)
-          this.set_message_coltypes(this.env.coltypes);
-
-        // enable mail commands
-        this.enable_command('list', 'checkmail', 'compose', 'add-contact', 'search', 'reset-search', 'collapse-folder', true);
+          // load messages
+          if (this.env.messagecount)
+            this.command('list');
+          }
 
         if (this.env.search_text != null && document.getElementById('quicksearchbox') != null)
           document.getElementById('quicksearchbox').value = this.env.search_text;
@@ -243,8 +248,10 @@
           this.init_messageform();
           }
 
-        if (this.env.messagecount)
+        if (this.env.messagecount) {
           this.enable_command('select-all', 'select-none', 'expunge', true);
+          this.enable_command('expand-all', 'expand-unread', 'collapse-all', this.env.threading);
+        }
 
         if (this.purge_mailbox_test())
           this.enable_command('purge', true);
@@ -333,7 +340,7 @@
           this.enable_command('save', 'delete', 'edit', true);
         }
         else if (this.env.action=='folders')
-          this.enable_command('subscribe', 'unsubscribe', 'create-folder', 'rename-folder', 'delete-folder', true);
+          this.enable_command('subscribe', 'unsubscribe', 'create-folder', 'rename-folder', 'delete-folder', 'enable-threading', 'disable-threading', true);
 
         if (this.gui_objects.identitieslist)
           {
@@ -351,6 +358,7 @@
           this.sections_list.addEventListener('select', function(o){ p.section_select(o); });
           this.sections_list.init();
           this.sections_list.focus();
+          this.sections_list.select_first();
         }
         else if (this.gui_objects.subscriptionlist)
           this.init_subscription_list();
@@ -403,106 +411,6 @@
     // start keep-alive interval
     this.start_keepalive();
   };
-
-  // start interval for keep-alive/recent_check signal
-  this.start_keepalive = function()
-    {
-    if (this.env.keep_alive && !this.env.framed && this.task=='mail' && this.gui_objects.mailboxlist)
-      this._int = setInterval(function(){ ref.check_for_recent(false); }, this.env.keep_alive * 1000);
-    else if (this.env.keep_alive && !this.env.framed && this.task!='login')
-      this._int = setInterval(function(){ ref.send_keep_alive(); }, this.env.keep_alive * 1000);
-    }
-
-  this.init_message_row = function(row)
-  {
-    var uid = row.uid;
-    if (uid && this.env.messages[uid])
-      {
-      row.deleted = this.env.messages[uid].deleted ? true : false;
-      row.unread = this.env.messages[uid].unread ? true : false;
-      row.replied = this.env.messages[uid].replied ? true : false;
-      row.flagged = this.env.messages[uid].flagged ? true : false;
-      row.forwarded = this.env.messages[uid].forwarded ? true : false;
-      }
-
-    // set eventhandler to message icon
-    if (row.icon = row.obj.getElementsByTagName('td')[0].getElementsByTagName('img')[0])
-      {
-      var p = this;
-      row.icon.id = 'msgicn_'+row.uid;
-      row.icon._row = row.obj;
-      row.icon.onmousedown = function(e) { p.command('toggle_status', this); };
-      }
-
-    // global variable 'flagged_col' may be not defined yet
-    if (!this.env.flagged_col && this.env.coltypes)
-      {
-      var found;
-      if((found = find_in_array('flag', this.env.coltypes)) >= 0)
-        this.set_env('flagged_col', found+1);
-      }
-
-    // set eventhandler to flag icon, if icon found
-    if (this.env.flagged_col && (row.flagged_icon = row.obj.getElementsByTagName('td')[this.env.flagged_col].getElementsByTagName('img')[0]))
-      {
-      var p = this;
-      row.flagged_icon.id = 'flaggedicn_'+row.uid;
-      row.flagged_icon._row = row.obj;
-      row.flagged_icon.onmousedown = function(e) { p.command('toggle_flag', this); };
-      }
-      
-    this.triggerEvent('insertrow', { uid:uid, row:row });
-  };
-
-  // init message compose form: set focus and eventhandlers
-  this.init_messageform = function()
-    {
-    if (!this.gui_objects.messageform)
-      return false;
-    
-    //this.messageform = this.gui_objects.messageform;
-    var input_from = $("[name='_from']");
-    var input_to = $("[name='_to']");
-    var input_subject = $("input[name='_subject']");
-    var input_message = $("[name='_message']").get(0);
-    var html_mode = $("input[name='_is_html']").val() == '1';
-
-    // init live search events
-    this.init_address_input_events(input_to);
-    this.init_address_input_events($("[name='_cc']"));
-    this.init_address_input_events($("[name='_bcc']"));
-    
-    if (!html_mode)
-      this.set_caret_pos(input_message, this.env.top_posting ? 0 : $(input_message).val().length);
-
-    // add signature according to selected identity
-    if (input_from.attr('type') == 'select-one' && $("input[name='_draft_saveid']").val() == ''
-        && !html_mode) {  // if we have HTML editor, signature is added in callback
-      this.change_identity(input_from[0]);
-    }
-    else if (!html_mode) 
-      this.set_caret_pos(input_message, this.env.top_posting ? 0 : $(input_message).val().length);
-
-    if (input_to.val() == '')
-      input_to.focus();
-    else if (input_subject.val() == '')
-      input_subject.focus();
-    else if (input_message && !html_mode)
-      input_message.focus();
-
-    // get summary of all field values
-    this.compose_field_hash(true);
- 
-    // start the auto-save timer
-    this.auto_save_start();
-    };
-
-  this.init_address_input_events = function(obj)
-    {
-    var handler = function(e){ return ref.ksearch_keypress(e,this); };
-    obj.bind((bw.safari || bw.ie ? 'keydown' : 'keypress'), handler);
-    obj.attr('autocomplete', 'off');
-    };
 
 
   /*********************************************************/
@@ -580,6 +488,12 @@
           parent.location.href = this.env.permaurl;
         break;
 
+      case 'menu-open':
+      case 'menu-save':
+	this.triggerEvent(command, {props:props});
+	return false;
+        break;
+
       case 'open':
         var uid;
         if (uid = this.get_single_uid())
@@ -625,13 +539,8 @@
         else
           sort_order = 'ASC';
 
-        // set table header class
-        $('#rcm'+this.env.sort_col).removeClass('sorted'+(this.env.sort_order.toUpperCase()));
-        $('#rcm'+sort_col).addClass('sorted'+sort_order);
-
-        // save new sort properties
-        this.env.sort_col = sort_col;
-        this.env.sort_order = sort_order;
+        // set table header and update env
+        this.set_list_sorting(sort_col, sort_order);
 
         // reload message list
         this.list_mailbox('', '', sort_col+'_'+sort_order);
@@ -756,7 +665,6 @@
           this.delete_identity();
         break;
 
-
       // mail task commands
       case 'move':
       case 'moveto':
@@ -849,6 +757,22 @@
 
       case 'select-none':
         this.message_list.clear_selection();
+        break;
+
+      case 'expand-all':
+        this.env.autoexpand_threads = 1;
+        this.message_list.expand_all();
+        break;
+
+      case 'expand-unread':
+        this.env.autoexpand_threads = 2;
+        this.message_list.collapse_all();
+        this.expand_unread();
+        break;
+
+      case 'collapse-all':
+        this.env.autoexpand_threads = 0;
+        this.message_list.collapse_all();
         break;
 
       case 'nextmessage':
@@ -1116,7 +1040,15 @@
       case 'unsubscribe':
         this.unsubscribe_folder(props);
         break;
-        
+
+      case 'enable-threading':
+        this.enable_threading(props);
+        break;
+
+      case 'disable-threading':
+        this.disable_threading(props);
+        break;
+
       case 'create-folder':
         this.create_folder(props);
         break;
@@ -1450,7 +1382,7 @@
     if (this.preview_timer)
       clearTimeout(this.preview_timer);
 
-    var selected = list.selection.length==1;
+    var selected = list.get_single_selection() != null;
 
     // Hide certain command buttons when Drafts folder is selected
     if (this.env.mailbox == this.env.drafts_mailbox)
@@ -1505,6 +1437,12 @@
       this.show_contentframe(false);
   };
   
+  this.msglist_expand = function(row)
+  {
+    if (this.env.messages[row.uid])
+      this.env.messages[row.uid].expanded = row.expanded;
+  };
+  
   this.check_droptarget = function(id)
   {
     if (this.task == 'mail')
@@ -1519,6 +1457,217 @@
   /*********************************************************/
   /*********     (message) list functionality      *********/
   /*********************************************************/
+
+  this.init_message_row = function(row)
+  {
+    var self = this;
+    var uid = row.uid;
+    
+    if (uid && this.env.messages[uid])
+      $.extend(row, this.env.messages[uid]);
+
+    // set eventhandler to message icon
+    if (this.env.subject_col != null && (row.icon = document.getElementById('msgicn'+row.uid))) {
+      row.icon._row = row.obj;
+      row.icon.onmousedown = function(e) { self.command('toggle_status', this); };
+    }
+
+    // set eventhandler to flag icon, if icon found
+    if (this.env.flagged_col != null && (row.flagged_icon = document.getElementById('flaggedicn'+row.uid))) {
+      row.flagged_icon._row = row.obj;
+      row.flagged_icon.onmousedown = function(e) { self.command('toggle_flag', this); };
+    }
+
+    var expando;
+    if (!row.depth && row.has_children && (expando = document.getElementById('rcmexpando'+row.uid))) {
+      expando.onmousedown = function(e) { return self.expand_message_row(e, uid); };
+    }
+
+    this.triggerEvent('insertrow', { uid:uid, row:row });
+  };
+
+  // create a table row in the message list
+  this.add_message_row = function(uid, cols, flags, attop)
+  {
+    if (!this.gui_objects.messagelist || !this.message_list)
+      return false;
+
+    if (this.message_list.background)
+      var tbody = this.message_list.background;
+    else
+      var tbody = this.gui_objects.messagelist.tBodies[0];
+    
+    var rows = this.message_list.rows;
+    var rowcount = tbody.rows.length;
+    var even = rowcount%2;
+    
+    if (!this.env.messages[uid])
+      this.env.messages[uid] = {};
+    
+    // merge flags over local message object
+    $.extend(this.env.messages[uid], {
+      deleted: flags.deleted?1:0,
+      replied: flags.replied?1:0,
+      unread: flags.unread?1:0,
+      forwarded: flags.forwarded?1:0,
+      flagged: flags.flagged?1:0,
+      has_children: flags.has_children?1:0,
+      depth: flags.depth?flags.depth:0,
+      unread_children: flags.unread_children,
+      parent_uid: flags.parent_uid
+    });
+
+    var message = this.env.messages[uid];
+
+    var css_class = 'message'
+        + (even ? ' even' : ' odd')
+        + (flags.unread ? ' unread' : '')
+        + (flags.deleted ? ' deleted' : '')
+        + (flags.flagged ? ' flagged' : '')
+        + (flags.unread_children && !flags.unread ? ' unroot' : '')
+        + (this.message_list.in_selection(uid) ? ' selected' : '');
+
+    // for performance use DOM instead of jQuery here
+    var row = document.createElement('tr');
+    row.id = 'rcmrow'+uid;
+    row.className = css_class;
+    
+    var icon = this.env.messageicon;
+    if (!flags.unread && flags.unread_children > 0 && this.env.unreadchildrenicon)
+      icon = this.env.unreadchildrenicon;
+    else if (flags.deleted && this.env.deletedicon)
+      icon = this.env.deletedicon;
+    else if (flags.replied && this.env.repliedicon)
+      {
+      if (flags.forwarded && this.env.forwardedrepliedicon)
+        icon = this.env.forwardedrepliedicon;
+      else
+        icon = this.env.repliedicon;
+      }
+    else if (flags.forwarded && this.env.forwardedicon)
+      icon = this.env.forwardedicon;
+    else if(flags.unread && this.env.unreadicon)
+      icon = this.env.unreadicon;
+ 
+    var tree = expando = '';
+
+    if (this.env.threading)
+      {
+      // This assumes that div width is hardcoded to 15px,
+      var width = message.depth * 15;
+      if (message.depth) {
+        if ((this.env.autoexpand_threads == 0 || this.env.autoexpand_threads == 2) &&
+            (!rows[message.parent_uid] || !rows[message.parent_uid].expanded)) {
+          row.style.display = 'none';
+          message.expanded = false;
+        }
+        else
+          message.expanded = true;
+      }
+      else if (message.has_children) {
+        if (typeof(message.expanded) == 'undefined' && (this.env.autoexpand_threads == 1 || (this.env.autoexpand_threads == 2 && message.unread_children))) {
+          message.expanded = true;
+        }
+      }
+
+      if (width)
+        tree += '<span id="rcmtab' + uid + '" class="branch" style="width:' + width + 'px;">&nbsp;&nbsp;</span>';
+
+      if (message.has_children && !message.depth)
+        expando = '<div id="rcmexpando' + uid + '" class="' + (message.expanded ? 'expanded' : 'collapsed') + '">&nbsp;&nbsp;</div>';
+      }
+
+    tree += icon ? '<img id="msgicn'+uid+'" src="'+icon+'" alt="" class="msgicon" />' : '';
+    
+    // first col is always there
+    var col = document.createElement('td');
+    col.className = 'threads';
+    col.innerHTML = expando;
+    row.appendChild(col);
+    
+    // build subject link 
+    if (!bw.ie && cols.subject) {
+      var action = flags.mbox == this.env.drafts_mailbox ? 'compose' : 'show';
+      var uid_param = flags.mbox == this.env.drafts_mailbox ? '_draft_uid' : '_uid';
+      cols.subject = '<a href="./?_task=mail&_action='+action+'&_mbox='+urlencode(flags.mbox)+'&'+uid_param+'='+uid+'"'+
+        ' onclick="return rcube_event.cancel(event)">'+cols.subject+'</a>';
+    }
+
+    // add each submitted col
+    for (var n = 0; n < this.env.coltypes.length; n++) {
+      var c = this.env.coltypes[n];
+      col = document.createElement('td');
+      col.className = String(c).toLowerCase();
+
+      var html;
+      if (c=='flag') {
+        if (flags.flagged && this.env.flaggedicon)
+          html = '<img id="flaggedicn'+uid+'" src="'+this.env.flaggedicon+'" class="flagicon" alt="" />';
+        else if(!flags.flagged && this.env.unflaggedicon)
+          html = '<img id="flaggedicn'+uid+'" src="'+this.env.unflaggedicon+'" class="flagicon" alt="" />';
+      }
+      else if (c=='attachment')
+        html = flags.attachment && this.env.attachmenticon ? '<img src="'+this.env.attachmenticon+'" alt="" />' : '&nbsp;';
+      else if (c=='subject')
+        html = tree + cols[c];
+      else
+        html = cols[c];
+
+      col.innerHTML = html;
+
+      row.appendChild(col);
+    }
+
+    this.message_list.insert_row(row, attop);
+
+    // remove 'old' row
+    if (attop && this.env.pagesize && this.message_list.rowcount > this.env.pagesize) {
+      var uid = this.message_list.get_last_row();
+      this.message_list.remove_row(uid);
+      this.message_list.clear_selection(uid);
+    }
+  };
+
+  // messages list handling in background (for performance)
+  this.offline_message_list = function(flag)
+    {
+      if (this.message_list)
+      	this.message_list.set_background_mode(flag);
+    };
+
+  this.set_list_sorting = function(sort_col, sort_order)
+    {
+    // set table header class
+    $('#rcm'+this.env.sort_col).removeClass('sorted'+(this.env.sort_order.toUpperCase()));
+    if (sort_col)
+      $('#rcm'+sort_col).addClass('sorted'+sort_order);
+    
+    this.env.sort_col = sort_col;
+    this.env.sort_order = sort_order;
+    }
+
+  this.set_list_options = function(cols, sort_col, sort_order, threads)
+    {
+    var update, add_url = '';
+
+    if (this.env.sort_col != sort_col || this.env.sort_order != sort_order) {
+      update = 1;
+      this.set_list_sorting(sort_col, sort_order);
+      }
+    
+    if (this.env.threading != threads) {
+      update = 1;
+      add_url += '&_threads=' + threads;     
+      }
+
+    if (cols.join() != this.env.coltypes.join()) {
+      update = 1;
+      add_url += '&_cols=' + cols.join(',');
+      }
+
+    if (update)
+      this.list_mailbox('', '', sort_col+'_'+sort_order, add_url);
+    }
 
   // when user doble-clicks on a row
   this.show_message = function(id, safe, preview)
@@ -1554,6 +1703,7 @@
       if (action == 'preview' && this.message_list && this.message_list.rows[id] && this.message_list.rows[id].unread)
         {
         this.set_message(id, 'unread', false);
+        this.update_thread_root(id, 'read');
         if (this.env.unread_counts[this.env.mailbox])
           {
           this.env.unread_counts[this.env.mailbox] -= 1;
@@ -1621,23 +1771,25 @@
           + (this.env.mailbox ? '&_mbox='+urlencode(this.env.mailbox) : ''), true);
     }
 
-
   // list messages of a specific mailbox
-  this.list_mailbox = function(mbox, page, sort)
+  this.list_mailbox = function(mbox, page, sort, add_url)
     {
-    var add_url = '';
+    var url = '';
     var target = window;
 
     if (!mbox)
       mbox = this.env.mailbox;
 
+    if (add_url)
+      url += add_url;
+
     // add sort to url if set
     if (sort)
-      add_url += '&_sort=' + sort;
+      url += '&_sort=' + sort;
 
     // also send search request to get the right messages
     if (this.env.search_request)
-      add_url += '&_search='+this.env.search_request;
+      url += '&_search='+this.env.search_request;
 
     // set page=1 if changeing to another mailbox
     if (!page && this.env.mailbox != mbox)
@@ -1648,7 +1800,7 @@
       }
 
     if (mbox != this.env.mailbox || (mbox == this.env.mailbox && !page && !sort))
-      add_url += '&_refresh=1';
+      url += '&_refresh=1';
 
     // unselect selected messages
     this.last_selected = 0;
@@ -1661,21 +1813,21 @@
     // load message list remotely
     if (this.gui_objects.messagelist)
       {
-      this.list_mailbox_remote(mbox, page, add_url);
+      this.list_mailbox_remote(mbox, page, url);
       return;
       }
     
     if (this.env.contentframe && window.frames && window.frames[this.env.contentframe])
       {
       target = window.frames[this.env.contentframe];
-      add_url += '&_framed=1';
+      url += '&_framed=1';
       }
 
     // load message list to target frame/window
     if (mbox)
       {
       this.set_busy(true, 'loading');
-      target.location.href = this.env.comm_path+'&_mbox='+urlencode(mbox)+(page ? '&_page='+page : '')+add_url;
+      target.location.href = this.env.comm_path+'&_mbox='+urlencode(mbox)+(page ? '&_page='+page : '')+url;
       }
     };
 
@@ -1691,53 +1843,177 @@
     this.http_request('list', url+add_url, true);
     };
 
-  this.expunge_mailbox = function(mbox)
+  // expand all threads with unread children
+  this.expand_unread = function()
     {
-    var lock = false;
-    var add_url = '';
+    var tbody = this.gui_objects.messagelist.tBodies[0];
+    var new_row = tbody.firstChild;
+    var r;
     
-    // lock interface if it's the active mailbox
-    if (mbox == this.env.mailbox)
-       {
-       lock = true;
-       this.set_busy(true, 'loading');
-       add_url = '&_reload=1';
-       }
-
-    // send request to server
-    var url = '_mbox='+urlencode(mbox);
-    this.http_post('expunge', url+add_url, lock);
+    while (new_row) {
+      if (new_row.nodeType == 1 && (r = this.message_list.rows[new_row.uid])
+	    && r.unread_children) {
+	this.message_list.expand_all(r);
+	var expando = document.getElementById('rcmexpando' + r.uid);
+	if (expando)
+	  expando.className = 'expanded';
+	this.set_unread_children(r.uid);
+        }
+      new_row = new_row.nextSibling;
+      }
+    return false;
     };
 
-  this.purge_mailbox = function(mbox)
+  // thread expanding/collapsing handler    
+  this.expand_message_row = function(e, uid)
     {
-    var lock = false;
-    var add_url = '';
-    
-    if (!confirm(this.get_label('purgefolderconfirm')))
-      return false;
-    
-    // lock interface if it's the active mailbox
-    if (mbox == this.env.mailbox)
-       {
-       lock = true;
-       this.set_busy(true, 'loading');
-       add_url = '&_reload=1';
-       }
+    var row = this.message_list.rows[uid];
 
-    // send request to server
-    var url = '_mbox='+urlencode(mbox);
-    this.http_post('purge', url+add_url, lock);
-    return true;
+    // handle unread_children mark
+    row.expanded = !row.expanded;
+    this.set_unread_children(uid);
+    row.expanded = !row.expanded;
+
+    this.message_list.expand_row(e, uid);
     };
 
-  // test if purge command is allowed
-  this.purge_mailbox_test = function()
+  // message list expanding
+  this.expand_threads = function()
+    {    
+    if (!this.env.threading || !this.env.autoexpand_threads || !this.message_list)
+      return;
+    
+    switch (this.env.autoexpand_threads) {
+      case 2: this.expand_unread(); break;
+      case 1: this.message_list.expand_all(); break;
+      }
+    //  this.message_list.expand(null);
+    }
+
+  // update parent in a thread
+  this.update_thread_root = function(uid, flag)
   {
-    return (this.env.messagecount && (this.env.mailbox == this.env.trash_mailbox || this.env.mailbox == this.env.junk_mailbox 
-      || this.env.mailbox.match('^' + RegExp.escape(this.env.trash_mailbox) + RegExp.escape(this.env.delimiter)) 
-      || this.env.mailbox.match('^' + RegExp.escape(this.env.junk_mailbox) + RegExp.escape(this.env.delimiter))));
+    if (!this.env.threading)
+      return;
+
+    var root = this.find_thread_root(uid);
+    
+    if (uid == root)
+      return;
+
+    var p = this.message_list.rows[root];
+
+    if (flag == 'read' && p.unread_children) {
+      p.unread_children--;
+    } else if (flag == 'unread' && p.has_children) {
+      // unread_children may be undefined
+      p.unread_children = p.unread_children ? p.unread_children + 1 : 1;
+    } else {
+      return;
+    }
+
+    this.set_message_icon(root);
+    this.set_unread_children(root);
   };
+
+  // finds root message for specified thread
+  this.find_thread_root = function(uid)
+  {
+    var r = this.message_list.rows[uid];
+  
+    if (r.parent_uid)
+      return this.find_thread_root(r.parent_uid);
+    else
+      return uid;
+  }
+
+  // update thread indicators for all messages in a thread below the specified message
+  // return number of removed/added root level messages
+  this.update_thread = function (uid)
+  {
+    if (!this.env.threading)
+      return 0;
+
+    var rows = this.message_list.rows;
+    var row = rows[uid]
+    var depth = rows[uid].depth;
+    var r, parent, count = 0;
+    var roots = new Array();
+
+    if (!row.depth) // root message: decrease roots count
+      count--;
+    else if (row.unread) {
+      // update unread_children for thread root
+      var parent = this.find_thread_root(uid);
+      rows[parent].unread_children--;
+      this.set_unread_children(parent);
+      }
+
+    parent = row.parent_uid;
+
+    // childrens
+    row = row.obj.nextSibling;
+    while (row) {
+      if (row.nodeType == 1 && (r = rows[row.uid])) {
+	if (!r.depth || r.depth <= depth)
+	  break;
+
+	r.depth--; // move left
+	$('#rcmtab'+r.uid).width(r.depth * 15);
+        if (!r.depth) { // a new root
+	  count++; // increase roots count
+	  r.parent_uid = 0;
+	  if (r.has_children) {
+	    // replace 'leaf' with 'collapsed'
+	    $('#rcmrow'+r.uid+' '+'.leaf:first')
+              .attr('id', 'rcmexpando' + r.uid)
+	      .attr('class', (r.obj.style.display != 'none' ? 'expanded' : 'collapsed'))
+    	      .bind('mousedown', {uid:r.uid, p:this},
+	        function(e) { return e.data.p.expand_message_row(e, e.data.uid); });
+
+	    r.unread_children = 0;
+	    roots[roots.length] = r;
+	    }
+	  // show if it was hidden
+	  if (r.obj.style.display == 'none')
+	    $(r.obj).show();
+	  }
+	else {
+	  if (r.depth == depth)
+	    r.parent_uid = parent;
+	  if (r.unread && roots.length) {
+	    roots[roots.length-1].unread_children++;
+	    }
+	  }
+	}
+	row = row.nextSibling;
+      }
+    
+    // update unread_children for roots
+    for (var i=0; i<roots.length; i++)
+      this.set_unread_children(roots[i].uid);
+
+    return count;
+  };
+
+  this.delete_excessive_thread_rows = function()
+  {
+    var rows = this.message_list.rows;    
+    var tbody = this.message_list.list.tBodies[0];
+    var row = tbody.firstChild;
+    var cnt = this.env.pagesize + 1;
+    
+    while (row) {
+      if (row.nodeType == 1 && (r = rows[row.uid])) {
+	if (!r.depth && cnt)
+	  cnt--;
+
+        if (!cnt)
+	  this.message_list.remove_row(row.uid);
+	}
+	row = row.nextSibling;
+      }
+  }
 
   // set message icon
   this.set_message_icon = function(uid)
@@ -1747,8 +2023,10 @@
 
     if (!rows[uid])
       return false;
-
-    if (rows[uid].deleted && this.env.deletedicon)
+    if (!rows[uid].unread && rows[uid].unread_children && this.env.unreadchildrenicon) {
+      icn_src = this.env.unreadchildrenicon;
+    }
+    else if (rows[uid].deleted && this.env.deletedicon)
       icn_src = this.env.deletedicon;
     else if (rows[uid].replied && this.env.repliedicon)
       {
@@ -1773,7 +2051,6 @@
       icn_src = this.env.flaggedicon;
     else if (!rows[uid].flagged && this.env.unflaggedicon)
       icn_src = this.env.unflaggedicon;
-
     if (rows[uid].flagged_icon && icn_src)
       rows[uid].flagged_icon.src = icn_src;
   }
@@ -1796,7 +2073,7 @@
     else if (flag == 'flagged')
       rows[uid].flagged = status;
 
-    this.env.messages[uid] = rows[uid];
+//    this.env.messages[uid] = rows[uid];
     }
 
   // set message row status, class and icon
@@ -1808,43 +2085,41 @@
     
     if (flag)
       this.set_message_status(uid, flag, status);
-    
+
     var rowobj = $(rows[uid].obj);
-    if (rows[uid].unread && rows[uid].classname.indexOf('unread')<0)
-      {
-      rows[uid].classname += ' unread';
+
+    if (rows[uid].unread && !rowobj.hasClass('unread'))
       rowobj.addClass('unread');
-      }
-    else if (!rows[uid].unread && rows[uid].classname.indexOf('unread')>=0)
-      {
-      rows[uid].classname = rows[uid].classname.replace(/\s*unread/, '');
+    else if (!rows[uid].unread && rowobj.hasClass('unread'))
       rowobj.removeClass('unread');
-      }
     
-    if (rows[uid].deleted && rows[uid].classname.indexOf('deleted')<0)
-      {
-      rows[uid].classname += ' deleted';
+    if (rows[uid].deleted && !rowobj.hasClass('deleted'))
       rowobj.addClass('deleted');
-      }
-    else if (!rows[uid].deleted && rows[uid].classname.indexOf('deleted')>=0)
-      {
-      rows[uid].classname = rows[uid].classname.replace(/\s*deleted/, '');
+    else if (!rows[uid].deleted && rowobj.hasClass('deleted'))
       rowobj.removeClass('deleted');
-      }
 
-    if (rows[uid].flagged && rows[uid].classname.indexOf('flagged')<0)
-      {
-      rows[uid].classname += ' flagged';
+    if (rows[uid].flagged && !rowobj.hasClass('flagged'))
       rowobj.addClass('flagged');
-      }
-    else if (!rows[uid].flagged && rows[uid].classname.indexOf('flagged')>=0)
-      {
-      rows[uid].classname = rows[uid].classname.replace(/\s*flagged/, '');
+    else if (!rows[uid].flagged && rowobj.hasClass('flagged'))
       rowobj.removeClass('flagged');
-      }
 
+    this.set_unread_children(uid);
     this.set_message_icon(uid);
-    }
+    };
+
+  // sets unroot (unread_children) class of parent row
+  this.set_unread_children = function(uid)
+    {
+    var row = this.message_list.rows[uid];
+    
+    if (row.parent_uid || !row.has_children)
+      return;
+
+    if (!row.unread && row.unread_children && !row.expanded)
+      $(row.obj).addClass('unroot');
+    else
+      $(row.obj).removeClass('unroot');
+    };
 
   // move selected messages to the specified mailbox
   this.move_messages = function(mbox)
@@ -1881,8 +2156,10 @@
       return;
 
     // if config is set to flag for deletion
-    if (this.env.flag_for_deletion)
+    if (this.env.flag_for_deletion) {
       this.mark_message('delete');
+      return false;
+      }
     // if there isn't a defined trash mailbox or we are in it
     else if (!this.env.trash_mailbox || this.env.mailbox == this.env.trash_mailbox) 
       this.permanently_remove_messages();
@@ -1897,6 +2174,8 @@
       else
         this.move_messages(this.env.trash_mailbox);
       }
+
+    return true;
   };
 
   // delete the selected messages permanently
@@ -1910,22 +2189,23 @@
     this._with_selected_messages('delete', false, '&_from='+(this.env.action ? this.env.action : ''));
     };
 
-  // Send a specifc request with UIDs of all selected messages
+  // Send a specifc moveto/delete request with UIDs of all selected messages
   // @private
-  this._with_selected_messages = function(action, lock, add_url, remove)
+  this._with_selected_messages = function(action, lock, add_url)
   {
     var a_uids = new Array();
+    var count = 0;
 
     if (this.env.uid)
       a_uids[0] = this.env.uid;
     else
     {
       var selection = this.message_list.get_selection();
-      var rows = this.message_list.rows;
       var id;
       for (var n=0; n<selection.length; n++) {
         id = selection[n];
         a_uids[a_uids.length] = id;
+	count += this.update_thread(id);
         this.message_list.remove_row(id, (this.env.display_next && n == selection.length-1));
       }
       // make sure there are no selected rows
@@ -1939,6 +2219,12 @@
 
     if (this.env.display_next && this.env.next_uid)
       add_url += '&_next_uid='+this.env.next_uid;
+
+    if (count < 0)
+      add_url += '&_count='+(count*-1);
+    else if (count > 0) 
+      // remove threads from the end of the list
+      this.delete_excessive_thread_rows();
 
     // send request to server
     this.http_post(action, '_uid='+a_uids.join(',')+'&_mbox='+urlencode(this.env.mailbox)+add_url, lock);
@@ -2009,6 +2295,9 @@
       this.set_message(a_uids[i], 'unread', (flag=='unread' ? true : false));
 
     this.http_post('mark', '_uid='+a_uids.join(',')+'&_flag='+flag);
+
+    for (var i=0; i<a_uids.length; i++)
+      this.update_thread_root(a_uids[i], flag);
   };
 
   // set image to flagged or unflagged
@@ -2071,7 +2360,8 @@
     var add_url = '';
     var r_uids = new Array();
     var rows = this.message_list ? this.message_list.rows : new Array();
-    
+    var count = 0;
+
     for (var i=0; i<a_uids.length; i++)
       {
       uid = a_uids[i];
@@ -2080,16 +2370,25 @@
         if (rows[uid].unread)
           r_uids[r_uids.length] = uid;
 
-	if (this.env.skip_deleted)
+	if (this.env.skip_deleted) {
+	  count += this.update_thread(uid);
           this.message_list.remove_row(uid, (this.env.display_next && i == this.message_list.selection.length-1));
+	  }
 	else
 	  this.set_message(uid, 'deleted', true);
         }
       }
 
     // make sure there are no selected rows
-    if (this.env.skip_deleted && !this.env.display_next && this.message_list)
+    if (this.env.skip_deleted && this.message_list) {
+      if(!this.env.display_next)
       this.message_list.clear_selection();
+      if (count < 0)
+        add_url += '&_count='+(count*-1);
+      else if (count > 0) 
+        // remove threads from the end of the list
+        this.delete_excessive_thread_rows();
+      }
 
     add_url = '&_from='+(this.env.action ? this.env.action : '');
     
@@ -2126,7 +2425,60 @@
         this.set_message(uid, 'unread', false);
       }
   };
-  
+
+
+  /*********************************************************/
+  /*********       mailbox folders methods         *********/
+  /*********************************************************/
+
+  this.expunge_mailbox = function(mbox)
+    {
+    var lock = false;
+    var add_url = '';
+    
+    // lock interface if it's the active mailbox
+    if (mbox == this.env.mailbox)
+       {
+       lock = true;
+       this.set_busy(true, 'loading');
+       add_url = '&_reload=1';
+       }
+
+    // send request to server
+    var url = '_mbox='+urlencode(mbox);
+    this.http_post('expunge', url+add_url, lock);
+    };
+
+  this.purge_mailbox = function(mbox)
+    {
+    var lock = false;
+    var add_url = '';
+    
+    if (!confirm(this.get_label('purgefolderconfirm')))
+      return false;
+    
+    // lock interface if it's the active mailbox
+    if (mbox == this.env.mailbox)
+       {
+       lock = true;
+       this.set_busy(true, 'loading');
+       add_url = '&_reload=1';
+       }
+
+    // send request to server
+    var url = '_mbox='+urlencode(mbox);
+    this.http_post('purge', url+add_url, lock);
+    return true;
+    };
+
+  // test if purge command is allowed
+  this.purge_mailbox_test = function()
+  {
+    return (this.env.messagecount && (this.env.mailbox == this.env.trash_mailbox || this.env.mailbox == this.env.junk_mailbox 
+      || this.env.mailbox.match('^' + RegExp.escape(this.env.trash_mailbox) + RegExp.escape(this.env.delimiter)) 
+      || this.env.mailbox.match('^' + RegExp.escape(this.env.junk_mailbox) + RegExp.escape(this.env.delimiter))));
+  };
+
   
   /*********************************************************/
   /*********           login form methods          *********/
@@ -2152,9 +2504,59 @@
   /*********        message compose methods        *********/
   /*********************************************************/
   
+  // init message compose form: set focus and eventhandlers
+  this.init_messageform = function()
+  {
+    if (!this.gui_objects.messageform)
+      return false;
+    
+    //this.messageform = this.gui_objects.messageform;
+    var input_from = $("[name='_from']");
+    var input_to = $("[name='_to']");
+    var input_subject = $("input[name='_subject']");
+    var input_message = $("[name='_message']").get(0);
+    var html_mode = $("input[name='_is_html']").val() == '1';
+
+    // init live search events
+    this.init_address_input_events(input_to);
+    this.init_address_input_events($("[name='_cc']"));
+    this.init_address_input_events($("[name='_bcc']"));
+    
+    if (!html_mode)
+      this.set_caret_pos(input_message, this.env.top_posting ? 0 : $(input_message).val().length);
+
+    // add signature according to selected identity
+    if (input_from.attr('type') == 'select-one' && $("input[name='_draft_saveid']").val() == ''
+        && !html_mode) {  // if we have HTML editor, signature is added in callback
+      this.change_identity(input_from[0]);
+    }
+    else if (!html_mode) 
+      this.set_caret_pos(input_message, this.env.top_posting ? 0 : $(input_message).val().length);
+
+    if (input_to.val() == '')
+      input_to.focus();
+    else if (input_subject.val() == '')
+      input_subject.focus();
+    else if (input_message && !html_mode)
+      input_message.focus();
+
+    // get summary of all field values
+    this.compose_field_hash(true);
+ 
+    // start the auto-save timer
+    this.auto_save_start();
+  };
+
+  this.init_address_input_events = function(obj)
+  {
+    var handler = function(e){ return ref.ksearch_keypress(e,this); };
+    obj.bind((bw.safari || bw.ie ? 'keydown' : 'keypress'), handler);
+    obj.attr('autocomplete', 'off');
+  };
+
   // checks the input fields before sending a message
   this.check_compose_input = function()
-    {
+  {
     // check input fields
     var input_to = $("[name='_to']");
     var input_cc = $("[name='_cc']");
@@ -2189,36 +2591,30 @@
     }
     
     // display localized warning for missing subject
-    if (input_subject.val() == '')
-      {
+    if (input_subject.val() == '') {
       var subject = prompt(this.get_label('nosubjectwarning'), this.get_label('nosubject'));
 
       // user hit cancel, so don't send
-      if (!subject && subject !== '')
-        {
+      if (!subject && subject !== '') {
         input_subject.focus();
         return false;
-        }
-      else
-        {
-        input_subject.val((subject ? subject : this.get_label('nosubject')));
-        }
       }
+      else
+        input_subject.val((subject ? subject : this.get_label('nosubject')));
+    }
 
     // check for empty body
     if ((!window.tinyMCE || !tinyMCE.get(this.env.composebody))
-	&& input_message.val() == '' && !confirm(this.get_label('nobodywarning')))
-      {
+        && input_message.val() == '' && !confirm(this.get_label('nobodywarning'))) {
       input_message.focus();
       return false;
-      }
+    }
     else if (window.tinyMCE && tinyMCE.get(this.env.composebody)
-	&& !tinyMCE.get(this.env.composebody).getContent()
-	&& !confirm(this.get_label('nobodywarning')))
-      {
+        && !tinyMCE.get(this.env.composebody).getContent()
+        && !confirm(this.get_label('nobodywarning'))) {
       tinyMCE.get(this.env.composebody).focus();
       return false;
-      }
+    }
 
     // Apply spellcheck changes if spell checker is active
     this.stop_spellchecking();
@@ -2228,26 +2624,26 @@
       tinyMCE.triggerSave();
 
     return true;
-    };
+  };
 
   this.stop_spellchecking = function()
-    {
+  {
     if (this.env.spellcheck && !this.spellcheck_ready) {
       $(this.env.spellcheck.spell_span).trigger('click');
       this.set_spellcheck_state('ready');
-      }
-    };
+    }
+  };
 
   this.display_spellcheck_controls = function(vis)
-    {
+  {
     if (this.env.spellcheck) {
       // stop spellchecking process
       if (!vis)
-	this.stop_spellchecking();
+        this.stop_spellchecking();
 
       $(this.env.spellcheck.spell_container).css('visibility', vis ? 'visible' : 'hidden');
       }
-    };
+  };
 
   this.set_spellcheck_state = function(s)
     {
@@ -3511,7 +3907,20 @@
     if (folder)
       this.http_post('unsubscribe', '_mbox='+urlencode(folder));
     };
+
+  this.enable_threading = function(folder)
+    {
+    if (folder)
+      this.http_post('enable-threading', '_mbox='+urlencode(folder));
+    };
+
+  this.disable_threading = function(folder)
+    {
+    if (folder)
+      this.http_post('disable-threading', '_mbox='+urlencode(folder));
+    };
     
+
   // helper method to find a specific mailbox row ID
   this.get_folder_row_id = function(folder)
     {
@@ -3816,135 +4225,65 @@
     return null;
   };
 
-  // for reordering column array, Konqueror workaround
-  this.set_message_coltypes = function(coltypes) 
+  // for reordering column array (Konqueror workaround)
+  // and for setting some message list global variables
+  this.set_message_coltypes = function(coltypes, repl)
   { 
-    this.coltypes = coltypes;
+    this.env.coltypes = coltypes;
     
     // set correct list titles
-    var cell, col;
     var thead = this.gui_objects.messagelist ? this.gui_objects.messagelist.tHead : null;
-    for (var n=0; thead && n<this.coltypes.length; n++) 
+
+    // replace old column headers
+    if (thead && repl) {
+      for (var cell, c=0; c < repl.length; c++) {
+        cell = thead.rows[0].cells[c];
+        if (!cell) {
+          cell = document.createElement('td');
+          thead.rows[0].appendChild(cell);
+        }
+        cell.innerHTML = repl[c].html;
+        if (repl[c].id) cell.id = repl[c].id;
+        if (repl[c].className) cell.className = repl[c].className;
+      }
+    }
+
+    var cell, col, n;
+    for (n=0; thead && n<this.env.coltypes.length; n++)
       {
-      col = this.coltypes[n];
+      col = this.env.coltypes[n];
       if ((cell = thead.rows[0].cells[n+1]) && (col=='from' || col=='to'))
         {
         // if we have links for sorting, it's a bit more complicated...
         if (cell.firstChild && cell.firstChild.tagName.toLowerCase()=='a')
           {
-          cell.firstChild.innerHTML = this.get_label(this.coltypes[n]);
+          cell.firstChild.innerHTML = this.get_label(this.env.coltypes[n]);
           cell.firstChild.onclick = function(){ return rcmail.command('sort', this.__col, this); };
           cell.firstChild.__col = col;
           }
         else
-          cell.innerHTML = this.get_label(this.coltypes[n]);
+          cell.innerHTML = this.get_label(this.env.coltypes[n]);
 
         cell.id = 'rcm'+col;
         }
-      else if (col == 'subject' && this.message_list)
-        this.message_list.subject_col = n+1;
-      }
-  };
-
-  // create a table row in the message list
-  this.add_message_row = function(uid, cols, flags, attachment, attop)
-    {
-    if (!this.gui_objects.messagelist || !this.message_list)
-      return false;
-
-    if (this.message_list.background)
-      var tbody = this.message_list.background;
-    else
-      var tbody = this.gui_objects.messagelist.tBodies[0];
-    
-    var rowcount = tbody.rows.length;
-    var even = rowcount%2;
-    
-    this.env.messages[uid] = {
-      deleted: flags.deleted?1:0,
-      replied: flags.replied?1:0,
-      unread: flags.unread?1:0,
-      forwarded: flags.forwarded?1:0,
-      flagged:flags.flagged?1:0
-    };
-
-    var css_class = 'message'
-        + (even ? ' even' : ' odd')
-        + (flags.unread ? ' unread' : '')
-        + (flags.deleted ? ' deleted' : '')
-        + (flags.flagged ? ' flagged' : '')
-        + (this.message_list.in_selection(uid) ? ' selected' : '');
-
-    // for performance use DOM instead of jQuery here
-    var row = document.createElement('tr');
-    row.id = 'rcmrow'+uid;
-    row.className = css_class;
-    
-    var icon = this.env.messageicon;
-    if (flags.deleted && this.env.deletedicon)
-      icon = this.env.deletedicon;
-    else if (flags.replied && this.env.repliedicon)
-      {
-      if (flags.forwarded && this.env.forwardedrepliedicon)
-        icon = this.env.forwardedrepliedicon;
-      else
-        icon = this.env.repliedicon;
-      }
-    else if (flags.forwarded && this.env.forwardedicon)
-      icon = this.env.forwardedicon;
-    else if(flags.unread && this.env.unreadicon)
-      icon = this.env.unreadicon;
-    
-    // add icon col
-    var col = document.createElement('td');
-    col.className = 'icon';
-    col.innerHTML = icon ? '<img src="'+icon+'" alt="" />' : '';
-    row.appendChild(col);
-
-    // build subject link
-    if (!bw.ie && cols.subject) {
-      var action = cols.mbox == this.env.drafts_mailbox ? 'compose' : 'show';
-      var uid_param = cols.mbox == this.env.drafts_mailbox ? '_draft_uid' : '_uid';
-      cols.subject = '<a href="./?_task=mail&_action='+action+'&_mbox='+urlencode(cols.mbox)+'&'+uid_param+'='+uid+'"'+
-         ' onclick="return rcube_event.cancel(event)">'+cols.subject+'</a>';
-    }
-
-    // add each submitted col
-    for (var n = 0; n < this.coltypes.length; n++) {
-      var c = this.coltypes[n];
-      col = document.createElement('td');
-      col.className = String(c).toLowerCase();
-            
-      if (c=='flag') {
-        if (flags.flagged && this.env.flaggedicon)
-          col.innerHTML = '<img src="'+this.env.flaggedicon+'" alt="" />';
-        else if(!flags.flagged && this.env.unflaggedicon)
-          col.innerHTML = '<img src="'+this.env.unflaggedicon+'" alt="" />';
-        }
-      else if (c=='attachment')
-        col.innerHTML = (attachment && this.env.attachmenticon ? '<img src="'+this.env.attachmenticon+'" alt="" />' : '&nbsp;');
-      else
-        col.innerHTML = cols[c];
-
-      row.appendChild(col);
       }
 
-    this.message_list.insert_row(row, attop);
+    // remove excessive columns
+    for (var i=n+1; thead && i<thead.rows[0].cells.length; i++)
+      thead.rows[0].removeChild(thead.rows[0].cells[i]);
 
-    // remove 'old' row
-    if (attop && this.env.pagesize && this.message_list.rowcount > this.env.pagesize) {
-      var uid = this.message_list.get_last_row();
-      this.message_list.remove_row(uid);
-      this.message_list.clear_selection(uid);
-      }
-    };
+    this.env.subject_col = null;
+    this.env.flagged_col = null;
 
-  // messages list handling in background (for performance)
-  this.offline_message_list = function(flag)
-    {
+    var found;
+    if((found = find_in_array('subject', this.env.coltypes)) >= 0) {
+      this.set_env('subject_col', found);
       if (this.message_list)
-      	this.message_list.set_background_mode(flag);
-    };
+        this.message_list.subject_col = found+1;
+      }
+    if((found = find_in_array('flag', this.env.coltypes)) >= 0)
+      this.set_env('flagged_col', found);
+  };
 
   // replace content of row count display
   this.set_rowcount = function(text)
@@ -4260,7 +4599,7 @@
       for (var i=0; i < response.callbacks.length; i++)
         this.triggerEvent(response.callbacks[i][0], response.callbacks[i][1]);
     }
- 
+
     // process the response data according to the sent action
     switch (response.action) {
       case 'delete':
@@ -4288,19 +4627,25 @@
           // disable commands useless when mailbox is empty
           this.enable_command('show', 'reply', 'reply-all', 'forward', 'moveto', 'delete', 
             'mark', 'viewsource', 'open', 'edit', 'download', 'print', 'load-attachment', 
-            'purge', 'expunge', 'select-all', 'select-none', 'sort', false);
+            'purge', 'expunge', 'select-all', 'select-none', 'sort',
+            'expand-all', 'expand-unread', 'collapse-all', false);
         }
         break;
 
       case 'check-recent':
       case 'getunread':
+      case 'search':
       case 'list':
         if (this.task == 'mail') {
-          if (this.message_list && response.action == 'list')
+          if (this.message_list && (response.action == 'list' || response.action == 'search')) {
             this.msglist_select(this.message_list);
+            this.expand_threads();
+          }
           this.enable_command('show', 'expunge', 'select-all', 'select-none', 'sort', (this.env.messagecount > 0));
           this.enable_command('purge', this.purge_mailbox_test());
-          
+         
+          this.enable_command('expand-all', 'expand-unread', 'collapse-all', this.env.threading && this.env.messagecount);
+
           if (response.action == 'list')
             this.triggerEvent('listupdate', { folder:this.env.mailbox, rowcount:this.message_list.rowcount });
         }
@@ -4332,6 +4677,15 @@
     var d = new Date();
     this.http_request('keep-alive', '_t='+d.getTime());
     };
+
+  // start interval for keep-alive/recent_check signal
+  this.start_keepalive = function()
+    {
+    if (this.env.keep_alive && !this.env.framed && this.task=='mail' && this.gui_objects.mailboxlist)
+      this._int = setInterval(function(){ ref.check_for_recent(false); }, this.env.keep_alive * 1000);
+    else if (this.env.keep_alive && !this.env.framed && this.task!='login')
+      this._int = setInterval(function(){ ref.send_keep_alive(); }, this.env.keep_alive * 1000);
+    }
 
   // send periodic request to check for recent messages
   this.check_for_recent = function(refresh)
@@ -4432,7 +4786,6 @@
     };
     
 }  // end object rcube_webmail
-
 
 // copy event engine prototype
 rcube_webmail.prototype.addEventListener = rcube_event_engine.prototype.addEventListener;
diff --git a/program/js/common.js b/program/js/common.js
index 70ef444..829da8a 100644
--- a/program/js/common.js
+++ b/program/js/common.js
@@ -292,7 +292,7 @@
     e = this;
   else if (typeof e == 'object')
     e.event = evt;
-  
+
   if (this._events && this._events[evt] && !this._event_exec) {
     this._event_exec = true;
     for (var i=0; i < this._events[evt].length; i++) {
diff --git a/program/js/list.js b/program/js/list.js
index 357a77a..3ab4b1a 100644
--- a/program/js/list.js
+++ b/program/js/list.js
@@ -37,6 +37,7 @@
   this.subject_col = -1;
   this.shiftkey = false;
   this.multiselect = false;
+  this.multiexpand = false;
   this.multi_selecting = false;
   this.draggable = false;
   this.keyboard = false;
@@ -76,7 +77,7 @@
     for(var r=0; r<this.list.tBodies[0].childNodes.length; r++)
     {
       row = this.list.tBodies[0].childNodes[r];
-      while (row && (row.nodeType != 1 || row.style.display == 'none'))
+      while (row && row.nodeType != 1)
       {
         row = row.nextSibling;
         r++;
@@ -108,7 +109,7 @@
     var p = this;
     var uid = RegExp.$1;
     row.uid = uid;
-    this.rows[uid] = {uid:uid, id:row.id, obj:row, classname:row.className};
+    this.rows[uid] = {uid:uid, id:row.id, obj:row};
 
     // set eventhandlers to table row
     row.onmousedown = function(e){ return p.drag_row(e, this.uid); };
@@ -319,6 +320,188 @@
 },
 
 
+expand_row: function(e, id)
+{
+  var row = this.rows[id];
+  var evtarget = rcube_event.get_target(e);
+  var mod_key = rcube_event.get_modifier(e);
+
+  // Don't select this message
+  this.dont_select = true;
+  // Don't treat double click on the expando as double click on the message.
+  row.clicked = 0;
+
+  if (row.expanded) {
+    evtarget.className = "collapsed";
+    if (mod_key == CONTROL_KEY || this.multiexpand)
+      this.collapse_all(row);
+    else
+      this.collapse(row);
+  }
+  else {
+    evtarget.className = "expanded";
+    if (mod_key == CONTROL_KEY || this.multiexpand)
+      this.expand_all(row);
+    else
+     this.expand(row);
+  }
+},
+
+collapse: function(row)
+{
+  row.expanded = false;
+  this.triggerEvent('expandcollapse', { uid:row.uid, expanded:row.expanded });
+  var depth = row.depth;
+  var new_row = row ? row.obj.nextSibling : null;
+  var r;
+
+  while (new_row) {
+    if (new_row.nodeType == 1) {
+      var r = this.rows[new_row.uid];
+      if (r && r.depth <= depth)
+        break;
+      $(new_row).hide();
+      r.expanded = false;
+      this.triggerEvent('expandcollapse', { uid:r.uid, expanded:r.expanded });
+    }
+    new_row = new_row.nextSibling;
+  }
+
+  return false;
+},
+
+expand: function(row)
+{
+  var depth, new_row;
+  var last_expanded_parent_depth;
+
+  if (row) {
+    row.expanded = true;
+    depth = row.depth;
+    new_row = row.obj.nextSibling;
+    this.triggerEvent('expandcollapse', { uid:row.uid, expanded:row.expanded });
+  }
+  else {
+    var tbody = this.list.tBodies[0];
+    new_row = tbody.firstChild;
+    depth = 0;
+    last_expanded_parent_depth = 0;
+  }
+
+  while (new_row) {
+    if (new_row.nodeType == 1) {
+      var r = this.rows[new_row.uid];
+      if (r) {
+        if (row && (!r.depth || r.depth <= depth))
+          break;
+
+        if (r.parent_uid) {
+          var p = this.rows[r.parent_uid];
+          if (p && p.expanded) {
+            if ((row && p == row) || last_expanded_parent_depth >= p.depth - 1) {
+              last_expanded_parent_depth = p.depth;
+              $(new_row).show();
+              r.expanded = true;
+              this.triggerEvent('expandcollapse', { uid:r.uid, expanded:r.expanded });
+            }
+          }
+          else
+            if (row && (! p || p.depth <= depth))
+              break;
+        }
+      }
+    }
+    new_row = new_row.nextSibling;
+  }
+
+  return false;
+},
+
+
+collapse_all: function(row)
+{
+  var depth, new_row;
+  var r;
+
+  if (row) {
+    row.expanded = false;
+    depth = row.depth;
+    new_row = row.obj.nextSibling;
+    this.triggerEvent('expandcollapse', { uid:row.uid, expanded:row.expanded });
+    
+    // don't collapse sub-root tree in multiexpand mode 
+    if (depth && this.multiexpand)
+      return false; 
+  }
+  else {
+    var tbody = this.list.tBodies[0];
+    new_row = tbody.firstChild;
+    depth = 0;
+  }
+
+  while (new_row) {
+    if (new_row.nodeType == 1) {
+      var r = this.rows[new_row.uid];
+      if (r) {
+        if (row && (!r.depth || r.depth <= depth))
+          break;
+
+        if (row || r.depth)
+          $(new_row).hide();
+        if (r.has_children) {
+          r.expanded = false;
+          var expando = document.getElementById('rcmexpando' + r.uid);
+          if (expando)
+            expando.className = 'collapsed';
+          this.triggerEvent('expandcollapse', { uid:r.uid, expanded:r.expanded });
+        }
+      }
+    }
+    new_row = new_row.nextSibling;
+  }
+
+  return false;
+},
+
+expand_all: function(row)
+{
+  var depth, new_row;
+  var r;
+
+  if (row) {
+    row.expanded = true;
+    depth = row.depth;
+    new_row = row.obj.nextSibling;
+    this.triggerEvent('expandcollapse', { uid:row.uid, expanded:row.expanded });
+  }
+  else {
+    var tbody = this.list.tBodies[0];
+    new_row = tbody.firstChild;
+    depth = 0;
+  }
+
+  while (new_row) {
+    if (new_row.nodeType == 1) {
+      var r = this.rows[new_row.uid];
+      if (r) {
+        if (row && r.depth <= depth)
+          break;
+
+        $(new_row).show();
+        if (r.has_children) {
+          r.expanded = true;
+          var expando = document.getElementById('rcmexpando' + r.uid);
+          if (expando)
+            expando.className = 'expanded';
+          this.triggerEvent('expandcollapse', { uid:r.uid, expanded:r.expanded });
+        }
+      }
+    }
+    new_row = new_row.nextSibling;
+  }
+  return false;
+},
+
 /**
  * get first/next/previous/last rows that are not hidden
  */
@@ -495,13 +678,15 @@
   {
     if ((this.rows[n].obj.rowIndex >= i) && (this.rows[n].obj.rowIndex <= j))
     {
-      if (!this.in_selection(n))
+      if (!this.in_selection(n)) {
         this.highlight_row(n, true);
+      }
     }
     else
     {
-      if  (this.in_selection(n) && !control)
+      if  (this.in_selection(n) && !control) {
         this.highlight_row(n, true);
+      }
     }
   }
 },
@@ -516,7 +701,7 @@
     if (this.selection[n]==id)
       return true;
 
-  return false;    
+  return false;
 },
 
 
@@ -567,7 +752,7 @@
   var select_before = this.selection.join(',');
   
   for (var n in this.rows)
-    this.highlight_row(n, true);    
+    this.highlight_row(n, true);
 
   // trigger event if selection changed
   if (this.selection.join(',') != select_before)
@@ -685,6 +870,16 @@
       // Stop propagation so that the browser doesn't scroll
       rcube_event.cancel(e);
       return this.use_arrow_key(keyCode, mod_key);
+    case 61:
+    case 107: // Plus sign on a numeric keypad (fc11 + firefox 3.5.2)
+    case 109:
+    case 32:
+      // Stop propagation
+      rcube_event.cancel(e);
+      var ret = this.use_plusminus_key(keyCode, mod_key);
+      this.key_pressed = keyCode;
+      this.triggerEvent('keypress');
+      return ret;
     default:
       this.shiftkey = e.shiftKey;
       this.key_pressed = keyCode;
@@ -712,6 +907,10 @@
     case 38: 
     case 63233:
     case 63232:
+    case 61:
+    case 107:
+    case 109:
+    case 32:
       if (!rcube_event.get_modifier(e) && this.focused)
         return rcube_event.cancel(e);
         
@@ -740,6 +939,36 @@
     this.select_row(new_row.uid, mod_key, true);
     this.scrollto(new_row.uid);
   }
+
+  return false;
+},
+
+
+/**
+ * Special handling method for +/- keys
+ */
+use_plusminus_key: function(keyCode, mod_key)
+{
+  var selected_row = this.rows[this.last_selected];
+  if (!selected_row)
+    return;
+
+  if (keyCode == 32)
+    keyCode = selected_row.expanded ? 109 : 61;
+  if (keyCode == 61 || keyCode == 107)
+    if (mod_key == CONTROL_KEY || this.multiexpand)
+      this.expand_all(selected_row);
+    else
+     this.expand(selected_row);
+  else
+    if (mod_key == CONTROL_KEY || this.multiexpand)
+      this.collapse_all(selected_row);
+    else
+      this.collapse(selected_row);
+
+  var expando = document.getElementById('rcmexpando' + selected_row.uid);
+  if (expando)
+    expando.className = selected_row.expanded?'expanded':'collapsed';
 
   return false;
 },
@@ -779,9 +1008,9 @@
     if (!this.draglayer)
       this.draglayer = $('<div>').attr('id', 'rcmdraglayer').css({ position:'absolute', display:'none', 'z-index':2000 }).appendTo(document.body);
 
-    // get subjects of selectedd messages
+    // get subjects of selected messages
     var names = '';
-    var c, i, node, subject, obj;
+    var c, i, subject, obj;
     for(var n=0; n<this.selection.length; n++)
     {
       if (n>12)  // only show 12 lines
@@ -790,24 +1019,29 @@
         break;
       }
 
-      if (this.rows[this.selection[n]].obj)
+      if (obj = this.rows[this.selection[n]].obj)
       {
-        obj = this.rows[this.selection[n]].obj;
         subject = '';
 
-        for(c=0, i=0; i<obj.childNodes.length; i++)
+        for (c=0, i=0; i<obj.childNodes.length; i++)
         {
-          if (obj.childNodes[i].nodeName == 'TD')
+	  if (obj.childNodes[i].nodeName == 'TD')
           {
-            if (((node = obj.childNodes[i].firstChild) && (node.nodeType==3 || node.nodeName=='A')) &&
-              (this.subject_col < 0 || (this.subject_col >= 0 && this.subject_col == c)))
-            {
-	      if (n == 0) {
-	        if (node.nodeType == 3)
-		  this.drag_start_pos = $(obj.childNodes[i]).offset();
-		else
-                  this.drag_start_pos = $(node).offset();
+            if (n == 0)
+	      this.drag_start_pos = $(obj.childNodes[i]).offset();
+
+	    if (this.subject_col < 0 || (this.subject_col >= 0 && this.subject_col == c))
+	    {
+	      var node, tmp_node, nodes = obj.childNodes[i].childNodes;
+	      // find text node
+	      for (m=0; m<nodes.length; m++) {
+	        if ((tmp_node = obj.childNodes[i].childNodes[m]) && (tmp_node.nodeType==3 || tmp_node.nodeName=='A'))
+	          node = tmp_node;
 	      }
+	      
+	      if (!node)
+	        break;
+
               subject = node.nodeType==3 ? node.data : node.innerHTML;
 	      // remove leading spaces
 	      subject = subject.replace(/^\s+/i, '');
diff --git a/program/lib/imap.inc b/program/lib/imap.inc
index 1a6a7ac..4267716 100644
--- a/program/lib/imap.inc
+++ b/program/lib/imap.inc
@@ -173,18 +173,10 @@
 	var $forwarded = false;
 	var $junk = false;
 	var $flagged = false;
+	var $has_children = false;
+	var $depth = 0;
+	var $unread_children = 0;
 	var $others = array();
-}
-
-/**
- * @todo Change class vars to public/private
- */
-class iilThreadHeader
-{
-	var $id;
-	var $sbj;
-	var $irt;
-	var $mid;
 }
 
 function iil_xor($string, $string2) {
@@ -873,7 +865,7 @@
 	
 	$fields = array('ARRIVAL' => 1,'CC' => 1,'DATE' => 1,
         'FROM' => 1, 'SIZE' => 1, 'SUBJECT' => 1, 'TO' => 1);
-	
+
 	if (!$fields[$field]) {
 	    return false;
 	}
@@ -885,9 +877,12 @@
     
 	$is_uid = $is_uid ? 'UID ' : '';
 	
-	if (!empty($add)) {
+	// message IDs
+	if (is_array($add))
+		$add = iil_CompressMessageSet(join(',', $add));
+
+	if (!empty($add))
 	    $add = " $add";
-	}
 
 	$command  = 's ' . $is_uid . 'SORT (' . $field . ') ';
 	$command .= $encoding . ' ALL' . $add;
@@ -917,20 +912,27 @@
 
 function iil_C_FetchHeaderIndex(&$conn, $mailbox, $message_set, $index_field='', $skip_deleted=true, $uidfetch=false) {
 
-	list($from_idx, $to_idx) = explode(':', $message_set);
-	if (empty($message_set) ||
-		(isset($to_idx) && $to_idx != '*' && (int)$from_idx > (int)$to_idx)) {
-		return false;
+	if (is_array($message_set)) {
+		if (!($message_set = iil_CompressMessageSet(join(',', $message_set))))
+			return false;
+	} else {
+		list($from_idx, $to_idx) = explode(':', $message_set);
+		if (empty($message_set) ||
+			(isset($to_idx) && $to_idx != '*' && (int)$from_idx > (int)$to_idx)) {
+			return false;
+		}
 	}
-
+	
 	$index_field = empty($index_field) ? 'DATE' : strtoupper($index_field);
 	
 	$fields_a['DATE']         = 1;
 	$fields_a['INTERNALDATE'] = 4;
+	$fields_a['ARRIVAL'] 	  = 4;
 	$fields_a['FROM']         = 1;
 	$fields_a['REPLY-TO']     = 1;
 	$fields_a['SENDER']       = 1;
 	$fields_a['TO']           = 1;
+	$fields_a['CC']           = 1;
 	$fields_a['SUBJECT']      = 1;
 	$fields_a['UID']          = 2;
 	$fields_a['SIZE']         = 2;
@@ -1031,22 +1033,6 @@
 		}
 	} while (!iil_StartsWith($line, $key, true));
 
-/*
-	//check number of elements...
-	if (is_numeric($from_idx) && is_numeric($to_idx)) {
-		//count how many we should have
-		$should_have = $to_idx - $from_idx + 1;
-		
-		//if we have less, try and fill in the "gaps"
-		if (count($result) < $should_have) {
-			for ($i=$from_idx; $i<=$to_idx; $i++) {
-				if (!isset($result[$i])) {
-			    		$result[$i] = '';
-            			}
-        		}
-		}
-	}
-*/
 	return $result;	
 }
 
@@ -1122,307 +1108,6 @@
 	return iil_C_FetchHeaderIndex($conn, $mailbox, $message_set, 'UID');
 }
 
-function iil_SortThreadHeaders($headers, $index_a, $uids) {
-	asort($index_a);
-	$result = array();
-	foreach ($index_a as $mid=>$foobar) {
-		$uid = $uids[$mid];
-		$result[$uid] = $headers[$uid];
-	}
-	return $result;
-}
-
-function iil_C_FetchThreadHeaders(&$conn, $mailbox, $message_set) {
-	global $clock;
-	global $index_a;
-	
-	list($from_idx, $to_idx) = explode(':', $message_set);
-	if (empty($message_set) || (isset($to_idx)
-        && (int)$from_idx > (int)$to_idx)) {
-		return false;
-	}
-
-	$result = array();
-	$uids   = iil_C_FetchUIDs($conn, $mailbox);
-	$debug  = false;
-	
-	$message_set = iil_CompressMessageSet($message_set);
-    
-	/* if we're missing any, get them */
-	if ($message_set) {
-		/* FETCH date,from,subject headers */
-		$key        = 'fh';
-		$fp         = $conn->fp;
-		$request    = $key . " FETCH $message_set ";
-    		$request   .= "(BODY.PEEK[HEADER.FIELDS (SUBJECT MESSAGE-ID IN-REPLY-TO)])";
-		$mid_to_id  = array();
-		if (!iil_PutLine($fp, $request)) {
-		    return false;
-    		}
-		do {
-			$line = chop(iil_ReadLine($fp, 1024));
-			if ($debug) {
-			    echo $line . "\n";
-        		}
-			if (preg_match('/\{[0-9]+\}$/', $line)) {
-				$a 	 = explode(' ', $line);
-				$new = array();
-
-				$new_thhd = new iilThreadHeader;
-				$new_thhd->id = $a[1];
-				do {
-					$line = chop(iil_ReadLine($fp, 1024), "\r\n");
-					if (iil_StartsWithI($line, 'Message-ID:')
-                    				|| (iil_StartsWithI($line,'In-Reply-To:'))
-                    				|| (iil_StartsWithI($line,'SUBJECT:'))) {
-
-						$pos        = strpos($line, ':');
-						$field_name = substr($line, 0, $pos);
-						$field_val  = substr($line, $pos+1);
-
-						$new[strtoupper($field_name)] = trim($field_val);
-
-					} else if (preg_match('/^\s+/', $line)) {
-						$new[strtoupper($field_name)] .= trim($line);
-					}
-				} while ($line[0] != ')');
-                
-				$new_thhd->sbj = $new['SUBJECT'];
-				$new_thhd->mid = substr($new['MESSAGE-ID'], 1, -1);
-				$new_thhd->irt = substr($new['IN-REPLY-TO'], 1, -1);
-				
-				$result[$uids[$new_thhd->id]] = $new_thhd;
-			}
-		} while (!iil_StartsWith($line, 'fh'));
-	}
-	
-	/* sort headers */
-	if (is_array($index_a)) {
-		$result = iil_SortThreadHeaders($result, $index_a, $uids);	
-	}
-	
-	//echo 'iil_FetchThreadHeaders:'."\n";
-	//print_r($result);
-	
-	return $result;
-}
-
-function iil_C_BuildThreads2(&$conn, $mailbox, $message_set, &$clock) {
-	global $index_a;
-
-	list($from_idx, $to_idx) = explode(':', $message_set);
-	if (empty($message_set) || (isset($to_idx)
-    		&& (int)$from_idx > (int)$to_idx)) {
-		return false;
-	}
-    
-	$result    = array();
-	$roots     = array();
-	$root_mids = array();
-	$sub_mids  = array();
-	$strays    = array();
-	$messages  = array();
-	$fp        = $conn->fp;
-	$debug     = false;
-	
-	$sbj_filter_pat = '/[a-z]{2,3}(\[[0-9]*\])?:(\s*)/i';
-	
-	/*  Do "SELECT" command */
-	if (!iil_C_Select($conn, $mailbox)) {
-	    return false;
-	}
-    
-	/* FETCH date,from,subject headers */
-	$mid_to_id = array();
-	$messages  = array();
-	$headers   = iil_C_FetchThreadHeaders($conn, $mailbox, $message_set);
-	if ($clock) {
-	    $clock->register('fetched headers');
-	}
-    
-	if ($debug) {
-	    print_r($headers);
-	}
-    
-	/* go through header records */
-	foreach ($headers as $header) {
-		//$id = $header['i'];
-		//$new = array('id'=>$id, 'MESSAGE-ID'=>$header['m'], 
-		//			'IN-REPLY-TO'=>$header['r'], 'SUBJECT'=>$header['s']);
-		$id  = $header->id;
-		$new = array('id' => $id, 'MESSAGE-ID' => $header->mid, 
-        		'IN-REPLY-TO' => $header->irt, 'SUBJECT' => $header->sbj);
-
-		/* add to message-id -> mid lookup table */
-		$mid_to_id[$new['MESSAGE-ID']] = $id;
-		
-		/* if no subject, use message-id */
-		if (empty($new['SUBJECT'])) {
-		    $new['SUBJECT'] = $new['MESSAGE-ID'];
-		}
-        
-		/* if subject contains 'RE:' or has in-reply-to header, it's a reply */
-		$sbj_pre = '';
-		$has_re = false;
-		if (preg_match($sbj_filter_pat, $new['SUBJECT'])) {
-		    $has_re = true;
-		}
-    		if ($has_re || $new['IN-REPLY-TO']) {
-        	    $sbj_pre = 'RE:';
-		}
-        
-		/* strip out 're:', 'fw:' etc */
-		if ($has_re) {
-		    $sbj = preg_replace($sbj_filter_pat, '', $new['SUBJECT']);
-		} else {
-		    $sbj = $new['SUBJECT'];
-		}
-    		$new['SUBJECT'] = $sbj_pre.$sbj;
-		
-		
-		/* if subject not a known thread-root, add to list */
-		if ($debug) {
-		    echo $id . ' ' . $new['SUBJECT'] . "\t" . $new['MESSAGE-ID'] . "\n";
-		}
-    		$root_id = $roots[$sbj];
-		
-		if ($root_id && ($has_re || !$root_in_root[$root_id])) {
-			if ($debug) {
-			    echo "\tfound root: $root_id\n";
-			}
-        		$sub_mids[$new['MESSAGE-ID']] = $root_id;
-			$result[$root_id][]           = $id;
-		} else if (!isset($roots[$sbj]) || (!$has_re && $root_in_root[$root_id])) {
-			/* try to use In-Reply-To header to find root 
-				unless subject contains 'Re:' */
-			if ($has_re&&$new['IN-REPLY-TO']) {
-				if ($debug) {
-				    echo "\tlooking: ".$new['IN-REPLY-TO']."\n";
-				}
-				//reply to known message?
-				$temp = $sub_mids[$new['IN-REPLY-TO']];
-				
-				if ($temp) {
-					//found it, root:=parent's root
-					if ($debug) {
-					    echo "\tfound parent: ".$new['SUBJECT']."\n";
-					}
-                			$result[$temp][]              = $id;
-					$sub_mids[$new['MESSAGE-ID']] = $temp;
-					$sbj                          = '';
-				} else {
-					//if we can't find referenced parent, it's a "stray"
-					$strays[$id] = $new['IN-REPLY-TO'];
-				}
-			}
-			
-			//add subject as root
-			if ($sbj) {
-				if ($debug) {
-				    echo "\t added to root\n";
-				}
-            			$roots[$sbj]                  = $id;
-				$root_in_root[$id]            = !$has_re;
-				$sub_mids[$new['MESSAGE-ID']] = $id;
-				$result[$id]                  = array($id);
-			}
-			if ($debug) {
-			    echo $new['MESSAGE-ID'] . "\t" . $sbj . "\n";
-        		}
-		}
-	}
-	
-	//now that we've gone through all the messages,
-	//go back and try and link up the stray threads
-	if (count($strays) > 0) {
-		foreach ($strays as $id=>$irt) {
-			$root_id = $sub_mids[$irt];
-			if (!$root_id || $root_id==$id) {
-			    continue;
-			}
-        		$result[$root_id] = array_merge($result[$root_id],$result[$id]);
-			unset($result[$id]);
-		}
-	}
-	
-	if ($clock) {
-	    $clock->register('data prepped');
-	}
-    
-	if ($debug) {
-	    print_r($roots);
-	}
-
-	return $result;
-}
-
-function iil_SortThreads(&$tree, $index, $sort_order = 'ASC') {
-	if (!is_array($tree) || !is_array($index)) {
-	    return false;
-	}
-    
-	//create an id to position lookup table
-	$i = 0;
-	foreach ($index as $id=>$val) {
-		$i++;
-		$index[$id] = $i;
-	}
-	$max = $i+1;
-	
-	//for each tree, set array key to position
-	$itree = array();
-	foreach ($tree as $id=>$node) {
-		if (count($tree[$id])<=1) {
-			//for "threads" with only one message, key is position of that message
-			$n         = $index[$id];
-			$itree[$n] = array($n=>$id);
-		} else {
-			//for "threads" with multiple messages, 
-			$min   = $max;
-			$new_a = array();
-			foreach ($tree[$id] as $mid) {
-				$new_a[$index[$mid]] = $mid;		//create new sub-array mapping position to id
-				$pos                 = $index[$mid];
-				if ($pos&&$pos<$min) {
-				    $min = $index[$mid];	//find smallest position
-				}
-			}
-			$n = $min;	//smallest position of child is thread position
-			
-			//assign smallest position to root level key
-			//set children array to one created above
-			ksort($new_a);
-			$itree[$n] = $new_a;
-		}
-	}
-	
-	//sort by key, this basically sorts all threads
-	ksort($itree);
-	$i   = 0;
-	$out = array();
-	foreach ($itree as $k=>$node) {
-		$out[$i] = $itree[$k];
-		$i++;
-	}
-	
-	return $out;
-}
-
-function iil_IndexThreads(&$tree) {
-	/* creates array mapping mid to thread id */
-	
-	if (!is_array($tree)) {
-	    return false;
-	}
-    
-	$t_index = array();
-	foreach ($tree as $pos=>$kids) {
-		foreach ($kids as $kid) $t_index[$kid] = $pos;
-	}
-	
-	return $t_index;
-}
-
 function iil_C_FetchHeaders(&$conn, $mailbox, $message_set, $uidfetch=false, $bodystr=false, $add='')
 {
 	global $IMAP_USE_INTERNAL_DATE;
@@ -1435,6 +1120,9 @@
 		$conn->error = "Couldn't select $mailbox";
 		return false;
 	}
+
+	if (is_array($message_set))
+		$message_set = join(',', $message_set);
 
 	$message_set = iil_CompressMessageSet($message_set);
 
@@ -1878,6 +1566,87 @@
 	return $result;
 }
 
+// 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
+function iil_ParseThread($str, $begin, $end, $root, $parent, $depth, &$depthmap, &$haschildren) {
+	$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] = iil_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 += iil_ParseThread($str, $start + 1, $off - 1, $root, $parent, $depth, $depthmap, $haschildren);
+		}
+	}
+	
+	return $node;
+}
+
+function iil_C_Thread(&$conn, $folder, $algorithm='REFERENCES', $criteria='',
+    $encoding='US-ASCII') {
+
+	if (iil_C_Select($conn, $folder)) {
+	
+		$encoding = $encoding ? trim($encoding) : 'US-ASCII';
+		$algorithm = $algorithm ? trim($algorithm) : 'REFERENCES';
+		$criteria = $criteria ? 'ALL '.trim($criteria) : 'ALL';
+		
+		iil_PutLineC($conn->fp, "thrd1 THREAD $algorithm $encoding $criteria");
+		do {
+			$line = trim(iil_ReadLine($conn->fp, 10000));
+			if (preg_match('/^\* THREAD/', $line)) {
+				$str = trim(substr($line, 8));
+				$depthmap = array();
+				$haschildren = array();
+				$tree = iil_ParseThread($str, 0, strlen($str), null, null, 0, $depthmap, $haschildren);
+			}
+		} while (!iil_StartsWith($line, 'thrd1', true));
+
+		$result_code = iil_ParseResult($line);
+		if ($result_code == 0) {
+		    return array($tree, $depthmap, $haschildren);
+		}
+		$conn->error = 'iil_C_Thread: ' . $line . "\n";
+		return false;	
+	}
+	$conn->error = "iil_C_Thread: Couldn't select \"$folder\"\n";
+	return false;
+}
+
 function iil_C_Search(&$conn, $folder, $criteria) {
 
 	if (iil_C_Select($conn, $folder)) {
diff --git a/program/localization/de_CH/labels.inc b/program/localization/de_CH/labels.inc
index cca06fa..d89bf89 100644
--- a/program/localization/de_CH/labels.inc
+++ b/program/localization/de_CH/labels.inc
@@ -127,6 +127,30 @@
 $labels['deleted'] = 'Gelöschte';
 $labels['invert'] = 'Umkehren';
 $labels['filter'] = 'Filter';
+
+$labels['list'] = 'Liste';
+$labels['threads'] = 'Konversationen';
+$labels['expand-all'] = 'All aufklappen';
+$labels['expand-unread'] = 'Ungelesene aufklappen';
+$labels['collapse-all'] = 'Alle zuklappen';
+$labels['threaded'] = 'Gruppiert';
+
+$labels['autoexpand_threads'] = 'Konversationen aufklappen';
+$labels['do_expand'] = 'alle';
+$labels['expand_only_unread'] = 'nur ungelesene';
+$labels['fromto'] = 'Sender/Empfänger';
+$labels['flag'] = 'Markierung';
+$labels['attachment'] = 'Anhang';
+$labels['nonesort'] = 'Keine';
+$labels['sentdate'] = 'Sendedatum';
+$labels['arrival'] = 'Empfangsdatum';
+$labels['asc'] = 'aufsteigend';
+$labels['desc'] = 'absteigend';
+$labels['listcolumns'] = 'Spalten';
+$labels['listsorting'] = 'Sortierung';
+$labels['listorder'] = 'Ordnung';
+$labels['listmode'] = 'Anzeigemodus';
+
 $labels['compact'] = 'Packen';
 $labels['empty'] = 'Leeren';
 $labels['purge'] = 'Aufräumen';
diff --git a/program/localization/en_US/labels.inc b/program/localization/en_US/labels.inc
index 3462b8d..3059692 100644
--- a/program/localization/en_US/labels.inc
+++ b/program/localization/en_US/labels.inc
@@ -56,6 +56,7 @@
 
 $labels['mailboxlist'] = 'Folders';
 $labels['messagesfromto'] = 'Messages $from to $to of $count';
+$labels['threadsfromto'] = 'Threads $from to $to of $count';
 $labels['messagenrof'] = 'Message $nr of $count';
 
 $labels['moveto']   = 'Move to...';
@@ -149,6 +150,29 @@
 $labels['deleted'] = 'Deleted';
 $labels['invert'] = 'Invert';
 $labels['filter'] = 'Filter';
+
+$labels['list'] = 'List';
+$labels['threads'] = 'Threads';
+$labels['expand-all'] = 'Expand All';
+$labels['expand-unread'] = 'Expand Unread';
+$labels['collapse-all'] = 'Collapse All';
+$labels['threaded'] = 'Threaded';
+
+$labels['autoexpand_threads'] = 'Expand message threads';
+$labels['do_expand'] = 'all threads';
+$labels['expand_only_unread'] = 'only with unread messages';
+$labels['fromto'] = 'Sender/Recipient';
+$labels['flag'] = 'Flag';
+$labels['attachment'] = 'Attachment';
+$labels['nonesort'] = 'None';
+$labels['sentdate'] = 'Sent date';
+$labels['arrival'] = 'Arrival date';
+$labels['asc'] = 'ascending';
+$labels['desc'] = 'descending';
+$labels['listcolumns'] = 'List columns';
+$labels['listsorting'] = 'Sorting column';
+$labels['listorder'] = 'Sorting order';
+$labels['listmode'] = 'List view mode';
 
 $labels['compact'] = 'Compact';
 $labels['empty'] = 'Empty';
@@ -308,7 +332,6 @@
 $labels['focusonnewmessage'] = 'Focus browser window on new message';
 $labels['checkallfolders'] = 'Check all folders for new messages';
 $labels['displaynext'] = 'After message delete/move display the next message';
-$labels['indexsort'] = 'Use message index for sorting by date';
 $labels['mainoptions'] = 'Main Options';
 $labels['section'] = 'Section';
 $labels['maintenance'] = 'Maintenance';
diff --git a/program/localization/pl_PL/labels.inc b/program/localization/pl_PL/labels.inc
index 3ceb0ac..4045e0f 100644
--- a/program/localization/pl_PL/labels.inc
+++ b/program/localization/pl_PL/labels.inc
@@ -130,7 +130,7 @@
 $labels['messageactions'] = 'Więcej akcji...';
 $labels['select'] = 'Zaznacz';
 $labels['all'] = 'Wszystkie';
-$labels['none'] = 'Anuluj';
+$labels['none'] = 'Brak';
 $labels['unread'] = 'Nieprzeczytane';
 $labels['flagged'] = 'Oznaczone';
 $labels['unanswered'] = 'Bez odpowiedzi';
diff --git a/program/steps/mail/check_recent.inc b/program/steps/mail/check_recent.inc
index 9bc9e6f..3c28275 100644
--- a/program/steps/mail/check_recent.inc
+++ b/program/steps/mail/check_recent.inc
@@ -32,13 +32,12 @@
       }
       
       // get overall message count; allow caching because rcube_imap::recent_uids() did a refresh
-      $all_count = $IMAP->messagecount();
+      $all_count = $IMAP->messagecount(NULL, $IMAP->threading ? 'THREADS' : 'ALL');
       
       $unread_count = $IMAP->messagecount(NULL, 'UNSEEN', TRUE);
       $_SESSION['unseen_count'][$mbox_name] = $unread_count;
 
       $OUTPUT->set_env('messagecount', $all_count);
-      $OUTPUT->set_env('pagesize', $IMAP->page_size);
       $OUTPUT->set_env('pagecount', ceil($all_count/$IMAP->page_size));
       $OUTPUT->command('set_unread_count', $mbox_name, $unread_count, ($mbox_name == 'INBOX'));
       $OUTPUT->command('set_rowcount', rcmail_get_messagecount_text($all_count));
@@ -56,17 +55,27 @@
       if (empty($_GET['_list']))
         continue;
 
-      // use SEARCH/SORT to find recent messages
-      $search_str = 'UID '.min($recents).':'.max($recents);
-      if ($search_request)
-        $search_str .= ' '.$IMAP->search_string;
+      if ($IMAP->threading) {
+        $OUTPUT->command('message_list.clear');
+        $sort_col   = isset($_SESSION['sort_col'])   ? $_SESSION['sort_col']   : $CONFIG['message_sort_col'];
+        $sort_order = isset($_SESSION['sort_order']) ? $_SESSION['sort_order'] : $CONFIG['message_sort_order'];
+        $result_h = $IMAP->list_headers($mbox_name, NULL, $sort_col, $sort_order);
+        // add to the list
+        rcmail_js_message_list($result_h);
+      }
+      else {
+        // use SEARCH/SORT to find recent messages
+        $search_str = 'UID '.min($recents).':'.max($recents);
+        if ($search_request)
+          $search_str .= ' '.$IMAP->search_string;
 
-      if ($IMAP->search($mbox_name, $search_str, NULL, 'date')) {
-        // revert sort order
-        $order = $_SESSION['sort_col'] == 'date' && $_SESSION['sort_order'] == 'DESC' ? 'ASC' : 'DESC';
-        // get the headers and add them to the list
-        $result_h = $IMAP->list_headers($mbox_name, 1, 'date', $order);
-        rcmail_js_message_list($result_h, true, false);
+        if ($IMAP->search($mbox_name, $search_str, NULL, 'date')) {
+          // revert sort order
+          $order = $_SESSION['sort_col'] == 'date' && $_SESSION['sort_order'] == 'DESC' ? 'ASC' : 'DESC';
+          // get the headers and add them to the list
+          $result_h = $IMAP->list_headers($mbox_name, 1, 'date', $order);
+          rcmail_js_message_list($result_h, true, false);
+        }
       }
     }
     else {
diff --git a/program/steps/mail/func.inc b/program/steps/mail/func.inc
index 1968d20..5f52161 100644
--- a/program/steps/mail/func.inc
+++ b/program/steps/mail/func.inc
@@ -59,6 +59,17 @@
 if (!isset($_SESSION['sort_order']))
   $_SESSION['sort_order'] = $CONFIG['message_sort_order'];
 
+// set threads mode
+$a_threading = $RCMAIL->config->get('message_threading', array());
+if (isset($_GET['_threads'])) {
+  if ($_GET['_threads'])
+    $a_threading[$_SESSION['mbox']] = true;
+  else
+    unset($a_threading[$_SESSION['mbox']]);
+  $RCMAIL->user->save_prefs(array('message_threading' => $a_threading));
+}
+$IMAP->set_threading($a_threading[$_SESSION['mbox']]);
+
 // set message set for search result
 if (!empty($_REQUEST['_search']) && isset($_SESSION['search'][$_REQUEST['_search']]))
   {
@@ -88,13 +99,20 @@
       $OUTPUT->set_env('search_mods', $search_mods);
       
       // make sure the message count is refreshed (for default view)
-      $IMAP->messagecount($mbox_name, 'ALL', true);
+      $IMAP->messagecount($mbox_name, $IMAP->threading ? 'THREADS' : 'ALL', true);
     }
 	
-  // set current mailbox in client environment
+  // set current mailbox and some other vars in client environment
   $OUTPUT->set_env('mailbox', $mbox_name);
+  $OUTPUT->set_env('pagesize', $IMAP->page_size);
   $OUTPUT->set_env('quota', $IMAP->get_capability('quota'));
   $OUTPUT->set_env('delimiter', $IMAP->get_hierarchy_delimiter());
+  $OUTPUT->set_env('threading', (bool) $IMAP->threading);
+  $OUTPUT->set_env('threads', $IMAP->threading
+	|| $IMAP->get_capability('thread=references')
+        || $IMAP->get_capability('thread=orderedsubject')
+        || $IMAP->get_capability('thread=refs')  
+  );
 
   if ($CONFIG['flag_for_deletion'])
     $OUTPUT->set_env('flag_for_deletion', true);
@@ -123,30 +141,15 @@
  * return the message list as HTML table
  */
 function rcmail_message_list($attrib)
-  {
-  global $IMAP, $CONFIG, $COMM_PATH, $OUTPUT;
+{
+  global $IMAP, $CONFIG, $OUTPUT;
 
-  $skin_path = $CONFIG['skin_path'];
-  $image_tag = '<img src="%s%s" alt="%s" />';
-
-  // check to see if we have some settings for sorting
-  $sort_col   = $_SESSION['sort_col'];
-  $sort_order = $_SESSION['sort_order'];
-  
   // add some labels to client
   $OUTPUT->add_label('from', 'to');
-
-  // get message headers
-  $a_headers = $IMAP->list_headers('', '', $sort_col, $sort_order);
 
   // add id to message list table if not specified
   if (!strlen($attrib['id']))
     $attrib['id'] = 'rcubemessagelist';
-
-  // allow the following attributes to be added to the <table> tag
-  $attrib_str = create_attrib_string($attrib, array('style', 'class', 'id', 'cellpadding', 'cellspacing', 'border', 'summary'));
-
-  $out = '<table' . $attrib_str . ">\n";
 
   // define list of cols to be displayed based on parameter or config
   if (empty($attrib['columns']))
@@ -154,12 +157,10 @@
   else
       $a_show_cols = preg_split('/[\s,;]+/', strip_quotes($attrib['columns']));
 
-  // store column list in a session-variable
+  // save some variables for use in ajax list
   $_SESSION['list_columns'] = $a_show_cols;
+  $_SESSION['list_attrib'] = $attrib;
   
-  // define sortable columns
-  $a_sort_cols = array('subject', 'date', 'from', 'to', 'size');
-
   $mbox = $IMAP->get_mailbox_name();
   $delim = $IMAP->get_hierarchy_delimiter();
 
@@ -167,198 +168,19 @@
   if ((strpos($mbox.$delim, $CONFIG['sent_mbox'].$delim)===0 || strpos($mbox.$delim, $CONFIG['drafts_mbox'].$delim)===0)
       && (($f = array_search('from', $a_show_cols)) !== false) && array_search('to', $a_show_cols) === false)
     $a_show_cols[$f] = 'to';
-  
-  // add col definition
-  $out .= '<colgroup>';
-  $out .= '<col class="icon" />';
 
-  foreach ($a_show_cols as $col)
-    $out .= ($col!='attachment') ? sprintf('<col class="%s" />', $col) : '<col class="icon" />';
-
-  $out .= "</colgroup>\n";
-
-  // add table title
-  $out .= "<thead><tr>\n<td class=\"icon\">&nbsp;</td>\n";
-
-  $javascript = '';
-  foreach ($a_show_cols as $col)
-    {
-    // get column name
-    switch ($col)
-      {
-      case 'flag':
-        $col_name = sprintf($image_tag, $skin_path, $attrib['unflaggedicon'], '');
-        break;
-      case 'attachment':
-        $col_name = sprintf($image_tag, $skin_path, $attrib['attachmenticon'], '');
-        break;
-      default:
-        $col_name = Q(rcube_label($col));
-    }
-
-    // make sort links
-    $sort = '';
-    if (in_array($col, $a_sort_cols))
-      {
-      // have buttons configured
-      if (!empty($attrib['sortdescbutton']) || !empty($attrib['sortascbutton']))
-        {
-        $sort = '&nbsp;&nbsp;';
-
-        // asc link
-        if (!empty($attrib['sortascbutton']))
-          {
-          $sort .= $OUTPUT->button(array(
-            'command' => 'sort',
-            'prop' => $col.'_ASC',
-            'image' => $attrib['sortascbutton'],
-            'align' => 'absmiddle',
-            'title' => 'sortasc'));
-          }       
-        
-        // desc link
-        if (!empty($attrib['sortdescbutton']))
-          {
-          $sort .= $OUTPUT->button(array(
-            'command' => 'sort',
-            'prop' => $col.'_DESC',
-            'image' => $attrib['sortdescbutton'],
-            'align' => 'absmiddle',
-            'title' => 'sortdesc'));
-          }
-        }
-      // just add a link tag to the header
-      else
-        {
-        $col_name = sprintf(
-          '<a href="./#sort" onclick="return %s.command(\'sort\',\'%s\',this)" title="%s">%s</a>',
-          JS_OBJECT_NAME,
-          $col,
-          rcube_label('sortby'),
-          $col_name);
-        }
-      }
-      
-    $sort_class = $col==$sort_col ? " sorted$sort_order" : '';
-
-    // put it all together
-    if ($col!='attachment')
-      $out .= '<td class="'.$col.$sort_class.'" id="rcm'.$col.'">' . "$col_name$sort</td>\n";
-    else    
-      $out .= '<td class="icon" id="rcm'.$col.'">' . "$col_name$sort</td>\n";
-    }
-
-  $out .= "</tr></thead>\n<tbody>\n";
-
-  // no messages in this mailbox
-  if (!sizeof($a_headers))
-    $OUTPUT->show_message('nomessagesfound', 'notice');
-
-  $a_js_message_arr = array();
-
-  // create row for each message
-  foreach ($a_headers as $i => $header)  //while (list($i, $header) = each($a_headers))
-    {
-    $message_icon = $attach_icon = $flagged_icon = '';
-    $js_row_arr = array();
-    $zebra_class = $i%2 ? ' even' : ' odd';
-
-    // set messag attributes to javascript array
-    if ($header->deleted)
-      $js_row_arr['deleted'] = true;
-    if (!$header->seen)
-      $js_row_arr['unread'] = true;
-    if ($header->answered)
-      $js_row_arr['replied'] = true;
-    if ($header->forwarded)
-      $js_row_arr['forwarded'] = true;
-    if ($header->flagged)
-      $js_row_arr['flagged'] = true;
-
-    // set message icon  
-    if ($attrib['deletedicon'] && $header->deleted)
-      $message_icon = $attrib['deletedicon'];
-    else if ($attrib['repliedicon'] && $header->answered)
-      {
-      if ($attrib['forwardedrepliedicon'] && $header->forwarded)
-        $message_icon = $attrib['forwardedrepliedicon'];
-      else
-        $message_icon = $attrib['repliedicon'];
-      }
-    else if ($attrib['forwardedicon'] && $header->forwarded)
-      $message_icon = $attrib['forwardedicon'];
-    else if ($attrib['unreadicon'] && !$header->seen)
-      $message_icon = $attrib['unreadicon'];
-    else if ($attrib['messageicon'])
-      $message_icon = $attrib['messageicon'];
-
-    if ($attrib['flaggedicon'] && $header->flagged)
-      $flagged_icon = $attrib['flaggedicon'];
-    else if ($attrib['unflaggedicon'] && !$header->flagged)
-      $flagged_icon = $attrib['unflaggedicon'];
-    
-    // set attachment icon
-    if ($attrib['attachmenticon'] && preg_match("/multipart\/m/i", $header->ctype))
-      $attach_icon = $attrib['attachmenticon'];
-        
-    $out .= sprintf('<tr id="rcmrow%d" class="message%s%s%s%s">'."\n",
-                    $header->uid,
-                    $header->seen ? '' : ' unread',
-                    $header->deleted ? ' deleted' : '',
-                    $header->flagged ? ' flagged' : '',
-                    $zebra_class);
-    
-    $out .= sprintf("<td class=\"icon\">%s</td>\n", $message_icon ? sprintf($image_tag, $skin_path, $message_icon, '') : '');
-
-    $IMAP->set_charset(!empty($header->charset) ? $header->charset : $CONFIG['default_charset']);
-  
-    // format each col
-    foreach ($a_show_cols as $col)
-      {
-      if (in_array($col, array('from', 'to', 'cc', 'replyto')))
-        $cont = Q(rcmail_address_string($header->$col, 3, false, $attrib['addicon']), 'show');
-      else if ($col=='subject')
-        {
-        $action = $mbox==$CONFIG['drafts_mbox'] ? 'compose' : 'show';
-        $uid_param = $mbox==$CONFIG['drafts_mbox'] ? '_draft_uid' : '_uid';
-        $cont = abbreviate_string(trim($IMAP->decode_header($header->$col)), 160);
-        if (empty($cont)) $cont = rcube_label('nosubject');
-        $cont = $OUTPUT->browser->ie ? Q($cont) : sprintf('<a href="%s" onclick="return rcube_event.cancel(event)">%s</a>', Q(rcmail_url($action, array($uid_param=>$header->uid, '_mbox'=>$mbox))), Q($cont));
-        }
-      else if ($col=='flag')
-        $cont = $flagged_icon ? sprintf($image_tag, $skin_path, $flagged_icon, '') : '';
-      else if ($col=='size')
-        $cont = show_bytes($header->$col);
-      else if ($col=='date')
-        $cont = format_date($header->date);
-      else
-        $cont = Q($header->$col);
-        
-      if ($col!='attachment')
-        $out .= '<td class="'.$col.'">' . $cont . "</td>\n";
-      else
-        $out .= sprintf("<td class=\"icon\">%s</td>\n", $attach_icon ? sprintf($image_tag, $skin_path, $attach_icon, '') : '&nbsp;');
-      }
-
-    $out .= "</tr>\n";
-    
-    if (sizeof($js_row_arr))
-      $a_js_message_arr[$header->uid] = $js_row_arr;
-    }
-  
-  // complete message table
-  $out .= "</tbody></table>\n";
-  
-  $message_count = $IMAP->messagecount();
+  $skin_path = $_SESSION['skin_path'] = $CONFIG['skin_path'];
+  $message_count = $IMAP->messagecount(NULL, $IMAP->threading ? 'THREADS' : 'ALL');
   
   // set client env
   $OUTPUT->add_gui_object('mailcontframe', 'mailcontframe');
   $OUTPUT->add_gui_object('messagelist', $attrib['id']);
+  $OUTPUT->set_env('autoexpand_threads', intval($CONFIG['autoexpand_threads']));
   $OUTPUT->set_env('messagecount', $message_count);
   $OUTPUT->set_env('current_page', $IMAP->list_page);
   $OUTPUT->set_env('pagecount', ceil($message_count/$IMAP->page_size));
-  $OUTPUT->set_env('sort_col', $sort_col);
-  $OUTPUT->set_env('sort_order', $sort_order);
+  $OUTPUT->set_env('sort_col', $_SESSION['sort_col']);
+  $OUTPUT->set_env('sort_order', $_SESSION['sort_order']);
   
   if ($attrib['messageicon'])
     $OUTPUT->set_env('messageicon', $skin_path . $attrib['messageicon']);
@@ -378,22 +200,35 @@
     $OUTPUT->set_env('flaggedicon', $skin_path . $attrib['flaggedicon']);
   if ($attrib['unflaggedicon'])
     $OUTPUT->set_env('unflaggedicon', $skin_path . $attrib['unflaggedicon']);
+  if ($attrib['unreadchildrenicon'])
+    $OUTPUT->set_env('unreadchildrenicon', $skin_path . $attrib['unreadchildrenicon']);
   
-  $OUTPUT->set_env('messages', $a_js_message_arr);
+  $OUTPUT->set_env('messages', array());
   $OUTPUT->set_env('coltypes', $a_show_cols);
+  
+  if (!$message_count)
+    $OUTPUT->show_message('nomessagesfound', 'notice');
   
   $OUTPUT->include_script('list.js');
   
-  return $out;
-  }
+  $thead = '';
+  foreach (rcmail_message_list_head($attrib, $a_show_cols) as $cell)
+    $thead .= html::tag('td', array('class' => $cell['className'], 'id' => $cell['id']), $cell['html']);
+  
+  return html::tag('table',
+    $attrib,
+    html::tag('thead', null, html::tag('tr', null, $thead)) .
+      html::tag('tbody', null, ''),
+    array('style', 'class', 'id', 'cellpadding', 'cellspacing', 'border', 'summary'));
+}
 
 
 /**
  * return javascript commands to add rows to the message list
  * or to replace the whole list (IE only)
  */
-function rcmail_js_message_list($a_headers, $insert_top=FALSE, $replace=TRUE)
-  {
+function rcmail_js_message_list($a_headers, $insert_top=FALSE, $replace=TRUE, $head_replace=FALSE)
+{
   global $CONFIG, $IMAP, $OUTPUT;
 
   if (empty($_SESSION['list_columns']))
@@ -409,9 +244,12 @@
       && (($f = array_search('from', $a_show_cols)) !== false) && array_search('to', $a_show_cols) === false)
     $a_show_cols[$f] = 'to';
 
-  $browser = new rcube_browser;
+  $thead = $head_replace ? rcmail_message_list_head($_SESSION['list_attrib'], $a_show_cols) : NULL;
+  
+  $OUTPUT->command('set_message_coltypes', $a_show_cols, $thead);
 
-  $OUTPUT->command('set_message_coltypes', $a_show_cols);
+  if (empty($a_headers))
+    return;
 
   // remove 'attachment' and 'flag' columns, we don't need them here
   if(($key = array_search('attachment', $a_show_cols)) !== FALSE)
@@ -419,7 +257,7 @@
   if(($key = array_search('flag', $a_show_cols)) !== FALSE)
     unset($a_show_cols[$key]);
 
-  if ($browser->ie && $replace)
+  if ($OUTPUT->browser->ie && $replace)
     $OUTPUT->command('offline_message_list', true);
 
   // loop through message headers
@@ -440,10 +278,9 @@
         $cont = Q(rcmail_address_string($header->$col, 3), 'show');
       else if ($col=='subject')
         {
-	$cont = abbreviate_string(trim($IMAP->decode_header($header->$col)), 160);
+        $cont = abbreviate_string(trim($IMAP->decode_header($header->$col)), 160);
         if (!$cont) $cont = rcube_label('nosubject');
-	$cont = Q($cont);
-	$a_msg_cols['mbox'] = $mbox;
+        $cont = Q($cont);
         }
       else if ($col=='size')
         $cont = show_bytes($header->$col);
@@ -455,6 +292,14 @@
       $a_msg_cols[$col] = $cont;
       }
 
+    if ($header->depth)
+      $a_msg_flags['depth'] = $header->depth;
+    if ($header->parent_uid)
+      $a_msg_flags['parent_uid'] = $header->parent_uid;
+    if ($header->has_children)
+      $a_msg_flags['has_children'] = $header->has_children;
+    if ($header->unread_children)
+      $a_msg_flags['unread_children'] = $header->unread_children;
     if ($header->deleted)
       $a_msg_flags['deleted'] = 1;
     if (!$header->seen)
@@ -465,18 +310,75 @@
       $a_msg_flags['forwarded'] = 1;
     if ($header->flagged)
       $a_msg_flags['flagged'] = 1;
-      
+    if(preg_match("/multipart\/m/i", $header->ctype))
+      $a_msg_flags['attachment'] = 1;
+    $a_msg_flags['mbox'] = $mbox;
+
     $OUTPUT->command('add_message_row',
       $header->uid,
       $a_msg_cols,
       $a_msg_flags,
-      preg_match("/multipart\/m/i", $header->ctype),
       $insert_top);
     }
 
   if ($browser->ie && $replace)
     $OUTPUT->command('offline_message_list', false);
   }
+
+
+/*
+ * Creates <THEAD> for message list table
+ */
+function rcmail_message_list_head($attrib, $a_show_cols)
+{
+  global $CONFIG;
+
+  $skin_path = $_SESSION['skin_path'];
+  $image_tag = html::img(array('src' => "%s%s", 'alt' => "%s"));
+
+  // check to see if we have some settings for sorting
+  $sort_col   = $_SESSION['sort_col'];
+  $sort_order = $_SESSION['sort_order'];
+
+  // define sortable columns
+  $a_sort_cols = array('subject', 'date', 'from', 'to', 'size', 'cc');
+  
+  if (!empty($attrib['optionsmenuicon']))
+    $list_menu = html::a(
+      array('href' => '#', 'onclick' => 'return '.JS_OBJECT_NAME.".command('menu-open', 'messagelistmenu')"),
+      html::img(array('src' => $skin_path . $attrib['optionsmenuicon'], 'id' => 'listmenulink', 'title' => rcube_label('listoptions')))
+    );
+  else
+    $list_menu = '';
+
+  $cells = array(array('className' => 'threads', 'html' => $list_menu));
+
+  foreach ($a_show_cols as $col) {
+    // get column name
+    switch ($col) {
+      case 'flag':
+        $col_name = sprintf($image_tag, $skin_path, $attrib['unflaggedicon'], '');
+        break;
+      case 'attachment':
+        $col_name = sprintf($image_tag, $skin_path, $attrib['attachmenticon'], '');
+        break;
+      default:
+        $col_name = Q(rcube_label($col));
+    }
+
+    // make sort links
+    if (in_array($col, $a_sort_cols))
+      $col_name = html::a(array('href'=>"./#sort", 'onclick' => 'return '.JS_OBJECT_NAME.".command('sort','".$col."',this)", 'title' => rcube_label('sortby')), $col_name);
+
+    $sort_class = $col == $sort_col ? " sorted$sort_order" : '';
+    $class_name = $col == 'attachment' ? 'icon' : $col.$sort_class;
+
+    // put it all together
+    $cells[] = array('className' => $class_name, 'id' => "rcm$col", 'html' => $col_name);
+  }
+
+  return $cells;
+}
 
 
 /**
@@ -513,7 +415,7 @@
 
 function rcmail_quota_display($attrib)
   {
-  global $OUTPUT, $COMM_PATH;
+  global $OUTPUT;
 
   if (!$attrib['id'])
     $attrib['id'] = 'rcmquotadisplay';
@@ -582,23 +484,23 @@
   if (isset($MESSAGE->index))
     {
     return rcube_label(array('name' => 'messagenrof',
-                             'vars' => array('nr'  => $MESSAGE->index+1,
-                                             'count' => $count!==NULL ? $count : $IMAP->messagecount())));
+        'vars' => array('nr'  => $MESSAGE->index+1,
+        'count' => $count!==NULL ? $count : $IMAP->messagecount(NULL, 'ALL')))); // Only messages, no threads here
     }
 
   if ($page===NULL)
     $page = $IMAP->list_page;
     
   $start_msg = ($page-1) * $IMAP->page_size + 1;
-  $max = $count!==NULL ? $count : $IMAP->messagecount();
+  $max = $count!==NULL ? $count : $IMAP->messagecount(NULL, $IMAP->threading ? 'THREADS' : 'ALL');
 
   if ($max==0)
     $out = rcube_label('mailboxempty');
   else
-    $out = rcube_label(array('name' => 'messagesfromto',
-                              'vars' => array('from'  => $start_msg,
-                                              'to'    => min($max, $start_msg + $IMAP->page_size - 1),
-                                              'count' => $max)));
+    $out = rcube_label(array('name' => $IMAP->threading ? 'threadsfromto' : 'messagesfromto',
+            'vars' => array('from'  => $start_msg,
+            'to'    => min($max, $start_msg + $IMAP->page_size - 1),
+            'count' => $max)));
 
   return Q($out);
   }
diff --git a/program/steps/mail/list.inc b/program/steps/mail/list.inc
index cd1a314..89d127c 100644
--- a/program/steps/mail/list.inc
+++ b/program/steps/mail/list.inc
@@ -33,8 +33,6 @@
   $save_arr = array();
   $_SESSION['sort_col'] = $save_arr['message_sort_col'] = $sort_col;
   $_SESSION['sort_order'] = $save_arr['message_sort_order'] = $sort_order;
-  
-  $RCMAIL->user->save_prefs($save_arr);
 }
 else
 {
@@ -42,6 +40,16 @@
   $sort_col   = isset($_SESSION['sort_col'])   ? $_SESSION['sort_col']   : $CONFIG['message_sort_col'];
   $sort_order = isset($_SESSION['sort_order']) ? $_SESSION['sort_order'] : $CONFIG['message_sort_order'];
 }
+
+// is there a set of columns for this request?
+if ($cols = get_input_value('_cols', RCUBE_INPUT_GET))
+{
+  $save_arr = array();
+  $_SESSION['list_columns'] = $save_arr['list_cols'] = explode(',', $cols);
+}
+
+if ($save_arr)  
+  $RCMAIL->user->save_prefs($save_arr);
 
 $mbox_name = $IMAP->get_mailbox_name();
 
@@ -55,8 +63,12 @@
 }
 
 // fetch message headers
-if ($count = $IMAP->messagecount($mbox_name, 'ALL', !empty($_REQUEST['_refresh'])))
+if ($count = $IMAP->messagecount($mbox_name, $IMAP->threading ? 'THREADS' : 'ALL', !empty($_REQUEST['_refresh'])))
   $a_headers = $IMAP->list_headers($mbox_name, NULL, $sort_col, $sort_order);
+
+// update search set (possible change of threading mode)
+if (!empty($_REQUEST['_search']) && isset($_SESSION['search'][$_REQUEST['_search']]))
+  $_SESSION['search'][$_REQUEST['_search']] = $IMAP->get_search_set();
 
 // update mailboxlist
 rcmail_send_unread_count($mbox_name, !empty($_REQUEST['_refresh']));
@@ -65,13 +77,14 @@
 $pages = ceil($count/$IMAP->page_size);
 $OUTPUT->set_env('messagecount', $count);
 $OUTPUT->set_env('pagecount', $pages);
+$OUTPUT->set_env('threading', (bool) $IMAP->threading);
 $OUTPUT->command('set_rowcount', rcmail_get_messagecount_text($count));
 $OUTPUT->command('set_mailboxname', rcmail_get_mailbox_name_text());
 
 // add message rows
+rcmail_js_message_list($a_headers, FALSE, TRUE, (bool) $cols);
 if (isset($a_headers) && count($a_headers))
 {
-  rcmail_js_message_list($a_headers);
   if ($search_request)
     $OUTPUT->show_message('searchsuccessful', 'confirmation', array('nr' => $count));
 }
diff --git a/program/steps/mail/mark.inc b/program/steps/mail/mark.inc
index 3e0ee9e..c37a6e4 100644
--- a/program/steps/mail/mark.inc
+++ b/program/steps/mail/mark.inc
@@ -36,7 +36,7 @@
 
   if ($flag == 'DELETED' && $CONFIG['skip_deleted'] && $_POST['_from'] != 'show') {
     // count messages before changing anything
-    $old_count = $IMAP->messagecount();
+    $old_count = $IMAP->messagecount(NULL, $IMAP->threading ? 'THREADS' : 'ALL');
     $old_pages = ceil($old_count / $IMAP->page_size);
     $count = sizeof(explode(',', $uids));
   }
@@ -75,7 +75,7 @@
         $_SESSION['search'][$search_request] = $IMAP->refresh_search();
       }
 
-      $msg_count      = $IMAP->messagecount();
+      $msg_count      = $IMAP->messagecount(NULL, $IMAP->threading ? 'THREADS' : 'ALL');
       $pages          = ceil($msg_count / $IMAP->page_size);
       $nextpage_count = $old_count - $IMAP->page_size * $IMAP->list_page;
       $remaining      = $msg_count - $IMAP->page_size * ($IMAP->list_page - 1);
@@ -103,8 +103,11 @@
       }
       $OUTPUT->command('set_rowcount', rcmail_get_messagecount_text($msg_count));
 
+      if ($IMAP->threading)
+	$count = get_input_value('_count', RCUBE_INPUT_POST);
+
       // add new rows from next page (if any)
-      if (($jump_back || $nextpage_count > 0)) {
+      if ($count && ($jump_back || $nextpage_count > 0)) {
         $sort_col   = isset($_SESSION['sort_col'])   ? $_SESSION['sort_col']   : $CONFIG['message_sort_col'];
         $sort_order = isset($_SESSION['sort_order']) ? $_SESSION['sort_order'] : $CONFIG['message_sort_order'];
   
diff --git a/program/steps/mail/move_del.inc b/program/steps/mail/move_del.inc
index b8bd165..f884a78 100644
--- a/program/steps/mail/move_del.inc
+++ b/program/steps/mail/move_del.inc
@@ -24,7 +24,7 @@
   return;
 
 // count messages before changing anything
-$old_count = $IMAP->messagecount();
+$old_count = $IMAP->messagecount(NULL, $IMAP->threading ? 'THREADS' : 'ALL');
 $old_pages = ceil($old_count / $IMAP->page_size);
 
 // move messages
@@ -50,6 +50,7 @@
 else if ($RCMAIL->action=='delete' && !empty($_POST['_uid'])) {
     $count = sizeof(explode(',', ($uids = get_input_value('_uid', RCUBE_INPUT_POST))));
     $mbox = get_input_value('_mbox', RCUBE_INPUT_POST);
+
     $del = $IMAP->delete_message($uids, $mbox);
   
     if (!$del) {
@@ -82,7 +83,7 @@
 }
 else
 {
-  $msg_count      = $IMAP->messagecount();
+  $msg_count      = $IMAP->messagecount(NULL, $IMAP->threading ? 'THREADS' : 'ALL');
   $pages          = ceil($msg_count / $IMAP->page_size);
   $nextpage_count = $old_count - $IMAP->page_size * $IMAP->list_page;
   $remaining      = $msg_count - $IMAP->page_size * ($IMAP->list_page - 1);
@@ -116,8 +117,11 @@
   $OUTPUT->command('set_quota', rcmail_quota_content());
   $OUTPUT->command('set_rowcount', rcmail_get_messagecount_text($msg_count));
 
+  if ($IMAP->threading)
+    $count = get_input_value('_count', RCUBE_INPUT_POST);
+
   // add new rows from next page (if any)
-  if ($addrows && ($jump_back || $nextpage_count > 0)) {
+  if ($addrows && $count && ($jump_back || $nextpage_count > 0)) {
     $sort_col   = isset($_SESSION['sort_col'])   ? $_SESSION['sort_col']   : $CONFIG['message_sort_col'];
     $sort_order = isset($_SESSION['sort_order']) ? $_SESSION['sort_order'] : $CONFIG['message_sort_order'];
 
diff --git a/program/steps/mail/search.inc b/program/steps/mail/search.inc
index 54c7650..5246995 100644
--- a/program/steps/mail/search.inc
+++ b/program/steps/mail/search.inc
@@ -104,7 +104,7 @@
 
 // Get the headers
 $result_h = $IMAP->list_headers($mbox, 1, $_SESSION['sort_col'], $_SESSION['sort_order']);
-$count = $IMAP->messagecount();
+$count = $IMAP->messagecount(NULL, $IMAP->threading ? 'THREADS' : 'ALL');
 
 // save search results in session
 if (!is_array($_SESSION['search']))
@@ -120,7 +120,7 @@
 {
   rcmail_js_message_list($result_h);
   if ($search_str)
-    $OUTPUT->show_message('searchsuccessful', 'confirmation', array('nr' => $count));
+    $OUTPUT->show_message('searchsuccessful', 'confirmation', array('nr' => $IMAP->messagecount(NULL, 'ALL')));
 }
 else
 {
diff --git a/program/steps/mail/show.inc b/program/steps/mail/show.inc
index 33e87d5..02e1f48 100644
--- a/program/steps/mail/show.inc
+++ b/program/steps/mail/show.inc
@@ -104,7 +104,7 @@
     $next = $prev = $first = $last = -1;
 
     if ($_SESSION['sort_col'] == 'date' && $_SESSION['sort_order'] != 'DESC'
-        && empty($_REQUEST['_search']) && !$IMAP->skip_deleted)
+        && empty($_REQUEST['_search']) && !$CONFIG['skip_deleted'] && !$IMAP->threading)
       {
       // this assumes that we are sorted by date_DESC
       $cnt = $IMAP->messagecount();
@@ -142,7 +142,7 @@
 
   if (!$MESSAGE->headers->seen)
     $RCMAIL->plugins->exec_hook('message_read', array('uid' => $MESSAGE->uid,
-      'mailbox' => $IMAP->mailbox, 'message' => $MESSAGE));
+      'mailbox' => $mbox_name, 'message' => $MESSAGE));
 }
 
 
diff --git a/program/steps/mail/viewsource.inc b/program/steps/mail/viewsource.inc
index 17e3824..2e2228a 100644
--- a/program/steps/mail/viewsource.inc
+++ b/program/steps/mail/viewsource.inc
@@ -25,7 +25,7 @@
 if ($uid = get_input_value('_uid', RCUBE_INPUT_GET))
 {
   $headers = $IMAP->get_headers($uid);
-  $charset = $headers->charset ? $headers->charset : $IMAP->default_charset;
+  $charset = $headers->charset ? $headers->charset : $CONFIG['default_charset'];
   header("Content-Type: text/plain; charset={$charset}");
 
   if (!empty($_GET['_save'])) {
diff --git a/program/steps/settings/func.inc b/program/steps/settings/func.inc
index 44225c7..421fb37 100644
--- a/program/steps/settings/func.inc
+++ b/program/steps/settings/func.inc
@@ -248,17 +248,6 @@
       );
     }
 
-    // Show checkbox for toggling 'index_sort' 
-    if (!isset($no_override['index_sort'])) {
-      $field_id = 'rcmfd_indexsort';
-      $input_indexsort = new html_checkbox(array('name' => '_index_sort', 'id' => $field_id, 'value' => 1));
-
-      $blocks['list']['options']['index_sort'] = array(
-        'title' => html::label($field_id, Q(rcube_label('indexsort'))),
-        'content' => $input_indexsort->show($config['index_sort']?1:0),
-      );
-    }
-
     // show drop-down for available skins
     if (!isset($no_override['skin'])) {
       $skins = rcmail_get_skins();
@@ -311,6 +300,19 @@
       );
     }
 
+    if (!isset($no_override['autoexpand_threads'])) {
+      $field_id = 'rcmfd_autoexpand_threads';
+      $select_autoexpand_threads = new html_select(array('name' => '_autoexpand_threads', 'id' => $field_id));
+      $select_autoexpand_threads->add(rcube_label('never'), 0);
+      $select_autoexpand_threads->add(rcube_label('do_expand'), 1);
+      $select_autoexpand_threads->add(rcube_label('expand_only_unread'), 2);
+      
+      $blocks['main']['options']['autoexpand_threads'] = array(
+	'title' => html::label($field_id, Q(rcube_label('autoexpand_threads'))),
+	'content' => $select_autoexpand_threads->show($config['autoexpand_threads']),
+      );
+    }
+
     if (!isset($no_override['focus_on_new_message'])) {
       $field_id = 'rcmfd_focus_on_new_message';
       $input_focus_on_new_message = new html_checkbox(array('name' => '_focus_on_new_message', 'id' => $field_id, 'value' => 1));
diff --git a/program/steps/settings/manage_folders.inc b/program/steps/settings/manage_folders.inc
index 06fee7a..fa5a4db 100644
--- a/program/steps/settings/manage_folders.inc
+++ b/program/steps/settings/manage_folders.inc
@@ -38,6 +38,20 @@
     $IMAP->unsubscribe(array($mbox));
   }
 
+// enable threading for one or more mailboxes
+else if ($RCMAIL->action=='enable-threading')
+  {
+  if ($mbox = get_input_value('_mbox', RCUBE_INPUT_POST, false, 'UTF7-IMAP'))
+    rcube_set_threading($mbox, true);
+  }
+
+// enable threading for one or more mailboxes
+else if ($RCMAIL->action=='disable-threading')
+  {
+  if ($mbox = get_input_value('_mbox', RCUBE_INPUT_POST, false, 'UTF7-IMAP'))
+    rcube_set_threading($mbox, false);
+  }
+
 // create a new mailbox
 else if ($RCMAIL->action=='create-folder')
   {
@@ -77,6 +91,24 @@
     $oldname = rcube_charset_convert($oldname_utf8, RCMAIL_CHARSET, 'UTF7-IMAP');
 
     $rename = $IMAP->rename_mailbox($oldname, $name);
+    }
+
+  // update per-folder options for modified folder and its subfolders
+  if ($rename) {
+    $a_threaded = $RCMAIL->config->get('message_threading', array()); 
+    $delimiter = $IMAP->get_hierarchy_delimiter();
+    $oldprefix = '/^' . preg_quote($oldname . $delimiter, '/') . '/';
+    foreach ($a_threaded as $key => $val)
+      if ($key == $oldname) {
+        unset($a_threaded[$key]);
+	$a_threaded[$name] = true;
+        }
+      else if (preg_match($oldprefix, $key)) {
+        unset($a_threaded[$key]);
+	$a_threaded[preg_replace($oldprefix, $name.$delimiter, $key)] = true;      
+      }
+      
+    $RCMAIL->user->save_prefs(array('message_threading' => $a_threaded));
     }
 
   if ($rename && $OUTPUT->ajax_call)
@@ -159,7 +191,11 @@
 // build table with all folders listed by server
 function rcube_subscription_form($attrib)
   {
-  global $IMAP, $CONFIG, $OUTPUT;
+  global $RCMAIL, $IMAP, $CONFIG, $OUTPUT;
+
+  $threading_supported = $IMAP->get_capability('thread=references')
+    || $IMAP->get_capability('thread=orderedsubject')
+    || $IMAP->get_capability('thread=refs');
 
   list($form_start, $form_end) = get_form_tags($attrib, 'folders');
   unset($attrib['form']);
@@ -173,15 +209,17 @@
   $table->add_header('name', rcube_label('foldername'));
   $table->add_header('msgcount', rcube_label('messagecount'));
   $table->add_header('subscribed', rcube_label('subscribed'));
+  if ($threading_supported)
+    $table->add_header('threaded', rcube_label('threaded'));
   $table->add_header('rename', '&nbsp;');
   $table->add_header('delete', '&nbsp;');
-
 
   // get folders from server
   $IMAP->clear_cache('mailboxes');
 
   $a_unsubscribed = $IMAP->list_unsubscribed();
   $a_subscribed = $IMAP->list_mailboxes();
+  $a_threaded = $a_threaded_copy = $RCMAIL->config->get('message_threading', array()); 
   $delimiter = $IMAP->get_hierarchy_delimiter();
   $a_js_folders = $seen_folders = $list_folders = array();
 
@@ -203,13 +241,27 @@
       }
     }
     
+    unset($a_threaded_copy[$folder]);
+    
     $list_folders[] = array('id' => $folder, 'name' => $name, 'level' => $level);
     $seen[$folder]++;
+  }
+
+  // remove 'message_threading' option for not existing folders
+  if ($a_threaded_copy) {
+    foreach ($a_threaded_copy as $key => $val)
+      unset($a_threaded[$key]);
+    unset($a_threaded_copy);
+    $RCMAIL->user->save_prefs(array('message_threading' => $a_threaded));
   }
 
   $checkbox_subscribe = new html_checkbox(array(
     'name' => '_subscribed[]',
     'onclick' => JS_OBJECT_NAME.".command(this.checked?'subscribe':'unsubscribe',this.value)",
+  ));
+  $checkbox_threaded = new html_checkbox(array(
+    'name' => '_threaded[]',
+    'onclick' => JS_OBJECT_NAME.".command(this.checked?'enable-threading':'disable-threading',this.value)",
   ));
   
   if (!empty($attrib['deleteicon']))
@@ -226,6 +278,7 @@
   foreach ($list_folders as $i => $folder) {
     $idx = $i + 1;
     $subscribed = in_array($folder['id'], $a_subscribed);
+    $threaded = $a_threaded[$folder['id']];
     $protected = ($CONFIG['protect_default_folders'] == true && in_array($folder['id'], $CONFIG['default_imap_folders']));
     $classes = array($i%2 ? 'even' : 'odd');
     $folder_js = JQ($folder['id']);
@@ -238,9 +291,13 @@
     $table->add_row(array('id' => 'rcmrow'.$idx, 'class' => join(' ', $classes)));
     
     $table->add('name', Q($display_folder));
-    $table->add('msgcount', ($folder['virtual'] ? '' : $IMAP->messagecount($folder['id'])));
+    $table->add('msgcount', ($folder['virtual'] ? '' : $IMAP->messagecount($folder['id']))); // XXX: Use THREADS or ALL?
     $table->add('subscribed', ($protected || $folder['virtual']) ? ($subscribed ? '&nbsp;&#x2022;' : '&nbsp;') :
         $checkbox_subscribe->show(($subscribed ? $folder_utf8 : ''), array('value' => $folder_utf8)));
+    if ($IMAP->get_capability('thread=references')) {
+      $table->add('threaded',
+                  $checkbox_threaded->show(($threaded ? $folder_utf8 : ''), array('value' => $folder_utf8)));
+    }
     
     // add rename and delete buttons
     if (!$protected && !$folder['virtual']) {
@@ -335,6 +392,27 @@
   return $out;
   }
 
+
+// (un)set 'threading' for selected folder
+function rcube_set_threading($mbox, $state=true)
+  {
+  global $RCMAIL;
+  $mbox = (array)$mbox;
+  $a_prefs = (array)$RCMAIL->config->get('message_threading');
+
+  if ($state) {
+    foreach ($mbox as $box)
+      $a_prefs[$box] = true;
+    }
+  else {
+    foreach ($mbox as $box)
+      unset($a_prefs[$box]);
+    }
+
+  $RCMAIL->user->save_prefs(array('message_threading' => $a_prefs));
+  }
+
+
 $OUTPUT->set_pagetitle(rcube_label('folders'));
 $OUTPUT->include_script('list.js');
 
diff --git a/program/steps/settings/save_prefs.inc b/program/steps/settings/save_prefs.inc
index eeb6403..4eebef2 100644
--- a/program/steps/settings/save_prefs.inc
+++ b/program/steps/settings/save_prefs.inc
@@ -32,7 +32,6 @@
       'timezone'     => isset($_POST['_timezone']) ? (is_numeric($_POST['_timezone']) ? floatval($_POST['_timezone']) : get_input_value('_timezone', RCUBE_INPUT_POST)) : $CONFIG['timezone'],
       'dst_active'   => isset($_POST['_dst_active']) ? TRUE : FALSE,
       'pagesize'     => is_numeric($_POST['_pagesize']) ? max(2, intval($_POST['_pagesize'])) : $CONFIG['pagesize'],
-      'index_sort'   => isset($_POST['_index_sort']) ? TRUE : FALSE,
       'prettydate'   => isset($_POST['_pretty_date']) ? TRUE : FALSE,
       'skin' 	     => isset($_POST['_skin']) ? get_input_value('_skin', RCUBE_INPUT_POST) : $CONFIG['skin'],
     );
@@ -42,6 +41,7 @@
     $a_user_prefs = array(
       'focus_on_new_message' => isset($_POST['_focus_on_new_message']) ? TRUE : FALSE,
       'preview_pane' 	     => isset($_POST['_preview_pane']) ? TRUE : FALSE,
+      'autoexpand_threads'   => isset($_POST['_autoexpand_threads']) ? intval($_POST['_autoexpand_threads']) : 0,
       'mdn_requests'         => isset($_POST['_mdn_requests']) ? intval($_POST['_mdn_requests']) : 0,
       'keep_alive'           => isset($_POST['_keep_alive']) ? intval($_POST['_keep_alive'])*60 : $CONFIG['keep_alive'],
       'check_all_folders'    => isset($_POST['_check_all_folders']) ? TRUE : FALSE,
diff --git a/skins/default/common.css b/skins/default/common.css
index 8ec5b59..44bb4f5 100644
--- a/skins/default/common.css
+++ b/skins/default/common.css
@@ -537,7 +537,7 @@
 
 ul.toolbarmenu
 {
-  margin: 0;
+  margin: -4px 0 -4px 0;
   padding: 0;
   list-style: none;
 }
@@ -547,13 +547,14 @@
   font-size: 11px;
   white-space: nowrap;
   min-width: 130px;
+  margin: 3px -4px;
 }
 
 ul.toolbarmenu li a
 {
   display: block;
   color: #a0a0a0;
-  padding: 2px 8px 3px 22px;
+  padding: 2px 12px 3px 28px;
   text-decoration: none;
   min-height: 14px;
 }
@@ -597,3 +598,8 @@
   margin-top: 2px;
   padding-top: 2px;
 }
+
+.disabled
+{
+  color: #999;
+}
diff --git a/skins/default/functions.js b/skins/default/functions.js
index 47a121d..4ee2a9d 100644
--- a/skins/default/functions.js
+++ b/skins/default/functions.js
@@ -124,6 +124,7 @@
   this.markmenu = $('#markmessagemenu');
   this.searchmenu = $('#searchmenu');
   this.messagemenu = $('#messagemenu');
+  this.listmenu = $('#listmenu');
 }
 
 rcube_mail_ui.prototype = {
@@ -186,15 +187,89 @@
     rcmail.env.search_mods[rcmail.env.mailbox][elem.value] = elem.value;
 },
 
+show_listmenu: function(show)
+{
+  if (typeof show == 'undefined')
+    show = this.listmenu.is(':visible') ? false : true;
+
+  var ref = rcube_find_object('listmenulink');
+  if (show && ref) {
+    var pos = $(ref).offset();
+    this.listmenu.css({ left:pos.left, top:(pos.top + ref.offsetHeight + 2)});
+    // set form values
+    $('input[name="sort_col"][value="'+rcmail.env.sort_col+'"]').attr('checked', 1);
+    $('input[name="sort_ord"][value="DESC"]').attr('checked', rcmail.env.sort_order=='DESC' ? 1 : 0);
+    $('input[name="sort_ord"][value="ASC"]').attr('checked', rcmail.env.sort_order=='DESC' ? 0 : 1);
+    $('input[name="view"][value="thread"]').attr('checked', rcmail.env.threading ? 1 : 0);
+    $('input[name="view"][value="list"]').attr('checked', rcmail.env.threading ? 0 : 1);
+    // list columns
+    var cols = $('input[name="list_col[]"]');
+    for (var i=0; i<cols.length; i++) {
+      var found = 0;
+      if (cols[i].value != 'from')
+        found = jQuery.inArray(cols[i].value, rcmail.env.coltypes) != -1;
+      else
+        found = (jQuery.inArray('from', rcmail.env.coltypes) != -1
+	    || jQuery.inArray('to', rcmail.env.coltypes) != -1);
+      $(cols[i]).attr('checked',found ? 1 : 0);
+    }
+  }
+
+  this.listmenu[show?'show':'hide']();
+
+  if (show) {
+    var maxheight=0;
+    $('#listmenu fieldset').each(function() {
+      var height = $(this).height();
+      if (height > maxheight) {
+        maxheight = height;
+      }
+    });
+    $('#listmenu fieldset').css("min-height", maxheight+"px")
+    // IE6 complains if you set this attribute using either method:
+    //$('#listmenu fieldset').css({'height':'auto !important'});
+    //$('#listmenu fieldset').css("height","auto !important");
+      .height(maxheight);
+  };
+},
+
+open_listmenu: function(e)
+{
+  this.show_listmenu();
+},
+
+save_listmenu: function()
+{
+  this.show_listmenu();
+
+  var sort = $('input[name="sort_col"]:checked').val();
+  var ord = $('input[name="sort_ord"]:checked').val();
+  var thread = $('input[name="view"]:checked').val();
+  var cols = $('input[name="list_col[]"]:checked')
+    .map(function(){ return this.value; }).get();
+
+  rcmail.set_list_options(cols, sort, ord, thread == 'thread' ? 1 : 0);
+},
+
 body_mouseup: function(evt, p)
 {
-  if (this.markmenu && this.markmenu.is(':visible') && rcube_event.get_target(evt) != rcube_find_object('markreadbutton'))
+  var target = rcube_event.get_target(evt);
+
+  if (this.markmenu && this.markmenu.is(':visible') && target != rcube_find_object('markreadbutton'))
     this.show_markmenu(false);
-  else if (this.messagemenu && this.messagemenu.is(':visible') && rcube_event.get_target(evt) != rcube_find_object('messagemenulink'))
+  else if (this.messagemenu && this.messagemenu.is(':visible') && target != rcube_find_object('messagemenulink'))
     this.show_messagemenu(false);
-  else if (this.searchmenu && this.searchmenu.is(':visible') && rcube_event.get_target(evt) != rcube_find_object('searchmod')) {
+  else if (this.listmenu && this.listmenu.is(':visible') && target != rcube_find_object('listmenulink')) {
+    var menu = rcube_find_object('listmenu');
+    while (target.parentNode) {
+      if (target.parentNode == menu)
+        return;
+      target = target.parentNode;
+    }
+    this.show_listmenu(false);
+  }
+  else if (this.searchmenu && this.searchmenu.is(':visible') && target != rcube_find_object('searchmod')) {
     var menu = rcube_find_object('searchmenu');
-    var target = rcube_event.get_target(evt);
     while (target.parentNode) {
       if (target.parentNode == menu)
         return;
@@ -213,6 +288,8 @@
       this.show_searchmenu(false);
     if (this.messagemenu && this.messagemenu.is(':visible'))
       this.show_messagemenu(false);
+    if (this.listmenu && this.listmenu.is(':visible'))
+      this.show_listmenu(false);
   }
 }
 
@@ -225,4 +302,6 @@
   rcmail_ui = new rcube_mail_ui();
   rcube_event.add_listener({ object:rcmail_ui, method:'body_mouseup', event:'mouseup' });
   rcube_event.add_listener({ object:rcmail_ui, method:'body_keypress', event:'keypress' });
+  rcmail.addEventListener('menu-open', 'open_listmenu', rcmail_ui);
+  rcmail.addEventListener('menu-save', 'save_listmenu', rcmail_ui);
 }
diff --git a/skins/default/ie6hacks.css b/skins/default/ie6hacks.css
index e256ea1..11a5b1d 100644
--- a/skins/default/ie6hacks.css
+++ b/skins/default/ie6hacks.css
@@ -14,7 +14,9 @@
   background-image: url('images/display/icons.gif');
 }
 
-#messagemenu li a
+#messagemenu li a,
+#messagelist tr td div.expanded,
+#messagelist tr td div.collapsed
 {
   background-image: url('images/messageactions.gif');
 }
@@ -47,13 +49,12 @@
   background-image: url('images/abook_toolbar.gif');
 }
 
-ul.toolbarmenu li
-{
-  width: auto;
-  border: 1px solid #f6f6f6;
-}
-
 ul.toolbarmenu li a
 {
   clear: left;
 }
+
+ul.toolbarmenu li.separator_below
+{
+  padding-bottom: 8px;
+}
diff --git a/skins/default/iehacks.css b/skins/default/iehacks.css
index dd40b32..e9bbe67 100644
--- a/skins/default/iehacks.css
+++ b/skins/default/iehacks.css
@@ -24,13 +24,9 @@
   filter: alpha(opacity=85);
 }
 
-#markmessagemenu,
-#searchmenu,
-#messagemenu
+.popupmenu
 {
-  -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=90)";
-  filter: alpha(opacity=90);
-
+	background-color: #ffffff;
 }
 
 #tabsbar
@@ -160,6 +156,11 @@
   border-collapse: collapse;
 }
 
+#messagelist tbody tr.unroot td.subject
+{
+  text-decoration: underline;
+}
+
 #messageframe
 {
   width: expression((parseInt(this.parentNode.offsetWidth)-180)+'px');
@@ -243,6 +244,11 @@
   filter: alpha(opacity=70);
 }
 
+ul.toolbarmenu
+{
+  margin: 0 0 -4px 0;
+}
+
 ul.toolbarmenu li
 {
   min-width: auto;
@@ -263,3 +269,9 @@
 {
   height: 19px;
 }
+
+#listmenu fieldset
+{
+  margin: 0 4px;
+  padding: 0.8em;
+}
diff --git a/skins/default/images/icons/columnpicker.gif b/skins/default/images/icons/columnpicker.gif
new file mode 100644
index 0000000..e776519
--- /dev/null
+++ b/skins/default/images/icons/columnpicker.gif
Binary files differ
diff --git a/skins/default/images/icons/unread_children.png b/skins/default/images/icons/unread_children.png
new file mode 100644
index 0000000..45e671d
--- /dev/null
+++ b/skins/default/images/icons/unread_children.png
Binary files differ
diff --git a/skins/default/images/mail_footer.png b/skins/default/images/mail_footer.png
index b1b6361..ab56b83 100644
--- a/skins/default/images/mail_footer.png
+++ b/skins/default/images/mail_footer.png
Binary files differ
diff --git a/skins/default/images/messageactions.gif b/skins/default/images/messageactions.gif
index 5f0a533..94fcba4 100644
--- a/skins/default/images/messageactions.gif
+++ b/skins/default/images/messageactions.gif
Binary files differ
diff --git a/skins/default/images/messageactions.png b/skins/default/images/messageactions.png
index 8dfb389..b3e648e 100644
--- a/skins/default/images/messageactions.png
+++ b/skins/default/images/messageactions.png
Binary files differ
diff --git a/skins/default/includes/messagemenu.html b/skins/default/includes/messagemenu.html
index ad2a6c6..e89155a 100644
--- a/skins/default/includes/messagemenu.html
+++ b/skins/default/includes/messagemenu.html
@@ -1,4 +1,4 @@
-<div id="messagemenu">
+<div id="messagemenu" class="popupmenu">
   <ul class="toolbarmenu">
     <li><roundcube:button class="printlink" command="print" label="printmessage" classAct="printlink active" /></li>
     <li><roundcube:button class="downloadlink" command="download" label="emlsave" classAct="downloadlink active" /></li>
diff --git a/skins/default/mail.css b/skins/default/mail.css
index eb7e190..7d1cb33 100644
--- a/skins/default/mail.css
+++ b/skins/default/mail.css
@@ -164,25 +164,38 @@
   padding-left: 2px;
 }
 
-#markmessagemenu,
-#searchmenu,
-#messagemenu
+.popupmenu
 {
   position: absolute;
   top: 32px;
   left: 90px;
   width: auto;
   display: none;
-  background-color: #F9F9F9;
-  border: 1px solid #CCC;
-  padding: 1px;
-  opacity: 0.9;
+  background-color: #fff;
+  background-color: rgba(255, 255, 255, 0.95);
+  border: 1px solid #999;
+  padding: 4px;
   z-index: 240;
+  border-radius: 3px;
+  -moz-border-radius: 3px;
+  -webkit-border-radius: 3px;
+  -moz-box-shadow: 1px 1px 12px #999;
+  -webkit-box-shadow: #999 1px 1px 12px;
 }
 
 #searchmenu
 {
-  width: 172px;
+  width: 160px;
+}
+
+#searchmenu ul.toolbarmenu
+{
+  margin: 0;
+}
+
+#searchmenu ul.toolbarmenu li
+{
+  margin: 1px 4px 1px;
 }
 
 #messagemenu li a.active:hover,
@@ -194,53 +207,53 @@
 
 #messagemenu li a
 {
-  background: url('images/messageactions.png') no-repeat 1px 0;
-  background-position: 0px 20px;
+  background: url('images/messageactions.png') no-repeat 7px 0;
+  background-position: 7px 20px;
 }
 
 #messagemenu li a.printlink
 {
-  background-position: 1px 1px;
+  background-position: 7px 1px;
 }
 
 #messagemenu li a.downloadlink
 {
-  background-position: 1px -17px;
+  background-position: 7px -17px;
 }
 
 #messagemenu li a.sourcelink
 {
-  background-position: 1px -35px;
+  background-position: 7px -35px;
 }
 
 #messagemenu li a.openlink
 {
-  background-position: 1px -53px;
+  background-position: 7px -53px;
 }
 
 #messagemenu li a.editlink
 {
-  background-position: 1px -71px;
+  background-position: 7px -71px;
 }
 
 #markmessagemenu a.readlink
 {
-  background: url('images/icons/dot.png') no-repeat 2px;
+  background: url('images/icons/dot.png') no-repeat 7px 2px;
 }
 
 #markmessagemenu a.unreadlink
 {
-  background: url('images/icons/unread.png') no-repeat 2px;
+  background: url('images/icons/unread.png') no-repeat 7px 2px;
 }
 
 #markmessagemenu a.flaggedlink
 {
-  background: url('images/icons/flagged.png') no-repeat 2px;
+  background: url('images/icons/flagged.png') no-repeat 7px 2px;
 }
 
 #markmessagemenu a.unflaggedlink
 {
-  background: url('images/icons/unflagged.png') no-repeat 2px;
+  background: url('images/icons/unflagged.png') no-repeat 7px 2px;
 }
 
 #searchfilter
@@ -613,6 +626,30 @@
   background-position: -75px -15px;
 }
 
+#listcontrols a.expand-all {
+  background-position: -90px 0;
+}
+
+#listcontrols a.expand-allsel {
+  background-position: -90px -15px;
+}
+
+#listcontrols a.collapse-all {
+  background-position: -105px 0;
+}
+
+#listcontrols a.collapse-allsel {
+  background-position: -105px -15px;
+}
+
+#listcontrols a.expand-unread {
+  background-position: -120px 0;
+}
+
+#listcontrols a.expand-unreadsel {
+  background-position: -120px -15px;
+}
+
 #countcontrols
 {
   height: 15px;
@@ -720,20 +757,69 @@
   vertical-align: middle;
 }
 
+#messagelist thead tr td.subject
+{
+  padding-left: 22px;
+}
+
 #messagelist thead tr td.icon,
-#messagelist thead tr td.flag
+#messagelist thead tr td.flag,
+#messagelist thead tr td.threads
 {
   width: 22px;
   padding: 0;
   text-align: center;
 }
 
-#messagelist tbody tr td.icon,
+#messagelist thead tr td.threads
+{
+  width: 18px;
+}
+
 #messagelist tbody tr td.flag
 {
   padding: 2px 3px 2px 3px;
   vertical-align: middle;
   cursor: pointer;
+}
+
+#messagelist tr td span.branch
+{
+  display: inline-block;
+  width: 15px;
+  height: 15px;
+}
+
+#messagelist tr td.subject img.msgicon
+{
+  vertical-align: middle;
+}
+
+#messagelist tbody td img.msgicon
+{
+  position: relative;
+  top: 0px;
+  margin-right: 5px;
+}
+
+#messagelist tr td div.collapsed,
+#messagelist tr td div.expanded,
+#messagelist tr td img.flagicon,
+#messagelist tr td img.msgicon
+{
+  cursor: pointer;
+}
+
+#messagelist tr td div.collapsed
+{
+  display: block;
+  background: url('images/messageactions.png') center -91px no-repeat;
+}
+
+#messagelist tr td div.expanded
+{
+  display: block;
+  background: url('images/messageactions.png') center -109px no-repeat;
 }
 
 #messagelist tbody tr td.flag img:hover,
@@ -747,6 +833,12 @@
   overflow: hidden;
   vertical-align: middle;
   width: 99%;
+}
+
+/* thread parent message with unread children */
+#messagelist tbody tr.unroot td.subject a
+{
+  text-decoration: underline;
 }
 
 #messagelist tr td.size
@@ -1267,3 +1359,27 @@
 {
   font-weight: bold;
 }
+
+#listmenu
+{
+  padding: 6px;
+}
+
+#listmenu legend
+{
+  color: #999999;
+}
+
+#listmenu fieldset
+{
+  border: 1px solid #999999;
+  margin: 0 5px;
+  float: left;
+}
+
+#listmenu div
+{
+  padding: 8px 0 3px 0;
+  text-align: center;
+  clear: both;
+}
diff --git a/skins/default/templates/mail.html b/skins/default/templates/mail.html
index 9e07cf7..c66bc58 100644
--- a/skins/default/templates/mail.html
+++ b/skins/default/templates/mail.html
@@ -59,7 +59,9 @@
   forwardedrepliedIcon="/images/icons/forwarded_replied.png"
   attachmentIcon="/images/icons/attachment.png"
   flaggedIcon="/images/icons/flagged.png"
-  unflaggedIcon="/images/icons/blank.gif" />
+  unflaggedIcon="/images/icons/blank.gif"
+  unreadchildrenIcon="" 
+  optionsmenuIcon="/images/icons/columnpicker.gif" />
 </div>
 
 <roundcube:if condition="config:preview_pane == true" />
@@ -82,6 +84,10 @@
       <roundcube:button command="select-all" type="link" prop="unread" title="unread" class="buttonPas unread" classAct="button unread" classSel="button unreadsel" content=" " />
       <roundcube:button command="select-all" type="link" prop="invert" title="invert" class="buttonPas invert" classAct="button invert" classSel="button invertsel" content=" " />
       <roundcube:button command="select-none" type="link" title="none" class="buttonPas none" classAct="button none" classSel="button nonesel" content=" " />
+      <span style="margin-left: 20px"><roundcube:label name="threads" />:&nbsp;</span>
+      <roundcube:button command="expand-all" type="link" title="expand-all" class="buttonPas expand-all" classAct="button expand-all" classSel="button expand-allsel" content=" "  />
+      <roundcube:button command="expand-unread" type="link" title="expand-unread" class="buttonPas expand-unread" classAct="button expand-unread" classSel="button expand-unreadsel" content=" " />
+      <roundcube:button command="collapse-all" type="link" title="collapse-all" class="buttonPas collapse-all" classAct="button collapse-all" classSel="button collapse-allsel" content=" " />
       <roundcube:container name="listcontrols" id="listcontrols" />
   <roundcube:if condition="env:quota" />
   <span style="margin-left: 20px; margin-right: 5px"><roundcube:label name="quota" />:</span>
@@ -111,7 +117,7 @@
 <roundcube:button name="markreadbutton" id="markreadbutton" type="link" class="button markmessage" title="markmessages" onclick="rcmail_ui.show_markmenu();return false" content=" " />
 <roundcube:button name="messagemenulink" id="messagemenulink" type="link" class="button messagemenu" title="messageactions" onclick="rcmail_ui.show_messagemenu();return false" content=" " />
 
-<div id="markmessagemenu">
+<div id="markmessagemenu" class="popupmenu">
   <ul class="toolbarmenu">
     <li><roundcube:button command="mark" prop="read" label="markread" classAct="readlink active" class="readlink" /></li>
     <li><roundcube:button command="mark" prop="unread" label="markunread" classAct="unreadlink active" class="unreadlink" /></li>
@@ -125,7 +131,7 @@
 
 </div>
 
-<div id="searchmenu">
+<div id="searchmenu" class="popupmenu">
   <ul class="toolbarmenu">
     <li><input type="checkbox" name="s_mods[]" value="subject" id="s_mod_subject" onclick="rcmail_ui.set_searchmod(this)" /><label for="s_mod_subject"><roundcube:label name="subject" /></label></li>
     <li><input type="checkbox" name="s_mods[]" value="from" id="s_mod_from" onclick="rcmail_ui.set_searchmod(this)" /><label for="s_mod_from"><roundcube:label name="from" /></label></li>
@@ -146,5 +152,52 @@
 <roundcube:button command="reset-search" id="searchreset" image="/images/icons/reset.gif" title="resetsearch" />
 </div>
 
+<div id="listmenu" class="popupmenu">
+<fieldset class="thinbordered"><legend><roundcube:label name="listmode" /></legend>
+  <ul class="toolbarmenu">
+    <li><input type="radio" name="view" value="list" id="view_default" /><label for="view_default"><roundcube:label name="list" /></label></li>
+    <roundcube:if condition="env:threads" />
+      <li><input type="radio" name="view" value="thread" id="view_thread" /><label for="view_thread"><roundcube:label name="threads" /></label></li>
+    <roundcube:else />
+      <li><input type="radio" name="view" value="thread" id="view_thread" disabled="disabled" /><label for="view_thread" class="disabled"><roundcube:label name="threads" /></label></li>
+    <roundcube:endif />
+  </ul>
+</fieldset>
+<fieldset class="thinbordered"><legend><roundcube:label name="listcolumns" /></legend>
+  <ul class="toolbarmenu">
+    <li><input type="checkbox" name="list_col[]" value="flag" id="cols_flag" /><label for="cols_flag"><roundcube:label name="flag" /></label></li>
+    <li><input type="checkbox" name="list_col[]" value="subject" id="cols_subject" checked="checked" disabled="disabled" /><label for="cols_subject" class="disabled"><roundcube:label name="subject" /></label></li>
+    <li><input type="checkbox" name="list_col[]" value="from" id="cols_fromto" /><label for="cols_fromto"><roundcube:label name="fromto" /></label></li>
+    <li><input type="checkbox" name="list_col[]" value="replyto" id="cols_replyto" /><label for="cols_replyto"><roundcube:label name="replyto" /></label></li>
+    <li><input type="checkbox" name="list_col[]" value="cc" id="cols_cc" /><label for="cols_cc"><roundcube:label name="cc" /></label></li>
+    <li><input type="checkbox" name="list_col[]" value="date" id="cols_date" /><label for="cols_date"><roundcube:label name="date" /></label></li>
+    <li><input type="checkbox" name="list_col[]" value="size" id="cols_size" /><label for="cols_size"><roundcube:label name="size" /></label></li>
+    <li><input type="checkbox" name="list_col[]" value="attachment" id="cols_attachment" /><label for="cols_attachment"><roundcube:label name="attachment" /></label></li>
+  </ul>
+</fieldset>
+<fieldset class="thinbordered"><legend><roundcube:label name="listsorting" /></legend>
+  <ul class="toolbarmenu">
+    <li><input type="radio" name="sort_col" value="" id="sort_default" /><label for="sort_default"><roundcube:label name="nonesort" /></label></li>
+    <li><input type="radio" name="sort_col" value="arrival" id="sort_arrival" /><label for="sort_arrival"><roundcube:label name="arrival" /></label></li>
+    <li><input type="radio" name="sort_col" value="date" id="sort_date" /><label for="sort_date"><roundcube:label name="sentdate" /></label></li>
+    <li><input type="radio" name="sort_col" value="subject" id="sort_subject" /><label for="sort_subject"><roundcube:label name="subject" /></label></li>
+    <li><input type="radio" name="sort_col" value="from" id="sort_fromto" /><label for="sort_fromto"><roundcube:label name="fromto" /></label></li>
+    <li><input type="radio" name="sort_col" value="to" id="sort_replyto" /><label for="sort_replyto"><roundcube:label name="replyto" /></label></li>
+    <li><input type="radio" name="sort_col" value="cc" id="sort_cc" /><label for="sort_cc"><roundcube:label name="cc" /></label></li>
+    <li><input type="radio" name="sort_col" value="size" id="sort_size" /><label for="sort_size"><roundcube:label name="size" /></label></li>
+  </ul>
+</fieldset>
+<fieldset><legend><roundcube:label name="listorder" /></legend> 
+	  <ul class="toolbarmenu"> 
+	    <li><input type="radio" name="sort_ord" value="ASC" id="sort_asc" /><label for="sort_asc"><roundcube:label name="asc" /></label></li> 
+	    <li><input type="radio" name="sort_ord" value="DESC" id="sort_desc" /><label for="sort_desc"><roundcube:label name="desc" /></label></li> 
+	  </ul> 
+</fieldset>
+<div>
+  <roundcube:button command="menu-open" id="listmenucancel" type="input" class="button" label="cancel" />
+  <roundcube:button command="menu-save" id="listmenusave" type="input" class="button mainaction" label="save" />
+</div>
+</div>
+
 </body>
 </html>

--
Gitblit v1.9.1