From aba92c698599d55c2b308c91abd0cafce8e5bc78 Mon Sep 17 00:00:00 2001 From: denbond7 Date: Thu, 24 Apr 2025 09:56:06 +0300 Subject: [PATCH 1/2] Improved loading and parsing PGPMime Encrypted messages.| #3017 --- .../flowcrypt/email/api/email/EmailUtil.kt | 2 +- .../email/api/email/model/AttachmentInfo.kt | 2 +- .../viewmodel/ProcessMessageViewModel.kt | 29 ++++++++++++++++--- .../security/pgp/ProcessMimeMessageTest.kt | 4 +-- ....eml => protonmail_pgp_mime_encrypted.eml} | 0 5 files changed, 29 insertions(+), 8 deletions(-) rename FlowCrypt/src/test/resources/mime/{protonmail_pgp_mime.eml => protonmail_pgp_mime_encrypted.eml} (100%) 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 2932404c85..853cbc122c 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 @@ -983,8 +983,8 @@ class EmailUtil { //match signature item.isMimeType("application/pgp-signature") || //match PGP/MIME version identification + //https://datatracker.ietf.org/doc/html/rfc3156#section-4 item.isMimeType("application/pgp-encrypted") - && item.description.equals("PGP/MIME version identification", false) ) -> true isAttachment -> false diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/model/AttachmentInfo.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/model/AttachmentInfo.kt index 0fde65ebe0..30c791c3f6 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/model/AttachmentInfo.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/model/AttachmentInfo.kt @@ -121,7 +121,7 @@ data class AttachmentInfo( fun isHidden() = when { //https://github.com/FlowCrypt/flowcrypt-android/issues/1475 - name.isNullOrEmpty() && type.lowercase() == "application/pgp-encrypted; name=\"\"" -> true + name.isNullOrEmpty() && "application/pgp-encrypted" == type.asContentTypeOrNull()?.baseType -> true //https://github.com/FlowCrypt/flowcrypt-android/issues/2540 "application/pgp-signature" == type.asContentTypeOrNull()?.baseType -> true 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 607b225d91..69bc565a68 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 @@ -26,6 +26,7 @@ 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.isTrashed import com.flowcrypt.email.extensions.java.lang.printStackTraceIfDebugOnly +import com.flowcrypt.email.extensions.kotlin.asContentTypeOrNull import com.flowcrypt.email.jetpack.workmanager.sync.UpdateMsgsSeenStateWorker import com.flowcrypt.email.model.MessageEncryptionType import com.flowcrypt.email.security.pgp.PgpKey @@ -187,13 +188,33 @@ class ProcessMessageViewModel( ) val activeAccount = getActiveAccountSuspend() ?: error("No active account") - - val attachments = roomDatabase.attachmentDao().getAttachments( + val attachmentsAsMimePart = roomDatabase.attachmentDao().getAttachments( account = activeAccount.email, accountType = activeAccount.accountType, label = messageEntity.folder, uid = messageEntity.uid - ).map { it.toAttInfo() } + ) + + val filteredAttachments = attachmentsAsMimePart.mapNotNull { + when { + processedMimeMessageResult.verificationResult.hasEncryptedParts -> { + when { + //PGP MIME Encrypted. Attachments can be dropped + //https://datatracker.ietf.org/doc/html/rfc3156#section-4 + attachmentsAsMimePart.size == 2 + && "application/pgp-encrypted" == + attachmentsAsMimePart[0].type.asContentTypeOrNull()?.baseType + && "application/octet-stream" == + attachmentsAsMimePart[1].type.asContentTypeOrNull()?.baseType + -> null + + else -> it.toAttInfo() + } + } + + else -> it.toAttInfo() + } + } val inlinedAttachmentInfoList = mutableListOf() @@ -217,7 +238,7 @@ class ProcessMessageViewModel( data = message.copy( messageEntity = messageEntity, incomingMessageInfo = incomingMessageInfo, - attachments = attachments + inlinedAttachmentInfoList + attachments = filteredAttachments + inlinedAttachmentInfoList ) ) } catch (e: Exception) { diff --git a/FlowCrypt/src/test/java/com/flowcrypt/email/security/pgp/ProcessMimeMessageTest.kt b/FlowCrypt/src/test/java/com/flowcrypt/email/security/pgp/ProcessMimeMessageTest.kt index d0c1ba00c3..e64d126361 100644 --- a/FlowCrypt/src/test/java/com/flowcrypt/email/security/pgp/ProcessMimeMessageTest.kt +++ b/FlowCrypt/src/test/java/com/flowcrypt/email/security/pgp/ProcessMimeMessageTest.kt @@ -25,11 +25,11 @@ import java.util.Properties */ class ProcessMimeMessageTest { @Test - fun testProcessProtonmailPgpMime() { + fun testProcessProtonmailPgpMimeEncrypted() { val processedMimeMessageResult = PgpMsg.processMimeMessage( MimeMessage( Session.getInstance(Properties()), - TestUtil.readResourceAsByteArray("mime/protonmail_pgp_mime.eml").inputStream() + TestUtil.readResourceAsByteArray("mime/protonmail_pgp_mime_encrypted.eml").inputStream() ), VERIFICATION_PUBLIC_KEYS, SECRET_KEYS, diff --git a/FlowCrypt/src/test/resources/mime/protonmail_pgp_mime.eml b/FlowCrypt/src/test/resources/mime/protonmail_pgp_mime_encrypted.eml similarity index 100% rename from FlowCrypt/src/test/resources/mime/protonmail_pgp_mime.eml rename to FlowCrypt/src/test/resources/mime/protonmail_pgp_mime_encrypted.eml From e410b8080f1d46b0a709c14bb88b13c81c1626aa Mon Sep 17 00:00:00 2001 From: denbond7 Date: Fri, 25 Apr 2025 11:01:10 +0300 Subject: [PATCH 2/2] Added ThreadDetailsGmailApiFlowTest.testThreadDetailsWithPgpMimeMessages().| #3017 --- .../email/ui/base/BaseComposeGmailFlow.kt | 104 ---------- .../email/ui/base/BaseGmailApiTest.kt | 180 +++++++++++------- .../gmailapi/ThreadDetailsGmailApiFlowTest.kt | 33 ++++ 3 files changed, 142 insertions(+), 175 deletions(-) diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/base/BaseComposeGmailFlow.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/base/BaseComposeGmailFlow.kt index 4b988a41c9..2fe80c170b 100644 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/base/BaseComposeGmailFlow.kt +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/base/BaseComposeGmailFlow.kt @@ -19,8 +19,6 @@ import androidx.test.espresso.matcher.ViewMatchers.withParent import com.flowcrypt.email.Constants import com.flowcrypt.email.R 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.database.entity.AccountEntity import com.flowcrypt.email.database.entity.RecipientEntity @@ -34,22 +32,16 @@ import com.flowcrypt.email.matchers.ToolBarTitleMatcher.Companion.withText import com.flowcrypt.email.rules.OutgoingMessageConfigurationRule import com.flowcrypt.email.security.model.PgpKeyRingDetails import com.flowcrypt.email.security.pgp.PgpDecryptAndOrVerify -import com.flowcrypt.email.security.pgp.PgpEncryptAndOrSign import com.flowcrypt.email.security.pgp.PgpKey import com.flowcrypt.email.util.FileAndDirectoryUtils import com.flowcrypt.email.util.OutgoingMessagesManager import com.google.api.client.json.gson.GsonFactory -import com.google.api.services.gmail.model.MessagePartHeader -import jakarta.activation.DataHandler -import jakarta.activation.DataSource import jakarta.mail.BodyPart import jakarta.mail.Message import jakarta.mail.Part import jakarta.mail.Session import jakarta.mail.internet.InternetAddress -import jakarta.mail.internet.MimeBodyPart import jakarta.mail.internet.MimeMessage -import jakarta.mail.internet.MimeMultipart import kotlinx.coroutines.runBlocking import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.RecordedRequest @@ -66,10 +58,7 @@ import org.pgpainless.key.protection.PasswordBasedSecretKeyRingProtector import org.pgpainless.util.Passphrase import java.io.ByteArrayOutputStream import java.io.InputStream -import java.io.OutputStream import java.net.HttpURLConnection -import java.text.SimpleDateFormat -import java.util.Date import java.util.Properties import java.util.concurrent.TimeUnit @@ -397,99 +386,6 @@ abstract class BaseComposeGmailFlow(accountEntity: AccountEntity = BASE_ACCOUNT_ Thread.sleep(TimeUnit.SECONDS.toMillis(1)) } - private fun preparePgpMessageWithMimeContent(): String { - val mimeMessage = FlowCryptMimeMessage(Session.getInstance(Properties())) - mimeMessage.subject = SUBJECT_EXISTING_PGP_MIME - mimeMessage.setFrom(addAccountToDatabaseRule.account.email) - mimeMessage.setRecipients(Message.RecipientType.TO, addAccountToDatabaseRule.account.email) - mimeMessage.setContent(MimeMultipart().apply { - addBodyPart( - MimeBodyPart().apply { - setText(MESSAGE_EXISTING_PGP_MIME) - } - ) - - for ((index, attachment) in attachments.withIndex()) { - addBodyPart( - MimeBodyPart().apply { - dataHandler = DataHandler(object : DataSource { - override fun getInputStream(): InputStream = attachmentsDataCache[index].inputStream() - - override fun getOutputStream(): OutputStream? = null - - override fun getContentType(): String { - return if (index == 2) { - Constants.MIME_TYPE_BINARY_DATA - } else { - JavaEmailConstants.MIME_TYPE_TEXT_PLAIN - } - } - - override fun getName(): String = attachment.name - }) - - fileName = attachment.name - contentID = EmailUtil.generateContentId() - }) - } - }) - - val byteArrayOutputStream = ByteArrayOutputStream() - mimeMessage.writeTo(byteArrayOutputStream) - - return PgpEncryptAndOrSign.encryptAndOrSignMsg( - msg = byteArrayOutputStream.toString(), - pubKeys = listOf( - addPrivateKeyToDatabaseRule.pgpKeyRingDetails.publicKey, - defaultFromPgpKeyDetails.publicKey, - existingCcPgpKeyDetails.publicKey, - ), - prvKeys = listOf( - requireNotNull(defaultFromPgpKeyDetails.privateKey) - ), - secretKeyRingProtector = secretKeyRingProtector - ) - } - - private fun prepareMessageHeaders( - subject: String, - dateInMilliseconds: Long, - boundary: String - ) = listOf( - MessagePartHeader().apply { - name = "MIME-Version" - value = "1.0" - }, - MessagePartHeader().apply { - name = "Date" - value = SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss Z").format(Date(dateInMilliseconds)) - }, - MessagePartHeader().apply { - name = "Message-ID" - value = EmailUtil.generateContentId() - }, - MessagePartHeader().apply { - name = "Subject" - value = subject - }, - MessagePartHeader().apply { - name = "From" - value = DEFAULT_FROM_RECIPIENT - }, - MessagePartHeader().apply { - name = "To" - value = EXISTING_MESSAGE_TO_RECIPIENT - }, - MessagePartHeader().apply { - name = "Cc" - value = EXISTING_MESSAGE_CC_RECIPIENT - }, - MessagePartHeader().apply { - name = "Content-Type" - value = "multipart/mixed; boundary=\\\"$boundary\\\"" - }, - ) - private fun openComposeScreenAndFillDataIfNeeded() { val outgoingMessageConfiguration = requireNotNull(outgoingMessageConfigurationRule.outgoingMessageConfiguration) 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 ca1a6c69a1..1d47f31fe1 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 @@ -12,6 +12,7 @@ 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.gmail.api.GmaiAPIMimeMessage import com.flowcrypt.email.api.email.model.LocalFolder import com.flowcrypt.email.api.retrofit.ApiHelper import com.flowcrypt.email.api.retrofit.response.api.EkmPrivateKeysResponse @@ -359,12 +360,8 @@ abstract class BaseGmailApiTest(val accountEntity: AccountEntity = BASE_ACCOUNT_ ) } - request.method == "GET" && request.path == "/gmail/v1/users/me/messages/${MESSAGE_ID_EXISTING_PGP_MIME}?fields=raw&format=raw" -> { - MockResponse().setResponseCode(HttpURLConnection.HTTP_OK) - .setBody( - Base64.getEncoder() - .encodeToString(preparePgpMimeMessage(preparePgpMessageWithMimeContent()).toByteArray()) - ) + request.method == "GET" && request.path?.matches(REGEX_USER_MESSAGES_GET_PGP_MIME_FORMAT_RAW) == true -> { + genPgpMimeRawResponse(request.path ?: "") } request.method == "POST" && request.path == "/gmail/v1/users/me/messages/batchModify" -> { @@ -391,6 +388,7 @@ abstract class BaseGmailApiTest(val accountEntity: AccountEntity = BASE_ACCOUNT_ MESSAGE_ID_THREAD_FEW_MESSAGES_WITH_SINGLE_DRAFT_1, MESSAGE_ID_THREAD_FEW_MESSAGES_WITH_SINGLE_DRAFT_2, MESSAGE_ID_THREAD_FEW_MESSAGES_WITH_SINGLE_DRAFT_3, + MESSAGE_ID_THREAD_PGP_MIME_MESSAGES_1, ) if (handledIds.any { batchModifyMessagesRequest.ids.contains(it) }) { @@ -519,6 +517,43 @@ abstract class BaseGmailApiTest(val accountEntity: AccountEntity = BASE_ACCOUNT_ } } + private fun genPgpMimeRawResponse(path: String): MockResponse { + val messageId = + REGEX_USER_MESSAGES_GET_PGP_MIME_FORMAT_RAW.find(path)?.groups?.get(1)?.value?.trim() + val gmailMessage = when (messageId) { + MESSAGE_ID_THREAD_PGP_MIME_MESSAGES_1 -> genPGPMimeMessage( + threadId = THREAD_ID_PGP_MIME, + messageId = MESSAGE_ID_THREAD_PGP_MIME_MESSAGES_1, + isFullFormat = true + ) + + MESSAGE_ID_EXISTING_PGP_MIME -> genPGPMimeMessage( + threadId = THREAD_ID_EXISTING_PGP_MIME, + messageId = MESSAGE_ID_EXISTING_PGP_MIME, + isFullFormat = true + ) + + else -> null + } + + return if (gmailMessage != null) { + MockResponse().setResponseCode(HttpURLConnection.HTTP_OK) + .setBody( + Base64.getEncoder().encodeToString( + ByteArrayOutputStream().apply { + GmaiAPIMimeMessage( + Session.getInstance(Properties()), + gmailMessage + ).writeTo(this) + }.toByteArray() + ) + ) + } else { + MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_FOUND) + } + } + + private fun genUserMessagesGetWithFieldsFormatFullResponse(path: String): MockResponse { val messageId = REGEX_USER_MESSAGES_GET_WITH_FIELDS_FORMAT_FULL.find(path)?.groups?.get(1)?.value?.trim() @@ -691,7 +726,19 @@ abstract class BaseGmailApiTest(val accountEntity: AccountEntity = BASE_ACCOUNT_ ).toString() ) - MESSAGE_ID_EXISTING_PGP_MIME -> baseResponse.setBody(genPGPMimeMessage()) + MESSAGE_ID_EXISTING_PGP_MIME -> baseResponse.setBody( + genPGPMimeMessage( + threadId = THREAD_ID_EXISTING_PGP_MIME, + messageId = MESSAGE_ID_EXISTING_PGP_MIME + ).toString() + ) + + MESSAGE_ID_THREAD_PGP_MIME_MESSAGES_1 -> baseResponse.setBody( + genPGPMimeMessage( + threadId = THREAD_ID_PGP_MIME, + messageId = MESSAGE_ID_THREAD_PGP_MIME_MESSAGES_1 + ).toString() + ) else -> MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_FOUND) } @@ -720,7 +767,19 @@ abstract class BaseGmailApiTest(val accountEntity: AccountEntity = BASE_ACCOUNT_ ) MESSAGE_ID_EXISTING_PGP_MIME -> baseResponse.setBody( - genPGPMimeMessage(isFullFormat = true) + genPGPMimeMessage( + threadId = THREAD_ID_EXISTING_PGP_MIME, + messageId = MESSAGE_ID_EXISTING_PGP_MIME, + isFullFormat = true + ).toString() + ) + + MESSAGE_ID_THREAD_PGP_MIME_MESSAGES_1 -> baseResponse.setBody( + genPGPMimeMessage( + threadId = THREAD_ID_PGP_MIME, + messageId = MESSAGE_ID_THREAD_PGP_MIME_MESSAGES_1, + isFullFormat = true + ).toString() ) MESSAGE_ID_THREAD_SINGLE_STANDARD_MESSAGE -> baseResponse.setBody( @@ -866,27 +925,14 @@ abstract class BaseGmailApiTest(val accountEntity: AccountEntity = BASE_ACCOUNT_ genThreadWithMixedMessages() ) + THREAD_ID_PGP_MIME -> baseResponse.setBody( + genThreadWithPGPMimeMessages() + ) + else -> MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_FOUND) } } - private fun genPathForMessageWithSomeFields(messageId: String) = - "/gmail/v1/users/me/messages/$messageId?fields=" + - "id," + - "threadId," + - "labelIds," + - "snippet," + - "sizeEstimate," + - "historyId," + - "internalDate," + - "payload/partId," + - "payload/mimeType," + - "payload/filename," + - "payload/headers," + - "payload/body," + - "payload/parts(partId,mimeType,filename,headers,body/size,body/attachmentId)" + - "&format=full" - private fun genPathToGetAttachment(messageId: String, attachmentId: String) = "/gmail/v1/users/me/messages/${messageId}/attachments/${attachmentId}" + "?fields=data&prettyPrint=false" @@ -1105,6 +1151,17 @@ abstract class BaseGmailApiTest(val accountEntity: AccountEntity = BASE_ACCOUNT_ ) }.toString() + private fun genThreadWithPGPMimeMessages() = Thread().apply { + factory = GsonFactory.getDefaultInstance() + id = THREAD_ID_PGP_MIME + messages = listOf( + genPGPMimeMessage( + threadId = THREAD_ID_PGP_MIME, + messageId = MESSAGE_ID_THREAD_PGP_MIME_MESSAGES_1 + ) + ) + }.toString() + protected fun genStandardMessage( threadId: String, messageId: String, @@ -1390,9 +1447,9 @@ abstract class BaseGmailApiTest(val accountEntity: AccountEntity = BASE_ACCOUNT_ return byteArrayOutputStream } - private fun preparePgpMessageWithMimeContent(): String { + private fun preparePgpMessageWithMimeContent(subject: String): String { val mimeMessage = FlowCryptMimeMessage(Session.getInstance(Properties())) - mimeMessage.subject = SUBJECT_EXISTING_PGP_MIME + mimeMessage.subject = subject mimeMessage.setFrom(addAccountToDatabaseRule.account.email) mimeMessage.setRecipients(Message.RecipientType.TO, addAccountToDatabaseRule.account.email) mimeMessage.setContent(MimeMultipart().apply { @@ -1487,13 +1544,19 @@ abstract class BaseGmailApiTest(val accountEntity: AccountEntity = BASE_ACCOUNT_ }, ) - private fun genPGPMimeMessage(isFullFormat: Boolean = false) = + private fun genPGPMimeMessage( + threadId: String, + messageId: String, + labels: List = listOf(JavaEmailConstants.FOLDER_INBOX), + subject: String = SUBJECT_EXISTING_PGP_MIME, + isFullFormat: Boolean = false + ) = com.google.api.services.gmail.model.Message().apply { factory = GsonFactory.getDefaultInstance() - id = MESSAGE_ID_EXISTING_PGP_MIME - threadId = THREAD_ID_EXISTING_PGP_MIME - labelIds = listOf(JavaEmailConstants.FOLDER_INBOX) - snippet = SUBJECT_EXISTING_PGP_MIME + id = messageId + this.threadId = threadId + labelIds = labels + snippet = subject historyId = HISTORY_ID_PGP_MIME val boundary = "000000000000fbd8c4060ea7c69b" payload = MessagePart().apply { @@ -1501,10 +1564,10 @@ abstract class BaseGmailApiTest(val accountEntity: AccountEntity = BASE_ACCOUNT_ mimeType = "multipart/encrypted" filename = "" headers = prepareMessageHeaders( - subject = SUBJECT_EXISTING_PGP_MIME, + subject = subject, dateInMilliseconds = DATE_EXISTING_PGP_MIME, boundary = boundary, - messageId = MESSAGE_ID_EXISTING_PGP_MIME + messageId = messageId ).filterNot { it.name == "Content-Type" }.toMutableList().apply { @@ -1561,7 +1624,10 @@ abstract class BaseGmailApiTest(val accountEntity: AccountEntity = BASE_ACCOUNT_ }, ) body = MessagePartBody().apply { - val pgpMessageWithMimeContent = preparePgpMessageWithMimeContent() + val pgpMessageWithMimeContent = preparePgpMessageWithMimeContent(subject = subject) + if (isFullFormat) { + data = Base64.getEncoder().encodeToString(pgpMessageWithMimeContent.toByteArray()) + } setSize(pgpMessageWithMimeContent.length) } } @@ -1569,39 +1635,7 @@ abstract class BaseGmailApiTest(val accountEntity: AccountEntity = BASE_ACCOUNT_ } internalDate = DATE_EXISTING_PGP_MIME sizeEstimate = 0 // we don't care about this parameter - }.toString() - - private fun preparePgpMimeMessage(pgpMessage: String): String { - return "Return-Path: \n" + - "Delivered-To: default@flowcrypt.test\n" + - "Message-ID: <0af3b089-d018-42ba-b897-c1553caae9d5@flowcrypt.test>\n" + - "Date: Thu, 7 Mar 2024 18:00:18 +0200\n" + - "Mime-Version: 1.0\n" + - "Content-Language: en-US\n" + - "To: default@flowcrypt.test\n" + - "From: Default User \n" + - "Subject: ...\n" + - "Content-Type: multipart/encrypted;\n" + - " protocol=\"application/pgp-encrypted\";\n" + - " boundary=\"------------vjUmb0D80S09zqu10qP9Vv0s\"\n" + - "\n" + - "This is an OpenPGP/MIME encrypted message (RFC 4880 and 3156)\n" + - "--------------vjUmb0D80S09zqu10qP9Vv0s\n" + - "Content-Type: application/pgp-encrypted\n" + - "Content-Description: PGP/MIME version identification\n" + - "\n" + - "Version: 1\n" + - "\n" + - "--------------vjUmb0D80S09zqu10qP9Vv0s\n" + - "Content-Type: application/octet-stream; name=\"encrypted.asc\"\n" + - "Content-Description: OpenPGP encrypted message\n" + - "Content-Disposition: inline; filename=\"encrypted.asc\"\n" + - "\n" + - pgpMessage + - "\n" + - "\n" + - "--------------vjUmb0D80S09zqu10qP9Vv0s--\n" - } + } private fun createFilesForCommonAttachments() { attachments.clear() @@ -1637,7 +1671,6 @@ abstract class BaseGmailApiTest(val accountEntity: AccountEntity = BASE_ACCOUNT_ const val THREAD_ID_ONLY_ENCRYPTED = "200000e222d6c002" const val MESSAGE_ID_THREAD_ONLY_ENCRYPTED_1 = "5555555559992001" const val MESSAGE_ID_THREAD_ONLY_ENCRYPTED_2 = "5555555559992002" - const val THREAD_ID_STANDARD_AND_ENCRYPTED = "200000e222d6c003" const val THREAD_ID_NO_ATTACHMENTS = "200000e222d6c004" const val MESSAGE_ID_THREAD_NO_ATTACHMENTS_1 = "5555555559993001" const val MESSAGE_ID_THREAD_NO_ATTACHMENTS_2 = "5555555559993002" @@ -1663,6 +1696,8 @@ abstract class BaseGmailApiTest(val accountEntity: AccountEntity = BASE_ACCOUNT_ 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 THREAD_ID_PGP_MIME = "200000e222d6c011" + const val MESSAGE_ID_THREAD_PGP_MIME_MESSAGES_1 = "5555555559911001" const val SUBJECT_NO_ATTACHMENTS = "No attachments" const val SUBJECT_SINGLE_STANDARD = "Single standard message" @@ -1691,8 +1726,8 @@ abstract class BaseGmailApiTest(val accountEntity: AccountEntity = BASE_ACCOUNT_ const val MESSAGE_ID_EXISTING_PGP_MIME = "5555555555555553" const val THREAD_ID_EXISTING_PGP_MIME = "1111111111111113" const val DATE_EXISTING_PGP_MIME = 1704963581000 - const val SUBJECT_EXISTING_PGP_MIME = "PGP/MIME Encrypted" - const val MESSAGE_EXISTING_PGP_MIME = "PGP/MIME" + const val SUBJECT_EXISTING_PGP_MIME = "PGP/MIME" + const val MESSAGE_EXISTING_PGP_MIME = "PGP/MIME message" val HISTORY_ID_STANDARD = BigInteger("53163127") val HISTORY_ID_ENCRYPTED = BigInteger("53163327") @@ -1706,6 +1741,8 @@ abstract class BaseGmailApiTest(val accountEntity: AccountEntity = BASE_ACCOUNT_ "body/size,body/attachmentId\\)&format=full").toRegex() val REGEX_USER_MESSAGES_GET_FORMAT_FULL = ("/gmail/v1/users/me/messages/(.{16})\\?format=full").toRegex() + val REGEX_USER_MESSAGES_GET_PGP_MIME_FORMAT_RAW = + ("/gmail/v1/users/me/messages/(.{16})\\?fields=raw&format=raw").toRegex() val REGEX_USER_THREADS_GET_FORMAT_FULL = ("/gmail/v1/users/me/threads/(.{16})\\?format=full").toRegex() @@ -1771,6 +1808,7 @@ abstract class BaseGmailApiTest(val accountEntity: AccountEntity = BASE_ACCOUNT_ Thread().apply { id = THREAD_ID_FEW_MESSAGES_WITH_FEW_DRAFTS }, Thread().apply { id = THREAD_ID_ONE_MESSAGE_WITH_FEW_DRAFTS }, Thread().apply { id = THREAD_ID_MIXED_MESSAGES }, + Thread().apply { id = THREAD_ID_PGP_MIME }, ) } 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 3693a58a20..f5dee2137f 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 @@ -410,4 +410,37 @@ class ThreadDetailsGmailApiFlowTest : BaseThreadDetailsGmailApiFlowTest() { ) ) } + + @Test + fun testThreadDetailsWithPgpMimeMessages() { + openThreadBasedOnPosition(9) + checkCorrectThreadDetails( + messagesCount = 1, + threadSubject = SUBJECT_EXISTING_PGP_MIME, + labels = listOf( + GmailApiLabelsListAdapter.Label("Inbox") + ) + ) + checkBaseMessageDetailsInTread( + fromAddress = "From", + datetimeInMilliseconds = DATE_EXISTING_ENCRYPTED + ) + checkPgpBadges( + 2, + PgpBadgeListAdapter.PgpBadge.Type.ENCRYPTED, + PgpBadgeListAdapter.PgpBadge.Type.SIGNED + ) + checkWebViewText(MESSAGE_EXISTING_PGP_MIME) + checkAttachments( + listOf( + Pair(ATTACHMENT_NAME_1, attachmentsDataCache[0].size.toLong()), + Pair(ATTACHMENT_NAME_2, attachmentsDataCache[1].size.toLong()), + Pair(ATTACHMENT_NAME_3, attachmentsDataCache[2].size.toLong()) + ) + ) + checkReplyButtons(isEncryptedMode = true) + //hasAttachments = false because the app doesn't update UI when found attachments + //in decrypted PGPMime message + checkCollapsedState(1, hasPgp = true, hasAttachments = false) + } } \ No newline at end of file