From 2c04d4473c6dd44c4a8afd5ae4a4fec37bedc33e Mon Sep 17 00:00:00 2001 From: Maurus Decimus <11444311+mdecimus@users.noreply.github.com> Date: Sun, 19 Jul 2026 16:02:20 +0200 Subject: [PATCH] Fix IMAP: Pipelined `STORE` and `EXPUNGE` can execute out of order --- CHANGELOG.md | 4 +++- crates/imap/src/core/client.rs | 5 ++++- crates/imap/src/op/store.rs | 13 +++++++++++-- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b07c6f6..67954903 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,9 @@ If you are upgrading from v0.16.x, replace the binary (or run `docker pull`). If ## Changed ## Fixed -- IMAP: Mailbox object-quota only enforced in JMAP. +- IMAP: + - Mailbox object-quota only enforced in JMAP. + - Pipelined `STORE` and `EXPUNGE` can execute out of order. - JMAP: - Read-only sharee cannot set `isSubscribed` on a shared mailbox. - Web Push payloads with `Content-Encoding: aes128gcm` should not be base64-encoded but sent as raw bytes. diff --git a/crates/imap/src/core/client.rs b/crates/imap/src/core/client.rs index a3a703b9..bd3cb9f9 100644 --- a/crates/imap/src/core/client.rs +++ b/crates/imap/src/core/client.rs @@ -30,11 +30,14 @@ impl Session { let mut bytes = bytes.iter(); let mut requests = Vec::with_capacity(2); let mut needs_literal = None; + let mut has_expunge = false; loop { match self.receiver.parse(&mut bytes) { Ok(request) => match self.is_allowed(request).await { Ok(request) => { + has_expunge |= + matches!(request.command, Command::Expunge(_) | Command::Close); requests.push(request); } Err(err) => { @@ -140,7 +143,7 @@ impl Session { .await .map(|_| SessionResult::Continue), Command::Store(is_uid) => self - .handle_store(request, is_uid) + .handle_store(request, is_uid, !has_expunge) .await .map(|_| SessionResult::Continue), Command::Copy(is_uid) => self diff --git a/crates/imap/src/op/store.rs b/crates/imap/src/op/store.rs index f1c9b096..40837cac 100644 --- a/crates/imap/src/op/store.rs +++ b/crates/imap/src/op/store.rs @@ -43,6 +43,7 @@ impl Session { &mut self, request: Request, is_uid: bool, + spawn: bool, ) -> trc::Result<()> { // Validate access self.assert_has_permission(Permission::ImapStore)?; @@ -52,13 +53,21 @@ impl Session { let (data, mailbox) = self.state.select_data(); let is_condstore = self.is_condstore || mailbox.is_condstore; - spawn_op!(data, { + if spawn { + spawn_op!(data, { + let response = data + .store(arguments, mailbox, is_uid, is_condstore, op_start) + .await?; + + data.write_bytes(response).await + }) + } else { let response = data .store(arguments, mailbox, is_uid, is_condstore, op_start) .await?; data.write_bytes(response).await - }) + } } }