diff options
Diffstat (limited to 'program/lib')
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; + } } |