diff options
-rw-r--r-- | CHANGELOG | 1 | ||||
-rw-r--r-- | program/include/rcube_imap.php | 19 | ||||
-rw-r--r-- | program/include/rcube_imap_generic.php | 92 |
3 files changed, 92 insertions, 20 deletions
@@ -38,6 +38,7 @@ CHANGELOG Roundcube Webmail - Fix decoding of e-mail address strings in message headers (#1487068) - Fix handling of attachments when Content-Disposition is not inline nor attachment (#1487051) - Improve performance of unseen messages counting (#1487058) +- Improve performance of messages counting using ESEARCH extension (RFC4731) RELEASE 0.4.2 ------------- diff --git a/program/include/rcube_imap.php b/program/include/rcube_imap.php index 5d90fe41f..4bf7cac88 100644 --- a/program/include/rcube_imap.php +++ b/program/include/rcube_imap.php @@ -539,19 +539,26 @@ class rcube_imap // use SEARCH for message counting else if ($this->skip_deleted) { $search_str = "ALL UNDELETED"; + $keys = array('COUNT'); + $need_uid = false; - // get message count and store in cache - if ($mode == 'UNSEEN') + if ($mode == 'UNSEEN') { $search_str .= " UNSEEN"; - // get message count using SEARCH + } + else if ($status) { + $keys[] = 'MAX'; + $need_uid = true; + } + + // get message count using (E)SEARCH // not very performant but more precise (using UNDELETED) - $index = $this->conn->search($mailbox, $search_str); + $index = $this->conn->search($mailbox, $search_str, $need_uid, $keys); - $count = is_array($index) ? count($index) : 0; + $count = is_array($index) ? $index['COUNT'] : 0; if ($mode == 'ALL' && $status) { $this->set_folder_stats($mailbox, 'cnt', $count); - $this->set_folder_stats($mailbox, 'maxuid', $index ? $this->_id2uid(max($index), $mailbox) : 0); + $this->set_folder_stats($mailbox, 'maxuid', is_array($index) ? $index['MAX'] : 0); } } else { diff --git a/program/include/rcube_imap_generic.php b/program/include/rcube_imap_generic.php index 4c082b9e4..a8bbb89d4 100644 --- a/program/include/rcube_imap_generic.php +++ b/program/include/rcube_imap_generic.php @@ -813,10 +813,9 @@ class rcube_imap_generic } // Invoke SEARCH as a fallback - // @TODO: ESEARCH support - $index = $this->search($mailbox, 'ALL UNSEEN'); + $index = $this->search($mailbox, 'ALL UNSEEN', false, array('COUNT')); if (is_array($index)) { - return count($index); + return (int) $index['COUNT']; } return false; @@ -983,17 +982,17 @@ class rcube_imap_generic return $result; } - private function compressMessageSet($message_set) + private function compressMessageSet($message_set, $force=false) { // given a comma delimited list of independent mid's, // compresses by grouping sequences together // if less than 255 bytes long, let's not bother - if (strlen($message_set)<255) { + if (!$force && strlen($message_set)<255) { return $message_set; } - // see if it's already been compress + // see if it's already been compressed if (strpos($message_set, ':') !== false) { return $message_set; } @@ -1561,27 +1560,92 @@ class rcube_imap_generic return false; } - function search($folder, $criteria, $return_uid=false) + /** + * Executes SEARCH command + * + * @param string $mailbox Mailbox name + * @param string $criteria Searching criteria + * @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 + */ + function search($mailbox, $criteria, $return_uid=false, $items=array()) { $old_sel = $this->selected; - if (!$this->select($folder)) { + if (!$this->select($mailbox)) { return false; } // return empty result when folder is empty and we're just after SELECT - if ($old_sel != $folder && !$this->data['EXISTS']) { - return array(); + if ($old_sel != $mailbox && !$this->data['EXISTS']) { + if (!empty($items)) + return array_combine($items, array_fill(0, count($items), 0)); + else + return array(); } + $esearch = empty($items) ? false : $this->getCapability('ESEARCH'); + $criteria = trim($criteria); + $params = ''; + + // RFC4731: ESEARCH + if (!empty($items) && $esearch) { + $params .= 'RETURN (' . implode(' ', $items) . ')'; + } + if (!empty($criteria)) { + $params .= ($params ? ' ' : '') . $criteria; + } + else { + $params .= 'ALL'; + } + list($code, $response) = $this->execute($return_uid ? 'UID SEARCH' : 'SEARCH', - array(trim($criteria))); + array($params)); if ($code == self::ERROR_OK) { // remove prefix and \r\n from raw response - $response = str_replace("\r\n", '', substr($response, 9)); - return preg_split('/\s+/', $response, -1, PREG_SPLIT_NO_EMPTY); - } + $response = substr($response, $esearch ? 10 : 9); + $response = str_replace("\r\n", '', $response); + + 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 results in 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+/', $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(implode(',', $response), true); + + return $result; + } + else { + return $response; + } + } + } return false; } |