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.
This commit is contained in:
iequidoo
2025-10-28 15:36:08 -03:00
committed by iequidoo
parent e2ae6ae013
commit c6894f56b2
5 changed files with 66 additions and 16 deletions

View File

@@ -445,6 +445,12 @@ pub enum Config {
/// Return an error from `receive_imf_inner()` for a fully downloaded message. For tests. /// Return an error from `receive_imf_inner()` for a fully downloaded message. For tests.
FailOnReceivingFullMsg, FailOnReceivingFullMsg,
/// Enable composing emails with Header Protection as defined in
/// <https://www.rfc-editor.org/rfc/rfc9788.html> "Header Protection for Cryptographically
/// Protected Email".
#[strum(props(default = "1"))]
StdHeaderProtectionComposing,
} }
impl Config { impl Config {

View File

@@ -1077,6 +1077,13 @@ impl Context {
.await? .await?
.unwrap_or_default(), .unwrap_or_default(),
); );
res.insert(
"std_header_protection_composing",
self.sql
.get_raw_config("std_header_protection_composing")
.await?
.unwrap_or_default(),
);
let elapsed = time_elapsed(&self.creation_time); let elapsed = time_elapsed(&self.creation_time);
res.insert("uptime", duration_to_str(elapsed)); res.insert("uptime", duration_to_str(elapsed));

View File

@@ -1083,6 +1083,9 @@ impl MimeFactory {
} }
} }
let use_std_header_protection = context
.get_config_bool(Config::StdHeaderProtectionComposing)
.await?;
let outer_message = if let Some(encryption_pubkeys) = self.encryption_pubkeys { let outer_message = if let Some(encryption_pubkeys) = self.encryption_pubkeys {
// Store protected headers in the inner message. // Store protected headers in the inner message.
let message = protected_headers let message = protected_headers
@@ -1098,6 +1101,22 @@ impl MimeFactory {
message.header(header, value) message.header(header, value)
}); });
if use_std_header_protection {
message = unprotected_headers
.iter()
// Structural headers shouldn't be added as "HP-Outer". They are defined in
// <https://www.rfc-editor.org/rfc/rfc9787.html#structural-header-fields>.
.filter(|(name, _)| {
!(name.eq_ignore_ascii_case("mime-version")
|| name.eq_ignore_ascii_case("content-type")
|| name.eq_ignore_ascii_case("content-transfer-encoding")
|| name.eq_ignore_ascii_case("content-disposition"))
})
.fold(message, |message, (name, value)| {
message.header(format!("HP-Outer: {name}"), value.clone())
});
}
// Add gossip headers in chats with multiple recipients // Add gossip headers in chats with multiple recipients
let multiple_recipients = let multiple_recipients =
encryption_pubkeys.len() > 1 || context.get_config_bool(Config::BccSelf).await?; encryption_pubkeys.len() > 1 || context.get_config_bool(Config::BccSelf).await?;
@@ -1187,7 +1206,13 @@ impl MimeFactory {
for (h, v) in &mut message.headers { for (h, v) in &mut message.headers {
if h == "Content-Type" { if h == "Content-Type" {
if let mail_builder::headers::HeaderType::ContentType(ct) = v { if let mail_builder::headers::HeaderType::ContentType(ct) = v {
*ct = ct.clone().attribute("protected-headers", "v1"); let mut ct_new = ct.clone();
ct_new = ct_new.attribute("protected-headers", "v1");
if use_std_header_protection {
ct_new = ct_new.attribute("hp", "cipher");
}
*ct = ct_new;
break;
} }
} }
} }
@@ -1325,7 +1350,13 @@ impl MimeFactory {
for (h, v) in &mut message.headers { for (h, v) in &mut message.headers {
if h == "Content-Type" { if h == "Content-Type" {
if let mail_builder::headers::HeaderType::ContentType(ct) = v { if let mail_builder::headers::HeaderType::ContentType(ct) = v {
*ct = ct.clone().attribute("protected-headers", "v1"); let mut ct_new = ct.clone();
ct_new = ct_new.attribute("protected-headers", "v1");
if use_std_header_protection {
ct_new = ct_new.attribute("hp", "clear");
}
*ct = ct_new;
break;
} }
} }
} }

View File

@@ -832,8 +832,8 @@ async fn test_protected_headers_directive() -> Result<()> {
.count(), .count(),
1 1
); );
assert_eq!(part.match_indices("Subject:").count(), 1); assert_eq!(part.match_indices("Subject:").count(), 2);
assert_eq!(part.match_indices("HP-Outer: Subject:").count(), 1);
Ok(()) Ok(())
} }

View File

@@ -1401,22 +1401,28 @@ async fn test_x_microsoft_original_message_id_precedence() -> Result<()> {
} }
#[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_extra_imf_chat_header() -> Result<()> { async fn test_extra_imf_headers() -> Result<()> {
let mut tcm = TestContextManager::new(); let mut tcm = TestContextManager::new();
let t = &tcm.alice().await; let t = &tcm.alice().await;
let chat_id = t.get_self_chat().await.id; let chat_id = t.get_self_chat().await.id;
chat::send_text_msg(t, chat_id, "hi!".to_string()).await?; for std_hp_composing in [false, true] {
let sent_msg = t.pop_sent_msg().await; t.set_config_bool(Config::StdHeaderProtectionComposing, std_hp_composing)
// Check removal of some nonexistent "Chat-*" header to protect the code from future breakages. .await?;
let payload = sent_msg chat::send_text_msg(t, chat_id, "hi!".to_string()).await?;
.payload let sent_msg = t.pop_sent_msg().await;
.replace("Message-ID:", "Chat-Forty-Two: 42\r\nMessage-ID:"); // Check removal of some nonexistent "Chat-*" header to protect the code from future
let msg = MimeMessage::from_bytes(t, payload.as_bytes(), None) // breakages. But headers not prefixed with "Chat-" remain unless a message has standard
.await // Header Protection.
.unwrap(); let payload = sent_msg.payload.replace(
assert!(msg.headers.contains_key("chat-version")); "Message-ID:",
assert!(!msg.headers.contains_key("chat-forty-two")); "Chat-Forty-Two: 42\r\nForty-Two: 42\r\nMessage-ID:",
);
let msg = MimeMessage::from_bytes(t, payload.as_bytes(), None).await?;
assert!(msg.headers.contains_key("chat-version"));
assert!(!msg.headers.contains_key("chat-forty-two"));
assert_ne!(msg.headers.contains_key("forty-two"), std_hp_composing);
}
Ok(()) Ok(())
} }