| | |
| |
|
| | use std::collections::HashSet; |
| |
|
| | use anyhow::Result; |
| | use mailparse::ParsedMail; |
| |
|
| | use crate::key::{Fingerprint, SignedPublicKey, SignedSecretKey}; |
| | use crate::pgp; |
| |
|
| | |
| | |
| | |
| | |
| | pub fn try_decrypt<'a>( |
| | mail: &'a ParsedMail<'a>, |
| | private_keyring: &'a [SignedSecretKey], |
| | shared_secrets: &[String], |
| | ) -> Result<Option<::pgp::composed::Message<'static>>> { |
| | let Some(encrypted_data_part) = get_encrypted_mime(mail) else { |
| | return Ok(None); |
| | }; |
| |
|
| | let data = encrypted_data_part.get_body_raw()?; |
| | let msg = pgp::decrypt(data, private_keyring, shared_secrets)?; |
| |
|
| | Ok(Some(msg)) |
| | } |
| |
|
| | |
| | pub(crate) fn get_encrypted_mime<'a, 'b>(mail: &'a ParsedMail<'b>) -> Option<&'a ParsedMail<'b>> { |
| | get_autocrypt_mime(mail) |
| | .or_else(|| get_mixed_up_mime(mail)) |
| | .or_else(|| get_attachment_mime(mail)) |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | fn get_mixed_up_mime<'a, 'b>(mail: &'a ParsedMail<'b>) -> Option<&'a ParsedMail<'b>> { |
| | if mail.ctype.mimetype != "multipart/mixed" { |
| | return None; |
| | } |
| | if let [first_part, second_part, third_part] = &mail.subparts[..] { |
| | if first_part.ctype.mimetype == "text/plain" |
| | && second_part.ctype.mimetype == "application/pgp-encrypted" |
| | && third_part.ctype.mimetype == "application/octet-stream" |
| | { |
| | Some(third_part) |
| | } else { |
| | None |
| | } |
| | } else { |
| | None |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | fn get_attachment_mime<'a, 'b>(mail: &'a ParsedMail<'b>) -> Option<&'a ParsedMail<'b>> { |
| | if mail.ctype.mimetype != "multipart/mixed" { |
| | return None; |
| | } |
| | if let [first_part, second_part] = &mail.subparts[..] { |
| | if first_part.ctype.mimetype == "text/plain" |
| | && second_part.ctype.mimetype == "multipart/encrypted" |
| | { |
| | get_autocrypt_mime(second_part) |
| | } else { |
| | None |
| | } |
| | } else { |
| | None |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| | fn get_autocrypt_mime<'a, 'b>(mail: &'a ParsedMail<'b>) -> Option<&'a ParsedMail<'b>> { |
| | if mail.ctype.mimetype != "multipart/encrypted" { |
| | return None; |
| | } |
| | if let [first_part, second_part] = &mail.subparts[..] { |
| | if first_part.ctype.mimetype == "application/pgp-encrypted" |
| | && second_part.ctype.mimetype == "application/octet-stream" |
| | { |
| | Some(second_part) |
| | } else { |
| | None |
| | } |
| | } else { |
| | None |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | pub(crate) fn validate_detached_signature<'a, 'b>( |
| | mail: &'a ParsedMail<'b>, |
| | public_keyring_for_validate: &[SignedPublicKey], |
| | ) -> Option<(&'a ParsedMail<'b>, HashSet<Fingerprint>)> { |
| | if mail.ctype.mimetype != "multipart/signed" { |
| | return None; |
| | } |
| |
|
| | if let [first_part, second_part] = &mail.subparts[..] { |
| | |
| | let content = first_part.raw_bytes; |
| | let ret_valid_signatures = match second_part.get_body_raw() { |
| | Ok(signature) => pgp::pk_validate(content, &signature, public_keyring_for_validate) |
| | .unwrap_or_default(), |
| | Err(_) => Default::default(), |
| | }; |
| | Some((first_part, ret_valid_signatures)) |
| | } else { |
| | None |
| | } |
| | } |
| |
|
| | #[cfg(test)] |
| | mod tests { |
| | use super::*; |
| | use crate::receive_imf::receive_imf; |
| | use crate::test_utils::TestContext; |
| |
|
| | #[tokio::test(flavor = "multi_thread", worker_threads = 2)] |
| | async fn test_mixed_up_mime() -> Result<()> { |
| | |
| | |
| | |
| | let mixed_up_mime = include_bytes!("../test-data/message/protonmail-mixed-up.eml"); |
| | let mail = mailparse::parse_mail(mixed_up_mime)?; |
| | assert!(get_autocrypt_mime(&mail).is_none()); |
| | assert!(get_mixed_up_mime(&mail).is_some()); |
| | assert!(get_attachment_mime(&mail).is_none()); |
| |
|
| | |
| | |
| | |
| | |
| | |
| | let repaired_mime = include_bytes!("../test-data/message/protonmail-repaired.eml"); |
| | let mail = mailparse::parse_mail(repaired_mime)?; |
| | assert!(get_autocrypt_mime(&mail).is_some()); |
| | assert!(get_mixed_up_mime(&mail).is_none()); |
| | assert!(get_attachment_mime(&mail).is_none()); |
| |
|
| | |
| | |
| | let attachment_mime = include_bytes!("../test-data/message/google-workspace-mixed-up.eml"); |
| | let mail = mailparse::parse_mail(attachment_mime)?; |
| | assert!(get_autocrypt_mime(&mail).is_none()); |
| | assert!(get_mixed_up_mime(&mail).is_none()); |
| | assert!(get_attachment_mime(&mail).is_some()); |
| |
|
| | let bob = TestContext::new_bob().await; |
| | receive_imf(&bob, attachment_mime, false).await?; |
| | let msg = bob.get_last_msg().await; |
| | |
| | assert_eq!(msg.text, "Hello, Bob! – Hello from Thunderbird!"); |
| |
|
| | Ok(()) |
| | } |
| |
|
| | #[tokio::test(flavor = "multi_thread", worker_threads = 2)] |
| | async fn test_mixed_up_mime_long() -> Result<()> { |
| | |
| | |
| | let mixed_up_mime = include_bytes!("../test-data/message/mixed-up-long.eml"); |
| | let bob = TestContext::new_bob().await; |
| | receive_imf(&bob, mixed_up_mime, false).await?; |
| | let msg = bob.get_last_msg().await; |
| | assert!(!msg.get_text().is_empty()); |
| | assert!(msg.has_html()); |
| | assert!(msg.id.get_html(&bob).await?.unwrap().len() > 40000); |
| | Ok(()) |
| | } |
| | } |
| |
|