changeset 14592:5344ff4215b4

imap: Implemented CATENATE extension.
author Stephan Bosch <stephan@rename-it.nl>
date Sat, 02 Jun 2012 18:14:16 +0300
parents 07e6ca397a72
children e445670e7332
files README configure.in src/imap/cmd-append.c
diffstat 3 files changed, 451 insertions(+), 181 deletions(-) [+]
line wrap: on
line diff
--- a/README	Sat Jun 02 17:56:27 2012 +0300
+++ b/README	Sat Jun 02 18:14:16 2012 +0300
@@ -40,6 +40,7 @@
  3691       - IMAP4 UNSELECT command
  4314       - IMAP4 Access Control List (ACL) Extension
  4315       - IMAP UIDPLUS extension
+ 4469       - IMAP CATENATE Extension
  4551       - IMAP Extension for Conditional STORE Operation
               or Quick Flag Changes Resynchronization
  4731       - IMAP4 Extension to SEARCH Command for Controlling
--- a/configure.in	Sat Jun 02 17:56:27 2012 +0300
+++ b/configure.in	Sat Jun 02 18:14:16 2012 +0300
@@ -2704,7 +2704,7 @@
 dnl IDLE doesn't really belong to banner. It's there just to make Blackberries
 dnl happy, because otherwise BIS server disables push email.
 capability_banner="IMAP4rev1 LITERAL+ SASL-IR LOGIN-REFERRALS ID ENABLE IDLE"
-capability="$capability_banner SORT SORT=DISPLAY THREAD=REFERENCES THREAD=REFS MULTIAPPEND UNSELECT CHILDREN NAMESPACE UIDPLUS LIST-EXTENDED I18NLEVEL=1 CONDSTORE QRESYNC ESEARCH ESORT SEARCHRES WITHIN CONTEXT=SEARCH LIST-STATUS SPECIAL-USE"
+capability="$capability_banner SORT SORT=DISPLAY THREAD=REFERENCES THREAD=REFS MULTIAPPEND URL-PARTIAL CATENATE UNSELECT CHILDREN NAMESPACE UIDPLUS LIST-EXTENDED I18NLEVEL=1 CONDSTORE QRESYNC ESEARCH ESORT SEARCHRES WITHIN CONTEXT=SEARCH LIST-STATUS SPECIAL-USE"
 AC_DEFINE_UNQUOTED(CAPABILITY_STRING, "$capability", IMAP capabilities)
 AC_DEFINE_UNQUOTED(CAPABILITY_BANNER_STRING, "$capability_banner", IMAP capabilities advertised in banner) 
 
--- a/src/imap/cmd-append.c	Sat Jun 02 17:56:27 2012 +0300
+++ b/src/imap/cmd-append.c	Sat Jun 02 18:14:16 2012 +0300
@@ -3,12 +3,14 @@
 #include "imap-common.h"
 #include "ioloop.h"
 #include "istream.h"
+#include "istream-chain.h"
 #include "ostream.h"
 #include "str.h"
 #include "imap-parser.h"
 #include "imap-date.h"
 #include "imap-util.h"
 #include "imap-commands.h"
+#include "imap-msgpart-url.h"
 
 #include <sys/time.h>
 
@@ -26,14 +28,19 @@
         struct mailbox_transaction_context *t;
 	time_t started;
 
+	struct istream_chain *catchain;
+	uoff_t cat_msg_size;
+
 	struct istream *input;
-	uoff_t msg_size;
+	struct istream *litinput;
+	uoff_t literal_size;
 
 	struct imap_parser *save_parser;
 	struct mail_save_context *save_ctx;
 	unsigned int count;
 
 	unsigned int message_input:1;
+	unsigned int catenate:1;
 	unsigned int failed:1;
 };
 
@@ -50,7 +57,7 @@
 		    ctx->count, secs);
 	if (ctx->input != NULL) {
 		str_printfa(str, ", %"PRIuUOFF_T"/%"PRIuUOFF_T" bytes",
-			    ctx->input->v_offset, ctx->msg_size);
+			    ctx->input->v_offset, ctx->literal_size);
 	}
 	str_append_c(str, ')');
 	return str_c(str);
@@ -112,37 +119,6 @@
 		client_continue_pending_input(client);
 }
 
