diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/base/BaseGmailApiTest.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/base/BaseGmailApiTest.kt index 7134b7e76d..ca1a6c69a1 100644 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/base/BaseGmailApiTest.kt +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/base/BaseGmailApiTest.kt @@ -11,6 +11,7 @@ import com.flowcrypt.email.TestConstants import com.flowcrypt.email.api.email.EmailUtil import com.flowcrypt.email.api.email.FlowCryptMimeMessage import com.flowcrypt.email.api.email.JavaEmailConstants +import com.flowcrypt.email.api.email.gmail.GmailApiHelper import com.flowcrypt.email.api.email.model.LocalFolder import com.flowcrypt.email.api.retrofit.ApiHelper import com.flowcrypt.email.api.retrofit.response.api.EkmPrivateKeysResponse @@ -1093,6 +1094,14 @@ abstract class BaseGmailApiTest(val accountEntity: AccountEntity = BASE_ACCOUNT_ subject = "Re: $SUBJECT_MIXED_MESSAGES", isFullFormat = true ), + genStandardMessage( + threadId = THREAD_ID_MIXED_MESSAGES, + messageId = MESSAGE_ID_THREAD_MIXED_MESSAGES_3, + subject = "Re: $SUBJECT_MIXED_MESSAGES", + labels = listOf(GmailApiHelper.LABEL_TRASH), + includeAttachments = false, + isFullFormat = true + ), ) }.toString() @@ -1653,6 +1662,7 @@ abstract class BaseGmailApiTest(val accountEntity: AccountEntity = BASE_ACCOUNT_ const val THREAD_ID_MIXED_MESSAGES = "200000e222d6c010" const val MESSAGE_ID_THREAD_MIXED_MESSAGES_1 = "5555555559910001" const val MESSAGE_ID_THREAD_MIXED_MESSAGES_2 = "5555555559910002" + const val MESSAGE_ID_THREAD_MIXED_MESSAGES_3 = "5555555559910003" const val SUBJECT_NO_ATTACHMENTS = "No attachments" const val SUBJECT_SINGLE_STANDARD = "Single standard message" diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/gmailapi/ThreadDetailsGmailApiFlowTest.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/gmailapi/ThreadDetailsGmailApiFlowTest.kt index f40a5ca919..3693a58a20 100644 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/gmailapi/ThreadDetailsGmailApiFlowTest.kt +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/gmailapi/ThreadDetailsGmailApiFlowTest.kt @@ -227,6 +227,10 @@ class ThreadDetailsGmailApiFlowTest : BaseThreadDetailsGmailApiFlowTest() { ) } + /** + * This conversation contains 1 standard + 1 encrypted + 1 deleted messages + * The app should show only 2 messages when we open INBOX(deleted message should be skipped) + */ @Test fun testThreadDetailsWithMixedMessages() { openThreadBasedOnPosition(2) diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/gmailapi/ThreadsListGmailApiFlowTest.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/gmailapi/ThreadsListGmailApiFlowTest.kt index 3d1d87e834..fc316d5abd 100644 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/gmailapi/ThreadsListGmailApiFlowTest.kt +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/gmailapi/ThreadsListGmailApiFlowTest.kt @@ -104,6 +104,17 @@ class ThreadsListGmailApiFlowTest : BaseGmailApiTest( hasPgp = true ) + /* + test thread with 1 standard + 1 encrypted + 1 deleted messages + the app should show only 2 messages when we open INBOX(deleted message should be skipped) + */ + checkThreadRowDetails( + subject = SUBJECT_MIXED_MESSAGES, + senderPattern = "From (2)", + hasAttachments = true, + hasPgp = true + ) + //test thread with 2 standard messages without attachments checkThreadRowDetails( subject = SUBJECT_NO_ATTACHMENTS, diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/GmailApiHelper.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/GmailApiHelper.kt index 833a762b6b..fef94a43ce 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/GmailApiHelper.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/GmailApiHelper.kt @@ -364,6 +364,7 @@ class GmailApiHelper { suspend fun loadGmailThreadInfoInParallel( context: Context, accountEntity: AccountEntity, + localFolder: LocalFolder? = null, threads: List, format: String = RESPONSE_FORMAT_FULL, fields: List? = null, @@ -374,6 +375,7 @@ class GmailApiHelper { loadThreadsInfo( context = context, accountEntity = accountEntity, + localFolder = localFolder, threads = list, format = format, fields = fields @@ -384,6 +386,7 @@ class GmailApiHelper { suspend fun loadThreadsInfo( context: Context, accountEntity: AccountEntity, + localFolder: LocalFolder? = null, threads: Collection, format: String = RESPONSE_FORMAT_FULL, metadataHeaders: List? = null, @@ -418,7 +421,7 @@ class GmailApiHelper { responseHeaders: HttpHeaders? ) { t?.let { thread -> - listResult.add(thread.toThreadInfo(context, accountEntity)) + listResult.add(thread.toThreadInfo(context, accountEntity, localFolder)) } } @@ -436,6 +439,7 @@ class GmailApiHelper { suspend fun loadThreadInfo( context: Context, accountEntity: AccountEntity, + localFolder: LocalFolder, threadId: String, format: String = RESPONSE_FORMAT_FULL, metadataHeaders: List? = null, @@ -449,7 +453,7 @@ class GmailApiHelper { format = format, metadataHeaders = metadataHeaders, fields = fields - )?.toThreadInfo(context, accountEntity) + )?.toThreadInfo(context, accountEntity, localFolder) } suspend fun getThread( diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/GmailHistoryHandler.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/GmailHistoryHandler.kt index bdcdfdc2e7..3199486a35 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/GmailHistoryHandler.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/GmailHistoryHandler.kt @@ -106,6 +106,7 @@ object GmailHistoryHandler { val gmailThreadsListWithBaseInfo = GmailApiHelper.loadGmailThreadInfoInParallel( context = context, accountEntity = accountEntity, + localFolder = localFolder, threads = threadIds.map { Thread().apply { id = it } }, format = GmailApiHelper.RESPONSE_FORMAT_FULL, fields = GmailApiHelper.THREAD_BASE_INFO @@ -150,6 +151,7 @@ object GmailHistoryHandler { val gmailThreadInfoList = GmailApiHelper.loadGmailThreadInfoInParallel( context = context, accountEntity = accountEntity, + localFolder = localFolder, threads = uniqueThreadIdList.map { Thread().apply { id = it } }, format = GmailApiHelper.RESPONSE_FORMAT_FULL ) @@ -170,7 +172,7 @@ object GmailHistoryHandler { label = localFolder.fullName, msgsList = threadsToBeAdded, isNew = isNew, - onlyPgpModeEnabled = accountEntity.showOnlyEncrypted ?: false + onlyPgpModeEnabled = accountEntity.showOnlyEncrypted == true ) { message, messageEntity -> val thread = gmailThreadInfoList.firstOrNull { it.id == message.threadId } ?: return@genMessageEntities messageEntity @@ -205,7 +207,7 @@ object GmailHistoryHandler { label = localFolder.fullName, msgsList = listOf(thread.lastMessage), isNew = isNew, - onlyPgpModeEnabled = accountEntity.showOnlyEncrypted ?: false + onlyPgpModeEnabled = accountEntity.showOnlyEncrypted == true ) { message, messageEntity -> if (message.threadId == thread.id) { messageEntity.toUpdatedThreadCopy(threadMessageEntity, thread) @@ -299,7 +301,7 @@ object GmailHistoryHandler { label = localFolder.fullName, msgsList = messages, isNew = isNew, - onlyPgpModeEnabled = accountEntity.showOnlyEncrypted ?: false, + onlyPgpModeEnabled = accountEntity.showOnlyEncrypted == true, draftIdsMap = draftIdsMap ) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/extensions/com/google/api/services/gmail/model/MessageExt.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/extensions/com/google/api/services/gmail/model/MessageExt.kt index 03d428a9c8..1e50b13a7b 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/extensions/com/google/api/services/gmail/model/MessageExt.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/extensions/com/google/api/services/gmail/model/MessageExt.kt @@ -6,8 +6,10 @@ package com.flowcrypt.email.extensions.com.google.api.services.gmail.model import com.flowcrypt.email.api.email.EmailUtil +import com.flowcrypt.email.api.email.FoldersManager import com.flowcrypt.email.api.email.JavaEmailConstants import com.flowcrypt.email.api.email.gmail.GmailApiHelper +import com.flowcrypt.email.api.email.model.LocalFolder import com.flowcrypt.email.extensions.kotlin.asContentTypeOrNull import com.flowcrypt.email.extensions.kotlin.asInternetAddresses import com.google.api.services.gmail.model.Message @@ -38,7 +40,7 @@ fun Message.hasPgp(): Boolean { && "multipart/encrypted" == baseContentType?.baseType?.lowercase() && baseContentType.getParameter("protocol")?.lowercase() == "application/pgp-encrypted" - val hasEncryptedParts = payload?.parts?.any { it.hasPgp() } ?: false + val hasEncryptedParts = payload?.parts?.any { it.hasPgp() } == true return EmailUtil.hasEncryptedData(snippet) || EmailUtil.hasSignedData(snippet) @@ -72,13 +74,29 @@ fun Message.getMessageId(): String? { } fun Message.isDraft(): Boolean { - return labelIds?.contains(GmailApiHelper.LABEL_DRAFT) ?: false + return labelIds?.contains(GmailApiHelper.LABEL_DRAFT) == true } fun Message.hasAttachments(): Boolean { - return payload?.hasAttachments() ?: false + return payload?.hasAttachments() == true } fun Message.filterHeadersWithName(name: String): List { return payload?.headers?.filter { header -> header.name == name } ?: emptyList() +} + +fun Message.containsLabel(localFolder: LocalFolder?): Boolean? { + return labelIds?.contains(localFolder?.fullName) +} + +fun Message.isTrashed(): Boolean? { + return labelIds.contains(GmailApiHelper.LABEL_TRASH) +} + +fun Message.canBeUsed(localFolder: LocalFolder?): Boolean { + return if (localFolder?.getFolderType() == FoldersManager.FolderType.TRASH) { + isTrashed() == true + } else { + isTrashed()?.not() != false + } } \ No newline at end of file diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/extensions/com/google/api/services/gmail/model/ThreadExt.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/extensions/com/google/api/services/gmail/model/ThreadExt.kt index 378ff0df71..ee053a18bd 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/extensions/com/google/api/services/gmail/model/ThreadExt.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/extensions/com/google/api/services/gmail/model/ThreadExt.kt @@ -10,26 +10,29 @@ import com.flowcrypt.email.R import com.flowcrypt.email.api.email.gmail.GmailApiHelper import com.flowcrypt.email.api.email.gmail.GmailApiHelper.Companion.LABEL_DRAFT import com.flowcrypt.email.api.email.gmail.model.GmailThreadInfo +import com.flowcrypt.email.api.email.model.LocalFolder import com.flowcrypt.email.database.entity.AccountEntity import com.flowcrypt.email.extensions.kotlin.asInternetAddresses +import com.google.api.services.gmail.model.Message import com.google.api.services.gmail.model.Thread import jakarta.mail.internet.InternetAddress /** * @author Denys Bondarenko */ -fun Thread.getUniqueRecipients(account: String): List { +fun Thread.getUniqueRecipients(account: String, localFolder: LocalFolder?): List { return mutableListOf().apply { - if (messages.isNullOrEmpty()) { - return@apply - } - + val filteredMessages = messages?.filter { + it.canBeUsed(localFolder) + }?.takeIf { + it.isNotEmpty() + } ?: return@apply val fromHeaderName = "From" - val filteredHeaders = if (messages.size > 1) { + val filteredHeaders = if (filteredMessages.size > 1) { //if we have more than one message in a conversation, //firstly we will try to filter only active recipients - messages.flatMap { message -> + filteredMessages.flatMap { message -> val listOfAcceptedHeaders = listOf( fromHeaderName, "To", @@ -43,10 +46,10 @@ fun Thread.getUniqueRecipients(account: String): List { } else emptyList() }.ifEmpty { //otherwise we will use all recipients - messages.flatMap { it.filterHeadersWithName(fromHeaderName) } + filteredMessages.flatMap { it.filterHeadersWithName(fromHeaderName) } } } else { - messages.first().filterHeadersWithName(fromHeaderName) + filteredMessages.first().filterHeadersWithName(fromHeaderName) } val mapOfUniqueRecipients = mutableMapOf() @@ -66,68 +69,81 @@ fun Thread.getUniqueRecipients(account: String): List { } } -fun Thread.getUniqueLabelsSet(): Set { - return messages?.flatMap { message -> - message.labelIds ?: emptyList() +fun Thread.getUniqueLabelsSet(localFolder: LocalFolder?): Set { + return messages?.filter { + it.canBeUsed(localFolder) + }?.flatMap { + it.labelIds ?: emptyList() }?.toSortedSet() ?: emptySet() } -fun Thread.getDraftsCount(): Int { - return messages?.filter { it.labelIds.contains(LABEL_DRAFT) }?.size ?: 0 +fun Thread.getDraftsCount(localFolder: LocalFolder?): Int { + return messages?.filter { + it.canBeUsed(localFolder) && it.labelIds.contains(LABEL_DRAFT) + }?.size ?: 0 } -fun Thread.hasUnreadMessages(): Boolean { - return messages?.any { message -> - message.labelIds?.contains(GmailApiHelper.LABEL_UNREAD) == true - } ?: false +fun Thread.hasUnreadMessages(localFolder: LocalFolder?): Boolean { + return messages?.filter { + it.canBeUsed(localFolder) + }?.any { + it.labelIds?.contains(GmailApiHelper.LABEL_UNREAD) == true + } == true } -fun Thread.hasAttachments(): Boolean { - return messages?.any { message -> - message.hasAttachments() - } ?: false +fun Thread.hasAttachments(localFolder: LocalFolder?): Boolean { + return messages?.filter { it.canBeUsed(localFolder) }?.any { it.hasAttachments() } == true } -fun Thread.hasPgp(): Boolean { - return messages?.any { message -> - message.hasPgp() - } ?: false +fun Thread.hasPgp(localFolder: LocalFolder?): Boolean { + return messages?.filter { it.canBeUsed(localFolder) }?.any { it.hasPgp() } == true } -fun Thread.extractSubject(context: Context, receiverEmail: String): String { - return messages?.getOrNull(0)?.takeIf { message -> +fun Thread.extractSubject( + context: Context, + receiverEmail: String, + localFolder: LocalFolder? +): String { + val filteredMessages = messages?.filter { it.canBeUsed(localFolder) } + + return filteredMessages?.getOrNull(0)?.takeIf { message -> (message.getRecipients("From").any { internetAddress -> internetAddress.address.equals(receiverEmail, true) - } || (messages?.size ?: 0) == 1) && !message.isDraft() + } || (filteredMessages.size) == 1) && !message.isDraft() }?.getSubject() - ?: messages.firstOrNull { message -> + ?: filteredMessages?.firstOrNull { message -> message.getRecipients("From").any { internetAddress -> internetAddress.address.equals(receiverEmail, true) } && !message.isDraft() }?.getSubject() - ?: messages?.getOrNull(0)?.getSubject() + ?: filteredMessages?.getOrNull(0)?.getSubject() ?: context.getString(R.string.no_subject) } +fun Thread.filteredMessages(localFolder: LocalFolder?): List { + return messages?.filter { it.canBeUsed(localFolder) } ?: emptyList() +} + fun Thread.toThreadInfo( context: Context, - accountEntity: AccountEntity + accountEntity: AccountEntity, + localFolder: LocalFolder? = null ): GmailThreadInfo { val receiverEmail = accountEntity.email + val lastMessage = messages?.lastOrNull { + !it.labelIds.contains(LABEL_DRAFT) && it.canBeUsed(localFolder) + } ?: messages?.first() val gmailThreadInfo = GmailThreadInfo( id = id, - lastMessage = requireNotNull( - messages?.lastOrNull { - !it.labelIds.contains(LABEL_DRAFT) - } ?: messages.first()), - messagesCount = messages?.size ?: 0, - draftsCount = getDraftsCount(), - recipients = getUniqueRecipients(receiverEmail), - subject = extractSubject(context, receiverEmail), - labels = getUniqueLabelsSet(), - hasAttachments = hasAttachments(), - hasPgpThings = hasPgp(), - hasUnreadMessages = hasUnreadMessages() + lastMessage = requireNotNull(lastMessage), + messagesCount = messages?.filter { it.canBeUsed(localFolder) }?.size ?: 0, + draftsCount = getDraftsCount(localFolder), + recipients = getUniqueRecipients(receiverEmail, localFolder), + subject = extractSubject(context, receiverEmail, localFolder), + labels = getUniqueLabelsSet(localFolder), + hasAttachments = hasAttachments(localFolder), + hasPgpThings = hasPgp(localFolder), + hasUnreadMessages = hasUnreadMessages(localFolder) ) return gmailThreadInfo } \ No newline at end of file diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MessagesViewModel.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MessagesViewModel.kt index 5053007c6e..68250c8628 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MessagesViewModel.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MessagesViewModel.kt @@ -64,7 +64,6 @@ import java.io.File import java.io.IOException import java.math.BigInteger import java.net.HttpURLConnection -import java.util.Arrays import java.util.Properties @@ -487,6 +486,7 @@ class MessagesViewModel(application: Application) : AccountViewModel(application gmailThreadInfoList = GmailApiHelper.loadGmailThreadInfoInParallel( context = getApplication(), accountEntity = accountEntity, + localFolder = localFolder, threads = threadsResponse.threads ?: emptyList(), format = GmailApiHelper.RESPONSE_FORMAT_FULL ) @@ -603,7 +603,7 @@ class MessagesViewModel(application: Application) : AccountViewModel(application val email = account.email val folder = localFolder.fullName - val isOnlyPgpModeEnabled = account.showOnlyEncrypted ?: false + val isOnlyPgpModeEnabled = account.showOnlyEncrypted == true val msgEntities = MessageEntity.genMessageEntities( context = getApplication(), account = email, @@ -635,7 +635,7 @@ class MessagesViewModel(application: Application) : AccountViewModel(application msgs: Array = emptyArray(), hasPgpAfterAdditionalSearchSet: Set = emptySet() ) = withContext(Dispatchers.IO) { - val isOnlyPgpModeEnabled = account.showOnlyEncrypted ?: false + val isOnlyPgpModeEnabled = account.showOnlyEncrypted == true val msgEntities = MessageEntity.genMessageEntities( context = getApplication(), account = account.email, @@ -721,6 +721,7 @@ class MessagesViewModel(application: Application) : AccountViewModel(application gmailThreadInfoList = GmailApiHelper.loadGmailThreadInfoInParallel( context = getApplication(), accountEntity = accountEntity, + localFolder = localFolder, threads = threadsResponse.threads ?: emptyList(), format = GmailApiHelper.RESPONSE_FORMAT_FULL ) @@ -844,7 +845,7 @@ class MessagesViewModel(application: Application) : AccountViewModel(application if (end < 1) { handleSearchResults(account = accountEntity, remoteFolder = imapFolder) } else { - val bufferedMsgs = Arrays.copyOfRange(foundMsgs, start - 1, end) + val bufferedMsgs = foundMsgs.copyOfRange(start - 1, end) //fetch base details EmailUtil.fetchMsgs(imapFolder, bufferedMsgs) @@ -882,7 +883,7 @@ class MessagesViewModel(application: Application) : AccountViewModel(application folder = remoteFolder, msgs = msgs, isNew = false, - isOnlyPgpModeEnabled = account.showOnlyEncrypted ?: false, + isOnlyPgpModeEnabled = account.showOnlyEncrypted == true, hasPgpAfterAdditionalSearchSet = hasPgpAfterAdditionalSearchSet ) @@ -903,7 +904,7 @@ class MessagesViewModel(application: Application) : AccountViewModel(application messageEntity: MessageEntity ) -> MessageEntity ) = withContext(Dispatchers.IO) { - val isOnlyPgpModeEnabled = account.showOnlyEncrypted ?: false + val isOnlyPgpModeEnabled = account.showOnlyEncrypted == true val msgEntities = MessageEntity.genMessageEntities( context = getApplication(), account = account.email, @@ -1071,7 +1072,7 @@ class MessagesViewModel(application: Application) : AccountViewModel(application val newCandidates = EmailUtil.genNewCandidates(msgsUIDs, remoteFolder, newMsgs) - val isOnlyPgpModeEnabled = accountEntity.showOnlyEncrypted ?: false + val isOnlyPgpModeEnabled = accountEntity.showOnlyEncrypted == true val isNew = !context.isAppForegrounded() && folderType === FoldersManager.FolderType.INBOX val msgEntities = MessageEntity.genMessageEntities( diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/ProcessMessageViewModel.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/ProcessMessageViewModel.kt index 757171958c..4bb6378290 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/ProcessMessageViewModel.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/ProcessMessageViewModel.kt @@ -24,6 +24,8 @@ import com.flowcrypt.email.api.retrofit.response.model.PublicKeyMsgBlock import com.flowcrypt.email.database.MessageState import com.flowcrypt.email.database.entity.MessageEntity import com.flowcrypt.email.extensions.com.flowcrypt.email.util.processing +import com.flowcrypt.email.extensions.com.google.api.services.gmail.model.containsLabel +import com.flowcrypt.email.extensions.com.google.api.services.gmail.model.isDraft import com.flowcrypt.email.extensions.java.lang.printStackTraceIfDebugOnly import com.flowcrypt.email.jetpack.workmanager.sync.UpdateMsgsSeenStateWorker import com.flowcrypt.email.model.MessageEncryptionType @@ -31,6 +33,7 @@ import com.flowcrypt.email.security.pgp.PgpKey import com.flowcrypt.email.security.pgp.PgpMsg import com.flowcrypt.email.ui.adapter.MessagesInThreadListAdapter import com.flowcrypt.email.util.cache.DiskLruCache +import com.flowcrypt.email.util.exception.MessageNotFoundException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow @@ -48,6 +51,7 @@ import kotlinx.coroutines.withContext */ class ProcessMessageViewModel( private val message: MessagesInThreadListAdapter.Message, + private val localFolder: LocalFolder, private val skipAttachmentsRawData: Boolean, application: Application ) : AccountViewModel(application) { @@ -285,6 +289,11 @@ class ProcessMessageViewModel( fields = null, format = GmailApiHelper.RESPONSE_FORMAT_FULL ) + + if (!msgFullInfo.isDraft() && msgFullInfo.containsLabel(localFolder) == false) { + throw MessageNotFoundException("Message doesn't contain label = ${localFolder.fullName}") + } + val originalMsg = GmaiAPIMimeMessage( message = msgFullInfo, context = getApplication(), diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/ThreadDetailsViewModel.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/ThreadDetailsViewModel.kt index 1a48c635b7..ef6c67eaa2 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/ThreadDetailsViewModel.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/ThreadDetailsViewModel.kt @@ -23,6 +23,7 @@ import com.flowcrypt.email.database.entity.MessageEntity import com.flowcrypt.email.database.entity.RecipientEntity import com.flowcrypt.email.database.entity.relation.RecipientWithPubKeys import com.flowcrypt.email.extensions.com.flowcrypt.email.util.processing +import com.flowcrypt.email.extensions.com.google.api.services.gmail.model.filteredMessages import com.flowcrypt.email.extensions.com.google.api.services.gmail.model.getInReplyTo import com.flowcrypt.email.extensions.com.google.api.services.gmail.model.getMessageId import com.flowcrypt.email.extensions.com.google.api.services.gmail.model.isDraft @@ -89,19 +90,6 @@ class ThreadDetailsViewModel( val allOutboxMessagesFlow = roomDatabase.msgDao().getAllOutboxMessagesFlow() - @OptIn(ExperimentalCoroutinesApi::class) - val localFolderFlow: StateFlow = - threadMessageEntityFlow.mapLatest { threadMessageEntity -> - val activeAccount = getActiveAccountSuspend() - ?: return@mapLatest null - val foldersManager = FoldersManager.fromDatabaseSuspend(getApplication(), activeAccount) - return@mapLatest foldersManager.getFolderByFullName(threadMessageEntity?.folder) - }.stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5000), - initialValue = null - ) - @OptIn(ExperimentalCoroutinesApi::class) private val threadHeaderFlow = threadMessageEntityFlow.mapLatest { prepareThreadHeader(it) @@ -299,7 +287,7 @@ class ThreadDetailsViewModel( } fun getMessageActionAvailability(messageAction: MessageAction): Boolean { - return messageActionsAvailabilityStateFlow.value[messageAction] ?: false + return messageActionsAvailabilityStateFlow.value[messageAction] == true } fun changeMsgState(newMsgState: MessageState) { @@ -400,7 +388,7 @@ class ThreadDetailsViewModel( format = GmailApiHelper.RESPONSE_FORMAT_FULL ) ?: error("Thread not found") - val threadInfo = thread.toThreadInfo(getApplication(), activeAccount) + val threadInfo = thread.toThreadInfo(getApplication(), activeAccount, localFolder) if (!threadInfo.labels.contains(localFolder.fullName)) { val context: Context = getApplication() @@ -416,14 +404,14 @@ class ThreadDetailsViewModel( label = getFolderFullName(), msgsList = listOf(threadInfo.lastMessage), isNew = false, - onlyPgpModeEnabled = activeAccount.showOnlyEncrypted ?: false + onlyPgpModeEnabled = activeAccount.showOnlyEncrypted == true ) { message, messageEntity -> if (message.threadId == threadMessageEntity.threadIdAsHEX) { messageEntity.toUpdatedThreadCopy(threadMessageEntity, threadInfo) } else messageEntity }) - val messagesInThread = (thread.messages ?: emptyList()).toMutableList().apply { + val messagesInThread = thread.filteredMessages(localFolder).toMutableList().apply { //try to put drafts in the right position val drafts = filter { it.isDraft() } drafts.forEach { draft -> @@ -452,7 +440,7 @@ class ThreadDetailsViewModel( ).associateBy({ it.message.id }, { it.id }) } else emptyMap() - val isOnlyPgpModeEnabled = activeAccount.showOnlyEncrypted ?: false + val isOnlyPgpModeEnabled = activeAccount.showOnlyEncrypted == true val messageEntitiesBasedOnServerResult = MessageEntity.genMessageEntities( context = getApplication(), account = activeAccount.email, @@ -585,6 +573,7 @@ class ThreadDetailsViewModel( val latestLabelIds = (threadInfo ?: GmailApiHelper.loadThreadInfo( context = getApplication(), accountEntity = account, + localFolder = localFolder, threadId = messageEntity?.threadIdAsHEX ?: "", fields = listOf("id", "messages/labelIds"), format = GmailApiHelper.RESPONSE_FORMAT_MINIMAL diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/security/pgp/PgpMsg.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/security/pgp/PgpMsg.kt index 60343563b3..1986be71a9 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/security/pgp/PgpMsg.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/security/pgp/PgpMsg.kt @@ -355,10 +355,7 @@ object PgpMsg { part.isMultipart() -> { val multiPart = part.content as Multipart if (part.isMultipartAlternative()) { - val parts = mutableListOf() - for (partCount in 0 until multiPart.count) { - parts.add(multiPart.getBodyPart(partCount)) - } + val parts = (0 until multiPart.count).map { multiPart.getBodyPart(it) } val partWithPlainText = parts.firstOrNull { it.isPlainText() } if (partWithPlainText != null) { @@ -960,48 +957,50 @@ object PgpMsg { var hasInvalidSignatures = false val keyIdsOfSigningKeys = mutableSetOf() - if (block.type in MsgBlock.Type.SIGNED_BLOCK_TYPES) { - val messageMetadata = when (block) { - is DecryptedAndOrSignedContentMsgBlock -> { - block.messageMetadata - } + if (block.type !in MsgBlock.Type.SIGNED_BLOCK_TYPES) { + return + } - is SignedMsgBlock -> { - block.openPgpMetadata - } + val messageMetadata = when (block) { + is DecryptedAndOrSignedContentMsgBlock -> { + block.messageMetadata + } - else -> null + is SignedMsgBlock -> { + block.openPgpMetadata } - hasEncryptedContent = messageMetadata?.isEncrypted == true + else -> null + } - if (messageMetadata?.isSigned == true) { - hasSignedContent = true + hasEncryptedContent = messageMetadata?.isEncrypted == true - if (messageMetadata.rejectedInlineSignatures.isNotEmpty() - || messageMetadata.rejectedDetachedSignatures.isNotEmpty() - ) { - val invalidSignatureFailures = messageMetadata.rejectedInlineSignatures + - messageMetadata.rejectedDetachedSignatures + if (messageMetadata?.isSigned == true) { + hasSignedContent = true - hasInvalidSignatures = invalidSignatureFailures.any { - it.validationException.underlyingException != null - } + if (messageMetadata.rejectedInlineSignatures.isNotEmpty() + || messageMetadata.rejectedDetachedSignatures.isNotEmpty() + ) { + val invalidSignatureFailures = messageMetadata.rejectedInlineSignatures + + messageMetadata.rejectedDetachedSignatures - keyIdsOfSigningKeys.addAll(invalidSignatureFailures.filter { - it.validationException.message?.matches("Missing verification key.?".toRegex()) == true - }.map { it.signature.keyID }) + hasInvalidSignatures = invalidSignatureFailures.any { + it.validationException.underlyingException != null } - } - action.invoke( - hasEncryptedContent, - hasSignedContent, - hasInvalidSignatures, - keyIdsOfSigningKeys, - messageMetadata?.verifiedSignatures ?: emptyList() - ) + keyIdsOfSigningKeys.addAll(invalidSignatureFailures.filter { + it.validationException.message?.matches("Missing verification key.?".toRegex()) == true + }.map { it.signature.keyID }) + } } + + action.invoke( + hasEncryptedContent, + hasSignedContent, + hasInvalidSignatures, + keyIdsOfSigningKeys, + messageMetadata?.verifiedSignatures ?: emptyList() + ) } private fun extractInnerBlocks( diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/ThreadDetailsFragment.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/ThreadDetailsFragment.kt index 47cd28c4ee..674954e964 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/ThreadDetailsFragment.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/ThreadDetailsFragment.kt @@ -32,7 +32,6 @@ import com.flowcrypt.email.Constants import com.flowcrypt.email.R import com.flowcrypt.email.api.email.FoldersManager import com.flowcrypt.email.api.email.model.AttachmentInfo -import com.flowcrypt.email.api.email.model.LocalFolder import com.flowcrypt.email.api.email.model.ServiceInfo import com.flowcrypt.email.api.retrofit.response.base.Result import com.flowcrypt.email.database.MessageState @@ -121,8 +120,6 @@ class ThreadDetailsFragment : BaseFragment(), Prog get() = threadDetailsViewModel.messagesInThreadFlow.value.status == Result.Status.SUCCESS private val threadMessageEntity: MessageEntity? get() = threadDetailsViewModel.threadMessageEntityFlow.value - private val localFolder: LocalFolder? - get() = threadDetailsViewModel.localFolderFlow.value private val args by navArgs() private var isActive: Boolean = false @@ -340,7 +337,7 @@ class ThreadDetailsFragment : BaseFragment(), Prog R.id.menuActionDeleteMessage -> { val messageEntity = threadMessageEntity ?: return true if (!messageEntity.isOutboxMsg) { - if (localFolder?.getFolderType() == FoldersManager.FolderType.TRASH) { + if (args.localFolder.getFolderType() == FoldersManager.FolderType.TRASH) { showTwoWayDialog( requestKey = REQUEST_KEY_TWO_WAY_DIALOG_BASE + args.messageEntityId.toString(), requestCode = REQUEST_CODE_DELETE_MESSAGE_DIALOG, @@ -513,12 +510,6 @@ class ThreadDetailsFragment : BaseFragment(), Prog } } - launchAndRepeatWithViewLifecycle { - threadDetailsViewModel.localFolderFlow.collect { - //do nothing. Just subscribe for updates to have the latest value async - } - } - launchAndRepeatWithViewLifecycle { threadDetailsViewModel.allOutboxMessagesFlow.collect { messageEntities -> val threadId = @@ -645,7 +636,7 @@ class ThreadDetailsFragment : BaseFragment(), Prog val requestCode = bundle.getInt(ProcessMessageDialogFragment.KEY_REQUEST_CODE) if (message != null) { val hasUnverifiedSignatures = - message.incomingMessageInfo?.verificationResult?.hasUnverifiedSignatures ?: false + message.incomingMessageInfo?.verificationResult?.hasUnverifiedSignatures == true val hasActiveSignatureVerification = if (hasUnverifiedSignatures) { val messageFromAddresses = message.incomingMessageInfo?.getFrom()?.map { it.address.lowercase() @@ -1286,6 +1277,7 @@ class ThreadDetailsFragment : BaseFragment(), Prog requestKey = REQUEST_KEY_PROCESS_MESSAGE + args.messageEntityId.toString(), requestCode = requestCode, message = message, + localFolder = args.localFolder, attachmentId = attachmentId ).toBundle() } diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/dialog/ProcessMessageDialogFragment.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/dialog/ProcessMessageDialogFragment.kt index fd0892b4e8..f20fbb6859 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/dialog/ProcessMessageDialogFragment.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/dialog/ProcessMessageDialogFragment.kt @@ -27,6 +27,8 @@ import com.flowcrypt.email.extensions.visible import com.flowcrypt.email.jetpack.lifecycle.CustomAndroidViewModelFactory import com.flowcrypt.email.jetpack.viewmodel.ProcessMessageViewModel import com.flowcrypt.email.ui.activity.fragment.base.ProgressBehaviour +import com.flowcrypt.email.util.exception.GmailAPIException +import com.flowcrypt.email.util.exception.MessageNotFoundException /** * @author Denys Bondarenko @@ -40,6 +42,7 @@ class ProcessMessageDialogFragment : BaseDialogFragment(), ProgressBehaviour { override fun create(modelClass: Class): T { return ProcessMessageViewModel( message = args.message, + localFolder = args.localFolder, skipAttachmentsRawData = args.attachmentId == null, application = requireActivity().application ) as T @@ -118,8 +121,19 @@ class ProcessMessageDialogFragment : BaseDialogFragment(), ProgressBehaviour { } Result.Status.EXCEPTION -> { - showStatus(msg = it.exceptionMsg) - (dialog as? AlertDialog)?.getButton(AlertDialog.BUTTON_POSITIVE)?.visible() + when { + (it.exception is MessageNotFoundException) + || (it.exception is GmailAPIException && it.exception.code == 404) -> { + (dialog as? AlertDialog)?.getButton(AlertDialog.BUTTON_NEGATIVE)?.text = + getString(android.R.string.ok) + showStatus(msg = getString(R.string.message_not_found_please_reload_the_thread)) + } + + else -> { + showStatus(msg = it.exceptionMsg) + (dialog as? AlertDialog)?.getButton(AlertDialog.BUTTON_POSITIVE)?.visible() + } + } } else -> {} diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/util/exception/MessageNotFoundException.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/util/exception/MessageNotFoundException.kt new file mode 100644 index 0000000000..610242c3b2 --- /dev/null +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/util/exception/MessageNotFoundException.kt @@ -0,0 +1,11 @@ +/* + * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com + * Contributors: denbond7 + */ + +package com.flowcrypt.email.util.exception + +/** + * @author Denys Bondarenko + */ +class MessageNotFoundException(errorMsg: String) : IllegalStateException(errorMsg) \ No newline at end of file diff --git a/FlowCrypt/src/main/res/navigation/process_message_dialog_graph.xml b/FlowCrypt/src/main/res/navigation/process_message_dialog_graph.xml index d6fe258604..392110439d 100644 --- a/FlowCrypt/src/main/res/navigation/process_message_dialog_graph.xml +++ b/FlowCrypt/src/main/res/navigation/process_message_dialog_graph.xml @@ -22,6 +22,9 @@ + \'%1$s\' уже существует в Download. Хотите загрузить его снова? Общиe Отключить интеллектуальный режим для предварительного просмотра вложений + Сообщение не найдено. Похоже, оно было перемещено или удалено. Пожалуйста, перезагрузите переписку, чтобы получить последние обновления. Черновик Черновики(%1$d) diff --git a/FlowCrypt/src/main/res/values-uk/strings.xml b/FlowCrypt/src/main/res/values-uk/strings.xml index d4c9af51fa..753583d72a 100644 --- a/FlowCrypt/src/main/res/values-uk/strings.xml +++ b/FlowCrypt/src/main/res/values-uk/strings.xml @@ -647,6 +647,7 @@ \'%1$s\' уже існує в Download. Бажаєте завантажити його знову? Загальні Вимкнути розумний режим для попереднього перегляду вкладень + Повідомлення не знайдено. Схоже, його перемістили або видалили. Оновіть бесіду, щоб отримати останні оновлення. Чорновик Чорновики(%1$d) diff --git a/FlowCrypt/src/main/res/values/strings.xml b/FlowCrypt/src/main/res/values/strings.xml index 246e7ef2d6..5a0ff93be6 100644 --- a/FlowCrypt/src/main/res/values/strings.xml +++ b/FlowCrypt/src/main/res/values/strings.xml @@ -656,6 +656,7 @@ \'%1$s\' already exists in Download. Would you like to download it again? General Disable smart mode for attachments preview + Message not found. It looks like it was moved or deleted. Please reload the thread to get the latest updates. Draft Drafts(%1$d)