Change the way we replace drafts on the server

Don't overwrite the previous draft in the database. Create a new message entry and convert the previous one to a 'deleted' placeholder.
This also introduces a new pending command 'replace'. It is implemented as upload + delete.
This commit is contained in:
cketti 2020-10-18 18:51:19 +02:00
parent 1ef1af8b0f
commit 54e5d8af9c
7 changed files with 191 additions and 21 deletions

View file

@ -1,10 +1,16 @@
package com.fsck.k9.controller
import com.fsck.k9.Account
import com.fsck.k9.K9
import com.fsck.k9.backend.api.Backend
import com.fsck.k9.controller.MessagingControllerCommands.PendingAppend
import com.fsck.k9.controller.MessagingControllerCommands.PendingReplace
import com.fsck.k9.mail.FetchProfile
import com.fsck.k9.mail.Flag
import com.fsck.k9.mail.Message
import com.fsck.k9.mail.MessagingException
import com.fsck.k9.mailstore.LocalFolder
import com.fsck.k9.mailstore.LocalMessage
import timber.log.Timber
internal class DraftOperations(private val messagingController: MessagingController) {
@ -22,31 +28,129 @@ internal class DraftOperations(private val messagingController: MessagingControl
val localFolder = localStore.getFolder(draftsFolderId)
localFolder.open()
if (existingDraftId != null) {
val uid = localFolder.getMessageUidById(existingDraftId)
message.uid = uid
val localMessage = if (messagingController.supportsUpload(account)) {
saveAndUploadDraft(account, message, localFolder, existingDraftId)
} else {
saveDraftLocally(message, localFolder, existingDraftId)
}
// Save the message to the store.
localFolder.appendMessages(listOf(message))
// Fetch the message back from the store. This is the Message that's returned to the caller.
val localMessage = localFolder.getMessage(message.uid)
localMessage.setFlag(Flag.X_DOWNLOADED_FULL, true)
if (plaintextSubject != null) {
localMessage.setCachedDecryptedSubject(plaintextSubject)
}
if (messagingController.supportsUpload(account)) {
val command = PendingAppend.create(localFolder.databaseId, localMessage.uid)
messagingController.queuePendingCommand(account, command)
messagingController.processPendingCommands(account)
}
localMessage
} catch (e: MessagingException) {
Timber.e(e, "Unable to save message as draft.")
null
}
}
private fun saveAndUploadDraft(
account: Account,
message: Message,
localFolder: LocalFolder,
existingDraftId: Long?
): LocalMessage {
localFolder.appendMessages(listOf(message))
val localMessage = localFolder.getMessage(message.uid)
val previousDraftMessage = if (existingDraftId != null) localFolder.getMessage(existingDraftId) else null
val folderId = localFolder.databaseId
if (previousDraftMessage != null) {
previousDraftMessage.delete()
val uploadMessageId = localMessage.databaseId
val deleteMessageId = previousDraftMessage.databaseId
val command = PendingReplace.create(folderId, uploadMessageId, deleteMessageId)
messagingController.queuePendingCommand(account, command)
} else {
val command = PendingAppend.create(folderId, localMessage.uid)
messagingController.queuePendingCommand(account, command)
}
messagingController.processPendingCommands(account)
return localMessage
}
private fun saveDraftLocally(message: Message, localFolder: LocalFolder, existingDraftId: Long?): LocalMessage {
if (existingDraftId != null) {
// Setting the UID will cause LocalFolder.appendMessages() to replace the existing draft.
message.uid = localFolder.getMessageUidById(existingDraftId)
}
localFolder.appendMessages(listOf(message))
return localFolder.getMessage(message.uid)
}
fun processPendingReplace(command: PendingReplace, account: Account) {
val localStore = messagingController.getLocalStoreOrThrow(account)
val localFolder = localStore.getFolder(command.folderId)
localFolder.open()
val backend = messagingController.getBackend(account)
val uploadMessageId = command.uploadMessageId
val localMessage = localFolder.getMessage(uploadMessageId)
if (localMessage == null) {
Timber.w("Couldn't find local copy of message to upload [ID: %d]", uploadMessageId)
return
} else if (!localMessage.uid.startsWith(K9.LOCAL_UID_PREFIX)) {
Timber.i("Message [ID: %d] to be uploaded already has a server ID set. Skipping upload.", uploadMessageId)
} else {
uploadMessage(backend, account, localFolder, localMessage)
}
deleteMessage(backend, localFolder, command.deleteMessageId)
}
private fun uploadMessage(
backend: Backend,
account: Account,
localFolder: LocalFolder,
localMessage: LocalMessage
) {
val folderServerId = localFolder.serverId
Timber.d("Uploading message [ID: %d] to remote folder '%s'", localMessage.databaseId, folderServerId)
val fetchProfile = FetchProfile().apply {
add(FetchProfile.Item.BODY)
}
localFolder.fetch(listOf(localMessage), fetchProfile, null)
val messageServerId = backend.uploadMessage(folderServerId, localMessage)
if (messageServerId == null) {
Timber.w(
"Failed to get a server ID for the uploaded message. Removing local copy [ID: %d]",
localMessage.databaseId
)
localMessage.destroy()
} else {
val oldUid = localMessage.uid
localMessage.uid = messageServerId
localFolder.changeUid(localMessage)
for (listener in messagingController.listeners) {
listener.messageUidChanged(account, localFolder.databaseId, oldUid, localMessage.uid)
}
}
}
private fun deleteMessage(backend: Backend, localFolder: LocalFolder, messageId: Long) {
val messageServerId = localFolder.getMessageUidById(messageId) ?: run {
Timber.i("Couldn't find local copy of message [ID: %d] to be deleted. Skipping delete.", messageId)
return
}
val messageServerIds = listOf(messageServerId)
val folderServerId = localFolder.serverId
backend.deleteMessages(folderServerId, messageServerIds)
messagingController.destroyPlaceholderMessages(localFolder, messageServerIds)
}
}

