IMAP responses to APPEND and EXPUNGE should include HIGHESTMODSEQ when CONDSTORE is enabled.

This commit is contained in:
mdecimus 2024-01-03 14:54:01 +01:00
parent 7152dcdb3a
commit 172c8afae0
6 changed files with 65 additions and 19 deletions

View file

@ -10,6 +10,7 @@ All notable changes to this project will be documented in this file. This projec
### Fixed
- IMAP command `SEARCH <seqnum>` 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

View file

@ -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<u64>,
pub highest_modseq: Option<HighestModSeq>,
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<u8>) {
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<u8> {
let mut buf = Vec::with_capacity(40);
self.serialize(&mut buf);
buf
}
}
impl Exists {
pub fn serialize(&self, buf: &mut Vec<u8>) {
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",

View file

@ -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<T: AsyncRead> Session<T> {
pub async fn handle_append(&mut self, request: Request<Command>) -> 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 });

View file

@ -43,6 +43,8 @@ use tokio::io::AsyncRead;
use crate::core::{ImapId, SavedSearch, SelectedMailbox, Session, SessionData};
use super::ToModSeq;
impl<T: AsyncRead> Session<T> {
pub async fn handle_expunge(
&mut self,
@ -108,13 +110,17 @@ impl<T: AsyncRead> Session<T> {
// 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())

View file

@ -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<T: AsyncRead> Session<T> {
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
};

View file

@ -19,7 +19,7 @@ idle = "30m"
[imap.rate-limit]
requests = "2000/1m"
concurrent = 4
concurrent = 6
[imap.protocol]
uidplus = false