From 5dfa02e381154c03adee33262e247797d9a2f8ff Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Tue, 9 Jan 2024 16:53:32 +0100 Subject: [PATCH 01/21] Disable UNSEEN again as it was a volunteer decision to not implement it --- src/imap/capability.rs | 2 +- src/imap/command/authenticated.rs | 1 + src/imap/mailbox_view.rs | 6 ++++-- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/imap/capability.rs b/src/imap/capability.rs index feadb6b..21b95cb 100644 --- a/src/imap/capability.rs +++ b/src/imap/capability.rs @@ -26,7 +26,7 @@ impl Default for ServerCapability { Capability::Move, Capability::LiteralPlus, capability_unselect(), - //capability_condstore(), + capability_condstore(), //capability_qresync(), ])) } diff --git a/src/imap/command/authenticated.rs b/src/imap/command/authenticated.rs index 1481a80..8e5b2e6 100644 --- a/src/imap/command/authenticated.rs +++ b/src/imap/command/authenticated.rs @@ -404,6 +404,7 @@ impl<'a> AuthenticatedContext<'a> { it is therefore correct to not return it even if there are unseen messages RFC9051 (imap4rev2) says that OK [UNSEEN] responses are deprecated after SELECT and EXAMINE For Aerogramme, we just don't send the OK [UNSEEN], it's correct to do in both specifications. + 20 select "INBOX.achats" * FLAGS (\Answered \Flagged \Deleted \Seen \Draft $Forwarded JUNK $label1) diff --git a/src/imap/mailbox_view.rs b/src/imap/mailbox_view.rs index 513567f..e9f85a6 100644 --- a/src/imap/mailbox_view.rs +++ b/src/imap/mailbox_view.rs @@ -130,8 +130,8 @@ impl MailboxView { data.extend(self.flags_status()?.into_iter()); data.push(self.uidvalidity_status()?); data.push(self.uidnext_status()?); - self.unseen_first_status()? - .map(|unseen_status| data.push(unseen_status)); + /*self.unseen_first_status()? + .map(|unseen_status| data.push(unseen_status));*/ Ok(data) } @@ -403,6 +403,7 @@ impl MailboxView { Ok(Body::Data(Data::Recent(self.recent()?))) } + #[allow(dead_code)] fn unseen_first_status(&self) -> Result>> { Ok(self .unseen_first()? @@ -412,6 +413,7 @@ impl MailboxView { .transpose()?) } + #[allow(dead_code)] fn unseen_first(&self) -> Result> { Ok(self .0 From 6e798b90f590e21bb68535f0431fc547e5e2390c Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Tue, 9 Jan 2024 17:40:23 +0100 Subject: [PATCH 02/21] prepare condstore --- src/imap/capability.rs | 12 ++++-- src/imap/command/authenticated.rs | 6 +-- src/imap/command/examined.rs | 2 +- src/imap/command/selected.rs | 2 +- src/imap/mailbox_view.rs | 71 +++++++++++++++++++------------ 5 files changed, 58 insertions(+), 35 deletions(-) diff --git a/src/imap/capability.rs b/src/imap/capability.rs index 21b95cb..d88673c 100644 --- a/src/imap/capability.rs +++ b/src/imap/capability.rs @@ -48,15 +48,21 @@ impl ServerCapability { } } -enum ClientStatus { +pub enum ClientStatus { NotSupportedByServer, Disabled, Enabled, } +impl ClientStatus { + pub fn is_enabled(&self) -> bool { + matches!(self, Self::Enabled) + } +} + pub struct ClientCapability { - condstore: ClientStatus, - utf8kind: Option, + pub condstore: ClientStatus, + pub utf8kind: Option, } impl ClientCapability { diff --git a/src/imap/command/authenticated.rs b/src/imap/command/authenticated.rs index 8e5b2e6..954e758 100644 --- a/src/imap/command/authenticated.rs +++ b/src/imap/command/authenticated.rs @@ -292,7 +292,7 @@ impl<'a> AuthenticatedContext<'a> { } }; - let view = MailboxView::new(mb).await; + let view = MailboxView::new(mb, self.client_capabilities.condstore.is_enabled()).await; let mut ret_attrs = vec![]; for attr in attributes.iter() { @@ -439,7 +439,7 @@ impl<'a> AuthenticatedContext<'a> { }; tracing::info!(username=%self.user.username, mailbox=%name, "mailbox.selected"); - let mb = MailboxView::new(mb).await; + let mb = MailboxView::new(mb, self.client_capabilities.condstore.is_enabled()).await; let data = mb.summary()?; Ok(( @@ -474,7 +474,7 @@ impl<'a> AuthenticatedContext<'a> { }; tracing::info!(username=%self.user.username, mailbox=%name, "mailbox.examined"); - let mb = MailboxView::new(mb).await; + let mb = MailboxView::new(mb, self.client_capabilities.condstore.is_enabled()).await; let data = mb.summary()?; Ok(( diff --git a/src/imap/command/examined.rs b/src/imap/command/examined.rs index 3dd11e2..4767340 100644 --- a/src/imap/command/examined.rs +++ b/src/imap/command/examined.rs @@ -127,7 +127,7 @@ impl<'a> ExaminedContext<'a> { } pub async fn noop(self) -> Result<(Response<'static>, flow::Transition)> { - self.mailbox.0.mailbox.force_sync().await?; + self.mailbox.internal.mailbox.force_sync().await?; let updates = self.mailbox.update().await?; Ok(( diff --git a/src/imap/command/selected.rs b/src/imap/command/selected.rs index 35c3eb4..c38c5d3 100644 --- a/src/imap/command/selected.rs +++ b/src/imap/command/selected.rs @@ -152,7 +152,7 @@ impl<'a> SelectedContext<'a> { } pub async fn noop(self) -> Result<(Response<'static>, flow::Transition)> { - self.mailbox.0.mailbox.force_sync().await?; + self.mailbox.internal.mailbox.force_sync().await?; let updates = self.mailbox.update().await?; Ok(( diff --git a/src/imap/mailbox_view.rs b/src/imap/mailbox_view.rs index e9f85a6..046acfa 100644 --- a/src/imap/mailbox_view.rs +++ b/src/imap/mailbox_view.rs @@ -8,7 +8,7 @@ use futures::stream::{FuturesOrdered, StreamExt}; use imap_codec::imap_types::core::Charset; use imap_codec::imap_types::fetch::{MacroOrMessageDataItemNames, MessageDataItem}; use imap_codec::imap_types::flag::{Flag, FlagFetch, FlagPerm, StoreResponse, StoreType}; -use imap_codec::imap_types::response::{Code, Data, Status}; +use imap_codec::imap_types::response::{Code, CodeOther, Data, Status}; use imap_codec::imap_types::search::SearchKey; use imap_codec::imap_types::sequence::SequenceSet; @@ -39,12 +39,18 @@ const DEFAULT_FLAGS: [Flag; 5] = [ /// To do this, it keeps a variable `known_state` that corresponds to /// what the client knows, and produces IMAP messages to be sent to the /// client that go along updates to `known_state`. -pub struct MailboxView(pub FrozenMailbox); +pub struct MailboxView { + pub internal: FrozenMailbox, + pub is_condstore: bool, +} impl MailboxView { /// Creates a new IMAP view into a mailbox. - pub async fn new(mailbox: Arc) -> Self { - Self(mailbox.frozen().await) + pub async fn new(mailbox: Arc, is_cond: bool) -> Self { + Self { + internal: mailbox.frozen().await, + is_condstore: is_cond, + } } /// Create an updated view, useful to make a diff @@ -54,8 +60,8 @@ impl MailboxView { /// This does NOT trigger a sync, it bases itself on what is currently /// loaded in RAM by Bayou. pub async fn update(&mut self) -> Result>> { - let old_snapshot = self.0.update().await; - let new_snapshot = &self.0.snapshot; + let old_snapshot = self.internal.update().await; + let new_snapshot = &self.internal.snapshot; let mut data = Vec::::new(); @@ -130,6 +136,9 @@ impl MailboxView { data.extend(self.flags_status()?.into_iter()); data.push(self.uidvalidity_status()?); data.push(self.uidnext_status()?); + if self.is_condstore { + data.push(self.highestmodseq_status()?); + } /*self.unseen_first_status()? .map(|unseen_status| data.push(unseen_status));*/ @@ -144,7 +153,7 @@ impl MailboxView { flags: &[Flag<'a>], is_uid_store: &bool, ) -> Result>> { - self.0.sync().await?; + self.internal.sync().await?; let flags = flags.iter().map(|x| x.to_string()).collect::>(); @@ -153,13 +162,13 @@ impl MailboxView { for mi in mails.iter() { match kind { StoreType::Add => { - self.0.mailbox.add_flags(mi.uuid, &flags[..]).await?; + self.internal.mailbox.add_flags(mi.uuid, &flags[..]).await?; } StoreType::Remove => { - self.0.mailbox.del_flags(mi.uuid, &flags[..]).await?; + self.internal.mailbox.del_flags(mi.uuid, &flags[..]).await?; } StoreType::Replace => { - self.0.mailbox.set_flags(mi.uuid, &flags[..]).await?; + self.internal.mailbox.set_flags(mi.uuid, &flags[..]).await?; } } } @@ -169,8 +178,8 @@ impl MailboxView { } pub async fn expunge(&mut self) -> Result>> { - self.0.sync().await?; - let state = self.0.peek().await; + self.internal.sync().await?; + let state = self.internal.peek().await; let deleted_flag = Flag::Deleted.to_string(); let msgs = state @@ -180,7 +189,7 @@ impl MailboxView { .map(|(uuid, _)| *uuid); for msg in msgs { - self.0.mailbox.delete(msg).await?; + self.internal.mailbox.delete(msg).await?; } self.update().await @@ -197,7 +206,7 @@ impl MailboxView { let mut new_uuids = vec![]; for mi in mails.iter() { - new_uuids.push(to.copy_from(&self.0.mailbox, mi.uuid).await?); + new_uuids.push(to.copy_from(&self.internal.mailbox, mi.uuid).await?); } let mut ret = vec![]; @@ -224,7 +233,7 @@ impl MailboxView { let mails = idx.fetch(sequence_set, *is_uid_copy)?; for mi in mails.iter() { - to.move_from(&self.0.mailbox, mi.uuid).await?; + to.move_from(&self.internal.mailbox, mi.uuid).await?; } let mut ret = vec![]; @@ -268,7 +277,7 @@ impl MailboxView { .iter() .map(|midx| midx.uuid) .collect::>(); - let query_result = self.0.query(&uuids, query_scope).fetch().await?; + let query_result = self.internal.query(&uuids, query_scope).fetch().await?; // [3/6] Derive an IMAP-specific view from the results, apply the filters let views = query_result @@ -294,7 +303,7 @@ impl MailboxView { .filter(|(_mv, seen)| matches!(seen, SeenFlag::MustAdd)) .map(|(mv, _seen)| async move { let seen_flag = Flag::Seen.to_string(); - self.0 + self.internal .mailbox .add_flags(*mv.query_result.uuid(), &[seen_flag]) .await?; @@ -332,7 +341,7 @@ impl MailboxView { // 4. Fetch additional info about the emails let query_scope = crit.query_scope(); let uuids = to_fetch.iter().map(|midx| midx.uuid).collect::>(); - let query_result = self.0.query(&uuids, query_scope).fetch().await?; + let query_result = self.internal.query(&uuids, query_scope).fetch().await?; // 5. If needed, filter the selection based on the body let kept_query = crit.filter_on_query(&to_fetch, &query_result)?; @@ -354,7 +363,7 @@ impl MailboxView { /// It's not trivial to refactor the code to do that, so we are doing /// some useless computation for now... fn index<'a>(&'a self) -> Result> { - Index::new(&self.0.snapshot) + Index::new(&self.internal.snapshot) } /// Produce an OK [UIDVALIDITY _] message corresponding to `known_state` @@ -369,7 +378,7 @@ impl MailboxView { } pub(crate) fn uidvalidity(&self) -> ImapUidvalidity { - self.0.snapshot.uidvalidity + self.internal.snapshot.uidvalidity } /// Produce an OK [UIDNEXT _] message corresponding to `known_state` @@ -384,7 +393,15 @@ impl MailboxView { } pub(crate) fn uidnext(&self) -> ImapUid { - self.0.snapshot.uidnext + self.internal.snapshot.uidnext + } + + pub(crate) fn highestmodseq_status(&self) -> Result> { + Ok(Body::Status(Status::ok( + None, + Some(Code::Other(CodeOther::unvalidated(format!("HIGHESTMODSEQ {}", 0).into_bytes()))), + "Highest", + )?)) } /// Produce an EXISTS message corresponding to the number of mails @@ -394,7 +411,7 @@ impl MailboxView { } pub(crate) fn exists(&self) -> Result { - Ok(u32::try_from(self.0.snapshot.idx_by_uid.len())?) + Ok(u32::try_from(self.internal.snapshot.idx_by_uid.len())?) } /// Produce a RECENT message corresponding to the number of @@ -416,7 +433,7 @@ impl MailboxView { #[allow(dead_code)] fn unseen_first(&self) -> Result> { Ok(self - .0 + .internal .snapshot .table .values() @@ -428,7 +445,7 @@ impl MailboxView { pub(crate) fn recent(&self) -> Result { let recent = self - .0 + .internal .snapshot .idx_by_flag .get(&"\\Recent".to_string()) @@ -445,7 +462,7 @@ impl MailboxView { // 1. Collecting all the possible flags in the mailbox // 1.a Fetch them from our index let mut known_flags: Vec = self - .0 + .internal .snapshot .idx_by_flag .flags() @@ -485,9 +502,9 @@ impl MailboxView { } pub(crate) fn unseen_count(&self) -> usize { - let total = self.0.snapshot.table.len(); + let total = self.internal.snapshot.table.len(); let seen = self - .0 + .internal .snapshot .idx_by_flag .get(&Flag::Seen.to_string()) From 184328ebcf100496d8b6df0cc570c773a2203a2e Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Tue, 9 Jan 2024 19:16:55 +0100 Subject: [PATCH 03/21] Optional Parameters with the SELECT/EXAMINE Commands See: https://datatracker.ietf.org/doc/html/rfc4466#section-2.4 --- Cargo.lock | 2 -- Cargo.toml | 6 ++++-- src/imap/capability.rs | 18 +++++++++++++++++- src/imap/command/authenticated.rs | 14 ++++++++++++-- 4 files changed, 33 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2c264f2..f8d3bf9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1807,7 +1807,6 @@ dependencies = [ [[package]] name = "imap-codec" version = "1.0.0" -source = "git+https://github.com/duesee/imap-codec?branch=v2#1f490146bb6197eee6032205e3aa7f297efd9b39" dependencies = [ "abnf-core", "base64 0.21.5", @@ -1834,7 +1833,6 @@ dependencies = [ [[package]] name = "imap-types" version = "1.0.0" -source = "git+https://github.com/duesee/imap-codec?branch=v2#1f490146bb6197eee6032205e3aa7f297efd9b39" dependencies = [ "base64 0.21.5", "bounded-static", diff --git a/Cargo.toml b/Cargo.toml index 68a46e3..1e8cc16 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -64,8 +64,10 @@ imap-flow = { git = "https://github.com/duesee/imap-flow.git", rev = "e45ce7bb6a [dev-dependencies] [patch.crates-io] -imap-types = { git = "https://github.com/duesee/imap-codec", branch = "v2" } -imap-codec = { git = "https://github.com/duesee/imap-codec", branch = "v2" } +#imap-types = { git = "https://github.com/duesee/imap-codec", branch = "v2" } +#imap-codec = { git = "https://github.com/duesee/imap-codec", branch = "v2" } +imap-types = { path = "../../imap-codec/imap-types" } +imap-codec = { path = "../../imap-codec/imap-codec" } [[test]] name = "behavior" diff --git a/src/imap/capability.rs b/src/imap/capability.rs index d88673c..be1d4b6 100644 --- a/src/imap/capability.rs +++ b/src/imap/capability.rs @@ -1,4 +1,4 @@ -use imap_codec::imap_types::core::NonEmptyVec; +use imap_codec::imap_types::core::{Atom, NonEmptyVec}; use imap_codec::imap_types::extensions::enable::{CapabilityEnable, Utf8Kind}; use imap_codec::imap_types::response::Capability; use std::collections::HashSet; @@ -48,6 +48,7 @@ impl ServerCapability { } } +#[derive(Clone)] pub enum ClientStatus { NotSupportedByServer, Disabled, @@ -57,6 +58,13 @@ impl ClientStatus { pub fn is_enabled(&self) -> bool { matches!(self, Self::Enabled) } + + pub fn enable(&self) -> Self { + match self { + Self::Disabled => Self::Enabled, + other => other.clone(), + } + } } @@ -76,6 +84,14 @@ impl ClientCapability { } } + pub fn select_enable(&mut self, atoms: &[Atom]) { + for at in atoms.iter() { + if at.as_ref().to_uppercase() == "CONDSTORE" { + self.condstore = self.condstore.enable(); + } + } + } + pub fn try_enable( &mut self, caps: &[CapabilityEnable<'static>], diff --git a/src/imap/command/authenticated.rs b/src/imap/command/authenticated.rs index 954e758..5af8e98 100644 --- a/src/imap/command/authenticated.rs +++ b/src/imap/command/authenticated.rs @@ -58,8 +58,8 @@ pub async fn dispatch<'a>( } => ctx.status(mailbox, item_names).await, CommandBody::Subscribe { mailbox } => ctx.subscribe(mailbox).await, CommandBody::Unsubscribe { mailbox } => ctx.unsubscribe(mailbox).await, - CommandBody::Select { mailbox } => ctx.select(mailbox).await, - CommandBody::Examine { mailbox } => ctx.examine(mailbox).await, + CommandBody::Select { mailbox, parameters } => ctx.select(mailbox, parameters).await, + CommandBody::Examine { mailbox, parameters } => ctx.examine(mailbox, parameters).await, CommandBody::Append { mailbox, flags, @@ -421,7 +421,12 @@ impl<'a> AuthenticatedContext<'a> { async fn select( self, mailbox: &MailboxCodec<'a>, + parameters: &Option>>, ) -> Result<(Response<'static>, flow::Transition)> { + parameters.as_ref().map(|plist| + self.client_capabilities.select_enable(plist.as_ref()) + ); + let name: &str = MailboxName(mailbox).try_into()?; let mb_opt = self.user.open_mailbox(&name).await?; @@ -456,7 +461,12 @@ impl<'a> AuthenticatedContext<'a> { async fn examine( self, mailbox: &MailboxCodec<'a>, + parameters: &Option>>, ) -> Result<(Response<'static>, flow::Transition)> { + parameters.as_ref().map(|plist| + self.client_capabilities.select_enable(plist.as_ref()) + ); + let name: &str = MailboxName(mailbox).try_into()?; let mb_opt = self.user.open_mailbox(&name).await?; From a2d6efc962dbf5de64a70cf7d9f293534bd5369a Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Wed, 10 Jan 2024 11:24:01 +0100 Subject: [PATCH 04/21] [broken compilation] update mail internal --- src/imap/command/selected.rs | 9 ++- src/mail/mailbox.rs | 14 ++-- src/mail/uidindex.rs | 132 +++++++++++++++++++++++++++-------- 3 files changed, 115 insertions(+), 40 deletions(-) diff --git a/src/imap/command/selected.rs b/src/imap/command/selected.rs index c38c5d3..ef2654e 100644 --- a/src/imap/command/selected.rs +++ b/src/imap/command/selected.rs @@ -1,8 +1,8 @@ use std::sync::Arc; use anyhow::Result; -use imap_codec::imap_types::command::{Command, CommandBody}; -use imap_codec::imap_types::core::Charset; +use imap_codec::imap_types::command::{Command, CommandBody, StoreModifier}; +use imap_codec::imap_types::core::{Charset, Atom}; use imap_codec::imap_types::fetch::MacroOrMessageDataItemNames; use imap_codec::imap_types::flag::{Flag, StoreResponse, StoreType}; use imap_codec::imap_types::mailbox::Mailbox as MailboxCodec; @@ -56,8 +56,9 @@ pub async fn dispatch<'a>( kind, response, flags, + modifiers, uid, - } => ctx.store(sequence_set, kind, response, flags, uid).await, + } => ctx.store(sequence_set, kind, response, flags, modifiers, uid).await, CommandBody::Copy { sequence_set, mailbox, @@ -185,8 +186,10 @@ impl<'a> SelectedContext<'a> { kind: &StoreType, response: &StoreResponse, flags: &[Flag<'a>], + modifiers: &[(Atom<'a>, StoreModifier<'a>)], uid: &bool, ) -> Result<(Response<'static>, flow::Transition)> { + tracing::info!(modifiers=?modifiers); let data = self .mailbox .store(sequence_set, kind, response, flags, uid) diff --git a/src/mail/mailbox.rs b/src/mail/mailbox.rs index aab200b..84fa5af 100644 --- a/src/mail/mailbox.rs +++ b/src/mail/mailbox.rs @@ -113,7 +113,7 @@ impl Mailbox { msg: IMF<'a>, ident: Option, flags: &[Flag], - ) -> Result<(ImapUidvalidity, ImapUid)> { + ) -> Result<(ImapUidvalidity, ImapUid, ModSeq)> { self.mbox.write().await.append(msg, ident, flags).await } @@ -271,7 +271,7 @@ impl MailboxInternal { mail: IMF<'_>, ident: Option, flags: &[Flag], - ) -> Result<(ImapUidvalidity, ImapUid)> { + ) -> Result<(ImapUidvalidity, ImapUid, ModSeq)> { let ident = ident.unwrap_or_else(gen_ident); let message_key = gen_key(); @@ -312,14 +312,14 @@ impl MailboxInternal { let add_mail_op = uid_state.op_mail_add(ident, flags.to_vec()); let uidvalidity = uid_state.uidvalidity; - let uid = match add_mail_op { - UidIndexOp::MailAdd(_, uid, _) => uid, + let (uid, modseq) = match add_mail_op { + UidIndexOp::MailAdd(_, uid, modseq, _) => (uid, modseq), _ => unreachable!(), }; self.uid_index.push(add_mail_op).await?; - Ok((uidvalidity, uid)) + Ok((uidvalidity, uid, modseq)) } async fn append_from_s3<'a>( @@ -432,7 +432,7 @@ impl MailboxInternal { .table .get(&source_id) .ok_or(anyhow!("Source mail not found"))? - .1 + .2 .clone(); futures::try_join!( @@ -476,7 +476,7 @@ fn dump(uid_index: &Bayou) { "{} {} {}", uid, hex::encode(ident.0), - s.table.get(ident).cloned().unwrap().1.join(", ") + s.table.get(ident).cloned().unwrap().2.join(", ") ); } println!(); diff --git a/src/mail/uidindex.rs b/src/mail/uidindex.rs index 01f8c9c..f1c522c 100644 --- a/src/mail/uidindex.rs +++ b/src/mail/uidindex.rs @@ -6,10 +6,11 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer}; use crate::bayou::*; use crate::mail::unique_ident::UniqueIdent; +pub type ModSeq = NonZeroU32; pub type ImapUid = NonZeroU32; pub type ImapUidvalidity = NonZeroU32; pub type Flag = String; -pub type IndexEntry = (ImapUid, Vec); +pub type IndexEntry = (ImapUid, ModSeq, Vec); /// A UidIndex handles the mutable part of a mailbox /// It is built by running the event log on it @@ -23,28 +24,33 @@ pub struct UidIndex { // Indexes optimized for queries pub idx_by_uid: OrdMap, + pub idx_by_modseq: OrdMap, pub idx_by_flag: FlagIndex, - // Counters + // "Public" Counters pub uidvalidity: ImapUidvalidity, pub uidnext: ImapUid, + pub highestmodseq: ModSeq, + + // "Internal" Counters pub internalseq: ImapUid, + pub internalmodseq: ModSeq, } #[derive(Clone, Serialize, Deserialize, Debug)] pub enum UidIndexOp { - MailAdd(UniqueIdent, ImapUid, Vec), + MailAdd(UniqueIdent, ImapUid, ModSeq, Vec), MailDel(UniqueIdent), - FlagAdd(UniqueIdent, Vec), - FlagDel(UniqueIdent, Vec), - FlagSet(UniqueIdent, Vec), + FlagAdd(UniqueIdent, ModSeq, Vec), + FlagDel(UniqueIdent, ModSeq, Vec), + FlagSet(UniqueIdent, ModSeq, Vec), BumpUidvalidity(u32), } impl UidIndex { #[must_use] pub fn op_mail_add(&self, ident: UniqueIdent, flags: Vec) -> UidIndexOp { - UidIndexOp::MailAdd(ident, self.internalseq, flags) + UidIndexOp::MailAdd(ident, self.internalseq, self.internalmodseq, flags) } #[must_use] @@ -54,17 +60,17 @@ impl UidIndex { #[must_use] pub fn op_flag_add(&self, ident: UniqueIdent, flags: Vec) -> UidIndexOp { - UidIndexOp::FlagAdd(ident, flags) + UidIndexOp::FlagAdd(ident, self.internalmodseq, flags) } #[must_use] pub fn op_flag_del(&self, ident: UniqueIdent, flags: Vec) -> UidIndexOp { - UidIndexOp::FlagDel(ident, flags) + UidIndexOp::FlagDel(ident, self.internalmodseq, flags) } #[must_use] pub fn op_flag_set(&self, ident: UniqueIdent, flags: Vec) -> UidIndexOp { - UidIndexOp::FlagSet(ident, flags) + UidIndexOp::FlagSet(ident, self.internalmodseq, flags) } #[must_use] @@ -74,18 +80,19 @@ impl UidIndex { // INTERNAL functions to keep state consistent - fn reg_email(&mut self, ident: UniqueIdent, uid: ImapUid, flags: &[Flag]) { + fn reg_email(&mut self, ident: UniqueIdent, uid: ImapUid, modseq: ModSeq, flags: &[Flag]) { // Insert the email in our table - self.table.insert(ident, (uid, flags.to_owned())); + self.table.insert(ident, (uid, modseq, flags.to_owned())); // Update the indexes/caches self.idx_by_uid.insert(uid, ident); self.idx_by_flag.insert(uid, flags); + self.idx_by_modseq.insert(modseq, ident); } fn unreg_email(&mut self, ident: &UniqueIdent) { // We do nothing if the mail does not exist - let (uid, flags) = match self.table.get(ident) { + let (uid, modseq, flags) = match self.table.get(ident) { Some(v) => v, None => return, }; @@ -93,6 +100,7 @@ impl UidIndex { // Delete all cache entries self.idx_by_uid.remove(uid); self.idx_by_flag.remove(*uid, flags); + self.idx_by_modseq.remove(modseq); // Remove from source of trust self.table.remove(ident); @@ -103,11 +111,17 @@ impl Default for UidIndex { fn default() -> Self { Self { table: OrdMap::new(), + idx_by_uid: OrdMap::new(), + idx_by_modseq: OrdMap::new(), idx_by_flag: FlagIndex::new(), + uidvalidity: NonZeroU32::new(1).unwrap(), uidnext: NonZeroU32::new(1).unwrap(), + highestmodseq: NonZeroU32::new(1).unwrap(), + internalseq: NonZeroU32::new(1).unwrap(), + internalmodseq: NonZeroU32::new(1).unwrap(), } } } @@ -118,17 +132,24 @@ impl BayouState for UidIndex { fn apply(&self, op: &UidIndexOp) -> Self { let mut new = self.clone(); match op { - UidIndexOp::MailAdd(ident, uid, flags) => { - // Change UIDValidity if there is a conflict - if *uid < new.internalseq { + UidIndexOp::MailAdd(ident, uid, modseq, flags) => { + // Change UIDValidity if there is a UID conflict or a MODSEQ conflict + // @FIXME Need to prove that summing work + // The intuition: we increase the UIDValidity by the number of possible conflicts + if *uid < new.internalseq || *modseq < new.highestmodseq { + let bump_uid = new.internalseq.get() - uid.get(); + let bump_modseq = new.internalmodseq.get() - modseq.get(); new.uidvalidity = - NonZeroU32::new(new.uidvalidity.get() + new.internalseq.get() - uid.get()) + NonZeroU32::new(new.uidvalidity.get() + bump_uid + bump_modseq) .unwrap(); } // Assign the real uid of the email let new_uid = new.internalseq; + // Assign the real modseq of the email and its new flags + let new_modseq = new.highestmodseq; + // Delete the previous entry if any. // Our proof has no assumption on `ident` uniqueness, // so we must handle this case even it is very unlikely @@ -137,10 +158,14 @@ impl BayouState for UidIndex { new.unreg_email(ident); // We record our email and update ou caches - new.reg_email(*ident, new_uid, flags); + new.reg_email(*ident, new_uid, new_modseq, flags); // Update counters + new.highestmodseq = new.internalmodseq; + new.internalseq = NonZeroU32::new(new.internalseq.get() + 1).unwrap(); + new.internalmodseq = NonZeroU32::new(new.internalmodseq.get() + 1).unwrap(); + new.uidnext = new.internalseq; } UidIndexOp::MailDel(ident) => { @@ -150,8 +175,16 @@ impl BayouState for UidIndex { // We update the counter new.internalseq = NonZeroU32::new(new.internalseq.get() + 1).unwrap(); } - UidIndexOp::FlagAdd(ident, new_flags) => { - if let Some((uid, existing_flags)) = new.table.get_mut(ident) { + UidIndexOp::FlagAdd(ident, modseq, new_flags) => { + if let Some((uid, modseq, existing_flags)) = new.table.get_mut(ident) { + // Bump UIDValidity if required + if *modseq < new.highestmodseq { + let bump_modseq = new.internalmodseq.get() - modseq.get(); + new.uidvalidity = + NonZeroU32::new(new.uidvalidity.get() + bump_modseq) + .unwrap(); + } + // Add flags to the source of trust and the cache let mut to_add: Vec = new_flags .iter() @@ -159,18 +192,38 @@ impl BayouState for UidIndex { .cloned() .collect(); new.idx_by_flag.insert(*uid, &to_add); + new.idx_by_modseq.insert(*modseq, *ident); existing_flags.append(&mut to_add); + + // Update counters + new.highestmodseq = new.internalmodseq; + new.internalmodseq = NonZeroU32::new(new.internalmodseq.get() + 1).unwrap(); } } - UidIndexOp::FlagDel(ident, rm_flags) => { - if let Some((uid, existing_flags)) = new.table.get_mut(ident) { + UidIndexOp::FlagDel(ident, modseq, rm_flags) => { + if let Some((uid, modseq, existing_flags)) = new.table.get_mut(ident) { + // Bump UIDValidity if required + if *modseq < new.highestmodseq { + let bump_modseq = new.internalmodseq.get() - modseq.get(); + new.uidvalidity = + NonZeroU32::new(new.uidvalidity.get() + bump_modseq) + .unwrap(); + } + // Remove flags from the source of trust and the cache existing_flags.retain(|x| !rm_flags.contains(x)); new.idx_by_flag.remove(*uid, rm_flags); + + // Register that email has been modified + new.idx_by_modseq.insert(*modseq, *ident); + + // Update counters + new.highestmodseq = new.internalmodseq; + new.internalmodseq = NonZeroU32::new(new.internalmodseq.get() + 1).unwrap(); } } - UidIndexOp::FlagSet(ident, new_flags) => { - if let Some((uid, existing_flags)) = new.table.get_mut(ident) { + UidIndexOp::FlagSet(ident, modseq, new_flags) => { + if let Some((uid, modseq, existing_flags)) = new.table.get_mut(ident) { // Remove flags from the source of trust and the cache let (keep_flags, rm_flags): (Vec, Vec) = existing_flags .iter() @@ -185,6 +238,13 @@ impl BayouState for UidIndex { existing_flags.append(&mut to_add); new.idx_by_flag.remove(*uid, &rm_flags); new.idx_by_flag.insert(*uid, &to_add); + + // Register that email has been modified + new.idx_by_modseq.insert(*modseq, *ident); + + // Update counters + new.highestmodseq = new.internalmodseq; + new.internalmodseq = NonZeroU32::new(new.internalmodseq.get() + 1).unwrap(); } } UidIndexOp::BumpUidvalidity(count) => { @@ -238,10 +298,14 @@ impl FlagIndex { #[derive(Serialize, Deserialize)] struct UidIndexSerializedRepr { - mails: Vec<(ImapUid, UniqueIdent, Vec)>, + mails: Vec<(ImapUid, ModSeq, UniqueIdent, Vec)>, + uidvalidity: ImapUidvalidity, uidnext: ImapUid, + highestmodseq: ModSeq, + internalseq: ImapUid, + internalmodseq: ModSeq, } impl<'de> Deserialize<'de> for UidIndex { @@ -253,16 +317,22 @@ impl<'de> Deserialize<'de> for UidIndex { let mut uidindex = UidIndex { table: OrdMap::new(), + idx_by_uid: OrdMap::new(), + idx_by_modseq: OrdMap::new(), idx_by_flag: FlagIndex::new(), + uidvalidity: val.uidvalidity, uidnext: val.uidnext, + highestmodseq: val.highestmodseq, + internalseq: val.internalseq, + internalmodseq: val.internalmodseq, }; val.mails .iter() - .for_each(|(u, i, f)| uidindex.reg_email(*i, *u, f)); + .for_each(|(uid, modseq, uuid, flags)| uidindex.reg_email(*uuid, *uid, *modseq, flags)); Ok(uidindex) } @@ -274,15 +344,17 @@ impl Serialize for UidIndex { S: Serializer, { let mut mails = vec![]; - for (ident, (uid, flags)) in self.table.iter() { - mails.push((*uid, *ident, flags.clone())); + for (ident, (uid, modseq, flags)) in self.table.iter() { + mails.push((*uid, *modseq, *ident, flags.clone())); } let val = UidIndexSerializedRepr { mails, uidvalidity: self.uidvalidity, uidnext: self.uidnext, + highestmodseq: self.highestmodseq, internalseq: self.internalseq, + internalmodseq: self.internalmodseq, }; val.serialize(serializer) @@ -308,7 +380,7 @@ mod tests { // Early checks assert_eq!(state.table.len(), 1); - let (uid, flags) = state.table.get(&m).unwrap(); + let (uid, modseq, flags) = state.table.get(&m).unwrap(); assert_eq!(*uid, NonZeroU32::new(1).unwrap()); assert_eq!(flags.len(), 2); let ident = state.idx_by_uid.get(&NonZeroU32::new(1).unwrap()).unwrap(); @@ -364,7 +436,7 @@ mod tests { { let m = UniqueIdent([0x03; 24]); let f = vec!["\\Archive".to_string(), "\\Recent".to_string()]; - let ev = UidIndexOp::MailAdd(m, NonZeroU32::new(1).unwrap(), f); + let ev = UidIndexOp::MailAdd(m, NonZeroU32::new(1).unwrap(), NonZeroU32::new(1).unwrap(), f); state = state.apply(&ev); } From 51510c97f7b76a73a2032eb089552cf9a13e9274 Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Wed, 10 Jan 2024 12:55:38 +0100 Subject: [PATCH 05/21] fix some logic error in the internals --- src/imap/command/authenticated.rs | 8 +++--- src/imap/index.rs | 2 +- src/imap/mailbox_view.rs | 6 ++--- src/mail/uidindex.rs | 41 ++++++++++++++++++++----------- 4 files changed, 34 insertions(+), 23 deletions(-) diff --git a/src/imap/command/authenticated.rs b/src/imap/command/authenticated.rs index 5af8e98..f083ac8 100644 --- a/src/imap/command/authenticated.rs +++ b/src/imap/command/authenticated.rs @@ -507,7 +507,7 @@ impl<'a> AuthenticatedContext<'a> { ) -> Result<(Response<'static>, flow::Transition)> { let append_tag = self.req.tag.clone(); match self.append_internal(mailbox, flags, date, message).await { - Ok((_mb, uidvalidity, uid)) => Ok(( + Ok((_mb, uidvalidity, uid, _modseq)) => Ok(( Response::build() .tag(append_tag) .message("APPEND completed") @@ -548,7 +548,7 @@ impl<'a> AuthenticatedContext<'a> { flags: &[Flag<'a>], date: &Option, message: &Literal<'a>, - ) -> Result<(Arc, ImapUidvalidity, ImapUidvalidity)> { + ) -> Result<(Arc, ImapUidvalidity, ImapUid, ModSeq)> { let name: &str = MailboxName(mailbox).try_into()?; let mb_opt = self.user.open_mailbox(&name).await?; @@ -566,9 +566,9 @@ impl<'a> AuthenticatedContext<'a> { let flags = flags.iter().map(|x| x.to_string()).collect::>(); // TODO: filter allowed flags? ping @Quentin - let (uidvalidity, uid) = mb.append(msg, None, &flags[..]).await?; + let (uidvalidity, uid, modseq) = mb.append(msg, None, &flags[..]).await?; - Ok((mb, uidvalidity, uid)) + Ok((mb, uidvalidity, uid, modseq)) } } diff --git a/src/imap/index.rs b/src/imap/index.rs index 4853374..95c16b3 100644 --- a/src/imap/index.rs +++ b/src/imap/index.rs @@ -21,7 +21,7 @@ impl<'a> Index<'a> { .table .get(&uuid) .ok_or(anyhow!("mail is missing from index"))? - .1 + .2 .as_ref(); let i_int: u32 = (i_enum + 1).try_into()?; let i: NonZeroU32 = i_int.try_into()?; diff --git a/src/imap/mailbox_view.rs b/src/imap/mailbox_view.rs index 046acfa..90cfc70 100644 --- a/src/imap/mailbox_view.rs +++ b/src/imap/mailbox_view.rs @@ -108,7 +108,7 @@ impl MailboxView { let old_mail = old_snapshot.table.get(uuid); let new_mail = new_snapshot.table.get(uuid); if old_mail.is_some() && old_mail != new_mail { - if let Some((uid, flags)) = new_mail { + if let Some((uid, _modseq, flags)) = new_mail { data.push(Body::Data(Data::Fetch { seq: NonZeroU32::try_from((i + 1) as u32).unwrap(), items: vec![ @@ -185,7 +185,7 @@ impl MailboxView { let msgs = state .table .iter() - .filter(|(_uuid, (_uid, flags))| flags.iter().any(|x| *x == deleted_flag)) + .filter(|(_uuid, (_uid, _modseq, flags))| flags.iter().any(|x| *x == deleted_flag)) .map(|(uuid, _)| *uuid); for msg in msgs { @@ -438,7 +438,7 @@ impl MailboxView { .table .values() .enumerate() - .find(|(_i, (_imap_uid, flags))| !flags.contains(&"\\Seen".to_string())) + .find(|(_i, (_imap_uid, _modseq, flags))| !flags.contains(&"\\Seen".to_string())) .map(|(i, _)| NonZeroU32::try_from(i as u32 + 1)) .transpose()?) } diff --git a/src/mail/uidindex.rs b/src/mail/uidindex.rs index f1c522c..e7023cf 100644 --- a/src/mail/uidindex.rs +++ b/src/mail/uidindex.rs @@ -136,7 +136,7 @@ impl BayouState for UidIndex { // Change UIDValidity if there is a UID conflict or a MODSEQ conflict // @FIXME Need to prove that summing work // The intuition: we increase the UIDValidity by the number of possible conflicts - if *uid < new.internalseq || *modseq < new.highestmodseq { + if *uid < new.internalseq || *modseq < new.internalmodseq { let bump_uid = new.internalseq.get() - uid.get(); let bump_modseq = new.internalmodseq.get() - modseq.get(); new.uidvalidity = @@ -148,7 +148,7 @@ impl BayouState for UidIndex { let new_uid = new.internalseq; // Assign the real modseq of the email and its new flags - let new_modseq = new.highestmodseq; + let new_modseq = new.internalmodseq; // Delete the previous entry if any. // Our proof has no assumption on `ident` uniqueness, @@ -175,11 +175,11 @@ impl BayouState for UidIndex { // We update the counter new.internalseq = NonZeroU32::new(new.internalseq.get() + 1).unwrap(); } - UidIndexOp::FlagAdd(ident, modseq, new_flags) => { - if let Some((uid, modseq, existing_flags)) = new.table.get_mut(ident) { + UidIndexOp::FlagAdd(ident, candidate_modseq, new_flags) => { + if let Some((uid, email_modseq, existing_flags)) = new.table.get_mut(ident) { // Bump UIDValidity if required - if *modseq < new.highestmodseq { - let bump_modseq = new.internalmodseq.get() - modseq.get(); + if *candidate_modseq < new.internalmodseq { + let bump_modseq = new.internalmodseq.get() - candidate_modseq.get(); new.uidvalidity = NonZeroU32::new(new.uidvalidity.get() + bump_modseq) .unwrap(); @@ -192,7 +192,8 @@ impl BayouState for UidIndex { .cloned() .collect(); new.idx_by_flag.insert(*uid, &to_add); - new.idx_by_modseq.insert(*modseq, *ident); + *email_modseq = new.internalmodseq; + new.idx_by_modseq.insert(new.internalmodseq, *ident); existing_flags.append(&mut to_add); // Update counters @@ -200,11 +201,11 @@ impl BayouState for UidIndex { new.internalmodseq = NonZeroU32::new(new.internalmodseq.get() + 1).unwrap(); } } - UidIndexOp::FlagDel(ident, modseq, rm_flags) => { - if let Some((uid, modseq, existing_flags)) = new.table.get_mut(ident) { + UidIndexOp::FlagDel(ident, candidate_modseq, rm_flags) => { + if let Some((uid, email_modseq, existing_flags)) = new.table.get_mut(ident) { // Bump UIDValidity if required - if *modseq < new.highestmodseq { - let bump_modseq = new.internalmodseq.get() - modseq.get(); + if *candidate_modseq < new.internalmodseq { + let bump_modseq = new.internalmodseq.get() - candidate_modseq.get(); new.uidvalidity = NonZeroU32::new(new.uidvalidity.get() + bump_modseq) .unwrap(); @@ -215,15 +216,24 @@ impl BayouState for UidIndex { new.idx_by_flag.remove(*uid, rm_flags); // Register that email has been modified - new.idx_by_modseq.insert(*modseq, *ident); + new.idx_by_modseq.insert(new.internalmodseq, *ident); + *email_modseq = new.internalmodseq; // Update counters new.highestmodseq = new.internalmodseq; new.internalmodseq = NonZeroU32::new(new.internalmodseq.get() + 1).unwrap(); } } - UidIndexOp::FlagSet(ident, modseq, new_flags) => { - if let Some((uid, modseq, existing_flags)) = new.table.get_mut(ident) { + UidIndexOp::FlagSet(ident, candidate_modseq, new_flags) => { + if let Some((uid, email_modseq, existing_flags)) = new.table.get_mut(ident) { + // Bump UIDValidity if required + if *candidate_modseq < new.internalmodseq { + let bump_modseq = new.internalmodseq.get() - candidate_modseq.get(); + new.uidvalidity = + NonZeroU32::new(new.uidvalidity.get() + bump_modseq) + .unwrap(); + } + // Remove flags from the source of trust and the cache let (keep_flags, rm_flags): (Vec, Vec) = existing_flags .iter() @@ -240,7 +250,8 @@ impl BayouState for UidIndex { new.idx_by_flag.insert(*uid, &to_add); // Register that email has been modified - new.idx_by_modseq.insert(*modseq, *ident); + new.idx_by_modseq.insert(new.internalmodseq, *ident); + *email_modseq = new.internalmodseq; // Update counters new.highestmodseq = new.internalmodseq; From 20193aa023b3f6a6d24d8127f1d8dcb81a43aa3b Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Wed, 10 Jan 2024 13:59:43 +0100 Subject: [PATCH 06/21] Return highestmodseq in select+examine --- src/imap/mailbox_view.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/imap/mailbox_view.rs b/src/imap/mailbox_view.rs index 90cfc70..a3d56f0 100644 --- a/src/imap/mailbox_view.rs +++ b/src/imap/mailbox_view.rs @@ -399,7 +399,7 @@ impl MailboxView { pub(crate) fn highestmodseq_status(&self) -> Result> { Ok(Body::Status(Status::ok( None, - Some(Code::Other(CodeOther::unvalidated(format!("HIGHESTMODSEQ {}", 0).into_bytes()))), + Some(Code::Other(CodeOther::unvalidated(format!("HIGHESTMODSEQ {}", self.internal.snapshot.highestmodseq).into_bytes()))), "Highest", )?)) } From 0c6e745d11725b4a15d2c380f5bb60067c26ddd9 Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Wed, 10 Jan 2024 14:45:36 +0100 Subject: [PATCH 07/21] update imap-codec --- Cargo.lock | 8 +++++--- Cargo.toml | 10 ++++------ src/imap/index.rs | 14 +++++--------- src/imap/mod.rs | 5 +++++ 4 files changed, 19 insertions(+), 18 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f8d3bf9..fed2cd5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1806,7 +1806,8 @@ dependencies = [ [[package]] name = "imap-codec" -version = "1.0.0" +version = "2.0.0" +source = "git+https://github.com/superboum/imap-codec?branch=custom/aerogramme#4c7891d400e69c864fc079463bbd2c28e2d46cf3" dependencies = [ "abnf-core", "base64 0.21.5", @@ -1821,7 +1822,7 @@ dependencies = [ [[package]] name = "imap-flow" version = "0.1.0" -source = "git+https://github.com/duesee/imap-flow.git?rev=e45ce7bb6ab6bda3c71a0c7b05e9b558a5902e90#e45ce7bb6ab6bda3c71a0c7b05e9b558a5902e90" +source = "git+https://github.com/superboum/imap-flow.git?branch=custom/aerogramme#525429781f375580cba824de584593b242245ff7" dependencies = [ "bounded-static", "bytes", @@ -1832,7 +1833,8 @@ dependencies = [ [[package]] name = "imap-types" -version = "1.0.0" +version = "2.0.0" +source = "git+https://github.com/superboum/imap-codec?branch=custom/aerogramme#4c7891d400e69c864fc079463bbd2c28e2d46cf3" dependencies = [ "base64 0.21.5", "bounded-static", diff --git a/Cargo.toml b/Cargo.toml index 1e8cc16..55f0284 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -58,16 +58,14 @@ aws-sdk-s3 = "1.9.0" eml-codec = { git = "https://git.deuxfleurs.fr/Deuxfleurs/eml-codec.git", branch = "main" } smtp-message = { git = "http://github.com/Alexis211/kannader", branch = "feature/lmtp" } smtp-server = { git = "http://github.com/Alexis211/kannader", branch = "feature/lmtp" } -imap-codec = { version = "1.0.0", features = ["bounded-static", "ext_condstore_qresync"] } -imap-flow = { git = "https://github.com/duesee/imap-flow.git", rev = "e45ce7bb6ab6bda3c71a0c7b05e9b558a5902e90" } +imap-codec = { version = "2.0.0", features = ["bounded-static", "ext_condstore_qresync"] } +imap-flow = { git = "https://github.com/superboum/imap-flow.git", branch = "custom/aerogramme" } [dev-dependencies] [patch.crates-io] -#imap-types = { git = "https://github.com/duesee/imap-codec", branch = "v2" } -#imap-codec = { git = "https://github.com/duesee/imap-codec", branch = "v2" } -imap-types = { path = "../../imap-codec/imap-types" } -imap-codec = { path = "../../imap-codec/imap-codec" } +imap-types = { git = "https://github.com/superboum/imap-codec", branch = "custom/aerogramme" } +imap-codec = { git = "https://github.com/superboum/imap-codec", branch = "custom/aerogramme" } [[test]] name = "behavior" diff --git a/src/imap/index.rs b/src/imap/index.rs index 95c16b3..da94022 100644 --- a/src/imap/index.rs +++ b/src/imap/index.rs @@ -1,7 +1,7 @@ use std::num::NonZeroU32; use anyhow::{anyhow, Result}; -use imap_codec::imap_types::sequence::{self, SeqOrUid, Sequence, SequenceSet}; +use imap_codec::imap_types::sequence::{SeqOrUid, Sequence, SequenceSet}; use crate::mail::uidindex::{ImapUid, UidIndex}; use crate::mail::unique_ident::UniqueIdent; @@ -61,10 +61,8 @@ impl<'a> Index<'a> { if self.imap_index.is_empty() { return vec![]; } - let iter_strat = sequence::Strategy::Naive { - largest: self.last().expect("The mailbox is not empty").uid, - }; - let mut unroll_seq = sequence_set.iter(iter_strat).collect::>(); + let largest = self.last().expect("The mailbox is not empty").uid; + let mut unroll_seq = sequence_set.iter(largest).collect::>(); unroll_seq.sort(); let start_seq = match unroll_seq.iter().next() { @@ -103,11 +101,9 @@ impl<'a> Index<'a> { if self.imap_index.is_empty() { return Ok(vec![]); } - let iter_strat = sequence::Strategy::Naive { - largest: NonZeroU32::try_from(self.imap_index.len() as u32)?, - }; + let largest = NonZeroU32::try_from(self.imap_index.len() as u32)?; let mut acc = sequence_set - .iter(iter_strat) + .iter(largest) .map(|wanted_id| { self.imap_index .get((wanted_id.get() as usize) - 1) diff --git a/src/imap/mod.rs b/src/imap/mod.rs index 2640183..61a265a 100644 --- a/src/imap/mod.rs +++ b/src/imap/mod.rs @@ -175,6 +175,11 @@ async fn client(mut ctx: ClientContext) -> Result<()> { } } }, + flow => { + server.enqueue_status(Status::bye(None, "Unsupported server flow event").unwrap()); + tracing::error!("session task exited for {:?} due to unsupported flow {:?}", ctx.addr, flow); + + } }, // Managing response generated by Aerogramme From 96332c9bfe6a9f01e3fdb0b2b565ad669fb526b1 Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Wed, 10 Jan 2024 15:15:12 +0100 Subject: [PATCH 08/21] upgrading imap-flow,codec,types --- Cargo.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fed2cd5..39bc590 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1807,7 +1807,7 @@ dependencies = [ [[package]] name = "imap-codec" version = "2.0.0" -source = "git+https://github.com/superboum/imap-codec?branch=custom/aerogramme#4c7891d400e69c864fc079463bbd2c28e2d46cf3" +source = "git+https://github.com/superboum/imap-codec?branch=custom/aerogramme#88bdca2b571f02bccb52257bee7355daebe7d123" dependencies = [ "abnf-core", "base64 0.21.5", @@ -1822,7 +1822,7 @@ dependencies = [ [[package]] name = "imap-flow" version = "0.1.0" -source = "git+https://github.com/superboum/imap-flow.git?branch=custom/aerogramme#525429781f375580cba824de584593b242245ff7" +source = "git+https://github.com/superboum/imap-flow.git?branch=custom/aerogramme#0f548a2070aace09f9f9a0b6ef221efefb8b110b" dependencies = [ "bounded-static", "bytes", @@ -1834,7 +1834,7 @@ dependencies = [ [[package]] name = "imap-types" version = "2.0.0" -source = "git+https://github.com/superboum/imap-codec?branch=custom/aerogramme#4c7891d400e69c864fc079463bbd2c28e2d46cf3" +source = "git+https://github.com/superboum/imap-codec?branch=custom/aerogramme#88bdca2b571f02bccb52257bee7355daebe7d123" dependencies = [ "base64 0.21.5", "bounded-static", From 9cec7803d28617f1bfd1ac1621c2eda9582201d4 Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Wed, 10 Jan 2024 17:07:07 +0100 Subject: [PATCH 09/21] Implement HIGHESTMODSEQ for STATUS --- Cargo.lock | 4 ++-- src/imap/capability.rs | 8 +++++++- src/imap/command/authenticated.rs | 5 +++-- src/imap/mailbox_view.rs | 8 ++++++-- src/mail/mailbox.rs | 3 +++ src/mail/query.rs | 7 ------- src/mail/uidindex.rs | 24 ++++++++++++------------ 7 files changed, 33 insertions(+), 26 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 39bc590..e2992b7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1807,7 +1807,7 @@ dependencies = [ [[package]] name = "imap-codec" version = "2.0.0" -source = "git+https://github.com/superboum/imap-codec?branch=custom/aerogramme#88bdca2b571f02bccb52257bee7355daebe7d123" +source = "git+https://github.com/superboum/imap-codec?branch=custom/aerogramme#eb37f06a0e8d2543f60063ec80cde9e9dcb150f1" dependencies = [ "abnf-core", "base64 0.21.5", @@ -1834,7 +1834,7 @@ dependencies = [ [[package]] name = "imap-types" version = "2.0.0" -source = "git+https://github.com/superboum/imap-codec?branch=custom/aerogramme#88bdca2b571f02bccb52257bee7355daebe7d123" +source = "git+https://github.com/superboum/imap-codec?branch=custom/aerogramme#eb37f06a0e8d2543f60063ec80cde9e9dcb150f1" dependencies = [ "base64 0.21.5", "bounded-static", diff --git a/src/imap/capability.rs b/src/imap/capability.rs index be1d4b6..37f14df 100644 --- a/src/imap/capability.rs +++ b/src/imap/capability.rs @@ -11,9 +11,11 @@ fn capability_condstore() -> Capability<'static> { Capability::try_from("CONDSTORE").unwrap() } +/* fn capability_qresync() -> Capability<'static> { Capability::try_from("QRESYNC").unwrap() } +*/ #[derive(Debug, Clone)] pub struct ServerCapability(HashSet>); @@ -84,10 +86,14 @@ impl ClientCapability { } } + pub fn enable_condstore(&mut self) { + self.condstore = self.condstore.enable(); + } + pub fn select_enable(&mut self, atoms: &[Atom]) { for at in atoms.iter() { if at.as_ref().to_uppercase() == "CONDSTORE" { - self.condstore = self.condstore.enable(); + self.enable_condstore(); } } } diff --git a/src/imap/command/authenticated.rs b/src/imap/command/authenticated.rs index f083ac8..da41182 100644 --- a/src/imap/command/authenticated.rs +++ b/src/imap/command/authenticated.rs @@ -311,8 +311,9 @@ impl<'a> AuthenticatedContext<'a> { bail!("quota not implemented, can't return freed storage after EXPUNGE will be run"); }, StatusDataItemName::HighestModSeq => { - bail!("highestmodseq not yet implemented"); - } + self.client_capabilities.enable_condstore(); + StatusDataItem::HighestModSeq(view.highestmodseq().get()) + }, }); } diff --git a/src/imap/mailbox_view.rs b/src/imap/mailbox_view.rs index a3d56f0..b3848b2 100644 --- a/src/imap/mailbox_view.rs +++ b/src/imap/mailbox_view.rs @@ -15,7 +15,7 @@ use imap_codec::imap_types::sequence::SequenceSet; use crate::mail::mailbox::Mailbox; use crate::mail::query::QueryScope; use crate::mail::snapshot::FrozenMailbox; -use crate::mail::uidindex::{ImapUid, ImapUidvalidity}; +use crate::mail::uidindex::{ImapUid, ImapUidvalidity, ModSeq}; use crate::imap::attributes::AttributesProxy; use crate::imap::flags; @@ -399,11 +399,15 @@ impl MailboxView { pub(crate) fn highestmodseq_status(&self) -> Result> { Ok(Body::Status(Status::ok( None, - Some(Code::Other(CodeOther::unvalidated(format!("HIGHESTMODSEQ {}", self.internal.snapshot.highestmodseq).into_bytes()))), + Some(Code::Other(CodeOther::unvalidated(format!("HIGHESTMODSEQ {}", self.highestmodseq()).into_bytes()))), "Highest", )?)) } + pub(crate) fn highestmodseq(&self) -> ModSeq { + self.internal.snapshot.highestmodseq + } + /// Produce an EXISTS message corresponding to the number of mails /// in `known_state` fn exists_status(&self) -> Result> { diff --git a/src/mail/mailbox.rs b/src/mail/mailbox.rs index 84fa5af..5e95f32 100644 --- a/src/mail/mailbox.rs +++ b/src/mail/mailbox.rs @@ -465,6 +465,9 @@ impl MailboxInternal { } } +// Can be useful to debug so we want this code +// to be available to developers +#[allow(dead_code)] fn dump(uid_index: &Bayou) { let s = uid_index.state(); println!("---- MAILBOX STATE ----"); diff --git a/src/mail/query.rs b/src/mail/query.rs index 0838800..a183c5a 100644 --- a/src/mail/query.rs +++ b/src/mail/query.rs @@ -125,13 +125,6 @@ impl QueryResult { } } - fn into_partial(self, metadata: MailMeta) -> Option { - match self { - Self::IndexResult { uuid } => Some(Self::PartialResult { uuid, metadata }), - _ => None, - } - } - fn into_full(self, content: Vec) -> Option { match self { Self::PartialResult { uuid, metadata } => Some(Self::FullResult { diff --git a/src/mail/uidindex.rs b/src/mail/uidindex.rs index e7023cf..f703d04 100644 --- a/src/mail/uidindex.rs +++ b/src/mail/uidindex.rs @@ -1,4 +1,4 @@ -use std::num::NonZeroU32; +use std::num::{NonZeroU32, NonZeroU64}; use im::{HashMap, OrdMap, OrdSet}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; @@ -6,7 +6,7 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer}; use crate::bayou::*; use crate::mail::unique_ident::UniqueIdent; -pub type ModSeq = NonZeroU32; +pub type ModSeq = NonZeroU64; pub type ImapUid = NonZeroU32; pub type ImapUidvalidity = NonZeroU32; pub type Flag = String; @@ -118,10 +118,10 @@ impl Default for UidIndex { uidvalidity: NonZeroU32::new(1).unwrap(), uidnext: NonZeroU32::new(1).unwrap(), - highestmodseq: NonZeroU32::new(1).unwrap(), + highestmodseq: NonZeroU64::new(1).unwrap(), internalseq: NonZeroU32::new(1).unwrap(), - internalmodseq: NonZeroU32::new(1).unwrap(), + internalmodseq: NonZeroU64::new(1).unwrap(), } } } @@ -138,7 +138,7 @@ impl BayouState for UidIndex { // The intuition: we increase the UIDValidity by the number of possible conflicts if *uid < new.internalseq || *modseq < new.internalmodseq { let bump_uid = new.internalseq.get() - uid.get(); - let bump_modseq = new.internalmodseq.get() - modseq.get(); + let bump_modseq = (new.internalmodseq.get() - modseq.get()) as u32; new.uidvalidity = NonZeroU32::new(new.uidvalidity.get() + bump_uid + bump_modseq) .unwrap(); @@ -164,7 +164,7 @@ impl BayouState for UidIndex { new.highestmodseq = new.internalmodseq; new.internalseq = NonZeroU32::new(new.internalseq.get() + 1).unwrap(); - new.internalmodseq = NonZeroU32::new(new.internalmodseq.get() + 1).unwrap(); + new.internalmodseq = NonZeroU64::new(new.internalmodseq.get() + 1).unwrap(); new.uidnext = new.internalseq; } @@ -179,7 +179,7 @@ impl BayouState for UidIndex { if let Some((uid, email_modseq, existing_flags)) = new.table.get_mut(ident) { // Bump UIDValidity if required if *candidate_modseq < new.internalmodseq { - let bump_modseq = new.internalmodseq.get() - candidate_modseq.get(); + let bump_modseq = (new.internalmodseq.get() - candidate_modseq.get()) as u32; new.uidvalidity = NonZeroU32::new(new.uidvalidity.get() + bump_modseq) .unwrap(); @@ -198,14 +198,14 @@ impl BayouState for UidIndex { // Update counters new.highestmodseq = new.internalmodseq; - new.internalmodseq = NonZeroU32::new(new.internalmodseq.get() + 1).unwrap(); + new.internalmodseq = NonZeroU64::new(new.internalmodseq.get() + 1).unwrap(); } } UidIndexOp::FlagDel(ident, candidate_modseq, rm_flags) => { if let Some((uid, email_modseq, existing_flags)) = new.table.get_mut(ident) { // Bump UIDValidity if required if *candidate_modseq < new.internalmodseq { - let bump_modseq = new.internalmodseq.get() - candidate_modseq.get(); + let bump_modseq = (new.internalmodseq.get() - candidate_modseq.get()) as u32; new.uidvalidity = NonZeroU32::new(new.uidvalidity.get() + bump_modseq) .unwrap(); @@ -221,14 +221,14 @@ impl BayouState for UidIndex { // Update counters new.highestmodseq = new.internalmodseq; - new.internalmodseq = NonZeroU32::new(new.internalmodseq.get() + 1).unwrap(); + new.internalmodseq = NonZeroU64::new(new.internalmodseq.get() + 1).unwrap(); } } UidIndexOp::FlagSet(ident, candidate_modseq, new_flags) => { if let Some((uid, email_modseq, existing_flags)) = new.table.get_mut(ident) { // Bump UIDValidity if required if *candidate_modseq < new.internalmodseq { - let bump_modseq = new.internalmodseq.get() - candidate_modseq.get(); + let bump_modseq = (new.internalmodseq.get() - candidate_modseq.get()) as u32; new.uidvalidity = NonZeroU32::new(new.uidvalidity.get() + bump_modseq) .unwrap(); @@ -255,7 +255,7 @@ impl BayouState for UidIndex { // Update counters new.highestmodseq = new.internalmodseq; - new.internalmodseq = NonZeroU32::new(new.internalmodseq.get() + 1).unwrap(); + new.internalmodseq = NonZeroU64::new(new.internalmodseq.get() + 1).unwrap(); } } UidIndexOp::BumpUidvalidity(count) => { From f5b73182f25dfdcdc34f7b3c6664c5112ce93c1c Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Wed, 10 Jan 2024 18:08:44 +0100 Subject: [PATCH 10/21] Fetch now support MODSEQ data item --- Cargo.lock | 4 ++-- src/imap/index.rs | 10 +++++----- src/imap/mail_view.rs | 5 +++++ 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e2992b7..2f9e5b4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1807,7 +1807,7 @@ dependencies = [ [[package]] name = "imap-codec" version = "2.0.0" -source = "git+https://github.com/superboum/imap-codec?branch=custom/aerogramme#eb37f06a0e8d2543f60063ec80cde9e9dcb150f1" +source = "git+https://github.com/superboum/imap-codec?branch=custom/aerogramme#990e709450ff4f8986b08d2b84e28f651b74f181" dependencies = [ "abnf-core", "base64 0.21.5", @@ -1834,7 +1834,7 @@ dependencies = [ [[package]] name = "imap-types" version = "2.0.0" -source = "git+https://github.com/superboum/imap-codec?branch=custom/aerogramme#eb37f06a0e8d2543f60063ec80cde9e9dcb150f1" +source = "git+https://github.com/superboum/imap-codec?branch=custom/aerogramme#990e709450ff4f8986b08d2b84e28f651b74f181" dependencies = [ "base64 0.21.5", "bounded-static", diff --git a/src/imap/index.rs b/src/imap/index.rs index da94022..44109d5 100644 --- a/src/imap/index.rs +++ b/src/imap/index.rs @@ -3,7 +3,7 @@ use std::num::NonZeroU32; use anyhow::{anyhow, Result}; use imap_codec::imap_types::sequence::{SeqOrUid, Sequence, SequenceSet}; -use crate::mail::uidindex::{ImapUid, UidIndex}; +use crate::mail::uidindex::{ImapUid, ModSeq, UidIndex}; use crate::mail::unique_ident::UniqueIdent; pub struct Index<'a> { @@ -17,12 +17,10 @@ impl<'a> Index<'a> { .iter() .enumerate() .map(|(i_enum, (&uid, &uuid))| { - let flags = internal + let (_, modseq, flags) = internal .table .get(&uuid) - .ok_or(anyhow!("mail is missing from index"))? - .2 - .as_ref(); + .ok_or(anyhow!("mail is missing from index"))?; let i_int: u32 = (i_enum + 1).try_into()?; let i: NonZeroU32 = i_int.try_into()?; @@ -30,6 +28,7 @@ impl<'a> Index<'a> { i, uid, uuid, + modseq: *modseq, flags, }) }) @@ -134,6 +133,7 @@ pub struct MailIndex<'a> { pub i: NonZeroU32, pub uid: ImapUid, pub uuid: UniqueIdent, + pub modseq: ModSeq, pub flags: &'a Vec, } diff --git a/src/imap/mail_view.rs b/src/imap/mail_view.rs index eeb6b4b..a8db733 100644 --- a/src/imap/mail_view.rs +++ b/src/imap/mail_view.rs @@ -90,6 +90,7 @@ impl<'a> MailView<'a> { Ok(body) } MessageDataItemName::InternalDate => self.internal_date(), + MessageDataItemName::ModSeq => Ok(self.modseq()), }) .collect::, _>>()?; @@ -252,6 +253,10 @@ impl<'a> MailView<'a> { .ok_or(anyhow!("Unable to parse internal date"))?; Ok(MessageDataItem::InternalDate(DateTime::unvalidated(dt))) } + + fn modseq(&self) -> MessageDataItem<'static> { + MessageDataItem::ModSeq(self.in_idx.modseq) + } } pub enum SeenFlag { From f4cbf665496640f4ac7cf4ac63ab2beced674978 Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Wed, 10 Jan 2024 18:38:21 +0100 Subject: [PATCH 11/21] Fecth MODSEQ now enables the CONDSTORE capability --- src/imap/attributes.rs | 6 ++++++ src/imap/command/examined.rs | 21 +++++++++++++-------- src/imap/command/selected.rs | 21 +++++++++++++-------- src/imap/mailbox_view.rs | 4 ++-- 4 files changed, 34 insertions(+), 18 deletions(-) diff --git a/src/imap/attributes.rs b/src/imap/attributes.rs index cf7cb52..6cf52b5 100644 --- a/src/imap/attributes.rs +++ b/src/imap/attributes.rs @@ -34,6 +34,12 @@ impl AttributesProxy { Self { attrs: fetch_attrs } } + pub fn is_enabling_condstore(&self) -> bool { + self.attrs.iter().any(|x| { + matches!(x, MessageDataItemName::ModSeq) + }) + } + pub fn need_body(&self) -> bool { self.attrs.iter().any(|x| { match x { diff --git a/src/imap/command/examined.rs b/src/imap/command/examined.rs index 4767340..ef5cecc 100644 --- a/src/imap/command/examined.rs +++ b/src/imap/command/examined.rs @@ -91,14 +91,19 @@ impl<'a> ExaminedContext<'a> { uid: &bool, ) -> Result<(Response<'static>, flow::Transition)> { match self.mailbox.fetch(sequence_set, attributes, uid).await { - Ok(resp) => Ok(( - Response::build() - .to_req(self.req) - .message("FETCH completed") - .set_body(resp) - .ok()?, - flow::Transition::None, - )), + Ok((resp, enable_condstore)) => { + if enable_condstore { + self.client_capabilities.enable_condstore(); + } + Ok(( + Response::build() + .to_req(self.req) + .message("FETCH completed") + .set_body(resp) + .ok()?, + flow::Transition::None, + )) + }, Err(e) => Ok(( Response::build() .to_req(self.req) diff --git a/src/imap/command/selected.rs b/src/imap/command/selected.rs index ef2654e..24e1f41 100644 --- a/src/imap/command/selected.rs +++ b/src/imap/command/selected.rs @@ -117,14 +117,19 @@ impl<'a> SelectedContext<'a> { uid: &bool, ) -> Result<(Response<'static>, flow::Transition)> { match self.mailbox.fetch(sequence_set, attributes, uid).await { - Ok(resp) => Ok(( - Response::build() - .to_req(self.req) - .message("FETCH completed") - .set_body(resp) - .ok()?, - flow::Transition::None, - )), + Ok((resp, enable_condstore)) => { + if enable_condstore { + self.client_capabilities.enable_condstore(); + } + Ok(( + Response::build() + .to_req(self.req) + .message("FETCH completed") + .set_body(resp) + .ok()?, + flow::Transition::None, + )) + }, Err(e) => Ok(( Response::build() .to_req(self.req) diff --git a/src/imap/mailbox_view.rs b/src/imap/mailbox_view.rs index b3848b2..61beed5 100644 --- a/src/imap/mailbox_view.rs +++ b/src/imap/mailbox_view.rs @@ -259,7 +259,7 @@ impl MailboxView { sequence_set: &SequenceSet, attributes: &'b MacroOrMessageDataItemNames<'static>, is_uid_fetch: &bool, - ) -> Result>> { + ) -> Result<(Vec>, bool)> { // [1/6] Pre-compute data // a. what are the uuids of the emails we want? // b. do we need to fetch the full body? @@ -316,7 +316,7 @@ impl MailboxView { .collect::>()?; // [6/6] Build the final result that will be sent to the client. - Ok(imap_ret) + Ok((imap_ret, ap.is_enabling_condstore())) } /// A naive search implementation... From 917c32ae0b6fa3161cebdfeec748b2db0bbc1c70 Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Thu, 11 Jan 2024 10:10:00 +0100 Subject: [PATCH 12/21] MODSEQ search key first implementation --- Cargo.lock | 4 ++-- src/imap/search.rs | 18 ++++++++++++++---- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2f9e5b4..54972a4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1807,7 +1807,7 @@ dependencies = [ [[package]] name = "imap-codec" version = "2.0.0" -source = "git+https://github.com/superboum/imap-codec?branch=custom/aerogramme#990e709450ff4f8986b08d2b84e28f651b74f181" +source = "git+https://github.com/superboum/imap-codec?branch=custom/aerogramme#088fa93bfb4040fc4364aa6e9487fff4375429c3" dependencies = [ "abnf-core", "base64 0.21.5", @@ -1834,7 +1834,7 @@ dependencies = [ [[package]] name = "imap-types" version = "2.0.0" -source = "git+https://github.com/superboum/imap-codec?branch=custom/aerogramme#990e709450ff4f8986b08d2b84e28f651b74f181" +source = "git+https://github.com/superboum/imap-codec?branch=custom/aerogramme#088fa93bfb4040fc4364aa6e9487fff4375429c3" dependencies = [ "base64 0.21.5", "bounded-static", diff --git a/src/imap/search.rs b/src/imap/search.rs index c4888d0..12bad51 100644 --- a/src/imap/search.rs +++ b/src/imap/search.rs @@ -1,8 +1,8 @@ -use std::num::NonZeroU32; +use std::num::{NonZeroU32, NonZeroU64}; use anyhow::Result; use imap_codec::imap_types::core::NonEmptyVec; -use imap_codec::imap_types::search::SearchKey; +use imap_codec::imap_types::search::{SearchKey, MetadataItemSearch}; use imap_codec::imap_types::sequence::{SeqOrUid, Sequence, SequenceSet}; use crate::imap::index::MailIndex; @@ -176,6 +176,7 @@ impl<'a> Criteria<'a> { // Sequence logic maybe_seq if is_sk_seq(maybe_seq) => is_keep_seq(maybe_seq, midx).into(), maybe_flag if is_sk_flag(maybe_flag) => is_keep_flag(maybe_flag, midx).into(), + ModSeq { metadata_item , modseq } => is_keep_modseq(metadata_item, modseq, midx).into(), // All the stuff we can't evaluate yet Bcc(_) | Cc(_) | From(_) | Header(..) | SentBefore(_) | SentOn(_) | SentSince(_) @@ -210,9 +211,10 @@ impl<'a> Criteria<'a> { Not(expr) => !Criteria(expr).is_keep_on_query(mail_view), All => true, - // Reevaluating our previous logic... + //@FIXME Reevaluating our previous logic... maybe_seq if is_sk_seq(maybe_seq) => is_keep_seq(maybe_seq, &mail_view.in_idx), maybe_flag if is_sk_flag(maybe_flag) => is_keep_flag(maybe_flag, &mail_view.in_idx), + ModSeq { metadata_item , modseq } => is_keep_modseq(metadata_item, modseq, &mail_view.in_idx).into(), // Filter on mail meta Before(search_naive) => match mail_view.stored_naive_date() { @@ -318,7 +320,8 @@ fn approx_sequence_set_size(seq_set: &SequenceSet) -> u64 { } // This is wrong as sequence UID can have holes, -// as we don't know the number of messages in the mailbox also +// as we don't know the number of messages in the mailbox also +// we gave to guess fn approx_sequence_size(seq: &Sequence) -> u64 { match seq { Sequence::Single(_) => 1, @@ -458,3 +461,10 @@ fn is_keep_seq(sk: &SearchKey, midx: &MailIndex) -> bool { _ => unreachable!(), } } + +fn is_keep_modseq(filter: &Option, modseq: &NonZeroU64, midx: &MailIndex) -> bool { + if filter.is_some() { + tracing::warn!(filter=?filter, "Ignoring search metadata filter as it's not supported yet"); + } + modseq <= &midx.modseq +} From fbf2e9aa9670c991f5384350b2c78ad38dc3baf8 Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Thu, 11 Jan 2024 11:48:02 +0100 Subject: [PATCH 13/21] Enable CONDSTORE if SEARCH MODSEQ is queried --- src/imap/command/examined.rs | 5 ++++- src/imap/command/selected.rs | 5 ++++- src/imap/mailbox_view.rs | 7 +++++-- src/imap/search.rs | 11 +++++++++++ 4 files changed, 24 insertions(+), 4 deletions(-) diff --git a/src/imap/command/examined.rs b/src/imap/command/examined.rs index ef5cecc..bb05250 100644 --- a/src/imap/command/examined.rs +++ b/src/imap/command/examined.rs @@ -120,7 +120,10 @@ impl<'a> ExaminedContext<'a> { criteria: &SearchKey<'a>, uid: &bool, ) -> Result<(Response<'static>, flow::Transition)> { - let found = self.mailbox.search(charset, criteria, *uid).await?; + let (found, enable_condstore) = self.mailbox.search(charset, criteria, *uid).await?; + if enable_condstore { + self.client_capabilities.enable_condstore(); + } Ok(( Response::build() .to_req(self.req) diff --git a/src/imap/command/selected.rs b/src/imap/command/selected.rs index 24e1f41..de59ed3 100644 --- a/src/imap/command/selected.rs +++ b/src/imap/command/selected.rs @@ -146,7 +146,10 @@ impl<'a> SelectedContext<'a> { criteria: &SearchKey<'a>, uid: &bool, ) -> Result<(Response<'static>, flow::Transition)> { - let found = self.mailbox.search(charset, criteria, *uid).await?; + let (found, enable_condstore) = self.mailbox.search(charset, criteria, *uid).await?; + if enable_condstore { + self.client_capabilities.enable_condstore(); + } Ok(( Response::build() .to_req(self.req) diff --git a/src/imap/mailbox_view.rs b/src/imap/mailbox_view.rs index 61beed5..ecdd745 100644 --- a/src/imap/mailbox_view.rs +++ b/src/imap/mailbox_view.rs @@ -325,7 +325,7 @@ impl MailboxView { _charset: &Option>, search_key: &SearchKey<'a>, uid: bool, - ) -> Result>> { + ) -> Result<(Vec>, bool)> { // 1. Compute the subset of sequence identifiers we need to fetch // based on the search query let crit = search::Criteria(search_key); @@ -354,7 +354,10 @@ impl MailboxView { _ => final_selection.map(|in_idx| in_idx.i).collect(), }; - Ok(vec![Body::Data(Data::Search(selection_fmt))]) + // 7. Add the modseq entry if needed + let is_modseq = crit.is_modseq(); + + Ok((vec![Body::Data(Data::Search(selection_fmt))], is_modseq)) } // ---- diff --git a/src/imap/search.rs b/src/imap/search.rs index 12bad51..61cbad5 100644 --- a/src/imap/search.rs +++ b/src/imap/search.rs @@ -112,6 +112,17 @@ impl<'a> Criteria<'a> { } } + pub fn is_modseq(&self) -> bool { + use SearchKey::*; + match self.0 { + And(and_list) => and_list.as_ref().iter().any(|child| Criteria(child).is_modseq()), + Or(left, right) => Criteria(left).is_modseq() || Criteria(right).is_modseq(), + Not(child) => Criteria(child).is_modseq(), + ModSeq { .. } => true, + _ => false, + } + } + /// Returns emails that we now for sure we want to keep /// but also a second list of emails we need to investigate further by /// fetching some remote data From a9d33c67080fd08b057501c36a07fade351bd83d Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Thu, 11 Jan 2024 11:55:40 +0100 Subject: [PATCH 14/21] MODSEQ is now returned on non empty search results --- Cargo.lock | 4 ++-- src/imap/mailbox_view.rs | 13 ++++++++++--- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 54972a4..dae5cb9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1807,7 +1807,7 @@ dependencies = [ [[package]] name = "imap-codec" version = "2.0.0" -source = "git+https://github.com/superboum/imap-codec?branch=custom/aerogramme#088fa93bfb4040fc4364aa6e9487fff4375429c3" +source = "git+https://github.com/superboum/imap-codec?branch=custom/aerogramme#3fdc1f3184ec121823d44b3f39e56b448ac80751" dependencies = [ "abnf-core", "base64 0.21.5", @@ -1834,7 +1834,7 @@ dependencies = [ [[package]] name = "imap-types" version = "2.0.0" -source = "git+https://github.com/superboum/imap-codec?branch=custom/aerogramme#088fa93bfb4040fc4364aa6e9487fff4375429c3" +source = "git+https://github.com/superboum/imap-codec?branch=custom/aerogramme#3fdc1f3184ec121823d44b3f39e56b448ac80751" dependencies = [ "base64 0.21.5", "bounded-static", diff --git a/src/imap/mailbox_view.rs b/src/imap/mailbox_view.rs index ecdd745..9e9e785 100644 --- a/src/imap/mailbox_view.rs +++ b/src/imap/mailbox_view.rs @@ -1,4 +1,4 @@ -use std::num::NonZeroU32; +use std::num::{NonZeroU32, NonZeroU64}; use std::sync::Arc; use anyhow::{anyhow, Error, Result}; @@ -348,7 +348,7 @@ impl MailboxView { // 6. Format the result according to the client's taste: // either return UID or ID. - let final_selection = kept_idx.into_iter().chain(kept_query.into_iter()); + let final_selection = kept_idx.iter().chain(kept_query.iter()); let selection_fmt = match uid { true => final_selection.map(|in_idx| in_idx.uid).collect(), _ => final_selection.map(|in_idx| in_idx.i).collect(), @@ -356,8 +356,15 @@ impl MailboxView { // 7. Add the modseq entry if needed let is_modseq = crit.is_modseq(); + let maybe_modseq = match is_modseq { + true => { + let final_selection = kept_idx.iter().chain(kept_query.iter()); + final_selection.map(|in_idx| in_idx.modseq).max().map(|r| NonZeroU64::try_from(r)).transpose()? + }, + _ => None, + }; - Ok((vec![Body::Data(Data::Search(selection_fmt))], is_modseq)) + Ok((vec![Body::Data(Data::Search(selection_fmt, maybe_modseq))], is_modseq)) } // ---- From 60a166185a034019d9e55136ee4417386ff57703 Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Thu, 11 Jan 2024 16:55:37 +0100 Subject: [PATCH 15/21] Fetch and store modifiers are parsed --- Cargo.lock | 4 ++-- src/imap/capability.rs | 11 ++++++----- src/imap/command/authenticated.rs | 18 +++++++----------- src/imap/command/examined.rs | 6 ++++-- src/imap/command/selected.rs | 10 ++++++---- 5 files changed, 25 insertions(+), 24 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index dae5cb9..e00317b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1807,7 +1807,7 @@ dependencies = [ [[package]] name = "imap-codec" version = "2.0.0" -source = "git+https://github.com/superboum/imap-codec?branch=custom/aerogramme#3fdc1f3184ec121823d44b3f39e56b448ac80751" +source = "git+https://github.com/superboum/imap-codec?branch=custom/aerogramme#b0a80c4826f3d8bf2d2e69f68443c261e62bb40f" dependencies = [ "abnf-core", "base64 0.21.5", @@ -1834,7 +1834,7 @@ dependencies = [ [[package]] name = "imap-types" version = "2.0.0" -source = "git+https://github.com/superboum/imap-codec?branch=custom/aerogramme#3fdc1f3184ec121823d44b3f39e56b448ac80751" +source = "git+https://github.com/superboum/imap-codec?branch=custom/aerogramme#b0a80c4826f3d8bf2d2e69f68443c261e62bb40f" dependencies = [ "base64 0.21.5", "bounded-static", diff --git a/src/imap/capability.rs b/src/imap/capability.rs index 37f14df..53d7b7d 100644 --- a/src/imap/capability.rs +++ b/src/imap/capability.rs @@ -1,4 +1,5 @@ -use imap_codec::imap_types::core::{Atom, NonEmptyVec}; +use imap_codec::imap_types::command::SelectExamineModifier; +use imap_codec::imap_types::core::NonEmptyVec; use imap_codec::imap_types::extensions::enable::{CapabilityEnable, Utf8Kind}; use imap_codec::imap_types::response::Capability; use std::collections::HashSet; @@ -90,10 +91,10 @@ impl ClientCapability { self.condstore = self.condstore.enable(); } - pub fn select_enable(&mut self, atoms: &[Atom]) { - for at in atoms.iter() { - if at.as_ref().to_uppercase() == "CONDSTORE" { - self.enable_condstore(); + pub fn select_enable(&mut self, mods: &[SelectExamineModifier]) { + for m in mods.iter() { + match m { + SelectExamineModifier::Condstore => self.enable_condstore(), } } } diff --git a/src/imap/command/authenticated.rs b/src/imap/command/authenticated.rs index da41182..9b6bb24 100644 --- a/src/imap/command/authenticated.rs +++ b/src/imap/command/authenticated.rs @@ -2,7 +2,7 @@ use std::collections::BTreeMap; use std::sync::Arc; use anyhow::{anyhow, bail, Result}; -use imap_codec::imap_types::command::{Command, CommandBody}; +use imap_codec::imap_types::command::{Command, CommandBody, SelectExamineModifier}; use imap_codec::imap_types::core::{Atom, Literal, NonEmptyVec, QuotedChar}; use imap_codec::imap_types::datetime::DateTime; use imap_codec::imap_types::extensions::enable::CapabilityEnable; @@ -58,8 +58,8 @@ pub async fn dispatch<'a>( } => ctx.status(mailbox, item_names).await, CommandBody::Subscribe { mailbox } => ctx.subscribe(mailbox).await, CommandBody::Unsubscribe { mailbox } => ctx.unsubscribe(mailbox).await, - CommandBody::Select { mailbox, parameters } => ctx.select(mailbox, parameters).await, - CommandBody::Examine { mailbox, parameters } => ctx.examine(mailbox, parameters).await, + CommandBody::Select { mailbox, modifiers } => ctx.select(mailbox, modifiers).await, + CommandBody::Examine { mailbox, modifiers } => ctx.examine(mailbox, modifiers).await, CommandBody::Append { mailbox, flags, @@ -422,11 +422,9 @@ impl<'a> AuthenticatedContext<'a> { async fn select( self, mailbox: &MailboxCodec<'a>, - parameters: &Option>>, + modifiers: &[SelectExamineModifier], ) -> Result<(Response<'static>, flow::Transition)> { - parameters.as_ref().map(|plist| - self.client_capabilities.select_enable(plist.as_ref()) - ); + self.client_capabilities.select_enable(modifiers); let name: &str = MailboxName(mailbox).try_into()?; @@ -462,11 +460,9 @@ impl<'a> AuthenticatedContext<'a> { async fn examine( self, mailbox: &MailboxCodec<'a>, - parameters: &Option>>, + modifiers: &[SelectExamineModifier], ) -> Result<(Response<'static>, flow::Transition)> { - parameters.as_ref().map(|plist| - self.client_capabilities.select_enable(plist.as_ref()) - ); + self.client_capabilities.select_enable(modifiers); let name: &str = MailboxName(mailbox).try_into()?; diff --git a/src/imap/command/examined.rs b/src/imap/command/examined.rs index bb05250..a8077e3 100644 --- a/src/imap/command/examined.rs +++ b/src/imap/command/examined.rs @@ -1,7 +1,7 @@ use std::sync::Arc; use anyhow::Result; -use imap_codec::imap_types::command::{Command, CommandBody}; +use imap_codec::imap_types::command::{Command, CommandBody, FetchModifier}; use imap_codec::imap_types::core::Charset; use imap_codec::imap_types::fetch::MacroOrMessageDataItemNames; use imap_codec::imap_types::search::SearchKey; @@ -37,8 +37,9 @@ pub async fn dispatch(ctx: ExaminedContext<'_>) -> Result<(Response<'static>, fl CommandBody::Fetch { sequence_set, macro_or_item_names, + modifiers, uid, - } => ctx.fetch(sequence_set, macro_or_item_names, uid).await, + } => ctx.fetch(sequence_set, macro_or_item_names, modifiers, uid).await, CommandBody::Search { charset, criteria, @@ -88,6 +89,7 @@ impl<'a> ExaminedContext<'a> { self, sequence_set: &SequenceSet, attributes: &'a MacroOrMessageDataItemNames<'static>, + modifiers: &[FetchModifier], uid: &bool, ) -> Result<(Response<'static>, flow::Transition)> { match self.mailbox.fetch(sequence_set, attributes, uid).await { diff --git a/src/imap/command/selected.rs b/src/imap/command/selected.rs index de59ed3..862d4aa 100644 --- a/src/imap/command/selected.rs +++ b/src/imap/command/selected.rs @@ -1,8 +1,8 @@ use std::sync::Arc; use anyhow::Result; -use imap_codec::imap_types::command::{Command, CommandBody, StoreModifier}; -use imap_codec::imap_types::core::{Charset, Atom}; +use imap_codec::imap_types::command::{Command, CommandBody, FetchModifier, StoreModifier}; +use imap_codec::imap_types::core::Charset; use imap_codec::imap_types::fetch::MacroOrMessageDataItemNames; use imap_codec::imap_types::flag::{Flag, StoreResponse, StoreType}; use imap_codec::imap_types::mailbox::Mailbox as MailboxCodec; @@ -43,8 +43,9 @@ pub async fn dispatch<'a>( CommandBody::Fetch { sequence_set, macro_or_item_names, + modifiers, uid, - } => ctx.fetch(sequence_set, macro_or_item_names, uid).await, + } => ctx.fetch(sequence_set, macro_or_item_names, modifiers, uid).await, CommandBody::Search { charset, criteria, @@ -114,6 +115,7 @@ impl<'a> SelectedContext<'a> { self, sequence_set: &SequenceSet, attributes: &'a MacroOrMessageDataItemNames<'static>, + modifiers: &[FetchModifier], uid: &bool, ) -> Result<(Response<'static>, flow::Transition)> { match self.mailbox.fetch(sequence_set, attributes, uid).await { @@ -194,7 +196,7 @@ impl<'a> SelectedContext<'a> { kind: &StoreType, response: &StoreResponse, flags: &[Flag<'a>], - modifiers: &[(Atom<'a>, StoreModifier<'a>)], + modifiers: &[StoreModifier], uid: &bool, ) -> Result<(Response<'static>, flow::Transition)> { tracing::info!(modifiers=?modifiers); From d24eb9918e3ab0c69af05c8cb92424ecaba903f3 Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Thu, 11 Jan 2024 17:13:59 +0100 Subject: [PATCH 16/21] Enable CONDSTORE on STORE/FETCH modifier --- src/imap/capability.rs | 22 +++++++++++++++++++++- src/imap/command/examined.rs | 15 ++++++++++----- src/imap/command/selected.rs | 20 +++++++++++++------- src/imap/mailbox_view.rs | 10 +++++----- 4 files changed, 49 insertions(+), 18 deletions(-) diff --git a/src/imap/capability.rs b/src/imap/capability.rs index 53d7b7d..6533ccb 100644 --- a/src/imap/capability.rs +++ b/src/imap/capability.rs @@ -1,9 +1,11 @@ -use imap_codec::imap_types::command::SelectExamineModifier; +use imap_codec::imap_types::command::{FetchModifier, StoreModifier, SelectExamineModifier}; use imap_codec::imap_types::core::NonEmptyVec; use imap_codec::imap_types::extensions::enable::{CapabilityEnable, Utf8Kind}; use imap_codec::imap_types::response::Capability; use std::collections::HashSet; +use crate::imap::attributes::AttributesProxy; + fn capability_unselect() -> Capability<'static> { Capability::try_from("UNSELECT").unwrap() } @@ -91,6 +93,24 @@ impl ClientCapability { self.condstore = self.condstore.enable(); } + pub fn attributes_enable(&mut self, ap: &AttributesProxy) { + if ap.is_enabling_condstore() { + self.enable_condstore() + } + } + + pub fn fetch_modifiers_enable(&mut self, mods: &[FetchModifier]) { + if mods.iter().any(|x| matches!(x, FetchModifier::ChangedSince(..))) { + self.enable_condstore() + } + } + + pub fn store_modifiers_enable(&mut self, mods: &[StoreModifier]) { + if mods.iter().any(|x| matches!(x, StoreModifier::UnchangedSince(..))) { + self.enable_condstore() + } + } + pub fn select_enable(&mut self, mods: &[SelectExamineModifier]) { for m in mods.iter() { match m { diff --git a/src/imap/command/examined.rs b/src/imap/command/examined.rs index a8077e3..cdebc6d 100644 --- a/src/imap/command/examined.rs +++ b/src/imap/command/examined.rs @@ -7,6 +7,7 @@ use imap_codec::imap_types::fetch::MacroOrMessageDataItemNames; use imap_codec::imap_types::search::SearchKey; use imap_codec::imap_types::sequence::SequenceSet; +use crate::imap::attributes::AttributesProxy; use crate::imap::capability::{ClientCapability, ServerCapability}; use crate::imap::command::{anystate, authenticated}; use crate::imap::flow; @@ -92,11 +93,15 @@ impl<'a> ExaminedContext<'a> { modifiers: &[FetchModifier], uid: &bool, ) -> Result<(Response<'static>, flow::Transition)> { - match self.mailbox.fetch(sequence_set, attributes, uid).await { - Ok((resp, enable_condstore)) => { - if enable_condstore { - self.client_capabilities.enable_condstore(); - } + let ap = AttributesProxy::new(attributes, *uid); + + match self.mailbox.fetch(sequence_set, &ap, uid).await { + Ok(resp) => { + // Capabilities enabling logic only on successful command + // (according to my understanding of the spec) + self.client_capabilities.attributes_enable(&ap); + self.client_capabilities.fetch_modifiers_enable(modifiers); + Ok(( Response::build() .to_req(self.req) diff --git a/src/imap/command/selected.rs b/src/imap/command/selected.rs index 862d4aa..d7aa94f 100644 --- a/src/imap/command/selected.rs +++ b/src/imap/command/selected.rs @@ -15,7 +15,7 @@ use crate::imap::command::{anystate, authenticated, MailboxName}; use crate::imap::flow; use crate::imap::mailbox_view::MailboxView; use crate::imap::response::Response; - +use crate::imap::attributes::AttributesProxy; use crate::mail::user::User; pub struct SelectedContext<'a> { @@ -118,11 +118,16 @@ impl<'a> SelectedContext<'a> { modifiers: &[FetchModifier], uid: &bool, ) -> Result<(Response<'static>, flow::Transition)> { - match self.mailbox.fetch(sequence_set, attributes, uid).await { - Ok((resp, enable_condstore)) => { - if enable_condstore { - self.client_capabilities.enable_condstore(); - } + let ap = AttributesProxy::new(attributes, *uid); + + match self.mailbox.fetch(sequence_set, &ap, uid).await { + Ok(resp) => { + // Capabilities enabling logic only on successful command + // (according to my understanding of the spec) + self.client_capabilities.attributes_enable(&ap); + self.client_capabilities.fetch_modifiers_enable(modifiers); + + // Response to the client Ok(( Response::build() .to_req(self.req) @@ -199,12 +204,13 @@ impl<'a> SelectedContext<'a> { modifiers: &[StoreModifier], uid: &bool, ) -> Result<(Response<'static>, flow::Transition)> { - tracing::info!(modifiers=?modifiers); let data = self .mailbox .store(sequence_set, kind, response, flags, uid) .await?; + self.client_capabilities.store_modifiers_enable(modifiers); + Ok(( Response::build() .to_req(self.req) diff --git a/src/imap/mailbox_view.rs b/src/imap/mailbox_view.rs index 9e9e785..c3900cf 100644 --- a/src/imap/mailbox_view.rs +++ b/src/imap/mailbox_view.rs @@ -6,7 +6,7 @@ use anyhow::{anyhow, Error, Result}; use futures::stream::{FuturesOrdered, StreamExt}; use imap_codec::imap_types::core::Charset; -use imap_codec::imap_types::fetch::{MacroOrMessageDataItemNames, MessageDataItem}; +use imap_codec::imap_types::fetch::MessageDataItem; use imap_codec::imap_types::flag::{Flag, FlagFetch, FlagPerm, StoreResponse, StoreType}; use imap_codec::imap_types::response::{Code, CodeOther, Data, Status}; use imap_codec::imap_types::search::SearchKey; @@ -257,13 +257,13 @@ impl MailboxView { pub async fn fetch<'b>( &self, sequence_set: &SequenceSet, - attributes: &'b MacroOrMessageDataItemNames<'static>, + ap: &AttributesProxy, is_uid_fetch: &bool, - ) -> Result<(Vec>, bool)> { + ) -> Result>> { // [1/6] Pre-compute data // a. what are the uuids of the emails we want? // b. do we need to fetch the full body? - let ap = AttributesProxy::new(attributes, *is_uid_fetch); + //let ap = AttributesProxy::new(attributes, *is_uid_fetch); let query_scope = match ap.need_body() { true => QueryScope::Full, _ => QueryScope::Partial, @@ -316,7 +316,7 @@ impl MailboxView { .collect::>()?; // [6/6] Build the final result that will be sent to the client. - Ok((imap_ret, ap.is_enabling_condstore())) + Ok(imap_ret) } /// A naive search implementation... From 3c7186ab5ac5a66f782a038d937b6679780df458 Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Thu, 11 Jan 2024 23:02:03 +0100 Subject: [PATCH 17/21] Finalize implementation of CONDSTORE --- src/imap/attributes.rs | 11 ++++- src/imap/command/examined.rs | 15 ++++-- src/imap/command/selected.rs | 28 ++++++++--- src/imap/index.rs | 32 ++++++++++++- src/imap/mailbox_view.rs | 91 +++++++++++++++++++++++++++++------- 5 files changed, 147 insertions(+), 30 deletions(-) diff --git a/src/imap/attributes.rs b/src/imap/attributes.rs index 6cf52b5..d094f1a 100644 --- a/src/imap/attributes.rs +++ b/src/imap/attributes.rs @@ -1,4 +1,5 @@ use imap_codec::imap_types::fetch::{MacroOrMessageDataItemNames, MessageDataItemName, Section}; +use imap_codec::imap_types::command::FetchModifier; /// Internal decisions based on fetched attributes /// passed by the client @@ -7,7 +8,7 @@ pub struct AttributesProxy { pub attrs: Vec>, } impl AttributesProxy { - pub fn new(attrs: &MacroOrMessageDataItemNames<'static>, is_uid_fetch: bool) -> Self { + pub fn new(attrs: &MacroOrMessageDataItemNames<'static>, modifiers: &[FetchModifier], is_uid_fetch: bool) -> Self { // Expand macros let mut fetch_attrs = match attrs { MacroOrMessageDataItemNames::Macro(m) => { @@ -31,6 +32,14 @@ impl AttributesProxy { fetch_attrs.push(MessageDataItemName::Uid); } + // Handle inferred MODSEQ tag + let is_changed_since = modifiers + .iter() + .any(|m| matches!(m, FetchModifier::ChangedSince(..))); + if is_changed_since && !fetch_attrs.contains(&MessageDataItemName::ModSeq) { + fetch_attrs.push(MessageDataItemName::ModSeq); + } + Self { attrs: fetch_attrs } } diff --git a/src/imap/command/examined.rs b/src/imap/command/examined.rs index cdebc6d..9fc0990 100644 --- a/src/imap/command/examined.rs +++ b/src/imap/command/examined.rs @@ -1,4 +1,5 @@ use std::sync::Arc; +use std::num::NonZeroU64; use anyhow::Result; use imap_codec::imap_types::command::{Command, CommandBody, FetchModifier}; @@ -11,7 +12,7 @@ use crate::imap::attributes::AttributesProxy; use crate::imap::capability::{ClientCapability, ServerCapability}; use crate::imap::command::{anystate, authenticated}; use crate::imap::flow; -use crate::imap::mailbox_view::MailboxView; +use crate::imap::mailbox_view::{MailboxView, UpdateParameters}; use crate::imap::response::Response; use crate::mail::user::User; @@ -93,9 +94,15 @@ impl<'a> ExaminedContext<'a> { modifiers: &[FetchModifier], uid: &bool, ) -> Result<(Response<'static>, flow::Transition)> { - let ap = AttributesProxy::new(attributes, *uid); + let ap = AttributesProxy::new(attributes, modifiers, *uid); + let mut changed_since: Option = None; + modifiers.iter().for_each(|m| match m { + FetchModifier::ChangedSince(val) => { + changed_since = Some(*val); + }, + }); - match self.mailbox.fetch(sequence_set, &ap, uid).await { + match self.mailbox.fetch(sequence_set, &ap, changed_since, uid).await { Ok(resp) => { // Capabilities enabling logic only on successful command // (according to my understanding of the spec) @@ -144,7 +151,7 @@ impl<'a> ExaminedContext<'a> { pub async fn noop(self) -> Result<(Response<'static>, flow::Transition)> { self.mailbox.internal.mailbox.force_sync().await?; - let updates = self.mailbox.update().await?; + let updates = self.mailbox.update(UpdateParameters::default()).await?; Ok(( Response::build() .to_req(self.req) diff --git a/src/imap/command/selected.rs b/src/imap/command/selected.rs index d7aa94f..d694fd1 100644 --- a/src/imap/command/selected.rs +++ b/src/imap/command/selected.rs @@ -1,4 +1,5 @@ use std::sync::Arc; +use std::num::NonZeroU64; use anyhow::Result; use imap_codec::imap_types::command::{Command, CommandBody, FetchModifier, StoreModifier}; @@ -13,7 +14,7 @@ use imap_codec::imap_types::sequence::SequenceSet; use crate::imap::capability::{ClientCapability, ServerCapability}; use crate::imap::command::{anystate, authenticated, MailboxName}; use crate::imap::flow; -use crate::imap::mailbox_view::MailboxView; +use crate::imap::mailbox_view::{MailboxView, UpdateParameters}; use crate::imap::response::Response; use crate::imap::attributes::AttributesProxy; use crate::mail::user::User; @@ -118,9 +119,15 @@ impl<'a> SelectedContext<'a> { modifiers: &[FetchModifier], uid: &bool, ) -> Result<(Response<'static>, flow::Transition)> { - let ap = AttributesProxy::new(attributes, *uid); + let ap = AttributesProxy::new(attributes, modifiers, *uid); + let mut changed_since: Option = None; + modifiers.iter().for_each(|m| match m { + FetchModifier::ChangedSince(val) => { + changed_since = Some(*val); + }, + }); - match self.mailbox.fetch(sequence_set, &ap, uid).await { + match self.mailbox.fetch(sequence_set, &ap, changed_since, uid).await { Ok(resp) => { // Capabilities enabling logic only on successful command // (according to my understanding of the spec) @@ -170,7 +177,7 @@ impl<'a> SelectedContext<'a> { pub async fn noop(self) -> Result<(Response<'static>, flow::Transition)> { self.mailbox.internal.mailbox.force_sync().await?; - let updates = self.mailbox.update().await?; + let updates = self.mailbox.update(UpdateParameters::default()).await?; Ok(( Response::build() .to_req(self.req) @@ -204,10 +211,18 @@ impl<'a> SelectedContext<'a> { modifiers: &[StoreModifier], uid: &bool, ) -> Result<(Response<'static>, flow::Transition)> { - let data = self + let mut unchanged_since: Option = None; + modifiers.iter().for_each(|m| match m { + StoreModifier::UnchangedSince(val) => { + unchanged_since = Some(*val); + }, + }); + + let (data, modified) = self .mailbox - .store(sequence_set, kind, response, flags, uid) + .store(sequence_set, kind, response, flags, unchanged_since, uid) .await?; + let modified_str = format!("MODIFIED {}", modified.into_iter().map(|x| x.to_string()).collect::>().join(",")); self.client_capabilities.store_modifiers_enable(modifiers); @@ -215,6 +230,7 @@ impl<'a> SelectedContext<'a> { Response::build() .to_req(self.req) .message("STORE completed") + .code(Code::Other(CodeOther::unvalidated(modified_str.into_bytes()))) .set_body(data) .ok()?, flow::Transition::None, diff --git a/src/imap/index.rs b/src/imap/index.rs index 44109d5..9b794b8 100644 --- a/src/imap/index.rs +++ b/src/imap/index.rs @@ -1,4 +1,4 @@ -use std::num::NonZeroU32; +use std::num::{NonZeroU32, NonZeroU64}; use anyhow::{anyhow, Result}; use imap_codec::imap_types::sequence::{SeqOrUid, Sequence, SequenceSet}; @@ -126,6 +126,36 @@ impl<'a> Index<'a> { _ => self.fetch_on_id(sequence_set), } } + + pub fn fetch_changed_since( + self: &'a Index<'a>, + sequence_set: &SequenceSet, + maybe_modseq: Option, + by_uid: bool, + ) -> Result>> { + let raw = self.fetch(sequence_set, by_uid)?; + let res = match maybe_modseq { + Some(pit) => raw.into_iter().filter(|midx| midx.modseq > pit).collect(), + None => raw, + }; + + Ok(res) + } + + pub fn fetch_unchanged_since( + self: &'a Index<'a>, + sequence_set: &SequenceSet, + maybe_modseq: Option, + by_uid: bool, + ) -> Result<(Vec<&'a MailIndex<'a>>, Vec<&'a MailIndex<'a>>)> { + let raw = self.fetch(sequence_set, by_uid)?; + let res = match maybe_modseq { + Some(pit) => raw.into_iter().partition(|midx| midx.modseq <= pit), + None => (raw, vec![]), + }; + + Ok(res) + } } #[derive(Clone, Debug)] diff --git a/src/imap/mailbox_view.rs b/src/imap/mailbox_view.rs index c3900cf..683e209 100644 --- a/src/imap/mailbox_view.rs +++ b/src/imap/mailbox_view.rs @@ -1,5 +1,6 @@ use std::num::{NonZeroU32, NonZeroU64}; use std::sync::Arc; +use std::collections::HashSet; use anyhow::{anyhow, Error, Result}; @@ -12,6 +13,7 @@ use imap_codec::imap_types::response::{Code, CodeOther, Data, Status}; use imap_codec::imap_types::search::SearchKey; use imap_codec::imap_types::sequence::SequenceSet; +use crate::mail::unique_ident::UniqueIdent; use crate::mail::mailbox::Mailbox; use crate::mail::query::QueryScope; use crate::mail::snapshot::FrozenMailbox; @@ -32,6 +34,21 @@ const DEFAULT_FLAGS: [Flag; 5] = [ Flag::Draft, ]; +pub struct UpdateParameters { + pub silence: HashSet, + pub with_modseq: bool, + pub with_uid: bool, +} +impl Default for UpdateParameters { + fn default() -> Self { + Self { + silence: HashSet::new(), + with_modseq: false, + with_uid: false, + } + } +} + /// A MailboxView is responsible for giving the client the information /// it needs about a mailbox, such as an initial summary of the mailbox's /// content and continuous updates indicating when the content @@ -59,7 +76,7 @@ impl MailboxView { /// what the client knows and what is actually in the mailbox. /// This does NOT trigger a sync, it bases itself on what is currently /// loaded in RAM by Bayou. - pub async fn update(&mut self) -> Result>> { + pub async fn update(&mut self, params: UpdateParameters) -> Result>> { let old_snapshot = self.internal.update().await; let new_snapshot = &self.internal.snapshot; @@ -105,19 +122,31 @@ impl MailboxView { } else { // - if flags changed for existing mails, tell client for (i, (_uid, uuid)) in new_snapshot.idx_by_uid.iter().enumerate() { + if params.silence.contains(uuid) { + continue; + } + let old_mail = old_snapshot.table.get(uuid); let new_mail = new_snapshot.table.get(uuid); if old_mail.is_some() && old_mail != new_mail { - if let Some((uid, _modseq, flags)) = new_mail { + if let Some((uid, modseq, flags)) = new_mail { + let mut items = vec![ + MessageDataItem::Flags( + flags.iter().filter_map(|f| flags::from_str(f)).collect(), + ), + ]; + + if params.with_uid { + items.push(MessageDataItem::Uid(*uid)); + } + + if params.with_modseq { + items.push(MessageDataItem::ModSeq(*modseq)); + } + data.push(Body::Data(Data::Fetch { seq: NonZeroU32::try_from((i + 1) as u32).unwrap(), - items: vec![ - MessageDataItem::Uid(*uid), - MessageDataItem::Flags( - flags.iter().filter_map(|f| flags::from_str(f)).collect(), - ), - ] - .try_into()?, + items: items.try_into()?, })); } } @@ -149,17 +178,20 @@ impl MailboxView { &mut self, sequence_set: &SequenceSet, kind: &StoreType, - _response: &StoreResponse, + response: &StoreResponse, flags: &[Flag<'a>], + unchanged_since: Option, is_uid_store: &bool, - ) -> Result>> { + ) -> Result<(Vec>, Vec)> { self.internal.sync().await?; let flags = flags.iter().map(|x| x.to_string()).collect::>(); let idx = self.index()?; - let mails = idx.fetch(sequence_set, *is_uid_store)?; - for mi in mails.iter() { + let (editable, in_conflict) = idx + .fetch_unchanged_since(sequence_set, unchanged_since, *is_uid_store)?; + + for mi in editable.iter() { match kind { StoreType::Add => { self.internal.mailbox.add_flags(mi.uuid, &flags[..]).await?; @@ -173,8 +205,23 @@ impl MailboxView { } } - // @TODO: handle _response - self.update().await + let silence = match response { + StoreResponse::Answer => HashSet::new(), + StoreResponse::Silent => editable.iter().map(|midx| midx.uuid).collect(), + }; + + let conflict_id_or_uid = match is_uid_store { + true => in_conflict.into_iter().map(|midx| midx.uid).collect(), + _ => in_conflict.into_iter().map(|midx| midx.i).collect(), + }; + + let summary = self.update(UpdateParameters { + with_uid: *is_uid_store, + with_modseq: unchanged_since.is_some(), + silence, + }).await?; + + Ok((summary, conflict_id_or_uid)) } pub async fn expunge(&mut self) -> Result>> { @@ -192,7 +239,7 @@ impl MailboxView { self.internal.mailbox.delete(msg).await?; } - self.update().await + self.update(UpdateParameters::default()).await } pub async fn copy( @@ -247,7 +294,10 @@ impl MailboxView { ret.push((mi.uid, dest_uid)); } - let update = self.update().await?; + let update = self.update(UpdateParameters { + with_uid: *is_uid_copy, + ..UpdateParameters::default() + }).await?; Ok((to_state.uidvalidity, ret, update)) } @@ -258,6 +308,7 @@ impl MailboxView { &self, sequence_set: &SequenceSet, ap: &AttributesProxy, + changed_since: Option, is_uid_fetch: &bool, ) -> Result>> { // [1/6] Pre-compute data @@ -270,7 +321,11 @@ impl MailboxView { }; tracing::debug!("Query scope {:?}", query_scope); let idx = self.index()?; - let mail_idx_list = idx.fetch(sequence_set, *is_uid_fetch)?; + let mail_idx_list = idx.fetch_changed_since( + sequence_set, + changed_since, + *is_uid_fetch + )?; // [2/6] Fetch the emails let uuids = mail_idx_list From 69632879865bb351373c86daa1585942b8505618 Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Fri, 12 Jan 2024 09:54:58 +0100 Subject: [PATCH 18/21] Fix unit tests --- src/imap/mailbox_view.rs | 6 ++++-- src/mail/uidindex.rs | 3 ++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/imap/mailbox_view.rs b/src/imap/mailbox_view.rs index 683e209..07fa3ad 100644 --- a/src/imap/mailbox_view.rs +++ b/src/imap/mailbox_view.rs @@ -612,6 +612,7 @@ mod tests { peek: false, }, ]), + &[], false, ); @@ -623,12 +624,13 @@ mod tests { rfc822_size: 8usize, }; - let index_entry = (NonZeroU32::MIN, vec![]); + let index_entry = (NonZeroU32::MIN, NonZeroU64::MIN, vec![]); let mail_in_idx = MailIndex { i: NonZeroU32::MIN, uid: index_entry.0, + modseq: index_entry.1, uuid: unique_ident::gen_ident(), - flags: &index_entry.1, + flags: &index_entry.2, }; let rfc822 = b"Subject: hello\r\nFrom: a@a.a\r\nTo: b@b.b\r\nDate: Thu, 12 Oct 2023 08:45:28 +0000\r\n\r\nhello world"; let qr = QueryResult::FullResult { diff --git a/src/mail/uidindex.rs b/src/mail/uidindex.rs index f703d04..248aab1 100644 --- a/src/mail/uidindex.rs +++ b/src/mail/uidindex.rs @@ -393,6 +393,7 @@ mod tests { assert_eq!(state.table.len(), 1); let (uid, modseq, flags) = state.table.get(&m).unwrap(); assert_eq!(*uid, NonZeroU32::new(1).unwrap()); + assert_eq!(*modseq, NonZeroU64::new(1).unwrap()); assert_eq!(flags.len(), 2); let ident = state.idx_by_uid.get(&NonZeroU32::new(1).unwrap()).unwrap(); assert_eq!(&m, ident); @@ -447,7 +448,7 @@ mod tests { { let m = UniqueIdent([0x03; 24]); let f = vec!["\\Archive".to_string(), "\\Recent".to_string()]; - let ev = UidIndexOp::MailAdd(m, NonZeroU32::new(1).unwrap(), NonZeroU32::new(1).unwrap(), f); + let ev = UidIndexOp::MailAdd(m, NonZeroU32::new(1).unwrap(), NonZeroU64::new(1).unwrap(), f); state = state.apply(&ev); } From c1e7f7264a68533262bfe1d181061e577e508030 Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Fri, 12 Jan 2024 13:01:22 +0100 Subject: [PATCH 19/21] fix a condstore bug --- src/imap/command/selected.rs | 24 ++++++++++------ tests/behavior.rs | 51 ++++++++++++++++++++++++---------- tests/common/fragments.rs | 53 +++++++++++++++++++++++++----------- tests/common/mod.rs | 9 ++++++ 4 files changed, 98 insertions(+), 39 deletions(-) diff --git a/src/imap/command/selected.rs b/src/imap/command/selected.rs index d694fd1..c13b71a 100644 --- a/src/imap/command/selected.rs +++ b/src/imap/command/selected.rs @@ -222,17 +222,25 @@ impl<'a> SelectedContext<'a> { .mailbox .store(sequence_set, kind, response, flags, unchanged_since, uid) .await?; - let modified_str = format!("MODIFIED {}", modified.into_iter().map(|x| x.to_string()).collect::>().join(",")); + + let mut ok_resp = Response::build() + .to_req(self.req) + .message("STORE completed") + .set_body(data); + + + match modified[..] { + [] => (), + [_head, ..] => { + let modified_str = format!("MODIFIED {}", modified.into_iter().map(|x| x.to_string()).collect::>().join(",")); + ok_resp = ok_resp.code(Code::Other(CodeOther::unvalidated(modified_str.into_bytes()))); + }, + }; + self.client_capabilities.store_modifiers_enable(modifiers); - Ok(( - Response::build() - .to_req(self.req) - .message("STORE completed") - .code(Code::Other(CodeOther::unvalidated(modified_str.into_bytes()))) - .set_body(data) - .ok()?, + Ok((ok_resp.ok()?, flow::Transition::None, )) } diff --git a/tests/behavior.rs b/tests/behavior.rs index 17f3a72..3d9604e 100644 --- a/tests/behavior.rs +++ b/tests/behavior.rs @@ -9,10 +9,12 @@ fn main() { rfc5161_imapext_enable(); rfc6851_imapext_move(); rfc7888_imapext_literal(); + rfc4551_imapext_condstore(); + println!("โœ… SUCCESS ๐ŸŒŸ๐Ÿš€๐Ÿฅณ๐Ÿ™๐Ÿฅน"); } fn rfc3501_imap4rev1_base() { - println!("rfc3501_imap4rev1_base"); + println!("๐Ÿงช rfc3501_imap4rev1_base"); common::aerogramme_provider_daemon_dev(|imap_socket, lmtp_socket| { connect(imap_socket).context("server says hello")?; capability(imap_socket, Extension::None).context("check server capabilities")?; @@ -20,18 +22,19 @@ fn rfc3501_imap4rev1_base() { create_mailbox(imap_socket, Mailbox::Archive).context("created mailbox archive")?; // UNSUBSCRIBE IS NOT IMPLEMENTED YET //unsubscribe_mailbox(imap_socket).context("unsubscribe from archive")?; - select(imap_socket, Mailbox::Inbox, None).context("select inbox")?; + select(imap_socket, Mailbox::Inbox, SelectMod::None, None).context("select inbox")?; check(imap_socket).context("check must run")?; status_mailbox(imap_socket, Mailbox::Archive).context("status of archive from inbox")?; lmtp_handshake(lmtp_socket).context("handshake lmtp done")?; lmtp_deliver_email(lmtp_socket, Email::Multipart).context("mail delivered successfully")?; - noop_exists(imap_socket).context("noop loop must detect a new email")?; + noop_exists(imap_socket, 1).context("noop loop must detect a new email")?; fetch_rfc822(imap_socket, Selection::FirstId, Email::Multipart) .context("fetch rfc822 message, should be our first message")?; copy(imap_socket, Selection::FirstId, Mailbox::Archive) .context("copy message to the archive mailbox")?; append_email(imap_socket, Email::Basic).context("insert email in INBOX")?; - // SEARCH IS NOT IMPLEMENTED YET + noop_exists(imap_socket, 2).context("noop loop must detect a new email")?; + // SEARCH IS NOT TESTED YET //search(imap_socket).expect("search should return something"); add_flags_email(imap_socket, Selection::FirstId, Flag::Deleted) .context("should add delete flag to the email")?; @@ -45,7 +48,7 @@ fn rfc3501_imap4rev1_base() { } fn rfc3691_imapext_unselect() { - println!("rfc3691_imapext_unselect"); + println!("๐Ÿงช rfc3691_imapext_unselect"); common::aerogramme_provider_daemon_dev(|imap_socket, lmtp_socket| { connect(imap_socket).context("server says hello")?; @@ -54,17 +57,17 @@ fn rfc3691_imapext_unselect() { capability(imap_socket, Extension::Unselect).context("check server capabilities")?; login(imap_socket, Account::Alice).context("login test")?; - select(imap_socket, Mailbox::Inbox, None).context("select inbox")?; - noop_exists(imap_socket).context("noop loop must detect a new email")?; + select(imap_socket, Mailbox::Inbox, SelectMod::None, None).context("select inbox")?; + noop_exists(imap_socket, 1).context("noop loop must detect a new email")?; add_flags_email(imap_socket, Selection::FirstId, Flag::Deleted) .context("add delete flags to the email")?; unselect(imap_socket) .context("unselect inbox while preserving email with the \\Delete flag")?; - select(imap_socket, Mailbox::Inbox, Some(1)).context("select inbox again")?; + select(imap_socket, Mailbox::Inbox, SelectMod::None, Some(1)).context("select inbox again")?; fetch_rfc822(imap_socket, Selection::FirstId, Email::Basic) .context("message is still present")?; close(imap_socket).context("close inbox and expunge message")?; - select(imap_socket, Mailbox::Inbox, Some(0)) + select(imap_socket, Mailbox::Inbox, SelectMod::None, Some(0)) .context("select inbox again and check it's empty")?; Ok(()) @@ -73,7 +76,7 @@ fn rfc3691_imapext_unselect() { } fn rfc5161_imapext_enable() { - println!("rfc5161_imapext_enable"); + println!("๐Ÿงช rfc5161_imapext_enable"); common::aerogramme_provider_daemon_dev(|imap_socket, _lmtp_socket| { connect(imap_socket).context("server says hello")?; login(imap_socket, Account::Alice).context("login test")?; @@ -87,25 +90,25 @@ fn rfc5161_imapext_enable() { } fn rfc6851_imapext_move() { - println!("rfc6851_imapext_move"); + println!("๐Ÿงช rfc6851_imapext_move"); common::aerogramme_provider_daemon_dev(|imap_socket, lmtp_socket| { connect(imap_socket).context("server says hello")?; capability(imap_socket, Extension::Move).context("check server capabilities")?; login(imap_socket, Account::Alice).context("login test")?; create_mailbox(imap_socket, Mailbox::Archive).context("created mailbox archive")?; - select(imap_socket, Mailbox::Inbox, None).context("select inbox")?; + select(imap_socket, Mailbox::Inbox, SelectMod::None, None).context("select inbox")?; lmtp_handshake(lmtp_socket).context("handshake lmtp done")?; lmtp_deliver_email(lmtp_socket, Email::Basic).context("mail delivered successfully")?; - noop_exists(imap_socket).context("noop loop must detect a new email")?; + noop_exists(imap_socket, 1).context("noop loop must detect a new email")?; r#move(imap_socket, Selection::FirstId, Mailbox::Archive) .context("message from inbox moved to archive")?; unselect(imap_socket) .context("unselect inbox while preserving email with the \\Delete flag")?; - select(imap_socket, Mailbox::Archive, Some(1)).context("select archive")?; + select(imap_socket, Mailbox::Archive, SelectMod::None, Some(1)).context("select archive")?; fetch_rfc822(imap_socket, Selection::FirstId, Email::Basic).context("check mail exists")?; logout(imap_socket).context("must quit")?; @@ -115,7 +118,7 @@ fn rfc6851_imapext_move() { } fn rfc7888_imapext_literal() { - println!("rfc7888_imapext_literal"); + println!("๐Ÿงช rfc7888_imapext_literal"); common::aerogramme_provider_daemon_dev(|imap_socket, _lmtp_socket| { connect(imap_socket).context("server says hello")?; @@ -126,3 +129,21 @@ fn rfc7888_imapext_literal() { }) .expect("test fully run"); } + +fn rfc4551_imapext_condstore() { + println!("๐Ÿงช rfc4551_imapext_condstore"); + common::aerogramme_provider_daemon_dev(|imap_socket, lmtp_socket| { + lmtp_handshake(lmtp_socket).context("handshake lmtp done")?; + lmtp_deliver_email(lmtp_socket, Email::Basic).context("mail delivered successfully")?; + lmtp_deliver_email(lmtp_socket, Email::Multipart).context("mail delivered successfully")?; + + connect(imap_socket).context("server says hello")?; + capability(imap_socket, Extension::Condstore).context("check server capabilities")?; + login(imap_socket, Account::Alice).context("login test")?; + select(imap_socket, Mailbox::Inbox, SelectMod::Condstore, None).context("select inbox")?; + noop_exists(imap_socket, 2).context("noop loop must detect a new email")?; + + Ok(()) + }) + .expect("test fully run"); +} diff --git a/tests/common/fragments.rs b/tests/common/fragments.rs index 2e2fbd4..0fd1c45 100644 --- a/tests/common/fragments.rs +++ b/tests/common/fragments.rs @@ -34,7 +34,7 @@ pub enum Extension { None, Unselect, Move, - CondStore, + Condstore, LiteralPlus, } @@ -65,6 +65,11 @@ pub enum Selection { SecondId, } +pub enum SelectMod { + None, + Condstore, +} + pub fn capability(imap: &mut TcpStream, ext: Extension) -> Result<()> { imap.write(&b"5 capability\r\n"[..])?; @@ -72,7 +77,7 @@ pub fn capability(imap: &mut TcpStream, ext: Extension) -> Result<()> { Extension::None => None, Extension::Unselect => Some("UNSELECT"), Extension::Move => Some("MOVE"), - Extension::CondStore => Some("CONDSTORE"), + Extension::Condstore => Some("CONDSTORE"), Extension::LiteralPlus => Some("LITERAL+"), }; @@ -125,7 +130,7 @@ pub fn create_mailbox(imap: &mut TcpStream, mbx: Mailbox) -> Result<()> { Ok(()) } -pub fn select(imap: &mut TcpStream, mbx: Mailbox, maybe_exists: Option) -> Result<()> { +pub fn select(imap: &mut TcpStream, mbx: Mailbox, modifier: SelectMod, maybe_exists: Option) -> Result<()> { let mut buffer: [u8; 6000] = [0; 6000]; let mbx_str = match mbx { @@ -133,7 +138,13 @@ pub fn select(imap: &mut TcpStream, mbx: Mailbox, maybe_exists: Option) -> Mailbox::Archive => "Archive", Mailbox::Drafts => "Drafts", }; - imap.write(format!("20 select {}\r\n", mbx_str).as_bytes())?; + + let mod_str = match modifier { + SelectMod::Condstore => " (CONDSTORE)", + SelectMod::None => "", + }; + + imap.write(format!("20 select {}{}\r\n", mbx_str, mod_str).as_bytes())?; let read = read_lines(imap, &mut buffer, Some(&b"20 OK"[..]))?; let srv_msg = std::str::from_utf8(read)?; @@ -141,6 +152,12 @@ pub fn select(imap: &mut TcpStream, mbx: Mailbox, maybe_exists: Option) -> let expected = format!("* {} EXISTS", exists); assert!(srv_msg.contains(&expected)); } + match modifier { + SelectMod::Condstore => { + assert!(srv_msg.contains("[HIGHESTMODSEQ")); + } + _ => (), + } Ok(()) } @@ -206,7 +223,7 @@ pub fn lmtp_deliver_email(lmtp: &mut TcpStream, email_type: Email) -> Result<()> Ok(()) } -pub fn noop_exists(imap: &mut TcpStream) -> Result<()> { +pub fn noop_exists(imap: &mut TcpStream, must_exists: u32) -> Result<()> { let mut buffer: [u8; 6000] = [0; 6000]; let mut max_retry = 20; @@ -216,16 +233,23 @@ pub fn noop_exists(imap: &mut TcpStream) -> Result<()> { let read = read_lines(imap, &mut buffer, Some(&b"30 OK"[..]))?; let srv_msg = std::str::from_utf8(read)?; - match (max_retry, srv_msg.lines().count()) { - (_, cnt) if cnt > 1 => break, - (0, _) => bail!("no more retry"), - _ => (), + for line in srv_msg.lines() { + if line.contains("EXISTS") { + let got = read_first_u32(line)?; + if got == must_exists { + // Done + return Ok(()); + } + } + } + + if max_retry <= 0 { + // Failed + bail!("no more retry"); } thread::sleep(SMALL_DELAY); } - - Ok(()) } pub fn fetch_rfc822(imap: &mut TcpStream, selection: Selection, r#ref: Email) -> Result<()> { @@ -281,9 +305,6 @@ pub fn append_email(imap: &mut TcpStream, content: Email) -> Result<()> { let read = read_lines(imap, &mut buffer, None)?; assert_eq!(&read[..5], &b"47 OK"[..]); - // we check that noop detects the change - noop_exists(imap)?; - Ok(()) } @@ -292,7 +313,7 @@ pub fn add_flags_email(imap: &mut TcpStream, selection: Selection, flag: Flag) - assert!(matches!(selection, Selection::FirstId)); assert!(matches!(flag, Flag::Deleted)); imap.write(&b"50 store 1 +FLAGS (\\Deleted)\r\n"[..])?; - let _read = read_lines(imap, &mut buffer, Some(&b"50 OK STORE"[..]))?; + let _read = read_lines(imap, &mut buffer, Some(&b"50 OK"[..]))?; Ok(()) } @@ -302,7 +323,7 @@ pub fn add_flags_email(imap: &mut TcpStream, selection: Selection, flag: Flag) - pub fn search(imap: &mut TcpStream) -> Result<()> { imap.write(&b"55 search text \"OoOoO\"\r\n"[..])?; let mut buffer: [u8; 1500] = [0; 1500]; - let _read = read_lines(imap, &mut buffer, Some(&b"55 OK SEARCH"[..]))?; + let _read = read_lines(imap, &mut buffer, Some(&b"55 OK"[..]))?; Ok(()) } diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 810fd79..cbe0271 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -88,3 +88,12 @@ pub fn read_lines<'a, F: Read>( println!("read: {}", std::str::from_utf8(&buffer[..nbytes])?); Ok(&buffer[..nbytes]) } + +pub fn read_first_u32(inp: &str) -> Result { + Ok(inp + .chars() + .skip_while(|c| !c.is_digit(10)) + .take_while(|c| c.is_digit(10)) + .collect::() + .parse::()?) +} From 22cd0764d810e94e8ea9258b71d4dc25085585bd Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Fri, 12 Jan 2024 15:02:02 +0100 Subject: [PATCH 20/21] rewrite store testing logic --- tests/behavior.rs | 24 +++++++++----- tests/common/fragments.rs | 67 ++++++++++++++++++++++++++++++++------- 2 files changed, 71 insertions(+), 20 deletions(-) diff --git a/tests/behavior.rs b/tests/behavior.rs index 3d9604e..bcab4c4 100644 --- a/tests/behavior.rs +++ b/tests/behavior.rs @@ -34,9 +34,9 @@ fn rfc3501_imap4rev1_base() { .context("copy message to the archive mailbox")?; append_email(imap_socket, Email::Basic).context("insert email in INBOX")?; noop_exists(imap_socket, 2).context("noop loop must detect a new email")?; - // SEARCH IS NOT TESTED YET - //search(imap_socket).expect("search should return something"); - add_flags_email(imap_socket, Selection::FirstId, Flag::Deleted) + // Missing STORE command + search(imap_socket).expect("search should return something"); + store(imap_socket, Selection::FirstId, Flag::Deleted, StoreAction::AddFlags, StoreMod::None) .context("should add delete flag to the email")?; expunge(imap_socket).context("expunge emails")?; rename_mailbox(imap_socket, Mailbox::Archive, Mailbox::Drafts) @@ -59,7 +59,7 @@ fn rfc3691_imapext_unselect() { login(imap_socket, Account::Alice).context("login test")?; select(imap_socket, Mailbox::Inbox, SelectMod::None, None).context("select inbox")?; noop_exists(imap_socket, 1).context("noop loop must detect a new email")?; - add_flags_email(imap_socket, Selection::FirstId, Flag::Deleted) + store(imap_socket, Selection::FirstId, Flag::Deleted, StoreAction::AddFlags, StoreMod::None) .context("add delete flags to the email")?; unselect(imap_socket) .context("unselect inbox while preserving email with the \\Delete flag")?; @@ -133,16 +133,24 @@ fn rfc7888_imapext_literal() { fn rfc4551_imapext_condstore() { println!("๐Ÿงช rfc4551_imapext_condstore"); common::aerogramme_provider_daemon_dev(|imap_socket, lmtp_socket| { - lmtp_handshake(lmtp_socket).context("handshake lmtp done")?; - lmtp_deliver_email(lmtp_socket, Email::Basic).context("mail delivered successfully")?; - lmtp_deliver_email(lmtp_socket, Email::Multipart).context("mail delivered successfully")?; - + // Setup the test connect(imap_socket).context("server says hello")?; capability(imap_socket, Extension::Condstore).context("check server capabilities")?; login(imap_socket, Account::Alice).context("login test")?; + + // Check that the condstore modifier works select(imap_socket, Mailbox::Inbox, SelectMod::Condstore, None).context("select inbox")?; + + + lmtp_handshake(lmtp_socket).context("handshake lmtp done")?; + lmtp_deliver_email(lmtp_socket, Email::Basic).context("mail delivered successfully")?; + lmtp_deliver_email(lmtp_socket, Email::Multipart).context("mail delivered successfully")?; noop_exists(imap_socket, 2).context("noop loop must detect a new email")?; + + + //store( + Ok(()) }) .expect("test fully run"); diff --git a/tests/common/fragments.rs b/tests/common/fragments.rs index 0fd1c45..e07413e 100644 --- a/tests/common/fragments.rs +++ b/tests/common/fragments.rs @@ -63,6 +63,7 @@ pub enum Email { pub enum Selection { FirstId, SecondId, + All, } pub enum SelectMod { @@ -70,6 +71,20 @@ pub enum SelectMod { Condstore, } +pub enum StoreAction { + AddFlags, + DelFlags, + SetFlags, + AddFlagsSilent, + DelFlagsSilent, + SetFlagsSilent, +} + +pub enum StoreMod { + None, + UnchangedSince(u64), +} + pub fn capability(imap: &mut TcpStream, ext: Extension) -> Result<()> { imap.write(&b"5 capability\r\n"[..])?; @@ -308,18 +323,6 @@ pub fn append_email(imap: &mut TcpStream, content: Email) -> Result<()> { Ok(()) } -pub fn add_flags_email(imap: &mut TcpStream, selection: Selection, flag: Flag) -> Result<()> { - let mut buffer: [u8; 1500] = [0; 1500]; - assert!(matches!(selection, Selection::FirstId)); - assert!(matches!(flag, Flag::Deleted)); - imap.write(&b"50 store 1 +FLAGS (\\Deleted)\r\n"[..])?; - let _read = read_lines(imap, &mut buffer, Some(&b"50 OK"[..]))?; - - Ok(()) -} - -#[allow(dead_code)] -/// Not yet implemented pub fn search(imap: &mut TcpStream) -> Result<()> { imap.write(&b"55 search text \"OoOoO\"\r\n"[..])?; let mut buffer: [u8; 1500] = [0; 1500]; @@ -327,6 +330,46 @@ pub fn search(imap: &mut TcpStream) -> Result<()> { Ok(()) } +pub fn store( + imap: &mut TcpStream, + sel: Selection, + flag: Flag, + action: StoreAction, + modifier: StoreMod +) -> Result { + let mut buffer: [u8; 6000] = [0; 6000]; + + let seq = match sel { + Selection::FirstId => "1", + Selection::SecondId => "2", + Selection::All => "1:*", + }; + + let modif = match modifier { + StoreMod::None => "".into(), + StoreMod::UnchangedSince(val) => format!(" (UNCHANGEDSINCE {})", val), + }; + + let flags_str = match flag { + Flag::Deleted => "(\\Deleted)", + Flag::Important => "(\\Important)", + }; + + let action_str = match action { + StoreAction::AddFlags => "+FLAGS", + StoreAction::DelFlags => "-FLAGS", + StoreAction::SetFlags => "FLAGS", + StoreAction::AddFlagsSilent => "+FLAGS.SILENT", + StoreAction::DelFlagsSilent => "-FLAGS.SILENT", + StoreAction::SetFlagsSilent => "FLAGS.SILENT", + }; + + imap.write(format!("57 STORE {}{} {} {}\r\n", seq, modif, action_str, flags_str).as_bytes())?; + let read = read_lines(imap, &mut buffer, Some(&b"57 OK"[..]))?; + let srv_msg = std::str::from_utf8(read)?; + Ok(srv_msg.to_string()) +} + pub fn expunge(imap: &mut TcpStream) -> Result<()> { imap.write(&b"60 expunge\r\n"[..])?; let mut buffer: [u8; 1500] = [0; 1500]; From 81bfed3b7df354148f284ed4e3708c1a086d6e58 Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Mon, 15 Jan 2024 08:06:04 +0100 Subject: [PATCH 21/21] testing condstore --- tests/behavior.rs | 77 ++++++++++++++++++++++++------- tests/common/fragments.rs | 96 ++++++++++++++++++++++++++------------- 2 files changed, 125 insertions(+), 48 deletions(-) diff --git a/tests/behavior.rs b/tests/behavior.rs index bcab4c4..205f5e1 100644 --- a/tests/behavior.rs +++ b/tests/behavior.rs @@ -2,6 +2,7 @@ use anyhow::Context; mod common; use crate::common::fragments::*; +use crate::common::constants::*; fn main() { rfc3501_imap4rev1_base(); @@ -22,20 +23,25 @@ fn rfc3501_imap4rev1_base() { create_mailbox(imap_socket, Mailbox::Archive).context("created mailbox archive")?; // UNSUBSCRIBE IS NOT IMPLEMENTED YET //unsubscribe_mailbox(imap_socket).context("unsubscribe from archive")?; - select(imap_socket, Mailbox::Inbox, SelectMod::None, None).context("select inbox")?; + let select_res = select(imap_socket, Mailbox::Inbox, SelectMod::None).context("select inbox")?; + assert!(select_res.contains("* 0 EXISTS")); + check(imap_socket).context("check must run")?; - status_mailbox(imap_socket, Mailbox::Archive).context("status of archive from inbox")?; + status(imap_socket, Mailbox::Archive, StatusKind::UidNext).context("status of archive from inbox")?; lmtp_handshake(lmtp_socket).context("handshake lmtp done")?; lmtp_deliver_email(lmtp_socket, Email::Multipart).context("mail delivered successfully")?; noop_exists(imap_socket, 1).context("noop loop must detect a new email")?; - fetch_rfc822(imap_socket, Selection::FirstId, Email::Multipart) + + let srv_msg = fetch(imap_socket, Selection::FirstId, FetchKind::Rfc822, FetchMod::None) .context("fetch rfc822 message, should be our first message")?; + let orig_email = std::str::from_utf8(EMAIL1)?; + assert!(srv_msg.contains(orig_email)); + copy(imap_socket, Selection::FirstId, Mailbox::Archive) .context("copy message to the archive mailbox")?; append_email(imap_socket, Email::Basic).context("insert email in INBOX")?; noop_exists(imap_socket, 2).context("noop loop must detect a new email")?; - // Missing STORE command - search(imap_socket).expect("search should return something"); + search(imap_socket, SearchKind::Text("OoOoO")).expect("search should return something"); store(imap_socket, Selection::FirstId, Flag::Deleted, StoreAction::AddFlags, StoreMod::None) .context("should add delete flag to the email")?; expunge(imap_socket).context("expunge emails")?; @@ -57,18 +63,26 @@ fn rfc3691_imapext_unselect() { capability(imap_socket, Extension::Unselect).context("check server capabilities")?; login(imap_socket, Account::Alice).context("login test")?; - select(imap_socket, Mailbox::Inbox, SelectMod::None, None).context("select inbox")?; + let select_res = select(imap_socket, Mailbox::Inbox, SelectMod::None).context("select inbox")?; + assert!(select_res.contains("* 0 EXISTS")); + noop_exists(imap_socket, 1).context("noop loop must detect a new email")?; store(imap_socket, Selection::FirstId, Flag::Deleted, StoreAction::AddFlags, StoreMod::None) .context("add delete flags to the email")?; unselect(imap_socket) .context("unselect inbox while preserving email with the \\Delete flag")?; - select(imap_socket, Mailbox::Inbox, SelectMod::None, Some(1)).context("select inbox again")?; - fetch_rfc822(imap_socket, Selection::FirstId, Email::Basic) + let select_res = select(imap_socket, Mailbox::Inbox, SelectMod::None).context("select inbox again")?; + assert!(select_res.contains("* 1 EXISTS")); + + let srv_msg = fetch(imap_socket, Selection::FirstId, FetchKind::Rfc822, FetchMod::None) .context("message is still present")?; + let orig_email = std::str::from_utf8(EMAIL2)?; + assert!(srv_msg.contains(orig_email)); + close(imap_socket).context("close inbox and expunge message")?; - select(imap_socket, Mailbox::Inbox, SelectMod::None, Some(0)) + let select_res = select(imap_socket, Mailbox::Inbox, SelectMod::None) .context("select inbox again and check it's empty")?; + assert!(select_res.contains("* 0 EXISTS")); Ok(()) }) @@ -97,7 +111,8 @@ fn rfc6851_imapext_move() { capability(imap_socket, Extension::Move).context("check server capabilities")?; login(imap_socket, Account::Alice).context("login test")?; create_mailbox(imap_socket, Mailbox::Archive).context("created mailbox archive")?; - select(imap_socket, Mailbox::Inbox, SelectMod::None, None).context("select inbox")?; + let select_res = select(imap_socket, Mailbox::Inbox, SelectMod::None).context("select inbox")?; + assert!(select_res.contains("* 0 EXISTS")); lmtp_handshake(lmtp_socket).context("handshake lmtp done")?; lmtp_deliver_email(lmtp_socket, Email::Basic).context("mail delivered successfully")?; @@ -108,8 +123,18 @@ fn rfc6851_imapext_move() { unselect(imap_socket) .context("unselect inbox while preserving email with the \\Delete flag")?; - select(imap_socket, Mailbox::Archive, SelectMod::None, Some(1)).context("select archive")?; - fetch_rfc822(imap_socket, Selection::FirstId, Email::Basic).context("check mail exists")?; + let select_res = select(imap_socket, Mailbox::Archive, SelectMod::None).context("select archive")?; + assert!(select_res.contains("* 1 EXISTS")); + + let srv_msg = fetch( + imap_socket, + Selection::FirstId, + FetchKind::Rfc822, + FetchMod::None, + ).context("check mail exists")?; + let orig_email = std::str::from_utf8(EMAIL2)?; + assert!(srv_msg.contains(orig_email)); + logout(imap_socket).context("must quit")?; Ok(()) @@ -135,21 +160,41 @@ fn rfc4551_imapext_condstore() { common::aerogramme_provider_daemon_dev(|imap_socket, lmtp_socket| { // Setup the test connect(imap_socket).context("server says hello")?; + + // RFC 3.1.1 Advertising Support for CONDSTORE capability(imap_socket, Extension::Condstore).context("check server capabilities")?; login(imap_socket, Account::Alice).context("login test")?; - // Check that the condstore modifier works - select(imap_socket, Mailbox::Inbox, SelectMod::Condstore, None).context("select inbox")?; - + // RFC 3.1.8. CONDSTORE Parameter to SELECT and EXAMINE + let select_res = select(imap_socket, Mailbox::Inbox, SelectMod::Condstore).context("select inbox")?; + // RFC 3.1.2 New OK Untagged Responses for SELECT and EXAMINE + assert!(select_res.contains("[HIGHESTMODSEQ 1]")); + // RFC 3.1.3. STORE and UID STORE Commands lmtp_handshake(lmtp_socket).context("handshake lmtp done")?; lmtp_deliver_email(lmtp_socket, Email::Basic).context("mail delivered successfully")?; lmtp_deliver_email(lmtp_socket, Email::Multipart).context("mail delivered successfully")?; noop_exists(imap_socket, 2).context("noop loop must detect a new email")?; + let store_res = store(imap_socket, Selection::All, Flag::Important, StoreAction::AddFlags, StoreMod::UnchangedSince(1))?; + assert!(store_res.contains("[MODIFIED 2]")); + assert!(store_res.contains("* 1 FETCH (FLAGS (\\Important) MODSEQ (3))")); + assert!(!store_res.contains("* 2 FETCH")); + assert_eq!(store_res.lines().count(), 2); + // RFC 3.1.4. FETCH and UID FETCH Commands + let fetch_res = fetch(imap_socket, Selection::All, FetchKind::Rfc822Size, FetchMod::ChangedSince(2))?; + assert!(fetch_res.contains("* 1 FETCH (RFC822.SIZE 84 MODSEQ (3))")); + assert!(!fetch_res.contains("* 2 FETCH")); + assert_eq!(store_res.lines().count(), 2); + // RFC 3.1.5. MODSEQ Search Criterion in SEARCH + let search_res = search(imap_socket, SearchKind::ModSeq(3))?; + // RFC 3.1.6. Modified SEARCH Untagged Response + assert!(search_res.contains("* SEARCH 1 (MODSEQ 3)")); - //store( + // RFC 3.1.7 HIGHESTMODSEQ Status Data Items + let status_res = status(imap_socket, Mailbox::Inbox, StatusKind::HighestModSeq)?; + assert!(status_res.contains("HIGHESTMODSEQ 3")); Ok(()) }) diff --git a/tests/common/fragments.rs b/tests/common/fragments.rs index e07413e..f9ad87e 100644 --- a/tests/common/fragments.rs +++ b/tests/common/fragments.rs @@ -85,6 +85,26 @@ pub enum StoreMod { UnchangedSince(u64), } +pub enum FetchKind { + Rfc822, + Rfc822Size, +} + +pub enum FetchMod { + None, + ChangedSince(u64), +} + +pub enum SearchKind<'a> { + Text(&'a str), + ModSeq(u64), +} + +pub enum StatusKind { + UidNext, + HighestModSeq, +} + pub fn capability(imap: &mut TcpStream, ext: Extension) -> Result<()> { imap.write(&b"5 capability\r\n"[..])?; @@ -145,7 +165,7 @@ pub fn create_mailbox(imap: &mut TcpStream, mbx: Mailbox) -> Result<()> { Ok(()) } -pub fn select(imap: &mut TcpStream, mbx: Mailbox, modifier: SelectMod, maybe_exists: Option) -> Result<()> { +pub fn select(imap: &mut TcpStream, mbx: Mailbox, modifier: SelectMod) -> Result { let mut buffer: [u8; 6000] = [0; 6000]; let mbx_str = match mbx { @@ -163,18 +183,8 @@ pub fn select(imap: &mut TcpStream, mbx: Mailbox, modifier: SelectMod, maybe_exi let read = read_lines(imap, &mut buffer, Some(&b"20 OK"[..]))?; let srv_msg = std::str::from_utf8(read)?; - if let Some(exists) = maybe_exists { - let expected = format!("* {} EXISTS", exists); - assert!(srv_msg.contains(&expected)); - } - match modifier { - SelectMod::Condstore => { - assert!(srv_msg.contains("[HIGHESTMODSEQ")); - } - _ => (), - } - Ok(()) + Ok(srv_msg.to_string()) } pub fn unselect(imap: &mut TcpStream) -> Result<()> { @@ -194,13 +204,22 @@ pub fn check(imap: &mut TcpStream) -> Result<()> { Ok(()) } -pub fn status_mailbox(imap: &mut TcpStream, mbx: Mailbox) -> Result<()> { - assert!(matches!(mbx, Mailbox::Archive)); - imap.write(&b"25 STATUS Archive (UIDNEXT MESSAGES)\r\n"[..])?; +pub fn status(imap: &mut TcpStream, mbx: Mailbox, sk: StatusKind) -> Result { + let mbx_str = match mbx { + Mailbox::Inbox => "INBOX", + Mailbox::Archive => "Archive", + Mailbox::Drafts => "Drafts", + }; + let sk_str = match sk { + StatusKind::UidNext => "(UIDNEXT)", + StatusKind::HighestModSeq => "(HIGHESTMODSEQ)", + }; + imap.write(format!("25 STATUS {} {}\r\n", mbx_str, sk_str).as_bytes())?; let mut buffer: [u8; 6000] = [0; 6000]; - let _read = read_lines(imap, &mut buffer, Some(&b"25 OK"[..]))?; + let read = read_lines(imap, &mut buffer, Some(&b"25 OK"[..]))?; + let srv_msg = std::str::from_utf8(read)?; - Ok(()) + Ok(srv_msg.to_string()) } pub fn lmtp_handshake(lmtp: &mut TcpStream) -> Result<()> { @@ -267,23 +286,31 @@ pub fn noop_exists(imap: &mut TcpStream, must_exists: u32) -> Result<()> { } } -pub fn fetch_rfc822(imap: &mut TcpStream, selection: Selection, r#ref: Email) -> Result<()> { +pub fn fetch(imap: &mut TcpStream, selection: Selection, kind: FetchKind, modifier: FetchMod) -> Result { let mut buffer: [u8; 65535] = [0; 65535]; - assert!(matches!(selection, Selection::FirstId)); - imap.write(&b"40 fetch 1 rfc822\r\n"[..])?; + let sel_str = match selection { + Selection::FirstId => "1", + Selection::SecondId => "2", + Selection::All => "1:*", + }; + + let kind_str = match kind { + FetchKind::Rfc822 => "RFC822", + FetchKind::Rfc822Size => "RFC822.SIZE", + }; + + let mod_str = match modifier { + FetchMod::None => "".into(), + FetchMod::ChangedSince(val) => format!(" (CHANGEDSINCE {})", val), + }; + + imap.write(format!("40 fetch {} {}{}\r\n", sel_str, kind_str, mod_str).as_bytes())?; let read = read_lines(imap, &mut buffer, Some(&b"40 OK FETCH"[..]))?; let srv_msg = std::str::from_utf8(read)?; - let ref_mail = match r#ref { - Email::Basic => EMAIL2, - Email::Multipart => EMAIL1, - }; - let orig_email = std::str::from_utf8(ref_mail)?; - assert!(srv_msg.contains(orig_email)); - - Ok(()) + Ok(srv_msg.to_string()) } pub fn copy(imap: &mut TcpStream, selection: Selection, to: Mailbox) -> Result<()> { @@ -323,11 +350,16 @@ pub fn append_email(imap: &mut TcpStream, content: Email) -> Result<()> { Ok(()) } -pub fn search(imap: &mut TcpStream) -> Result<()> { - imap.write(&b"55 search text \"OoOoO\"\r\n"[..])?; +pub fn search(imap: &mut TcpStream, sk: SearchKind) -> Result { + let sk_str = match sk { + SearchKind::Text(x) => format!("TEXT \"{}\"", x), + SearchKind::ModSeq(x) => format!("MODSEQ {}", x), + }; + imap.write(format!("55 SEARCH {}\r\n", sk_str).as_bytes())?; let mut buffer: [u8; 1500] = [0; 1500]; - let _read = read_lines(imap, &mut buffer, Some(&b"55 OK"[..]))?; - Ok(()) + let read = read_lines(imap, &mut buffer, Some(&b"55 OK"[..]))?; + let srv_msg = std::str::from_utf8(read)?; + Ok(srv_msg.to_string()) } pub fn store(