mirror of
https://github.com/stalwartlabs/mail-server.git
synced 2025-09-14 07:54:34 +08:00
IMAP responses to APPEND
and EXPUNGE
should include HIGHESTMODSEQ
when CONDSTORE
is enabled.
This commit is contained in:
parent
7152dcdb3a
commit
172c8afae0
6 changed files with 65 additions and 19 deletions
|
@ -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
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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 });
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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
|
||||
};
|
||||
|
|
|
@ -19,7 +19,7 @@ idle = "30m"
|
|||
|
||||
[imap.rate-limit]
|
||||
requests = "2000/1m"
|
||||
concurrent = 4
|
||||
concurrent = 6
|
||||
|
||||
[imap.protocol]
|
||||
uidplus = false
|
||||
|
|
Loading…
Add table
Reference in a new issue