Index: skins/default/templates/mail.html =================================================================== --- skins/default/templates/mail.html (revision 2976) +++ 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" /> @@ -87,6 +88,13 @@ + + + :  +   +   + + : Index: skins/default/mail.css =================================================================== --- skins/default/mail.css (revision 2976) +++ skins/default/mail.css (working copy) @@ -318,6 +318,9 @@ #mailboxcontrols a, #mailboxcontrols a:active, #mailboxcontrols a:visited, +#threadcontrols a, +#threadcontrols a:active, +#threadcontrols a:visited, td.formlinks a, td.formlinks a:visited { @@ -332,6 +335,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 { @@ -339,12 +348,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; } @@ -655,6 +666,7 @@ } #listcontrols, +#threadcontrols, #countcontrols, #quotabox { @@ -781,6 +793,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 2976) +++ index.php (working copy) @@ -224,6 +224,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 2976) +++ SQL/mysql.initial.sql (working copy) @@ -63,6 +63,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 2976) +++ config/main.inc.php.dist (working copy) @@ -45,6 +45,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'; @@ -76,6 +80,16 @@ $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'; + +// 0 - Do not expand threads +// 1 - Expand all threads automatically +// 2 - Expand only threads with unread messages +$rcmail_config['autoexpand_threads'] = 0; + // 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 @@ -340,7 +354,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'; @@ -403,6 +417,9 @@ // default setting if preview pane is enabled $rcmail_config['preview_pane'] = FALSE; +// show messages in threads +$rcmail_config['message_threading'] = false; + // focus new window if new message arrives $rcmail_config['focus_on_new_message'] = true; Index: program/include/rcube_imap.php =================================================================== --- program/include/rcube_imap.php (revision 2976) +++ 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('auth_method' => 'check'); + var $threading = false; private $host, $user, $pass, $port, $ssl; @@ -491,7 +493,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'); @@ -500,8 +502,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 @@ -537,7 +543,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 @@ -574,66 +599,119 @@ $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) + if ($this->caching_enabled && $cache_status==-1 && !$recursive) { - $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); - $result = array_values($a_msg_headers); - if ($slice) - $result = array_slice($result, -$slice, $slice); - return $result; - } - // cache is dirty, sync it - else 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, $slice); } - // 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); + } - if ($slice) - $msg_index = array_slice($msg_index, ($this->sort_order == 'DESC' ? 0 : -$slice), $slice); + // 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); + } - if ($slice) - $msg_index = array_slice($msg_index, ($this->sort_order == 'DESC' ? 0 : -$slice), $slice); + // 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); + } - // fetch reqested headers from server - $this->_fetch_headers($mailbox, join(",", $msg_index), $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 // ... @@ -641,13 +719,32 @@ 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 (!$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); + 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++; + } + if (!empty($parents)) + $a_msg_headers[$uid]->parent_uid = end($parents); + 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); } @@ -888,7 +985,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); @@ -897,11 +996,11 @@ } else { - $a_index = iil_C_FetchHeaderIndex($this->conn, $mailbox, "1:*", $this->sort_field, $this->skip_deleted); + $a_index = iil_C_FetchHeaderIndex($this->conn, $mailbox, "1:*", ($this->sort_field == 'default') ? '' : $this->sort_field, $this->skip_deleted); if ($this->sort_order=="ASC") asort($a_index); - else if ($this->sort_order=="DESC") + else if ($this->sort_field == 'default' || $this->sort_order=="DESC") arsort($a_index); $this->cache[$key] = array_keys($a_index); @@ -1032,7 +1131,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); @@ -2057,6 +2156,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 @@ -2269,25 +2379,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); @@ -2488,6 +2602,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 * --------------------------------*/ @@ -2943,6 +3231,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]); @@ -3117,3 +3409,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 2976) +++ program/include/rcmail.php (working copy) @@ -371,6 +371,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 2976) +++ 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(); } @@ -1861,6 +1864,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) { if (iil_C_Select($conn, $folder)) { Index: program/localization/en_GB/labels.inc =================================================================== --- program/localization/en_GB/labels.inc (revision 2976) +++ 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,13 @@ $labels['unanswered'] = 'Unanswered'; $labels['deleted'] = 'Deleted'; $labels['invert'] = 'Invert'; +$labels['threads'] = 'Threads'; +$labels['expand-all'] = 'Expand All'; +$labels['collapse-all'] = 'Collapse All'; +$labels['autoexpand_threads'] = 'Autoexpand threads'; +$labels['do_expand'] = 'All threads'; +$labels['dont_expand'] = 'None'; +$labels['expand_only_unread'] = 'Only with unread messages'; $labels['filter'] = 'Filter'; $labels['compact'] = 'Compact'; $labels['empty'] = 'Empty'; @@ -253,6 +261,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/ru_RU/labels.inc =================================================================== --- program/localization/ru_RU/labels.inc (revision 2976) +++ program/localization/ru_RU/labels.inc (working copy) @@ -45,6 +45,7 @@ $labels['reply-to'] = 'Ответить'; $labels['mailboxlist'] = 'Папки'; $labels['messagesfromto'] = 'Сообщения с $from по $to из $count'; +$labels['threadsfromto'] = 'Обсуждения с $from по $to из $count'; $labels['messagenrof'] = 'Сообщение $nr из $count'; $labels['moveto'] = 'Переместить в...'; $labels['download'] = 'Загрузить'; @@ -126,6 +127,13 @@ $labels['deleted'] = 'Удаленное'; $labels['invert'] = 'Инвертное'; $labels['filter'] = 'Фильтр'; +$labels['threads'] = 'Обсуждения'; +$labels['expand-all'] = 'Развернуть'; +$labels['collapse-all'] = 'Свернуть'; +$labels['autoexpand_threads'] = 'Разворачивать обсуждения'; +$labels['do_expand'] = 'Все обсуждения'; +$labels['dont_expand'] = 'Не разворачивать'; +$labels['expand_only_unread'] = 'Только с новыми сообщениями'; $labels['compact'] = 'Сжать'; $labels['empty'] = 'Опустошить'; $labels['purge'] = 'Очистить'; @@ -261,6 +269,7 @@ $labels['foldername'] = 'Имя папки'; $labels['subscribed'] = 'Подписан'; $labels['messagecount'] = 'Сообщения'; +$labels['threaded'] = 'Обсуждения'; $labels['create'] = 'Создать'; $labels['createfolder'] = 'Создать новую папку'; $labels['rename'] = 'Переименовать'; Index: program/localization/en_US/labels.inc =================================================================== --- program/localization/en_US/labels.inc (revision 2976) +++ 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,15 @@ $labels['invert'] = 'Invert'; $labels['filter'] = 'Filter'; +$labels['threads'] = 'Threads'; +$labels['expand-all'] = 'Expand All'; +$labels['collapse-all'] = 'Collapse All'; + +$labels['autoexpand_threads'] = 'Autoexpand threads'; +$labels['do_expand'] = 'All threads'; +$labels['dont_expand'] = 'None'; +$labels['expand_only_unread'] = 'Only with unread messages'; + $labels['compact'] = 'Compact'; $labels['empty'] = 'Empty'; $labels['purge'] = 'Purge'; @@ -317,6 +327,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 2976) +++ program/js/list.js (working copy) @@ -76,7 +76,7 @@ for(var r=0; r= p.depth - 1) + { + last_expanded_parent_depth = p.depth; + new_row.style.display = 'table-row'; + } + } + else + if (row && (! p || p.depth <= depth)) + break; + } + } + } + new_row = new_row.nextSibling; + } + + return false; +}, + + +collapse_all: function(row) +{ + var depth, new_row; + var r; + + if (row) + { + row.expanded = false; + depth = row.depth; + new_row = row.obj.nextSibling; + } + else + { + var tbody = this.list.tBodies[0]; + new_row = tbody.firstChild; + depth = 0; + } + + while (new_row) { + if (new_row.nodeType == 1) + { + var r = this.rows[new_row.uid]; + if (row && r.depth <= depth) + break; + if (row || 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; + } + + // Add nested messages to selection on collapse + if (row) + this.select_thread(row.uid, false); + + return false; +}, + +expand_all: function(row) +{ + var depth, new_row; + var r; + + if (row) + { + // Remove nested messages from selection on expand if only one thread is selected + if (this.get_single_selection() == row.uid) + this.select_thread(row.uid, true); + + row.expanded = true; + depth = row.depth; + new_row = row.obj.nextSibling; + } + else + { + var tbody = this.list.tBodies[0]; + new_row = tbody.firstChild; + depth = 0; + } + + while (new_row) + { + if (new_row.nodeType == 1) + { + var r = this.rows[new_row.uid]; + if (row && r.depth <= depth) + break; + 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; +}, + +expand_unread: function() +{ + var tbody = this.list.tBodies[0]; + new_row = tbody.firstChild; + var r; + var p; + + while (new_row) { + if (new_row.nodeType == 1) + { + r = this.rows[new_row.uid]; + p = this.rows[r.parent_uid]; + if ((r.has_children && r.unread_children > 0) || (!r.has_children && r.unread)) + { + 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'; + } + } + else if (p && p.unread_children > 0) + { + // Show all neighbours with the same parent at the same depth level (parent is expanded) + new_row.style.display = 'table-row'; + } + } + new_row = new_row.nextSibling; + } + return false; +}, + /** * get next/previous/last rows that are not hidden */ @@ -492,7 +727,7 @@ if (this.selection[n]==id) return true; - return false; + return false; }, @@ -513,7 +748,7 @@ if (!filter || (this.rows[n] && this.rows[n][filter] == true)) { this.last_selected = n; - this.highlight_row(n, true); + this.highlight_row(n, true, true); } else if (this.rows[n]) { @@ -543,7 +778,7 @@ var select_before = this.selection.join(','); for (var n in this.rows) - this.highlight_row(n, true); + this.highlight_row(n, true, true); // trigger event if selection changed if (this.selection.join(',') != select_before) @@ -590,9 +825,9 @@ /** * Getter for the selection array */ -get_selection: function() +get_selection: function(action) { - return this.selection; + return this.selection; }, @@ -603,15 +838,61 @@ { if (this.selection.length == 1) return this.selection[0]; + else if (this.selection.length > 1) + { // Return first element only if it is a threaded list and only one thread is in a selection + if (!this.rows[this.selection[0]].depth) + return null; + for (var n = 1; n < this.selection.length; n++) + if (this.rows[this.selection[n]] && this.rows[this.selection[n]].depth) + if (this.rows[this.selection[n]].depth <= this.rows[this.selection[0]].depth) + return null; + return this.selection[0]; + } else return null; }, +/** + * Helper function to expand selection to nested rows of a thread + */ +select_thread: function(id, unselect) +{ + var current_row = this.rows[id]; + var starting_depth; + if (current_row.has_children && !current_row.expanded) + { + // move to the next row and record the depth + id = current_row.obj.nextElementSibling.uid; + current_row = this.rows[id]; + starting_depth = current_row.depth; + do + { + if (unselect == true && this.in_selection(id)) // unselect row + { + var p = find_in_array(id, this.selection); + var a_pre = this.selection.slice(0, p); + var a_post = this.selection.slice(p+1, this.selection.length); + this.selection = a_pre.concat(a_post); + $(this.rows[id].obj).removeClass('selected').removeClass('unfocused'); + } + else if (unselect != true && !this.in_selection(id)) // select row + { + this.selection[this.selection.length] = id; + $(this.rows[id].obj).addClass('selected'); + } + if (!current_row.obj.nextElementSibling) + break; + id = current_row.obj.nextElementSibling.uid; + current_row = this.rows[id]; + } while (current_row.depth >= starting_depth); + } +}, + /** * Highlight/unhighlight a row */ -highlight_row: function(id, multiple) +highlight_row: function(id, multiple, skip_threads) { if (this.rows[id] && !multiple) { @@ -620,6 +901,8 @@ this.clear_selection(); this.selection[0] = id; $(this.rows[id].obj).addClass('selected'); + if (!skip_threads) + this.select_thread(id); } } else if (this.rows[id]) @@ -628,6 +911,8 @@ { this.selection[this.selection.length] = id; $(this.rows[id].obj).addClass('selected'); + if (!skip_threads) + this.select_thread(id); } else // unselect row { @@ -636,6 +921,8 @@ var a_post = this.selection.slice(p+1, this.selection.length); this.selection = a_pre.concat(a_post); $(this.rows[id].obj).removeClass('selected').removeClass('unfocused'); + if (!skip_threads) + this.select_thread(id, true); } } }, @@ -661,6 +948,16 @@ // Stop propagation so that the browser doesn't scroll rcube_event.cancel(e); return this.use_arrow_key(keyCode, mod_key); + case 61: + case 107: // Plus sign on a numeric keypad (fc11 + firefox 3.5.2) + case 109: + case 32: + // Stop propagation + rcube_event.cancel(e); + var ret = this.use_plusminus_key(keyCode, mod_key); + this.key_pressed = keyCode; + this.triggerEvent('keypress'); + return ret; default: this.shiftkey = e.shiftKey; this.key_pressed = keyCode; @@ -688,6 +985,10 @@ case 38: case 63233: case 63232: + case 61: + case 107: + case 109: + case 32: if (!rcube_event.get_modifier(e) && this.focused) return rcube_event.cancel(e); @@ -722,6 +1023,34 @@ /** + * 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 || keyCode == 107) + if (mod_key == CONTROL_KEY) + this.expand_all(last_selected_row); + else + this.expand(last_selected_row); + else + if (mod_key == CONTROL_KEY) + this.collapse_all(last_selected_row); + else + this.collapse(last_selected_row); + var expando = document.getElementById('rcmexpando' + last_selected_row.uid); + if (expando) + expando.className = last_selected_row.expanded?'expanded':'collapsed'; + + return false; +}, + + +/** * Try to scroll the list to make the specified row visible */ scrollto: function(id) @@ -766,17 +1095,17 @@ 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 (((node = obj.childNodes[i].childNodes[obj.childNodes[i].childElementCount - 1]) || + (node = obj.childNodes[i].firstChild) && (node.nodeType==3 || node.nodeName=='A')) && + (this.subject_col < 0 || (this.subject_col >= 0 && this.subject_col == c))) { if (n == 0) { if (node.nodeType == 3) Index: program/js/app.js =================================================================== --- program/js/app.js (revision 2976) +++ program/js/app.js (working copy) @@ -179,8 +179,22 @@ this.gui_objects.mailcontframe.onmousedown = function(e){ return p.click_on_list(e); }; else this.message_list.focus(); + + switch (this.env.autoexpand_threads) { + case 2: + this.message_list.expand_unread(); + break; + case 1: + this.message_list.expand_all(); + break; + case 0: + default: + break; + } + this.message_list.expand(null); + this.save_expand_state(); } - + if (this.env.coltypes) this.set_message_coltypes(this.env.coltypes); @@ -244,8 +258,13 @@ } 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.env.threaded_display) { + this.set_threaded(true); + } else { + this.set_threaded(false); + } if (this.purge_mailbox_test()) this.enable_command('purge', true); @@ -333,7 +352,7 @@ this.enable_command('save', 'delete', 'edit', true); } else if (this.env.action=='folders') - this.enable_command('subscribe', 'unsubscribe', 'create-folder', 'rename-folder', 'delete-folder', true); + this.enable_command('subscribe', 'unsubscribe', 'create-folder', 'rename-folder', 'delete-folder', 'enable-threading', 'disable-threading', true); if (this.gui_objects.identitieslist) { @@ -424,6 +443,11 @@ 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; + row.parent_uid = this.env.messages[uid].parent_uid; } // set eventhandler to message icon @@ -440,7 +464,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 @@ -453,6 +477,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 @@ -613,13 +646,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); @@ -742,7 +794,22 @@ case 'delete': // mail task if (this.task=='mail') + { this.delete_messages(); + if (this.env.threaded_display) + { + // It is very hard to re-thread message list if some messages were removed from + // the middle of a thread (we need to fully implement RFC5256 algorythm, because + // we are a "disconnected client" if we re-threading without a servers help) + // Reload message list + var sort_param; + if (this.env.sort_col && this.env.sort_order) + { + sort_param = this.env.sort_col +'_'+ this.env.sort_order + } + this.list_mailbox(this.env.mailbox, this.env.current_page, sort_param); + } + } // addressbook task else if (this.task=='addressbook') this.delete_contacts(); @@ -756,7 +823,19 @@ case 'move': case 'moveto': if (this.task == 'mail') + { this.move_messages(props); + if (this.env.threaded_display) + { + // The same as for 'delete' + var sort_param; + if (this.env.sort_col && this.env.sort_order) + { + sort_param = this.env.sort_col +'_'+ this.env.sort_order + } + this.list_mailbox(this.env.mailbox, this.env.current_page, sort_param); + } + } else if (this.task == 'addressbook' && this.drag_active) this.copy_contact(null, props); break; @@ -846,6 +925,16 @@ this.message_list.clear_selection(); break; + case 'expand-all': + this.message_list.expand_all(); + this.save_expand_state(); + break; + + case 'collapse-all': + this.message_list.collapse_all(); + this.save_expand_state(); + break; + case 'nextmessage': if (this.env.next_uid) this.show_message(this.env.next_uid, false, this.env.action=='preview'); @@ -1107,7 +1196,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; @@ -1441,7 +1538,7 @@ if (this.preview_timer) clearTimeout(this.preview_timer); - var selected = list.selection.length==1; + var selected = list.get_single_selection() != null; // Hide certain command buttons when Drafts folder is selected if (this.env.mailbox == this.env.drafts_mailbox) @@ -1483,6 +1580,8 @@ this.command('delete'); else if (list.key_pressed == list.BACKSPACE_KEY) this.command('delete'); + else if (list.key_pressed == 32 || list.key_pressed == 107 || list.key_pressed == 109 || list.key_pressed == 61) + this.save_expand_state(); else list.shiftkey = false; }; @@ -1545,6 +1644,7 @@ if (action == 'preview' && this.message_list && this.message_list.rows[id] && this.message_list.rows[id].unread) { this.set_message(id, 'unread', false); + this.update_parents(id, 'read'); if (this.env.unread_counts[this.env.mailbox]) { this.env.unread_counts[this.env.mailbox] -= 1; @@ -1730,6 +1830,24 @@ || this.env.mailbox.match('^' + RegExp.escape(this.env.junk_mailbox) + RegExp.escape(this.env.delimiter)))); }; + // update parents in a thread + this.update_parents = function(uid, flag) + { + var r = this.message_list.rows[uid]; + if (r.parent_uid) { + var p = this.message_list.rows[r.parent_uid]; + if (flag == 'read' && p.unread_children > 0) { + p.unread_children--; + } else if (flag == 'unread') { + p.unread_children++; + } else { + return; + } + this.set_message_icon(r.parent_uid); + this.update_parents(r.parent_uid, flag); + } + }; + // set message icon this.set_message_icon = function(uid) { @@ -1738,8 +1856,10 @@ if (!rows[uid]) return false; - - if (rows[uid].deleted && this.env.deletedicon) + if (!rows[uid].unread && rows[uid].unread_children > 0 && this.env.unreadchildrenicon) { + icn_src = this.env.unreadchildrenicon; + } + else if (rows[uid].deleted && this.env.deletedicon) icn_src = this.env.deletedicon; else if (rows[uid].replied && this.env.repliedicon) { @@ -1940,7 +2060,7 @@ { var a_uids = new Array(); var r_uids = new Array(); - var selection = this.message_list ? this.message_list.get_selection() : new Array(); + var selection = this.message_list ? this.message_list.get_selection('mark') : new Array(); if (uid) a_uids[0] = uid; @@ -2000,6 +2120,9 @@ this.set_message(a_uids[i], 'unread', (flag=='unread' ? true : false)); this.http_post('mark', '_uid='+a_uids.join(',')+'&_flag='+flag); + + for (var i=0; i 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) { @@ -3779,29 +3954,45 @@ icon = this.env.forwardedicon; else if(flags.unread && this.env.unreadicon) icon = this.env.unreadicon; + var tree = ''; + if (depth > 0) + { + // XXX: This assumes that div width is hardcoded to 15px, + // Chris did it a bit differently in an original patch, he was adding so much divs as depth is + // I replaced logic in list.js:drag_mouse_move() so subject text is picked defferently, so + // either method of could be used (that was only the one problem I noted with these added divs). + // The same is true for an offline list (program/steps/mail/func.inc:rcmail_message_list()). + // Bubble + var width = (depth - 1) * 15; + tree += '
 
