changeset 7764:aaeae4a920f7 HEAD

Moved fdatasync_path() to a global function.
author Timo Sirainen <tss@iki.fi>
date Tue, 04 Mar 2008 03:47:57 +0200
parents 028939047ffb
children 5597b0835902
files src/lib-index/mail-index-view-sync.c src/lib-index/mail-index.h src/lib-storage/index/dbox/dbox-file.c src/lib-storage/index/dbox/dbox-file.h src/lib-storage/index/dbox/dbox-mail.c src/lib-storage/index/dbox/dbox-storage.c src/lib-storage/index/dbox/dbox-storage.h src/lib-storage/index/dbox/dbox-sync-file.c src/lib-storage/index/dbox/dbox-sync.c src/lib-storage/index/index-mail.c src/lib-storage/index/maildir/maildir-save.c src/lib/Makefile.am src/lib/fdatasync-path.c src/lib/fdatasync-path.h src/master/child-process.c src/master/child-process.h src/master/mail-process.c src/master/mail-process.h src/master/main.c src/plugins/expire/Makefile.am src/plugins/expire/expire-env.c src/plugins/expire/expire-env.h src/plugins/expire/expire-plugin.c src/plugins/expire/expire-tool.c
diffstat 24 files changed, 502 insertions(+), 193 deletions(-) [+]
line wrap: on
line diff
--- a/src/lib-index/mail-index-view-sync.c	Mon Mar 03 08:59:10 2008 +0200
+++ b/src/lib-index/mail-index-view-sync.c	Tue Mar 04 03:47:57 2008 +0200
@@ -502,7 +502,7 @@
 
 #define FLAG_UPDATE_IS_INTERNAL(u) \
 	((((u)->add_flags | (u)->remove_flags) & \
-	  ~MAIL_INDEX_MAIL_FLAG_DIRTY) == 0)
+	  MAIL_INDEX_FLAGS_MASK) == 0)
 
 static bool
 mail_index_view_sync_get_rec(struct mail_index_view_sync_ctx *ctx,
--- a/src/lib-index/mail-index.h	Mon Mar 03 08:59:10 2008 +0200
+++ b/src/lib-index/mail-index.h	Tue Mar 04 03:47:57 2008 +0200
@@ -36,7 +36,10 @@
 };
 
 enum mail_index_mail_flags {
-	MAIL_INDEX_MAIL_FLAG_DIRTY = 0x80
+	/* For private use by backend. Replacing flags doesn't change this. */
+	MAIL_INDEX_MAIL_FLAG_BACKEND	= 0x40,
+	/* Message flags haven't been written to backend */
+	MAIL_INDEX_MAIL_FLAG_DIRTY	= 0x80
 };
 
 #define MAIL_INDEX_FLAGS_MASK \
@@ -245,9 +248,9 @@
    Changes done to the returned transaction are expected to describe the
    mailbox's current state.
 
-   The returned view already contains all the changes, so if e.g. a record's
-   flags are different in view than in the mailbox you can assume that they
-   were changed externally, and you need to update the index.
+   The returned view already contains all the changes (except expunge
+   requests). After applying sync records on top of backend flags they should
+   match flags in the view. If they don't, there have been external changes.
 
    Returned expunges are treated as expunge requests. They're not really
    removed from the index until you mark them expunged to the returned
--- a/src/lib-storage/index/dbox/dbox-file.c	Mon Mar 03 08:59:10 2008 +0200
+++ b/src/lib-storage/index/dbox/dbox-file.c	Tue Mar 04 03:47:57 2008 +0200
@@ -7,6 +7,8 @@
 #include "hostpid.h"
 #include "istream.h"
 #include "ostream.h"
+#include "mkdir-parents.h"
+#include "fdatasync-path.h"
 #include "write-full.h"
 #include "str.h"
 #include "dbox-storage.h"
@@ -375,6 +377,7 @@
 static int dbox_file_open_fd(struct dbox_file *file)
 {
 	const char *path;
+	bool alt = FALSE;
 	int i;
 
 	/* try the primary path first */
@@ -398,7 +401,11 @@
 		/* try the alternative path */
 		path = t_strdup_printf("%s/%s", file->mbox->alt_path,
 				       file->fname);
+		alt = TRUE;
 	}
+	i_free(file->current_path);
+	file->current_path = i_strdup(path);
+	file->alt_path = alt;
 	return 1;
 }
 
@@ -1015,7 +1022,7 @@
 	} else {
 		i_error("%s: Metadata changed unexpectedly",
 			dbox_file_get_path(file));
-		ret = 0;
+		ret = -1;
 	}
 
 	dbox_index_unlock_file(file->mbox->dbox_index, file->file_id);
@@ -1199,6 +1206,113 @@
 	return TRUE;
 }
 
