changeset 4523:99699cf9df43 HEAD

Initial import of expire plugin code. Seems to work with at least one user. :)
author Timo Sirainen <timo.sirainen@movial.fi>
date Mon, 31 Jul 2006 02:12:51 +0300
parents 706c0cb1c821
children da7c51c193e2
files configure.in dovecot-example.conf src/plugins/Makefile.am src/plugins/expire/.cvsignore src/plugins/expire/Makefile.am src/plugins/expire/auth-client.c src/plugins/expire/auth-client.h src/plugins/expire/expire-env.c src/plugins/expire/expire-env.h src/plugins/expire/expire-plugin.c src/plugins/expire/expire-plugin.h src/plugins/expire/expire-tool.c
diffstat 12 files changed, 968 insertions(+), 1 deletions(-) [+]
line wrap: on
line diff
--- a/configure.in	Mon Jul 31 02:07:46 2006 +0300
+++ b/configure.in	Mon Jul 31 02:12:51 2006 +0300
@@ -1793,6 +1793,7 @@
 src/plugins/Makefile
 src/plugins/acl/Makefile
 src/plugins/convert/Makefile
+src/plugins/expire/Makefile
 src/plugins/quota/Makefile
 src/plugins/imap-quota/Makefile
 src/plugins/trash/Makefile
--- a/dovecot-example.conf	Mon Jul 31 02:07:46 2006 +0300
+++ b/dovecot-example.conf	Mon Jul 31 02:12:51 2006 +0300
@@ -1001,4 +1001,13 @@
   # is a text file where each line is in format: <priority> <mailbox name>
   # Mails are first deleted in lowest -> highest priority number order
   #trash = /etc/dovecot-trash.conf
+
+  # Expire plugin. Mails are expunged from mailboxes after being there the
+  # configurable time. The first expiration date for each mailbox is stored in
+  # a dictionary so it can be quickly determined which mailboxes contain
+  # expired mails. The actual expunging is done in a nightly cronjob, which
+  # you must set up:
+  #   dovecot --exec-mail ext /usr/libexec/dovecot/expire-mails
+  #expire = Trash 7 Spam 30
+  #expire_dict = db:/var/lib/dovecot/expire.db
 }
--- a/src/plugins/Makefile.am	Mon Jul 31 02:07:46 2006 +0300
+++ b/src/plugins/Makefile.am	Mon Jul 31 02:12:51 2006 +0300
@@ -2,4 +2,4 @@
 ZLIB = zlib
 endif
 