-/* 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(const struct imap_arg *args,
-			 const struct imap_arg **flags_r,
-			 const char **internal_date_r, uoff_t *msg_size_r,
-			 bool *nonsync_r)
-{
-	/* [<flags>] */
-	if (!imap_arg_get_list(args, flags_r))
-		*flags_r = NULL;
-	else
-		args++;
-
-	/* [<internal date>] */
-	if (args->type != IMAP_ARG_STRING)
-		*internal_date_r = NULL;
-	else {
-		*internal_date_r = imap_arg_as_astring(args);
-		args++;
-	}
-
-	if (!imap_arg_get_literal_size(args, msg_size_r)) {
-		*nonsync_r = FALSE;
-		return FALSE;
-	}
-
-	*nonsync_r = args->type == IMAP_ARG_LITERAL_SIZE_NONSYNC;
-	return TRUE;
-}
-
 static void cmd_append_finish(struct cmd_append_context *ctx)
 {
 	imap_parser_unref(&ctx->save_parser);
@@ -155,6 +131,8 @@
 	o_stream_set_flush_callback(ctx->client->output,
 				    client_output, ctx->client);
 
+	if (ctx->litinput != NULL)
+		i_stream_unref(&ctx->litinput);
 	if (ctx->input != NULL)
 		i_stream_unref(&ctx->input);
 	if (ctx->save_ctx != NULL)
@@ -184,11 +162,10 @@
 		return TRUE;
 	}
 
