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