Index: skins/default/templates/mail.html
===================================================================
--- skins/default/templates/mail.html (revision 2716)
+++ skins/default/templates/mail.html (working copy)
@@ -58,7 +58,8 @@
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="/images/icons/unread_children.png" />
@@ -86,6 +87,11 @@
+
+ :
+
+
+
:
Index: skins/default/mail.css
===================================================================
--- skins/default/mail.css (revision 2716)
+++ skins/default/mail.css (working copy)
@@ -204,6 +204,9 @@
#mailboxcontrols a,
#mailboxcontrols a:active,
#mailboxcontrols a:visited,
+#threadcontrols a,
+#threadcontrols a:active,
+#threadcontrols a:visited,
td.formlinks a,
td.formlinks a:visited
{
@@ -218,6 +221,12 @@
#mailboxcontrols a.active,
#mailboxcontrols a.active:active,
#mailboxcontrols a.active:visited,
+#threadcontrols a,
+#threadcontrols a:active,
+#threadcontrols a:visited,
+ul.toolbarmenu li a.active,
+ul.toolbarmenu li a.active:active,
+ul.toolbarmenu li a.active:visited,
td.formlinks a,
td.formlinks a:visited
{
@@ -225,12 +234,14 @@
}
#listcontrols a.active:hover,
-#mailboxcontrols a.active:hover
+#mailboxcontrols a.active:hover,
+#threadcontrols a.active:hover
{
text-decoration: underline;
}
-#listcontrols
+#listcontrols,
+#threadcontrols
{
padding-right: 2em;
}
@@ -646,6 +657,30 @@
cursor: pointer;
}
+#messagelist tr td div
+{
+ display: table-cell; /* For FireFox and Opera */
+ display: inline-block; /* For Opera and IE */
+ width: 15px;
+ height: 15px;
+}
+
+#messagelist tr td div.collapsed,
+#messagelist tr td div.expanded
+{
+ cursor: pointer;
+}
+
+#messagelist tr td div.collapsed
+{
+ background: url(images/icons/collapsed.png) center center no-repeat;
+}
+
+#messagelist tr td div.expanded
+{
+ background: url(images/icons/expanded.png) center center no-repeat;
+}
+
#messagelist tbody tr td.flag img:hover,
#messagelist thead tr td.flag img
{
Index: index.php
===================================================================
--- index.php (revision 2716)
+++ index.php (working copy)
@@ -212,6 +212,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',
)
);
Index: SQL/mysql.initial.sql
===================================================================
--- SQL/mysql.initial.sql (revision 2716)
+++ SQL/mysql.initial.sql (working copy)
@@ -62,6 +62,29 @@
) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
+-- Table structure for table `threads`
+
+CREATE TABLE `threads` (
+ `thread_id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT,
+ `user_id` int(10) UNSIGNED NOT NULL DEFAULT '0',
+ `mailbox` varchar(128) /*!40101 CHARACTER SET ascii COLLATE ascii_general_ci */ NOT NULL,
+ `idx` int(11) UNSIGNED NOT NULL DEFAULT '0',
+ `has_children` tinyint(1) NOT NULL DEFAULT '0',
+ `is_root` tinyint(1) NOT NULL DEFAULT '0',
+ `root` int(11) UNSIGNED NOT NULL DEFAULT '0',
+ `depth` int(4) UNSIGNED NOT NULL DEFAULT '0',
+ PRIMARY KEY(`thread_id`),
+ UNIQUE `uniqueness` (`user_id`, `mailbox`, `idx`),
+ INDEX `is_root_index` (`is_root`),
+ INDEX `root_index` (`root`),
+ CONSTRAINT `user_id_fk_threads` FOREIGN KEY (`user_id`)
+ REFERENCES `users`(`user_id`)
+ /*!40008
+ ON DELETE CASCADE
+ ON UPDATE CASCADE */
+) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
+
+
-- Table structure for table `cache`
CREATE TABLE `cache` (
Index: config/main.inc.php.dist
===================================================================
--- config/main.inc.php.dist (revision 2716)
+++ config/main.inc.php.dist (working copy)
@@ -46,6 +46,10 @@
// this is recommended if the IMAP server does not run on the same machine
$rcmail_config['enable_caching'] = TRUE;
+// enable caching of threads in the local database.
+// this is not recommended unless you are using SQLite
+$rcmail_config['enable_thread_caching'] = FALSE;
+
// lifetime of message cache
// possible units: s, m, h, d, w
$rcmail_config['message_cache_lifetime'] = '10d';
@@ -77,6 +81,14 @@
$rcmail_config['imap_root'] = null;
$rcmail_config['imap_delimiter'] = null;
+// The default IMAP message THREAD retrieval algorithm.
+// A common one for threading would be REFERENCES.
+// Make sure that your IMAP server supports this!
+$rcmail_config['imap_thread_algorithm'] = 'REFERENCES';
+
+// if set to true all threads are automatically expanded otherwise they are collapsed (default)
+$rcmail_config['autoexpand_threads'] = FALSE;
+
// Automatically add this domain to user names for login
// Only for IMAP servers that require full e-mail addresses for login
// Specify an array with 'host' => 'domain' values to support multiple hosts
@@ -328,7 +340,7 @@
$rcmail_config['mime_magic'] = '/usr/share/misc/magic';
// default sort col
-$rcmail_config['message_sort_col'] = 'date';
+$rcmail_config['message_sort_col'] = 'default';
// default sort order
$rcmail_config['message_sort_order'] = 'DESC';
Index: program/include/rcube_imap.php
===================================================================
--- program/include/rcube_imap.php (revision 2716)
+++ program/include/rcube_imap.php (working copy)
@@ -52,6 +52,7 @@
var $sort_order = 'DESC';
var $delimiter = NULL;
var $caching_enabled = FALSE;
+ var $thread_caching_enabled = FALSE;
var $default_charset = 'ISO-8859-1';
var $struct_charset = NULL;
var $default_folders = array('INBOX');
@@ -70,6 +71,7 @@
var $debug_level = 1;
var $error_code = 0;
var $options = array('imap' => 'check');
+ var $threading = false;
/**
@@ -482,7 +484,7 @@
$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');
@@ -491,8 +493,12 @@
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='.rcmail::get_instance()->config->get('imap_thread_algorithm')) &&
+ $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
@@ -528,7 +534,26 @@
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, rcmail::get_instance()->config->get('imap_thread_algorithm'), 'ALL');
+ $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
@@ -563,77 +588,155 @@
return $this->_list_header_set($mailbox, $page, $sort_field, $sort_order);
$this->_set_sort_order($sort_field, $sort_order);
-
+
+ $message_threading = $this->get_capability('thread='.rcmail::get_instance()->config->get('imap_thread_algorithm')) &&
+ rcmail::get_instance()->imap->threading;
$page = $page ? $page : $this->list_page;
$cache_key = $mailbox.'.msg';
$cache_status = $this->check_cache_status($mailbox, $cache_key);
+ $headers_sorted = FALSE;
- // cache is OK, we can get all messages from local cache
- if ($cache_status>0)
- {
- $start_msg = ($page-1) * $this->page_size;
- $a_msg_headers = $this->get_message_cache($cache_key, $start_msg, $start_msg+$this->page_size, $this->sort_field, $this->sort_order);
- return array_values($a_msg_headers);
- }
// cache is dirty, sync it
- else if ($this->caching_enabled && $cache_status==-1 && !$recursive)
+ if ($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);
}
- // retrieve headers from IMAP
- $a_msg_headers = array();
+ 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
+ $start_msg = ($page-1) * $this->page_size;
+ $roots = $this->get_thread_cache_roots_sorted($mailbox, $start_msg, $start_msg+$this->page_size, $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, rcmail::get_instance()->config->get('imap_thread_algorithm'), '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') && ($msg_index = iil_C_Sort($this->conn, $mailbox, $this->sort_field, $this->skip_deleted ? 'UNDELETED' : '')))
- {
- list($begin, $end) = $this->_get_message_range(count($msg_index), $page);
- $max = max($msg_index);
- $msg_index = array_slice($msg_index, $begin, $end-$begin);
+ if ($this->sort_field != 'default' &&
+ $this->get_capability('sort') && ($sort_index = iil_C_Sort($this->conn, $mailbox, $this->sort_field, $this->skip_deleted ? 'UNDELETED' : ''))) {
+ // 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
+ list($begin, $end) = $this->_get_message_range(count($roots), $page);
+ $roots = array_slice($roots, $begin, $end - $begin);
+ if ($this->sort_order == 'DESC')
+ $roots = array_reverse($roots);
+ }
- // fetch reqested headers from server
- $this->_fetch_headers($mailbox, join(',', $msg_index), $a_msg_headers, $cache_key);
- }
- else
- {
- $a_index = iil_C_FetchHeaderIndex($this->conn, $mailbox, "1:*", $this->sort_field, $this->skip_deleted);
+ 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->sort_field != 'default' &&
+ $this->get_capability('sort') && ($msg_index = iil_C_Sort($this->conn, $mailbox, $this->sort_field, $this->skip_deleted ? 'UNDELETED' : ''))) {
+ // nothing more to do
+ } else {
+ $a_index = iil_C_FetchHeaderIndex($this->conn, $mailbox, "1:*", ($this->sort_field == 'default') ? '' : $this->sort_field, $this->skip_deleted);
- if (empty($a_index))
- return array();
+ if (empty($a_index))
+ return array();
- asort($a_index); // ASC
- $msg_index = array_keys($a_index);
- $max = max($msg_index);
- list($begin, $end) = $this->_get_message_range(count($msg_index), $page);
- $msg_index = array_slice($msg_index, $begin, $end-$begin);
+ asort($a_index); // ASC
+ $msg_index = array_keys($a_index);
+ }
+ $max = max($msg_index);
+ list($begin, $end) = $this->_get_message_range(count($msg_index), $page);
+ $msg_index = array_slice($msg_index, $begin, $end-$begin);
+ if ($this->sort_order == 'DESC')
+ $msg_index = array_reverse($msg_index);
+ }
- // fetch reqested headers from server
- $this->_fetch_headers($mailbox, join(",", $msg_index), $a_msg_headers, $cache_key);
- }
+ // 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 {
+ // retrieve headers from IMAP
+ $a_msg_headers = array();
+ $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
+ if ($message_threading)
+ $max = $this->_messagecount($mailbox, 'ALL');
+ $this->clear_message_cache($cache_key, $max + 1);
+ }
- // 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();
-
- // 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);
+ 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 ($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)
*
@@ -864,7 +967,9 @@
}
// fetch complete message index
- if ($this->get_capability('sort') && ($a_index = iil_C_Sort($this->conn, $mailbox, $this->sort_field, $this->skip_deleted ? 'UNDELETED' : '')))
+ $msg_count = $this->_messagecount($mailbox);
+ if ($this->sort_field != 'default' &&
+ $this->get_capability('sort') && ($a_index = iil_C_Sort($this->conn, $mailbox, $this->sort_field, $this->skip_deleted ? 'UNDELETED' : '')))
{
if ($this->sort_order == 'DESC')
$a_index = array_reverse($a_index);
@@ -1008,7 +1113,7 @@
if ($this->skip_deleted && !preg_match('/UNDELETED/', $criteria))
$criteria = 'UNDELETED '.$criteria;
- if ($sort_field && $this->get_capability('sort'))
+ if ($sort_field && $sort_field != 'default' && $this->get_capability('sort'))
{
$charset = $charset ? $charset : $this->default_charset;
$a_messages = iil_C_Sort($this->conn, $mailbox, $sort_field, $criteria, FALSE, $charset);
@@ -2038,6 +2143,17 @@
/**
* @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
@@ -2248,25 +2364,29 @@
/**
* @access private
*/
- private function get_message_cache($key, $from, $to, $sort_field, $sort_order)
+ private 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();
+ 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->limitquery(
"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);
@@ -2470,6 +2590,180 @@
}
+ /* --------------------------------
+ * 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)
+ 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 ();
+
+ if ($sort_field == 'default')
+ $sort_field = 'thread_id';
+ $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
* --------------------------------*/
@@ -2928,6 +3222,10 @@
// 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]);
@@ -3102,3 +3400,115 @@
return $posa - $posb;
}
}
+
+
+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;
+ }
+}
Index: program/include/rcmail.php
===================================================================
--- program/include/rcmail.php (revision 2716)
+++ program/include/rcmail.php (working copy)
@@ -356,6 +356,10 @@
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));
Index: program/lib/imap.inc
===================================================================
--- program/lib/imap.inc (revision 2716)
+++ program/lib/imap.inc (working copy)
@@ -173,6 +173,9 @@
var $forwarded = false;
var $junk = false;
var $flagged = false;
+ var $has_children = false;
+ var $depth = 0;
+ var $unread_children = 0;
var $others = array();
}
@@ -1868,6 +1871,82 @@
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, $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, 0, strlen($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)) {
Index: program/localization/en_GB/labels.inc
===================================================================
--- program/localization/en_GB/labels.inc (revision 2716)
+++ program/localization/en_GB/labels.inc (working copy)
@@ -45,6 +45,7 @@
$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';
@@ -124,6 +125,9 @@
$labels['unanswered'] = 'Unanswered';
$labels['deleted'] = 'Deleted';
$labels['invert'] = 'Invert';
+$labels['threads'] = 'Threads';
+$labels['expand-all'] = 'Expand All';
+$labels['collapse-all'] = 'Collapse All';
$labels['filter'] = 'Filter';
$labels['compact'] = 'Compact';
$labels['empty'] = 'Empty';
@@ -246,6 +250,7 @@
$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';
@@ -253,6 +258,7 @@
$labels['foldername'] = 'Folder name';
$labels['subscribed'] = 'Subscribed';
$labels['messagecount'] = 'Messages';
+$labels['threaded'] = 'Threaded';
$labels['create'] = 'Create';
$labels['createfolder'] = 'Create new folder';
$labels['rename'] = 'Rename';
Index: program/localization/en_US/labels.inc
===================================================================
--- program/localization/en_US/labels.inc (revision 2716)
+++ program/localization/en_US/labels.inc (working copy)
@@ -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...';
@@ -154,6 +155,10 @@
$labels['invert'] = 'Invert';
$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';
@@ -303,6 +308,7 @@
$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';
@@ -311,6 +317,7 @@
$labels['foldername'] = 'Folder name';
$labels['subscribed'] = 'Subscribed';
$labels['messagecount'] = 'Messages';
+$labels['threaded'] = 'Threaded';
$labels['create'] = 'Create';
$labels['createfolder'] = 'Create new folder';
$labels['rename'] = 'Rename';
Index: program/js/list.js
===================================================================
--- program/js/list.js (revision 2716)
+++ program/js/list.js (working copy)
@@ -76,7 +76,7 @@
for(var r=0; r 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
*/
@@ -659,6 +787,12 @@
// 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;
@@ -686,6 +820,9 @@
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);
@@ -720,6 +857,28 @@
/**
+ * 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)
@@ -764,16 +923,15 @@
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= 0 && this.subject_col == c)))
{
if (n == 0) {
@@ -782,7 +940,13 @@
else
this.drag_start_pos = $(node).offset();
}
- subject = node.nodeType==3 ? node.data : node.innerHTML;
+ if (node.nodeType == 3) {
+ subject = node.data;
+ } else if (node.nodeType == 1) {
+ subject = node.offsetParent.textContent;
+ } else {
+ subject = node.innerHTML;
+ }
// remove leading spaces
subject = subject.replace(/^\s+/i, '');
// truncate line to 50 characters
Index: program/js/app.js
===================================================================
--- program/js/app.js (revision 2716)
+++ program/js/app.js (working copy)
@@ -237,7 +237,7 @@
}
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);
@@ -331,7 +331,7 @@
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)
{
@@ -415,6 +415,10 @@
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
@@ -431,7 +435,7 @@
{
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
@@ -444,6 +448,15 @@
}
this.triggerEvent('insertrow', { uid:uid, row:row });
+
+ // 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
@@ -604,13 +617,32 @@
case 'sort':
- var sort_order, sort_col = props;
+ var sort_order = null, sort_col = props;
- if (this.env.sort_col==sort_col)
+ // date column cycles between date and default
+ if (sort_col == 'date' && this.env.sort_col=='date' && this.env.sort_order=='ASC')
+ {
+ sort_col = 'default';
+ sort_order = 'DESC';
+ }
+ else if (sort_col == 'date' && this.env.sort_col=='default' && this.env.sort_order=='DESC')
+ {
+ sort_col = 'date';
+ sort_order = 'DESC';
+ }
+ // no sort order specified: toggle
+ else if (this.env.sort_col==sort_col)
+ {
sort_order = this.env.sort_order=='ASC' ? 'DESC' : 'ASC';
+ }
else
- sort_order = 'ASC';
-
+ {
+ sort_order = 'ASC';
+ }
+
+ if (this.env.sort_col==sort_col && this.env.sort_order==sort_order)
+ break;
+
// set table header class
$('#rcm'+this.env.sort_col).removeClass('sorted'+(this.env.sort_order.toUpperCase()));
$('#rcm'+sort_col).addClass('sorted'+sort_order);
@@ -837,6 +869,14 @@
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');
@@ -1098,7 +1138,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;
@@ -2086,6 +2134,13 @@
};
+ 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 *********/
/*********************************************************/
@@ -3299,7 +3354,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)
{
@@ -3566,7 +3634,7 @@
for (var n=0; thead && n 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)
{
@@ -3634,28 +3719,39 @@
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 ? '
' : '';
- row.appendChild(col);
-
+ var tree = '';
+ if (depth > 0)
+ {
+ for (var i=1;i ';
+ tree += flags.has_children?'
':'
';
+ if (depth > 1)
+ row.style.display = 'none';
+ }
+
+ tree += icon ? '
' : '';
+
// 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') {
+
+ var html;
+ if (c=='flag')
+ {
if (flags.flagged && this.env.flaggedicon)
- col.innerHTML = '
';
+ html = '
';
else if(!flags.flagged && this.env.unflaggedicon)
- col.innerHTML = '
';
- }
+ html = '
';
+ }
else if (c=='attachment')
- col.innerHTML = (attachment && this.env.attachmenticon ? '
' : ' ');
+ html = attachment && this.env.attachmenticon ? '
' : ' ';
else
- col.innerHTML = cols[c];
+ html = cols[c];
+ if (n == 0)
+ html = tree + html;
+ col.innerHTML = html;
row.appendChild(col);
}
@@ -3670,6 +3766,7 @@
}
};
+
// messages list handling in background (for performance)
this.offline_message_list = function(flag)
{
@@ -3677,6 +3774,12 @@
this.message_list.set_background_mode(flag);
};
+ // 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)
{
@@ -3767,6 +3870,14 @@
}
};
+ // 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()
{
@@ -3990,7 +4101,8 @@
// 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',
+ 'colapse-all', false);
}
break;
@@ -4000,7 +4112,7 @@
if (this.task == 'mail') {
if (this.message_list && response.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());
if (response.action == 'list')
@@ -4133,7 +4245,6 @@
} // end object rcube_webmail
-
// copy event engine prototype
rcube_webmail.prototype.addEventListener = rcube_event_engine.prototype.addEventListener;
rcube_webmail.prototype.removeEventListener = rcube_event_engine.prototype.removeEventListener;
Index: program/steps/settings/func.inc
===================================================================
--- program/steps/settings/func.inc (revision 2716)
+++ program/steps/settings/func.inc (working copy)
@@ -170,6 +170,8 @@
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));
Index: program/steps/settings/manage_folders.inc
===================================================================
--- program/steps/settings/manage_folders.inc (revision 2716)
+++ program/steps/settings/manage_folders.inc (working copy)
@@ -38,6 +38,28 @@
$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')
{
@@ -161,6 +183,8 @@
{
global $IMAP, $CONFIG, $OUTPUT;
+ $threading_supported = $IMAP->get_capability('thread=references');
+
list($form_start, $form_end) = get_form_tags($attrib, 'folders');
unset($attrib['form']);
@@ -173,6 +197,8 @@
$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', ' ');
$table->add_header('delete', ' ');
@@ -182,6 +208,7 @@
$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();
@@ -211,6 +238,10 @@
'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')));
@@ -226,6 +257,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 +270,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 ? ' •' : ' ') :
$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']) {
Index: program/steps/mail/rss.inc
===================================================================
--- program/steps/mail/rss.inc (revision 2716)
+++ program/steps/mail/rss.inc (working copy)
@@ -42,7 +42,7 @@
$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';
Index: program/steps/mail/list.inc
===================================================================
--- program/steps/mail/list.inc (revision 2716)
+++ program/steps/mail/list.inc (working copy)
@@ -57,7 +57,7 @@
// fetch message headers
-if ($count = $IMAP->messagecount($mbox_name, 'ALL', !empty($_REQUEST['_refresh'])))
+if ($count = $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);
$unseen = $count ? $IMAP->messagecount($mbox_name, 'UNSEEN', !empty($_REQUEST['_refresh'])) : 0;
@@ -68,6 +68,7 @@
$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))
@@ -80,10 +81,13 @@
$OUTPUT->show_message('searchnomatch', 'notice');
else
$OUTPUT->show_message('nomessagesfound', 'notice');
-
+
// update mailboxlist
$OUTPUT->command('set_unread_count', $mbox_name, $unseen, ($mbox_name == 'INBOX'));
+if (rcmail::get_instance()->imap->threading && !empty($CONFIG['autoexpand_threads']))
+ $OUTPUT->command('message_list.expand_all');
+
// send response
$OUTPUT->send();
Index: program/steps/mail/check_recent.inc
===================================================================
--- program/steps/mail/check_recent.inc (revision 2716)
+++ program/steps/mail/check_recent.inc (working copy)
@@ -28,9 +28,9 @@
// 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,27 @@
if (empty($_GET['_list']))
continue;
- // use SEARCH/SORT to find recent messages
- $search_str = 'RECENT';
- if ($search_request)
- $search_str .= ' '.$IMAP->search_string;
+ 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;
- $result = $IMAP->search($mbox_name, $search_str, NULL, 'date');
+ $result = $IMAP->search($mbox_name, $search_str, NULL, 'date');
- 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, false);
+ 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);
+ }
}
}
}
Index: program/steps/mail/move_del.inc
===================================================================
--- program/steps/mail/move_del.inc (revision 2716)
+++ program/steps/mail/move_del.inc (working copy)
@@ -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
@@ -82,7 +82,7 @@
}
else
{
- $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);
@@ -118,7 +118,11 @@
$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, false, false);
}
Index: program/steps/mail/func.inc
===================================================================
--- program/steps/mail/func.inc (revision 2716)
+++ program/steps/mail/func.inc (working copy)
@@ -44,6 +44,8 @@
$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'])));
@@ -81,7 +83,7 @@
$OUTPUT->set_env('search_mods', $_SESSION['search_mods'] ? $_SESSION['search_mods'] : array('subject'=>'subject'));
// 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
@@ -160,7 +162,6 @@
// add col definition
$out .= '';
- $out .= '';
foreach ($a_show_cols as $col)
$out .= ($col!='attachment') ? sprintf('', $col) : '';
@@ -168,7 +169,7 @@
$out .= "\n";
// add table title
- $out .= "\n| | \n";
+ $out .= "\n";
$javascript = '';
foreach ($a_show_cols as $col)
@@ -264,9 +265,15 @@
$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)
{
@@ -291,18 +298,28 @@
if ($attrib['attachmenticon'] && preg_match("/multipart\/m/i", $header->ctype))
$attach_icon = $attrib['attachmenticon'];
- $out .= sprintf('
'."\n",
+ $out .= sprintf('
'."\n",
$header->uid,
$header->seen ? '' : ' unread',
$header->deleted ? ' deleted' : '',
$header->flagged ? ' flagged' : '',
- $zebra_class);
+ $zebra_class,
+ ($header->depth > 1) ? ' style="display: none"' : '');
- $out .= sprintf("| %s | \n", $message_icon ? sprintf($image_tag, $skin_path, $message_icon, '') : '');
+ $tree = '';
+ $depth = $header->depth;
+ if ($depth > 0)
+ {
+ for ($i=1;$i<$depth;$i++)
+ $tree .= ' 
';
+ $tree .= $header->has_children?'
':'
';
+ }
+ $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')
@@ -324,6 +341,10 @@
else
$cont = Q($header->$col);
+ if ($first) {
+ $first = false;
+ $cont = $tree . $cont;
+ }
if ($col!='attachment')
$out .= '' . $cont . " | \n";
else
@@ -339,7 +360,7 @@
// complete message table
$out .= "\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');
@@ -368,6 +389,8 @@
$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);
@@ -404,6 +427,12 @@
if ($browser->ie && $replace)
$OUTPUT->command('offline_message_list', true);
+ // 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)
{
@@ -415,12 +444,6 @@
$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)
{
@@ -465,6 +488,8 @@
if ($browser->ie && $replace)
$OUTPUT->command('offline_message_list', false);
+
+ $OUTPUT->command('expand_threads');
}
@@ -588,19 +613,19 @@
{
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)));
Index: program/steps/mail/search.inc
===================================================================
--- program/steps/mail/search.inc (revision 2716)
+++ program/steps/mail/search.inc (working copy)
@@ -99,7 +99,7 @@
// 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']))