mirror of
https://github.com/chatmail/core.git
synced 2026-05-16 21:36:30 +03:00
feat: Remove "Delete Messages from Server" (delete_server_after) config option, second try
This commit is contained in:
@@ -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,23 +623,6 @@ impl Context {
|
||||
self.get_config_bool(Config::MdnsEnabled).await
|
||||
}
|
||||
|
||||
/// Gets configured "delete_server_after" value.
|
||||
///
|
||||
/// `None` means never delete the message, `Some(0)` means delete
|
||||
/// at once, `Some(x)` means delete after `x` seconds.
|
||||
pub async fn get_config_delete_server_after(&self) -> Result<Option<i64>> {
|
||||
let val = match self
|
||||
.get_config_parsed::<i64>(Config::DeleteServerAfter)
|
||||
.await?
|
||||
.unwrap_or(0)
|
||||
{
|
||||
0 => None,
|
||||
1 => Some(0),
|
||||
x => Some(x),
|
||||
};
|
||||
Ok(val)
|
||||
}
|
||||
|
||||
/// Gets the configured provider.
|
||||
///
|
||||
/// The provider is determined by the current primary transport.
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -973,12 +973,6 @@ 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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
152
src/ephemeral.rs
152
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.
|
||||
//!
|
||||
//! `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,
|
||||
//! but does not delete messages from the server.
|
||||
//!
|
||||
//! ## How messages are deleted
|
||||
//!
|
||||
@@ -60,9 +59,8 @@
|
||||
//!
|
||||
//! 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;
|
||||
use std::fmt;
|
||||
use std::num::ParseIntError;
|
||||
@@ -75,10 +73,11 @@ use serde::{Deserialize, Serialize};
|
||||
use tokio::time::timeout;
|
||||
|
||||
use crate::chat::{ChatId, ChatIdBlocked, send_msg};
|
||||
use crate::config::Config;
|
||||
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::download::DownloadState;
|
||||
use crate::events::EventType;
|
||||
use crate::log::{LogExt, warn};
|
||||
use crate::message::{Message, MessageState, MsgId, Viewtype};
|
||||
@@ -652,36 +651,115 @@ 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<()> {
|
||||
pub(crate) async fn delete_expired_imap_messages(
|
||||
context: &Context,
|
||||
transport_id: u32,
|
||||
is_chatmail: bool,
|
||||
) -> Result<()> {
|
||||
let now = time();
|
||||
|
||||
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, MIN_DELETE_SERVER_AFTER),
|
||||
),
|
||||
};
|
||||
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE imap
|
||||
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 <= ?))
|
||||
)",
|
||||
(threshold_timestamp, threshold_timestamp_extended, now),
|
||||
)
|
||||
.await?;
|
||||
if !context.get_config_bool(Config::BccSelf).await? && is_chatmail {
|
||||
info!(
|
||||
context,
|
||||
"dbg marking all as deleted 1 - rfc724_mids: {:#?}",
|
||||
context
|
||||
.sql
|
||||
.query_map_vec(
|
||||
"SELECT rfc724_mid FROM msgs
|
||||
WHERE (ephemeral_timestamp!=0 AND ephemeral_timestamp<=?)
|
||||
OR download_state=?",
|
||||
(now, DownloadState::Done),
|
||||
|row| Ok(row.get::<_, String>(0)?)
|
||||
)
|
||||
.await
|
||||
);
|
||||
info!(
|
||||
context,
|
||||
"dbg marking all as deleted 1 - pre_rfc724_mids: {:#?}",
|
||||
context
|
||||
.sql
|
||||
.query_map_vec(
|
||||
"SELECT pre_rfc724_mid FROM msgs
|
||||
WHERE pre_rfc724_mid!=''",
|
||||
(),
|
||||
|row| Ok(row.get::<_, String>(0)?)
|
||||
)
|
||||
.await
|
||||
);
|
||||
// This the only device using this relay.
|
||||
// Mark all downloaded messages for deletion, because they are not needed anymore.
|
||||
//
|
||||
// For pre- and post-messages, `rfc724_mid` contains the post-message's Message-Id.
|
||||
// The pre-message's Message-Id is in pre_rfc724_mid, if it exists.
|
||||
//
|
||||
// Pre-messages can be deleted even if the message wasn't fully downloaded yet,
|
||||
// because it's only the post-message that hasn't been downloaded.
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE imap
|
||||
SET target=''
|
||||
WHERE transport_id=?1
|
||||
AND rfc724_mid IN (
|
||||
SELECT rfc724_mid FROM msgs
|
||||
WHERE (ephemeral_timestamp!=0 AND ephemeral_timestamp<=?2)
|
||||
OR download_state=?3
|
||||
UNION
|
||||
SELECT pre_rfc724_mid FROM msgs
|
||||
WHERE pre_rfc724_mid!=''
|
||||
)",
|
||||
(transport_id, now, DownloadState::Done),
|
||||
)
|
||||
.await?;
|
||||
} else {
|
||||
info!(
|
||||
context,
|
||||
"dbg marking ephemeral as deleted 1 - rfc724_mids: {:#?}",
|
||||
context
|
||||
.sql
|
||||
.query_map_vec(
|
||||
"SELECT rfc724_mid FROM msgs
|
||||
WHERE (ephemeral_timestamp!=0 AND ephemeral_timestamp<=?2)",
|
||||
(transport_id, now),
|
||||
|row| Ok(row.get::<_, String>(0)?)
|
||||
)
|
||||
.await
|
||||
);
|
||||
info!(
|
||||
context,
|
||||
"dbg marking ephemeral as deleted 1 - pre_rfc724_mids: {:#?}",
|
||||
context
|
||||
.sql
|
||||
.query_map_vec(
|
||||
"SELECT pre_rfc724_mid FROM msgs
|
||||
WHERE pre_rfc724_mid!=''
|
||||
AND (ephemeral_timestamp!=0 AND ephemeral_timestamp<=?)",
|
||||
(now,),
|
||||
|row| Ok(row.get::<_, String>(0)?)
|
||||
)
|
||||
.await
|
||||
);
|
||||
// There may be other devices using this relay,
|
||||
// either because there is multi-relay or because this is a classical email server.
|
||||
// Only delete expired ephemeral messages.
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE imap
|
||||
SET target=''
|
||||
WHERE transport_id=?1
|
||||
AND rfc724_mid IN (
|
||||
SELECT rfc724_mid FROM msgs
|
||||
WHERE (ephemeral_timestamp!=0 AND ephemeral_timestamp<=?2)
|
||||
UNION
|
||||
SELECT pre_rfc724_mid FROM msgs
|
||||
WHERE pre_rfc724_mid!=''
|
||||
AND (ephemeral_timestamp!=0 AND ephemeral_timestamp<=?2)
|
||||
)",
|
||||
(transport_id, now),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -451,105 +451,178 @@ async fn test_delete_expired_imap_messages() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
const HOUR: i64 = 60 * 60;
|
||||
let now = time();
|
||||
let transport_id = 1;
|
||||
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),
|
||||
(2000, now - 18 * HOUR, now - HOUR),
|
||||
(2020, now - 17 * HOUR, now + HOUR),
|
||||
(3000, now + HOUR, 0),
|
||||
] {
|
||||
let message_id = id.to_string();
|
||||
t.sql
|
||||
.execute(
|
||||
"INSERT INTO msgs (id, rfc724_mid, timestamp, ephemeral_timestamp) VALUES (?,?,?,?);",
|
||||
(id, &message_id, timestamp, ephemeral_timestamp),
|
||||
)
|
||||
.await?;
|
||||
let transport_id: u32 = 1;
|
||||
let other_transport_id: u32 = 2;
|
||||
let uidvalidity = 12345u32;
|
||||
|
||||
async fn is_deleted(context: &Context, mid: &str) -> Result<bool> {
|
||||
Ok(context
|
||||
.sql
|
||||
.count(
|
||||
"SELECT COUNT(*) FROM imap WHERE target='' AND rfc724_mid=?",
|
||||
(mid,),
|
||||
)
|
||||
.await?
|
||||
== 1)
|
||||
}
|
||||
|
||||
async fn reset_targets(context: &Context) {
|
||||
context
|
||||
.sql
|
||||
.execute("UPDATE imap SET target='INBOX'", ())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// ── Test messages ────────────────────────────────────────────────────────
|
||||
//
|
||||
// (id, rfc724_mid, ephemeral_timestamp, download_state, pre_rfc724_mid)
|
||||
//
|
||||
// "expired" – expired ephemeral, no pre-msg
|
||||
// "no_expire" – ephemeral_timestamp=0, not Done → never deleted
|
||||
// "future" – future ephemeral, not Done → never deleted
|
||||
// "done" – Done, no ephemeral → branch 1 only
|
||||
// "pre_no_expire_*" – has pre-msg, but no expiry/Done
|
||||
// "pre_expired_*" – has pre-msg, expired ephemeral
|
||||
// "pre_future_*" – has pre-msg, future ephemeral
|
||||
// "wrong_tid" – expired+Done, but wrong transport_id in imap
|
||||
let msgs: &[(&str, i64, DownloadState, &str)] = &[
|
||||
("expired", now - HOUR, DownloadState::Available, ""),
|
||||
("no_expire", 0, DownloadState::Available, ""),
|
||||
("future", now + HOUR, DownloadState::Available, ""),
|
||||
("done", 0, DownloadState::Done, ""),
|
||||
(
|
||||
"pre_no_expire_post",
|
||||
0,
|
||||
DownloadState::Available,
|
||||
"pre_no_expire_pre",
|
||||
),
|
||||
(
|
||||
"pre_expired_post",
|
||||
now - HOUR,
|
||||
DownloadState::Available,
|
||||
"pre_expired_pre",
|
||||
),
|
||||
(
|
||||
"pre_future_post",
|
||||
now + HOUR,
|
||||
DownloadState::Available,
|
||||
"pre_future_pre",
|
||||
),
|
||||
("wrong_tid", now - HOUR, DownloadState::Done, ""),
|
||||
];
|
||||
for (mid, eph_ts, dl_state, pre_mid) in msgs {
|
||||
t.sql
|
||||
.execute(
|
||||
"INSERT INTO imap (transport_id, rfc724_mid, folder, uid, target, uidvalidity) VALUES (?, ?,'INBOX',?, 'INBOX', ?);",
|
||||
(transport_id, &message_id, id, uidvalidity),
|
||||
"INSERT INTO msgs \
|
||||
(rfc724_mid, timestamp, ephemeral_timestamp, download_state, pre_rfc724_mid) \
|
||||
VALUES (?,?,0,?,?,?)",
|
||||
(*mid, *eph_ts, *dl_state, *pre_mid),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
async fn test_marked_for_deletion(context: &Context, id: u32) -> Result<()> {
|
||||
assert_eq!(
|
||||
context
|
||||
.sql
|
||||
.count(
|
||||
"SELECT COUNT(*) FROM imap WHERE target='' AND rfc724_mid=?",
|
||||
(id.to_string(),),
|
||||
)
|
||||
.await?,
|
||||
1
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn remove_uid(context: &Context, id: u32) -> Result<()> {
|
||||
context
|
||||
.sql
|
||||
.execute("DELETE FROM imap WHERE rfc724_mid=?", (id.to_string(),))
|
||||
// One imap row per mid (including separate rows for pre-messages),
|
||||
// plus "wrong_tid" on a different transport_id.
|
||||
let imap_rows: &[(&str, u32)] = &[
|
||||
("expired", transport_id),
|
||||
("no_expire", transport_id),
|
||||
("future", transport_id),
|
||||
("done", transport_id),
|
||||
("pre_no_expire_post", transport_id),
|
||||
("pre_no_expire_pre", transport_id), // the pre-message's own imap row
|
||||
("pre_expired_post", transport_id),
|
||||
("pre_expired_pre", transport_id),
|
||||
("pre_future_post", transport_id),
|
||||
("pre_future_pre", transport_id),
|
||||
("wrong_tid", other_transport_id), // transport_id filter test
|
||||
];
|
||||
for (i, (mid, tid)) in imap_rows.iter().enumerate() {
|
||||
t.sql
|
||||
.execute(
|
||||
"INSERT INTO imap \
|
||||
(transport_id, rfc724_mid, folder, uid, target, uidvalidity) \
|
||||
VALUES (?,?,'INBOX',?,'INBOX',?)",
|
||||
(*tid, *mid, (i + 1) as u32, uidvalidity),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// This should mark message 2000 for deletion.
|
||||
delete_expired_imap_messages(&t).await?;
|
||||
test_marked_for_deletion(&t, 2000).await?;
|
||||
remove_uid(&t, 2000).await?;
|
||||
// No other messages are marked for deletion.
|
||||
assert_eq!(
|
||||
t.sql
|
||||
.count("SELECT COUNT(*) FROM imap WHERE target=''", ())
|
||||
.await?,
|
||||
0
|
||||
);
|
||||
// ── Branch 1: is_chatmail=true, BccSelf=false (default) ─────────────────
|
||||
//
|
||||
// SQL deletes: (ephemeral_timestamp!=0 AND <=now) OR download_state=Done
|
||||
// Pre-messages: ALL with pre_rfc724_mid!='' unconditionally.
|
||||
delete_expired_imap_messages(&t, transport_id, true).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?;
|
||||
// Tests (ephemeral_timestamp!=0 AND ephemeral_timestamp<=now) path.
|
||||
assert!(is_deleted(&t, "expired").await?);
|
||||
// Tests the ephemeral_timestamp!=0 guard: timestamp=0 satisfies <=now but must not match.
|
||||
assert!(!is_deleted(&t, "no_expire").await?);
|
||||
// Tests the ephemeral_timestamp<=now guard.
|
||||
assert!(!is_deleted(&t, "future").await?);
|
||||
// Tests the OR download_state=Done clause.
|
||||
assert!(is_deleted(&t, "done").await?);
|
||||
// Post-message: no expiry, not Done → not deleted.
|
||||
assert!(!is_deleted(&t, "pre_no_expire_post").await?);
|
||||
// Pre-message: deleted unconditionally (tests UNION SELECT pre_rfc724_mid ... WHERE pre_rfc724_mid!='').
|
||||
assert!(is_deleted(&t, "pre_no_expire_pre").await?);
|
||||
// Post-message with expired ephemeral → deleted.
|
||||
assert!(is_deleted(&t, "pre_expired_post").await?);
|
||||
// Pre-message of expired post → deleted (unconditional pre path).
|
||||
assert!(is_deleted(&t, "pre_expired_pre").await?);
|
||||
// Post-message with future ephemeral → not deleted.
|
||||
assert!(!is_deleted(&t, "pre_future_post").await?);
|
||||
// Pre-message of future post → still deleted (branch 1 pre path has NO ephemeral condition).
|
||||
// If the pre UNION clause gains an ephemeral condition, this would wrongly not be deleted.
|
||||
assert!(is_deleted(&t, "pre_future_pre").await?);
|
||||
// Tests transport_id=?1: expired+Done but on wrong transport_id → not deleted.
|
||||
assert!(!is_deleted(&t, "wrong_tid").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?;
|
||||
reset_targets(&t).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?;
|
||||
// ── Branch 2: is_chatmail=false ──────────────────────────────────────────
|
||||
//
|
||||
// SQL deletes: ephemeral_timestamp!=0 AND <=now only (no Done).
|
||||
// Pre-messages: only when the post also satisfies the ephemeral condition.
|
||||
delete_expired_imap_messages(&t, transport_id, false).await?;
|
||||
|
||||
MsgId::new(1010)
|
||||
.update_download_state(&t, DownloadState::Available)
|
||||
.await?;
|
||||
delete_expired_imap_messages(&t).await?;
|
||||
// Keep downloadable for now.
|
||||
assert_eq!(
|
||||
t.sql
|
||||
.count("SELECT COUNT(*) FROM imap WHERE target=''", ())
|
||||
.await?,
|
||||
0
|
||||
);
|
||||
// Expired ephemeral → deleted.
|
||||
assert!(is_deleted(&t, "expired").await?);
|
||||
// ephemeral_timestamp=0 → not deleted (tests !=0 guard in branch 2).
|
||||
assert!(!is_deleted(&t, "no_expire").await?);
|
||||
// Future ephemeral → not deleted (tests <=now guard in branch 2).
|
||||
assert!(!is_deleted(&t, "future").await?);
|
||||
// Done without expired ephemeral → NOT deleted (key branch 1 vs 2 difference).
|
||||
// If download_state=Done were added to branch 2, this would wrongly be deleted.
|
||||
assert!(!is_deleted(&t, "done").await?);
|
||||
// Post-message: no expiry → not deleted.
|
||||
assert!(!is_deleted(&t, "pre_no_expire_post").await?);
|
||||
// Pre-message of non-expiring post → NOT deleted
|
||||
// (tests ephemeral_timestamp!=0 in branch 2's pre subquery).
|
||||
assert!(!is_deleted(&t, "pre_no_expire_pre").await?);
|
||||
// Post-message with expired ephemeral → deleted.
|
||||
assert!(is_deleted(&t, "pre_expired_post").await?);
|
||||
// Pre-message of expired post → deleted (tests full ephemeral condition in pre subquery).
|
||||
assert!(is_deleted(&t, "pre_expired_pre").await?);
|
||||
// Post-message with future ephemeral → not deleted.
|
||||
assert!(!is_deleted(&t, "pre_future_post").await?);
|
||||
// Pre-message of future post → NOT deleted
|
||||
// (tests ephemeral_timestamp<=now in branch 2's pre subquery).
|
||||
// If the <=now guard were removed there, this would wrongly be deleted.
|
||||
assert!(!is_deleted(&t, "pre_future_pre").await?);
|
||||
// Wrong transport_id → not deleted.
|
||||
assert!(!is_deleted(&t, "wrong_tid").await?);
|
||||
|
||||
t.set_config(Config::DeleteServerAfter, Some("1")).await?;
|
||||
delete_expired_imap_messages(&t).await?;
|
||||
test_marked_for_deletion(&t, 3000).await?;
|
||||
reset_targets(&t).await;
|
||||
|
||||
// ── BccSelf=true forces branch 2 even when is_chatmail=true ─────────────
|
||||
//
|
||||
// Tests the `!BccSelf` part of the Rust condition.
|
||||
// If `!BccSelf` were dropped, Done would be deleted here (branch 1 behaviour).
|
||||
t.set_config(Config::BccSelf, Some("1")).await?;
|
||||
delete_expired_imap_messages(&t, transport_id, true).await?;
|
||||
assert!(!is_deleted(&t, "done").await?); // must stay on branch 2
|
||||
assert!(is_deleted(&t, "expired").await?); // branch 2 still runs normally
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
28
src/imap.rs
28
src/imap.rs
@@ -21,9 +21,6 @@ use futures_lite::FutureExt;
|
||||
use ratelimit::Ratelimit;
|
||||
use url::Url;
|
||||
|
||||
use crate::calls::{
|
||||
UnresolvedIceServer, create_fallback_ice_servers, create_ice_servers_from_metadata,
|
||||
};
|
||||
use crate::chat::{self, ChatId, ChatIdBlocked, add_device_msg};
|
||||
use crate::chatlist_events;
|
||||
use crate::config::Config;
|
||||
@@ -49,6 +46,10 @@ use crate::tools::{self, create_id, duration_to_str, time};
|
||||
use crate::transport::{
|
||||
ConfiguredLoginParam, ConfiguredServerLoginParam, prioritize_server_login_params,
|
||||
};
|
||||
use crate::{
|
||||
calls::{UnresolvedIceServer, create_fallback_ice_servers, create_ice_servers_from_metadata},
|
||||
ephemeral::delete_expired_imap_messages,
|
||||
};
|
||||
|
||||
pub(crate) mod capabilities;
|
||||
mod client;
|
||||
@@ -525,6 +526,12 @@ impl Imap {
|
||||
context.scheduler.interrupt_ephemeral_task().await;
|
||||
}
|
||||
|
||||
// Mark expired messages for deletion. Note that `delete_expired_imap_messages` is not
|
||||
// not well optimized and should not be called before fetching.
|
||||
delete_expired_imap_messages(context, session.transport_id(), session.is_chatmail())
|
||||
.await
|
||||
.context("delete_expired_imap_messages")?;
|
||||
|
||||
session
|
||||
.move_delete_messages(context, watch_folder)
|
||||
.await
|
||||
@@ -1379,6 +1386,21 @@ impl Session {
|
||||
"Passing message UID {} to receive_imf().", request_uid
|
||||
);
|
||||
let res = receive_imf_inner(context, rfc724_mid, body, is_seen).await;
|
||||
|
||||
// TODO I don't think this code is needed anymore:
|
||||
// // 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 {
|
||||
Err(err) => {
|
||||
warn!(context, "receive_imf error: {err:#}.");
|
||||
|
||||
27
src/imex.rs
27
src/imex.rs
@@ -979,21 +979,16 @@ 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())
|
||||
);
|
||||
assert_eq!(
|
||||
context1.get_config(Config::DeleteServerAfter).await?,
|
||||
Some("1".to_string())
|
||||
);
|
||||
context1.set_config_bool(Config::IsChatmail, true).await?;
|
||||
|
||||
assert_eq!(context1.get_config_bool(Config::IsMuted).await?, false);
|
||||
context1.set_config_bool(Config::IsMuted, true).await?;
|
||||
assert_eq!(context1.get_config_bool(Config::IsMuted).await?, true);
|
||||
|
||||
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 +1005,9 @@ mod tests {
|
||||
assert!(context2.is_configured().await?);
|
||||
assert!(context2.is_chatmail().await?);
|
||||
for ctx in [context1, context2] {
|
||||
assert_eq!(
|
||||
ctx.get_config(Config::BccSelf).await?,
|
||||
Some("1".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
ctx.get_config(Config::DeleteServerAfter).await?,
|
||||
Some("0".to_string())
|
||||
);
|
||||
assert_eq!(ctx.get_config_delete_server_after().await?, None);
|
||||
// BccSelf should be enabled automatically when exporting a backup
|
||||
assert_eq!(ctx.get_config_bool(Config::BccSelf).await?, true);
|
||||
assert_eq!(ctx.get_config_bool(Config::IsMuted).await?, true);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -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" chat 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<usize> {
|
||||
ensure!(
|
||||
!from_server,
|
||||
"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)
|
||||
.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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -2382,4 +2376,4 @@ pub(crate) static PROVIDER_IDS: LazyLock<HashMap<&'static str, &'static Provider
|
||||
});
|
||||
|
||||
pub static _PROVIDER_UPDATED: LazyLock<chrono::NaiveDate> =
|
||||
LazyLock::new(|| chrono::NaiveDate::from_ymd_opt(2026, 4, 21).unwrap());
|
||||
LazyLock::new(|| chrono::NaiveDate::from_ymd_opt(2026, 5, 6).unwrap());
|
||||
|
||||
@@ -902,10 +902,8 @@ 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) {
|
||||
let target = if received_msg.needs_delete_job {
|
||||
Some("".to_string())
|
||||
} else {
|
||||
None
|
||||
|
||||
@@ -1976,15 +1976,10 @@ 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());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -484,14 +484,6 @@ async fn fetch_idle(ctx: &Context, connection: &mut Imap, mut session: Session)
|
||||
.await
|
||||
.context("fetch_move_delete")?;
|
||||
|
||||
// Mark expired messages for deletion. Marked messages will be deleted from the server
|
||||
// on the next iteration of `fetch_move_delete`. `delete_expired_imap_messages` is not
|
||||
// called right before `fetch_move_delete` because it is not well optimized and would
|
||||
// otherwise slow down message fetching.
|
||||
delete_expired_imap_messages(ctx)
|
||||
.await
|
||||
.context("delete_expired_imap_messages")?;
|
||||
|
||||
download_known_post_messages_without_pre_message(ctx, &mut session).await?;
|
||||
download_msgs(ctx, &mut session)
|
||||
.await
|
||||
|
||||
34
src/smtp.rs
34
src/smtp.rs
@@ -699,26 +699,22 @@ pub(crate) async fn add_self_recipients(
|
||||
recipients: &mut Vec<String>,
|
||||
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.
|
||||
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);
|
||||
}
|
||||
// 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
|
||||
// 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(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user