diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/EmailUtil.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/EmailUtil.kt index 79005596bb..2932404c85 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/EmailUtil.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/EmailUtil.kt @@ -452,7 +452,10 @@ class EmailUtil { try { for (msg in msgs) { val flags = map[folder.getUID(msg)] ?: "" - if (!flags.equals(msg.flags.toString(), ignoreCase = true)) { + val existingFlags = flags.split(" ").map { it.uppercase() }.toSet() + val receivedFlags = msg.flags.toString().split(" ").map { it.uppercase() }.toSet() + + if (existingFlags != receivedFlags) { updateCandidates.add(msg) } } @@ -694,7 +697,21 @@ class EmailUtil { } return@withContext when (outgoingMsgInfo.messageType) { - MessageType.NEW, MessageType.DRAFT -> { + MessageType.DRAFT -> { + prepareReplyFromDraft( + context = context, + accountEntity = accountEntity, + session = session, + outgoingMsgInfo = outgoingMsgInfo, + pubKeys = pubKeys, + protectedPubKeys = protectedPubKeys, + prvKeys = prvKeys, + protector = ringProtector, + hideArmorMeta = hideArmorMeta + ) + } + + MessageType.NEW -> { prepareNewMsg( session = session, info = outgoingMsgInfo, @@ -1033,6 +1050,42 @@ class EmailUtil { return@withContext msg } + suspend fun prepareReplyFromDraft( + context: Context, + accountEntity: AccountEntity, + session: Session, + outgoingMsgInfo: OutgoingMessageInfo, + pubKeys: List? = null, + protectedPubKeys: List? = null, + prvKeys: List? = null, + protector: SecretKeyRingProtector? = null, + hideArmorMeta: Boolean = false, + ): MimeMessage = withContext(Dispatchers.IO) { + return@withContext prepareNewMsg( + session = session, + info = outgoingMsgInfo, + pubKeys = pubKeys, + protectedPubKeys = protectedPubKeys, + prvKeys = prvKeys, + protector = protector, + hideArmorMeta = hideArmorMeta + ).apply { + //we need to restore 'References' and 'In-Reply-To' headers to support correct conversation + val replyToMimeMessage = + getReplyToMimeMessage(context, accountEntity, session, outgoingMsgInfo) + + replyToMimeMessage.getHeader(JavaEmailConstants.HEADER_REFERENCES)?.firstOrNull() + ?.let { references -> + setHeader(JavaEmailConstants.HEADER_REFERENCES, references) + } + + replyToMimeMessage.getHeader(JavaEmailConstants.HEADER_IN_REPLY_TO)?.firstOrNull() + ?.let { inReplyTo -> + setHeader(JavaEmailConstants.HEADER_IN_REPLY_TO, inReplyTo) + } + } + } + private suspend fun prepareForwardedMsg( context: Context, accountEntity: AccountEntity, @@ -1171,7 +1224,7 @@ class EmailUtil { val attBodyPart = genBodyPartWithAtt( context = context, att = att, - shouldBeEncrypted = msgEntity.isEncrypted ?: false, + shouldBeEncrypted = msgEntity.isEncrypted == true, publicKeys = publicKeys, secretKeys = secretKeys, ringProtector = ringProtector @@ -1294,9 +1347,16 @@ class EmailUtil { protector: SecretKeyRingProtector? = null, hideArmorMeta: Boolean = false, ): BodyPart { + val finalText = (info.msg + info.quotedTextForReply).takeIf { + info.messageType in arrayOf( + MessageType.REPLY, + MessageType.REPLY_ALL + ) && !info.quotedTextForReply.isNullOrEmpty() + } ?: info.msg ?: "" + return if (info.encryptionType == MessageEncryptionType.ENCRYPTED) { val encryptedContent = PgpEncryptAndOrSign.encryptAndOrSignMsg( - msg = info.msg ?: "", + msg = finalText, pubKeys = pubKeys ?: emptyList(), protectedPubKeys = protectedPubKeys, prvKeys = prvKeys, @@ -1319,7 +1379,7 @@ class EmailUtil { } } else { MimeBodyPart().apply { - setText(info.msg ?: "") + setText(finalText) } } } diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/model/LocalFolder.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/model/LocalFolder.kt index 9f2b173852..377f85115e 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/model/LocalFolder.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/model/LocalFolder.kt @@ -1,6 +1,6 @@ /* * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com - * Contributors: DenBond7 + * Contributors: denbond7 */ package com.flowcrypt.email.api.email.model @@ -18,7 +18,7 @@ import kotlinx.parcelize.Parcelize * @author Denys Bondarenko */ @Parcelize -data class LocalFolder constructor( +data class LocalFolder( val account: String, val fullName: String, var folderAlias: String? = null, @@ -52,6 +52,9 @@ data class LocalFolder constructor( @IgnoredOnParcel val isDrafts: Boolean = FoldersManager.FolderType.DRAFTS == getFolderType() + @IgnoredOnParcel + val isTrash: Boolean = FoldersManager.FolderType.TRASH == getFolderType() + fun getFolderType(): FoldersManager.FolderType? { return FoldersManager.getFolderType(this) } diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/MessageEntity.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/MessageEntity.kt index fd4443caff..2b08ccb1f9 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/MessageEntity.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/MessageEntity.kt @@ -148,11 +148,11 @@ data class MessageEntity( @IgnoredOnParcel @Ignore - val isSeen: Boolean = flags?.contains(MessageFlag.SEEN.value) ?: false + val isSeen: Boolean = flags?.contains(MessageFlag.SEEN.value) == true @IgnoredOnParcel @Ignore - val isDraft: Boolean = flags?.contains(MessageFlag.DRAFT.value) ?: false + val isDraft: Boolean = flags?.contains(MessageFlag.DRAFT.value) == true @IgnoredOnParcel @Ignore @@ -168,12 +168,16 @@ data class MessageEntity( @IgnoredOnParcel @Ignore - val isPasswordProtected = password?.isNotEmpty() ?: false + val isPasswordProtected = password?.isNotEmpty() == true @IgnoredOnParcel @Ignore val isGmailThread: Boolean = threadMessagesCount != null && threadMessagesCount > 0 + @IgnoredOnParcel + @Ignore + val flagsValueSet = flags?.split(" ")?.filter { it.isNotEmpty() }?.map { it.uppercase() }?.toSet() + /** * Generate a list of the all recipients. * @@ -473,17 +477,21 @@ data class MessageEntity( fromAddresses = InternetAddress.toString(thread.recipients.toTypedArray()), hasPgp = thread.hasPgpThings, flags = if (thread.hasUnreadMessages) { - messageEntity.flags?.replace(MessageFlag.SEEN.value, "") + messageEntity.flagsStringAfterRemoveSome(MessageFlag.SEEN.value) } else { if (messageEntity.flags?.contains(MessageFlag.SEEN.value) == true) { messageEntity.flags } else { - messageEntity.flags.plus("${MessageFlag.SEEN.value} ") + messageEntity.flags.plus(" ${MessageFlag.SEEN.value}") } } ) } + fun flagsStringAfterRemoveSome(flagValue: String): String? { + return flags?.replace("(\\s)?$flagValue(\\s)?".toRegex(), "") + } + private fun prepareDateHeaderValue(context: Context): String { val dateInMilliseconds: Long = if (JavaEmailConstants.FOLDER_OUTBOX.equals(folder, ignoreCase = true)) { 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 1e50b13a7b..0982c12ddd 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 @@ -93,6 +93,10 @@ fun Message.isTrashed(): Boolean? { return labelIds.contains(GmailApiHelper.LABEL_TRASH) } +fun Message.isSent(): Boolean { + return labelIds.contains(GmailApiHelper.LABEL_SENT) == true +} + fun Message.canBeUsed(localFolder: LocalFolder?): Boolean { return if (localFolder?.getFolderType() == FoldersManager.FolderType.TRASH) { isTrashed() == true diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/DraftViewModel.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/DraftViewModel.kt index 0fc5d28cbc..018e797d14 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/DraftViewModel.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/DraftViewModel.kt @@ -205,11 +205,10 @@ class DraftViewModel( accountEntity = activeAccount, outgoingMsgInfo = outgoingMessageInfo, signingRequired = false, - hideArmorMeta = activeAccount.clientConfiguration?.shouldHideArmorMeta() ?: false + hideArmorMeta = activeAccount.clientConfiguration?.shouldHideArmorMeta() == true ) - val existingSnapshot = MsgsCacheManager.getMsgSnapshot(draftMessageEntity.id.toString()) - if (existingSnapshot != null) { - existingSnapshot.getUri(0)?.let { fileUri -> + MsgsCacheManager.getMsgSnapshot(draftMessageEntity.id.toString())?.getUri(0) + ?.let { fileUri -> (getApplication() as Context).contentResolver?.openInputStream(fileUri) ?.let { inputStream -> val keys = PGPainless.readKeyRing() @@ -239,7 +238,6 @@ class DraftViewModel( mimeMessage.setHeader(JavaEmailConstants.HEADER_IN_REPLY_TO, inReplyTo) } } - } } val draftsDir = CacheManager.getDraftDirectory(getApplication()) @@ -344,6 +342,6 @@ class DraftViewModel( ) companion object { - val DELAY_TIMEOUT = TimeUnit.SECONDS.toMillis(5) + val DELAY_TIMEOUT = TimeUnit.SECONDS.toMillis(30) } } 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 68250c8628..a8102d99f1 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 @@ -357,7 +357,7 @@ class MessagesViewModel(application: Application) : AccountViewModel(application MessageState.PENDING_MARK_UNREAD -> { msgEntity.copy( state = newMsgState.value, - flags = msgEntity.flags?.replace(MessageFlag.SEEN.value, "") + flags = msgEntity.flagsStringAfterRemoveSome(MessageFlag.SEEN.value) ) } diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MsgDetailsViewModel.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MsgDetailsViewModel.kt index 2881e44a88..67f5a6f153 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MsgDetailsViewModel.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MsgDetailsViewModel.kt @@ -466,7 +466,7 @@ class MsgDetailsViewModel( MessageState.PENDING_MARK_UNREAD -> { msgEntity.copy( state = newMsgState.value, - flags = msgEntity.flags?.replace(MessageFlag.SEEN.value, "") + flags = msgEntity.flagsStringAfterRemoveSome(MessageFlag.SEEN.value) ) } @@ -531,7 +531,7 @@ class MsgDetailsViewModel( } fun getMessageActionAvailability(messageAction: MessageAction): Boolean { - return messageActionsAvailabilityStateFlow.value[messageAction] ?: false + return messageActionsAvailabilityStateFlow.value[messageAction] == true } private suspend fun reVerifySignaturesInternal(): Result = @@ -839,10 +839,10 @@ class MsgDetailsViewModel( if (msgEntity.flags?.contains(MessageFlag.SEEN.value) == true) { msgEntity.flags } else { - msgEntity.flags?.plus("${MessageFlag.SEEN.value} ") + msgEntity.flags?.plus(" ${MessageFlag.SEEN.value}") } } else { - msgEntity.flags?.replace(MessageFlag.SEEN.value, "") + msgEntity.flagsStringAfterRemoveSome(MessageFlag.SEEN.value) } ) ) 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 4bb6378290..607b225d91 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,8 +24,7 @@ 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.com.google.api.services.gmail.model.isTrashed import com.flowcrypt.email.extensions.java.lang.printStackTraceIfDebugOnly import com.flowcrypt.email.jetpack.workmanager.sync.UpdateMsgsSeenStateWorker import com.flowcrypt.email.model.MessageEncryptionType @@ -290,8 +289,8 @@ class ProcessMessageViewModel( format = GmailApiHelper.RESPONSE_FORMAT_FULL ) - if (!msgFullInfo.isDraft() && msgFullInfo.containsLabel(localFolder) == false) { - throw MessageNotFoundException("Message doesn't contain label = ${localFolder.fullName}") + if (!localFolder.isTrash && msgFullInfo.isTrashed() == true) { + throw MessageNotFoundException("Message was moved to trash") } val originalMsg = GmaiAPIMimeMessage( @@ -375,10 +374,10 @@ class ProcessMessageViewModel( if (msgEntity.flags?.contains(MessageFlag.SEEN.value) == true) { msgEntity.flags } else { - msgEntity.flags?.plus("${MessageFlag.SEEN.value} ") + msgEntity.flags?.plus(" ${MessageFlag.SEEN.value}") } } else { - msgEntity.flags?.replace(MessageFlag.SEEN.value, "") + msgEntity.flagsStringAfterRemoveSome(MessageFlag.SEEN.value) } ) ) 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 ef6c67eaa2..05f5b5b80c 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 @@ -309,7 +309,7 @@ class ThreadDetailsViewModel( MessageState.PENDING_MARK_UNREAD -> { messageEntity.copy( state = newMsgState.value, - flags = messageEntity.flags?.replace(MessageFlag.SEEN.value, "") + flags = messageEntity.flagsStringAfterRemoveSome(MessageFlag.SEEN.value) ) } @@ -479,7 +479,7 @@ class ThreadDetailsViewModel( candidatesToBeInserted.add(entity) } else if (existingVersion.copy(id = null) != entity) { candidatesToBeUpdated.add(entity.copy(id = existingVersion.id)) - if (existingVersion.flags != entity.flags) { + if (existingVersion.flagsValueSet != entity.flagsValueSet) { MsgsCacheManager.removeMessage(existingVersion.id.toString()) } } @@ -527,7 +527,7 @@ class ThreadDetailsViewModel( it.id == messageItemBasedOnDataFromServer.id }?.let { item -> val existingMessageItem = item as Message - if (existingMessageItem.messageEntity.flags != messageEntity.flags) { + if (existingMessageItem.messageEntity.flagsValueSet != messageEntity.flagsValueSet) { messageItemBasedOnDataFromServer } else { existingMessageItem.copy(messageEntity = messageEntity) @@ -649,10 +649,10 @@ class ThreadDetailsViewModel( if (messageEntity.flags?.contains(MessageFlag.SEEN.value) == true) { messageEntity.flags } else { - messageEntity.flags?.plus("${MessageFlag.SEEN.value} ") + messageEntity.flags?.plus(" ${MessageFlag.SEEN.value}") } } else { - messageEntity.flags?.replace(MessageFlag.SEEN.value, "") + messageEntity.flagsStringAfterRemoveSome(MessageFlag.SEEN.value) } ) ) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/sync/BaseIdleWorker.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/sync/BaseIdleWorker.kt index 585ecdc98d..132f319a96 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/sync/BaseIdleWorker.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/sync/BaseIdleWorker.kt @@ -60,7 +60,7 @@ abstract class BaseIdleWorker(context: Context, params: WorkerParameters) : accountEntity: AccountEntity, folderFullName: String ) = withContext(Dispatchers.IO) { val updateCandidates = EmailUtil.genUpdateCandidates(mapOfUIDAndMsgFlags, remoteFolder, msgs) - .map { remoteFolder.getUID(it) to it.flags }.toMap() + .associate { remoteFolder.getUID(it) to it.flags } processUpdatedMsgs(accountEntity, folderFullName, updateCandidates) } diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/CreateMessageFragment.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/CreateMessageFragment.kt index ac735ef8fa..0a3c02376f 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/CreateMessageFragment.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/CreateMessageFragment.kt @@ -318,11 +318,7 @@ class CreateMessageFragment : BaseFragment(), composeMsgViewModel.updateOutgoingMessageInfo( composeMsgViewModel.outgoingMessageInfoStateFlow.value.copy( messageType = args.messageType, - replyToMessageEntityId = if (args.incomingMessageInfo?.msgEntity?.isDraft == true) { - null - } else { - args.incomingMessageInfo?.msgEntity?.id - }, + replyToMessageEntityId = args.incomingMessageInfo?.msgEntity?.id, quotedTextForReply = EmailUtil.genReplyContent(args.incomingMessageInfo).takeIf { args.messageType in arrayOf( MessageType.REPLY, @@ -1076,19 +1072,7 @@ class CreateMessageFragment : BaseFragment(), CreateMessageFragmentDirections .actionCreateMessageFragmentToCreateOutgoingMessageDialogFragment( requestKey = REQUEST_KEY_CREATE_OUTGOING_MESSAGE, - outgoingMessageInfo = outgoingMessageInfo.copy( - msg = if (outgoingMessageInfo.quotedTextForReply?.isNotEmpty() == true && - args.messageType in arrayOf( - MessageType.REPLY, - MessageType.REPLY_ALL - ) - ) { - outgoingMessageInfo.msg + EmailUtil.genReplyContent(args.incomingMessageInfo) - } else { - outgoingMessageInfo.msg - }, - password = usePasswordIfNeeded(), - ) + outgoingMessageInfo = outgoingMessageInfo.copy(password = usePasswordIfNeeded()) ) ) }