/* statuscache_db.c -- Status caching routines
*
* Copyright (c) 1994-2008 Carnegie Mellon University. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in
* the documentation and/or other materials provided with the
* distribution.
*
* 3. The name "Carnegie Mellon University" must not be used to
* endorse or promote products derived from this software without
* prior written permission. For permission or any legal
* details, please contact
* Carnegie Mellon University
* Center for Technology Transfer and Enterprise Creation
* 4615 Forbes Avenue
* Suite 302
* Pittsburgh, PA 15213
* (412) 268-7393, fax: (412) 268-7395
* innovation@andrew.cmu.edu
*
* 4. Redistributions of any form whatsoever must retain the following
* acknowledgment:
* "This product includes software developed by Computing Services
* at Carnegie Mellon University (http://www.cmu.edu/computing/)."
*
* CARNEGIE MELLON UNIVERSITY DISCLAIMS ALL WARRANTIES WITH REGARD TO
* THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS, IN NO EVENT SHALL CARNEGIE MELLON UNIVERSITY BE LIABLE
* FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
* AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
* OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
#include <config.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#ifdef HAVE_UNISTD_H
#include <unistd.h>
#endif
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/uio.h>
#include <fcntl.h>
#include <syslog.h>
#include "assert.h"
#include "cyrusdb.h"
#include "imapd.h"
#include "global.h"
#include "mboxlist.h"
#include "mailbox.h"
#include "seen.h"
#include "util.h"
#include "xmalloc.h"
#include "xstrlcpy.h"
/* generated headers are not necessarily in current directory */
#include "imap/imap_err.h"
#include "statuscache.h"
#define DB config_statuscache_db
static struct db *statuscachedb;
static int statuscache_dbopen = 0;
char *statuscache_filename(void)
{
const char *fname = config_getstring(IMAPOPT_STATUSCACHE_DB_PATH);
if (fname)
return xstrdup(fname);
/* create db file name */
return strconcat(config_dir, FNAME_STATUSCACHEDB, (char *)NULL);
}
EXPORTED void statuscache_open(void)
{
char *fname = statuscache_filename();
int ret;
ret = cyrusdb_open(DB, fname, CYRUSDB_CREATE, &statuscachedb);
if (ret != 0) {
syslog(LOG_ERR, "DBERROR: opening %s: %s", fname,
cyrusdb_strerror(ret));
syslog(LOG_ERR, "statuscache in degraded mode");
goto out;
}
statuscache_dbopen = 1;
out:
free(fname);
}
EXPORTED void statuscache_close(void)
{
int r;
if (statuscache_dbopen) {
r = cyrusdb_close(statuscachedb);
if (r) {
syslog(LOG_ERR, "DBERROR: error closing statuscache: %s",
cyrusdb_strerror(r));
}
statuscache_dbopen = 0;
}
}
HIDDEN void statuscache_fill(struct statusdata *sdata, const char *userid,
struct mailbox *mailbox, unsigned statusitems,
unsigned numrecent, unsigned numunseen)
{
assert(sdata);
assert(mailbox);
sdata->userid = userid;
sdata->statusitems = statusitems;
sdata->messages = mailbox->i.exists;
sdata->recent = numrecent;
sdata->uidnext = mailbox->i.last_uid+1;
sdata->uidvalidity = mailbox->i.uidvalidity;
sdata->unseen = numunseen;
sdata->highestmodseq = mailbox->i.highestmodseq;
}
EXPORTED void statuscache_done(void)
{
/* DB->done() handled by cyrus_done() */
}
static char *statuscache_buildkey(const char *mailboxname, const char *userid,
size_t *keylen)
{
static char key[MAX_MAILBOX_BUFFER];
size_t len;
/* Build statuscache key */
len = strlcpy(key, mailboxname, sizeof(key));
/* double % is a safe separator, it can't exist in a mailboxname */
key[len++] = '%';
key[len++] = '%';
if (userid)
len += strlcpy(key + len, userid, sizeof(key) - len);
*keylen = len;
return key;
}
/*
* Performs a STATUS command - note: state MAY be NULL here.
*/
EXPORTED int status_lookup(const char *mboxname, const char *userid,
unsigned statusitems, struct statusdata *sdata)
{
struct mailbox *mailbox = NULL;
unsigned numrecent = 0;
unsigned numunseen = 0;
unsigned c_statusitems;
int r;
/* Check status cache if possible */
if (config_getswitch(IMAPOPT_STATUSCACHE)) {
/* Do actual lookup of cache item. */
r = statuscache_lookup(mboxname, userid, statusitems, sdata);
/* Seen/recent status uses "push" invalidation events from
* seen_db.c. This avoids needing to open cyrus.header to get
* the mailbox uniqueid to open the seen db and get the
* unseen_mtime and recentuid.
*/
if (!r) {
syslog(LOG_DEBUG, "statuscache, '%s', '%s', '0x%02x', 'yes'",
mboxname, userid, statusitems);
return 0;
}
syslog(LOG_DEBUG, "statuscache, '%s', '%s', '0x%02x', 'no'",
mboxname, userid, statusitems);
}
/* Missing or invalid cache entry */
r = mailbox_open_irl(mboxname, &mailbox);
if (r) return r;
/* We always have message count, uidnext,
uidvalidity, and highestmodseq for cache */
c_statusitems = STATUS_MESSAGES | STATUS_UIDNEXT |
STATUS_UIDVALIDITY | STATUS_HIGHESTMODSEQ;
if (!mailbox->i.exists) {
/* no messages, so these two must also be zero */
c_statusitems |= STATUS_RECENT | STATUS_UNSEEN;
}
else if (statusitems & (STATUS_RECENT | STATUS_UNSEEN)) {
/* Read \Seen state */
struct seqset *seq = NULL;
int internalseen = mailbox_internal_seen(mailbox, userid);
unsigned recentuid;
if (internalseen) {
recentuid = mailbox->i.recentuid;
} else {
struct seen *seendb = NULL;
struct seendata sd = SEENDATA_INITIALIZER;
r = seen_open(userid, SEEN_CREATE, &seendb);
if (!r) r = seen_read(seendb, mailbox->uniqueid, &sd);
seen_close(&seendb);
if (r) goto done;
recentuid = sd.lastuid;
seq = seqset_parse(sd.seenuids, NULL, recentuid);
seen_freedata(&sd);
}
struct mailbox_iter *iter = mailbox_iter_init(mailbox, 0, ITER_SKIP_EXPUNGED);
const message_t *msg;
while ((msg = mailbox_iter_step(iter))) {
const struct index_record *record = msg_record(msg);
if (record->uid > recentuid)
numrecent++;
if (internalseen) {
if (!(record->system_flags & FLAG_SEEN))
numunseen++;
}
else {
if (!seqset_ismember(seq, record->uid))
numunseen++;
}
}
mailbox_iter_done(&iter);
seqset_free(seq);
/* we've calculated the correct values for both */
c_statusitems |= STATUS_RECENT | STATUS_UNSEEN;
}
statuscache_fill(sdata, userid, mailbox, c_statusitems,
numrecent, numunseen);
/* cache the new value while unlocking */
mailbox_unlock_index(mailbox, sdata);
done:
mailbox_close(&mailbox);
return r;
}
/*
* Performs a STATUS command on an open mailbox - note: state MAY be NULL here.
*/
EXPORTED int status_lookup_mailbox(struct mailbox *mailbox, const char *userid,
unsigned statusitems, struct statusdata *sdata)
{
/* XXX Apart from not opening mailbox, this is a copy of status_lookup.
If it is here to stay, then refactor with status_lookup. */
unsigned numrecent = 0;
unsigned numunseen = 0;
unsigned c_statusitems;
int r = 0;
/* Check status cache if possible */
if (config_getswitch(IMAPOPT_STATUSCACHE)) {
/* Do actual lookup of cache item. */
r = statuscache_lookup(mailbox->name, userid, statusitems, sdata);
/* Seen/recent status uses "push" invalidation events from
* seen_db.c. This avoids needing to open cyrus.header to get
* the mailbox uniqueid to open the seen db and get the
* unseen_mtime and recentuid.
*/
if (!r) {
syslog(LOG_DEBUG, "statuscache, '%s', '%s', '0x%02x', 'yes'",
mailbox->name, userid, statusitems);
return 0;
}
syslog(LOG_DEBUG, "statuscache, '%s', '%s', '0x%02x', 'no'",
mailbox->name, userid, statusitems);
}
r = 0;
/* We always have message count, uidnext,
uidvalidity, and highestmodseq for cache */
c_statusitems = STATUS_MESSAGES | STATUS_UIDNEXT |
STATUS_UIDVALIDITY | STATUS_HIGHESTMODSEQ;
if (!mailbox->i.exists) {
/* no messages, so these two must also be zero */
c_statusitems |= STATUS_RECENT | STATUS_UNSEEN;
}
else if (statusitems & (STATUS_RECENT | STATUS_UNSEEN)) {
/* Read \Seen state */
struct seqset *seq = NULL;
int internalseen = mailbox_internal_seen(mailbox, userid);
unsigned recentuid;
if (internalseen) {
recentuid = mailbox->i.recentuid;
} else {
struct seen *seendb = NULL;
struct seendata sd = SEENDATA_INITIALIZER;
r = seen_open(userid, SEEN_CREATE, &seendb);
if (!r) r = seen_read(seendb, mailbox->uniqueid, &sd);
seen_close(&seendb);
if (r) goto done;
recentuid = sd.lastuid;
seq = seqset_parse(sd.seenuids, NULL, recentuid);
seen_freedata(&sd);
}
struct mailbox_iter *iter = mailbox_iter_init(mailbox, 0, ITER_SKIP_EXPUNGED);
const message_t *msg;
while ((msg = mailbox_iter_step(iter))) {
const struct index_record *record = msg_record(msg);
if (record->uid > recentuid)
numrecent++;
if (internalseen) {
if (!(record->system_flags & FLAG_SEEN))
numunseen++;
}
else {
if (!seqset_ismember(seq, record->uid))
numunseen++;
}
}
mailbox_iter_done(&iter);
/* we've calculated the correct values for both */
c_statusitems |= STATUS_RECENT | STATUS_UNSEEN;
}
statuscache_fill(sdata, userid, mailbox, c_statusitems,
numrecent, numunseen);
done:
return r;
}
EXPORTED int statuscache_lookup(const char *mboxname, const char *userid,
unsigned statusitems, struct statusdata *sdata)
{
size_t keylen, datalen;
int r = 0;
const char *data = NULL, *dend;
char *p, *key = statuscache_buildkey(mboxname, userid, &keylen);
unsigned version;
/* Don't access DB if it hasn't been opened */
if (!statuscache_dbopen)
return IMAP_NO_NOSUCHMSG;
/* Check if there is an entry in the database */
do {
r = cyrusdb_fetch(statuscachedb, key, keylen, &data, &datalen, NULL);
} while (r == CYRUSDB_AGAIN);
if (r || !data || ((size_t) datalen < sizeof(unsigned))) {
return IMAP_NO_NOSUCHMSG;
}
dend = data + datalen;
version = (unsigned) strtoul(data, &p, 10);
if (version != (unsigned) STATUSCACHE_VERSION) {
/* Wrong version */
return IMAP_NO_NOSUCHMSG;
}
if (p < dend) sdata->statusitems = strtoul(p, &p, 10);
if (p < dend) sdata->messages = strtoul(p, &p, 10);
if (p < dend) sdata->recent = strtoul(p, &p, 10);
if (p < dend) sdata->uidnext = strtoul(p, &p, 10);
if (p < dend) sdata->uidvalidity = strtoul(p, &p, 10);
if (p < dend) sdata->unseen = strtoul(p, &p, 10);
if (p < dend) sdata->highestmodseq = strtoull(p, &p, 10);
/* Sanity check the data */
if (!sdata->statusitems || !sdata->uidnext || !sdata->uidvalidity) {
return IMAP_NO_NOSUCHMSG;
}
if ((sdata->statusitems & statusitems) != statusitems) {
/* Don't have all of the requested information */
return IMAP_NO_NOSUCHMSG;
}
return 0;
}
static int statuscache_store(const char *mboxname,
struct statusdata *sdata,
struct txn **tidptr)
{
char data[250]; /* enough room for 11*(UULONG + SP) */
size_t keylen, datalen;
char *key = statuscache_buildkey(mboxname, sdata->userid, &keylen);
int r;
/* Don't access DB if it hasn't been opened */
if (!statuscache_dbopen)
return 0;
/* The trailing whitespace is necessary because we
* use non-length-based functions to parse the values.
* Any non-digit char would be fine, but whitespace
* looks less ugly in dbtool output */
datalen = snprintf(data, sizeof(data),
"%u %u %u %u %u %u %u " MODSEQ_FMT " ",
STATUSCACHE_VERSION,
sdata->statusitems, sdata->messages,
sdata->recent, sdata->uidnext,
sdata->uidvalidity, sdata->unseen,
sdata->highestmodseq);
r = cyrusdb_store(statuscachedb, key, keylen, data, datalen, tidptr);
if (r != CYRUSDB_OK) {
syslog(LOG_ERR, "DBERROR: error updating database: %s (%s)",
mboxname, cyrusdb_strerror(r));
}
return r;
}
struct statuscache_deleterock {
struct db *db;
struct txn *tid;
};
static int delete_cb(void *rockp,
const char *key, size_t keylen,
const char *data __attribute__((unused)),
size_t datalen __attribute__((unused)))
{
int r;
char buf[4096];
struct statuscache_deleterock *rp = (struct statuscache_deleterock *)rockp;
/* error if it's too big */
if (keylen > 4096)
return 1;
/* we need to cache a copy, because the delete might re-map
* the mmap space */
memcpy(buf, key, keylen);
/* Delete db entry */
r = cyrusdb_delete(rp->db, buf, keylen, &rp->tid, 1);
if (r != CYRUSDB_OK) {
syslog(LOG_ERR, "DBERROR: error deleting from database: %s",
cyrusdb_strerror(r));
}
return 0;
}
HIDDEN int statuscache_invalidate(const char *mboxname, struct statusdata *sdata)
{
size_t keylen;
char *key;
int r;
int doclose = 0;
struct statuscache_deleterock drock;
/* if it's disabled then skip */
if (!config_getswitch(IMAPOPT_STATUSCACHE))
return 0;
/* Open DB if it hasn't been opened */
if (!statuscache_dbopen) {
statuscache_open();
doclose = 1;
}
drock.db = statuscachedb;
drock.tid = NULL;
key = statuscache_buildkey(mboxname, /*userid*/NULL, &keylen);
r = cyrusdb_foreach(drock.db, key, keylen, NULL, delete_cb,
&drock, &drock.tid);
if (r != CYRUSDB_OK) {
syslog(LOG_ERR, "DBERROR: error invalidating: %s (%s)",
mboxname, cyrusdb_strerror(r));
}
if (!r && sdata) {
r = statuscache_store(mboxname, sdata, &drock.tid);
}
if (r == CYRUSDB_OK) {
cyrusdb_commit(drock.db, drock.tid);
}
else {
syslog(LOG_NOTICE, "DBERROR: failed to store statuscace data for %s", mboxname);
if (drock.tid) cyrusdb_abort(drock.db, drock.tid);
}
if (doclose)
statuscache_close();
return 0;
}