changeset 1015:40a327d356de HEAD

Support for MULTIAPPEND extension. COPY now behaves like spec says - if it fails, none of the messages are copied. maildir_copy_with_hardlinks didn't actually work.
author Timo Sirainen <tss@iki.fi>
date Wed, 22 Jan 2003 21:23:28 +0200
parents 26a028e6d7d5
children d1caf5d0e77c
files configure.in doc/multiaccess.txt src/imap/client.c src/imap/cmd-append.c src/imap/cmd-copy.c src/imap/common.h src/lib-storage/index/index-copy.c src/lib-storage/index/index-fetch.c src/lib-storage/index/index-messageset.c src/lib-storage/index/index-messageset.h src/lib-storage/index/index-save.c src/lib-storage/index/index-storage.h src/lib-storage/index/index-update-flags.c src/lib-storage/index/maildir/maildir-copy.c src/lib-storage/index/maildir/maildir-save.c src/lib-storage/index/maildir/maildir-storage.c src/lib-storage/index/maildir/maildir-storage.h src/lib-storage/index/mbox/mbox-save.c src/lib-storage/index/mbox/mbox-storage.c src/lib-storage/index/mbox/mbox-storage.h src/lib-storage/mail-storage.h
diffstat 21 files changed, 597 insertions(+), 334 deletions(-) [+]
line wrap: on
line diff
--- a/configure.in	Wed Jan 22 20:41:29 2003 +0200
+++ b/configure.in	Wed Jan 22 21:23:28 2003 +0200
@@ -796,7 +796,7 @@
 dnl ** capabilities
 dnl **
 
-capability="IMAP4rev1 SORT THREAD=REFERENCES"
+capability="IMAP4rev1 SORT THREAD=REFERENCES MULTIAPPEND"
 AC_DEFINE_UNQUOTED(CAPABILITY_STRING, "$capability", IMAP capabilities)
 
 dnl **
--- a/doc/multiaccess.txt	Wed Jan 22 20:41:29 2003 +0200
+++ b/doc/multiaccess.txt	Wed Jan 22 21:23:28 2003 +0200
@@ -13,6 +13,4 @@
 
 SEARCH ignores expunged messages.
 
-COPY fails if any of the given messages were expunged. Messages copied
-until the failure is noticed are currently left to destination mailbox -
-this may change later.
+COPY fails if any of the given messages were expunged.
--- a/src/imap/client.c	Wed Jan 22 20:41:29 2003 +0200
+++ b/src/imap/client.c	Wed Jan 22 21:23:28 2003 +0200
@@ -12,10 +12,6 @@
 /* max. size of one parameter in line */
 #define MAX_INBUF_SIZE 8192
 
-/* max. number of IMAP argument elements to accept. The maximum memory usage
-   for command from user is around MAX_INBUF_SIZE * MAX_IMAP_ARG_ELEMENTS */
-#define MAX_IMAP_ARG_ELEMENTS 128
-
 /* If we can't send a buffer in a minute, disconnect the client */
 #define CLIENT_OUTPUT_TIMEOUT (60*1000)
 
@@ -164,6 +160,8 @@
 {
 	int ret;
 
+	i_assert(count <= INT_MAX);
+
 	ret = imap_parser_read_args(client->parser, count, flags, args);
 	if (ret >= (int)count) {
 		/* all parameters read successfully */
@@ -193,11 +191,15 @@
 	for (i = 0; i < count; i++) {
 		const char **ret = va_arg(va, const char **);
 
+		if (imap_args[i].type == IMAP_ARG_EOL) {
+			client_send_command_error(client, "Missing arguments.");
+			break;
+		}
+
 		str = imap_arg_string(&imap_args[i]);
 		if (str == NULL) {
-			client_send_command_error(client, "Missing arguments.");
-			va_end(va);
-			return FALSE;
+			client_send_command_error(client, "Invalid arguments.");
+			break;
 		}
 
 		if (ret != NULL)
@@ -205,7 +207,7 @@
 	}
 	va_end(va);
 
-	return TRUE;
+	return i == count;
 }
 
 static void client_reset_command(struct client *client)
@@ -219,12 +221,6 @@
         imap_parser_reset(client->parser);
 }
 
-static void client_command_finished(struct client *client)
-{
-	client->input_skip_line = TRUE;
-        client_reset_command(client);
-}
-
 /* Skip incoming data until newline is found,
    returns TRUE if newline was found. */
 static int client_skip_line(struct client *client)
@@ -249,9 +245,10 @@
 {
         if (client->cmd_func != NULL) {
 		/* command is being executed - continue it */
+		client->input_skip_line = TRUE;
 		if (client->cmd_func(client) || client->cmd_error) {
 			/* command execution was finished */
-			client_command_finished(client);
+			client_reset_command(client);
                         client->bad_counter = 0;
 			return TRUE;
 		}
@@ -292,11 +289,13 @@
 		/* unknown command */
 		client_send_command_error(client, t_strconcat(
 			"Unknown command '", client->cmd_name, "'", NULL));
-		client_command_finished(client);
+		client->input_skip_line = TRUE;
+		client_reset_command(client);
 	} else {
+		client->input_skip_line = TRUE;
 		if (client->cmd_func(client) || client->cmd_error) {
 			/* command execution was finished */
-			client_command_finished(client);
+			client_reset_command(client);
                         client->bad_counter = 0;
 		}
 	}
@@ -323,7 +322,7 @@
 		client->input_skip_line = TRUE;
 
 		client_send_command_error(client, "Too long argument.");
-		client_command_finished(client);
+		client_reset_command(client);
 		break;
 	}
 
--- a/src/imap/cmd-append.c	Wed Jan 22 20:41:29 2003 +0200
+++ b/src/imap/cmd-append.c	Wed Jan 22 21:23:28 2003 +0200
@@ -2,6 +2,7 @@
 
 #include "common.h"
 #include "ioloop.h"
+#include "istream.h"
 #include "ostream.h"
 #include "commands.h"
 #include "imap-parser.h"
@@ -12,107 +13,50 @@
 /* Returns -1 = error, 0 = need more data, 1 = successful. flags and
    internal_date may be NULL as a result, but mailbox and msg_size are always
    set when successful. */
