summaryrefslogtreecommitdiffstats
path: root/src/lib-storage/index/imapc/imapc-save.c
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-15 17:36:47 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-15 17:36:47 +0000
commit0441d265f2bb9da249c7abf333f0f771fadb4ab5 (patch)
tree3f3789daa2f6db22da6e55e92bee0062a7d613fe /src/lib-storage/index/imapc/imapc-save.c
parentInitial commit. (diff)
downloaddovecot-0441d265f2bb9da249c7abf333f0f771fadb4ab5.tar.xz
dovecot-0441d265f2bb9da249c7abf333f0f771fadb4ab5.zip
Adding upstream version 1:2.3.21+dfsg1.upstream/1%2.3.21+dfsg1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/lib-storage/index/imapc/imapc-save.c')
-rw-r--r--src/lib-storage/index/imapc/imapc-save.c829
1 files changed, 829 insertions, 0 deletions
diff --git a/src/lib-storage/index/imapc/imapc-save.c b/src/lib-storage/index/imapc/imapc-save.c
new file mode 100644
index 0000000..c50f46b
--- /dev/null
+++ b/src/lib-storage/index/imapc/imapc-save.c
@@ -0,0 +1,829 @@
+/* Copyright (c) 2011-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "istream.h"
+#include "istream-crlf.h"
+#include "ostream.h"
+#include "imap-date.h"
+#include "imap-util.h"
+#include "imap-seqset.h"
+#include "imap-quote.h"
+#include "index-mail.h"
+#include "mail-copy.h"
+#include "mailbox-list-private.h"
+#include "imapc-msgmap.h"
+#include "imapc-storage.h"
+#include "imapc-sync.h"
+#include "imapc-mail.h"
+#include "seq-set-builder.h"
+
+struct imapc_save_context {
+ struct mail_save_context ctx;
+
+ struct imapc_mailbox *mbox;
+ struct imapc_mailbox *src_mbox;
+ struct mail_index_transaction *trans;
+
+ int fd;
+ char *temp_path;
+ struct istream *input;
+
+ uint32_t dest_uid_validity;
+ ARRAY_TYPE(seq_range) dest_saved_uids;
+ unsigned int save_count;
+
+ bool failed:1;
+ bool finished:1;
+};
+
+struct imapc_save_cmd_context {
+ struct imapc_save_context *ctx;
+ int ret;
+};
+
+#define IMAPC_SAVECTX(s) container_of(s, struct imapc_save_context, ctx)
+#define IMAPC_SERVER_CMDLINE_MAX_LEN 8000
+
+void imapc_transaction_save_rollback(struct mail_save_context *_ctx);
+static void imapc_mail_copy_bulk_flush(struct imapc_mailbox *mbox);
+
+struct mail_save_context *
+imapc_save_alloc(struct mailbox_transaction_context *t)
+{
+ struct imapc_mailbox *mbox = IMAPC_MAILBOX(t->box);
+ struct imapc_save_context *ctx;
+
+ i_assert((t->flags & MAILBOX_TRANSACTION_FLAG_EXTERNAL) != 0);
+
+ if (t->save_ctx == NULL) {
+ ctx = i_new(struct imapc_save_context, 1);
+ ctx->ctx.transaction = t;
+ ctx->mbox = mbox;
+ ctx->src_mbox = NULL;
+ ctx->trans = t->itrans;
+ ctx->fd = -1;
+ t->save_ctx = &ctx->ctx;
+ }
+ return t->save_ctx;
+}
+
+int imapc_save_begin(struct mail_save_context *_ctx, struct istream *input)
+{
+ struct imapc_save_context *ctx = IMAPC_SAVECTX(_ctx);
+ const char *path;
+
+ i_assert(ctx->fd == -1);
+
+ if (imapc_storage_client_handle_auth_failure(ctx->mbox->storage->client))
+ return -1;
+
+ ctx->fd = imapc_client_create_temp_fd(ctx->mbox->storage->client->client,
+ &path);
+ if (ctx->fd == -1) {
+ mail_set_critical(_ctx->dest_mail,
+ "Couldn't create temp file %s", path);
+ ctx->failed = TRUE;
+ return -1;
+ }
+ /* we may not know the size of the input, or be sure that it contains
+ only CRLFs. so we'll always first write the mail to a temp file and
+ upload it from there to remote server. */
+ ctx->finished = FALSE;
+ ctx->temp_path = i_strdup(path);
+ ctx->input = i_stream_create_crlf(input);
+ _ctx->data.output = o_stream_create_fd_file(ctx->fd, 0, FALSE);
+ o_stream_cork(_ctx->data.output);
+ return 0;
+}
+
+int imapc_save_continue(struct mail_save_context *_ctx)
+{
+ struct imapc_save_context *ctx = IMAPC_SAVECTX(_ctx);
+
+ if (ctx->failed)
+ return -1;
+
+ if (index_storage_save_continue(_ctx, ctx->input, NULL) < 0) {
+ ctx->failed = TRUE;
+ return -1;
+ }
+ return 0;
+}
+
+static void imapc_save_appenduid(struct imapc_save_context *ctx,
+ const struct imapc_command_reply *reply,
+ uint32_t *uid_r)
+{
+ const char *const *args;
+ uint32_t uid_validity, dest_uid;
+
+ *uid_r = 0;
+
+ /* <uidvalidity> <dest uid-set> */
+ args = t_strsplit(reply->resp_text_value, " ");
+ if (str_array_length(args) != 2)
+ return;
+
+ if (str_to_uint32(args[0], &uid_validity) < 0)
+ return;
+ if (ctx->dest_uid_validity == 0)
+ ctx->dest_uid_validity = uid_validity;
+ else if (ctx->dest_uid_validity != uid_validity)
+ return;
+
+ if (str_to_uint32(args[1], &dest_uid) == 0) {
+ seq_range_array_add_with_init(&ctx->dest_saved_uids,
+ 32, dest_uid);
+ *uid_r = dest_uid;
+ }
+}
+
+static void
+imapc_save_add_to_index(struct imapc_save_context *ctx, uint32_t uid)
+{
+ struct mail *_mail = ctx->ctx.dest_mail;
+ struct index_mail *imail = INDEX_MAIL(_mail);
+ uint32_t seq;
+
+ /* we'll temporarily append messages and at commit time expunge
+ them all, since we can't guarantee that no one else has saved
+ messages to remote server during our transaction */
+ mail_index_append(ctx->trans, uid, &seq);
+ mail_set_seq_saving(_mail, seq);
+ imail->data.no_caching = TRUE;
+ imail->data.forced_no_caching = TRUE;
+
+ if (ctx->fd != -1) {
+ struct imapc_mail *imapc_mail = IMAPC_MAIL(_mail);
+ imail->data.stream = i_stream_create_fd_autoclose(&ctx->fd, 0);
+ imapc_mail->header_fetched = TRUE;
+ imapc_mail->body_fetched = TRUE;
+ /* The saved stream wasn't actually read, but it needs to be
+ set accessed to avoid assert-crash. */
+ _mail->mail_stream_accessed = TRUE;
+ imapc_mail_init_stream(imapc_mail);
+ }
+
+ ctx->save_count++;
+}
+
+static void imapc_save_callback(const struct imapc_command_reply *reply,
+ void *context)
+{
+ struct imapc_save_cmd_context *ctx = context;
+ uint32_t uid = 0;
+
+ if (reply->state == IMAPC_COMMAND_STATE_OK) {
+ if (reply->resp_text_key != NULL &&
+ strcasecmp(reply->resp_text_key, "APPENDUID") == 0)
+ imapc_save_appenduid(ctx->ctx, reply, &uid);
+ imapc_save_add_to_index(ctx->ctx, uid);
+ ctx->ret = 0;
+ } else if (imapc_storage_client_handle_auth_failure(ctx->ctx->mbox->storage->client)) {
+ ctx->ret = -1;
+ } else if (reply->state == IMAPC_COMMAND_STATE_NO) {
+ imapc_copy_error_from_reply(ctx->ctx->mbox->storage,
+ MAIL_ERROR_PARAMS, reply);
+ ctx->ret = -1;
+ } else {
+ mailbox_set_critical(&ctx->ctx->mbox->box,
+ "imapc: APPEND failed: %s", reply->text_full);
+ ctx->ret = -1;
+ }
+ imapc_client_stop(ctx->ctx->mbox->storage->client->client);
+}
+
+static void
+imapc_save_noop_callback(const struct imapc_command_reply *reply ATTR_UNUSED,
+ void *context)
+{
+ struct imapc_save_cmd_context *ctx = context;
+
+ /* we don't really care about the reply */
+ ctx->ret = 0;
+ imapc_client_stop(ctx->ctx->mbox->storage->client->client);
+}
+
+static void
+imapc_copy_rollback_store_callback(const struct imapc_command_reply *reply ATTR_UNUSED,
+ void *context)
+{
+ struct imapc_save_context *ctx = context;
+ /* Can't do much about a non successful STORE here */
+ if (reply->state != IMAPC_COMMAND_STATE_OK) {
+ e_error(ctx->src_mbox->box.event,
+ "imapc: Failed to set \\Deleted flag for rolling back "
+ "failed copy: %s", reply->text_full);
+ ctx->src_mbox->rollback_pending = FALSE;
+ ctx->finished = TRUE;
+ ctx->failed = TRUE;
+ } else {
+ i_assert(ctx->src_mbox->rollback_pending);
+ }
+ /* No need stop the imapc client here there is always an additional
+ expunge callback after this. */
+}
+
+static void
+imapc_copy_rollback_expunge_callback(const struct imapc_command_reply *reply ATTR_UNUSED,
+ void *context)
+{
+ struct imapc_save_context *ctx = context;
+
+ /* Can't do much about a non successful EXPUNGE here */
+ if (reply->state != IMAPC_COMMAND_STATE_OK) {
+ e_error(ctx->src_mbox->box.event,
+ "imapc: Failed to expunge messages for rolling back "
+ "failed copy: %s", reply->text_full);
+ ctx->src_mbox->rollback_pending = FALSE;
+ ctx->finished = TRUE;
+ ctx->failed = TRUE;
+ } else {
+ ctx->finished = TRUE;
+ ctx->src_mbox->rollback_pending = FALSE;
+ }
+ imapc_client_stop(ctx->src_mbox->storage->client->client);
+}
+
+static void
+imapc_append_keywords(string_t *str, struct mail_keywords *kw)
+{
+ const ARRAY_TYPE(keywords) *kw_arr;
+ const char *kw_str;
+ unsigned int i;
+
+ kw_arr = mail_index_get_keywords(kw->index);
+ for (i = 0; i < kw->count; i++) {
+ kw_str = array_idx_elem(kw_arr, kw->idx[i]);
+ if (str_len(str) > 1)
+ str_append_c(str, ' ');
+ str_append(str, kw_str);
+ }
+}
+
+static int imapc_save_append(struct imapc_save_context *ctx)
+{
+ struct mail_save_context *_ctx = &ctx->ctx;
+ struct mail_save_data *mdata = &_ctx->data;
+ struct imapc_command *cmd;
+ struct imapc_save_cmd_context sctx;
+ struct istream *input;
+ const char *flags = "", *internaldate = "";
+
+ if (mdata->flags != 0 || mdata->keywords != NULL) {
+ string_t *str = t_str_new(64);
+
+ str_append(str, " (");
+ imap_write_flags(str, mdata->flags & ENUM_NEGATE(MAIL_RECENT),
+ NULL);
+ if (mdata->keywords != NULL)
+ imapc_append_keywords(str, mdata->keywords);
+ str_append_c(str, ')');
+ flags = str_c(str);
+ }
+ if (mdata->received_date != (time_t)-1) {
+ internaldate = t_strdup_printf(" \"%s\"",
+ imap_to_datetime(mdata->received_date));
+ }
+
+ ctx->mbox->exists_received = FALSE;
+
+ input = i_stream_create_fd(ctx->fd, IO_BLOCK_SIZE);
+ sctx.ctx = ctx;
+ sctx.ret = -2;
+ cmd = imapc_client_cmd(ctx->mbox->storage->client->client,
+ imapc_save_callback, &sctx);
+ imapc_command_sendf(cmd, "APPEND %s%1s%1s %p",
+ imapc_mailbox_get_remote_name(ctx->mbox),
+ flags, internaldate, input);
+ i_stream_unref(&input);
+ while (sctx.ret == -2)
+ imapc_mailbox_run(ctx->mbox);
+
+ if (sctx.ret == 0 && ctx->mbox->selected &&
+ !ctx->mbox->exists_received) {
+ /* e.g. Courier doesn't send EXISTS reply before the tagged
+ APPEND reply. That isn't exactly required by the IMAP RFC,
+ but it makes the behavior better. See if NOOP finds
+ the mail. */
+ sctx.ret = -2;
+ cmd = imapc_client_cmd(ctx->mbox->storage->client->client,
+ imapc_save_noop_callback, &sctx);
+ imapc_command_set_flags(cmd, IMAPC_COMMAND_FLAG_RETRIABLE);
+ imapc_command_send(cmd, "NOOP");
+ while (sctx.ret == -2)
+ imapc_mailbox_run(ctx->mbox);
+ }
+ return sctx.ret;
+}
+
+int imapc_save_finish(struct mail_save_context *_ctx)
+{
+ struct imapc_save_context *ctx = IMAPC_SAVECTX(_ctx);
+ struct mail_storage *storage = _ctx->transaction->box->storage;
+
+ ctx->finished = TRUE;
+
+ if (!ctx->failed) {
+ if (o_stream_finish(_ctx->data.output) < 0) {
+ if (!mail_storage_set_error_from_errno(storage)) {
+ mail_set_critical(_ctx->dest_mail,
+ "write(%s) failed: %s", ctx->temp_path,
+ o_stream_get_error(_ctx->data.output));
+ }
+ ctx->failed = TRUE;
+ }
+ }
+
+ if (!ctx->failed) {
+ if (imapc_save_append(ctx) < 0)
+ ctx->failed = TRUE;
+ }
+
+ o_stream_unref(&_ctx->data.output);
+ i_stream_unref(&ctx->input);
+ i_close_fd_path(&ctx->fd, ctx->temp_path);
+ i_free(ctx->temp_path);
+ index_save_context_free(_ctx);
+ return ctx->failed ? -1 : 0;
+}
+
+void imapc_save_cancel(struct mail_save_context *_ctx)
+{
+ struct imapc_save_context *ctx = IMAPC_SAVECTX(_ctx);
+
+ ctx->failed = TRUE;
+ (void)imapc_transaction_save_commit_pre(_ctx);
+ (void)imapc_save_finish(_ctx);
+}
+
+static void imapc_copy_bulk_finish(struct imapc_save_context *ctx)
+{
+ while (ctx->src_mbox != NULL && ctx->src_mbox->pending_copy_request != NULL)
+ imapc_mailbox_run_nofetch(ctx->src_mbox);
+}
+
+int imapc_transaction_save_commit_pre(struct mail_save_context *_ctx)
+{
+ struct imapc_save_context *ctx = IMAPC_SAVECTX(_ctx);
+ struct mail_transaction_commit_changes *changes =
+ _ctx->transaction->changes;
+ uint32_t i, last_seq;
+
+ i_assert(ctx->finished || ctx->failed);
+
+ /* expunge all added messages from index before commit */
+ last_seq = mail_index_view_get_messages_count(_ctx->transaction->view);
+ if (last_seq == 0)
+ return -1;
+ for (i = 0; i < ctx->save_count; i++)
+ mail_index_expunge(ctx->trans, last_seq - i);
+
+ if (!ctx->failed && array_is_created(&ctx->dest_saved_uids)) {
+ changes->uid_validity = ctx->dest_uid_validity;
+ array_append_array(&changes->saved_uids, &ctx->dest_saved_uids);
+ }
+ return 0;
+}
+
+int imapc_transaction_save_commit(struct mailbox_transaction_context *t)
+{
+ struct imapc_save_context *ctx = NULL;
+ struct imapc_mailbox *src_mbox = NULL;
+
+ if (t->save_ctx != NULL) {
+ ctx = IMAPC_SAVECTX(t->save_ctx);
+ src_mbox = ctx->src_mbox;
+ }
+
+ if (src_mbox != NULL && src_mbox->pending_copy_request != NULL) {
+ /* If there is still a copy command to send flush it now */
+ imapc_mail_copy_bulk_flush(src_mbox);
+ imapc_copy_bulk_finish(ctx);
+ }
+
+ if (ctx != NULL)
+ return ctx->failed ? -1 : 0;
+ return 0;
+}
+
+void imapc_transaction_save_commit_post(struct mail_save_context *_ctx,
+ struct mail_index_transaction_commit_result *result ATTR_UNUSED)
+{
+ imapc_transaction_save_rollback(_ctx);
+}
+
+static void
+imapc_expunge_construct_cmd_str(string_t *store_cmd,
+ string_t *expunge_cmd,
+ string_t *uids)
+{
+ str_append(store_cmd, "UID STORE ");
+ str_append_str(store_cmd, uids);
+ str_append(store_cmd, " +FLAGS (\\Deleted)");
+ str_append(expunge_cmd, "UID EXPUNGE ");
+ str_append_str(expunge_cmd, uids);
+ /* Clear already appened uids */
+ str_truncate(uids, 0);
+}
+
+static void
+imapc_expunge_send_cmd_str(struct imapc_save_context *ctx,
+ string_t *uids)
+{
+ struct imapc_command *store_cmd, *expunge_cmd;
+
+ string_t *store_cmd_str, *expunge_cmd_str;
+ store_cmd_str = t_str_new(128);
+ expunge_cmd_str = t_str_new(128);
+
+ imapc_expunge_construct_cmd_str(store_cmd_str, expunge_cmd_str, uids);
+ /* Make sure line length is less than 8k */
+ i_assert(str_len(store_cmd_str) < IMAPC_SERVER_CMDLINE_MAX_LEN);
+ i_assert(str_len(expunge_cmd_str) < IMAPC_SERVER_CMDLINE_MAX_LEN);
+
+ store_cmd = imapc_client_mailbox_cmd(ctx->src_mbox->client_box,
+ imapc_copy_rollback_store_callback,
+ ctx);
+ expunge_cmd = imapc_client_mailbox_cmd(ctx->src_mbox->client_box,
+ imapc_copy_rollback_expunge_callback,
+ ctx);
+ ctx->src_mbox->rollback_pending = TRUE;
+ imapc_command_send(store_cmd, str_c(store_cmd_str));
+ imapc_command_send(expunge_cmd, str_c(expunge_cmd_str));
+}
+
+static void
+imapc_rollback_send_expunge(struct imapc_save_context *ctx)
+{
+ string_t *uids_str;
+ struct seqset_builder *seqset_builder;
+ struct seq_range_iter iter;
+ unsigned int i = 0;
+ uint32_t uid;
+
+ if (!array_not_empty(&ctx->src_mbox->copy_rollback_expunge_uids))
+ return;
+
+ uids_str = t_str_new(128);
+ seqset_builder = seqset_builder_init(uids_str);
+ seq_range_array_iter_init(&iter, &ctx->src_mbox->copy_rollback_expunge_uids);
+
+ /* Iterate over all uids that must be rolled back */
+ while (seq_range_array_iter_nth(&iter, i++, &uid)) {
+ /* Try to add the to the seqset builder while respecting
+ the maximum length of IMAPC_SERVER_CMDLINE_MAX_LEN. */
+ if (!seqset_builder_try_add(seqset_builder,
+ IMAPC_SERVER_CMDLINE_MAX_LEN -
+ strlen("UID STORE +FLAGS (\\Deleted)"),
+ uid)) {
+ /* Maximum length is reached send the rollback
+ and wait for it to be finished. */
+ imapc_expunge_send_cmd_str(ctx, uids_str);
+ while (ctx->src_mbox->rollback_pending)
+ imapc_mailbox_run_nofetch(ctx->src_mbox);
+
+ /* Truncate the uids_str and create a new
+ seqset_builder for the next command */
+ seqset_builder_deinit(&seqset_builder);
+ str_truncate(uids_str, 0);
+ seqset_builder = seqset_builder_init(uids_str);
+ /* Make sure the current uid which is part of
+ the next uid_str */
+ seqset_builder_add(seqset_builder, uid);
+ }
+ }
+ if (str_len(uids_str) > 0)
+ imapc_expunge_send_cmd_str(ctx, uids_str);
+ while (ctx->src_mbox->rollback_pending)
+ imapc_mailbox_run_nofetch(ctx->src_mbox);
+}
+
+static void imapc_copy_bulk_ctx_deinit(struct imapc_save_context *ctx)
+{
+ /* Clean up the pending copy and the context attached to it */
+ str_truncate(ctx->src_mbox->pending_copy_cmd, 0);
+ i_free(ctx->src_mbox->copy_dest_box);
+}
+
+void imapc_transaction_save_rollback(struct mail_save_context *_ctx)
+{
+ struct imapc_save_context *ctx = IMAPC_SAVECTX(_ctx);
+
+ if ((ctx->src_mbox != NULL && ctx->src_mbox->pending_copy_request != NULL) ||
+ !ctx->finished) {
+ /* There is still a pending copy which should not be send
+ as rollback() is called or the transaction has not yet
+ finished and rollback is called */
+ ctx->failed = TRUE;
+ (void)imapc_transaction_save_commit_pre(_ctx);
+
+ i_assert(ctx->finished || ctx->src_mbox != NULL);
+ /* Clean up the pending copy and the context attached to it */
+ if (ctx->src_mbox != NULL) {
+ if (ctx->src_mbox->pending_copy_request != NULL) {
+ seqset_builder_deinit(&ctx->src_mbox->pending_copy_request->uidset_builder);
+ i_free(ctx->src_mbox->pending_copy_request);
+ }
+ imapc_copy_bulk_ctx_deinit(ctx);
+ imapc_client_stop(ctx->src_mbox->storage->client->client);
+ }
+ }
+
+ /* Expunge all added messages from index */
+ if (ctx->failed && array_is_created(&ctx->dest_saved_uids)) {
+ i_assert(ctx->src_mbox != NULL);
+ seq_range_array_merge(&ctx->src_mbox->copy_rollback_expunge_uids, &ctx->dest_saved_uids);
+ /* Make sure context is not finished already */
+ ctx->finished = FALSE;
+ imapc_rollback_send_expunge(ctx);
+ array_free(&ctx->dest_saved_uids);
+ }
+
+ if (ctx->finished || ctx->failed) {
+ array_free(&ctx->dest_saved_uids);
+ i_free(ctx);
+ }
+}
+
+static bool imapc_save_copyuid(struct imapc_save_context *ctx,
+ const struct imapc_command_reply *reply,
+ uint32_t *uid_r)
+{
+ ARRAY_TYPE(seq_range) dest_uidset, source_uidset;
+ struct seq_range_iter iter;
+ const char *const *args;
+ uint32_t uid_validity;
+
+ *uid_r = 0;
+
+ /* <uidvalidity> <source uid-set> <dest uid-set> */
+ args = t_strsplit(reply->resp_text_value, " ");
+ if (str_array_length(args) != 3)
+ return FALSE;
+
+ if (str_to_uint32(args[0], &uid_validity) < 0)
+ return FALSE;
+ if (ctx->dest_uid_validity == 0)
+ ctx->dest_uid_validity = uid_validity;
+ else if (ctx->dest_uid_validity != uid_validity)
+ return FALSE;
+
+ t_array_init(&source_uidset, 8);
+ t_array_init(&dest_uidset, 8);
+
+ if (imap_seq_set_nostar_parse(args[1], &source_uidset) < 0)
+ return FALSE;
+ if (imap_seq_set_nostar_parse(args[2], &dest_uidset) < 0)
+ return FALSE;
+
+ if (!array_is_created(&ctx->dest_saved_uids))
+ i_array_init(&ctx->dest_saved_uids, 8);
+
+ seq_range_array_merge(&ctx->dest_saved_uids, &dest_uidset);
+
+ seq_range_array_iter_init(&iter, &dest_uidset);
+ (void)seq_range_array_iter_nth(&iter, 0, uid_r);
+ return TRUE;
+}
+
+static void imapc_copy_set_error(struct imapc_save_context *sctx,
+ const struct imapc_command_reply *reply)
+{
+ sctx->failed = TRUE;
+
+ if (reply->state != IMAPC_COMMAND_STATE_BAD)
+ imapc_copy_error_from_reply(sctx->mbox->storage,
+ MAIL_ERROR_PARAMS, reply);
+ else
+ mailbox_set_critical(&sctx->mbox->box,
+ "imapc: COPY failed: %s",
+ reply->text_full);
+}
+
+static void
+imapc_copy_simple_callback(const struct imapc_command_reply *reply,
+ void *context)
+{
+ struct imapc_save_cmd_context *ctx = context;
+ uint32_t uid = 0;
+
+ if (reply->state == IMAPC_COMMAND_STATE_OK) {
+ if (reply->resp_text_key != NULL &&
+ strcasecmp(reply->resp_text_key, "COPYUID") == 0)
+ imapc_save_copyuid(ctx->ctx, reply, &uid);
+ imapc_save_add_to_index(ctx->ctx, uid);
+ ctx->ret = 0;
+ } else if (reply->state == IMAPC_COMMAND_STATE_NO) {
+ imapc_copy_error_from_reply(ctx->ctx->mbox->storage,
+ MAIL_ERROR_PARAMS, reply);
+ ctx->ret = -1;
+ } else {
+ mailbox_set_critical(&ctx->ctx->mbox->box,
+ "imapc: COPY failed: %s", reply->text_full);
+ ctx->ret = -1;
+ }
+ imapc_client_stop(ctx->ctx->mbox->storage->client->client);
+}
+
+static int
+imapc_copy_simple(struct mail_save_context *_ctx, struct mail *mail)
+{
+ struct imapc_save_context *ctx = IMAPC_SAVECTX(_ctx);
+ struct mailbox_transaction_context *_t = _ctx->transaction;
+ struct imapc_save_cmd_context sctx;
+ struct imapc_command *cmd;
+
+ sctx.ret = -2;
+ sctx.ctx = ctx;
+ cmd = imapc_client_mailbox_cmd(ctx->src_mbox->client_box,
+ imapc_copy_simple_callback,
+ &sctx);
+ imapc_command_sendf(cmd, "UID COPY %u %s", mail->uid, _t->box->name);
+ while (sctx.ret == -2)
+ imapc_mailbox_run(ctx->src_mbox);
+ ctx->finished = TRUE;
+ return sctx.ret;
+}
+
+static void imapc_copy_bulk_callback(const struct imapc_command_reply *reply,
+ void *context)
+{
+ struct imapc_copy_request *request = context;
+ struct imapc_save_context *ctx = request->sctx;
+ struct imapc_mailbox *mbox = ctx->src_mbox;
+ unsigned int uid;
+
+ i_assert(mbox != NULL);
+ i_assert(request == mbox->pending_copy_request);
+
+ /* Check the reply state and add uid's to index and
+ dest_saved_uids. */
+ if (ctx->failed) {
+ /* If the saving already failed try to find UIDs already
+ copied from the reply so that rollback can expunge
+ them */
+ if (null_strcasecmp(reply->resp_text_key, "COPYUID") == 0) {
+ (void)imapc_save_copyuid(ctx, reply, &uid);
+ imapc_transaction_save_rollback(&ctx->ctx);
+ }
+ } else if (reply->state == IMAPC_COMMAND_STATE_OK) {
+ if (reply->resp_text_key != NULL &&
+ strcasecmp(reply->resp_text_key, "COPYUID") == 0 &&
+ imapc_save_copyuid(ctx, reply, &uid)) {
+ ctx->finished = TRUE;
+ }
+ } else {
+ imapc_copy_set_error(ctx, reply);
+ }
+
+ ctx->src_mbox->pending_copy_request = NULL;
+ i_free(request);
+ imapc_client_stop(mbox->storage->client->client);
+}
+
+static void imapc_mail_copy_bulk_flush(struct imapc_mailbox *mbox)
+{
+ struct imapc_command *cmd;
+
+ i_assert(mbox != NULL);
+ i_assert(mbox->pending_copy_request != NULL);
+ i_assert(mbox->client_box != NULL);
+
+ cmd = imapc_client_mailbox_cmd(mbox->client_box,
+ imapc_copy_bulk_callback,
+ mbox->pending_copy_request);
+
+ seqset_builder_deinit(&mbox->pending_copy_request->uidset_builder);
+
+ str_append(mbox->pending_copy_cmd, " ");
+ imap_append_astring(mbox->pending_copy_cmd, mbox->copy_dest_box);
+
+ imapc_command_send(cmd, str_c(mbox->pending_copy_cmd));
+
+ imapc_copy_bulk_ctx_deinit(mbox->pending_copy_request->sctx);
+}
+
+static bool
+imapc_mail_copy_bulk_try_merge(struct imapc_mailbox *mbox, uint32_t uid,
+ const char *box)
+{
+ i_assert(str_begins(str_c(mbox->pending_copy_cmd), "UID COPY "));
+
+ if (strcmp(box, mbox->copy_dest_box) != 0) {
+ /* Not the same mailbox merging not possible */
+ return FALSE;
+ }
+ return seqset_builder_try_add(mbox->pending_copy_request->uidset_builder,
+ IMAPC_SERVER_CMDLINE_MAX_LEN, uid);
+}
+
+static void
+imapc_mail_copy_bulk_delayed_send_or_merge(struct imapc_save_context *ctx,
+ uint32_t uid,
+ const char *box)
+{
+ struct imapc_mailbox *mbox = ctx->src_mbox;
+
+ if (mbox->pending_copy_request != NULL &&
+ !imapc_mail_copy_bulk_try_merge(mbox, uid, box)) {
+ /* send the previous COPY and create new one after
+ waiting for this one to be finished. */
+ imapc_mail_copy_bulk_flush(mbox);
+ imapc_copy_bulk_finish(mbox->pending_copy_request->sctx);
+ }
+ if (mbox->pending_copy_request == NULL) {
+ mbox->pending_copy_request =
+ i_new(struct imapc_copy_request, 1);
+ str_printfa(mbox->pending_copy_cmd, "UID COPY ");
+ mbox->pending_copy_request->uidset_builder =
+ seqset_builder_init(mbox->pending_copy_cmd);
+ seqset_builder_add(mbox->pending_copy_request->uidset_builder,
+ uid);
+ mbox->copy_dest_box = i_strdup(box);
+ } else {
+ i_assert(mbox->pending_copy_request->sctx == ctx);
+ }
+ mbox->pending_copy_request->sctx = ctx;
+}
+
+static int
+imapc_copy_bulk(struct imapc_save_context *ctx, struct mail *mail)
+{
+ struct imapc_mailbox *mbox = IMAPC_MAILBOX(ctx->ctx.transaction->box);
+
+ imapc_mail_copy_bulk_delayed_send_or_merge(ctx, mail->uid,
+ imapc_mailbox_get_remote_name(mbox));
+ imapc_save_add_to_index(ctx, 0);
+
+ return ctx->failed ? -1 : 0;
+}
+
+static bool imapc_is_mail_expunged(struct imapc_mailbox *mbox, uint32_t uid)
+{
+ if (array_is_created(&mbox->delayed_expunged_uids) &&
+ seq_range_exists(&mbox->delayed_expunged_uids, uid))
+ return TRUE;
+ if (mbox->delayed_sync_trans == NULL)
+ return FALSE;
+
+ struct mail_index_view *view =
+ mail_index_transaction_get_view(mbox->delayed_sync_trans);
+ uint32_t seq;
+ return mail_index_lookup_seq(view, uid, &seq) &&
+ mail_index_transaction_is_expunged(mbox->delayed_sync_trans, seq);
+}
+
+int imapc_copy(struct mail_save_context *_ctx, struct mail *mail)
+{
+ struct imapc_save_context *ctx = IMAPC_SAVECTX(_ctx);
+ struct mailbox_transaction_context *_t = _ctx->transaction;
+ struct imapc_msgmap *src_msgmap;
+ uint32_t rseq;
+ int ret;
+
+ i_assert((_t->flags & MAILBOX_TRANSACTION_FLAG_EXTERNAL) != 0);
+
+ if (_t->box->storage == mail->box->storage) {
+ /* Currently we don't support copying mails from multiple
+ different source mailboxes within the same transaction. */
+ i_assert(ctx->src_mbox == NULL || &ctx->src_mbox->box == mail->box);
+ ctx->src_mbox = IMAPC_MAILBOX(mail->box);
+ if (!mail->expunged && imapc_is_mail_expunged(ctx->mbox, mail->uid))
+ mail_set_expunged(mail);
+ /* same server, we can use COPY for the mail */
+ src_msgmap =
+ imapc_client_mailbox_get_msgmap(ctx->src_mbox->client_box);
+ if (mail->expunged ||
+ !imapc_msgmap_uid_to_rseq(src_msgmap, mail->uid, &rseq)) {
+ mail_storage_set_error(mail->box->storage,
+ MAIL_ERROR_EXPUNGED,
+ "Some of the requested messages no longer exist.");
+ ctx->finished = TRUE;
+ index_save_context_free(_ctx);
+ return -1;
+ }
+ /* Mail has not been expunged and can be copied. */
+ if (ctx->mbox->capabilities == 0) {
+ /* The destination mailbox has not yet been selected
+ so the capabilities are unknown */
+ if (imapc_client_get_capabilities(ctx->mbox->storage->client->client,
+ &ctx->mbox->capabilities) < 0) {
+ mail_storage_set_error(mail->box->storage,
+ MAIL_ERROR_UNAVAILABLE,
+ "Failed to determine capabilities for mailbox.");
+ ctx->finished = TRUE;
+ index_save_context_free(_ctx);
+ return -1;
+ }
+ }
+ if ((ctx->mbox->capabilities & IMAPC_CAPABILITY_UIDPLUS) != 0)
+ ret = imapc_copy_bulk(ctx, mail);
+ else
+ ret = imapc_copy_simple(_ctx, mail);
+ index_save_context_free(_ctx);
+ return ret;
+ }
+ return mail_storage_copy(_ctx, mail);
+}