Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,7 @@ class GmailApiHelper {
suspend fun loadGmailThreadInfoInParallel(
context: Context,
accountEntity: AccountEntity,
localFolder: LocalFolder? = null,
threads: List<Thread>,
format: String = RESPONSE_FORMAT_FULL,
fields: List<String>? = null,
Expand All @@ -374,6 +375,7 @@ class GmailApiHelper {
loadThreadsInfo(
context = context,
accountEntity = accountEntity,
localFolder = localFolder,
threads = list,
format = format,
fields = fields
Expand All @@ -384,6 +386,7 @@ class GmailApiHelper {
suspend fun loadThreadsInfo(
context: Context,
accountEntity: AccountEntity,
localFolder: LocalFolder? = null,
threads: Collection<Thread>,
format: String = RESPONSE_FORMAT_FULL,
metadataHeaders: List<String>? = null,
Expand Down Expand Up @@ -418,7 +421,7 @@ class GmailApiHelper {
responseHeaders: HttpHeaders?
) {
t?.let { thread ->
listResult.add(thread.toThreadInfo(context, accountEntity))
listResult.add(thread.toThreadInfo(context, accountEntity, localFolder))
}
}

Expand All @@ -436,6 +439,7 @@ class GmailApiHelper {
suspend fun loadThreadInfo(
context: Context,
accountEntity: AccountEntity,
localFolder: LocalFolder,
threadId: String,
format: String = RESPONSE_FORMAT_FULL,
metadataHeaders: List<String>? = null,
Expand All @@ -449,7 +453,7 @@ class GmailApiHelper {
format = format,
metadataHeaders = metadataHeaders,
fields = fields
)?.toThreadInfo(context, accountEntity)
)?.toThreadInfo(context, accountEntity, localFolder)
}

suspend fun getThread(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
)
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -299,7 +301,7 @@ object GmailHistoryHandler {
label = localFolder.fullName,
msgsList = messages,
isNew = isNew,
onlyPgpModeEnabled = accountEntity.showOnlyEncrypted ?: false,
onlyPgpModeEnabled = accountEntity.showOnlyEncrypted == true,
draftIdsMap = draftIdsMap
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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<MessagePartHeader> {
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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<InternetAddress> {
fun Thread.getUniqueRecipients(account: String, localFolder: LocalFolder?): List<InternetAddress> {
return mutableListOf<InternetAddress>().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",
Expand All @@ -43,10 +46,10 @@ fun Thread.getUniqueRecipients(account: String): List<InternetAddress> {
} 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<String, InternetAddress>()
Expand All @@ -66,68 +69,81 @@ fun Thread.getUniqueRecipients(account: String): List<InternetAddress> {
}
}

fun Thread.getUniqueLabelsSet(): Set<String> {
return messages?.flatMap { message ->
message.labelIds ?: emptyList()
fun Thread.getUniqueLabelsSet(localFolder: LocalFolder?): Set<String> {
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<Message> {
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
}
Loading