View file

@ -51,6 +51,7 @@ import com.fsck.k9.controller.MessagingControllerCommands.PendingExpunge;
import com.fsck.k9.controller.MessagingControllerCommands.PendingMarkAllAsRead;
import com.fsck.k9.controller.MessagingControllerCommands.PendingMoveAndMarkAsRead;
import com.fsck.k9.controller.MessagingControllerCommands.PendingMoveOrCopy;
import com.fsck.k9.controller.MessagingControllerCommands.PendingReplace;
import com.fsck.k9.controller.MessagingControllerCommands.PendingSetFlag;
import com.fsck.k9.controller.ProgressBodyFactory.ProgressListener;
import com.fsck.k9.helper.MutableBoolean;
@ -268,7 +269,7 @@ public class MessagingController {
throw new Error(e);
}
private Backend getBackend(Account account) {
Backend getBackend(Account account) {
return backendManager.getBackend(account);
}
@ -846,6 +847,10 @@ public class MessagingController {
}
}
void processPendingReplace(PendingReplace pendingReplace, Account account) {
draftOperations.processPendingReplace(pendingReplace, account);
}
private void queueMoveOrCopy(Account account, long srcFolderId, long destFolderId, MoveOrCopyFlavor operation,
Map<String, String> uidMap) {
PendingCommand command;
@ -957,7 +962,7 @@ public class MessagingController {
}
}
private void destroyPlaceholderMessages(LocalFolder localFolder, List<String> uids) throws MessagingException {
void destroyPlaceholderMessages(LocalFolder localFolder, List<String> uids) throws MessagingException {
for (String uid : uids) {
LocalMessage placeholderMessage = localFolder.getMessage(uid);
if (placeholderMessage == null) {

View file

@ -14,6 +14,7 @@ import static com.fsck.k9.helper.Preconditions.checkNotNull;
public class MessagingControllerCommands {
static final String COMMAND_APPEND = "append";
static final String COMMAND_REPLACE = "replace";
static final String COMMAND_MARK_ALL_AS_READ = "mark_all_as_read";
static final String COMMAND_SET_FLAG = "set_flag";
static final String COMMAND_DELETE = "delete";
@ -167,6 +168,33 @@ public class MessagingControllerCommands {
}
}
public static class PendingReplace extends PendingCommand {
public final long folderId;
public final long uploadMessageId;
public final long deleteMessageId;
public static PendingReplace create(long folderId, long uploadMessageId, long deleteMessageId) {
return new PendingReplace(folderId, uploadMessageId, deleteMessageId);
}
private PendingReplace(long folderId, long uploadMessageId, long deleteMessageId) {
this.folderId = folderId;
this.uploadMessageId = uploadMessageId;
this.deleteMessageId = deleteMessageId;
}
@Override
public String getCommandName() {
return COMMAND_REPLACE;
}
@Override
public void execute(MessagingController controller, Account account) throws MessagingException {
controller.processPendingReplace(this, account);
}
}
public static class PendingMarkAllAsRead extends PendingCommand {
public final long folderId;

View file

@ -15,6 +15,7 @@ import com.fsck.k9.controller.MessagingControllerCommands.PendingExpunge;
import com.fsck.k9.controller.MessagingControllerCommands.PendingMarkAllAsRead;
import com.fsck.k9.controller.MessagingControllerCommands.PendingMoveAndMarkAsRead;
import com.fsck.k9.controller.MessagingControllerCommands.PendingMoveOrCopy;
import com.fsck.k9.controller.MessagingControllerCommands.PendingReplace;
import com.fsck.k9.controller.MessagingControllerCommands.PendingSetFlag;
import com.squareup.moshi.JsonAdapter;
import com.squareup.moshi.Moshi;
@ -35,6 +36,7 @@ public class PendingCommandSerializer {
adapters.put(MessagingControllerCommands.COMMAND_MOVE_AND_MARK_AS_READ,
moshi.adapter(PendingMoveAndMarkAsRead.class));
adapters.put(MessagingControllerCommands.COMMAND_APPEND, moshi.adapter(PendingAppend.class));
adapters.put(MessagingControllerCommands.COMMAND_REPLACE, moshi.adapter(PendingReplace.class));
adapters.put(MessagingControllerCommands.COMMAND_EMPTY_TRASH, moshi.adapter(PendingEmptyTrash.class));
adapters.put(MessagingControllerCommands.COMMAND_EXPUNGE, moshi.adapter(PendingExpunge.class));
adapters.put(MessagingControllerCommands.COMMAND_MARK_ALL_AS_READ, moshi.adapter(PendingMarkAllAsRead.class));

View file

@ -7,6 +7,7 @@ import android.database.sqlite.SQLiteDatabase;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.fsck.k9.Account;
import com.fsck.k9.DI;
import com.fsck.k9.K9;
@ -730,6 +731,34 @@ public class LocalFolder {
}
}
@Nullable
public LocalMessage getMessage(long messageId) throws MessagingException {
return localStore.getDatabase().execute(false, db -> {
open();
LocalMessage message = new LocalMessage(localStore, messageId, LocalFolder.this);
Cursor cursor = db.rawQuery(
"SELECT " +
LocalStore.GET_MESSAGES_COLS +
"FROM messages " +
"LEFT JOIN message_parts ON (message_parts.id = messages.message_part_id) " +
"LEFT JOIN threads ON (threads.message_id = messages.id) " +
"WHERE messages.id = ? AND folder_id = ?",
new String[] { Long.toString(messageId), Long.toString(databaseId) });
try {
if (cursor.moveToNext()) {
message.populateFromGetMessageCursor(cursor);
} else {
return null;
}
} finally {
Utility.closeQuietly(cursor);
}
return message;
});
}
public List<LocalMessage> getMessages(MessageRetrievalListener<LocalMessage> listener) throws MessagingException {
return getMessages(listener, true);
}

View file

@ -51,6 +51,12 @@ public class LocalMessage extends MimeMessage {
this.mFolder = folder;
}
LocalMessage(LocalStore localStore, long databaseId, LocalFolder folder) {
this.localStore = localStore;
this.databaseId = databaseId;
this.mFolder = folder;
}
void populateFromGetMessageCursor(Cursor cursor) throws MessagingException {
final String subject = cursor.getString(LocalStore.MSG_INDEX_SUBJECT);
@ -300,7 +306,7 @@ public class LocalMessage extends MimeMessage {
* If a message is being marked as deleted we want to clear out its content. Delete will not actually remove the
* row since we need to retain the UID for synchronization purposes.
*/
private void delete() throws MessagingException {
public void delete() throws MessagingException {
try {
localStore.getDatabase().execute(true, new DbCallback<Void>() {
@Override

View file

@ -1535,10 +1535,6 @@ public class MessageCompose extends K9Activity implements OnClickListener,
changesMadeSinceLastSave = false;
currentMessageBuilder = null;
if (action == Action.EDIT_DRAFT && relatedMessageReference != null) {
message.setUid(relatedMessageReference.getUid());
}
new SaveMessageTask(getApplicationContext(), account, contacts, internalMessageHandler,
message, draftMessageId, plaintextSubject).execute();
if (finishAfterDraftSaved) {