Repository: thundernest/k-9
Issue: Remove placeholder in database when message deletion on server is finished

The "Clear messages (Dangerous!)" option in mailbox action popup menu suggests that ALL messages are deleted (either for privacy or to recreate corrupted mail-queue). However the placeholders are not deleted - exposing potentially sensitive information about deleted conversations.

I dont know if the clear() method here is used only by "Remove messages (dangerous!)" sub-menu so I decided to open an issue rather than simply removing "deleted = 0" from here:

https://github.com/k9mail/k-9/blob/c053964fefe1c758251aeab902563f8ec7dc454c/app/core/src/main/java/com/fsck/k9/mailstore/LocalStore.java#L327

Changed files:
app/core/src/main/java/com/fsck/k9/controller/MessagingController.java

diff --git a/app/core/src/main/java/com/fsck/k9/controller/MessagingController.java b/app/core/src/main/java/com/fsck/k9/controller/MessagingController.java
index 13a25745f..1c6dfe0a9 100644
--- a/app/core/src/main/java/com/fsck/k9/controller/MessagingController.java
+++ b/app/core/src/main/java/com/fsck/k9/controller/MessagingController.java
@@ -40,6 +40,7 @@ import com.fsck.k9.K9.Intents;
 import com.fsck.k9.Preferences;
 import com.fsck.k9.backend.BackendManager;
 import com.fsck.k9.backend.api.Backend;
+import com.fsck.k9.backend.api.BuildConfig;
 import com.fsck.k9.backend.api.SyncConfig;
 import com.fsck.k9.backend.api.SyncListener;
 import com.fsck.k9.cache.EmailProviderCache;
@@ -881,10 +882,13 @@ public class MessagingController {
                 throw new RuntimeException("Unsupported messaging operation");
         }
 
-        if (operation != MoveOrCopyFlavor.COPY && backend.getSupportsExpunge()
-                && account.getExpungePolicy() == Expunge.EXPUNGE_IMMEDIATELY) {
-            Timber.i("processingPendingMoveOrCopy expunging folder %s:%s", account.getDescription(), srcFolderServerId);
-            backend.expungeMessages(srcFolderServerId, uids);
+        if (operation != MoveOrCopyFlavor.COPY) {
+            if (backend.getSupportsExpunge() && account.getExpungePolicy() == Expunge.EXPUNGE_IMMEDIATELY) {
+                Timber.i("processingPendingMoveOrCopy expunging folder %s:%s", account.getDescription(), srcFolderServerId);
+                backend.expungeMessages(srcFolderServerId, uids);
+            }
+
+            destroyPlaceholderMessages(localSourceFolder, uids);
         }
 
         // TODO: Change Backend interface to ensure we never receive null for remoteUidMap
@@ -917,6 +921,26 @@ public class MessagingController {
         }
     }
 
+    private void destroyPlaceholderMessages(LocalFolder localFolder, List<String> uids) throws MessagingException {
+        for (String uid : uids) {
+            LocalMessage placeholderMessage = localFolder.getMessage(uid);
+            if (placeholderMessage == null) {
+                continue;
+            }
+
+            if (placeholderMessage.isSet(Flag.DELETED)) {
+                placeholderMessage.destroy();
+            } else {
+                Timber.w("Expected local message %s in folder %s to be a placeholder, but DELETE flag wasn't set",
+                        uid, localFolder.getServerId());
+
+                if (BuildConfig.DEBUG) {
+                    throw new AssertionError("Placeholder message must have the DELETED flag set");
+                }
+            }
+        }
+    }
+
     private void queueSetFlag(Account account, long folderId, boolean newState, Flag flag, List<String> uids) {
         putBackground("queueSetFlag", null, () -> {
             PendingCommand command = PendingSetFlag.create(folderId, newState, flag, uids);