+int dbox_file_move(struct dbox_file *file, bool alt_path)
+{
+	struct ostream *output;
+	const char *dest_dir, *temp_path, *dest_path;
+	struct stat st;
+	bool deleted;
+	int out_fd, ret = 0;
+
+	i_assert(file->input != NULL);
+
+	if (file->alt_path == alt_path)
+		return 0;
+
+	if (stat(file->current_path, &st) < 0 && errno == ENOENT) {
+		/* already expunged by another session */
+		return 0;
+	}
+
+	dest_dir = alt_path ? file->mbox->alt_path : file->mbox->path;
+	temp_path = t_strdup_printf("%s/%s", dest_dir,
+				    dbox_generate_tmp_filename());
+
+	/* first copy the file. make sure to catch every possible error
+	   since we really don't want to break the file. */
+	out_fd = open(temp_path, O_WRONLY | O_CREAT | O_TRUNC, 0600);
+	if (out_fd == -1 && errno == ENOENT) {
+		if (mkdir_parents(dest_dir, 0700) < 0) {
+			i_error("mkdir_parents(%s) failed: %m", dest_dir);
+			return -1;
+		}
+		out_fd = open(temp_path, O_WRONLY | O_CREAT | O_TRUNC, 0600);
+	}
+	if (out_fd == -1) {
+		i_error("open(%s, O_CREAT) failed: %m", temp_path);
+		return -1;
+	}
+	output = o_stream_create_fd_file(out_fd, 0, FALSE);
+	i_stream_seek(file->input, 0);
+	while ((ret = o_stream_send_istream(output, file->input)) > 0) ;
+	if (ret == 0)
+		ret = o_stream_flush(output);
+	if (output->stream_errno != 0) {
+		errno = output->stream_errno;
+		i_error("write(%s) failed: %m", temp_path);
+		ret = -1;
+	} else if (file->input->stream_errno != 0) {
+		errno = file->input->stream_errno;
+		i_error("read(%s) failed: %m", file->current_path);
+		ret = -1;
+	} else if (ret < 0) {
+		i_error("o_stream_send_istream(%s, %s) "
+			"failed with unknown error",
+			temp_path, file->current_path);
+	}
+	o_stream_unref(&output);
+
+	if (!file->mbox->ibox.fsync_disable && ret == 0) {
+		if (fsync(out_fd) < 0) {
+			i_error("fsync(%s) failed: %m", temp_path);
+			ret = -1;
+		}
+	}
+	if (close(out_fd) < 0) {
+		i_error("close(%s) failed: %m", temp_path);
+		ret = -1;
+	}
+	if (ret < 0) {
+		(void)unlink(temp_path);
+		return -1;
+	}
+
+	/* the temp file was successfully written. rename it now to the
+	   destination file. the destination shouldn't exist, but if it does
+	   its contents should be the same (except for maybe older metadata) */
+	dest_path = t_strdup_printf("%s/%s", dest_dir, file->fname);
+	if (rename(temp_path, dest_path) < 0) {
+		i_error("rename(%s, %s) failed: %m", temp_path, dest_path);
+		(void)unlink(temp_path);
+		return -1;
+	}
+	if (!file->mbox->ibox.fsync_disable) {
+		if (fdatasync_path(dest_dir) < 0) {
+			i_error("fdatasync(%s) failed: %m", dest_dir);
+			(void)unlink(dest_path);
+			return -1;
+		}
+	}
+	if (unlink(file->current_path) < 0) {
+		i_error("unlink(%s) failed: %m", file->current_path);
+		if (errno == EACCES) {
+			/* configuration problem? revert the write */
+			(void)unlink(dest_path);
+		}
+		/* who knows what happened to the file. keep both just to be
+		   sure both won't get deleted. */
+		return -1;
+	}
+
+	/* file was successfully moved - reopen it */
+	dbox_file_close(file);
+	if (dbox_file_open(file, TRUE, &deleted) <= 0) {
+		i_error("dbox_file_move(%s): reopening file failed", dest_path);
+		return -1;
+	}
+	return 0;
+}
+
 void dbox_mail_metadata_flags_append(string_t *str, enum mail_flags flags)
 {
 	unsigned int i;
--- a/src/lib-storage/index/dbox/dbox-file.h	Mon Mar 03 08:59:10 2008 +0200
+++ b/src/lib-storage/index/dbox/dbox-file.h	Tue Mar 04 03:47:57 2008 +0200
@@ -128,6 +128,7 @@
 	/* Includes the trailing LF that shouldn't be used */
 	unsigned int metadata_len;
 
+	unsigned int alt_path:1;
 	unsigned int maildir_file:1;
 	unsigned int nonappendable:1;
 	unsigned int deleted:1;
@@ -217,6 +218,9 @@
 bool dbox_file_lookup(struct dbox_mailbox *mbox, struct mail_index_view *view,
 		      uint32_t seq, uint32_t *file_id_r, uoff_t *offset_r);
 
+/* Move the file to alt path or back. */
+int dbox_file_move(struct dbox_file *file, bool alt_path);
+
 /* Append flags as metadata value to given string */
 void dbox_mail_metadata_flags_append(string_t *str, enum mail_flags flags);
 /* Append keywords as metadata value to given string */
--- a/src/lib-storage/index/dbox/dbox-mail.c	Mon Mar 03 08:59:10 2008 +0200
+++ b/src/lib-storage/index/dbox/dbox-mail.c	Tue Mar 04 03:47:57 2008 +0200
@@ -1,6 +1,7 @@
 /* Copyright (c) 2007-2008 Dovecot authors, see the included COPYING file */
 
 #include "lib.h"
+#include "ioloop.h"
 #include "istream.h"
 #include "index-mail.h"
 #include "dbox-storage.h"
@@ -119,7 +120,9 @@
 
 	value = dbox_file_metadata_get(file, DBOX_METADATA_SAVE_TIME);
 	data->save_date = value == NULL ? 0 : strtoul(value, NULL, 16);
-	*date_r = data->save_date;
+	/* if the time is missing or corrupted, use the current time and
+	   cache it */
+	*date_r = data->save_date == 0 ? ioloop_time : data->save_date;
 	return 0;
 }
 
--- a/src/lib-storage/index/dbox/dbox-storage.c	Mon Mar 03 08:59:10 2008 +0200
+++ b/src/lib-storage/index/dbox/dbox-storage.c	Tue Mar 04 03:47:57 2008 +0200
@@ -151,16 +151,21 @@
 static const char *
 dbox_get_alt_path(struct dbox_storage *storage, const char *path)
 {
+	const char *root;
 	unsigned int len;
 
 	if (storage->alt_dir == NULL)
 		return NULL;
 
-	len = strlen(storage->alt_dir);
-	if (strncmp(path, storage->alt_dir, len) != 0)
-		return t_strconcat(storage->alt_dir, path + len, NULL);
-	else
+	root = mailbox_list_get_path(storage->storage.list, NULL,
+				     MAILBOX_LIST_PATH_TYPE_DIR);
+
+	len = strlen(root);
+	if (strncmp(path, root, len) != 0 && path[len] == '/') {
+		/* can't determine the alt path - shouldn't happen */
 		return NULL;
+	}
+	return t_strconcat(storage->alt_dir, path + len, NULL);
 }
 
 static struct mailbox *
--- a/src/lib-storage/index/dbox/dbox-storage.h	Mon Mar 03 08:59:10 2008 +0200
+++ b/src/lib-storage/index/dbox/dbox-storage.h	Tue Mar 04 03:47:57 2008 +0200
@@ -26,6 +26,9 @@
 #define DBOX_DEFAULT_ROTATE_DAYS 0
 #define DBOX_DEFAULT_MAX_OPEN_FILES 64
 
