1use std::collections::HashSet;
5use std::io::Cursor;
6
7use ::pgp::composed::Message;
8use anyhow::Result;
9use mailparse::ParsedMail;
10
11use crate::key::{Fingerprint, SignedPublicKey};
12use crate::pgp;
13
14pub fn get_encrypted_pgp_message<'a>(mail: &'a ParsedMail<'a>) -> Result<Option<Message<'static>>> {
15 let Some(encrypted_data_part) = get_encrypted_mime(mail) else {
16 return Ok(None);
17 };
18 let data = encrypted_data_part.get_body_raw()?;
19 let cursor = Cursor::new(data);
20 let (msg, _headers) = Message::from_armor(cursor)?;
21 Ok(Some(msg))
22}
23
24pub fn get_encrypted_mime<'a, 'b>(mail: &'a ParsedMail<'b>) -> Option<&'a ParsedMail<'b>> {
26 get_autocrypt_mime(mail)
27 .or_else(|| get_mixed_up_mime(mail))
28 .or_else(|| get_attachment_mime(mail))
29}
30
31fn get_mixed_up_mime<'a, 'b>(mail: &'a ParsedMail<'b>) -> Option<&'a ParsedMail<'b>> {
49 if mail.ctype.mimetype != "multipart/mixed" {
50 return None;
51 }
52 if let [first_part, second_part, third_part] = &mail.subparts[..] {
53 if first_part.ctype.mimetype == "text/plain"
54 && second_part.ctype.mimetype == "application/pgp-encrypted"
55 && third_part.ctype.mimetype == "application/octet-stream"
56 {
57 Some(third_part)
58 } else {
59 None
60 }
61 } else {
62 None
63 }
64}
65
66fn get_attachment_mime<'a, 'b>(mail: &'a ParsedMail<'b>) -> Option<&'a ParsedMail<'b>> {
74 if mail.ctype.mimetype != "multipart/mixed" {
75 return None;
76 }
77 if let [first_part, second_part] = &mail.subparts[..] {
78 if first_part.ctype.mimetype == "text/plain"
79 && second_part.ctype.mimetype == "multipart/encrypted"
80 {
81 get_autocrypt_mime(second_part)
82 } else {
83 None
84 }
85 } else {
86 None
87 }
88}
89
90fn get_autocrypt_mime<'a, 'b>(mail: &'a ParsedMail<'b>) -> Option<&'a ParsedMail<'b>> {
94 if mail.ctype.mimetype != "multipart/encrypted" {
95 return None;
96 }
97 if let [first_part, second_part] = &mail.subparts[..] {
98 if first_part.ctype.mimetype == "application/pgp-encrypted"
99 && second_part.ctype.mimetype == "application/octet-stream"
100 {
101 Some(second_part)
102 } else {
103 None
104 }
105 } else {
106 None
107 }
108}
109
110pub(crate) fn validate_detached_signature<'a, 'b>(
117 mail: &'a ParsedMail<'b>,
118 public_keyring_for_validate: &[SignedPublicKey],
119) -> Option<(&'a ParsedMail<'b>, HashSet<Fingerprint>)> {
120 if mail.ctype.mimetype != "multipart/signed" {
121 return None;
122 }
123
124 if let [first_part, second_part] = &mail.subparts[..] {
125 let content = first_part.raw_bytes;
127 let ret_valid_signatures = match second_part.get_body_raw() {
128 Ok(signature) => pgp::pk_validate(content, &signature, public_keyring_for_validate)
129 .unwrap_or_default(),
130 Err(_) => Default::default(),
131 };
132 Some((first_part, ret_valid_signatures))
133 } else {
134 None
135 }
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141 use crate::receive_imf::receive_imf;
142 use crate::test_utils::TestContext;
143
144 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
145 async fn test_mixed_up_mime() -> Result<()> {
146 let mixed_up_mime = include_bytes!("../test-data/message/protonmail-mixed-up.eml");
150 let mail = mailparse::parse_mail(mixed_up_mime)?;
151 assert!(get_autocrypt_mime(&mail).is_none());
152 assert!(get_mixed_up_mime(&mail).is_some());
153 assert!(get_attachment_mime(&mail).is_none());
154
155 let repaired_mime = include_bytes!("../test-data/message/protonmail-repaired.eml");
161 let mail = mailparse::parse_mail(repaired_mime)?;
162 assert!(get_autocrypt_mime(&mail).is_some());
163 assert!(get_mixed_up_mime(&mail).is_none());
164 assert!(get_attachment_mime(&mail).is_none());
165
166 let attachment_mime = include_bytes!("../test-data/message/google-workspace-mixed-up.eml");
169 let mail = mailparse::parse_mail(attachment_mime)?;
170 assert!(get_autocrypt_mime(&mail).is_none());
171 assert!(get_mixed_up_mime(&mail).is_none());
172 assert!(get_attachment_mime(&mail).is_some());
173
174 let bob = TestContext::new_bob().await;
175 receive_imf(&bob, attachment_mime, false).await?;
176 let msg = bob.get_last_msg().await;
177 assert_eq!(msg.text, "Hello, Bob! – Hello from Thunderbird!");
179
180 Ok(())
181 }
182
183 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
184 async fn test_mixed_up_mime_long() -> Result<()> {
185 let mixed_up_mime = include_bytes!("../test-data/message/mixed-up-long.eml");
188 let bob = TestContext::new_bob().await;
189 receive_imf(&bob, mixed_up_mime, false).await?;
190 let msg = bob.get_last_msg().await;
191 assert!(!msg.get_text().is_empty());
192 assert!(msg.has_html());
193 assert!(msg.id.get_html(&bob).await?.unwrap().len() > 40000);
194 Ok(())
195 }
196}