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.PendingAppend
import com.fsck.k9.controller.MessagingControllerCommands.PendingReplace import com.fsck.k9.controller.MessagingControllerCommands.PendingReplace
import com.fsck.k9.mail.FetchProfile import com.fsck.k9.mail.FetchProfile
import com.fsck.k9.mail.Flag
import com.fsck.k9.mail.Message import com.fsck.k9.mail.Message
import com.fsck.k9.mail.MessagingException import com.fsck.k9.mail.MessagingException
import com.fsck.k9.mailstore.LocalFolder import com.fsck.k9.mailstore.LocalFolder
import com.fsck.k9.mailstore.LocalMessage 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 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( fun saveDraft(
account: Account, account: Account,
@ -24,22 +31,13 @@ internal class DraftOperations(private val messagingController: MessagingControl
return try { return try {
val draftsFolderId = account.draftsFolderId ?: error("No Drafts folder configured") val draftsFolderId = account.draftsFolderId ?: error("No Drafts folder configured")
val localStore = messagingController.getLocalStoreOrThrow(account) val messageId = if (messagingController.supportsUpload(account)) {
val localFolder = localStore.getFolder(draftsFolderId) saveAndUploadDraft(account, message, draftsFolderId, existingDraftId, plaintextSubject)
localFolder.open()
val localMessage = if (messagingController.supportsUpload(account)) {
saveAndUploadDraft(account, message, localFolder, existingDraftId)
} else { } else {
saveDraftLocally(message, localFolder, existingDraftId) saveDraftLocally(account, message, draftsFolderId, existingDraftId, plaintextSubject)
} }
localMessage.setFlag(Flag.X_DOWNLOADED_FULL, true) messageId
if (plaintextSubject != null) {
localMessage.setCachedDecryptedSubject(plaintextSubject)
}
localMessage.databaseId
} catch (e: MessagingException) { } catch (e: MessagingException) {
Timber.e(e, "Unable to save message as draft.") Timber.e(e, "Unable to save message as draft.")
null null
@ -49,41 +47,52 @@ internal class DraftOperations(private val messagingController: MessagingControl
private fun saveAndUploadDraft( private fun saveAndUploadDraft(
account: Account, account: Account,
message: Message, message: Message,
localFolder: LocalFolder, folderId: Long,
existingDraftId: Long? existingDraftId: Long?,
): LocalMessage { subject: String?
localFolder.appendMessages(listOf(message)) ): Long {
val messageStore = messageStoreManager.getMessageStore(account)
val localMessage = localFolder.getMessage(message.uid) val messageId = messageStore.saveLocalMessage(folderId, message.toSaveMessageData(subject))
val previousDraftMessage = if (existingDraftId != null) localFolder.getMessage(existingDraftId) else null
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) { if (previousDraftMessage != null) {
previousDraftMessage.delete() previousDraftMessage.delete()
val uploadMessageId = localMessage.databaseId
val deleteMessageId = previousDraftMessage.databaseId val deleteMessageId = previousDraftMessage.databaseId
val command = PendingReplace.create(folderId, uploadMessageId, deleteMessageId) val command = PendingReplace.create(folderId, messageId, deleteMessageId)
messagingController.queuePendingCommand(account, command) messagingController.queuePendingCommand(account, command)
} else { } else {
val command = PendingAppend.create(folderId, localMessage.uid) val fakeMessageServerId = messageStore.getMessageServerId(messageId)
val command = PendingAppend.create(folderId, fakeMessageServerId)
messagingController.queuePendingCommand(account, command) messagingController.queuePendingCommand(account, command)
} }
messagingController.processPendingCommands(account) messagingController.processPendingCommands(account)
return localMessage return messageId
} }
private fun saveDraftLocally(message: Message, localFolder: LocalFolder, existingDraftId: Long?): LocalMessage { private fun saveDraftLocally(
if (existingDraftId != null) { account: Account,
// Setting the UID will cause LocalFolder.appendMessages() to replace the existing draft. message: Message,
message.uid = localFolder.getMessageUidById(existingDraftId) folderId: Long,
} existingDraftId: Long?,
plaintextSubject: String?
): Long {
val messageStore = messageStoreManager.getMessageStore(account)
val messageData = message.toSaveMessageData(plaintextSubject)
localFolder.appendMessages(listOf(message)) return messageStore.saveLocalMessage(folderId, messageData, existingDraftId)
return localFolder.getMessage(message.uid)
} }
fun processPendingReplace(command: PendingReplace, account: Account) { fun processPendingReplace(command: PendingReplace, account: Account) {
@ -153,4 +162,8 @@ internal class DraftOperations(private val messagingController: MessagingControl
messagingController.destroyPlaceholderMessages(localFolder, messageServerIds) 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.backend.BackendManager
import com.fsck.k9.mailstore.LocalStoreProvider import com.fsck.k9.mailstore.LocalStoreProvider
import com.fsck.k9.mailstore.MessageStoreManager import com.fsck.k9.mailstore.MessageStoreManager
import com.fsck.k9.mailstore.SaveMessageDataCreator
import com.fsck.k9.notification.NotificationController import com.fsck.k9.notification.NotificationController
import com.fsck.k9.notification.NotificationStrategy import com.fsck.k9.notification.NotificationStrategy
import org.koin.core.qualifier.named import org.koin.core.qualifier.named
@ -21,6 +22,7 @@ val controllerModule = module {
get<BackendManager>(), get<BackendManager>(),
get<Preferences>(), get<Preferences>(),
get<MessageStoreManager>(), get<MessageStoreManager>(),
get<SaveMessageDataCreator>(),
get(named("controllerExtensions")) 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.MessageStoreManager;
import com.fsck.k9.mailstore.OutboxState; import com.fsck.k9.mailstore.OutboxState;
import com.fsck.k9.mailstore.OutboxStateRepository; 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.SendState;
import com.fsck.k9.mailstore.UnavailableStorageException; import com.fsck.k9.mailstore.UnavailableStorageException;
import com.fsck.k9.notification.NotificationController; import com.fsck.k9.notification.NotificationController;
@ -116,6 +118,7 @@ public class MessagingController {
private final BackendManager backendManager; private final BackendManager backendManager;
private final Preferences preferences; private final Preferences preferences;
private final MessageStoreManager messageStoreManager; private final MessageStoreManager messageStoreManager;
private final SaveMessageDataCreator saveMessageDataCreator;
private final Thread controllerThread; private final Thread controllerThread;
@ -140,7 +143,7 @@ public class MessagingController {
NotificationStrategy notificationStrategy, LocalStoreProvider localStoreProvider, NotificationStrategy notificationStrategy, LocalStoreProvider localStoreProvider,
UnreadMessageCountProvider unreadMessageCountProvider, BackendManager backendManager, UnreadMessageCountProvider unreadMessageCountProvider, BackendManager backendManager,
Preferences preferences, MessageStoreManager messageStoreManager, Preferences preferences, MessageStoreManager messageStoreManager,
List<ControllerExtension> controllerExtensions) { SaveMessageDataCreator saveMessageDataCreator, List<ControllerExtension> controllerExtensions) {
this.context = context; this.context = context;
this.notificationController = notificationController; this.notificationController = notificationController;
this.notificationStrategy = notificationStrategy; this.notificationStrategy = notificationStrategy;
@ -149,6 +152,7 @@ public class MessagingController {
this.backendManager = backendManager; this.backendManager = backendManager;
this.preferences = preferences; this.preferences = preferences;
this.messageStoreManager = messageStoreManager; this.messageStoreManager = messageStoreManager;
this.saveMessageDataCreator = saveMessageDataCreator;
controllerThread = new Thread(new Runnable() { controllerThread = new Thread(new Runnable() {
@Override @Override
@ -162,7 +166,7 @@ public class MessagingController {
initializeControllerExtensions(controllerExtensions); initializeControllerExtensions(controllerExtensions);
draftOperations = new DraftOperations(this); draftOperations = new DraftOperations(this, messageStoreManager, saveMessageDataCreator);
} }
private void initializeControllerExtensions(List<ControllerExtension> controllerExtensions) { 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 * Stores the given message in the Outbox and starts a sendPendingMessages command to attempt to send the message.
* attempt to send the message.
*/ */
public void sendMessage(final Account account, public void sendMessage(Account account, Message message, String plaintextSubject, MessagingListener listener) {
final Message message,
String plaintextSubject,
MessagingListener listener) {
try { try {
LocalStore localStore = localStoreProvider.getInstance(account); Long outboxFolderId = account.getOutboxFolderId();
LocalFolder localFolder = localStore.getFolder(account.getOutboxFolderId()); if (outboxFolderId == null) {
localFolder.open(); Timber.e("Error sending message. No Outbox folder configured.");
message.setFlag(Flag.SEEN, true); return;
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);
} }
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 outboxStateRepository = localStore.getOutboxStateRepository();
outboxStateRepository.initializeOutboxState(messageId); outboxStateRepository.initializeOutboxState(messageId);
sendPendingMessages(account, listener); sendPendingMessages(account, listener);
} catch (Exception e) { } catch (Exception e) {
/*
for (MessagingListener l : getListeners())
{
// TODO general failed
}
*/
Timber.e(e, "Error sending message"); Timber.e(e, "Error sending message");
} }
} }

View file

@ -795,21 +795,6 @@ public class LocalFolder {
return folder.appendMessages(msgs, true); 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) { public void destroyMessages(final List<LocalMessage> messages) {
try { try {
this.localStore.getDatabase().execute(true, new DbCallback<Void>() { this.localStore.getDatabase().execute(true, new DbCallback<Void>() {

View file

@ -19,6 +19,14 @@ interface MessageStore {
*/ */
fun saveRemoteMessage(folderId: Long, messageServerId: String, messageData: SaveMessageData) 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. * Move a message to another folder.
* *

View file

@ -12,16 +12,17 @@ class SaveMessageDataCreator(
private val messageFulltextCreator: MessageFulltextCreator, private val messageFulltextCreator: MessageFulltextCreator,
private val attachmentCounter: AttachmentCounter 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 now = System.currentTimeMillis()
val date = message.sentDate?.time ?: now val date = message.sentDate?.time ?: now
val internalDate = message.internalDate?.time ?: now val internalDate = message.internalDate?.time ?: now
val displaySubject = subject ?: message.subject
val encryptionResult = encryptionExtractor.extractEncryption(message) val encryptionResult = encryptionExtractor.extractEncryption(message)
return if (encryptionResult != null) { return if (encryptionResult != null) {
SaveMessageData( SaveMessageData(
message = message, message = message,
subject = message.subject, subject = displaySubject,
date = date, date = date,
internalDate = internalDate, internalDate = internalDate,
partialMessage = partialMessage, partialMessage = partialMessage,
@ -33,7 +34,7 @@ class SaveMessageDataCreator(
} else { } else {
SaveMessageData( SaveMessageData(
message = message, message = message,
subject = message.subject, subject = displaySubject,
date = date, date = date,
internalDate = internalDate, internalDate = internalDate,
partialMessage = partialMessage, 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.MessageStoreManager;
import com.fsck.k9.mailstore.OutboxState; import com.fsck.k9.mailstore.OutboxState;
import com.fsck.k9.mailstore.OutboxStateRepository; import com.fsck.k9.mailstore.OutboxStateRepository;
import com.fsck.k9.mailstore.SaveMessageDataCreator;
import com.fsck.k9.mailstore.SendState; import com.fsck.k9.mailstore.SendState;
import com.fsck.k9.mailstore.UnavailableStorageException; import com.fsck.k9.mailstore.UnavailableStorageException;
import com.fsck.k9.notification.NotificationController; import com.fsck.k9.notification.NotificationController;
@ -78,6 +79,8 @@ public class MessagingControllerTest extends K9RobolectricTest {
@Mock @Mock
private MessageStoreManager messageStoreManager; private MessageStoreManager messageStoreManager;
@Mock @Mock
private SaveMessageDataCreator saveMessageDataCreator;
@Mock
private SimpleMessagingListener listener; private SimpleMessagingListener listener;
@Mock @Mock
private LocalSearch search; private LocalSearch search;
@ -133,7 +136,7 @@ public class MessagingControllerTest extends K9RobolectricTest {
controller = new MessagingController(appContext, notificationController, notificationStrategy, controller = new MessagingController(appContext, notificationController, notificationStrategy,
localStoreProvider, unreadMessageCountProvider, backendManager, preferences, messageStoreManager, localStoreProvider, unreadMessageCountProvider, backendManager, preferences, messageStoreManager,
Collections.<ControllerExtension>emptyList()); saveMessageDataCreator, Collections.<ControllerExtension>emptyList());
configureAccount(); configureAccount();
configureBackendManager(); configureBackendManager();

View file

@ -48,6 +48,10 @@ class K9MessageStore(
localStore.notifyChange() 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 { override fun moveMessage(messageId: Long, destinationFolderId: Long): Long {
return moveMessageOperations.moveMessage(messageId, destinationFolderId).also { return moveMessageOperations.moveMessage(messageId, destinationFolderId).also {
localStore.notifyChange() localStore.notifyChange()

View file

@ -2,6 +2,7 @@ package com.fsck.k9.storage.messages
import android.content.ContentValues import android.content.ContentValues
import android.database.sqlite.SQLiteDatabase import android.database.sqlite.SQLiteDatabase
import com.fsck.k9.K9
import com.fsck.k9.mail.Address import com.fsck.k9.mail.Address
import com.fsck.k9.mail.Body import com.fsck.k9.mail.Body
import com.fsck.k9.mail.BoundaryGenerator import com.fsck.k9.mail.BoundaryGenerator
@ -28,6 +29,7 @@ import java.io.IOException
import java.io.InputStream import java.io.InputStream
import java.util.Locale import java.util.Locale
import java.util.Stack import java.util.Stack
import java.util.UUID
import org.apache.commons.io.IOUtils import org.apache.commons.io.IOUtils
import org.apache.james.mime4j.codec.Base64InputStream import org.apache.james.mime4j.codec.Base64InputStream
import org.apache.james.mime4j.codec.QuotedPrintableInputStream import org.apache.james.mime4j.codec.QuotedPrintableInputStream
@ -42,11 +44,45 @@ internal class SaveMessageOperations(
private val threadMessageOperations: ThreadMessageOperations private val threadMessageOperations: ThreadMessageOperations
) { ) {
fun saveRemoteMessage(folderId: Long, messageServerId: String, messageData: SaveMessageData) { 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 message = messageData.message
val existingMessageInfo = getMessage(folderId, messageServerId) val existingMessageInfo = getMessage(folderId, messageServerId)
if (existingMessageInfo != null) { return@execute if (existingMessageInfo != null) {
val (existingMessageId, existingRootMessagePartId) = existingMessageInfo val (existingMessageId, existingRootMessagePartId) = existingMessageInfo
replaceMessage( replaceMessage(
database, database,
@ -56,29 +92,42 @@ internal class SaveMessageOperations(
existingRootMessagePartId, existingRootMessagePartId,
messageData 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( private fun replaceMessage(
database: SQLiteDatabase, database: SQLiteDatabase,
folderId: Long, 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) { private fun deleteMessagePartsAndDataFromDisk(database: SQLiteDatabase, rootMessagePartId: Long) {
deleteMessageDataFromDisk(database, rootMessagePartId) deleteMessageDataFromDisk(database, rootMessagePartId)
deleteMessageParts(database, rootMessagePartId) deleteMessageParts(database, rootMessagePartId)

View file

@ -1,5 +1,6 @@
package com.fsck.k9.storage.messages package com.fsck.k9.storage.messages
import com.fsck.k9.K9
import com.fsck.k9.mail.Address import com.fsck.k9.mail.Address
import com.fsck.k9.mail.Flag import com.fsck.k9.mail.Flag
import com.fsck.k9.mail.Message import com.fsck.k9.mail.Message
@ -358,6 +359,143 @@ class SaveMessageOperationsTest : RobolectricTest() {
assertThat(thread.messageId).isEqualTo(message.id) 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( private fun Message.toSaveMessageData(
subject: String? = getSubject(), subject: String? = getSubject(),
date: Long = sentDate?.time ?: System.currentTimeMillis(), date: Long = sentDate?.time ?: System.currentTimeMillis(),