diff options
Diffstat (limited to 'src/imap')
-rw-r--r-- | src/imap/attributes.rs | 17 | ||||
-rw-r--r-- | src/imap/capability.rs | 57 | ||||
-rw-r--r-- | src/imap/command/authenticated.rs | 32 | ||||
-rw-r--r-- | src/imap/command/examined.rs | 52 | ||||
-rw-r--r-- | src/imap/command/selected.rs | 89 | ||||
-rw-r--r-- | src/imap/index.rs | 56 | ||||
-rw-r--r-- | src/imap/mail_view.rs | 5 | ||||
-rw-r--r-- | src/imap/mailbox_view.rs | 208 | ||||
-rw-r--r-- | src/imap/mod.rs | 5 | ||||
-rw-r--r-- | src/imap/search.rs | 29 |
10 files changed, 417 insertions, 133 deletions
diff --git a/src/imap/attributes.rs b/src/imap/attributes.rs index cf7cb52..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<MessageDataItemName<'static>>, } 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,9 +32,23 @@ 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 } } + 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/capability.rs b/src/imap/capability.rs index feadb6b..6533ccb 100644 --- a/src/imap/capability.rs +++ b/src/imap/capability.rs @@ -1,8 +1,11 @@ +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() } @@ -11,9 +14,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<Capability<'static>>); @@ -26,7 +31,7 @@ impl Default for ServerCapability { Capability::Move, Capability::LiteralPlus, capability_unselect(), - //capability_condstore(), + capability_condstore(), //capability_qresync(), ])) } @@ -48,15 +53,29 @@ impl ServerCapability { } } -enum ClientStatus { +#[derive(Clone)] +pub enum ClientStatus { NotSupportedByServer, Disabled, Enabled, } +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(), + } + } +} + pub struct ClientCapability { - condstore: ClientStatus, - utf8kind: Option<Utf8Kind>, + pub condstore: ClientStatus, + pub utf8kind: Option<Utf8Kind>, } impl ClientCapability { @@ -70,6 +89,36 @@ impl ClientCapability { } } + pub fn enable_condstore(&mut self) { + 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 { + SelectExamineModifier::Condstore => self.enable_condstore(), + } + } + } + pub fn try_enable( &mut self, caps: &[CapabilityEnable<'static>], diff --git a/src/imap/command/authenticated.rs b/src/imap/command/authenticated.rs index 1481a80..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 } => ctx.select(mailbox).await, - CommandBody::Examine { mailbox } => ctx.examine(mailbox).await, + CommandBody::Select { mailbox, modifiers } => ctx.select(mailbox, modifiers).await, + CommandBody::Examine { mailbox, modifiers } => ctx.examine(mailbox, modifiers).await, CommandBody::Append { mailbox, flags, @@ -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() { @@ -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()) + }, }); } @@ -404,6 +405,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) @@ -420,7 +422,10 @@ impl<'a> AuthenticatedContext<'a> { async fn select( self, mailbox: &MailboxCodec<'a>, + modifiers: &[SelectExamineModifier], ) -> Result<(Response<'static>, flow::Transition)> { + self.client_capabilities.select_enable(modifiers); + let name: &str = MailboxName(mailbox).try_into()?; let mb_opt = self.user.open_mailbox(&name).await?; @@ -438,7 +443,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(( @@ -455,7 +460,10 @@ impl<'a> AuthenticatedContext<'a> { async fn examine( self, mailbox: &MailboxCodec<'a>, + modifiers: &[SelectExamineModifier], ) -> Result<(Response<'static>, flow::Transition)> { + self.client_capabilities.select_enable(modifiers); + let name: &str = MailboxName(mailbox).try_into()?; let mb_opt = self.user.open_mailbox(&name).await?; @@ -473,7 +481,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(( @@ -496,7 +504,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") @@ -537,7 +545,7 @@ impl<'a> AuthenticatedContext<'a> { flags: &[Flag<'a>], date: &Option<DateTime>, message: &Literal<'a>, - ) -> Result<(Arc<Mailbox>, ImapUidvalidity, ImapUidvalidity)> { + ) -> Result<(Arc<Mailbox>, ImapUidvalidity, ImapUid, ModSeq)> { let name: &str = MailboxName(mailbox).try_into()?; let mb_opt = self.user.open_mailbox(&name).await?; @@ -555,9 +563,9 @@ impl<'a> AuthenticatedContext<'a> { let flags = flags.iter().map(|x| x.to_string()).collect::<Vec<_>>(); // 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/command/examined.rs b/src/imap/command/examined.rs index 3dd11e2..9fc0990 100644 --- a/src/imap/command/examined.rs +++ b/src/imap/command/examined.rs @@ -1,16 +1,18 @@ use std::sync::Arc; +use std::num::NonZeroU64; 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; 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; -use crate::imap::mailbox_view::MailboxView; +use crate::imap::mailbox_view::{MailboxView, UpdateParameters}; use crate::imap::response::Response; use crate::mail::user::User; @@ -37,8 +39,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,17 +91,33 @@ 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 { - Ok(resp) => Ok(( - Response::build() - .to_req(self.req) - .message("FETCH completed") - .set_body(resp) - .ok()?, - flow::Transition::None, - )), + let ap = AttributesProxy::new(attributes, modifiers, *uid); + let mut changed_since: Option<NonZeroU64> = None; + modifiers.iter().for_each(|m| match m { + FetchModifier::ChangedSince(val) => { + changed_since = Some(*val); + }, + }); + + 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) + self.client_capabilities.attributes_enable(&ap); + self.client_capabilities.fetch_modifiers_enable(modifiers); + + 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) @@ -115,7 +134,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) @@ -127,9 +149,9 @@ 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?; + 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 35c3eb4..c13b71a 100644 --- a/src/imap/command/selected.rs +++ b/src/imap/command/selected.rs @@ -1,7 +1,8 @@ use std::sync::Arc; +use std::num::NonZeroU64; use anyhow::Result; -use imap_codec::imap_types::command::{Command, CommandBody}; +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}; @@ -13,9 +14,9 @@ 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; pub struct SelectedContext<'a> { @@ -43,8 +44,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, @@ -56,8 +58,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, @@ -113,17 +116,34 @@ 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 { - Ok(resp) => Ok(( - Response::build() - .to_req(self.req) - .message("FETCH completed") - .set_body(resp) - .ok()?, - flow::Transition::None, - )), + let ap = AttributesProxy::new(attributes, modifiers, *uid); + let mut changed_since: Option<NonZeroU64> = None; + modifiers.iter().for_each(|m| match m { + FetchModifier::ChangedSince(val) => { + changed_since = Some(*val); + }, + }); + + 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) + self.client_capabilities.attributes_enable(&ap); + self.client_capabilities.fetch_modifiers_enable(modifiers); + + // Response to the client + 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) @@ -140,7 +160,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) @@ -152,9 +175,9 @@ 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?; + let updates = self.mailbox.update(UpdateParameters::default()).await?; Ok(( Response::build() .to_req(self.req) @@ -185,19 +208,39 @@ impl<'a> SelectedContext<'a> { kind: &StoreType, response: &StoreResponse, flags: &[Flag<'a>], + modifiers: &[StoreModifier], uid: &bool, ) -> Result<(Response<'static>, flow::Transition)> { - let data = self + let mut unchanged_since: Option<NonZeroU64> = 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?; - Ok(( - Response::build() + let mut ok_resp = Response::build() .to_req(self.req) .message("STORE completed") - .set_body(data) - .ok()?, + .set_body(data); + + + match modified[..] { + [] => (), + [_head, ..] => { + let modified_str = format!("MODIFIED {}", modified.into_iter().map(|x| x.to_string()).collect::<Vec<_>>().join(",")); + ok_resp = ok_resp.code(Code::Other(CodeOther::unvalidated(modified_str.into_bytes()))); + }, + }; + + + self.client_capabilities.store_modifiers_enable(modifiers); + + Ok((ok_resp.ok()?, flow::Transition::None, )) } diff --git a/src/imap/index.rs b/src/imap/index.rs index 4853374..9b794b8 100644 --- a/src/imap/index.rs +++ b/src/imap/index.rs @@ -1,9 +1,9 @@ -use std::num::NonZeroU32; +use std::num::{NonZeroU32, NonZeroU64}; 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::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"))? - .1 - .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, }) }) @@ -61,10 +60,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::<Vec<_>>(); + let largest = self.last().expect("The mailbox is not empty").uid; + let mut unroll_seq = sequence_set.iter(largest).collect::<Vec<_>>(); unroll_seq.sort(); let start_seq = match unroll_seq.iter().next() { @@ -103,11 +100,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) @@ -131,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<NonZeroU64>, + by_uid: bool, + ) -> Result<Vec<&'a MailIndex<'a>>> { + 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<NonZeroU64>, + 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)] @@ -138,6 +163,7 @@ pub struct MailIndex<'a> { pub i: NonZeroU32, pub uid: ImapUid, pub uuid: UniqueIdent, + pub modseq: ModSeq, pub flags: &'a Vec<String>, } 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::<Result<Vec<_>, _>>()?; @@ -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 { diff --git a/src/imap/mailbox_view.rs b/src/imap/mailbox_view.rs index 513567f..07fa3ad 100644 --- a/src/imap/mailbox_view.rs +++ b/src/imap/mailbox_view.rs @@ -1,21 +1,23 @@ -use std::num::NonZeroU32; +use std::num::{NonZeroU32, NonZeroU64}; use std::sync::Arc; +use std::collections::HashSet; 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, 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; +use crate::mail::unique_ident::UniqueIdent; 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; @@ -32,6 +34,21 @@ const DEFAULT_FLAGS: [Flag; 5] = [ Flag::Draft, ]; +pub struct UpdateParameters { + pub silence: HashSet<UniqueIdent>, + 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 @@ -39,12 +56,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<Mailbox>) -> Self { - Self(mailbox.frozen().await) + pub async fn new(mailbox: Arc<Mailbox>, is_cond: bool) -> Self { + Self { + internal: mailbox.frozen().await, + is_condstore: is_cond, + } } /// Create an updated view, useful to make a diff @@ -53,9 +76,9 @@ 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<Vec<Body<'static>>> { - let old_snapshot = self.0.update().await; - let new_snapshot = &self.0.snapshot; + pub async fn update(&mut self, params: UpdateParameters) -> Result<Vec<Body<'static>>> { + let old_snapshot = self.internal.update().await; + let new_snapshot = &self.internal.snapshot; let mut data = Vec::<Body>::new(); @@ -99,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, 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()?, })); } } @@ -130,8 +165,11 @@ 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)); + if self.is_condstore { + data.push(self.highestmodseq_status()?); + } + /*self.unseen_first_status()? + .map(|unseen_status| data.push(unseen_status));*/ Ok(data) } @@ -140,50 +178,68 @@ impl MailboxView { &mut self, sequence_set: &SequenceSet, kind: &StoreType, - _response: &StoreResponse, + response: &StoreResponse, flags: &[Flag<'a>], + unchanged_since: Option<NonZeroU64>, is_uid_store: &bool, - ) -> Result<Vec<Body<'static>>> { - self.0.sync().await?; + ) -> Result<(Vec<Body<'static>>, Vec<NonZeroU32>)> { + self.internal.sync().await?; let flags = flags.iter().map(|x| x.to_string()).collect::<Vec<_>>(); 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.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?; } } } - // @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<Vec<Body<'static>>> { - 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 .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 { - self.0.mailbox.delete(msg).await?; + self.internal.mailbox.delete(msg).await?; } - self.update().await + self.update(UpdateParameters::default()).await } pub async fn copy( @@ -197,7 +253,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 +280,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![]; @@ -238,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)) } @@ -248,27 +307,32 @@ impl MailboxView { pub async fn fetch<'b>( &self, sequence_set: &SequenceSet, - attributes: &'b MacroOrMessageDataItemNames<'static>, + ap: &AttributesProxy, + changed_since: Option<NonZeroU64>, is_uid_fetch: &bool, ) -> Result<Vec<Body<'static>>> { // [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, }; 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 .iter() .map(|midx| midx.uuid) .collect::<Vec<_>>(); - 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 +358,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?; @@ -316,7 +380,7 @@ impl MailboxView { _charset: &Option<Charset<'a>>, search_key: &SearchKey<'a>, uid: bool, - ) -> Result<Vec<Body<'static>>> { + ) -> Result<(Vec<Body<'static>>, bool)> { // 1. Compute the subset of sequence identifiers we need to fetch // based on the search query let crit = search::Criteria(search_key); @@ -332,20 +396,30 @@ 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::<Vec<_>>(); - 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)?; // 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(), }; - Ok(vec![Body::Data(Data::Search(selection_fmt))]) + // 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, maybe_modseq))], is_modseq)) } // ---- @@ -354,7 +428,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<'a>> { - Index::new(&self.0.snapshot) + Index::new(&self.internal.snapshot) } /// Produce an OK [UIDVALIDITY _] message corresponding to `known_state` @@ -369,7 +443,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 +458,19 @@ impl MailboxView { } pub(crate) fn uidnext(&self) -> ImapUid { - self.0.snapshot.uidnext + self.internal.snapshot.uidnext + } + + pub(crate) fn highestmodseq_status(&self) -> Result<Body<'static>> { + Ok(Body::Status(Status::ok( + None, + 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 @@ -394,7 +480,7 @@ impl MailboxView { } pub(crate) fn exists(&self) -> Result<u32> { - 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 @@ -403,6 +489,7 @@ impl MailboxView { Ok(Body::Data(Data::Recent(self.recent()?))) } + #[allow(dead_code)] fn unseen_first_status(&self) -> Result<Option<Body<'static>>> { Ok(self .unseen_first()? @@ -412,21 +499,22 @@ impl MailboxView { .transpose()?) } + #[allow(dead_code)] fn unseen_first(&self) -> Result<Option<NonZeroU32>> { Ok(self - .0 + .internal .snapshot .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()?) } pub(crate) fn recent(&self) -> Result<u32> { let recent = self - .0 + .internal .snapshot .idx_by_flag .get(&"\\Recent".to_string()) @@ -443,7 +531,7 @@ impl MailboxView { // 1. Collecting all the possible flags in the mailbox // 1.a Fetch them from our index let mut known_flags: Vec<Flag> = self - .0 + .internal .snapshot .idx_by_flag .flags() @@ -483,9 +571,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()) @@ -524,6 +612,7 @@ mod tests { peek: false, }, ]), + &[], false, ); @@ -535,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/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 diff --git a/src/imap/search.rs b/src/imap/search.rs index c4888d0..61cbad5 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; @@ -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 @@ -176,6 +187,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 +222,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 +331,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 +472,10 @@ fn is_keep_seq(sk: &SearchKey, midx: &MailIndex) -> bool { _ => unreachable!(), } } + +fn is_keep_modseq(filter: &Option<MetadataItemSearch>, 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 +} |