From 411519d5a63cbe1213d949219e7c042c51142dc4 Mon Sep 17 00:00:00 2001 From: Hocuri Date: Tue, 5 May 2026 22:40:41 +0200 Subject: [PATCH 01/20] Remove delete_server_after code. Rust tests are passing --- deltachat-ffi/deltachat.h | 9 +-- deltachat-jsonrpc/src/api.rs | 13 ++++- deltachat-rpc-client/tests/test_folders.py | 8 ++- .../tests/test_multidevice.py | 35 ------------ deltachat-rpc-client/tests/test_something.py | 2 +- python/src/deltachat/testplugin.py | 1 - python/tests/test_0_complex_or_slow.py | 4 +- src/config.rs | 35 ++---------- src/config/config_tests.rs | 22 -------- src/context.rs | 7 +-- src/download.rs | 6 -- src/ephemeral.rs | 18 +++--- src/ephemeral/ephemeral_tests.rs | 53 +++++++++--------- src/imex.rs | 18 +++--- src/message.rs | 55 ++++++++----------- src/provider/data.rs | 14 ++--- src/receive_imf/receive_imf_tests.rs | 19 ++++--- src/smtp.rs | 38 +++++++++---- 18 files changed, 135 insertions(+), 222 deletions(-) diff --git a/deltachat-ffi/deltachat.h b/deltachat-ffi/deltachat.h index f15e75c92d..1b4da477b2 100644 --- a/deltachat-ffi/deltachat.h +++ b/deltachat-ffi/deltachat.h @@ -413,11 +413,6 @@ char* dc_get_blobdir (const dc_context_t* context); * Messages in the "saved messages" chat (see dc_chat_is_self_talk()) are skipped. * Messages are deleted whether they were seen or not, the UI should clearly point that out. * See also dc_estimate_deletion_cnt(). - * - `delete_server_after` = 0=do not delete messages from server automatically (default), - * 1=delete messages directly after receiving from server, mvbox is skipped. - * >1=seconds, after which messages are deleted automatically from the server, mvbox is used as defined. - * "Saved messages" are deleted from the server as well as emails, the UI should clearly point that out. - * See also dc_estimate_deletion_cnt(). * - `media_quality` = DC_MEDIA_QUALITY_BALANCED (0) = * good outgoing images/videos/voice quality at reasonable sizes (default) * DC_MEDIA_QUALITY_WORSE (1) @@ -1461,13 +1456,13 @@ dc_chatlist_t* dc_get_similar_chatlist (dc_context_t* context, uint32_t ch /** * Estimate the number of messages that will be deleted - * by the dc_set_config()-options `delete_device_after` or `delete_server_after`. + * by the dc_set_config()-option `delete_device_after`. * This is typically used to show the estimated impact to the user * before actually enabling deletion of old messages. * * @memberof dc_context_t * @param context The context object as returned from dc_context_new(). - * @param from_server 1=Estimate deletion count for server, 0=Estimate deletion count for device + * @param from_server Deprecated, pass 0 here * @param seconds Count messages older than the given number of seconds. * @return Number of messages that are older than the given number of seconds. * Messages in the "saved messages" folder are not counted as they will not be deleted automatically. diff --git a/deltachat-jsonrpc/src/api.rs b/deltachat-jsonrpc/src/api.rs index 2ee3c443b7..e3aac15eec 100644 --- a/deltachat-jsonrpc/src/api.rs +++ b/deltachat-jsonrpc/src/api.rs @@ -735,10 +735,19 @@ impl CommandApi { Ok(msg_ids) } - /// Estimate the number of messages that will be deleted - /// by the set_config()-options `delete_device_after` or `delete_server_after`. + /// Estimates the number of messages that will be deleted + /// by the `set_config()`-option `delete_device_after`. + /// /// This is typically used to show the estimated impact to the user /// before actually enabling deletion of old messages. + /// + /// Messages in the "saved messages" folder are not counted as they will not be deleted automatically. + /// + /// Parameters: + /// - `from_server`: Deprecated, pass `false` here + /// - `seconds`: Count messages older than the given number of seconds. + /// + /// Returns the number of messages that are older than the given number of seconds. async fn estimate_auto_deletion_count( &self, account_id: u32, diff --git a/deltachat-rpc-client/tests/test_folders.py b/deltachat-rpc-client/tests/test_folders.py index 85cd8e110e..3b917d6a3f 100644 --- a/deltachat-rpc-client/tests/test_folders.py +++ b/deltachat-rpc-client/tests/test_folders.py @@ -17,7 +17,7 @@ def test_moved_markseen(acfactory, direct_imap, log): log.section("ac2: creating DeltaChat folder") ac2_direct_imap = direct_imap(ac2) ac2_direct_imap.create_folder("DeltaChat") - ac2.set_config("delete_server_after", "0") + # ac2.set_config("delete_server_after", "0") # TODO check if this causes a test failure ac2.set_config("sync_msgs", "0") # Do not send a sync message when accepting a contact request. ac2.add_or_update_transport({"addr": addr, "password": password, "imapFolder": "DeltaChat"}) @@ -58,7 +58,9 @@ def test_markseen_message_and_mdn(acfactory, direct_imap): ac1, ac2 = acfactory.get_online_accounts(2) for ac in ac1, ac2: - ac.set_config("delete_server_after", "0") + # TODO check if this causes a test failure + # ac.set_config("delete_server_after", "0") + pass # Do not send BCC to self, we only want to test MDN on ac1. ac1.set_config("bcc_self", "0") @@ -91,7 +93,7 @@ def test_trash_multiple_messages(acfactory, direct_imap, log): ac1, ac2 = acfactory.get_online_accounts(2) ac2.stop_io() - ac2.set_config("delete_server_after", "0") + # TODO check if this causes a test failure: ac2.set_config("delete_server_after", "0") ac2.set_config("sync_msgs", "0") ac2.start_io() diff --git a/deltachat-rpc-client/tests/test_multidevice.py b/deltachat-rpc-client/tests/test_multidevice.py index 0ccbd6564a..008ed46573 100644 --- a/deltachat-rpc-client/tests/test_multidevice.py +++ b/deltachat-rpc-client/tests/test_multidevice.py @@ -4,41 +4,6 @@ from deltachat_rpc_client.const import MessageState -def test_bcc_self_delete_server_after_defaults(acfactory): - """Test default values for bcc_self and delete_server_after.""" - ac = acfactory.get_online_account() - - # Initially after getting online - # the setting bcc_self is set to 0 because there is only one device - # and delete_server_after is "1", meaning immediate deletion. - assert ac.get_config("bcc_self") == "0" - assert ac.get_config("delete_server_after") == "1" - - # Setup a second device. - ac_clone = ac.clone() - ac_clone.bring_online() - - # Second device setup - # enables bcc_self and changes default delete_server_after. - assert ac.get_config("bcc_self") == "1" - assert ac.get_config("delete_server_after") == "0" - - assert ac_clone.get_config("bcc_self") == "1" - assert ac_clone.get_config("delete_server_after") == "0" - - # Manually disabling bcc_self - # also restores the default for delete_server_after. - ac.set_config("bcc_self", "0") - assert ac.get_config("bcc_self") == "0" - assert ac.get_config("delete_server_after") == "1" - - # Cloning the account again enables bcc_self - # even though it was manually disabled. - ac_clone = ac.clone() - assert ac.get_config("bcc_self") == "1" - assert ac.get_config("delete_server_after") == "0" - - def test_one_account_send_bcc_setting(acfactory, log, direct_imap): ac1, ac2 = acfactory.get_online_accounts(2) ac1_clone = ac1.clone() diff --git a/deltachat-rpc-client/tests/test_something.py b/deltachat-rpc-client/tests/test_something.py index fe329b4bf7..557f3cd8f5 100644 --- a/deltachat-rpc-client/tests/test_something.py +++ b/deltachat-rpc-client/tests/test_something.py @@ -1235,7 +1235,7 @@ def test_immediate_autodelete(acfactory, direct_imap, log): ac1, ac2 = acfactory.get_online_accounts(2) # "1" means delete immediately, while "0" means do not delete - ac2.set_config("delete_server_after", "1") + # TODO check if this causes a test failure: ac2.set_config("delete_server_after", "1") log.section("ac1: create chat with ac2") chat1 = ac1.create_chat(ac2) diff --git a/python/src/deltachat/testplugin.py b/python/src/deltachat/testplugin.py index d82e794d61..3306d70077 100644 --- a/python/src/deltachat/testplugin.py +++ b/python/src/deltachat/testplugin.py @@ -521,7 +521,6 @@ def prepare_account_from_liveconfig(self, configdict) -> Account: assert "addr" in configdict and "mail_pw" in configdict, configdict configdict.setdefault("bcc_self", False) configdict.setdefault("sync_msgs", False) - configdict.setdefault("delete_server_after", 0) ac.update_config(configdict) self._acsetup._account2config[ac] = configdict self._preconfigure_key(ac) diff --git a/python/tests/test_0_complex_or_slow.py b/python/tests/test_0_complex_or_slow.py index 854bfd2cf8..3361da0128 100644 --- a/python/tests/test_0_complex_or_slow.py +++ b/python/tests/test_0_complex_or_slow.py @@ -314,7 +314,7 @@ def test_verified_group_vs_delete_server_after(acfactory, tmp_path, lp): ac2_offl = acfactory.new_online_configuring_account(cloned_from=ac2) for ac in [ac2, ac2_offl]: ac.set_config("bcc_self", "1") - ac2.set_config("delete_server_after", "1") + # TODO check if this causes a test failure: ac2.set_config("delete_server_after", "1") ac2.set_config("gossip_period", "0") # Re-gossip in every message acfactory.bring_accounts_online() dir = tmp_path / "exportdir" @@ -337,7 +337,7 @@ def test_verified_group_vs_delete_server_after(acfactory, tmp_path, lp): ac2._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_DELETED") lp.sec("ac1: sending 'hi' to the group") - ac2.set_config("delete_server_after", "0") + # TODO check if this causes a test failure: ac2.set_config("delete_server_after", "0") chat1.send_text("hi") lp.sec("ac2_offl: going online, checking the 'hi' message") diff --git a/src/config.rs b/src/config.rs index f0ca2fafb6..11b8e3cd5f 100644 --- a/src/config.rs +++ b/src/config.rs @@ -194,17 +194,6 @@ pub enum Config { #[strum(props(default = "0"))] // also change MediaQuality.default() on changes MediaQuality, - /// Timer in seconds after which the message is deleted from the - /// server. - /// - /// 0 means messages are never deleted by Delta Chat. - /// - /// Value 1 is treated as "delete at once": messages are deleted - /// immediately, without moving to DeltaChat folder. - /// - /// Default is 1 for chatmail accounts without `BccSelf`, 0 otherwise. - DeleteServerAfter, - /// Timer in seconds after which the message is deleted from the /// device. /// @@ -554,14 +543,6 @@ impl Context { // Default values let val = match key { Config::ConfiguredInboxFolder => Some("INBOX".to_string()), - Config::DeleteServerAfter => { - match !Box::pin(self.get_config_bool(Config::BccSelf)).await? - && Box::pin(self.is_chatmail()).await? - { - true => Some("1".to_string()), - false => Some("0".to_string()), - } - } Config::Addr => self.get_config_opt(Config::ConfiguredAddr).await?, _ => key.get_str("default").map(|s| s.to_string()), }; @@ -642,19 +623,15 @@ impl Context { self.get_config_bool(Config::MdnsEnabled).await } - /// Gets configured "delete_server_after" value. + /// Gets configured "delete_server_after" /// /// `None` means never delete the message, `Some(0)` means delete - /// at once, `Some(x)` means delete after `x` seconds. + /// at once, `Some(x)` is never returned + // TODO rename and refactor pub async fn get_config_delete_server_after(&self) -> Result> { - let val = match self - .get_config_parsed::(Config::DeleteServerAfter) - .await? - .unwrap_or(0) - { - 0 => None, - 1 => Some(0), - x => Some(x), + let val = match !self.get_config_bool(Config::BccSelf).await? && self.is_chatmail().await? { + true => Some(0), + false => None, }; Ok(val) } diff --git a/src/config/config_tests.rs b/src/config/config_tests.rs index af041385e9..018a4dc122 100644 --- a/src/config/config_tests.rs +++ b/src/config/config_tests.rs @@ -142,28 +142,6 @@ async fn test_mdns_default_behaviour() -> Result<()> { Ok(()) } -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn test_delete_server_after_default() -> Result<()> { - let t = &TestContext::new_alice().await; - - // Check that the settings are displayed correctly. - assert_eq!(t.get_config(Config::BccSelf).await?, Some("1".to_string())); - assert_eq!( - t.get_config(Config::DeleteServerAfter).await?, - Some("0".to_string()) - ); - - // Leaving emails on the server even w/o `BccSelf` is a good default at least because other - // MUAs do so even if the server doesn't save sent messages to some sentbox (like Gmail - // does). - t.set_config_bool(Config::BccSelf, false).await?; - assert_eq!( - t.get_config(Config::DeleteServerAfter).await?, - Some("0".to_string()) - ); - Ok(()) -} - const SAVED_MESSAGES_DEDUPLICATED_FILE: &str = "969142cb84015bc135767bc2370934a.png"; #[tokio::test(flavor = "multi_thread", worker_threads = 2)] diff --git a/src/context.rs b/src/context.rs index 95ce3c7774..4a898c234a 100644 --- a/src/context.rs +++ b/src/context.rs @@ -973,12 +973,7 @@ impl Context { .await? .to_string(), ); - res.insert( - "delete_server_after", - self.get_config_int(Config::DeleteServerAfter) - .await? - .to_string(), - ); + res.insert( "last_housekeeping", self.get_config_int(Config::LastHousekeeping) diff --git a/src/download.rs b/src/download.rs index b36f71921b..3c037cc16d 100644 --- a/src/download.rs +++ b/src/download.rs @@ -15,12 +15,6 @@ use crate::{EventType, chatlist_events}; pub(crate) mod post_msg_metadata; pub(crate) use post_msg_metadata::PostMsgMetadata; -/// If a message is downloaded only partially -/// and `delete_server_after` is set to small timeouts (eg. "at once"), -/// the user might have no chance to actually download that message. -/// `MIN_DELETE_SERVER_AFTER` increases the timeout in this case. -pub(crate) const MIN_DELETE_SERVER_AFTER: i64 = 48 * 60 * 60; - /// From this point onward outgoing messages are considered large /// and get a Pre-Message, which announces the Post-Message. /// This is only about sending so we can modify it any time. diff --git a/src/ephemeral.rs b/src/ephemeral.rs index 42bf9cb280..f3aa01d74d 100644 --- a/src/ephemeral.rs +++ b/src/ephemeral.rs @@ -23,16 +23,15 @@ //! ## Device settings //! //! In addition to per-chat ephemeral message setting, each device has -//! two global user-configured settings that complement per-chat -//! settings: `delete_device_after` and `delete_server_after`. These -//! settings are not synchronized among devices and apply to all +//! a global user-configured setting that complements per-chat +//! settings, `delete_device_after`. +//! This setting is not synchronized among devices and applies to all //! messages known to the device, including messages sent or received //! before configuring the setting. +//! It deletes messages only from the device, not from the server. //! //! `delete_device_after` configures the maximum time device is -//! storing the messages locally. `delete_server_after` configures the -//! time after which device will delete the messages it knows about -//! from the server. +//! storing the messages locally. //! //! ## How messages are deleted //! @@ -60,7 +59,7 @@ //! //! Server deletion happens by updating the `imap` table based on //! the database entries which are expired either according to their -//! ephemeral message timers or global `delete_server_after` setting. +//! ephemeral message timers. use std::cmp::max; use std::collections::BTreeSet; @@ -78,7 +77,6 @@ use crate::chat::{ChatId, ChatIdBlocked, send_msg}; use crate::constants::{DC_CHAT_ID_LAST_SPECIAL, DC_CHAT_ID_TRASH}; use crate::contact::ContactId; use crate::context::Context; -use crate::download::MIN_DELETE_SERVER_AFTER; use crate::events::EventType; use crate::log::{LogExt, warn}; use crate::message::{Message, MessageState, MsgId, Viewtype}; @@ -654,6 +652,8 @@ pub(crate) async fn ephemeral_loop(context: &Context, interrupt_receiver: Receiv #[expect(clippy::arithmetic_side_effects)] pub(crate) async fn delete_expired_imap_messages(context: &Context) -> Result<()> { let now = time(); + // TODO if is_chatmail, but not bcc_self, then delete after downloading + // apart from this, we may be able to remove the delete_server_after part let (threshold_timestamp, threshold_timestamp_extended) = match context.get_config_delete_server_after().await? { @@ -664,7 +664,7 @@ pub(crate) async fn delete_expired_imap_messages(context: &Context) -> Result<() 0 => i64::MAX, _ => now - delete_server_after, }, - now - max(delete_server_after, MIN_DELETE_SERVER_AFTER), + now - max(delete_server_after, 48 * 60 * 60), ), }; diff --git a/src/ephemeral/ephemeral_tests.rs b/src/ephemeral/ephemeral_tests.rs index 693c1b8722..663e752f6f 100644 --- a/src/ephemeral/ephemeral_tests.rs +++ b/src/ephemeral/ephemeral_tests.rs @@ -455,7 +455,6 @@ async fn test_delete_expired_imap_messages() -> Result<()> { let uidvalidity = 12345; for (id, timestamp, ephemeral_timestamp) in &[ (900, now - 2 * HOUR, 0), - (1000, now - 23 * HOUR - MIN_DELETE_SERVER_AFTER, 0), (1010, now - 23 * HOUR, 0), (1020, now - 21 * HOUR, 0), (1030, now - 19 * HOUR, 0), @@ -512,28 +511,29 @@ async fn test_delete_expired_imap_messages() -> Result<()> { 0 ); - t.set_config(Config::DeleteServerAfter, Some(&*(25 * HOUR).to_string())) - .await?; - delete_expired_imap_messages(&t).await?; - test_marked_for_deletion(&t, 1000).await?; - - MsgId::new(1000) - .update_download_state(&t, DownloadState::Available) - .await?; - t.sql - .execute("UPDATE imap SET target=folder WHERE rfc724_mid='1000'", ()) - .await?; - delete_expired_imap_messages(&t).await?; - test_marked_for_deletion(&t, 1000).await?; // Delete downloadable anyway. - remove_uid(&t, 1000).await?; - - t.set_config(Config::DeleteServerAfter, Some(&*(22 * HOUR).to_string())) - .await?; - delete_expired_imap_messages(&t).await?; - test_marked_for_deletion(&t, 1010).await?; - t.sql - .execute("UPDATE imap SET target=folder WHERE rfc724_mid='1010'", ()) - .await?; + // t.set_config(Config::DeleteServerAfter, Some(&*(25 * HOUR).to_string())) + // .await?; + // delete_expired_imap_messages(&t).await?; + // test_marked_for_deletion(&t, 1000).await?; + + // MsgId::new(1000) + // .update_download_state(&t, DownloadState::Available) + // .await?; + // t.sql + // .execute("UPDATE imap SET target=folder WHERE rfc724_mid='1000'", ()) + // .await?; + // delete_expired_imap_messages(&t).await?; + // test_marked_for_deletion(&t, 1000).await?; // Delete downloadable anyway. + // remove_uid(&t, 1000).await?; + + // t.set_config(Config::DeleteServerAfter, Some(&*(22 * HOUR).to_string())) + // .await?; + // delete_expired_imap_messages(&t).await?; + // test_marked_for_deletion(&t, 1010).await?; + // t.sql + // .execute("UPDATE imap SET target=folder WHERE rfc724_mid='1010'", ()) + // .await?; + // TODO check if removing this code makes the test fail MsgId::new(1010) .update_download_state(&t, DownloadState::Available) @@ -547,9 +547,10 @@ async fn test_delete_expired_imap_messages() -> Result<()> { 0 ); - t.set_config(Config::DeleteServerAfter, Some("1")).await?; - delete_expired_imap_messages(&t).await?; - test_marked_for_deletion(&t, 3000).await?; + // TODO instead, test that setting bcc_self to 0 removes messages + // t.set_config(Config::DeleteServerAfter, Some("1")).await?; + // delete_expired_imap_messages(&t).await?; + // test_marked_for_deletion(&t, 3000).await?; Ok(()) } diff --git a/src/imex.rs b/src/imex.rs index 5a6d22a219..e519df1067 100644 --- a/src/imex.rs +++ b/src/imex.rs @@ -979,21 +979,19 @@ mod tests { context1.set_config(Config::BccSelf, None).await?; // Check that the settings are displayed correctly. - assert_eq!( - context1.get_config(Config::DeleteServerAfter).await?, - Some("0".to_string()) - ); - context1.set_config_bool(Config::IsChatmail, true).await?; assert_eq!( context1.get_config(Config::BccSelf).await?, Some("0".to_string()) ); + context1.set_config_bool(Config::IsChatmail, true).await?; + + assert_eq!(context1.get_config(Config::IsMuted).await?, None); + context1.set_config_bool(Config::IsMuted, true).await?; assert_eq!( - context1.get_config(Config::DeleteServerAfter).await?, + context1.get_config(Config::IsMuted).await?, Some("1".to_string()) ); - assert_eq!(context1.get_config_delete_server_after().await?, Some(0)); imex(context1, ImexMode::ExportBackup, backup_dir.path(), None).await?; let _event = context1 .evtracker @@ -1010,15 +1008,15 @@ mod tests { assert!(context2.is_configured().await?); assert!(context2.is_chatmail().await?); for ctx in [context1, context2] { + // BccSelf should be enabled automatically when exporting a backup assert_eq!( ctx.get_config(Config::BccSelf).await?, Some("1".to_string()) ); assert_eq!( - ctx.get_config(Config::DeleteServerAfter).await?, - Some("0".to_string()) + ctx.get_config(Config::IsMuted).await?, + Some("1".to_string()) ); - assert_eq!(ctx.get_config_delete_server_after().await?, None); } Ok(()) } diff --git a/src/message.rs b/src/message.rs index e31bf83103..ac84bc23b1 100644 --- a/src/message.rs +++ b/src/message.rs @@ -2099,63 +2099,52 @@ pub async fn get_request_msg_cnt(context: &Context) -> usize { } /// Estimates the number of messages that will be deleted -/// by the options `delete_device_after` or `delete_server_after`. +/// by the `set_config()`-option `delete_device_after`. /// /// This is typically used to show the estimated impact to the user /// before actually enabling deletion of old messages. /// -/// If `from_server` is true, -/// estimate deletion count for server, -/// otherwise estimate deletion count for device. +/// Messages in the "saved messages" folder are not counted as they will not be deleted automatically. /// -/// Count messages older than the given number of `seconds`. +/// Parameters: +/// - `from_server`: Deprecated, pass `false` here +/// - `seconds`: Count messages older than the given number of seconds. /// /// Returns the number of messages that are older than the given number of seconds. -/// Messages in the "saved messages" folder are not counted as they will not be deleted automatically. #[expect(clippy::arithmetic_side_effects)] pub async fn estimate_deletion_cnt( context: &Context, from_server: bool, seconds: i64, ) -> Result { + ensure!( + !from_server, + "The `delete_server_after` config option was removed." + ); + let self_chat_id = ChatIdBlocked::lookup_by_contact(context, ContactId::SELF) .await? .map(|c| c.id) .unwrap_or_default(); let threshold_timestamp = time() - seconds; - let cnt = if from_server { - context - .sql - .count( - "SELECT COUNT(*) - FROM msgs m - WHERE m.id > ? - AND timestamp < ? - AND chat_id != ? - AND EXISTS (SELECT * FROM imap WHERE rfc724_mid=m.rfc724_mid);", - (DC_MSG_ID_LAST_SPECIAL, threshold_timestamp, self_chat_id), - ) - .await? - } else { - context - .sql - .count( - "SELECT COUNT(*) + let cnt = context + .sql + .count( + "SELECT COUNT(*) FROM msgs m WHERE m.id > ? AND timestamp < ? AND chat_id != ? AND chat_id != ? AND hidden = 0;", - ( - DC_MSG_ID_LAST_SPECIAL, - threshold_timestamp, - self_chat_id, - DC_CHAT_ID_TRASH, - ), - ) - .await? - }; + ( + DC_MSG_ID_LAST_SPECIAL, + threshold_timestamp, + self_chat_id, + DC_CHAT_ID_TRASH, + ), + ) + .await?; Ok(cnt) } diff --git a/src/provider/data.rs b/src/provider/data.rs index 9db00790ef..3def883aa5 100644 --- a/src/provider/data.rs +++ b/src/provider/data.rs @@ -890,16 +890,10 @@ static P_NAUTA_CU: Provider = Provider { strict_tls: false, ..ProviderOptions::new() }, - config_defaults: Some(&[ - ConfigDefault { - key: Config::DeleteServerAfter, - value: "1", - }, - ConfigDefault { - key: Config::MediaQuality, - value: "1", - }, - ]), + config_defaults: Some(&[ConfigDefault { + key: Config::MediaQuality, + value: "1", + }]), oauth2_authorizer: None, }; diff --git a/src/receive_imf/receive_imf_tests.rs b/src/receive_imf/receive_imf_tests.rs index f1aa32aba6..4c4f3cfe25 100644 --- a/src/receive_imf/receive_imf_tests.rs +++ b/src/receive_imf/receive_imf_tests.rs @@ -1976,15 +1976,16 @@ async fn test_no_smtp_job_for_self_chat() -> Result<()> { assert!(bob.pop_sent_msg_opt(Duration::ZERO).await.is_none()); bob.set_config_bool(Config::BccSelf, true).await?; - bob.set_config(Config::DeleteServerAfter, Some("1")).await?; - let mut msg = Message::new_text("Happy birthday to me".to_string()); - chat::send_msg(bob, chat_id, &mut msg).await?; - assert!(bob.pop_sent_msg_opt(Duration::ZERO).await.is_none()); - - bob.set_config(Config::DeleteServerAfter, None).await?; - let mut msg = Message::new_text("Happy birthday to me".to_string()); - chat::send_msg(bob, chat_id, &mut msg).await?; - assert!(bob.pop_sent_msg_opt(Duration::ZERO).await.is_some()); + // TODO instead, test setting bcc_self + // bob.set_config(Config::DeleteServerAfter, Some("1")).await?; + // let mut msg = Message::new_text("Happy birthday to me".to_string()); + // chat::send_msg(bob, chat_id, &mut msg).await?; + // assert!(bob.pop_sent_msg_opt(Duration::ZERO).await.is_none()); + + // bob.set_config(Config::DeleteServerAfter, None).await?; + // let mut msg = Message::new_text("Happy birthday to me".to_string()); + // chat::send_msg(bob, chat_id, &mut msg).await?; + // assert!(bob.pop_sent_msg_opt(Duration::ZERO).await.is_some()); Ok(()) } diff --git a/src/smtp.rs b/src/smtp.rs index 020c575fd3..ef86ec944e 100644 --- a/src/smtp.rs +++ b/src/smtp.rs @@ -707,18 +707,34 @@ pub(crate) async fn add_self_recipients( // and connection is frequently lost // before receiving status line. NB: This is not a problem for chatmail servers, so `BccSelf` // disabled by default is fine. - if context.get_config_delete_server_after().await? != Some(0) || !recipients.is_empty() { - // Avoid sending unencrypted messages to all transports, chatmail relays won't accept - // them. Normally the user should have a non-chatmail primary transport to send unencrypted - // messages. - if encrypted { - for addr in context.get_published_secondary_self_addrs().await? { - recipients.push(addr); - } + + // Seems like the correct replacement is `if true`. + + // Before my change, we're adding the self-recipient iff: + // - Messages are NOT deleted at once + // - OR there are recipients + // + // i.e. we skip adding the self-recipient iff: + // - Messages are deleted at once + // - AND there are no recipients + // probably because in this case, it's not necesary to send anything. + // + // Messages are deleted at once iff BccSelf is off in a chatmail profile now. + // But BccSelf is always on when this function is called. + // So, we can just remove the condition. + // TODO remove commented-out code + // if context.get_config_delete_server_after().await? != Some(0) || !recipients.is_empty() { + // Avoid sending unencrypted messages to all transports, chatmail relays won't accept + // them. Normally the user should have a non-chatmail primary transport to send unencrypted + // messages. + if encrypted { + for addr in context.get_published_secondary_self_addrs().await? { + recipients.push(addr); } - // `from` must be the last addr, see `receive_imf_inner()` why. - let from = context.get_primary_self_addr().await?; - recipients.push(from); } + // `from` must be the last addr, see `receive_imf_inner()` why. + let from = context.get_primary_self_addr().await?; + recipients.push(from); + // } Ok(()) } From f174d14f745f7567dac718d8c0a96affcac4c24c Mon Sep 17 00:00:00 2001 From: Hocuri Date: Wed, 6 May 2026 14:18:55 +0200 Subject: [PATCH 02/20] Mark message for deletion right after receiving it --- src/config.rs | 13 ------------- src/ephemeral.rs | 23 ++--------------------- src/imap.rs | 24 ++++++++++++++++++++++++ src/receive_imf.rs | 8 +++++--- 4 files changed, 31 insertions(+), 37 deletions(-) diff --git a/src/config.rs b/src/config.rs index 11b8e3cd5f..63cf1d4424 100644 --- a/src/config.rs +++ b/src/config.rs @@ -623,19 +623,6 @@ impl Context { self.get_config_bool(Config::MdnsEnabled).await } - /// Gets configured "delete_server_after" - /// - /// `None` means never delete the message, `Some(0)` means delete - /// at once, `Some(x)` is never returned - // TODO rename and refactor - pub async fn get_config_delete_server_after(&self) -> Result> { - let val = match !self.get_config_bool(Config::BccSelf).await? && self.is_chatmail().await? { - true => Some(0), - false => None, - }; - Ok(val) - } - /// Gets the configured provider. /// /// The provider is determined by the current primary transport. diff --git a/src/ephemeral.rs b/src/ephemeral.rs index f3aa01d74d..5ab5483af3 100644 --- a/src/ephemeral.rs +++ b/src/ephemeral.rs @@ -61,7 +61,6 @@ //! the database entries which are expired either according to their //! ephemeral message timers. -use std::cmp::max; use std::collections::BTreeSet; use std::fmt; use std::num::ParseIntError; @@ -652,22 +651,6 @@ pub(crate) async fn ephemeral_loop(context: &Context, interrupt_receiver: Receiv #[expect(clippy::arithmetic_side_effects)] pub(crate) async fn delete_expired_imap_messages(context: &Context) -> Result<()> { let now = time(); - // TODO if is_chatmail, but not bcc_self, then delete after downloading - // apart from this, we may be able to remove the delete_server_after part - - let (threshold_timestamp, threshold_timestamp_extended) = - match context.get_config_delete_server_after().await? { - None => (0, 0), - Some(delete_server_after) => ( - match delete_server_after { - // Guarantee immediate deletion. - 0 => i64::MAX, - _ => now - delete_server_after, - }, - now - max(delete_server_after, 48 * 60 * 60), - ), - }; - context .sql .execute( @@ -675,11 +658,9 @@ pub(crate) async fn delete_expired_imap_messages(context: &Context) -> Result<() SET target='' WHERE rfc724_mid IN ( SELECT rfc724_mid FROM msgs - WHERE ((download_state = 0 AND timestamp < ?) OR - (download_state != 0 AND timestamp < ?) OR - (ephemeral_timestamp != 0 AND ephemeral_timestamp <= ?)) + WHERE ephemeral_timestamp != 0 AND ephemeral_timestamp <= ? )", - (threshold_timestamp, threshold_timestamp_extended, now), + (now,), ) .await?; diff --git a/src/imap.rs b/src/imap.rs index bedc0376cc..d763172d93 100644 --- a/src/imap.rs +++ b/src/imap.rs @@ -1281,6 +1281,7 @@ impl Session { if request_uids.is_empty() { return Ok(()); } + let is_chatmail = self.is_chatmail(); for (request_uids, set) in build_sequence_sets(&request_uids)? { info!(context, "Starting UID FETCH of message set \"{}\".", set); @@ -1379,6 +1380,29 @@ impl Session { "Passing message UID {} to receive_imf().", request_uid ); let res = receive_imf_inner(context, rfc724_mid, body, is_seen).await; + + // If the message is not needed anymore on the server, mark it for deletion: + info!( + context, + "dbg Marking for deletion?: bcc_self={}, is_chatmail={}", + context.get_config_bool(Config::BccSelf).await?, + is_chatmail + ); + if !context.get_config_bool(Config::BccSelf).await? && is_chatmail { + info!(context, "dbg Marking {rfc724_mid} for deletion"); + context + .sql + .execute( + &format!("UPDATE imap SET target='' WHERE rfc724_mid=?"), + (rfc724_mid,), + ) + .await?; + context.scheduler.interrupt_inbox().await; + } else { + info!(context, "dbg NOT marking {rfc724_mid} for deletion"); + } + + // If there was an error receiving the message, show a device message: let received_msg = match res { Err(err) => { warn!(context, "receive_imf error: {err:#}."); diff --git a/src/receive_imf.rs b/src/receive_imf.rs index 62db198105..218c149017 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -902,10 +902,12 @@ UPDATE config SET value=? WHERE keyname='configured_addr' AND value!=?1 } // Get user-configured server deletion - let delete_server_after = context.get_config_delete_server_after().await?; - if !received_msg.msg_ids.is_empty() { - let target = if received_msg.needs_delete_job || delete_server_after == Some(0) { + info!( + context, + "dbg Would mark for deletion previously: rfc724_mid={rfc724_mid}, rfc724_mid_orig={rfc724_mid_orig}" + ); + let target = if received_msg.needs_delete_job { Some("".to_string()) } else { None From e29d21876d85534a58fddc4560dfb1b326ce6284 Mon Sep 17 00:00:00 2001 From: Hocuri Date: Wed, 6 May 2026 14:23:07 +0200 Subject: [PATCH 03/20] Linters --- deltachat-rpc-client/tests/test_folders.py | 6 ------ src/ephemeral.rs | 1 - src/imap.rs | 2 +- 3 files changed, 1 insertion(+), 8 deletions(-) diff --git a/deltachat-rpc-client/tests/test_folders.py b/deltachat-rpc-client/tests/test_folders.py index 3b917d6a3f..132dfb6170 100644 --- a/deltachat-rpc-client/tests/test_folders.py +++ b/deltachat-rpc-client/tests/test_folders.py @@ -17,7 +17,6 @@ def test_moved_markseen(acfactory, direct_imap, log): log.section("ac2: creating DeltaChat folder") ac2_direct_imap = direct_imap(ac2) ac2_direct_imap.create_folder("DeltaChat") - # ac2.set_config("delete_server_after", "0") # TODO check if this causes a test failure ac2.set_config("sync_msgs", "0") # Do not send a sync message when accepting a contact request. ac2.add_or_update_transport({"addr": addr, "password": password, "imapFolder": "DeltaChat"}) @@ -57,11 +56,6 @@ def test_moved_markseen(acfactory, direct_imap, log): def test_markseen_message_and_mdn(acfactory, direct_imap): ac1, ac2 = acfactory.get_online_accounts(2) - for ac in ac1, ac2: - # TODO check if this causes a test failure - # ac.set_config("delete_server_after", "0") - pass - # Do not send BCC to self, we only want to test MDN on ac1. ac1.set_config("bcc_self", "0") diff --git a/src/ephemeral.rs b/src/ephemeral.rs index 5ab5483af3..35b6bafbed 100644 --- a/src/ephemeral.rs +++ b/src/ephemeral.rs @@ -648,7 +648,6 @@ pub(crate) async fn ephemeral_loop(context: &Context, interrupt_receiver: Receiv } /// Schedules expired IMAP messages for deletion. -#[expect(clippy::arithmetic_side_effects)] pub(crate) async fn delete_expired_imap_messages(context: &Context) -> Result<()> { let now = time(); context diff --git a/src/imap.rs b/src/imap.rs index d763172d93..33a432694a 100644 --- a/src/imap.rs +++ b/src/imap.rs @@ -1393,7 +1393,7 @@ impl Session { context .sql .execute( - &format!("UPDATE imap SET target='' WHERE rfc724_mid=?"), + "UPDATE imap SET target='' WHERE rfc724_mid=?", (rfc724_mid,), ) .await?; From 850b782493c40c5b74bf377938289841a9c3877a Mon Sep 17 00:00:00 2001 From: Hocuri Date: Wed, 6 May 2026 14:27:56 +0200 Subject: [PATCH 04/20] Fix test_trash_multiple_messages() --- deltachat-rpc-client/tests/test_folders.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deltachat-rpc-client/tests/test_folders.py b/deltachat-rpc-client/tests/test_folders.py index 132dfb6170..c4e1a02d74 100644 --- a/deltachat-rpc-client/tests/test_folders.py +++ b/deltachat-rpc-client/tests/test_folders.py @@ -87,7 +87,7 @@ def test_trash_multiple_messages(acfactory, direct_imap, log): ac1, ac2 = acfactory.get_online_accounts(2) ac2.stop_io() - # TODO check if this causes a test failure: ac2.set_config("delete_server_after", "0") + ac2.set_config("bcc_self", "1") # Make sure that messages are not immediately auto-deleted ac2.set_config("sync_msgs", "0") ac2.start_io() From 7ba752b5e6aec0d51d263aa185099f0001a46499 Mon Sep 17 00:00:00 2001 From: Hocuri Date: Wed, 6 May 2026 14:39:29 +0200 Subject: [PATCH 05/20] Fix test_markseen_message_and_mdn --- deltachat-rpc-client/tests/test_folders.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/deltachat-rpc-client/tests/test_folders.py b/deltachat-rpc-client/tests/test_folders.py index c4e1a02d74..307b5ced6a 100644 --- a/deltachat-rpc-client/tests/test_folders.py +++ b/deltachat-rpc-client/tests/test_folders.py @@ -56,8 +56,9 @@ def test_moved_markseen(acfactory, direct_imap, log): def test_markseen_message_and_mdn(acfactory, direct_imap): ac1, ac2 = acfactory.get_online_accounts(2) - # Do not send BCC to self, we only want to test MDN on ac1. - ac1.set_config("bcc_self", "0") + # Make sure that messages are not immediately auto-deleted: + ac1.set_config("bcc_self", "1") + ac2.set_config("bcc_self", "1") acfactory.get_accepted_chat(ac1, ac2).send_text("hi") msg = ac2.wait_for_incoming_msg() @@ -77,10 +78,9 @@ def test_markseen_message_and_mdn(acfactory, direct_imap): ac1_direct_imap.select_folder("INBOX") ac2_direct_imap.select_folder("INBOX") - # Check that the mdn is marked as seen - assert len(list(ac1_direct_imap.conn.fetch(AND(seen=True), mark_seen=False))) == 1 - # Check original message is marked as seen - assert len(list(ac2_direct_imap.conn.fetch(AND(seen=True), mark_seen=False))) == 1 + # Check that the mdn and original message is marked as seen + assert len(list(ac1_direct_imap.conn.fetch(AND(seen=True), mark_seen=False))) == 2 + assert len(list(ac2_direct_imap.conn.fetch(AND(seen=True), mark_seen=False))) == 2 def test_trash_multiple_messages(acfactory, direct_imap, log): From d315257fb1f33ba278cfff2916f178cd390c09ec Mon Sep 17 00:00:00 2001 From: Hocuri Date: Wed, 6 May 2026 14:42:52 +0200 Subject: [PATCH 06/20] Fix test_moved_markseen() --- deltachat-rpc-client/tests/test_folders.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/deltachat-rpc-client/tests/test_folders.py b/deltachat-rpc-client/tests/test_folders.py index 307b5ced6a..b2dbb642ae 100644 --- a/deltachat-rpc-client/tests/test_folders.py +++ b/deltachat-rpc-client/tests/test_folders.py @@ -14,6 +14,10 @@ def test_moved_markseen(acfactory, direct_imap, log): ac2.add_or_update_transport({"addr": addr, "password": password}) ac2.bring_online() + # Make sure that messages are not immediately auto-deleted: + ac1.set_config("bcc_self", "1") + ac2.set_config("bcc_self", "1") + log.section("ac2: creating DeltaChat folder") ac2_direct_imap = direct_imap(ac2) ac2_direct_imap.create_folder("DeltaChat") From 2bc53fa717ffe4c9b2501a36f5bf1726a3d6486c Mon Sep 17 00:00:00 2001 From: Hocuri Date: Wed, 6 May 2026 15:16:31 +0200 Subject: [PATCH 07/20] Try to fix test_delete_multiple_messages(), remove some commented-out code, add some comments --- deltachat-rpc-client/tests/test_something.py | 4 +++ python/tests/test_1_online.py | 2 ++ src/ephemeral/ephemeral_tests.rs | 29 ----------------- src/receive_imf/receive_imf_tests.rs | 11 ------- src/smtp.rs | 34 ++++---------------- 5 files changed, 13 insertions(+), 67 deletions(-) diff --git a/deltachat-rpc-client/tests/test_something.py b/deltachat-rpc-client/tests/test_something.py index 557f3cd8f5..90fe36d522 100644 --- a/deltachat-rpc-client/tests/test_something.py +++ b/deltachat-rpc-client/tests/test_something.py @@ -1232,6 +1232,10 @@ def test_leave_and_delete_group(acfactory, log): def test_immediate_autodelete(acfactory, direct_imap, log): + """ + `bcc_self` is off by default, + so that messages are supposed to be immediately autodeleted + """ ac1, ac2 = acfactory.get_online_accounts(2) # "1" means delete immediately, while "0" means do not delete diff --git a/python/tests/test_1_online.py b/python/tests/test_1_online.py index 236da0ceae..6b54182725 100644 --- a/python/tests/test_1_online.py +++ b/python/tests/test_1_online.py @@ -1073,6 +1073,8 @@ def test_send_receive_locations(acfactory, lp): def test_delete_multiple_messages(acfactory, lp): ac1, ac2 = acfactory.get_online_accounts(2) + # Make sure that messages are not immediately auto-deleted: + ac2.set_config("bcc_self", "1") chat12 = acfactory.get_accepted_chat(ac1, ac2) lp.sec("ac1: sending seven messages") diff --git a/src/ephemeral/ephemeral_tests.rs b/src/ephemeral/ephemeral_tests.rs index 663e752f6f..543ad68b95 100644 --- a/src/ephemeral/ephemeral_tests.rs +++ b/src/ephemeral/ephemeral_tests.rs @@ -511,30 +511,6 @@ async fn test_delete_expired_imap_messages() -> Result<()> { 0 ); - // t.set_config(Config::DeleteServerAfter, Some(&*(25 * HOUR).to_string())) - // .await?; - // delete_expired_imap_messages(&t).await?; - // test_marked_for_deletion(&t, 1000).await?; - - // MsgId::new(1000) - // .update_download_state(&t, DownloadState::Available) - // .await?; - // t.sql - // .execute("UPDATE imap SET target=folder WHERE rfc724_mid='1000'", ()) - // .await?; - // delete_expired_imap_messages(&t).await?; - // test_marked_for_deletion(&t, 1000).await?; // Delete downloadable anyway. - // remove_uid(&t, 1000).await?; - - // t.set_config(Config::DeleteServerAfter, Some(&*(22 * HOUR).to_string())) - // .await?; - // delete_expired_imap_messages(&t).await?; - // test_marked_for_deletion(&t, 1010).await?; - // t.sql - // .execute("UPDATE imap SET target=folder WHERE rfc724_mid='1010'", ()) - // .await?; - // TODO check if removing this code makes the test fail - MsgId::new(1010) .update_download_state(&t, DownloadState::Available) .await?; @@ -547,11 +523,6 @@ async fn test_delete_expired_imap_messages() -> Result<()> { 0 ); - // TODO instead, test that setting bcc_self to 0 removes messages - // t.set_config(Config::DeleteServerAfter, Some("1")).await?; - // delete_expired_imap_messages(&t).await?; - // test_marked_for_deletion(&t, 3000).await?; - Ok(()) } diff --git a/src/receive_imf/receive_imf_tests.rs b/src/receive_imf/receive_imf_tests.rs index 4c4f3cfe25..42764fcb7e 100644 --- a/src/receive_imf/receive_imf_tests.rs +++ b/src/receive_imf/receive_imf_tests.rs @@ -1975,17 +1975,6 @@ async fn test_no_smtp_job_for_self_chat() -> Result<()> { chat::send_msg(bob, chat_id, &mut msg).await?; assert!(bob.pop_sent_msg_opt(Duration::ZERO).await.is_none()); - bob.set_config_bool(Config::BccSelf, true).await?; - // TODO instead, test setting bcc_self - // bob.set_config(Config::DeleteServerAfter, Some("1")).await?; - // let mut msg = Message::new_text("Happy birthday to me".to_string()); - // chat::send_msg(bob, chat_id, &mut msg).await?; - // assert!(bob.pop_sent_msg_opt(Duration::ZERO).await.is_none()); - - // bob.set_config(Config::DeleteServerAfter, None).await?; - // let mut msg = Message::new_text("Happy birthday to me".to_string()); - // chat::send_msg(bob, chat_id, &mut msg).await?; - // assert!(bob.pop_sent_msg_opt(Duration::ZERO).await.is_some()); Ok(()) } diff --git a/src/smtp.rs b/src/smtp.rs index ef86ec944e..5065fbdf87 100644 --- a/src/smtp.rs +++ b/src/smtp.rs @@ -699,31 +699,6 @@ pub(crate) async fn add_self_recipients( recipients: &mut Vec, encrypted: bool, ) -> Result<()> { - // Previous versions of Delta Chat did not send BCC self - // if DeleteServerAfter was set to immediately delete messages - // from the server. This is not the case anymore - // because BCC-self messages are also used to detect - // that message was sent if SMTP server is slow to respond - // and connection is frequently lost - // before receiving status line. NB: This is not a problem for chatmail servers, so `BccSelf` - // disabled by default is fine. - - // Seems like the correct replacement is `if true`. - - // Before my change, we're adding the self-recipient iff: - // - Messages are NOT deleted at once - // - OR there are recipients - // - // i.e. we skip adding the self-recipient iff: - // - Messages are deleted at once - // - AND there are no recipients - // probably because in this case, it's not necesary to send anything. - // - // Messages are deleted at once iff BccSelf is off in a chatmail profile now. - // But BccSelf is always on when this function is called. - // So, we can just remove the condition. - // TODO remove commented-out code - // if context.get_config_delete_server_after().await? != Some(0) || !recipients.is_empty() { // Avoid sending unencrypted messages to all transports, chatmail relays won't accept // them. Normally the user should have a non-chatmail primary transport to send unencrypted // messages. @@ -732,9 +707,14 @@ pub(crate) async fn add_self_recipients( recipients.push(addr); } } - // `from` must be the last addr, see `receive_imf_inner()` why. + // `from` must be the last addr + // because `receive_imf_inner()` marks the message as 'delivered' + // if it arrives to the self-server via `bcc_self`. + // This helps with marking messages as delivered + // if the server is slow and we never get an `OK` response + // before the connection times out. let from = context.get_primary_self_addr().await?; recipients.push(from); - // } + Ok(()) } From 95afe2388789e0961dd3014350cb6da28b77a1e6 Mon Sep 17 00:00:00 2001 From: Hocuri Date: Wed, 6 May 2026 15:25:12 +0200 Subject: [PATCH 08/20] Try to fix test_webxdc_message(), test_send_and_receive_message_markseen(), test_mdn_asymmetric() --- deltachat-rpc-client/tests/test_folders.py | 4 ++-- python/tests/test_1_online.py | 11 ++++++++++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/deltachat-rpc-client/tests/test_folders.py b/deltachat-rpc-client/tests/test_folders.py index b2dbb642ae..bbf6d15208 100644 --- a/deltachat-rpc-client/tests/test_folders.py +++ b/deltachat-rpc-client/tests/test_folders.py @@ -14,7 +14,7 @@ def test_moved_markseen(acfactory, direct_imap, log): ac2.add_or_update_transport({"addr": addr, "password": password}) ac2.bring_online() - # Make sure that messages are not immediately auto-deleted: + # Make sure that messages are not immediately auto-deleted on the server: ac1.set_config("bcc_self", "1") ac2.set_config("bcc_self", "1") @@ -60,7 +60,7 @@ def test_moved_markseen(acfactory, direct_imap, log): def test_markseen_message_and_mdn(acfactory, direct_imap): ac1, ac2 = acfactory.get_online_accounts(2) - # Make sure that messages are not immediately auto-deleted: + # Make sure that messages are not immediately auto-deleted on the server: ac1.set_config("bcc_self", "1") ac2.set_config("bcc_self", "1") diff --git a/python/tests/test_1_online.py b/python/tests/test_1_online.py index 6b54182725..dd0afe7c69 100644 --- a/python/tests/test_1_online.py +++ b/python/tests/test_1_online.py @@ -162,6 +162,9 @@ def test_webxdc_message(acfactory, data, lp): ac1, ac2 = acfactory.get_online_accounts(2) chat = acfactory.get_accepted_chat(ac1, ac2) + # Make sure that messages are not immediately auto-deleted on the server: + ac2.set_config("bcc_self", "1") + lp.sec("ac1: prepare and send text message to ac2") msg1 = chat.send_text("message0") assert not msg1.is_webxdc() @@ -362,6 +365,9 @@ def test_send_and_receive_message_markseen(acfactory, lp): # make DC's life harder wrt to encodings ac1.set_config("displayname", "ä name") + # Make sure that messages are not immediately auto-deleted on the server: + ac2.set_config("bcc_self", "1") + # clear any fresh device messages ac1.get_device_chat().mark_noticed() ac2.get_device_chat().mark_noticed() @@ -506,6 +512,9 @@ def test_mdn_asymmetric(acfactory, lp): ac1.set_config("mdns_enabled", "1") ac2.set_config("mdns_enabled", "1") + # Make sure that the mdn is not immediately auto-deleted on the server:: + ac1.set_config("bcc_self", "1") + lp.sec("sending text message from ac1 to ac2") msg_out = chat.send_text("message1") @@ -1073,7 +1082,7 @@ def test_send_receive_locations(acfactory, lp): def test_delete_multiple_messages(acfactory, lp): ac1, ac2 = acfactory.get_online_accounts(2) - # Make sure that messages are not immediately auto-deleted: + # Make sure that messages are not immediately auto-deleted on the server: ac2.set_config("bcc_self", "1") chat12 = acfactory.get_accepted_chat(ac1, ac2) From 10f151522ec23165a4f326d16d2fc105a661606f Mon Sep 17 00:00:00 2001 From: Hocuri Date: Wed, 6 May 2026 15:25:49 +0200 Subject: [PATCH 09/20] Remove test_verified_group_vs_delete_server_after() because the feature it tests was removed --- python/tests/test_0_complex_or_slow.py | 67 -------------------------- 1 file changed, 67 deletions(-) diff --git a/python/tests/test_0_complex_or_slow.py b/python/tests/test_0_complex_or_slow.py index 3361da0128..151f72247d 100644 --- a/python/tests/test_0_complex_or_slow.py +++ b/python/tests/test_0_complex_or_slow.py @@ -298,73 +298,6 @@ def test_use_new_verified_group_after_going_online(acfactory, data, tmp_path, lp assert msg_in.text == msg_out.text -def test_verified_group_vs_delete_server_after(acfactory, tmp_path, lp): - """Test for the issue #4346: - - User is added to a verified group. - - First device of the user downloads "member added" from the group. - - First device removes "member added" from the server. - - Some new messages are sent to the group. - - Second device comes online, receives these new messages. - The result is an unverified group with unverified members. - - First device re-gossips Autocrypt keys to the group. - - Now the second device has all members and group verified. - """ - ac1, ac2 = acfactory.get_online_accounts(2) - acfactory.remove_preconfigured_keys() - ac2_offl = acfactory.new_online_configuring_account(cloned_from=ac2) - for ac in [ac2, ac2_offl]: - ac.set_config("bcc_self", "1") - # TODO check if this causes a test failure: ac2.set_config("delete_server_after", "1") - ac2.set_config("gossip_period", "0") # Re-gossip in every message - acfactory.bring_accounts_online() - dir = tmp_path / "exportdir" - dir.mkdir() - ac2.export_self_keys(str(dir)) - ac2_offl.import_self_keys(str(dir)) - ac2_offl.stop_io() - - lp.sec("ac1: create verified-group QR, ac2 scans and joins") - chat1 = ac1.create_group_chat("hello") - qr = chat1.get_join_qr() - lp.sec("ac2: start QR-code based join-group protocol") - chat2 = ac2.qr_join_chat(qr) - ac1._evtracker.wait_securejoin_inviter_progress(1000) - # Wait for "Member Me () added by ." message. - msg_in = ac2._evtracker.wait_next_incoming_message() - assert msg_in.is_system_message() - - lp.sec("ac2: waiting for 'member added' to be deleted on the server") - ac2._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_DELETED") - - lp.sec("ac1: sending 'hi' to the group") - # TODO check if this causes a test failure: ac2.set_config("delete_server_after", "0") - chat1.send_text("hi") - - lp.sec("ac2_offl: going online, checking the 'hi' message") - ac2_offl.start_io() - msg_in = ac2_offl._evtracker.wait_next_incoming_message() - assert not msg_in.is_system_message() - assert msg_in.text == "hi" - ac2_offl_ac1_contact = msg_in.get_sender_contact() - assert ac2_offl_ac1_contact.addr == ac1.get_config("addr") - assert not ac2_offl_ac1_contact.is_verified() - chat2_offl = msg_in.chat - - lp.sec("ac2: sending message re-gossiping Autocrypt keys") - chat2.send_text("hi2") - - lp.sec("ac2_offl: receiving message") - ev = ac2_offl._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED") - msg_in = ac2_offl.get_message_by_id(ev.data2) - assert not msg_in.is_system_message() - assert msg_in.text == "hi2" - assert msg_in.chat == chat2_offl - assert msg_in.get_sender_contact().addr == ac2.get_config("addr") - # Until we reset verifications and then send the _verified header, - # verification is not gossiped here: - assert not ac2_offl_ac1_contact.is_verified() - - def test_deleted_msgs_dont_reappear(acfactory): ac1 = acfactory.new_online_configuring_account() acfactory.bring_accounts_online() From 7fb8949b60172cc852fcfbec875f5dd22c1c98a9 Mon Sep 17 00:00:00 2001 From: Hocuri Date: Wed, 6 May 2026 15:27:20 +0200 Subject: [PATCH 10/20] Try to fix test_basic_imap_api() --- python/tests/test_1_online.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/python/tests/test_1_online.py b/python/tests/test_1_online.py index dd0afe7c69..cc45518a6b 100644 --- a/python/tests/test_1_online.py +++ b/python/tests/test_1_online.py @@ -15,6 +15,9 @@ def test_basic_imap_api(acfactory, tmp_path): ac1, ac2 = acfactory.get_online_accounts(2) chat12 = acfactory.get_accepted_chat(ac1, ac2) + # Make sure that messages are not immediately auto-deleted on the server: + ac2.set_config("bcc_self", "1") + imap2 = ac2.direct_imap with imap2.idle() as idle2: From e4eb798c17e70ac382fa8627c1d809f8d7b6a872 Mon Sep 17 00:00:00 2001 From: Hocuri Date: Wed, 6 May 2026 16:19:19 +0200 Subject: [PATCH 11/20] Next try to fix test_send_and_receive_message_markseen() and test_mdn_asymmetric() --- python/tests/test_1_online.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/python/tests/test_1_online.py b/python/tests/test_1_online.py index cc45518a6b..97ac0bd31d 100644 --- a/python/tests/test_1_online.py +++ b/python/tests/test_1_online.py @@ -369,6 +369,7 @@ def test_send_and_receive_message_markseen(acfactory, lp): ac1.set_config("displayname", "ä name") # Make sure that messages are not immediately auto-deleted on the server: + ac1.set_config("bcc_self", "1") ac2.set_config("bcc_self", "1") # clear any fresh device messages @@ -515,12 +516,15 @@ def test_mdn_asymmetric(acfactory, lp): ac1.set_config("mdns_enabled", "1") ac2.set_config("mdns_enabled", "1") - # Make sure that the mdn is not immediately auto-deleted on the server:: + # Make sure that the mdn is not immediately auto-deleted on the server: ac1.set_config("bcc_self", "1") lp.sec("sending text message from ac1 to ac2") msg_out = chat.send_text("message1") + # Wait for the message to be marked as seen on IMAP. + ac1._evtracker.get_info_contains("Marked messages [0-9]+ in folder INBOX as seen.") + assert len(chat.get_messages()) == 1 + E2EE_INFO_MSGS lp.sec("disable ac1 MDNs") @@ -537,7 +541,7 @@ def test_mdn_asymmetric(acfactory, lp): lp.sec("ac1: waiting for incoming activity") assert len(chat.get_messages()) == 1 + E2EE_INFO_MSGS - # Wait for the message to be marked as seen on IMAP. + # Wait for the mdn to be marked as seen on IMAP. ac1._evtracker.get_info_contains("Marked messages [0-9]+ in folder INBOX as seen.") # MDN is received even though MDNs are already disabled From 5e02f3ca53abc58ae189c882e370c3f56924b3e4 Mon Sep 17 00:00:00 2001 From: Hocuri Date: Wed, 6 May 2026 16:31:38 +0200 Subject: [PATCH 12/20] small comment tweaks --- deltachat-rpc-client/tests/test_folders.py | 4 +++- deltachat-rpc-client/tests/test_something.py | 3 --- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/deltachat-rpc-client/tests/test_folders.py b/deltachat-rpc-client/tests/test_folders.py index bbf6d15208..85a5e03128 100644 --- a/deltachat-rpc-client/tests/test_folders.py +++ b/deltachat-rpc-client/tests/test_folders.py @@ -91,7 +91,9 @@ def test_trash_multiple_messages(acfactory, direct_imap, log): ac1, ac2 = acfactory.get_online_accounts(2) ac2.stop_io() - ac2.set_config("bcc_self", "1") # Make sure that messages are not immediately auto-deleted + # Make sure that messages are not immediately auto-deleted on the server: + ac2.set_config("bcc_self", "1") + ac2.set_config("sync_msgs", "0") ac2.start_io() diff --git a/deltachat-rpc-client/tests/test_something.py b/deltachat-rpc-client/tests/test_something.py index 90fe36d522..74e9e1af9c 100644 --- a/deltachat-rpc-client/tests/test_something.py +++ b/deltachat-rpc-client/tests/test_something.py @@ -1238,9 +1238,6 @@ def test_immediate_autodelete(acfactory, direct_imap, log): """ ac1, ac2 = acfactory.get_online_accounts(2) - # "1" means delete immediately, while "0" means do not delete - # TODO check if this causes a test failure: ac2.set_config("delete_server_after", "1") - log.section("ac1: create chat with ac2") chat1 = ac1.create_chat(ac2) ac2.create_chat(ac1) From 13c58670dab1b6b20f53aea49cfa15cb0d8f2cbb Mon Sep 17 00:00:00 2001 From: Hocuri Date: Wed, 6 May 2026 16:40:26 +0200 Subject: [PATCH 13/20] Restore one of the removed tests --- .../tests/test_multidevice.py | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/deltachat-rpc-client/tests/test_multidevice.py b/deltachat-rpc-client/tests/test_multidevice.py index 008ed46573..14470b6c00 100644 --- a/deltachat-rpc-client/tests/test_multidevice.py +++ b/deltachat-rpc-client/tests/test_multidevice.py @@ -4,6 +4,31 @@ from deltachat_rpc_client.const import MessageState +def test_bcc_self_is_enabled_when_setting_up_second_device(acfactory): + ac = acfactory.get_online_account() + + # Initially after getting online + # the setting bcc_self is set to 0 because there is only one device + assert ac.get_config("bcc_self") == "0" + + # Setup a second device. + ac_clone = ac.clone() + ac_clone.bring_online() + + # Second device setup enables bcc_self. + assert ac.get_config("bcc_self") == "1" + assert ac_clone.get_config("bcc_self") == "1" + + # Test manually disabling bcc_self + ac.set_config("bcc_self", "0") + assert ac.get_config("bcc_self") == "0" + + # Cloning the account again enables bcc_self again + # even though it was manually disabled. + ac_clone = ac.clone() + assert ac.get_config("bcc_self") == "1" + + def test_one_account_send_bcc_setting(acfactory, log, direct_imap): ac1, ac2 = acfactory.get_online_accounts(2) ac1_clone = ac1.clone() From 0e3897c5ef230b41506751bb68f8daea47f2dc29 Mon Sep 17 00:00:00 2001 From: Hocuri Date: Wed, 6 May 2026 16:48:21 +0200 Subject: [PATCH 14/20] More small tweaks after self-rewiewing --- src/context.rs | 1 - src/ephemeral.rs | 4 ++-- src/imap.rs | 9 --------- src/message.rs | 2 +- src/receive_imf.rs | 4 ---- 5 files changed, 3 insertions(+), 17 deletions(-) diff --git a/src/context.rs b/src/context.rs index 4a898c234a..6bfe2465e0 100644 --- a/src/context.rs +++ b/src/context.rs @@ -973,7 +973,6 @@ impl Context { .await? .to_string(), ); - res.insert( "last_housekeeping", self.get_config_int(Config::LastHousekeeping) diff --git a/src/ephemeral.rs b/src/ephemeral.rs index 35b6bafbed..c44acb96d3 100644 --- a/src/ephemeral.rs +++ b/src/ephemeral.rs @@ -28,10 +28,10 @@ //! This setting is not synchronized among devices and applies to all //! messages known to the device, including messages sent or received //! before configuring the setting. -//! It deletes messages only from the device, not from the server. //! //! `delete_device_after` configures the maximum time device is -//! storing the messages locally. +//! storing the messages locally, +//! but does not delete messages from the server. //! //! ## How messages are deleted //! diff --git a/src/imap.rs b/src/imap.rs index 33a432694a..896ee1c851 100644 --- a/src/imap.rs +++ b/src/imap.rs @@ -1382,14 +1382,7 @@ impl Session { let res = receive_imf_inner(context, rfc724_mid, body, is_seen).await; // If the message is not needed anymore on the server, mark it for deletion: - info!( - context, - "dbg Marking for deletion?: bcc_self={}, is_chatmail={}", - context.get_config_bool(Config::BccSelf).await?, - is_chatmail - ); if !context.get_config_bool(Config::BccSelf).await? && is_chatmail { - info!(context, "dbg Marking {rfc724_mid} for deletion"); context .sql .execute( @@ -1398,8 +1391,6 @@ impl Session { ) .await?; context.scheduler.interrupt_inbox().await; - } else { - info!(context, "dbg NOT marking {rfc724_mid} for deletion"); } // If there was an error receiving the message, show a device message: diff --git a/src/message.rs b/src/message.rs index ac84bc23b1..abf26b83a2 100644 --- a/src/message.rs +++ b/src/message.rs @@ -2119,7 +2119,7 @@ pub async fn estimate_deletion_cnt( ) -> Result { ensure!( !from_server, - "The `delete_server_after` config option was removed." + "The `delete_server_after` config option was removed. You need to pass `false` for `from_server`." ); let self_chat_id = ChatIdBlocked::lookup_by_contact(context, ContactId::SELF) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index 218c149017..005ec14cf5 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -903,10 +903,6 @@ UPDATE config SET value=? WHERE keyname='configured_addr' AND value!=?1 // Get user-configured server deletion if !received_msg.msg_ids.is_empty() { - info!( - context, - "dbg Would mark for deletion previously: rfc724_mid={rfc724_mid}, rfc724_mid_orig={rfc724_mid_orig}" - ); let target = if received_msg.needs_delete_job { Some("".to_string()) } else { From 6c862ed298abb33c41379c92a208c04f6d204b22 Mon Sep 17 00:00:00 2001 From: Hocuri Date: Wed, 6 May 2026 22:31:28 +0200 Subject: [PATCH 15/20] Update provider database --- scripts/update-provider-database.sh | 2 +- src/provider/data.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/update-provider-database.sh b/scripts/update-provider-database.sh index d905ca95dc..dfde951aa3 100755 --- a/scripts/update-provider-database.sh +++ b/scripts/update-provider-database.sh @@ -6,7 +6,7 @@ set -euo pipefail export TZ=UTC # Provider database revision. -REV=ad097ee40579c884e7757de2d3bb0a51f481a32a +REV=2cba4b72f4c6e6417b83ba549aff7781be5f166c CORE_ROOT="$PWD" TMP="$(mktemp -d)" diff --git a/src/provider/data.rs b/src/provider/data.rs index 3def883aa5..bc9293f901 100644 --- a/src/provider/data.rs +++ b/src/provider/data.rs @@ -2376,4 +2376,4 @@ pub(crate) static PROVIDER_IDS: LazyLock = - LazyLock::new(|| chrono::NaiveDate::from_ymd_opt(2026, 4, 21).unwrap()); + LazyLock::new(|| chrono::NaiveDate::from_ymd_opt(2026, 5, 6).unwrap()); From f617e5ced79eff226bd2e15782d110645e81020a Mon Sep 17 00:00:00 2001 From: Hocuri Date: Thu, 7 May 2026 10:23:25 +0200 Subject: [PATCH 16/20] Update deltachat-jsonrpc/src/api.rs Co-authored-by: iequidoo <117991069+iequidoo@users.noreply.github.com> --- deltachat-jsonrpc/src/api.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deltachat-jsonrpc/src/api.rs b/deltachat-jsonrpc/src/api.rs index e3aac15eec..e70c2771b8 100644 --- a/deltachat-jsonrpc/src/api.rs +++ b/deltachat-jsonrpc/src/api.rs @@ -741,7 +741,7 @@ impl CommandApi { /// This is typically used to show the estimated impact to the user /// before actually enabling deletion of old messages. /// - /// Messages in the "saved messages" folder are not counted as they will not be deleted automatically. + /// Messages in the "Saved Messages" chat are not counted as they will not be deleted automatically. /// /// Parameters: /// - `from_server`: Deprecated, pass `false` here From 7d432c7fb42390895270dd0bedb8b1315e32688a Mon Sep 17 00:00:00 2001 From: Hocuri Date: Thu, 7 May 2026 10:23:35 +0200 Subject: [PATCH 17/20] Update src/message.rs Co-authored-by: iequidoo <117991069+iequidoo@users.noreply.github.com> --- src/message.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/message.rs b/src/message.rs index abf26b83a2..b2be0169d9 100644 --- a/src/message.rs +++ b/src/message.rs @@ -2104,7 +2104,7 @@ pub async fn get_request_msg_cnt(context: &Context) -> usize { /// This is typically used to show the estimated impact to the user /// before actually enabling deletion of old messages. /// -/// Messages in the "saved messages" folder are not counted as they will not be deleted automatically. +/// Messages in the "Saved Messages" chat are not counted as they will not be deleted automatically. /// /// Parameters: /// - `from_server`: Deprecated, pass `false` here From 450b2f46108cb5b153965190eefe41e2a3b3dbcb Mon Sep 17 00:00:00 2001 From: Hocuri Date: Thu, 7 May 2026 10:24:10 +0200 Subject: [PATCH 18/20] iequidoo's review --- deltachat-ffi/deltachat.h | 2 +- src/imex.rs | 17 ++++------------- src/receive_imf/receive_imf_tests.rs | 5 +++++ 3 files changed, 10 insertions(+), 14 deletions(-) diff --git a/deltachat-ffi/deltachat.h b/deltachat-ffi/deltachat.h index 1b4da477b2..043fe7273d 100644 --- a/deltachat-ffi/deltachat.h +++ b/deltachat-ffi/deltachat.h @@ -1465,7 +1465,7 @@ dc_chatlist_t* dc_get_similar_chatlist (dc_context_t* context, uint32_t ch * @param from_server Deprecated, pass 0 here * @param seconds Count messages older than the given number of seconds. * @return Number of messages that are older than the given number of seconds. - * Messages in the "saved messages" folder are not counted as they will not be deleted automatically. + * Messages in the "Saved Messages" chat are not counted as they will not be deleted automatically. */ int dc_estimate_deletion_cnt (dc_context_t* context, int from_server, int64_t seconds); diff --git a/src/imex.rs b/src/imex.rs index e519df1067..cff99a4012 100644 --- a/src/imex.rs +++ b/src/imex.rs @@ -985,12 +985,9 @@ mod tests { ); context1.set_config_bool(Config::IsChatmail, true).await?; - assert_eq!(context1.get_config(Config::IsMuted).await?, None); + assert_eq!(context1.get_config_bool(Config::IsMuted).await?, false); context1.set_config_bool(Config::IsMuted, true).await?; - assert_eq!( - context1.get_config(Config::IsMuted).await?, - Some("1".to_string()) - ); + assert_eq!(context1.get_config_bool(Config::IsMuted).await?, true); imex(context1, ImexMode::ExportBackup, backup_dir.path(), None).await?; let _event = context1 @@ -1009,14 +1006,8 @@ mod tests { assert!(context2.is_chatmail().await?); for ctx in [context1, context2] { // BccSelf should be enabled automatically when exporting a backup - assert_eq!( - ctx.get_config(Config::BccSelf).await?, - Some("1".to_string()) - ); - assert_eq!( - ctx.get_config(Config::IsMuted).await?, - Some("1".to_string()) - ); + assert_eq!(ctx.get_config_bool(Config::BccSelf).await?, true); + assert_eq!(ctx.get_config_bool(Config::IsMuted).await?, true); } Ok(()) } diff --git a/src/receive_imf/receive_imf_tests.rs b/src/receive_imf/receive_imf_tests.rs index 42764fcb7e..be9389e207 100644 --- a/src/receive_imf/receive_imf_tests.rs +++ b/src/receive_imf/receive_imf_tests.rs @@ -1975,6 +1975,11 @@ async fn test_no_smtp_job_for_self_chat() -> Result<()> { chat::send_msg(bob, chat_id, &mut msg).await?; assert!(bob.pop_sent_msg_opt(Duration::ZERO).await.is_none()); + bob.set_config_bool(Config::BccSelf, true).await?; + let mut msg = Message::new_text("Happy birthday to me".to_string()); + chat::send_msg(bob, chat_id, &mut msg).await?; + assert!(bob.pop_sent_msg_opt(Duration::ZERO).await.is_some()); + Ok(()) } From a8b5171825a90df0d5e5a6875b395745308ff171 Mon Sep 17 00:00:00 2001 From: Hocuri Date: Tue, 12 May 2026 22:37:16 +0200 Subject: [PATCH 19/20] --wip-- [skip ci] --- src/imap.rs | 42 +++++++++++++++++++++++++++++++----------- src/message.rs | 30 ++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 11 deletions(-) diff --git a/src/imap.rs b/src/imap.rs index 896ee1c851..ca0d525e4c 100644 --- a/src/imap.rs +++ b/src/imap.rs @@ -1282,6 +1282,7 @@ impl Session { return Ok(()); } let is_chatmail = self.is_chatmail(); + let transport_id = self.transport_id(); for (request_uids, set) in build_sequence_sets(&request_uids)? { info!(context, "Starting UID FETCH of message set \"{}\".", set); @@ -1381,17 +1382,7 @@ impl Session { ); let res = receive_imf_inner(context, rfc724_mid, body, is_seen).await; - // If the message is not needed anymore on the server, mark it for deletion: - if !context.get_config_bool(Config::BccSelf).await? && is_chatmail { - context - .sql - .execute( - "UPDATE imap SET target='' WHERE rfc724_mid=?", - (rfc724_mid,), - ) - .await?; - context.scheduler.interrupt_inbox().await; - } + maybe_mark_for_deletion(is_chatmail, transport_id, rfc724_mid, context).await?; // If there was an error receiving the message, show a device message: let received_msg = match res { @@ -1664,6 +1655,26 @@ impl Session { } } +async fn maybe_mark_for_deletion( + is_chatmail: bool, + transport_id: u32, + rfc724_mid: &String, + context: &Context, +) -> Result<()> { + if !context.get_config_bool(Config::BccSelf).await? && is_chatmail { + context + .sql + .execute( + "UPDATE imap SET target='' WHERE rfc724_mid=? AND transport_id=?", + (rfc724_mid, transport_id), + ) + .await?; + context.scheduler.interrupt_inbox().await; + } + + Ok(()) +} + fn format_setmetadata(folder: &str, device_token: &str) -> String { let device_token_len = device_token.len(); format!( @@ -1972,6 +1983,15 @@ pub(crate) fn create_message_id() -> String { } /// Determines whether the message should be downloaded based on prefetched headers. +// We want to call maybe_delete() iff it's not a post-message OR the post-message was already downloaded. +// I.e. if it's a post-message that is Available, Failure, or InProgress, then we do not want to delete it. +// WRT InProgress, we do want to delete it later on _all_ relays later, after we downloaded it. +// So, maybe when choosing what to delete, we should not filter by transport. +// I'm not sure about checking is_chatmail - maybe it's fine to just remove a message from all transports +// if it also arrived via a chatmail transport, +// because generally the only reason why this happens is because the chatpartner has DC with multi-relay. +// +// I.e., we want to delete everything for which prefetch_should_download() returns false, EXCEPT for the case where a post-message's download failed or is in progress. pub(crate) async fn prefetch_should_download( context: &Context, headers: &[mailparse::MailHeader<'_>], diff --git a/src/message.rs b/src/message.rs index b2be0169d9..90ec049b74 100644 --- a/src/message.rs +++ b/src/message.rs @@ -2220,6 +2220,36 @@ pub(crate) async fn rfc724_mid_download_tried(context: &Context, rfc724_mid: &st Ok(res) } +/// Returns `true` iff there is a message +/// with the given `rfc724_mid` +/// and a download state other than `DownloadState::Available`, +/// i.e. it was already tried to download the message or it's sent locally. +pub(crate) async fn rfc724_mid_was_downloaded(context: &Context, rfc724_mid: &str) -> Result { + let rfc724_mid = rfc724_mid.trim_start_matches('<').trim_end_matches('>'); + if rfc724_mid.is_empty() { + warn!( + context, + "Empty rfc724_mid passed to rfc724_mid_download_tried" + ); + return Ok(false); + } + + let res = context + .sql + .exists( + "SELECT COUNT(*) FROM msgs + WHERE rfc724_mid=? AND download_state=? OR download_state=?", + ( + rfc724_mid, + DownloadState::Done, + DownloadState::Undecipherable, + ), + ) + .await?; + + Ok(res) +} + /// Given a list of Message-IDs, returns the most relevant message found in the database. /// /// Relevance here is `(download_state == Done, index)`, where `index` is an index of Message-ID in From a981f9ef68eb269afa28f5455756f13a696bfa15 Mon Sep 17 00:00:00 2001 From: Hocuri Date: Wed, 13 May 2026 13:07:10 +0200 Subject: [PATCH 20/20] Revert "--wip-- [skip ci]" This reverts commit 1355112e5a4d657404f8499e3cfbc61f45371048. --- src/imap.rs | 42 +++++++++++------------------------------- src/message.rs | 30 ------------------------------ 2 files changed, 11 insertions(+), 61 deletions(-) diff --git a/src/imap.rs b/src/imap.rs index ca0d525e4c..896ee1c851 100644 --- a/src/imap.rs +++ b/src/imap.rs @@ -1282,7 +1282,6 @@ impl Session { return Ok(()); } let is_chatmail = self.is_chatmail(); - let transport_id = self.transport_id(); for (request_uids, set) in build_sequence_sets(&request_uids)? { info!(context, "Starting UID FETCH of message set \"{}\".", set); @@ -1382,7 +1381,17 @@ impl Session { ); let res = receive_imf_inner(context, rfc724_mid, body, is_seen).await; - maybe_mark_for_deletion(is_chatmail, transport_id, rfc724_mid, context).await?; + // If the message is not needed anymore on the server, mark it for deletion: + if !context.get_config_bool(Config::BccSelf).await? && is_chatmail { + context + .sql + .execute( + "UPDATE imap SET target='' WHERE rfc724_mid=?", + (rfc724_mid,), + ) + .await?; + context.scheduler.interrupt_inbox().await; + } // If there was an error receiving the message, show a device message: let received_msg = match res { @@ -1655,26 +1664,6 @@ impl Session { } } -async fn maybe_mark_for_deletion( - is_chatmail: bool, - transport_id: u32, - rfc724_mid: &String, - context: &Context, -) -> Result<()> { - if !context.get_config_bool(Config::BccSelf).await? && is_chatmail { - context - .sql - .execute( - "UPDATE imap SET target='' WHERE rfc724_mid=? AND transport_id=?", - (rfc724_mid, transport_id), - ) - .await?; - context.scheduler.interrupt_inbox().await; - } - - Ok(()) -} - fn format_setmetadata(folder: &str, device_token: &str) -> String { let device_token_len = device_token.len(); format!( @@ -1983,15 +1972,6 @@ pub(crate) fn create_message_id() -> String { } /// Determines whether the message should be downloaded based on prefetched headers. -// We want to call maybe_delete() iff it's not a post-message OR the post-message was already downloaded. -// I.e. if it's a post-message that is Available, Failure, or InProgress, then we do not want to delete it. -// WRT InProgress, we do want to delete it later on _all_ relays later, after we downloaded it. -// So, maybe when choosing what to delete, we should not filter by transport. -// I'm not sure about checking is_chatmail - maybe it's fine to just remove a message from all transports -// if it also arrived via a chatmail transport, -// because generally the only reason why this happens is because the chatpartner has DC with multi-relay. -// -// I.e., we want to delete everything for which prefetch_should_download() returns false, EXCEPT for the case where a post-message's download failed or is in progress. pub(crate) async fn prefetch_should_download( context: &Context, headers: &[mailparse::MailHeader<'_>], diff --git a/src/message.rs b/src/message.rs index 90ec049b74..b2be0169d9 100644 --- a/src/message.rs +++ b/src/message.rs @@ -2220,36 +2220,6 @@ pub(crate) async fn rfc724_mid_download_tried(context: &Context, rfc724_mid: &st Ok(res) } -/// Returns `true` iff there is a message -/// with the given `rfc724_mid` -/// and a download state other than `DownloadState::Available`, -/// i.e. it was already tried to download the message or it's sent locally. -pub(crate) async fn rfc724_mid_was_downloaded(context: &Context, rfc724_mid: &str) -> Result { - let rfc724_mid = rfc724_mid.trim_start_matches('<').trim_end_matches('>'); - if rfc724_mid.is_empty() { - warn!( - context, - "Empty rfc724_mid passed to rfc724_mid_download_tried" - ); - return Ok(false); - } - - let res = context - .sql - .exists( - "SELECT COUNT(*) FROM msgs - WHERE rfc724_mid=? AND download_state=? OR download_state=?", - ( - rfc724_mid, - DownloadState::Done, - DownloadState::Undecipherable, - ), - ) - .await?; - - Ok(res) -} - /// Given a list of Message-IDs, returns the most relevant message found in the database. /// /// Relevance here is `(download_state == Done, index)`, where `index` is an index of Message-ID in