From 870de493c84c6c3134d14ee8a234f124360354a7 Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Sat, 6 Jan 2024 18:51:21 +0100 Subject: [PATCH] Search is made more clear --- src/imap/mailbox_view.rs | 11 ++- src/imap/search.rs | 192 ++++++++++++++++++++++++++------------- 2 files changed, 136 insertions(+), 67 deletions(-) diff --git a/src/imap/mailbox_view.rs b/src/imap/mailbox_view.rs index a07f6a4..3c43be8 100644 --- a/src/imap/mailbox_view.rs +++ b/src/imap/mailbox_view.rs @@ -319,24 +319,25 @@ impl MailboxView { let selection = self.index().fetch(&seq_set, seq_type.is_uid())?; // 3. Filter the selection based on the ID / UID / Flags - let selection = crit.filter_on_idx(&selection); + let (kept_idx, to_fetch) = crit.filter_on_idx(&selection); // 4. Fetch additional info about the emails let query_scope = crit.query_scope(); - let uuids = selection + let uuids = to_fetch .iter() .map(|midx| midx.uuid) .collect::>(); let query_result = self.0.query(&uuids, query_scope).fetch().await?; // 5. If needed, filter the selection based on the body - let selection = crit.filter_on_query(&selection, &query_result); + 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 selection_fmt = match uid { - true => selection.into_iter().map(|in_idx| in_idx.uid).collect(), - _ => selection.into_iter().map(|in_idx| in_idx.i).collect(), + 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))]) diff --git a/src/imap/search.rs b/src/imap/search.rs index 2a1119c..0ab0300 100644 --- a/src/imap/search.rs +++ b/src/imap/search.rs @@ -17,6 +17,7 @@ impl SeqType { } } + pub struct Criteria<'a>(pub &'a SearchKey<'a>); impl<'a> Criteria<'a> { /// Returns a set of email identifiers that is greater or equal @@ -82,6 +83,14 @@ impl<'a> Criteria<'a> { pub fn query_scope(&self) -> QueryScope { use SearchKey::*; match self.0 { + // Combinators + And(and_list) => and_list.as_ref().iter().fold(QueryScope::Index, |prev, sk| { + prev.union(&Criteria(sk).query_scope()) + }), + Not(inner) => Criteria(inner).query_scope(), + Or(left, right) => Criteria(left).query_scope().union(&Criteria(right).query_scope()), + All => QueryScope::Index, + // IMF Headers Bcc(_) | Cc(_) | From(_) | Header(..) | SentBefore(_) | SentOn(_) | SentSince(_) | Subject(_) | To(_) => QueryScope::Partial, @@ -91,25 +100,34 @@ impl<'a> Criteria<'a> { Larger(_) | Smaller(_) => QueryScope::Partial, // Text and Body require that we fetch the full content! Text(_) | Body(_) => QueryScope::Full, - And(and_list) => and_list.as_ref().iter().fold(QueryScope::Index, |prev, sk| { - prev.union(&Criteria(sk).query_scope()) - }), - Not(inner) => Criteria(inner).query_scope(), - Or(left, right) => Criteria(left).query_scope().union(&Criteria(right).query_scope()), + _ => QueryScope::Index, } } - pub fn filter_on_idx<'b>(&self, midx_list: &[MailIndex<'b>]) -> Vec> { - midx_list + /// 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 + pub fn filter_on_idx<'b>(&self, midx_list: &[MailIndex<'b>]) -> (Vec>, Vec>) { + let (p1, p2): (Vec<_>, Vec<_>) = midx_list .iter() - .filter(|x| self.is_keep_on_idx(x).is_keep()) - .map(|x| (*x).clone()) - .collect::>() + .map(|x| (x, self.is_keep_on_idx(x))) + .filter(|(_midx, decision)| decision.is_keep()) + .map(|(midx, decision)| ((*midx).clone(), decision)) + .partition(|(_midx, decision)| matches!(decision, PartialDecision::Keep)); + + let to_keep = p1.into_iter().map(|(v, _)| v).collect(); + let to_fetch = p2.into_iter().map(|(v, _)| v).collect(); + (to_keep, to_fetch) } - pub fn filter_on_query(&self, midx_list: &[MailIndex], query_result: &Vec>) -> Vec { - unimplemented!(); + pub fn filter_on_query<'b>(&self, midx_list: &[MailIndex<'b>], query_result: &Vec>) -> Vec> { + midx_list + .iter() + .zip(query_result.iter()) + .filter(|(midx, qr)| self.is_keep_on_query(midx, qr)) + .map(|(midx, _qr)| midx.clone()) + .collect() } // ---- @@ -137,65 +155,37 @@ impl<'a> Criteria<'a> { All => PartialDecision::Keep, // Sequence logic - SequenceSet(seq_set) => seq_set.0.as_ref().iter().fold(PartialDecision::Discard, |acc, seq| { - let local_decision: PartialDecision = midx.is_in_sequence_i(seq).into(); - acc.or(&local_decision) - }), - Uid(seq_set) => seq_set.0.as_ref().iter().fold(PartialDecision::Discard, |acc, seq| { - let local_decision: PartialDecision = midx.is_in_sequence_uid(seq).into(); - acc.or(&local_decision) - }), - - // Flag logic - Answered => midx.is_flag_set("\\Answered").into(), - Deleted => midx.is_flag_set("\\Deleted").into(), - Draft => midx.is_flag_set("\\Draft").into(), - Flagged => midx.is_flag_set("\\Flagged").into(), - Keyword(kw) => midx.is_flag_set(kw.inner()).into(), - New => { - let is_recent: PartialDecision = midx.is_flag_set("\\Recent").into(); - let is_seen: PartialDecision = midx.is_flag_set("\\Seen").into(); - is_recent.and(&is_seen.not()) - }, - Old => { - let is_recent: PartialDecision = midx.is_flag_set("\\Recent").into(); - is_recent.not() - }, - Recent => midx.is_flag_set("\\Recent").into(), - Seen => midx.is_flag_set("\\Seen").into(), - Unanswered => { - let is_answered: PartialDecision = midx.is_flag_set("\\Recent").into(); - is_answered.not() - }, - Undeleted => { - let is_deleted: PartialDecision = midx.is_flag_set("\\Deleted").into(); - is_deleted.not() - }, - Undraft => { - let is_draft: PartialDecision = midx.is_flag_set("\\Draft").into(); - is_draft.not() - }, - Unflagged => { - let is_flagged: PartialDecision = midx.is_flag_set("\\Flagged").into(); - is_flagged.not() - }, - Unkeyword(kw) => { - let is_keyword_set: PartialDecision = midx.is_flag_set(kw.inner()).into(); - is_keyword_set.not() - }, - Unseen => { - let is_seen: PartialDecision = midx.is_flag_set("\\Seen").into(); - is_seen.not() - }, + 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(), // All the stuff we can't evaluate yet Bcc(_) | Cc(_) | From(_) | Header(..) | SentBefore(_) | SentOn(_) | SentSince(_) | Subject(_) | To(_) | Before(_) | On(_) | Since(_) | Larger(_) | Smaller(_) | Text(_) | Body(_) => PartialDecision::Postpone, + + _ => unreachable!(), + } + } + + fn is_keep_on_query(&self, midx: &MailIndex, qr: &QueryResult) -> bool { + use SearchKey::*; + match self.0 { + // Combinator logic + And(expr_list) => expr_list + .as_ref() + .iter() + .any(|cur| Criteria(cur).is_keep_on_query(midx, qr)), + Or(left, right) => { + Criteria(left).is_keep_on_query(midx, qr) || Criteria(right).is_keep_on_query(midx, qr) + } + Not(expr) => !Criteria(expr).is_keep_on_query(midx, qr), + All => true, + _ => unimplemented!(), } } } +// ---- Sequence things ---- fn sequence_set_all() -> SequenceSet { SequenceSet::from(Sequence::Range( SeqOrUid::Value(NonZeroU32::MIN), @@ -224,6 +214,8 @@ fn approx_sequence_size(seq: &Sequence) -> u64 { } } +// --- Partial decision things ---- + enum PartialDecision { Keep, Discard, @@ -266,3 +258,79 @@ impl PartialDecision { !matches!(self, Self::Discard) } } + +// ----- Search Key things --- +fn is_sk_flag(sk: &SearchKey) -> bool { + use SearchKey::*; + match sk { + Answered | Deleted | Draft | Flagged | Keyword(..) | New | Old + | Recent | Seen | Unanswered | Undeleted | Undraft + | Unflagged | Unkeyword(..) | Unseen => true, + _ => false, + } +} + +fn is_keep_flag(sk: &SearchKey, midx: &MailIndex) -> bool { + use SearchKey::*; + match sk { + Answered => midx.is_flag_set("\\Answered"), + Deleted => midx.is_flag_set("\\Deleted"), + Draft => midx.is_flag_set("\\Draft"), + Flagged => midx.is_flag_set("\\Flagged"), + Keyword(kw) => midx.is_flag_set(kw.inner()), + New => { + let is_recent = midx.is_flag_set("\\Recent"); + let is_seen = midx.is_flag_set("\\Seen"); + is_recent && !is_seen + }, + Old => { + let is_recent = midx.is_flag_set("\\Recent"); + !is_recent + }, + Recent => midx.is_flag_set("\\Recent"), + Seen => midx.is_flag_set("\\Seen"), + Unanswered => { + let is_answered = midx.is_flag_set("\\Recent"); + !is_answered + }, + Undeleted => { + let is_deleted = midx.is_flag_set("\\Deleted"); + !is_deleted + }, + Undraft => { + let is_draft = midx.is_flag_set("\\Draft"); + !is_draft + }, + Unflagged => { + let is_flagged = midx.is_flag_set("\\Flagged"); + !is_flagged + }, + Unkeyword(kw) => { + let is_keyword_set = midx.is_flag_set(kw.inner()); + !is_keyword_set + }, + Unseen => { + let is_seen = midx.is_flag_set("\\Seen"); + !is_seen + }, + + // Not flag logic + _ => unreachable!(), + } +} + +fn is_sk_seq(sk: &SearchKey) -> bool { + use SearchKey::*; + match sk { + SequenceSet(..) | Uid(..) => true, + _ => false, + } +} +fn is_keep_seq(sk: &SearchKey, midx: &MailIndex) -> bool { + use SearchKey::*; + match sk { + SequenceSet(seq_set) => seq_set.0.as_ref().iter().any(|seq| midx.is_in_sequence_i(seq)), + Uid(seq_set) => seq_set.0.as_ref().iter().any(|seq| midx.is_in_sequence_uid(seq)), + _ => unreachable!(), + } +}