+/* Flag specifies if the message should be in primary or alternative storage */
+#define DBOX_INDEX_FLAG_ALT MAIL_INDEX_MAIL_FLAG_BACKEND
+
 struct dbox_index_header {
 	uint32_t last_dirty_flush_stamp;
 };
--- a/src/lib-storage/index/dbox/dbox-sync-file.c	Mon Mar 03 08:59:10 2008 +0200
+++ b/src/lib-storage/index/dbox/dbox-sync-file.c	Tue Mar 04 03:47:57 2008 +0200
@@ -324,9 +324,9 @@
 		first_expunge_seq = (uint32_t)-1;
 	}
 
-	if (array_is_created(&entry->changes))
+	if (array_is_created(&entry->changes)) {
 		seqs = array_get(&entry->changes, &count);
-	else {
+	} else {
 		seqs = NULL;
 		count = 0;
 	}
@@ -347,6 +347,31 @@
 }
 
 static void
+dbox_sync_file_move_if_needed(struct dbox_sync_context *ctx,
+			      struct dbox_file *file,
+			      const struct dbox_sync_file_entry *entry)
+{
+	const struct seq_range *seq;
+	const struct mail_index_record *rec;
+	bool new_alt_path;
+
+	if (!array_is_created(&entry->changes))
+		return;
+
+	/* check if we want to move the file to alt path or back.
+	   FIXME: change this check somehow when a file may contain
+	   multiple messages. */
+	seq = array_idx(&entry->changes, 0);
+	rec = mail_index_lookup(ctx->sync_view, seq[0].seq1);
+	new_alt_path = (rec->flags & DBOX_INDEX_FLAG_ALT) != 0;
+	if (new_alt_path != file->alt_path) {
+		/* move the file. if it fails, nothing broke so
+		   don't worry about it. */
+		(void)dbox_file_move(file, new_alt_path);
+	}
+}
+
+static void
 dbox_sync_mark_single_file_expunged(struct dbox_sync_context *ctx,
 				    const struct dbox_sync_file_entry *entry)
 {
@@ -394,8 +419,10 @@
 		}
 	} else {
 		ret = dbox_file_open_or_create(file, TRUE, &deleted);
-		if (ret > 0 && !deleted)
+		if (ret > 0 && !deleted) {
+			dbox_sync_file_move_if_needed(ctx, file, entry);
 			ret = dbox_sync_file_int(ctx, file, entry, locked);
+		}
 	}
 	dbox_file_unref(&file);
 	return ret;
