diff --git a/Cargo.lock b/Cargo.lock index 6cfec14dd..cc2df8217 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1136,6 +1136,7 @@ dependencies = [ "proptest", "qrcodegen", "quick-xml", + "quoted_printable", "rand 0.8.5", "ratelimit", "regex", diff --git a/Cargo.toml b/Cargo.toml index 80ed3cca4..611a61ba7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -76,6 +76,7 @@ pin-project = "1" pretty_env_logger = { version = "0.5", optional = true } qrcodegen = "1.7.0" quick-xml = "0.31" +quoted_printable = "0.4" rand = "0.8" regex = "1.9" reqwest = { version = "0.11.23", features = ["json"] } diff --git a/src/e2ee.rs b/src/e2ee.rs index e0affc5d0..63a5a7030 100644 --- a/src/e2ee.rs +++ b/src/e2ee.rs @@ -52,7 +52,7 @@ impl EncryptHelper { &self, context: &Context, e2ee_guaranteed: bool, - peerstates: &[(Option, &str)], + peerstates: &[(Option, String)], ) -> Result { let mut prefer_encrypt_count = if self.prefer_encrypt == EncryptPreference::Mutual { 1 @@ -94,7 +94,7 @@ impl EncryptHelper { context: &Context, verified: bool, mail_to_encrypt: lettre_email::PartBuilder, - peerstates: Vec<(Option, &str)>, + peerstates: Vec<(Option, String)>, ) -> Result { let mut keyring: Vec = Vec::new(); @@ -117,7 +117,7 @@ impl EncryptHelper { // Encrypt to secondary verified keys // if we also encrypt to the introducer ("verifier") of the key. if verified { - for (peerstate, _addr) in peerstates { + for (peerstate, _addr) in &peerstates { if let Some(peerstate) = peerstate { if let (Some(key), Some(verifier)) = ( peerstate.secondary_verified_key.as_ref(), @@ -293,7 +293,7 @@ Sent with my Delta Chat Messenger: https://delta.chat"; Ok(()) } - fn new_peerstates(prefer_encrypt: EncryptPreference) -> Vec<(Option, &'static str)> { + fn new_peerstates(prefer_encrypt: EncryptPreference) -> Vec<(Option, String)> { let addr = "bob@foo.bar"; let pub_key = bob_keypair().public; let peerstate = Peerstate { @@ -315,7 +315,7 @@ Sent with my Delta Chat Messenger: https://delta.chat"; backward_verified_key_id: None, fingerprint_changed: false, }; - vec![(Some(peerstate), addr)] + vec![(Some(peerstate), addr.to_string())] } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] @@ -340,7 +340,7 @@ Sent with my Delta Chat Messenger: https://delta.chat"; assert!(encrypt_helper.should_encrypt(&t, false, &ps).unwrap()); // test with missing peerstate - let ps = vec![(None, "bob@foo.bar")]; + let ps = vec![(None, "bob@foo.bar".to_string())]; assert!(encrypt_helper.should_encrypt(&t, true, &ps).is_err()); assert!(!encrypt_helper.should_encrypt(&t, false, &ps).unwrap()); } diff --git a/src/mimefactory.rs b/src/mimefactory.rs index 967391d1a..4c4a417c3 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -277,7 +277,7 @@ impl<'a> MimeFactory<'a> { async fn peerstates_for_recipients( &self, context: &Context, - ) -> Result, &str)>> { + ) -> Result, String)>> { let self_addr = context.get_primary_self_addr().await?; let mut res = Vec::new(); @@ -286,7 +286,7 @@ impl<'a> MimeFactory<'a> { .iter() .filter(|(_, addr)| addr != &self_addr) { - res.push((Peerstate::from_addr(context, addr).await?, addr.as_str())); + res.push((Peerstate::from_addr(context, addr).await?, addr.clone())); } Ok(res) @@ -917,6 +917,16 @@ impl<'a> MimeFactory<'a> { Ok(Some(part)) } + fn add_message_text(&self, part: PartBuilder, mut text: String) -> PartBuilder { + // This is needed to protect from ESPs (such as gmx.at) doing their own Quoted-Printable + // encoding and thus breaking messages and signatures. It's unlikely that the reader uses a + // MUA not supporting Quoted-Printable encoding. And RFC 2646 "4.6" also recommends it for + // encrypted messages. + let part = part.header(("Content-Transfer-Encoding", "quoted-printable")); + text = quoted_printable::encode_to_str(text); + part.body(text) + } + #[allow(clippy::cognitive_complexity)] async fn render_message( &mut self, @@ -1214,13 +1224,11 @@ impl<'a> MimeFactory<'a> { footer ); - // Message is sent as text/plain, with charset = utf-8 - let mut main_part = PartBuilder::new() - .header(( - "Content-Type".to_string(), - "text/plain; charset=utf-8; format=flowed; delsp=no".to_string(), - )) - .body(message_text); + let mut main_part = PartBuilder::new().header(( + "Content-Type", + "text/plain; charset=utf-8; format=flowed; delsp=no", + )); + main_part = self.add_message_text(main_part, message_text); if is_reaction { main_part = main_part.header(("Content-Disposition", "reaction")); @@ -1347,15 +1355,12 @@ impl<'a> MimeFactory<'a> { }; let p2 = stock_str::read_rcpt_mail_body(context, &p1).await; let message_text = format!("{}\r\n", format_flowed(&p2)); - message = message.child( - PartBuilder::new() - .header(( - "Content-Type".to_string(), - "text/plain; charset=utf-8; format=flowed; delsp=no".to_string(), - )) - .body(message_text) - .build(), - ); + let text_part = PartBuilder::new().header(( + "Content-Type".to_string(), + "text/plain; charset=utf-8; format=flowed; delsp=no".to_string(), + )); + let text_part = self.add_message_text(text_part, message_text); + message = message.child(text_part.build()); // second body part: machine-readable, always REQUIRED by RFC 6522 let message_text2 = format!( @@ -2198,6 +2203,7 @@ mod tests { assert_eq!(inner.match_indices("Message-ID:").count(), 1); assert_eq!(inner.match_indices("Chat-User-Avatar:").count(), 1); assert_eq!(inner.match_indices("Subject:").count(), 0); + assert_eq!(inner.match_indices("quoted-printable").count(), 1); assert_eq!(body.match_indices("this is the text!").count(), 1); @@ -2218,6 +2224,7 @@ mod tests { assert_eq!(inner.match_indices("Message-ID:").count(), 1); assert_eq!(inner.match_indices("Chat-User-Avatar:").count(), 0); assert_eq!(inner.match_indices("Subject:").count(), 0); + assert_eq!(inner.match_indices("quoted-printable").count(), 1); assert_eq!(body.match_indices("this is the text!").count(), 1); @@ -2274,6 +2281,7 @@ mod tests { assert_eq!(part.match_indices("Message-ID:").count(), 1); assert_eq!(part.match_indices("Chat-User-Avatar:").count(), 1); assert_eq!(part.match_indices("Subject:").count(), 0); + assert_eq!(part.match_indices("quoted-printable").count(), 1); let body = payload.next().unwrap(); assert_eq!(body.match_indices("this is the text!").count(), 1); @@ -2321,6 +2329,7 @@ mod tests { assert_eq!(part.match_indices("Message-ID:").count(), 1); assert_eq!(part.match_indices("Chat-User-Avatar:").count(), 0); assert_eq!(part.match_indices("Subject:").count(), 0); + assert_eq!(part.match_indices("quoted-printable").count(), 1); let body = payload.next().unwrap(); assert_eq!(body.match_indices("this is the text!").count(), 1);