Merge pull request #5259 from k9mail/save_messages
This commit is contained in:
commit
c43f27a9ff
10 changed files with 316 additions and 99 deletions
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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");
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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>() {
|
||||||
|
|
|
@ -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.
|
||||||
*
|
*
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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(),
|
||||||
|
|
Loading…
Reference in a new issue