notmuch_more/parse/
body.rs

1use anyhow::anyhow;
2use email::mimeheaders::MimeContentType;
3use email::MimeMultipartType;
4use itertools::Itertools;
5use serde::Serialize;
6
7use crate::NotmuchMoreError;
8
9#[derive(Clone, Debug, Default, Serialize)]
10pub struct EmlBody {
11    pub alternatives: Vec<EmlBody>,
12    pub content: String,
13    pub content_encoded: Option<Vec<u8>>,
14    pub disposition: String,
15    pub extra: Vec<EmlBody>,
16    pub filename: Option<String>,
17    pub is_cleaned_html: bool,
18    pub mimetype: String,
19    pub signature: Option<Box<EmlBody>>,
20    pub size: Option<String>,
21}
22
23pub(crate) fn parse_body_part(part: &mailparse::ParsedMail) -> Result<EmlBody, NotmuchMoreError> {
24    let mimect: MimeContentType = part
25        .ctype
26        .mimetype
27        .split_once('/')
28        .map(|(s1, s2)| (s1.into(), s2.into()))
29        .ok_or_else(|| anyhow!("Failed to parse mimetype: {}", part.ctype.mimetype))?;
30
31    let content_disp = part.get_content_disposition();
32
33    match MimeMultipartType::from_content_type(mimect) {
34        None => match part.ctype.mimetype.as_str() {
35            "text/html" => Ok(EmlBody {
36                content: ammonia::Builder::default()
37                    .set_tag_attribute_value("a", "target", "_blank")
38                    .rm_tag_attributes("img", &["src"])
39                    .clean(&part.get_body()?)
40                    .to_string(),
41                disposition: format!("{:?}", content_disp.disposition),
42                filename: content_disp.params.get("filename").map(|f| f.into()),
43                is_cleaned_html: true,
44                mimetype: part.ctype.mimetype.to_owned(),
45                size: content_disp.params.get("size").map(|f| f.into()),
46                ..Default::default()
47            }),
48            _ => Ok(EmlBody {
49                content: part.get_body()?,
50                content_encoded: Some(part.get_body_raw()?),
51                disposition: format!("{:?}", content_disp.disposition),
52                filename: content_disp.params.get("filename").map(|f| f.into()),
53                mimetype: part.ctype.mimetype.to_owned(),
54                size: content_disp.params.get("size").map(|f| f.into()),
55                ..Default::default()
56            }),
57        },
58
59        Some(MimeMultipartType::Alternative) => {
60            let mut first = parse_body_part(&part.subparts[0])?;
61            first.alternatives = part.subparts[1..]
62                .iter()
63                .map(parse_body_part)
64                .collect::<Result<_, _>>()?;
65            Ok(first)
66        }
67
68        Some(MimeMultipartType::Mixed) => {
69            let mut first = parse_body_part(&part.subparts[0])?;
70            first.extra = part.subparts[1..]
71                .iter()
72                .map(parse_body_part)
73                .collect::<Result<_, _>>()?;
74
75            Ok(first)
76        }
77
78        Some(MimeMultipartType::Signed) => {
79            let mut first = parse_body_part(&part.subparts[0])?;
80            first.signature = Some(Box::new(parse_body_part(
81                part.subparts[1..]
82                    .iter()
83                    .exactly_one()
84                    .map_err(|_| anyhow!("Expected exactly one signature for signed part"))?,
85            )?));
86
87            Ok(first)
88        }
89
90        Some(t) => Err(anyhow!("Not implemented: {:?}", t).into()),
91    }
92}