From 3ddef9b875536e2a49fc575b8c4ed0694aaf4b34 Mon Sep 17 00:00:00 2001 From: numericOverflow Date: Wed, 20 May 2026 16:41:55 -0500 Subject: [PATCH 1/2] Change message save operation from replace to update fix #9142 --- .../java/com/fsck/k9/storage/messages/SaveMessageOperations.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/legacy/storage/src/main/java/com/fsck/k9/storage/messages/SaveMessageOperations.kt b/legacy/storage/src/main/java/com/fsck/k9/storage/messages/SaveMessageOperations.kt index 1490ae8966b..23dd03105e5 100644 --- a/legacy/storage/src/main/java/com/fsck/k9/storage/messages/SaveMessageOperations.kt +++ b/legacy/storage/src/main/java/com/fsck/k9/storage/messages/SaveMessageOperations.kt @@ -426,8 +426,7 @@ internal class SaveMessageOperations( } return if (replaceMessageId != null) { - values.put("id", replaceMessageId) - database.replace("messages", null, values) + database.update("messages", values, "id = ?", arrayOf(replaceMessageId.toString())) replaceMessageId } else { database.insert("messages", null, values) From ded696933e47cf0aa9c8560e64004bf03dbdb15d Mon Sep 17 00:00:00 2001 From: numericOverflow Date: Wed, 20 May 2026 16:43:42 -0500 Subject: [PATCH 2/2] Add test for replacing envelope message with full message --- .../messages/SaveMessageOperationsTest.kt | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/legacy/storage/src/test/java/com/fsck/k9/storage/messages/SaveMessageOperationsTest.kt b/legacy/storage/src/test/java/com/fsck/k9/storage/messages/SaveMessageOperationsTest.kt index fe8fd7c86b2..ba20d70fd73 100644 --- a/legacy/storage/src/test/java/com/fsck/k9/storage/messages/SaveMessageOperationsTest.kt +++ b/legacy/storage/src/test/java/com/fsck/k9/storage/messages/SaveMessageOperationsTest.kt @@ -379,6 +379,46 @@ class SaveMessageOperationsTest : RobolectricTest() { assertThat(thread.messageId).isEqualTo(message.id) } + @Test + fun `replace envelope message with full message should preserve thread entry`() { + // Arrange: simulate remote search saving a message as ENVELOPE (no body) + val envelopeMessageData = buildMessage { + header("Message-ID", "") + header("Subject", "Search Result") + }.toSaveMessageData( + downloadState = MessageDownloadState.ENVELOPE, + ) + saveMessageOperations.saveRemoteMessage(folderId = 1, messageServerId = "uid1", envelopeMessageData) + + val threadsBeforeReplace = sqliteDatabase.readThreads() + assertThat(threadsBeforeReplace).hasSize(1) + val threadBeforeReplace = threadsBeforeReplace.first() + + // Act: simulate sync re-downloading the same message as FULL + val fullMessageData = buildMessage { + header("Message-ID", "") + header("Subject", "Search Result") + textBody("Full body content") + }.toSaveMessageData( + downloadState = MessageDownloadState.FULL, + ) + saveMessageOperations.saveRemoteMessage(folderId = 1, messageServerId = "uid1", fullMessageData) + + // Assert: thread entry must still exist and point to the same message + val messages = sqliteDatabase.readMessages() + assertThat(messages).hasSize(1) + val message = messages.first() + assertThat(message.flags).isEqualTo("X_DOWNLOADED_FULL") + + val threads = sqliteDatabase.readThreads() + assertThat(threads).hasSize(1) + val thread = threads.first() + assertThat(thread.id).isEqualTo(threadBeforeReplace.id) + assertThat(thread.messageId).isEqualTo(message.id) + assertThat(thread.root).isEqualTo(thread.id) + assertThat(thread.parent).isNull() + } + @Test fun `save local message`() { val messageData = buildMessage {