aboutsummaryrefslogtreecommitdiff
path: root/aero-proto/imap/mail_view.rs
diff options
context:
space:
mode:
Diffstat (limited to 'aero-proto/imap/mail_view.rs')
-rw-r--r--aero-proto/imap/mail_view.rs306
1 files changed, 306 insertions, 0 deletions
diff --git a/aero-proto/imap/mail_view.rs b/aero-proto/imap/mail_view.rs
new file mode 100644
index 0000000..a8db733
--- /dev/null
+++ b/aero-proto/imap/mail_view.rs
@@ -0,0 +1,306 @@
+use std::num::NonZeroU32;
+
+use anyhow::{anyhow, bail, Result};
+use chrono::{naive::NaiveDate, DateTime as ChronoDateTime, Local, Offset, TimeZone, Utc};
+
+use imap_codec::imap_types::core::NString;
+use imap_codec::imap_types::datetime::DateTime;
+use imap_codec::imap_types::fetch::{
+ MessageDataItem, MessageDataItemName, Section as FetchSection,
+};
+use imap_codec::imap_types::flag::Flag;
+use imap_codec::imap_types::response::Data;
+
+use eml_codec::{
+ imf,
+ part::{composite::Message, AnyPart},
+};
+
+use crate::mail::query::QueryResult;
+
+use crate::imap::attributes::AttributesProxy;
+use crate::imap::flags;
+use crate::imap::imf_view::ImfView;
+use crate::imap::index::MailIndex;
+use crate::imap::mime_view;
+use crate::imap::response::Body;
+
+pub struct MailView<'a> {
+ pub in_idx: &'a MailIndex<'a>,
+ pub query_result: &'a QueryResult,
+ pub content: FetchedMail<'a>,
+}
+
+impl<'a> MailView<'a> {
+ pub fn new(query_result: &'a QueryResult, in_idx: &'a MailIndex<'a>) -> Result<MailView<'a>> {
+ Ok(Self {
+ in_idx,
+ query_result,
+ content: match query_result {
+ QueryResult::FullResult { content, .. } => {
+ let (_, parsed) =
+ eml_codec::parse_message(&content).or(Err(anyhow!("Invalid mail body")))?;
+ FetchedMail::full_from_message(parsed)
+ }
+ QueryResult::PartialResult { metadata, .. } => {
+ let (_, parsed) = eml_codec::parse_message(&metadata.headers)
+ .or(Err(anyhow!("unable to parse email headers")))?;
+ FetchedMail::partial_from_message(parsed)
+ }
+ QueryResult::IndexResult { .. } => FetchedMail::IndexOnly,
+ },
+ })
+ }
+
+ pub fn imf(&self) -> Option<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)> {
+ let mut seen = SeenFlag::DoNothing;
+ let res_attrs = ap
+ .attrs
+ .iter()
+ .map(|attr| match attr {
+ MessageDataItemName::Uid => Ok(self.uid()),
+ MessageDataItemName::Flags => Ok(self.flags()),
+ MessageDataItemName::Rfc822Size => self.rfc_822_size(),
+ MessageDataItemName::Rfc822Header => self.rfc_822_header(),
+ MessageDataItemName::Rfc822Text => self.rfc_822_text(),
+ MessageDataItemName::Rfc822 => {
+ if self.is_not_yet_seen() {
+ seen = SeenFlag::MustAdd;
+ }
+ self.rfc822()
+ }
+ MessageDataItemName::Envelope => Ok(self.envelope()),
+ MessageDataItemName::Body => self.body(),
+ MessageDataItemName::BodyStructure => self.body_structure(),
+ MessageDataItemName::BodyExt {
+ section,
+ partial,
+ peek,
+ } => {
+ let (body, has_seen) = self.body_ext(section, partial, peek)?;
+ seen = has_seen;
+ Ok(body)
+ }
+ MessageDataItemName::InternalDate => self.internal_date(),
+ MessageDataItemName::ModSeq => Ok(self.modseq()),
+ })
+ .collect::<Result<Vec<_>, _>>()?;
+
+ Ok((
+ Body::Data(Data::Fetch {
+ seq: self.in_idx.i,
+ items: res_attrs.try_into()?,
+ }),
+ seen,
+ ))
+ }
+
+ 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())
+ }
+
+ 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())
+ }
+
+ fn flags(&self) -> MessageDataItem<'static> {
+ MessageDataItem::Flags(
+ self.in_idx
+ .flags
+ .iter()
+ .filter_map(|f| flags::from_str(f))
+ .collect(),
+ )
+ }
+
+ fn rfc_822_size(&self) -> Result<MessageDataItem<'static>> {
+ let sz = self
+ .query_result
+ .metadata()
+ .ok_or(anyhow!("mail metadata are required"))?
+ .rfc822_size;
+ Ok(MessageDataItem::Rfc822Size(sz as u32))
+ }
+
+ fn rfc_822_header(&self) -> Result<MessageDataItem<'static>> {
+ let hdrs: NString = self
+ .query_result
+ .metadata()
+ .ok_or(anyhow!("mail metadata are required"))?
+ .headers
+ .to_vec()
+ .try_into()?;
+ Ok(MessageDataItem::Rfc822Header(hdrs))
+ }
+
+ fn rfc_822_text(&self) -> Result<MessageDataItem<'static>> {
+ let txt: NString = self.content.as_msg()?.raw_body.to_vec().try_into()?;
+ Ok(MessageDataItem::Rfc822Text(txt))
+ }
+
+ fn rfc822(&self) -> Result<MessageDataItem<'static>> {
+ let full: NString = self.content.as_msg()?.raw_part.to_vec().try_into()?;
+ Ok(MessageDataItem::Rfc822(full))
+ }
+
+ fn envelope(&self) -> MessageDataItem<'static> {
+ MessageDataItem::Envelope(
+ self.imf()
+ .expect("an imf object is derivable from fetchedmail")
+ .message_envelope(),
+ )
+ }
+
+ fn body(&self) -> Result<MessageDataItem<'static>> {
+ Ok(MessageDataItem::Body(mime_view::bodystructure(
+ self.content.as_msg()?.child.as_ref(),
+ false,
+ )?))
+ }
+
+ fn body_structure(&self) -> Result<MessageDataItem<'static>> {
+ Ok(MessageDataItem::BodyStructure(mime_view::bodystructure(
+ self.content.as_msg()?.child.as_ref(),
+ true,
+ )?))
+ }
+
+ fn is_not_yet_seen(&self) -> bool {
+ let seen_flag = Flag::Seen.to_string();
+ !self.in_idx.flags.iter().any(|x| *x == seen_flag)
+ }
+
+ /// maps to BODY[<section>]<<partial>> and BODY.PEEK[<section>]<<partial>>
+ /// peek does not implicitly set the \Seen flag
+ /// eg. BODY[HEADER.FIELDS (DATE FROM)]
+ /// eg. BODY[]<0.2048>
+ fn body_ext(
+ &self,
+ section: &Option<FetchSection<'static>>,
+ partial: &Option<(u32, NonZeroU32)>,
+ peek: &bool,
+ ) -> Result<(MessageDataItem<'static>, SeenFlag)> {
+ // Manage Seen flag
+ let mut seen = SeenFlag::DoNothing;
+ if !peek && self.is_not_yet_seen() {
+ // Add \Seen flag
+ //self.mailbox.add_flags(uuid, &[seen_flag]).await?;
+ seen = SeenFlag::MustAdd;
+ }
+
+ // Process message
+ let (text, origin) =
+ match mime_view::body_ext(self.content.as_anypart()?, section, partial)? {
+ mime_view::BodySection::Full(body) => (body, None),
+ mime_view::BodySection::Slice { body, origin_octet } => (body, Some(origin_octet)),
+ };
+
+ let data: NString = text.to_vec().try_into()?;
+
+ return Ok((
+ MessageDataItem::BodyExt {
+ section: section.as_ref().map(|fs| fs.clone()),
+ origin,
+ data,
+ },
+ seen,
+ ));
+ }
+
+ fn internal_date(&self) -> Result<MessageDataItem<'static>> {
+ let dt = Utc
+ .fix()
+ .timestamp_opt(
+ i64::try_from(
+ self.query_result
+ .metadata()
+ .ok_or(anyhow!("mail metadata were not fetched"))?
+ .internaldate
+ / 1000,
+ )?,
+ 0,
+ )
+ .earliest()
+ .ok_or(anyhow!("Unable to parse internal date"))?;
+ Ok(MessageDataItem::InternalDate(DateTime::unvalidated(dt)))
+ }
+
+ fn modseq(&self) -> MessageDataItem<'static> {
+ MessageDataItem::ModSeq(self.in_idx.modseq)
+ }
+}
+
+pub enum SeenFlag {
+ DoNothing,
+ MustAdd,
+}
+
+// -------------------
+
+pub enum FetchedMail<'a> {
+ IndexOnly,
+ Partial(AnyPart<'a>),
+ Full(AnyPart<'a>),
+}
+impl<'a> FetchedMail<'a> {
+ 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))
+ }
+
+ pub 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"),
+ }
+ }
+
+ pub 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."),
+ }
+ }
+
+ pub fn as_imf(&self) -> Option<&imf::Imf<'a>> {
+ match self {
+ FetchedMail::Full(AnyPart::Msg(x)) => Some(&x.imf),
+ FetchedMail::Partial(AnyPart::Msg(x)) => Some(&x.imf),
+ _ => None,
+ }
+ }
+}