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 aero_collections::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> { 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 { 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)> { 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::, _>>()?; Ok(( Body::Data(Data::Fetch { seq: self.in_idx.i, items: res_attrs.try_into()?, }), seen, )) } 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()) } 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> { 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> { 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> { 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_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> { Ok(MessageDataItem::Body(mime_view::bodystructure( self.content.as_msg()?.child.as_ref(), false, )?)) } fn body_structure(&self) -> Result> { 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[
]<> and BODY.PEEK[
]<> /// peek does not implicitly set the \Seen flag /// eg. BODY[HEADER.FIELDS (DATE FROM)] /// eg. BODY[]<0.2048> fn body_ext( &self, section: &Option>, 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> { 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, } } }