--- a/src/lib-storage/index/dbox/dbox-sync.c	Mon Mar 03 08:59:10 2008 +0200
+++ b/src/lib-storage/index/dbox/dbox-sync.c	Tue Mar 04 03:47:57 2008 +0200
@@ -17,12 +17,13 @@
 #define DBOX_REBUILD_COUNT 3
 
 static int dbox_sync_add_seq(struct dbox_sync_context *ctx,
-			     enum mail_index_sync_type type, uint32_t seq)
+			     const struct mail_index_sync_rec *sync_rec,
+			     uint32_t seq)
 {
 	struct dbox_sync_file_entry *entry;
 	uint32_t file_id;
 	uoff_t offset;
-	bool uid_file;
+	bool uid_file, add;
 
 	if (!dbox_file_lookup(ctx->mbox, ctx->sync_view, seq,
 			      &file_id, &offset))
@@ -30,8 +31,22 @@
 
 	entry = hash_lookup(ctx->syncs, POINTER_CAST(file_id));
 	if (entry == NULL) {
-		if (type != MAIL_INDEX_SYNC_TYPE_EXPUNGE &&
-		    !ctx->flush_dirty_flags) {
+		if (sync_rec->type == MAIL_INDEX_SYNC_TYPE_EXPUNGE ||
+		    ctx->flush_dirty_flags) {
+			/* expunges / flushing dirty flags */
+			add = TRUE;
+		} else if (sync_rec->type != MAIL_INDEX_SYNC_TYPE_FLAGS) {
+			/* keywords, not flushing dirty flags */
+			add = FALSE;
+		} else {
+			/* add if we're moving from/to alternative storage
+			   and we actually have an alt directory specified */
+			add = ((sync_rec->add_flags | sync_rec->remove_flags) &
+			       DBOX_INDEX_FLAG_ALT) != 0 &&
+				ctx->mbox->alt_path != NULL;
+		}
+
+		if (!add) {
 			mail_index_update_flags(ctx->trans, seq, MODIFY_ADD,
 				(enum mail_flags)MAIL_INDEX_MAIL_FLAG_DIRTY);
 			return 0;
@@ -43,7 +58,7 @@
 	}
 	uid_file = (file_id & DBOX_FILE_ID_FLAG_UID) != 0;
 
-	if (type == MAIL_INDEX_SYNC_TYPE_EXPUNGE) {
+	if (sync_rec->type == MAIL_INDEX_SYNC_TYPE_EXPUNGE) {
 		if (!array_is_created(&entry->expunges)) {
 			p_array_init(&entry->expunges, ctx->pool,
 				     uid_file ? 1 : 3);
@@ -81,7 +96,7 @@
 	}
 
 	for (seq = seq1; seq <= seq2; seq++) {
-		if (dbox_sync_add_seq(ctx, sync_rec->type, seq) < 0)
+		if (dbox_sync_add_seq(ctx, sync_rec, seq) < 0)
 			return -1;
 	}
 	return 0;
--- a/src/lib-storage/index/index-mail.c	Mon Mar 03 08:59:10 2008 +0200
+++ b/src/lib-storage/index/index-mail.c	Tue Mar 04 03:47:57 2008 +0200
@@ -1116,7 +1116,8 @@
 
 	rec = mail_index_lookup(mail->trans->trans_view, seq);
 	data->seq = seq;
-	data->flags = rec->flags & MAIL_FLAGS_NONRECENT;
+	data->flags = rec->flags & (MAIL_FLAGS_NONRECENT |
+				    MAIL_INDEX_MAIL_FLAG_BACKEND);
 
 	mail->mail.mail.seq = seq;
 	mail->mail.mail.uid = rec->uid;
@@ -1310,8 +1311,9 @@
 {
 	struct index_mail *imail = (struct index_mail *)mail;
 
+	flags &= MAIL_FLAGS_NONRECENT | MAIL_INDEX_MAIL_FLAG_BACKEND;
 	mail_index_update_flags(imail->trans->trans, mail->seq, modify_type,
-				flags & MAIL_FLAGS_NONRECENT);
+				flags);
 }
 
 void index_mail_update_keywords(struct mail *mail, enum modify_type modify_type,
--- a/src/lib-storage/index/maildir/maildir-save.c	Mon Mar 03 08:59:10 2008 +0200
+++ b/src/lib-storage/index/maildir/maildir-save.c	Tue Mar 04 03:47:57 2008 +0200
@@ -7,6 +7,7 @@
 #include "istream.h"
 #include "istream-crlf.h"
 #include "ostream.h"
+#include "fdatasync-path.h"
 #include "str.h"
 #include "index-mail.h"
 #include "maildir-storage.h"
@@ -571,25 +572,6 @@
 	ctx->files = pos;
 }
 
-static int fdatasync_path(const char *path)
-{
-	int fd, ret = 0;
-
-	/* Directories need to be opened as read-only.
-	   fsync() doesn't appear to care about it. */
-	fd = open(path, O_RDONLY);
-	if (fd == -1) {
-		i_error("open(%s) failed: %m", path);
-		return -1;
-	}
-	if (fdatasync(fd) < 0) {
-		i_error("fdatasync(%s) failed: %m", path);
-		ret = -1;
-	}
-	(void)close(fd);
-	return ret;
-}
-
 static int maildir_transaction_fsync_dirs(struct maildir_save_context *ctx,
 					  bool new_changed, bool cur_changed)
 {
@@ -597,12 +579,16 @@
 		return 0;
 
 	if (new_changed) {
-		if (fdatasync_path(ctx->newdir) < 0)
+		if (fdatasync_path(ctx->newdir) < 0) {
+			i_error("fdatasync_path(%s) failed: %m", ctx->newdir);
 			return -1;
+		}
 	}
 	if (cur_changed) {
-		if (fdatasync_path(ctx->curdir) < 0)
+		if (fdatasync_path(ctx->curdir) < 0) {
+			i_error("fdatasync_path(%s) failed: %m", ctx->curdir);
 			return -1;
+		}
 	}
 	return 0;
 }
--- a/src/lib/Makefile.am	Mon Mar 03 08:59:10 2008 +0200
+++ b/src/lib/Makefile.am	Tue Mar 04 03:47:57 2008 +0200
@@ -22,6 +22,7 @@
 	failures.c \
 	fd-close-on-exec.c \
 	fd-set-nonblock.c \
+	fdatasync-path.c \
 	fdpass.c \
 	file-cache.c \
 	file-copy.c \
@@ -120,6 +121,7 @@
 	failures.h \
 	fd-close-on-exec.h \
 	fd-set-nonblock.h \
+	fdatasync-path.h \
 	fdpass.h \
 	file-cache.h \
 	file-copy.h \
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/lib/fdatasync-path.c	Tue Mar 04 03:47:57 2008 +0200
@@ -0,0 +1,22 @@
+/* Copyright (c) 2008 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "fdatasync-path.h"
+
+#include <fcntl.h>
+#include <unistd.h>
+
+int fdatasync_path(const char *path)
+{
+	int fd, ret = 0;
+
+	/* Directories need to be opened as read-only.
+	   fsync() doesn't appear to care about it. */
+	fd = open(path, O_RDONLY);
+	if (fd == -1)
+		return -1;
+	if (fdatasync(fd) < 0)
+		ret = -1;
+	(void)close(fd);
+	return ret;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/lib/fdatasync-path.h	Tue Mar 04 03:47:57 2008 +0200
@@ -0,0 +1,7 @@
+#ifndef FDATASYNC_PATH_H
+#define FDATASYNC_PATH_H
+
+/* Open and fdatasync() the path. Works for files and directories. */
+int fdatasync_path(const char *path);
+
+#endif
--- a/src/master/child-process.c	Mon Mar 03 08:59:10 2008 +0200
+++ b/src/master/child-process.c	Tue Mar 04 03:47:57 2008 +0200
@@ -67,7 +67,7 @@
 
 void client_process_exec(const char *cmd, const char *title)
 {
-	const char *executable, *p, **argv;
+	const char **argv;
 
 	/* very simple argument splitting. */
 	if (*title == '\0')
@@ -75,7 +75,12 @@
 	else
 		argv = t_strsplit(t_strconcat(cmd, " ", title, NULL), " ");
 
-	executable = argv[0];
+	client_process_exec_argv(argv[0], argv);
+}
+
+void client_process_exec_argv(const char *executable, const char **argv)
+{
+	const char *p;
 
 	/* hide the path, it's ugly */
 	p = strrchr(argv[0], '/');
--- a/src/master/child-process.h	Mon Mar 03 08:59:10 2008 +0200
+++ b/src/master/child-process.h	Tue Mar 04 03:47:57 2008 +0200
@@ -32,6 +32,7 @@
 
 void child_process_init_env(void);
 void client_process_exec(const char *cmd, const char *title);
+void client_process_exec_argv(const char *executable, const char **argv);
 
 void child_process_set_destroy_callback(enum process_type type,
 					child_process_destroy_callback_t *cb);
--- a/src/master/mail-process.c	Mon Mar 03 08:59:10 2008 +0200
+++ b/src/master/mail-process.c	Tue Mar 04 03:47:57 2008 +0200
@@ -412,7 +412,7 @@
 	}
 }
 
-void mail_process_exec(const char *protocol, const char *section)
+void mail_process_exec(const char *protocol, const char **args)
 {
 	struct server_settings *server = settings_root;
 	const struct var_expand_table *var_expand_table;
@@ -421,11 +421,13 @@
 
 	if (strcmp(protocol, "ext") == 0) {
 		/* external binary. section contains path for it. */
-		if (section == NULL)
+		if (*args == NULL)
 			i_fatal("External binary parameter not given");
 		set = server->defaults;
-		executable = section;
+		executable = *args;
 	} else {
+		const char *section = *args;
+
 		if (section != NULL) {
 			for (; server != NULL; server = server->next) {
 				if (strcmp(server->name, section) == 0)
@@ -442,6 +444,7 @@
 		else
 			i_fatal("Unknown protocol: '%s'", protocol);
 		executable = set->mail_executable;
+		args = NULL;
 	}
 
 	var_expand_table =
@@ -468,7 +471,10 @@
 
 	mail_process_set_environment(set, getenv("MAIL"), var_expand_table,
 				     FALSE);
-        client_process_exec(executable, "");
+	if (args == NULL)
+		client_process_exec(executable, "");
+	else
+		client_process_exec_argv(executable, args);
 
 	i_fatal_status(FATAL_EXEC, "execv(%s) failed: %m", executable);
 }
--- a/src/master/mail-process.h	Mon Mar 03 08:59:10 2008 +0200
+++ b/src/master/mail-process.h	Tue Mar 04 03:47:57 2008 +0200
@@ -6,9 +6,7 @@
 struct login_group;
 struct auth_master_reply;
 
-void mail_process_exec(const char *protocol, const char *section)
-	ATTR_NORETURN;
-
+void mail_process_exec(const char *protocol, const char **args) ATTR_NORETURN;
 
 enum master_login_status
 create_mail_process(enum process_type process_type, struct settings *set,
--- a/src/master/main.c	Mon Mar 03 08:59:10 2008 +0200
+++ b/src/master/main.c	Tue Mar 04 03:47:57 2008 +0200
@@ -322,7 +322,7 @@
 {
 	printf(
 "Usage: dovecot [-F] [-c <config file>] [-p] [-n] [-a]\n"
-"       [--exec-mail <protocol>] [--version] [--build-options]\n");
+"       [--version] [--build-options] [--exec-mail <protocol> [<args>]]\n");
 }
 
 static void print_build_options(void)
@@ -431,7 +431,7 @@
 int main(int argc, char *argv[])
 {
 	/* parse arguments */
-	const char *exec_protocol = NULL, *exec_section = NULL, *user, *home;
+	const char *exec_protocol = NULL, **exec_args = NULL, *user, *home;
 	bool foreground = FALSE, ask_key_pass = FALSE, log_error = FALSE;
 	bool dump_config = FALSE, dump_config_nondefaults = FALSE;
 	int i;
@@ -460,13 +460,13 @@
 			/* Ask SSL private key password */
 			ask_key_pass = TRUE;
 		} else if (strcmp(argv[i], "--exec-mail") == 0) {
-			/* <protocol> [<server section>]
+			/* <protocol> [<args>]
 			   read configuration and execute mail process */
 			i++;
 			if (i == argc) i_fatal("Missing protocol argument");
 			exec_protocol = argv[i];
-			if (i+1 != argc) 
-				exec_section = argv[++i];
+			exec_args = (const char **)&argv[i+1];
+			break;
 		} else if (strcmp(argv[i], "--version") == 0) {
 			printf("%s\n", VERSION);
 			return 0;
@@ -540,7 +540,7 @@
 		/* Put back user and home */
 		env_put(t_strconcat("USER=", user, NULL));
 		env_put(t_strconcat("HOME=", home, NULL));
-		mail_process_exec(exec_protocol, exec_section);
+		mail_process_exec(exec_protocol, exec_args);
 	}
 
 	if (!log_error)
--- a/src/plugins/expire/Makefile.am	Mon Mar 03 08:59:10 2008 +0200
+++ b/src/plugins/expire/Makefile.am	Tue Mar 04 03:47:57 2008 +0200
@@ -4,6 +4,7 @@
 	-I$(top_srcdir)/src/lib \
 	-I$(top_srcdir)/src/lib-dict \
 	-I$(top_srcdir)/src/lib-mail \
+	-I$(top_srcdir)/src/lib-imap \
 	-I$(top_srcdir)/src/lib-index \
 	-I$(top_srcdir)/src/lib-storage \
 	-I$(top_srcdir)/src/lib-storage/index \
--- a/src/plugins/expire/expire-env.c	Mon Mar 03 08:59:10 2008 +0200
+++ b/src/plugins/expire/expire-env.c	Tue Mar 04 03:47:57 2008 +0200
@@ -2,42 +2,70 @@
 
 #include "lib.h"
 #include "array.h"
+#include "imap-match.h"
 #include "expire-env.h"
 
 #include <stdlib.h>
 
+enum expire_type {
+	EXPIRE_TYPE_EXPUNGE,
+	EXPIRE_TYPE_ALTMOVE
+};
+
+struct expire_box {
+	const char *pattern;
+	struct imap_match_glob *glob;
+
+	enum expire_type type;
+	unsigned int expire_secs;
+};
+
 struct expire_env {
 	pool_t pool;
 	ARRAY_DEFINE(expire_boxes, struct expire_box);
 };
 
-struct expire_env *expire_env_init(const char *str)
+static void expire_env_parse(struct expire_env *env, const char *str,
+			     enum expire_type type)
 {
-	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;
+	if (str == NULL)
+		return;
 
-	names = p_strsplit(pool, str, " ");
+	names = p_strsplit(env->pool, str, " ");
 	len = str_array_length((const char *const *)names);
 
-	p_array_init(&env->expire_boxes, pool, len / 2);
+	p_array_init(&env->expire_boxes, env->pool, 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;
+		box.pattern = *names;
+		/* FIXME: hardcoded separator isn't very good */
+		box.glob = imap_match_init(env->pool, box.pattern, TRUE, '/');
+		box.type = type;
+		box.expire_secs = strtoul(names[1], NULL, 10)/* * 3600 * 24*/ * 10;//FIXME
+
 		array_append(&env->expire_boxes, &box, 1);
 	}
+}
 
+struct expire_env *expire_env_init(const char *expunges, const char *altmoves)
+{
+	struct expire_env *env;
+	pool_t pool;
+
+	pool = pool_alloconly_create("Expire pool", 512);
+	env = p_new(pool, struct expire_env, 1);
+	env->pool = pool;
+
+	expire_env_parse(env, expunges, EXPIRE_TYPE_EXPUNGE);
+	expire_env_parse(env, altmoves, EXPIRE_TYPE_ALTMOVE);
 	return env;
 }
 
@@ -46,16 +74,41 @@
 	pool_unref(&env->pool);
 }
 
-const struct expire_box *expire_box_find(struct expire_env *env,
-					 const char *name)
+bool expire_box_find(struct expire_env *env, const char *name,
+		     unsigned int *expunge_secs_r,
+		     unsigned int *altmove_secs_r)
 {
 	const struct expire_box *expire_boxes;
 	unsigned int i, count;
+	unsigned int secs, expunge_min = 0, altmove_min = 0;
 
 	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];
+		if (imap_match(expire_boxes[i].glob, name) == IMAP_MATCH_YES) {
+			secs = expire_boxes[i].expire_secs;
+			i_assert(secs > 0);
+
+			switch (expire_boxes[i].type) {
+			case EXPIRE_TYPE_EXPUNGE:
+				if (expunge_min == 0 || expunge_min > secs)
+					expunge_min = secs;
+				break;
+			case EXPIRE_TYPE_ALTMOVE:
+				if (altmove_min == 0 || altmove_min > secs)
+					altmove_min = secs;
+				break;
+			}
+		}
 	}
-	return NULL;
+	*expunge_secs_r = expunge_min;
+	*altmove_secs_r = altmove_min;
+	return expunge_min > 0 || altmove_min > 0;
 }
+
+unsigned int expire_box_find_min_secs(struct expire_env *env, const char *name)
+{
+	unsigned int secs1, secs2;
+
+	(void)expire_box_find(env, name, &secs1, &secs2);
+	return secs1 < secs2 && secs1 != 0 ? secs1 : secs2;
+}
--- a/src/plugins/expire/expire-env.h	Mon Mar 03 08:59:10 2008 +0200
+++ b/src/plugins/expire/expire-env.h	Tue Mar 04 03:47:57 2008 +0200
@@ -3,15 +3,13 @@
 
 struct expire_env;
 
-struct expire_box {
-	const char *name;
-	time_t expire_secs;
-};
-
-struct expire_env *expire_env_init(const char *str);
+struct expire_env *expire_env_init(const char *expunges, const char *altmoves);
 void expire_env_deinit(struct expire_env *env);
 
-const struct expire_box *expire_box_find(struct expire_env *env,
-					 const char *name);
+bool expire_box_find(struct expire_env *env, const char *name,
+		     unsigned int *expunge_secs_r,
+		     unsigned int *altmove_secs_r);
+
+unsigned int expire_box_find_min_secs(struct expire_env *env, const char *name);
 
 #endif
--- a/src/plugins/expire/expire-plugin.c	Mon Mar 03 08:59:10 2008 +0200
+++ b/src/plugins/expire/expire-plugin.c	Tue Mar 04 03:47:57 2008 +0200
@@ -3,6 +3,7 @@
 #include "lib.h"
 #include "ioloop.h"
 #include "array.h"
+#include "str.h"
 #include "dict.h"
 #include "mail-namespace.h"
 #include "index-mail.h"
@@ -33,9 +34,7 @@
 struct expire_transaction_context {
 	union mailbox_transaction_module_context module_ctx;
 
-	struct mail *mail;
-	time_t first_save_time;
-
+	unsigned int saves:1;
 	unsigned int first_expunged:1;
 };
 
@@ -56,7 +55,6 @@
 
 	t = xpr_box->module_ctx.super.transaction_begin(box, flags);
 	xt = i_new(struct expire_transaction_context, 1);
-	xt->mail = mail_alloc(t, 0, NULL);
 
 	MODULE_CONTEXT_SET(t, expire_storage_module, xt);
 	return t;
@@ -67,21 +65,26 @@
 {
 	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;
+	struct mail *mail;
 	uint32_t seq;
 
+	mail = mail_alloc(_t, 0, NULL);
+
 	/* 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++) {
 		if (!mail_index_is_expunged(view, seq)) {
-			mail_set_seq(xt->mail, seq);
-			if (mail_get_save_date(xt->mail, stamp_r) == 0)
+			mail_set_seq(mail, seq);
+			if (mail_get_save_date(mail, stamp_r) == 0) {
+				mail_free(&mail);
 				return;
+			}
 		}
 	}
+	mail_free(&mail);
 
 	/* everything expunged */
 	*stamp_r = 0;
@@ -106,7 +109,6 @@
 		update_dict = TRUE;
 	}
 
-	mail_free(&xt->mail);
 	if (xpr_box->module_ctx.super.
 	    	transaction_commit(t, uid_validity_r,
 				   first_saved_uid_r, last_saved_uid_r) < 0) {
@@ -114,27 +116,33 @@
 		return -1;
 	}
 
-	T_BEGIN {
+	if (xt->first_expunged || xt->saves) T_BEGIN {
 		const char *key, *value;
 
 		key = t_strconcat(DICT_PATH_SHARED, expire.username, "/",
 				  mailbox_name, NULL);
-		if (!xt->first_expunged) {
+		if (!xt->first_expunged && xt->saves) {
 			/* 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;
+			/* may not be exactly the first message's save time
+			   but a few second difference doesn't matter */
+			new_stamp = ioloop_time;
 		}
 
 		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));
+			if (new_stamp == 0) {
+				/* everything expunged */
+				dict_unset(dctx, key);
+			} else {
+				new_stamp += xpr_box->expire_secs;
+				dict_set(dctx, key, dec2str(new_stamp));
+			}
 			dict_transaction_commit(dctx);
 		}
 	} T_END;
@@ -148,8 +156,6 @@
 	struct expire_mailbox *xpr_box = EXPIRE_CONTEXT(t->box);
 	struct expire_transaction_context *xt = EXPIRE_CONTEXT(t);
 
-	mail_free(&xt->mail);
-
 	xpr_box->module_ctx.super.transaction_rollback(t);
 	i_free(xt);
 }
