/* cyr_expire.c -- Program to expire deliver.db entries and messages
*
* 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>
#ifdef HAVE_UNISTD_H
#include <unistd.h>
#endif
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <syslog.h>
#include <signal.h>
#include <sasl/sasl.h>
#include "annotate.h"
#include "duplicate.h"
#include "exitcodes.h"
#include "global.h"
#include "hash.h"
#include "libcyr_cfg.h"
#include "mboxevent.h"
#include "mboxlist.h"
#include "conversations.h"
#include "util.h"
#include "xmalloc.h"
#include "strarray.h"
/* generated headers are not necessarily in current directory */
#include "imap/imap_err.h"
/* global state */
static volatile sig_atomic_t sigquit = 0;
static int verbose = 0;
static int keep_flagged = 1;
/* current namespace */
static struct namespace expire_namespace;
static void usage(void)
{
fprintf(stderr,
"cyr_expire [-C <altconfig>] [-E <expire-duration>] [-D <delete-duration] [-X <expunge-duration>] [-p prefix] [-a] [-v] [-x]\n");
exit(-1);
}
struct expire_rock {
struct hash_table table;
time_t expire_mark;
time_t expunge_mark;
unsigned long mailboxes_seen;
unsigned long messages_seen;
unsigned long messages_expired;
unsigned long messages_expunged;
int skip_annotate;
bit32 userflags[MAX_USER_FLAGS/32];
int do_userflags;
unsigned long userflags_expunged;
};
struct conversations_rock {
struct hash_table seen;
time_t expire_mark;
unsigned long databases_seen;
unsigned long msgids_seen;
unsigned long msgids_expired;
};
struct delete_rock {
time_t delete_mark;
strarray_t to_delete;
};
/*
* Parse a non-negative duration string as seconds.
*
* Convert "23.5m" to fractional days. Accepts the suffixes "d" (day),
* (day), "h" (hour), "m" (minute) and "s" (second). If no suffix, assume
* days.
* Returns 1 if successful and *secondsp is filled in, or 0 if the suffix
* is unknown or on error.
*/
static int parse_duration(const char *s, int *secondsp)
{
char *end = NULL;
double val;
int multiplier = 86400; /* default is days */
/* no negative or empty numbers please */
if (!*s || *s == '-')
return 0;
val = strtod(s, &end);
/* Allow 'd', 'h', 'm' and 's' as end, else return error. */
if (*end) {
if (end[1]) return 0; /* trailing extra junk */
switch (*end) {
case 'd':
/* already the default */
break;
case 'h':
multiplier = 3600;
break;
case 'm':
multiplier = 60;
break;
case 's':
multiplier = 1;
break;
default:
return 0;
}
}
*secondsp = multiplier * val;
return 1;
}
static int expunge_userflags(struct mailbox *mailbox, struct expire_rock *erock)
{
unsigned int i;
int r;
for (i = 0; i < MAX_USER_FLAGS; i++) {
if (erock->userflags[i/32] & 1<<(i&31))
continue;
if (!mailbox->flagname[i])
continue;
if (verbose)
fprintf(stderr, "Expunging userflag %u (%s) from %s\n",
i, mailbox->flagname[i], mailbox->name);
r = mailbox_remove_user_flag(mailbox, i);
if (r) return r;
erock->userflags_expunged++;
}
return 0;
}
/*
* mailbox_expunge() callback to expunge expired articles.
*/
static unsigned expire_cb(struct mailbox *mailbox __attribute__((unused)),
const struct index_record *record,
void *rock)
{
struct expire_rock *erock = (struct expire_rock *) rock;
unsigned int i;
/* otherwise, we're expiring messages by sent date */
if (record->gmtime < erock->expire_mark) {
erock->messages_expired++;
return 1;
}
/* record which user flags are set */
for (i = 0; i < (MAX_USER_FLAGS/32); i++)
erock->userflags[i] |= record->user_flags[i];
return 0;
}
/*
* mailbox_expunge() callback to *only* count userflags.
*/
static unsigned userflag_cb(struct mailbox *mailbox __attribute__((unused)),
const struct index_record *record,
void *rock)
{
struct expire_rock *erock = (struct expire_rock *) rock;
unsigned int i;
/* record which user flags are set */
for (i = 0; i < (MAX_USER_FLAGS/32); i++)
erock->userflags[i] |= record->user_flags[i];
return 0; /* always keep the message */
}
static int archive(const mbentry_t *mbentry, void *rock)
{
struct mailbox *mailbox = NULL;
if (sigquit)
return 1;
if (mbentry->mbtype & MBTYPE_DELETED)
goto done;
if (mbentry->mbtype & MBTYPE_REMOTE)
goto done;
if (mailbox_open_iwl(mbentry->name, &mailbox))
goto done;
if (verbose)
fprintf(stderr, "archiving mailbox %s\n", mbentry->name);
mailbox_archive(mailbox, NULL, rock, ITER_SKIP_EXPUNGED);
done:
mailbox_close(&mailbox);
/* move on to the next mailbox regardless of errors */
return 0;
}
/*
* callback function to:
* - expire messages from mailboxes,
* - build a hash table of mailboxes in which we expired messages,
* - and perform a cleanup of expunged messages
*/
static int expire(const mbentry_t *mbentry, void *rock)
{
struct expire_rock *erock = (struct expire_rock *) rock;
char *buf;
struct buf attrib = BUF_INITIALIZER;
int r;
struct mailbox *mailbox = NULL;
unsigned numexpunged = 0;
int expire_seconds = 0;
int did_expunge = 0;
if (sigquit) {
/* don't care if we leak some memory, we are shutting down */
return 1;
}
/* Skip remote mailboxes */
if (mbentry->mbtype & MBTYPE_REMOTE)
goto done;
/* clean up deleted entries after 7 days */
if (mbentry->mbtype & MBTYPE_DELETED) {
if (time(0) - mbentry->mtime > 86400*7) {
if (verbose)
fprintf(stderr, "Removing stale tombstone for %s\n", mbentry->name);
syslog(LOG_NOTICE, "Removing stale tombstone for %s", mbentry->name);
mboxlist_delete(mbentry->name);
}
goto done;
}
buf = xstrdup(mbentry->name);
/* see if we need to expire messages.
* since mailboxes inherit /vendor/cmu/cyrus-imapd/expire,
* we need to iterate all the way up to "" (server entry)
*/
if (!erock->skip_annotate) {
do {
buf_free(&attrib);
r = annotatemore_lookup(buf, IMAP_ANNOT_NS "expire", "",
&attrib);
if (r || /* error */
attrib.s) /* found an entry */
break;
} while (mboxname_make_parent(buf));
}
free(buf);
memset(erock->userflags, 0, sizeof(erock->userflags));
r = mailbox_open_iwl(mbentry->name, &mailbox);
if (r) {
/* mailbox corrupt/nonexistent -- skip it */
syslog(LOG_WARNING, "unable to open mailbox %s: %s",
mbentry->name, error_message(r));
goto done;
}
if (attrib.s && parse_duration(attrib.s, &expire_seconds)) {
/* add mailbox to table */
erock->expire_mark = expire_seconds ?
time(0) - expire_seconds : 0 /* never */ ;
hash_insert(mbentry->name,
xmemdup(&erock->expire_mark, sizeof(erock->expire_mark)),
&erock->table);
if (expire_seconds) {
if (verbose) {
fprintf(stderr,
"expiring messages in %s older than %0.2f days\n",
mbentry->name, ((double)expire_seconds/86400));
}
r = mailbox_expunge(mailbox, expire_cb, erock, NULL,
EVENT_MESSAGE_EXPIRE);
if (r)
syslog(LOG_ERR, "failed to expire old messages: %s", mbentry->name);
did_expunge = 1;
}
}
buf_free(&attrib);
if (!did_expunge && erock->do_userflags) {
r = mailbox_expunge(mailbox, userflag_cb, erock, NULL,
EVENT_MESSAGE_EXPIRE);
if (r)
syslog(LOG_ERR, "failed to scan user flags for %s: %s",
mbentry->name, error_message(r));
}
erock->messages_seen += mailbox->i.num_records;
if (erock->do_userflags)
expunge_userflags(mailbox, erock);
if (verbose)
fprintf(stderr, "cleaning up expunged messages in %s\n", mbentry->name);
r = mailbox_expunge_cleanup(mailbox, erock->expunge_mark, &numexpunged);
erock->messages_expunged += numexpunged;
erock->mailboxes_seen++;
if (r) {
syslog(LOG_WARNING, "failure expiring %s: %s", mbentry->name, error_message(r));
annotate_state_abort(&mailbox->annot_state);
}
done:
mailbox_close(&mailbox);
/* Even if we had a problem with one mailbox, continue with the others */
return 0;
}
static int delete(const mbentry_t *mbentry, void *rock)
{
struct delete_rock *drock = (struct delete_rock *) rock;
time_t timestamp;
if (sigquit)
return 1;
if (mbentry->mbtype & MBTYPE_DELETED)
goto done;
if (mbentry->mbtype & MBTYPE_REMOTE)
goto done;
/* check if this is a mailbox we want to examine */
if (!mboxname_isdeletedmailbox(mbentry->name, ×tamp))
goto done;
if ((timestamp == 0) || (timestamp > drock->delete_mark))
goto done;
if (verbose)
fprintf(stderr, "Cleaning up %s\n", mbentry->name);
/* Add this mailbox to list of mailboxes to delete */
strarray_append(&drock->to_delete, mbentry->name);
done:
/* Even if we had a problem with one mailbox, continue with the others */
return 0;
}
static int expire_conversations(const mbentry_t *mbentry, void *rock)
{
struct conversations_rock *crock = (struct conversations_rock *)rock;
struct conversations_state *state = NULL;
unsigned int nseen = 0, ndeleted = 0;
char *filename = NULL;
if (sigquit)
return 1;
if (mbentry->mbtype & MBTYPE_DELETED)
goto done;
if (mbentry->mbtype & MBTYPE_REMOTE)
goto done;
if (mboxname_isdeletedmailbox(mbentry->name, NULL))
goto done;
filename = conversations_getmboxpath(mbentry->name);
if (!filename)
goto done;
if (hash_lookup(filename, &crock->seen))
goto done;
if (verbose)
fprintf(stderr, "Pruning conversations from db %s\n", filename);
if (!conversations_open_path(filename, &state)) {
conversations_prune(state, crock->expire_mark, &nseen, &ndeleted);
conversations_commit(&state);
}
hash_insert(filename, (void *)1, &crock->seen);
crock->databases_seen++;
crock->msgids_seen += nseen;
crock->msgids_expired += ndeleted;
done:
free(filename);
return 0;
}
static void sighandler (int sig __attribute((unused)))
{
sigquit = 1;
return;
}
int main(int argc, char *argv[])
{
extern char *optarg;
int opt, r = 0;
int do_expunge = 1; /* gnb:TODO bool */
int expunge_seconds = -1;
int archive_seconds = -1;
int delete_seconds = -1;
int expire_seconds = 0;
int cid_expire_seconds;
int do_cid_expire = -1;
char *alt_config = NULL;
const char *find_prefix = NULL;
const char *do_user = NULL;
struct expire_rock erock;
struct delete_rock drock;
struct conversations_rock crock;
struct sigaction action;
/* zero the expire_rock & delete_rock */
memset(&erock, 0, sizeof(erock));
construct_hash_table(&erock.table, 10000, 1);
memset(&drock, 0, sizeof(drock));
strarray_init(&drock.to_delete);
memset(&crock, 0, sizeof(crock));
construct_hash_table(&crock.seen, 100, 1);
while ((opt = getopt(argc, argv, "C:D:E:X:A:p:u:vaxtcFS:")) != EOF) {
switch (opt) {
case 'C': /* alt config file */
alt_config = optarg;
break;
case 'A':
if (archive_seconds >= 0) usage();
if (!parse_duration(optarg, &archive_seconds)) usage();
break;
case 'D':
if (delete_seconds >= 0) usage();
if (!parse_duration(optarg, &delete_seconds)) usage();
break;
case 'E':
if (expire_seconds > 0) usage();
if (!parse_duration(optarg, &expire_seconds)) usage();
break;
case 'X':
if (expunge_seconds >= 0) usage();
if (!parse_duration(optarg, &expunge_seconds)) usage();
break;
case 'F':
keep_flagged = 0;
break;
case 'p':
find_prefix = optarg;
break;
case 'u':
do_user = optarg;
break;
case 'v':
verbose++;
break;
case 'a':
erock.skip_annotate = 1;
break;
case 'x':
if (!do_expunge) usage();
do_expunge = 0;
break;
case 't':
erock.do_userflags = 1;
break;
case 'c':
if (!do_cid_expire) usage();
do_cid_expire = 0;
break;
default:
usage();
break;
}
}
if (!expire_seconds &&
delete_seconds == -1 &&
expunge_seconds == -1 &&
archive_seconds == -1 &&
!erock.do_userflags)
usage();
sigemptyset(&action.sa_mask);
action.sa_flags = 0;
action.sa_handler = sighandler;
if (sigaction(SIGQUIT, &action, NULL) < 0)
fatal("unable to install signal handler for SIGQUIT", EC_TEMPFAIL);
if (sigaction(SIGINT, &action, NULL) < 0)
fatal("unable to install signal handler for SIGINT", EC_TEMPFAIL);
if (sigaction(SIGTERM, &action, NULL) < 0)
fatal("unable to install signal handler for SIGTERM", EC_TEMPFAIL);
cyrus_init(alt_config, "cyr_expire", 0, 0);
global_sasl_init(1, 0, NULL);
if (do_cid_expire < 0)
do_cid_expire = config_getswitch(IMAPOPT_CONVERSATIONS);
annotate_init(NULL, NULL);
annotatemore_open();
mboxlist_init(0);
mboxlist_open(NULL);
/* open the quota db, we'll need it for expunge */
quotadb_init(0);
quotadb_open(NULL);
/* setup for mailbox event notifications */
mboxevent_init();
/* Set namespace -- force standard (internal) */
if ((r = mboxname_init_namespace(&expire_namespace, 1)) != 0) {
syslog(LOG_ERR, "%s", error_message(r));
fatal(error_message(r), EC_CONFIG);
}
mboxevent_setnamespace(&expire_namespace);
if (duplicate_init(NULL) != 0) {
fprintf(stderr,
"cyr_expire: unable to init duplicate delivery database\n");
exit(1);
}
if (archive_seconds >= 0) {
time_t archive_mark = time(0) - archive_seconds;
/* XXX - add syslog? */
if (do_user)
mboxlist_usermboxtree(do_user, archive, &archive_mark, MBOXTREE_DELETED);
else
mboxlist_allmbox(find_prefix, archive, &archive_mark, 0);
}
if (do_expunge && (expunge_seconds >= 0 || expire_seconds || erock.do_userflags)) {
/* xxx better way to determine a size for this table? */
/* expire messages from mailboxes,
* build a hash table of mailboxes in which we expired messages,
* and perform a cleanup of expunged messages
*/
if (expunge_seconds < 0) {
erock.expunge_mark = 0;
} else {
erock.expunge_mark = time(0) - expunge_seconds;
if (verbose) {
fprintf(stderr,
"Expunging deleted messages in mailboxes older than %0.2f days\n",
((double)expunge_seconds/86400));
}
}
if (do_user)
mboxlist_usermboxtree(do_user, expire, &erock, MBOXTREE_DELETED);
else
mboxlist_allmbox(find_prefix, expire, &erock, 0);
syslog(LOG_NOTICE, "Expired %lu and expunged %lu out of %lu "
"messages from %lu mailboxes",
erock.messages_expired,
erock.messages_expunged,
erock.messages_seen,
erock.mailboxes_seen);
if (erock.do_userflags)
syslog(LOG_NOTICE, "Expunged %lu user flags",
erock.userflags_expunged);
if (verbose) {
fprintf(stderr, "\nExpired %lu and expunged %lu out of %lu "
"messages from %lu mailboxes\n",
erock.messages_expired,
erock.messages_expunged,
erock.messages_seen,
erock.mailboxes_seen);
if (erock.do_userflags)
fprintf(stderr, "Expunged %lu user flags\n",
erock.userflags_expunged);
}
}
if (sigquit) {
goto finish;
}
if (do_cid_expire) {
cid_expire_seconds = config_getint(IMAPOPT_CONVERSATIONS_EXPIRE_DAYS) * 86400;
crock.expire_mark = time(0) - cid_expire_seconds;
if (verbose)
fprintf(stderr,
"Removing conversation entries older than %0.2f days\n",
(double)(cid_expire_seconds/86400));
if (do_user)
mboxlist_usermboxtree(do_user, expire_conversations, &crock, MBOXTREE_DELETED);
else
mboxlist_allmbox(find_prefix, expire_conversations, &crock, 0);
syslog(LOG_NOTICE, "Expired %lu entries of %lu entries seen "
"in %lu conversation databases",
crock.msgids_expired,
crock.msgids_seen,
crock.databases_seen);
if (verbose)
fprintf(stderr, "Expired %lu entries of %lu entries seen "
"in %lu conversation databases\n",
crock.msgids_expired,
crock.msgids_seen,
crock.databases_seen);
}
if (sigquit) {
goto finish;
}
if ((delete_seconds >= 0) && mboxlist_delayed_delete_isenabled() &&
config_getstring(IMAPOPT_DELETEDPREFIX)) {
int count = 0;
int i;
if (verbose) {
fprintf(stderr,
"Removing deleted mailboxes older than %0.2f days\n",
((double)delete_seconds/86400));
}
drock.delete_mark = time(0) - delete_seconds;
if (do_user)
mboxlist_usermboxtree(do_user, delete, &drock, MBOXTREE_DELETED);
else
mboxlist_allmbox(find_prefix, delete, &drock, 0);
for (i = 0 ; i < drock.to_delete.count ; i++) {
char *name = drock.to_delete.data[i];
if (sigquit) {
goto finish;
}
if (verbose) {
fprintf(stderr, "Removing: %s\n", name);
}
r = mboxlist_deletemailbox(name, 1, NULL, NULL, NULL, 0, 0, 0);
count++;
}
if (verbose) {
if (count != 1) {
fprintf(stderr, "Removed %d deleted mailboxes\n", count);
} else {
fprintf(stderr, "Removed 1 deleted mailbox\n");
}
}
syslog(LOG_NOTICE, "Removed %d deleted mailboxes", count);
}
if (sigquit) {
goto finish;
}
/* purge deliver.db entries of expired messages */
if (expire_seconds > 0)
r = duplicate_prune(expire_seconds, &erock.table);
finish:
free_hash_table(&erock.table, free);
free_hash_table(&crock.seen, NULL);
strarray_fini(&drock.to_delete);
quotadb_close();
quotadb_done();
mboxlist_close();
mboxlist_done();
annotatemore_close();
annotate_done();
duplicate_done();
sasl_done();
cyrus_done();
exit(r);
}