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