'; + tree += flags.has_children?'
 
':'
 
'; + if (depth > 1) + row.style.display = 'none'; + } - // add icon col - var col = document.createElement('td'); - col.className = 'icon'; - col.innerHTML = icon ? '' : ''; - row.appendChild(col); - + 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); } @@ -3816,6 +4007,7 @@ } }; + // messages list handling in background (for performance) this.offline_message_list = function(flag) { @@ -3823,6 +4015,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) { @@ -3913,6 +4111,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() { @@ -4106,7 +4312,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; @@ -4116,7 +4323,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') @@ -4249,7 +4456,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/save_prefs.inc =================================================================== --- program/steps/settings/save_prefs.inc (revision 2976) +++ program/steps/settings/save_prefs.inc (working copy) @@ -41,6 +41,7 @@ $a_user_prefs = array( 'focus_on_new_message' => isset($_POST['_focus_on_new_message']) ? TRUE : FALSE, 'preview_pane' => isset($_POST['_preview_pane']) ? TRUE : FALSE, + 'autoexpand_threads' => isset($_POST['_autoexpand_threads']) ? intval($_POST['_autoexpand_threads']) : 0, 'mdn_requests' => isset($_POST['_mdn_requests']) ? intval($_POST['_mdn_requests']) : 0, 'keep_alive' => isset($_POST['_keep_alive']) ? intval($_POST['_keep_alive'])*60 : $CONFIG['keep_alive'], 'check_all_folders' => isset($_POST['_check_all_folders']) ? TRUE : FALSE, Index: program/steps/settings/edit_prefs.inc =================================================================== --- program/steps/settings/edit_prefs.inc (revision 2976) +++ program/steps/settings/edit_prefs.inc (working copy) @@ -201,6 +201,21 @@ ); } +// $RCMAIL->imap_init(true); + + if (!isset($no_override['autoexpand_threads'])) { + $field_id = 'rcmfd_autoexpand_threads'; + $select_autoexpand_threads = new html_select(array('name' => '_autoexpand_threads', 'id' => $field_id)); + $select_autoexpand_threads->add(rcube_label('dont_expand'), 0); + $select_autoexpand_threads->add(rcube_label('do_expand'), 1); + $select_autoexpand_threads->add(rcube_label('expand_only_unread'), 2); + + $blocks['main']['options']['autoexpand_threads'] = array( + 'title' => html::label($field_id, Q(rcube_label('autoexpand_threads'))), + 'content' => $select_autoexpand_threads->show($config['autoexpand_threads']), + ); + } + if (!isset($no_override['mdn_requests'])) { $field_id = 'rcmfd_mdn_requests'; $select_mdn_requests = new html_select(array('name' => '_mdn_requests', 'id' => $field_id)); Index: program/steps/settings/manage_folders.inc =================================================================== --- program/steps/settings/manage_folders.inc (revision 2976) +++ 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, 'UTF7-IMAP')) + $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, 'UTF7-IMAP')) + $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 2976) +++ 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 2976) +++ 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); // update mailboxlist @@ -69,6 +69,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)) @@ -82,6 +83,23 @@ else $OUTPUT->show_message('nomessagesfound', 'notice'); +// deal with threaded view +if (rcmail::get_instance()->imap->threading) { + switch ($RCMAIL->config->get('autoexpand_threads')) { + case 2: + $OUTPUT->command('message_list.expand_unread'); + break; + case 1: + $OUTPUT->command('message_list.expand_all'); + break; + case 0: + default: + break; + } + $OUTPUT->command('expand_threads'); + $OUTPUT->command('save_expand_state'); +} + // send response $OUTPUT->send(); Index: program/steps/mail/check_recent.inc =================================================================== --- program/steps/mail/check_recent.inc (revision 2976) +++ 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); @@ -55,18 +55,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); + } } } else { Index: program/steps/mail/move_del.inc =================================================================== --- program/steps/mail/move_del.inc (revision 2976) +++ program/steps/mail/move_del.inc (working copy) @@ -24,7 +24,7 @@ return; // 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); @@ -122,6 +122,11 @@ $sort_order = isset($_SESSION['sort_order']) ? $_SESSION['sort_order'] : $CONFIG['message_sort_order']; $a_headers = $IMAP->list_headers($mbox, NULL, $sort_col, $sort_order, $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 2976) +++ program/steps/mail/func.inc (working copy) @@ -42,6 +42,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']))); @@ -79,13 +81,14 @@ $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 $OUTPUT->set_env('mailbox', $mbox_name); $OUTPUT->set_env('quota', $IMAP->get_capability('quota')); $OUTPUT->set_env('delimiter', $IMAP->get_hierarchy_delimiter()); + $OUTPUT->set_env('threaded_display', rcmail::get_instance()->imap->threading); if ($CONFIG['flag_for_deletion']) $OUTPUT->set_env('flag_for_deletion', true); @@ -119,7 +122,6 @@ $skin_path = $CONFIG['skin_path']; $image_tag = '%s'; - // check to see if we have some settings for sorting $sort_col = $_SESSION['sort_col']; $sort_order = $_SESSION['sort_order']; @@ -161,7 +163,6 @@ // add col definition $out .= ''; - $out .= ''; foreach ($a_show_cols as $col) $out .= ($col!='attachment') ? sprintf('', $col) : ''; @@ -169,7 +170,7 @@ $out .= "\n"; // add table title - $out .= "\n \n"; + $out .= "\n"; $javascript = ''; foreach ($a_show_cols as $col) @@ -265,9 +266,16 @@ $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['parent_uid'] = $header->parent_uid; + $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) { @@ -292,18 +300,34 @@ 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) + { + // XXX: This assumes that div width is hardcoded to 15px, + // Chris did it a bit differently in an original patch, he was adding so much divs as depth is + // I replaced logic in list.js:drag_mouse_move() so subject text is picked defferently, so + // either method of could be used (that was only the one problem I noted with these added divs). + // The same is true for an online list (program/js/app.js:add_message_row()). + // Bubble + $width = ($depth - 1) * 15; + $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') @@ -325,6 +349,10 @@ else $cont = Q($header->$col); + if ($first) { + $first = false; + $cont = $tree . $cont; + } if ($col!='attachment') $out .= '' . $cont . "\n"; else @@ -340,11 +368,12 @@ // 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'); $OUTPUT->add_gui_object('messagelist', $attrib['id']); + $OUTPUT->set_env('autoexpand_threads', $CONFIG['autoexpand_threads']); $OUTPUT->set_env('messagecount', $message_count); $OUTPUT->set_env('current_page', $IMAP->list_page); $OUTPUT->set_env('pagecount', ceil($message_count/$IMAP->page_size)); @@ -369,6 +398,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); @@ -406,6 +437,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) { @@ -417,12 +454,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) { @@ -446,6 +477,13 @@ $a_msg_cols[$col] = $cont; } + if ($header->depth) + $a_msg_cols["depth"] = $header->depth; + if ($header->parent_uid) + $a_msg_cols["parent_uid"] = $header->parent_uid; + if ($header->has_children) + $a_msg_flags["has_children"] = $header->has_children; + $a_msg_cols["unread_children"] = $header->unread_children; if ($header->deleted) $a_msg_flags['deleted'] = 1; if (!$header->seen) @@ -467,6 +505,8 @@ if ($browser->ie && $replace) $OUTPUT->command('offline_message_list', false); + + $OUTPUT->command('expand_threads'); } @@ -578,19 +618,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, 'ALL')))); // Only messages, no threads here } if ($page===NULL) $page = $IMAP->list_page; $start_msg = ($page-1) * $IMAP->page_size + 1; - $max = $count!==NULL ? $count : $IMAP->messagecount(); + $max = $count!==NULL ? $count : $IMAP->messagecount(NULL, 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 2976) +++ 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']))