use anyhow::{anyhow, Context, Result}; use himalaya_lib::account::AccountConfig; use mailparse::MailHeaderMap; use serde::Serialize; use std::{ env, fs, ops::{Deref, DerefMut}, }; use uuid::Uuid; #[derive(Debug, Clone, Default, Serialize)] pub struct TextPlainPart { pub content: String, } #[derive(Debug, Clone, Default, Serialize)] pub struct TextHtmlPart { pub content: String, } #[derive(Debug, Clone, Default, Serialize)] pub struct BinaryPart { pub filename: String, pub mime: String, pub content: Vec, } #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub enum Part { TextPlain(TextPlainPart), TextHtml(TextHtmlPart), Binary(BinaryPart), } impl Part { pub fn new_text_plain(content: String) -> Self { Self::TextPlain(TextPlainPart { content }) } } #[derive(Debug, Clone, Default, Serialize)] #[serde(rename_all = "camelCase")] pub struct Parts(pub Vec); impl Parts { pub fn replace_text_plain_parts_with(&mut self, part: TextPlainPart) { self.retain(|part| !matches!(part, Part::TextPlain(_))); self.push(Part::TextPlain(part)); } pub fn from_parsed_mail<'a>( account: &'a AccountConfig, part: &'a mailparse::ParsedMail<'a>, ) -> Result { let mut parts = vec![]; if part.subparts.is_empty() && part.get_headers().get_first_value("content-type").is_none() { let content = part.get_body().unwrap_or_default(); parts.push(Part::TextPlain(TextPlainPart { content })) } else { build_parts_map_rec(account, part, &mut parts)?; } Ok(Self(parts)) } } impl Deref for Parts { type Target = Vec; fn deref(&self) -> &Self::Target { &self.0 } } impl DerefMut for Parts { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } } fn build_parts_map_rec( account: &AccountConfig, parsed_mail: &mailparse::ParsedMail, parts: &mut Vec, ) -> Result<()> { if parsed_mail.subparts.is_empty() { let cdisp = parsed_mail.get_content_disposition(); match cdisp.disposition { mailparse::DispositionType::Attachment => { let filename = cdisp .params .get("filename") .map(String::from) .unwrap_or_else(|| String::from("noname")); let content = parsed_mail.get_body_raw().unwrap_or_default(); let mime = tree_magic::from_u8(&content); parts.push(Part::Binary(BinaryPart { filename, mime, content, })); } // TODO: manage other use cases _ => { if let Some(ctype) = parsed_mail.get_headers().get_first_value("content-type") { let content = parsed_mail.get_body().unwrap_or_default(); if ctype.starts_with("text/plain") { parts.push(Part::TextPlain(TextPlainPart { content })) } else if ctype.starts_with("text/html") { parts.push(Part::TextHtml(TextHtmlPart { content })) } } } }; } else { let ctype = parsed_mail .get_headers() .get_first_value("content-type") .ok_or_else(|| anyhow!("cannot get content type of multipart"))?; if ctype.starts_with("multipart/encrypted") { let decrypted_part = parsed_mail .subparts .get(1) .ok_or_else(|| anyhow!("cannot find encrypted part of multipart")) .and_then(|part| decrypt_part(account, part)) .context("cannot decrypt part of multipart")?; let parsed_mail = mailparse::parse_mail(decrypted_part.as_bytes()) .context("cannot parse decrypted part of multipart")?; build_parts_map_rec(account, &parsed_mail, parts)?; } else { for part in parsed_mail.subparts.iter() { build_parts_map_rec(account, part, parts)?; } } } Ok(()) } fn decrypt_part(account: &AccountConfig, msg: &mailparse::ParsedMail) -> Result { let msg_path = env::temp_dir().join(Uuid::new_v4().to_string()); let msg_body = msg .get_body() .context("cannot get body from encrypted part")?; fs::write(msg_path.clone(), &msg_body) .context(format!("cannot write encrypted part to temporary file"))?; account .pgp_decrypt_file(msg_path.clone())? .ok_or_else(|| anyhow!("cannot find pgp decrypt command in config")) }