Skip to content

Commit 95f9dfc

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 1601239 commit 95f9dfc

File tree

5 files changed

+91
-9
lines changed

5 files changed

+91
-9
lines changed

src/context.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,13 @@ 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+
#[cfg(test)]
308+
#[allow(clippy::type_complexity)]
309+
/// Transforms the root of the cryptographic payload before encryption.
310+
pub(crate) pre_encrypt_mime_cb: parking_lot::Mutex<
311+
Option<fn(mail_builder::mime::MimePart) -> mail_builder::mime::MimePart>,
312+
>,
306313
}
307314

308315
/// The state of ongoing process.
@@ -467,6 +474,8 @@ impl Context {
467474
iroh: Arc::new(RwLock::new(None)),
468475
self_fingerprint: OnceLock::new(),
469476
connectivities: parking_lot::Mutex::new(Vec::new()),
477+
#[cfg(test)]
478+
pre_encrypt_mime_cb: None.into(),
470479
};
471480

472481
let ctx = Context {

src/mimefactory.rs

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

1235+
#[cfg(test)]
1236+
{
1237+
message = crate::test_utils::pre_encrypt_mime(context, message);
1238+
}
1239+
12351240
let encrypted = if let Some(shared_secret) = shared_secret {
12361241
encrypt_helper
12371242
.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: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1751,6 +1751,38 @@ 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.pre_encrypt_mime_cb.lock() = Some(|mut mime| {
1769+
for (h, v) in &mut mime.headers {
1770+
if h == "Content-Type" {
1771+
if let mail_builder::headers::HeaderType::ContentType(ct) = v {
1772+
*ct = ct.clone().attribute("hp-legacy-display", "1");
1773+
}
1774+
}
1775+
}
1776+
mime
1777+
});
1778+
let sent_msg = alice.send_msg(chat_id, &mut msg).await;
1779+
1780+
let msg_bob = bob.recv_msg(&sent_msg).await;
1781+
assert_eq!(msg_bob.subject, "Dinner plans");
1782+
assert_eq!(msg_bob.text, "Let's eat");
1783+
Ok(())
1784+
}
1785+
17541786
/// Tests that subject is not prepended to the message
17551787
/// when bot receives it.
17561788
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]

src/test_utils.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ use std::time::{Duration, Instant};
1313
use async_channel::{self as channel, Receiver, Sender};
1414
use chat::ChatItem;
1515
use deltachat_contact_tools::{ContactAddress, EmailAddress};
16+
use mail_builder::mime::MimePart;
1617
use nu_ansi_term::Color;
1718
use pretty_assertions::assert_eq;
1819
use tempfile::{TempDir, tempdir};
@@ -1661,6 +1662,14 @@ async fn write_msg(context: &Context, prefix: &str, msg: &Message, buf: &mut Str
16611662
.unwrap();
16621663
}
16631664

1665+
/// See [`crate::context::InnerContext::pre_encrypt_mime_cb`].
1666+
pub(crate) fn pre_encrypt_mime(ctx: &Context, mime: MimePart<'static>) -> MimePart<'static> {
1667+
if let Some(cb) = &*ctx.pre_encrypt_mime_cb.lock() {
1668+
return cb(mime);
1669+
}
1670+
mime
1671+
}
1672+
16641673
/// When dropped after a test failure,
16651674
/// prints a note about a possible false-possible caused by SystemTime::shift().
16661675
pub(crate) struct TimeShiftFalsePositiveNote;
@@ -1679,7 +1688,6 @@ Until the false-positive is fixed:
16791688
}
16801689
}
16811690

1682-
#[cfg(test)]
16831691
mod tests {
16841692
use super::*;
16851693

0 commit comments

Comments
 (0)