- Fixed issues with big memory allocation of IMAP results, improved a lot of rcube_imap class

pull/1/head
alecpl 13 years ago
parent 86130d6366
commit 40c45e9de9

@ -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)

File diff suppressed because it is too large Load Diff

@ -108,7 +108,7 @@ class rcube_imap_cache
/**
* 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 @@ class rcube_imap_cache
{
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 @@ class rcube_imap_cache
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 @@ class rcube_imap_cache
$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 @@ class rcube_imap_cache
// 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)
@ -250,8 +244,6 @@ class rcube_imap_cache
$this->icache[$mailbox]['thread_queried'] = true;
}
$data = null;
// Entry exist, check cache status
if (!empty($index)) {
$exists = true;
@ -269,22 +261,21 @@ class rcube_imap_cache
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,29 +283,16 @@ class rcube_imap_cache
* 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
$sql_result = $this->db->query(
"SELECT uid, data, flags"
@ -334,10 +312,6 @@ class rcube_imap_cache
// 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 @@ class rcube_imap_cache
// 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 @@ class rcube_imap_cache
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
@ -616,46 +585,12 @@ class rcube_imap_cache
}
/**
* @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 @@ class rcube_imap_cache
$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 @@ class rcube_imap_cache
$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 @@ class rcube_imap_cache
/**
* 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 @@ class rcube_imap_cache
/**
* 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 @@ class rcube_imap_cache
*/
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 @@ class rcube_imap_cache
// 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 @@ class rcube_imap_cache
// @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 @@ class rcube_imap_cache
// 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 @@ class rcube_imap_cache
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 @@ class rcube_imap_cache
}
$sort_field = $index['sort_field'];
$sort_order = $index['sort_order'];
$sort_order = $index['object']->getParameters('ORDER');
$exists = true;
// Validate index
@ -1069,14 +1000,14 @@ class rcube_imap_cache
$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;
}
@ -1102,20 +1033,6 @@ class rcube_imap_cache
}
/**
* 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);
}
}
/**
* Saves message stored in internal cache
*/
@ -1170,43 +1087,18 @@ class rcube_imap_cache
*/
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;
}
}

@ -26,7 +26,6 @@
*/
/**
* Struct representing an e-mail message header
*
@ -1218,8 +1217,8 @@ class rcube_imap_generic
// 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 @@ class rcube_imap_generic
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 @@ class rcube_imap_generic
'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 @@ class rcube_imap_generic
}
// build FETCH command string
$key = $this->nextTag();
$cmd = $uidfetch ? 'UID FETCH' : 'FETCH';
$deleted = $skip_deleted ? ' FLAGS' : '';
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)";
$key = $this->nextTag();
$cmd = $uidfetch ? 'UID FETCH' : 'FETCH';
$fields = array();
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 @@ class rcube_imap_generic
$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 @@ class rcube_imap_generic
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,23 +1615,16 @@ class rcube_imap_generic
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);
}
/**
* FETCH command (RFC3501)
*
@ -1970,68 +2018,30 @@ class rcube_imap_generic
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)
{
$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);
}
}
return $node;
}
function thread($mailbox, $algorithm='REFERENCES', $criteria='', $encoding='US-ASCII')
/**
* 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')
{
require_once dirname(__FILE__) . '/rcube_result_thread.php';
$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 @@ class rcube_imap_generic
$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 @@ class rcube_imap_generic
* @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 @@ class rcube_imap_generic
if (!empty($items) && $esearch) {
$params .= 'RETURN (' . implode(' ', $items) . ')';
}
if (!empty($criteria)) {
$modseq = stripos($criteria, 'MODSEQ') !== false;
$params .= ($params ? ' ' : '') . $criteria;
@ -2108,65 +2110,11 @@ class rcube_imap_generic
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);
}
/**

@ -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'];
}
}

@ -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;
}
}

@ -93,7 +93,7 @@ if (empty($RCMAIL->action) || $RCMAIL->action == 'list') {
$_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);

@ -19,52 +19,29 @@
*/
$uid = get_input_value('_uid', RCUBE_INPUT_GET);
// 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;
$uid = get_input_value('_uid', RCUBE_INPUT_GET);
$index = $IMAP->message_index(null, $_SESSION['sort_col'], $_SESSION['sort_order']);
$cnt = $index->countMessages();
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 @@ $OUTPUT->set_env('messagecount', 1);
// 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();

@ -109,10 +109,6 @@ $search_str = trim($search_str);
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 @@ if ($search_str) {
}
$_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);

@ -701,11 +701,11 @@ if ($store_target) {
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 @@ if ($savedraft) {
// 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;

Loading…
Cancel
Save