Skip to content

Commit 2762a90

Browse files
committed
fix: Prune tombstone when a message isn't expected to appear on IMAP anymore (#7115)
This allows to receive deleted messages again if they're re-sent: - After a manual deletion, - If both DeleteServerAfter and DeleteDeviceAfter are set, w/o waiting for 2 days when stale tombstones are GC-ed. This is particularly useful for webxdc. If only DeleteDeviceAfter is set though, this changes nothing and maybe a separate fix is needed. This may also greately reduce the db size for some bots which receive and immediately delete many messages. Also insert a tombstone to the db if a deletion request (incl. sync messages) can't find `rfc724_mid`. This may happen in case of message reordering and the deleted message mustn't appear when it finally arrives.
1 parent 0bc9fe8 commit 2762a90

File tree

5 files changed

+120
-39
lines changed

5 files changed

+120
-39
lines changed

src/imap.rs

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -634,6 +634,9 @@ impl Imap {
634634
let target = if let Some(message_id) = &message_id {
635635
let msg_info =
636636
message::rfc724_mid_exists_ex(context, message_id, "deleted=1").await?;
637+
if msg_info.is_some_and(|(_, ts_sent, _)| ts_sent == 0) {
638+
message::prune_tombstone(context, message_id).await?;
639+
}
637640
let delete = if let Some((_, _, true)) = msg_info {
638641
info!(context, "Deleting locally deleted message {message_id}.");
639642
true
@@ -2342,10 +2345,16 @@ pub(crate) async fn prefetch_should_download(
23422345
message_id: &str,
23432346
mut flags: impl Iterator<Item = Flag<'_>>,
23442347
) -> Result<bool> {
2345-
if message::rfc724_mid_exists(context, message_id)
2346-
.await?
2347-
.is_some()
2348+
if let Some((_, _, is_trash)) = message::rfc724_mid_exists_ex(
2349+
context,
2350+
message_id,
2351+
"chat_id=3", // Trash
2352+
)
2353+
.await?
23482354
{
2355+
if is_trash {
2356+
message::prune_tombstone(context, message_id).await?;
2357+
}
23492358
markseen_on_imap_table(context, message_id).await?;
23502359
return Ok(false);
23512360
}

src/message.rs

Lines changed: 52 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -112,18 +112,20 @@ impl MsgId {
112112
.unwrap_or_default())
113113
}
114114

115-
/// Put message into trash chat and delete message text.
115+
/// Put message into trash chat or delete it.
116116
///
117117
/// It means the message is deleted locally, but not on the server.
118-
/// We keep some infos to
119-
/// 1. not download the same message again
120-
/// 2. be able to delete the message on the server if we want to
121118
///
122-
/// * `on_server`: Delete the message on the server also if it is seen on IMAP later, but only
123-
/// if all parts of the message are trashed with this flag. `true` if the user explicitly
124-
/// deletes the message. As for trashing a partially downloaded message when replacing it with
125-
/// a fully downloaded one, see `receive_imf::add_parts()`.
119+
/// * `on_server`: Keep some info to delete the message on the server also if it is seen on IMAP
120+
/// later.
126121
pub(crate) async fn trash(self, context: &Context, on_server: bool) -> Result<()> {
122+
if !on_server {
123+
context
124+
.sql
125+
.execute("DELETE FROM msgs WHERE id=?1", (self,))
126+
.await?;
127+
return Ok(());
128+
}
127129
context
128130
.sql
129131
.execute(
@@ -132,7 +134,7 @@ impl MsgId {
132134
// still adds to the db if chat_id is TRASH.
133135
"INSERT OR REPLACE INTO msgs (id, rfc724_mid, timestamp, chat_id, deleted)
134136
SELECT ?1, rfc724_mid, timestamp, ?, ? FROM msgs WHERE id=?1",
135-
(self, DC_CHAT_ID_TRASH, on_server),
137+
(self, DC_CHAT_ID_TRASH, true),
136138
)
137139
.await?;
138140

@@ -1591,11 +1593,15 @@ pub(crate) async fn get_mime_headers(context: &Context, msg_id: MsgId) -> Result
15911593

15921594
/// Delete a single message from the database, including references in other tables.
15931595
/// This may be called in batches; the final events are emitted in delete_msgs_locally_done() then.
1594-
pub(crate) async fn delete_msg_locally(context: &Context, msg: &Message) -> Result<()> {
1596+
pub(crate) async fn delete_msg_locally(
1597+
context: &Context,
1598+
msg: &Message,
1599+
keep_tombstone: bool,
1600+
) -> Result<()> {
15951601
if msg.location_id > 0 {
15961602
delete_poi_location(context, msg.location_id).await?;
15971603
}
1598-
let on_server = true;
1604+
let on_server = keep_tombstone;
15991605
msg.id
16001606
.trash(context, on_server)
16011607
.await
@@ -1663,6 +1669,7 @@ pub async fn delete_msgs_ex(
16631669
let mut modified_chat_ids = HashSet::new();
16641670
let mut deleted_rfc724_mid = Vec::new();
16651671
let mut res = Ok(());
1672+
let mut msgs = Vec::with_capacity(msg_ids.len());
16661673

16671674
for &msg_id in msg_ids {
16681675
let msg = Message::load_from_db(context, msg_id).await?;
@@ -1680,18 +1687,24 @@ pub async fn delete_msgs_ex(
16801687

16811688
let target = context.get_delete_msgs_target().await?;
16821689
let update_db = |trans: &mut rusqlite::Transaction| {
1683-
trans.execute(
1690+
// NB: If the message isn't sent yet, keeping its tombstone is unnecessary, but safe.
1691+
let keep_tombstone = trans.execute(
16841692
"UPDATE imap SET target=? WHERE rfc724_mid=?",
1685-
(target, msg.rfc724_mid),
1686-
)?;
1693+
(&target, msg.rfc724_mid),
1694+
)? == 0
1695+
|| !target.is_empty();
16871696
trans.execute("DELETE FROM smtp WHERE msg_id=?", (msg_id,))?;
1688-
Ok(())
1697+
Ok(keep_tombstone)
16891698
};
1690-
if let Err(e) = context.sql.transaction(update_db).await {
1691-
error!(context, "delete_msgs: failed to update db: {e:#}.");
1692-
res = Err(e);
1693-
continue;
1694-
}
1699+
let keep_tombstone = match context.sql.transaction(update_db).await {
1700+
Ok(v) => v,
1701+
Err(e) => {
1702+
error!(context, "delete_msgs: failed to update db: {e:#}.");
1703+
res = Err(e);
1704+
continue;
1705+
}
1706+
};
1707+
msgs.push((msg_id, keep_tombstone));
16951708
}
16961709
res?;
16971710

@@ -1720,9 +1733,9 @@ pub async fn delete_msgs_ex(
17201733
.await?;
17211734
}
17221735

1723-
for &msg_id in msg_ids {
1736+
for (msg_id, keep_tombstone) in msgs {
17241737
let msg = Message::load_from_db(context, msg_id).await?;
1725-
delete_msg_locally(context, &msg).await?;
1738+
delete_msg_locally(context, &msg, keep_tombstone).await?;
17261739
}
17271740
delete_msgs_locally_done(context, msg_ids, modified_chat_ids).await?;
17281741

@@ -1732,6 +1745,23 @@ pub async fn delete_msgs_ex(
17321745
Ok(())
17331746
}
17341747

1748+
/// Removes from the database a locally deleted message that also doesn't have a server UID.
1749+
pub(crate) async fn prune_tombstone(context: &Context, rfc724_mid: &str) -> Result<()> {
1750+
context
1751+
.sql
1752+
.execute(
1753+
"DELETE FROM msgs
1754+
WHERE rfc724_mid=?
1755+
AND chat_id=?
1756+
AND NOT EXISTS (
1757+
SELECT * FROM imap WHERE msgs.rfc724_mid=rfc724_mid AND target!=''
1758+
)",
1759+
(rfc724_mid, DC_CHAT_ID_TRASH),
1760+
)
1761+
.await?;
1762+
Ok(())
1763+
}
1764+
17351765
/// Marks requested messages as seen.
17361766
pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()> {
17371767
if msg_ids.is_empty() {

src/receive_imf.rs

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ use crate::securejoin::{self, handle_securejoin_handshake, observe_securejoin_on
4444
use crate::simplify;
4545
use crate::stock_str;
4646
use crate::sync::Sync::*;
47-
use crate::tools::{self, buf_compress, remove_subject_prefix};
47+
use crate::tools::{self, buf_compress, remove_subject_prefix, time};
4848
use crate::{chatlist_events, ensure_and_debug_assert, ensure_and_debug_assert_eq, location};
4949
use crate::{contact, imap};
5050

@@ -557,21 +557,25 @@ pub(crate) async fn receive_imf_inner(
557557
// make sure, this check is done eg. before securejoin-processing.
558558
let (replace_msg_id, replace_chat_id);
559559
if let Some((old_msg_id, _)) = message::rfc724_mid_exists(context, rfc724_mid).await? {
560-
let msg = Message::load_from_db(context, old_msg_id).await?;
560+
let msg = Message::load_from_db_optional(context, old_msg_id).await?;
561+
if msg.is_none() {
562+
message::prune_tombstone(context, rfc724_mid).await?;
563+
}
561564
replace_msg_id = Some(old_msg_id);
562-
replace_chat_id = if msg.download_state() != DownloadState::Done {
565+
if let Some(msg) = msg.filter(|msg| msg.download_state() != DownloadState::Done) {
563566
// the message was partially downloaded before and is fully downloaded now.
564567
info!(context, "Message already partly in DB, replacing.");
565-
Some(msg.chat_id)
568+
replace_chat_id = Some(msg.chat_id);
566569
} else {
567-
None
568-
};
570+
replace_chat_id = None;
571+
}
569572
} else {
570573
replace_msg_id = if rfc724_mid_orig == rfc724_mid {
571574
None
572575
} else if let Some((old_msg_id, old_ts_sent)) =
573576
message::rfc724_mid_exists(context, rfc724_mid_orig).await?
574577
{
578+
message::prune_tombstone(context, rfc724_mid_orig).await?;
575579
if imap::is_dup_msg(
576580
mime_parser.has_chat_version(),
577581
mime_parser.timestamp_sent,
@@ -2196,10 +2200,7 @@ RETURNING id
21962200
}
21972201

21982202
if let Some(replace_msg_id) = replace_msg_id {
2199-
// Trash the "replace" placeholder with a message that has no parts. If it has the original
2200-
// "Message-ID", mark the placeholder for server-side deletion so as if the user deletes the
2201-
// fully downloaded message later, the server-side deletion is issued.
2202-
let on_server = rfc724_mid == rfc724_mid_orig;
2203+
let on_server = false;
22032204
replace_msg_id.trash(context, on_server).await?;
22042205
}
22052206

@@ -2307,7 +2308,8 @@ async fn handle_edit_delete(
23072308
{
23082309
if let Some(msg) = Message::load_from_db_optional(context, msg_id).await? {
23092310
if msg.from_id == from_id {
2310-
message::delete_msg_locally(context, &msg).await?;
2311+
let keep_tombstone = false;
2312+
message::delete_msg_locally(context, &msg, keep_tombstone).await?;
23112313
msg_ids.push(msg.id);
23122314
modified_chat_ids.insert(msg.chat_id);
23132315
} else {
@@ -2318,6 +2320,14 @@ async fn handle_edit_delete(
23182320
}
23192321
} else {
23202322
warn!(context, "Delete message: {rfc724_mid:?} not found.");
2323+
context
2324+
.sql
2325+
.execute(
2326+
"INSERT INTO msgs (rfc724_mid, timestamp, chat_id)
2327+
VALUES (?, ?, ?)",
2328+
(rfc724_mid, time(), DC_CHAT_ID_TRASH),
2329+
)
2330+
.await?;
23212331
}
23222332
}
23232333
message::delete_msgs_locally_done(context, &msg_ids, modified_chat_ids).await?;

src/receive_imf/receive_imf_tests.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -815,6 +815,31 @@ async fn test_concat_multiple_ndns() -> Result<()> {
815815
Ok(())
816816
}
817817

818+
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
819+
async fn test_receive_resent_deleted_msg() -> Result<()> {
820+
let alice = &TestContext::new_alice().await;
821+
let bob = &TestContext::new_bob().await;
822+
let alice_bob_id = alice.add_or_lookup_contact_id(bob).await;
823+
let alice_chat_id = ChatId::create_for_contact(alice, alice_bob_id).await?;
824+
let sent = alice.send_text(alice_chat_id, "hi").await;
825+
let alice_msg = Message::load_from_db(alice, sent.sender_msg_id).await?;
826+
bob.sql
827+
.execute(
828+
"INSERT INTO imap (rfc724_mid, folder, uid, uidvalidity, target)
829+
VALUES (?1, ?2, ?3, ?4, ?5)",
830+
(&alice_msg.rfc724_mid, "INBOX", 1, 1, "INBOX"),
831+
)
832+
.await?;
833+
let bob_msg = bob.recv_msg(&sent).await;
834+
let bob_chat_id = bob_msg.chat_id;
835+
message::delete_msgs(bob, &[bob_msg.id]).await?;
836+
chat::resend_msgs(alice, &[alice_msg.id]).await?;
837+
let bob_msg = bob.recv_msg(&alice.pop_sent_msg().await).await;
838+
assert_eq!(bob_msg.chat_id, bob_chat_id);
839+
assert_eq!(bob_msg.text, "hi");
840+
Ok(())
841+
}
842+
818843
async fn load_imf_email(context: &Context, imf_raw: &[u8]) -> Message {
819844
context
820845
.set_config(Config::ShowEmails, Some("2"))

src/sync.rs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize};
66

77
use crate::chat::{self, ChatId};
88
use crate::config::Config;
9-
use crate::constants::Blocked;
9+
use crate::constants::{self, Blocked};
1010
use crate::contact::ContactId;
1111
use crate::context::Context;
1212
use crate::log::LogExt;
@@ -317,14 +317,21 @@ impl Context {
317317
for rfc724_mid in msgs {
318318
if let Some((msg_id, _)) = message::rfc724_mid_exists(self, rfc724_mid).await? {
319319
if let Some(msg) = Message::load_from_db_optional(self, msg_id).await? {
320-
message::delete_msg_locally(self, &msg).await?;
320+
let keep_tombstone = false;
321+
message::delete_msg_locally(self, &msg, keep_tombstone).await?;
321322
msg_ids.push(msg.id);
322323
modified_chat_ids.insert(msg.chat_id);
323324
} else {
324325
warn!(self, "Sync message delete: Database entry does not exist.");
325326
}
326327
} else {
327-
warn!(self, "Sync message delete: {rfc724_mid:?} not found.");
328+
info!(self, "sync_message_deletion: {rfc724_mid:?} not found.");
329+
self.sql
330+
.execute(
331+
"INSERT INTO msgs (rfc724_mid, timestamp, chat_id) VALUES (?, ?, ?)",
332+
(rfc724_mid, time(), constants::DC_CHAT_ID_TRASH),
333+
)
334+
.await?;
328335
}
329336
}
330337
message::delete_msgs_locally_done(self, &msg_ids, modified_chat_ids).await?;

0 commit comments

Comments
 (0)