source: subversion/trunk/roundcubemail/program/include/rcube_imap_cache.php @ 6020

Last change on this file since 6020 was 6020, checked in by thomasb, 14 months ago

Don't set variable which will be used later on with wrong data

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