Merge pull request #5259 from k9mail/save_messages

This commit is contained in:
cketti 2021-04-20 21:23:47 +02:00 committed by GitHub
commit c43f27a9ff
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 316 additions and 99 deletions

View file

@ -6,14 +6,21 @@ 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 com.fsck.k9.mailstore.MessageStoreManager
import com.fsck.k9.mailstore.SaveMessageData
import com.fsck.k9.mailstore.SaveMessageDataCreator
import org.jetbrains.annotations.NotNull
import timber.log.Timber
internal class DraftOperations(private val messagingController: MessagingController) {
internal class DraftOperations(
private val messagingController: @NotNull MessagingController,
private val messageStoreManager: @NotNull MessageStoreManager,
private val saveMessageDataCreator: SaveMessageDataCreator
) {
fun saveDraft(
account: Account,
@ -24,22 +31,13 @@ internal class DraftOperations(private val messagingController: MessagingControl
return try {
val draftsFolderId = account.draftsFolderId ?: error("No Drafts folder configured")
val localStore = messagingController.getLocalStoreOrThrow(account)
val localFolder = localStore.getFolder(draftsFolderId)
localFolder.open()
val localMessage = if (messagingController.supportsUpload(account)) {
saveAndUploadDraft(account, message, localFolder, existingDraftId)
val messageId = if (messagingController.supportsUpload(account)) {
saveAndUploadDraft(account, message, draftsFolderId, existingDraftId, plaintextSubject)
} else {
saveDraftLocally(message, localFolder, existingDraftId)
saveDraftLocally(account, message, draftsFolderId, existingDraftId, plaintextSubject)
}
localMessage.setFlag(Flag.X_DOWNLOADED_FULL, true)
if (plaintextSubject != null) {
localMessage.setCachedDecryptedSubject(plaintextSubject)
}
localMessage.databaseId
messageId
} catch (e: MessagingException) {
Timber.e(e, "Unable to save message as draft.")
null
@ -49,41 +47,52 @@ internal class DraftOperations(private val messagingController: MessagingControl
private fun saveAndUploadDraft(
account: Account,
message: Message,
localFolder: LocalFolder,
existingDraftId: Long?
): LocalMessage {
localFolder.appendMessages(listOf(message))
folderId: Long,
existingDraftId: Long?,
subject: String?
): Long {
val messageStore = messageStoreManager.getMessageStore(account)
val localMessage = localFolder.getMessage(message.uid)
val previousDraftMessage = if (existingDraftId != null) localFolder.getMessage(existingDraftId) else null
val messageId = messageStore.saveLocalMessage(folderId, message.toSaveMessageData(subject))
val previousDraftMessage = if (existingDraftId != null) {
val localStore = messagingController.getLocalStoreOrThrow(account)
val localFolder = localStore.getFolder(folderId)
localFolder.open()
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)
val command = PendingReplace.create(folderId, messageId, deleteMessageId)
messagingController.queuePendingCommand(account, command)
} else {
val command = PendingAppend.create(folderId, localMessage.uid)
val fakeMessageServerId = messageStore.getMessageServerId(messageId)
val command = PendingAppend.create(folderId, fakeMessageServerId)
messagingController.queuePendingCommand(account, command)
}
messagingController.processPendingCommands(account)
return localMessage
return messageId
}
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)
}
private fun saveDraftLocally(
account: Account,
message: Message,
folderId: Long,
existingDraftId: Long?,
plaintextSubject: String?
): Long {
val messageStore = messageStoreManager.getMessageStore(account)
val messageData = message.toSaveMessageData(plaintextSubject)
localFolder.appendMessages(listOf(message))
return localFolder.getMessage(message.uid)
return messageStore.saveLocalMessage(folderId, messageData, existingDraftId)
}
fun processPendingReplace(command: PendingReplace, account: Account) {
@ -153,4 +162,8 @@ internal class DraftOperations(private val messagingController: MessagingControl
messagingController.destroyPlaceholderMessages(localFolder, messageServerIds)
}
private fun Message.toSaveMessageData(subject: String?): SaveMessageData {
return saveMessageDataCreator.createSaveMessageData(this, partialMessage = false, subject)
}
}

View file

@ -5,6 +5,7 @@ import com.fsck.k9.Preferences
import com.fsck.k9.backend.BackendManager
import com.fsck.k9.mailstore.LocalStoreProvider
import com.fsck.k9.mailstore.MessageStoreManager
import com.fsck.k9.mailstore.SaveMessageDataCreator
import com.fsck.k9.notification.NotificationController
import com.fsck.k9.notification.NotificationStrategy
import org.koin.core.qualifier.named
@ -21,6 +22,7 @@ val controllerModule = module {
get<BackendManager>(),
get<Preferences>(),
get<MessageStoreManager>(),
get<SaveMessageDataCreator>(),
get(named("controllerExtensions"))
)
}

View file

@ -72,6 +72,8 @@ import com.fsck.k9.mailstore.MessageStore;
import com.fsck.k9.mailstore.MessageStoreManager;
import com.fsck.k9.mailstore.OutboxState;
import com.fsck.k9.mailstore.OutboxStateRepository;
import com.fsck.k9.mailstore.SaveMessageData;
import com.fsck.k9.mailstore.SaveMessageDataCreator;
import com.fsck.k9.mailstore.SendState;
import com.fsck.k9.mailstore.UnavailableStorageException;
import com.fsck.k9.notification.NotificationController;
@ -116,6 +118,7 @@ public class MessagingController {
private final BackendManager backendManager;
private final Preferences preferences;
private final MessageStoreManager messageStoreManager;
private final SaveMessageDataCreator saveMessageDataCreator;
private final Thread controllerThread;
@ -140,7 +143,7 @@ public class MessagingController {
NotificationStrategy notificationStrategy, LocalStoreProvider localStoreProvider,
UnreadMessageCountProvider unreadMessageCountProvider, BackendManager backendManager,
Preferences preferences, MessageStoreManager messageStoreManager,
List<ControllerExtension> controllerExtensions) {
SaveMessageDataCreator saveMessageDataCreator, List<ControllerExtension> controllerExtensions) {
this.context = context;
this.notificationController = notificationController;
this.notificationStrategy = notificationStrategy;
@ -149,6 +152,7 @@ public class MessagingController {
this.backendManager = backendManager;
this.preferences = preferences;
this.messageStoreManager = messageStoreManager;
this.saveMessageDataCreator = saveMessageDataCreator;
controllerThread = new Thread(new Runnable() {
@Override
@ -162,7 +166,7 @@ public class MessagingController {
initializeControllerExtensions(controllerExtensions);
draftOperations = new DraftOperations(this);
draftOperations = new DraftOperations(this, messageStoreManager, saveMessageDataCreator);
}
private void initializeControllerExtensions(List<ControllerExtension> controllerExtensions) {
@ -1360,39 +1364,29 @@ public class MessagingController {
}
/**
* Stores the given message in the Outbox and starts a sendPendingMessages command to
* attempt to send the message.
* Stores the given message in the Outbox and starts a sendPendingMessages command to attempt to send the message.
*/
public void sendMessage(final Account account,
final Message message,
String plaintextSubject,
MessagingListener listener) {
public void sendMessage(Account account, Message message, String plaintextSubject, MessagingListener listener) {
try {
LocalStore localStore = localStoreProvider.getInstance(account);
LocalFolder localFolder = localStore.getFolder(account.getOutboxFolderId());
localFolder.open();
message.setFlag(Flag.SEEN, true);
localFolder.appendMessages(Collections.singletonList(message));
LocalMessage localMessage = localFolder.getMessage(message.getUid());
long messageId = localMessage.getDatabaseId();
localMessage.setFlag(Flag.X_DOWNLOADED_FULL, true);
if (plaintextSubject != null) {
localMessage.setCachedDecryptedSubject(plaintextSubject);
Long outboxFolderId = account.getOutboxFolderId();
if (outboxFolderId == null) {
Timber.e("Error sending message. No Outbox folder configured.");
return;
}
message.setFlag(Flag.SEEN, true);
MessageStore messageStore = messageStoreManager.getMessageStore(account);
SaveMessageData messageData = saveMessageDataCreator.createSaveMessageData(message, false, plaintextSubject);
long messageId = messageStore.saveLocalMessage(outboxFolderId, messageData, null);
LocalStore localStore = localStoreProvider.getInstance(account);
OutboxStateRepository outboxStateRepository = localStore.getOutboxStateRepository();
outboxStateRepository.initializeOutboxState(messageId);
sendPendingMessages(account, listener);
} catch (Exception e) {
/*
for (MessagingListener l : getListeners())
{
// TODO general failed
}
*/
Timber.e(e, "Error sending message");
}
}

View file

@ -795,21 +795,6 @@ public class LocalFolder {
return folder.appendMessages(msgs, true);
}
/**
* The method differs slightly from the contract; If an incoming message already has a uid
* assigned and it matches the uid of an existing message then this message will replace the
* old message. It is implemented as a delete/insert. This functionality is used in saving
* of drafts and re-synchronization of updated server messages.
*
* NOTE that although this method is located in the LocalStore class, it is not guaranteed
* that the messages supplied as parameters are actually {@link LocalMessage} instances (in
* fact, in most cases, they are not). Therefore, if you want to make local changes only to a
* message, retrieve the appropriate local message instance first (if it already exists).
*/
public Map<String, String> appendMessages(List<Message> messages) throws MessagingException {
return appendMessages(messages, false);
}
public void destroyMessages(final List<LocalMessage> messages) {
try {
this.localStore.getDatabase().execute(true, new DbCallback<Void>() {

View file

@ -19,6 +19,14 @@ interface MessageStore {
*/
fun saveRemoteMessage(folderId: Long, messageServerId: String, messageData: SaveMessageData)
/**
* Save a local message in this store.
*
* @param existingMessageId The message with this ID is replaced if not `null`.
* @return The message ID of the saved message.
*/
fun saveLocalMessage(folderId: Long, messageData: SaveMessageData, existingMessageId: Long? = null): Long
/**
* Move a message to another folder.
*

View file

@ -12,16 +12,17 @@ class SaveMessageDataCreator(
private val messageFulltextCreator: MessageFulltextCreator,
private val attachmentCounter: AttachmentCounter
) {
fun createSaveMessageData(message: Message, partialMessage: Boolean): SaveMessageData {
fun createSaveMessageData(message: Message, partialMessage: Boolean, subject: String? = null): SaveMessageData {
val now = System.currentTimeMillis()
val date = message.sentDate?.time ?: now
val internalDate = message.internalDate?.time ?: now
val displaySubject = subject ?: message.subject
val encryptionResult = encryptionExtractor.extractEncryption(message)
return if (encryptionResult != null) {
SaveMessageData(
message = message,
subject = message.subject,
subject = displaySubject,
date = date,
internalDate = internalDate,
partialMessage = partialMessage,
@ -33,7 +34,7 @@ class SaveMessageDataCreator(
} else {
SaveMessageData(
message = message,
subject = message.subject,
subject = displaySubject,
date = date,
internalDate = internalDate,
partialMessage = partialMessage,

View file

@ -26,6 +26,7 @@ import com.fsck.k9.mailstore.LocalStoreProvider;
import com.fsck.k9.mailstore.MessageStoreManager;
import com.fsck.k9.mailstore.OutboxState;
import com.fsck.k9.mailstore.OutboxStateRepository;
import com.fsck.k9.mailstore.SaveMessageDataCreator;
import com.fsck.k9.mailstore.SendState;
import com.fsck.k9.mailstore.UnavailableStorageException;
import com.fsck.k9.notification.NotificationController;
@ -78,6 +79,8 @@ public class MessagingControllerTest extends K9RobolectricTest {
@Mock
private MessageStoreManager messageStoreManager;
@Mock
private SaveMessageDataCreator saveMessageDataCreator;
@Mock
private SimpleMessagingListener listener;
@Mock
private LocalSearch search;
@ -133,7 +136,7 @@ public class MessagingControllerTest extends K9RobolectricTest {
controller = new MessagingController(appContext, notificationController, notificationStrategy,
localStoreProvider, unreadMessageCountProvider, backendManager, preferences, messageStoreManager,
Collections.<ControllerExtension>emptyList());
saveMessageDataCreator, Collections.<ControllerExtension>emptyList());
configureAccount();
configureBackendManager();

View file

@ -48,6 +48,10 @@ class K9MessageStore(
localStore.notifyChange()
}
override fun saveLocalMessage(folderId: Long, messageData: SaveMessageData, existingMessageId: Long?): Long {
return saveMessageOperations.saveLocalMessage(folderId, messageData, existingMessageId)
}
override fun moveMessage(messageId: Long, destinationFolderId: Long): Long {
return moveMessageOperations.moveMessage(messageId, destinationFolderId).also {
localStore.notifyChange()

View file

@ -2,6 +2,7 @@ package com.fsck.k9.storage.messages
import android.content.ContentValues
import android.database.sqlite.SQLiteDatabase
import com.fsck.k9.K9
import com.fsck.k9.mail.Address
import com.fsck.k9.mail.Body
import com.fsck.k9.mail.BoundaryGenerator
@ -28,6 +29,7 @@ import java.io.IOException
import java.io.InputStream
import java.util.Locale
import java.util.Stack
import java.util.UUID
import org.apache.commons.io.IOUtils
import org.apache.james.mime4j.codec.Base64InputStream
import org.apache.james.mime4j.codec.QuotedPrintableInputStream
@ -42,11 +44,45 @@ internal class SaveMessageOperations(
private val threadMessageOperations: ThreadMessageOperations
) {
fun saveRemoteMessage(folderId: Long, messageServerId: String, messageData: SaveMessageData) {
lockableDatabase.execute(true) { database ->
saveMessage(folderId, messageServerId, messageData)
}
fun saveLocalMessage(folderId: Long, messageData: SaveMessageData, existingMessageId: Long?): Long {
return if (existingMessageId == null) {
saveLocalMessage(folderId, messageData)
} else {
replaceLocalMessage(folderId, existingMessageId, messageData)
}
}
private fun saveLocalMessage(folderId: Long, messageData: SaveMessageData): Long {
val fakeServerId = K9.LOCAL_UID_PREFIX + UUID.randomUUID().toString()
return saveMessage(folderId, fakeServerId, messageData)
}
private fun replaceLocalMessage(folderId: Long, messageId: Long, messageData: SaveMessageData): Long {
return lockableDatabase.execute(true) { database ->
val (messageServerId, rootMessagePartId) = getLocalMessageInfo(folderId, messageId)
replaceMessage(
database,
folderId,
messageServerId,
existingMessageId = messageId,
existingRootMessagePartId = rootMessagePartId,
messageData
)
messageId
}
}
private fun saveMessage(folderId: Long, messageServerId: String, messageData: SaveMessageData): Long {
return lockableDatabase.execute(true) { database ->
val message = messageData.message
val existingMessageInfo = getMessage(folderId, messageServerId)
if (existingMessageInfo != null) {
return@execute if (existingMessageInfo != null) {
val (existingMessageId, existingRootMessagePartId) = existingMessageInfo
replaceMessage(
database,
@ -56,29 +92,42 @@ internal class SaveMessageOperations(
existingRootMessagePartId,
messageData
)
return@execute
existingMessageId
} else {
insertMessage(database, folderId, messageServerId, message, messageData)
}
val threadInfo = threadMessageOperations.doMessageThreading(database, folderId, message.toThreadHeaders())
val rootMessagePartId = saveMessageParts(database, message)
val messageId = saveMessage(
database,
folderId,
messageServerId,
rootMessagePartId,
messageData,
replaceMessageId = threadInfo.messageId
)
if (threadInfo.threadId == null) {
threadMessageOperations.createThreadEntry(database, messageId, threadInfo.rootId, threadInfo.parentId)
}
createOrReplaceFulltextEntry(database, messageId, messageData)
}
}
private fun insertMessage(
database: SQLiteDatabase,
folderId: Long,
messageServerId: String,
message: Message,
messageData: SaveMessageData
): Long {
val threadInfo = threadMessageOperations.doMessageThreading(database, folderId, message.toThreadHeaders())
val rootMessagePartId = saveMessageParts(database, message)
val messageId = saveMessage(
database,
folderId,
messageServerId,
rootMessagePartId,
messageData,
replaceMessageId = threadInfo.messageId
)
if (threadInfo.threadId == null) {
threadMessageOperations.createThreadEntry(database, messageId, threadInfo.rootId, threadInfo.parentId)
}
createOrReplaceFulltextEntry(database, messageId, messageData)
return messageId
}
private fun replaceMessage(
database: SQLiteDatabase,
folderId: Long,
@ -413,6 +462,26 @@ internal class SaveMessageOperations(
}
}
private fun getLocalMessageInfo(folderId: Long, messageId: Long): Pair<String, Long?> {
return lockableDatabase.execute(false) { db ->
db.query(
"messages",
arrayOf("uid", "message_part_id"),
"folder_id = ? AND id = ?",
arrayOf(folderId.toString(), messageId.toString()),
null,
null,
null
).use { cursor ->
if (!cursor.moveToFirst()) error("Local message not found $folderId:$messageId")
val messageServerId = cursor.getString(0)!!
val messagePartId = cursor.getLong(1)
messageServerId to messagePartId
}
}
}
private fun deleteMessagePartsAndDataFromDisk(database: SQLiteDatabase, rootMessagePartId: Long) {
deleteMessageDataFromDisk(database, rootMessagePartId)
deleteMessageParts(database, rootMessagePartId)

View file

@ -1,5 +1,6 @@
package com.fsck.k9.storage.messages
import com.fsck.k9.K9
import com.fsck.k9.mail.Address
import com.fsck.k9.mail.Flag
import com.fsck.k9.mail.Message
@ -358,6 +359,143 @@ class SaveMessageOperationsTest : RobolectricTest() {
assertThat(thread.messageId).isEqualTo(message.id)
}
@Test
fun `save local message`() {
val messageData = buildMessage {
textBody("local")
}.toSaveMessageData(
subject = "Provided subject",
date = 1618191720000L,
internalDate = 1618191720000L,
previewResult = PreviewResult.text("Preview")
)
val newMessageId = saveMessageOperations.saveLocalMessage(folderId = 1, messageData, existingMessageId = null)
val messages = sqliteDatabase.readMessages()
assertThat(messages).hasSize(1)
val message = messages.first()
with(message) {
assertThat(id).isEqualTo(newMessageId)
assertThat(deleted).isEqualTo(0)
assertThat(folderId).isEqualTo(1)
assertThat(uid).startsWith(K9.LOCAL_UID_PREFIX)
assertThat(subject).isEqualTo("Provided subject")
assertThat(date).isEqualTo(1618191720000L)
assertThat(internalDate).isEqualTo(1618191720000L)
assertThat(flags).isEqualTo("X_DOWNLOADED_FULL")
assertThat(senderList).isEqualTo("")
assertThat(toList).isEqualTo("")
assertThat(ccList).isEqualTo("")
assertThat(bccList).isEqualTo("")
assertThat(replyToList).isEqualTo("")
assertThat(attachmentCount).isEqualTo(0)
assertThat(messageId).isNull()
assertThat(previewType).isEqualTo("text")
assertThat(preview).isEqualTo("Preview")
assertThat(mimeType).isEqualTo("text/plain")
assertThat(empty).isEqualTo(0)
assertThat(read).isEqualTo(0)
assertThat(flagged).isEqualTo(0)
assertThat(answered).isEqualTo(0)
assertThat(forwarded).isEqualTo(0)
assertThat(encryptionType).isNull()
}
val messageParts = sqliteDatabase.readMessageParts()
assertThat(messageParts).hasSize(1)
val messagePart = messageParts.first()
with(messagePart) {
assertThat(type).isEqualTo(MessagePartType.UNKNOWN)
assertThat(root).isEqualTo(messagePart.id)
assertThat(parent).isEqualTo(-1)
assertThat(mimeType).isEqualTo("text/plain")
assertThat(displayName).isEqualTo("noname.txt")
assertThat(header).isNotNull()
assertThat(encoding).isEqualTo("quoted-printable")
assertThat(charset).isNull()
assertThat(dataLocation).isEqualTo(DataLocation.IN_DATABASE)
assertThat(decodedBodySize).isEqualTo(5)
assertThat(data?.toString(Charsets.UTF_8)).isEqualTo("local")
assertThat(preamble).isNull()
assertThat(epilogue).isNull()
assertThat(boundary).isNull()
assertThat(contentId).isNull()
assertThat(serverExtra).isNull()
}
val threads = sqliteDatabase.readThreads()
assertThat(threads).hasSize(1)
val thread = threads.first()
assertThat(thread.root).isEqualTo(thread.id)
assertThat(thread.parent).isNull()
assertThat(thread.messageId).isEqualTo(message.id)
}
@Test
fun `replace local message`() {
val existingMessageData = buildMessage {
multipart("alternative") {
bodyPart("text/plain") {
textBody("plain")
}
bodyPart("text/html") {
textBody("html")
}
}
}.toSaveMessageData()
val existingMessageId = saveMessageOperations.saveLocalMessage(
folderId = 1,
existingMessageData,
existingMessageId = null
)
val messageData = buildMessage {
textBody("new")
}.toSaveMessageData()
val newMessageId = saveMessageOperations.saveLocalMessage(folderId = 1, messageData, existingMessageId)
val messages = sqliteDatabase.readMessages()
assertThat(messages).hasSize(1)
assertThat(messages.first().id).isEqualTo(newMessageId)
val messageParts = sqliteDatabase.readMessageParts()
assertThat(messageParts).hasSize(1)
val messagePart = messageParts.first()
with(messagePart) {
assertThat(type).isEqualTo(MessagePartType.UNKNOWN)
assertThat(root).isEqualTo(messagePart.id)
assertThat(parent).isEqualTo(-1)
assertThat(mimeType).isEqualTo("text/plain")
assertThat(displayName).isEqualTo("noname.txt")
assertThat(header).isNotNull()
assertThat(encoding).isEqualTo("quoted-printable")
assertThat(charset).isNull()
assertThat(dataLocation).isEqualTo(DataLocation.IN_DATABASE)
assertThat(decodedBodySize).isEqualTo(3)
assertThat(data?.toString(Charsets.UTF_8)).isEqualTo("new")
assertThat(preamble).isNull()
assertThat(epilogue).isNull()
assertThat(boundary).isNull()
assertThat(contentId).isNull()
assertThat(serverExtra).isNull()
}
val threads = sqliteDatabase.readThreads()
assertThat(threads).hasSize(1)
val thread = threads.first()
val message = messages.first()
assertThat(thread.root).isEqualTo(thread.id)
assertThat(thread.parent).isNull()
assertThat(thread.messageId).isEqualTo(message.id)
}
private fun Message.toSaveMessageData(
subject: String? = getSubject(),
date: Long = sentDate?.time ?: System.currentTimeMillis(),