CHANGELOG | ●●●●● patch | view | raw | blame | history | |
program/include/rcube_imap.php | ●●●●● patch | view | raw | blame | history | |
program/include/rcube_imap_cache.php | ●●●●● patch | view | raw | blame | history | |
program/include/rcube_imap_generic.php | ●●●●● patch | view | raw | blame | history | |
program/include/rcube_result_index.php | ●●●●● patch | view | raw | blame | history | |
program/include/rcube_result_thread.php | ●●●●● patch | view | raw | blame | history | |
program/steps/mail/func.inc | ●●●●● patch | view | raw | blame | history | |
program/steps/mail/pagenav.inc | ●●●●● patch | view | raw | blame | history | |
program/steps/mail/search.inc | ●●●●● patch | view | raw | blame | history | |
program/steps/mail/sendmail.inc | ●●●●● patch | view | raw | blame | history |
CHANGELOG
@@ -1,6 +1,7 @@ CHANGELOG Roundcube Webmail =========================== - Fix issues with big memory allocation of IMAP results - Replace prompt() with jQuery UI dialog (#1485135) - Fix navigation in messages search results - Improved handling of some malformed values encoded with quoted-printable (#1488232) program/include/rcube_imap.php
@@ -431,40 +431,39 @@ * Save a set of message ids for future message listing methods * * @param string IMAP Search query * @param array List of message ids or NULL if empty * @param rcube_result_index|rcube_result_thread Result set * @param string Charset of search string * @param string Sorting field * @param string True if set is sorted (SORT was used for searching) */ function set_search_set($str=null, $msgs=null, $charset=null, $sort_field=null, $threads=false, $sorted=false) function set_search_set($str=null, $msgs=null, $charset=null, $sort_field=null, $sorted=false) { if (is_array($str) && $msgs == null) list($str, $msgs, $charset, $sort_field, $threads, $sorted) = $str; if ($msgs === false) $msgs = array(); else if ($msgs != null && !is_array($msgs)) $msgs = explode(',', $msgs); if (is_array($str) && $msgs === null) list($str, $msgs, $charset, $sort_field, $sorted) = $str; $this->search_string = $str; $this->search_set = $msgs; $this->search_charset = $charset; $this->search_sort_field = $sort_field; $this->search_threads = $threads; $this->search_sorted = $sorted; $this->search_threads = is_a($this->search_set, 'rcube_result_thread'); } /** * Return the saved search set as hash array * * @param bool $clone Clone result object * * @return array Search set */ function get_search_set() { return array($this->search_string, return array( $this->search_string, $this->search_set, $this->search_charset, $this->search_sort_field, $this->search_threads, $this->search_sorted, ); } @@ -689,10 +688,10 @@ // count search set if ($this->search_string && $mailbox == $this->mailbox && ($mode == 'ALL' || $mode == 'THREADS') && !$force) { if ($this->search_threads) return $mode == 'ALL' ? count((array)$this->search_set['depth']) : count((array)$this->search_set['tree']); if ($mode == 'ALL') return $this->search_set->countMessages(); else return count((array)$this->search_set); return $this->search_set->count(); } $a_mailbox_cache = $this->get_cache('messagecount'); @@ -705,12 +704,13 @@ $a_mailbox_cache[$mailbox] = array(); if ($mode == 'THREADS') { $res = $this->_threadcount($mailbox, $msg_count); $count = $res['count']; $res = $this->fetch_threads($mailbox, $force); $count = $res->count(); if ($status) { $this->set_folder_stats($mailbox, 'cnt', $res['msgcount']); $this->set_folder_stats($mailbox, 'maxuid', $res['maxuid'] ? $this->id2uid($res['maxuid'], $mailbox) : 0); $msg_count = $res->countMessages(); $this->set_folder_stats($mailbox, 'cnt', $msg_count); $this->set_folder_stats($mailbox, 'maxuid', $msg_count ? $this->id2uid($msg_count, $mailbox) : 0); } } // RECENT count is fetched a bit different @@ -721,7 +721,6 @@ else if ($this->skip_deleted) { $search_str = "ALL UNDELETED"; $keys = array('COUNT'); $need_uid = false; if ($mode == 'UNSEEN') { $search_str .= " UNSEEN"; @@ -732,24 +731,23 @@ } if ($status) { $keys[] = 'MAX'; $need_uid = true; } } // @TODO: if $force==false && $mode == 'ALL' we could try to use cache index here // get message count using (E)SEARCH // not very performant but more precise (using UNDELETED) $index = $this->conn->search($mailbox, $search_str, $need_uid, $keys); $count = is_array($index) ? $index['COUNT'] : 0; $index = $this->conn->search($mailbox, $search_str, true, $keys); $count = $index->count(); if ($mode == 'ALL') { if ($this->messages_caching) { // Save additional info required by cache status check $this->icache['undeleted_idx'] = array($mailbox, $index['ALL'], $index['COUNT']); } // Cache index data, will be used in message_index_direct() $this->icache['undeleted_idx'] = $index; if ($status) { $this->set_folder_stats($mailbox, 'cnt', $count); $this->set_folder_stats($mailbox, 'maxuid', is_array($index) ? $index['MAX'] : 0); $this->set_folder_stats($mailbox, 'maxuid', $index->max()); } } } @@ -775,40 +773,6 @@ /** * Private method for getting nr of threads * * @param string $mailbox Folder name * * @returns array Array containing items: 'count' - threads count, * 'msgcount' = messages count, 'maxuid' = max. UID in the set * @access private */ private function _threadcount($mailbox) { $result = array(); if (!empty($this->icache['threads'])) { $dcount = count($this->icache['threads']['depth']); $result = array( 'count' => count($this->icache['threads']['tree']), 'msgcount' => $dcount, 'maxuid' => $dcount ? max(array_keys($this->icache['threads']['depth'])) : 0, ); } else if (is_array($result = $this->fetch_threads($mailbox))) { $dcount = count($result[1]); $result = array( 'count' => count($result[0]), 'msgcount' => $dcount, 'maxuid' => $dcount ? max(array_keys($result[1])) : 0, ); } return $result; } /** * Public method for listing headers * convert mailbox name with root dir first * @@ -817,10 +781,11 @@ * @param string $sort_field Header field to sort by * @param string $sort_order Sort order [ASC|DESC] * @param int $slice Number of slice items to extract from result array * * @return array Indexed array with message header objects * @access public */ function list_headers($mailbox='', $page=NULL, $sort_field=NULL, $sort_order=NULL, $slice=0) public function list_headers($mailbox='', $page=NULL, $sort_field=NULL, $sort_order=NULL, $slice=0) { if (!strlen($mailbox)) { $mailbox = $this->mailbox; @@ -844,110 +809,40 @@ */ private function _list_headers($mailbox='', $page=NULL, $sort_field=NULL, $sort_order=NULL, $slice=0) { if (!strlen($mailbox)) if (!strlen($mailbox)) { return array(); } // use saved message set if ($this->search_string && $mailbox == $this->mailbox) return $this->_list_header_set($mailbox, $page, $sort_field, $sort_order, $slice); if ($this->threading) return $this->_list_thread_headers($mailbox, $page, $sort_field, $sort_order, $slice); $this->_set_sort_order($sort_field, $sort_order); $this->set_sort_order($sort_field, $sort_order); $page = $page ? $page : $this->list_page; // Use messages cache if ($mcache = $this->get_mcache_engine()) { $msg_index = $mcache->get_index($mailbox, $this->sort_field, $this->sort_order); if (empty($msg_index)) return array(); $from = ($page-1) * $this->page_size; $to = $from + $this->page_size; $msg_index = array_values($msg_index); // UIDs $is_uid = true; $sorted = true; if ($from || $to) $msg_index = array_slice($msg_index, $from, $to - $from); if ($slice) $msg_index = array_slice($msg_index, -$slice, $slice); $a_msg_headers = $mcache->get_messages($mailbox, $msg_index); } // retrieve headers from IMAP // use message index sort as default sorting (for better performance) else if (!$this->sort_field) { if ($this->skip_deleted) { // @TODO: this could be cached if ($msg_index = $this->_search_index($mailbox, 'ALL UNDELETED')) { list($begin, $end) = $this->_get_message_range(count($msg_index), $page); $msg_index = array_slice($msg_index, $begin, $end-$begin); } } else if ($max = $this->conn->countMessages($mailbox)) { list($begin, $end) = $this->_get_message_range($max, $page); $msg_index = range($begin+1, $end); } else $msg_index = array(); if ($slice && $msg_index) $msg_index = array_slice($msg_index, ($this->sort_order == 'DESC' ? 0 : -$slice), $slice); // fetch reqested headers from server if ($msg_index) $a_msg_headers = $this->fetch_headers($mailbox, $msg_index); } // use SORT command else if ($this->get_capability('SORT') && // Courier-IMAP provides SORT capability but allows to disable it by admin (#1486959) ($msg_index = $this->conn->sort($mailbox, $this->sort_field, $this->skip_deleted ? 'UNDELETED' : '', true)) !== false ) { if (!empty($msg_index)) { list($begin, $end) = $this->_get_message_range(count($msg_index), $page); $msg_index = array_slice($msg_index, $begin, $end-$begin); $is_uid = true; if ($slice) $msg_index = array_slice($msg_index, ($this->sort_order == 'DESC' ? 0 : -$slice), $slice); // fetch reqested headers from server $a_msg_headers = $this->fetch_headers($mailbox, $msg_index, true); } } // fetch specified header for all messages and sort else if ($msg_index = $this->conn->fetchHeaderIndex($mailbox, "1:*", $this->sort_field, $this->skip_deleted) ) { asort($msg_index); // ASC $msg_index = array_keys($msg_index); list($begin, $end) = $this->_get_message_range(count($msg_index), $page); $msg_index = array_slice($msg_index, $begin, $end-$begin); if ($slice) $msg_index = array_slice($msg_index, ($this->sort_order == 'DESC' ? 0 : -$slice), $slice); // fetch reqested headers from server $a_msg_headers = $this->fetch_headers($mailbox, $msg_index); // use saved message set if ($this->search_string && $mailbox == $this->mailbox) { return $this->_list_header_set($mailbox, $page, $slice); } // return empty array if no messages found if (!is_array($a_msg_headers) || empty($a_msg_headers)) if ($this->threading) { return $this->_list_thread_headers($mailbox, $page, $slice); } // get UIDs of all messages in the folder, sorted $index = $this->message_index($mailbox, $this->sort_field, $this->sort_order); if ($index->isEmpty()) { return array(); } // use this class for message sorting $sorter = new rcube_header_sorter(); $sorter->set_index($msg_index, $is_uid); $sorter->sort_headers($a_msg_headers); $from = ($page-1) * $this->page_size; $to = $from + $this->page_size; if ($this->sort_order == 'DESC' && !$sorted) $a_msg_headers = array_reverse($a_msg_headers); $index->slice($from, $to - $from); if ($slice) $index->slice(-$slice, $slice); // fetch reqested messages headers $a_index = $index->get(); $a_msg_headers = $this->fetch_headers($mailbox, $a_index); return array_values($a_msg_headers); } @@ -958,33 +853,20 @@ * * @param string $mailbox Mailbox/folder name * @param int $page Current page to list * @param string $sort_field Header field to sort by * @param string $sort_order Sort order [ASC|DESC] * @param int $slice Number of slice items to extract from result array * * @return array Indexed array with message header objects * @see rcube_imap::list_headers */ private function _list_thread_headers($mailbox, $page=NULL, $sort_field=NULL, $sort_order=NULL, $slice=0) private function _list_thread_headers($mailbox, $page, $slice=0) { $this->_set_sort_order($sort_field, $sort_order); $page = $page ? $page : $this->list_page; $mcache = $this->get_mcache_engine(); // get all threads (not sorted) if ($mcache) list ($thread_tree, $msg_depth, $has_children) = $mcache->get_thread($mailbox); if ($mcache = $this->get_mcache_engine()) $threads = $mcache->get_thread($mailbox); else list ($thread_tree, $msg_depth, $has_children) = $this->fetch_threads($mailbox); $threads = $this->fetch_threads($mailbox); if (empty($thread_tree)) return array(); $msg_index = $this->sort_threads($mailbox, $thread_tree); return $this->_fetch_thread_headers($mailbox, $thread_tree, $msg_depth, $has_children, $msg_index, $page, $slice); return $this->_fetch_thread_headers($mailbox, $threads, $page, $slice); } @@ -994,7 +876,7 @@ * @param string $mailbox Folder name * @param bool $force Use IMAP server, no cache * * @return array Array with thread data * @return rcube_imap_thread Thread data object */ function fetch_threads($mailbox, $force = false) { @@ -1006,72 +888,50 @@ if (empty($this->icache['threads'])) { // get all threads $result = $this->conn->thread($mailbox, $this->threading, $this->skip_deleted ? 'UNDELETED' : ''); $this->skip_deleted ? 'UNDELETED' : '', true); // add to internal (fast) cache $this->icache['threads'] = array(); $this->icache['threads']['tree'] = is_array($result) ? $result[0] : array(); $this->icache['threads']['depth'] = is_array($result) ? $result[1] : array(); $this->icache['threads']['has_children'] = is_array($result) ? $result[2] : array(); $this->icache['threads'] = $result; } return array( $this->icache['threads']['tree'], $this->icache['threads']['depth'], $this->icache['threads']['has_children'], ); return $this->icache['threads']; } /** * Private method for fetching threaded messages headers * * @param string $mailbox Mailbox name * @param array $thread_tree Thread tree data * @param array $msg_depth Thread depth data * @param array $has_children Thread children data * @param array $msg_index Messages index * @param int $page List page number * @param int $slice Number of threads to slice * @param string $mailbox Mailbox name * @param rcube_result_thread $threads Threads data object * @param int $page List page number * @param int $slice Number of threads to slice * * @return array Messages headers * @access private */ private function _fetch_thread_headers($mailbox, $thread_tree, $msg_depth, $has_children, $msg_index, $page, $slice=0) private function _fetch_thread_headers($mailbox, $threads, $page, $slice=0) { // now get IDs for current page list($begin, $end) = $this->_get_message_range(count($msg_index), $page); $msg_index = array_slice($msg_index, $begin, $end-$begin); // Sort thread structure $this->sort_threads($threads); $from = ($page-1) * $this->page_size; $to = $from + $this->page_size; $threads->slice($from, $to - $from); if ($slice) $msg_index = array_slice($msg_index, ($this->sort_order == 'DESC' ? 0 : -$slice), $slice); $threads->slice(-$slice, $slice); if ($this->sort_order == 'DESC') $msg_index = array_reverse($msg_index); // flatten threads array // @TODO: fetch children only in expanded mode (?) $all_ids = array(); foreach ($msg_index as $root) { $all_ids[] = $root; if (!empty($thread_tree[$root])) $all_ids = array_merge($all_ids, array_keys_recursive($thread_tree[$root])); } // Get UIDs of all messages in all threads $a_index = $threads->get(); // fetch reqested headers from server $a_msg_headers = $this->fetch_headers($mailbox, $all_ids); $a_msg_headers = $this->fetch_headers($mailbox, $a_index); // return empty array if no messages found if (!is_array($a_msg_headers) || empty($a_msg_headers)) return array(); // use this class for message sorting $sorter = new rcube_header_sorter(); $sorter->set_index($all_ids); $sorter->sort_headers($a_msg_headers); unset($a_index); // Set depth, has_children and unread_children fields in headers $this->_set_thread_flags($a_msg_headers, $msg_depth, $has_children); $this->_set_thread_flags($a_msg_headers, $threads); return array_values($a_msg_headers); } @@ -1081,30 +941,31 @@ * Private method for setting threaded messages flags: * depth, has_children and unread_children * * @param array $headers Reference to headers array indexed by message ID * @param array $msg_depth Array of messages depth indexed by message ID * @param array $msg_children Array of messages children flags indexed by message ID * @return array Message headers array indexed by message ID * @param array $headers Reference to headers array indexed by message UID * @param rcube_imap_result $threads Threads data object * * @return array Message headers array indexed by message UID * @access private */ private function _set_thread_flags(&$headers, $msg_depth, $msg_children) private function _set_thread_flags(&$headers, $threads) { $parents = array(); foreach ($headers as $idx => $header) { $id = $header->id; $depth = $msg_depth[$id]; list ($msg_depth, $msg_children) = $threads->getThreadData(); foreach ($headers as $uid => $header) { $depth = $msg_depth[$uid]; $parents = array_slice($parents, 0, $depth); if (!empty($parents)) { $headers[$idx]->parent_uid = end($parents); $headers[$uid]->parent_uid = end($parents); if (empty($header->flags['SEEN'])) $headers[$parents[0]]->unread_children++; } array_push($parents, $header->uid); array_push($parents, $uid); $headers[$idx]->depth = $depth; $headers[$idx]->has_children = $msg_children[$id]; $headers[$uid]->depth = $depth; $headers[$uid]->has_children = $msg_children[$uid]; } } @@ -1112,133 +973,114 @@ /** * Private method for listing a set of message headers (search results) * * @param string $mailbox Mailbox/folder name * @param int $page Current page to list * @param string $sort_field Header field to sort by * @param string $sort_order Sort order [ASC|DESC] * @param int $slice Number of slice items to extract from result array * @param string $mailbox Mailbox/folder name * @param int $page Current page to list * @param int $slice Number of slice items to extract from result array * * @return array Indexed array with message header objects * @access private * @see rcube_imap::list_header_set() */ private function _list_header_set($mailbox, $page=NULL, $sort_field=NULL, $sort_order=NULL, $slice=0) private function _list_header_set($mailbox, $page, $slice=0) { if (!strlen($mailbox) || empty($this->search_set)) if (!strlen($mailbox) || empty($this->search_set) || $this->search_set->isEmpty()) { return array(); } // use saved messages from searching if ($this->threading) return $this->_list_thread_header_set($mailbox, $page, $sort_field, $sort_order, $slice); if ($this->threading) { return $this->_list_thread_header_set($mailbox, $page, $slice); } // search set is threaded, we need a new one if ($this->search_threads) { if (empty($this->search_set['tree'])) return array(); $this->search('', $this->search_string, $this->search_charset, $sort_field); $this->search('', $this->search_string, $this->search_charset, $this->sort_field); } $msgs = $this->search_set; $a_msg_headers = array(); $page = $page ? $page : $this->list_page; $start_msg = ($page-1) * $this->page_size; $index = clone $this->search_set; $from = ($page-1) * $this->page_size; $to = $from + $this->page_size; $this->_set_sort_order($sort_field, $sort_order); // return empty array if no messages found if ($index->isEmpty()) return array(); // quickest method (default sorting) if (!$this->search_sort_field && !$this->sort_field) { if ($sort_order == 'DESC') $msgs = array_reverse($msgs); $got_index = true; } // sorted messages, so we can first slice array and then fetch only wanted headers else if ($this->search_sorted) { // SORT searching result $got_index = true; // reset search set if sorting field has been changed if ($this->sort_field && $this->search_sort_field != $this->sort_field) { $this->search('', $this->search_string, $this->search_charset, $this->sort_field); $index = clone $this->search_set; // return empty array if no messages found if ($index->isEmpty()) return array(); } } if ($got_index) { if ($this->sort_order != $index->getParameters('ORDER')) { $index->revert(); } // get messages uids for one page $msgs = array_slice(array_values($msgs), $start_msg, min(count($msgs)-$start_msg, $this->page_size)); $index->slice($from, $to-$from); if ($slice) $msgs = array_slice($msgs, -$slice, $slice); $index->slice(-$slice, $slice); // fetch headers $a_msg_headers = $this->fetch_headers($mailbox, $msgs); // I didn't found in RFC that FETCH always returns messages sorted by index $sorter = new rcube_header_sorter(); $sorter->set_index($msgs); $sorter->sort_headers($a_msg_headers); $a_index = $index->get(); $a_msg_headers = $this->fetch_headers($mailbox, $a_index); return array_values($a_msg_headers); } // sorted messages, so we can first slice array and then fetch only wanted headers if ($this->search_sorted) { // SORT searching result // reset search set if sorting field has been changed if ($this->sort_field && $this->search_sort_field != $this->sort_field) $msgs = $this->search('', $this->search_string, $this->search_charset, $this->sort_field); // SEARCH result, need sorting $cnt = $index->count(); // 300: experimantal value for best result if (($cnt > 300 && $cnt > $this->page_size) || !$this->sort_field) { // use memory less expensive (and quick) method for big result set $index = clone $this->message_index('', $this->sort_field, $this->sort_order); // get messages uids for one page... $index->slice($start_msg, min($cnt-$from, $this->page_size)); if ($slice) $index->slice(-$slice, $slice); // ...and fetch headers $a_index = $index->get(); $a_msg_headers = $this->fetch_headers($mailbox, $a_index); return array_values($a_msg_headers); } else { // for small result set we can fetch all messages headers $a_index = $index->get(); $a_msg_headers = $this->fetch_headers($mailbox, $a_index, false); // return empty array if no messages found if (empty($msgs)) if (!is_array($a_msg_headers) || empty($a_msg_headers)) return array(); if ($sort_order == 'DESC') $msgs = array_reverse($msgs); // if not already sorted $a_msg_headers = $this->conn->sortHeaders( $a_msg_headers, $this->sort_field, $this->sort_order); // get messages uids for one page $msgs = array_slice(array_values($msgs), $start_msg, min(count($msgs)-$start_msg, $this->page_size)); // 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)); if ($slice) $msgs = array_slice($msgs, -$slice, $slice); $a_msg_headers = array_slice($a_msg_headers, -$slice, $slice); // fetch headers $a_msg_headers = $this->fetch_headers($mailbox, $msgs); $sorter = new rcube_header_sorter(); $sorter->set_index($msgs); $sorter->sort_headers($a_msg_headers); return array_values($a_msg_headers); } else { // SEARCH result, need sorting $cnt = count($msgs); // 300: experimantal value for best result if (($cnt > 300 && $cnt > $this->page_size) || !$this->sort_field) { // use memory less expensive (and quick) method for big result set $a_index = $this->message_index('', $this->sort_field, $this->sort_order); // get messages uids for one page... $msgs = array_slice($a_index, $start_msg, min($cnt-$start_msg, $this->page_size)); if ($slice) $msgs = array_slice($msgs, -$slice, $slice); // ...and fetch headers $a_msg_headers = $this->fetch_headers($mailbox, $msgs); // return empty array if no messages found if (!is_array($a_msg_headers) || empty($a_msg_headers)) return array(); $sorter = new rcube_header_sorter(); $sorter->set_index($msgs); $sorter->sort_headers($a_msg_headers); return array_values($a_msg_headers); } else { // for small result set we can fetch all messages headers $a_msg_headers = $this->fetch_headers($mailbox, $msgs); // return empty array if no messages found if (!is_array($a_msg_headers) || empty($a_msg_headers)) return array(); // if not already sorted $a_msg_headers = $this->conn->sortHeaders( $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), $start_msg, min($cnt-$start_msg, $this->page_size)); if ($slice) $a_msg_headers = array_slice($a_msg_headers, -$slice, $slice); return $a_msg_headers; } return $a_msg_headers; } } @@ -1248,105 +1090,62 @@ * * @param string $mailbox Mailbox/folder name * @param int $page Current page to list * @param string $sort_field Header field to sort by * @param string $sort_order Sort order [ASC|DESC] * @param int $slice Number of slice items to extract from result array * * @return array Indexed array with message header objects * @access private * @see rcube_imap::list_header_set() */ private function _list_thread_header_set($mailbox, $page=NULL, $sort_field=NULL, $sort_order=NULL, $slice=0) private function _list_thread_header_set($mailbox, $page, $slice=0) { // update search_set if previous data was fetched with disabled threading if (!$this->search_threads) { if (empty($this->search_set)) if ($this->search_set->isEmpty()) return array(); $this->search('', $this->search_string, $this->search_charset, $sort_field); $this->search('', $this->search_string, $this->search_charset, $this->sort_field); } // empty result if (empty($this->search_set['tree'])) return array(); $thread_tree = $this->search_set['tree']; $msg_depth = $this->search_set['depth']; $has_children = $this->search_set['children']; $a_msg_headers = array(); $page = $page ? $page : $this->list_page; $start_msg = ($page-1) * $this->page_size; $this->_set_sort_order($sort_field, $sort_order); $msg_index = $this->sort_threads($mailbox, $thread_tree, array_keys($msg_depth)); return $this->_fetch_thread_headers($mailbox, $thread_tree, $msg_depth, $has_children, $msg_index, $page, $slice=0); return $this->_fetch_thread_headers($mailbox, clone $this->search_set, $page, $slice); } /** * Helper function to get first and last index of the requested set * * @param int $max Messages count * @param mixed $page Page number to show, or string 'all' * @return array Array with two values: first index, last index * @access private */ private function _get_message_range($max, $page) { $start_msg = ($page-1) * $this->page_size; if ($page=='all') { $begin = 0; $end = $max; } else if ($this->sort_order=='DESC') { $begin = $max - $this->page_size - $start_msg; $end = $max - $start_msg; } else { $begin = $start_msg; $end = $start_msg + $this->page_size; } if ($begin < 0) $begin = 0; if ($end < 0) $end = $max; if ($end > $max) $end = $max; return array($begin, $end); } /** * Fetches messages headers * Fetches messages headers (by UID) * * @param string $mailbox Mailbox name * @param array $msgs Messages sequence numbers * @param bool $is_uid Enable if $msgs numbers are UIDs * @param array $msgs Message UIDs * @param bool $sort Enables result sorting by $msgs * @param bool $force Disables cache use * * @return array Messages headers indexed by UID * @access private */ function fetch_headers($mailbox, $msgs, $is_uid = false, $force = false) function fetch_headers($mailbox, $msgs, $sort = true, $force = false) { if (empty($msgs)) return array(); if (!$force && ($mcache = $this->get_mcache_engine())) { return $mcache->get_messages($mailbox, $msgs, $is_uid); $headers = $mcache->get_messages($mailbox, $msgs); } else { // fetch reqested headers from server $headers = $this->conn->fetchHeaders( $mailbox, $msgs, true, false, $this->get_fetch_headers()); } // fetch reqested headers from server $index = $this->conn->fetchHeaders( $mailbox, $msgs, $is_uid, false, $this->get_fetch_headers()); if (empty($index)) if (empty($headers)) return array(); foreach ($index as $headers) { $a_msg_headers[$headers->uid] = $headers; foreach ($headers as $h) { $a_msg_headers[$h->uid] = $h; } if ($sort) { // use this class for message sorting $sorter = new rcube_header_sorter(); $sorter->set_index($msgs); $sorter->sort_headers($a_msg_headers); } return $a_msg_headers; @@ -1362,7 +1161,7 @@ * @param string $mailbox Mailbox/folder name * @return int Folder status */ function mailbox_status($mailbox = null) public function mailbox_status($mailbox = null) { if (!strlen($mailbox)) { $mailbox = $this->mailbox; @@ -1425,205 +1224,171 @@ /** * Return sorted array of message IDs (not UIDs) * Return sorted list of message UIDs * * @param string $mailbox Mailbox to get index from * @param string $sort_field Sort column * @param string $sort_order Sort order [ASC, DESC] * @return array Indexed array with message IDs * * @return rcube_result_index|rcube_result_thread List of messages (UIDs) */ function message_index($mailbox='', $sort_field=NULL, $sort_order=NULL) public function message_index($mailbox='', $sort_field=NULL, $sort_order=NULL) { if ($this->threading) return $this->thread_index($mailbox, $sort_field, $sort_order); $this->_set_sort_order($sort_field, $sort_order); $this->set_sort_order($sort_field, $sort_order); if (!strlen($mailbox)) { $mailbox = $this->mailbox; } $key = "{$mailbox}:{$this->sort_field}:{$this->sort_order}:{$this->search_string}.msgi"; // we have a saved search result, get index from there if (!isset($this->icache[$key]) && $this->search_string && !$this->search_threads && $mailbox == $this->mailbox) { // use message index sort as default sorting if (!$this->sort_field) { $msgs = $this->search_set; if ($this->search_sort_field != 'date') sort($msgs); if ($this->sort_order == 'DESC') $this->icache[$key] = array_reverse($msgs); else $this->icache[$key] = $msgs; if ($this->search_string) { if ($this->search_threads) { $this->search($mailbox, $this->search_string, $this->search_charset, $this->sort_field); } // sort with SORT command else if ($this->search_sorted) { if ($this->sort_field && $this->search_sort_field != $this->sort_field) $this->search('', $this->search_string, $this->search_charset, $this->sort_field); if ($this->sort_order == 'DESC') $this->icache[$key] = array_reverse($this->search_set); else $this->icache[$key] = $this->search_set; // use message index sort as default sorting if (!$this->sort_field || $this->search_sorted) { if ($this->sort_field && $this->search_sort_field != $this->sort_field) { $this->search($mailbox, $this->search_string, $this->search_charset, $this->sort_field); } $index = $this->search_set; } else { $a_index = $this->conn->fetchHeaderIndex($mailbox, join(',', $this->search_set), $this->sort_field, $this->skip_deleted); if (is_array($a_index)) { if ($this->sort_order=="ASC") asort($a_index); else if ($this->sort_order=="DESC") arsort($a_index); $this->icache[$key] = array_keys($a_index); } else { $this->icache[$key] = array(); } $index = $this->conn->index($mailbox, $this->search_set->get(), $this->sort_field, $this->skip_deleted, true, true); } } // have stored it in RAM if (isset($this->icache[$key])) return $this->icache[$key]; if ($this->sort_order != $index->getParameters('ORDER')) { $index->revert(); } return $index; } // check local cache if ($mcache = $this->get_mcache_engine()) { $a_index = $mcache->get_index($mailbox, $this->sort_field, $this->sort_order); $this->icache[$key] = array_keys($a_index); $index = $mcache->get_index($mailbox, $this->sort_field, $this->sort_order); } // fetch from IMAP server else { $this->icache[$key] = $this->message_index_direct( $index = $this->message_index_direct( $mailbox, $this->sort_field, $this->sort_order); } return $this->icache[$key]; return $index; } /** * Return sorted array of message IDs (not UIDs) directly from IMAP server. * Doesn't use cache and ignores current search settings. * Return sorted list of message UIDs ignoring current search settings. * Doesn't uses cache by default. * * @param string $mailbox Mailbox to get index from * @param string $sort_field Sort column * @param string $sort_order Sort order [ASC, DESC] * @param bool $skip_cache Disables cache usage * * @return array Indexed array with message IDs * @return rcube_result_index Sorted list of message UIDs */ function message_index_direct($mailbox, $sort_field = null, $sort_order = null) public function message_index_direct($mailbox, $sort_field = null, $sort_order = null, $skip_cache = true) { if (!$skip_cache && ($mcache = $this->get_mcache_engine())) { $index = $mcache->get_index($mailbox, $sort_field, $sort_order); } // use message index sort as default sorting if (!$sort_field) { if ($this->skip_deleted) { $a_index = $this->conn->search($mailbox, 'ALL UNDELETED'); // I didn't found that SEARCH should return sorted IDs if (is_array($a_index)) sort($a_index); } else if ($max = $this->_messagecount($mailbox, 'ALL', true, false)) { $a_index = range(1, $max); else if (!$sort_field) { if ($this->skip_deleted && !empty($this->icache['undeleted_idx']) && $this->icache['undeleted_idx']->getParameters('MAILBOX') == $mailbox ) { $index = $this->icache['undeleted_idx']; } if ($a_index !== false && $sort_order == 'DESC') $a_index = array_reverse($a_index); else { $index = $this->conn->search($mailbox, 'ALL' .($this->skip_deleted ? ' UNDELETED' : ''), true); } } // fetch complete message index else if ($this->get_capability('SORT') && ($a_index = $this->conn->sort($mailbox, $sort_field, $this->skip_deleted ? 'UNDELETED' : '')) !== false ) { if ($sort_order == 'DESC') $a_index = array_reverse($a_index); } else if ($a_index = $this->conn->fetchHeaderIndex( $mailbox, "1:*", $sort_field, $skip_deleted)) { if ($sort_order=="ASC") asort($a_index); else if ($sort_order=="DESC") arsort($a_index); else { if ($this->get_capability('SORT')) { $index = $this->conn->sort($mailbox, $sort_field, $this->skip_deleted ? 'UNDELETED' : '', true); } $a_index = array_keys($a_index); if (empty($index) || $index->isError()) { $index = $this->conn->index($mailbox, "1:*", $sort_field, $this->skip_deleted, false, true); } } return $a_index !== false ? $a_index : array(); if ($sort_order != $index->getParameters('ORDER')) { $index->revert(); } return $index; } /** * Return sorted array of threaded message IDs (not UIDs) * Return index of threaded message UIDs * * @param string $mailbox Mailbox to get index from * @param string $sort_field Sort column * @param string $sort_order Sort order [ASC, DESC] * @return array Indexed array with message IDs * * @return rcube_result_thread Message UIDs */ function thread_index($mailbox='', $sort_field=NULL, $sort_order=NULL) { $this->_set_sort_order($sort_field, $sort_order); if (!strlen($mailbox)) { $mailbox = $this->mailbox; } $key = "{$mailbox}:{$this->sort_field}:{$this->sort_order}:{$this->search_string}.thi"; // we have a saved search result, get index from there if (!isset($this->icache[$key]) && $this->search_string && $this->search_threads && $mailbox == $this->mailbox) { // use message IDs for better performance $ids = array_keys_recursive($this->search_set['tree']); $this->icache[$key] = $this->_flatten_threads($mailbox, $this->search_set['tree'], $ids); if ($this->search_string && $this->search_threads && $mailbox == $this->mailbox) { $threads = $this->search_set; } else { // get all threads (default sort order) $threads = $this->fetch_threads($mailbox); } // have stored it in RAM if (isset($this->icache[$key])) return $this->icache[$key]; $this->set_sort_order($sort_field, $sort_order); $this->sort_threads($threads); // get all threads (default sort order) list ($thread_tree) = $this->fetch_threads($mailbox); $this->icache[$key] = $this->_flatten_threads($mailbox, $thread_tree); return $this->icache[$key]; return $threads; } /** * Return array of threaded messages (all, not only roots) * Sort threaded result, using THREAD=REFS method * * @param string $mailbox Mailbox to get index from * @param array $thread_tree Threaded messages array (see fetch_threads()) * @param array $ids Message IDs if we know what we need (e.g. search result) * for better performance * @return array Indexed array with message IDs * * @access private * @param rcube_result_thread $threads Threads result set */ private function _flatten_threads($mailbox, $thread_tree, $ids=null) private function sort_threads($threads) { if (empty($thread_tree)) return array(); $msg_index = $this->sort_threads($mailbox, $thread_tree, $ids); if ($this->sort_order == 'DESC') $msg_index = array_reverse($msg_index); // flatten threads array $all_ids = array(); foreach ($msg_index as $root) { $all_ids[] = $root; if (!empty($thread_tree[$root])) { foreach (array_keys_recursive($thread_tree[$root]) as $val) $all_ids[] = $val; } if ($threads->isEmpty()) { return; } return $all_ids; // THREAD=ORDEREDSUBJECT: sorting by sent date of root message // THREAD=REFERENCES: sorting by sent date of root message // THREAD=REFS: sorting by the most recent date in each thread if ($this->sort_field && ($this->sort_field != 'date' || $this->get_capability('THREAD') != 'REFS')) { $index = $this->message_index_direct($this->mailbox, $this->sort_field, $this->sort_order, false); if (!$index->isEmpty()) { $threads->sort($index); } } else { if ($this->sort_order != $threads->getParameters('ORDER')) { $threads->revert(); } } } @@ -1634,13 +1399,12 @@ * @param string $str Search criteria * @param string $charset Search charset * @param string $sort_field Header field to sort by * @return array search results as list of message IDs * @access public */ function search($mailbox='', $str=NULL, $charset=NULL, $sort_field=NULL) function search($mailbox='', $str='ALL', $charset=NULL, $sort_field=NULL) { if (!$str) return false; $str = 'ALL'; if (!strlen($mailbox)) { $mailbox = $this->mailbox; @@ -1648,10 +1412,8 @@ $results = $this->_search_index($mailbox, $str, $charset, $sort_field); $this->set_search_set($str, $results, $charset, $sort_field, (bool)$this->threading, $this->set_search_set($str, $results, $charset, $sort_field, $this->threading || $this->search_sorted ? true : false); return $results; } @@ -1663,7 +1425,7 @@ * @param string $charset Charset * @param string $sort_field Sorting field * * @return array search results as list of message ids * @return rcube_result_index|rcube_result_thread Search results (UIDs) * @see rcube_imap::search() */ private function _search_index($mailbox, $criteria='ALL', $charset=NULL, $sort_field=NULL) @@ -1674,63 +1436,44 @@ $criteria = 'UNDELETED '.$criteria; if ($this->threading) { $a_messages = $this->conn->thread($mailbox, $this->threading, $criteria, $charset); $threads = $this->conn->thread($mailbox, $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 ($a_messages === false && $charset && $charset != 'US-ASCII') $a_messages = $this->conn->thread($mailbox, $this->threading, $this->convert_criteria($criteria, $charset), 'US-ASCII'); if ($threads->isError() && $charset && $charset != 'US-ASCII') $threads = $this->conn->thread($mailbox, $this->threading, $this->convert_criteria($criteria, $charset), true, 'US-ASCII'); if ($a_messages !== false) { list ($thread_tree, $msg_depth, $has_children) = $a_messages; $a_messages = array( 'tree' => $thread_tree, 'depth'=> $msg_depth, 'children' => $has_children ); } return $a_messages; return $threads; } if ($sort_field && $this->get_capability('SORT')) { $charset = $charset ? $charset : $this->default_charset; $a_messages = $this->conn->sort($mailbox, $sort_field, $criteria, false, $charset); $charset = $charset ? $charset : $this->default_charset; $messages = $this->conn->sort($mailbox, $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 ($a_messages === false && $charset && $charset != 'US-ASCII') $a_messages = $this->conn->sort($mailbox, $sort_field, $this->convert_criteria($criteria, $charset), false, 'US-ASCII'); if ($messages->isError() && $charset && $charset != 'US-ASCII') $messages = $this->conn->sort($mailbox, $sort_field, $this->convert_criteria($criteria, $charset), true, 'US-ASCII'); if ($a_messages !== false) { if (!$messages->isError()) { $this->search_sorted = true; return $a_messages; return $messages; } } if ($orig_criteria == 'ALL') { $max = $this->_messagecount($mailbox, 'ALL', true, false); $a_messages = $max ? range(1, $max) : array(); } else { $a_messages = $this->conn->search($mailbox, ($charset ? "CHARSET $charset " : '') . $criteria); $messages = $this->conn->search($mailbox, ($charset ? "CHARSET $charset " : '') . $criteria, true); // Error, try with US-ASCII (some servers may support only US-ASCII) if ($a_messages === false && $charset && $charset != 'US-ASCII') $a_messages = $this->conn->search($mailbox, 'CHARSET US-ASCII ' . $this->convert_criteria($criteria, $charset)); // I didn't found that SEARCH should return sorted IDs if (is_array($a_messages) && !$this->sort_field) sort($a_messages); } // Error, try with US-ASCII (some servers may support only US-ASCII) if ($messages->isError() && $charset && $charset != 'US-ASCII') $messages = $this->conn->search($mailbox, 'CHARSET US-ASCII ' . $this->convert_criteria($criteria, $charset), true); $this->search_sorted = false; return $a_messages; return $messages; } @@ -1742,18 +1485,20 @@ * @param string $str Search string * @param boolean $ret_uid True if UIDs should be returned * * @return array Search results as list of message IDs or UIDs * @return rcube_result_index Search result (UIDs) */ function search_once($mailbox='', $str=NULL, $ret_uid=false) function search_once($mailbox='', $str='ALL') { if (!$str) return false; return 'ALL'; if (!strlen($mailbox)) { $mailbox = $this->mailbox; } return $this->conn->search($mailbox, $str, $ret_uid); $index = $this->conn->search($mailbox, $str, true); return $index; } @@ -1791,139 +1536,33 @@ /** * Sort thread * * @param string $mailbox Mailbox name * @param array $thread_tree Unsorted thread tree (rcube_imap_generic::thread() result) * @param array $ids Message IDs if we know what we need (e.g. search result) * * @return array Sorted roots IDs */ function sort_threads($mailbox, $thread_tree, $ids = null) { // THREAD=ORDEREDSUBJECT: sorting by sent date of root message // THREAD=REFERENCES: sorting by sent date of root message // THREAD=REFS: sorting by the most recent date in each thread // default sorting if (!$this->sort_field || ($this->sort_field == 'date' && $this->threading == 'REFS')) { return array_keys((array)$thread_tree); } // here we'll implement REFS sorting else { if ($mcache = $this->get_mcache_engine()) { $a_index = $mcache->get_index($mailbox, $this->sort_field, 'ASC'); if (is_array($a_index)) { $a_index = array_keys($a_index); // now we must remove IDs that doesn't exist in $ids if (!empty($ids)) $a_index = array_intersect($a_index, $ids); } } // use SORT command else if ($this->get_capability('SORT') && ($a_index = $this->conn->sort($mailbox, $this->sort_field, !empty($ids) ? $ids : ($this->skip_deleted ? 'UNDELETED' : ''))) !== false ) { // do nothing } else { // fetch specified headers for all messages and sort them $a_index = $this->conn->fetchHeaderIndex($mailbox, !empty($ids) ? $ids : "1:*", $this->sort_field, $this->skip_deleted); // return unsorted tree if we've got no index data if (!empty($a_index)) { asort($a_index); // ASC $a_index = array_values($a_index); } } if (empty($a_index)) return array_keys((array)$thread_tree); return $this->_sort_thread_refs($thread_tree, $a_index); } } /** * THREAD=REFS sorting implementation * * @param array $tree Thread tree array (message identifiers as keys) * @param array $index Array of sorted message identifiers * * @return array Array of sorted roots messages */ private function _sort_thread_refs($tree, $index) { if (empty($tree)) return array(); $index = array_combine(array_values($index), $index); // assign roots foreach ($tree as $idx => $val) { $index[$idx] = $idx; if (!empty($val)) { $idx_arr = array_keys_recursive($tree[$idx]); foreach ($idx_arr as $subidx) $index[$subidx] = $idx; } } $index = array_values($index); // create sorted array of roots $msg_index = array(); if ($this->sort_order != 'DESC') { foreach ($index as $idx) if (!isset($msg_index[$idx])) $msg_index[$idx] = $idx; $msg_index = array_values($msg_index); } else { for ($x=count($index)-1; $x>=0; $x--) if (!isset($msg_index[$index[$x]])) $msg_index[$index[$x]] = $index[$x]; $msg_index = array_reverse($msg_index); } return $msg_index; } /** * Refresh saved search set * * @return array Current search set */ function refresh_search() { if (!empty($this->search_string)) $this->search_set = $this->search('', $this->search_string, $this->search_charset, $this->search_sort_field, $this->search_threads, $this->search_sorted); if (!empty($this->search_string)) { $this->search('', $this->search_string, $this->search_charset, $this->search_sort_field); } return $this->get_search_set(); } /** * Check if the given message ID is part of the current search set * Check if the given message UID is part of the current search set * * @param string $msgid Message id * @param string $msgid Message UID * * @return boolean True on match or if no search request is stored */ function in_searchset($msgid) function in_searchset($uid) { if (!empty($this->search_string)) { if ($this->search_threads) return isset($this->search_set['depth']["$msgid"]); else return in_array("$msgid", (array)$this->search_set, true); return $this->search_set->exists($uid); } else return true; return true; } @@ -1981,7 +1620,7 @@ // message doesn't exist? if (empty($headers)) return null; return null; // structure might be cached if (!empty($headers->structure)) @@ -2656,14 +2295,8 @@ // threads are too complicated to just remove messages from set if ($this->search_threads || $all_mode) $this->refresh_search(); else { $a_uids = explode(',', $uids); foreach ($a_uids as $uid) $a_mids[] = $this->uid2id($uid, $from_mbox); $this->search_set = array_diff($this->search_set, $a_mids); } unset($a_mids); unset($a_uids); else $this->search_set->filter(explode(',', $uids)); } // remove cached messages @@ -2756,14 +2389,8 @@ // threads are too complicated to just remove messages from set if ($this->search_threads || $all_mode) $this->refresh_search(); else { $a_uids = explode(',', $uids); foreach ($a_uids as $uid) $a_mids[] = $this->uid2id($uid, $mailbox); $this->search_set = array_diff($this->search_set, $a_mids); unset($a_uids); unset($a_mids); } else $this->search_set->filter(explode(',', $uids)); } // remove cached messages @@ -2882,19 +2509,8 @@ $all = true; } // get UIDs from current search set // @TODO: skip fetchUIDs() and work with IDs instead of UIDs (?) else { if ($this->search_threads) $uids = $this->conn->fetchUIDs($mailbox, array_keys($this->search_set['depth'])); else $uids = $this->conn->fetchUIDs($mailbox, $this->search_set); // save ID-to-UID mapping in local cache if (is_array($uids)) foreach ($uids as $id => $uid) $this->uid_id_map[$mailbox][$uid] = $id; $uids = join(',', $uids); $uids = join(',', $this->search_set->get()); } } else { @@ -2907,43 +2523,6 @@ return array($uids, (bool) $all); } /** * Translate UID to message ID * * @param int $uid Message UID * @param string $mailbox Mailbox name * * @return int Message ID */ function get_id($uid, $mailbox=null) { if (!strlen($mailbox)) { $mailbox = $this->mailbox; } return $this->uid2id($uid, $mailbox); } /** * Translate message number to UID * * @param int $id Message ID * @param string $mailbox Mailbox name * * @return int Message UID */ function get_uid($id, $mailbox=null) { if (!strlen($mailbox)) { $mailbox = $this->mailbox; } return $this->id2uid($id, $mailbox); } /* -------------------------------- @@ -3592,9 +3171,10 @@ $data = $this->conn->data; // add (E)SEARCH result for ALL UNDELETED query if (!empty($this->icache['undeleted_idx']) && $this->icache['undeleted_idx'][0] == $mailbox) { $data['ALL_UNDELETED'] = $this->icache['undeleted_idx'][1]; $data['COUNT_UNDELETED'] = $this->icache['undeleted_idx'][2]; if (!empty($this->icache['undeleted_idx']) && $this->icache['undeleted_idx']->getParameters('MAILBOX') == $mailbox ) { $data['UNDELETED'] = $this->icache['undeleted_idx']; } return $data; @@ -4268,23 +3848,6 @@ } /** * Convert body charset to RCMAIL_CHARSET according to the ctype_parameters * * @param string $body Part body to decode * @param string $ctype_param Charset to convert from * @return string Content converted to internal charset */ function charset_decode($body, $ctype_param) { if (is_array($ctype_param) && !empty($ctype_param['charset'])) return rcube_charset_convert($body, $ctype_param['charset']); // defaults to what is specified in the class header return rcube_charset_convert($body, $this->default_charset); } /* -------------------------------- * private methods * --------------------------------*/ @@ -4296,7 +3859,7 @@ * @param string $sort_order Sort order * @access private */ private function _set_sort_order($sort_field, $sort_order) private function set_sort_order($sort_field, $sort_order) { if ($sort_field != null) $this->sort_field = asciiwords($sort_field); @@ -4366,46 +3929,14 @@ /** * Finds message sequence ID for specified UID * * @param int $uid Message UID * @param string $mailbox Mailbox name * @param bool $force True to skip cache * * @return int Message (sequence) ID */ function uid2id($uid, $mailbox = null, $force = false) { if (!strlen($mailbox)) { $mailbox = $this->mailbox; } if (!empty($this->uid_id_map[$mailbox][$uid])) { return $this->uid_id_map[$mailbox][$uid]; } if (!$force && ($mcache = $this->get_mcache_engine())) $id = $mcache->uid2id($mailbox, $uid); if (empty($id)) $id = $this->conn->UID2ID($mailbox, $uid); $this->uid_id_map[$mailbox][$uid] = $id; return $id; } /** * Find UID of the specified message sequence ID * * @param int $id Message (sequence) ID * @param string $mailbox Mailbox name * @param bool $force True to skip cache * * @return int Message UID */ function id2uid($id, $mailbox = null, $force = false) function id2uid($id, $mailbox = null) { if (!strlen($mailbox)) { $mailbox = $this->mailbox; @@ -4415,11 +3946,7 @@ return $uid; } if (!$force && ($mcache = $this->get_mcache_engine())) $uid = $mcache->id2uid($mailbox, $id); if (empty($uid)) $uid = $this->conn->ID2UID($mailbox, $id); $uid = $this->conn->ID2UID($mailbox, $id); $this->uid_id_map[$mailbox][$uid] = $id; @@ -4705,24 +4232,19 @@ */ class rcube_header_sorter { private $seqs = array(); private $uids = array(); /** * Set the predetermined sort order. * * @param array $index Numerically indexed array of IMAP ID or UIDs * @param bool $is_uid Set to true if $index contains UIDs * @param array $index Numerically indexed array of IMAP UIDs */ function set_index($index, $is_uid = false) function set_index($index) { $index = array_flip($index); if ($is_uid) $this->uids = $index; else $this->seqs = $index; $this->uids = $index; } /** @@ -4732,30 +4254,7 @@ */ function sort_headers(&$headers) { if (!empty($this->uids)) uksort($headers, array($this, "compare_uids")); else uasort($headers, array($this, "compare_seqnums")); } /** * Sort method called by uasort() * * @param rcube_mail_header $a * @param rcube_mail_header $b */ function compare_seqnums($a, $b) { // First get the sequence number from the header object (the 'id' field). $seqa = $a->id; $seqb = $b->id; // then find each sequence number in my ordered list $posa = isset($this->seqs[$seqa]) ? intval($this->seqs[$seqa]) : -1; $posb = isset($this->seqs[$seqb]) ? intval($this->seqs[$seqb]) : -1; // return the relative position as the comparison value return $posa - $posb; uksort($headers, array($this, "compare_uids")); } /** program/include/rcube_imap_cache.php
@@ -108,7 +108,7 @@ /** * Return (sorted) messages index. * Return (sorted) messages index (UIDs). * If index doesn't exist or is invalid, will be updated. * * @param string $mailbox Folder name @@ -122,22 +122,22 @@ { if (empty($this->icache[$mailbox])) $this->icache[$mailbox] = array(); console('cache::get_index'); $sort_order = strtoupper($sort_order) == 'ASC' ? 'ASC' : 'DESC'; // 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('result', $this->icache[$mailbox]['index'])) { if (!array_key_exists('object', $this->icache[$mailbox]['index'])) { $index = $this->icache[$mailbox]['index']; } // We've got a valid index else if ($sort_field == 'ANY' || $this->icache[$mailbox]['index']['sort_field'] == $sort_field ) { if ($this->icache[$mailbox]['index']['sort_order'] == $sort_order) return $this->icache[$mailbox]['index']['result']; else return array_reverse($this->icache[$mailbox]['index']['result'], true); else if ($sort_field == 'ANY' || $this->icache[$mailbox]['index']['sort_field'] == $sort_field) { $result = $this->icache[$mailbox]['index']['object']; if ($result->getParameters('ORDER') != $sort_order) { $result->revert(); } return $result; } } @@ -173,13 +173,13 @@ else { $is_valid = $this->validate($mailbox, $index, $exists); } console("valid:".$is_valid); if ($is_valid) { // build index, assign sequence IDs to unique IDs $data = array_combine($index['seq'], $index['uid']); $data = $index['object']; // revert the order if needed if ($index['sort_order'] != $sort_order) $data = array_reverse($data, true); if ($data->getParameters('ORDER') != $sort_order) { $data->revert(); } } } else { @@ -201,14 +201,12 @@ $data = $this->get_index_data($mailbox, $sort_field, $sort_order, $mbox_data); // insert/update $this->add_index_row($mailbox, $sort_field, $sort_order, $data, $mbox_data, $exists, $index['modseq']); $this->add_index_row($mailbox, $sort_field, $data, $mbox_data, $exists, $index['modseq']); } $this->icache[$mailbox]['index'] = array( 'result' => $data, 'object' => $data, 'sort_field' => $sort_field, 'sort_order' => $sort_order, 'modseq' => !empty($index['modseq']) ? $index['modseq'] : $mbox_data['HIGHESTMODSEQ'] ); @@ -233,11 +231,7 @@ // Seek in internal cache if (array_key_exists('thread', $this->icache[$mailbox])) { return array( $this->icache[$mailbox]['thread']['tree'], $this->icache[$mailbox]['thread']['depth'], $this->icache[$mailbox]['thread']['children'], ); return $this->icache[$mailbox]['thread']['object']; } // Get thread from DB (if DB wasn't already queried) @@ -249,8 +243,6 @@ // get_thread() is called more than once or after clear() $this->icache[$mailbox]['thread_queried'] = true; } $data = null; // Entry exist, check cache status if (!empty($index)) { @@ -269,22 +261,21 @@ if ($mbox_data['EXISTS']) { // get all threads (default sort order) list ($thread_tree, $msg_depth, $has_children) = $this->imap->fetch_threads($mailbox, true); $threads = $this->imap->fetch_threads($mailbox, true); } else { $threads = new rcube_imap_result($mailbox, ''); } $index = array( 'tree' => !empty($thread_tree) ? $thread_tree : array(), 'depth' => !empty($msg_depth) ? $msg_depth : array(), 'children' => !empty($has_children) ? $has_children : array(), ); $index['object'] = $threads; // insert/update $this->add_thread_row($mailbox, $index, $mbox_data, $exists); $this->add_thread_row($mailbox, $threads, $mbox_data, $exists); } $this->icache[$mailbox]['thread'] = $index; return array($index['tree'], $index['depth'], $index['children']); return $index['object']; } @@ -292,27 +283,14 @@ * Returns list of messages (headers). See rcube_imap::fetch_headers(). * * @param string $mailbox Folder name * @param array $msgs Message sequence numbers * @param bool $is_uid True if $msgs contains message UIDs * @param array $msgs Message UIDs * * @return array The list of messages (rcube_mail_header) indexed by UID */ function get_messages($mailbox, $msgs = array(), $is_uid = true) function get_messages($mailbox, $msgs = array()) { if (empty($msgs)) { return array(); } // @TODO: it would be nice if we could work with UIDs only // then index would be not needed. For now we need it to // map id to uid here and to update message id for cached message // Convert IDs to UIDs $index = $this->get_index($mailbox, 'ANY'); if (!$is_uid) { foreach ($msgs as $idx => $msgid) if ($uid = $index[$msgid]) $msgs[$idx] = $uid; } // Fetch messages from cache @@ -334,10 +312,6 @@ // save memory, we don't need message body here (?) $result[$uid]->body = null; // update message ID according to index data if (!empty($index) && ($id = array_search($uid, $index))) $result[$uid]->id = $id; if (!empty($result[$uid])) { unset($msgs[$uid]); } @@ -345,7 +319,7 @@ // Fetch not found messages from IMAP server if (!empty($msgs)) { $messages = $this->imap->fetch_headers($mailbox, array_keys($msgs), true, true); $messages = $this->imap->fetch_headers($mailbox, array_keys($msgs), false, true); // Insert to DB and add to result list if (!empty($messages)) { @@ -391,11 +365,6 @@ if ($sql_arr = $this->db->fetch_assoc($sql_result)) { $message = $this->build_message($sql_arr); $found = true; // update message ID according to index data $index = $this->get_index($mailbox, 'ANY'); if (!empty($index) && ($id = array_search($uid, $index))) $message->id = $id; } // Get the message from IMAP server @@ -617,45 +586,11 @@ /** * @param string $mailbox Folder name * @param int $id Message (sequence) ID * * @return int Message UID */ function id2uid($mailbox, $id) { if (!empty($this->icache['pending_index_update'])) return null; // get index if it exists $index = $this->get_index($mailbox, 'ANY', null, true); return $index[$id]; } /** * @param string $mailbox Folder name * @param int $uid Message UID * * @return int Message (sequence) ID */ function uid2id($mailbox, $uid) { if (!empty($this->icache['pending_index_update'])) return null; // get index if it exists $index = $this->get_index($mailbox, 'ANY', null, true); return array_search($uid, (array)$index); } /** * Fetches index data from database */ private function get_index_row($mailbox) { console('cache::get_index_row'); // Get index from DB $sql_result = $this->db->query( "SELECT data, valid" @@ -665,18 +600,22 @@ $this->userid, $mailbox); if ($sql_arr = $this->db->fetch_assoc($sql_result)) { $data = explode('@', $sql_arr['data']); $data = explode('@', $sql_arr['data']); $index = @unserialize($data[0]); unset($data[0]); if (empty($index)) { $index = new rcube_result_index($mailbox); } return array( 'valid' => $sql_arr['valid'], 'seq' => explode(',', $data[0]), 'uid' => explode(',', $data[1]), 'sort_field' => $data[2], 'sort_order' => $data[3], 'deleted' => $data[4], 'validity' => $data[5], 'uidnext' => $data[6], 'modseq' => $data[7], 'object' => $index, 'sort_field' => $data[1], 'deleted' => $data[2], 'validity' => $data[3], 'uidnext' => $data[4], 'modseq' => $data[5], ); } @@ -698,20 +637,16 @@ $this->userid, $mailbox); if ($sql_arr = $this->db->fetch_assoc($sql_result)) { $data = explode('@', $sql_arr['data']); $data = explode('@', $sql_arr['data']); $thread = @unserialize($data[0]); unset($data[0]); // Uncompress data, see add_thread_row() // $data[0] = str_replace(array('*', '^', '#'), array(';a:0:{}', 'i:', ';a:1:'), $data[0]); $data[0] = unserialize($data[0]); // build 'depth' and 'children' arrays $depth = $children = array(); $this->build_thread_data($data[0], $depth, $children); if (empty($thread)) { $thread = new rcube_imap_result($mailbox); } return array( 'tree' => $data[0], 'depth' => $depth, 'children' => $children, 'object' => $thread, 'deleted' => $data[1], 'validity' => $data[2], 'uidnext' => $data[3], @@ -725,14 +660,12 @@ /** * Saves index data into database */ private function add_index_row($mailbox, $sort_field, $sort_order, $data = array(), $mbox_data = array(), $exists = false, $modseq = null) private function add_index_row($mailbox, $sort_field, $data, $mbox_data = array(), $exists = false, $modseq = null) { $data = array( implode(',', array_keys($data)), implode(',', array_values($data)), serialize($data), $sort_field, $sort_order, (int) $this->skip_deleted, (int) $mbox_data['UIDVALIDITY'], (int) $mbox_data['UIDNEXT'], @@ -759,14 +692,10 @@ /** * Saves thread data into database */ private function add_thread_row($mailbox, $data = array(), $mbox_data = array(), $exists = false) private function add_thread_row($mailbox, $data, $mbox_data = array(), $exists = false) { $tree = serialize($data['tree']); // This significantly reduces data length // $tree = str_replace(array(';a:0:{}', 'i:', ';a:1:'), array('*', '^', '#'), $tree); $data = array( $tree, serialize($data), (int) $this->skip_deleted, (int) $mbox_data['UIDVALIDITY'], (int) $mbox_data['UIDNEXT'], @@ -794,7 +723,8 @@ */ private function validate($mailbox, $index, &$exists = true) { $is_thread = isset($index['tree']); $object = $index['object']; $is_thread = is_a($object, 'rcube_result_thread'); // Get mailbox data (UIDVALIDITY, counters, etc.) for status check $mbox_data = $this->imap->mailbox_data($mailbox); @@ -814,21 +744,21 @@ // Folder is empty but cache isn't if (empty($mbox_data['EXISTS'])) { if (!empty($index['seq']) || !empty($index['tree'])) { if (!$object->isEmpty()) { $this->clear($mailbox); $exists = false; return false; } } // Folder is not empty but cache is else if (empty($index['seq']) && empty($index['tree'])) { else if ($object->isEmpty()) { unset($this->icache[$mailbox][$is_thread ? 'thread' : 'index']); return false; } // Validation flag if (!$is_thread && empty($index['valid'])) { unset($this->icache[$mailbox][$is_thread ? 'thread' : 'index']); unset($this->icache[$mailbox]['index']); return false; } @@ -853,7 +783,7 @@ // @TODO: find better validity check for threaded index if ($is_thread) { // check messages number... if (!$this->skip_deleted && $mbox_data['EXISTS'] != @max(array_keys($index['depth']))) { if (!$this->skip_deleted && $mbox_data['EXISTS'] != $object->countMessages()) { return false; } return true; @@ -862,14 +792,15 @@ // The rest of checks, more expensive if (!empty($this->skip_deleted)) { // compare counts if available if ($mbox_data['COUNT_UNDELETED'] != null && $mbox_data['COUNT_UNDELETED'] != count($index['uid'])) { if (!empty($mbox_data['UNDELETED']) && $mbox_data['UNDELETED']->count() != $object->count() ) { return false; } // compare UID sets if ($mbox_data['ALL_UNDELETED'] != null) { $uids_new = rcube_imap_generic::uncompressMessageSet($mbox_data['ALL_UNDELETED']); $uids_old = $index['uid']; if (!empty($mbox_data['UNDELETED'])) { $uids_new = $mbox_data['UNDELETED']->get(); $uids_old = $object->get(); if (count($uids_new) != count($uids_old)) { return false; @@ -884,20 +815,20 @@ else { // get all undeleted messages excluding cached UIDs $ids = $this->imap->search_once($mailbox, 'ALL UNDELETED NOT UID '. rcube_imap_generic::compressMessageSet($index['uid'])); rcube_imap_generic::compressMessageSet($object->get())); if (!empty($ids)) { if (!$ids->isEmpty()) { return false; } } } else { // check messages number... if ($mbox_data['EXISTS'] != max($index['seq'])) { if ($mbox_data['EXISTS'] != $object->count()) { return false; } // ... and max UID if (max($index['uid']) != $this->imap->id2uid($mbox_data['EXISTS'], $mailbox, true)) { if ($object->max() != $this->imap->id2uid($mbox_data['EXISTS'], $mailbox, true)) { return false; } } @@ -1060,7 +991,7 @@ } $sort_field = $index['sort_field']; $sort_order = $index['sort_order']; $sort_order = $index['object']->getParameters('ORDER'); $exists = true; // Validate index @@ -1069,14 +1000,14 @@ $data = $this->get_index_data($mailbox, $sort_field, $sort_order, $mbox_data); } else { $data = array_combine($index['seq'], $index['uid']); $data = $index['object']; } // update index and/or HIGHESTMODSEQ value $this->add_index_row($mailbox, $sort_field, $sort_order, $data, $mbox_data, $exists); $this->add_index_row($mailbox, $sort_field, $data, $mbox_data, $exists); // update internal cache for get_index() $this->icache[$mailbox]['index']['result'] = $data; $this->icache[$mailbox]['index']['object'] = $data; } @@ -1099,20 +1030,6 @@ } return $message; } /** * Creates 'depth' and 'children' arrays from stored thread 'tree' data. */ private function build_thread_data($data, &$depth, &$children, $level = 0) { foreach ((array)$data as $key => $val) { $children[$key] = !empty($val); $depth[$key] = $level; if (!empty($val)) $this->build_thread_data($val, $depth, $children, $level + 1); } } @@ -1170,43 +1087,18 @@ */ private function get_index_data($mailbox, $sort_field, $sort_order, $mbox_data = array()) { $data = array(); if (empty($mbox_data)) { $mbox_data = $this->imap->mailbox_data($mailbox); } // Prevent infinite loop. // It happens when rcube_imap::message_index_direct() is called. // There id2uid() is called which will again call get_index() and so on. if (!$sort_field && !$this->skip_deleted) $this->icache['pending_index_update'] = true; if ($mbox_data['EXISTS']) { // fetch sorted sequence numbers $data_seq = $this->imap->message_index_direct($mailbox, $sort_field, $sort_order); // fetch UIDs if (!empty($data_seq)) { // Seek in internal cache if (array_key_exists('index', (array)$this->icache[$mailbox]) && array_key_exists('result', (array)$this->icache[$mailbox]['index']) ) $data_uid = $this->icache[$mailbox]['index']['result']; else $data_uid = $this->imap->conn->fetchUIDs($mailbox, $data_seq); // build index if (!empty($data_uid)) { foreach ($data_seq as $seq) if ($uid = $data_uid[$seq]) $data[$seq] = $uid; } } $index = $this->imap->message_index_direct($mailbox, $sort_field, $sort_order); } else { $index = new rcube_result_index($mailbox, '* SORT'); } // Reset internal flags $this->icache['pending_index_update'] = false; return $data; return $index; } } program/include/rcube_imap_generic.php
@@ -26,7 +26,6 @@ */ /** * Struct representing an e-mail message header * @@ -1218,8 +1217,8 @@ // Invoke SEARCH as a fallback $index = $this->search($mailbox, 'ALL UNSEEN', false, array('COUNT')); if (is_array($index)) { return (int) $index['COUNT']; if (!$index->isError()) { return $index->count(); } return false; @@ -1293,8 +1292,21 @@ return false; } function sort($mailbox, $field, $add='', $is_uid=FALSE, $encoding = 'US-ASCII') /** * Executes SORT command * * @param string $mailbox Mailbox name * @param string $field Field to sort by (ARRIVAL, CC, DATE, FROM, SIZE, SUBJECT, TO) * @param string $add Searching criteria * @param bool $return_uid Enables UID SORT usage * @param string $encoding Character set * * @return rcube_result_index Response data */ function sort($mailbox, $field, $add='', $return_uid=false, $encoding = 'US-ASCII') { require_once dirname(__FILE__) . '/rcube_result_index.php'; $field = strtoupper($field); if ($field == 'INTERNALDATE') { $field = 'ARRIVAL'; @@ -1304,33 +1316,61 @@ 'FROM' => 1, 'SIZE' => 1, 'SUBJECT' => 1, 'TO' => 1); if (!$fields[$field]) { return false; return new rcube_result_index($mailbox); } if (!$this->select($mailbox)) { return false; return new rcube_result_index($mailbox); } // message IDs if (!empty($add)) $add = $this->compressMessageSet($add); list($code, $response) = $this->execute($is_uid ? 'UID SORT' : 'SORT', list($code, $response) = $this->execute($return_uid ? 'UID SORT' : 'SORT', array("($field)", $encoding, 'ALL' . (!empty($add) ? ' '.$add : ''))); if ($code == self::ERROR_OK) { // remove prefix and unilateral untagged server responses $response = substr($response, stripos($response, '* SORT') + 7); if ($pos = strpos($response, '*')) { $response = substr($response, 0, $pos); } return preg_split('/[\s\r\n]+/', $response, -1, PREG_SPLIT_NO_EMPTY); if ($code != self::ERROR_OK) { $response = null; } return false; return new rcube_result_index($mailbox, $response); } function fetchHeaderIndex($mailbox, $message_set, $index_field='', $skip_deleted=true, $uidfetch=false) /** * Simulates SORT command by using FETCH and sorting. * * @param string $mailbox Mailbox name * @param string|array $message_set Searching criteria (list of messages to return) * @param string $index_field Field to sort by (ARRIVAL, CC, DATE, FROM, SIZE, SUBJECT, TO) * @param bool $skip_deleted Makes that DELETED messages will be skipped * @param bool $uidfetch Enables UID FETCH usage * @param bool $return_uid Enables returning UIDs instead of IDs * * @return rcube_result_index Response data */ function index($mailbox, $message_set, $index_field='', $skip_deleted=true, $uidfetch=false, $return_uid=false) { require_once dirname(__FILE__) . '/rcube_result_index.php'; $msg_index = $this->fetchHeaderIndex($mailbox, $message_set, $index_field, $skip_deleted, $uidfetch, $return_uid); if (!empty($msg_index)) { asort($msg_index); // ASC $msg_index = array_keys($msg_index); $msg_index = '* SEARCH ' . implode(' ', $msg_index); } else { $msg_index = is_array($msg_index) ? '* SEARCH' : null; } return new rcube_result_index($mailbox, $msg_index); } function fetchHeaderIndex($mailbox, $message_set, $index_field='', $skip_deleted=true, $uidfetch=false, $return_uid=false) { if (is_array($message_set)) { if (!($message_set = $this->compressMessageSet($message_set))) @@ -1370,25 +1410,32 @@ } // build FETCH command string $key = $this->nextTag(); $cmd = $uidfetch ? 'UID FETCH' : 'FETCH'; $deleted = $skip_deleted ? ' FLAGS' : ''; $key = $this->nextTag(); $cmd = $uidfetch ? 'UID FETCH' : 'FETCH'; $fields = array(); if ($mode == 1 && $index_field == 'DATE') $request = " $cmd $message_set (INTERNALDATE BODY.PEEK[HEADER.FIELDS (DATE)]$deleted)"; else if ($mode == 1) $request = " $cmd $message_set (BODY.PEEK[HEADER.FIELDS ($index_field)]$deleted)"; if ($return_uid) $fields[] = 'UID'; if ($skip_deleted) $fields[] = 'FLAGS'; if ($mode == 1) { if ($index_field == 'DATE') $fields[] = 'INTERNALDATE'; $fields[] = "BODY.PEEK[HEADER.FIELDS ($index_field)]"; } else if ($mode == 2) { if ($index_field == 'SIZE') $request = " $cmd $message_set (RFC822.SIZE$deleted)"; else $request = " $cmd $message_set ($index_field$deleted)"; } else if ($mode == 3) $request = " $cmd $message_set (FLAGS)"; else // 4 $request = " $cmd $message_set (INTERNALDATE$deleted)"; $fields[] = 'RFC822.SIZE'; else if (!$return_uid || $index_field != 'UID') $fields[] = $index_field; } else if ($mode == 3 && !$skip_deleted) $fields[] = 'FLAGS'; else if ($mode == 4) $fields[] = 'INTERNALDATE'; $request = $key . $request; $request = "$key $cmd $message_set (" . implode(' ', $fields) . ")"; if (!$this->putLine($request)) { $this->setError(self::ERROR_COMMAND, "Unable to send command: $request"); @@ -1405,6 +1452,12 @@ $id = $m[1]; $flags = NULL; if ($return_uid) { if (preg_match('/UID ([0-9]+)/', $line, $matches)) $id = (int) $matches[1]; else continue; } if ($skip_deleted && preg_match('/FLAGS \(([^)]+)\)/', $line, $matches)) { $flags = explode(' ', strtoupper($matches[1])); if (in_array('\\DELETED', $flags)) { @@ -1534,9 +1587,11 @@ function UID2ID($mailbox, $uid) { if ($uid > 0) { $id_a = $this->search($mailbox, "UID $uid"); if (is_array($id_a) && count($id_a) == 1) { return (int) $id_a[0]; $index = $this->search($mailbox, "UID $uid"); if ($index->count() == 1) { $arr = $index->get(); return (int) $arr[0]; } } return null; @@ -1560,21 +1615,14 @@ return null; } list($code, $response) = $this->execute('FETCH', array($id, '(UID)')); $index = $this->search($mailbox, $id, true); if ($code == self::ERROR_OK && preg_match("/^\* $id FETCH \(UID (.*)\)/i", $response, $m)) { return (int) $m[1]; if ($index->count() == 1) { $arr = $index->get(); return (int) $arr[0]; } return null; } function fetchUIDs($mailbox, $message_set=null) { if (empty($message_set)) $message_set = '1:*'; return $this->fetchHeaderIndex($mailbox, $message_set, 'UID', false); } /** @@ -1970,68 +2018,30 @@ return $r; } // Don't be tempted to change $str to pass by reference to speed this up - it will slow it down by about // 7 times instead :-) See comments on http://uk2.php.net/references and this article: // http://derickrethans.nl/files/phparch-php-variables-article.pdf private function parseThread($str, $begin, $end, $root, $parent, $depth, &$depthmap, &$haschildren) /** * Executes THREAD command * * @param string $mailbox Mailbox name * @param string $algorithm Threading algorithm (ORDEREDSUBJECT, REFERENCES, REFS) * @param string $criteria Searching criteria * @param bool $return_uid Enables UIDs in result instead of sequence numbers * @param string $encoding Character set * * @return rcube_result_thread Thread data */ function thread($mailbox, $algorithm='REFERENCES', $criteria='', $return_uid=false, $encoding='US-ASCII') { $node = array(); if ($str[$begin] != '(') { $stop = $begin + strspn($str, '1234567890', $begin, $end - $begin); $msg = substr($str, $begin, $stop - $begin); if ($msg == 0) return $node; if (is_null($root)) $root = $msg; $depthmap[$msg] = $depth; $haschildren[$msg] = false; if (!is_null($parent)) $haschildren[$parent] = true; if ($stop + 1 < $end) $node[$msg] = $this->parseThread($str, $stop + 1, $end, $root, $msg, $depth + 1, $depthmap, $haschildren); else $node[$msg] = array(); } else { $off = $begin; while ($off < $end) { $start = $off; $off++; $n = 1; while ($n > 0) { $p = strpos($str, ')', $off); if ($p === false) { error_log("Mismatched brackets parsing IMAP THREAD response:"); error_log(substr($str, ($begin < 10) ? 0 : ($begin - 10), $end - $begin + 20)); error_log(str_repeat(' ', $off - (($begin < 10) ? 0 : ($begin - 10)))); return $node; } $p1 = strpos($str, '(', $off); if ($p1 !== false && $p1 < $p) { $off = $p1 + 1; $n++; } else { $off = $p + 1; $n--; } } $node += $this->parseThread($str, $start + 1, $off - 1, $root, $parent, $depth, $depthmap, $haschildren); } } require_once dirname(__FILE__) . '/rcube_result_thread.php'; return $node; } function thread($mailbox, $algorithm='REFERENCES', $criteria='', $encoding='US-ASCII') { $old_sel = $this->selected; if (!$this->select($mailbox)) { return false; return new rcube_result_thread($mailbox); } // return empty result when folder is empty and we're just after SELECT if ($old_sel != $mailbox && !$this->data['EXISTS']) { return array(array(), array(), array()); return new rcube_result_thread($mailbox); } $encoding = $encoding ? trim($encoding) : 'US-ASCII'; @@ -2039,28 +2049,14 @@ $criteria = $criteria ? 'ALL '.trim($criteria) : 'ALL'; $data = ''; list($code, $response) = $this->execute('THREAD', array( $algorithm, $encoding, $criteria)); list($code, $response) = $this->execute($return_uid ? 'UID THREAD' : 'THREAD', array($algorithm, $encoding, $criteria)); if ($code == self::ERROR_OK) { // remove prefix... $response = substr($response, stripos($response, '* THREAD') + 9); // ...unilateral untagged server responses if ($pos = strpos($response, '*')) { $response = substr($response, 0, $pos); } $response = str_replace("\r\n", '', $response); $depthmap = array(); $haschildren = array(); $tree = $this->parseThread($response, 0, strlen($response), null, null, 0, $depthmap, $haschildren); return array($tree, $depthmap, $haschildren); if ($code != self::ERROR_OK) { $response = null; } return false; return new rcube_result_thread($mailbox, $response); } /** @@ -2071,22 +2067,27 @@ * @param bool $return_uid Enable UID in result instead of sequence ID * @param array $items Return items (MIN, MAX, COUNT, ALL) * * @return array Message identifiers or item-value hash * @return rcube_result_index Result data */ function search($mailbox, $criteria, $return_uid=false, $items=array()) { require_once dirname(__FILE__) . '/rcube_result_index.php'; $old_sel = $this->selected; if (!$this->select($mailbox)) { return false; return new rcube_result_index($mailbox); } // return empty result when folder is empty and we're just after SELECT if ($old_sel != $mailbox && !$this->data['EXISTS']) { if (!empty($items)) return array_combine($items, array_fill(0, count($items), 0)); else return array(); return new rcube_result_index($mailbox, '* SEARCH'); } // If ESEARCH is supported always use ALL // but not when items are specified or using simple id2uid search if (empty($items) && ((int) $criteria != $criteria)) { $items = array('ALL'); } $esearch = empty($items) ? false : $this->getCapability('ESEARCH'); @@ -2097,6 +2098,7 @@ if (!empty($items) && $esearch) { $params .= 'RETURN (' . implode(' ', $items) . ')'; } if (!empty($criteria)) { $modseq = stripos($criteria, 'MODSEQ') !== false; $params .= ($params ? ' ' : '') . $criteria; @@ -2108,65 +2110,11 @@ list($code, $response) = $this->execute($return_uid ? 'UID SEARCH' : 'SEARCH', array($params)); if ($code == self::ERROR_OK) { // remove prefix... $response = substr($response, stripos($response, $esearch ? '* ESEARCH' : '* SEARCH') + ($esearch ? 10 : 9)); // ...and unilateral untagged server responses if ($pos = strpos($response, '*')) { $response = rtrim(substr($response, 0, $pos)); } // remove MODSEQ response if ($modseq) { if (preg_match('/\(MODSEQ ([0-9]+)\)$/', $response, $m)) { $response = substr($response, 0, -strlen($m[0])); } } if ($esearch) { // Skip prefix: ... (TAG "A285") UID ... $this->tokenizeResponse($response, $return_uid ? 2 : 1); $result = array(); for ($i=0; $i<count($items); $i++) { // If the SEARCH returns no matches, the server MUST NOT // include the item result option in the ESEARCH response if ($ret = $this->tokenizeResponse($response, 2)) { list ($name, $value) = $ret; $result[$name] = $value; } } return $result; } else { $response = preg_split('/[\s\r\n]+/', $response, -1, PREG_SPLIT_NO_EMPTY); if (!empty($items)) { $result = array(); if (in_array('COUNT', $items)) { $result['COUNT'] = count($response); } if (in_array('MIN', $items)) { $result['MIN'] = !empty($response) ? min($response) : 0; } if (in_array('MAX', $items)) { $result['MAX'] = !empty($response) ? max($response) : 0; } if (in_array('ALL', $items)) { $result['ALL'] = $this->compressMessageSet($response, true); } return $result; } else { return $response; } } if ($code != self::ERROR_OK) { $response = null; } return false; return new rcube_result_index($mailbox, $response); } /** program/include/rcube_result_index.php
New file @@ -0,0 +1,446 @@ <?php /* +-----------------------------------------------------------------------+ | program/include/rcube_result_index.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 GPL | | | | PURPOSE: | | SORT/SEARCH/ESEARCH response handler | | | +-----------------------------------------------------------------------+ | Author: Thomas Bruederli <roundcube@gmail.com> | | Author: Aleksander Machniak <alec@alec.pl> | +-----------------------------------------------------------------------+ $Id: rcube_imap.php 5347 2011-10-19 06:35:29Z alec $ */ /** * Class for accessing IMAP's SORT/SEARCH/ESEARCH result */ class rcube_result_index { private $raw_data; private $mailbox; private $meta = array(); private $params = array(); private $order = 'ASC'; const SEPARATOR_ELEMENT = ' '; /** * Object constructor. */ public function __construct($mailbox = null, $data = null) { $this->mailbox = $mailbox; $this->init($data); } /** * Initializes object with SORT command response * * @param string $data IMAP response string */ public function init($data = null) { $this->meta = array(); $data = explode('*', (string)$data); // ...skip unilateral untagged server responses for ($i=0, $len=count($data); $i<$len; $i++) { $data_item = &$data[$i]; if (preg_match('/^ SORT/i', $data_item)) { $data_item = substr($data_item, 5); break; } else if (preg_match('/^ (E?SEARCH)/i', $data_item, $m)) { $data_item = substr($data_item, strlen($m[0])); if (strtoupper($m[1]) == 'ESEARCH') { $data_item = trim($data_item); // remove MODSEQ response if (preg_match('/\(MODSEQ ([0-9]+)\)$/i', $data_item, $m)) { $data_item = substr($data_item, 0, -strlen($m[0])); $this->params['MODSEQ'] = $m[1]; } // remove TAG response part if (preg_match('/^\(TAG ["a-z0-9]+\)\s*/i', $data_item, $m)) { $data_item = substr($data_item, strlen($m[0])); } // remove UID $data_item = preg_replace('/^UID\s*/i', '', $data_item); // ESEARCH parameters while (preg_match('/^([a-z]+) ([0-9:,]+)\s*/i', $data_item, $m)) { $param = strtoupper($m[1]); $value = $m[2]; $this->params[strtoupper($m[1])] = $value; $data_item = substr($data_item, strlen($m[0])); if (in_array($param, array('COUNT', 'MIN', 'MAX'))) { $this->meta[strtolower($param)] = (int) $m[2]; } } // @TODO: Implement compression using compressMessageSet() in __sleep() and __wakeup() ? // @TODO: work with compressed result?! if (isset($this->params['ALL'])) { $data[$idx] = implode(self::SEPARATOR_ELEMENT, rcube_imap_generic::uncompressMessageSet($this->params['ALL'])); } } break; } unset($data[$i]); } if (empty($data)) { return; } $data = array_shift($data); $data = trim($data); $data = preg_replace('/[\r\n]/', '', $data); $data = preg_replace('/\s+/', ' ', $data); $this->raw_data = $data; } /** * Checks the result from IMAP command * * @return bool True if the result is an error, False otherwise */ public function isError() { return $this->raw_data === null ? true : false; } /** * Checks if the result is empty * * @return bool True if the result is empty, False otherwise */ public function isEmpty() { return empty($this->raw_data) ? true : false; } /** * Returns number of elements in the result * * @return int Number of elements */ public function count() { if ($this->meta['count'] !== null) return $this->meta['count']; if (empty($this->raw_data)) { $this->meta['count'] = 0; $this->meta['length'] = 0; } else // @TODO: check performance substr_count() vs. explode() $this->meta['count'] = 1 + substr_count($this->raw_data, self::SEPARATOR_ELEMENT); 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 countMessages() { return $this->count(); } /** * Returns maximal message identifier in the result * * @return int Maximal message identifier */ public function max() { if (!isset($this->meta['max'])) { // @TODO: do it by parsing raw_data? $this->meta['max'] = (int) @max($this->get()); } return $this->meta['max']; } /** * Returns minimal message identifier in the result * * @return int Minimal message identifier */ public function min() { if (!isset($this->meta['min'])) { // @TODO: do it by parsing raw_data? $this->meta['min'] = (int) @min($this->get()); } return $this->meta['min']; } /** * 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 = $this->get(); $data = array_slice($data, $offset, $length); $this->meta = array(); $this->meta['count'] = count($data); $this->raw_data = implode(self::SEPARATOR_ELEMENT, $data); } /** * Filters data set. Removes elements listed in $ids list. * * @param array $ids List of IDs to remove. */ public function filter($ids = array()) { $data = $this->get(); $data = array_diff($data, $ids); $this->meta = array(); $this->meta['count'] = count($data); $this->raw_data = implode(self::SEPARATOR_ELEMENT, $data); } /** * Filters data set. Removes elements not listed in $ids list. * * @param array $ids List of IDs to keep. */ public function intersect($ids = array()) { $data = $this->get(); $data = array_intersect($data, $ids); $this->meta = array(); $this->meta['count'] = count($data); $this->raw_data = implode(self::SEPARATOR_ELEMENT, $data); } /** * Reverts order of elements in the result */ public function revert() { $this->order = $this->order == 'ASC' ? 'DESC' : 'ASC'; if (empty($this->raw_data)) { return; } // @TODO: maybe do this in chunks $data = $this->get(); $data = array_reverse($data); $this->raw_data = implode(self::SEPARATOR_ELEMENT, $data); $this->meta['pos'] = array(); } /** * 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->raw_data)) { return false; } $msgid = (int) $msgid; $begin = implode('|', array('^', preg_quote(self::SEPARATOR_ELEMENT, '/'))); $end = implode('|', array('$', preg_quote(self::SEPARATOR_ELEMENT, '/'))); if (preg_match("/($begin)$msgid($end)/", $this->raw_data, $m, $get_index ? PREG_OFFSET_CAPTURE : null) ) { if ($get_index) { $idx = 0; if ($m[0][1]) { $idx = 1 + substr_count($this->raw_data, self::SEPARATOR_ELEMENT, 0, $m[0][1]); } // cache position of this element, so we can use it in getElement() $this->meta['pos'][$idx] = (int)$m[0][1]; return $idx; } return true; } return false; } /** * Return all messages in the result. * * @return array List of message IDs */ public function get() { if (empty($this->raw_data)) { return array(); } return explode(self::SEPARATOR_ELEMENT, $this->raw_data); } /** * Return all messages in the result. * * @return array List of message IDs */ public function getCompressed() { if (empty($this->raw_data)) { return ''; } return rcube_imap_generic::compressMessageSet($this->get()); } /** * Return result element at specified index * * @param int|string $index Element's index or "FIRST" or "LAST" * * @return int Element value */ public function getElement($index) { $count = $this->count(); if (!$count) { return null; } // first element if ($index === 0 || $index === '0' || $index === 'FIRST') { $pos = strpos($this->raw_data, self::SEPARATOR_ELEMENT); if ($pos === false) $result = (int) $this->raw_data; else $result = (int) substr($this->raw_data, 0, $pos); return $result; } // last element if ($index === 'LAST' || $index == $count-1) { $pos = strrpos($this->raw_data, self::SEPARATOR_ELEMENT); if ($pos === false) $result = (int) $this->raw_data; else $result = (int) substr($this->raw_data, $pos); return $result; } // do we know the position of the element or the neighbour of it? if (!empty($this->meta['pos'])) { if (isset($this->meta['pos'][$index])) $pos = $this->meta['pos'][$index]; else if (isset($this->meta['pos'][$index-1])) $pos = strpos($this->raw_data, self::SEPARATOR_ELEMENT, $this->meta['pos'][$index-1] + 1); else if (isset($this->meta['pos'][$index+1])) $pos = strrpos($this->raw_data, self::SEPARATOR_ELEMENT, $this->meta['pos'][$index+1] - $this->length() - 1); if (isset($pos) && preg_match('/([0-9]+)/', $this->raw_data, $m, null, $pos)) { return (int) $m[1]; } } // Finally use less effective method $data = explode(self::SEPARATOR_ELEMENT, $this->raw_data); return $data[$index]; } /** * 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 getParameters($param=null) { $params = $this->params; $params['MAILBOX'] = $this->mailbox; $params['ORDER'] = $this->order; if ($param !== null) { return $params[$param]; } return $params; } /** * Returns length of internal data representation * * @return int Data length */ private function length() { if (!isset($this->meta['length'])) { $this->meta['length'] = strlen($this->raw_data); } return $this->meta['length']; } } program/include/rcube_result_thread.php
New file @@ -0,0 +1,670 @@ <?php /* +-----------------------------------------------------------------------+ | program/include/rcube_result_thread.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 GPL | | | | PURPOSE: | | THREAD response handler | | | +-----------------------------------------------------------------------+ | Author: Thomas Bruederli <roundcube@gmail.com> | | Author: Aleksander Machniak <alec@alec.pl> | +-----------------------------------------------------------------------+ $Id: rcube_imap.php 5347 2011-10-19 06:35:29Z alec $ */ /** * Class for accessing IMAP's THREAD result */ class rcube_result_thread { private $raw_data; private $mailbox; private $meta = array(); private $order = 'ASC'; const SEPARATOR_ELEMENT = ' '; const SEPARATOR_ITEM = '~'; const SEPARATOR_LEVEL = ':'; /** * Object constructor. */ public function __construct($mailbox = null, $data = null) { $this->mailbox = $mailbox; $this->init($data); } /** * Initializes object with IMAP command response * * @param string $data IMAP response string */ public function init($data = null) { $this->meta = array(); $data = explode('*', (string)$data); // ...skip unilateral untagged server responses for ($i=0, $len=count($data); $i<$len; $i++) { if (preg_match('/^ THREAD/i', $data[$i])) { $data[$i] = substr($data[$i], 7); break; } unset($data[$i]); } if (empty($data)) { return; } $data = array_shift($data); $data = trim($data); $data = preg_replace('/[\r\n]/', '', $data); $data = preg_replace('/\s+/', ' ', $data); $this->raw_data = $this->parseThread($data); } /** * Checks the result from IMAP command * * @return bool True if the result is an error, False otherwise */ public function isError() { return $this->raw_data === null ? true : false; } /** * Checks if the result is empty * * @return bool True if the result is empty, False otherwise */ public function isEmpty() { return empty($this->raw_data) ? true : false; } /** * Returns number of elements (threads) in the result * * @return int Number of elements */ public function count() { if ($this->meta['count'] !== null) return $this->meta['count']; if (empty($this->raw_data)) { $this->meta['count'] = 0; } else $this->meta['count'] = 1 + substr_count($this->raw_data, self::SEPARATOR_ELEMENT); if (!$this->meta['count']) $this->meta['messages'] = 0; return $this->meta['count']; } /** * Returns number of all messages in the result * * @return int Number of elements */ public function countMessages() { if ($this->meta['messages'] !== null) return $this->meta['messages']; if (empty($this->raw_data)) { $this->meta['messages'] = 0; } else { $regexp = '/((^|' . preg_quote(self::SEPARATOR_ELEMENT, '/') . '|' . preg_quote(self::SEPARATOR_ITEM, '/') . ')[0-9]+)/'; // @TODO: can we do this in a better way? $this->meta['messages'] = preg_match_all($regexp, $this->raw_data, $m); } if ($this->meta['messages'] == 0 || $this->meta['messages'] == 1) $this->meta['count'] = $this->meta['messages']; return $this->meta['messages']; } /** * Returns maximum message identifier in the result * * @return int Maximum message identifier */ public function max() { if (!isset($this->meta['max'])) { // @TODO: do it by parsing raw_data? $this->meta['max'] = (int) @max($this->get()); } return $this->meta['max']; } /** * Returns minimum message identifier in the result * * @return int Minimum message identifier */ public function min() { if (!isset($this->meta['min'])) { // @TODO: do it by parsing raw_data? $this->meta['min'] = (int) @min($this->get()); } return $this->meta['min']; } /** * 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 = explode(self::SEPARATOR_ELEMENT, $this->raw_data); $data = array_slice($data, $offset, $length); $this->meta = array(); $this->meta['count'] = count($data); $this->raw_data = implode(self::SEPARATOR_ELEMENT, $data); } /** * Filters data set. Removes threads not listed in $roots list. * * @param array $roots List of IDs of thread roots. */ public function filter($roots) { $datalen = strlen($this->raw_data); $roots = array_flip($roots); $result = ''; $start = 0; $this->meta = array(); $this->meta['count'] = 0; while (($pos = @strpos($this->raw_data, self::SEPARATOR_ELEMENT, $start)) || ($start < $datalen && ($pos = $datalen)) ) { $len = $pos - $start; $elem = substr($this->raw_data, $start, $len); $start = $pos + 1; // extract root message ID if ($npos = strpos($elem, self::SEPARATOR_ITEM)) { $root = (int) substr($elem, 0, $npos); } else { $root = $elem; } if (isset($roots[$root])) { $this->meta['count']++; $result .= self::SEPARATOR_ELEMENT . $elem; } } $this->raw_data = ltrim($result, self::SEPARATOR_ELEMENT); } /** * Reverts order of elements in the result */ public function revert() { $this->order = $this->order == 'ASC' ? 'DESC' : 'ASC'; if (empty($this->raw_data)) { return; } $this->meta['pos'] = array(); $datalen = strlen($this->raw_data); $result = ''; $start = 0; while (($pos = @strpos($this->raw_data, self::SEPARATOR_ELEMENT, $start)) || ($start < $datalen && ($pos = $datalen)) ) { $len = $pos - $start; $elem = substr($this->raw_data, $start, $len); $start = $pos + 1; $result = $elem . self::SEPARATOR_ELEMENT . $result; } $this->raw_data = rtrim($result, self::SEPARATOR_ELEMENT); } /** * 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 boolean True on success, False if message ID doesn't exist */ public function exists($msgid, $get_index = false) { $msgid = (int) $msgid; $begin = implode('|', array( '^', preg_quote(self::SEPARATOR_ELEMENT, '/'), preg_quote(self::SEPARATOR_LEVEL, '/'), )); $end = implode('|', array( '$', preg_quote(self::SEPARATOR_ELEMENT, '/'), preg_quote(self::SEPARATOR_ITEM, '/'), )); if (preg_match("/($begin)$msgid($end)/", $this->raw_data, $m, $get_index ? PREG_OFFSET_CAPTURE : null) ) { if ($get_index) { $idx = 0; if ($m[0][1]) { $idx = substr_count($this->raw_data, self::SEPARATOR_ELEMENT, 0, $m[0][1]+1) + substr_count($this->raw_data, self::SEPARATOR_ITEM, 0, $m[0][1]+1); } // cache position of this element, so we can use it in getElement() $this->meta['pos'][$idx] = (int)$m[0][1]; return $idx; } return true; } return false; } /** * Return IDs of all messages in the result. Threaded data will be flattened. * * @return array List of message identifiers */ public function get() { if (empty($this->raw_data)) { return array(); } $regexp = '/(' . preg_quote(self::SEPARATOR_ELEMENT, '/') . '|' . preg_quote(self::SEPARATOR_ITEM, '/') . '[0-9]+' . preg_quote(self::SEPARATOR_LEVEL, '/') .')/'; return preg_split($regexp, $this->raw_data); } /** * Return all messages in the result. * * @return array List of message identifiers */ public function getCompressed() { if (empty($this->raw_data)) { return ''; } return rcube_imap_generic::compressMessageSet($this->get()); } /** * Return result element at specified index (all messages, not roots) * * @param int|string $index Element's index or "FIRST" or "LAST" * * @return int Element value */ public function getElement($index) { $count = $this->count(); if (!$count) { return null; } // first element if ($index === 0 || $index === '0' || $index === 'FIRST') { preg_match('/^([0-9]+)/', $this->raw_data, $m); $result = (int) $m[1]; return $result; } // last element if ($index === 'LAST' || $index == $count-1) { preg_match('/([0-9]+)$/', $this->raw_data, $m); $result = (int) $m[1]; return $result; } // do we know the position of the element or the neighbour of it? if (!empty($this->meta['pos'])) { $element = preg_quote(self::SEPARATOR_ELEMENT, '/'); $item = preg_quote(self::SEPARATOR_ITEM, '/') . '[0-9]+' . preg_quote(self::SEPARATOR_LEVEL, '/') .'?'; $regexp = '(' . $element . '|' . $item . ')'; if (isset($this->meta['pos'][$index])) { if (preg_match('/([0-9]+)/', $this->raw_data, $m, null, $this->meta['pos'][$index])) $result = $m[1]; } else if (isset($this->meta['pos'][$index-1])) { // get chunk of data after previous element $data = substr($this->raw_data, $this->meta['pos'][$index-1]+1, 50); $data = preg_replace('/^[0-9]+/', '', $data); // remove UID at $index position $data = preg_replace("/^$regexp/", '', $data); // remove separator if (preg_match('/^([0-9]+)/', $data, $m)) $result = $m[1]; } else if (isset($this->meta['pos'][$index+1])) { // get chunk of data before next element $pos = max(0, $this->meta['pos'][$index+1] - 50); $len = min(50, $this->meta['pos'][$index+1]); $data = substr($this->raw_data, $pos, $len); $data = preg_replace("/$regexp\$/", '', $data); // remove separator if (preg_match('/([0-9]+)$/', $data, $m)) $result = $m[1]; } if (isset($result)) { return (int) $result; } } // Finally use less effective method $data = $this->get(); return $data[$index]; } /** * Returns response parameters e.g. MAILBOX, ORDER * * @param string $param Parameter name * * @return array|string Response parameters or parameter value */ public function getParameters($param=null) { $params = $this->params; $params['MAILBOX'] = $this->mailbox; $params['ORDER'] = $this->order; if ($param !== null) { return $params[$param]; } return $params; } /** * THREAD=REFS sorting implementation (based on provided index) * * @param rcube_result_index $index Sorted message identifiers */ public function sort($index) { $this->sort_order = $index->getParameters('ORDER'); if (empty($this->raw_data)) { return; } // when sorting search result it's good to make the index smaller if ($index->count() != $this->countMessages()) { $index->intersect($this->get()); } $result = array_fill_keys($index->get(), null); $datalen = strlen($this->raw_data); $start = 0; // Here we're parsing raw_data twice, we want only one big array // in memory at a time // Assign roots while (($pos = @strpos($this->raw_data, self::SEPARATOR_ELEMENT, $start)) || ($start < $datalen && ($pos = $datalen)) ) { $len = $pos - $start; $elem = substr($this->raw_data, $start, $len); $start = $pos + 1; $items = explode(self::SEPARATOR_ITEM, $elem); $root = (int) array_shift($items); $result[$elem] = $elem; foreach ($items as $item) { list($lv, $id) = explode(self::SEPARATOR_LEVEL, $item); $result[$id] = $root; } } // get only unique roots $result = array_filter($result); // make sure there are no nulls $result = array_unique($result, SORT_NUMERIC); // Re-sort raw data $result = array_fill_keys($result, null); $start = 0; while (($pos = @strpos($this->raw_data, self::SEPARATOR_ELEMENT, $start)) || ($start < $datalen && ($pos = $datalen)) ) { $len = $pos - $start; $elem = substr($this->raw_data, $start, $len); $start = $pos + 1; $npos = strpos($elem, self::SEPARATOR_ITEM); $root = (int) ($npos ? substr($elem, 0, $npos) : $elem); $result[$root] = $elem; } $this->raw_data = implode(self::SEPARATOR_ELEMENT, $result); } /** * Returns data as tree * * @return array Data tree */ public function getTree() { $datalen = strlen($this->raw_data); $result = array(); $start = 0; while (($pos = @strpos($this->raw_data, self::SEPARATOR_ELEMENT, $start)) || ($start < $datalen && ($pos = $datalen)) ) { $len = $pos - $start; $elem = substr($this->raw_data, $start, $len); $items = explode(self::SEPARATOR_ITEM, $elem); $result[array_shift($items)] = $this->buildThread($items); $start = $pos + 1; } return $result; } /** * Returns thread depth and children data * * @return array Thread data */ public function getThreadData() { $data = $this->getTree(); $depth = array(); $children = array(); $this->buildThreadData($data, $depth, $children); return array($depth, $children); } /** * Creates 'depth' and 'children' arrays from stored thread 'tree' data. */ private function buildThreadData($data, &$depth, &$children, $level = 0) { foreach ((array)$data as $key => $val) { $children[$key] = !empty($val); $depth[$key] = $level; if (!empty($val)) $this->buildThreadData($val, $depth, $children, $level + 1); } } /** * Converts part of the raw thread into an array */ private function buildThread($items, $level = 1, &$pos = 0) { $result = array(); for ($len=count($items); $pos < $len; $pos++) { list($lv, $id) = explode(self::SEPARATOR_LEVEL, $items[$pos]); if ($level == $lv) { $pos++; $result[$id] = $this->buildThread($items, $level+1, $pos); } else { $pos--; break; } } return $result; } /** * IMAP THREAD response parser */ private function parseThread($str, $begin = 0, $end = 0, $depth = 0) { // Don't be tempted to change $str to pass by reference to speed this up - it will slow it down by about // 7 times instead :-) See comments on http://uk2.php.net/references and this article: // http://derickrethans.nl/files/phparch-php-variables-article.pdf $node = ''; if (!$end) { $end = strlen($str); } // Let's try to store data in max. compacted stracture as a string, // arrays handling is much more expensive // For the following structure: THREAD (2)(3 6 (4 23)(44 7 96)) // -- 2 // // -- 3 // \-- 6 // |-- 4 // | \-- 23 // | // \-- 44 // \-- 7 // \-- 96 // // The output will be: 2,3^1:6^2:4^3:23^2:44^3:7^4:96 if ($str[$begin] != '(') { $stop = $begin + strspn($str, '1234567890', $begin, $end - $begin); $msg = substr($str, $begin, $stop - $begin); if (!$msg) { return $node; } $this->meta['messages']++; $node .= ($depth ? self::SEPARATOR_ITEM.$depth.self::SEPARATOR_LEVEL : '').$msg; if ($stop + 1 < $end) { $node .= $this->parseThread($str, $stop + 1, $end, $depth + 1); } } else { $off = $begin; while ($off < $end) { $start = $off; $off++; $n = 1; while ($n > 0) { $p = strpos($str, ')', $off); if ($p === false) { // error, wrong structure, mismatched brackets in IMAP THREAD response // @TODO: write error to the log or maybe set $this->raw_data = null; return $node; } $p1 = strpos($str, '(', $off); if ($p1 !== false && $p1 < $p) { $off = $p1 + 1; $n++; } else { $off = $p + 1; $n--; } } $thread = $this->parseThread($str, $start + 1, $off - 1, $depth); if ($thread) { if (!$depth) { if ($node) { $node .= self::SEPARATOR_ELEMENT; } } $node .= $thread; } } } return $node; } } program/steps/mail/func.inc
@@ -93,7 +93,7 @@ $_SESSION['search'] = $IMAP->get_search_set(); $_SESSION['search_request'] = $search_request; $OUTPUT->set_env('search_request', $search_request); } } $search_mods = $RCMAIL->config->get('search_mods', $SEARCH_MODS_DEFAULT); $OUTPUT->set_env('search_mods', $search_mods); program/steps/mail/pagenav.inc
@@ -19,52 +19,29 @@ */ $uid = get_input_value('_uid', RCUBE_INPUT_GET); $uid = get_input_value('_uid', RCUBE_INPUT_GET); $index = $IMAP->message_index(null, $_SESSION['sort_col'], $_SESSION['sort_order']); $cnt = $index->countMessages(); // Select mailbox first, for better performance $mbox_name = $IMAP->get_mailbox_name(); $IMAP->select_mailbox($mbox_name); // Get messages count (only messages, no threads here) $cnt = $IMAP->messagecount(NULL, 'ALL'); if ($_SESSION['sort_col'] == 'date' && $_SESSION['sort_order'] == 'DESC' && empty($_REQUEST['_search']) && !$CONFIG['skip_deleted'] && !$IMAP->threading ) { // this assumes that we are sorted by date_DESC $seq = $IMAP->get_id($uid); $index = $cnt - $seq; $prev = $IMAP->get_uid($seq + 1); $first = $IMAP->get_uid($cnt); $next = $IMAP->get_uid($seq - 1); $last = $IMAP->get_uid(1); } else { // Only if we use custom sorting $a_msg_index = $IMAP->message_index(NULL, $_SESSION['sort_col'], $_SESSION['sort_order']); $index = array_search($IMAP->get_id($uid), $a_msg_index); $count = count($a_msg_index); $prev = isset($a_msg_index[$index-1]) ? $IMAP->get_uid($a_msg_index[$index-1]) : -1; $first = $count > 1 ? $IMAP->get_uid($a_msg_index[0]) : -1; $next = isset($a_msg_index[$index+1]) ? $IMAP->get_uid($a_msg_index[$index+1]) : -1; $last = $count > 1 ? $IMAP->get_uid($a_msg_index[$count-1]) : -1; if ($cnt && ($pos = $index->exists($uid, true)) !== false) { $prev = $pos ? $index->getElement($pos-1) : 0; $first = $pos ? $index->getElement('FIRST') : 0; $next = $pos < $cnt-1 ? $index->getElement($pos+1) : 0; $last = $pos < $cnt-1 ? $index->getElement('LAST') : 0; } // Set UIDs and activate navigation buttons if ($prev > 0) { if ($prev) { $OUTPUT->set_env('prev_uid', $prev); $OUTPUT->command('enable_command', 'previousmessage', 'firstmessage', true); } if ($next > 0) { if ($next) { $OUTPUT->set_env('next_uid', $next); $OUTPUT->command('enable_command', 'nextmessage', 'lastmessage', true); } if ($first > 0) if ($first) $OUTPUT->set_env('first_uid', $first); if ($last > 0) if ($last) $OUTPUT->set_env('last_uid', $last); // Don't need a real messages count value @@ -73,7 +50,7 @@ // Set rowcount text $OUTPUT->command('set_rowcount', rcube_label(array( 'name' => 'messagenrof', 'vars' => array('nr' => $index+1, 'count' => $cnt) 'vars' => array('nr' => $pos+1, 'count' => $cnt) ))); $OUTPUT->send(); program/steps/mail/search.inc
@@ -109,10 +109,6 @@ if ($search_str) $IMAP->search($mbox, $search_str, $imap_charset, $_SESSION['sort_col']); // Get the headers $result_h = $IMAP->list_headers($mbox, 1, $_SESSION['sort_col'], $_SESSION['sort_order']); $count = $IMAP->messagecount(NULL, $IMAP->threading ? 'THREADS' : 'ALL'); // save search results in session if (!is_array($_SESSION['search'])) $_SESSION['search'] = array(); @@ -123,6 +119,12 @@ } $_SESSION['search_request'] = $search_request; // Get the headers $result_h = $IMAP->list_headers($mbox, 1, $_SESSION['sort_col'], $_SESSION['sort_order']); $count = $IMAP->messagecount($mbox, $IMAP->threading ? 'THREADS' : 'ALL'); // Make sure we got the headers if (!empty($result_h)) { rcmail_js_message_list($result_h); program/steps/mail/sendmail.inc
@@ -701,11 +701,11 @@ if ($olddraftmessageid) { // delete previous saved draft // @TODO: use message UID (remember to check UIDVALIDITY) to skip this SEARCH $a_deleteid = $IMAP->search_once($CONFIG['drafts_mbox'], 'HEADER Message-ID '.$olddraftmessageid, true); $delete_idx = $IMAP->search_once($CONFIG['drafts_mbox'], 'HEADER Message-ID '.$olddraftmessageid); if (!empty($a_deleteid)) { $deleted = $IMAP->delete_message($a_deleteid, $CONFIG['drafts_mbox']); if ($del_uid = $delete_idx->getElement('FIRST')) { $deleted = $IMAP->delete_message($del_uid, $CONFIG['drafts_mbox']); // raise error if deletion of old draft failed if (!$deleted) @@ -726,8 +726,8 @@ // remember new draft-uid ($saved could be an UID or TRUE here) if (is_bool($saved)) { $draftuids = $IMAP->search_once($CONFIG['drafts_mbox'], 'HEADER Message-ID '.$msgid, true); $saved = $draftuids[0]; $draft_idx = $IMAP->search_once($CONFIG['drafts_mbox'], 'HEADER Message-ID '.$msgid); $saved = $draft_idx->getElement('FIRST'); } $COMPOSE['param']['draft_uid'] = $saved;