aboutsummaryrefslogtreecommitdiff
path: root/src/imap
diff options
context:
space:
mode:
authorQuentin <quentin@dufour.io>2024-01-15 07:07:06 +0000
committerQuentin <quentin@dufour.io>2024-01-15 07:07:06 +0000
commit55e26d24a08519ded6a6898453dcd6db287f45c8 (patch)
tree074d9f8dbc161d2aa8f59be26826b40c09d3d658 /src/imap
parentd49a2355f71fe555a67a815c31800f901a0d0a71 (diff)
parent81bfed3b7df354148f284ed4e3708c1a086d6e58 (diff)
downloadaerogramme-55e26d24a08519ded6a6898453dcd6db287f45c8.tar.gz
aerogramme-55e26d24a08519ded6a6898453dcd6db287f45c8.zip
Merge pull request 'CONDSTORE' (#71) from feat/condstore-try-2 into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/aerogramme/pulls/71
Diffstat (limited to 'src/imap')
-rw-r--r--src/imap/attributes.rs17
-rw-r--r--src/imap/capability.rs57
-rw-r--r--src/imap/command/authenticated.rs32
-rw-r--r--src/imap/command/examined.rs52
-rw-r--r--src/imap/command/selected.rs89
-rw-r--r--src/imap/index.rs56
-rw-r--r--src/imap/mail_view.rs5
-rw-r--r--src/imap/mailbox_view.rs208
-rw-r--r--src/imap/mod.rs5
-rw-r--r--src/imap/search.rs29
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
+}