Skip to content

Commit

Permalink
IMAP responses to APPEND and EXPUNGE should include `HIGHESTMODSE…
Browse files Browse the repository at this point in the history
…Q` when `CONDSTORE` is enabled.
  • Loading branch information
mdecimus committed Jan 3, 2024
1 parent 7152dcd commit 172c8af
Show file tree
Hide file tree
Showing 6 changed files with 65 additions and 19 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
31 changes: 26 additions & 5 deletions crates/imap-proto/src/protocol/select.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
}

Expand Down Expand Up @@ -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());
Expand All @@ -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"* ");
Expand All @@ -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 [
Expand All @@ -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",
Expand Down
21 changes: 17 additions & 4 deletions crates/imap/src/op/append.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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) {
Expand Down Expand Up @@ -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
Expand All @@ -184,7 +196,7 @@ impl SessionData {
)
}

_ => {
_ if self.imap.enable_uidplus => {
let mailbox = self
.fetch_messages(&mailbox)
.await
Expand All @@ -198,6 +210,7 @@ impl SessionData {
mailbox.uid_validity,
)
}
_ => (vec![], 0),
};
if !uids.is_empty() {
response = response.with_code(ResponseCode::AppendUid { uid_validity, uids });
Expand Down
20 changes: 13 additions & 7 deletions crates/imap/src/op/expunge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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())
Expand Down
9 changes: 7 additions & 2 deletions crates/imap/src/op/select.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand Down Expand Up @@ -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
};
Expand Down
2 changes: 1 addition & 1 deletion resources/config/imap/settings.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ idle = "30m"

[imap.rate-limit]
requests = "2000/1m"
concurrent = 4
concurrent = 6

[imap.protocol]
uidplus = false

0 comments on commit 172c8af

Please sign in to comment.