mirror of
https://github.com/chatmail/core.git
synced 2026-04-02 05:22:14 +03:00
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:
@@ -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
|
||||
/// <https://www.rfc-editor.org/rfc/rfc9788.html> "Header Protection for Cryptographically
|
||||
/// Protected Email".
|
||||
#[strum(props(default = "1"))]
|
||||
StdHeaderProtectionComposing,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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
|
||||
// <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
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user