From fc30aa959d2c3f284961e32cf13141ebc51c8316 Mon Sep 17 00:00:00 2001 From: iequidoo Date: Sun, 26 Oct 2025 16:33:21 -0300 Subject: [PATCH 1/4] feat: Ignore unprotected headers if Content-Type has "hp" parameter (#7130) This is a part of implementation of https://www.rfc-editor.org/rfc/rfc9788 "Header Protection for Cryptographically Protected Email". --- src/mimeparser.rs | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/src/mimeparser.rs b/src/mimeparser.rs index 5d103eef62..1891de91ea 100644 --- a/src/mimeparser.rs +++ b/src/mimeparser.rs @@ -271,7 +271,7 @@ impl MimeMessage { &mut from, &mut list_post, &mut chat_disposition_notification_to, - &mail.headers, + &mail, ); headers.retain(|k, _| { !is_hidden(k) || { @@ -299,7 +299,7 @@ impl MimeMessage { &mut from, &mut list_post, &mut chat_disposition_notification_to, - &part.headers, + part, ); (part, part.ctype.mimetype.parse::()?) } else { @@ -536,7 +536,7 @@ impl MimeMessage { &mut inner_from, &mut list_post, &mut chat_disposition_notification_to, - &mail.headers, + mail, ); if !signatures.is_empty() { @@ -1632,6 +1632,11 @@ impl MimeMessage { } } + /// Merges headers from the email `part` into `headers` respecting header protection. + /// Should only be called with nonempty `headers` if `part` is a root of the Cryptographic + /// Payload as defined in "Header Protection for + /// Cryptographically Protected Email", otherwise this may unnecessarily discard headers from + /// outer parts. #[allow(clippy::too_many_arguments)] fn merge_headers( context: &Context, @@ -1642,10 +1647,14 @@ impl MimeMessage { from: &mut Option, list_post: &mut Option, chat_disposition_notification_to: &mut Option, - fields: &[mailparse::MailHeader<'_>], + part: &mailparse::ParsedMail, ) { + let fields = &part.headers; + // See . + let has_header_protection = part.ctype.params.contains_key("hp"); + headers.retain(|k, _| { - !is_protected(k) || { + !(has_header_protection || is_protected(k)) || { headers_removed.insert(k.to_string()); false } @@ -2096,7 +2105,8 @@ pub(crate) fn parse_message_id(ids: &str) -> Result { } /// Returns whether the outer header value must be ignored if the message contains a signed (and -/// optionally encrypted) part. +/// optionally encrypted) part. This is independent from the modern Header Protection defined in +/// . /// /// NB: There are known cases when Subject and List-ID only appear in the outer headers of /// signed-only messages. Such messages are shown as unencrypted anyway. From a6c8f8d8a98df3e4167aa4a5eb43da37bcd68355 Mon Sep 17 00:00:00 2001 From: iequidoo Date: Tue, 28 Oct 2025 04:48:15 -0300 Subject: [PATCH 2/4] feat: mimeparser: Omit Legacy Display Elements (#7130) Omit Legacy Display Elements from "text/plain" and "text/html" (implement 4.5.3.{2,3} of https://www.rfc-editor.org/rfc/rfc9788 "Header Protection for Cryptographically Protected Email"). --- src/config.rs | 5 ++++ src/context.rs | 19 ++++++++++++ src/dehtml.rs | 46 ++++++++++++++++++++++------ src/mimefactory.rs | 6 ++++ src/mimeparser.rs | 48 +++++++++++++++++++++++++----- src/mimeparser/mimeparser_tests.rs | 33 ++++++++++++++++++++ src/test_utils.rs | 1 - 7 files changed, 140 insertions(+), 18 deletions(-) diff --git a/src/config.rs b/src/config.rs index 7cb7617f1d..f8e3c8ed95 100644 --- a/src/config.rs +++ b/src/config.rs @@ -438,6 +438,11 @@ pub enum Config { /// storing the same token multiple times on the server. EncryptedDeviceToken, + /// Enables running test hooks, e.g. see `InnerContext::pre_encrypt_mime_hook`. + /// This way is better than conditional compilation, i.e. `#[cfg(test)]`, because tests not + /// using this still run unmodified code. + TestHooks, + /// Return an error from `receive_imf_inner()` for a fully downloaded message. For tests. FailOnReceivingFullMsg, } diff --git a/src/context.rs b/src/context.rs index 9287777c96..8574ceb5c3 100644 --- a/src/context.rs +++ b/src/context.rs @@ -303,6 +303,17 @@ pub struct InnerContext { /// `Connectivity` values for mailboxes, unordered. Used to compute the aggregate connectivity, /// see [`Context::get_connectivity()`]. pub(crate) connectivities: parking_lot::Mutex>, + + #[expect(clippy::type_complexity)] + /// Transforms the root of the cryptographic payload before encryption. + pub(crate) pre_encrypt_mime_hook: parking_lot::Mutex< + Option< + for<'a> fn( + &Context, + mail_builder::mime::MimePart<'a>, + ) -> mail_builder::mime::MimePart<'a>, + >, + >, } /// The state of ongoing process. @@ -467,6 +478,7 @@ impl Context { iroh: Arc::new(RwLock::new(None)), self_fingerprint: OnceLock::new(), connectivities: parking_lot::Mutex::new(Vec::new()), + pre_encrypt_mime_hook: None.into(), }; let ctx = Context { @@ -1051,6 +1063,13 @@ impl Context { .await? .to_string(), ); + res.insert( + "test_hooks", + self.sql + .get_raw_config("test_hooks") + .await? + .unwrap_or_default(), + ); res.insert( "fail_on_receiving_full_msg", self.sql diff --git a/src/dehtml.rs b/src/dehtml.rs index a6d70b1f7d..12cd4e6098 100644 --- a/src/dehtml.rs +++ b/src/dehtml.rs @@ -13,6 +13,7 @@ use quick_xml::{ use crate::simplify::{SimplifiedText, simplify_quote}; +#[derive(Default)] struct Dehtml { strbuilder: String, quote: String, @@ -25,6 +26,9 @@ struct Dehtml { /// Everything between `
` and `
` is usually metadata /// If this is > `0`, then we are inside a `
`. divs_since_quoted_content_div: u32, + /// `
` elements should be omitted, see + /// . + divs_since_hp_legacy_display: u32, /// All-Inkl just puts the quote into `
`. This count is /// increased at each `
` and decreased at each `
`. blockquotes_since_blockquote: u32, @@ -48,20 +52,25 @@ impl Dehtml { } fn get_add_text(&self) -> AddText { - if self.divs_since_quote_div > 0 && self.divs_since_quoted_content_div == 0 { - AddText::No // Everything between `
` and `
` is metadata which we don't want + // Everything between `
` and `
` is + // metadata which we don't want. + if self.divs_since_quote_div > 0 && self.divs_since_quoted_content_div == 0 + || self.divs_since_hp_legacy_display > 0 + { + AddText::No } else { self.add_text } } } -#[derive(Debug, PartialEq, Clone, Copy)] +#[derive(Debug, Default, PartialEq, Clone, Copy)] enum AddText { /// Inside `