-static int validate_args(struct client *client, const char **mailbox,
-			 struct imap_arg_list **flags,
-			 const char **internal_date,
-			 uoff_t *msg_size, unsigned int count)
+static int validate_args(struct imap_arg *args, struct imap_arg_list **flags,
+			 const char **internal_date, uoff_t *msg_size)
 {
-	struct imap_arg *args;
-
-	i_assert(count >= 2 && count <= 4);
-
-	*flags = NULL;
-	*internal_date = NULL;
-
-	if (!client_read_args(client, count, IMAP_PARSE_FLAG_LITERAL_SIZE,
-			      &args))
-		return 0;
-
-	switch (count) {
-	case 2:
-		/* do we have flags or internal date parameter? */
-		if (args[1].type == IMAP_ARG_LIST ||
-		    args[1].type == IMAP_ARG_STRING)
-			return validate_args(client, mailbox, flags,
-					     internal_date, msg_size, 3);
-
-		break;
-	case 3:
-		/* do we have both flags and internal date? */
-		if (args[1].type == IMAP_ARG_LIST &&
-		    args[2].type == IMAP_ARG_STRING)
-			return validate_args(client, mailbox, flags,
-					     internal_date, msg_size, 4);
-
-		if (args[1].type == IMAP_ARG_LIST)
-			*flags = IMAP_ARG_LIST(&args[1]);
-		else if (args[1].type == IMAP_ARG_STRING)
-			*internal_date = IMAP_ARG_STR(&args[1]);
-		else
-			return -1;
-		break;
-	case 4:
-		/* we have all parameters */
-		*flags = IMAP_ARG_LIST(&args[1]);
-		*internal_date = IMAP_ARG_STR(&args[2]);
-		break;
-	default:
-                i_unreached();
+	/* [<flags>] */
+	if (args->type != IMAP_ARG_LIST)
+		*flags = NULL;
+	else {
+		*flags = IMAP_ARG_LIST(args);
+		args++;
 	}
 
-	/* check that mailbox and message arguments are ok */
-	*mailbox = imap_arg_string(&args[0]);
-	if (*mailbox == NULL)
-		return -1;
+	/* [<internal date>] */
+	if (args->type != IMAP_ARG_STRING)
+		*internal_date = NULL;
+	else {
+		*internal_date = IMAP_ARG_STR(args);
+		args++;
+	}
 
-	if (args[count-1].type != IMAP_ARG_LITERAL_SIZE)
-		return -1;
+	if (args->type != IMAP_ARG_LITERAL_SIZE)
+		return FALSE;
 
-	*msg_size = IMAP_ARG_LITERAL_SIZE(&args[count-1]);
-	return 1;
+	*msg_size = IMAP_ARG_LITERAL_SIZE(args);
+	return TRUE;
 }
 
 int cmd_append(struct client *client)
 {
+	struct mailbox *box;
+	struct mail_save_context *ctx;
+	struct imap_parser *save_parser;
+	struct imap_arg *args;
 	struct imap_arg_list *flags_list;
-	struct mailbox *box;
 	struct mail_full_flags flags;
 	time_t internal_date;
 	const char *mailbox, *internal_date_str;
 	uoff_t msg_size;
-	int failed, timezone_offset;
-
-	/* <mailbox> [<flags>] [<internal date>] <message literal> */
-	switch (validate_args(client, &mailbox, &flags_list,
-			      &internal_date_str, &msg_size, 2)) {
-	case -1:
-		/* error */
-		client_send_command_error(client, NULL);
-		return TRUE;
-	case 0:
-		/* need more data */
-		return FALSE;
-	}
+	unsigned int count;
+	int ret, failed, timezone_offset;
 
-	if (flags_list != NULL) {
-		if (!client_parse_mail_flags(client, flags_list->args,
-					     &flags))
-			return TRUE;
-	} else {
-		memset(&flags, 0, sizeof(flags));
-	}
+	/* <mailbox> */
+	if (!client_read_string_args(client, 1, &mailbox))
+		return FALSE;
 
-	if (internal_date_str == NULL) {
-		/* no time given, default to now. */
-		internal_date = ioloop_time;
-                timezone_offset = ioloop_timezone.tz_minuteswest;
-	} else if (!imap_parse_datetime(internal_date_str, &internal_date,
-					&timezone_offset)) {
-		client_send_tagline(client, "BAD Invalid internal date.");
-		return TRUE;
-	}
-
-	/* open the mailbox */
 	if (!client_verify_mailbox_name(client, mailbox, TRUE, FALSE))
 		return TRUE;
 
@@ -123,17 +67,120 @@
 		return TRUE;
 	}
 
-	o_stream_send(client->output, "+ OK\r\n", 6);
-	o_stream_flush(client->output);
+	ctx = box->save_init(box, TRUE);
+	if (ctx == NULL) {
+		client_send_storage_error(client);
+		return TRUE;
+	}
+
+	/* if error occurs, the CRLF is already read. */
+	client->input_skip_line = FALSE;
+
+	count = 0;
+	failed = TRUE;
+	save_parser = imap_parser_create(client->input, client->output,
+					 0, MAX_IMAP_ARG_ELEMENTS);
+
+	for (;;) {
+		/* [<flags>] [<internal date>] <message literal> */
+		imap_parser_reset(save_parser);
+		for (;;) {
+			ret = imap_parser_read_args(save_parser, 0,
+						   IMAP_PARSE_FLAG_LITERAL_SIZE,
+						   &args);
+			if (ret >= 0)
+				break;
+			if (ret == -1) {
+				client_send_command_error(client,
+					imap_parser_get_error(save_parser));
+				break;
+			}
+
+			/* need more data */
+			ret = i_stream_read(client->input);
+			if (ret == -2) {
+				client_send_command_error(client,
+							  "Too long argument.");
+				break;
+			}
+			if (ret < 0) {
+				/* disconnected */
+				client->cmd_error = TRUE;
+				break;
+			}
+		}
+
+		if (client->cmd_error)
+			break;
+
+		if (args->type == IMAP_ARG_EOL) {
+			/* last one */
+			if (count > 0)
+				failed = FALSE;
+			client->input_skip_line = TRUE;
+			break;
+		}
 
-	/* save the mail */
-	failed = !box->save(box, &flags, internal_date, timezone_offset,
-			    client->input, msg_size);
+		if (!validate_args(args, &flags_list, &internal_date_str,
+				   &msg_size)) {
+			/* error */
+			client_send_command_error(client, "Invalid arguments.");
+			break;
+		}
+
+		if (flags_list != NULL) {
+			if (!client_parse_mail_flags(client, flags_list->args,
+						     &flags))
+				break;
+		} else {
+			memset(&flags, 0, sizeof(flags));
+		}
+
+		if (internal_date_str == NULL) {
+			/* no time given, default to now. */
+			internal_date = ioloop_time;
+			timezone_offset = ioloop_timezone.tz_minuteswest;
+		} else if (!imap_parse_datetime(internal_date_str,
+						&internal_date,
+						&timezone_offset)) {
+			client_send_tagline(client,
+					    "BAD Invalid internal date.");
+			break;
+		}
+
+		if (msg_size == 0) {
+			/* no message data, abort */
+			client_send_tagline(client, "NO Append aborted.");
+			break;
+		}
+
+		o_stream_send(client->output, "+ OK\r\n", 6);
+		o_stream_flush(client->output);
+
+		/* save the mail */
+		i_stream_set_read_limit(client->input,
+					client->input->v_offset + msg_size);
+		if (!box->save_next(ctx, &flags, internal_date,
+				    timezone_offset, client->input)) {
+			client_send_storage_error(client);
+			break;
+		}
+		i_stream_set_read_limit(client->input, 0);
+
+		if (client->input->closed)
+			break;
+
+		count++;
+	}
+
+	if (!box->save_deinit(ctx, failed)) {
+		failed = TRUE;
+		client_send_storage_error(client);
+	}
+
 	box->close(box);
 
-	if (failed) {
-		client_send_storage_error(client);
-	} else {
+	if (!failed) {
 		client_sync_full(client);
 		client_send_tagline(client, "OK Append completed.");
 	}
--- a/src/imap/cmd-copy.c	Wed Jan 22 20:41:29 2003 +0200
+++ b/src/imap/cmd-copy.c	Wed Jan 22 21:23:28 2003 +0200
@@ -7,6 +7,7 @@
 {
 	struct mailbox *destbox;
 	const char *messageset, *mailbox;
+	int ret;
 
 	/* <message set> <mailbox> */
 	if (!client_read_string_args(client, 2, &messageset, &mailbox))
@@ -27,11 +28,16 @@
 	}
 
 	/* copy the mail */
-	if (client->mailbox->copy(client->mailbox, destbox,
-				  messageset, client->cmd_uid)) {
-                client_sync_full(client);
+	ret = client->mailbox->copy(client->mailbox, destbox,
+				    messageset, client->cmd_uid);
+
+	/* sync always - if COPY fails because of expunges they'll get
+	   synced here */
+	client_sync_full(client);
+
+	if (ret)
 		client_send_tagline(client, "OK Copy completed.");
-	} else
+	else
 		client_send_storage_error(client);
 
 	destbox->close(destbox);
--- a/src/imap/common.h	Wed Jan 22 20:41:29 2003 +0200
+++ b/src/imap/common.h	Wed Jan 22 21:23:28 2003 +0200
@@ -4,6 +4,10 @@
 #include "lib.h"
 #include "client.h"
 
+/* max. number of IMAP argument elements to accept. The maximum memory usage
+   for command from user is around MAX_INBUF_SIZE * MAX_IMAP_ARG_ELEMENTS */
+#define MAX_IMAP_ARG_ELEMENTS 128
+
 extern struct ioloop *ioloop;
 
 #endif
--- a/src/lib-storage/index/index-copy.c	Wed Jan 22 20:41:29 2003 +0200
+++ b/src/lib-storage/index/index-copy.c	Wed Jan 22 21:23:28 2003 +0200
@@ -8,13 +8,15 @@
 
 #include <unistd.h>
 
-static int copy_messageset(struct messageset_context *ctx,
-			   struct index_mailbox *src, struct mailbox *dest)
+static int copy_messageset(struct messageset_context *msgset_ctx,
+                           struct mail_save_context *save_ctx,
+			   struct index_mailbox *src,
+			   struct mailbox *dest)
 {
         const struct messageset_mail *mail;
 	struct mail_full_flags flags;
 	struct istream *input;
-	time_t internal_date;
+	time_t received_date;
 	int failed, deleted;
 
 	memset(&flags, 0, sizeof(flags));
@@ -22,19 +24,15 @@
 		mail_custom_flags_list_get(src->index->custom_flags);
 	flags.custom_flags_count = MAIL_CUSTOM_FLAGS_COUNT;
 
-	while ((mail = index_messageset_next(ctx)) != NULL) {
+	while ((mail = index_messageset_next(msgset_ctx)) != NULL) {
 		input = src->index->open_mail(src->index, mail->rec,
-					      &internal_date, &deleted);
-		if (input == NULL) {
-			if (deleted)
-				continue;
+					      &received_date, &deleted);
+		if (input == NULL)
 			return FALSE;
-		}
 
-		/* save it in destination mailbox */
 		flags.flags = mail->rec->msg_flags;
-		failed = !dest->save(dest, &flags, internal_date, 0,
-				     input, input->v_limit);
+		failed = !dest->save_next(save_ctx, &flags, received_date,
+					  0, input);
 		i_stream_unref(input);
 
 		if (failed)
@@ -48,9 +46,9 @@
 		       const char *messageset, int uidset)
 {
 	struct index_mailbox *ibox = (struct index_mailbox *) box;
-        struct messageset_context *ctx;
-	enum mail_lock_type lock_type;
-	int ret, copy_inside_mailbox;
+	struct messageset_context *msgset_ctx;
+        struct mail_save_context *save_ctx;
+	int ret, ret2, copy_inside_mailbox;
 
 	if (destbox->readonly) {
 		mail_storage_set_error(box->storage,
@@ -63,31 +61,36 @@
 		strcmp(destbox->name, box->name) == 0;
 
 	if (copy_inside_mailbox) {
-		/* copying inside same mailbox */
 		if (!index_storage_lock(ibox, MAIL_LOCK_EXCLUSIVE))
 			return FALSE;
-
-		/* kludgy.. */
-		((struct index_mailbox *) destbox)->delay_save_unlocking = TRUE;
-
-		lock_type = MAIL_LOCK_EXCLUSIVE;
 	} else {
-		lock_type = MAIL_LOCK_SHARED;
+		if (!index_storage_sync_and_lock(ibox, TRUE, MAIL_LOCK_SHARED))
+			return FALSE;
 	}
 
-	if (!index_storage_sync_and_lock(ibox, TRUE, lock_type))
-		return FALSE;
+	save_ctx = destbox->save_init(destbox, TRUE);
+	if (save_ctx == NULL)
+		ret = FALSE;
+	else {
+		/* abort if any of the messages are expunged */
+		msgset_ctx = index_messageset_init(ibox, messageset, uidset,
+						   FALSE);
+		ret = copy_messageset(msgset_ctx, save_ctx, ibox, destbox);
+		ret2 = index_messageset_deinit(msgset_ctx);
+		if (ret2 < 0)
+			ret = FALSE;
+		else if (ret2 == 0) {
+			mail_storage_set_error(ibox->box.storage,
+			     "Some of the requested messages no longer exist.");
+			ret = FALSE;
+		}
 
-	ctx = index_messageset_init(ibox, messageset, uidset);
-	ret = copy_messageset(ctx, ibox, destbox);
-	if (index_messageset_deinit(ctx) < 0)
-		ret = FALSE;
-
-	if (copy_inside_mailbox)
-		((struct index_mailbox *) destbox)->delay_save_unlocking = TRUE;
+		if (!destbox->save_deinit(save_ctx, !ret))
+			ret = FALSE;
+	}
 
 	if (!index_storage_lock(ibox, MAIL_LOCK_UNLOCK))
-		return FALSE;
+		ret = FALSE;
 
 	return ret;
 }
--- a/src/lib-storage/index/index-fetch.c	Wed Jan 22 20:41:29 2003 +0200
+++ b/src/lib-storage/index/index-fetch.c	Wed Jan 22 21:23:28 2003 +0200
@@ -58,7 +58,7 @@
 	ctx->update_seen = *update_seen;
 
 	index_mail_init(ibox, &ctx->mail, wanted_fields, NULL);
-	ctx->msgset_ctx = index_messageset_init(ibox, messageset, uidset);
+	ctx->msgset_ctx = index_messageset_init(ibox, messageset, uidset, TRUE);
 	return ctx;
 }
 
--- a/src/lib-storage/index/index-messageset.c	Wed Jan 22 20:41:29 2003 +0200
+++ b/src/lib-storage/index/index-messageset.c	Wed Jan 22 21:23:28 2003 +0200
@@ -19,7 +19,7 @@
 	unsigned int num1, num2;
 
 	const char *messageset, *p;
-	int uidset;
+	int uidset, skip_expunged;
 
 	int first, ret;
 	const char *error;
@@ -30,7 +30,7 @@
 
 struct messageset_context *
 index_messageset_init(struct index_mailbox *ibox,
-		      const char *messageset, int uidset)
+		      const char *messageset, int uidset, int skip_expunged)
 {
 	struct messageset_context *ctx;
 
@@ -42,6 +42,7 @@
 	ctx->messages_count = ibox->synced_messages_count;
 	ctx->p = ctx->messageset = messageset;
 	ctx->uidset = uidset;
+	ctx->skip_expunged = skip_expunged;
 
 	/* Reset index errors, we rely on it to check for failures */
 	index_reset_error(ctx->index);
@@ -55,7 +56,7 @@
 {
 	struct messageset_context *ctx;
 
-	ctx = index_messageset_init(ibox, NULL, uidset);
+	ctx = index_messageset_init(ibox, NULL, uidset, TRUE);
 	if (num1 <= num2) {
 		ctx->num1 = num1;
 		ctx->num2 = num2;
@@ -283,6 +284,13 @@
 				ctx->ret = uidset_init(ctx);
 			else
 				ctx->ret = seqset_init(ctx);
+
+			if (ctx->expunges_found && !ctx->skip_expunged) {
+				/* we wish to abort if there's any
+				   expunged messages */
+				ctx->ret = 1;
+				return NULL;
+			}
 		} while (ctx->ret == 1);
 
 		if (ctx->ret != 0)
--- a/src/lib-storage/index/index-messageset.h	Wed Jan 22 20:41:29 2003 +0200
+++ b/src/lib-storage/index/index-messageset.h	Wed Jan 22 21:23:28 2003 +0200
@@ -13,13 +13,13 @@
 
 struct messageset_context *
 index_messageset_init(struct index_mailbox *ibox,
-		      const char *messageset, int uidset);
+		      const char *messageset, int uidset, int skip_expunged);
 
 struct messageset_context *
 index_messageset_init_range(struct index_mailbox *ibox,
 			    unsigned int num1, unsigned int num2, int uidset);
 
-/* Returns 1 if all were found, 0 if some messages were deleted,
+/* Returns 1 if all were found, 0 if some messages were expunged,
    -1 if internal error occured or -2 if messageset was invalid. */
 int index_messageset_deinit(struct messageset_context *ctx);
 
--- a/src/lib-storage/index/index-save.c	Wed Jan 22 20:41:29 2003 +0200
+++ b/src/lib-storage/index/index-save.c	Wed Jan 22 21:23:28 2003 +0200
@@ -71,8 +71,7 @@
 }
 
 int index_storage_save(struct mail_storage *storage, const char *path,
-		       struct istream *input, struct ostream *output,
-		       uoff_t data_size)
+		       struct istream *input, struct ostream *output)
 {
 	int (*write_func)(struct ostream *, const unsigned char *, size_t);
 	const unsigned char *data;
@@ -83,13 +82,13 @@
 	write_func = getenv("MAIL_SAVE_CRLF") ? write_with_crlf : write_with_lf;
 
 	failed = FALSE;
-	while (data_size > 0) {
+	for (;;) {
 		ret = i_stream_read(input);
 		if (ret < 0) {
 			errno = input->stream_errno;
 			if (errno == 0) {
-				mail_storage_set_error(storage,
-					"Client disconnected");
+				/* EOF */
+				break;
 			} else if (errno == EAGAIN) {
 				mail_storage_set_error(storage,
 					"Timeout while waiting for input");
@@ -97,13 +96,11 @@
 				mail_storage_set_critical(storage,
 					"Error reading mail from client: %m");
 			}
-			return FALSE;
+			failed = TRUE;
+			break;
 		}
 
 		data = i_stream_get_data(input, &size);
-		if (size > data_size)
-			size = (size_t)data_size;
-
 		if (!failed) {
 			ret = write_func(output, data, size);
 			if (ret < 0) {
@@ -122,7 +119,6 @@
 			}
 		}
 
-		data_size -= size;
 		i_stream_skip(input, size);
 	}
 
--- a/src/lib-storage/index/index-storage.h	Wed Jan 22 20:41:29 2003 +0200
+++ b/src/lib-storage/index/index-storage.h	Wed Jan 22 21:23:28 2003 +0200
@@ -25,7 +25,6 @@
 	time_t next_lock_notify; /* temporary */
 
 	unsigned int sent_diskspace_warning:1;
-	unsigned int delay_save_unlocking:1; /* For COPYing inside mailbox */
 };
 
 int mail_storage_set_index_error(struct index_mailbox *ibox);
@@ -61,8 +60,7 @@
 		       unsigned int seq, int notify);
 
 int index_storage_save(struct mail_storage *storage, const char *path,
-		       struct istream *input, struct ostream *output,
-		       uoff_t data_size);
+		       struct istream *input, struct ostream *output);
 
 void index_mailbox_check_add(struct index_mailbox *ibox, const char *path);
 void index_mailbox_check_remove(struct index_mailbox *ibox);
--- a/src/lib-storage/index/index-update-flags.c	Wed Jan 22 20:41:29 2003 +0200
+++ b/src/lib-storage/index/index-update-flags.c	Wed Jan 22 21:23:28 2003 +0200
@@ -85,7 +85,7 @@
 
 	mail_flags &= ~MAIL_RECENT; /* \Recent can't be changed */
 
-	ctx = index_messageset_init(ibox, messageset, uidset);
+	ctx = index_messageset_init(ibox, messageset, uidset, TRUE);
 	ret = update_messageset(ctx, ibox, mail_flags, modify_type, notify);
 	ret2 = index_messageset_deinit(ctx);
 
--- a/src/lib-storage/index/maildir/maildir-copy.c	Wed Jan 22 20:41:29 2003 +0200
+++ b/src/lib-storage/index/maildir/maildir-copy.c	Wed Jan 22 21:23:28 2003 +0200
@@ -10,24 +10,38 @@
 #include <stdlib.h>
 #include <unistd.h>
 
+struct rollback {
+	struct rollback *next;
+	const char *fname;
+};
+
 static int hardlink_messageset(struct messageset_context *ctx,
 			       struct index_mailbox *src,
 			       struct index_mailbox *dest)
 {
-        struct mail_index *index = src->index;
+	struct mail_index *index = src->index;
+	pool_t pool;
+        struct rollback *rollbacks, *rb;
         const struct messageset_mail *mail;
 	enum mail_flags flags;
 	const char **custom_flags;
-	const char *fname, *src_fname, *dest_fname;
+	const char *fname, *src_path, *dest_fname, *dest_path;
+	int ret;
+
+	pool = pool_alloconly_create("hard copy rollbacks", 2048);
+	rollbacks = NULL;
 
 	custom_flags = mail_custom_flags_list_get(index->custom_flags);
 
+	ret = 1;
 	while ((mail = index_messageset_next(ctx)) != NULL) {
 		flags = mail->rec->msg_flags;
 		if (!index_mailbox_fix_custom_flags(dest, &flags,
 						    custom_flags,
-						    MAIL_CUSTOM_FLAGS_COUNT))
-			return -1;
+						    MAIL_CUSTOM_FLAGS_COUNT)) {
+			ret = -1;
+			break;
+		}
 
 		/* link the file */
 		fname = index->lookup_field(index, mail->rec,
@@ -36,31 +50,50 @@
 			index_set_corrupted(index,
 				"Missing location field for record %u",
 				mail->rec->uid);
-			return -1;
+			ret = -1;
+			break;
 		}
 
 		t_push();
-		src_fname = t_strconcat(index->mailbox_path, "cur/",
-					fname, NULL);
-		dest_fname = t_strconcat(dest->index->mailbox_path, "new/",
-                	maildir_filename_set_flags(
+		src_path = t_strconcat(index->mailbox_path, "/cur/",
+				       fname, NULL);
+		dest_fname = t_strconcat(maildir_filename_set_flags(
 				maildir_generate_tmp_filename(), flags), NULL);
+		dest_path = t_strconcat(dest->index->mailbox_path, "/new/",
+					dest_fname, NULL);
 
-		if (link(src_fname, dest_fname) < 0) {
+		if (link(src_path, dest_path) == 0) {
+			rb = p_new(pool, struct rollback, 1);
+			rb->fname = p_strdup(pool, dest_fname);
+			rb->next = rollbacks;
+			rollbacks = rb;
+		} else {
 			if (errno != EXDEV) {
 				mail_storage_set_critical(src->box.storage,
 					"link(%s, %s) failed: %m",
-					src_fname, dest_fname);
+					src_path, dest_path);
 				t_pop();
-				return -1;
+				ret = -1;
+				break;
 			}
 			t_pop();
-			return 0;
+			ret = 0;
+			break;
 		}
 		t_pop();
 	}
 
-	return 1;
+	if (ret <= 0) {
+		for (rb = rollbacks; rb != NULL; rb = rb->next) {
+			t_push();
+			(void)unlink(t_strconcat(dest->index->mailbox_path,
+						 "new/", rb->fname, NULL));
+			t_pop();
+		}
+	}
+
+	pool_unref(pool);
+	return ret;
 }
 
 static int copy_with_hardlinks(struct index_mailbox *src,
@@ -68,15 +101,21 @@
 			       const char *messageset, int uidset)
 {
         struct messageset_context *ctx;
-	int ret;
+	int ret, ret2;
 
 	if (!index_storage_sync_and_lock(src, TRUE, MAIL_LOCK_SHARED))
 		return -1;
 
-	ctx = index_messageset_init(src, messageset, uidset);
+	ctx = index_messageset_init(src, messageset, uidset, FALSE);
 	ret = hardlink_messageset(ctx, src, dest);
-	if (index_messageset_deinit(ctx) < 0)
+	ret2 = index_messageset_deinit(ctx);
+	if (ret2 < 0)
 		ret = -1;
+	else {
+		mail_storage_set_error(src->box.storage,
+			"Some of the requested messages no longer exist.");
+		ret = -1;
+	}
 
 	(void)index_storage_lock(src, MAIL_LOCK_UNLOCK);
 
--- a/src/lib-storage/index/maildir/maildir-save.c	Wed Jan 22 20:41:29 2003 +0200
+++ b/src/lib-storage/index/maildir/maildir-save.c	Wed Jan 22 21:23:28 2003 +0200
@@ -12,6 +12,21 @@
 #include <fcntl.h>
 #include <utime.h>
 
+struct mail_filename {
+	struct mail_filename *next;
+	const char *src, *dest;
+};
+
+struct mail_save_context {
+	pool_t pool;
+
+	struct index_mailbox *ibox;
+	int transaction;
+
+	const char *tmpdir, *newdir;
+	struct mail_filename *files;
+};
+
 const char *maildir_generate_tmp_filename(void)
 {
 	static unsigned int create_count = 0;
@@ -49,7 +64,7 @@
 
 static const char *
 maildir_read_into_tmp(struct mail_storage *storage, const char *dir,
-		      struct istream *input, uoff_t data_size)
+		      struct istream *input)
 {
 	const char *fname, *path;
 	struct ostream *output;
@@ -65,7 +80,7 @@
 	o_stream_set_blocking(output, 60000, NULL, NULL);
 
 	path = t_strconcat(dir, "/", fname, NULL);
-	if (!index_storage_save(storage, path, input, output, data_size))
+	if (!index_storage_save(storage, path, input, output))
 		fname = NULL;
 
 	o_stream_unref(output);
@@ -78,25 +93,48 @@
 	return fname;
 }
 
-int maildir_storage_save(struct mailbox *box,
-			 const struct mail_full_flags *flags,
-			 time_t internal_date,
-			 int timezone_offset __attr_unused__,
-			 struct istream *data, uoff_t data_size)
+static int maildir_copy(struct mail_save_context *ctx,
+			const char *src, const char *dest)
 {
-        struct index_mailbox *ibox = (struct index_mailbox *) box;
+	const char *tmp_path, *new_path;
+
+	t_push();
+
+	tmp_path = t_strconcat(ctx->tmpdir, "/", src, NULL);
+	new_path = t_strconcat(ctx->newdir, "/", dest, NULL);
+
+	if (rename(tmp_path, new_path) == 0) {
+		t_pop();
+		return TRUE;
+	}
+
+	if (errno == ENOSPC) {
+		mail_storage_set_error(ctx->ibox->box.storage,
+				       "Not enough disk space");
+	} else {
+		mail_storage_set_critical(ctx->ibox->box.storage,
+					  "rename(%s, %s) failed: %m",
+					  tmp_path, new_path);
+	}
+
+	(void)unlink(tmp_path);
+	t_pop();
+	return FALSE;
+}
+
+int maildir_storage_save_next(struct mail_save_context *ctx,
+			      const struct mail_full_flags *flags,
+			      time_t received_date,
+			      int timezone_offset __attr_unused__,
+			      struct istream *data)
+{
 	enum mail_flags mail_flags;
         struct utimbuf buf;
-	const char *tmpdir, *fname, *tmp_path, *new_path;
+	const char *fname, *dest_fname, *tmp_path;
 	int failed;
 
-	if (box->readonly) {
-		mail_storage_set_error(box->storage, "Mailbox is read-only");
-		return FALSE;
-	}
-
 	mail_flags = flags->flags;
-	if (!index_mailbox_fix_custom_flags(ibox, &mail_flags,
+	if (!index_mailbox_fix_custom_flags(ctx->ibox, &mail_flags,
 					    flags->custom_flags,
 					    flags->custom_flags_count))
 		return FALSE;
@@ -104,44 +142,100 @@
 	t_push();
 
 	/* create the file into tmp/ directory */
-	tmpdir = t_strconcat(ibox->index->mailbox_path, "/tmp", NULL);
-	fname = maildir_read_into_tmp(box->storage, tmpdir, data, data_size);
+	fname = maildir_read_into_tmp(ctx->ibox->box.storage,
+				      ctx->tmpdir, data);
 	if (fname == NULL) {
 		t_pop();
 		return FALSE;
 	}
-	tmp_path = t_strconcat(tmpdir, "/", fname, NULL);
 
-	fname = maildir_filename_set_flags(fname, mail_flags);
-	new_path = t_strconcat(ibox->index->mailbox_path, "/new/", fname, NULL);
+	tmp_path = t_strconcat(ctx->tmpdir, "/", fname, NULL);
 
-	/* set the internal_date by modifying mtime */
+	/* set the received_date by modifying mtime */
 	buf.actime = ioloop_time;
-	buf.modtime = internal_date;
+	buf.modtime = received_date;
 	if (utime(tmp_path, &buf) < 0) {
-		/* just warn, don't bother actually failing */
-		mail_storage_set_critical(box->storage, "utime() failed for "
-					  "%s: %m", tmp_path);
+		mail_storage_set_critical(ctx->ibox->box.storage,
+					  "utime() failed for %s: %m",
+					  tmp_path);
+		t_pop();
+		return FALSE;
 	}
 
-	/* move the file into new/ directory - syncing will pick it
-	   up from there */
-	if (rename(tmp_path, new_path) == 0)
+	/* now, if we want to be able to rollback the whole append session,
+	   we'll just store the name of this temp file and move it later
+	   into new/ */
+	dest_fname = maildir_filename_set_flags(fname, mail_flags);
+	if (ctx->transaction) {
+		struct mail_filename *mf;
+
+		mf = p_new(ctx->pool, struct mail_filename, 1);
+		mf->next = ctx->files;
+		mf->src = p_strdup(ctx->pool, fname);
+		mf->dest = p_strdup(ctx->pool, dest_fname);
+		ctx->files = mf;
+
 		failed = FALSE;
-	else {
-		if (errno == ENOSPC) {
-			mail_storage_set_error(box->storage,
-					       "Not enough disk space");
-		} else {
-			mail_storage_set_critical(box->storage,
-						  "rename(%s, %s) failed: %m",
-						  tmp_path, new_path);
-		}
-
-		(void)unlink(tmp_path);
-		failed = TRUE;
+	} else {
+		failed = !maildir_copy(ctx, fname, dest_fname);
 	}
 
 	t_pop();
 	return !failed;
 }
+
+struct mail_save_context *
+maildir_storage_save_init(struct mailbox *box, int transaction)
+{
+	struct index_mailbox *ibox = (struct index_mailbox *) box;
+	struct mail_save_context *ctx;
+	pool_t pool;
+
+	if (box->readonly) {
+		mail_storage_set_error(box->storage, "Mailbox is read-only");
+		return NULL;
+	}
+
+	pool = pool_alloconly_create("mail_save_context", 2048);
+	ctx = p_new(pool, struct mail_save_context, 1);
+	ctx->pool = pool;
+	ctx->ibox = ibox;
+	ctx->transaction = transaction;
+
+	ctx->tmpdir = p_strconcat(pool, ibox->index->mailbox_path,
+				  "/tmp", NULL);
+	ctx->newdir = p_strconcat(pool, ibox->index->mailbox_path,
+				  "/new", NULL);
+
+	return ctx;
+}
+
+int maildir_storage_save_deinit(struct mail_save_context *ctx, int rollback)
+{
+	struct mail_filename *mf, *mf2;
+	const char *new_path;
+	int failed = FALSE;
+
+	if (!rollback) {
+		for (mf = ctx->files; mf != NULL; mf = mf->next) {
+			if (!maildir_copy(ctx, mf->src, mf->dest)) {
+				failed = TRUE;
+				break;
+			}
+		}
+
+		if (failed) {
+			/* failed, try to unlink the mails already moved */
+			for (mf2 = ctx->files; mf2 != mf; mf2 = mf2->next) {
+				t_push();
+				new_path = t_strconcat(ctx->newdir, "/",
+						       mf2->dest, NULL);
+				(void)unlink(new_path);
+				t_pop();
+			}
+		}
+	}
+
+	pool_unref(ctx->pool);
+	return !failed;
+}
--- a/src/lib-storage/index/maildir/maildir-storage.c	Wed Jan 22 20:41:29 2003 +0200
+++ b/src/lib-storage/index/maildir/maildir-storage.c	Wed Jan 22 21:23:28 2003 +0200
@@ -569,7 +569,9 @@
 	index_storage_search_init,
 	index_storage_search_deinit,
 	index_storage_search_next,
-	maildir_storage_save,
+	maildir_storage_save_init,
+	maildir_storage_save_deinit,
+	maildir_storage_save_next,
 	mail_storage_is_inconsistency_error,
 
 	FALSE,
--- a/src/lib-storage/index/maildir/maildir-storage.h	Wed Jan 22 20:41:29 2003 +0200
+++ b/src/lib-storage/index/maildir/maildir-storage.h	Wed Jan 22 21:23:28 2003 +0200
@@ -5,10 +5,14 @@
 
 int maildir_storage_copy(struct mailbox *box, struct mailbox *destbox,
 			 const char *messageset, int uidset);
-int maildir_storage_save(struct mailbox *box,
-			 const struct mail_full_flags *flags,
-			 time_t internal_date, int timezone_offset,
-			 struct istream *data, uoff_t data_size);
+
+struct mail_save_context *
+maildir_storage_save_init(struct mailbox *box, int transaction);
+int maildir_storage_save_deinit(struct mail_save_context *ctx, int rollback);
+int maildir_storage_save_next(struct mail_save_context *ctx,
+			      const struct mail_full_flags *flags,
+			      time_t received_date, int timezone_offset,
+			      struct istream *data);
 
 int maildir_find_mailboxes(struct mail_storage *storage, const char *mask,
 			   mailbox_list_callback_t callback, void *context);
--- a/src/lib-storage/index/mbox/mbox-save.c	Wed Jan 22 20:41:29 2003 +0200
+++ b/src/lib-storage/index/mbox/mbox-save.c	Wed Jan 22 21:23:28 2003 +0200
@@ -14,68 +14,82 @@
 #include <sys/stat.h>
 #include <netdb.h>
 
+struct mail_save_context {
+	pool_t pool;
+
+	struct index_mailbox *ibox;
+	int transaction;
+
+	struct ostream *output;
+	uoff_t sync_offset;
+};
+
 static char my_hostdomain[256] = "";
 
-static int write_error(struct mail_storage *storage, const char *mbox_path)
+static int write_error(struct mail_save_context *ctx)
 {
-	if (errno == ENOSPC)
-		mail_storage_set_error(storage, "Not enough disk space");
-	else {
-		mail_storage_set_critical(storage,
-			"Error writing to mbox file %s: %m", mbox_path);
+	if (errno == ENOSPC) {
+		mail_storage_set_error(ctx->ibox->box.storage,
+				       "Not enough disk space");
+	} else {
+		mail_storage_set_critical(ctx->ibox->box.storage,
+			"Error writing to mbox file %s: %m",
+			ctx->ibox->index->mailbox_path);
 	}
 
 	return FALSE;
 }
 
-static int mbox_seek_to_end(struct mail_storage *storage, int fd,
-			    const char *mbox_path, off_t *pos)
+static int mbox_seek_to_end(struct mail_save_context *ctx, off_t *offset)
 {
 	struct stat st;
 	char ch;
+	int fd;
 
+	fd = ctx->ibox->index->mbox_fd;
 	if (fstat(fd, &st) < 0) {
-		mail_storage_set_critical(storage,
-			"fstat() failed for mbox file %s: %m", mbox_path);
+		mail_storage_set_critical(ctx->ibox->box.storage,
+					  "fstat() failed for mbox file %s: %m",
+					  ctx->ibox->index->mailbox_path);
 		return FALSE;
 	}
 
-	*pos = st.st_size;
+	*offset = st.st_size;
 	if (st.st_size == 0)
 		return TRUE;
 
 	if (lseek(fd, st.st_size-1, SEEK_SET) < 0) {
-		mail_storage_set_critical(storage,
-			"lseek() failed for mbox file %s: %m", mbox_path);
+		mail_storage_set_critical(ctx->ibox->box.storage,
+					  "lseek() failed for mbox file %s: %m",
+					 ctx->ibox->index->mailbox_path);
 		return FALSE;
 	}
 
 	if (read(fd, &ch, 1) != 1) {
-		mail_storage_set_critical(storage,
-			"read() failed for mbox file %s: %m", mbox_path);
+		mail_storage_set_critical(ctx->ibox->box.storage,
+					  "read() failed for mbox file %s: %m",
+					  ctx->ibox->index->mailbox_path);
 		return FALSE;
 	}
 
 	if (ch != '\n') {
 		if (write_full(fd, "\n", 1) < 0)
-			return write_error(storage, mbox_path);
-		*pos += 1;
+			return write_error(ctx);
+		*offset += 1;
 	}
 
 	return TRUE;
 }
 
-static int mbox_append_lf(struct mail_storage *storage, struct ostream *output,
-			  const char *mbox_path)
+static int mbox_append_lf(struct mail_save_context *ctx)
 {
-	if (o_stream_send(output, "\n", 1) < 0)
-		return write_error(storage, mbox_path);
+	if (o_stream_send(ctx->output, "\n", 1) < 0)
+		return write_error(ctx);
 
 	return TRUE;
 }
 
-static int write_from_line(struct mail_storage *storage, struct ostream *output,
-			   const char *mbox_path, time_t internal_date)
+static int write_from_line(struct mail_save_context *ctx, time_t received_date)
 {
 	const char *sender, *line, *name;
 
@@ -94,19 +108,19 @@
 		strocpy(my_hostdomain, name, sizeof(my_hostdomain));
 	}
 
-	sender = t_strconcat(storage->user, "@", my_hostdomain, NULL);
+	sender = t_strconcat(ctx->ibox->box.storage->user, "@",
+			     my_hostdomain, NULL);
 
 	/* save in local timezone, no matter what it was given with */
-	line = mbox_from_create(sender, internal_date);
+	line = mbox_from_create(sender, received_date);
 
-	if (o_stream_send_str(output, line) < 0)
-		return write_error(storage, mbox_path);
+	if (o_stream_send_str(ctx->output, line) < 0)
+		return write_error(ctx);
 
 	return TRUE;
 }
 
-static int write_flags(struct mail_storage *storage, struct ostream *output,
-		       const char *mbox_path,
+static int write_flags(struct mail_save_context *ctx,
 		       const struct mail_full_flags *full_flags)
 {
 	enum mail_flags flags = full_flags->flags;
@@ -118,8 +132,8 @@
 		return TRUE;
 
 	if (flags & MAIL_SEEN) {
-		if (o_stream_send_str(output, "Status: R\n") < 0)
-			return write_error(storage, mbox_path);
+		if (o_stream_send_str(ctx->output, "Status: R\n") < 0)
+			return write_error(ctx);
 	}
 
 	if (flags & (MAIL_ANSWERED|MAIL_DRAFT|MAIL_FLAGGED|MAIL_DELETED)) {
@@ -130,100 +144,134 @@
 				  (flags & MAIL_DELETED) ? "T" : "",
 				  "\n", NULL);
 
-		if (o_stream_send_str(output, str) < 0)
-			return write_error(storage, mbox_path);
+		if (o_stream_send_str(ctx->output, str) < 0)
+			return write_error(ctx);
 	}
 
 	if (flags & MAIL_CUSTOM_FLAGS_MASK) {
-		if (o_stream_send_str(output, "X-Keywords:") < 0)
-			return write_error(storage, mbox_path);
+		if (o_stream_send_str(ctx->output, "X-Keywords:") < 0)
+			return write_error(ctx);
 
 		field = 1 << MAIL_CUSTOM_FLAG_1_BIT;
 		for (i = 0; i < full_flags->custom_flags_count; i++) {
 			const char *custom_flag = full_flags->custom_flags[i];
 
 			if ((flags & field) && custom_flag != NULL) {
-				if (o_stream_send(output, " ", 1) < 0)
-					return write_error(storage, mbox_path);
+				if (o_stream_send(ctx->output, " ", 1) < 0)
+					return write_error(ctx);
 
-				if (o_stream_send_str(output, custom_flag) < 0)
-					return write_error(storage, mbox_path);
+				if (o_stream_send_str(ctx->output,
+						      custom_flag) < 0)
+					return write_error(ctx);
 			}
 
                         field <<= 1;
 		}
 
-		if (o_stream_send(output, "\n", 1) < 0)
-			return write_error(storage, mbox_path);
+		if (o_stream_send(ctx->output, "\n", 1) < 0)
+			return write_error(ctx);
 	}
 
 	return TRUE;
 }
 
-int mbox_storage_save(struct mailbox *box, const struct mail_full_flags *flags,
-		      time_t internal_date,
-		      int timezone_offset __attr_unused__,
-		      struct istream *data, uoff_t data_size)
+int mbox_storage_save_next(struct mail_save_context *ctx,
+			   const struct mail_full_flags *flags,
+			   time_t received_date,
+			   int timezone_offset __attr_unused__,
+			   struct istream *data)
 {
-	struct index_mailbox *ibox = (struct index_mailbox *) box;
-	struct mail_index *index;
 	enum mail_flags real_flags;
-	const char *mbox_path;
-	struct ostream *output;
 	int failed;
-	off_t pos;
-
-	if (box->readonly) {
-		mail_storage_set_error(box->storage, "Mailbox is read-only");
-		return FALSE;
-	}
 
 	/* we don't need the real flag positions, easier to keep using our own.
 	   they need to be checked/added though. */
 	real_flags = flags->flags;
-	if (!index_mailbox_fix_custom_flags(ibox, &real_flags,
+	if (!index_mailbox_fix_custom_flags(ctx->ibox, &real_flags,
 					    flags->custom_flags,
 					    flags->custom_flags_count))
 		return FALSE;
 
-	if (!index_storage_sync_and_lock(ibox, FALSE, MAIL_LOCK_EXCLUSIVE))
-		return FALSE;
-
-	index = ibox->index;
-	mbox_path = index->mailbox_path;
-	if (!mbox_seek_to_end(box->storage, index->mbox_fd, mbox_path, &pos))
+	t_push();
+	if (!write_from_line(ctx, received_date) ||
+	    !write_flags(ctx, flags) ||
+	    !index_storage_save(ctx->ibox->box.storage,
+				ctx->ibox->index->mailbox_path,
+				data, ctx->output) ||
+	    !mbox_append_lf(ctx)) {
+		/* failed, truncate file back to original size.
+		   output stream needs to be flushed before truncating
+		   so unref() won't write anything. */
+		o_stream_flush(ctx->output);
+		if (ctx->sync_offset != (uoff_t)-1) {
+			(void)ftruncate(ctx->ibox->index->mbox_fd,
+					ctx->sync_offset);
+			ctx->sync_offset = (uoff_t)-1;
+		}
 		failed = TRUE;
-	else {
+	} else {
+		if (!ctx->transaction)
+			ctx->sync_offset = ctx->output->offset;
 		failed = FALSE;
-
-		t_push();
-		output = o_stream_create_file(index->mbox_fd,
-					      data_stack_pool, 4096,
-					      0, FALSE);
-		o_stream_set_blocking(output, 60000, NULL, NULL);
-
-		if (!write_from_line(box->storage, output, mbox_path,
-				     internal_date) ||
-		    !write_flags(box->storage, output, mbox_path, flags) ||
-		    !index_storage_save(box->storage, mbox_path,
-					data, output, data_size) ||
-		    !mbox_append_lf(box->storage, output, mbox_path)) {
-			/* failed, truncate file back to original size.
-			   output stream needs to be flushed before truncating
-			   so unref() won't write anything. */
-			o_stream_flush(output);
-			(void)ftruncate(index->mbox_fd, pos);
-			failed = TRUE;
-		}
-		o_stream_unref(output);
-		t_pop();
 	}
-
-	/* kludgy.. for copying inside same mailbox. */
-	if (!ibox->delay_save_unlocking) {
-		if (!index_storage_lock(ibox, MAIL_LOCK_UNLOCK))
-			return FALSE;
-	}
+	t_pop();
 
 	return !failed;
 }
+
+struct mail_save_context *
+mbox_storage_save_init(struct mailbox *box, int transaction)
+{
+	struct index_mailbox *ibox = (struct index_mailbox *) box;
+	struct mail_save_context *ctx;
+	pool_t pool;
+
+	if (box->readonly) {
+		mail_storage_set_error(box->storage, "Mailbox is read-only");
+		return NULL;
+	}
+
+	if (!index_storage_sync_and_lock(ibox, FALSE, MAIL_LOCK_EXCLUSIVE))
+		return NULL;
+
+	pool = pool_alloconly_create("mail_save_context", 2048);
+	ctx = p_new(pool, struct mail_save_context, 1);
+	ctx->pool = pool;
+	ctx->ibox = ibox;
+	ctx->transaction = transaction;
+
+	if (!mbox_seek_to_end(ctx, &ctx->sync_offset)) {
+		pool_unref(pool);
+		return NULL;
+	}
+
+	ctx->output = o_stream_create_file(ibox->index->mbox_fd,
+					   ctx->pool, 4096, 0, FALSE);
+	o_stream_set_blocking(ctx->output, 60000, NULL, NULL);
+	return ctx;
+}
+
+int mbox_storage_save_deinit(struct mail_save_context *ctx, int rollback)
+{
+	int failed = FALSE;
+
+	if (!index_storage_lock(ctx->ibox, MAIL_LOCK_UNLOCK))
+		failed = TRUE;
+
+	if (o_stream_flush(ctx->output) < 0)
+		failed = TRUE;
+	o_stream_unref(ctx->output);
+
+	if (rollback && ctx->sync_offset != (uoff_t)-1) {
+		if (ftruncate(ctx->ibox->index->mbox_fd,
+			      ctx->sync_offset) < 0) {
+			mail_storage_set_critical(ctx->ibox->box.storage,
+				"ftruncate(%s) failed: %m",
+				ctx->ibox->index->mailbox_path);
+			failed = TRUE;
+		}
+	}
+
+	pool_unref(ctx->pool);
+	return !failed;
+}
--- a/src/lib-storage/index/mbox/mbox-storage.c	Wed Jan 22 20:41:29 2003 +0200
+++ b/src/lib-storage/index/mbox/mbox-storage.c	Wed Jan 22 21:23:28 2003 +0200
@@ -630,7 +630,9 @@
 	index_storage_search_init,
 	index_storage_search_deinit,
 	index_storage_search_next,
-	mbox_storage_save,
+	mbox_storage_save_init,
+	mbox_storage_save_deinit,
+	mbox_storage_save_next,
 	mail_storage_is_inconsistency_error,
 
 	FALSE,
--- a/src/lib-storage/index/mbox/mbox-storage.h	Wed Jan 22 20:41:29 2003 +0200
+++ b/src/lib-storage/index/mbox/mbox-storage.h	Wed Jan 22 21:23:28 2003 +0200
@@ -5,9 +5,14 @@
 
 int mbox_storage_copy(struct mailbox *box, struct mailbox *destbox,
 		      const char *messageset, int uidset);
-int mbox_storage_save(struct mailbox *box, const struct mail_full_flags *flags,
-		      time_t internal_date, int timezone_offset,
-		      struct istream *data, uoff_t data_size);
+
+struct mail_save_context *
+mbox_storage_save_init(struct mailbox *box, int transaction);
+int mbox_storage_save_deinit(struct mail_save_context *ctx, int rollback);
+int mbox_storage_save_next(struct mail_save_context *ctx,
+			   const struct mail_full_flags *flags,
+			   time_t received_date, int timezone_offset,
+			   struct istream *data);
 
 int mbox_find_mailboxes(struct mail_storage *storage, const char *mask,
 			mailbox_list_callback_t callback, void *context);
--- a/src/lib-storage/mail-storage.h	Wed Jan 22 20:41:29 2003 +0200
+++ b/src/lib-storage/mail-storage.h	Wed Jan 22 21:23:28 2003 +0200
@@ -268,12 +268,22 @@
 	   the next call to search_next() or search_deinit(). */
 	struct mail *(*search_next)(struct mail_search_context *ctx);
 
-	/* Save a new mail into mailbox. timezone_offset specifies the
-	   timezone in minutes which received_date was originally given
-	   with. */
-	int (*save)(struct mailbox *box, const struct mail_full_flags *flags,
-		    time_t received_date, int timezone_offset,
-		    struct istream *data, uoff_t data_size);
+	/* Initialize saving one or more mails. If transaction is TRUE, all
+	   the saved mails are deleted if an error occurs or save_deinit()
+	   is called with rollback TRUE. */
+	struct mail_save_context *(*save_init)(struct mailbox *box,
+					       int transaction);
+	/* Deinitialize saving. rollback has effect only if save_init() was
+	   called with transaction being TRUE. If rollback is FALSE but
+	   committing the changes fails, all the commits are rollbacked if
+	   possible. */
+	int (*save_deinit)(struct mail_save_context *ctx, int rollback);
+	/* Save a mail into mailbox. timezone_offset specifies the timezone in
+	   minutes in which received_date was originally given with. */
+	int (*save_next)(struct mail_save_context *ctx,
+			 const struct mail_full_flags *flags,
+			 time_t received_date, int timezone_offset,
+			 struct istream *data);
 
 	/* Returns TRUE if mailbox is now in inconsistent state, meaning that
 	   the message IDs etc. may have changed - only way to recover this