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