From f7dd463174887b622ad3154465fdc845d0bde244 Mon Sep 17 00:00:00 2001 From: Aleksander Machniak <alec@alec.pl> Date: Fri, 17 Jul 2015 12:25:40 -0400 Subject: [PATCH] Fix multi-folder search result sorting by arrival date (#1490450) --- program/lib/Roundcube/rcube_imap_generic.php | 434 +++++++++++++++++++++++++++++++---------------------- 1 files changed, 251 insertions(+), 183 deletions(-) diff --git a/program/lib/Roundcube/rcube_imap_generic.php b/program/lib/Roundcube/rcube_imap_generic.php index 5aaad0a..5d27c52 100644 --- a/program/lib/Roundcube/rcube_imap_generic.php +++ b/program/lib/Roundcube/rcube_imap_generic.php @@ -1,6 +1,6 @@ <?php -/** +/* +-----------------------------------------------------------------------+ | This file is part of the Roundcube Webmail client | | Copyright (C) 2005-2012, The Roundcube Dev Team | @@ -47,8 +47,6 @@ 'MDNSENT' => '$MDNSent', '*' => '\\*', ); - - public static $mupdate; protected $fp; protected $host; @@ -1108,7 +1106,8 @@ // folder name with spaces. Let's try to handle this situation if (!is_array($items) && ($pos = strpos($response, '(')) !== false) { $response = substr($response, $pos); - $items = $this->tokenizeResponse($response, 1); + $items = $this->tokenizeResponse($response, 1); + if (!is_array($items)) { return $result; } @@ -1146,7 +1145,7 @@ } // Clear internal status cache - unset($this->data['STATUS:'.$mailbox]); + $this->clear_status_cache($mailbox); if (!empty($messages) && $messages != '*' && $this->hasCapability('UIDPLUS')) { $messages = self::compressMessageSet($messages); @@ -1293,15 +1292,15 @@ * * @param string $ref Reference name * @param string $mailbox Mailbox name - * @param array $status_opts (see self::_listMailboxes) + * @param array $return_opts (see self::_listMailboxes) * @param array $select_opts (see self::_listMailboxes) * - * @return array List of mailboxes or hash of options if $status_opts argument - * is non-empty. + * @return array|bool List of mailboxes or hash of options if STATUS/MYROGHTS response + * is requested, False on error. */ - function listMailboxes($ref, $mailbox, $status_opts=array(), $select_opts=array()) + function listMailboxes($ref, $mailbox, $return_opts=array(), $select_opts=array()) { - return $this->_listMailboxes($ref, $mailbox, false, $status_opts, $select_opts); + return $this->_listMailboxes($ref, $mailbox, false, $return_opts, $select_opts); } /** @@ -1309,14 +1308,14 @@ * * @param string $ref Reference name * @param string $mailbox Mailbox name - * @param array $status_opts (see self::_listMailboxes) + * @param array $return_opts (see self::_listMailboxes) * - * @return array List of mailboxes or hash of options if $status_opts argument - * is non-empty. + * @return array|bool List of mailboxes or hash of options if STATUS/MYROGHTS response + * is requested, False on error. */ - function listSubscribed($ref, $mailbox, $status_opts=array()) + function listSubscribed($ref, $mailbox, $return_opts=array()) { - return $this->_listMailboxes($ref, $mailbox, true, $status_opts, NULL); + return $this->_listMailboxes($ref, $mailbox, true, $return_opts, NULL); } /** @@ -1325,18 +1324,18 @@ * @param string $ref Reference name * @param string $mailbox Mailbox name * @param bool $subscribed Enables returning subscribed mailboxes only - * @param array $status_opts List of STATUS options - * (RFC5819: LIST-STATUS: MESSAGES, RECENT, UIDNEXT, UIDVALIDITY, UNSEEN) - * or RETURN options (RFC5258: LIST_EXTENDED: SUBSCRIBED, CHILDREN) + * @param array $return_opts List of RETURN options (RFC5819: LIST-STATUS, RFC5258: LIST-EXTENDED) + * Possible: MESSAGES, RECENT, UIDNEXT, UIDVALIDITY, UNSEEN, + * MYRIGHTS, SUBSCRIBED, CHILDREN * @param array $select_opts List of selection options (RFC5258: LIST-EXTENDED) * Possible: SUBSCRIBED, RECURSIVEMATCH, REMOTE, * SPECIAL-USE (RFC6154) * - * @return array List of mailboxes or hash of options if $status_ops argument - * is non-empty. + * @return array|bool List of mailboxes or hash of options if STATUS/MYROGHTS response + * is requested, False on error. */ protected function _listMailboxes($ref, $mailbox, $subscribed=false, - $status_opts=array(), $select_opts=array()) + $return_opts=array(), $select_opts=array()) { if (!strlen($mailbox)) { $mailbox = '*'; @@ -1354,16 +1353,24 @@ $args[] = $this->escape($ref); $args[] = $this->escape($mailbox); - if (!empty($status_opts) && $this->getCapability('LIST-EXTENDED')) { - $rets = array_intersect($status_opts, array('SUBSCRIBED', 'CHILDREN')); + if (!empty($return_opts) && $this->getCapability('LIST-EXTENDED')) { + $ext_opts = array('SUBSCRIBED', 'CHILDREN'); + $rets = array_intersect($return_opts, $ext_opts); + $return_opts = array_diff($return_opts, $rets); } - if (!empty($status_opts) && $this->getCapability('LIST-STATUS')) { - $status_opts = array_intersect($status_opts, array('MESSAGES', 'RECENT', 'UIDNEXT', 'UIDVALIDITY', 'UNSEEN')); + if (!empty($return_opts) && $this->getCapability('LIST-STATUS')) { + $lstatus = true; + $status_opts = array('MESSAGES', 'RECENT', 'UIDNEXT', 'UIDVALIDITY', 'UNSEEN'); + $opts = array_diff($return_opts, $status_opts); + $status_opts = array_diff($return_opts, $opts); if (!empty($status_opts)) { - $lstatus = true; $rets[] = 'STATUS (' . implode(' ', $status_opts) . ')'; + } + + if (!empty($opts)) { + $rets = array_merge($rets, $opts); } } @@ -1388,9 +1395,10 @@ $line = substr($response, $last, $pos - $last); $last = $pos + 2; - if (!preg_match('/^\* (LIST|LSUB|STATUS) /i', $line, $m)) { + if (!preg_match('/^\* (LIST|LSUB|STATUS|MYRIGHTS) /i', $line, $m)) { continue; } + $cmd = strtoupper($m[1]); $line = substr($line, strlen($m[0])); @@ -1421,13 +1429,20 @@ $this->data['LIST'][$mailbox], $opts)); } } - // * STATUS <mailbox> (<result>) - else if ($cmd == 'STATUS') { - list($mailbox, $status) = $this->tokenizeResponse($line, 2); + else if ($lstatus) { + // * STATUS <mailbox> (<result>) + if ($cmd == 'STATUS') { + list($mailbox, $status) = $this->tokenizeResponse($line, 2); - for ($i=0, $len=count($status); $i<$len; $i += 2) { - list($name, $value) = $this->tokenizeResponse($status, 2); - $folders[$mailbox][$name] = $value; + for ($i=0, $len=count($status); $i<$len; $i += 2) { + list($name, $value) = $this->tokenizeResponse($status, 2); + $folders[$mailbox][$name] = $value; + } + } + // * MYRIGHTS <mailbox> <acl> + else if ($cmd == 'MYRIGHTS') { + list($mailbox, $acl) = $this->tokenizeResponse($line, 2); + $folders[$mailbox]['MYRIGHTS'] = $acl; } } } @@ -1445,13 +1460,9 @@ * * @return int Number of messages, False on error */ - function countMessages($mailbox, $refresh = false) + function countMessages($mailbox) { - if ($refresh) { - $this->selected = null; - } - - if ($this->selected === $mailbox) { + if ($this->selected === $mailbox && isset($this->data['EXISTS'])) { return $this->data['EXISTS']; } @@ -1479,14 +1490,20 @@ */ function countRecent($mailbox) { - if (!strlen($mailbox)) { - $mailbox = 'INBOX'; + if ($this->selected === $mailbox && isset($this->data['RECENT'])) { + return $this->data['RECENT']; } - $this->select($mailbox); + // Check internal cache + $cache = $this->data['STATUS:'.$mailbox]; + if (!empty($cache) && isset($cache['RECENT'])) { + return (int) $cache['RECENT']; + } - if ($this->selected === $mailbox) { - return $this->data['RECENT']; + // Try STATUS (should be faster than SELECT) + $counts = $this->status($mailbox, array('RECENT')); + if (is_array($counts)) { + return (int) $counts['RECENT']; } return false; @@ -1688,7 +1705,6 @@ $encoding = $encoding ? trim($encoding) : 'US-ASCII'; $algorithm = $algorithm ? trim($algorithm) : 'REFERENCES'; $criteria = $criteria ? 'ALL '.trim($criteria) : 'ALL'; - $data = ''; list($code, $response) = $this->execute($return_uid ? 'UID THREAD' : 'THREAD', array($algorithm, $encoding, $criteria)); @@ -1902,8 +1918,8 @@ $result[$id] = ''; } } else if ($mode == 2) { - if (preg_match('/(UID|RFC822\.SIZE) ([0-9]+)/', $line, $matches)) { - $result[$id] = trim($matches[2]); + if (preg_match('/' . $index_field . ' ([0-9]+)/', $line, $matches)) { + $result[$id] = trim($matches[1]); } else { $result[$id] = 0; } @@ -2012,10 +2028,6 @@ */ protected function modFlag($mailbox, $messages, $flag, $mod = '+') { - if ($mod != '+' && $mod != '-') { - $mod = '+'; - } - if (!$this->select($mailbox)) { return false; } @@ -2025,12 +2037,31 @@ return false; } + if ($this->flags[strtoupper($flag)]) { + $flag = $this->flags[strtoupper($flag)]; + } + + if (!$flag) { + return false; + } + + // if PERMANENTFLAGS is not specified all flags are allowed + if (!empty($this->data['PERMANENTFLAGS']) + && !in_array($flag, (array) $this->data['PERMANENTFLAGS']) + && !in_array('\\*', (array) $this->data['PERMANENTFLAGS']) + ) { + return false; + } + // Clear internal status cache if ($flag == 'SEEN') { unset($this->data['STATUS:'.$mailbox]['UNSEEN']); } - $flag = $this->flags[strtoupper($flag)]; + if ($mod != '+' && $mod != '-') { + $mod = '+'; + } + $result = $this->execute('UID STORE', array( $this->compressMessageSet($messages), $mod . 'FLAGS.SILENT', "($flag)"), self::COMMAND_NORESPONSE); @@ -2093,7 +2124,7 @@ // Clear internal status cache unset($this->data['STATUS:'.$to]); - unset($this->data['STATUS:'.$from]); + $this->clear_status_cache($from); $result = $this->execute('UID MOVE', array( $this->compressMessageSet($messages), $this->escape($to)), @@ -2407,7 +2438,16 @@ return false; } - function sortHeaders($a, $field, $flag) + /** + * Sort messages by specified header field + * + * @param array $messages Array of rcube_message_header objects + * @param string $field Name of the property to sort by + * @param string $flag Sorting order (ASC|DESC) + * + * @return array Sorted input array + */ + public static function sortHeaders($messages, $field, $flag) { if (empty($field)) { $field = 'uid'; @@ -2416,57 +2456,65 @@ $field = strtolower($field); } - if ($field == 'date' || $field == 'internaldate') { - $field = 'timestamp'; - } - if (empty($flag)) { $flag = 'ASC'; - } else { + } + else { $flag = strtoupper($flag); } - $c = count($a); - if ($c > 0) { - // Strategy: - // First, we'll create an "index" array. - // Then, we'll use sort() on that array, - // and use that to sort the main array. + // Strategy: First, we'll create an "index" array. + // Then, we'll use sort() on that array, and use that to sort the main array. - // create "index" array - $index = array(); - reset($a); - while (list($key, $val) = each($a)) { - if ($field == 'timestamp') { - $data = $this->strToTime($val->date); - if (!$data) { - $data = $val->timestamp; - } - } else { - $data = $val->$field; - if (is_string($data)) { - $data = str_replace('"', '', $data); - if ($field == 'subject') { - $data = preg_replace('/^(Re: \s*|Fwd:\s*|Fw:\s*)+/i', '', $data); - } - $data = strtoupper($data); - } + $index = array(); + $result = array(); + + reset($messages); + + while (list($key, $headers) = each($messages)) { + $value = null; + + switch ($field) { + case 'arrival': + $field = 'internaldate'; + case 'date': + case 'internaldate': + case 'timestamp': + $value = self::strToTime($headers->$field); + if (!$value && $field != 'timestamp') { + $value = $headers->timestamp; } - $index[$key] = $data; + + break; + + default: + // @TODO: decode header value, convert to UTF-8 + $value = $headers->$field; + if (is_string($value)) { + $value = str_replace('"', '', $value); + if ($field == 'subject') { + $value = preg_replace('/^(Re:\s*|Fwd:\s*|Fw:\s*)+/i', '', $value); + } + + $data = strtoupper($value); + } } + $index[$key] = $value; + } + + if (!empty($index)) { // sort index if ($flag == 'ASC') { asort($index); - } else { + } + else { arsort($index); } // form new array based on index - $result = array(); - reset($index); while (list($key, $val) = each($index)) { - $result[$key] = $a[$key]; + $result[$key] = $messages[$key]; } } @@ -2538,50 +2586,61 @@ return false; } - switch ($encoding) { - case 'base64': - $mode = 1; - break; - case 'quoted-printable': - $mode = 2; - break; - case 'x-uuencode': - case 'x-uue': - case 'uue': - case 'uuencode': - $mode = 3; - break; - default: - $mode = 0; - } - - // Use BINARY extension when possible (and safe) - $binary = $mode && preg_match('/^[0-9.]+$/', $part) && $this->hasCapability('BINARY'); - $fetch_mode = $binary ? 'BINARY' : 'BODY'; - $partial = $max_bytes ? sprintf('<0.%d>', $max_bytes) : ''; - - // format request - $key = $this->nextTag(); - $request = $key . ($is_uid ? ' UID' : '') . " FETCH $id ($fetch_mode.PEEK[$part]$partial)"; - $result = false; - $found = false; - - // send request - if (!$this->putLine($request)) { - $this->setError(self::ERROR_COMMAND, "Unable to send command: $request"); - return false; - } - - if ($binary) { - // WARNING: Use $formatted argument with care, this may break binary data stream - $mode = -1; - } + $binary = true; do { + if (!$initiated) { + switch ($encoding) { + case 'base64': + $mode = 1; + break; + case 'quoted-printable': + $mode = 2; + break; + case 'x-uuencode': + case 'x-uue': + case 'uue': + case 'uuencode': + $mode = 3; + break; + default: + $mode = 0; + } + + // Use BINARY extension when possible (and safe) + $binary = $binary && $mode && preg_match('/^[0-9.]+$/', $part) && $this->hasCapability('BINARY'); + $fetch_mode = $binary ? 'BINARY' : 'BODY'; + $partial = $max_bytes ? sprintf('<0.%d>', $max_bytes) : ''; + + // format request + $key = $this->nextTag(); + $request = $key . ($is_uid ? ' UID' : '') . " FETCH $id ($fetch_mode.PEEK[$part]$partial)"; + $result = false; + $found = false; + $initiated = true; + + // send request + if (!$this->putLine($request)) { + $this->setError(self::ERROR_COMMAND, "Unable to send command: $request"); + return false; + } + + if ($binary) { + // WARNING: Use $formatted argument with care, this may break binary data stream + $mode = -1; + } + } + $line = trim($this->readLine(1024)); if (!$line) { break; + } + + // handle UNKNOWN-CTE response - RFC 3516, try again with standard BODY request + if ($binary && !$found && preg_match('/^' . $key . ' NO \[UNKNOWN-CTE\]/i', $line)) { + $binary = $initiated = false; + continue; } // skip irrelevant untagged responses (we have a result already) @@ -2644,7 +2703,7 @@ // BASE64 if ($mode == 1) { - $line = rtrim($line, "\t\r\n\0\x0B"); + $line = preg_replace('|[^a-zA-Z0-9+=/]|', '', $line); // create chunks with proper length for base64 decoding $line = $prev.$line; $length = strlen($line); @@ -2689,7 +2748,7 @@ } } } - } while (!$this->startsWith($line, $key, true)); + } while (!$this->startsWith($line, $key, true) || !$initiated); if ($result !== false) { if ($file) { @@ -2859,57 +2918,64 @@ /** * Returns QUOTA information * + * @param string $mailbox Mailbox name + * * @return array Quota information */ - function getQuota() + function getQuota($mailbox = null) { - /* - * GETQUOTAROOT "INBOX" - * QUOTAROOT INBOX user/rchijiiwa1 - * QUOTA user/rchijiiwa1 (STORAGE 654 9765) - * OK Completed - */ - $result = false; - $quota_lines = array(); - $key = $this->nextTag(); - $command = $key . ' GETQUOTAROOT INBOX'; - - // get line(s) containing quota info - if ($this->putLine($command)) { - do { - $line = rtrim($this->readLine(5000)); - if (preg_match('/^\* QUOTA /', $line)) { - $quota_lines[] = $line; - } - } while (!$this->startsWith($line, $key, true, true)); - } - else { - $this->setError(self::ERROR_COMMAND, "Unable to send command: $command"); + if ($mailbox === null || $mailbox === '') { + $mailbox = 'INBOX'; } - // return false if not found, parse if found + // a0001 GETQUOTAROOT INBOX + // * QUOTAROOT INBOX user/sample + // * QUOTA user/sample (STORAGE 654 9765) + // a0001 OK Completed + + list($code, $response) = $this->execute('GETQUOTAROOT', array($this->escape($mailbox))); + + $result = false; $min_free = PHP_INT_MAX; - foreach ($quota_lines as $key => $quota_line) { - $quota_line = str_replace(array('(', ')'), '', $quota_line); - $parts = explode(' ', $quota_line); - $storage_part = array_search('STORAGE', $parts); + $all = array(); - if (!$storage_part) { - continue; + if ($code == self::ERROR_OK) { + foreach (explode("\n", $response) as $line) { + if (preg_match('/^\* QUOTA /', $line)) { + list(, , $quota_root) = $this->tokenizeResponse($line, 3); + + while ($line) { + list($type, $used, $total) = $this->tokenizeResponse($line, 1); + $type = strtolower($type); + + if ($type && $total) { + $all[$quota_root][$type]['used'] = intval($used); + $all[$quota_root][$type]['total'] = intval($total); + } + } + + if (empty($all[$quota_root]['storage'])) { + continue; + } + + $used = $all[$quota_root]['storage']['used']; + $total = $all[$quota_root]['storage']['total']; + $free = $total - $used; + + // calculate lowest available space from all storage quotas + if ($free < $min_free) { + $min_free = $free; + $result['used'] = $used; + $result['total'] = $total; + $result['percent'] = min(100, round(($used/max(1,$total))*100)); + $result['free'] = 100 - $result['percent']; + } + } } + } - $used = intval($parts[$storage_part+1]); - $total = intval($parts[$storage_part+2]); - $free = $total - $used; - - // return lowest available space from all quotas - if ($free < $min_free) { - $min_free = $free; - $result['used'] = $used; - $result['total'] = $total; - $result['percent'] = min(100, round(($used/max(1,$total))*100)); - $result['free'] = 100 - $result['percent']; - } + if (!empty($result)) { + $result['all'] = $all; } return $result; @@ -3167,9 +3233,9 @@ for ($i=0; $i<$size; $i++) { if (isset($mbox) && is_array($data[$i])) { $size_sub = count($data[$i]); - for ($x=0; $x<$size_sub; $x++) { + for ($x=0; $x<$size_sub; $x+=2) { if ($data[$i][$x+1] !== null) - $result[$mbox][$data[$i][$x]] = $data[$i][++$x]; + $result[$mbox][$data[$i][$x]] = $data[$i][$x+1]; } unset($data[$i]); } @@ -3187,8 +3253,8 @@ } } else if (isset($mbox)) { - if ($data[$i+1] !== null) - $result[$mbox][$data[$i]] = $data[++$i]; + if ($data[++$i] !== null) + $result[$mbox][$data[$i-1]] = $data[$i]; unset($data[$i]); unset($data[$i-1]); } @@ -3222,11 +3288,6 @@ } foreach ($data as $entry) { - // Workaround cyrus-murder bug, the entry[2] string needs to be escaped - if (self::$mupdate) { - $entry[2] = addcslashes($entry[2], '\\"'); - } - // ANNOTATEMORE drafts before version 08 require quoted parameters $entries[] = sprintf('%s (%s %s)', $this->escape($entry[0], true), $this->escape($entry[1], true), $this->escape($entry[2], true)); @@ -3719,6 +3780,17 @@ } /** + * Clear internal status cache + */ + protected function clear_status_cache($mailbox) + { + unset($this->data['STATUS:' . $mailbox]); + unset($this->data['EXISTS']); + unset($this->data['RECENT']); + unset($this->data['UNSEEN']); + } + + /** * Converts flags array into string for inclusion in IMAP command * * @param array $flags Flags (see self::flags) @@ -3789,10 +3861,6 @@ if (!isset($this->prefs['literal+']) && in_array('LITERAL+', $this->capability)) { $this->prefs['literal+'] = true; - } - - if (preg_match('/(\[| )MUPDATE=.*/', $str)) { - self::$mupdate = true; } if ($trusted) { -- Gitblit v1.9.1