Thomas Bruederli
2013-04-25 ddfdd8938d78b40842a984d310e3c35af30ece0a
commit | author | age
80152b 1 <?php
A 2
3 /*
4  +-----------------------------------------------------------------------+
5  | This file is part of the Roundcube Webmail client                     |
982353 6  | Copyright (C) 2005-2012, The Roundcube Dev Team                       |
7fe381 7  |                                                                       |
T 8  | Licensed under the GNU General Public License version 3 or            |
9  | any later version with exceptions for skins & plugins.                |
10  | See the README file for a full license statement.                     |
80152b 11  |                                                                       |
A 12  | PURPOSE:                                                              |
13  |   Caching of IMAP folder contents (messages and index)                |
14  +-----------------------------------------------------------------------+
15  | Author: Thomas Bruederli <roundcube@gmail.com>                        |
16  | Author: Aleksander Machniak <alec@alec.pl>                            |
17  +-----------------------------------------------------------------------+
18 */
19
20 /**
21  * Interface class for accessing Roundcube messages cache
22  *
9ab346 23  * @package    Framework
AM 24  * @subpackage Storage
80152b 25  * @author     Thomas Bruederli <roundcube@gmail.com>
A 26  * @author     Aleksander Machniak <alec@alec.pl>
27  */
28 class rcube_imap_cache
29 {
30     /**
31      * Instance of rcube_imap
32      *
33      * @var rcube_imap
34      */
35     private $imap;
36
37     /**
0d94fd 38      * Instance of rcube_db
80152b 39      *
0d94fd 40      * @var rcube_db
80152b 41      */
A 42     private $db;
43
44     /**
45      * User ID
46      *
47      * @var int
48      */
49     private $userid;
50
51     /**
52      * Internal (in-memory) cache
53      *
54      * @var array
55      */
56     private $icache = array();
57
58     private $skip_deleted = false;
59
609d39 60     /**
A 61      * List of known flags. Thanks to this we can handle flag changes
62      * with good performance. Bad thing is we need to know used flags.
63      */
64     public $flags = array(
65         1       => 'SEEN',          // RFC3501
66         2       => 'DELETED',       // RFC3501
67         4       => 'ANSWERED',      // RFC3501
68         8       => 'FLAGGED',       // RFC3501
69         16      => 'DRAFT',         // RFC3501
70         32      => 'MDNSENT',       // RFC3503
71         64      => 'FORWARDED',     // RFC5550
72         128     => 'SUBMITPENDING', // RFC5550
73         256     => 'SUBMITTED',     // RFC5550
74         512     => 'JUNK',
75         1024    => 'NONJUNK',
76         2048    => 'LABEL1',
77         4096    => 'LABEL2',
78         8192    => 'LABEL3',
79         16384   => 'LABEL4',
80         32768   => 'LABEL5',
81     );
80152b 82
A 83
84     /**
85      * Object constructor.
86      */
87     function __construct($db, $imap, $userid, $skip_deleted)
88     {
89         $this->db           = $db;
90         $this->imap         = $imap;
0c2596 91         $this->userid       = $userid;
80152b 92         $this->skip_deleted = $skip_deleted;
A 93     }
94
95
96     /**
97      * Cleanup actions (on shutdown).
98      */
99     public function close()
100     {
101         $this->save_icache();
102         $this->icache = null;
103     }
104
105
106     /**
40c45e 107      * Return (sorted) messages index (UIDs).
80152b 108      * If index doesn't exist or is invalid, will be updated.
A 109      *
110      * @param string  $mailbox     Folder name
111      * @param string  $sort_field  Sorting column
112      * @param string  $sort_order  Sorting order (ASC|DESC)
113      * @param bool    $exiting     Skip index initialization if it doesn't exist in DB
114      *
115      * @return array Messages index
116      */
117     function get_index($mailbox, $sort_field = null, $sort_order = null, $existing = false)
118     {
a267c6 119         if (empty($this->icache[$mailbox])) {
80152b 120             $this->icache[$mailbox] = array();
a267c6 121         }
2a5702 122
80152b 123         $sort_order = strtoupper($sort_order) == 'ASC' ? 'ASC' : 'DESC';
A 124
125         // Seek in internal cache
609d39 126         if (array_key_exists('index', $this->icache[$mailbox])) {
A 127             // The index was fetched from database already, but not validated yet
40c45e 128             if (!array_key_exists('object', $this->icache[$mailbox]['index'])) {
609d39 129                 $index = $this->icache[$mailbox]['index'];
A 130             }
131             // We've got a valid index
40c45e 132             else if ($sort_field == 'ANY' || $this->icache[$mailbox]['index']['sort_field'] == $sort_field) {
A 133                 $result = $this->icache[$mailbox]['index']['object'];
c321a9 134                 if ($result->get_parameters('ORDER') != $sort_order) {
40c45e 135                     $result->revert();
A 136                 }
137                 return $result;
609d39 138             }
80152b 139         }
A 140
141         // Get index from DB (if DB wasn't already queried)
609d39 142         if (empty($index) && empty($this->icache[$mailbox]['index_queried'])) {
80152b 143             $index = $this->get_index_row($mailbox);
A 144
145             // set the flag that DB was already queried for index
146             // this way we'll be able to skip one SELECT, when
147             // get_index() is called more than once
148             $this->icache[$mailbox]['index_queried'] = true;
149         }
609d39 150
A 151         $data = null;
80152b 152
A 153         // @TODO: Think about skipping validation checks.
154         // If we could check only every 10 minutes, we would be able to skip
155         // expensive checks, mailbox selection or even IMAP connection, this would require
156         // additional logic to force cache invalidation in some cases
157         // and many rcube_imap changes to connect when needed
158
609d39 159         // Entry exists, check cache status
80152b 160         if (!empty($index)) {
A 161             $exists = true;
162
163             if ($sort_field == 'ANY') {
164                 $sort_field = $index['sort_field'];
165             }
166
167             if ($sort_field != $index['sort_field']) {
168                 $is_valid = false;
169             }
170             else {
171                 $is_valid = $this->validate($mailbox, $index, $exists);
172             }
2a5702 173
80152b 174             if ($is_valid) {
40c45e 175                 $data = $index['object'];
80152b 176                 // revert the order if needed
c321a9 177                 if ($data->get_parameters('ORDER') != $sort_order) {
40c45e 178                     $data->revert();
A 179                 }
80152b 180             }
A 181         }
182         else {
183             if ($existing) {
184                 return null;
185             }
186             else if ($sort_field == 'ANY') {
187                 $sort_field = '';
188             }
609d39 189
A 190             // Got it in internal cache, so the row already exist
191             $exists = array_key_exists('index', $this->icache[$mailbox]);
80152b 192         }
A 193
194         // Index not found, not valid or sort field changed, get index from IMAP server
195         if ($data === null) {
196             // Get mailbox data (UIDVALIDITY, counters, etc.) for status check
c321a9 197             $mbox_data = $this->imap->folder_data($mailbox);
609d39 198             $data      = $this->get_index_data($mailbox, $sort_field, $sort_order, $mbox_data);
80152b 199
A 200             // insert/update
40c45e 201             $this->add_index_row($mailbox, $sort_field, $data, $mbox_data, $exists, $index['modseq']);
80152b 202         }
A 203
204         $this->icache[$mailbox]['index'] = array(
40c45e 205             'object'     => $data,
80152b 206             'sort_field' => $sort_field,
609d39 207             'modseq'     => !empty($index['modseq']) ? $index['modseq'] : $mbox_data['HIGHESTMODSEQ']
80152b 208         );
A 209
210         return $data;
211     }
212
213
214     /**
215      * Return messages thread.
216      * If threaded index doesn't exist or is invalid, will be updated.
217      *
218      * @param string  $mailbox     Folder name
219      * @param string  $sort_field  Sorting column
220      * @param string  $sort_order  Sorting order (ASC|DESC)
221      *
222      * @return array Messages threaded index
223      */
224     function get_thread($mailbox)
225     {
a267c6 226         if (empty($this->icache[$mailbox])) {
80152b 227             $this->icache[$mailbox] = array();
a267c6 228         }
80152b 229
A 230         // Seek in internal cache
231         if (array_key_exists('thread', $this->icache[$mailbox])) {
40c45e 232             return $this->icache[$mailbox]['thread']['object'];
80152b 233         }
A 234
609d39 235         // Get thread from DB (if DB wasn't already queried)
A 236         if (empty($this->icache[$mailbox]['thread_queried'])) {
237             $index = $this->get_thread_row($mailbox);
238
239             // set the flag that DB was already queried for thread
240             // this way we'll be able to skip one SELECT, when
241             // get_thread() is called more than once or after clear()
242             $this->icache[$mailbox]['thread_queried'] = true;
243         }
80152b 244
A 245         // Entry exist, check cache status
246         if (!empty($index)) {
247             $exists   = true;
248             $is_valid = $this->validate($mailbox, $index, $exists);
249
250             if (!$is_valid) {
251                 $index = null;
252             }
253         }
254
255         // Index not found or not valid, get index from IMAP server
256         if ($index === null) {
257             // Get mailbox data (UIDVALIDITY, counters, etc.) for status check
c321a9 258             $mbox_data = $this->imap->folder_data($mailbox);
80152b 259
A 260             if ($mbox_data['EXISTS']) {
261                 // get all threads (default sort order)
40c45e 262                 $threads = $this->imap->fetch_threads($mailbox, true);
A 263             }
264             else {
2a5702 265                 $threads = new rcube_result_thread($mailbox, '* THREAD');
80152b 266             }
A 267
40c45e 268             $index['object'] = $threads;
80152b 269
A 270             // insert/update
40c45e 271             $this->add_thread_row($mailbox, $threads, $mbox_data, $exists);
80152b 272         }
A 273
274         $this->icache[$mailbox]['thread'] = $index;
275
40c45e 276         return $index['object'];
80152b 277     }
A 278
279
280     /**
281      * Returns list of messages (headers). See rcube_imap::fetch_headers().
282      *
283      * @param string $mailbox  Folder name
40c45e 284      * @param array  $msgs     Message UIDs
80152b 285      *
0c2596 286      * @return array The list of messages (rcube_message_header) indexed by UID
80152b 287      */
40c45e 288     function get_messages($mailbox, $msgs = array())
80152b 289     {
A 290         if (empty($msgs)) {
291             return array();
292         }
293
294         // Fetch messages from cache
295         $sql_result = $this->db->query(
609d39 296             "SELECT uid, data, flags"
0c2596 297             ." FROM ".$this->db->table_name('cache_messages')
80152b 298             ." WHERE user_id = ?"
A 299                 ." AND mailbox = ?"
300                 ." AND uid IN (".$this->db->array2list($msgs, 'integer').")",
301             $this->userid, $mailbox);
302
303         $msgs   = array_flip($msgs);
304         $result = array();
305
306         while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
307             $uid          = intval($sql_arr['uid']);
308             $result[$uid] = $this->build_message($sql_arr);
609d39 309
80152b 310             if (!empty($result[$uid])) {
7eb4f2 311                 // save memory, we don't need message body here (?)
AM 312                 $result[$uid]->body = null;
313
80152b 314                 unset($msgs[$uid]);
A 315             }
316         }
317
318         // Fetch not found messages from IMAP server
319         if (!empty($msgs)) {
40c45e 320             $messages = $this->imap->fetch_headers($mailbox, array_keys($msgs), false, true);
80152b 321
A 322             // Insert to DB and add to result list
323             if (!empty($messages)) {
324                 foreach ($messages as $msg) {
325                     $this->add_message($mailbox, $msg, !array_key_exists($msg->uid, $result));
326                     $result[$msg->uid] = $msg;
327                 }
328             }
329         }
330
331         return $result;
332     }
333
334
335     /**
336      * Returns message data.
337      *
338      * @param string $mailbox  Folder name
339      * @param int    $uid      Message UID
609d39 340      * @param bool   $update   If message doesn't exists in cache it will be fetched
A 341      *                         from IMAP server
342      * @param bool   $no_cache Enables internal cache usage
80152b 343      *
0c2596 344      * @return rcube_message_header Message data
80152b 345      */
609d39 346     function get_message($mailbox, $uid, $update = true, $cache = true)
80152b 347     {
A 348         // Check internal cache
982353 349         if ($this->icache['__message']
AM 350             && $this->icache['__message']['mailbox'] == $mailbox
351             && $this->icache['__message']['object']->uid == $uid
80152b 352         ) {
982353 353             return $this->icache['__message']['object'];
80152b 354         }
A 355
356         $sql_result = $this->db->query(
609d39 357             "SELECT flags, data"
0c2596 358             ." FROM ".$this->db->table_name('cache_messages')
80152b 359             ." WHERE user_id = ?"
A 360                 ." AND mailbox = ?"
361                 ." AND uid = ?",
362                 $this->userid, $mailbox, (int)$uid);
363
364         if ($sql_arr = $this->db->fetch_assoc($sql_result)) {
365             $message = $this->build_message($sql_arr);
366             $found   = true;
367         }
368
369         // Get the message from IMAP server
609d39 370         if (empty($message) && $update) {
c321a9 371             $message = $this->imap->get_message_headers($uid, $mailbox, true);
80152b 372             // cache will be updated in close(), see below
A 373         }
374
375         // Save the message in internal cache, will be written to DB in close()
376         // Common scenario: user opens unseen message
377         // - get message (SELECT)
378         // - set message headers/structure (INSERT or UPDATE)
379         // - set \Seen flag (UPDATE)
380         // This way we can skip one UPDATE
609d39 381         if (!empty($message) && $cache) {
80152b 382             // Save current message from internal cache
A 383             $this->save_icache();
384
982353 385             $this->icache['__message'] = array(
80152b 386                 'object'  => $message,
A 387                 'mailbox' => $mailbox,
388                 'exists'  => $found,
389                 'md5sum'  => md5(serialize($message)),
390             );
391         }
392
393         return $message;
394     }
395
396
397     /**
398      * Saves the message in cache.
399      *
0c2596 400      * @param string               $mailbox  Folder name
A 401      * @param rcube_message_header $message  Message data
402      * @param bool                 $force    Skips message in-cache existance check
80152b 403      */
A 404     function add_message($mailbox, $message, $force = false)
405     {
a267c6 406         if (!is_object($message) || empty($message->uid)) {
80152b 407             return;
a267c6 408         }
80152b 409
609d39 410         $msg   = serialize($this->db->encode(clone $message));
A 411         $flags = 0;
80152b 412
609d39 413         if (!empty($message->flags)) {
a267c6 414             foreach ($this->flags as $idx => $flag) {
A 415                 if (!empty($message->flags[$flag])) {
609d39 416                     $flags += $idx;
a267c6 417                 }
A 418             }
609d39 419         }
A 420         unset($msg->flags);
80152b 421
A 422         // update cache record (even if it exists, the update
423         // here will work as select, assume row exist if affected_rows=0)
424         if (!$force) {
425             $res = $this->db->query(
0c2596 426                 "UPDATE ".$this->db->table_name('cache_messages')
609d39 427                 ." SET flags = ?, data = ?, changed = ".$this->db->now()
80152b 428                 ." WHERE user_id = ?"
A 429                     ." AND mailbox = ?"
430                     ." AND uid = ?",
609d39 431                 $flags, $msg, $this->userid, $mailbox, (int) $message->uid);
80152b 432
a267c6 433             if ($this->db->affected_rows()) {
80152b 434                 return;
a267c6 435             }
80152b 436         }
A 437
438         // insert new record
439         $this->db->query(
0c2596 440             "INSERT INTO ".$this->db->table_name('cache_messages')
609d39 441             ." (user_id, mailbox, uid, flags, changed, data)"
A 442             ." VALUES (?, ?, ?, ?, ".$this->db->now().", ?)",
443             $this->userid, $mailbox, (int) $message->uid, $flags, $msg);
80152b 444     }
A 445
446
447     /**
448      * Sets the flag for specified message.
449      *
450      * @param string  $mailbox  Folder name
451      * @param array   $uids     Message UIDs or null to change flag
452      *                          of all messages in a folder
453      * @param string  $flag     The name of the flag
454      * @param bool    $enabled  Flag state
455      */
456     function change_flag($mailbox, $uids, $flag, $enabled = false)
457     {
982353 458         if (empty($uids)) {
AM 459             return;
460         }
461
609d39 462         $flag = strtoupper($flag);
A 463         $idx  = (int) array_search($flag, $this->flags);
982353 464         $uids = (array) $uids;
80152b 465
609d39 466         if (!$idx) {
A 467             return;
468         }
80152b 469
609d39 470         // Internal cache update
982353 471         if (($message = $this->icache['__message'])
AM 472             && $message['mailbox'] === $mailbox
473             && in_array($message['object']->uid, $uids)
609d39 474         ) {
A 475             $message['object']->flags[$flag] = $enabled;
982353 476
AM 477             if (count($uids) == 1) {
478                 return;
479             }
80152b 480         }
609d39 481
A 482         $this->db->query(
0c2596 483             "UPDATE ".$this->db->table_name('cache_messages')
609d39 484             ." SET changed = ".$this->db->now()
A 485             .", flags = flags ".($enabled ? "+ $idx" : "- $idx")
486             ." WHERE user_id = ?"
487                 ." AND mailbox = ?"
336d20 488                 .(!empty($uids) ? " AND uid IN (".$this->db->array2list($uids, 'integer').")" : "")
609d39 489                 ." AND (flags & $idx) ".($enabled ? "= 0" : "= $idx"),
A 490             $this->userid, $mailbox);
80152b 491     }
A 492
493
494     /**
495      * Removes message(s) from cache.
496      *
497      * @param string $mailbox  Folder name
498      * @param array  $uids     Message UIDs, NULL removes all messages
499      */
500     function remove_message($mailbox = null, $uids = null)
501     {
502         if (!strlen($mailbox)) {
503             $this->db->query(
0c2596 504                 "DELETE FROM ".$this->db->table_name('cache_messages')
80152b 505                 ." WHERE user_id = ?",
A 506                 $this->userid);
507         }
508         else {
509             // Remove the message from internal cache
982353 510             if (!empty($uids) && ($message = $this->icache['__message'])
AM 511                 && $message['mailbox'] === $mailbox
512                 && in_array($message['object']->uid, (array)$uids)
80152b 513             ) {
982353 514                 $this->icache['__message'] = null;
80152b 515             }
A 516
517             $this->db->query(
0c2596 518                 "DELETE FROM ".$this->db->table_name('cache_messages')
80152b 519                 ." WHERE user_id = ?"
0c2596 520                     ." AND mailbox = ?"
80152b 521                     .($uids !== null ? " AND uid IN (".$this->db->array2list((array)$uids, 'integer').")" : ""),
0c2596 522                 $this->userid, $mailbox);
80152b 523         }
A 524     }
525
526
527     /**
528      * Clears index cache.
529      *
530      * @param string  $mailbox     Folder name
609d39 531      * @param bool    $remove      Enable to remove the DB row
80152b 532      */
609d39 533     function remove_index($mailbox = null, $remove = false)
80152b 534     {
609d39 535         // The index should be only removed from database when
A 536         // UIDVALIDITY was detected or the mailbox is empty
537         // otherwise use 'valid' flag to not loose HIGHESTMODSEQ value
a267c6 538         if ($remove) {
609d39 539             $this->db->query(
0c2596 540                 "DELETE FROM ".$this->db->table_name('cache_index')
A 541                 ." WHERE user_id = ?"
542                     .(strlen($mailbox) ? " AND mailbox = ".$this->db->quote($mailbox) : ""),
543                 $this->userid
609d39 544             );
a267c6 545         }
A 546         else {
609d39 547             $this->db->query(
0c2596 548                 "UPDATE ".$this->db->table_name('cache_index')
609d39 549                 ." SET valid = 0"
0c2596 550                 ." WHERE user_id = ?"
A 551                     .(strlen($mailbox) ? " AND mailbox = ".$this->db->quote($mailbox) : ""),
552                 $this->userid
609d39 553             );
a267c6 554         }
80152b 555
609d39 556         if (strlen($mailbox)) {
80152b 557             unset($this->icache[$mailbox]['index']);
609d39 558             // Index removed, set flag to skip SELECT query in get_index()
A 559             $this->icache[$mailbox]['index_queried'] = true;
560         }
a267c6 561         else {
80152b 562             $this->icache = array();
a267c6 563         }
80152b 564     }
A 565
566
567     /**
568      * Clears thread cache.
569      *
570      * @param string  $mailbox     Folder name
571      */
572     function remove_thread($mailbox = null)
573     {
574         $this->db->query(
0c2596 575             "DELETE FROM ".$this->db->table_name('cache_thread')
A 576             ." WHERE user_id = ?"
577                 .(strlen($mailbox) ? " AND mailbox = ".$this->db->quote($mailbox) : ""),
578             $this->userid
80152b 579         );
A 580
609d39 581         if (strlen($mailbox)) {
80152b 582             unset($this->icache[$mailbox]['thread']);
609d39 583             // Thread data removed, set flag to skip SELECT query in get_thread()
A 584             $this->icache[$mailbox]['thread_queried'] = true;
585         }
a267c6 586         else {
80152b 587             $this->icache = array();
a267c6 588         }
80152b 589     }
A 590
591
592     /**
593      * Clears the cache.
594      *
595      * @param string $mailbox  Folder name
596      * @param array  $uids     Message UIDs, NULL removes all messages in a folder
597      */
598     function clear($mailbox = null, $uids = null)
599     {
609d39 600         $this->remove_index($mailbox, true);
80152b 601         $this->remove_thread($mailbox);
A 602         $this->remove_message($mailbox, $uids);
603     }
604
605
606     /**
fec2d8 607      * Delete cache entries older than TTL
T 608      *
609      * @param string $ttl  Lifetime of message cache entries
610      */
611     function expunge($ttl)
612     {
613         // get expiration timestamp
614         $ts = get_offset_time($ttl, -1);
615
6075f0 616         $this->db->query("DELETE FROM ".$this->db->table_name('cache_messages')
fec2d8 617               ." WHERE changed < " . $this->db->fromunixtime($ts));
T 618
6075f0 619         $this->db->query("DELETE FROM ".$this->db->table_name('cache_index')
fec2d8 620               ." WHERE changed < " . $this->db->fromunixtime($ts));
T 621
6075f0 622         $this->db->query("DELETE FROM ".$this->db->table_name('cache_thread')
fec2d8 623               ." WHERE changed < " . $this->db->fromunixtime($ts));
T 624     }
625
626
627     /**
80152b 628      * Fetches index data from database
A 629      */
630     private function get_index_row($mailbox)
631     {
632         // Get index from DB
633         $sql_result = $this->db->query(
609d39 634             "SELECT data, valid"
0c2596 635             ." FROM ".$this->db->table_name('cache_index')
80152b 636             ." WHERE user_id = ?"
A 637                 ." AND mailbox = ?",
638             $this->userid, $mailbox);
639
640         if ($sql_arr = $this->db->fetch_assoc($sql_result)) {
40c45e 641             $data  = explode('@', $sql_arr['data']);
A 642             $index = @unserialize($data[0]);
643             unset($data[0]);
644
645             if (empty($index)) {
646                 $index = new rcube_result_index($mailbox);
647             }
80152b 648
A 649             return array(
609d39 650                 'valid'      => $sql_arr['valid'],
40c45e 651                 'object'     => $index,
A 652                 'sort_field' => $data[1],
653                 'deleted'    => $data[2],
654                 'validity'   => $data[3],
655                 'uidnext'    => $data[4],
656                 'modseq'     => $data[5],
80152b 657             );
A 658         }
659
660         return null;
661     }
662
663
664     /**
665      * Fetches thread data from database
666      */
667     private function get_thread_row($mailbox)
668     {
669         // Get thread from DB
670         $sql_result = $this->db->query(
671             "SELECT data"
0c2596 672             ." FROM ".$this->db->table_name('cache_thread')
80152b 673             ." WHERE user_id = ?"
A 674                 ." AND mailbox = ?",
675             $this->userid, $mailbox);
676
677         if ($sql_arr = $this->db->fetch_assoc($sql_result)) {
40c45e 678             $data   = explode('@', $sql_arr['data']);
A 679             $thread = @unserialize($data[0]);
680             unset($data[0]);
80152b 681
40c45e 682             if (empty($thread)) {
2a5702 683                 $thread = new rcube_result_thread($mailbox);
40c45e 684             }
80152b 685
A 686             return array(
40c45e 687                 'object'   => $thread,
80152b 688                 'deleted'  => $data[1],
A 689                 'validity' => $data[2],
690                 'uidnext'  => $data[3],
691             );
692         }
693
694         return null;
695     }
696
697
698     /**
699      * Saves index data into database
700      */
40c45e 701     private function add_index_row($mailbox, $sort_field,
A 702         $data, $mbox_data = array(), $exists = false, $modseq = null)
80152b 703     {
A 704         $data = array(
40c45e 705             serialize($data),
80152b 706             $sort_field,
A 707             (int) $this->skip_deleted,
708             (int) $mbox_data['UIDVALIDITY'],
709             (int) $mbox_data['UIDNEXT'],
609d39 710             $modseq ? $modseq : $mbox_data['HIGHESTMODSEQ'],
80152b 711         );
A 712         $data = implode('@', $data);
713
a267c6 714         if ($exists) {
80152b 715             $sql_result = $this->db->query(
0c2596 716                 "UPDATE ".$this->db->table_name('cache_index')
609d39 717                 ." SET data = ?, valid = 1, changed = ".$this->db->now()
80152b 718                 ." WHERE user_id = ?"
A 719                     ." AND mailbox = ?",
720                 $data, $this->userid, $mailbox);
a267c6 721         }
A 722         else {
80152b 723             $sql_result = $this->db->query(
0c2596 724                 "INSERT INTO ".$this->db->table_name('cache_index')
609d39 725                 ." (user_id, mailbox, data, valid, changed)"
A 726                 ." VALUES (?, ?, ?, 1, ".$this->db->now().")",
80152b 727                 $this->userid, $mailbox, $data);
a267c6 728         }
80152b 729     }
A 730
731
732     /**
733      * Saves thread data into database
734      */
40c45e 735     private function add_thread_row($mailbox, $data, $mbox_data = array(), $exists = false)
80152b 736     {
A 737         $data = array(
40c45e 738             serialize($data),
80152b 739             (int) $this->skip_deleted,
A 740             (int) $mbox_data['UIDVALIDITY'],
741             (int) $mbox_data['UIDNEXT'],
742         );
743         $data = implode('@', $data);
744
a267c6 745         if ($exists) {
80152b 746             $sql_result = $this->db->query(
0c2596 747                 "UPDATE ".$this->db->table_name('cache_thread')
80152b 748                 ." SET data = ?, changed = ".$this->db->now()
A 749                 ." WHERE user_id = ?"
750                     ." AND mailbox = ?",
751                 $data, $this->userid, $mailbox);
a267c6 752         }
A 753         else {
80152b 754             $sql_result = $this->db->query(
0c2596 755                 "INSERT INTO ".$this->db->table_name('cache_thread')
80152b 756                 ." (user_id, mailbox, data, changed)"
A 757                 ." VALUES (?, ?, ?, ".$this->db->now().")",
758                 $this->userid, $mailbox, $data);
a267c6 759         }
80152b 760     }
A 761
762
763     /**
764      * Checks index/thread validity
765      */
766     private function validate($mailbox, $index, &$exists = true)
767     {
40c45e 768         $object    = $index['object'];
A 769         $is_thread = is_a($object, 'rcube_result_thread');
982353 770
AM 771         // sanity check
772         if (empty($object)) {
773             return false;
774         }
80152b 775
A 776         // Get mailbox data (UIDVALIDITY, counters, etc.) for status check
c321a9 777         $mbox_data = $this->imap->folder_data($mailbox);
80152b 778
A 779         // @TODO: Think about skipping validation checks.
780         // If we could check only every 10 minutes, we would be able to skip
781         // expensive checks, mailbox selection or even IMAP connection, this would require
782         // additional logic to force cache invalidation in some cases
783         // and many rcube_imap changes to connect when needed
784
785         // Check UIDVALIDITY
786         if ($index['validity'] != $mbox_data['UIDVALIDITY']) {
609d39 787             $this->clear($mailbox);
80152b 788             $exists = false;
A 789             return false;
790         }
791
792         // Folder is empty but cache isn't
37d511 793         if (empty($mbox_data['EXISTS'])) {
c321a9 794             if (!$object->is_empty()) {
37d511 795                 $this->clear($mailbox);
A 796                 $exists = false;
797                 return false;
798             }
799         }
800         // Folder is not empty but cache is
c321a9 801         else if ($object->is_empty()) {
37d511 802             unset($this->icache[$mailbox][$is_thread ? 'thread' : 'index']);
80152b 803             return false;
A 804         }
805
609d39 806         // Validation flag
A 807         if (!$is_thread && empty($index['valid'])) {
40c45e 808             unset($this->icache[$mailbox]['index']);
80152b 809             return false;
A 810         }
811
812         // Index was created with different skip_deleted setting
813         if ($this->skip_deleted != $index['deleted']) {
609d39 814             return false;
A 815         }
816
817         // Check HIGHESTMODSEQ
818         if (!empty($index['modseq']) && !empty($mbox_data['HIGHESTMODSEQ'])
819             && $index['modseq'] == $mbox_data['HIGHESTMODSEQ']
820         ) {
821             return true;
822         }
823
824         // Check UIDNEXT
825         if ($index['uidnext'] != $mbox_data['UIDNEXT']) {
826             unset($this->icache[$mailbox][$is_thread ? 'thread' : 'index']);
80152b 827             return false;
A 828         }
829
830         // @TODO: find better validity check for threaded index
831         if ($is_thread) {
832             // check messages number...
c321a9 833             if (!$this->skip_deleted && $mbox_data['EXISTS'] != $object->count_messages()) {
80152b 834                 return false;
A 835             }
836             return true;
837         }
838
839         // The rest of checks, more expensive
840         if (!empty($this->skip_deleted)) {
841             // compare counts if available
40c45e 842             if (!empty($mbox_data['UNDELETED'])
A 843                 && $mbox_data['UNDELETED']->count() != $object->count()
844             ) {
80152b 845                 return false;
A 846             }
847             // compare UID sets
40c45e 848             if (!empty($mbox_data['UNDELETED'])) {
A 849                 $uids_new = $mbox_data['UNDELETED']->get();
850                 $uids_old = $object->get();
80152b 851
A 852                 if (count($uids_new) != count($uids_old)) {
853                     return false;
854                 }
855
856                 sort($uids_new, SORT_NUMERIC);
857                 sort($uids_old, SORT_NUMERIC);
858
859                 if ($uids_old != $uids_new)
860                     return false;
861             }
862             else {
863                 // get all undeleted messages excluding cached UIDs
864                 $ids = $this->imap->search_once($mailbox, 'ALL UNDELETED NOT UID '.
40c45e 865                     rcube_imap_generic::compressMessageSet($object->get()));
80152b 866
c321a9 867                 if (!$ids->is_empty()) {
37d511 868                     return false;
80152b 869                 }
A 870             }
871         }
872         else {
873             // check messages number...
40c45e 874             if ($mbox_data['EXISTS'] != $object->count()) {
80152b 875                 return false;
A 876             }
877             // ... and max UID
40c45e 878             if ($object->max() != $this->imap->id2uid($mbox_data['EXISTS'], $mailbox, true)) {
80152b 879                 return false;
A 880             }
881         }
882
883         return true;
884     }
885
886
887     /**
609d39 888      * Synchronizes the mailbox.
A 889      *
890      * @param string $mailbox Folder name
891      */
892     function synchronize($mailbox)
893     {
894         // RFC4549: Synchronization Operations for Disconnected IMAP4 Clients
895         // RFC4551: IMAP Extension for Conditional STORE Operation
896         //          or Quick Flag Changes Resynchronization
897         // RFC5162: IMAP Extensions for Quick Mailbox Resynchronization
898
899         // @TODO: synchronize with other methods?
900         $qresync   = $this->imap->get_capability('QRESYNC');
901         $condstore = $qresync ? true : $this->imap->get_capability('CONDSTORE');
902
903         if (!$qresync && !$condstore) {
904             return;
905         }
906
907         // Get stored index
908         $index = $this->get_index_row($mailbox);
909
910         // database is empty
911         if (empty($index)) {
912             // set the flag that DB was already queried for index
913             // this way we'll be able to skip one SELECT in get_index()
914             $this->icache[$mailbox]['index_queried'] = true;
915             return;
916         }
917
918         $this->icache[$mailbox]['index'] = $index;
919
920         // no last HIGHESTMODSEQ value
921         if (empty($index['modseq'])) {
922             return;
923         }
924
c321a9 925         if (!$this->imap->check_connection()) {
T 926             return;
927         }
928
609d39 929         // Enable QRESYNC
A 930         $res = $this->imap->conn->enable($qresync ? 'QRESYNC' : 'CONDSTORE');
7ab9c1 931         if ($res === false) {
609d39 932             return;
A 933         }
934
7ab9c1 935         // Close mailbox if already selected to get most recent data
AM 936         if ($this->imap->conn->selected == $mailbox) {
937             $this->imap->conn->close();
938         }
939
609d39 940         // Get mailbox data (UIDVALIDITY, HIGHESTMODSEQ, counters, etc.)
c321a9 941         $mbox_data = $this->imap->folder_data($mailbox);
609d39 942
A 943         if (empty($mbox_data)) {
944              return;
945         }
946
947         // Check UIDVALIDITY
948         if ($index['validity'] != $mbox_data['UIDVALIDITY']) {
949             $this->clear($mailbox);
950             return;
951         }
952
953         // QRESYNC not supported on specified mailbox
954         if (!empty($mbox_data['NOMODSEQ']) || empty($mbox_data['HIGHESTMODSEQ'])) {
955             return;
956         }
957
958         // Nothing new
959         if ($mbox_data['HIGHESTMODSEQ'] == $index['modseq']) {
960             return;
961         }
962
e74274 963         $uids    = array();
AM 964         $removed = array();
965
966         // Get known UIDs
609d39 967         $sql_result = $this->db->query(
A 968             "SELECT uid"
0c2596 969             ." FROM ".$this->db->table_name('cache_messages')
609d39 970             ." WHERE user_id = ?"
A 971                 ." AND mailbox = ?",
972             $this->userid, $mailbox);
973
974         while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
e74274 975             $uids[] = $sql_arr['uid'];
609d39 976         }
A 977
e74274 978         // Synchronize messages data
AM 979         if (!empty($uids)) {
980             // Get modified flags and vanished messages
981             // UID FETCH 1:* (FLAGS) (CHANGEDSINCE 0123456789 VANISHED)
982             $result = $this->imap->conn->fetch($mailbox,
983                 $uids, true, array('FLAGS'), $index['modseq'], $qresync);
609d39 984
e74274 985             if (!empty($result)) {
AM 986                 foreach ($result as $id => $msg) {
987                     $uid = $msg->uid;
988                     // Remove deleted message
989                     if ($this->skip_deleted && !empty($msg->flags['DELETED'])) {
990                         $removed[] = $uid;
37d511 991                         // Invalidate index
A 992                         $index['valid'] = false;
e74274 993                         continue;
37d511 994                     }
609d39 995
e74274 996                     $flags = 0;
AM 997                     if (!empty($msg->flags)) {
998                         foreach ($this->flags as $idx => $flag) {
999                             if (!empty($msg->flags[$flag])) {
1000                                 $flags += $idx;
1001                             }
1002                         }
1003                     }
609d39 1004
e74274 1005                     $this->db->query(
AM 1006                         "UPDATE ".$this->db->table_name('cache_messages')
1007                         ." SET flags = ?, changed = ".$this->db->now()
1008                         ." WHERE user_id = ?"
1009                             ." AND mailbox = ?"
1010                             ." AND uid = ?"
1011                             ." AND flags <> ?",
1012                         $flags, $this->userid, $mailbox, $uid, $flags);
1013                 }
609d39 1014             }
A 1015
e74274 1016             // VANISHED found?
AM 1017             if ($qresync) {
1018                 $mbox_data = $this->imap->folder_data($mailbox);
609d39 1019
e74274 1020                 // Removed messages found
609d39 1021                 $uids = rcube_imap_generic::uncompressMessageSet($mbox_data['VANISHED']);
A 1022                 if (!empty($uids)) {
e74274 1023                     $removed = array_merge($removed, $uids);
37d511 1024                     // Invalidate index
A 1025                     $index['valid'] = false;
609d39 1026                 }
A 1027             }
e74274 1028
AM 1029             // remove messages from database
1030             if (!empty($removed)) {
1031                 $this->remove_message($mailbox, $removed);
1032             }
1033         }
1034
1035         // Invalidate thread index (?)
1036         if (!$index['valid']) {
1037             $this->remove_thread($mailbox);
609d39 1038         }
A 1039
1040         $sort_field = $index['sort_field'];
c321a9 1041         $sort_order = $index['object']->get_parameters('ORDER');
609d39 1042         $exists     = true;
A 1043
1044         // Validate index
1045         if (!$this->validate($mailbox, $index, $exists)) {
1046             // Update index
1047             $data = $this->get_index_data($mailbox, $sort_field, $sort_order, $mbox_data);
1048         }
1049         else {
40c45e 1050             $data = $index['object'];
609d39 1051         }
A 1052
1053         // update index and/or HIGHESTMODSEQ value
40c45e 1054         $this->add_index_row($mailbox, $sort_field, $data, $mbox_data, $exists);
609d39 1055
A 1056         // update internal cache for get_index()
40c45e 1057         $this->icache[$mailbox]['index']['object'] = $data;
609d39 1058     }
A 1059
1060
1061     /**
80152b 1062      * Converts cache row into message object.
A 1063      *
1064      * @param array $sql_arr Message row data
1065      *
0c2596 1066      * @return rcube_message_header Message object
80152b 1067      */
A 1068     private function build_message($sql_arr)
1069     {
1070         $message = $this->db->decode(unserialize($sql_arr['data']));
1071
1072         if ($message) {
609d39 1073             $message->flags = array();
a267c6 1074             foreach ($this->flags as $idx => $flag) {
A 1075                 if (($sql_arr['flags'] & $idx) == $idx) {
609d39 1076                     $message->flags[$flag] = true;
a267c6 1077                 }
A 1078            }
80152b 1079         }
A 1080
1081         return $message;
1082     }
1083
1084
1085     /**
1086      * Saves message stored in internal cache
1087      */
1088     private function save_icache()
1089     {
1090         // Save current message from internal cache
982353 1091         if ($message = $this->icache['__message']) {
71f72f 1092             // clean up some object's data
A 1093             $object = $this->message_object_prepare($message['object']);
80152b 1094
A 1095             // calculate current md5 sum
1096             $md5sum = md5(serialize($object));
1097
1098             if ($message['md5sum'] != $md5sum) {
1099                 $this->add_message($message['mailbox'], $object, !$message['exists']);
1100             }
1101
982353 1102             $this->icache['__message']['md5sum'] = $md5sum;
80152b 1103         }
A 1104     }
1105
71f72f 1106
A 1107     /**
1108      * Prepares message object to be stored in database.
1109      */
609d39 1110     private function message_object_prepare($msg)
71f72f 1111     {
609d39 1112         // Remove body too big (>25kB)
A 1113         if ($msg->body && strlen($msg->body) > 25 * 1024) {
71f72f 1114             unset($msg->body);
A 1115         }
1116
1117         // Fix mimetype which might be broken by some code when message is displayed
1118         // Another solution would be to use object's copy in rcube_message class
1119         // to prevent related issues, however I'm not sure which is better
1120         if ($msg->mimetype) {
1121             list($msg->ctype_primary, $msg->ctype_secondary) = explode('/', $msg->mimetype);
1122         }
1123
1124         if (is_array($msg->structure->parts)) {
1125             foreach ($msg->structure->parts as $idx => $part) {
609d39 1126                 $msg->structure->parts[$idx] = $this->message_object_prepare($part);
71f72f 1127             }
A 1128         }
1129
1130         return $msg;
1131     }
609d39 1132
A 1133
1134     /**
1135      * Fetches index data from IMAP server
1136      */
1137     private function get_index_data($mailbox, $sort_field, $sort_order, $mbox_data = array())
1138     {
1139         if (empty($mbox_data)) {
c321a9 1140             $mbox_data = $this->imap->folder_data($mailbox);
609d39 1141         }
A 1142
1143         if ($mbox_data['EXISTS']) {
1144             // fetch sorted sequence numbers
c321a9 1145             $index = $this->imap->index_direct($mailbox, $sort_field, $sort_order);
40c45e 1146         }
A 1147         else {
1148             $index = new rcube_result_index($mailbox, '* SORT');
609d39 1149         }
A 1150
40c45e 1151         return $index;
609d39 1152     }
80152b 1153 }
43918d 1154
AM 1155 // for backward compat.
1156 class rcube_mail_header extends rcube_message_header { }