]> git.ipfire.org Git - thirdparty/dovecot/core.git/commitdiff
quota: Fix maildir quota drop to zero after IMAP MOVE/REPLACE
authorAki Tuomi <aki.tuomi@open-xchange.com>
Wed, 13 May 2026 15:33:08 +0000 (15:33 +0000)
committerAki Tuomi <aki.tuomi@open-xchange.com>
Wed, 13 May 2026 15:33:21 +0000 (15:33 +0000)
When mailbox_move() or IMAP REPLACE is used with the maildir quota
driver, the source mail expunge is double-counted, causing the quota
to drop to zero.

quota_try_alloc() called quota_alloc_with_size(ctx, size, expunged_size),
which subtracted expunged_size from the destination transaction's
bytes_used. At the same time mail_expunge() on the source/expunged mail
fired quota_mail_expunge(), and the expunge was committed independently
via qbox->expunge_qt in sync_notify. Both deltas commit to the same
quota root, netting -size when size == expunged_size.

Co-Authored: Aleksei Fedorov <aleksei.fedorov@webpros.com>

src/plugins/quota/quota.c

index 9a41b9ceecf04025dfccef41f797c6a74609d51d..ddddc21e5d62c448696e6ebc588f792befda4bec 100644 (file)
@@ -1180,7 +1180,33 @@ quota_try_alloc(struct quota_transaction_context *ctx,
           quota_alloc() or quota_free_bytes() was already used within the same
           transaction, but that doesn't normally happen. */
        ctx->auto_updating = FALSE;
-       quota_alloc_with_size(ctx, size, expunged_size);
+       /* Do not subtract expunged_size from the persistent counter here:
+          the expunge is tracked separately via quota_mail_expunge() ->
+          qbox->expunge_qt (committed independently via sync_notify), so
+          subtracting it again would double-count. expunged_size is still
+          passed to quota_test_alloc() above so that moves/replaces at the
+          quota boundary are correctly permitted. */
+       quota_alloc_with_size(ctx, size, 0);
+
+       /* Record the pending expunge per visible root so that subsequent
+          quota_try_alloc() calls in the same transaction (e.g. a batch
+          MOVE/REPLACE of several mails at the quota boundary) see the
+          accumulated expunged amount in their limit calculations. The
+          actual quota decrement still happens via qbox->expunge_qt. */
+       if (expunged_mail != NULL && expunged_box != NULL) {
+               struct quota_root *const *roots;
+               unsigned int i, count;
+
+               roots = array_get(&ctx->quota->all_roots, &count);
+               for (i = 0; i < count; i++) {
+                       if (!quota_root_is_visible(roots[i], ctx->box) ||
+                           !quota_root_is_visible(roots[i], expunged_box))
+                               continue;
+                       quota_transaction_root_expunged(&ctx->roots[i], 1,
+                                                       expunged_size);
+               }
+               quota_transaction_update_expunged(ctx);
+       }
        return QUOTA_ALLOC_RESULT_OK;
 }