Skip to content

fix: Correctly set and read the deleted flag to read and remove messages on the server - #8460

Open
Hocuri wants to merge 9 commits into
mainfrom
hoc/fix-deleting-messages-on-server
Open

fix: Correctly set and read the deleted flag to read and remove messages on the server#8460
Hocuri wants to merge 9 commits into
mainfrom
hoc/fix-deleting-messages-on-server

Conversation

@Hocuri

@Hocuri Hocuri commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator

Make delete_expired_messages() set the deleted flag in the msgs table, and make delete_expired_imap_messages() check it. This fixes two bugs:

  • Ephemeral messages were not deleted on the server: delete_expired_messages() deletes the message locally, then delete_expired_imap_messages() doesn't find the message anymore, and can't mark it for deletion. I could reproduce this bug.
  • Very cornercasy bug: After a UID resync, messages that were marked for deletion were not deleted anymore (a UID resync is very rare, though); found at refactor: Remove FolderMeaning and target_folder #8456 (comment)

Additionally, this improves the performance of the SQL statements by removing the superfluous AND id>9. The AND id>9 made SQLite think that it's smart to use the id index, but since almost all messages have id>9, using this index actually worsened the performance by preventing the use of other, better-suited indexes.

For explanation of the logic, as it was before this PR:

msgs already has a deleted column, but it's only checked if the message later arrives via IMAP (in receive_imf with message::rfc724_mid_exists_ex(context, message_id, "deleted=1")). And it is only set by ChatId::delete_ex() and MsgId::trash(). It is:

  • not set by delete_expired_messages() (which deletes ephemeral messages locally)
  • not checked by delete_expired_imap_messages() (which marks ephemeral messages for deletion on IMAP, and if bcc_self is off it marks all fully downloaded messages for deletion on IMAP [non non-chatmail accounts, only encrypted ones])

@Hocuri
Hocuri marked this pull request as draft July 24, 2026 20:30
@Hocuri Hocuri changed the title fix: Fix deleting messages on the server in some cornercases fix: Fix deleting messages on the server in some cases Jul 24, 2026
@Hocuri Hocuri changed the title fix: Fix deleting messages on the server in some cases fix: Correctly set and read the deleted flag to read and remove messages on the server Jul 24, 2026
@Hocuri
Hocuri requested a review from link2xt July 24, 2026 21:26
Comment thread src/ephemeral.rs Outdated
Base automatically changed from hoc/remove-folder-meaning to main July 27, 2026 12:54
@Hocuri
Hocuri force-pushed the hoc/fix-deleting-messages-on-server branch from 815cf2d to a5300e3 Compare July 29, 2026 12:53
@Hocuri
Hocuri force-pushed the hoc/fix-deleting-messages-on-server branch from 8344b16 to 77cb235 Compare July 29, 2026 13:35
@Hocuri
Hocuri force-pushed the hoc/fix-deleting-messages-on-server branch from 77cb235 to bcc88c9 Compare July 29, 2026 13:37
@Hocuri
Hocuri requested review from hpk42 and link2xt July 29, 2026 14:21
@Hocuri
Hocuri marked this pull request as ready for review July 29, 2026 14:21
// `delete_expired_messages()` is called before `delete_tombstoned_messages_from_imap()`,
// because this matches the behavior in production
// where the ephemeral loop calls `delete_expired_messages()` as soon as a message timer expired.
delete_expired_messages(&t, time()).await?;

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I checked that the test fails now without my fix.

Comment thread src/ephemeral.rs
Comment on lines +695 to +697
// This uses `AND chat_id={DC_CHAT_ID_TRASH}`, so that the index on `chat_id` can be used.
// Only messages in the trash chat are ever marked as deleted, which makes this optimization possible.
// This speeds up this SQL query by a factor of ~3.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering whether we should add AND chat_id={DC_CHAT_ID_TRASH} to the other two SQL statements just to get the same behavior in case someone marks non-trashed messages as deleted, even though it doesn't improve performance there because sqlite anyways needs to do a full scan.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it makes sense even just for consistency sake.

Comment thread src/imap.rs
Comment on lines -606 to -623
let delete = if let Some(message_id) = &message_id {
message::rfc724_mid_exists_ex(context, message_id, "deleted=1")
.await?
.is_some_and(|(_msg_id, deleted)| deleted)
} else {
false
};

// Generate a fake Message-ID to identify the message in the database
// if the message has no real Message-ID.
let message_id = message_id.unwrap_or_else(create_message_id);

if delete {
info!(context, "Deleting locally deleted message {message_id}.");
}

let target = if delete { "" } else { folder };

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This logic is not needed anymore because delete_tombstoned_messages_from_imap() now cares about this.

Comment thread src/ephemeral.rs
Comment on lines 723 to +732
WHERE transport_id=?1
AND rfc724_mid IN (
SELECT rfc724_mid FROM msgs
WHERE id>9
AND ((ephemeral_timestamp!=0 AND ephemeral_timestamp<=?2) OR
((param GLOB '*\nc=1*' OR param GLOB 'c=1*') AND download_state=?3))
WHERE ((param GLOB '*\nc=1*' OR param GLOB 'c=1*') AND download_state=?2) OR
deleted=1
UNION
SELECT pre_rfc724_mid FROM msgs
WHERE pre_rfc724_mid!=''
AND id>9
AND ((ephemeral_timestamp!=0 AND ephemeral_timestamp<=?2) OR
(param GLOB '*\nc=1*' OR param GLOB 'c=1*'))
AND param GLOB '*\nc=1*' OR param GLOB 'c=1*' OR deleted=1
)",
(transport_id, now, DownloadState::Done),
(transport_id, DownloadState::Done),

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FTR, we will probably remove this whole SQL statement once we remove is_chatmail. I fixed it anyways now.

Comment thread src/imap.rs
Comment on lines -649 to -650
if folder == target
&& prefetch_should_download(context, &headers, &message_id, fetch_response.flags())

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The folder==target condition is not needed because prefetch_should_download() will return false if a trashed message exists.

Although that there is a bug in prefetch_should_download() that it returns true for pre-messages even when they were already downloaded; I will make a PR for that.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will make a PR for that.

Let's maybe merge that one first then.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made a PR: #8488

Comment thread src/ephemeral.rs Outdated
Comment thread src/ephemeral.rs Outdated

@hpk42 hpk42 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thinking more about it, i think we shouldn't remove messages on the server because of device_delete_after. It means deleting encrypted messages after maybe just an hour or a day, blocking all other devices from ever seeing those. This is clearly not the intention or the understanding of a user who sets "device-delete-after" in the current UIs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants