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