diff --git a/CHANGELOG.md b/CHANGELOG.md index 82e9db41b..001d28ce9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ All notable changes to this project will be documented in this file. This projec ### Fixed - IMAP command `SEARCH ` is using UIDs rather than sequence numbers. +- IMAP responses to `APPEND` and `EXPUNGE` should include `HIGHESTMODSEQ` when `CONDSTORE` is enabled. ## [0.5.1] - 2024-01-02 diff --git a/crates/imap-proto/src/protocol/select.rs b/crates/imap-proto/src/protocol/select.rs index fbb7e3932..3da1c859f 100644 --- a/crates/imap-proto/src/protocol/select.rs +++ b/crates/imap-proto/src/protocol/select.rs @@ -41,6 +41,9 @@ pub struct QResync { pub seq_match: Option<(Sequence, Sequence)>, } +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct HighestModSeq(u64); + #[derive(Debug, Clone)] pub struct Response { pub mailbox: ListItem, @@ -51,7 +54,7 @@ pub struct Response { pub uid_next: u32, pub is_rev2: bool, pub closed_previous: bool, - pub highest_modseq: Option, + pub highest_modseq: Option, pub mailbox_id: String, } @@ -100,9 +103,7 @@ impl ImapResponse for Response { buf.extend_from_slice(self.uid_next.to_string().as_bytes()); buf.extend_from_slice(b"] Next predicted UID\r\n"); if let Some(highest_modseq) = self.highest_modseq { - buf.extend_from_slice(b"* OK [HIGHESTMODSEQ "); - buf.extend_from_slice(highest_modseq.to_string().as_bytes()); - buf.extend_from_slice(b"] Highest Modseq\r\n"); + highest_modseq.serialize(&mut buf); } buf.extend_from_slice(b"* OK [MAILBOXID ("); buf.extend_from_slice(self.mailbox_id.as_bytes()); @@ -111,6 +112,24 @@ impl ImapResponse for Response { } } +impl HighestModSeq { + pub fn new(modseq: u64) -> Self { + Self(modseq) + } + + pub fn serialize(&self, buf: &mut Vec) { + buf.extend_from_slice(b"* OK [HIGHESTMODSEQ "); + buf.extend_from_slice(self.0.to_string().as_bytes()); + buf.extend_from_slice(b"] Highest Modseq\r\n"); + } + + pub fn into_bytes(self) -> Vec { + let mut buf = Vec::with_capacity(40); + self.serialize(&mut buf); + buf + } +} + impl Exists { pub fn serialize(&self, buf: &mut Vec) { buf.extend_from_slice(b"* "); @@ -129,6 +148,8 @@ impl Exists { mod tests { use crate::protocol::{list::ListItem, ImapResponse}; + use super::HighestModSeq; + #[test] fn serialize_select() { for (mut response, _tag, expected_v2, expected_v1) in [ @@ -142,7 +163,7 @@ mod tests { uid_next: 4392, closed_previous: false, is_rev2: true, - highest_modseq: 100.into(), + highest_modseq: HighestModSeq::new(100).into(), mailbox_id: "abc".into(), }, "A142", diff --git a/crates/imap/src/op/append.rs b/crates/imap/src/op/append.rs index 1c8568188..618f647eb 100644 --- a/crates/imap/src/op/append.rs +++ b/crates/imap/src/op/append.rs @@ -24,7 +24,9 @@ use std::sync::Arc; use imap_proto::{ - protocol::append::Arguments, receiver::Request, Command, ResponseCode, StatusResponse, + protocol::{append::Arguments, select::HighestModSeq}, + receiver::Request, + Command, ResponseCode, StatusResponse, }; use jmap::email::ingest::IngestEmail; @@ -34,6 +36,8 @@ use tokio::io::AsyncRead; use crate::core::{MailboxId, SelectedMailbox, Session, SessionData}; +use super::ToModSeq; + impl Session { pub async fn handle_append(&mut self, request: Request) -> crate::OpResult { match request.parse_append(self.version) { @@ -167,12 +171,20 @@ impl SessionData { .await; } - if !created_ids.is_empty() && self.imap.enable_uidplus { + if !created_ids.is_empty() { let (uids, uid_validity) = match selected_mailbox { Some(selected_mailbox) if selected_mailbox.id == mailbox => { - self.write_mailbox_changes(&selected_mailbox, is_qresync) + let modseq = self + .write_mailbox_changes(&selected_mailbox, is_qresync) .await .map_err(|r| r.with_tag(&arguments.tag))?; + + // Write updated modseq + if is_qresync { + self.write_bytes(HighestModSeq::new(modseq.to_modseq()).into_bytes()) + .await; + } + let mailbox = selected_mailbox.state.lock(); ( created_ids @@ -184,7 +196,7 @@ impl SessionData { ) } - _ => { + _ if self.imap.enable_uidplus => { let mailbox = self .fetch_messages(&mailbox) .await @@ -198,6 +210,7 @@ impl SessionData { mailbox.uid_validity, ) } + _ => (vec![], 0), }; if !uids.is_empty() { response = response.with_code(ResponseCode::AppendUid { uid_validity, uids }); diff --git a/crates/imap/src/op/expunge.rs b/crates/imap/src/op/expunge.rs index 32ebf1a9b..5d30e4f22 100644 --- a/crates/imap/src/op/expunge.rs +++ b/crates/imap/src/op/expunge.rs @@ -43,6 +43,8 @@ use tokio::io::AsyncRead; use crate::core::{ImapId, SavedSearch, SelectedMailbox, Session, SessionData}; +use super::ToModSeq; + impl Session { pub async fn handle_expunge( &mut self, @@ -108,13 +110,17 @@ impl Session { // Synchronize messages match data.write_mailbox_changes(&mailbox, self.is_qresync).await { - Ok(_) => { - self.write_bytes( - StatusResponse::completed(Command::Expunge(is_uid)) - .with_tag(request.tag) - .into_bytes(), - ) - .await + Ok(modseq) => { + let mut response = + StatusResponse::completed(Command::Expunge(is_uid)).with_tag(request.tag); + + if self.is_condstore { + response = response.with_code(ResponseCode::HighestModseq { + modseq: modseq.to_modseq(), + }); + } + + self.write_bytes(response.into_bytes()).await } Err(response) => { self.write_bytes(response.with_tag(request.tag).into_bytes()) diff --git a/crates/imap/src/op/select.rs b/crates/imap/src/op/select.rs index 230598cbd..e3375a8b1 100644 --- a/crates/imap/src/op/select.rs +++ b/crates/imap/src/op/select.rs @@ -24,7 +24,12 @@ use std::sync::Arc; use imap_proto::{ - protocol::{fetch, list::ListItem, select::Response, ImapResponse, Sequence}, + protocol::{ + fetch, + list::ListItem, + select::{HighestModSeq, Response}, + ImapResponse, Sequence, + }, receiver::Request, Command, ResponseCode, StatusResponse, }; @@ -64,7 +69,7 @@ impl Session { let uid_next = state.uid_next; let total_messages = state.total_messages; let highest_modseq = if is_condstore { - state.modseq.to_modseq().into() + HighestModSeq::new(state.modseq.to_modseq()).into() } else { None }; diff --git a/resources/config/imap/settings.toml b/resources/config/imap/settings.toml index a8573d497..6ffecc6a7 100644 --- a/resources/config/imap/settings.toml +++ b/resources/config/imap/settings.toml @@ -19,7 +19,7 @@ idle = "30m" [imap.rate-limit] requests = "2000/1m" -concurrent = 4 +concurrent = 6 [imap.protocol] uidplus = false