summaryrefslogtreecommitdiff
path: root/roundcubemail/program
diff options
context:
space:
mode:
authorNathan Kinkade <nkinkade@nkinka.de>2009-03-05 20:48:46 +0000
committerNathan Kinkade <nkinkade@nkinka.de>2009-03-11 12:51:51 +0000
commit32ea464bdfe5f8a22f46bfac50dcdc26c36fc497 (patch)
tree50b13882e43a778a9e9dcec19f18d18bc8bb1aa9 /roundcubemail/program
parent8013cb2424ca6912419dc7df73a9b8b77da7bdb0 (diff)
Applied message threading patch from Chris January: http://www.atomice.com/blog/?p=33
Diffstat (limited to 'roundcubemail/program')
-rw-r--r--roundcubemail/program/include/rcmail.php4
-rw-r--r--roundcubemail/program/include/rcube_imap.php503
-rw-r--r--roundcubemail/program/js/app.js131
-rw-r--r--roundcubemail/program/js/list.js161
-rw-r--r--roundcubemail/program/lib/imap.inc74
-rw-r--r--roundcubemail/program/localization/en_GB/labels.inc6
-rw-r--r--roundcubemail/program/localization/en_US/labels.inc7
-rw-r--r--roundcubemail/program/steps/mail/check_recent.inc32
-rw-r--r--roundcubemail/program/steps/mail/func.inc64
-rw-r--r--roundcubemail/program/steps/mail/list.inc5
-rw-r--r--roundcubemail/program/steps/mail/move_del.inc11
-rw-r--r--roundcubemail/program/steps/mail/rss.inc2
-rw-r--r--roundcubemail/program/steps/mail/search.inc2
-rw-r--r--roundcubemail/program/steps/settings/func.inc2
-rw-r--r--roundcubemail/program/steps/settings/manage_folders.inc38
15 files changed, 922 insertions, 120 deletions
diff --git a/roundcubemail/program/include/rcmail.php b/roundcubemail/program/include/rcmail.php
index f109c16fd..96937b58a 100644
--- a/roundcubemail/program/include/rcmail.php
+++ b/roundcubemail/program/include/rcmail.php
@@ -349,6 +349,10 @@ class rcmail
if ($this->config->get('enable_caching')) {
$this->imap->set_caching(true);
}
+
+ if ($this->config->get('enable_thread_caching')) {
+ $this->imap->set_thread_caching(true);
+ }
// set pagesize from config
$this->imap->set_pagesize($this->config->get('pagesize', 50));
diff --git a/roundcubemail/program/include/rcube_imap.php b/roundcubemail/program/include/rcube_imap.php
index 637e25b92..788b0ff58 100644
--- a/roundcubemail/program/include/rcube_imap.php
+++ b/roundcubemail/program/include/rcube_imap.php
@@ -52,6 +52,7 @@ class rcube_imap
var $sort_order = 'DESC';
var $delimiter = NULL;
var $caching_enabled = FALSE;
+ var $thread_caching_enabled = FALSE;
var $default_charset = 'ISO-8859-1';
var $default_folders = array('INBOX');
var $default_folders_lc = array('inbox');
@@ -68,6 +69,7 @@ class rcube_imap
var $debug_level = 1;
var $error_code = 0;
var $options = array('imap' => 'check');
+ var $threading = false;
/**
@@ -472,7 +474,7 @@ class rcube_imap
$mailbox = $this->mailbox;
// count search set
- if ($this->search_string && $mailbox == $this->mailbox && $mode == 'ALL' && !$force)
+ if ($this->search_string && $mailbox == $this->mailbox && ($mode == 'ALL' || $mode == 'THREADS') && !$force)
return count((array)$this->search_set);
$a_mailbox_cache = $this->get_cache('messagecount');
@@ -481,8 +483,12 @@ class rcube_imap
if (!$force && is_array($a_mailbox_cache[$mailbox]) && isset($a_mailbox_cache[$mailbox][$mode]))
return $a_mailbox_cache[$mailbox][$mode];
+ if ($this->get_capability('thread=references') &&
+ $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
@@ -524,6 +530,25 @@ class rcube_imap
return (int)$count;
}
+ function _threadcount($mailbox)
+ {
+ if (isset ($this->cache['__threads']['tree']))
+ return count($this->cache['__threads']['tree']);
+ if ($this->check_thread_cache_status($mailbox) > 0) {
+ $sql_result = $this->db->query("SELECT COUNT(*)
+ FROM " .
+ get_table_name('threads') . "
+ WHERE user_id=? AND mailbox=?",
+ $_SESSION['user_id'],
+ $mailbox);
+
+ if ($sql_arr = $this->db->fetch_array($sql_result))
+ return $sql_arr[0];
+ }
+ list ($thread_tree, $msg_depth, $has_children) = iil_C_Thread($this->conn, $mailbox, 'REFERENCES', 'ALL');
+ $this->update_thread_cache($mailbox, $thread_tree, $msg_depth, $has_children);
+ return count($thread_tree);
+ }
/**
* Public method for listing headers
@@ -560,8 +585,9 @@ class rcube_imap
$this->_set_sort_order($sort_field, $sort_order);
- $max = $this->_messagecount($mailbox);
- $start_msg = ($this->list_page-1) * $this->page_size;
+ $message_threading = $this->get_capability('thread=references') &&
+ rcmail::get_instance()->imap->threading;
+ $max = $this->_messagecount($mailbox, $message_threading ? 'THREADS' : 'ALL');
list($begin, $end) = $this->_get_message_range($max, $page);
@@ -573,65 +599,139 @@ class rcube_imap
$cache_key = $mailbox.'.msg';
$cache_status = $this->check_cache_status($mailbox, $cache_key);
- // cache is OK, we can get all messages from local cache
- if ($cache_status>0)
- {
- $a_msg_headers = $this->get_message_cache($cache_key, $start_msg, $start_msg+$this->page_size, $this->sort_field, $this->sort_order);
- $headers_sorted = TRUE;
- }
- // cache is dirty, sync it
- else if ($this->caching_enabled && $cache_status==-1 && !$recursive)
+ if ($cache_status <= 0 && $this->caching_enabled && $cache_status == -1 && !$recursive)
{
$this->sync_header_index($mailbox);
return $this->_list_headers($mailbox, $page, $this->sort_field, $this->sort_order, TRUE);
}
- else
- {
- // retrieve headers from IMAP
- if ($this->get_capability('sort') && ($msg_index = iil_C_Sort($this->conn, $mailbox, $this->sort_field, $this->skip_deleted ? 'UNDELETED' : '')))
- {
- $mymsgidx = array_slice ($msg_index, $begin, $end-$begin);
- $msgs = join(",", $mymsgidx);
- }
- else
- {
- $msgs = sprintf("%d:%d", $begin+1, $end);
- $msg_index = range($begin, $end);
- }
+ if ($message_threading) {
+ $thread_cache_status = $this->check_thread_cache_status($mailbox);
+ if ($thread_cache_status > 0 && $cache_status > 0) {
+ // use a JOIN of the threads and messages tables to get a sorted list of roots
+ $roots = $this->get_thread_cache_roots_sorted($mailbox, $begin, $end, $this->sort_field, $this->sort_order);
+ } else {
+ if ($thread_cache_status > 0) {
+ // get a list of thread roots from the cache
+ $roots = $this->get_thread_cache_roots($mailbox);
+ } else {
+ if (isset ($this->cache['__threads']['tree'])) {
+ $thread_tree = $this->cache['__threads']['tree'];
+ $msg_depth = $this->cache['__threads']['depth'];
+ $has_children = $this->cache['__threads']['has_children'];
+ } else {
+ list ($thread_tree, $msg_depth, $has_children) = iil_C_Thread($this->conn, $mailbox, 'REFERENCES', 'ALL');
+ }
+ $this->update_thread_cache($mailbox, $thread_tree, $msg_depth, $has_children);
+ // the keys of thread_tree are the thread roots
+ $roots = array_keys($thread_tree);
+ }
+
+ if ($this->get_capability('sort') && ($sort_index = iil_C_Sort($this->conn, $mailbox, $this->sort_field, $this->skip_deleted ? 'UNDELETED' : ''))) {
+ if ($this->sort_order == 'DESC')
+ $sort_index = array_reverse($sort_index);
+ } else {
+ if ($this->sort_order == 'DESC')
+ $msg_index = range($max, 1);
+ else
+ $msg_index = range(1, $max);
+ }
- // fetch reuested headers from server
- $a_msg_headers = array();
- $deleted_count = $this->_fetch_headers($mailbox, $msgs, $a_msg_headers, $cache_key);
+ // sort the roots according to the order defined by $sort_index
+ $sorter = new rcube_thread_root_sorter();
+ $sorter->set_sequence_numbers($sort_index);
+ $sorter->sort_threads($roots);
+
+ // get the roots for this page
+ $roots = array_slice($roots, $begin, $end - $begin);
+ }
- // delete cached messages with a higher index than $max+1
- // Changed $max to $max+1 to fix this bug : #1484295
- $this->clear_message_cache($cache_key, $max + 1);
+ if ($thread_cache_status > 0) {
+ // get the children of $roots from the cache
+ $threads = $this->get_thread_cache_threads($mailbox, $roots);
+ $msg_index = array ();
+ $msg_depth = array ();
+ $has_children = array ();
+ foreach ($threads as $thread) {
+ foreach ($thread as $msg) {
+ $idx = $msg['idx'];
+ $msg_index[] = $idx;
+ $msg_depth[$idx] = $msg['depth'];
+ $has_children[$idx] = $msg['has_children'];
+ }
+ }
+ } else {
+ // flatten the thread tree
+ $msg_index = array ();
+ foreach ($roots as $root) {
+ $msg_index[] = $root;
+ $msg_index = array_merge($msg_index, array_keys_recursive($thread_tree[$root]));
+ }
+ }
+ } else { // no threading
+ if ($this->get_capability('sort') && ($msg_index = iil_C_Sort($this->conn, $mailbox, $this->sort_field, $this->skip_deleted ? 'UNDELETED' : ''))) {
+ if ($this->sort_order == 'DESC')
+ $msg_index = array_reverse($msg_index);
+ $msg_index = array_slice($msg_index, $begin, $end - $begin);
+ } else {
+ if ($this->sort_order == 'DESC')
+ $msg_index = range($end, $begin + 1);
+ else
+ $msg_index = range($begin + 1, $end);
+ }
+ }
- // kick child process to sync cache
- // ...
- }
+ // fetch requested headers from server
+ $msgs = join(",", $msg_index);
+ // cache is OK, we can get all messages from local cache
+ if ($cache_status > 0) {
+ // if $message_threading is true then no point sorting them as they will need re-sorting later
+ $a_msg_headers = $this->get_message_cache($cache_key, $message_threading?null:$this->sort_field, $this->sort_order, $msgs);
+ // if $message_threading is true then $a_msg_headers will need re-sorting as the tree structure won't have been preserved'
+ $headers_sorted = !$message_threading;
+ } else {
+ $a_msg_headers = array();
+ $deleted_count = $this->_fetch_headers($mailbox, $msgs, $a_msg_headers, $cache_key);
+ // delete cached messages with a higher index than $max+1
+ // Changed $max to $max+1 to fix this bug : #1484295
+ $this->clear_message_cache($cache_key, $max + 1);
+ }
+
+ // kick child process to sync cache
+ // ...
// return empty array if no messages found
if (!is_array($a_msg_headers) || empty($a_msg_headers)) {
return array();
}
- // if not already sorted
if (!$headers_sorted)
{
// use this class for message sorting
$sorter = new rcube_header_sorter();
$sorter->set_sequence_numbers($msg_index);
$sorter->sort_headers($a_msg_headers);
-
- if ($this->sort_order == 'DESC')
- $a_msg_headers = array_reverse($a_msg_headers);
}
- return array_values($a_msg_headers);
+ if ($message_threading) {
+ // Set depth, has_children and unread_children fields in headers
+ $parents = array();
+ foreach ($a_msg_headers as $uid => $headers) {
+ $id = $headers->id;
+ $depth = $msg_depth[$id];
+ $parents = array_slice($parents, 0, $depth - 1);
+ if (!$headers->seen) {
+ foreach ($parents as $parent)
+ $a_msg_headers[$parent]->unread_children++;
+ }
+ array_push($parents, $uid);
+ $a_msg_headers[$uid]->depth = $depth;
+ $a_msg_headers[$uid]->has_children = $has_children[$id];
+ }
}
+ return array_values($a_msg_headers);
+ }
/**
* Private method for listing a set of message headers (search results)
@@ -738,11 +838,6 @@ class rcube_imap
$begin = 0;
$end = $max;
}
- else if ($this->sort_order=='DESC')
- {
- $begin = $max - $this->page_size - $start_msg;
- $end = $max - $start_msg;
- }
else
{
$begin = $start_msg;
@@ -756,8 +851,6 @@ class rcube_imap
return array($begin, $end);
}
-
-
/**
* Fetches message headers
* Used for loop
@@ -2046,6 +2139,17 @@ class rcube_imap
/**
* @access private
*/
+ function set_thread_caching($set)
+ {
+ if ($set && is_object($this->db))
+ $this->thread_caching_enabled = TRUE;
+ else
+ $this->thread_caching_enabled = FALSE;
+ }
+
+ /**
+ * @access private
+ */
function get_cache($key)
{
// read cache
@@ -2226,6 +2330,9 @@ class rcube_imap
if ($cache_count==$msg_count)
{
+ if ($msg_count == 0)
+ return 1;
+
// get highest index
$header = iil_C_FetchHeader($this->conn, $mailbox, "$msg_count");
$cache_uid = array_pop($cache_index);
@@ -2247,26 +2354,29 @@ class rcube_imap
/**
* @access private
*/
- function get_message_cache($key, $from, $to, $sort_field, $sort_order)
+ function get_message_cache($key, $sort_field, $sort_order, $msgs)
{
- $cache_key = "$key:$from:$to:$sort_field:$sort_order";
+ $cache_key = "$key:$sort_field:$sort_order:$msgs";
$db_header_fields = array('idx', 'uid', 'subject', 'from', 'to', 'cc', 'date', 'size');
- if (!in_array($sort_field, $db_header_fields))
- $sort_field = 'idx';
-
if ($this->caching_enabled && !isset($this->cache[$cache_key]))
{
$this->cache[$cache_key] = array();
- $sql_result = $this->db->limitquery(
+ if (is_null($sort_field)) {
+ $sort = '';
+ } else {
+ if (!in_array($sort_field, $db_header_fields))
+ $sort_field = 'idx';
+ $sort = "ORDER BY ".$this->db->quoteIdentifier($sort_field)." ".
+ strtoupper($sort_order);
+ }
+ $sql_result = $this->db->query(
"SELECT idx, uid, headers
FROM ".get_table_name('messages')."
WHERE user_id=?
AND cache_key=?
- ORDER BY ".$this->db->quoteIdentifier($sort_field)." ".
- strtoupper($sort_order),
- $from,
- $to-$from,
+ AND idx IN ($msgs)
+ $sort",
$_SESSION['user_id'],
$key);
@@ -2444,8 +2554,175 @@ class rcube_imap
$start_index);
}
+ /* --------------------------------
+ * thread caching methods
+ * --------------------------------*/
+ /**
+ * Checks if the thread cache is up-to-date
+ *
+ * @param string Mailbox name
+ * @return int 0 = ok, -1 = invalid, -2 = cache disabled
+ */
+ function check_thread_cache_status($mailbox)
+ {
+ if (!$this->caching_enabled)
+ return -2;
+
+ $cache_uid = $this->get_cache('thread_cache_last_uid');
+ $cache_count = $this->get_cache('thread_cache_msg_count');
+ $msg_count = $this->_messagecount($mailbox, 'ALL', TRUE);
+
+ if ($cache_count[$mailbox] == $msg_count) {
+ if ($msg_count == 0)
+ return 1;
+
+ // get highest index
+ $header = iil_C_FetchHeader($this->conn, $mailbox, "$msg_count");
+
+ // uids of highest message matches -> cache seems OK
+ if ($cache_uid[$mailbox] == $header->uid)
+ return 1;
+ // cache is invaid
+ return -1;
+ } else {
+ return -1;
+ }
+ }
+
+ function update_thread_cache($mailbox, $thread_tree, $msg_depth, $has_children)
+ {
+ if (empty ($mailbox))
+ return;
+
+ // add to internal (fast) cache
+ $this->cache['__threads'] = array();
+ $this->cache['__threads']['tree'] = $thread_tree;
+ $this->cache['__threads']['depth'] = $msg_depth;
+ $this->cache['__threads']['has_children'] = $has_children;
+
+ // no further caching
+ if (!$this->thread_caching_enabled || true)
+ return;
+
+ $this->db->query("DELETE FROM " .
+ get_table_name('threads') ."
+ WHERE user_id = ?
+ AND mailbox = ?", $_SESSION['user_id'], $mailbox);
+ foreach ($thread_tree as $root => $subtree) {
+ $this->db->query("INSERT INTO " .
+ get_table_name('threads') . "
+ (user_id, mailbox, idx, has_children, is_root, root, depth)
+ VALUES (?, ?, ?, ?, ?, ?, ?)", $_SESSION['user_id'], $mailbox, $root, $has_children[$root] ? 1 : 0, 1, $root, $msg_depth[$root]);
+ $children = array_keys_recursive($subtree);
+ foreach ($children as $idx) {
+ $this->db->query("INSERT INTO " .
+ get_table_name('threads') . "
+ (user_id, mailbox, idx, has_children, is_root, root, depth)
+ VALUES (?, ?, ?, ?, ?, ?, ?)", $_SESSION['user_id'], $mailbox, $idx, $has_children[$idx] ? 1 : 0, 0, $root, $msg_depth[$idx]);
+ }
+ }
+
+ $cache_uid = $this->get_cache('thread_cache_last_uid');
+ $cache_count = $this->get_cache('thread_cache_msg_count');
+ $msg_count = $this->_messagecount($mailbox);
+ if ($msg_count > 0) {
+ $header = iil_C_FetchHeader($this->conn, $mailbox, "$msg_count");
+ $cache_uid[$mailbox] = $header->uid;
+ }
+ $cache_count[$mailbox] = $msg_count;
+ $this->update_cache('thread_cache_last_uid', $cache_uid);
+ $this->update_cache('thread_cache_msg_count', $cache_count);
+ }
+
+ function get_thread_cache_roots($mailbox)
+ {
+ if (!$this->thread_caching_enabled || empty ($mailbox))
+ return array ();
+
+ $cache_key = "threads:$mailbox";
+
+ if ($this->thread_caching_enabled && !isset ($this->cache[$cache_key])) {
+ $this->cache[$cache_key] = array ();
+
+ $sql_result = $this->db->query("SELECT idx
+ FROM " .
+ get_table_name('threads') . "
+ WHERE user_id=?
+ AND mailbox=?
+ AND is_root", $_SESSION['user_id'], $mailbox);
+
+ while ($sql_arr = $this->db->fetch_array($sql_result))
+ $this->cache[$cache_key][] = $sql_arr[0];
+ }
+
+ return $this->cache[$cache_key];
+ }
+
+ function get_thread_cache_roots_sorted($mailbox, $from, $to, $sort_field, $sort_order)
+ {
+ // empty key -> empty array
+ if (!$this->thread_caching_enabled || !$this->caching_enabled || empty ($mailbox))
+ return array ();
+
+ $cache_key = "threads:$mailbox:$from:$to:$sort_field:$sort_order";
+
+ if ($this->thread_caching_enabled && $this->caching_enabled && !isset ($this->cache[$cache_key])) {
+ $this->cache[$cache_key] = array ();
+
+ $sql_result = $this->db->limitquery("SELECT threads.idx
+ FROM " .
+ get_table_name('threads') . "
+ INNER JOIN ".
+ get_table_name('messages') . "
+ ON threads.idx = messages.idx
+ AND threads.user_id = messages.user_id
+ WHERE threads.user_id=?
+ AND mailbox=?
+ AND cache_key=?
+ AND is_root
+ ORDER BY ".$this->db->quoteIdentifier($sort_field)." ".
+ strtoupper($sort_order),
+ $from,
+ $to-$from,
+ $_SESSION['user_id'], $mailbox, $mailbox.'.msg');
+
+ while ($sql_arr = $this->db->fetch_array($sql_result))
+ $this->cache[$cache_key][] = $sql_arr[0];
+ }
+
+ return $this->cache[$cache_key];
+ }
+
+ function get_thread_cache_threads($mailbox, $roots)
+ {
+ // empty key -> empty array
+ if (!$this->thread_caching_enabled || empty ($mailbox))
+ return array ();
+
+ $cache_key = "threads:$mailbox:" . join(',', $roots);
+
+ if ($this->thread_caching_enabled && !isset ($this->cache[$cache_key])) {
+ $this->cache[$cache_key] = array ();
+ // setup the key order according to $roots
+ foreach ($roots as $root)
+ $this->cache[$cache_key][$root] = array();
+
+ $sql_result = $this->db->query("SELECT idx, has_children, root, depth
+ FROM " .
+ get_table_name('threads') . "
+ WHERE user_id=?
+ AND mailbox=?
+ AND root IN (" . join(',', $roots) .")
+ ORDER BY thread_id", $_SESSION['user_id'], $mailbox);
+
+ while ($sql_arr = $this->db->fetch_assoc($sql_result))
+ $this->cache[$cache_key][$sql_arr['root']][] = $sql_arr;
+ }
+
+ return $this->cache[$cache_key];
+ }
/* --------------------------------
* encoding/decoding methods
@@ -2899,6 +3176,10 @@ class rcube_imap
// add incremental value to messagecount
$a_mailbox_cache[$mailbox][$mode] += $increment;
+ // If ALL changes then THREADS may be invalid
+ if ($mode == 'ALL')
+ unset ($a_mailbox_cache[$mailbox]['THREADS']);
+
// there's something wrong, delete from cache
if ($a_mailbox_cache[$mailbox][$mode] < 0)
unset($a_mailbox_cache[$mailbox][$mode]);
@@ -3134,3 +3415,113 @@ function quoted_printable_encode($input, $line_max=76, $space_conv=false)
}
+function array_keys_recursive($array)
+{
+ $keys = array ();
+ foreach ($array as $key => $child)
+ {
+ $keys[] = $key;
+ $keys = array_merge($keys, array_keys_recursive($child));
+ }
+ return $keys;
+}
+
+
+/**
+ * Class for sorting a thread tree in a predetermined order.
+ *
+ * @package Mail
+ * @author Chris January
+ */
+class rcube_thread_sorter
+{
+ var $sequence_numbers = array ();
+
+ /**
+ * Set the predetermined sort order.
+ *
+ * @param array Numerically indexed array of IMAP message sequence numbers
+ */
+ function set_sequence_numbers($seqnums)
+ {
+ $this->sequence_numbers = array_flip($seqnums);
+ }
+
+ /**
+ * Sort the array of header objects
+ *
+ * @param threads A thread tree
+ */
+ function sort_threads(& $threads)
+ {
+ uksort($threads, array (
+ $this,
+ "compare_seqnums"
+ ));
+ foreach (array_keys($threads) as $root)
+ {
+ $this->sort_threads($threads[$root]);
+ }
+ }
+
+ /**
+ * Sort method called by uasort()
+ */
+ function compare_seqnums($seqa, $seqb)
+ {
+ // find each sequence number in my ordered list
+ $posa = isset ($this->sequence_numbers[$seqa]) ? intval($this->sequence_numbers[$seqa]) : -1;
+ $posb = isset ($this->sequence_numbers[$seqb]) ? intval($this->sequence_numbers[$seqb]) : -1;
+
+ // return the relative position as the comparison value
+ return $posa - $posb;
+ }
+}
+
+
+/**
+ * Class for sorting thread roots in a predetermined order.
+ *
+ * @package Mail
+ * @author Chris January
+ */
+class rcube_thread_root_sorter
+{
+ var $sequence_numbers = array ();
+
+ /**
+ * Set the predetermined sort order.
+ *
+ * @param array Numerically indexed array of IMAP message sequence numbers
+ */
+ function set_sequence_numbers($seqnums)
+ {
+ $this->sequence_numbers = array_flip($seqnums);
+ }
+
+ /**
+ * Sort the array of header objects
+ *
+ * @param threads A thread tree
+ */
+ function sort_threads(& $threads)
+ {
+ usort($threads, array (
+ $this,
+ "compare_seqnums"
+ ));
+ }
+
+ /**
+ * Sort method called by uasort()
+ */
+ function compare_seqnums($seqa, $seqb)
+ {
+ // find each sequence number in my ordered list
+ $posa = isset ($this->sequence_numbers[$seqa]) ? intval($this->sequence_numbers[$seqa]) : -1;
+ $posb = isset ($this->sequence_numbers[$seqb]) ? intval($this->sequence_numbers[$seqb]) : -1;
+
+ // return the relative position as the comparison value
+ return $posa - $posb;
+ }
+}
diff --git a/roundcubemail/program/js/app.js b/roundcubemail/program/js/app.js
index 7f1e7241b..03984427d 100644
--- a/roundcubemail/program/js/app.js
+++ b/roundcubemail/program/js/app.js
@@ -207,7 +207,7 @@ function rcube_webmail()
}
if (this.env.messagecount)
- this.enable_command('select-all', 'select-none', 'expunge', true);
+ this.enable_command('select-all', 'select-none', 'expunge', 'expand-all', 'collapse-all', true);
if (this.purge_mailbox_test())
this.enable_command('purge', true);
@@ -298,7 +298,7 @@ function rcube_webmail()
this.enable_command('save', true);
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)
{
@@ -381,6 +381,10 @@ function rcube_webmail()
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;
+ row.has_children = this.env.messages[uid].has_children ? true : false;
+ row.expanded = this.env.messages[uid].expanded ? true : false;
+ row.depth = this.env.messages[uid].depth;
+ row.unread_children = this.env.messages[uid].unread_children;
}
// set eventhandler to message icon
@@ -397,7 +401,7 @@ function rcube_webmail()
{
var found;
if((found = find_in_array('flag', this.env.coltypes)) >= 0)
- this.set_env('flagged_col', found+1);
+ this.set_env('flagged_col', found);
}
// set eventhandler to flag icon, if icon found
@@ -409,6 +413,15 @@ function rcube_webmail()
row.flagged_icon._row = row.obj;
row.flagged_icon.onmousedown = function(e) { p.command('toggle_flag', this); };
}
+
+ // expando is handled here rather than in rcube_list_widget so that the
+ // expanded state may be persisted in this.env.messages
+ var expando = document.getElementById('rcmexpando' + uid);
+ if (expando != null)
+ {
+ var p = this;
+ expando.onmousedown = function(e) { return p.expando_clicked(e, uid); };
+ }
};
// init message compose form: set focus and eventhandlers
@@ -794,6 +807,14 @@ function rcube_webmail()
this.message_list.clear_selection();
break;
+ case 'expand-all':
+ this.message_list.expand_all();
+ break;
+
+ case 'collapse-all':
+ this.message_list.collapse_all();
+ break;
+
case 'nextmessage':
if (this.env.next_uid)
this.show_message(this.env.next_uid, false, this.env.action=='preview');
@@ -1049,7 +1070,15 @@ function rcube_webmail()
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;
@@ -1976,6 +2005,13 @@ function rcube_webmail()
};
+ this.expando_clicked = function(e, id)
+ {
+ this.message_list.expando_clicked(e, id);
+ this.env.messages[id].expanded = this.message_list.rows[id].expanded;
+ },
+
+
/*********************************************************/
/********* login form methods *********/
/*********************************************************/
@@ -3195,7 +3231,20 @@ function rcube_webmail()
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)
{
@@ -3479,7 +3528,7 @@ function rcube_webmail()
for (var n=0; thead && n<this.coltypes.length; n++)
{
col = this.coltypes[n];
- if ((cell = thead.rows[0].cells[n+1]) && (col=='from' || col=='to'))
+ if ((cell = thead.rows[0].cells[n]) && (col=='from' || col=='to'))
{
// if we have links for sorting, it's a bit more complicated...
if (cell.firstChild && cell.firstChild.tagName=='A')
@@ -3494,7 +3543,7 @@ function rcube_webmail()
cell.id = 'rcm'+col;
}
else if (col == 'subject' && this.message_list)
- this.message_list.subject_col = n+1;
+ this.message_list.subject_col = n;
}
};
@@ -3507,12 +3556,21 @@ function rcube_webmail()
var tbody = this.gui_objects.messagelist.tBodies[0];
var rowcount = tbody.rows.length;
var even = rowcount%2;
+ var depth = cols['depth'];
+ var unread_children = cols['unread_children'];
- 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};
+ // don't overwrite the existing values - we want to preserve expanded
+ var message = this.env.messages[uid];
+ if (!message)
+ message = this.env.messages[uid] = Array();
+ message.deleted = flags.deleted?1:0;
+ message.replied = flags.replied?1:0;
+ message.unread = flags.unread?1:0;
+ message.forwarded = flags.forwarded?1:0;
+ message.flagged = flags.flagged?1:0;
+ message.has_children = flags.has_children?1:0;
+ message.unread_children = unread_children;
+ message.depth = depth;
var row = document.createElement('TR');
row.id = 'rcmrow'+uid;
@@ -3526,7 +3584,9 @@ function rcube_webmail()
row.className += ' selected';
var icon = this.env.messageicon;
- if (flags.deleted && this.env.deletedicon)
+ if (!flags.unread && 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)
{
@@ -3540,10 +3600,17 @@ function rcube_webmail()
else if(flags.unread && this.env.unreadicon)
icon = this.env.unreadicon;
- var col = document.createElement('TD');
- col.className = 'icon';
- col.innerHTML = icon ? '<img src="'+icon+'" alt="" />' : '';
- row.appendChild(col);
+ var tree = '';
+ if (depth > 0)
+ {
+ for (var i=1;i<depth;i++)
+ tree += '<div class="branch">&nbsp</div>';
+ tree += flags.has_children?'<div id="rcmexpando' + uid + '" class="' + (message.expanded?'expanded':'collapsed') + '">&nbsp;</div>':'<div class="leaf">&nbsp;</div>';
+ if (depth > 1)
+ row.style.display = 'none';
+ }
+
+ tree += icon ? '<img src="'+icon+'" alt="" />' : '';
// add each submitted col
for (var n = 0; n < this.coltypes.length; n++)
@@ -3552,17 +3619,21 @@ function rcube_webmail()
col = document.createElement('TD');
col.className = String(c).toLowerCase();
+ var html;
if (c=='flag')
{
if (flags.flagged && this.env.flaggedicon)
- col.innerHTML = '<img src="'+this.env.flaggedicon+'" alt="" />';
+ html = '<img src="'+this.env.flaggedicon+'" alt="" />';
else if(!flags.flagged && this.env.unflaggedicon)
- col.innerHTML = '<img src="'+this.env.unflaggedicon+'" alt="" />';
+ html = '<img src="'+this.env.unflaggedicon+'" alt="" />';
}
else if (c=='attachment')
- col.innerHTML = attachment && this.env.attachmenticon ? '<img src="'+this.env.attachmenticon+'" alt="" />' : '&nbsp;';
+ html = attachment && this.env.attachmenticon ? '<img src="'+this.env.attachmenticon+'" alt="" />' : '&nbsp;';
else
- col.innerHTML = cols[c];
+ html = cols[c];
+ if (n == 0)
+ html = tree + html;
+ col.innerHTML = html;
row.appendChild(col);
}
@@ -3578,6 +3649,12 @@ function rcube_webmail()
}
};
+ // expand any threads that were open
+ this.expand_threads = function()
+ {
+ this.message_list.expand(null);
+ }
+
// replace content of row count display
this.set_rowcount = function(text)
{
@@ -3667,6 +3744,14 @@ function rcube_webmail()
}
};
+ // replace content of row count display
+ this.set_threaded = function(threaded)
+ {
+ var controls = document.getElementById("threadcontrols");
+ if (controls)
+ controls.style.display = threaded?'inline':'none';
+ };
+
// notifies that a new message(s) has hit the mailbox
this.new_message_focus = function()
{
@@ -3925,7 +4010,8 @@ function rcube_webmail()
this.show_contentframe(false);
// disable commands useless when mailbox is empty
this.enable_command('show', 'reply', 'reply-all', 'forward', 'moveto', 'delete', 'mark', 'viewsource',
- 'print', 'load-attachment', 'purge', 'expunge', 'select-all', 'select-none', 'sort', false);
+ 'print', 'load-attachment', 'purge', 'expunge', 'select-all', 'select-none', 'sort', 'expand-all',
+ 'collapse-all', false);
}
break;
@@ -3935,7 +4021,7 @@ function rcube_webmail()
if (this.task == 'mail') {
if (this.message_list && request_obj.__action == 'list')
this.msglist_select(this.message_list);
- this.enable_command('show', 'expunge', 'select-all', 'select-none', 'sort', (this.env.messagecount > 0));
+ this.enable_command('show', 'expunge', 'select-all', 'select-none', 'sort', 'expand-all', 'collapse-all', (this.env.messagecount > 0));
this.enable_command('purge', this.purge_mailbox_test());
}
else if (this.task == 'addressbook')
@@ -4187,6 +4273,7 @@ function rcube_http_request()
}
catch(err)
{
+ alert(err.stack);
this.onerror(this);
this.busy = false;
}
diff --git a/roundcubemail/program/js/list.js b/roundcubemail/program/js/list.js
index 8d8f9e965..bca1ac9bb 100644
--- a/roundcubemail/program/js/list.js
+++ b/roundcubemail/program/js/list.js
@@ -77,7 +77,7 @@ init: function()
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++;
@@ -118,6 +118,10 @@ init_row: function(row)
if (document.all)
row.onselectstart = function() { return false; };
+ var expando = document.getElementById('rcmexpando' + uid);
+ if (expando != null)
+ expando.onmousedown = function(e) { return p.expando_clicked(e, uid); };
+
this.row_init(this.rows[uid]);
}
},
@@ -319,6 +323,130 @@ click_row: function(e, id)
},
+expando_clicked: function(e, id)
+{
+ var row = this.rows[id];
+ var evtarget = rcube_event.get_target(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";
+ this.collapse(row);
+ } else {
+ evtarget.className = "expanded";
+ this.expand(row);
+ }
+},
+
+
+collapse: function(row)
+{
+ row.expanded = false;
+ 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.style.display = 'none';
+ }
+ new_row = new_row.nextSibling;
+ }
+ return false;
+},
+
+
+expand: function(row)
+{
+ var depth, new_row;
+ if (row) {
+ row.expanded = true;
+ depth = row.depth;
+ new_row = row.obj.nextSibling;
+ } else {
+ var tbody = this.list.tBodies[0];
+ new_row = tbody.firstChild;
+ depth = 0;
+ }
+ var current_expand = true;
+ var current_depth = depth;
+ var expand_stack = new Array();
+ var depth_stack = new Array();
+ while (new_row) {
+ if (new_row.nodeType == 1) {
+ var r = this.rows[new_row.uid];
+ if (r) {
+ if (r.depth <= depth)
+ break;
+ while (r.depth <= current_depth) {
+ current_expand = expand_stack.pop();
+ current_depth = depth_stack.pop();
+ }
+ if (current_expand)
+ new_row.style.display = 'table-row';
+ if (r.has_children) {
+ expand_stack.push(current_expand);
+ depth_stack.push(current_depth);
+ current_expand = r.expanded;
+ current_depth = r.depth;
+ }
+ }
+ }
+ new_row = new_row.nextSibling;
+ }
+ return false;
+},
+
+
+collapse_all: function()
+{
+ var tbody = this.list.tBodies[0];
+ new_row = tbody.firstChild;
+ var r;
+ while (new_row) {
+ if (new_row.nodeType == 1) {
+ var r = this.rows[new_row.uid];
+ if (r.depth > 1)
+ new_row.style.display = 'none';
+ if (r.has_children) {
+ r.expanded = false;
+ var expando = document.getElementById('rcmexpando' + r.uid);
+ if (expando)
+ expando.className = 'collapsed';
+ }
+ }
+ new_row = new_row.nextSibling;
+ }
+ return false;
+},
+
+
+expand_all: function()
+{
+ var tbody = this.list.tBodies[0];
+ new_row = tbody.firstChild;
+ var r;
+ while (new_row) {
+ if (new_row.nodeType == 1) {
+ var r = this.rows[new_row.uid];
+ new_row.style.display = 'table-row';
+ if (r.has_children) {
+ r.expanded = true;
+ var expando = document.getElementById('rcmexpando' + r.uid);
+ if (expando)
+ expando.className = 'expanded';
+ }
+ }
+ new_row = new_row.nextSibling;
+ }
+ return false;
+},
+
+
/**
* get next/previous/last rows that are not hidden
*/
@@ -641,6 +769,12 @@ key_press: function(e)
// Stop propagation so that the browser doesn't scroll
rcube_event.cancel(e);
return this.use_arrow_key(keyCode, mod_key);
+ case 61:
+ case 109:
+ case 32:
+ // Stop propagation
+ rcube_event.cancel(e);
+ return this.use_plusminus_key(keyCode, mod_key);
default:
this.shiftkey = e.shiftKey;
this.key_pressed = keyCode;
@@ -664,6 +798,9 @@ key_down: function(e)
case 38:
case 63233:
case 63232:
+ case 61:
+ case 109:
+ case 32:
if (!rcube_event.get_modifier(e) && this.focused)
return rcube_event.cancel(e);
@@ -698,6 +835,28 @@ use_arrow_key: function(keyCode, mod_key)
/**
+ * Special handling method for +/- keys
+ */
+use_plusminus_key: function(keyCode, mod_key)
+{
+ var last_selected_row = this.rows[this.last_selected];
+ if (!last_selected_row)
+ return;
+ if (keyCode == 32)
+ keyCode = last_selected_row.expanded ? 109 : 61;
+ if (keyCode == 61)
+ this.expand(last_selected_row);
+ else
+ this.collapse(last_selected_row);
+ var expando = document.getElementById('rcmexpando' + last_selected_row.uid);
+ if (expando)
+ last_selected_row.className = last_selected_row.expanded?'expanded':'collapsed';
+
+ return false;
+},
+
+
+/**
* Try to scroll the list to make the specified row visible
*/
scrollto: function(id)
diff --git a/roundcubemail/program/lib/imap.inc b/roundcubemail/program/lib/imap.inc
index 67a2b7b3c..2229f27f0 100644
--- a/roundcubemail/program/lib/imap.inc
+++ b/roundcubemail/program/lib/imap.inc
@@ -182,6 +182,9 @@ class iilBasicHeader
var $forwarded = false;
var $junk = false;
var $flagged = false;
+ var $has_children = false;
+ var $depth = 0;
+ var $unread_children = 0;
}
/**
@@ -2146,6 +2149,77 @@ function iil_C_ID2UID(&$conn, $folder, $id) {
return $result;
}
+function iil_ParseThread($str, $root, $parent, $depth, &$depthmap, &$haschildren) {
+ $node = array();
+ if (strlen($str) == 0)
+ return $node;
+ if (substr($str, 0, 1) != '(') {
+ $p = split('[^0-9]', $str, 2);
+ $msg = $p[0];
+ $str = $p[1];
+ if (is_null($root))
+ $root = $msg;
+ $depthmap[$msg] = $depth;
+ $haschildren[$msg] = false;
+ if (!is_null($parent))
+ $haschildren[$parent] = true;
+ $node[$msg] = iil_ParseThread($str, $root, $msg, $depth + 1, $depthmap, $haschildren);
+ } else {
+ $off = 0;
+ $len = strlen($str);
+ while ($off < $len) {
+ $start = $off;
+ $off++;
+ $n = 1;
+ while ($n > 0) {
+ $p = strpos($str, ')', $off);
+ if ($p === false) {
+ error_log("Can't parse (".substr($str, $off).") - mismatched brackets");
+ return $node;
+ }
+ $p1 = strpos($str, '(', $off);
+ if ($p1 !== false && $p1 < $p) {
+ $off = $p1 + 1;
+ $n++;
+ } else {
+ $off = $p + 1;
+ $n--;
+ }
+ }
+ $s = substr($str, $start + 1, $off - $start - 2);
+ $node += iil_ParseThread($s, $root, $parent, $depth, $depthmap, $haschildren);
+ }
+ }
+
+ return $node;
+}
+
+function iil_C_Thread(&$conn, $folder, $algorithm, $criteria) {
+ $fp = $conn->fp;
+ if (iil_C_Select($conn, $folder)) {
+ $query = 'thrd1 THREAD ' . chop($algorithm). ' UTF-8 ' . chop($criteria);
+ iil_PutLineC($fp, $query);
+ do {
+ $line=trim(iil_ReadLine($fp, 10000));
+ if (eregi("^\* THREAD", $line)) {
+ $str = trim(substr($line, 8));
+ $depthmap = array();
+ $haschildren = array();
+ $tree = iil_ParseThread($str, null, null, 1, $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) {
$fp = $conn->fp;
if (iil_C_Select($conn, $folder)) {
diff --git a/roundcubemail/program/localization/en_GB/labels.inc b/roundcubemail/program/localization/en_GB/labels.inc
index fae87c558..b46db1528 100644
--- a/roundcubemail/program/localization/en_GB/labels.inc
+++ b/roundcubemail/program/localization/en_GB/labels.inc
@@ -45,6 +45,7 @@ $labels['organization'] = 'Organisation';
$labels['reply-to'] = 'Reply-To';
$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...';
$labels['download'] = 'Download';
@@ -122,6 +123,9 @@ $labels['none'] = 'None';
$labels['unread'] = 'Unread';
$labels['flagged'] = 'Flagged';
$labels['unanswered'] = 'Unanswered';
+$labels['threads'] = 'Threads';
+$labels['expand-all'] = 'Expand All';
+$labels['collapse-all'] = 'Collapse All';
$labels['filter'] = 'Filter';
$labels['compact'] = 'Compact';
$labels['empty'] = 'Empty';
@@ -242,6 +246,7 @@ $labels['2231folding'] = 'Full RFC 2231 (Thunderbird)';
$labels['miscfolding'] = 'RFC 2047/2231 (MS Outlook)';
$labels['2047folding'] = 'Full RFC 2047 (other)';
$labels['advancedoptions'] = 'Advanced options';
+$labels['messagethreading'] = 'Show messages in threads';
$labels['focusonnewmessage'] = 'Focus browser window on new message';
$labels['checkallfolders'] = 'Check all folders for new messages';
$labels['folder'] = 'Folder';
@@ -249,6 +254,7 @@ $labels['folders'] = 'Folders';
$labels['foldername'] = 'Folder name';
$labels['subscribed'] = 'Subscribed';
$labels['messagecount'] = 'Messages';
+$labels['threaded'] = 'Threaded';
$labels['create'] = 'Create';
$labels['createfolder'] = 'Create new folder';
$labels['rename'] = 'Rename';
diff --git a/roundcubemail/program/localization/en_US/labels.inc b/roundcubemail/program/localization/en_US/labels.inc
index 7f0538703..39b145c1b 100644
--- a/roundcubemail/program/localization/en_US/labels.inc
+++ b/roundcubemail/program/localization/en_US/labels.inc
@@ -56,6 +56,7 @@ $labels['reply-to'] = $labels['replyto'];
$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...';
@@ -151,6 +152,10 @@ $labels['flagged'] = 'Flagged';
$labels['unanswered'] = 'Unanswered';
$labels['filter'] = 'Filter';
+$labels['threads'] = 'Threads';
+$labels['expand-all'] = 'Expand All';
+$labels['collapse-all'] = 'Collapse All';
+
$labels['compact'] = 'Compact';
$labels['empty'] = 'Empty';
$labels['purge'] = 'Purge';
@@ -297,6 +302,7 @@ $labels['2231folding'] = 'Full RFC 2231 (Thunderbird)';
$labels['miscfolding'] = 'RFC 2047/2231 (MS Outlook)';
$labels['2047folding'] = 'Full RFC 2047 (other)';
$labels['advancedoptions'] = 'Advanced options';
+$labels['messagethreading'] = 'Show messages in threads';
$labels['focusonnewmessage'] = 'Focus browser window on new message';
$labels['checkallfolders'] = 'Check all folders for new messages';
@@ -305,6 +311,7 @@ $labels['folders'] = 'Folders';
$labels['foldername'] = 'Folder name';
$labels['subscribed'] = 'Subscribed';
$labels['messagecount'] = 'Messages';
+$labels['threaded'] = 'Threaded';
$labels['create'] = 'Create';
$labels['createfolder'] = 'Create new folder';
$labels['rename'] = 'Rename';
diff --git a/roundcubemail/program/steps/mail/check_recent.inc b/roundcubemail/program/steps/mail/check_recent.inc
index a0668537d..d13eef163 100644
--- a/roundcubemail/program/steps/mail/check_recent.inc
+++ b/roundcubemail/program/steps/mail/check_recent.inc
@@ -28,9 +28,9 @@ foreach ($a_mailboxes as $mbox_name) {
// refresh saved search set
if (($search_request = get_input_value('_search', RCUBE_INPUT_GPC)) && isset($_SESSION['search'][$search_request])) {
$_SESSION['search'][$search_request] = $IMAP->refresh_search();
- $all_count = $IMAP->messagecount();
+ $all_count = $IMAP->messagecount(NULL, rcmail::get_instance()->imap->threading?'THREADS':'ALL');
} else {
- $all_count = $IMAP->messagecount(NULL, 'ALL', TRUE);
+ $all_count = $IMAP->messagecount(NULL, rcmail::get_instance()->imap->threading?'THREADS':'ALL', TRUE);
}
$unread_count = $IMAP->messagecount(NULL, 'UNSEEN', TRUE);
@@ -51,18 +51,24 @@ foreach ($a_mailboxes as $mbox_name) {
if (empty($_GET['_list']))
continue;
- // use SEARCH/SORT to find recent messages
- $search_str = 'RECENT';
- if ($search_request)
- $search_str .= ' '.$IMAP->search_string;
-
- $result = $IMAP->search($mbox_name, $search_str, NULL, 'date');
+ if (rcmail::get_instance()->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 = 'RECENT';
+ if ($search_request)
+ $search_str .= ' '.$IMAP->search_string;
- if ($result) {
- // get the headers
- $result_h = $IMAP->list_headers($mbox_name, 1, 'date', 'DESC');
- // add to the list
- rcmail_js_message_list($result_h, TRUE);
+ $result = $IMAP->search($mbox_name, $search_str, NULL, 'date');
+ // get the headers
+ $result_h = $IMAP->list_headers($mbox_name, 1, 'date', 'DESC');
+ // add to the list
+ rcmail_js_message_list($result_h, TRUE);
}
}
}
diff --git a/roundcubemail/program/steps/mail/func.inc b/roundcubemail/program/steps/mail/func.inc
index 79e148d8a..bac49463e 100644
--- a/roundcubemail/program/steps/mail/func.inc
+++ b/roundcubemail/program/steps/mail/func.inc
@@ -44,6 +44,8 @@ if ($mbox = get_input_value('_mbox', RCUBE_INPUT_GPC))
$IMAP->set_mailbox(($_SESSION['mbox'] = $mbox));
else
$_SESSION['mbox'] = $IMAP->get_mailbox_name();
+$a_message_threading = $RCMAIL->config->get('message_threading', array());
+rcmail::get_instance()->imap->threading = $a_message_threading[$_SESSION['mbox']];
if (!empty($_GET['_page']))
$IMAP->set_page(($_SESSION['page'] = intval($_GET['_page'])));
@@ -80,7 +82,7 @@ if (empty($RCMAIL->action) || $RCMAIL->action == 'list')
}
// make sure the message count is refreshed (for default view)
- $IMAP->messagecount($mbox_name, 'ALL', true);
+ $IMAP->messagecount($mbox_name, rcmail::get_instance()->imap->threading?'THREADS':'ALL', true);
}
// set current mailbox in client environment
@@ -152,7 +154,6 @@ function rcmail_message_list($attrib)
// 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" />';
@@ -160,7 +161,7 @@ function rcmail_message_list($attrib)
$out .= "</colgroup>\n";
// add table title
- $out .= "<thead><tr>\n<td class=\"icon\">&nbsp;</td>\n";
+ $out .= "<thead><tr>\n";
$javascript = '';
foreach ($a_show_cols as $col)
@@ -257,9 +258,15 @@ function rcmail_message_list($attrib)
$js_row_arr['forwarded'] = true;
if ($header->flagged)
$js_row_arr['flagged'] = true;
+ if ($header->has_children)
+ $js_row_arr['has_children'] = true;
+ $js_row_arr['depth'] = $header->depth;
+ $js_row_arr['unread_children'] = $header->unread_children;
- // set message icon
- if ($attrib['deletedicon'] && $header->deleted)
+ // set message icon
+ if ($header->seen && $attrib['unreadchildrenicon'] && $header->unread_children > 0)
+ $message_icon = $attrib['unreadchildrenicon'];
+ else if ($attrib['deletedicon'] && $header->deleted)
$message_icon = $attrib['deletedicon'];
else if ($attrib['repliedicon'] && $header->answered)
{
@@ -284,19 +291,28 @@ function rcmail_message_list($attrib)
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",
+ $out .= sprintf('<tr id="rcmrow%d" class="message%s%s%s%s"%s>'."\n",
$header->uid,
$header->seen ? '' : ' unread',
$header->deleted ? ' deleted' : '',
$header->flagged ? ' flagged' : '',
- $zebra_class);
+ $zebra_class,
+ ($header->depth > 1) ? ' style="display: none"' : '');
- $out .= sprintf("<td class=\"icon\">%s</td>\n", $message_icon ? sprintf($image_tag, $skin_path, $message_icon, '') : '');
-
+ $tree = '';
+ $depth = $header->depth;
+ if ($depth > 0)
+ {
+ for ($i=1;$i<$depth;$i++)
+ $tree .= '<div class="branch">&nbsp</div>';
+ $tree .= $header->has_children?'<div id="rcmexpando' . $header->uid . '" class="collapsed">&nbsp;</div>':'<div class="leaf">&nbsp;</div>';
+ }
+ $tree .= $message_icon ? sprintf($image_tag, $skin_path, $message_icon, '') : '';
$IMAP->set_charset(!empty($header->charset) ? $header->charset : $CONFIG['default_charset']);
// format each col
+ $first = true;
foreach ($a_show_cols as $col)
{
if ($col=='from' || $col=='to')
@@ -318,6 +334,10 @@ function rcmail_message_list($attrib)
else
$cont = Q($header->$col);
+ if ($first) {
+ $first = false;
+ $cont = $tree . $cont;
+ }
if ($col!='attachment')
$out .= '<td class="'.$col.'">' . $cont . "</td>\n";
else
@@ -333,7 +353,7 @@ function rcmail_message_list($attrib)
// complete message table
$out .= "</tbody></table>\n";
- $message_count = $IMAP->messagecount();
+ $message_count = $IMAP->messagecount(NULL, rcmail::get_instance()->imap->threading?'THREADS':'ALL');
// set client env
$OUTPUT->add_gui_object('mailcontframe', 'mailcontframe');
@@ -362,6 +382,8 @@ function rcmail_message_list($attrib)
$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('coltypes', $a_show_cols);
@@ -393,6 +415,12 @@ function rcmail_js_message_list($a_headers, $insert_top=FALSE)
$OUTPUT->command('set_message_coltypes', $a_show_cols);
+ // remove 'attachment' and 'flag' columns, we don't need them here
+ if(($key = array_search('attachment', $a_show_cols)) !== FALSE)
+ unset($a_show_cols[$key]);
+ if(($key = array_search('flag', $a_show_cols)) !== FALSE)
+ unset($a_show_cols[$key]);
+
// loop through message headers
foreach ($a_headers as $n => $header)
{
@@ -404,12 +432,6 @@ function rcmail_js_message_list($a_headers, $insert_top=FALSE)
$IMAP->set_charset(!empty($header->charset) ? $header->charset : $CONFIG['default_charset']);
- // remove 'attachment' and 'flag' columns, we don't need them here
- if(($key = array_search('attachment', $a_show_cols)) !== FALSE)
- unset($a_show_cols[$key]);
- if(($key = array_search('flag', $a_show_cols)) !== FALSE)
- unset($a_show_cols[$key]);
-
// format each col; similar as in rcmail_message_list()
foreach ($a_show_cols as $col)
{
@@ -438,6 +460,9 @@ function rcmail_js_message_list($a_headers, $insert_top=FALSE)
$a_msg_flags['replied'] = $header->answered ? 1 : 0;
$a_msg_flags['forwarded'] = $header->forwarded ? 1 : 0;
$a_msg_flags['flagged'] = $header->flagged ? 1 : 0;
+ $a_msg_flags['has_children'] = $header->has_children ? 1 : 0;
+ $a_msg_cols['depth'] = $header->depth;
+ $a_msg_cols['unread_children'] = $header->unread_children;
$OUTPUT->command('add_message_row',
$header->uid,
@@ -446,6 +471,7 @@ function rcmail_js_message_list($a_headers, $insert_top=FALSE)
preg_match("/multipart\/m/i", $header->ctype),
$insert_top);
}
+ $OUTPUT->command('expand_threads');
}
@@ -569,19 +595,19 @@ function rcmail_get_messagecount_text($count=NULL, $page=NULL)
{
return rcube_label(array('name' => 'messagenrof',
'vars' => array('nr' => $MESSAGE->index+1,
- 'count' => $count!==NULL ? $count : $IMAP->messagecount())));
+ 'count' => $count!==NULL ? $count : $IMAP->messagecount(NULL, rcmail::get_instance()->imap->threading?'THREADS':'ALL'))));
}
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, rcmail::get_instance()->imap->threading?'THREADS':'ALL');
if ($max==0)
$out = rcube_label('mailboxempty');
else
- $out = rcube_label(array('name' => 'messagesfromto',
+ $out = rcube_label(array('name' => rcmail::get_instance()->imap->threading?'threadsfromto':'messagesfromto',
'vars' => array('from' => $start_msg,
'to' => min($max, $start_msg + $IMAP->page_size - 1),
'count' => $max)));
diff --git a/roundcubemail/program/steps/mail/list.inc b/roundcubemail/program/steps/mail/list.inc
index 5cc2a574d..4d5169c29 100644
--- a/roundcubemail/program/steps/mail/list.inc
+++ b/roundcubemail/program/steps/mail/list.inc
@@ -53,10 +53,10 @@ if ($_SESSION['search_filter'] && $_SESSION['search_filter'] != 'ALL')
// fetch message headers
-if ($IMAP->messagecount($mbox_name, 'ALL', !empty($_REQUEST['_refresh'])))
+if ($IMAP->messagecount($mbox_name, rcmail::get_instance()->imap->threading?'THREADS':'ALL', !empty($_REQUEST['_refresh'])))
$a_headers = $IMAP->list_headers($mbox_name, NULL, $sort_col, $sort_order);
-$count = $IMAP->messagecount($mbox_name);
+$count = $IMAP->messagecount($mbox_name, rcmail::get_instance()->imap->threading?'THREADS':'ALL');
$unseen = $IMAP->messagecount($mbox_name, 'UNSEEN', !empty($_REQUEST['_refresh']));
// update message count display
@@ -65,6 +65,7 @@ $OUTPUT->set_env('messagecount', $count);
$OUTPUT->set_env('pagecount', $pages);
$OUTPUT->command('set_rowcount', rcmail_get_messagecount_text($count));
$OUTPUT->command('set_mailboxname', rcmail_get_mailbox_name_text());
+$OUTPUT->command('set_threaded', rcmail::get_instance()->imap->threading);
// add message rows
if (isset($a_headers) && count($a_headers))
diff --git a/roundcubemail/program/steps/mail/move_del.inc b/roundcubemail/program/steps/mail/move_del.inc
index 673bd800b..bf78429f1 100644
--- a/roundcubemail/program/steps/mail/move_del.inc
+++ b/roundcubemail/program/steps/mail/move_del.inc
@@ -20,7 +20,7 @@
*/
// count messages before changing anything
-$old_count = $IMAP->messagecount();
+$old_count = $IMAP->messagecount(NULL, rcmail::get_instance()->imap->threading?'THREADS':'ALL');
$old_pages = ceil($old_count / $IMAP->page_size);
// move messages
@@ -71,7 +71,7 @@ if (($search_request = get_input_value('_search', RCUBE_INPUT_GPC)) && $IMAP->se
$_SESSION['search'][$search_request] = $IMAP->refresh_search();
}
-$msg_count = $IMAP->messagecount();
+$msg_count = $IMAP->messagecount(NULL, rcmail::get_instance()->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);
@@ -90,7 +90,6 @@ $OUTPUT->set_env('current_page', $IMAP->list_page);
$OUTPUT->command('set_rowcount', rcmail_get_messagecount_text($msg_count));
// update mailboxlist
-$mbox = $IMAP->get_mailbox_name();
$OUTPUT->command('set_unread_count', $mbox, $IMAP->messagecount($mbox, 'UNSEEN'), ($mbox == 'INBOX'));
if ($RCMAIL->action=='moveto' && $target) {
@@ -106,7 +105,11 @@ if ($addrows && $_POST['_from']!='show' && ($jump_back || $nextpage_count > 0))
$a_headers = $IMAP->list_headers($mbox, NULL, $sort_col, $sort_order);
if (!$jump_back) {
- $a_headers = array_slice($a_headers, -$count, $count);
+ if ($_SESSION['threads'])
+ // TODO: count number of roots deleted and slice that many roots from the end of $a_headers
+ $OUTPUT->command('message_list.clear');
+ else
+ $a_headers = array_slice($a_headers, -$count, $count);
}
rcmail_js_message_list($a_headers);
}
diff --git a/roundcubemail/program/steps/mail/rss.inc b/roundcubemail/program/steps/mail/rss.inc
index 72317c68f..4e75811c9 100644
--- a/roundcubemail/program/steps/mail/rss.inc
+++ b/roundcubemail/program/steps/mail/rss.inc
@@ -42,7 +42,7 @@ if (dirname($_SERVER['SCRIPT_NAME']) != '/')
$webmail_url .= '?_task=mail';
$messagecount_unread = $IMAP->messagecount('INBOX', 'UNSEEN', TRUE);
-$messagecount = $IMAP->messagecount();
+$messagecount = $IMAP->messagecount(NULL, rcmail::get_instance()->imap->threading?'THREADS':'ALL');
$sort_col = 'date';
$sort_order = 'DESC';
diff --git a/roundcubemail/program/steps/mail/search.inc b/roundcubemail/program/steps/mail/search.inc
index 95ca67a12..d74a23030 100644
--- a/roundcubemail/program/steps/mail/search.inc
+++ b/roundcubemail/program/steps/mail/search.inc
@@ -90,7 +90,7 @@ if ($search_str)
// Get the headers
$result_h = $IMAP->list_headers($mbox, 1, $_SESSION['sort_col'], $_SESSION['sort_order']);
-$count = $IMAP->messagecount();
+$count = $IMAP->messagecount(NULL, rcmail::get_instance()->imap->threading?'THREADS':'ALL');
// save search results in session
if (!is_array($_SESSION['search']))
diff --git a/roundcubemail/program/steps/settings/func.inc b/roundcubemail/program/steps/settings/func.inc
index d0a3ae69c..6515ae4f6 100644
--- a/roundcubemail/program/steps/settings/func.inc
+++ b/roundcubemail/program/steps/settings/func.inc
@@ -168,6 +168,8 @@ function rcmail_user_prefs_block($part, $no_override, $attrib)
case 'mailbox':
$table = new html_table(array('cols' => 2));
+ $RCMAIL->imap_init(true);
+
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/roundcubemail/program/steps/settings/manage_folders.inc b/roundcubemail/program/steps/settings/manage_folders.inc
index 9affded98..80a6b7c65 100644
--- a/roundcubemail/program/steps/settings/manage_folders.inc
+++ b/roundcubemail/program/steps/settings/manage_folders.inc
@@ -38,6 +38,28 @@ else if ($RCMAIL->action=='unsubscribe')
$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, 'UTF-7'))
+ $a_user_prefs = $USER->get_prefs();
+ if (!is_array($a_user_prefs['message_threading']))
+ $a_user_prefs['message_threading'] = array();
+ $a_user_prefs['message_threading'][$mbox] = true;
+ $USER->save_prefs($a_user_prefs);
+ }
+
+// enable threading for one or more mailboxes
+else if ($RCMAIL->action=='disable-threading')
+ {
+ if ($mbox = get_input_value('_mbox', RCUBE_INPUT_POST, false, 'UTF-7'))
+ $a_user_prefs = $USER->get_prefs();
+ if (!is_array($a_user_prefs['message_threading']))
+ $a_user_prefs['message_threading'] = array();
+ unset($a_user_prefs['message_threading'][$mbox]);
+ $USER->save_prefs($a_user_prefs);
+ }
+
// create a new mailbox
else if ($RCMAIL->action=='create-folder')
{
@@ -162,6 +184,8 @@ function rcube_subscription_form($attrib)
{
global $IMAP, $CONFIG, $OUTPUT;
+ $threading_supported = $IMAP->get_capability('thread=references');
+
list($form_start, $form_end) = get_form_tags($attrib, 'folders');
unset($attrib['form']);
@@ -174,6 +198,8 @@ function rcube_subscription_form($attrib)
$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;');
@@ -183,6 +209,7 @@ function rcube_subscription_form($attrib)
$a_unsubscribed = $IMAP->list_unsubscribed();
$a_subscribed = $IMAP->list_mailboxes();
+ $a_threaded =rcmail::get_instance()->config->get('message_threading', array());
$delimiter = $IMAP->get_hierarchy_delimiter();
$a_js_folders = $seen_folders = $list_folders = array();
@@ -212,6 +239,10 @@ function rcube_subscription_form($attrib)
'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']))
$del_button = html::img(array('src' => $CONFIG['skin_path'] . $attrib['deleteicon'], 'alt' => rcube_label('delete')));
@@ -227,6 +258,7 @@ function rcube_subscription_form($attrib)
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']);
@@ -239,9 +271,13 @@ function rcube_subscription_form($attrib)
$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']) {