diff --git a/deltachat-ffi/deltachat.h b/deltachat-ffi/deltachat.h index f15e75c92..fb5a2e12a 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 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/deltachat-rpc-client/tests/test_chatlist_events.py b/deltachat-rpc-client/tests/test_chatlist_events.py index f13769e07..a6152049d 100644 --- a/deltachat-rpc-client/tests/test_chatlist_events.py +++ b/deltachat-rpc-client/tests/test_chatlist_events.py @@ -87,6 +87,7 @@ def test_delivery_status_failed(acfactory: ACFactory) -> None: Test change status on chatlistitem when status changes failed """ (alice,) = acfactory.get_online_accounts(1) + alice.set_config("force_encryption", "0") invalid_contact = alice.create_contact("example@example.com", "invalid address") invalid_chat = alice.get_chat_by_id(alice._rpc.create_chat_by_contact_id(alice.id, invalid_contact.id)) diff --git a/src/chat.rs b/src/chat.rs index 8f1b82bdd..ab9810205 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -2823,7 +2823,12 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) - .await?; } - let needs_encryption = msg.param.get_bool(Param::GuaranteeE2ee).unwrap_or_default(); + let needs_encryption = msg.param.get_bool(Param::GuaranteeE2ee).unwrap_or_default() + || (!msg + .param + .get_bool(Param::ForcePlaintext) + .unwrap_or_default() + && context.get_config_bool(Config::ForceEncryption).await?); let mimefactory = match MimeFactory::from_msg(context, msg.clone()).await { Ok(mf) => mf, Err(err) => { 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 f8128e1af..9cd63c654 100644 --- a/src/context.rs +++ b/src/context.rs @@ -1053,6 +1053,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/e2ee.rs b/src/e2ee.rs index 45edc4d6b..e49804906 100644 --- a/src/e2ee.rs +++ b/src/e2ee.rs @@ -109,6 +109,7 @@ pub async fn ensure_secret_key_exists(context: &Context) -> Result<()> { #[cfg(test)] mod tests { use super::*; + use crate::chat; use crate::chat::send_text_msg; use crate::config::Config; use crate::message::Message; @@ -155,6 +156,19 @@ Sent with my Delta Chat Messenger: https://delta.chat"; ); } + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_cannot_send_unencrypted_by_default() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = &tcm.alice().await; + let bob = &tcm.bob().await; + let chat = alice.create_email_chat(bob).await; + + let mut msg = Message::new_text("Hello!".to_string()); + assert!(chat::send_msg(alice, chat.id, &mut msg).await.is_err()); + + Ok(()) + } + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_chatmail_can_send_unencrypted() -> Result<()> { let mut tcm = TestContextManager::new(); 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/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 4de21b2c0..e32bd9897 100644 --- a/src/receive_imf/receive_imf_tests.rs +++ b/src/receive_imf/receive_imf_tests.rs @@ -4498,12 +4498,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 @@ -4513,9 +4513,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(()) } }