Implement search #61
4 changed files with 77 additions and 29 deletions
|
@ -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::core::{IString, NString};
|
||||||
use imap_codec::imap_types::envelope::{Address, Envelope};
|
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>);
|
pub struct ImfView<'a>(pub &'a imf::Imf<'a>);
|
||||||
|
|
||||||
impl<'a> ImfView<'a> {
|
impl<'a> ImfView<'a> {
|
||||||
|
pub fn naive_date(&self) -> Result<NaiveDate> {
|
||||||
|
Ok(self.0.date.ok_or(anyhow!("date is not set"))?.date_naive())
|
||||||
|
}
|
||||||
|
|
||||||
/// Envelope rules are defined in RFC 3501, section 7.4.2
|
/// Envelope rules are defined in RFC 3501, section 7.4.2
|
||||||
/// https://datatracker.ietf.org/doc/html/rfc3501#section-7.4.2
|
/// https://datatracker.ietf.org/doc/html/rfc3501#section-7.4.2
|
||||||
///
|
///
|
||||||
|
|
|
@ -40,12 +40,12 @@ impl<'a> MailView<'a> {
|
||||||
QueryResult::FullResult { content, .. } => {
|
QueryResult::FullResult { content, .. } => {
|
||||||
let (_, parsed) =
|
let (_, parsed) =
|
||||||
eml_codec::parse_message(&content).or(Err(anyhow!("Invalid mail body")))?;
|
eml_codec::parse_message(&content).or(Err(anyhow!("Invalid mail body")))?;
|
||||||
FetchedMail::new_from_message(parsed)
|
FetchedMail::full_from_message(parsed)
|
||||||
}
|
}
|
||||||
QueryResult::PartialResult { metadata, .. } => {
|
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")))?;
|
.or(Err(anyhow!("unable to parse email headers")))?;
|
||||||
FetchedMail::Partial(parsed)
|
FetchedMail::partial_from_message(parsed)
|
||||||
}
|
}
|
||||||
QueryResult::IndexResult { .. } => FetchedMail::IndexOnly,
|
QueryResult::IndexResult { .. } => FetchedMail::IndexOnly,
|
||||||
},
|
},
|
||||||
|
@ -53,7 +53,11 @@ impl<'a> MailView<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn imf(&self) -> Option<ImfView> {
|
pub fn imf(&self) -> Option<ImfView> {
|
||||||
self.content.imf().map(ImfView)
|
self.content.as_imf().map(ImfView)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn selected_mime(&'a self) -> Option<mime_view::SelectedMime<'a>> {
|
||||||
|
self.content.as_anypart().ok().map(mime_view::SelectedMime)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn filter(&self, ap: &AttributesProxy) -> Result<(Body<'static>, SeenFlag)> {
|
pub fn filter(&self, ap: &AttributesProxy) -> Result<(Body<'static>, SeenFlag)> {
|
||||||
|
@ -103,6 +107,20 @@ impl<'a> MailView<'a> {
|
||||||
Ok(msg_date.date_naive())
|
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!
|
// Private function, mainly for filter!
|
||||||
fn uid(&self) -> MessageDataItem<'static> {
|
fn uid(&self) -> MessageDataItem<'static> {
|
||||||
MessageDataItem::Uid(self.in_idx.uid.clone())
|
MessageDataItem::Uid(self.in_idx.uid.clone())
|
||||||
|
@ -139,12 +157,12 @@ impl<'a> MailView<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn rfc_822_text(&self) -> Result<MessageDataItem<'static>> {
|
fn rfc_822_text(&self) -> Result<MessageDataItem<'static>> {
|
||||||
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))
|
Ok(MessageDataItem::Rfc822Text(txt))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn rfc822(&self) -> Result<MessageDataItem<'static>> {
|
fn rfc822(&self) -> Result<MessageDataItem<'static>> {
|
||||||
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))
|
Ok(MessageDataItem::Rfc822(full))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -154,13 +172,13 @@ impl<'a> MailView<'a> {
|
||||||
|
|
||||||
fn body(&self) -> Result<MessageDataItem<'static>> {
|
fn body(&self) -> Result<MessageDataItem<'static>> {
|
||||||
Ok(MessageDataItem::Body(mime_view::bodystructure(
|
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<MessageDataItem<'static>> {
|
fn body_structure(&self) -> Result<MessageDataItem<'static>> {
|
||||||
Ok(MessageDataItem::Body(mime_view::bodystructure(
|
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> {
|
pub enum FetchedMail<'a> {
|
||||||
IndexOnly,
|
IndexOnly,
|
||||||
Partial(imf::Imf<'a>),
|
Partial(AnyPart<'a>),
|
||||||
Full(AnyPart<'a>),
|
Full(AnyPart<'a>),
|
||||||
}
|
}
|
||||||
impl<'a> FetchedMail<'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))
|
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>> {
|
fn as_anypart(&self) -> Result<&AnyPart<'a>> {
|
||||||
match self {
|
match self {
|
||||||
FetchedMail::Full(x) => Ok(&x),
|
FetchedMail::Full(x) => Ok(&x),
|
||||||
|
FetchedMail::Partial(x) => Ok(&x),
|
||||||
_ => bail!("The full message must be fetched, not only its headers"),
|
_ => 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 {
|
match self {
|
||||||
FetchedMail::Full(AnyPart::Msg(x)) => Ok(&x),
|
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."),
|
_ => 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 {
|
match self {
|
||||||
FetchedMail::Full(AnyPart::Msg(x)) => Some(&x.imf),
|
FetchedMail::Full(AnyPart::Msg(x)) => Some(&x.imf),
|
||||||
FetchedMail::Partial(x) => Some(&x),
|
FetchedMail::Partial(AnyPart::Msg(x)) => Some(&x.imf),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -164,8 +164,23 @@ impl<'a> SubsettedSection<'a> {
|
||||||
/// Used for current MIME inspection
|
/// Used for current MIME inspection
|
||||||
///
|
///
|
||||||
/// See NodeMime for recursive logic
|
/// See NodeMime for recursive logic
|
||||||
struct SelectedMime<'a>(&'a AnyPart<'a>);
|
pub struct SelectedMime<'a>(pub &'a AnyPart<'a>);
|
||||||
impl<'a> SelectedMime<'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
|
/// The subsetted fetch section basically tells us the
|
||||||
/// extraction logic to apply on our selected MIME.
|
/// extraction logic to apply on our selected MIME.
|
||||||
/// This function acts as a router for these logic.
|
/// This function acts as a router for these logic.
|
||||||
|
@ -200,6 +215,13 @@ impl<'a> SelectedMime<'a> {
|
||||||
Ok(ExtractedFull(bytes.to_vec().into()))
|
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
|
/// The [...] HEADER.FIELDS, and HEADER.FIELDS.NOT part
|
||||||
/// specifiers refer to the [RFC-2822] header of the message or of
|
/// specifiers refer to the [RFC-2822] header of the message or of
|
||||||
/// an encapsulated [MIME-IMT] MESSAGE/RFC822 message.
|
/// an encapsulated [MIME-IMT] MESSAGE/RFC822 message.
|
||||||
|
@ -231,10 +253,7 @@ impl<'a> SelectedMime<'a> {
|
||||||
.collect::<HashSet<_>>();
|
.collect::<HashSet<_>>();
|
||||||
|
|
||||||
// Extract MIME headers
|
// Extract MIME headers
|
||||||
let mime = match &self.0 {
|
let mime = self.eml_mime();
|
||||||
AnyPart::Msg(msg) => msg.child.mime(),
|
|
||||||
other => other.mime(),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Filter our MIME headers based on the field index
|
// Filter our MIME headers based on the field index
|
||||||
// 1. Keep only the correctly formatted headers
|
// 1. Keep only the correctly formatted headers
|
||||||
|
|
|
@ -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,
|
Smaller(size_ref) => mail_view.query_result.metadata().expect("metadata were fetched").rfc822_size < *size_ref as usize,
|
||||||
|
|
||||||
// Filter on well-known headers
|
// Filter on well-known headers
|
||||||
Bcc(_) => unimplemented!(),
|
Bcc(txt) => mail_view.is_header_contains_pattern(&b"bcc"[..], txt.as_ref()),
|
||||||
Cc(_) => unimplemented!(),
|
Cc(txt) => mail_view.is_header_contains_pattern(&b"cc"[..], txt.as_ref()),
|
||||||
From(_) => unimplemented!(),
|
From(txt) => mail_view.is_header_contains_pattern(&b"from"[..], txt.as_ref()),
|
||||||
Subject(_)=> unimplemented!(),
|
Subject(txt)=> mail_view.is_header_contains_pattern(&b"subject"[..], txt.as_ref()),
|
||||||
To(_) => unimplemented!(),
|
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 arbitrary header
|
|
||||||
Header(..) => unimplemented!(),
|
|
||||||
|
|
||||||
// Filter on Date header
|
// Filter on Date header
|
||||||
SentBefore(_) => 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(_) => unimplemented!(),
|
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(_) => unimplemented!(),
|
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
|
// Filter on the full content of the email
|
||||||
|
|
Loading…
Reference in a new issue