use std::num::NonZeroU32; use std::sync::Arc; use anyhow::{anyhow, bail, Error, Result}; use boitalettres::proto::res::body::Data as Body; use chrono::{Offset, TimeZone, Utc}; use futures::stream::{FuturesOrdered, StreamExt}; use imap_codec::types::address::Address; use imap_codec::types::body::{BasicFields, Body as FetchBody, BodyStructure, SpecificFields}; use imap_codec::types::core::{AString, Atom, IString, NString}; use imap_codec::types::datetime::MyDateTime; use imap_codec::types::envelope::Envelope; use imap_codec::types::fetch_attributes::{ FetchAttribute, MacroOrFetchAttributes, Section as FetchSection, }; use imap_codec::types::flag::{Flag, StoreResponse, StoreType}; use imap_codec::types::response::{Code, Data, MessageAttribute, Status}; use imap_codec::types::sequence::{self, SequenceSet}; use eml_codec::{ imf::{self as imf}, part::{AnyPart}, mime::r#type::Deductible, mime, }; use crate::mail::mailbox::Mailbox; use crate::mail::uidindex::{ImapUid, ImapUidvalidity, UidIndex}; use crate::mail::unique_ident::UniqueIdent; const DEFAULT_FLAGS: [Flag; 5] = [ Flag::Seen, Flag::Answered, Flag::Flagged, Flag::Deleted, Flag::Draft, ]; const BODY_CHECK: &str = "body attribute asked but only header is fetched, logic error"; /// A MailboxView is responsible for giving the client the information /// it needs about a mailbox, such as an initial summary of the mailbox's /// content and continuous updates indicating when the content /// of the mailbox has been changed. /// To do this, it keeps a variable `known_state` that corresponds to /// what the client knows, and produces IMAP messages to be sent to the /// client that go along updates to `known_state`. pub struct MailboxView { pub(crate) mailbox: Arc, known_state: UidIndex, } impl MailboxView { /// Creates a new IMAP view into a mailbox. /// Generates the necessary IMAP messages so that the client /// has a satisfactory summary of the current mailbox's state. /// These are the messages that are sent in response to a SELECT command. pub async fn new(mailbox: Arc) -> Result<(Self, Vec)> { let state = mailbox.current_uid_index().await; let new_view = Self { mailbox, known_state: state, }; let mut data = Vec::::new(); data.push(new_view.exists_status()?); data.push(new_view.recent_status()?); data.extend(new_view.flags_status()?.into_iter()); data.push(new_view.uidvalidity_status()?); data.push(new_view.uidnext_status()?); Ok((new_view, data)) } /// Produces a set of IMAP responses describing the change between /// what the client knows and what is actually in the mailbox. /// This does NOT trigger a sync, it bases itself on what is currently /// loaded in RAM by Bayou. pub async fn update(&mut self) -> Result> { let new_view = MailboxView { mailbox: self.mailbox.clone(), known_state: self.mailbox.current_uid_index().await, }; let mut data = Vec::::new(); // Calculate diff between two mailbox states // See example in IMAP RFC in section on NOOP command: // we want to produce something like this: // C: a047 NOOP // S: * 22 EXPUNGE // S: * 23 EXISTS // S: * 14 FETCH (UID 1305 FLAGS (\Seen \Deleted)) // S: a047 OK Noop completed // In other words: // - notify client of expunged mails // - if new mails arrived, notify client of number of existing mails // - if flags changed for existing mails, tell client // (for this last step: if uidvalidity changed, do nothing, // just notify of new uidvalidity and they will resync) // - notify client of expunged mails let mut n_expunge = 0; for (i, (_uid, uuid)) in self.known_state.idx_by_uid.iter().enumerate() { if !new_view.known_state.table.contains_key(uuid) { data.push(Body::Data(Data::Expunge( NonZeroU32::try_from((i + 1 - n_expunge) as u32).unwrap(), ))); n_expunge += 1; } } // - if new mails arrived, notify client of number of existing mails if new_view.known_state.table.len() != self.known_state.table.len() - n_expunge || new_view.known_state.uidvalidity != self.known_state.uidvalidity { data.push(new_view.exists_status()?); } if new_view.known_state.uidvalidity != self.known_state.uidvalidity { // TODO: do we want to push less/more info than this? data.push(new_view.uidvalidity_status()?); data.push(new_view.uidnext_status()?); } else { // - if flags changed for existing mails, tell client for (i, (_uid, uuid)) in new_view.known_state.idx_by_uid.iter().enumerate() { let old_mail = self.known_state.table.get(uuid); let new_mail = new_view.known_state.table.get(uuid); if old_mail.is_some() && old_mail != new_mail { if let Some((uid, flags)) = new_mail { data.push(Body::Data(Data::Fetch { seq_or_uid: NonZeroU32::try_from((i + 1) as u32).unwrap(), attributes: vec![ MessageAttribute::Uid(*uid), MessageAttribute::Flags( flags.iter().filter_map(|f| string_to_flag(f)).collect(), ), ], })); } } } } *self = new_view; Ok(data) } pub async fn store( &mut self, sequence_set: &SequenceSet, kind: &StoreType, _response: &StoreResponse, flags: &[Flag], is_uid_store: &bool, ) -> Result> { self.mailbox.opportunistic_sync().await?; let flags = flags.iter().map(|x| x.to_string()).collect::>(); let mails = self.get_mail_ids(sequence_set, *is_uid_store)?; for (_i, _uid, uuid) in mails.iter() { match kind { StoreType::Add => { self.mailbox.add_flags(*uuid, &flags[..]).await?; } StoreType::Remove => { self.mailbox.del_flags(*uuid, &flags[..]).await?; } StoreType::Replace => { self.mailbox.set_flags(*uuid, &flags[..]).await?; } } } // @TODO: handle _response self.update().await } pub async fn expunge(&mut self) -> Result> { self.mailbox.opportunistic_sync().await?; let deleted_flag = Flag::Deleted.to_string(); let state = self.mailbox.current_uid_index().await; let msgs = state .table .iter() .filter(|(_uuid, (_uid, flags))| flags.iter().any(|x| *x == deleted_flag)) .map(|(uuid, _)| *uuid); for msg in msgs { self.mailbox.delete(msg).await?; } self.update().await } pub async fn copy( &self, sequence_set: &SequenceSet, to: Arc, is_uid_copy: &bool, ) -> Result<(ImapUidvalidity, Vec<(ImapUid, ImapUid)>)> { let mails = self.get_mail_ids(sequence_set, *is_uid_copy)?; let mut new_uuids = vec![]; for (_i, _uid, uuid) in mails.iter() { new_uuids.push(to.copy_from(&self.mailbox, *uuid).await?); } let mut ret = vec![]; let to_state = to.current_uid_index().await; for ((_i, uid, _uuid), new_uuid) in mails.iter().zip(new_uuids.iter()) { let dest_uid = to_state .table .get(new_uuid) .ok_or(anyhow!("copied mail not in destination mailbox"))? .0; ret.push((*uid, dest_uid)); } Ok((to_state.uidvalidity, ret)) } /// Looks up state changes in the mailbox and produces a set of IMAP /// responses describing the new state. pub async fn fetch( &self, sequence_set: &SequenceSet, attributes: &MacroOrFetchAttributes, is_uid_fetch: &bool, ) -> Result> { let mails = self.get_mail_ids(sequence_set, *is_uid_fetch)?; let mails_uuid = mails .iter() .map(|(_i, _uid, uuid)| *uuid) .collect::>(); let mails_meta = self.mailbox.fetch_meta(&mails_uuid).await?; let mut fetch_attrs = match attributes { MacroOrFetchAttributes::Macro(m) => m.expand(), MacroOrFetchAttributes::FetchAttributes(a) => a.clone(), }; if *is_uid_fetch && !fetch_attrs.contains(&FetchAttribute::Uid) { fetch_attrs.push(FetchAttribute::Uid); } let need_body = fetch_attrs.iter().any(|x| { matches!( x, FetchAttribute::Body | FetchAttribute::BodyExt { .. } | FetchAttribute::Rfc822 | FetchAttribute::Rfc822Text | FetchAttribute::BodyStructure ) }); let mails = if need_body { let mut iter = mails .into_iter() .zip(mails_meta.into_iter()) .map(|((i, uid, uuid), meta)| async move { let body = self.mailbox.fetch_full(uuid, &meta.message_key).await?; Ok::<_, anyhow::Error>((i, uid, uuid, meta, Some(body))) }) .collect::>(); let mut mails = vec![]; while let Some(m) = iter.next().await { mails.push(m?); } mails } else { mails .into_iter() .zip(mails_meta.into_iter()) .map(|((i, uid, uuid), meta)| (i, uid, uuid, meta, None)) .collect::>() }; let mut ret = vec![]; for (i, uid, uuid, meta, body) in mails { let mut attributes = vec![]; let (_uid2, flags) = self .known_state .table .get(&uuid) .ok_or_else(|| anyhow!("Mail not in uidindex table: {}", uuid))?; let (parts, imf) = match &body { Some(m) => { let eml = eml_codec::parse_message(m).or(Err(anyhow!("Invalid mail body")))?.1; (Some(eml.child), eml.imf) } None => { let imf = eml_codec::parse_imf(&meta.headers).or(Err(anyhow!("Invalid mail headers")))?.1; (None, imf) } }; for attr in fetch_attrs.iter() { match attr { FetchAttribute::Uid => attributes.push(MessageAttribute::Uid(uid)), FetchAttribute::Flags => { attributes.push(MessageAttribute::Flags( flags.iter().filter_map(|f| string_to_flag(f)).collect(), )); } FetchAttribute::Rfc822Size => { attributes.push(MessageAttribute::Rfc822Size(meta.rfc822_size as u32)) } FetchAttribute::Rfc822Header => { attributes.push(MessageAttribute::Rfc822Header(NString( meta.headers.to_vec().try_into().ok().map(IString::Literal), ))) } FetchAttribute::Rfc822Text => { //@FIXME this is not efficient, this is a hack as we need to patch // eml_codec to correctly implement this behavior let txt = eml_codec::parse_imf(body.as_ref().expect(BODY_CHECK).as_slice()) .map(|(x, _)| x) .unwrap_or(b""); attributes.push(MessageAttribute::Rfc822Text(NString( txt.try_into().ok().map(IString::Literal), ))); } FetchAttribute::Rfc822 => attributes.push(MessageAttribute::Rfc822(NString( body.as_ref() .expect(BODY_CHECK) .clone() .try_into() .ok() .map(IString::Literal), ))), FetchAttribute::Envelope => { attributes.push(MessageAttribute::Envelope(message_envelope(&imf))) } FetchAttribute::Body => attributes.push(MessageAttribute::Body( build_imap_email_struct(parts.as_ref().expect(BODY_CHECK).as_ref())?, )), FetchAttribute::BodyStructure => attributes.push(MessageAttribute::Body( build_imap_email_struct(parts.as_ref().expect(BODY_CHECK).as_ref())?, )), FetchAttribute::BodyExt { section, partial, peek, } => { // @FIXME deactivated while eml_codec is integrated todo!(); // @TODO Add missing section specifiers /*match get_message_section(&parts.expect("body attribute asked but only header is fetched, logic error"), section) { Ok(text) => { let seen_flag = Flag::Seen.to_string(); if !peek && !flags.iter().any(|x| *x == seen_flag) { // Add \Seen flag self.mailbox.add_flags(uuid, &[seen_flag]).await?; } let (text, origin) = match partial { Some((begin, len)) => { if *begin as usize > text.len() { (&[][..], Some(*begin)) } else if (*begin + len.get()) as usize >= text.len() { (&text[*begin as usize..], Some(*begin)) } else { ( &text[*begin as usize ..(*begin + len.get()) as usize], Some(*begin), ) } } None => (&text[..], None), }; let data = NString(text.to_vec().try_into().ok().map(IString::Literal)); attributes.push(MessageAttribute::BodyExt { section: section.clone(), origin, data, }) } Err(e) => { tracing::error!( "Could not get section {:?} of message {}: {}", section, uuid, e ); } } */ } FetchAttribute::InternalDate => { let dt = Utc.fix().timestamp_opt(i64::try_from(meta.internaldate / 1000)?, 0).earliest().ok_or(anyhow!("Unable to parse internal date"))?; attributes.push(MessageAttribute::InternalDate(MyDateTime(dt))); } } } ret.push(Body::Data(Data::Fetch { seq_or_uid: i, attributes, })); } Ok(ret) } // ---- // Gets the UIDs and UUIDs of mails identified by a SequenceSet of // sequence numbers fn get_mail_ids( &self, sequence_set: &SequenceSet, by_uid: bool, ) -> Result> { let mail_vec = self .known_state .idx_by_uid .iter() .map(|(uid, uuid)| (*uid, *uuid)) .collect::>(); let mut mails = vec![]; if by_uid { if mail_vec.is_empty() { return Ok(vec![]); } let iter_strat = sequence::Strategy::Naive { largest: mail_vec.last().unwrap().0, }; let mut i = 0; for uid in sequence_set.iter(iter_strat) { while mail_vec.get(i).map(|mail| mail.0 < uid).unwrap_or(false) { i += 1; } if let Some(mail) = mail_vec.get(i) { if mail.0 == uid { mails.push((NonZeroU32::try_from(i as u32 + 1).unwrap(), mail.0, mail.1)); } } else { break; } } } else { if mail_vec.is_empty() { bail!("No such message (mailbox is empty)"); } let iter_strat = sequence::Strategy::Naive { largest: NonZeroU32::try_from((mail_vec.len()) as u32).unwrap(), }; for i in sequence_set.iter(iter_strat) { if let Some(mail) = mail_vec.get(i.get() as usize - 1) { mails.push((i, mail.0, mail.1)); } else { bail!("No such mail: {}", i); } } } Ok(mails) } // ---- /// Produce an OK [UIDVALIDITY _] message corresponding to `known_state` fn uidvalidity_status(&self) -> Result { let uid_validity = Status::ok( None, Some(Code::UidValidity(self.uidvalidity())), "UIDs valid", ) .map_err(Error::msg)?; Ok(Body::Status(uid_validity)) } pub(crate) fn uidvalidity(&self) -> ImapUidvalidity { self.known_state.uidvalidity } /// Produce an OK [UIDNEXT _] message corresponding to `known_state` fn uidnext_status(&self) -> Result { let next_uid = Status::ok( None, Some(Code::UidNext(self.uidnext())), "Predict next UID", ) .map_err(Error::msg)?; Ok(Body::Status(next_uid)) } pub(crate) fn uidnext(&self) -> ImapUid { self.known_state.uidnext } /// Produce an EXISTS message corresponding to the number of mails /// in `known_state` fn exists_status(&self) -> Result { Ok(Body::Data(Data::Exists(self.exists()?))) } pub(crate) fn exists(&self) -> Result { Ok(u32::try_from(self.known_state.idx_by_uid.len())?) } /// Produce a RECENT message corresponding to the number of /// recent mails in `known_state` fn recent_status(&self) -> Result { Ok(Body::Data(Data::Recent(self.recent()?))) } pub(crate) fn recent(&self) -> Result { let recent = self .known_state .idx_by_flag .get(&"\\Recent".to_string()) .map(|os| os.len()) .unwrap_or(0); Ok(u32::try_from(recent)?) } /// Produce a FLAGS and a PERMANENTFLAGS message that indicates /// the flags that are in `known_state` + default flags fn flags_status(&self) -> Result> { let mut flags: Vec = self .known_state .idx_by_flag .flags() .filter_map(|f| string_to_flag(f)) .collect(); for f in DEFAULT_FLAGS.iter() { if !flags.contains(f) { flags.push(f.clone()); } } let mut ret = vec![Body::Data(Data::Flags(flags.clone()))]; flags.push(Flag::Permanent); let permanent_flags = Status::ok(None, Some(Code::PermanentFlags(flags)), "Flags permitted") .map_err(Error::msg)?; ret.push(Body::Status(permanent_flags)); Ok(ret) } pub(crate) fn unseen_count(&self) -> usize { let total = self.known_state.table.len(); let seen = self .known_state .idx_by_flag .get(&Flag::Seen.to_string()) .map(|x| x.len()) .unwrap_or(0); total - seen } } fn string_to_flag(f: &str) -> Option { match f.chars().next() { Some('\\') => match f { "\\Seen" => Some(Flag::Seen), "\\Answered" => Some(Flag::Answered), "\\Flagged" => Some(Flag::Flagged), "\\Deleted" => Some(Flag::Deleted), "\\Draft" => Some(Flag::Draft), "\\Recent" => Some(Flag::Recent), _ => match Atom::try_from(f.strip_prefix('\\').unwrap().to_string()) { Err(_) => { tracing::error!(flag=%f, "Unable to encode flag as IMAP atom"); None } Ok(a) => Some(Flag::Extension(a)), }, }, Some(_) => match Atom::try_from(f.to_string()) { Err(_) => { tracing::error!(flag=%f, "Unable to encode flag as IMAP atom"); None } Ok(a) => Some(Flag::Keyword(a)), }, None => None, } } /// 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. //@FIXME return an error if the envelope is invalid instead of panicking //@FIXME some fields must be defaulted if there are not set. fn message_envelope(msg: &imf::Imf) -> Envelope { let from = msg.from.iter().map(convert_mbx).collect::>(); 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: 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()), ), } } fn convert_addresses(addrlist: &Vec) -> Vec
{ let mut acc = vec![]; for item in addrlist { match item { imf::address::AddressRef::Single(a) => acc.push(convert_mbx(a)), imf::address::AddressRef::Many(l) => acc.extend(l.participants.iter().map(convert_mbx)) } } return acc } fn convert_mbx(addr: &imf::mailbox::MailboxRef) -> Address { Address::new( NString(addr.name.as_ref().map(|x| IString::try_from(x.to_string()).unwrap())), // SMTP at-domain-list (source route) seems obsolete since at least 1991 // https://www.mhonarc.org/archive/html/ietf-822/1991-06/msg00060.html NString(None), NString(Some(IString::try_from(addr.addrspec.local_part.to_string()).unwrap())), NString(Some(IString::try_from(addr.addrspec.domain.to_string()).unwrap())), ) } /* --CAPTURE-- b fetch 29878:29879 (BODY) * 29878 FETCH (BODY (("text" "plain" ("charset" "utf-8") NIL NIL "quoted-printable" 3264 82)("text" "html" ("charset" "utf-8") NIL NIL "quoted-printable" 31834 643) "alternative")) * 29879 FETCH (BODY ("text" "html" ("charset" "us-ascii") NIL NIL "7bit" 4107 131)) ^^^^^^^^^^^^^^^^^^^^^^ ^^^ ^^^ ^^^^^^ ^^^^ ^^^ | | | | | | number of lines | | | | | size | | | | content transfer encoding | | | description | | id | parameter list b OK Fetch completed (0.001 + 0.000 secs). */ fn build_imap_email_struct<'a>(part: &AnyPart<'a>) -> Result { match part { AnyPart::Mult(x) => { let itype = &x.mime.interpreted_type; let subtype = IString::try_from(itype.subtype.to_string()).unwrap_or(unchecked_istring("alternative")); Ok(BodyStructure::Multi { bodies: x.children .iter() .filter_map(|inner| build_imap_email_struct(&inner).ok()) .collect(), subtype, extension_data: None, /*Some(MultipartExtensionData { parameter_list: vec![], disposition: None, language: None, location: None, extension: vec![], })*/ }) } AnyPart::Txt(x) => { let mut basic = basic_fields(&x.mime.fields, x.body.len())?; // Get the interpreted content type, set it let itype = match &x.mime.interpreted_type { Deductible::Inferred(v) | Deductible::Explicit(v) => v }; let subtype = IString::try_from(itype.subtype.to_string()).unwrap_or(unchecked_istring("plain")); // Add charset to the list of parameters if we know it has been inferred as it will be // missing from the parsed content. if let Deductible::Inferred(charset) = &itype.charset { basic.parameter_list.push(( unchecked_istring("charset"), IString::try_from(charset.to_string()).unwrap_or(unchecked_istring("us-ascii")) )); } Ok(BodyStructure::Single { body: FetchBody { basic, specific: SpecificFields::Text { subtype, number_of_lines: nol(x.body), }, }, extension: None, }) } AnyPart::Bin(x) => { let basic = basic_fields(&x.mime.fields, x.body.len())?; let default = mime::r#type::NaiveType { main: &b"application"[..], sub: &b"octet-stream"[..], params: vec![] }; let ct = x.mime.fields.ctype.as_ref().unwrap_or(&default); let type_ = IString::try_from(String::from_utf8_lossy(ct.main).to_string()) .or(Err(anyhow!("Unable to build IString from given Content-Type type given")))?; let subtype = IString::try_from(String::from_utf8_lossy(ct.sub).to_string()) .or(Err(anyhow!("Unable to build IString from given Content-Type subtype given")))?; Ok(BodyStructure::Single { body: FetchBody { basic, specific: SpecificFields::Basic { type_, subtype }, }, extension: None, }) } AnyPart::Msg(x) => { let basic = basic_fields(&x.mime.fields, x.raw_part.len())?; Ok(BodyStructure::Single { body: FetchBody { basic, specific: SpecificFields::Message { envelope: message_envelope(&x.imf), body_structure: Box::new(build_imap_email_struct(x.child.as_ref())?), number_of_lines: nol(x.raw_part), }, }, extension: None, }) } } } fn nol(input: &[u8]) -> u32 { input.iter() .filter(|x| **x == b'\n') .count() .try_into() .unwrap_or(0) } /// s is set to static to ensure that only compile time values /// checked by developpers are passed. fn unchecked_istring(s: &'static str) -> IString { IString::try_from(s).expect("this value is expected to be a valid imap-codec::IString") } fn basic_fields(m: &mime::NaiveMIME, sz: usize) -> Result { let parameter_list = m.ctype .as_ref() .map(|x| x.params.iter() .map(|p| (IString::try_from(String::from_utf8_lossy(p.name).to_string()), IString::try_from(p.value.to_string()))) .filter(|(k, v)| k.is_ok() && v.is_ok()) .map(|(k, v)| (k.unwrap(), v.unwrap())) .collect()) .unwrap_or(vec![]); Ok(BasicFields { parameter_list, id: NString( m.id.as_ref() .and_then(|ci| IString::try_from(ci.to_string()).ok()), ), description: NString( m.description.as_ref() .and_then(|cd| IString::try_from(cd.to_string()).ok()), ), content_transfer_encoding: match m.transfer_encoding { mime::mechanism::Mechanism::_8Bit => unchecked_istring("8bit"), mime::mechanism::Mechanism::Binary => unchecked_istring("binary"), mime::mechanism::Mechanism::QuotedPrintable => unchecked_istring("quoted-printable"), mime::mechanism::Mechanism::Base64 => unchecked_istring("base64"), _ => unchecked_istring("7bit"), }, // @FIXME we can't compute the size of the message currently... size: u32::try_from(sz)?, }) } /* fn get_message_section<'a>( parsed: &'a Message<'a>, section: &Option, ) -> Result> { match section { Some(FetchSection::Text(None)) => { let rp = parsed.root_part(); Ok(parsed .raw_message .get(rp.offset_body..rp.offset_end) .ok_or(Error::msg( "Unable to extract email body, cursors out of bound. This is a bug.", ))? .into()) } Some(FetchSection::Text(Some(part))) => { map_subpart_msg(parsed, part.0.as_slice(), |part_msg| { let rp = part_msg.root_part(); Ok(part_msg .raw_message .get(rp.offset_body..rp.offset_end) .ok_or(Error::msg( "Unable to extract email body, cursors out of bound. This is a bug.", ))? .to_vec() .into()) }) } Some(FetchSection::Header(part)) => map_subpart_msg( parsed, part.as_ref().map(|p| p.0.as_slice()).unwrap_or(&[]), |part_msg| { let rp = part_msg.root_part(); Ok(part_msg .raw_message .get(..rp.offset_body) .ok_or(Error::msg( "Unable to extract email header, cursors out of bound. This is a bug.", ))? .to_vec() .into()) }, ), Some( FetchSection::HeaderFields(part, fields) | FetchSection::HeaderFieldsNot(part, fields), ) => { let invert = matches!(section, Some(FetchSection::HeaderFieldsNot(_, _))); let fields = fields .iter() .map(|x| match x { AString::Atom(a) => a.as_bytes(), AString::String(IString::Literal(l)) => l.as_slice(), AString::String(IString::Quoted(q)) => q.as_bytes(), }) .collect::>(); map_subpart_msg( parsed, part.as_ref().map(|p| p.0.as_slice()).unwrap_or(&[]), |part_msg| { let mut ret = vec![]; for (hn, hv) in part_msg.headers_raw() { if fields .as_slice() .iter() .any(|x| (*x == hn.as_bytes()) ^ invert) { ret.extend(hn.as_bytes()); ret.extend(b": "); ret.extend(hv.as_bytes()); } } ret.extend(b"\r\n"); Ok(ret.into()) }, ) } Some(FetchSection::Part(part)) => map_subpart(parsed, part.0.as_slice(), |_msg, part| { let bytes = match &part.body { AnyPart::Txt(p) => p.as_bytes().to_vec(), AnyPart::Bin(p) => p.to_vec(), AnyPart::Msg(p) => p.raw_message.to_vec(), AnyPart::Multipart(_) => bail!("Multipart part has no body"), }; Ok(bytes.into()) }), Some(FetchSection::Mime(part)) => map_subpart(parsed, part.0.as_slice(), |msg, part| { let mut ret = vec![]; for head in part.headers.iter() { ret.extend(head.name.as_str().as_bytes()); ret.extend(b": "); ret.extend(&msg.raw_message[head.offset_start..head.offset_end]); } ret.extend(b"\r\n"); Ok(ret.into()) }), None => Ok(parsed.raw_message.clone()), } } fn map_subpart_msg(msg: &Message<'_>, path: &[NonZeroU32], f: F) -> Result where F: FnOnce(&Message<'_>) -> Result, { if path.is_empty() { f(msg) } else { let part = msg .parts .get(path[0].get() as usize - 1) .ok_or(anyhow!("No such subpart: {}", path[0]))?; if let PartType::Message(msg_attach) = &part.body { map_subpart_msg(msg_attach, &path[1..], f) } else { bail!("Subpart is not a message: {}", path[0]); } } } fn map_subpart(msg: &Message<'_>, path: &[NonZeroU32], f: F) -> Result where F: FnOnce(&Message<'_>, &MessagePart<'_>) -> Result, { if path.is_empty() { bail!("Unexpected empty path"); } else { let part = msg .parts .get(path[0].get() as usize - 1) .ok_or(anyhow!("No such subpart: {}", path[0]))?; if path.len() == 1 { f(msg, part) } else if let PartType::Message(msg_attach) = &part.body { map_subpart(msg_attach, &path[1..], f) } else { bail!("Subpart is not a message: {}", path[0]); } } }*/ #[cfg(test)] mod tests { use super::*; use imap_codec::codec::Encode; use std::fs; /// Future automated test. We use lossy utf8 conversion + lowercase everything, /// so this test might allow invalid results. But at least it allows us to quickly test a /// large variety of emails. /// Keep in mind that special cases must still be tested manually! #[test] fn fetch_body() -> Result<()> { let prefixes = [ /* *** MY OWN DATASET *** */ "tests/emails/dxflrs/0001_simple", "tests/emails/dxflrs/0002_mime", "tests/emails/dxflrs/0003_mime-in-mime", "tests/emails/dxflrs/0004_msg-in-msg", // eml_codec do not support continuation for the moment //"tests/emails/dxflrs/0005_mail-parser-readme", "tests/emails/dxflrs/0006_single-mime", "tests/emails/dxflrs/0007_raw_msg_in_rfc822", /* *** (STRANGE) RFC *** */ //"tests/emails/rfc/000", // must return text/enriched, we return text/plain //"tests/emails/rfc/001", // does not recognize the multipart/external-body, breaks the // whole parsing //"tests/emails/rfc/002", // wrong date in email //"tests/emails/rfc/003", // dovecot fixes \r\r: the bytes number is wrong + text/enriched /* *** THIRD PARTY *** */ //"tests/emails/thirdparty/000", // dovecot fixes \r\r: the bytes number is wrong //"tests/emails/thirdparty/001", // same "tests/emails/thirdparty/002", // same /* *** LEGACY *** */ //"tests/emails/legacy/000", // same issue with \r\r ]; for pref in prefixes.iter() { println!("{}", pref); let txt = fs::read(format!("{}.eml", pref))?; let exp = fs::read(format!("{}.dovecot.body", pref))?; let message = eml_codec::parse_message(&txt).unwrap().1; let mut resp = Vec::new(); MessageAttribute::Body(build_imap_email_struct(&message.child)?).encode(&mut resp).unwrap(); let resp_str = String::from_utf8_lossy(&resp).to_lowercase(); let exp_no_parenthesis = &exp[1..exp.len() - 1]; let exp_str = String::from_utf8_lossy(exp_no_parenthesis).to_lowercase(); println!("aerogramme: {}\n\ndovecot: {}\n\n", resp_str, exp_str); //println!("\n\n {} \n\n", String::from_utf8_lossy(&resp)); assert_eq!(resp_str, exp_str); } Ok(()) } }