diff --git a/src/imap/imf_view.rs b/src/imap/imf_view.rs index 4297769..8b52b9e 100644 --- a/src/imap/imf_view.rs +++ b/src/imap/imf_view.rs @@ -3,66 +3,71 @@ use imap_codec::imap_types::envelope::{Address, Envelope}; use eml_codec::imf; -/// Envelope rules are defined in RFC 3501, section 7.4.2 -/// https://datatracker.ietf.org/doc/html/rfc3501#section-7.4.2 -/// -/// Some important notes: -/// -/// If the Sender or Reply-To lines are absent in the [RFC-2822] -/// header, or are present but empty, the server sets the -/// corresponding member of the envelope to be the same value as -/// the from member (the client is not expected to know to do -/// this). Note: [RFC-2822] requires that all messages have a valid -/// From header. Therefore, the from, sender, and reply-to -/// members in the envelope can not be NIL. -/// -/// If the Date, Subject, In-Reply-To, and Message-ID header lines -/// are absent in the [RFC-2822] header, the corresponding member -/// of the envelope is NIL; if these header lines are present but -/// empty the corresponding member of the envelope is the empty -/// string. +pub struct ImfView<'a>(pub &'a imf::Imf<'a>); -//@FIXME return an error if the envelope is invalid instead of panicking -//@FIXME some fields must be defaulted if there are not set. -pub fn message_envelope(msg: &imf::Imf) -> Envelope<'static> { - let from = msg.from.iter().map(convert_mbx).collect::>(); +impl<'a> ImfView<'a> { + /// Envelope rules are defined in RFC 3501, section 7.4.2 + /// https://datatracker.ietf.org/doc/html/rfc3501#section-7.4.2 + /// + /// Some important notes: + /// + /// If the Sender or Reply-To lines are absent in the [RFC-2822] + /// header, or are present but empty, the server sets the + /// corresponding member of the envelope to be the same value as + /// the from member (the client is not expected to know to do + /// this). Note: [RFC-2822] requires that all messages have a valid + /// From header. Therefore, the from, sender, and reply-to + /// members in the envelope can not be NIL. + /// + /// If the Date, Subject, In-Reply-To, and Message-ID header lines + /// are absent in the [RFC-2822] header, the corresponding member + /// of the envelope is NIL; if these header lines are present but + /// empty the corresponding member of the envelope is the empty + /// string. - Envelope { - date: NString( - msg.date - .as_ref() - .map(|d| IString::try_from(d.to_rfc3339()).unwrap()), - ), - subject: NString( - msg.subject - .as_ref() - .map(|d| IString::try_from(d.to_string()).unwrap()), - ), - sender: msg - .sender - .as_ref() - .map(|v| vec![convert_mbx(v)]) - .unwrap_or(from.clone()), - reply_to: if msg.reply_to.is_empty() { - from.clone() - } else { - convert_addresses(&msg.reply_to) - }, - from, - to: convert_addresses(&msg.to), - cc: convert_addresses(&msg.cc), - bcc: convert_addresses(&msg.bcc), - in_reply_to: NString( - msg.in_reply_to - .iter() - .next() - .map(|d| IString::try_from(d.to_string()).unwrap()), - ), - message_id: NString( - msg.msg_id - .as_ref() - .map(|d| IString::try_from(d.to_string()).unwrap()), - ), + //@FIXME return an error if the envelope is invalid instead of panicking + //@FIXME some fields must be defaulted if there are not set. + pub fn message_envelope(&self) -> Envelope<'static> { + let msg = self.0; + let from = msg.from.iter().map(convert_mbx).collect::>(); + + Envelope { + date: NString( + msg.date + .as_ref() + .map(|d| IString::try_from(d.to_rfc3339()).unwrap()), + ), + subject: NString( + msg.subject + .as_ref() + .map(|d| IString::try_from(d.to_string()).unwrap()), + ), + sender: msg + .sender + .as_ref() + .map(|v| vec![convert_mbx(v)]) + .unwrap_or(from.clone()), + reply_to: if msg.reply_to.is_empty() { + from.clone() + } else { + convert_addresses(&msg.reply_to) + }, + from, + to: convert_addresses(&msg.to), + cc: convert_addresses(&msg.cc), + bcc: convert_addresses(&msg.bcc), + in_reply_to: NString( + msg.in_reply_to + .iter() + .next() + .map(|d| IString::try_from(d.to_string()).unwrap()), + ), + message_id: NString( + msg.msg_id + .as_ref() + .map(|d| IString::try_from(d.to_string()).unwrap()), + ), + } } } diff --git a/src/imap/mail_view.rs b/src/imap/mail_view.rs index fc36e21..3fef145 100644 --- a/src/imap/mail_view.rs +++ b/src/imap/mail_view.rs @@ -1,7 +1,7 @@ use std::num::NonZeroU32; use anyhow::{anyhow, bail, Result}; -use chrono::{Offset, TimeZone, Utc}; +use chrono::{Offset, TimeZone, Utc, DateTime as ChronoDateTime, Local, naive::NaiveDate}; use imap_codec::imap_types::core::NString; use imap_codec::imap_types::datetime::DateTime; @@ -20,7 +20,7 @@ use crate::mail::query::QueryResult; use crate::imap::attributes::AttributesProxy; use crate::imap::flags; -use crate::imap::imf_view::message_envelope; +use crate::imap::imf_view::ImfView; use crate::imap::index::MailIndex; use crate::imap::mime_view; use crate::imap::response::Body; @@ -52,6 +52,10 @@ impl<'a> MailView<'a> { }) } + pub fn imf(&self) -> Option { + self.content.imf().map(ImfView) + } + pub fn filter(&self, ap: &AttributesProxy) -> Result<(Body<'static>, SeenFlag)> { let mut seen = SeenFlag::DoNothing; let res_attrs = ap @@ -89,6 +93,16 @@ impl<'a> MailView<'a> { )) } + pub fn stored_naive_date(&self) -> Result { + let mail_meta = self.query_result.metadata().expect("metadata were fetched"); + let mail_ts: i64 = mail_meta.internaldate.try_into()?; + let msg_date: ChronoDateTime = ChronoDateTime::from_timestamp(mail_ts, 0) + .ok_or(anyhow!("unable to parse timestamp"))? + .with_timezone(&Local); + + Ok(msg_date.date_naive()) + } + // Private function, mainly for filter! fn uid(&self) -> MessageDataItem<'static> { MessageDataItem::Uid(self.in_idx.uid.clone()) @@ -135,7 +149,7 @@ impl<'a> MailView<'a> { } fn envelope(&self) -> MessageDataItem<'static> { - MessageDataItem::Envelope(message_envelope(self.content.imf().clone())) + MessageDataItem::Envelope(self.imf().expect("an imf object is derivable from fetchedmail").message_envelope()) } fn body(&self) -> Result> { @@ -239,11 +253,11 @@ impl<'a> FetchedMail<'a> { } } - fn imf(&self) -> &imf::Imf<'a> { + fn imf(&self) -> Option<&imf::Imf<'a>> { match self { - FetchedMail::Full(AnyPart::Msg(x)) => &x.imf, - FetchedMail::Partial(x) => &x, - _ => panic!("Can't contain AnyPart that is not a message"), + FetchedMail::Full(AnyPart::Msg(x)) => Some(&x.imf), + FetchedMail::Partial(x) => Some(&x), + _ => None, } } } diff --git a/src/imap/mailbox_view.rs b/src/imap/mailbox_view.rs index 3c43be8..362e2e2 100644 --- a/src/imap/mailbox_view.rs +++ b/src/imap/mailbox_view.rs @@ -330,7 +330,7 @@ impl MailboxView { let query_result = self.0.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); + 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. diff --git a/src/imap/mime_view.rs b/src/imap/mime_view.rs index 1f36c47..5175c76 100644 --- a/src/imap/mime_view.rs +++ b/src/imap/mime_view.rs @@ -12,7 +12,7 @@ use eml_codec::{ header, mime, mime::r#type::Deductible, part::composite, part::discrete, part::AnyPart, }; -use crate::imap::imf_view::message_envelope; +use crate::imap::imf_view::ImfView; pub enum BodySection<'a> { Full(Cow<'a, [u8]>), @@ -347,7 +347,7 @@ impl<'a> NodeMsg<'a> { body: FetchBody { basic, specific: SpecificFields::Message { - envelope: Box::new(message_envelope(&self.1.imf)), + envelope: Box::new(ImfView(&self.1.imf).message_envelope()), body_structure: Box::new(NodeMime(&self.1.child).structure()?), number_of_lines: nol(self.1.raw_part), }, diff --git a/src/imap/search.rs b/src/imap/search.rs index 0ab0300..0e00025 100644 --- a/src/imap/search.rs +++ b/src/imap/search.rs @@ -1,10 +1,13 @@ +use std::num::NonZeroU32; + +use anyhow::Result; 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; use crate::mail::query::{QueryScope, QueryResult}; use crate::imap::index::MailIndex; +use crate::imap::mail_view::MailView; pub enum SeqType { Undefined, @@ -121,13 +124,16 @@ impl<'a> Criteria<'a> { (to_keep, to_fetch) } - pub fn filter_on_query<'b>(&self, midx_list: &[MailIndex<'b>], query_result: &Vec>) -> Vec> { - midx_list + pub fn filter_on_query<'b>(&self, midx_list: &[MailIndex<'b>], query_result: &'b Vec>) -> Result>> { + Ok(midx_list .iter() .zip(query_result.iter()) - .filter(|(midx, qr)| self.is_keep_on_query(midx, qr)) - .map(|(midx, _qr)| midx.clone()) - .collect() + .map(|(midx, qr)| MailView::new(qr, midx.clone())) + .collect::, _>>()? + .into_iter() + .filter(|mail_view| self.is_keep_on_query(mail_view)) + .map(|mail_view| mail_view.in_idx) + .collect()) } // ---- @@ -163,24 +169,80 @@ impl<'a> Criteria<'a> { | Subject(_) | To(_) | Before(_) | On(_) | Since(_) | Larger(_) | Smaller(_) | Text(_) | Body(_) => PartialDecision::Postpone, - _ => unreachable!(), + unknown => { + tracing::error!("Unknown filter {:?}", unknown); + PartialDecision::Discard + }, } } - fn is_keep_on_query(&self, midx: &MailIndex, qr: &QueryResult) -> bool { + + /// @TODO we re-eveluate twice the same logic. The correct way would be, on each pass, + /// to simplify the searck query, by removing the elements that were already checked. + /// For example if we have AND(OR(seqid(X), body(Y)), body(X)), we can't keep for sure + /// the email, as body(x) might be false. So we need to check it. But as seqid(x) is true, + /// we could simplify the request to just body(x) and truncate the first OR. Today, we are + /// not doing that, and thus we reevaluate everything. + fn is_keep_on_query(&self, mail_view: &MailView) -> 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)), + .any(|cur| Criteria(cur).is_keep_on_query(mail_view)), Or(left, right) => { - Criteria(left).is_keep_on_query(midx, qr) || Criteria(right).is_keep_on_query(midx, qr) + Criteria(left).is_keep_on_query(mail_view) || Criteria(right).is_keep_on_query(mail_view) } - Not(expr) => !Criteria(expr).is_keep_on_query(midx, qr), + Not(expr) => !Criteria(expr).is_keep_on_query(mail_view), All => true, - _ => unimplemented!(), + + // 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), + + // Filter on mail meta + Before(search_naive) => match mail_view.stored_naive_date() { + Ok(msg_naive) => &msg_naive < search_naive.as_ref(), + _ => false, + }, + On(search_naive) => match mail_view.stored_naive_date() { + Ok(msg_naive) => &msg_naive == search_naive.as_ref(), + _ => false, + }, + Since(search_naive) => match mail_view.stored_naive_date() { + Ok(msg_naive) => &msg_naive > search_naive.as_ref(), + _ => false, + }, + + // Message size is also stored in MailMeta + Larger(size_ref) => mail_view.query_result.metadata().expect("metadata were fetched").rfc822_size > *size_ref as usize, + Smaller(size_ref) => mail_view.query_result.metadata().expect("metadata were fetched").rfc822_size < *size_ref as usize, + + // Filter on well-known headers + Bcc(_) => unimplemented!(), + Cc(_) => unimplemented!(), + From(_) => unimplemented!(), + Subject(_)=> unimplemented!(), + To(_) => unimplemented!(), + + // Filter on arbitrary header + Header(..) => unimplemented!(), + + // Filter on Date header + SentBefore(_) => unimplemented!(), + SentOn(_) => unimplemented!(), + SentSince(_) => unimplemented!(), + + + // Filter on the full content of the email + Text(_) => unimplemented!(), + Body(_) => unimplemented!(), + + unknown => { + tracing::error!("Unknown filter {:?}", unknown); + false + }, } } } @@ -240,16 +302,16 @@ impl PartialDecision { fn or(&self, other: &Self) -> Self { match (self, other) { - (Self::Postpone, _) | (_, Self::Postpone) => Self::Postpone, (Self::Keep, _) | (_, Self::Keep) => Self::Keep, + (Self::Postpone, _) | (_, Self::Postpone) => Self::Postpone, (Self::Discard, Self::Discard) => Self::Discard, } } fn and(&self, other: &Self) -> Self { match (self, other) { - (Self::Postpone, _) | (_, Self::Postpone) => Self::Postpone, (Self::Discard, _) | (_, Self::Discard) => Self::Discard, + (Self::Postpone, _) | (_, Self::Postpone) => Self::Postpone, (Self::Keep, Self::Keep) => Self::Keep, } }