summaryrefslogtreecommitdiff
path: root/program/lib/Roundcube
diff options
context:
space:
mode:
Diffstat (limited to 'program/lib/Roundcube')
-rw-r--r--program/lib/Roundcube/html.php2
-rw-r--r--program/lib/Roundcube/rcube.php84
-rw-r--r--program/lib/Roundcube/rcube_charset.php11
-rw-r--r--program/lib/Roundcube/rcube_csv2vcard.php2
-rw-r--r--program/lib/Roundcube/rcube_db.php7
-rw-r--r--program/lib/Roundcube/rcube_html2text.php3
-rw-r--r--program/lib/Roundcube/rcube_imap.php350
-rw-r--r--program/lib/Roundcube/rcube_imap_cache.php5
-rw-r--r--program/lib/Roundcube/rcube_imap_generic.php38
-rw-r--r--program/lib/Roundcube/rcube_imap_search.php234
-rw-r--r--program/lib/Roundcube/rcube_ldap.php13
-rw-r--r--program/lib/Roundcube/rcube_message.php5
-rw-r--r--program/lib/Roundcube/rcube_message_header.php9
-rw-r--r--program/lib/Roundcube/rcube_plugin_api.php6
-rw-r--r--program/lib/Roundcube/rcube_result_index.php2
-rw-r--r--program/lib/Roundcube/rcube_result_multifolder.php332
-rw-r--r--program/lib/Roundcube/rcube_result_thread.php2
-rw-r--r--program/lib/Roundcube/rcube_spellcheck_googie.php4
-rw-r--r--program/lib/Roundcube/rcube_storage.php80
-rw-r--r--program/lib/Roundcube/rcube_vcard.php5
-rw-r--r--program/lib/Roundcube/rcube_washtml.php107
21 files changed, 1124 insertions, 177 deletions
diff --git a/program/lib/Roundcube/html.php b/program/lib/Roundcube/html.php
index 64324dd8e..f47ef299a 100644
--- a/program/lib/Roundcube/html.php
+++ b/program/lib/Roundcube/html.php
@@ -285,7 +285,7 @@ class html
// ignore not allowed attributes
if (!empty($allowed)) {
- $is_data_attr = substr_compare($key, 'data-', 0, 5) === 0;
+ $is_data_attr = @substr_compare($key, 'data-', 0, 5) === 0;
if (!isset($allowed_f[$key]) && (!$is_data_attr || !isset($allowed_f['data-*']))) {
continue;
}
diff --git a/program/lib/Roundcube/rcube.php b/program/lib/Roundcube/rcube.php
index 69d95f023..707929951 100644
--- a/program/lib/Roundcube/rcube.php
+++ b/program/lib/Roundcube/rcube.php
@@ -355,29 +355,6 @@ class rcube
// for backward compat. (deprecated, will be removed)
$this->imap = $this->storage;
- // enable caching of mail data
- $storage_cache = $this->config->get("{$driver}_cache");
- $messages_cache = $this->config->get('messages_cache');
- // for backward compatybility
- if ($storage_cache === null && $messages_cache === null && $this->config->get('enable_caching')) {
- $storage_cache = 'db';
- $messages_cache = true;
- }
-
- if ($storage_cache) {
- $this->storage->set_caching($storage_cache);
- }
- if ($messages_cache) {
- $this->storage->set_messages_caching(true);
- }
-
- // set pagesize from config
- $pagesize = $this->config->get('mail_pagesize');
- if (!$pagesize) {
- $pagesize = $this->config->get('pagesize', 50);
- }
- $this->storage->set_pagesize($pagesize);
-
// set class options
$options = array(
'auth_type' => $this->config->get("{$driver}_auth_type", 'check'),
@@ -412,22 +389,65 @@ class rcube
/**
* Set storage parameters.
- * This must be done AFTER connecting to the server!
*/
protected function set_storage_prop()
{
$storage = $this->get_storage();
+ // set pagesize from config
+ $pagesize = $this->config->get('mail_pagesize');
+ if (!$pagesize) {
+ $pagesize = $this->config->get('pagesize', 50);
+ }
+
+ $storage->set_pagesize($pagesize);
$storage->set_charset($this->config->get('default_charset', RCUBE_CHARSET));
- if ($default_folders = $this->config->get('default_folders')) {
- $storage->set_default_folders($default_folders);
+ // enable caching of mail data
+ $driver = $this->config->get('storage_driver', 'imap');
+ $storage_cache = $this->config->get("{$driver}_cache");
+ $messages_cache = $this->config->get('messages_cache');
+ // for backward compatybility
+ if ($storage_cache === null && $messages_cache === null && $this->config->get('enable_caching')) {
+ $storage_cache = 'db';
+ $messages_cache = true;
+ }
+
+ if ($storage_cache) {
+ $storage->set_caching($storage_cache);
}
- if (isset($_SESSION['mbox'])) {
- $storage->set_folder($_SESSION['mbox']);
+ if ($messages_cache) {
+ $storage->set_messages_caching(true);
}
- if (isset($_SESSION['page'])) {
- $storage->set_page($_SESSION['page']);
+ }
+
+
+ /**
+ * Set special folders type association.
+ * This must be done AFTER connecting to the server!
+ */
+ protected function set_special_folders()
+ {
+ $storage = $this->get_storage();
+ $folders = $storage->get_special_folders(true);
+ $prefs = array();
+
+ // check SPECIAL-USE flags on IMAP folders
+ foreach ($folders as $type => $folder) {
+ $idx = $type . '_mbox';
+ if ($folder !== $this->config->get($idx)) {
+ $prefs[$idx] = $folder;
+ }
+ }
+
+ // Some special folders differ, update user preferences
+ if (!empty($prefs) && $this->user) {
+ $this->user->save_prefs($prefs);
+ }
+
+ // create default folders (on login)
+ if ($this->config->get('create_default_folders')) {
+ $storage->create_default_folders();
}
}
@@ -1187,8 +1207,8 @@ class rcube
}
// installer
- if (class_exists('rcube_install', false)) {
- $rci = rcube_install::get_instance();
+ if (class_exists('rcmail_install', false)) {
+ $rci = rcmail_install::get_instance();
$rci->raise_error($arg);
return;
}
diff --git a/program/lib/Roundcube/rcube_charset.php b/program/lib/Roundcube/rcube_charset.php
index 8612e7fca..ffec67376 100644
--- a/program/lib/Roundcube/rcube_charset.php
+++ b/program/lib/Roundcube/rcube_charset.php
@@ -759,7 +759,12 @@ class rcube_charset
// iconv/mbstring are much faster (especially with long strings)
if (function_exists('mb_convert_encoding')) {
- if (($res = mb_convert_encoding($input, 'UTF-8', 'UTF-8')) !== false) {
+ $msch = mb_substitute_character('none');
+ mb_substitute_character('none');
+ $res = mb_convert_encoding($input, 'UTF-8', 'UTF-8');
+ mb_substitute_character($msch);
+
+ if ($res !== false) {
return $res;
}
}
@@ -795,8 +800,8 @@ class rcube_charset
}
$seq = '';
$out .= $chr;
- // first (or second) byte of multibyte sequence
}
+ // first (or second) byte of multibyte sequence
else if ($ord >= 0xC0) {
if (strlen($seq) > 1) {
$out .= preg_match($regexp, $seq) ? $seq : '';
@@ -806,8 +811,8 @@ class rcube_charset
$seq = '';
}
$seq .= $chr;
- // next byte of multibyte sequence
}
+ // next byte of multibyte sequence
else if ($seq) {
$seq .= $chr;
}
diff --git a/program/lib/Roundcube/rcube_csv2vcard.php b/program/lib/Roundcube/rcube_csv2vcard.php
index aa385dce4..06bc387d5 100644
--- a/program/lib/Roundcube/rcube_csv2vcard.php
+++ b/program/lib/Roundcube/rcube_csv2vcard.php
@@ -56,7 +56,7 @@ class rcube_csv2vcard
//'email_2_type' => '',
//'email_3_address' => '', //@TODO
//'email_3_type' => '',
- 'email_address' => 'email:main',
+ 'email_address' => 'email:pref',
//'email_type' => '',
'first_name' => 'firstname',
'gender' => 'gender',
diff --git a/program/lib/Roundcube/rcube_db.php b/program/lib/Roundcube/rcube_db.php
index 2828f26ee..a2271fd6d 100644
--- a/program/lib/Roundcube/rcube_db.php
+++ b/program/lib/Roundcube/rcube_db.php
@@ -609,7 +609,8 @@ class rcube_db
{
// get tables if not cached
if ($this->tables === null) {
- $q = $this->query('SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES ORDER BY TABLE_NAME');
+ $q = $this->query('SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = ? ORDER BY TABLE_NAME',
+ array($this->db_dsnw_array['database']));
if ($q) {
$this->tables = $q->fetchAll(PDO::FETCH_COLUMN, 0);
@@ -631,8 +632,8 @@ class rcube_db
*/
public function list_cols($table)
{
- $q = $this->query('SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = ?',
- array($table));
+ $q = $this->query('SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = ? AND TABLE_SCHEMA = ?',
+ array($table, $this->db_dsnw_array['database']));
if ($q) {
return $q->fetchAll(PDO::FETCH_COLUMN, 0);
diff --git a/program/lib/Roundcube/rcube_html2text.php b/program/lib/Roundcube/rcube_html2text.php
index 3b4508da9..8628371d7 100644
--- a/program/lib/Roundcube/rcube_html2text.php
+++ b/program/lib/Roundcube/rcube_html2text.php
@@ -473,6 +473,9 @@ class rcube_html2text
// Replace known html entities
$text = html_entity_decode($text, ENT_QUOTES, $this->charset);
+ // Replace unicode nbsp to regular spaces
+ $text = preg_replace('/\xC2\xA0/', ' ', $text);
+
// Remove unknown/unhandled entities (this cannot be done in search-and-replace block)
$text = preg_replace('/&([a-zA-Z0-9]{2,6}|#[0-9]{2,4});/', '', $text);
diff --git a/program/lib/Roundcube/rcube_imap.php b/program/lib/Roundcube/rcube_imap.php
index 432227091..4204354b3 100644
--- a/program/lib/Roundcube/rcube_imap.php
+++ b/program/lib/Roundcube/rcube_imap.php
@@ -332,6 +332,10 @@ class rcube_imap extends rcube_storage
$this->search_sort_field = $set[3];
$this->search_sorted = $set[4];
$this->search_threads = is_a($this->search_set, 'rcube_result_thread');
+
+ if (is_a($this->search_set, 'rcube_result_multifolder')) {
+ $this->set_threading(false);
+ }
}
@@ -945,6 +949,75 @@ class rcube_imap extends rcube_storage
return array();
}
+ // gather messages from a multi-folder search
+ if ($this->search_set->multi) {
+ $page_size = $this->page_size;
+ $sort_field = $this->sort_field;
+ $search_set = $this->search_set;
+
+ // prepare paging
+ $cnt = $search_set->count();
+ $from = ($page-1) * $page_size;
+ $to = $from + $page_size;
+ $slice_length = min($page_size, $cnt - $from);
+
+ // fetch resultset headers, sort and slice them
+ if (!empty($sort_field)) {
+ $this->sort_field = null;
+ $this->page_size = 1000; // fetch up to 1000 matching messages per folder
+ $this->threading = false;
+
+ $a_msg_headers = array();
+ foreach ($search_set->sets as $resultset) {
+ if (!$resultset->is_empty()) {
+ $this->search_set = $resultset;
+ $this->search_threads = $resultset instanceof rcube_result_thread;
+ $a_msg_headers = array_merge($a_msg_headers, $this->list_search_messages($resultset->get_parameters('MAILBOX'), 1));
+ }
+ }
+
+ // sort headers
+ if (!empty($a_msg_headers)) {
+ $a_msg_headers = $this->conn->sortHeaders($a_msg_headers, $sort_field, $this->sort_order);
+ }
+
+ // store (sorted) message index
+ $search_set->set_message_index($a_msg_headers, $sort_field, $this->sort_order);
+
+ // only return the requested part of the set
+ $a_msg_headers = array_slice(array_values($a_msg_headers), $from, $slice_length);
+ }
+ else {
+ if ($this->sort_order != $search_set->get_parameters('ORDER')) {
+ $search_set->revert();
+ }
+
+ // slice resultset first...
+ $fetch = array();
+ foreach (array_slice($search_set->get(), $from, $slice_length) as $msg_id) {
+ list($uid, $folder) = explode('-', $msg_id, 2);
+ $fetch[$folder][] = $uid;
+ }
+
+ // ... and fetch the requested set of headers
+ $a_msg_headers = array();
+ foreach ($fetch as $folder => $a_index) {
+ $a_msg_headers = array_merge($a_msg_headers, array_values($this->fetch_headers($folder, $a_index)));
+ }
+ }
+
+ if ($slice) {
+ $a_msg_headers = array_slice($a_msg_headers, -$slice, $slice);
+ }
+
+ // restore members
+ $this->sort_field = $sort_field;
+ $this->page_size = $page_size;
+ $this->search_set = $search_set;
+
+ return $a_msg_headers;
+ }
+
// use saved messages from searching
if ($this->threading) {
return $this->list_search_thread_messages($folder, $page, $slice);
@@ -1111,6 +1184,7 @@ class rcube_imap extends rcube_storage
}
foreach ($headers as $h) {
+ $h->folder = $folder;
$a_msg_headers[$h->uid] = $h;
}
@@ -1234,8 +1308,13 @@ class rcube_imap extends rcube_storage
return new rcube_result_index($folder, '* SORT');
}
+ if ($this->search_set instanceof rcube_result_multifolder) {
+ $index = $this->search_set;
+ $index->folder = $folder;
+ // TODO: handle changed sorting
+ }
// search result is an index with the same sorting?
- if (($this->search_set instanceof rcube_result_index)
+ else if (($this->search_set instanceof rcube_result_index)
&& ((!$this->sort_field && !$this->search_sorted) ||
($this->search_sorted && $this->search_sort_field == $this->sort_field))
) {
@@ -1413,7 +1492,7 @@ class rcube_imap extends rcube_storage
* @param string $str Search criteria
* @param string $charset Search charset
* @param string $sort_field Header field to sort by
- *
+ * @return rcube_result_index Search result object
* @todo: Search criteria should be provided in non-IMAP format, eg. array
*/
public function search($folder='', $str='ALL', $charset=NULL, $sort_field=NULL)
@@ -1422,14 +1501,48 @@ class rcube_imap extends rcube_storage
$str = 'ALL';
}
- if (!strlen($folder)) {
- $folder = $this->folder;
- }
+ // multi-folder search
+ if (is_array($folder) && count($folder) > 1 && $str != 'ALL') {
+ new rcube_result_index; // trigger autoloader and make these classes available for threaded context
+ new rcube_result_thread;
- $results = $this->search_index($folder, $str, $charset, $sort_field);
+ // connect IMAP to have all the required classes and settings loaded
+ $this->check_connection();
+
+ // disable threading
+ $this->threading = false;
+
+ $searcher = new rcube_imap_search($this->options, $this->conn);
+
+ // set limit to not exceed the client's request timeout
+ $searcher->set_timelimit(60);
+
+ // continue existing incomplete search
+ if (!empty($this->search_set) && $this->search_set->incomplete && $str == $this->search_string) {
+ $searcher->set_results($this->search_set);
+ }
+
+ // execute the search
+ $results = $searcher->exec(
+ $folder,
+ $str,
+ $charset ? $charset : $this->default_charset,
+ $sort_field && $this->get_capability('SORT') ? $sort_field : null,
+ $this->threading
+ );
+ }
+ else {
+ $folder = is_array($folder) ? $folder[0] : $folder;
+ if (!strlen($folder)) {
+ $folder = $this->folder;
+ }
+ $results = $this->search_index($folder, $str, $charset, $sort_field);
+ }
$this->set_search_set(array($str, $results, $charset, $sort_field,
$this->threading || $this->search_sorted ? true : false));
+
+ return $results;
}
@@ -1443,20 +1556,27 @@ class rcube_imap extends rcube_storage
*/
public function search_once($folder = null, $str = 'ALL')
{
+ if (!$this->check_connection()) {
+ return new rcube_result_index();
+ }
+
if (!$str) {
$str = 'ALL';
}
- if (!strlen($folder)) {
- $folder = $this->folder;
+ // multi-folder search
+ if (is_array($folder) && count($folder) > 1) {
+ $searcher = new rcube_imap_search($this->options, $this->conn);
+ $index = $searcher->exec($folder, $str, $this->default_charset);
}
-
- if (!$this->check_connection()) {
- return new rcube_result_index();
+ else {
+ $folder = is_array($folder) ? $folder[0] : $folder;
+ if (!strlen($folder)) {
+ $folder = $this->folder;
+ }
+ $index = $this->conn->search($folder, $str, true);
}
- $index = $this->conn->search($folder, $str, true);
-
return $index;
}
@@ -1500,7 +1620,7 @@ class rcube_imap extends rcube_storage
// but I've seen that Courier doesn't support UTF-8)
if ($threads->is_error() && $charset && $charset != 'US-ASCII') {
$threads = $this->conn->thread($folder, $this->threading,
- $this->convert_criteria($criteria, $charset), true, 'US-ASCII');
+ self::convert_criteria($criteria, $charset), true, 'US-ASCII');
}
return $threads;
@@ -1514,7 +1634,7 @@ class rcube_imap extends rcube_storage
// but I've seen Courier with disabled UTF-8 support)
if ($messages->is_error() && $charset && $charset != 'US-ASCII') {
$messages = $this->conn->sort($folder, $sort_field,
- $this->convert_criteria($criteria, $charset), true, 'US-ASCII');
+ self::convert_criteria($criteria, $charset), true, 'US-ASCII');
}
if (!$messages->is_error()) {
@@ -1529,7 +1649,7 @@ class rcube_imap extends rcube_storage
// Error, try with US-ASCII (some servers may support only US-ASCII)
if ($messages->is_error() && $charset && $charset != 'US-ASCII') {
$messages = $this->conn->search($folder,
- $this->convert_criteria($criteria, $charset), true);
+ self::convert_criteria($criteria, $charset), true);
}
$this->search_sorted = false;
@@ -1547,7 +1667,7 @@ class rcube_imap extends rcube_storage
*
* @return string Search string
*/
- protected function convert_criteria($str, $charset, $dest_charset='US-ASCII')
+ public static function convert_criteria($str, $charset, $dest_charset='US-ASCII')
{
// convert strings to US_ASCII
if (preg_match_all('/\{([0-9]+)\}\r\n/', $str, $matches, PREG_OFFSET_CAPTURE)) {
@@ -1583,12 +1703,30 @@ class rcube_imap extends rcube_storage
public function refresh_search()
{
if (!empty($this->search_string)) {
- $this->search('', $this->search_string, $this->search_charset, $this->search_sort_field);
+ $this->search(
+ is_object($this->search_set) ? $this->search_set->get_parameters('MAILBOX') : '',
+ $this->search_string,
+ $this->search_charset,
+ $this->search_sort_field
+ );
}
return $this->get_search_set();
}
+ /**
+ * Flag certain result subsets as 'incomplete'.
+ * For subsequent refresh_search() calls to only refresh the updated parts.
+ */
+ protected function set_search_dirty($folder)
+ {
+ if ($this->search_set && is_a($this->search_set, 'rcube_result_multifolder')) {
+ if ($subset = $this->search_set->get_set($folder)) {
+ $subset->incomplete = $this->search_set->incomplete = true;
+ }
+ }
+ }
+
/**
* Return message headers object of a specific message
@@ -1601,6 +1739,11 @@ class rcube_imap extends rcube_storage
*/
public function get_message_headers($uid, $folder = null, $force = false)
{
+ // decode combined UID-folder identifier
+ if (preg_match('/^\d+-.+/', $uid)) {
+ list($uid, $folder) = explode('-', $uid, 2);
+ }
+
if (!strlen($folder)) {
$folder = $this->folder;
}
@@ -1615,6 +1758,9 @@ class rcube_imap extends rcube_storage
else {
$headers = $this->conn->fetchHeader(
$folder, $uid, true, true, $this->get_fetch_headers());
+
+ if (is_object($headers))
+ $headers->folder = $folder;
}
return $headers;
@@ -1636,6 +1782,11 @@ class rcube_imap extends rcube_storage
$folder = $this->folder;
}
+ // decode combined UID-folder identifier
+ if (preg_match('/^\d+-.+/', $uid)) {
+ list($uid, $folder) = explode('-', $uid, 2);
+ }
+
// Check internal cache
if (!empty($this->icache['message'])) {
if (($headers = $this->icache['message']) && $headers->uid == $uid) {
@@ -2282,6 +2433,8 @@ class rcube_imap extends rcube_storage
$this->clear_message_cache($folder, $all_mode ? null : explode(',', $uids));
}
}
+
+ $this->set_search_dirty($folder);
}
return $result;
@@ -2329,6 +2482,17 @@ class rcube_imap extends rcube_storage
if ($saved) {
// increase messagecount of the target folder
$this->set_messagecount($folder, 'ALL', 1);
+
+ rcube::get_instance()->plugins->exec_hook('message_saved', array(
+ 'folder' => $folder,
+ 'message' => $message,
+ 'headers' => $headers,
+ 'is_file' => $is_file,
+ 'flags' => $flags,
+ 'date' => $date,
+ 'binary' => $binary,
+ 'result' => $saved,
+ ));
}
return $saved;
@@ -2365,19 +2529,7 @@ class rcube_imap extends rcube_storage
return false;
}
- // make sure folder exists
- if ($to_mbox != 'INBOX' && !$this->folder_exists($to_mbox)) {
- if (in_array($to_mbox, $this->default_folders)) {
- if (!$this->create_folder($to_mbox, true)) {
- return false;
- }
- }
- else {
- return false;
- }
- }
-
- $config = rcube::get_instance()->config;
+ $config = rcube::get_instance()->config;
$to_trash = $to_mbox == $config->get('trash_mbox');
// flag messages as read before moving them
@@ -2392,6 +2544,9 @@ class rcube_imap extends rcube_storage
if ($moved) {
$this->clear_messagecount($from_mbox);
$this->clear_messagecount($to_mbox);
+
+ $this->set_search_dirty($from_mbox);
+ $this->set_search_dirty($to_mbox);
}
// moving failed
else if ($to_trash && $config->get('delete_always', false)) {
@@ -2408,8 +2563,8 @@ class rcube_imap extends rcube_storage
if ($this->search_threads || $all_mode) {
$this->refresh_search();
}
- else {
- $this->search_set->filter(explode(',', $uids));
+ else if (!$this->search_set->incomplete) {
+ $this->search_set->filter(explode(',', $uids), $this->folder);
}
}
@@ -2448,18 +2603,6 @@ class rcube_imap extends rcube_storage
return false;
}
- // make sure folder exists
- if ($to_mbox != 'INBOX' && !$this->folder_exists($to_mbox)) {
- if (in_array($to_mbox, $this->default_folders)) {
- if (!$this->create_folder($to_mbox, true)) {
- return false;
- }
- }
- else {
- return false;
- }
- }
-
// copy messages
$copied = $this->conn->copy($uids, $from_mbox, $to_mbox);
@@ -2508,13 +2651,15 @@ class rcube_imap extends rcube_storage
// unset threads internal cache
unset($this->icache['threads']);
+ $this->set_search_dirty($folder);
+
// remove message ids from search set
if ($this->search_set && $folder == $this->folder) {
// threads are too complicated to just remove messages from set
if ($this->search_threads || $all_mode) {
$this->refresh_search();
}
- else {
+ else if (!$this->search_set->incomplete) {
$this->search_set->filter(explode(',', $uids));
}
}
@@ -2975,16 +3120,17 @@ class rcube_imap extends rcube_storage
*
* @param string $folder New folder name
* @param boolean $subscribe True if the new folder should be subscribed
+ * @param string $type Optional folder type (junk, trash, drafts, sent, archive)
*
* @return boolean True on success
*/
- public function create_folder($folder, $subscribe=false)
+ public function create_folder($folder, $subscribe = false, $type = null)
{
if (!$this->check_connection()) {
return false;
}
- $result = $this->conn->createFolder($folder);
+ $result = $this->conn->createFolder($folder, $type ? array("\\" . ucfirst($type)) : null);
// try to subscribe it
if ($result) {
@@ -3109,19 +3255,84 @@ class rcube_imap extends rcube_storage
/**
- * Create all folders specified as default
+ * Detect special folder associations stored in storage backend
*/
- public function create_default_folders()
+ public function get_special_folders($forced = false)
{
- // create default folders if they do not exist
- foreach ($this->default_folders as $folder) {
- if (!$this->folder_exists($folder)) {
- $this->create_folder($folder, true);
+ $result = parent::get_special_folders();
+
+ if (isset($this->icache['special-use'])) {
+ return array_merge($result, $this->icache['special-use']);
+ }
+
+ if (!$forced || !$this->get_capability('SPECIAL-USE')) {
+ return $result;
+ }
+
+ if (!$this->check_connection()) {
+ return $result;
+ }
+
+ $types = array_map(function($value) { return "\\" . ucfirst($value); }, rcube_storage::$folder_types);
+ $special = array();
+
+ // request \Subscribed flag in LIST response as performance improvement for folder_exists()
+ $folders = $this->conn->listMailboxes('', '*', array('SUBSCRIBED'), array('SPECIAL-USE'));
+
+ foreach ($folders as $folder) {
+ if ($flags = $this->conn->data['LIST'][$folder]) {
+ foreach ($types as $type) {
+ if (in_array($type, $flags)) {
+ $type = strtolower(substr($type, 1));
+ $special[$type] = $folder;
+ }
+ }
}
- else if (!$this->folder_exists($folder, true)) {
- $this->subscribe($folder);
+ }
+
+ $this->icache['special-use'] = $special;
+ unset($this->icache['special-folders']);
+
+ return array_merge($result, $special);
+ }
+
+
+ /**
+ * Set special folder associations stored in storage backend
+ */
+ public function set_special_folders($specials)
+ {
+ if (!$this->get_capability('SPECIAL-USE') || !$this->get_capability('METADATA')) {
+ return false;
+ }
+
+ if (!$this->check_connection()) {
+ return false;
+ }
+
+ $folders = $this->get_special_folders(true);
+ $old = (array) $this->icache['special-use'];
+
+ foreach ($specials as $type => $folder) {
+ if (in_array($type, rcube_storage::$folder_types)) {
+ $old_folder = $old[$type];
+ if ($old_folder !== $folder) {
+ // unset old-folder metadata
+ if ($old_folder !== null) {
+ $this->delete_metadata($old_folder, array('/private/specialuse'));
+ }
+ // set new folder metadata
+ if ($folder) {
+ $this->set_metadata($folder, array('/private/specialuse' => "\\" . ucfirst($type)));
+ }
+ }
}
}
+
+ $this->icache['special-use'] = $specials;
+ unset($this->icache['special-folders']);
+
+ return true;
}
@@ -3133,13 +3344,13 @@ class rcube_imap extends rcube_storage
*
* @return boolean TRUE or FALSE
*/
- public function folder_exists($folder, $subscription=false)
+ public function folder_exists($folder, $subscription = false)
{
if ($folder == 'INBOX') {
return true;
}
- $key = $subscription ? 'subscribed' : 'existing';
+ $key = $subscription ? 'subscribed' : 'existing';
if (is_array($this->icache[$key]) && in_array($folder, $this->icache[$key])) {
return true;
@@ -3150,10 +3361,24 @@ class rcube_imap extends rcube_storage
}
if ($subscription) {
- $a_folders = $this->conn->listSubscribed('', $folder);
+ // It's possible we already called LIST command, check LIST data
+ if (!empty($this->conn->data['LIST']) && !empty($this->conn->data['LIST'][$folder])
+ && in_array('\\Subscribed', $this->conn->data['LIST'][$folder])
+ ) {
+ $a_folders = array($folder);
+ }
+ else {
+ $a_folders = $this->conn->listSubscribed('', $folder);
+ }
}
else {
- $a_folders = $this->conn->listMailboxes('', $folder);
+ // It's possible we already called LIST command, check LIST data
+ if (!empty($this->conn->data['LIST']) && isset($this->conn->data['LIST'][$folder])) {
+ $a_folders = array($folder);
+ }
+ else {
+ $a_folders = $this->conn->listMailboxes('', $folder);
+ }
}
if (is_array($a_folders) && in_array($folder, $a_folders)) {
@@ -3364,7 +3589,7 @@ class rcube_imap extends rcube_storage
$options['name'] = $folder;
$options['attributes'] = $this->folder_attributes($folder, true);
$options['namespace'] = $this->folder_namespace($folder);
- $options['special'] = in_array($folder, $this->default_folders);
+ $options['special'] = $this->is_special_folder($folder);
// Set 'noselect' flag
if (is_array($options['attributes'])) {
@@ -3902,6 +4127,7 @@ class rcube_imap extends rcube_storage
$a_out = $a_defaults = $folders = array();
$delimiter = $this->get_hierarchy_delimiter();
+ $specials = array_merge(array('INBOX'), array_values($this->get_special_folders()));
// find default folders and skip folders starting with '.'
foreach ($a_folders as $folder) {
@@ -3909,7 +4135,7 @@ class rcube_imap extends rcube_storage
continue;
}
- if (!$skip_default && ($p = array_search($folder, $this->default_folders)) !== false && !$a_defaults[$p]) {
+ if (!$skip_default && ($p = array_search($folder, $specials)) !== false && !$a_defaults[$p]) {
$a_defaults[$p] = $folder;
}
else {
diff --git a/program/lib/Roundcube/rcube_imap_cache.php b/program/lib/Roundcube/rcube_imap_cache.php
index 0c3edeaad..e49e77803 100644
--- a/program/lib/Roundcube/rcube_imap_cache.php
+++ b/program/lib/Roundcube/rcube_imap_cache.php
@@ -171,7 +171,7 @@ class rcube_imap_cache
// Seek in internal cache
if (array_key_exists('index', $this->icache[$mailbox])) {
// The index was fetched from database already, but not validated yet
- if (!array_key_exists('object', $this->icache[$mailbox]['index'])) {
+ if (empty($this->icache[$mailbox]['index']['validated'])) {
$index = $this->icache[$mailbox]['index'];
}
// We've got a valid index
@@ -248,6 +248,7 @@ class rcube_imap_cache
}
$this->icache[$mailbox]['index'] = array(
+ 'validated' => true,
'object' => $data,
'sort_field' => $sort_field,
'modseq' => !empty($index['modseq']) ? $index['modseq'] : $mbox_data['HIGHESTMODSEQ']
@@ -890,6 +891,8 @@ class rcube_imap_cache
return false;
}
+ $index['validated'] = true;
+
// Get mailbox data (UIDVALIDITY, counters, etc.) for status check
$mbox_data = $this->imap->folder_data($mailbox);
diff --git a/program/lib/Roundcube/rcube_imap_generic.php b/program/lib/Roundcube/rcube_imap_generic.php
index 4f5707924..f45694dd0 100644
--- a/program/lib/Roundcube/rcube_imap_generic.php
+++ b/program/lib/Roundcube/rcube_imap_generic.php
@@ -1191,13 +1191,20 @@ class rcube_imap_generic
* Folder creation (CREATE)
*
* @param string $mailbox Mailbox name
+ * @param array $types Optional folder types (RFC 6154)
*
* @return bool True on success, False on error
*/
- function createFolder($mailbox)
+ function createFolder($mailbox, $types = null)
{
- $result = $this->execute('CREATE', array($this->escape($mailbox)),
- self::COMMAND_NORESPONSE);
+ $args = array($this->escape($mailbox));
+
+ // RFC 6154: CREATE-SPECIAL-USE
+ if (!empty($types) && $this->getCapability('CREATE-SPECIAL-USE')) {
+ $args[] = '(USE (' . implode(' ', $types) . '))';
+ }
+
+ $result = $this->execute('CREATE', $args, self::COMMAND_NORESPONSE);
return ($result == self::ERROR_OK);
}
@@ -1293,10 +1300,12 @@ class rcube_imap_generic
* @param string $ref Reference name
* @param string $mailbox Mailbox name
* @param bool $subscribed Enables returning subscribed mailboxes only
- * @param array $status_opts List of STATUS options (RFC5819: LIST-STATUS)
- * Possible: MESSAGES, RECENT, UIDNEXT, UIDVALIDITY, UNSEEN
+ * @param array $status_opts List of STATUS options
+ * (RFC5819: LIST-STATUS: MESSAGES, RECENT, UIDNEXT, UIDVALIDITY, UNSEEN)
+ * or RETURN options (RFC5258: LIST_EXTENDED: SUBSCRIBED, CHILDREN)
* @param array $select_opts List of selection options (RFC5258: LIST-EXTENDED)
- * Possible: SUBSCRIBED, RECURSIVEMATCH, REMOTE
+ * Possible: SUBSCRIBED, RECURSIVEMATCH, REMOTE,
+ * SPECIAL-USE (RFC6154)
*
* @return array List of mailboxes or hash of options if $status_ops argument
* is non-empty.
@@ -1309,6 +1318,7 @@ class rcube_imap_generic
}
$args = array();
+ $rets = array();
if (!empty($select_opts) && $this->getCapability('LIST-EXTENDED')) {
$select_opts = (array) $select_opts;
@@ -1319,11 +1329,21 @@ class rcube_imap_generic
$args[] = $this->escape($ref);
$args[] = $this->escape($mailbox);
+ if (!empty($status_opts) && $this->getCapability('LIST-EXTENDED')) {
+ $rets = array_intersect($status_opts, array('SUBSCRIBED', 'CHILDREN'));
+ }
+
if (!empty($status_opts) && $this->getCapability('LIST-STATUS')) {
- $status_opts = (array) $status_opts;
- $lstatus = true;
+ $status_opts = array_intersect($status_opts, array('MESSAGES', 'RECENT', 'UIDNEXT', 'UIDVALIDITY', 'UNSEEN'));
+
+ if (!empty($status_opts)) {
+ $lstatus = true;
+ $rets[] = 'STATUS (' . implode(' ', $status_opts) . ')';
+ }
+ }
- $args[] = 'RETURN (STATUS (' . implode(' ', $status_opts) . '))';
+ if (!empty($rets)) {
+ $args[] = 'RETURN (' . implode(' ', $rets) . ')';
}
list($code, $response) = $this->execute($subscribed ? 'LSUB' : 'LIST', $args);
diff --git a/program/lib/Roundcube/rcube_imap_search.php b/program/lib/Roundcube/rcube_imap_search.php
new file mode 100644
index 000000000..0c44daf1b
--- /dev/null
+++ b/program/lib/Roundcube/rcube_imap_search.php
@@ -0,0 +1,234 @@
+<?php
+
+/*
+ +-----------------------------------------------------------------------+
+ | This file is part of the Roundcube Webmail client |
+ | |
+ | Copyright (C) 2013, The Roundcube Dev Team |
+ | Copyright (C) 2014, Kolab Systems AG |
+ | |
+ | Licensed under the GNU General Public License version 3 or |
+ | any later version with exceptions for skins & plugins. |
+ | See the README file for a full license statement. |
+ | |
+ | PURPOSE: |
+ | Execute (multi-threaded) searches in multiple IMAP folders |
+ +-----------------------------------------------------------------------+
+ | Author: Thomas Bruederli <roundcube@gmail.com> |
+ +-----------------------------------------------------------------------+
+*/
+
+/**
+ * Class to control search jobs on multiple IMAP folders.
+ *
+ * @package Framework
+ * @subpackage Storage
+ * @author Thomas Bruederli <roundcube@gmail.com>
+ */
+class rcube_imap_search
+{
+ public $options = array();
+
+ protected $jobs = array();
+ protected $timelimit = 0;
+ protected $results;
+ protected $conn;
+
+ /**
+ * Default constructor
+ */
+ public function __construct($options, $conn)
+ {
+ $this->options = $options;
+ $this->conn = $conn;
+ }
+
+ /**
+ * Invoke search request to IMAP server
+ *
+ * @param array $folders List of IMAP folders to search in
+ * @param string $str Search criteria
+ * @param string $charset Search charset
+ * @param string $sort_field Header field to sort by
+ * @param boolean $threading True if threaded listing is active
+ */
+ public function exec($folders, $str, $charset = null, $sort_field = null, $threading=null)
+ {
+ $start = floor(microtime(true));
+ $results = new rcube_result_multifolder($folders);
+
+ // start a search job for every folder to search in
+ foreach ($folders as $folder) {
+ // a complete result for this folder already exists
+ $result = $this->results ? $this->results->get_set($folder) : false;
+ if ($result && !$result->incomplete) {
+ $results->add($result);
+ }
+ else {
+ $job = new rcube_imap_search_job($folder, $str, $charset, $sort_field, $threading);
+ $job->worker = $this;
+ $this->jobs[] = $job;
+ }
+ }
+
+ // execute jobs and gather results
+ foreach ($this->jobs as $job) {
+ // only run search if within the configured time limit
+ // TODO: try to estimate the required time based on folder size and previous search performance
+ if (!$this->timelimit || floor(microtime(true)) - $start < $this->timelimit) {
+ $job->run();
+ }
+
+ // add result (may have ->incomplete flag set)
+ $results->add($job->get_result());
+ }
+
+ return $results;
+ }
+
+ /**
+ * Setter for timelimt property
+ */
+ public function set_timelimit($seconds)
+ {
+ $this->timelimit = $seconds;
+ }
+
+ /**
+ * Setter for previous (potentially incomplete) search results
+ */
+ public function set_results($res)
+ {
+ $this->results = $res;
+ }
+
+ /**
+ * Get connection to the IMAP server
+ * (used for single-thread mode)
+ */
+ public function get_imap()
+ {
+ return $this->conn;
+ }
+}
+
+
+/**
+ * Stackable item to run the search on a specific IMAP folder
+ */
+class rcube_imap_search_job /* extends Stackable */
+{
+ private $folder;
+ private $search;
+ private $charset;
+ private $sort_field;
+ private $threading;
+ private $searchset;
+ private $result;
+ private $pagesize = 100;
+
+ public function __construct($folder, $str, $charset = null, $sort_field = null, $threading=false)
+ {
+ $this->folder = $folder;
+ $this->search = $str;
+ $this->charset = $charset;
+ $this->sort_field = $sort_field;
+ $this->threading = $threading;
+
+ $this->result = new rcube_result_index($folder);
+ $this->result->incomplete = true;
+ }
+
+ public function run()
+ {
+ $this->result = $this->search_index();
+ }
+
+ /**
+ * Copy of rcube_imap::search_index()
+ */
+ protected function search_index()
+ {
+ $criteria = $this->search;
+ $charset = $this->charset;
+
+ $imap = $this->worker->get_imap();
+
+ if (!$imap->connected()) {
+ trigger_error("No IMAP connection for $this->folder", E_USER_WARNING);
+
+ if ($this->threading) {
+ return new rcube_result_thread($this->folder);
+ }
+ else {
+ return new rcube_result_index($this->folder);
+ }
+ }
+
+ if ($this->worker->options['skip_deleted'] && !preg_match('/UNDELETED/', $criteria)) {
+ $criteria = 'UNDELETED '.$criteria;
+ }
+
+ // unset CHARSET if criteria string is ASCII, this way
+ // SEARCH won't be re-sent after "unsupported charset" response
+ if ($charset && $charset != 'US-ASCII' && is_ascii($criteria)) {
+ $charset = 'US-ASCII';
+ }
+
+ if ($this->threading) {
+ $threads = $imap->thread($this->folder, $this->threading, $criteria, true, $charset);
+
+ // Error, try with US-ASCII (RFC5256: SORT/THREAD must support US-ASCII and UTF-8,
+ // but I've seen that Courier doesn't support UTF-8)
+ if ($threads->is_error() && $charset && $charset != 'US-ASCII') {
+ $threads = $imap->thread($this->folder, $this->threading,
+ rcube_imap::convert_criteria($criteria, $charset), true, 'US-ASCII');
+ }
+
+ return $threads;
+ }
+
+ if ($this->sort_field) {
+ $messages = $imap->sort($this->folder, $this->sort_field, $criteria, true, $charset);
+
+ // Error, try with US-ASCII (RFC5256: SORT/THREAD must support US-ASCII and UTF-8,
+ // but I've seen Courier with disabled UTF-8 support)
+ if ($messages->is_error() && $charset && $charset != 'US-ASCII') {
+ $messages = $imap->sort($this->folder, $this->sort_field,
+ rcube_imap::convert_criteria($criteria, $charset), true, 'US-ASCII');
+ }
+ }
+
+ if (!$messages || $messages->is_error()) {
+ $messages = $imap->search($this->folder,
+ ($charset && $charset != 'US-ASCII' ? "CHARSET $charset " : '') . $criteria, true);
+
+ // Error, try with US-ASCII (some servers may support only US-ASCII)
+ if ($messages->is_error() && $charset && $charset != 'US-ASCII') {
+ $messages = $imap->search($this->folder,
+ rcube_imap::convert_criteria($criteria, $charset), true);
+ }
+ }
+
+ return $messages;
+ }
+
+ public function get_search_set()
+ {
+ return array(
+ $this->search,
+ $this->result,
+ $this->charset,
+ $this->sort_field,
+ $this->threading,
+ );
+ }
+
+ public function get_result()
+ {
+ return $this->result;
+ }
+
+}
+
+
diff --git a/program/lib/Roundcube/rcube_ldap.php b/program/lib/Roundcube/rcube_ldap.php
index 55a64acec..5a4b9dd61 100644
--- a/program/lib/Roundcube/rcube_ldap.php
+++ b/program/lib/Roundcube/rcube_ldap.php
@@ -95,8 +95,8 @@ class rcube_ldap extends rcube_addressbook
if (empty($this->prop['groups']['scope']))
$this->prop['groups']['scope'] = 'sub';
// extend group objectclass => member attribute mapping
- if (!empty($this->prop['groups']['event-panel-summary']))
- $this->group_types = array_merge($this->group_types, $this->prop['groups']['event-panel-summary']);
+ if (!empty($this->prop['groups']['class_member_attr']))
+ $this->group_types = array_merge($this->group_types, $this->prop['groups']['class_member_attr']);
// add group name attrib to the list of attributes to be fetched
$fetch_attributes[] = $this->prop['groups']['name_attr'];
@@ -377,10 +377,11 @@ class rcube_ldap extends rcube_addressbook
// replace placeholders in filter settings
if (!empty($this->prop['filter']))
$this->prop['filter'] = strtr($this->prop['filter'], $replaces);
- if (!empty($this->prop['groups']['filter']))
- $this->prop['groups']['filter'] = strtr($this->prop['groups']['filter'], $replaces);
- if (!empty($this->prop['groups']['member_filter']))
- $this->prop['groups']['member_filter'] = strtr($this->prop['groups']['member_filter'], $replaces);
+
+ foreach (array('base_dn','filter','member_filter') as $k) {
+ if (!empty($this->prop['groups'][$k]))
+ $this->prop['groups'][$k] = strtr($this->prop['groups'][$k], $replaces);
+ }
if (!empty($this->prop['group_filters'])) {
foreach ($this->prop['group_filters'] as $i => $gf) {
diff --git a/program/lib/Roundcube/rcube_message.php b/program/lib/Roundcube/rcube_message.php
index f24ec3ed8..a648ae722 100644
--- a/program/lib/Roundcube/rcube_message.php
+++ b/program/lib/Roundcube/rcube_message.php
@@ -74,6 +74,11 @@ class rcube_message
*/
function __construct($uid, $folder = null)
{
+ // decode combined UID-folder identifier
+ if (preg_match('/^\d+-.+/', $uid)) {
+ list($uid, $folder) = explode('-', $uid, 2);
+ }
+
$this->uid = $uid;
$this->app = rcube::get_instance();
$this->storage = $this->app->get_storage();
diff --git a/program/lib/Roundcube/rcube_message_header.php b/program/lib/Roundcube/rcube_message_header.php
index 2c5e2b6c8..2bda930eb 100644
--- a/program/lib/Roundcube/rcube_message_header.php
+++ b/program/lib/Roundcube/rcube_message_header.php
@@ -167,6 +167,13 @@ class rcube_message_header
public $mdn_to;
/**
+ * IMAP folder this message is stored in
+ *
+ * @var string
+ */
+ public $folder;
+
+ /**
* Other message headers
*
* @var array
@@ -189,6 +196,8 @@ class rcube_message_header
'reply-to' => 'replyto',
'cc' => 'cc',
'bcc' => 'bcc',
+ 'mbox' => 'folder',
+ 'folder' => 'folder',
'content-transfer-encoding' => 'encoding',
'in-reply-to' => 'in_reply_to',
'content-type' => 'ctype',
diff --git a/program/lib/Roundcube/rcube_plugin_api.php b/program/lib/Roundcube/rcube_plugin_api.php
index 461c3cc07..feeeb192e 100644
--- a/program/lib/Roundcube/rcube_plugin_api.php
+++ b/program/lib/Roundcube/rcube_plugin_api.php
@@ -182,7 +182,7 @@ class rcube_plugin_api
}
// plugin already loaded
- if ($this->plugins[$plugin_name] || class_exists($plugin_name, false)) {
+ if ($this->plugins[$plugin_name]) {
return true;
}
@@ -190,7 +190,9 @@ class rcube_plugin_api
. DIRECTORY_SEPARATOR . $plugin_name . '.php';
if (file_exists($fn)) {
- include $fn;
+ if (!class_exists($plugin_name, false)) {
+ include $fn;
+ }
// instantiate class if exists
if (class_exists($plugin_name, false)) {
diff --git a/program/lib/Roundcube/rcube_result_index.php b/program/lib/Roundcube/rcube_result_index.php
index 058f25c6f..ffc1ad78a 100644
--- a/program/lib/Roundcube/rcube_result_index.php
+++ b/program/lib/Roundcube/rcube_result_index.php
@@ -26,6 +26,8 @@
*/
class rcube_result_index
{
+ public $incomplete = false;
+
protected $raw_data;
protected $mailbox;
protected $meta = array();
diff --git a/program/lib/Roundcube/rcube_result_multifolder.php b/program/lib/Roundcube/rcube_result_multifolder.php
new file mode 100644
index 000000000..4bbd2188d
--- /dev/null
+++ b/program/lib/Roundcube/rcube_result_multifolder.php
@@ -0,0 +1,332 @@
+<?php
+
+/*
+ +-----------------------------------------------------------------------+
+ | This file is part of the Roundcube Webmail client |
+ | Copyright (C) 2005-2011, The Roundcube Dev Team |
+ | Copyright (C) 2011, Kolab Systems AG |
+ | |
+ | Licensed under the GNU General Public License version 3 or |
+ | any later version with exceptions for skins & plugins. |
+ | See the README file for a full license statement. |
+ | |
+ | PURPOSE: |
+ | SORT/SEARCH/ESEARCH response handler |
+ +-----------------------------------------------------------------------+
+ | Author: Thomas Bruederli <roundcube@gmail.com> |
+ +-----------------------------------------------------------------------+
+*/
+
+/**
+ * Class holding a set of rcube_result_index instances that together form a
+ * result set of a multi-folder search
+ *
+ * @package Framework
+ * @subpackage Storage
+ */
+class rcube_result_multifolder
+{
+ public $multi = true;
+ public $sets = array();
+ public $incomplete = false;
+ public $folder;
+
+ protected $meta = array();
+ protected $index = array();
+ protected $folders = array();
+ protected $sorting;
+ protected $order = 'ASC';
+
+
+ /**
+ * Object constructor.
+ */
+ public function __construct($folders = array())
+ {
+ $this->folders = $folders;
+ $this->meta = array('count' => 0);
+ }
+
+
+ /**
+ * Initializes object with SORT command response
+ *
+ * @param string $data IMAP response string
+ */
+ public function add($result)
+ {
+ $this->sets[] = $result;
+
+ if ($result->count()) {
+ $this->append_result($result);
+ }
+ else if ($result->incomplete) {
+ $this->incomplete = true;
+ }
+ }
+
+ /**
+ * Append message UIDs from the given result to our index
+ */
+ protected function append_result($result)
+ {
+ $this->meta['count'] += $result->count();
+
+ // append UIDs to global index
+ $folder = $result->get_parameters('MAILBOX');
+ $index = array_map(function($uid) use ($folder) { return $uid . '-' . $folder; }, $result->get());
+ $this->index = array_merge($this->index, $index);
+ }
+
+ /**
+ * Store a global index of (sorted) message UIDs
+ */
+ public function set_message_index($headers, $sort_field, $sort_order)
+ {
+ $this->index = array();
+ foreach ($headers as $header) {
+ $this->index[] = $header->uid . '-' . $header->folder;
+ }
+
+ $this->sorting = $sort_field;
+ $this->order = $sort_order;
+ }
+
+ /**
+ * Checks the result from IMAP command
+ *
+ * @return bool True if the result is an error, False otherwise
+ */
+ public function is_error()
+ {
+ return false;
+ }
+
+
+ /**
+ * Checks if the result is empty
+ *
+ * @return bool True if the result is empty, False otherwise
+ */
+ public function is_empty()
+ {
+ return empty($this->sets) || $this->meta['count'] == 0;
+ }
+
+
+ /**
+ * Returns number of elements in the result
+ *
+ * @return int Number of elements
+ */
+ public function count()
+ {
+ return $this->meta['count'];
+ }
+
+
+ /**
+ * Returns number of elements in the result.
+ * Alias for count() for compatibility with rcube_result_thread
+ *
+ * @return int Number of elements
+ */
+ public function count_messages()
+ {
+ return $this->count();
+ }
+
+
+ /**
+ * Reverts order of elements in the result
+ */
+ public function revert()
+ {
+ $this->order = $this->order == 'ASC' ? 'DESC' : 'ASC';
+ $this->index = array();
+
+ // revert order in all sub-sets
+ foreach ($this->sets as $set) {
+ if ($this->order != $set->get_parameters('ORDER')) {
+ $set->revert();
+ }
+ $folder = $set->get_parameters('MAILBOX');
+ $index = array_map(function($uid) use ($folder) { return $uid . '-' . $folder; }, $set->get());
+ $this->index = array_merge($this->index, $index);
+ }
+ }
+
+
+ /**
+ * Check if the given message ID exists in the object
+ *
+ * @param int $msgid Message ID
+ * @param bool $get_index When enabled element's index will be returned.
+ * Elements are indexed starting with 0
+ * @return mixed False if message ID doesn't exist, True if exists or
+ * index of the element if $get_index=true
+ */
+ public function exists($msgid, $get_index = false)
+ {
+ if (!empty($this->folder)) {
+ $msgid .= '-' . $this->folder;
+ }
+ return array_search($msgid, $this->index);
+ }
+
+
+ /**
+ * Filters data set. Removes elements listed in $ids list.
+ *
+ * @param array $ids List of IDs to remove.
+ * @param string $folder IMAP folder
+ */
+ public function filter($ids = array(), $folder = null)
+ {
+ $this->meta['count'] = 0;
+ foreach ($this->sets as $set) {
+ if ($set->get_parameters('MAILBOX') == $folder) {
+ $set->filter($ids);
+ }
+ $this->meta['count'] += $set->count();
+ }
+ }
+
+ /**
+ * Slices data set.
+ *
+ * @param $offset Offset (as for PHP's array_slice())
+ * @param $length Number of elements (as for PHP's array_slice())
+ *
+ */
+ public function slice($offset, $length)
+ {
+ $data = array_slice($this->get(), $offset, $length);
+
+ $this->index = $data;
+ $this->meta['count'] = count($data);
+ }
+
+ /**
+ * Filters data set. Removes elements not listed in $ids list.
+ *
+ * @param array $ids List of IDs to keep.
+ */
+ public function intersect($ids = array())
+ {
+ // not implemented
+ }
+
+ /**
+ * Return all messages in the result.
+ *
+ * @return array List of message IDs
+ */
+ public function get()
+ {
+ return $this->index;
+ }
+
+
+ /**
+ * Return all messages in the result.
+ *
+ * @return array List of message IDs
+ */
+ public function get_compressed()
+ {
+ return '';
+ }
+
+
+ /**
+ * Return result element at specified index
+ *
+ * @param int|string $index Element's index or "FIRST" or "LAST"
+ *
+ * @return int Element value
+ */
+ public function get_element($idx)
+ {
+ switch ($idx) {
+ case 'FIRST': return $this->index[0];
+ case 'LAST': return end($this->index);
+ default: return $this->index[$idx];
+ }
+ }
+
+
+ /**
+ * Returns response parameters, e.g. ESEARCH's MIN/MAX/COUNT/ALL/MODSEQ
+ * or internal data e.g. MAILBOX, ORDER
+ *
+ * @param string $param Parameter name
+ *
+ * @return array|string Response parameters or parameter value
+ */
+ public function get_parameters($param=null)
+ {
+ $params = array(
+ 'SORT' => $this->sorting,
+ 'ORDER' => $this->order,
+ 'MAILBOX' => $this->folders,
+ );
+
+ if ($param !== null) {
+ return $params[$param];
+ }
+
+ return $params;
+ }
+
+ /**
+ * Returns the stored result object for a particular folder
+ *
+ * @param string $folder Folder name
+ * @return false|obejct rcube_result_* instance of false if none found
+ */
+ public function get_set($folder)
+ {
+ foreach ($this->sets as $set) {
+ if ($set->get_parameters('MAILBOX') == $folder) {
+ return $set;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns length of internal data representation
+ *
+ * @return int Data length
+ */
+ protected function length()
+ {
+ return $this->count();
+ }
+
+
+ /* Serialize magic methods */
+
+ public function __sleep()
+ {
+ return array('sets','folders','sorting','order');
+ }
+
+ public function __wakeup()
+ {
+ // restore index from saved result sets
+ $this->meta = array('count' => 0);
+
+ foreach ($this->sets as $result) {
+ if ($result->count()) {
+ $this->append_result($result);
+ }
+ else if ($result->incomplete) {
+ $this->incomplete = true;
+ }
+ }
+ }
+
+}
diff --git a/program/lib/Roundcube/rcube_result_thread.php b/program/lib/Roundcube/rcube_result_thread.php
index ceaaf59a6..168761696 100644
--- a/program/lib/Roundcube/rcube_result_thread.php
+++ b/program/lib/Roundcube/rcube_result_thread.php
@@ -26,6 +26,8 @@
*/
class rcube_result_thread
{
+ public $incomplete = false;
+
protected $raw_data;
protected $mailbox;
protected $meta = array();
diff --git a/program/lib/Roundcube/rcube_spellcheck_googie.php b/program/lib/Roundcube/rcube_spellcheck_googie.php
index 3777942a6..f9e450fdd 100644
--- a/program/lib/Roundcube/rcube_spellcheck_googie.php
+++ b/program/lib/Roundcube/rcube_spellcheck_googie.php
@@ -56,6 +56,10 @@ class rcube_spellcheck_googie extends rcube_spellcheck_engine
{
$this->content = $text;
+ if (empty($text)) {
+ return $this->matches = array();
+ }
+
// spell check uri is configured
$url = rcube::get_instance()->config->get('spellcheck_uri');
diff --git a/program/lib/Roundcube/rcube_storage.php b/program/lib/Roundcube/rcube_storage.php
index c09f05328..69d6d2fae 100644
--- a/program/lib/Roundcube/rcube_storage.php
+++ b/program/lib/Roundcube/rcube_storage.php
@@ -35,9 +35,15 @@ abstract class rcube_storage
*/
public $conn;
+ /**
+ * List of supported special folder types
+ *
+ * @var array
+ */
+ public static $folder_types = array('drafts', 'sent', 'junk', 'trash');
+
protected $folder = 'INBOX';
protected $default_charset = 'ISO-8859-1';
- protected $default_folders = array('INBOX');
protected $search_set;
protected $options = array('auth_type' => 'check');
protected $page_size = 10;
@@ -167,24 +173,6 @@ abstract class rcube_storage
/**
- * This list of folders will be listed above all other folders
- *
- * @param array $arr Indexed list of folder names
- */
- public function set_default_folders($arr)
- {
- if (is_array($arr)) {
- $this->default_folders = $arr;
-
- // add inbox if not included
- if (!in_array('INBOX', $this->default_folders)) {
- array_unshift($this->default_folders, 'INBOX');
- }
- }
- }
-
-
- /**
* Set internal folder reference.
* All operations will be perfomed on this folder.
*
@@ -858,15 +846,59 @@ abstract class rcube_storage
*/
public function create_default_folders()
{
+ $rcube = rcube::get_instance();
+
// create default folders if they do not exist
- foreach ($this->default_folders as $folder) {
- if (!$this->folder_exists($folder)) {
- $this->create_folder($folder, true);
+ foreach (self::$folder_types as $type) {
+ if ($folder = $rcube->config->get($type . '_mbox')) {
+ if (!$this->folder_exists($folder)) {
+ $this->create_folder($folder, true, $type);
+ }
+ else if (!$this->folder_exists($folder, true)) {
+ $this->subscribe($folder);
+ }
}
- else if (!$this->folder_exists($folder, true)) {
- $this->subscribe($folder);
+ }
+ }
+
+
+ /**
+ * Check if specified folder is a special folder
+ */
+ public function is_special_folder($name)
+ {
+ return $name == 'INBOX' || in_array($name, $this->get_special_folders());
+ }
+
+
+ /**
+ * Return configured special folders
+ */
+ public function get_special_folders($forced = false)
+ {
+ // getting config might be expensive, store special folders in memory
+ if (!isset($this->icache['special-folders'])) {
+ $rcube = rcube::get_instance();
+ $this->icache['special-folders'] = array();
+
+ foreach (self::$folder_types as $type) {
+ if ($folder = $rcube->config->get($type . '_mbox')) {
+ $this->icache['special-folders'][$type] = $folder;
+ }
}
}
+
+ return $this->icache['special-folders'];
+ }
+
+
+ /**
+ * Set special folder associations stored in backend
+ */
+ public function set_special_folders($specials)
+ {
+ // should be overriden by storage class if backend supports special folders (SPECIAL-USE)
+ unset($this->icache['special-folders']);
}
diff --git a/program/lib/Roundcube/rcube_vcard.php b/program/lib/Roundcube/rcube_vcard.php
index a54ee7e11..fb8fdd525 100644
--- a/program/lib/Roundcube/rcube_vcard.php
+++ b/program/lib/Roundcube/rcube_vcard.php
@@ -149,6 +149,11 @@ class rcube_vcard
$this->email[0] = $this->email[$pref_index];
$this->email[$pref_index] = $tmp;
}
+
+ // fix broken vcards from Outlook that only supply ORG but not the required N or FN properties
+ if (!strlen(trim($this->displayname . $this->surname . $this->firstname)) && strlen($this->organization)) {
+ $this->displayname = $this->organization;
+ }
}
/**
diff --git a/program/lib/Roundcube/rcube_washtml.php b/program/lib/Roundcube/rcube_washtml.php
index 51f7930aa..e23e5b21d 100644
--- a/program/lib/Roundcube/rcube_washtml.php
+++ b/program/lib/Roundcube/rcube_washtml.php
@@ -171,7 +171,7 @@ class rcube_washtml
*/
private function wash_style($style)
{
- $s = '';
+ $result = array();
foreach (explode(';', $style) as $declaration) {
if (preg_match('/^\s*([a-z\-]+)\s*:\s*(.*)\s*$/i', $declaration, $match)) {
@@ -179,54 +179,48 @@ class rcube_washtml
$str = $match[2];
$value = '';
- while (sizeof($str) > 0 &&
- preg_match('/^(url\(\s*[\'"]?([^\'"\)]*)[\'"]?\s*\)'./*1,2*/
- '|rgb\(\s*[0-9]+\s*,\s*[0-9]+\s*,\s*[0-9]+\s*\)'.
- '|-?[0-9.]+\s*(em|ex|px|cm|mm|in|pt|pc|deg|rad|grad|ms|s|hz|khz|%)?'.
- '|#[0-9a-f]{3,6}'.
- '|[a-z0-9"\', -]+'.
- ')\s*/i', $str, $match)
- ) {
- if ($match[2]) {
- if (($src = $this->config['cid_map'][$match[2]])
- || ($src = $this->config['cid_map'][$this->config['base_url'].$match[2]])
- ) {
- $value .= ' url('.htmlspecialchars($src, ENT_QUOTES) . ')';
- }
- else if (preg_match('!^(https?:)?//[a-z0-9/._+-]+$!i', $match[2], $url)) {
- if ($this->config['allow_remote']) {
- $value .= ' url('.htmlspecialchars($url[0], ENT_QUOTES).')';
+ foreach ($this->explode_style($str) as $val) {
+ if (preg_match('/^url\(/i', $val)) {
+ if (preg_match('/^url\(\s*[\'"]?([^\'"\)]*)[\'"]?\s*\)/iu', $val, $match)) {
+ $url = $match[1];
+ if (($src = $this->config['cid_map'][$url])
+ || ($src = $this->config['cid_map'][$this->config['base_url'].$url])
+ ) {
+ $value .= ' url('.htmlspecialchars($src, ENT_QUOTES) . ')';
}
- else {
- $this->extlinks = true;
+ else if (preg_match('!^(https?:)?//[a-z0-9/._+-]+$!i', $url, $m)) {
+ if ($this->config['allow_remote']) {
+ $value .= ' url('.htmlspecialchars($m[0], ENT_QUOTES).')';
+ }
+ else {
+ $this->extlinks = true;
+ }
+ }
+ else if (preg_match('/^data:.+/i', $url)) { // RFC2397
+ $value .= ' url('.htmlspecialchars($url, ENT_QUOTES).')';
}
- }
- else if (preg_match('/^data:.+/i', $match[2])) { // RFC2397
- $value .= ' url('.htmlspecialchars($match[2], ENT_QUOTES).')';
}
}
- else {
+ else if (!preg_match('/^(behavior|expression)/i', $val)) {
// whitelist ?
- $value .= ' ' . $match[0];
+ $value .= ' ' . $val;
// #1488535: Fix size units, so width:800 would be changed to width:800px
if (preg_match('/(left|right|top|bottom|width|height)/i', $cssid)
- && preg_match('/^[0-9]+$/', $match[0])
+ && preg_match('/^[0-9]+$/', $val)
) {
$value .= 'px';
}
}
-
- $str = substr($str, strlen($match[0]));
}
if (isset($value[0])) {
- $s .= ($s?' ':'') . $cssid . ':' . $value . ';';
+ $result[] = $cssid . ':' . $value;
}
}
}
- return $s;
+ return implode('; ', $result);
}
/**
@@ -283,10 +277,12 @@ class rcube_washtml
/**
* The main loop that recurse on a node tree.
- * It output only allowed tags with allowed attributes
- * and allowed inline styles
+ * It output only allowed tags with allowed attributes and allowed inline styles
+ *
+ * @param DOMNode $node HTML element
+ * @param int $level Recurrence level (safe initial value found empirically)
*/
- private function dumpHtml($node, $level = 0)
+ private function dumpHtml($node, $level = 20)
{
if (!$node->hasChildNodes()) {
return '';
@@ -576,4 +572,49 @@ class rcube_washtml
}
}
}
+
+ /**
+ * Explode css style value
+ */
+ protected function explode_style($style)
+ {
+ $style = trim($style);
+
+ // first remove comments
+ $pos = 0;
+ while (($pos = strpos($style, '/*', $pos)) !== false) {
+ $end = strpos($style, '*/', $pos+2);
+
+ if ($end === false) {
+ $style = substr($style, 0, $pos);
+ }
+ else {
+ $style = substr_replace($style, '', $pos, $end - $pos + 2);
+ }
+ }
+
+ $strlen = strlen($style);
+ $result = array();
+
+ // explode value
+ for ($p=$i=0; $i < $strlen; $i++) {
+ if (($style[$i] == "\"" || $style[$i] == "'") && $style[$i-1] != "\\") {
+ if ($q == $style[$i]) {
+ $q = false;
+ }
+ else if (!$q) {
+ $q = $style[$i];
+ }
+ }
+
+ if (!$q && $style[$i] == ' ' && !preg_match('/[,\(]/', $style[$i-1])) {
+ $result[] = substr($style, $p, $i - $p);
+ $p = $i + 1;
+ }
+ }
+
+ $result[] = (string) substr($style, $p);
+
+ return $result;
+ }
}