Skip to content

Commit 00f70a2

Browse files
committed
feat: Add Config::StdHeaderProtectionComposing (enables composing as defined in RFC 9788) (#7130)
And enable it by default as the standard Header Protection is backward-compatible. Also this tests extra IMF header removal when a message has standard Header Protection since now we can send such messages.
1 parent 95f9dfc commit 00f70a2

File tree

5 files changed

+66
-16
lines changed

5 files changed

+66
-16
lines changed

src/config.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -440,6 +440,12 @@ pub enum Config {
440440

441441
/// Return an error from `receive_imf_inner()` for a fully downloaded message. For tests.
442442
FailOnReceivingFullMsg,
443+
444+
/// Enable composing emails with Header Protection as defined in
445+
/// <https://www.rfc-editor.org/rfc/rfc9788.html> "Header Protection for Cryptographically
446+
/// Protected Email".
447+
#[strum(props(default = "1"))]
448+
StdHeaderProtectionComposing,
443449
}
444450

445451
impl Config {

src/context.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1067,6 +1067,13 @@ impl Context {
10671067
.await?
10681068
.unwrap_or_default(),
10691069
);
1070+
res.insert(
1071+
"std_header_protection_composing",
1072+
self.sql
1073+
.get_raw_config("std_header_protection_composing")
1074+
.await?
1075+
.unwrap_or_default(),
1076+
);
10701077

10711078
let elapsed = time_elapsed(&self.creation_time);
10721079
res.insert("uptime", duration_to_str(elapsed));

src/mimefactory.rs

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1083,6 +1083,9 @@ impl MimeFactory {
10831083
}
10841084
}
10851085

1086+
let use_std_header_protection = context
1087+
.get_config_bool(Config::StdHeaderProtectionComposing)
1088+
.await?;
10861089
let outer_message = if let Some(encryption_pubkeys) = self.encryption_pubkeys {
10871090
// Store protected headers in the inner message.
10881091
let message = protected_headers
@@ -1098,6 +1101,22 @@ impl MimeFactory {
10981101
message.header(header, value)
10991102
});
11001103

1104+
if use_std_header_protection {
1105+
message = unprotected_headers
1106+
.iter()
1107+
// Structural headers shouldn't be added as "HP-Outer". They are defined in
1108+
// <https://www.rfc-editor.org/rfc/rfc9787.html#structural-header-fields>.
1109+
.filter(|(name, _)| {
1110+
!(name.eq_ignore_ascii_case("mime-version")
1111+
|| name.eq_ignore_ascii_case("content-type")
1112+
|| name.eq_ignore_ascii_case("content-transfer-encoding")
1113+
|| name.eq_ignore_ascii_case("content-disposition"))
1114+
})
1115+
.fold(message, |message, (name, value)| {
1116+
message.header(format!("HP-Outer: {name}"), value.clone())
1117+
});
1118+
}
1119+
11011120
// Add gossip headers in chats with multiple recipients
11021121
let multiple_recipients =
11031122
encryption_pubkeys.len() > 1 || context.get_config_bool(Config::BccSelf).await?;
@@ -1187,7 +1206,13 @@ impl MimeFactory {
11871206
for (h, v) in &mut message.headers {
11881207
if h == "Content-Type" {
11891208
if let mail_builder::headers::HeaderType::ContentType(ct) = v {
1190-
*ct = ct.clone().attribute("protected-headers", "v1");
1209+
let mut ct_new = ct.clone();
1210+
ct_new = ct_new.attribute("protected-headers", "v1");
1211+
if use_std_header_protection {
1212+
ct_new = ct_new.attribute("hp", "cipher");
1213+
}
1214+
*ct = ct_new;
1215+
break;
11911216
}
11921217
}
11931218
}
@@ -1324,7 +1349,13 @@ impl MimeFactory {
13241349
for (h, v) in &mut message.headers {
13251350
if h == "Content-Type" {
13261351
if let mail_builder::headers::HeaderType::ContentType(ct) = v {
1327-
*ct = ct.clone().attribute("protected-headers", "v1");
1352+
let mut ct_new = ct.clone();
1353+
ct_new = ct_new.attribute("protected-headers", "v1");
1354+
if use_std_header_protection {
1355+
ct_new = ct_new.attribute("hp", "clear");
1356+
}
1357+
*ct = ct_new;
1358+
break;
13281359
}
13291360
}
13301361
}

src/mimefactory/mimefactory_tests.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -832,8 +832,8 @@ async fn test_protected_headers_directive() -> Result<()> {
832832
.count(),
833833
1
834834
);
835-
assert_eq!(part.match_indices("Subject:").count(), 1);
836-
835+
assert_eq!(part.match_indices("Subject:").count(), 2);
836+
assert_eq!(part.match_indices("HP-Outer: Subject:").count(), 1);
837837
Ok(())
838838
}
839839

src/mimeparser/mimeparser_tests.rs

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1401,22 +1401,28 @@ async fn test_x_microsoft_original_message_id_precedence() -> Result<()> {
14011401
}
14021402

14031403
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1404-
async fn test_extra_imf_chat_header() -> Result<()> {
1404+
async fn test_extra_imf_headers() -> Result<()> {
14051405
let mut tcm = TestContextManager::new();
14061406
let t = &tcm.alice().await;
14071407
let chat_id = t.get_self_chat().await.id;
14081408

1409-
chat::send_text_msg(t, chat_id, "hi!".to_string()).await?;
1410-
let sent_msg = t.pop_sent_msg().await;
1411-
// Check removal of some nonexistent "Chat-*" header to protect the code from future breakages.
1412-
let payload = sent_msg
1413-
.payload
1414-
.replace("Message-ID:", "Chat-Forty-Two: 42\r\nMessage-ID:");
1415-
let msg = MimeMessage::from_bytes(t, payload.as_bytes(), None)
1416-
.await
1417-
.unwrap();
1418-
assert!(msg.headers.contains_key("chat-version"));
1419-
assert!(!msg.headers.contains_key("chat-forty-two"));
1409+
for std_hp_composing in [false, true] {
1410+
t.set_config_bool(Config::StdHeaderProtectionComposing, std_hp_composing)
1411+
.await?;
1412+
chat::send_text_msg(t, chat_id, "hi!".to_string()).await?;
1413+
let sent_msg = t.pop_sent_msg().await;
1414+
// Check removal of some nonexistent "Chat-*" header to protect the code from future
1415+
// breakages. But headers not prefixed with "Chat-" remain unless a message has standard
1416+
// Header Protection.
1417+
let payload = sent_msg.payload.replace(
1418+
"Message-ID:",
1419+
"Chat-Forty-Two: 42\r\nForty-Two: 42\r\nMessage-ID:",
1420+
);
1421+
let msg = MimeMessage::from_bytes(t, payload.as_bytes(), None).await?;
1422+
assert!(msg.headers.contains_key("chat-version"));
1423+
assert!(!msg.headers.contains_key("chat-forty-two"));
1424+
assert_ne!(msg.headers.contains_key("forty-two"), std_hp_composing);
1425+
}
14201426
Ok(())
14211427
}
14221428

0 commit comments

Comments
 (0)