Skip to content

Commit 693863c

Browse files
committed
fix: Receiving of re-sent locally deleted messages (#7115)
Before, locally deleted messages (because of DeleteDeviceAfter set or external deletion requests) couldn't be received if they're re-sent because tombstones prevented that forever. This led to locally deleted webxdcs being unrecoverable.
1 parent 2762a90 commit 693863c

File tree

4 files changed

+68
-39
lines changed

4 files changed

+68
-39
lines changed

src/imap.rs

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2352,11 +2352,11 @@ pub(crate) async fn prefetch_should_download(
23522352
)
23532353
.await?
23542354
{
2355-
if is_trash {
2356-
message::prune_tombstone(context, message_id).await?;
2355+
let should = is_trash && !message::prune_tombstone(context, message_id).await?;
2356+
if !should {
2357+
markseen_on_imap_table(context, message_id).await?;
23572358
}
2358-
markseen_on_imap_table(context, message_id).await?;
2359-
return Ok(false);
2359+
return Ok(should);
23602360
}
23612361

23622362
// We do not know the Message-ID or the Message-ID is missing (in this case, we create one in
@@ -2429,8 +2429,11 @@ pub(crate) async fn prefetch_should_download(
24292429
pub(crate) fn is_dup_msg(is_chat_msg: bool, ts_sent: i64, ts_sent_old: i64) -> bool {
24302430
// If the existing message has timestamp_sent == 0, that means we don't know its actual sent
24312431
// timestamp, so don't delete the new message. E.g. outgoing messages have zero timestamp_sent
2432-
// because they are stored to the db before sending. Also consider as duplicates only messages
2433-
// with greater timestamp to avoid deleting both messages in a multi-device setting.
2432+
// because they are stored to the db before sending. Trashed messages also have zero
2433+
// timestamp_sent and mustn't make new messages "duplicates", otherwise if a webxdc message is
2434+
// deleted because of DeleteDeviceAfter set, it won't be recovered from a re-sent message. Also
2435+
// consider as duplicates only messages with greater timestamp to avoid deleting both messages
2436+
// in a multi-device setting.
24342437
is_chat_msg && ts_sent_old != 0 && ts_sent > ts_sent_old
24352438
}
24362439

src/message.rs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1746,8 +1746,9 @@ pub async fn delete_msgs_ex(
17461746
}
17471747

17481748
/// 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
1749+
/// Returns whether the removal happened.
1750+
pub(crate) async fn prune_tombstone(context: &Context, rfc724_mid: &str) -> Result<bool> {
1751+
Ok(context
17511752
.sql
17521753
.execute(
17531754
"DELETE FROM msgs
@@ -1758,8 +1759,8 @@ pub(crate) async fn prune_tombstone(context: &Context, rfc724_mid: &str) -> Resu
17581759
)",
17591760
(rfc724_mid, DC_CHAT_ID_TRASH),
17601761
)
1761-
.await?;
1762-
Ok(())
1762+
.await?
1763+
> 0)
17631764
}
17641765

17651766
/// Marks requested messages as seen.

src/receive_imf.rs

Lines changed: 38 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -558,43 +558,53 @@ pub(crate) async fn receive_imf_inner(
558558
let (replace_msg_id, replace_chat_id);
559559
if let Some((old_msg_id, _)) = message::rfc724_mid_exists(context, rfc724_mid).await? {
560560
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?;
561+
// The tombstone being pruned means that we expected the message to appear on IMAP after
562+
// deletion. NB: Not all such messages have `msgs.deleted=1`, see how external deletion
563+
// requests deal with message reordering.
564+
match msg.is_none() && !message::prune_tombstone(context, rfc724_mid).await? {
565+
true => replace_msg_id = None,
566+
false => replace_msg_id = Some(old_msg_id),
563567
}
564-
replace_msg_id = Some(old_msg_id);
565568
if let Some(msg) = msg.filter(|msg| msg.download_state() != DownloadState::Done) {
566569
// the message was partially downloaded before and is fully downloaded now.
567570
info!(context, "Message already partly in DB, replacing.");
568571
replace_chat_id = Some(msg.chat_id);
569572
} else {
570573
replace_chat_id = None;
571574
}
572-
} else {
573-
replace_msg_id = if rfc724_mid_orig == rfc724_mid {
574-
None
575-
} else if let Some((old_msg_id, old_ts_sent)) =
576-
message::rfc724_mid_exists(context, rfc724_mid_orig).await?
577-
{
578-
message::prune_tombstone(context, rfc724_mid_orig).await?;
579-
if imap::is_dup_msg(
580-
mime_parser.has_chat_version(),
581-
mime_parser.timestamp_sent,
582-
old_ts_sent,
583-
) {
584-
info!(context, "Deleting duplicate message {rfc724_mid_orig}.");
585-
let target = context.get_delete_msgs_target().await?;
586-
context
587-
.sql
588-
.execute(
589-
"UPDATE imap SET target=? WHERE folder=? AND uidvalidity=? AND uid=?",
590-
(target, folder, uidvalidity, uid),
591-
)
592-
.await?;
593-
}
594-
Some(old_msg_id)
575+
} else if rfc724_mid_orig == rfc724_mid {
576+
replace_msg_id = None;
577+
replace_chat_id = None;
578+
} else if let Some((old_msg_id, old_ts_sent, is_trash)) = message::rfc724_mid_exists_ex(
579+
context,
580+
rfc724_mid_orig,
581+
"chat_id=3", // Trash
582+
)
583+
.await?
584+
{
585+
if is_trash && !message::prune_tombstone(context, rfc724_mid_orig).await? {
586+
replace_msg_id = None;
587+
} else if imap::is_dup_msg(
588+
mime_parser.has_chat_version(),
589+
mime_parser.timestamp_sent,
590+
old_ts_sent,
591+
) {
592+
info!(context, "Deleting duplicate message {rfc724_mid_orig}.");
593+
let target = context.get_delete_msgs_target().await?;
594+
context
595+
.sql
596+
.execute(
597+
"UPDATE imap SET target=? WHERE folder=? AND uidvalidity=? AND uid=?",
598+
(target, folder, uidvalidity, uid),
599+
)
600+
.await?;
601+
replace_msg_id = Some(old_msg_id);
595602
} else {
596-
None
597-
};
603+
replace_msg_id = Some(old_msg_id);
604+
}
605+
replace_chat_id = None;
606+
} else {
607+
replace_msg_id = None;
598608
replace_chat_id = None;
599609
}
600610

src/webxdc/webxdc_tests.rs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1853,6 +1853,14 @@ async fn test_status_update_vs_delete_device_after() -> Result<()> {
18531853
.await?;
18541854
let alice_chat = alice.create_chat(bob).await;
18551855
let alice_instance = send_webxdc_instance(alice, alice_chat.id).await?;
1856+
// Needed to test receiving of re-sent locally deleted webxdc.
1857+
bob.sql
1858+
.execute(
1859+
"INSERT INTO imap (rfc724_mid, folder, uid, uidvalidity, target)
1860+
VALUES (?1, ?2, ?3, ?4, ?5)",
1861+
(&alice_instance.rfc724_mid, "INBOX", 1, 1, "INBOX"),
1862+
)
1863+
.await?;
18561864
let bob_instance = bob.recv_msg(&alice.pop_sent_msg().await).await;
18571865
assert_eq!(bob.add_or_lookup_contact(alice).await.is_bot(), false);
18581866

@@ -1882,8 +1890,15 @@ async fn test_status_update_vs_delete_device_after() -> Result<()> {
18821890
SystemTime::shift(Duration::from_secs(2700));
18831891
ephemeral::delete_expired_messages(bob, tools::time()).await?;
18841892
let bob_instance = Message::load_from_db(bob, bob_instance.id).await?;
1885-
assert_eq!(bob_instance.chat_id.is_trash(), false);
18861893

1894+
SystemTime::shift(Duration::from_secs(1800));
1895+
ephemeral::delete_expired_messages(bob, tools::time()).await?;
1896+
let bob_instance = Message::load_from_db_optional(bob, bob_instance.id).await?;
1897+
assert!(bob_instance.is_none());
1898+
1899+
// Additionally test that a re-sent instance can be received after deletion.
1900+
resend_msgs(alice, &[alice_instance.id]).await?;
1901+
bob.recv_msg(&alice.pop_sent_msg().await).await;
18871902
Ok(())
18881903
}
18891904

0 commit comments

Comments
 (0)