diff --git a/src/config.rs b/src/config.rs index f8e3c8ed9..b4b504666 100644 --- a/src/config.rs +++ b/src/config.rs @@ -445,6 +445,12 @@ pub enum Config { /// Return an error from `receive_imf_inner()` for a fully downloaded message. For tests. FailOnReceivingFullMsg, + + /// Enable composing emails with Header Protection as defined in + /// "Header Protection for Cryptographically + /// Protected Email". + #[strum(props(default = "1"))] + StdHeaderProtectionComposing, } impl Config { diff --git a/src/context.rs b/src/context.rs index 8574ceb5c..610b2887c 100644 --- a/src/context.rs +++ b/src/context.rs @@ -1077,6 +1077,13 @@ impl Context { .await? .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); res.insert("uptime", duration_to_str(elapsed)); diff --git a/src/mimefactory.rs b/src/mimefactory.rs index 337095991..00e02fb37 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -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 { // Store protected headers in the inner message. let message = protected_headers @@ -1098,6 +1101,22 @@ impl MimeFactory { 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 + // . + .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 let multiple_recipients = encryption_pubkeys.len() > 1 || context.get_config_bool(Config::BccSelf).await?; @@ -1187,7 +1206,13 @@ impl MimeFactory { for (h, v) in &mut message.headers { if h == "Content-Type" { 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 { if h == "Content-Type" { 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; } } } diff --git a/src/mimefactory/mimefactory_tests.rs b/src/mimefactory/mimefactory_tests.rs index 61cf0fec1..ce8b12c87 100644 --- a/src/mimefactory/mimefactory_tests.rs +++ b/src/mimefactory/mimefactory_tests.rs @@ -832,8 +832,8 @@ async fn test_protected_headers_directive() -> Result<()> { .count(), 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(()) } diff --git a/src/mimeparser/mimeparser_tests.rs b/src/mimeparser/mimeparser_tests.rs index 101966c69..0059039b0 100644 --- a/src/mimeparser/mimeparser_tests.rs +++ b/src/mimeparser/mimeparser_tests.rs @@ -1401,22 +1401,28 @@ async fn test_x_microsoft_original_message_id_precedence() -> Result<()> { } #[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 t = &tcm.alice().await; let chat_id = t.get_self_chat().await.id; - chat::send_text_msg(t, chat_id, "hi!".to_string()).await?; - let sent_msg = t.pop_sent_msg().await; - // Check removal of some nonexistent "Chat-*" header to protect the code from future breakages. - let payload = sent_msg - .payload - .replace("Message-ID:", "Chat-Forty-Two: 42\r\nMessage-ID:"); - let msg = MimeMessage::from_bytes(t, payload.as_bytes(), None) - .await - .unwrap(); - assert!(msg.headers.contains_key("chat-version")); - assert!(!msg.headers.contains_key("chat-forty-two")); + for std_hp_composing in [false, true] { + t.set_config_bool(Config::StdHeaderProtectionComposing, std_hp_composing) + .await?; + chat::send_text_msg(t, chat_id, "hi!".to_string()).await?; + let sent_msg = t.pop_sent_msg().await; + // Check removal of some nonexistent "Chat-*" header to protect the code from future + // breakages. But headers not prefixed with "Chat-" remain unless a message has standard + // Header Protection. + let payload = sent_msg.payload.replace( + "Message-ID:", + "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(()) }