diff --git a/src/plugins/quota/quota-private.h b/src/plugins/quota/quota-private.h index 4461dc9785..cb5466a29b 100644 --- a/src/plugins/quota/quota-private.h +++ b/src/plugins/quota/quota-private.h @@ -165,6 +165,11 @@ struct quota_transaction_context { bool auto_updating:1; /* Quota doesn't need to be updated within this transaction. */ bool no_quota_updates:1; + /* TRUE if this transaction is for an IMAP MOVE operation. Used by + quota_try_alloc() to avoid double-counting the expunged source + mail: quota_mail_expunge() already accounts for it in the source + transaction via quota_free_bytes(). */ + bool moving:1; }; void quota_add_user_namespace(struct quota *quota, const char *root_name, diff --git a/src/plugins/quota/quota-storage.c b/src/plugins/quota/quota-storage.c index a20325843e..120492ccf6 100644 --- a/src/plugins/quota/quota-storage.c +++ b/src/plugins/quota/quota-storage.c @@ -255,6 +255,7 @@ static int quota_check(struct mail_save_context *ctx) if (qt->failed) return 0; + qt->moving = ctx->moving; const char *error; ret = quota_try_alloc(qt, ctx->dest_mail, ctx->expunged_mail, NULL, &error); diff --git a/src/plugins/quota/quota.c b/src/plugins/quota/quota.c index 459d881a9b..6253d63df8 100644 --- a/src/plugins/quota/quota.c +++ b/src/plugins/quota/quota.c @@ -1180,7 +1180,14 @@ 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); + /* For IMAP MOVE, do not subtract expunged_size here: the source mail + expunge is already accounted for by quota_mail_expunge() -> + quota_free_bytes() in the source transaction. Subtracting it here + too causes double-counting (destination records bytes_used=0, source + records bytes_used=-size, net result: quota drops by size). + expunged_size is still passed to quota_test_alloc() above so that + moves at the quota boundary are correctly permitted. */ + quota_alloc_with_size(ctx, size, ctx->moving ? 0 : expunged_size); return QUOTA_ALLOC_RESULT_OK; }