1 /* statuscache_db.c -- Status caching routines
2  *
3  * Copyright (c) 1994-2008 Carnegie Mellon University.  All rights reserved.
4  *
5  * Redistribution and use in source and binary forms, with or without
6  * modification, are permitted provided that the following conditions
7  * are met:
8  *
9  * 1. Redistributions of source code must retain the above copyright
10  *    notice, this list of conditions and the following disclaimer.
11  *
12  * 2. Redistributions in binary form must reproduce the above copyright
13  *    notice, this list of conditions and the following disclaimer in
14  *    the documentation and/or other materials provided with the
15  *    distribution.
16  *
17  * 3. The name "Carnegie Mellon University" must not be used to
18  *    endorse or promote products derived from this software without
19  *    prior written permission. For permission or any legal
20  *    details, please contact
21  *      Carnegie Mellon University
22  *      Center for Technology Transfer and Enterprise Creation
23  *      4615 Forbes Avenue
24  *      Suite 302
25  *      Pittsburgh, PA  15213
26  *      (412) 268-7393, fax: (412) 268-7395
27  *      innovation@andrew.cmu.edu
28  *
29  * 4. Redistributions of any form whatsoever must retain the following
30  *    acknowledgment:
31  *    "This product includes software developed by Computing Services
32  *     at Carnegie Mellon University (http://www.cmu.edu/computing/)."
33  *
34  * CARNEGIE MELLON UNIVERSITY DISCLAIMS ALL WARRANTIES WITH REGARD TO
35  * THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
36  * AND FITNESS, IN NO EVENT SHALL CARNEGIE MELLON UNIVERSITY BE LIABLE
37  * FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
38  * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
39  * AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
40  * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
41  */
42 
43 #include <config.h>
44 
45 #include <stdio.h>
46 #include <stdlib.h>
47 #include <string.h>
48 #ifdef HAVE_UNISTD_H
49 #include <unistd.h>
50 #endif
51 #include <sys/types.h>
52 #include <sys/stat.h>
53 #include <sys/uio.h>
54 #include <fcntl.h>
55 #include <syslog.h>
56 
57 #include "assert.h"
58 #include "cyrusdb.h"
59 #include "imapd.h"
60 #include "global.h"
61 #include "mboxlist.h"
62 #include "mailbox.h"
63 #include "seen.h"
64 #include "util.h"
65 #include "xmalloc.h"
66 #include "xstrlcpy.h"
67 
68 /* generated headers are not necessarily in current directory */
69 #include "imap/imap_err.h"
70 
71 #include "statuscache.h"
72 
73 #define DB config_statuscache_db
74 
75 static struct db *statuscachedb = NULL;
76 static int _initted = 0;
77 
78 /********************* CACHE METHODS ***********************/
79 
statuscache_open(void)80 static void statuscache_open(void)
81 {
82     if (!config_getswitch(IMAPOPT_STATUSCACHE))
83         return;
84 
85     char *fname = xstrdupnull(config_getstring(IMAPOPT_STATUSCACHE_DB_PATH));
86     if (!fname)
87         fname = strconcat(config_dir, FNAME_STATUSCACHEDB, (char *)NULL);
88 
89     int r = cyrusdb_open(DB, fname, CYRUSDB_CREATE, &statuscachedb);
90     if (r) {
91         syslog(LOG_ERR, "DBERROR: opening %s: %s", fname,
92                cyrusdb_strerror(r));
93         syslog(LOG_ERR, "statuscache in degraded mode");
94         statuscachedb = NULL;
95     }
96 
97     free(fname);
98 }
99 
statuscache_close(void)100 static void statuscache_close(void)
101 {
102     if (!statuscachedb) return;
103 
104     int r = cyrusdb_close(statuscachedb);
105     if (r) {
106         syslog(LOG_ERR, "DBERROR: error closing statuscache: %s",
107               cyrusdb_strerror(r));
108     }
109 
110     statuscachedb = NULL;
111 }
112 
done_cb(void * rock)113 static void done_cb(void *rock __attribute__((unused)))
114 {
115     statuscache_close();
116 }
117 
init_internal()118 static void init_internal()
119 {
120     if (_initted) return;
121     statuscache_open();
122     cyrus_modules_add(done_cb, NULL);
123     _initted = 1;
124 }
125 
statuscache_buildkey(const char * mboxname,const char * userid,struct buf * buf)126 static void statuscache_buildkey(const char *mboxname, const char *userid,
127                                  struct buf *buf)
128 {
129     buf_setcstr(buf, mboxname);
130     /* double % is a safe separator, it can't exist in a mailboxname */
131     buf_putc(buf, '%');
132     if (userid) {
133         buf_putc(buf, '%');
134         buf_appendcstr(buf, userid);
135     }
136 }
137 
statuscache_read_index(const char * mboxname,struct statusdata * sdata)138 static void statuscache_read_index(const char *mboxname, struct statusdata *sdata)
139 {
140     struct buf keybuf = BUF_INITIALIZER;
141     const char *data = NULL;
142     size_t datalen = 0;
143 
144     /* Don't access DB if it hasn't been opened */
145     if (!statuscachedb)
146         return;
147 
148     /* Check if there is an entry in the database */
149     statuscache_buildkey(mboxname, NULL, &keybuf);
150     int r = cyrusdb_fetch(statuscachedb, keybuf.s, keybuf.len, &data, &datalen, NULL);
151     buf_free(&keybuf);
152 
153     if (r || !data || !datalen)
154         return;
155 
156     const char *dend = data + datalen;
157 
158     char *p = (char *)data;
159     if (*p++ != 'I') return;
160     if (*p++ != ' ') return;
161 
162     unsigned version = (unsigned) strtoul(p, &p, 10);
163     if (version != (unsigned) STATUSCACHE_VERSION) {
164         /* Wrong version */
165         return;
166     }
167 
168     if (*p++ != ' ') return;
169     if (*p++ != '(') return;
170 
171     // read the matched items
172     if (p < dend) sdata->messages = strtoul(p, &p, 10);
173     if (p < dend) sdata->uidnext = strtoul(p, &p, 10);
174     if (p < dend) sdata->uidvalidity = strtoul(p, &p, 10);
175     if (p < dend) sdata->mboptions = strtoul(p, &p, 10);
176     if (p < dend) sdata->size = strtoull(p, &p, 10);
177     if (p < dend) sdata->createdmodseq = strtoull(p, &p, 10);
178     if (p < dend) sdata->highestmodseq = strtoull(p, &p, 10);
179 
180     if (*p++ != ')') return;
181 
182     /* Sanity check the data */
183     if (!sdata->highestmodseq)
184         return;
185 
186     sdata->statusitems |= STATUS_INDEXITEMS | STATUS_UIDVALIDITY;
187 }
188 
statuscache_read_seen(const char * mboxname,const char * userid,struct statusdata * sdata)189 static void statuscache_read_seen(const char *mboxname, const char *userid,
190                                   struct statusdata *sdata)
191 {
192     struct buf keybuf = BUF_INITIALIZER;
193     const char *data = NULL;
194     size_t datalen = 0;
195 
196     if (!userid)
197         return;
198 
199     // if no messages, other counts must also be zero
200     if (!sdata->messages) {
201         sdata->recent = 0;
202         sdata->unseen = 0;
203         sdata->userid = userid;
204         sdata->statusitems |= STATUS_SEENITEMS;
205         return;
206     }
207 
208     /* Don't access DB if it hasn't been opened */
209     if (!statuscachedb)
210         return;
211 
212     // we must have a HIGHESTMODSEQ to compare against
213     if (!(sdata->statusitems & STATUS_HIGHESTMODSEQ))
214         return;
215 
216     /* Check if there is an entry in the database */
217     statuscache_buildkey(mboxname, userid, &keybuf);
218     int r = cyrusdb_fetch(statuscachedb, keybuf.s, keybuf.len, &data, &datalen, NULL);
219     buf_free(&keybuf);
220 
221     if (r || !data || !datalen)
222         return;
223 
224     const char *dend = data + datalen;
225     char *p = (char *)data;
226     if (*p++ != 'S') return;
227     if (*p++ != ' ') return;
228 
229     unsigned version = (unsigned) strtoul(p, &p, 10);
230     if (version != (unsigned) STATUSCACHE_VERSION) {
231         /* Wrong version */
232         return;
233     }
234 
235     if (*p++ != ' ') return;
236     if (*p++ != '(') return;
237 
238     // read the matched items
239     if (p < dend) sdata->recent = strtoul(p, &p, 10);
240     if (p < dend) sdata->unseen = strtoul(p, &p, 10);
241     modseq_t highestmodseq = strtoull(p, &p, 10);
242 
243     if (*p++ != ')') return;
244 
245     // doesn't match non-unseen key
246     if (highestmodseq != sdata->highestmodseq)
247         return;
248 
249     sdata->userid = userid;
250     sdata->statusitems |= STATUS_SEENITEMS;
251 }
252 
statuscache_lookup(const char * mboxname,const char * userid,unsigned statusitems,struct statusdata * sdata)253 static int statuscache_lookup(const char *mboxname, const char *userid,
254                        unsigned statusitems, struct statusdata *sdata)
255 {
256     // nothing to read!
257     if (!(statusitems & (STATUS_INDEXITEMS|STATUS_SEENITEMS)))
258         return 0;
259 
260     init_internal();
261 
262     statuscache_read_index(mboxname, sdata);
263     if (statusitems & STATUS_SEENITEMS)
264         statuscache_read_seen(mboxname, userid, sdata);
265 
266     // did we get everything we wanted?
267     if ((sdata->statusitems & statusitems) != statusitems)
268         return IMAP_NO_NOSUCHMSG;
269 
270     return 0;
271 }
272 
statuscache_store(const char * mboxname,struct statusdata * sdata,struct txn ** tidptr)273 static int statuscache_store(const char *mboxname,
274                              struct statusdata *sdata,
275                              struct txn **tidptr)
276 {
277     struct buf keybuf = BUF_INITIALIZER;
278     struct buf databuf = BUF_INITIALIZER;
279     int r = 0;
280 
281     statuscache_buildkey(mboxname, /*userid*/NULL, &keybuf);
282 
283     /* if we don't have a full index, just nuke the key */
284     if (!sdata || (sdata->statusitems & STATUS_INDEXITEMS) != STATUS_INDEXITEMS) {
285         r = cyrusdb_delete(statuscachedb, keybuf.s, keybuf.len, tidptr, 1);
286         if (r != CYRUSDB_OK) {
287             syslog(LOG_ERR, "DBERROR: error deleting statuscache for: %s (%s)",
288                    mboxname, cyrusdb_strerror(r));
289         }
290         goto done;
291     }
292 
293 
294     buf_printf(&databuf,
295                        "I %u (%u %u %u %u %llu " MODSEQ_FMT " " MODSEQ_FMT ")",
296                        STATUSCACHE_VERSION,
297                        sdata->messages, sdata->uidnext,
298                        sdata->uidvalidity, sdata->mboptions, sdata->size,
299                        sdata->createdmodseq, sdata->highestmodseq);
300 
301     r = cyrusdb_store(statuscachedb, keybuf.s, keybuf.len, databuf.s, databuf.len, tidptr);
302 
303     if (r != CYRUSDB_OK) {
304         syslog(LOG_ERR, "DBERROR: error updating database: %s (%s)",
305                mboxname, cyrusdb_strerror(r));
306         goto done;
307     }
308 
309     if ((sdata->statusitems & STATUS_SEENITEMS) != STATUS_SEENITEMS)
310         goto done;
311 
312     // if there's no userid, we don't store this stuff
313     if (!sdata->userid)
314         goto done;
315 
316     statuscache_buildkey(mboxname, sdata->userid, &keybuf);
317 
318     /* The trailing whitespace is necessary because we
319      * use non-length-based functions to parse the values.
320      * Any non-digit char would be fine, but whitespace
321      * looks less ugly in dbtool output */
322     buf_reset(&databuf);
323     buf_printf(&databuf,
324                        "S %u (%u %u " MODSEQ_FMT ")",
325                        STATUSCACHE_VERSION,
326                        sdata->recent, sdata->unseen,
327                        sdata->highestmodseq);
328 
329     r = cyrusdb_store(statuscachedb, keybuf.s, keybuf.len, databuf.s, databuf.len, tidptr);
330 
331     if (r != CYRUSDB_OK) {
332         syslog(LOG_ERR, "DBERROR: error updating database: %s (%s)",
333                mboxname, cyrusdb_strerror(r));
334         goto done;
335     }
336 
337 done:
338     buf_free(&keybuf);
339     buf_free(&databuf);
340     return r;
341 }
342 
statuscache_invalidate(const char * mboxname,struct statusdata * sdata)343 HIDDEN int statuscache_invalidate(const char *mboxname, struct statusdata *sdata)
344 {
345     int doclose = 0;
346     struct txn *tid = NULL;
347 
348     /* if it's disabled then skip */
349     if (!config_getswitch(IMAPOPT_STATUSCACHE))
350         return 0;
351 
352     /* if it's not already open, open and close it for just this */
353     if (!statuscachedb) {
354         statuscache_open();
355         // failed to open, oh well
356         if (!statuscachedb)
357             return 0;
358         doclose = 1;
359     }
360 
361     int r = statuscache_store(mboxname, sdata, &tid);
362 
363     if (!r) {
364         cyrusdb_commit(statuscachedb, tid);
365     }
366     else {
367         syslog(LOG_NOTICE, "DBERROR: failed to store statuscache data for %s", mboxname);
368         if (tid) cyrusdb_abort(statuscachedb, tid);
369     }
370 
371     // if we opened the DB, close it now
372     if (doclose)
373         statuscache_close();
374 
375     return 0;
376 }
377 
378 
379 
380 /****************** STATUSDATA FILLING METHODS ************************/
381 
status_fill_mbentry(const mbentry_t * mbentry,struct statusdata * sdata)382 HIDDEN void status_fill_mbentry(const mbentry_t *mbentry, struct statusdata *sdata)
383 {
384     assert(mbentry);
385     assert(sdata);
386 
387     sdata->uidvalidity = mbentry->uidvalidity;
388     sdata->mailboxid = mbentry->uniqueid;
389 
390     sdata->statusitems |= STATUS_MBENTRYITEMS;
391 }
392 
status_fill_mailbox(struct mailbox * mailbox,struct statusdata * sdata)393 HIDDEN void status_fill_mailbox(struct mailbox *mailbox, struct statusdata *sdata)
394 {
395     assert(mailbox);
396     assert(sdata);
397 
398     sdata->messages = mailbox->i.exists;
399     sdata->uidnext = mailbox->i.last_uid+1;
400     sdata->mboptions = mailbox->i.options;
401     sdata->size = mailbox->i.quota_mailbox_used;
402     sdata->createdmodseq = mailbox->i.createdmodseq;
403     sdata->highestmodseq = mailbox->i.highestmodseq;
404 
405     // mbentry items are also available from an open mailbox
406     sdata->uidvalidity = mailbox->i.uidvalidity;
407     sdata->mailboxid = mailbox->uniqueid;
408 
409     sdata->statusitems |= STATUS_INDEXITEMS | STATUS_MBENTRYITEMS;
410 }
411 
status_fill_seen(const char * userid,struct statusdata * sdata,unsigned numrecent,unsigned numunseen)412 HIDDEN void status_fill_seen(const char *userid, struct statusdata *sdata,
413                                   unsigned numrecent, unsigned numunseen)
414 {
415     assert(userid);
416     assert(sdata);
417 
418     // we need a matching parent record to exist for these values to be valid
419     assert(sdata->statusitems & STATUS_HIGHESTMODSEQ);
420 
421     sdata->userid = userid;
422     sdata->recent = numrecent;
423     sdata->unseen = numunseen;
424 
425     sdata->statusitems |= STATUS_SEENITEMS;
426 }
427 
status_load_mailbox(struct mailbox * mailbox,const char * userid,unsigned statusitems,struct statusdata * sdata)428 static int status_load_mailbox(struct mailbox *mailbox, const char *userid,
429                                unsigned statusitems, struct statusdata *sdata)
430 {
431     status_fill_mailbox(mailbox, sdata);
432 
433     if ((statusitems & STATUS_SEENITEMS) && mailbox->i.exists) {
434         unsigned numrecent = 0;
435         unsigned numunseen = 0;
436         /* Read \Seen state */
437         struct seqset *seq = NULL;
438         int internalseen = mailbox_internal_seen(mailbox, userid);
439         unsigned recentuid;
440 
441         if (internalseen) {
442             recentuid = mailbox->i.recentuid;
443         } else {
444             struct seen *seendb = NULL;
445             struct seendata sd = SEENDATA_INITIALIZER;
446 
447             int r = seen_open(userid, SEEN_CREATE, &seendb);
448             if (!r) r = seen_read(seendb, mailbox->uniqueid, &sd);
449             seen_close(&seendb);
450             if (r) return r;
451 
452             recentuid = sd.lastuid;
453             seq = seqset_parse(sd.seenuids, NULL, recentuid);
454             seen_freedata(&sd);
455         }
456 
457         struct mailbox_iter *iter = mailbox_iter_init(mailbox, 0, ITER_SKIP_EXPUNGED);
458         const message_t *msg;
459         while ((msg = mailbox_iter_step(iter))) {
460             const struct index_record *record = msg_record(msg);
461             if (record->uid > recentuid)
462                 numrecent++;
463             if (internalseen) {
464                 if (!(record->system_flags & FLAG_SEEN))
465                     numunseen++;
466             }
467             else {
468                 if (!seqset_ismember(seq, record->uid))
469                     numunseen++;
470             }
471         }
472         mailbox_iter_done(&iter);
473         seqset_free(seq);
474 
475         status_fill_seen(userid, sdata, numrecent, numunseen);
476     }
477 
478     statuscache_invalidate(mailbox->name, sdata);
479 
480     return 0;
481 }
482 
status_lookup_internal(const char * mboxname,const char * userid,unsigned statusitems,struct statusdata * sdata)483 static int status_lookup_internal(const char *mboxname, const char *userid,
484                                   unsigned statusitems, struct statusdata *sdata)
485 {
486     struct mailbox *mailbox = NULL;
487     int r = 0;
488 
489     /* Check status cache if possible */
490     if (config_getswitch(IMAPOPT_STATUSCACHE)) {
491         /* Do actual lookup of cache item. */
492         r = statuscache_lookup(mboxname, userid, statusitems, sdata);
493 
494         /* Seen/recent status uses "push" invalidation events from
495          * seen_db.c.   This avoids needing to open cyrus.header to get
496          * the mailbox uniqueid to open the seen db and get the
497          * unseen_mtime and recentuid.
498          */
499 
500         if (!r) {
501             syslog(LOG_DEBUG, "statuscache, '%s', '%s', '0x%02x', 'yes'",
502                    mboxname, userid, statusitems);
503             return 0;
504         }
505 
506         syslog(LOG_DEBUG, "statuscache, '%s', '%s', '0x%02x', 'no'",
507                mboxname, userid, statusitems);
508     }
509 
510     /* Missing or invalid cache entry */
511     r = mailbox_open_irl(mboxname, &mailbox);
512     if (r) return r;
513 
514     r = status_load_mailbox(mailbox, userid, statusitems, sdata);
515 
516     /* cache the new value while unlocking */
517     if (!r) mailbox_unlock_index(mailbox, sdata);
518     mailbox_close(&mailbox);
519 
520     return r;
521 }
522 
status_lookup_mbentry(const mbentry_t * mbentry,const char * userid,unsigned statusitems,struct statusdata * sdata)523 EXPORTED int status_lookup_mbentry(const mbentry_t *mbentry, const char *userid,
524                                   unsigned statusitems, struct statusdata *sdata)
525 {
526     // check if we can get everything we need from the mbentry
527     status_fill_mbentry(mbentry, sdata);
528     if ((sdata->statusitems & statusitems) == statusitems)
529         return 0;
530 
531     return status_lookup_internal(mbentry->name, userid, statusitems, sdata);
532 }
533 
status_lookup_mboxname(const char * mboxname,const char * userid,unsigned statusitems,struct statusdata * sdata)534 EXPORTED int status_lookup_mboxname(const char *mboxname, const char *userid,
535                                     unsigned statusitems, struct statusdata *sdata)
536 {
537     // we want an mbentry first, just in case we can get everything from there
538     if (statusitems & STATUS_MAILBOXID) {
539         mbentry_t *mbentry = NULL;
540         int r = mboxlist_lookup_allow_all(mboxname, &mbentry, NULL);
541         if (r) return r;
542         r = status_lookup_mbentry(mbentry, userid, statusitems, sdata);
543         mboxlist_entry_free(&mbentry);
544         return r;
545     }
546 
547     return status_lookup_internal(mboxname, userid, statusitems, sdata);
548 }
549 
550 
551 // this one has literally no smarts at all
status_lookup_mbname(const mbname_t * mbname,const char * userid,unsigned statusitems,struct statusdata * sdata)552 EXPORTED int status_lookup_mbname(const mbname_t *mbname, const char *userid,
553                                   unsigned statusitems, struct statusdata *sdata)
554 {
555     return status_lookup_mboxname(mbname_intname(mbname), userid, statusitems, sdata);
556 }
557 
558 /*
559  * Performs a STATUS command on an open mailbox
560  */
status_lookup_mailbox(struct mailbox * mailbox,const char * userid,unsigned statusitems,struct statusdata * sdata)561 EXPORTED int status_lookup_mailbox(struct mailbox *mailbox, const char *userid,
562                                    unsigned statusitems, struct statusdata *sdata)
563 {
564     // check if we already have all the data we need (includes any possible mbentry)
565     status_fill_mailbox(mailbox, sdata);
566     if ((sdata->statusitems & statusitems) == statusitems)
567         return 0;
568 
569     /* Check status cache if possible */
570     if (config_getswitch(IMAPOPT_STATUSCACHE)) {
571         /* Do actual lookup of cache item. */
572         int r = statuscache_lookup(mailbox->name, userid, statusitems, sdata);
573 
574         /* Seen/recent status uses "push" invalidation events from
575          * seen_db.c.   This avoids needing to open cyrus.header to get
576          * the mailbox uniqueid to open the seen db and get the
577          * unseen_mtime and recentuid.
578          */
579 
580         if (!r) {
581             syslog(LOG_DEBUG, "statuscache, '%s', '%s', '0x%02x', 'yes'",
582                    mailbox->name, userid, statusitems);
583             return 0;
584         }
585 
586         syslog(LOG_DEBUG, "statuscache, '%s', '%s', '0x%02x', 'no'",
587                mailbox->name, userid, statusitems);
588     }
589 
590     return status_load_mailbox(mailbox, userid, statusitems, sdata);
591 }
592