#include <config.h>
#include <stdlib.h>
#include <assert.h>
#include <syslog.h>
#include <string.h>
#include <ctype.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <errno.h>
#ifdef HAVE_UNISTD_H
#include <unistd.h>
#endif
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/uio.h>
#include "cyrusdb.h"
#include "map.h"
#include "bsearch.h"
#include "util.h"
#include "global.h"
#include "xmalloc.h"
#include "mailbox.h"
#include "imap_err.h"
#include "seen.h"
#define FNAME_SEENSUFFIX ".seen"
#define FNAME_SEEN "/cyrus.seen"
enum {
SEEN_VERSION = 1,
SEEN_DEBUG = 0
};
struct seen {
char *user;
const char *uniqueid;
const char *path;
struct db *db;
struct txn *tid;
int converting;
};
static struct seen *lastseen = NULL;
#define DB (config_seenstate_db)
static void abortcurrent(struct seen *s)
{
if (s && s->tid) {
int r = DB->abort(s->db, s->tid);
if (r) {
syslog(LOG_ERR, "DBERROR: error aborting txn: %s",
cyrusdb_strerror(r));
}
s->tid = NULL;
}
}
char *seen_getpath(const char *userid)
{
char *fname = xmalloc(strlen(config_dir) + sizeof(FNAME_DOMAINDIR) +
sizeof(FNAME_USERDIR) + strlen(userid) +
sizeof(FNAME_SEENSUFFIX) + 10);
char c, *domain;
if (config_virtdomains && (domain = strchr(userid, '@'))) {
char d = (char) dir_hash_c(domain+1);
*domain = '\0';
c = (char) dir_hash_c(userid);
sprintf(fname, "%s%s%c/%s%s%c/%s%s", config_dir, FNAME_DOMAINDIR, d,
domain+1, FNAME_USERDIR, c, userid, FNAME_SEENSUFFIX);
*domain = '@';
}
else {
c = (char) dir_hash_c(userid);
sprintf(fname, "%s%s%c/%s%s", config_dir, FNAME_USERDIR, c, userid,
FNAME_SEENSUFFIX);
}
return fname;
}
int seen_open(struct mailbox *mailbox,
const char *user,
int flags,
struct seen **seendbptr)
{
struct seen *seendb;
char *fname = NULL;
int r;
seendb = lastseen;
lastseen = NULL;
if (SEEN_DEBUG) {
syslog(LOG_DEBUG, "seen_db: seen_open(%s, %s)",
mailbox->uniqueid, user);
}
if (seendb && !strcmp(seendb->user, user)) {
abortcurrent(seendb);
seendb->uniqueid = mailbox->uniqueid;
seendb->path = mailbox->path;
*seendbptr = seendb;
return 0;
}
*seendbptr = NULL;
if (seendb) {
abortcurrent(seendb);
r = DB->close(seendb->db);
if (r) {
syslog(LOG_ERR, "DBERROR: error closing seendb: %s",
cyrusdb_strerror(r));
}
free(seendb->user);
} else {
seendb = (struct seen *) xmalloc(sizeof(struct seen));
}
fname = seen_getpath(user);
r = DB->open(fname, (flags & SEEN_CREATE) ? CYRUSDB_CREATE : 0,
&seendb->db);
if (r != 0) {
int level = (flags & SEEN_CREATE) ? LOG_ERR : LOG_DEBUG;
syslog(level, "DBERROR: opening %s: %s", fname,
cyrusdb_strerror(r));
r = IMAP_IOERROR;
free(seendb);
free(fname);
return r;
}
syslog(LOG_DEBUG, "seen_db: user %s opened %s", user, fname);
free(fname);
seendb->tid = NULL;
seendb->uniqueid = mailbox->uniqueid;
seendb->path = mailbox->path;
seendb->user = xstrdup(user);
*seendbptr = seendb;
return r;
}
static int seen_readold(struct seen *seendb,
time_t *lastreadptr, unsigned int *lastuidptr,
time_t *lastchangeptr, char **seenuidsptr)
{
char fnamebuf[MAX_MAILBOX_PATH+1];
struct stat sbuf;
int fd;
const char *base;
const char *buf = 0, *p;
unsigned long len = 0, linelen;
unsigned long offset = 0;
if (SEEN_DEBUG) {
syslog(LOG_DEBUG, "seen_db: seen_readold(%s, %s)",
seendb->path, seendb->user);
}
strlcpy(fnamebuf, seendb->path, sizeof(fnamebuf));
strlcat(fnamebuf, FNAME_SEEN, sizeof(fnamebuf));
fd = open(fnamebuf, O_RDWR, 0);
*lastreadptr = 0;
*lastuidptr = 0;
*lastchangeptr = 0;
if (fd == -1 && errno == ENOENT) {
*seenuidsptr = xstrdup("");
return 0;
} else if (fd == -1) {
syslog(LOG_ERR, "error opening '%s': %m", fnamebuf);
return IMAP_IOERROR;
}
if (fstat(fd, &sbuf) == -1) {
close(fd);
return IMAP_IOERROR;
}
map_refresh(fd, 1, &base, &len, sbuf.st_size, fnamebuf, 0);
offset = bsearch_mem(seendb->user, 1, base, len, 0, &linelen);
if (!linelen) {
*seenuidsptr = xstrdup("");
close(fd);
return 0;
}
buf = base + offset + strlen(seendb->user)+1;
*lastreadptr = strtol(buf, (char **) &p, 10); buf = p;
*lastuidptr = strtol(buf, (char **) &p, 10); buf = p;
*lastchangeptr = strtol(buf, (char **) &p, 10); buf = p;
while (isspace((int) *p)) p++;
buf = p;
while (p < base + offset + linelen && !isspace((int) *p)) p++;
*seenuidsptr = xmalloc(p - buf + 1);
strlcpy(*seenuidsptr, buf, p - buf + 1);
map_free(&base, &len);
close(fd);
return 0;
}
static int seen_readit(struct seen *seendb,
time_t *lastreadptr, unsigned int *lastuidptr,
time_t *lastchangeptr, char **seenuidsptr,
int rw)
{
int r;
const char *data, *dstart, *dend;
char *p;
int datalen;
int version;
int uidlen;
assert(seendb && seendb->uniqueid);
if (rw || seendb->tid) {
r = DB->fetchlock(seendb->db,
seendb->uniqueid, strlen(seendb->uniqueid),
&data, &datalen, &seendb->tid);
} else {
r = DB->fetch(seendb->db,
seendb->uniqueid, strlen(seendb->uniqueid),
&data, &datalen, NULL);
}
switch (r) {
case 0:
break;
case CYRUSDB_AGAIN:
syslog(LOG_DEBUG, "deadlock in seen database for '%s/%s'",
seendb->user, seendb->uniqueid);
return IMAP_AGAIN;
break;
case CYRUSDB_IOERROR:
syslog(LOG_ERR, "DBERROR: error fetching txn %s",
cyrusdb_strerror(r));
return IMAP_IOERROR;
break;
case CYRUSDB_NOTFOUND:
r = seen_readold(seendb, lastreadptr, lastuidptr,
lastchangeptr, seenuidsptr);
if (r) {
abortcurrent(seendb);
}
return r;
break;
}
dstart = data;
dend = data + datalen;
version = strtol(data, &p, 10); data = p;
assert(version == SEEN_VERSION);
*lastreadptr = strtol(data, &p, 10); data = p;
*lastuidptr = strtol(data, &p, 10); data = p;
*lastchangeptr = strtol(data, &p, 10); data = p;
while (p < dend && isspace((int) *p)) p++; data = p;
uidlen = dend - data;
*seenuidsptr = xmalloc(uidlen + 1);
memcpy(*seenuidsptr, data, uidlen);
(*seenuidsptr)[uidlen] = '\0';
return 0;
}
int seen_read(struct seen *seendb,
time_t *lastreadptr, unsigned int *lastuidptr,
time_t *lastchangeptr, char **seenuidsptr)
{
if (SEEN_DEBUG) {
syslog(LOG_DEBUG, "seen_db: seen_read(%s, %s)",
seendb->uniqueid, seendb->user);
}
return seen_readit(seendb, lastreadptr, lastuidptr, lastchangeptr,
seenuidsptr, 0);
}
int seen_lockread(struct seen *seendb,
time_t *lastreadptr, unsigned int *lastuidptr,
time_t *lastchangeptr, char **seenuidsptr)
{
if (SEEN_DEBUG) {
syslog(LOG_DEBUG, "seen_db: seen_lockread(%s, %s)",
seendb->uniqueid, seendb->user);
}
return seen_readit(seendb, lastreadptr, lastuidptr, lastchangeptr,
seenuidsptr, 1);
}
int seen_write(struct seen *seendb, time_t lastread, unsigned int lastuid,
time_t lastchange, char *seenuids)
{
int sz = strlen(seenuids) + 50;
char *data = xmalloc(sz);
int datalen;
int r;
assert(seendb && seendb->uniqueid);
assert(seendb->tid);
if (SEEN_DEBUG) {
syslog(LOG_DEBUG, "seen_db: seen_write(%s, %s)",
seendb->uniqueid, seendb->user);
}
snprintf(data, sz, "%d %d %d %d %s", SEEN_VERSION,
(int) lastread, lastuid, (int) lastchange, seenuids);
datalen = strlen(data);
r = DB->store(seendb->db, seendb->uniqueid, strlen(seendb->uniqueid),
data, datalen, &seendb->tid);
switch (r) {
case CYRUSDB_OK:
break;
case CYRUSDB_IOERROR:
r = IMAP_AGAIN;
break;
default:
syslog(LOG_ERR, "DBERROR: error updating database: %s",
cyrusdb_strerror(r));
r = IMAP_IOERROR;
break;
}
free(data);
return r;
}
int seen_close(struct seen *seendb)
{
int r;
if (SEEN_DEBUG) {
syslog(LOG_DEBUG, "seen_db: seen_close(%s, %s)",
seendb->uniqueid, seendb->user);
}
if (seendb->tid) {
r = DB->commit(seendb->db, seendb->tid);
if (r != CYRUSDB_OK) {
syslog(LOG_ERR, "DBERROR: error committing seen txn; "
"seen state lost: %s", cyrusdb_strerror(r));
}
seendb->tid = NULL;
}
seendb->uniqueid = NULL;
seendb->path = NULL;
if (lastseen) {
int r;
abortcurrent(lastseen);
r = DB->close(lastseen->db);
if (r != CYRUSDB_OK) {
syslog(LOG_ERR, "DBERROR: error closing lastseen: %s",
cyrusdb_strerror(r));
r = IMAP_IOERROR;
}
if(!r) lastseen->db = NULL;
free(lastseen->user);
free(lastseen);
lastseen = NULL;
}
lastseen = seendb;
return 0;
}
int seen_create_mailbox(struct mailbox *mailbox)
{
if (SEEN_DEBUG) {
syslog(LOG_DEBUG, "seen_db: seen_create_mailbox(%s)",
mailbox->uniqueid);
}
return 0;
}
int seen_delete_mailbox(struct mailbox *mailbox)
{
if (SEEN_DEBUG) {
syslog(LOG_DEBUG, "seen_db: seen_delete_mailbox(%s)",
mailbox->uniqueid);
}
return 0;
}
int seen_create_user(const char *user)
{
if (SEEN_DEBUG) {
syslog(LOG_DEBUG, "seen_db: seen_create_user(%s)",
user);
}
return 0;
}
int seen_delete_user(const char *user)
{
char *fname = seen_getpath(user);
int r = 0;
if (SEEN_DEBUG) {
syslog(LOG_DEBUG, "seen_db: seen_delete_user(%s)",
user);
}
r = unlink(fname);
if (r < 0 && errno == ENOENT) {
syslog(LOG_DEBUG, "can not unlink %s: %m", fname);
r = 0;
}
else if (r < 0) {
syslog(LOG_ERR, "error unlinking %s: %m", fname);
r = IMAP_IOERROR;
}
free(fname);
return r;
}
int seen_rename_user(const char *olduser, const char *newuser)
{
char *oldfname = seen_getpath(olduser);
char *newfname = seen_getpath(newuser);
int r;
if (SEEN_DEBUG) {
syslog(LOG_DEBUG, "seen_db: seen_rename_user(%s, %s)",
olduser, newuser);
}
r = seen_merge(oldfname, newfname);
free(oldfname);
free(newfname);
return r;
}
int seen_copy(struct mailbox *oldmailbox, struct mailbox *newmailbox)
{
if (SEEN_DEBUG) {
syslog(LOG_DEBUG, "seen_db: seen_copy(%s, %s)",
oldmailbox->uniqueid, newmailbox->uniqueid);
}
return 0;
}
int seen_unlock(struct seen *seendb)
{
int r;
assert(seendb);
if (!seendb->tid) return 0;
if (SEEN_DEBUG) {
syslog(LOG_DEBUG, "seen_db: seen_unlock(%s, %s)",
seendb->uniqueid, seendb->user);
}
r = DB->commit(seendb->db, seendb->tid);
if (r != CYRUSDB_OK) {
syslog(LOG_ERR, "DBERROR: error committing seen txn; "
"seen state lost: %s", cyrusdb_strerror(r));
}
seendb->tid = NULL;
return 0;
}
int seen_done(void)
{
int r = 0;
if (SEEN_DEBUG) {
syslog(LOG_DEBUG, "seen_db: seen_done()");
}
if (lastseen) {
abortcurrent(lastseen);
r = DB->close(lastseen->db);
if (r) {
syslog(LOG_ERR, "DBERROR: error closing lastseen: %s",
cyrusdb_strerror(r));
r = IMAP_IOERROR;
}
free(lastseen->user);
free(lastseen);
}
return r;
}
int seen_reconstruct(struct mailbox *mailbox __attribute__((unused)),
time_t report_time __attribute__((unused)),
time_t prune_time __attribute__((unused)),
int (*report_proc)() __attribute__((unused)),
void *report_rock __attribute__((unused)))
{
if (SEEN_DEBUG) {
syslog(LOG_DEBUG, "seen_db: seen_reconstruct()");
}
return 0;
}
struct seen_merge_rock
{
struct db *db;
struct txn *tid;
};
static int seen_merge_cb(void *rockp,
const char *key, int keylen,
const char *tmpdata, int tmpdatalen)
{
int r;
struct seen_merge_rock *rockdata = (struct seen_merge_rock *)rockp;
struct db *tgtdb = rockdata->db;
const char *tgtdata;
int tgtdatalen, dirty = 0;
if(!tgtdb) return IMAP_INTERNAL;
r = DB->fetchlock(tgtdb, key, keylen, &tgtdata, &tgtdatalen,
&(rockdata->tid));
if(!r && tgtdata) {
int version, tmplast, tgtlast;
char *p;
const char *tmp = tmpdata, *tgt = tgtdata;
version = strtol(tgt, &p, 10); tgt = p;
assert(version == SEEN_VERSION);
strtol(tgt, &p, 10); tgt = p;
strtol(tgt, &p, 10); tgt = p;
tgtlast = strtol(tgt, &p, 10);
version = strtol(tmp, &p, 10); tmp = p;
assert(version == SEEN_VERSION);
strtol(tmp, &p, 10); tmp = p;
strtol(tmp, &p, 10); tmp = p;
tmplast = strtol(tmp, &p, 10);
if(tmplast > tgtlast) dirty = 1;
} else {
dirty = 1;
}
if(dirty) {
return DB->store(tgtdb, key, keylen, tmpdata, tmpdatalen,
&(rockdata->tid));
} else {
return 0;
}
}
int seen_merge(const char *tmpfile, const char *tgtfile)
{
int r = 0;
struct db *tmp = NULL, *tgt = NULL;
struct seen_merge_rock rock;
r = DB->open(tmpfile, CYRUSDB_CREATE, &tmp);
if(r) goto done;
r = DB->open(tgtfile, CYRUSDB_CREATE, &tgt);
if(r) goto done;
rock.db = tgt;
rock.tid = NULL;
r = DB->foreach(tmp, "", 0, NULL, seen_merge_cb, &rock, &rock.tid);
if(r) DB->abort(rock.db, rock.tid);
else DB->commit(rock.db, rock.tid);
done:
if(tgt) DB->close(tgt);
if(tmp) DB->close(tmp);
return r;
}