Skip to content

Commit 0ded917

Browse files
committed
feat: mimeparser: Omit Legacy Display Elements from text/plain (#7130)
Implement 4.5.3.2 of https://www.rfc-editor.org/rfc/rfc9788 "Header Protection for Cryptographically Protected Email".
1 parent d2e3848 commit 0ded917

File tree

6 files changed

+93
-9
lines changed

6 files changed

+93
-9
lines changed

src/config.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -438,6 +438,12 @@ pub enum Config {
438438
/// storing the same token multiple times on the server.
439439
EncryptedDeviceToken,
440440

441+
/// Enables running test hooks, e.g. see
442+
/// [`crate::context::InnerContext::pre_encrypt_mime_hook`].
443+
/// This way is better than conditional compilation, i.e. `#[cfg(test)]`, because tests not
444+
/// using this still run unmodified code.
445+
TestHooks,
446+
441447
/// Return an error from `receive_imf_inner()` for a fully downloaded message. For tests.
442448
FailOnReceivingFullMsg,
443449
}

src/context.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,17 @@ pub struct InnerContext {
303303
/// `Connectivity` values for mailboxes, unordered. Used to compute the aggregate connectivity,
304304
/// see [`Context::get_connectivity()`].
305305
pub(crate) connectivities: parking_lot::Mutex<Vec<ConnectivityStore>>,
306+
307+
#[expect(clippy::type_complexity)]
308+
/// Transforms the root of the cryptographic payload before encryption.
309+
pub(crate) pre_encrypt_mime_hook: parking_lot::Mutex<
310+
Option<
311+
for<'a> fn(
312+
&Context,
313+
mail_builder::mime::MimePart<'a>,
314+
) -> mail_builder::mime::MimePart<'a>,
315+
>,
316+
>,
306317
}
307318

308319
/// The state of ongoing process.
@@ -467,6 +478,7 @@ impl Context {
467478
iroh: Arc::new(RwLock::new(None)),
468479
self_fingerprint: OnceLock::new(),
469480
connectivities: parking_lot::Mutex::new(Vec::new()),
481+
pre_encrypt_mime_hook: None.into(),
470482
};
471483

472484
let ctx = Context {

src/mimefactory.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1232,6 +1232,12 @@ impl MimeFactory {
12321232
// once new core versions are sufficiently deployed.
12331233
let anonymous_recipients = false;
12341234

1235+
if context.get_config_bool(Config::TestHooks).await? {
1236+
if let Some(hook) = &*context.pre_encrypt_mime_hook.lock() {
1237+
message = hook(context, message);
1238+
}
1239+
}
1240+
12351241
let encrypted = if let Some(shared_secret) = shared_secret {
12361242
encrypt_helper
12371243
.encrypt_symmetrically(context, &shared_secret, message, compress)

src/mimeparser.rs

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1350,16 +1350,30 @@ impl MimeMessage {
13501350

13511351
let (simplified_txt, simplified_quote) = if mime_type.type_() == mime::TEXT
13521352
&& mime_type.subtype() == mime::PLAIN
1353-
&& is_format_flowed
13541353
{
1355-
let delsp = if let Some(delsp) = mail.ctype.params.get("delsp") {
1356-
delsp.as_str().eq_ignore_ascii_case("yes")
1357-
} else {
1358-
false
1354+
// Don't check that we're inside an encrypted or signed part for
1355+
// simplicity.
1356+
let simplified_txt = match mail
1357+
.ctype
1358+
.params
1359+
.get("hp-legacy-display")
1360+
.is_some_and(|v| v == "1")
1361+
{
1362+
false => simplified_txt,
1363+
true => rm_legacy_display_elements(&simplified_txt),
13591364
};
1360-
let unflowed_text = unformat_flowed(&simplified_txt, delsp);
1361-
let unflowed_quote = top_quote.map(|q| unformat_flowed(&q, delsp));
1362-
(unflowed_text, unflowed_quote)
1365+
if is_format_flowed {
1366+
let delsp = if let Some(delsp) = mail.ctype.params.get("delsp") {
1367+
delsp.as_str().eq_ignore_ascii_case("yes")
1368+
} else {
1369+
false
1370+
};
1371+
let unflowed_text = unformat_flowed(&simplified_txt, delsp);
1372+
let unflowed_quote = top_quote.map(|q| unformat_flowed(&q, delsp));
1373+
(unflowed_text, unflowed_quote)
1374+
} else {
1375+
(simplified_txt, top_quote)
1376+
}
13631377
} else {
13641378
(simplified_txt, top_quote)
13651379
};
@@ -1981,6 +1995,20 @@ impl MimeMessage {
19811995
}
19821996
}
19831997

1998+
fn rm_legacy_display_elements(text: &str) -> String {
1999+
let mut res = None;
2000+
for l in text.lines() {
2001+
res = res.map(|r: String| match r.is_empty() {
2002+
true => l.to_string(),
2003+
false => r + "\r\n" + l,
2004+
});
2005+
if l.is_empty() {
2006+
res = Some(String::new());
2007+
}
2008+
}
2009+
res.unwrap_or_default()
2010+
}
2011+
19842012
fn remove_header(
19852013
headers: &mut HashMap<String, String>,
19862014
key: &str,

src/mimeparser/mimeparser_tests.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1751,6 +1751,39 @@ async fn test_time_in_future() -> Result<()> {
17511751
Ok(())
17521752
}
17531753

1754+
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1755+
async fn test_hp_legacy_display() -> Result<()> {
1756+
let mut tcm = TestContextManager::new();
1757+
let alice = &tcm.alice().await;
1758+
let bob = &tcm.bob().await;
1759+
1760+
let mut msg = Message::new_text(
1761+
"Subject: Dinner plans\n\
1762+
\n\
1763+
Let's eat"
1764+
.to_string(),
1765+
);
1766+
msg.set_subject("Dinner plans".to_string());
1767+
let chat_id = alice.create_chat(bob).await.id;
1768+
alice.set_config_bool(Config::TestHooks, true).await?;
1769+
*alice.pre_encrypt_mime_hook.lock() = Some(|_, mut mime| {
1770+
for (h, v) in &mut mime.headers {
1771+
if h == "Content-Type" {
1772+
if let mail_builder::headers::HeaderType::ContentType(ct) = v {
1773+
*ct = ct.clone().attribute("hp-legacy-display", "1");
1774+
}
1775+
}
1776+
}
1777+
mime
1778+
});
1779+
let sent_msg = alice.send_msg(chat_id, &mut msg).await;
1780+
1781+
let msg_bob = bob.recv_msg(&sent_msg).await;
1782+
assert_eq!(msg_bob.subject, "Dinner plans");
1783+
assert_eq!(msg_bob.text, "Let's eat");
1784+
Ok(())
1785+
}
1786+
17541787
/// Tests that subject is not prepended to the message
17551788
/// when bot receives it.
17561789
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]

src/test_utils.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1679,7 +1679,6 @@ Until the false-positive is fixed:
16791679
}
16801680
}
16811681

1682-
#[cfg(test)]
16831682
mod tests {
16841683
use super::*;
16851684

0 commit comments

Comments
 (0)