-	if (ctx->input->v_offset == ctx->msg_size) {
+	if (ctx->litinput->v_offset == ctx->literal_size) {
 		/* finished, but with MULTIAPPEND and LITERAL+ we may get
 		   more messages. */
-		i_stream_unref(&ctx->input);
-		ctx->input = NULL;
+		i_stream_unref(&ctx->litinput);
 
 		ctx->message_input = FALSE;
 		imap_parser_reset(ctx->save_parser);
@@ -210,7 +187,7 @@
 
 	/* we have to read the nonsynced literal so we don't treat the message
 	   data as commands. */
-	ctx->input = i_stream_create_limit(ctx->client->input, ctx->msg_size);
+	ctx->input = i_stream_create_limit(ctx->client->input, ctx->literal_size);
 
 	ctx->message_input = TRUE;
 	ctx->cmd->func = cmd_append_continue_cancel;
@@ -218,20 +195,405 @@
 	return cmd_append_continue_cancel(ctx->cmd);
 }
 
+static int
+cmd_append_catenate(struct client_command_context *cmd,
+		    const struct imap_arg *args, bool *nonsync_r)
+{
+	struct client *client = cmd->client;
+	struct cmd_append_context *ctx = cmd->context;
+	struct imap_msgpart_url *mpurl;
+	const char *catpart, *error;
+	uoff_t newsize;
+	int ret;
+
+	*nonsync_r = FALSE;
+
+	/* Handle URLs until a TEXT literal is encountered */
+	while (imap_arg_get_atom(args, &catpart)) {
+		const char *caturl;
+
+		if (strcasecmp(catpart, "URL") == 0 ) {
+			/* URL <url> */ 
+			args++;
+			if (!imap_arg_get_astring(args, &caturl))
+				break;
+			if (ctx->failed)
+				return -1;
+
+			mpurl = imap_msgpart_url_parse(client->user, client->mailbox, caturl, &error);
+			if (mpurl == NULL) {
+				/* invalid url, abort */
+				client_send_tagline(cmd,
+					t_strdup_printf("NO [BADURL %s] %s.", caturl, error));
+				return -1;
+			}
+
+			if (cmd->cancel) {
+				imap_msgpart_url_free(&mpurl);
+				cmd_append_finish(ctx);
+				return 1;
+			}
+
+			/* catenate URL */
+			if (ctx->save_ctx != NULL) {
+				struct istream *input = NULL;
+				uoff_t size;
+
+				if (!imap_msgpart_url_read_part(mpurl, &input, &size, &error)) {
+					/* invalid url, abort */
+					client_send_tagline(cmd,
+						t_strdup_printf("NO [BADURL %s] %s.", caturl, error));
+					return -1;
+				}
+
+				newsize = ctx->cat_msg_size + size;
+				if (newsize < ctx->cat_msg_size) {
+					client_send_tagline(cmd,
+						"NO [TOOBIG] Composed message grows too big.");
+					imap_msgpart_url_free(&mpurl);
+					return -1;
+				}
+
+				if (input != NULL) {
+					ctx->cat_msg_size = newsize;
+					i_stream_chain_append(ctx->catchain, input);
+
+					while (!input->eof) {
+						ret = i_stream_read(ctx->input);
+						if (mailbox_save_continue(ctx->save_ctx) < 0) {
+							/* we still have to finish reading the message
+							   from client */
+							mailbox_save_cancel(&ctx->save_ctx);
+							break;
+						}
+						if (ret == -1 || ret == 0)
+							break;
+					}
+
+					if (!input->eof) {
+						client_send_tagline(cmd, t_strdup_printf(
+							"NO [BADURL %s] Failed to read all data.", caturl));
+						imap_msgpart_url_free(&mpurl);
+						return -1;
+					}
+				}
+			}
+			imap_msgpart_url_free(&mpurl);
+		} else if (strcasecmp(catpart, "TEXT") == 0) {
+			/* TEXT <literal> */
+			args++;
+			if (!imap_arg_get_literal_size(args, &ctx->literal_size))
+				break;
+
+			*nonsync_r = args->type == IMAP_ARG_LITERAL_SIZE_NONSYNC;
+			if (ctx->failed) {
+				/* we failed earlier, make sure we just eat
+				   nonsync-literal if it's given. */
+				return -1;
+			}
+
+			newsize = ctx->cat_msg_size + ctx->literal_size;
+			if (newsize < ctx->cat_msg_size) {
+				client_send_tagline(cmd,
+					"NO [TOOBIG] Composed message grows too big.");
+				return -1;
+			}
+
+			/* save the mail */
+			ctx->cat_msg_size = newsize;
+			ctx->litinput = i_stream_create_limit(client->input, ctx->literal_size);
+			i_stream_chain_append(ctx->catchain, ctx->litinput);
+			return 1;
+		} else {
+			break;
+		}
+		args++;
+	}
+
+	if (IMAP_ARG_IS_EOL(args)) {
+		/* ")" */
+		return 0;
+	}
+	client_send_command_error(cmd, "Invalid arguments.");
+	return -1;
+}
+
+static void cmd_append_finish_catenate(struct client_command_context *cmd)
+{
+	struct cmd_append_context *ctx = cmd->context;
+
+	i_stream_chain_append(ctx->catchain, NULL);
+	i_stream_unref(&ctx->input);
+	ctx->input = NULL;
+	ctx->catenate = FALSE;
+
+	if (mailbox_save_finish(&ctx->save_ctx) < 0) {
+		ctx->failed = TRUE;
+		client_send_storage_error(cmd, ctx->storage);
+	}
+}
+
+static bool cmd_append_continue_catenate(struct client_command_context *cmd)
+{
+	struct client *client = cmd->client;
+	struct cmd_append_context *ctx = cmd->context;
+	const struct imap_arg *args;
+	const char *msg;
+	bool fatal, nonsync = FALSE;
+	int ret;
+
+	if (cmd->cancel) {
+		cmd_append_finish(ctx);
+		return TRUE;
+	}
+
+	ret = imap_parser_read_args(ctx->save_parser, 0,
+				    IMAP_PARSE_FLAG_LITERAL_SIZE |
+				    IMAP_PARSE_FLAG_INSIDE_LIST, &args);
+	if (ret == -1) {
+		if (!ctx->failed) {
+			msg = imap_parser_get_error(ctx->save_parser, &fatal);
+			if (fatal)
+				client_disconnect_with_error(client, msg);
+			else
+				client_send_command_error(cmd, msg);
+		}
+		client->input_skip_line = TRUE;
+		cmd_append_finish(ctx);
+		return TRUE;
+	}
+	if (ret < 0) {
+		/* need more data */
+		return FALSE;
+	}
+
+	if ((ret = cmd_append_catenate(cmd, args, &nonsync)) < 0) {
+		client->input_skip_line = TRUE;
+		return cmd_append_cancel(ctx, nonsync);
+	}
+
+	if (ret == 0) {
+		/* ")" */
+		cmd_append_finish_catenate(cmd);
+
+		/* last catenate part */
+		imap_parser_reset(ctx->save_parser);
+		cmd->func = cmd_append_continue_parsing;
+		return cmd_append_continue_parsing(cmd);
+	}
+
+	/* TEXT <literal> */
+
+	/* after literal comes CRLF, if we fail make sure we eat it away */
+	client->input_skip_line = TRUE;
+
+	if (!nonsync) {
+		o_stream_send(client->output, "+ OK\r\n", 6);
+		o_stream_flush(client->output);
+		o_stream_uncork(client->output);
+		o_stream_cork(client->output);
+	}
+
+	ctx->message_input = TRUE;
+	cmd->func = cmd_append_continue_message;
+	return cmd_append_continue_message(cmd);
+}
+
+static int
+cmd_append_handle_args(struct client_command_context *cmd,
+		       const struct imap_arg **args, bool *nonsync_r)
+{
+	struct client *client = cmd->client;
+	struct cmd_append_context *ctx = cmd->context;
+	const struct imap_arg *flags_list;
+	const struct imap_arg *cat_list = NULL;
+	enum mail_flags flags;
+	const char *const *keywords_list;
+	struct mail_keywords *keywords;
+	const char *internal_date_str;
+	time_t internal_date;
+	int ret, timezone_offset;
+	bool valid;
+
+	/* [<flags>] */
+	if (!imap_arg_get_list(*args, &flags_list))
+		flags_list = NULL;
+	else
+		(*args)++;
+
+	/* [<internal date>] */
+	if ((*args)->type != IMAP_ARG_STRING)
+		internal_date_str = NULL;
+	else {
+		internal_date_str = imap_arg_as_astring(*args);
+		(*args)++;
+	}
+
+	valid = FALSE;
+	*nonsync_r = FALSE;
+	ctx->catenate = FALSE;
+	if (imap_arg_atom_equals(*args, "CATENATE")) {
+		(*args)++;
+		if (imap_arg_get_list(*args, &cat_list)) {
+			valid = TRUE;
+			ctx->catenate = TRUE;
+		}
+	} else if (imap_arg_get_literal_size(*args, &ctx->literal_size)) {
+		*nonsync_r = (*args)->type == IMAP_ARG_LITERAL_SIZE_NONSYNC;
+		valid = TRUE;
+	}
+
+	if (!valid) {
+		client->input_skip_line = TRUE;
+		client_send_command_error(cmd, "Invalid arguments.");
+		return -1;
+	}
+
+	if (ctx->failed) {
+		/* we failed earlier, make sure we just eat nonsync-literal
+		   if it's given. */
+		return -1;
+	}
+
+	if (flags_list != NULL) {
+		if (!client_parse_mail_flags(cmd, flags_list,
+					     &flags, &keywords_list))
+			return -1;
+		if (keywords_list == NULL)
+			keywords = NULL;
+		else if (mailbox_keywords_create(ctx->box, keywords_list,
+						 &keywords) < 0) {
+			client_send_storage_error(cmd, ctx->storage);
+			return -1;
+		}
+	} else {
+		flags = 0;
+		keywords = NULL;
+	}
+
+	if (internal_date_str == NULL) {
+		/* no time given, default to now. */
+		internal_date = (time_t)-1;
+		timezone_offset = 0;
+	} else if (!imap_parse_datetime(internal_date_str,
+					&internal_date, &timezone_offset)) {
+		client_send_tagline(cmd, "BAD Invalid internal date.");
+		return -1;
+	}
+
+	if (internal_date != (time_t)-1 &&
+	    internal_date > ioloop_time + INTERNALDATE_MAX_FUTURE_SECS) {
+		/* the client specified a time in the future, set it to now. */
+		internal_date = (time_t)-1;
+		timezone_offset = 0;
+	}
+
+	if (cat_list != NULL) {
+		ctx->cat_msg_size = 0;
+		ctx->input = i_stream_create_chain(&ctx->catchain);
+	} else {
+		if (ctx->literal_size == 0) {
+			/* no message data, abort */
+			client_send_tagline(cmd, "NO Can't save a zero byte message.");
+			return -1;
+		}
+		ctx->litinput = i_stream_create_limit(client->input, ctx->literal_size);
+		ctx->input = ctx->litinput;
+		i_stream_ref(ctx->input);
+	}
+
+	/* save the mail */
+	ctx->save_ctx = mailbox_save_alloc(ctx->t);
+	mailbox_save_set_flags(ctx->save_ctx, flags, keywords);
+	mailbox_save_set_received_date(ctx->save_ctx,
+				       internal_date, timezone_offset);
+	ret = mailbox_save_begin(&ctx->save_ctx, ctx->input);
+	ctx->count++;
+
+	if (cat_list != NULL &&
+	    (ret = cmd_append_catenate(cmd, cat_list, nonsync_r)) <= 0) {
+		if (ret < 0)
+			client->input_skip_line = TRUE;
+		return ret;
+	}
+
+	if (keywords != NULL)
+		mailbox_keywords_unref(&keywords);
+
+	if (ret < 0) {
+		/* save initialization failed */
+		client_send_storage_error(cmd, ctx->storage);
+		return -1;
+	}
+	return 1;
+}
+
+static bool cmd_append_finish_parsing(struct client_command_context *cmd)
+{
+	struct client *client = cmd->client;
+	struct cmd_append_context *ctx = cmd->context;
+	enum mailbox_sync_flags sync_flags;
+	enum imap_sync_flags imap_flags;
+	struct mail_transaction_commit_changes changes;
+	unsigned int save_count;
+	string_t *msg;
+	int ret;
+
+	/* eat away the trailing CRLF */
+	client->input_skip_line = TRUE;
+
+	if (ctx->failed) {
+		/* we failed earlier, error message is sent */
+		cmd_append_finish(ctx);
+		return TRUE;
+	}
+	if (ctx->count == 0) {
+		client_send_tagline(cmd, "BAD Missing message size.");
+		cmd_append_finish(ctx);
+		return TRUE;
+	}
+
+	ret = mailbox_transaction_commit_get_changes(&ctx->t, &changes);
+	if (ret < 0) {
+		client_send_storage_error(cmd, ctx->storage);
+		cmd_append_finish(ctx);
+		return TRUE;
+	}
+
+	msg = t_str_new(256);
+	save_count = seq_range_count(&changes.saved_uids);
+	if (save_count == 0) {
+		/* not supported by backend (virtual) */
+		str_append(msg, "OK Append completed.");
+	} else {
+		i_assert(ctx->count == save_count);
+		str_printfa(msg, "OK [APPENDUID %u ",
+			    changes.uid_validity);
+		imap_write_seq_range(msg, &changes.saved_uids);
+		str_append(msg, "] Append completed.");
+	}
+	pool_unref(&changes.pool);
+
+	if (ctx->box == cmd->client->mailbox) {
+		sync_flags = 0;
+		imap_flags = IMAP_SYNC_FLAG_SAFE;
+	} else {
+		sync_flags = MAILBOX_SYNC_FLAG_FAST;
+		imap_flags = 0;
+	}
+
+	cmd_append_finish(ctx);
+	return cmd_sync(cmd, sync_flags, imap_flags, str_c(msg));
+}
+
 static bool cmd_append_continue_parsing(struct client_command_context *cmd)
 {
 	struct client *client = cmd->client;
 	struct cmd_append_context *ctx = cmd->context;
 	const struct imap_arg *args;
-	const struct imap_arg *flags_list;
-	enum mail_flags flags;
-	const char *const *keywords_list;
-	struct mail_keywords *keywords;
-	const char *internal_date_str, *msg;
-	time_t internal_date;
-	int ret, timezone_offset;
-	unsigned int save_count;
-	bool nonsync, fatal;
+	const char *msg;
+	bool fatal, nonsync;
+	int ret;
 
 	if (cmd->cancel) {
 		cmd_append_finish(ctx);
@@ -262,139 +624,40 @@
 
 	if (IMAP_ARG_IS_EOL(args)) {
 		/* last message */
-		enum mailbox_sync_flags sync_flags;
-		enum imap_sync_flags imap_flags;
-		struct mail_transaction_commit_changes changes;
-		string_t *msg;
-
-		/* eat away the trailing CRLF */
-		client->input_skip_line = TRUE;
-
-		if (ctx->failed) {
-			/* we failed earlier, error message is sent */
-			cmd_append_finish(ctx);
-			return TRUE;
-		}
-		if (ctx->count == 0) {
-			client_send_tagline(cmd, "BAD Missing message size.");
-			cmd_append_finish(ctx);
-			return TRUE;
-		}
-
-		ret = mailbox_transaction_commit_get_changes(&ctx->t, &changes);
-		if (ret < 0) {
-			client_send_storage_error(cmd, ctx->storage);
-			cmd_append_finish(ctx);
-			return TRUE;
-		}
-
-		msg = t_str_new(256);
-		save_count = seq_range_count(&changes.saved_uids);
-		if (save_count == 0) {
-			/* not supported by backend (virtual) */
-			str_append(msg, "OK Append completed.");
-		} else {
-			i_assert(ctx->count == save_count);
-			str_printfa(msg, "OK [APPENDUID %u ",
-				    changes.uid_validity);
-			imap_write_seq_range(msg, &changes.saved_uids);
-			str_append(msg, "] Append completed.");
-		}
-		pool_unref(&changes.pool);
-
-		if (ctx->box == cmd->client->mailbox) {
-			sync_flags = 0;
-			imap_flags = IMAP_SYNC_FLAG_SAFE;
-		} else {
-			sync_flags = MAILBOX_SYNC_FLAG_FAST;
-			imap_flags = 0;
-		}
-
-		cmd_append_finish(ctx);
-		return cmd_sync(cmd, sync_flags, imap_flags, str_c(msg));
+		return cmd_append_finish_parsing(cmd);
 	}
 
-	if (!validate_args(args, &flags_list, &internal_date_str,
-			   &ctx->msg_size, &nonsync)) {
-		client_send_command_error(cmd, "Invalid arguments.");
-		client->input_skip_line = TRUE;
-		return cmd_append_cancel(ctx, nonsync);
-	}
+	/* Handle multiple messages (IMAP URLs) while no literals are
+	   encountered) */
+	while ((ret = cmd_append_handle_args(cmd, &args, &nonsync)) == 0) {
+		cmd_append_finish_catenate(cmd);
 
-	if (ctx->failed) {
-		/* we failed earlier, make sure we just eat nonsync-literal
-		   if it's given. */
-		return cmd_append_cancel(ctx, nonsync);
+		/* Check for EOL, e.g.:
+		   APPEND <box> ( URL <url> URL <url> URL <url> ) */
+		args++;
+		if (IMAP_ARG_IS_EOL(args)) {
+			/* last message */
+			return cmd_append_finish_parsing(cmd);
+		}
 	}
 
-	if (flags_list != NULL) {
-		if (!client_parse_mail_flags(cmd, flags_list,
-					     &flags, &keywords_list))
-			return cmd_append_cancel(ctx, nonsync);
-		if (keywords_list == NULL)
-			keywords = NULL;
-		else if (mailbox_keywords_create(ctx->box, keywords_list,
-						 &keywords) < 0) {
-			client_send_storage_error(cmd, ctx->storage);
-			return cmd_append_cancel(ctx, nonsync);
-		}
-	} else {
-		flags = 0;
-		keywords = NULL;
-	}
+	if (ret < 0)
+		return cmd_append_cancel(ctx, nonsync);
+
+	if (!ctx->catenate) {
+		/* after literal comes CRLF, if we fail make sure
+		   we eat it away */
+		client->input_skip_line = TRUE;
 
-	if (internal_date_str == NULL) {
-		/* no time given, default to now. */
-		internal_date = (time_t)-1;
-		timezone_offset = 0;
-	} else if (!imap_parse_datetime(internal_date_str,
-					&internal_date, &timezone_offset)) {
-		client_send_tagline(cmd, "BAD Invalid internal date.");
-		return cmd_append_cancel(ctx, nonsync);
-	}
-
-	if (internal_date != (time_t)-1 &&
-	    internal_date > ioloop_time + INTERNALDATE_MAX_FUTURE_SECS) {
-		/* the client specified a time in the future, set it to now. */
-		internal_date = (time_t)-1;
-		timezone_offset = 0;
+		if (!nonsync) {
+			o_stream_send(client->output, "+ OK\r\n", 6);
+			o_stream_flush(client->output);
+			o_stream_uncork(client->output);
+			o_stream_cork(client->output);
+		}
+		ctx->message_input = TRUE;
 	}
 
-	if (ctx->msg_size == 0) {
-		/* no message data, abort */
-		client_send_tagline(cmd, "NO Can't save a zero byte message.");
-		return cmd_append_cancel(ctx, nonsync);
-	}
-
-	/* save the mail */
-	ctx->input = i_stream_create_limit(client->input, ctx->msg_size);
-	ctx->save_ctx = mailbox_save_alloc(ctx->t);
-	mailbox_save_set_flags(ctx->save_ctx, flags, keywords);
-	mailbox_save_set_received_date(ctx->save_ctx,
-				       internal_date, timezone_offset);
-	ret = mailbox_save_begin(&ctx->save_ctx, ctx->input);
-
-	if (keywords != NULL)
-		mailbox_keywords_unref(&keywords);
-
-	if (ret < 0) {
-		/* save initialization failed */
-		client_send_storage_error(cmd, ctx->storage);
-		return cmd_append_cancel(ctx, nonsync);
-	}
-
-	/* after literal comes CRLF, if we fail make sure we eat it away */
-	client->input_skip_line = TRUE;
-
-	if (!nonsync) {
-		o_stream_send(client->output, "+ OK\r\n", 6);
-		o_stream_flush(client->output);
-		o_stream_uncork(client->output);
-		o_stream_cork(client->output);
-	}
-
-	ctx->count++;
-	ctx->message_input = TRUE;
 	cmd->func = cmd_append_continue_message;
 	return cmd_append_continue_message(cmd);
 }
@@ -404,7 +667,7 @@
 	struct client *client = cmd->client;
 	struct cmd_append_context *ctx = cmd->context;
 	size_t size;
-	int ret;
+	int ret = 0;
 
 	if (cmd->cancel) {
 		cmd_append_finish(ctx);
@@ -412,7 +675,7 @@
 	}
 
 	if (ctx->save_ctx != NULL) {
-		while (ctx->input->v_offset != ctx->msg_size) {
+		while (ctx->litinput->v_offset != ctx->literal_size) {
 			ret = i_stream_read(ctx->input);
 			if (mailbox_save_continue(ctx->save_ctx) < 0) {
 				/* we still have to finish reading the message
@@ -431,12 +694,11 @@
 		i_stream_skip(ctx->input, size);
 	}
 
-	if (ctx->input->eof || client->input->closed) {
-		bool all_written = ctx->input->v_offset == ctx->msg_size;
+	if (ctx->litinput->eof || client->input->closed) {
+		bool all_written = ctx->litinput->v_offset == ctx->literal_size;
 
 		/* finished */
-		i_stream_unref(&ctx->input);
-		ctx->input = NULL;
+		i_stream_unref(&ctx->litinput);
 
 		if (ctx->save_ctx == NULL) {
 			/* failed above */
@@ -448,24 +710,31 @@
 			ctx->failed = TRUE;
 			mailbox_save_cancel(&ctx->save_ctx);
 			client_disconnect(client, "EOF while appending");
+		} else if (ctx->catenate) {
+			/* CATENATE isn't finished yet */
 		} else if (mailbox_save_finish(&ctx->save_ctx) < 0) {
 			ctx->failed = TRUE;
 			client_send_storage_error(cmd, ctx->storage);
 		}
-		ctx->save_ctx = NULL;
 
 		if (client->input->closed) {
 			cmd_append_finish(ctx);
 			return TRUE;
 		}
 
-		/* prepare for next message */
+		/* prepare for next message (part) */
 		ctx->message_input = FALSE;
 		imap_parser_reset(ctx->save_parser);
+
+		if (ctx->catenate) {
+			cmd->func = cmd_append_continue_catenate;
+			return cmd_append_continue_catenate(cmd);
+		}
+
+		i_stream_unref(&ctx->input);
 		cmd->func = cmd_append_continue_parsing;
 		return cmd_append_continue_parsing(cmd);
 	}
-
 	return FALSE;
 }