Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 9acef0e9 authored by cketti's avatar cketti
Browse files

Remove placeholder messages that have been kept accidentally in the past

parent 5fd08c44
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -12,7 +12,7 @@ import timber.log.Timber;


class StoreSchemaDefinition implements SchemaDefinition {
    static final int DB_VERSION = 73;
    static final int DB_VERSION = 74;

    private final MigrationsHelper migrationsHelper;

+124 −0
Original line number Diff line number Diff line
package com.fsck.k9.storage.migrations

import android.content.ContentValues
import android.database.sqlite.SQLiteDatabase
import com.fsck.k9.Account
import com.fsck.k9.Account.DeletePolicy

/**
 * Remove all placeholder entries in 'messages' table
 *
 * When the user moves or deletes an email, the row in the 'messages' table is first updated with 'deleted = 1', turning
 * it into a placeholder message. After the remote operation has completed, the row is removed.
 *
 * Placeholder messages are created to prevent an email from being downloaded again during a sync before the remote
 * operation has finished. The placeholder message is also necessary to remember deleted messages when using a delete
 * policy other than "delete from server".
 *
 * Previously these placeholder messages often weren't removed when they should have been. This will slowly grow the
 * database.
 * Here we remove all placeholder messages when the delete policy is "delete from server". This might also include
 * placeholder messages that shouldn't be removed, because the remote operation hasn't been performed yet.
 * However, since the app tries to execute all pending remote operations before doing a message sync, it's unlikely that
 * deleted messages are re-downloaded. And if they are, the next sync after the remote operation has completed will
 * remove them again.
 */
internal class MigrationTo74(private val db: SQLiteDatabase, private val account: Account) {

    fun removeDeletedMessages() {
        if (account.deletePolicy != DeletePolicy.ON_DELETE) return

        db.query("messages", arrayOf("id"), "deleted = 1", null, null, null, null).use { cursor ->
            while (cursor.moveToNext()) {
                destroyMessage(messageId = cursor.getLong(0))
            }
        }
    }

    private fun destroyMessage(messageId: Long) {
        if (hasThreadChildren(messageId)) {
            // This message has children in the thread structure so we need to make it an empty message.
            val cv = ContentValues().apply {
                put("deleted", 0)
                put("empty", 1)
                put("preview_type", "none")
                put("read", 0)
                put("flagged", 0)
                put("answered", 0)
                put("forwarded", 0)
                putNull("subject")
                putNull("sender_list")
                putNull("date")
                putNull("to_list")
                putNull("cc_list")
                putNull("bcc_list")
                putNull("preview")
                putNull("reply_to_list")
                putNull("message_part_id")
                putNull("flags")
                putNull("attachment_count")
                putNull("internal_date")
                putNull("mime_type")
                putNull("encryption_type")
            }
            db.update("messages", cv, "id = ?", arrayOf(messageId.toString()))

            // Nothing else to do
            return
        }

        // Get the message ID of the parent message if it's empty
        var currentId = getEmptyThreadParent(messageId)

        // Delete the placeholder message
        deleteMessageRow(messageId)

        // Walk the thread tree to delete all empty parents without children
        while (currentId != -1L) {
            if (hasThreadChildren(currentId)) {
                // We made sure there are no empty leaf nodes and can stop now.
                break
            }

            // Get ID of the (empty) parent for the next iteration
            val newId = getEmptyThreadParent(currentId)

            // Delete the empty message
            deleteMessageRow(currentId)
            currentId = newId
        }
    }

    private fun hasThreadChildren(messageId: Long): Boolean {
        return db.rawQuery(
            "SELECT COUNT(t2.id) " +
                "FROM threads t1 " +
                "JOIN threads t2 ON (t2.parent = t1.id) " +
                "WHERE t1.message_id = ?",
            arrayOf(messageId.toString())
        ).use { cursor ->
            cursor.moveToFirst() && !cursor.isNull(0) && cursor.getLong(0) > 0L
        }
    }

    private fun getEmptyThreadParent(messageId: Long): Long {
        return db.rawQuery(
            "SELECT m.id " +
                "FROM threads t1 " +
                "JOIN threads t2 ON (t1.parent = t2.id) " +
                "LEFT JOIN messages m ON (t2.message_id = m.id) " +
                "WHERE t1.message_id = ? AND m.empty = 1",
            arrayOf(messageId.toString())
        ).use { cursor ->
            if (cursor.moveToFirst() && !cursor.isNull(0)) cursor.getLong(0) else -1
        }
    }

    private fun deleteMessageRow(messageId: Long) {
        // Delete the message
        db.delete("messages", "id = ?", arrayOf(messageId.toString()))

        // Delete row in 'threads' table
        db.delete("threads", "message_id = ?", arrayOf(messageId.toString()))
    }
}
+1 −0
Original line number Diff line number Diff line
@@ -19,5 +19,6 @@ object Migrations {
        if (oldVersion < 71) MigrationTo71(db).cleanUpFolderClass()
        if (oldVersion < 72) MigrationTo72(db).createMessagePartsRootIndex()
        if (oldVersion < 73) MigrationTo73(db).rewritePendingCommandsToUseFolderIds()
        if (oldVersion < 74) MigrationTo74(db, migrationsHelper.account).removeDeletedMessages()
    }
}