Skip to content

Commit 06cccb7

Browse files
committed
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.
1 parent 1895f4c commit 06cccb7

File tree

4 files changed

+35
-24
lines changed

4 files changed

+35
-24
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ pin-project = "1"
7676
pretty_env_logger = { version = "0.5", optional = true }
7777
qrcodegen = "1.7.0"
7878
quick-xml = "0.31"
79+
quoted_printable = "0.4"
7980
rand = "0.8"
8081
regex = "1.9"
8182
reqwest = { version = "0.11.23", features = ["json"] }

src/e2ee.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ impl EncryptHelper {
5252
&self,
5353
context: &Context,
5454
e2ee_guaranteed: bool,
55-
peerstates: &[(Option<Peerstate>, &str)],
55+
peerstates: &[(Option<Peerstate>, String)],
5656
) -> Result<bool> {
5757
let mut prefer_encrypt_count = if self.prefer_encrypt == EncryptPreference::Mutual {
5858
1
@@ -94,7 +94,7 @@ impl EncryptHelper {
9494
context: &Context,
9595
verified: bool,
9696
mail_to_encrypt: lettre_email::PartBuilder,
97-
peerstates: Vec<(Option<Peerstate>, &str)>,
97+
peerstates: Vec<(Option<Peerstate>, String)>,
9898
) -> Result<String> {
9999
let mut keyring: Vec<SignedPublicKey> = Vec::new();
100100

@@ -117,7 +117,7 @@ impl EncryptHelper {
117117
// Encrypt to secondary verified keys
118118
// if we also encrypt to the introducer ("verifier") of the key.
119119
if verified {
120-
for (peerstate, _addr) in peerstates {
120+
for (peerstate, _addr) in &peerstates {
121121
if let Some(peerstate) = peerstate {
122122
if let (Some(key), Some(verifier)) = (
123123
peerstate.secondary_verified_key.as_ref(),
@@ -293,7 +293,7 @@ Sent with my Delta Chat Messenger: https://delta.chat";
293293
Ok(())
294294
}
295295

296-
fn new_peerstates(prefer_encrypt: EncryptPreference) -> Vec<(Option<Peerstate>, &'static str)> {
296+
fn new_peerstates(prefer_encrypt: EncryptPreference) -> Vec<(Option<Peerstate>, String)> {
297297
let addr = "bob@foo.bar";
298298
let pub_key = bob_keypair().public;
299299
let peerstate = Peerstate {
@@ -315,7 +315,7 @@ Sent with my Delta Chat Messenger: https://delta.chat";
315315
backward_verified_key_id: None,
316316
fingerprint_changed: false,
317317
};
318-
vec![(Some(peerstate), addr)]
318+
vec![(Some(peerstate), addr.to_string())]
319319
}
320320

321321
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
@@ -340,7 +340,7 @@ Sent with my Delta Chat Messenger: https://delta.chat";
340340
assert!(encrypt_helper.should_encrypt(&t, false, &ps).unwrap());
341341

342342
// test with missing peerstate
343-
let ps = vec![(None, "bob@foo.bar")];
343+
let ps = vec![(None, "bob@foo.bar".to_string())];
344344
assert!(encrypt_helper.should_encrypt(&t, true, &ps).is_err());
345345
assert!(!encrypt_helper.should_encrypt(&t, false, &ps).unwrap());
346346
}

src/mimefactory.rs

Lines changed: 27 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,7 @@ impl<'a> MimeFactory<'a> {
277277
async fn peerstates_for_recipients(
278278
&self,
279279
context: &Context,
280-
) -> Result<Vec<(Option<Peerstate>, &str)>> {
280+
) -> Result<Vec<(Option<Peerstate>, String)>> {
281281
let self_addr = context.get_primary_self_addr().await?;
282282

283283
let mut res = Vec::new();
@@ -286,7 +286,7 @@ impl<'a> MimeFactory<'a> {
286286
.iter()
287287
.filter(|(_, addr)| addr != &self_addr)
288288
{
289-
res.push((Peerstate::from_addr(context, addr).await?, addr.as_str()));
289+
res.push((Peerstate::from_addr(context, addr).await?, addr.clone()));
290290
}
291291

292292
Ok(res)
@@ -917,6 +917,16 @@ impl<'a> MimeFactory<'a> {
917917
Ok(Some(part))
918918
}
919919

920+
fn add_message_text(&self, part: PartBuilder, mut text: String) -> PartBuilder {
921+
// This is needed to protect from ESPs (such as gmx.at) doing their own Quoted-Printable
922+
// encoding and thus breaking messages and signatures. It's unlikely that the reader uses a
923+
// MUA not supporting Quoted-Printable encoding. And RFC 2646 "4.6" also recommends it for
924+
// encrypted messages.
925+
let part = part.header(("Content-Transfer-Encoding", "quoted-printable"));
926+
text = quoted_printable::encode_to_str(text);
927+
part.body(text)
928+
}
929+
920930
#[allow(clippy::cognitive_complexity)]
921931
async fn render_message(
922932
&mut self,
@@ -1214,13 +1224,11 @@ impl<'a> MimeFactory<'a> {
12141224
footer
12151225
);
12161226

1217-
// Message is sent as text/plain, with charset = utf-8
1218-
let mut main_part = PartBuilder::new()
1219-
.header((
1220-
"Content-Type".to_string(),
1221-
"text/plain; charset=utf-8; format=flowed; delsp=no".to_string(),
1222-
))
1223-
.body(message_text);
1227+
let mut main_part = PartBuilder::new().header((
1228+
"Content-Type",
1229+
"text/plain; charset=utf-8; format=flowed; delsp=no",
1230+
));
1231+
main_part = self.add_message_text(main_part, message_text);
12241232

12251233
if is_reaction {
12261234
main_part = main_part.header(("Content-Disposition", "reaction"));
@@ -1347,15 +1355,12 @@ impl<'a> MimeFactory<'a> {
13471355
};
13481356
let p2 = stock_str::read_rcpt_mail_body(context, &p1).await;
13491357
let message_text = format!("{}\r\n", format_flowed(&p2));
1350-
message = message.child(
1351-
PartBuilder::new()
1352-
.header((
1353-
"Content-Type".to_string(),
1354-
"text/plain; charset=utf-8; format=flowed; delsp=no".to_string(),
1355-
))
1356-
.body(message_text)
1357-
.build(),
1358-
);
1358+
let text_part = PartBuilder::new().header((
1359+
"Content-Type".to_string(),
1360+
"text/plain; charset=utf-8; format=flowed; delsp=no".to_string(),
1361+
));
1362+
let text_part = self.add_message_text(text_part, message_text);
1363+
message = message.child(text_part.build());
13591364

13601365
// second body part: machine-readable, always REQUIRED by RFC 6522
13611366
let message_text2 = format!(
@@ -2198,6 +2203,7 @@ mod tests {
21982203
assert_eq!(inner.match_indices("Message-ID:").count(), 1);
21992204
assert_eq!(inner.match_indices("Chat-User-Avatar:").count(), 1);
22002205
assert_eq!(inner.match_indices("Subject:").count(), 0);
2206+
assert_eq!(inner.match_indices("quoted-printable").count(), 1);
22012207

22022208
assert_eq!(body.match_indices("this is the text!").count(), 1);
22032209

@@ -2218,6 +2224,7 @@ mod tests {
22182224
assert_eq!(inner.match_indices("Message-ID:").count(), 1);
22192225
assert_eq!(inner.match_indices("Chat-User-Avatar:").count(), 0);
22202226
assert_eq!(inner.match_indices("Subject:").count(), 0);
2227+
assert_eq!(inner.match_indices("quoted-printable").count(), 1);
22212228

22222229
assert_eq!(body.match_indices("this is the text!").count(), 1);
22232230

@@ -2274,6 +2281,7 @@ mod tests {
22742281
assert_eq!(part.match_indices("Message-ID:").count(), 1);
22752282
assert_eq!(part.match_indices("Chat-User-Avatar:").count(), 1);
22762283
assert_eq!(part.match_indices("Subject:").count(), 0);
2284+
assert_eq!(part.match_indices("quoted-printable").count(), 1);
22772285

22782286
let body = payload.next().unwrap();
22792287
assert_eq!(body.match_indices("this is the text!").count(), 1);
@@ -2321,6 +2329,7 @@ mod tests {
23212329
assert_eq!(part.match_indices("Message-ID:").count(), 1);
23222330
assert_eq!(part.match_indices("Chat-User-Avatar:").count(), 0);
23232331
assert_eq!(part.match_indices("Subject:").count(), 0);
2332+
assert_eq!(part.match_indices("quoted-printable").count(), 1);
23242333

23252334
let body = payload.next().unwrap();
23262335
assert_eq!(body.match_indices("this is the text!").count(), 1);

0 commit comments

Comments
 (0)