From e17ca1fc99d0f5caf33edb030f062a94d94cc475 Mon Sep 17 00:00:00 2001 From: Aki Tuomi Date: Wed, 13 May 2026 15:33:08 +0000 Subject: [PATCH] quota: Fix maildir quota drop to zero after IMAP MOVE/REPLACE 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 --- src/plugins/quota/quota.c | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/src/plugins/quota/quota.c b/src/plugins/quota/quota.c index 9a41b9ceec..ddddc21e5d 100644 --- a/src/plugins/quota/quota.c +++ b/src/plugins/quota/quota.c @@ -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; } -- 2.47.3