-SUBDIRS = acl convert quota imap-quota trash $(ZLIB)
+SUBDIRS = acl convert expire quota imap-quota trash $(ZLIB)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/plugins/expire/.cvsignore	Mon Jul 31 02:12:51 2006 +0300
@@ -0,0 +1,9 @@
+*.la
+*.lo
+*.o
+.deps
+.libs
+Makefile
+Makefile.in
+so_locations
+expire-tool
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/plugins/expire/Makefile.am	Mon Jul 31 02:12:51 2006 +0300
@@ -0,0 +1,55 @@
+AM_CPPFLAGS = \
+	-I$(top_srcdir)/src/lib \
+	-I$(top_srcdir)/src/lib-dict \
+	-I$(top_srcdir)/src/lib-mail \
+	-I$(top_srcdir)/src/lib-index \
+	-I$(top_srcdir)/src/lib-storage \
+	-I$(top_srcdir)/src/lib-storage/index \
+	-DPKG_RUNDIR=\""$(rundir)"\"
+
+lib01_expire_plugin_la_LDFLAGS = -module -avoid-version
+
+module_LTLIBRARIES = \
+	lib01_expire_plugin.la
+
+lib01_expire_plugin_la_SOURCES = \
+	expire-env.c \
+	expire-plugin.c
+
+noinst_HEADERS = \
+	auth-client.h \
+	expire-env.h \
+	expire-plugin.h
+
+noinst_PROGRAMS = expire-tool
+
+expire_tool_SOURCES = \
+	auth-client.c \
+	expire-tool.c
+
+libs = \
+	$(top_builddir)/src/lib-storage/register/libstorage-register.a \
+	$(STORAGE_LIBS) \
+	$(top_builddir)/src/lib-storage/libstorage.a \
+	$(top_builddir)/src/lib-storage/subscription-file/libstorage_subscription_file.a \
+	$(top_builddir)/src/lib-imap/libimap.a \
+	$(top_builddir)/src/lib-mail/libmail.a \
+	$(top_builddir)/src/lib-dict/libdict.a \
+	$(top_builddir)/src/lib-charset/libcharset.a \
+	$(top_builddir)/src/lib/liblib.a
+
+expire_tool_LDADD = \
+	$(libs) \
+	$(LIBICONV) \
+	$(RAND_LIBS)
+
+expire_tool_DEPENDENCIES = $(libs)
+
+install-exec-local:
+	$(mkdir_p) $(DESTDIR)$(moduledir)/imap \
+	  $(DESTDIR)$(moduledir)/pop3 \
+	  $(DESTDIR)$(moduledir)/lda
+	for d in imap pop3 lda; do \
+	  rm -f $(DESTDIR)$(moduledir)/$$d/lib01_expire_plugin.so; \
+	  $(LN_S) ../lib01_expire_plugin.so $(DESTDIR)$(moduledir)/$$d; \
+	done
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/plugins/expire/auth-client.c	Mon Jul 31 02:12:51 2006 +0300
@@ -0,0 +1,221 @@
+/* Copyright (C) 2005-2006 Timo Sirainen */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "network.h"
+#include "istream.h"
+#include "ostream.h"
+#include "env-util.h"
+#include "restrict-access.h"
+#include "auth-client.h"
+
+#include <stdlib.h>
+#include <unistd.h>
+
+#define MAX_INBUF_SIZE 8192
+#define MAX_OUTBUF_SIZE 512
+
+struct auth_connection {
+	char *auth_socket;
+
+	int fd;
+	struct io *io;
+	struct istream *input;
+	struct ostream *output;
+
+	uid_t orig_uid, current_uid;
+	const char *current_user;
+	int return_value;
+
+	unsigned int handshaked:1;
+};
+
+static void auth_input(void *context);
+
+static int auth_connection_connect(struct auth_connection *conn)
+{
+	int fd;
+
+	if (conn->fd != -1)
+		return 0;
+
+	fd = net_connect_unix(conn->auth_socket);
+	if (fd < 0) {
+		i_error("net_connect(%s) failed: %m", conn->auth_socket);
+		return -1;
+	}
+
+	conn->fd = fd;
+	conn->input =
+		i_stream_create_file(fd, default_pool, MAX_INBUF_SIZE, FALSE);
+	conn->output =
+		o_stream_create_file(fd, default_pool, MAX_OUTBUF_SIZE, FALSE);
+	conn->io = io_add(fd, IO_READ, auth_input, conn);
+
+	o_stream_send_str(conn->output, "VERSION\t1\t0\n");
+	return 0;
+}
+
+static void auth_connection_close(struct auth_connection *conn)
+{
+	if (conn->fd == -1)
+		return;
+
+	io_remove(&conn->io);
+	i_stream_unref(&conn->input);
+	o_stream_unref(&conn->output);
+
+	if (close(conn->fd) < 0)
+		i_error("close() failed: %m");
+	conn->fd = -1;
+}
+
+struct auth_connection *auth_connection_init(const char *auth_socket)
+{
+	struct auth_connection *conn;
+
+	conn = i_new(struct auth_connection, 1);
+	conn->auth_socket = i_strdup(auth_socket);
+	conn->orig_uid = conn->current_uid = geteuid();
+	conn->fd = -1;
+
+	(void)auth_connection_connect(conn);
+	return conn;
+}
+
+void auth_connection_deinit(struct auth_connection *conn)
+{
+	auth_connection_close(conn);
+	i_free(conn->auth_socket);
+	i_free(conn);
+}
+
+static void auth_parse_input(struct auth_connection *conn, const char *args)
+{
+	const char *const *tmp, *key, *value;
+	uid_t uid = (uid_t)-1;
+	int home_found = FALSE;
+
+	for (tmp = t_strsplit(args, "\t"); *tmp != NULL; tmp++) {
+		if (strncmp(*tmp, "uid=", 4) == 0)
+			uid = strtoul(*tmp + 4, NULL, 10);
+		else if (strncmp(*tmp, "gid=", 4) == 0) {
+			gid_t gid = strtoul(*tmp + 4, NULL, 10);
+
+			if (conn->orig_uid == 0 || getegid() != gid) {
+				env_put(t_strconcat("RESTRICT_SETGID=",
+						    *tmp + 4, NULL));
+			}
+		} else if (strncmp(*tmp, "chroot=", 7) == 0) {
+			env_put(t_strconcat("RESTRICT_CHROOT=",
+					    *tmp + 7, NULL));
+		} else if (strncmp(*tmp, "home=", 5) == 0) {
+			home_found = TRUE;
+			env_put(t_strconcat("HOME=", *tmp + 5, NULL));
+		} else {
+			key = t_str_ucase(t_strcut(*tmp, '='));
+			value = strchr(*tmp, '=');
+			if (value != NULL)
+				env_put(t_strconcat(key, "=", value+1, NULL));
+		}
+	}
+
+	if (!home_found) {
+		/* we must have a home directory */
+		i_error("userdb(%s) didn't return a home directory",
+			conn->current_user);
+		return;
+	}
+
+	if (uid == (uid_t)-1) {
+		i_error("userdb(%s) didn't return uid", conn->current_user);
+		return;
+	}
+
+	/* we'll change only effective UID. This is a bit unfortunate since
+	   it allows reverting back to root, but we'll have to be able to
+	   access different users' mailboxes.. */
+	if (uid != conn->current_uid) {
+		if (conn->current_uid != 0) {
+			if (seteuid(0) != 0)
+				i_fatal("seteuid(0) failed: %m");
+		}
+		if (seteuid(uid) < 0)
+			i_fatal("seteuid(%s) failed: %m", dec2str(uid));
+		conn->current_uid = uid;
+	}
+
+	restrict_access_by_env(FALSE);
+	conn->return_value = 1;
+}
+
+static void auth_input(void *context)
+{
+	struct auth_connection *conn = context;
+	const char *line;
+
+	switch (i_stream_read(conn->input)) {
+	case 0:
+		return;
+	case -1:
+		/* disconnected */
+		auth_connection_close(conn);
+		return;
+	case -2:
+		/* buffer full */
+		i_error("BUG: Auth master sent us more than %d bytes",
+			MAX_INBUF_SIZE);
+		auth_connection_close(conn);
+		return;
+	}
+
+	if (!conn->handshaked) {
+		while ((line = i_stream_next_line(conn->input)) != NULL) {
+			if (strncmp(line, "VERSION\t", 8) == 0) {
+				if (strncmp(line + 8, "1\t", 2) != 0) {
+					i_error("Auth master version mismatch");
+					auth_connection_close(conn);
+					return;
+				}
+			} else if (strncmp(line, "SPID\t", 5) == 0) {
+				conn->handshaked = TRUE;
+				break;
+			}
+		}
+	}
+
+	line = i_stream_next_line(conn->input);
+	if (line != NULL) {
+		if (strncmp(line, "USER\t1\t", 7) == 0) {
+			auth_parse_input(conn, line + 7);
+		} else if (strcmp(line, "NOTFOUND\t1") == 0)
+			conn->return_value = 0;
+		else if (strncmp(line, "FAIL\t1\t", 7) == 0)
+			conn->return_value = -1;
+		else {
+			i_error("BUG: Unexpected input from auth master: %s",
+				line);
+			auth_connection_close(conn);
+		}
+		io_loop_stop(current_ioloop);
+	}
+}
+
+int auth_client_put_user_env(struct auth_connection *conn,
+			     const char *user)
+{
+	if (auth_connection_connect(conn) < 0)
+		return -1;
+
+	conn->current_user = user;
+	conn->return_value = -1;
+
+	o_stream_send_str(conn->output,
+			  t_strconcat("USER\t1\t", user, "\t"
+				      "service=expire\n", NULL));
+
+	io_loop_run(current_ioloop);
+
+	conn->current_user = NULL;
+	return conn->return_value;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/plugins/expire/auth-client.h	Mon Jul 31 02:12:51 2006 +0300
@@ -0,0 +1,11 @@
+#ifndef __AUTH_CLIENT_H
+#define __AUTH_CLIENT_H
+
+struct auth_connection *auth_connection_init(const char *auth_socket);
+void auth_connection_deinit(struct auth_connection *conn);
+
+/* Returns -1 = error, 0 = user not found, 1 = ok */
+int auth_client_put_user_env(struct auth_connection *conn,
+			     const char *user);
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/plugins/expire/expire-env.c	Mon Jul 31 02:12:51 2006 +0300
@@ -0,0 +1,61 @@
+/* Copyright (C) 2006 PT.COM / SAPO. Code by Timo Sirainen. */
+
+#include "lib.h"
+#include "array.h"
+#include "expire-env.h"
+
+#include <stdlib.h>
+
+struct expire_env {
+	pool_t pool;
+	ARRAY_DEFINE(expire_boxes, struct expire_box);
+};
+
+struct expire_env *expire_env_init(const char *str)
+{
+	struct expire_env *env;
+	struct expire_box box;
+	pool_t pool;
+	char *const *names;
+	unsigned int len;
+
+	pool = pool_alloconly_create("Expire pool", 512);
+	env = p_new(pool, struct expire_env, 1);
+	env->pool = pool;
+
+	names = p_strsplit(pool, str, " ");
+	len = strarray_length((const char *const *)names);
+
+	ARRAY_CREATE(&env->expire_boxes, pool, struct expire_box, len / 2);
+	for (; *names != NULL; names += 2) {
+		if (names[1] == NULL) {
+			i_fatal("expire: Missing expire days for mailbox '%s'",
+				*names);
+		}
+
+		box.name = *names;
+		box.expire_secs = strtoul(names[1], NULL, 10) * 3600 * 24;
+		array_append(&env->expire_boxes, &box, 1);
+	}
+
+	return env;
+}
+
+void expire_env_deinit(struct expire_env *env)
+{
+	pool_unref(env->pool);
+}
+
+const struct expire_box *expire_box_find(struct expire_env *env,
+					 const char *name)
+{
+	const struct expire_box *expire_boxes;
+	unsigned int i, count;
+
+	expire_boxes = array_get(&env->expire_boxes, &count);
+	for (i = 0; i < count; i++) {
+		if (strcmp(name, expire_boxes[i].name) == 0)
+			return &expire_boxes[i];
+	}
+	return NULL;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/plugins/expire/expire-env.h	Mon Jul 31 02:12:51 2006 +0300
@@ -0,0 +1,17 @@
+#ifndef __EXPIRE_ENV_H
+#define __EXPIRE_ENV_H
+
+struct expire_env;
+
+struct expire_box {
+	const char *name;
+	time_t expire_secs;
+};
+
+struct expire_env *expire_env_init(const char *str);
+void expire_env_deinit(struct expire_env *env);
+
+const struct expire_box *expire_box_find(struct expire_env *env,
+					 const char *name);
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/plugins/expire/expire-plugin.c	Mon Jul 31 02:12:51 2006 +0300
@@ -0,0 +1,346 @@
+/* Copyright (C) 2006 PT.COM / SAPO. Code by Tianyan Liu */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "array.h"
+#include "dict.h"
+#include "index-mail.h"
+#include "index-storage.h"
+#include "expire-env.h"
+#include "expire-plugin.h"
+
+#include <stdlib.h>
+
+#define EXPIRE_CONTEXT(obj) \
+	*((void **)array_idx_modifiable(&(obj)->module_contexts, \
+					expire.storage_module_id))
+
+struct expire {
+	struct dict *db;
+	struct expire_env *env;
+	const char *username;
+
+	unsigned int storage_module_id;
+	bool storage_module_id_set;
+
+	void (*next_hook_mail_storage_created)(struct mail_storage *storage);
+};
+
+struct expire_mail_storage {
+	struct mail_storage_vfuncs super;
+};
+
+struct expire_mailbox {
+	struct mailbox_vfuncs super;
+	time_t expire_secs;
+};
+
+struct expire_mail {
+	struct mail_vfuncs super;
+};
+
+struct expire_transaction_context {
+	struct mail *mail;
+	time_t first_save_time;
+
+	unsigned int first_expunged:1;
+};
+
+/* defined by imap, pop3, lda */
+extern void (*hook_mail_storage_created)(struct mail_storage *storage);
+
+static struct expire expire;
+
+static struct mailbox_transaction_context *
+expire_mailbox_transaction_begin(struct mailbox *box,
+				 enum mailbox_transaction_flags flags)
+{
+	struct expire_mailbox *xpr_box = EXPIRE_CONTEXT(box);
+	struct mailbox_transaction_context *t;
+	struct expire_transaction_context *xt;
+
+	t = xpr_box->super.transaction_begin(box, flags);
+	xt = i_new(struct expire_transaction_context, 1);
+	xt->mail = mail_alloc(t, 0, NULL);
+
+	array_idx_set(&t->module_contexts, expire.storage_module_id, &xt);
+	return t;
+}
+
+static int first_nonexpunged_timestamp(struct mailbox_transaction_context *_t,
+				       time_t *stamp_r)
+{
+	struct index_transaction_context *t =
+		(struct index_transaction_context *)_t;
+	struct expire_transaction_context *xt = EXPIRE_CONTEXT(_t);
+	struct mail_index_view *view = t->trans_view;
+	const struct mail_index_header *hdr;
+	const struct mail_index_record *rec;
+	uint32_t seq;
+	int ret = 0;
+
+	/* find the first non-expunged mail. we're here because the first
+	   mail was expunged, so don't bother checking it. */
+	hdr = mail_index_get_header(view);
+	for (seq = 2; seq <= hdr->messages_count; seq++) {
+		ret = mail_index_lookup(view, seq, &rec);
+		if (ret != 0)
+			break;
+	}
+	if (ret < 0) {
+		*stamp_r = 0;
+		return -1;
+	}
+
+	if (ret > 0) {
+		mail_set_seq(xt->mail, seq);
+		*stamp_r = mail_get_save_date(xt->mail);
+		if (*stamp_r == (time_t)-1)
+			return -1;
+	} else {
+		/* everything expunged */
+		*stamp_r = 0;
+	}
+	return 0;
+}
+
+static int
+expire_mailbox_transaction_commit(struct mailbox_transaction_context *t,
+				  enum mailbox_sync_flags flags)
+{
+	struct expire_mailbox *xpr_box = EXPIRE_CONTEXT(t->box);
+	struct expire_transaction_context *xt = EXPIRE_CONTEXT(t);
+	const char *key, *value;
+	time_t new_stamp;
+	bool update_dict;
+	int ret;
+
+	t_push();
+	key = t_strconcat(DICT_PATH_SHARED, expire.username, "/",
+			  t->box->name, NULL);
+
+	if (xt->first_expunged) {
+		/* first mail expunged. dict needs updating. */
+		update_dict = first_nonexpunged_timestamp(t, &new_stamp) == 0;
+	} else {
+		/* saved new mails. dict needs to be updated only if this is
+		   the first mail in the database */
+		ret = dict_lookup(expire.db, pool_datastack_create(),
+				  key, &value);
+		update_dict = ret == 0 || strtoul(value, NULL, 10) == 0;
+		new_stamp = xt->first_save_time;
+	}
+
+	mail_free(&xt->mail);
+	i_free(xt);
+
+	if (xpr_box->super.transaction_commit(t, flags) < 0) {
+		t_pop();
+		return -1;
+	}
+
+	if (update_dict) {
+		struct dict_transaction_context *dctx;
+
+		new_stamp += xpr_box->expire_secs;
+
+		dctx = dict_transaction_begin(expire.db);
+		dict_set(dctx, key, dec2str(new_stamp));
+		dict_transaction_commit(dctx);
+	}
+	t_pop();
+	return 0;
+}
+
+static void
+expire_mailbox_transaction_rollback(struct mailbox_transaction_context *t)
+{
+	struct expire_mailbox *xpr_box = EXPIRE_CONTEXT(t->box);
+	struct expire_transaction_context *xt = EXPIRE_CONTEXT(t);
+
+	mail_free(&xt->mail);
+
+	xpr_box->super.transaction_rollback(t);
+	i_free(xt);
+}
+
+static int expire_mail_expunge(struct mail *_mail)
+{
+	struct mail_private *mail = (struct mail_private *)_mail;
+	struct expire_mail *xpr_mail = EXPIRE_CONTEXT(mail);
+	struct expire_transaction_context *xt =
+		EXPIRE_CONTEXT(_mail->transaction);
+
+	if (xpr_mail->super.expunge(_mail) < 0)
+		return -1;
+
+	if (_mail->seq == 1) {
+		/* first mail expunged, database needs to be updated */
+		xt->first_expunged = TRUE;
+	}
+	return 0;
+}
+
+static struct mail *
+expire_mail_alloc(struct mailbox_transaction_context *t,
+		  enum mail_fetch_field wanted_fields,
+		  struct mailbox_header_lookup_ctx *wanted_headers)
+{
+	struct expire_mailbox *xpr_box = EXPIRE_CONTEXT(t->box);
+	struct expire_mail *xpr_mail;
+	struct mail *_mail;
+	struct mail_private *mail;
+
+	_mail = xpr_box->super.mail_alloc(t, wanted_fields, wanted_headers);
+	mail = (struct mail_private *)_mail;
+
+	xpr_mail = p_new(mail->pool, struct expire_mail, 1);
+	xpr_mail->super = mail->v;
+
+	mail->v.expunge = expire_mail_expunge;
+	array_idx_set(&mail->module_contexts, expire.storage_module_id,
+		      &xpr_mail);
+	return _mail;
+}
+
+static void
+mail_set_save_time(struct mailbox_transaction_context *t, uint32_t seq)
+{
+	struct expire_transaction_context *xt = EXPIRE_CONTEXT(t);
+	struct index_transaction_context *it =
+		(struct index_transaction_context *)t;
+
+	if (xt->first_save_time == 0)
+		xt->first_save_time = ioloop_time;
+
+	mail_cache_add(it->cache_trans, seq, MAIL_CACHE_SAVE_DATE,
+		       &ioloop_time, sizeof(ioloop_time));
+}
+
+static int
+expire_save_init(struct mailbox_transaction_context *t,
+		 enum mail_flags flags, struct mail_keywords *keywords,
+		 time_t received_date, int timezone_offset,
+		 const char *from_envelope, struct istream *input,
+		 struct mail *dest_mail, struct mail_save_context **ctx_r)
+{       
+	struct expire_transaction_context *xt = EXPIRE_CONTEXT(t);
+	struct expire_mailbox *xpr_box = EXPIRE_CONTEXT(t->box);
+	int ret;
+
+	if (dest_mail == NULL)
+		dest_mail = xt->mail;
+
+	ret = xpr_box->super.save_init(t, flags, keywords, received_date,
+				       timezone_offset, from_envelope, input,
+				       dest_mail, ctx_r);
+	if (ret >= 0)
+		mail_set_save_time(t, dest_mail->seq);
+	return ret;
+}
+
+static int
+expire_copy(struct mailbox_transaction_context *t, struct mail *mail,
+	    enum mail_flags flags, struct mail_keywords *keywords,
+	    struct mail *dest_mail)
+{
+	struct expire_transaction_context *xt = EXPIRE_CONTEXT(t);
+	struct expire_mailbox *xpr_box = EXPIRE_CONTEXT(t->box);
+	int ret;
+
+	if (dest_mail == NULL)
+		dest_mail = xt->mail;
+
+	ret = xpr_box->super.copy(t, mail, flags, keywords, dest_mail);
+	if (ret >= 0)
+		mail_set_save_time(t, dest_mail->seq);
+	return ret;
+}
+
+static void mailbox_expire_hook(struct mailbox *box, time_t expire_secs)
+{
+	struct expire_mailbox *xpr_box;
+
+	xpr_box = p_new(box->pool, struct expire_mailbox, 1);
+	xpr_box->super = box->v;
+
+	box->v.transaction_begin = expire_mailbox_transaction_begin;
+	box->v.transaction_commit = expire_mailbox_transaction_commit;
+	box->v.transaction_rollback = expire_mailbox_transaction_rollback;
+	box->v.mail_alloc = expire_mail_alloc;
+	box->v.save_init = expire_save_init;
+	box->v.copy = expire_copy;
+
+	xpr_box->expire_secs = expire_secs;
+
+	array_idx_set(&box->module_contexts,
+		      expire.storage_module_id, &xpr_box);
+}
+
+static struct mailbox *
+expire_mailbox_open(struct mail_storage *storage, const char *name,
+		    struct istream *input, enum mailbox_open_flags flags)
+{
+	struct expire_mail_storage *xpr_storage = EXPIRE_CONTEXT(storage);
+	struct mailbox *box;
+	const struct expire_box *expire_box;
+
+	box = xpr_storage->super.mailbox_open(storage, name, input, flags);
+	if (box != NULL) {
+		expire_box = expire_box_find(expire.env, name);
+		if (expire_box != NULL)
+			mailbox_expire_hook(box, expire_box->expire_secs);
+	}
+	return box;
+}
+
+static void expire_mail_storage_created(struct mail_storage *storage)
+{
+	struct expire_mail_storage *xpr_storage;
+
+	if (expire.next_hook_mail_storage_created != NULL)
+		expire.next_hook_mail_storage_created(storage);
+
+	xpr_storage = p_new(storage->pool, struct expire_mail_storage, 1);
+	xpr_storage->super = storage->v;
+	storage->v.mailbox_open = expire_mailbox_open;
+
+	if (!expire.storage_module_id_set) {
+		expire.storage_module_id = mail_storage_module_id++;
+		expire.storage_module_id_set = TRUE;
+	}
+
+	array_idx_set(&storage->module_contexts,
+		      expire.storage_module_id, &xpr_storage);
+}
+
+void expire_plugin_init(void)
+{
+	const char *env, *dict_uri;
+
+	env = getenv("EXPIRE");
+	if (env != NULL) {
+		dict_uri = getenv("EXPIRE_DICT");
+		if (dict_uri == NULL)
+			i_fatal("expire plugin: expire_dict setting missing");
+
+		expire.env = expire_env_init(env);
+		expire.db = dict_init(dict_uri, DICT_DATA_TYPE_UINT32, NULL);
+		expire.username = getenv("USER");
+
+		expire.next_hook_mail_storage_created =
+			hook_mail_storage_created;
+		hook_mail_storage_created = expire_mail_storage_created;
+	}
+}
+
+void expire_plugin_deinit(void)
+{
+	if (expire.db != NULL) {
+		hook_mail_storage_created =
+			expire.next_hook_mail_storage_created;
+
+		dict_deinit(&expire.db);
+		expire_env_deinit(expire.env);
+	}
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/plugins/expire/expire-plugin.h	Mon Jul 31 02:12:51 2006 +0300
@@ -0,0 +1,7 @@
+#ifndef __EXPIRE_PLUGIN_H
+#define __EXPIRE_PLUGIN_H
+
+void expire_plugin_init(void);
+void expire_plugin_deinit(void);
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/plugins/expire/expire-tool.c	Mon Jul 31 02:12:51 2006 +0300
@@ -0,0 +1,230 @@
+/* Copyright (C) 2006 Timo Sirainen */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "randgen.h"
+#include "lib-signals.h"
+#include "dict-client.h"
+#include "mail-search.h"
+#include "mail-storage.h"
+#include "auth-client.h"
+#include "expire-env.h"
+
+#include <stdlib.h>
+
+/* ugly, but automake doesn't like having it built as both static and
+   dynamic object.. */
+#include "expire-env.c"
+
+#define DEFAULT_AUTH_SOCKET_PATH PKG_RUNDIR"/auth-master"
+
+struct expire_context {
+	struct auth_connection *auth_conn;
+
+	char *user;
+	struct mail_storage *storage;
+};
+
+static int user_init(struct expire_context *ctx, const char *user)
+{
+	enum mail_storage_flags flags;
+	enum mail_storage_lock_method lock_method;
+	const char *mail_env;
+	int ret;
+
+	if ((ret = auth_client_put_user_env(ctx->auth_conn, user)) <= 0) {
+		if (ret < 0)
+			return ret;
+
+		/* user no longer exists */
+		return 0;
+	}
+
+	mail_env = getenv("MAIL");
+	mail_storage_parse_env(&flags, &lock_method);
+	ctx->storage = mail_storage_create_with_data(mail_env, user,
+						     flags, lock_method);
+	if (ctx->storage == NULL) {
+		i_error("Failed to create storage for '%s' with mail '%s'",
+			user, mail_env == NULL ? "(null)" : mail_env);
+		return -1;
+	}
+	return 1;
+}
+
+static void user_deinit(struct expire_context *ctx)
+{
+	mail_storage_destroy(&ctx->storage);
+	i_free_and_null(ctx->user);
+}
+
+static int
+mailbox_delete_old_mails(struct expire_context *ctx, const char *user,
+			 const char *mailbox, time_t expire_secs,
+			 time_t *oldest_r)
+{
+	struct mailbox *box;
+	struct mail_search_context *search_ctx;
+	struct mailbox_transaction_context *t;
+	struct mail_search_arg search_arg;
+	struct mail *mail;
+	time_t now, save_time;
+	int ret = 0;
+
+	*oldest_r = 0;
+
+	if (ctx->user != NULL && strcmp(user, ctx->user) != 0)
+		user_deinit(ctx);
+	if (ctx->user == NULL) {
+		if ((ret = user_init(ctx, user)) <= 0)
+			return ret;
+		ctx->user = i_strdup(user);
+	}
+
+	memset(&search_arg, 0, sizeof(search_arg));
+	search_arg.type = SEARCH_ALL;
+	search_arg.next = NULL;
+
+	box = mailbox_open(ctx->storage, mailbox, NULL, 0);
+	t = mailbox_transaction_begin(box, 0);
+	search_ctx = mailbox_search_init(t, NULL, &search_arg, NULL);
+	mail = mail_alloc(t, 0, NULL);
+
+	now = time(NULL);
+	while (mailbox_search_next(search_ctx, mail) > 0) {
+		save_time = mail_get_save_date(mail);
+		if (save_time == (time_t)-1) {
+			/* maybe just got expunged. anyway try again later. */
+			ret = -1;
+			break;
+		}
+
+		if (save_time + expire_secs <= now) {
+			if (mail_expunge(mail) < 0) {
+				ret = -1;
+				break;
+			}
+		} else {
+			/* first non-expunged one. */
+			*oldest_r = save_time;
+			break;
+		}
+	}
+	mail_free(&mail);
+
+	if (mailbox_search_deinit(&search_ctx) < 0)
+		ret = -1;
+	if (mailbox_transaction_commit(&t, MAILBOX_SYNC_FLAG_FULL_READ |
+				       MAILBOX_SYNC_FLAG_FULL_WRITE) < 0)
+		ret = -1;
+	mailbox_close(&box);
+	return ret < 0 ? -1 : 0;
+}
+
+static void expire_run(void)
+{
+	struct expire_context ctx;
+	struct dict *dict = NULL;
+	struct dict_transaction_context *trans;
+	struct dict_iterate_context *iter;
+	struct expire_env *env;
+	const struct expire_box *expire_box;
+	time_t oldest;
+	const char *auth_socket, *p, *key, *value;
+	const char *username, *mailbox;
+
+	dict_driver_register(&dict_driver_client);
+	mail_storage_init();
+	mail_storage_register_all();
+
+	if (getenv("EXPIRE") == NULL)
+		i_fatal("expire setting not set");
+	if (getenv("EXPIRE_DICT") == NULL)
+		i_fatal("expire_dict setting not set");
+
+	auth_socket = getenv("AUTH_SOCKET_PATH");
+	if (auth_socket == NULL)
+		auth_socket = DEFAULT_AUTH_SOCKET_PATH;
+
+	memset(&ctx, 0, sizeof(ctx));
+	ctx.auth_conn = auth_connection_init(auth_socket);
+	env = expire_env_init(getenv("EXPIRE"));
+	dict = dict_init(getenv("EXPIRE_DICT"), DICT_DATA_TYPE_UINT32, "");
+	trans = dict_transaction_begin(dict);
+	iter = dict_iterate_init(dict, DICT_PATH_SHARED,
+				 DICT_ITERATE_FLAG_SORT_BY_VALUE);
+
+	/* We'll get the oldest values (timestamps) first */
+	while (dict_iterate(iter, &key, &value) > 0) {
+		/* key = DICT_PATH_SHARED<user>/<mailbox> */
+		username = key + strlen(DICT_PATH_SHARED);
+
+		p = strchr(username, '/');
+		if (p == NULL) {
+			i_error("Expire dictionary contains invalid key: %s",
+				key);
+			continue;
+		}
+
+		t_push();
+		username = t_strdup_until(username, p);
+		mailbox = p + 1;
+
+		expire_box = expire_box_find(env, mailbox);
+		if (expire_box == NULL) {
+			/* we're no longer expunging old messages from here */
+			dict_unset(trans, key);
+		} else if (now < strtoul(value, NULL, 10)) {
+			/* this and the rest of the timestamps are in future,
+			   so stop processing */
+			t_pop();
+			break;
+		} else {
+			if (mailbox_delete_old_mails(&ctx, username, mailbox,
+						     expire_box->expire_secs,
+						     &oldest) == 0) {
+				/* successful update */
+				if (oldest == 0) {
+					/* no more messages or we're no longer
+					   expunging messages from here */
+					dict_unset(trans, key);
+				} else {
+					const char *new_value;
+
+					oldest += expire_box->expire_secs;
+					new_value = dec2str(oldest);
+					if (strcmp(value, new_value) != 0)
+						dict_set(trans, key, new_value);
+				}
+			}
+		}
+		t_pop();
+	}
+	dict_iterate_deinit(iter);
+	dict_transaction_commit(trans);
+	dict_deinit(&dict);
+
+	if (ctx.user != NULL)
+		user_deinit(&ctx);
+	auth_connection_deinit(ctx.auth_conn);
+
+	mail_storage_deinit();
+	dict_driver_unregister(&dict_driver_client);
+}
+
+int main(void)
+{
+	struct ioloop *ioloop;
+
+	lib_init();
+	lib_signals_init();
+	random_init();
+
+	ioloop = io_loop_create(system_pool);
+	expire_run();
+	io_loop_destroy(&ioloop);
+
+	lib_signals_deinit();
+	lib_deinit();
+	return 0;
+}