aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorQuentin Dufour <quentin@deuxfleurs.fr>2024-01-06 20:40:18 +0100
committerQuentin Dufour <quentin@deuxfleurs.fr>2024-01-06 20:40:18 +0100
commitea1772df425cb7faa4628b1c6c398ae3f77fca34 (patch)
tree5c6cf7f14b8046b3d063fb2fdf41712a8d1c28b4
parent870de493c84c6c3134d14ee8a234f124360354a7 (diff)
downloadaerogramme-ea1772df425cb7faa4628b1c6c398ae3f77fca34.tar.gz
aerogramme-ea1772df425cb7faa4628b1c6c398ae3f77fca34.zip
Searching on storage date is now possible
-rw-r--r--src/imap/imf_view.rs121
-rw-r--r--src/imap/mail_view.rs28
-rw-r--r--src/imap/mailbox_view.rs2
-rw-r--r--src/imap/mime_view.rs4
-rw-r--r--src/imap/search.rs90
5 files changed, 163 insertions, 82 deletions
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::<Vec<_>>();
+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::<Vec<_>>();
+
+ 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<ImfView> {
+ 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<NaiveDate> {
+ let mail_meta = self.query_result.metadata().expect("metadata were fetched");
+ let mail_ts: i64 = mail_meta.internaldate.try_into()?;
+ let msg_date: ChronoDateTime<Local> = 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<MessageDataItem<'static>> {
@@ -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<QueryResult<'_>>) -> Vec<MailIndex<'b>> {
- midx_list
+ pub fn filter_on_query<'b>(&self, midx_list: &[MailIndex<'b>], query_result: &'b Vec<QueryResult<'b>>) -> Result<Vec<MailIndex<'b>>> {
+ 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::<Result<Vec<_>, _>>()?
+ .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,
}
}