@@ -190,41 +196,14 @@
 	return _mail;
 }
 
-static void
-mail_set_save_time(struct mailbox_transaction_context *t, uint32_t seq)
+static int expire_save_finish(struct mail_save_context *ctx)
 {
-	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));
-}
+	struct expire_transaction_context *xt =
+		EXPIRE_CONTEXT(ctx->transaction);
+	struct expire_mailbox *xpr_box = EXPIRE_CONTEXT(ctx->transaction->box);
 
-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->module_ctx.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;
+	xt->saves = TRUE;
+	return xpr_box->module_ctx.super.save_finish(ctx);
 }
 
 static int
@@ -234,16 +213,10 @@
 {
 	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->module_ctx.super.
+	xt->saves = TRUE;
+	return xpr_box->module_ctx.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)
@@ -257,7 +230,7 @@
 	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.save_finish = expire_save_finish;
 	box->v.copy = expire_copy;
 
 	xpr_box->expire_secs = expire_secs;
@@ -272,15 +245,17 @@
 	union mail_storage_module_context *xpr_storage =
 		EXPIRE_CONTEXT(storage);
 	struct mailbox *box;
-	const struct expire_box *expire_box;
-	const char *full_name;
+	string_t *vname;
+	unsigned int secs;
 
 	box = xpr_storage->super.mailbox_open(storage, name, input, flags);
 	if (box != NULL) {
-		full_name = t_strconcat(storage->ns->prefix, name, NULL);
-		expire_box = expire_box_find(expire.env, full_name);
-		if (expire_box != NULL)
-			mailbox_expire_hook(box, expire_box->expire_secs);
+		vname = t_str_new(128);
+		(void)mail_namespace_get_vname(storage->ns, vname, name);
+
+		secs = expire_box_find_min_secs(expire.env, str_c(vname));
+		if (secs != 0)
+			mailbox_expire_hook(box, secs);
 	}
 	return box;
 }
