Skip to content

Commit 37e0aae

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 2cd54b7 commit 37e0aae

File tree

4 files changed

+87
-36
lines changed

4 files changed

+87
-36
lines changed

src/imap.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -595,6 +595,9 @@ impl Imap {
595595
let target = if let Some(message_id) = &message_id {
596596
let msg_info =
597597
message::rfc724_mid_exists_ex(context, message_id, "deleted=1").await?;
598+
if msg_info.is_some() {
599+
message::prune_tombstone(context, message_id).await?;
600+
}
598601
let delete = if let Some((_, _, true)) = msg_info {
599602
info!(context, "Deleting locally deleted message {message_id}.");
600603
true
@@ -2252,6 +2255,7 @@ pub(crate) async fn prefetch_should_download(
22522255
.await?
22532256
.is_some()
22542257
{
2258+
message::prune_tombstone(context, message_id).await?;
22552259
markseen_on_imap_table(context, message_id).await?;
22562260
return Ok(false);
22572261
}

src/message.rs

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

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

@@ -1668,11 +1670,15 @@ pub(crate) async fn get_mime_headers(context: &Context, msg_id: MsgId) -> Result
16681670

16691671
/// Delete a single message from the database, including references in other tables.
16701672
/// This may be called in batches; the final events are emitted in delete_msgs_locally_done() then.
1671-
pub(crate) async fn delete_msg_locally(context: &Context, msg: &Message) -> Result<()> {
1673+
pub(crate) async fn delete_msg_locally(
1674+
context: &Context,
1675+
msg: &Message,
1676+
keep_tombstone: bool,
1677+
) -> Result<()> {
16721678
if msg.location_id > 0 {
16731679
delete_poi_location(context, msg.location_id).await?;
16741680
}
1675-
let on_server = true;
1681+
let on_server = keep_tombstone;
16761682
msg.id
16771683
.trash(context, on_server)
16781684
.await
@@ -1740,6 +1746,7 @@ pub async fn delete_msgs_ex(
17401746
let mut modified_chat_ids = HashSet::new();
17411747
let mut deleted_rfc724_mid = Vec::new();
17421748
let mut res = Ok(());
1749+
let mut msgs = Vec::with_capacity(msg_ids.len());
17431750

17441751
for &msg_id in msg_ids {
17451752
let msg = Message::load_from_db(context, msg_id).await?;
@@ -1757,18 +1764,24 @@ pub async fn delete_msgs_ex(
17571764

17581765
let target = context.get_delete_msgs_target().await?;
17591766
let update_db = |trans: &mut rusqlite::Transaction| {
1760-
trans.execute(
1767+
// NB: If the message isn't sent yet, keeping its tombstone is unnecessary, but safe.
1768+
let keep_tombstone = trans.execute(
17611769
"UPDATE imap SET target=? WHERE rfc724_mid=?",
1762-
(target, msg.rfc724_mid),
1763-
)?;
1770+
(&target, msg.rfc724_mid),
1771+
)? == 0
1772+
|| !target.is_empty();
17641773
trans.execute("DELETE FROM smtp WHERE msg_id=?", (msg_id,))?;
1765-
Ok(())
1774+
Ok(keep_tombstone)
17661775
};
1767-
if let Err(e) = context.sql.transaction(update_db).await {
1768-
error!(context, "delete_msgs: failed to update db: {e:#}.");
1769-
res = Err(e);
1770-
continue;
1771-
}
1776+
let keep_tombstone = match context.sql.transaction(update_db).await {
1777+
Ok(v) => v,
1778+
Err(e) => {
1779+
error!(context, "delete_msgs: failed to update db: {e:#}.");
1780+
res = Err(e);
1781+
continue;
1782+
}
1783+
};
1784+
msgs.push((msg_id, keep_tombstone));
17721785
}
17731786
res?;
17741787

@@ -1797,9 +1810,9 @@ pub async fn delete_msgs_ex(
17971810
.await?;
17981811
}
17991812

1800-
for &msg_id in msg_ids {
1813+
for (msg_id, keep_tombstone) in msgs {
18011814
let msg = Message::load_from_db(context, msg_id).await?;
1802-
delete_msg_locally(context, &msg).await?;
1815+
delete_msg_locally(context, &msg, keep_tombstone).await?;
18031816
}
18041817
delete_msgs_locally_done(context, msg_ids, modified_chat_ids).await?;
18051818

@@ -1809,6 +1822,23 @@ pub async fn delete_msgs_ex(
18091822
Ok(())
18101823
}
18111824

1825+
/// Removes from the database a locally deleted message that also doesn't have a server UID.
1826+
pub(crate) async fn prune_tombstone(context: &Context, rfc724_mid: &str) -> Result<()> {
1827+
context
1828+
.sql
1829+
.execute(
1830+
"DELETE FROM msgs
1831+
WHERE rfc724_mid=?
1832+
AND chat_id=?
1833+
AND NOT EXISTS (
1834+
SELECT * FROM imap WHERE msgs.rfc724_mid=rfc724_mid AND target!=''
1835+
)",
1836+
(rfc724_mid, DC_CHAT_ID_TRASH),
1837+
)
1838+
.await?;
1839+
Ok(())
1840+
}
1841+
18121842
/// Marks requested messages as seen.
18131843
pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()> {
18141844
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

@@ -559,24 +559,28 @@ pub(crate) async fn receive_imf_inner(
559559
);
560560
return Ok(None);
561561
}
562-
let msg = Message::load_from_db(context, old_msg_id).await?;
562+
let msg = Message::load_from_db_optional(context, old_msg_id).await?;
563+
if msg.is_none() {
564+
message::prune_tombstone(context, rfc724_mid).await?;
565+
}
563566
replace_msg_id = Some(old_msg_id);
564-
replace_chat_id = if msg.download_state() != DownloadState::Done {
567+
if let Some(msg) = msg.filter(|msg| msg.download_state() != DownloadState::Done) {
565568
// the message was partially downloaded before and is fully downloaded now.
566569
info!(
567570
context,
568571
"Message already partly in DB, replacing by full message."
569572
);
570-
Some(msg.chat_id)
573+
replace_chat_id = Some(msg.chat_id);
571574
} else {
572-
None
573-
};
575+
replace_chat_id = None;
576+
}
574577
} else {
575578
replace_msg_id = if rfc724_mid_orig == rfc724_mid {
576579
None
577580
} else if let Some((old_msg_id, old_ts_sent)) =
578581
message::rfc724_mid_exists(context, rfc724_mid_orig).await?
579582
{
583+
message::prune_tombstone(context, rfc724_mid_orig).await?;
580584
if imap::is_dup_msg(
581585
mime_parser.has_chat_version(),
582586
mime_parser.timestamp_sent,
@@ -2194,10 +2198,7 @@ RETURNING id
21942198
}
21952199

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

@@ -2305,7 +2306,8 @@ async fn handle_edit_delete(
23052306
{
23062307
if let Some(msg) = Message::load_from_db_optional(context, msg_id).await? {
23072308
if msg.from_id == from_id {
2308-
message::delete_msg_locally(context, &msg).await?;
2309+
let keep_tombstone = false;
2310+
message::delete_msg_locally(context, &msg, keep_tombstone).await?;
23092311
msg_ids.push(msg.id);
23102312
modified_chat_ids.insert(msg.chat_id);
23112313
} else {
@@ -2316,6 +2318,14 @@ async fn handle_edit_delete(
23162318
}
23172319
} else {
23182320
warn!(context, "Delete message: {rfc724_mid:?} not found.");
2321+
context
2322+
.sql
2323+
.execute(
2324+
"INSERT INTO msgs (rfc724_mid, timestamp, chat_id)
2325+
VALUES (?, ?, ?)",
2326+
(rfc724_mid, time(), DC_CHAT_ID_TRASH),
2327+
)
2328+
.await?;
23192329
}
23202330
}
23212331
message::delete_msgs_locally_done(context, &msg_ids, modified_chat_ids).await?;

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;
@@ -310,14 +310,21 @@ impl Context {
310310
for rfc724_mid in msgs {
311311
if let Some((msg_id, _)) = message::rfc724_mid_exists(self, rfc724_mid).await? {
312312
if let Some(msg) = Message::load_from_db_optional(self, msg_id).await? {
313-
message::delete_msg_locally(self, &msg).await?;
313+
let keep_tombstone = false;
314+
message::delete_msg_locally(self, &msg, keep_tombstone).await?;
314315
msg_ids.push(msg.id);
315316
modified_chat_ids.insert(msg.chat_id);
316317
} else {
317318
warn!(self, "Sync message delete: Database entry does not exist.");
318319
}
319320
} else {
320-
warn!(self, "Sync message delete: {rfc724_mid:?} not found.");
321+
info!(self, "sync_message_deletion: {rfc724_mid:?} not found.");
322+
self.sql
323+
.execute(
324+
"INSERT INTO msgs (rfc724_mid, timestamp, chat_id) VALUES (?, ?, ?)",
325+
(rfc724_mid, time(), constants::DC_CHAT_ID_TRASH),
326+
)
327+
.await?;
321328
}
322329
}
323330
message::delete_msgs_locally_done(self, &msg_ids, modified_chat_ids).await?;

0 commit comments

Comments
 (0)