feat: Use Quoted-Printable for the text part (#3986)

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.
This commit is contained in:
iequidoo
2023-04-20 14:16:01 -04:00
committed by iequidoo
parent 1895f4c556
commit 06cccb77f8
4 changed files with 35 additions and 24 deletions

1
Cargo.lock generated
View File

@@ -1136,6 +1136,7 @@ dependencies = [
"proptest", "proptest",
"qrcodegen", "qrcodegen",
"quick-xml", "quick-xml",
"quoted_printable",
"rand 0.8.5", "rand 0.8.5",
"ratelimit", "ratelimit",
"regex", "regex",

View File

@@ -76,6 +76,7 @@ pin-project = "1"
pretty_env_logger = { version = "0.5", optional = true } pretty_env_logger = { version = "0.5", optional = true }
qrcodegen = "1.7.0" qrcodegen = "1.7.0"
quick-xml = "0.31" quick-xml = "0.31"
quoted_printable = "0.4"
rand = "0.8" rand = "0.8"
regex = "1.9" regex = "1.9"
reqwest = { version = "0.11.23", features = ["json"] } reqwest = { version = "0.11.23", features = ["json"] }

View File

@@ -52,7 +52,7 @@ impl EncryptHelper {
&self, &self,
context: &Context, context: &Context,
e2ee_guaranteed: bool, e2ee_guaranteed: bool,
peerstates: &[(Option<Peerstate>, &str)], peerstates: &[(Option<Peerstate>, String)],
) -> Result<bool> { ) -> Result<bool> {
let mut prefer_encrypt_count = if self.prefer_encrypt == EncryptPreference::Mutual { let mut prefer_encrypt_count = if self.prefer_encrypt == EncryptPreference::Mutual {
1 1
@@ -94,7 +94,7 @@ impl EncryptHelper {
context: &Context, context: &Context,
verified: bool, verified: bool,
mail_to_encrypt: lettre_email::PartBuilder, mail_to_encrypt: lettre_email::PartBuilder,
peerstates: Vec<(Option<Peerstate>, &str)>, peerstates: Vec<(Option<Peerstate>, String)>,
) -> Result<String> { ) -> Result<String> {
let mut keyring: Vec<SignedPublicKey> = Vec::new(); let mut keyring: Vec<SignedPublicKey> = Vec::new();
@@ -117,7 +117,7 @@ impl EncryptHelper {
// Encrypt to secondary verified keys // Encrypt to secondary verified keys
// if we also encrypt to the introducer ("verifier") of the key. // if we also encrypt to the introducer ("verifier") of the key.
if verified { if verified {
for (peerstate, _addr) in peerstates { for (peerstate, _addr) in &peerstates {
if let Some(peerstate) = peerstate { if let Some(peerstate) = peerstate {
if let (Some(key), Some(verifier)) = ( if let (Some(key), Some(verifier)) = (
peerstate.secondary_verified_key.as_ref(), peerstate.secondary_verified_key.as_ref(),
@@ -293,7 +293,7 @@ Sent with my Delta Chat Messenger: https://delta.chat";
Ok(()) Ok(())
} }
fn new_peerstates(prefer_encrypt: EncryptPreference) -> Vec<(Option<Peerstate>, &'static str)> { fn new_peerstates(prefer_encrypt: EncryptPreference) -> Vec<(Option<Peerstate>, String)> {
let addr = "bob@foo.bar"; let addr = "bob@foo.bar";
let pub_key = bob_keypair().public; let pub_key = bob_keypair().public;
let peerstate = Peerstate { let peerstate = Peerstate {
@@ -315,7 +315,7 @@ Sent with my Delta Chat Messenger: https://delta.chat";
backward_verified_key_id: None, backward_verified_key_id: None,
fingerprint_changed: false, fingerprint_changed: false,
}; };
vec![(Some(peerstate), addr)] vec![(Some(peerstate), addr.to_string())]
} }
#[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[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()); assert!(encrypt_helper.should_encrypt(&t, false, &ps).unwrap());
// test with missing peerstate // 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, true, &ps).is_err());
assert!(!encrypt_helper.should_encrypt(&t, false, &ps).unwrap()); assert!(!encrypt_helper.should_encrypt(&t, false, &ps).unwrap());
} }

View File

@@ -277,7 +277,7 @@ impl<'a> MimeFactory<'a> {
async fn peerstates_for_recipients( async fn peerstates_for_recipients(
&self, &self,
context: &Context, context: &Context,
) -> Result<Vec<(Option<Peerstate>, &str)>> { ) -> Result<Vec<(Option<Peerstate>, String)>> {
let self_addr = context.get_primary_self_addr().await?; let self_addr = context.get_primary_self_addr().await?;
let mut res = Vec::new(); let mut res = Vec::new();
@@ -286,7 +286,7 @@ impl<'a> MimeFactory<'a> {
.iter() .iter()
.filter(|(_, addr)| addr != &self_addr) .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) Ok(res)
@@ -917,6 +917,16 @@ impl<'a> MimeFactory<'a> {
Ok(Some(part)) 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)] #[allow(clippy::cognitive_complexity)]
async fn render_message( async fn render_message(
&mut self, &mut self,
@@ -1214,13 +1224,11 @@ impl<'a> MimeFactory<'a> {
footer footer
); );
// Message is sent as text/plain, with charset = utf-8 let mut main_part = PartBuilder::new().header((
let mut main_part = PartBuilder::new() "Content-Type",
.header(( "text/plain; charset=utf-8; format=flowed; delsp=no",
"Content-Type".to_string(), ));
"text/plain; charset=utf-8; format=flowed; delsp=no".to_string(), main_part = self.add_message_text(main_part, message_text);
))
.body(message_text);
if is_reaction { if is_reaction {
main_part = main_part.header(("Content-Disposition", "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 p2 = stock_str::read_rcpt_mail_body(context, &p1).await;
let message_text = format!("{}\r\n", format_flowed(&p2)); let message_text = format!("{}\r\n", format_flowed(&p2));
message = message.child( let text_part = PartBuilder::new().header((
PartBuilder::new() "Content-Type".to_string(),
.header(( "text/plain; charset=utf-8; format=flowed; delsp=no".to_string(),
"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());
.body(message_text)
.build(),
);
// second body part: machine-readable, always REQUIRED by RFC 6522 // second body part: machine-readable, always REQUIRED by RFC 6522
let message_text2 = format!( let message_text2 = format!(
@@ -2198,6 +2203,7 @@ mod tests {
assert_eq!(inner.match_indices("Message-ID:").count(), 1); assert_eq!(inner.match_indices("Message-ID:").count(), 1);
assert_eq!(inner.match_indices("Chat-User-Avatar:").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("Subject:").count(), 0);
assert_eq!(inner.match_indices("quoted-printable").count(), 1);
assert_eq!(body.match_indices("this is the text!").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("Message-ID:").count(), 1);
assert_eq!(inner.match_indices("Chat-User-Avatar:").count(), 0); assert_eq!(inner.match_indices("Chat-User-Avatar:").count(), 0);
assert_eq!(inner.match_indices("Subject:").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); 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("Message-ID:").count(), 1);
assert_eq!(part.match_indices("Chat-User-Avatar:").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("Subject:").count(), 0);
assert_eq!(part.match_indices("quoted-printable").count(), 1);
let body = payload.next().unwrap(); let body = payload.next().unwrap();
assert_eq!(body.match_indices("this is the text!").count(), 1); 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("Message-ID:").count(), 1);
assert_eq!(part.match_indices("Chat-User-Avatar:").count(), 0); assert_eq!(part.match_indices("Chat-User-Avatar:").count(), 0);
assert_eq!(part.match_indices("Subject:").count(), 0); assert_eq!(part.match_indices("Subject:").count(), 0);
assert_eq!(part.match_indices("quoted-printable").count(), 1);
let body = payload.next().unwrap(); let body = payload.next().unwrap();
assert_eq!(body.match_indices("this is the text!").count(), 1); assert_eq!(body.match_indices("this is the text!").count(), 1);