From 6ddb16d181e285d4f0ef0ef55bdd0ba787f1b583 Mon Sep 17 00:00:00 2001 From: Thomas Bruederli <thomas@roundcube.net> Date: Sat, 17 Nov 2012 10:24:09 -0500 Subject: [PATCH] Merge branch 'master' of github.com:roundcube/roundcubemail --- program/include/rcube_imap.php | 467 +++++++++++++++++++++++++++++++++++++--------------------- 1 files changed, 296 insertions(+), 171 deletions(-) diff --git a/program/include/rcube_imap.php b/program/include/rcube_imap.php index cec0ad8..9054b6b 100644 --- a/program/include/rcube_imap.php +++ b/program/include/rcube_imap.php @@ -19,19 +19,16 @@ | Author: Thomas Bruederli <roundcube@gmail.com> | | Author: Aleksander Machniak <alec@alec.pl> | +-----------------------------------------------------------------------+ - - $Id$ - */ /** * Interface class for accessing an IMAP server * - * @package Mail + * @package Framework + * @subpackage Storage * @author Thomas Bruederli <roundcube@gmail.com> * @author Aleksander Machniak <alec@alec.pl> - * @version 2.0 */ class rcube_imap extends rcube_storage { @@ -362,11 +359,11 @@ return array( $this->search_string, - $this->search_set, - $this->search_charset, - $this->search_sort_field, - $this->search_sorted, - ); + $this->search_set, + $this->search_charset, + $this->search_sort_field, + $this->search_sorted, + ); } @@ -404,15 +401,56 @@ */ public function check_permflag($flag) { - $flag = strtoupper($flag); - $imap_flag = $this->conn->flags[$flag]; + $flag = strtoupper($flag); + $imap_flag = $this->conn->flags[$flag]; + $perm_flags = $this->get_permflags($this->folder); - if ($this->folder !== null) { - $this->check_connection(); + return in_array_nocase($imap_flag, $perm_flags); + } + + + /** + * Returns PERMANENTFLAGS of the specified folder + * + * @param string $folder Folder name + * + * @return array Flags + */ + public function get_permflags($folder) + { + if (!strlen($folder)) { + return array(); } - // @TODO: cache permanent flags (?) +/* + Checking PERMANENTFLAGS is rather rare, so we disable caching of it + Re-think when we'll use it for more than only MDNSENT flag - return (in_array_nocase($imap_flag, $this->conn->data['PERMANENTFLAGS'])); + $cache_key = 'mailboxes.permanentflags.' . $folder; + $permflags = $this->get_cache($cache_key); + + if ($permflags !== null) { + return explode(' ', $permflags); + } +*/ + if (!$this->check_connection()) { + return array(); + } + + if ($this->conn->select($folder)) { + $permflags = $this->conn->data['PERMANENTFLAGS']; + } + else { + return array(); + } + + if (!is_array($permflags)) { + $permflags = array(); + } +/* + // Store permflags as string to limit cached object size + $this->update_cache($cache_key, implode(' ', $permflags)); +*/ + return $permflags; } @@ -974,8 +1012,8 @@ $a_msg_headers, $this->sort_field, $this->sort_order); // only return the requested part of the set - $a_msg_headers = array_slice(array_values($a_msg_headers), - $from, min($cnt-$to, $this->page_size)); + $slice_length = min($this->page_size, $cnt - ($to > $cnt ? $from : $to)); + $a_msg_headers = array_slice(array_values($a_msg_headers), $from, $slice_length); if ($slice) { $a_msg_headers = array_slice($a_msg_headers, -$slice, $slice); @@ -1396,6 +1434,12 @@ $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 = $this->conn->thread($folder, $this->threading, $criteria, true, $charset); @@ -1427,7 +1471,7 @@ } $messages = $this->conn->search($folder, - ($charset ? "CHARSET $charset " : '') . $criteria, true); + ($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') { @@ -1597,11 +1641,24 @@ $structure[1] = $m[2]; } else { - return $headers; + // Try to parse the message using Mail_mimeDecode package + // We need a better solution, Mail_mimeDecode parses message + // in memory, which wouldn't work for very big messages, + // (it uses up to 10x more memory than the message size) + // it's also buggy and not actively developed + if ($headers->size && rcube_utils::mem_check($headers->size * 10)) { + $raw_msg = $this->get_raw_body($uid); + $struct = rcube_mime::parse_message($raw_msg); + } + else { + return $headers; + } } } - $struct = $this->structure_part($structure, 0, '', $headers); + if (empty($struct)) { + $struct = $this->structure_part($structure, 0, '', $headers); + } // don't trust given content-type if (empty($struct->parts) && !empty($headers->ctype)) { @@ -2017,7 +2074,7 @@ if ($o_part && $o_part->size) { $body = $this->conn->handlePartBody($this->folder, $uid, true, - $part ? $part : 'TEXT', $o_part->encoding, $print, $fp); + $part ? $part : 'TEXT', $o_part->encoding, $print, $fp, $o_part->ctype_primary == 'text'); } if ($fp || $print) { @@ -2087,14 +2144,17 @@ /** * Sends the whole message source to stdout + * + * @param int $uid Message UID + * @param bool $formatted Enables line-ending formatting */ - public function print_raw_body($uid) + public function print_raw_body($uid, $formatted = true) { if (!$this->check_connection()) { return; } - $this->conn->handlePartBody($this->folder, $uid, true, NULL, NULL, true); + $this->conn->handlePartBody($this->folder, $uid, true, null, null, true, null, $formatted); } @@ -2159,23 +2219,33 @@ * @param string $message The message source string or filename * @param string $headers Headers string if $message contains only the body * @param boolean $is_file True if $message is a filename + * @param array $flags Message flags + * @param mixed $date Message internal date * * @return int|bool Appended message UID or True on success, False on error */ - public function save_message($folder, &$message, $headers='', $is_file=false) + public function save_message($folder, &$message, $headers='', $is_file=false, $flags = array(), $date = null) { if (!strlen($folder)) { $folder = $this->folder; } + if (!$this->check_connection()) { + return false; + } + // make sure folder exists - if ($this->folder_exists($folder)) { - if ($is_file) { - $saved = $this->conn->appendFromFile($folder, $message, $headers); - } - else { - $saved = $this->conn->append($folder, $message); - } + if (!$this->folder_exists($folder)) { + return false; + } + + $date = $this->date_format($date); + + if ($is_file) { + $saved = $this->conn->appendFromFile($folder, $message, $headers, $flags, $date); + } + else { + $saved = $this->conn->append($folder, $message, $flags, $date); } if ($saved) { @@ -2468,7 +2538,16 @@ return $a_mboxes; } - $a_mboxes = $this->_list_folders_subscribed($root, $name, $filter, $rights); + // Give plugins a chance to provide a list of folders + $data = rcube::get_instance()->plugins->exec_hook('storage_folders', + array('root' => $root, 'name' => $name, 'filter' => $filter, 'mode' => 'LSUB')); + + if (isset($data['folders'])) { + $a_mboxes = $data['folders']; + } + else { + $a_mboxes = $this->list_folders_subscribed_direct($root, $name); + } if (!is_array($a_mboxes)) { return array(); @@ -2497,76 +2576,74 @@ /** - * protected method for folders listing (LSUB) + * Method for direct folders listing (LSUB) * * @param string $root Optional root folder * @param string $name Optional name pattern - * @param mixed $filter Optional filter - * @param string $rights Optional ACL requirements * * @return array List of subscribed folders * @see rcube_imap::list_folders_subscribed() */ - protected function _list_folders_subscribed($root='', $name='*', $filter=null, $rights=null) + public function list_folders_subscribed_direct($root='', $name='*') { - $a_defaults = $a_out = array(); - - // Give plugins a chance to provide a list of folders - $data = rcube::get_instance()->plugins->exec_hook('storage_folders', - array('root' => $root, 'name' => $name, 'filter' => $filter, 'mode' => 'LSUB')); - - if (isset($data['folders'])) { - $a_folders = $data['folders']; - } - else if (!$this->check_connection()) { + if (!$this->check_connection()) { return null; } - else { - // Server supports LIST-EXTENDED, we can use selection options - $config = rcube::get_instance()->config; - // #1486225: Some dovecot versions returns wrong result using LIST-EXTENDED - if (!$config->get('imap_force_lsub') && $this->get_capability('LIST-EXTENDED')) { - // This will also set folder options, LSUB doesn't do that - $a_folders = $this->conn->listMailboxes($root, $name, - NULL, array('SUBSCRIBED')); - // unsubscribe non-existent folders, remove from the list - // we can do this only when LIST response is available - if (is_array($a_folders) && $name == '*' && !empty($this->conn->data['LIST'])) { - foreach ($a_folders as $idx => $folder) { - if (($opts = $this->conn->data['LIST'][$folder]) - && in_array('\\NonExistent', $opts) - ) { + $config = rcube::get_instance()->config; + + // Server supports LIST-EXTENDED, we can use selection options + // #1486225: Some dovecot versions returns wrong result using LIST-EXTENDED + $list_extended = !$config->get('imap_force_lsub') && $this->get_capability('LIST-EXTENDED'); + if ($list_extended) { + // This will also set folder options, LSUB doesn't do that + $a_folders = $this->conn->listMailboxes($root, $name, + NULL, array('SUBSCRIBED')); + } + else { + // retrieve list of folders from IMAP server using LSUB + $a_folders = $this->conn->listSubscribed($root, $name); + } + + if (!is_array($a_folders)) { + return array(); + } + + // #1486796: some server configurations doesn't return folders in all namespaces + if ($root == '' && $name == '*' && $config->get('imap_force_ns')) { + $this->list_folders_update($a_folders, ($list_extended ? 'ext-' : '') . 'subscribed'); + } + + if ($list_extended) { + // unsubscribe non-existent folders, remove from the list + // we can do this only when LIST response is available + if (is_array($a_folders) && $name == '*' && !empty($this->conn->data['LIST'])) { + foreach ($a_folders as $idx => $folder) { + if (($opts = $this->conn->data['LIST'][$folder]) + && in_array('\\NonExistent', $opts) + ) { + $this->conn->unsubscribe($folder); + unset($a_folders[$idx]); + } + } + } + } + else { + // unsubscribe non-existent folders, remove them from the list, + // we can do this only when LIST response is available + if (is_array($a_folders) && $name == '*' && !empty($this->conn->data['LIST'])) { + foreach ($a_folders as $idx => $folder) { + if (!isset($this->conn->data['LIST'][$folder]) + || in_array('\\Noselect', $this->conn->data['LIST'][$folder]) + ) { + // Some servers returns \Noselect for existing folders + if (!$this->folder_exists($folder)) { $this->conn->unsubscribe($folder); unset($a_folders[$idx]); } } } } - // retrieve list of folders from IMAP server using LSUB - else { - $a_folders = $this->conn->listSubscribed($root, $name); - - // unsubscribe non-existent folders, remove them from the list, - // we can do this only when LIST response is available - if (is_array($a_folders) && $name == '*' && !empty($this->conn->data['LIST'])) { - foreach ($a_folders as $idx => $folder) { - if (!isset($this->conn->data['LIST'][$folder]) - || in_array('\\Noselect', $this->conn->data['LIST'][$folder]) - ) { - // Some servers returns \Noselect for existing folders - if (!$this->folder_exists($folder)) { - $this->conn->unsubscribe($folder); - unset($a_folders[$idx]); - } - } - } - } - } - } - - if (!is_array($a_folders) || !sizeof($a_folders)) { - $a_folders = array(); } return $a_folders; @@ -2608,7 +2685,7 @@ } else { // retrieve list of folders from IMAP server - $a_mboxes = $this->_list_folders($root, $name); + $a_mboxes = $this->list_folders_direct($root, $name); } if (!is_array($a_mboxes)) { @@ -2643,7 +2720,7 @@ /** - * protected method for folders listing (LIST) + * Method for direct folders listing (LIST) * * @param string $root Optional root folder * @param string $name Optional name pattern @@ -2651,7 +2728,7 @@ * @return array List of folders * @see rcube_imap::list_folders() */ - protected function _list_folders($root='', $name='*') + public function list_folders_direct($root='', $name='*') { if (!$this->check_connection()) { return null; @@ -2663,50 +2740,71 @@ return array(); } - // #1486796: some server configurations doesn't - // return folders in all namespaces, we'll try to detect that situation - // and ask for these namespaces separately - if ($root == '' && $name == '*') { - $delim = $this->get_hierarchy_delimiter(); - $namespace = $this->get_namespace(); - $search = array(); + $config = rcube::get_instance()->config; - // build list of namespace prefixes - foreach ((array)$namespace as $ns) { - if (is_array($ns)) { - foreach ($ns as $ns_data) { - if (strlen($ns_data[0])) { - $search[] = $ns_data[0]; - } - } - } - } + // #1486796: some server configurations doesn't return folders in all namespaces + if ($root == '' && $name == '*' && $config->get('imap_force_ns')) { + $this->list_folders_update($result); + } - if (!empty($search)) { - // go through all folders detecting namespace usage - foreach ($result as $folder) { - foreach ($search as $idx => $prefix) { - if (strpos($folder, $prefix) === 0) { - unset($search[$idx]); - } - } - if (empty($search)) { - break; - } - } + return $result; + } - // get folders in hidden namespaces and add to the result - foreach ($search as $prefix) { - $list = $this->conn->listMailboxes($prefix, $name); - if (!empty($list)) { - $result = array_merge($result, $list); + /** + * Fix folders list by adding folders from other namespaces. + * Needed on some servers eg. Courier IMAP + * + * @param array $result Reference to folders list + * @param string $type Listing type (ext-subscribed, subscribed or all) + */ + private function list_folders_update(&$result, $type = null) + { + $delim = $this->get_hierarchy_delimiter(); + $namespace = $this->get_namespace(); + $search = array(); + + // build list of namespace prefixes + foreach ((array)$namespace as $ns) { + if (is_array($ns)) { + foreach ($ns as $ns_data) { + if (strlen($ns_data[0])) { + $search[] = $ns_data[0]; } } } } - return $result; + if (!empty($search)) { + // go through all folders detecting namespace usage + foreach ($result as $folder) { + foreach ($search as $idx => $prefix) { + if (strpos($folder, $prefix) === 0) { + unset($search[$idx]); + } + } + if (empty($search)) { + break; + } + } + + // get folders in hidden namespaces and add to the result + foreach ($search as $prefix) { + if ($type == 'ext-subscribed') { + $list = $this->conn->listMailboxes('', $prefix . '*', null, array('SUBSCRIBED')); + } + else if ($type == 'subscribed') { + $list = $this->conn->listSubscribed('', $prefix . '*'); + } + else { + $list = $this->conn->listMailboxes('', $prefix . '*'); + } + + if (!empty($list)) { + $result = array_merge($result, $list); + } + } + } } @@ -2847,11 +2945,11 @@ // get list of subscribed folders if ((strpos($folder, '%') === false) && (strpos($folder, '*') === false)) { - $a_subscribed = $this->_list_folders_subscribed('', $folder . $delm . '*'); + $a_subscribed = $this->list_folders_subscribed('', $folder . $delm . '*'); $subscribed = $this->folder_exists($folder, true); } else { - $a_subscribed = $this->_list_folders_subscribed(); + $a_subscribed = $this->list_folders_subscribed(); $subscribed = in_array($folder, $a_subscribed); } @@ -2919,7 +3017,7 @@ if (strpos($c_mbox, $folder.$delm) === 0) { $this->conn->unsubscribe($c_mbox); if ($this->conn->deleteFolder($c_mbox)) { - $this->clear_message_cache($c_mbox); + $this->clear_message_cache($c_mbox); } } } @@ -3150,8 +3248,9 @@ $cache_key = 'mailboxes.folder-info.' . $folder; $cached = $this->get_cache($cache_key); - if (is_array($cached)) + if (is_array($cached)) { return $cached; + } $acl = $this->get_capability('ACL'); $namespace = $this->get_namespace(); @@ -3188,10 +3287,9 @@ $options['name'] = $folder; $options['attributes'] = $this->folder_attributes($folder, true); $options['namespace'] = $this->folder_namespace($folder); - $options['rights'] = $acl && !$options['is_root'] ? (array)$this->my_rights($folder) : array(); $options['special'] = in_array($folder, $this->default_folders); - // Set 'noselect' and 'norename' flags + // Set 'noselect' flag if (is_array($options['attributes'])) { foreach ($options['attributes'] as $attrib) { $attrib = strtolower($attrib); @@ -3204,6 +3302,12 @@ $options['noselect'] = true; } + // Get folder rights (MYRIGHTS) + if ($acl && ($rights = $this->my_rights($folder))) { + $options['rights'] = $rights; + } + + // Set 'norename' flag if (!empty($options['rights'])) { $options['norename'] = !in_array('x', $options['rights']) && !in_array('d', $options['rights']); @@ -3398,7 +3502,7 @@ return false; } - $this->clear_cache($this->metadata_cache_key($folder)); + $this->clear_cache('mailboxes.metadata.', true); if ($this->get_capability('METADATA') || (!strlen($folder) && $this->get_capability('METADATA-SERVER')) @@ -3432,9 +3536,9 @@ return false; } - $this->clear_cache($this->metadata_cache_key($folder)); + $this->clear_cache('mailboxes.metadata.', true); - if ($this->get_capability('METADATA') || + if ($this->get_capability('METADATA') || (!strlen($folder) && $this->get_capability('METADATA-SERVER')) ) { return $this->conn->deleteMetadata($folder, $entries); @@ -3463,35 +3567,31 @@ */ public function get_metadata($folder, $entries, $options=array()) { - if (!$this->check_connection()) { - return null; - } - $entries = (array)$entries; - // check cached data - $cache_key = $this->metadata_cache_key($folder); - $cached_data = (array)$this->get_cache($cache_key); - $cached_result = array(); - $cached_count = 0; - foreach ($entries as $entry_key) { - if (isset($cached_data[$folder][$entry_key])) { - $cached_result[$folder][$entry_key] = $cached_data[$folder][$entry_key]; - $cached_count++; - } + // create cache key + // @TODO: this is the simplest solution, but we do the same with folders list + // maybe we should store data per-entry and merge on request + sort($options); + sort($entries); + $cache_key = 'mailboxes.metadata.' . $folder; + $cache_key .= '.' . md5(serialize($options).serialize($entries)); + + // get cached data + $cached_data = $this->get_cache($cache_key); + + if (is_array($cached_data)) { + return $cached_data; } - // all requested entries are cached - if ($cached_count == count($entries)) { - return $cached_result; + if (!$this->check_connection()) { + return null; } if ($this->get_capability('METADATA') || (!strlen($folder) && $this->get_capability('METADATA-SERVER')) ) { $res = $this->conn->getMetadata($folder, $entries, $options); - $this->update_cache($cache_key, array_merge_recursive($cached_data, $res)); - return $res; } else if ($this->get_capability('ANNOTATEMORE') || $this->get_capability('ANNOTATEMORE2')) { $queries = array(); @@ -3509,22 +3609,14 @@ $res = array_merge_recursive($res, $result); } } + } - $this->update_cache($cache_key, array_merge_recursive($cached_data, $res)); + if (isset($res)) { + $this->update_cache($cache_key, $res); return $res; } return null; - } - - - /** - * Helper method to compose the cache key for the given folder metadata - */ - protected function metadata_cache_key($folder) - { - $suffix = $folder == '' ? '[SERVER]' : (strpos($folder, '*') === false ? $folder : ''); - return 'mailboxes.metadata.' . $suffix; } @@ -3675,9 +3767,9 @@ { if ($this->messages_caching && !$this->mcache) { $rcube = rcube::get_instance(); - if ($dbh = $rcube->get_dbh()) { + if (($dbh = $rcube->get_dbh()) && ($userid = $rcube->get_user_id())) { $this->mcache = new rcube_imap_cache( - $dbh, $this, $rcube->get_user_id(), $this->options['skip_deleted']); + $dbh, $this, $userid, $this->options['skip_deleted']); } } @@ -3771,12 +3863,12 @@ protected function rsort($folder, $delimiter, &$list, &$out) { while (list($key, $name) = each($list)) { - if (strpos($name, $folder.$delimiter) === 0) { - // set the type of folder name variable (#1485527) - $out[] = (string) $name; - unset($list[$key]); - $this->rsort($name, $delimiter, $list, $out); - } + if (strpos($name, $folder.$delimiter) === 0) { + // set the type of folder name variable (#1485527) + $out[] = (string) $name; + unset($list[$key]); + $this->rsort($name, $delimiter, $list, $out); + } } reset($list); } @@ -3896,6 +3988,29 @@ /** + * Converts date string/object into IMAP date/time format + */ + protected function date_format($date) + { + if (empty($date)) { + return null; + } + + if (!is_object($date) || !is_a($date, 'DateTime')) { + try { + $timestamp = rcube_utils::strtotime($date); + $date = new DateTime("@".$timestamp); + } + catch (Exception $e) { + return null; + } + } + + return $date->format('d-M-Y H:i:s O'); + } + + + /** * This is our own debug handler for the IMAP connection * @access public */ @@ -3954,6 +4069,11 @@ return $this->list_messages($folder, $page, $sort_field, $sort_order, $slice); } + public function get_headers($uid, $folder = null, $force = false) + { + return $this->get_message_headers($uid, $folder, $force); + } + public function mailbox_status($folder = null) { return $this->folder_status($folder); @@ -3999,6 +4119,11 @@ return $this->delete_folder($folder); } + function clear_mailbox($folder = null) + { + return $this->clear_folder($folder); + } + public function mailbox_exists($folder, $subscription=false) { return $this->folder_exists($folder, $subscription); -- Gitblit v1.9.1