From 35591ff0608096b32d7bab22d719a6ceb8574c2c Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Fri, 5 Jan 2024 12:40:49 +0100 Subject: [PATCH] search first ultra minimal implementation --- src/imap/command/selected.rs | 12 ++-- src/imap/mailbox_view.rs | 26 +++++++++ src/imap/mod.rs | 1 + src/imap/search.rs | 107 +++++++++++++++++++++++++++++++++++ 4 files changed, 141 insertions(+), 5 deletions(-) create mode 100644 src/imap/search.rs diff --git a/src/imap/command/selected.rs b/src/imap/command/selected.rs index c8cc680..933f397 100644 --- a/src/imap/command/selected.rs +++ b/src/imap/command/selected.rs @@ -136,15 +136,17 @@ impl<'a> SelectedContext<'a> { pub async fn search( self, - _charset: &Option>, - _criteria: &SearchKey<'a>, - _uid: &bool, + charset: &Option>, + criteria: &SearchKey<'a>, + uid: &bool, ) -> Result<(Response<'static>, flow::Transition)> { + let found = self.mailbox.search(charset, criteria, *uid).await?; Ok(( Response::build() .to_req(self.req) - .message("Not implemented") - .bad()?, + .set_body(found) + .message("SEARCH completed") + .ok()?, flow::Transition::None, )) } diff --git a/src/imap/mailbox_view.rs b/src/imap/mailbox_view.rs index f5cf394..5311635 100644 --- a/src/imap/mailbox_view.rs +++ b/src/imap/mailbox_view.rs @@ -5,15 +5,18 @@ use anyhow::{anyhow, bail, 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::flag::{Flag, FlagFetch, FlagPerm, StoreResponse, StoreType}; use imap_codec::imap_types::response::{Code, Data, Status}; +use imap_codec::imap_types::search::SearchKey; use imap_codec::imap_types::sequence::{self, SequenceSet}; use crate::imap::attributes::AttributesProxy; use crate::imap::flags; use crate::imap::mail_view::SeenFlag; use crate::imap::response::Body; +use crate::imap::search; use crate::imap::selectors::MailSelectionBuilder; use crate::mail::mailbox::Mailbox; use crate::mail::uidindex::{ImapUid, ImapUidvalidity, UidIndex}; @@ -308,6 +311,7 @@ impl MailboxView { .iter() .filter_map(|mv| mv.filter(&ap).ok().map(|(body, seen)| (mv, body, seen))) .collect::>(); + // Register seen flags let future_flags = filtered_view .iter() @@ -333,6 +337,22 @@ impl MailboxView { Ok(command_body) } + /// A very naive search implementation... + pub async fn search<'a>( + &self, + _charset: &Option>, + search_key: &SearchKey<'a>, + uid: bool, + ) -> Result>> { + let (seq_set, seq_type) = search::Criteria(search_key).to_sequence_set(); + let mailids = MailIdentifiersList(self.get_mail_ids(&seq_set, seq_type.is_uid())?); + let mail_u32 = match uid { + true => mailids.uids(), + _ => mailids.ids(), + }; + Ok(vec![Body::Data(Data::Search(mail_u32))]) + } + // ---- // Gets the IMAP ID, the IMAP UIDs and, the Aerogramme UUIDs of mails identified by a SequenceSet of @@ -525,6 +545,12 @@ pub struct MailIdentifiers { pub struct MailIdentifiersList(Vec); impl MailIdentifiersList { + fn ids(&self) -> Vec { + self.0.iter().map(|mi| mi.i).collect() + } + fn uids(&self) -> Vec { + self.0.iter().map(|mi| mi.uid).collect() + } fn uuids(&self) -> Vec { self.0.iter().map(|mi| mi.uuid).collect() } diff --git a/src/imap/mod.rs b/src/imap/mod.rs index 4f33bfe..ea34629 100644 --- a/src/imap/mod.rs +++ b/src/imap/mod.rs @@ -8,6 +8,7 @@ mod mail_view; mod mailbox_view; mod mime_view; mod response; +mod search; mod selectors; mod session; diff --git a/src/imap/search.rs b/src/imap/search.rs new file mode 100644 index 0000000..bf1d30e --- /dev/null +++ b/src/imap/search.rs @@ -0,0 +1,107 @@ +use imap_codec::imap_types::core::NonEmptyVec; +use imap_codec::imap_types::search::SearchKey; +use imap_codec::imap_types::sequence::{SeqOrUid, Sequence, SequenceSet}; +use std::num::NonZeroU32; + +pub enum SeqType { + Undefined, + NonUid, + Uid, +} +impl SeqType { + pub fn is_uid(&self) -> bool { + matches!(self, Self::Uid) + } +} + +pub struct Criteria<'a>(pub &'a SearchKey<'a>); +impl<'a> Criteria<'a> { + /// Returns a set of email identifiers that is greater or equal + /// to the set of emails to return + pub fn to_sequence_set(&self) -> (SequenceSet, SeqType) { + match self.0 { + SearchKey::All => (sequence_set_all(), SeqType::Undefined), + SearchKey::SequenceSet(seq_set) => (seq_set.clone(), SeqType::NonUid), + SearchKey::Uid(seq_set) => (seq_set.clone(), SeqType::Uid), + SearchKey::Not(_inner) => { + tracing::debug!( + "using NOT in a search request is slow: it selects all identifiers" + ); + (sequence_set_all(), SeqType::Undefined) + } + SearchKey::Or(left, right) => { + tracing::debug!("using OR in a search request is slow: no deduplication is done"); + let (base, base_seqtype) = Self(&left).to_sequence_set(); + let (ext, ext_seqtype) = Self(&right).to_sequence_set(); + + // Check if we have a UID/ID conflict in fetching: now we don't know how to handle them + match (base_seqtype, ext_seqtype) { + (SeqType::Uid, SeqType::NonUid) | (SeqType::NonUid, SeqType::Uid) => { + (sequence_set_all(), SeqType::Undefined) + } + (SeqType::Undefined, x) | (x, _) => { + let mut new_vec = base.0.into_inner(); + new_vec.extend_from_slice(ext.0.as_ref()); + let seq = SequenceSet( + NonEmptyVec::try_from(new_vec) + .expect("merging non empty vec lead to non empty vec"), + ); + (seq, x) + } + } + } + SearchKey::And(search_list) => { + tracing::debug!( + "using AND in a search request is slow: no intersection is performed" + ); + search_list + .as_ref() + .iter() + .map(|crit| Self(&crit).to_sequence_set()) + .min_by(|(x, _), (y, _)| { + let x_size = approx_sequence_set_size(x); + let y_size = approx_sequence_set_size(y); + x_size.cmp(&y_size) + }) + .unwrap_or((sequence_set_all(), SeqType::Undefined)) + } + _ => (sequence_set_all(), SeqType::Undefined), + } + } + + fn need_meta(&self) { + unimplemented!(); + } + + fn need_body(&self) { + unimplemented!(); + } +} + +fn sequence_set_all() -> SequenceSet { + SequenceSet::from(Sequence::Range( + SeqOrUid::Value(NonZeroU32::MIN), + SeqOrUid::Asterisk, + )) +} + +// This is wrong as sequences can overlap +fn approx_sequence_set_size(seq_set: &SequenceSet) -> u64 { + seq_set.0.as_ref().iter().fold(0u64, |acc, seq| { + acc.saturating_add(approx_sequence_size(seq)) + }) +} + +// This is wrong as sequence UID can have holes, +// as we don't know the number of messages in the mailbox also +fn approx_sequence_size(seq: &Sequence) -> u64 { + match seq { + Sequence::Single(_) => 1, + Sequence::Range(SeqOrUid::Asterisk, _) | Sequence::Range(_, SeqOrUid::Asterisk) => u64::MAX, + Sequence::Range(SeqOrUid::Value(x1), SeqOrUid::Value(x2)) => { + let x2 = x2.get() as i64; + let x1 = x1.get() as i64; + (x2 - x1).abs().try_into().unwrap_or(1) + } + } +}