]> git.ipfire.org Git - thirdparty/dovecot/core.git/commitdiff
imap: copy: Abort if client disconnects during COPY
authorTimo Sirainen <timo.sirainen@open-xchange.com>
Thu, 29 Apr 2021 14:00:47 +0000 (17:00 +0300)
committeraki.tuomi <aki.tuomi@open-xchange.com>
Tue, 1 Jun 2021 17:19:32 +0000 (17:19 +0000)
Since the client didn't receive the COPY tagged reply, it doesn't know
whether the COPY succeeded or not. This likely causes it to try the COPY
again and duplicate the mails.

src/imap/cmd-copy.c

index 0a65c134073f7d3a874d58ffd45ae3ba147be7ea..59a4c7ef7538b73f0ec0e024408fbc3cdb39a43c 100644 (file)
@@ -2,6 +2,7 @@
 
 #include "imap-common.h"
 #include "str.h"
+#include "istream.h"
 #include "ostream.h"
 #include "imap-resp-code.h"
 #include "imap-util.h"
@@ -30,22 +31,25 @@ struct cmd_copy_context {
        enum mail_error mail_error;
 };
 
-static void client_send_sendalive_if_needed(struct client *client)
+static int client_send_sendalive_if_needed(struct client *client)
 {
        time_t now, last_io;
+       int ret = 0;
 
        if (o_stream_get_buffer_used_size(client->output) != 0)
-               return;
+               return 0;
 
        now = time(NULL);
        last_io = I_MAX(client->last_input, client->last_output);
        if (now - last_io > MAIL_STORAGE_STAYALIVE_SECS) {
                o_stream_nsend_str(client->output, "* OK Hang in there..\r\n");
                /* make sure it doesn't get stuck on the corked stream */
-               o_stream_uncork(client->output);
+               if (o_stream_uncork_flush(client->output) < 0)
+                       ret = -1;
                o_stream_cork(client->output);
                client->last_output = now;
        }
+       return ret;
 }
 
 static void copy_update_trashed(struct client *client, struct mailbox *box,
@@ -61,6 +65,22 @@ static void copy_update_trashed(struct client *client, struct mailbox *box,
                client->trashed_count += count;
 }
 
+static bool client_is_disconnected(struct client *client)
+{
+       if (client->fd_in == STDIN_FILENO) {
+               /* Skip this check for stdio clients. It's often used in
+                  testing where the test expects that all commands will be
+                  run even though stdin already has reached EOF. */
+               return FALSE;
+       }
+       ssize_t bytes = i_stream_read(client->input);
+       if (bytes == -1)
+               return TRUE;
+       if (bytes != 0)
+               i_stream_set_input_pending(client->input, TRUE);
+       return FALSE;
+}
+
 static int fetch_and_copy(struct cmd_copy_context *copy_ctx,
                          const struct mail_search_args *uid_search_args)
 {
@@ -114,8 +134,22 @@ static int fetch_and_copy(struct cmd_copy_context *copy_ctx,
                        break;
                }
 
-               if ((++copy_ctx->copy_count % COPY_CHECK_INTERVAL) == 0)
-                       client_send_sendalive_if_needed(client);
+               if ((++copy_ctx->copy_count % COPY_CHECK_INTERVAL) == 0) {
+                       /* If we're COPYing (not MOVEing), check if client has
+                          already disconnected. If yes, abort the COPY to
+                          avoid client duplicating the COPY again later.
+                          We can detect this as long as the client doesn't
+                          fill the input buffer full. */
+                       if (client_send_sendalive_if_needed(client) < 0 ||
+                           (!copy_ctx->move &&
+                            client_is_disconnected(client))) {
+                               /* Client disconnected. Use the same failure
+                                  code path as if some messages were
+                                  expunged. */
+                               ret = 0;
+                               break;
+                       }
+               }
 
                save_ctx = mailbox_save_alloc(t);
                mailbox_save_copy_flags(save_ctx, mail);
@@ -144,6 +178,11 @@ static int fetch_and_copy(struct cmd_copy_context *copy_ctx,
                ret = -1;
        }
 
+       /* Do a final check before committing COPY to see if the client has
+          already disconnected. */
+       if (!copy_ctx->move && client_is_disconnected(client))
+               ret = 0;
+
        if (ret <= 0)
                mailbox_transaction_rollback(&t);
        else if (mailbox_transaction_commit_get_changes(&t, &changes) < 0) {