From 5622a71cd163e4b18a3eabe8a28a5aedb23ee25d Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Sat, 6 Jan 2024 22:53:41 +0100 Subject: [PATCH] Search MIME headers --- src/imap/imf_view.rs | 7 ++++++ src/imap/mail_view.rs | 50 ++++++++++++++++++++++++++++++++----------- src/imap/mime_view.rs | 29 ++++++++++++++++++++----- src/imap/search.rs | 20 ++++++++--------- 4 files changed, 77 insertions(+), 29 deletions(-) diff --git a/src/imap/imf_view.rs b/src/imap/imf_view.rs index 8b52b9e..b56e27d 100644 --- a/src/imap/imf_view.rs +++ b/src/imap/imf_view.rs @@ -1,3 +1,6 @@ +use anyhow::{anyhow, Result}; +use chrono::naive::NaiveDate; + use imap_codec::imap_types::core::{IString, NString}; use imap_codec::imap_types::envelope::{Address, Envelope}; @@ -6,6 +9,10 @@ use eml_codec::imf; pub struct ImfView<'a>(pub &'a imf::Imf<'a>); impl<'a> ImfView<'a> { + pub fn naive_date(&self) -> Result { + Ok(self.0.date.ok_or(anyhow!("date is not set"))?.date_naive()) + } + /// Envelope rules are defined in RFC 3501, section 7.4.2 /// https://datatracker.ietf.org/doc/html/rfc3501#section-7.4.2 /// diff --git a/src/imap/mail_view.rs b/src/imap/mail_view.rs index 3fef145..365e535 100644 --- a/src/imap/mail_view.rs +++ b/src/imap/mail_view.rs @@ -40,12 +40,12 @@ impl<'a> MailView<'a> { QueryResult::FullResult { content, .. } => { let (_, parsed) = eml_codec::parse_message(&content).or(Err(anyhow!("Invalid mail body")))?; - FetchedMail::new_from_message(parsed) + FetchedMail::full_from_message(parsed) } QueryResult::PartialResult { metadata, .. } => { - let (_, parsed) = eml_codec::parse_imf(&metadata.headers) + let (_, parsed) = eml_codec::parse_message(&metadata.headers) .or(Err(anyhow!("unable to parse email headers")))?; - FetchedMail::Partial(parsed) + FetchedMail::partial_from_message(parsed) } QueryResult::IndexResult { .. } => FetchedMail::IndexOnly, }, @@ -53,7 +53,11 @@ impl<'a> MailView<'a> { } pub fn imf(&self) -> Option { - self.content.imf().map(ImfView) + self.content.as_imf().map(ImfView) + } + + pub fn selected_mime(&'a self) -> Option> { + self.content.as_anypart().ok().map(mime_view::SelectedMime) } pub fn filter(&self, ap: &AttributesProxy) -> Result<(Body<'static>, SeenFlag)> { @@ -103,6 +107,20 @@ impl<'a> MailView<'a> { Ok(msg_date.date_naive()) } + pub fn is_header_contains_pattern(&self, hdr: &[u8], pattern: &[u8]) -> bool { + let mime = match self.selected_mime() { + None => return false, + Some(x) => x, + }; + + let val = match mime.header_value(hdr) { + None => return false, + Some(x) => x, + }; + + val.windows(pattern.len()).any(|win| win == pattern) + } + // Private function, mainly for filter! fn uid(&self) -> MessageDataItem<'static> { MessageDataItem::Uid(self.in_idx.uid.clone()) @@ -139,12 +157,12 @@ impl<'a> MailView<'a> { } fn rfc_822_text(&self) -> Result> { - let txt: NString = self.content.as_full()?.raw_body.to_vec().try_into()?; + let txt: NString = self.content.as_msg()?.raw_body.to_vec().try_into()?; Ok(MessageDataItem::Rfc822Text(txt)) } fn rfc822(&self) -> Result> { - let full: NString = self.content.as_full()?.raw_part.to_vec().try_into()?; + let full: NString = self.content.as_msg()?.raw_part.to_vec().try_into()?; Ok(MessageDataItem::Rfc822(full)) } @@ -154,13 +172,13 @@ impl<'a> MailView<'a> { fn body(&self) -> Result> { Ok(MessageDataItem::Body(mime_view::bodystructure( - self.content.as_full()?.child.as_ref(), + self.content.as_msg()?.child.as_ref(), )?)) } fn body_structure(&self) -> Result> { Ok(MessageDataItem::Body(mime_view::bodystructure( - self.content.as_full()?.child.as_ref(), + self.content.as_msg()?.child.as_ref(), )?)) } @@ -231,32 +249,38 @@ pub enum SeenFlag { pub enum FetchedMail<'a> { IndexOnly, - Partial(imf::Imf<'a>), + Partial(AnyPart<'a>), Full(AnyPart<'a>), } impl<'a> FetchedMail<'a> { - pub fn new_from_message(msg: Message<'a>) -> Self { + pub fn full_from_message(msg: Message<'a>) -> Self { Self::Full(AnyPart::Msg(msg)) } + pub fn partial_from_message(msg: Message<'a>) -> Self { + Self::Partial(AnyPart::Msg(msg)) + } + fn as_anypart(&self) -> Result<&AnyPart<'a>> { match self { FetchedMail::Full(x) => Ok(&x), + FetchedMail::Partial(x) => Ok(&x), _ => bail!("The full message must be fetched, not only its headers"), } } - fn as_full(&self) -> Result<&Message<'a>> { + fn as_msg(&self) -> Result<&Message<'a>> { match self { FetchedMail::Full(AnyPart::Msg(x)) => Ok(&x), + FetchedMail::Partial(AnyPart::Msg(x)) => Ok(&x), _ => bail!("The full message must be fetched, not only its headers AND it must be an AnyPart::Msg."), } } - fn imf(&self) -> Option<&imf::Imf<'a>> { + fn as_imf(&self) -> Option<&imf::Imf<'a>> { match self { FetchedMail::Full(AnyPart::Msg(x)) => Some(&x.imf), - FetchedMail::Partial(x) => Some(&x), + FetchedMail::Partial(AnyPart::Msg(x)) => Some(&x.imf), _ => None, } } diff --git a/src/imap/mime_view.rs b/src/imap/mime_view.rs index 5175c76..cf6c751 100644 --- a/src/imap/mime_view.rs +++ b/src/imap/mime_view.rs @@ -164,8 +164,23 @@ impl<'a> SubsettedSection<'a> { /// Used for current MIME inspection /// /// See NodeMime for recursive logic -struct SelectedMime<'a>(&'a AnyPart<'a>); +pub struct SelectedMime<'a>(pub &'a AnyPart<'a>); impl<'a> SelectedMime<'a> { + pub fn header_value(&'a self, to_match_ext: &[u8]) -> Option<&'a [u8]> { + let to_match = to_match_ext.to_ascii_lowercase(); + + self.eml_mime() + .kv + .iter() + .filter_map(|field| match field { + header::Field::Good(header::Kv2(k, v)) => Some((k, v)), + _ => None, + }) + .find(|(k, _)| k.to_ascii_lowercase() == to_match) + .map(|(_, v)| v) + .copied() + } + /// The subsetted fetch section basically tells us the /// extraction logic to apply on our selected MIME. /// This function acts as a router for these logic. @@ -200,6 +215,13 @@ impl<'a> SelectedMime<'a> { Ok(ExtractedFull(bytes.to_vec().into())) } + fn eml_mime(&self) -> &eml_codec::mime::NaiveMIME<'_> { + match &self.0 { + AnyPart::Msg(msg) => msg.child.mime(), + other => other.mime(), + } + } + /// The [...] HEADER.FIELDS, and HEADER.FIELDS.NOT part /// specifiers refer to the [RFC-2822] header of the message or of /// an encapsulated [MIME-IMT] MESSAGE/RFC822 message. @@ -231,10 +253,7 @@ impl<'a> SelectedMime<'a> { .collect::>(); // Extract MIME headers - let mime = match &self.0 { - AnyPart::Msg(msg) => msg.child.mime(), - other => other.mime(), - }; + let mime = self.eml_mime(); // Filter our MIME headers based on the field index // 1. Keep only the correctly formatted headers diff --git a/src/imap/search.rs b/src/imap/search.rs index 0e00025..2fbfdcc 100644 --- a/src/imap/search.rs +++ b/src/imap/search.rs @@ -220,19 +220,17 @@ impl<'a> Criteria<'a> { 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!(), + Bcc(txt) => mail_view.is_header_contains_pattern(&b"bcc"[..], txt.as_ref()), + Cc(txt) => mail_view.is_header_contains_pattern(&b"cc"[..], txt.as_ref()), + From(txt) => mail_view.is_header_contains_pattern(&b"from"[..], txt.as_ref()), + Subject(txt)=> mail_view.is_header_contains_pattern(&b"subject"[..], txt.as_ref()), + To(txt) => mail_view.is_header_contains_pattern(&b"to"[..], txt.as_ref()), + Header(hdr, txt) => mail_view.is_header_contains_pattern(hdr.as_ref(), txt.as_ref()), // Filter on Date header - SentBefore(_) => unimplemented!(), - SentOn(_) => unimplemented!(), - SentSince(_) => unimplemented!(), + SentBefore(search_naive) => mail_view.imf().map(|imf| imf.naive_date().ok()).flatten().map(|msg_naive| &msg_naive < search_naive.as_ref()).unwrap_or(false), + SentOn(search_naive) => mail_view.imf().map(|imf| imf.naive_date().ok()).flatten().map(|msg_naive| &msg_naive == search_naive.as_ref()).unwrap_or(false), + SentSince(search_naive) => mail_view.imf().map(|imf| imf.naive_date().ok()).flatten().map(|msg_naive| &msg_naive > search_naive.as_ref()).unwrap_or(false), // Filter on the full content of the email