From 15d971f0dc3285c361e13bcfee76dfc2c5408f02 Mon Sep 17 00:00:00 2001 From: link2xt Date: Thu, 7 May 2026 07:10:17 +0200 Subject: [PATCH] feat: add option to force encryption --- deltachat-ffi/deltachat.h | 1 + src/config.rs | 12 +++++++++++- src/context.rs | 6 ++++++ src/imap.rs | 11 ++++++++++- src/imap/session.rs | 1 + src/mimefactory.rs | 3 ++- src/receive_imf.rs | 8 ++++++++ src/receive_imf/receive_imf_tests.rs | 9 ++++----- src/sync.rs | 19 +++++++++++++++++++ src/test_utils.rs | 3 +-- 10 files changed, 63 insertions(+), 10 deletions(-) diff --git a/deltachat-ffi/deltachat.h b/deltachat-ffi/deltachat.h index f15e75c92..51f19643e 100644 --- a/deltachat-ffi/deltachat.h +++ b/deltachat-ffi/deltachat.h @@ -487,6 +487,7 @@ char* dc_get_blobdir (const dc_context_t* context); * 0 = Everybody (except explicitly blocked contacts), * 1 = Contacts (default, does not include contact requests), * 2 = Nobody (calls never result in a notification). + * - `force_encryption` = 1 (default) to force encryption, 0 to allow receiving unencrypted messages. * * Also, there are configs that are only needed * if you want to use the deprecated dc_configure() API, such as: diff --git a/src/config.rs b/src/config.rs index f0ca2fafb..accf04724 100644 --- a/src/config.rs +++ b/src/config.rs @@ -486,6 +486,12 @@ pub enum Config { /// Experimental option denoting that the current profile is shared between multiple team members. /// For now, the only effect of this option is that seen flags are not synchronized. TeamProfile, + + /// Process unencrypted messages. + /// + /// Unencrypted messages are fetched and processed only if this setting is explicitly enabled. + #[strum(props(default = "1"))] + ForceEncryption, } impl Config { @@ -501,7 +507,11 @@ impl Config { pub(crate) fn is_synced(&self) -> bool { matches!( self, - Self::Displayname | Self::MdnsEnabled | Self::Selfavatar | Self::Selfstatus, + Self::Displayname + | Self::MdnsEnabled + | Self::Selfavatar + | Self::Selfstatus + | Self::ForceEncryption, ) } diff --git a/src/context.rs b/src/context.rs index 95ce3c777..2eb37cb7d 100644 --- a/src/context.rs +++ b/src/context.rs @@ -1056,6 +1056,12 @@ impl Context { "team_profile", self.get_config_bool(Config::TeamProfile).await?.to_string(), ); + res.insert( + "force_encryption", + self.get_config_bool(Config::ForceEncryption) + .await? + .to_string(), + ); let elapsed = time_elapsed(&self.creation_time); res.insert("uptime", duration_to_str(elapsed)); diff --git a/src/imap.rs b/src/imap.rs index bedc0376c..d6cabae0e 100644 --- a/src/imap.rs +++ b/src/imap.rs @@ -1994,12 +1994,21 @@ pub(crate) async fn prefetch_should_download( // prevent_rename=true as this might be a mailing list message and in this case it would be bad if we rename the contact. // (prevent_rename is the last argument of from_field_to_contact_id()) + let is_encrypted = if let Some(content_type) = headers.get_header_value(HeaderDef::ContentType) + { + mailparse::parse_content_type(&content_type).mimetype == "multipart/encrypted" + } else { + false + }; + if flags.any(|f| f == Flag::Draft) { info!(context, "Ignoring draft message"); return Ok(false); } - let should_download = !blocked_contact || maybe_ndn; + let should_download = maybe_ndn + || (!blocked_contact + && (is_encrypted || !context.get_config_bool(Config::ForceEncryption).await?)); Ok(should_download) } diff --git a/src/imap/session.rs b/src/imap/session.rs index 8d4e7e087..e1e397574 100644 --- a/src/imap/session.rs +++ b/src/imap/session.rs @@ -21,6 +21,7 @@ const PREFETCH_FLAGS: &str = "(UID RFC822.SIZE BODY.PEEK[HEADER.FIELDS (\ DATE \ X-MICROSOFT-ORIGINAL-MESSAGE-ID \ FROM \ + CONTENT-TYPE \ CHAT-VERSION \ CHAT-IS-POST-MESSAGE \ AUTOCRYPT-SETUP-MESSAGE\ diff --git a/src/mimefactory.rs b/src/mimefactory.rs index f5e04c215..a616b6a83 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -980,7 +980,8 @@ impl MimeFactory { )); } - let is_encrypted = self.will_be_encrypted(); + let is_encrypted = + self.will_be_encrypted() || context.get_config_bool(Config::ForceEncryption).await?; // Add ephemeral timer for non-MDN messages. // For MDNs it does not matter because they are not visible diff --git a/src/receive_imf.rs b/src/receive_imf.rs index 62db19810..5cb90a257 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -505,6 +505,14 @@ pub(crate) async fn receive_imf_inner( Ok(mime_parser) => mime_parser, }; + if !mime_parser.was_encrypted() + && mime_parser.get_header(HeaderDef::SecureJoin).is_none() + && context.get_config_bool(Config::ForceEncryption).await? + { + warn!(context, "Fetched unencrypted message, ignoring"); + return trash().await; + } + let rfc724_mid_orig = &mime_parser .get_rfc724_mid() .unwrap_or(rfc724_mid.to_string()); diff --git a/src/receive_imf/receive_imf_tests.rs b/src/receive_imf/receive_imf_tests.rs index 844f6cd2b..4f0b870ed 100644 --- a/src/receive_imf/receive_imf_tests.rs +++ b/src/receive_imf/receive_imf_tests.rs @@ -4495,12 +4495,12 @@ async fn test_outgoing_msg_forgery() -> Result<()> { imex(alice, ImexMode::ExportSelfKeys, export_dir.path(), None).await?; // We need Bob only to encrypt the forged message to Alice's key, actually Bob doesn't // participate in the scenario. - let bob = &TestContext::new().await; + let bob = &tcm.unconfigured().await; assert_eq!(crate::key::load_self_secret_keyring(bob).await?.len(), 0); bob.configure_addr("bob@example.net").await; imex(bob, ImexMode::ImportSelfKeys, export_dir.path(), None).await?; assert_eq!(crate::key::load_self_secret_keyring(bob).await?.len(), 1); - let malice = &TestContext::new().await; + let malice = &tcm.unconfigured().await; malice.configure_addr(alice_addr).await; let malice_chat_id = tcm @@ -4510,9 +4510,8 @@ async fn test_outgoing_msg_forgery() -> Result<()> { assert_eq!(crate::key::load_self_secret_keyring(bob).await?.len(), 1); let sent_msg = malice.send_text(malice_chat_id, "hi from malice").await; - let msg = alice.recv_msg(&sent_msg).await; - assert_eq!(msg.state, MessageState::OutDelivered); - assert!(!msg.get_showpadlock()); + let msg = alice.recv_msg_opt(&sent_msg).await; + assert!(msg.is_none()); Ok(()) } diff --git a/src/sync.rs b/src/sync.rs index 8b050456d..3f386bdea 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -810,4 +810,23 @@ mod tests { assert_eq!(msg.text, "Member Me added by alice@example.org."); Ok(()) } + + /// Tests that "force encryption" setting is synced. + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_sync_force_encryption() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = &tcm.alice().await; + let alice2 = &tcm.alice().await; + alice.set_config_bool(Config::SyncMsgs, true).await?; + alice2.set_config_bool(Config::SyncMsgs, true).await?; + + assert_eq!(alice.get_config_bool(Config::ForceEncryption).await?, true); + alice2 + .set_config_bool(Config::ForceEncryption, false) + .await?; + test_utils::sync(alice2, alice).await; + assert_eq!(alice.get_config_bool(Config::ForceEncryption).await?, false); + + Ok(()) + } } diff --git a/src/test_utils.rs b/src/test_utils.rs index 13493b162..8e8106ca2 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -1231,9 +1231,8 @@ ORDER BY id" } /// Allow reception of unencrypted messages. - #[expect(clippy::unused_async)] pub async fn allow_unencrypted(&self) -> Result<()> { - // Does nothing for now. + self.set_config_bool(Config::ForceEncryption, false).await?; Ok(()) } }