@@ -302,15 +277,16 @@
 
 void expire_plugin_init(void)
 {
-	const char *env, *dict_uri;
+	const char *expunge_env, *altmove_env, *dict_uri;
 
-	env = getenv("EXPIRE");
-	if (env != NULL) {
+	expunge_env = getenv("EXPIRE");
+	altmove_env = getenv("EXPIRE_ALTMOVE");
+	if (expunge_env != NULL || altmove_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.env = expire_env_init(expunge_env, altmove_env);
 		expire.db = dict_init(dict_uri, DICT_DATA_TYPE_UINT32, NULL);
 		expire.username = getenv("USER");
 
--- a/src/plugins/expire/expire-tool.c	Mon Mar 03 08:59:10 2008 +0200
+++ b/src/plugins/expire/expire-tool.c	Tue Mar 04 03:47:57 2008 +0200
@@ -6,6 +6,7 @@
 #include "randgen.h"
 #include "lib-signals.h"
 #include "dict-client.h"
+#include "mail-index.h"
 #include "mail-search.h"
 #include "mail-storage.h"
 #include "mail-namespace.h"
@@ -26,6 +27,7 @@
 	char *user;
 	pool_t namespace_pool;
 	struct mail_namespace *ns;
+	bool testrun;
 };
 
 static int user_init(struct expire_context *ctx, const char *user)
@@ -54,7 +56,8 @@
 
 static int
 mailbox_delete_old_mails(struct expire_context *ctx, const char *user,
-			 const char *mailbox, time_t expire_secs,
+			 const char *mailbox,
+			 unsigned int expunge_secs, unsigned int altmove_secs,
 			 time_t *oldest_r)
 {
 	struct mail_namespace *ns;
@@ -63,7 +66,10 @@
 	struct mailbox_transaction_context *t;
 	struct mail_search_arg search_arg;
 	struct mail *mail;
+	const char *ns_mailbox;
 	time_t now, save_time;
+	enum mail_error error;
+	enum mail_flags flags;
 	int ret;
 
 	*oldest_r = 0;
@@ -71,8 +77,11 @@
 	if (ctx->user != NULL && strcmp(user, ctx->user) != 0)
 		user_deinit(ctx);
 	if (ctx->user == NULL) {
-		if ((ret = user_init(ctx, user)) <= 0)
+		if ((ret = user_init(ctx, user)) <= 0) {
+			if (ctx->testrun)
+				i_info("User lookup failed: %s", user);
 			return ret;
+		}
 		ctx->user = i_strdup(user);
 	}
 
@@ -80,11 +89,25 @@
 	search_arg.type = SEARCH_ALL;
 	search_arg.next = NULL;
 
-	ns = mail_namespace_find(ctx->ns, &mailbox);
-	if (ns == NULL)
-		return -1;
+	ns_mailbox = mailbox;
+	ns = mail_namespace_find(ctx->ns, &ns_mailbox);
+	if (ns == NULL) {
+		/* entire namespace no longer exists, remove the entry */
+		if (ctx->testrun)
+			i_info("Namespace lookup failed: %s", mailbox);
+		return 0;
+	}
 
-	box = mailbox_open(ns->storage, mailbox, NULL, 0);
+	box = mailbox_open(ns->storage, ns_mailbox, NULL, 0);
+	if (box == NULL) {
+		(void)mail_storage_get_last_error(ns->storage, &error);
+		if (error != MAIL_ERROR_NOTFOUND)
+			return -1;
+		
+		/* mailbox no longer exists, remove the entry */
+		return 0;
+	}
+
 	t = mailbox_transaction_begin(box, 0);
 	search_ctx = mailbox_search_init(t, NULL, &search_arg, NULL);
 	mail = mail_alloc(t, 0, NULL);
@@ -93,14 +116,36 @@
 	while ((ret = mailbox_search_next(search_ctx, mail)) > 0) {
 		if (mail_get_save_date(mail, &save_time) < 0) {
 			/* maybe just got expunged. anyway try again later. */
+			if (ctx->testrun) {
+				i_info("%s: seq=%u uid=%u: "
+				       "Save date lookup failed",
+				       mailbox, mail->seq, mail->uid);
+			}
 			ret = -1;
 			break;
 		}
 
-		if (save_time + expire_secs <= now)
-			mail_expunge(mail);
-		else {
-			/* first non-expunged one. */
+		if (save_time + expunge_secs <= now && expunge_secs != 0) {
+			if (!ctx->testrun)
+				mail_expunge(mail);
+			else {
+				i_info("%s: seq=%u uid=%u: Expunge",
+				       mailbox, mail->seq, mail->uid);
+			}
+		} else if (save_time + altmove_secs <= now && altmove_secs != 0) {
+			/* works only with dbox */
+			flags = mail_get_flags(mail);
+			if ((flags & MAIL_INDEX_MAIL_FLAG_BACKEND) != 0) {
+				/* alread moved */
+			} else if (!ctx->testrun) {
+				mail_update_flags(mail, MODIFY_ADD,
+						  MAIL_INDEX_MAIL_FLAG_BACKEND);
+			} else {
+				i_info("%s: seq=%u uid=%u: Move to alt dir",
+				       mailbox, mail->seq, mail->uid);
+			}
+		} else {
+			/* first non-expired one. */
 			*oldest_r = save_time;
 			break;
 		}
@@ -109,21 +154,29 @@
 
 	if (mailbox_search_deinit(&search_ctx) < 0)
 		ret = -1;
-	if (mailbox_transaction_commit(&t) < 0)
+	if (!ctx->testrun) {
+		if (mailbox_transaction_commit(&t) < 0)
+			ret = -1;
+	} else {
+		mailbox_transaction_rollback(&t);
+	}
+
+	if (mailbox_sync(box, MAILBOX_SYNC_FLAG_FAST, 0, NULL) < 0)
 		ret = -1;
+
 	mailbox_close(&box);
 	return ret < 0 ? -1 : 0;
 }
 
-static void expire_run(void)
+static void expire_run(bool testrun)
 {
 	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;
+	unsigned int expunge_secs, altmove_secs;
 	const char *auth_socket, *p, *key, *value;
 	const char *userp, *mailbox;
 	int ret;
@@ -133,8 +186,8 @@
 	mail_storage_register_all();
 	mailbox_list_register_all();
 
-	if (getenv("EXPIRE") == NULL)
-		i_fatal("expire setting not set");
+	if (getenv("EXPIRE") == NULL && getenv("EXPIRE_ALTMOVE") == NULL)
+		i_fatal("expire and expire_altmove settings not set");
 	if (getenv("EXPIRE_DICT") == NULL)
 		i_fatal("expire_dict setting not set");
 
@@ -143,9 +196,10 @@
 		auth_socket = DEFAULT_AUTH_SOCKET_PATH;
 
 	memset(&ctx, 0, sizeof(ctx));
+	ctx.testrun = testrun;
 	ctx.auth_conn = auth_connection_init(auth_socket);
 	ctx.namespace_pool = pool_alloconly_create("namespaces", 1024);
-	env = expire_env_init(getenv("EXPIRE"));
+	env = expire_env_init(getenv("EXPIRE"), getenv("EXPIRE_ALTMOVE"));
 	dict = dict_init(getenv("EXPIRE_DICT"), DICT_DATA_TYPE_UINT32, "");
 	trans = dict_transaction_begin(dict);
 	iter = dict_iterate_init(dict, DICT_PATH_SHARED,
@@ -164,43 +218,64 @@
 		}
 
 		mailbox = p + 1;
-		expire_box = expire_box_find(env, mailbox);
-		if (expire_box == NULL) {
+		if (!expire_box_find(env, mailbox,
+				     &expunge_secs, &altmove_secs)) {
 			/* we're no longer expunging old messages from here */
-			dict_unset(trans, key);
-		} else if (time(NULL) < (time_t)strtoul(value, NULL, 10)) {
+			if (!testrun)
+				dict_unset(trans, key);
+			else
+				i_info("%s: removed from config", mailbox);
+			continue;
+		}
+		if (time(NULL) < (time_t)strtoul(value, NULL, 10)) {
 			/* this and the rest of the timestamps are in future,
 			   so stop processing */
+			if (testrun) {
+				i_info("%s: stop, expire time in future: %s",
+				       mailbox, value);
+			}
 			break;
-		} else {
-			T_BEGIN {
-				const char *username;
+		}
+
+		T_BEGIN {
+			const char *username;
+
+			username = t_strdup_until(userp, p);
+			ret = mailbox_delete_old_mails(&ctx, username,
+						       mailbox, expunge_secs,
+						       altmove_secs, &oldest);
+		} T_END;
 
-				username = t_strdup_until(userp, p);
-				ret = mailbox_delete_old_mails(&ctx, username,
-						mailbox,
-						expire_box->expire_secs,
-						&oldest);
-			} T_END;
-			if (ret < 0) {
-				/* failed to update */
-			} else if (oldest == 0) {
-				/* no more messages or we're no longer
-				   expunging messages from here */
+		if (ret < 0) {
+			/* failed to update */
+		} else if (oldest == 0) {
+			/* no more messages or mailbox deleted */
+			if (!testrun)
 				dict_unset(trans, key);
-			} else {
-				char new_value[MAX_INT_STRLEN];
+			else
+				i_info("%s: no messages left", mailbox);
+		} else {
+			char new_value[MAX_INT_STRLEN];
 
-				oldest += expire_box->expire_secs;
-				i_snprintf(new_value, sizeof(new_value), "%lu",
-					   (unsigned long)oldest);
-				if (strcmp(value, new_value) != 0)
-					dict_set(trans, key, new_value);
+			oldest += altmove_secs != 0 ?
+				altmove_secs : expunge_secs;
+			i_snprintf(new_value, sizeof(new_value), "%lu",
+				   (unsigned long)oldest);
+			if (strcmp(value, new_value) == 0) {
+				/* no change */
+			} else if (!testrun)
+				dict_set(trans, key, new_value);
+			else {
+				i_info("%s: timestamp %s -> %s",
+				       mailbox, value, new_value);
 			}
 		}
 	}
 	dict_iterate_deinit(iter);
-	dict_transaction_commit(trans);
+	if (!testrun)
+		dict_transaction_commit(trans);
+	else
+		dict_transaction_rollback(trans);
 	dict_deinit(&dict);
 
 	if (ctx.user != NULL)
@@ -211,16 +286,19 @@
 	dict_driver_unregister(&dict_driver_client);
 }
 
-int main(void)
+int main(int argc, const char *argv[])
 {
 	struct ioloop *ioloop;
+	bool test;
 
 	lib_init();
 	lib_signals_init();
 	random_init();
 
+	test = argc > 1 && strcmp(argv[1], "--test") == 0;
+
 	ioloop = io_loop_create();
-	expire_run();
+	expire_run(test);
 	io_loop_destroy(&ioloop);
 
 	lib_signals_deinit();