Skip to content

Commit ec27ee9

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 852907f commit ec27ee9

File tree

2 files changed

+73
-8
lines changed

2 files changed

+73
-8
lines changed

src/mimeparser.rs

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

13451345
let (simplified_txt, simplified_quote) = if mime_type.type_() == mime::TEXT
13461346
&& mime_type.subtype() == mime::PLAIN
1347-
&& is_format_flowed
13481347
{
1349-
let delsp = if let Some(delsp) = mail.ctype.params.get("delsp") {
1350-
delsp.as_str().eq_ignore_ascii_case("yes")
1351-
} else {
1352-
false
1348+
// Don't check that we're inside an encrypted or signed part for
1349+
// simplicity and ease of testing.
1350+
let simplified_txt = match mail
1351+
.ctype
1352+
.params
1353+
.get("hp-legacy-display")
1354+
.is_some_and(|v| v == "1")
1355+
{
1356+
false => simplified_txt,
1357+
true => rm_legacy_display_elements(&simplified_txt),
13531358
};
1354-
let unflowed_text = unformat_flowed(&simplified_txt, delsp);
1355-
let unflowed_quote = top_quote.map(|q| unformat_flowed(&q, delsp));
1356-
(unflowed_text, unflowed_quote)
1359+
if is_format_flowed {
1360+
let delsp = if let Some(delsp) = mail.ctype.params.get("delsp") {
1361+
delsp.as_str().eq_ignore_ascii_case("yes")
1362+
} else {
1363+
false
1364+
};
1365+
let unflowed_text = unformat_flowed(&simplified_txt, delsp);
1366+
let unflowed_quote = top_quote.map(|q| unformat_flowed(&q, delsp));
1367+
(unflowed_text, unflowed_quote)
1368+
} else {
1369+
(simplified_txt, top_quote)
1370+
}
13571371
} else {
13581372
(simplified_txt, top_quote)
13591373
};
@@ -1970,6 +1984,20 @@ impl MimeMessage {
19701984
}
19711985
}
19721986

1987+
fn rm_legacy_display_elements(text: &str) -> String {
1988+
let mut res = None;
1989+
for l in text.lines() {
1990+
res = res.map(|r: String| match r.is_empty() {
1991+
true => l.to_string(),
1992+
false => r + "\r\n" + l,
1993+
});
1994+
if l.is_empty() {
1995+
res = Some(String::new());
1996+
}
1997+
}
1998+
res.unwrap_or_default()
1999+
}
2000+
19732001
fn remove_header(
19742002
headers: &mut HashMap<String, String>,
19752003
key: &str,

src/mimeparser/mimeparser_tests.rs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1751,6 +1751,43 @@ 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 bob = &tcm.bob().await;
1758+
1759+
let msg = MimeMessage::from_bytes(
1760+
bob,
1761+
br#"Date: Fri, 21 Jan 2022 20:40:48 -0500
1762+
From: Alice <alice@example.net>
1763+
To: Bob <bob@example.net>
1764+
Subject: Dinner plans
1765+
Message-ID: <text-plain-legacy-display@lhp.example>
1766+
MIME-Version: 1.0
1767+
Content-Type: text/plain; charset="us-ascii"; hp-legacy-display="1";
1768+
hp="cipher"
1769+
HP-Outer: Date: Fri, 21 Jan 2022 20:40:48 -0500
1770+
HP-Outer: From: Alice <alice@example.net>
1771+
HP-Outer: To: Bob <bob@example.net>
1772+
HP-Outer: Subject: [...]
1773+
HP-Outer: Message-ID: <text-plain-legacy-display@lhp.example>
1774+
1775+
Subject: Dinner plans
1776+
1777+
Let's meet at Rama's Roti Shop at 8pm and go to the park
1778+
from there.
1779+
"#,
1780+
None,
1781+
)
1782+
.await?;
1783+
assert_eq!(
1784+
msg.parts[0].msg,
1785+
"Dinner plans — Let's meet at Rama's Roti Shop at 8pm and go to the park\r\n\
1786+
from there."
1787+
);
1788+
Ok(())
1789+
}
1790+
17541791
/// Tests that subject is not prepended to the message
17551792
/// when bot receives it.
17561793
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]

0 commit comments

Comments
 (0)