diff --git a/FlowCrypt/src/test/java/com/flowcrypt/email/core/msg/RawBlockParserTest.kt b/FlowCrypt/src/test/java/com/flowcrypt/email/core/msg/RawBlockParserTest.kt index 2df7922c4b..8536f1860e 100644 --- a/FlowCrypt/src/test/java/com/flowcrypt/email/core/msg/RawBlockParserTest.kt +++ b/FlowCrypt/src/test/java/com/flowcrypt/email/core/msg/RawBlockParserTest.kt @@ -1,15 +1,31 @@ /* * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com - * Contributors: Ivan Pizhenko + * Contributors: denbond7 */ package com.flowcrypt.email.core.msg import com.flowcrypt.email.extensions.kotlin.normalize +import com.flowcrypt.email.security.pgp.PgpSignature +import com.flowcrypt.email.util.TestUtil import org.junit.Assert.assertEquals import org.junit.Test class RawBlockParserTest { + + @Test + fun testExtractClearTextFromMsgSignedMessagePreserveNewlines() { + val text = TestUtil.readResourceAsString("pgp/messages/signed-message-preserve-newlines.txt") + val blocks = RawBlockParser.detectBlocks(text).toList() + val clearText = PgpSignature.extractClearText(text) + assertEquals( + "Standard message\n\nsigned inline\n\nshould easily verify\nThis is email footer", + clearText + ) + assertEquals(1, blocks.size) + assertEquals(RawBlockParser.RawBlockType.PGP_CLEARSIGN_MSG, blocks[0].type) + } + @Test fun testNoStringIndexOutOfBoundsExceptionInParser() { checkForSinglePlaintextBlock("-----BEGIN FOO-----\n") diff --git a/FlowCrypt/src/test/java/com/flowcrypt/email/security/pgp/PgpDecryptAndOrVerifyTest.kt b/FlowCrypt/src/test/java/com/flowcrypt/email/security/pgp/PgpDecryptAndOrVerifyTest.kt index ceb0c5c71c..8a220b06bc 100644 --- a/FlowCrypt/src/test/java/com/flowcrypt/email/security/pgp/PgpDecryptAndOrVerifyTest.kt +++ b/FlowCrypt/src/test/java/com/flowcrypt/email/security/pgp/PgpDecryptAndOrVerifyTest.kt @@ -1,33 +1,38 @@ /* * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com - * Contributors: DenBond7 + * Contributors: denbond7 */ package com.flowcrypt.email.security.pgp import com.flowcrypt.email.core.msg.RawBlockParser import com.flowcrypt.email.extensions.createFileWithRandomData +import com.flowcrypt.email.extensions.kotlin.toInputStream +import com.flowcrypt.email.util.TestUtil import com.flowcrypt.email.util.exception.DecryptionException import org.apache.commons.io.FileUtils import org.bouncycastle.openpgp.PGPPublicKeyRingCollection import org.bouncycastle.openpgp.PGPSecretKeyRing import org.bouncycastle.openpgp.PGPSecretKeyRingCollection import org.junit.Assert +import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue import org.junit.BeforeClass -import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.junit.rules.TemporaryFolder import org.pgpainless.PGPainless +import org.pgpainless.bouncycastle.extensions.certificate import org.pgpainless.key.protection.KeyRingProtectionSettings import org.pgpainless.key.protection.PasswordBasedSecretKeyRingProtector +import org.pgpainless.key.protection.SecretKeyRingProtector import org.pgpainless.key.protection.passphrase_provider.SecretKeyPassphraseProvider -import org.pgpainless.key.util.KeyRingUtils import org.pgpainless.util.Passphrase import java.io.ByteArrayInputStream import java.io.ByteArrayOutputStream +import java.nio.charset.Charset import java.util.UUID /** @@ -39,9 +44,148 @@ class PgpDecryptAndOrVerifyTest { val temporaryFolder: TemporaryFolder = TemporaryFolder() @Test - @Ignore("need to add realization") - fun testDecryptJustSignedFile() { + fun multipleDecryptionTest() { + listOf( + "decrypt - without a subject", + "decrypt - [enigmail] encrypted iso-2022-jp pgp-mime", + "decrypt - [enigmail] encrypted iso-2022-jp, plain text", + "decrypt - [gpg] signed fully armored message" + ).forEach { key -> + val decryptionResult = processMessage(key) + assertNotNull("Message not returned", decryptionResult.content) + findMessage(key).run { + checkContent( + expected = content, + actual = requireNotNull(decryptionResult.content?.toByteArray()), + charset = charset + ) + } + } + } + + @Test + fun missingMdcTest() { + val decryptionResult = processMessage("decrypt - [security] mdc - missing - error") + assertNull("Message is returned when should not", decryptionResult.content) + assertNotNull("Error not returned", decryptionResult.exception) + assertTrue( + "Missing MDC not detected", + decryptionResult.exception?.decryptionErrorType == PgpDecryptAndOrVerify.DecryptionErrorType.NO_MDC + ) + } + + @Test + fun badMdcTest() { + val decryptionResult = + processMessage("decrypt - [security] mdc - modification detected - error") + assertNull("Message is returned when should not", decryptionResult.content) + assertNotNull("Error not returned", decryptionResult.exception) + assertTrue( + "Bad MDC not detected", + decryptionResult.exception?.decryptionErrorType == PgpDecryptAndOrVerify.DecryptionErrorType.BAD_MDC + ) + } + + @Test + //https://github.com/FlowCrypt/flowcrypt-android/issues/1214 + //TODO: Should there be any error? + fun decryptionTestIssue1214() { + val decryptionResult = processMessage( + "decrypt - [everdesk] message encrypted for sub but claims encryptedFor-primary,sub" + ) + assertNotNull("Message not returned", decryptionResult.content) + assertNull("Error returned", decryptionResult.exception) + } + + @Test + fun missingArmorChecksumTest() { + // This is a test for the message with missing armor checksum - different from MDC. + // Usually the four digits at the and like p3Fc=. + // Such messages are still valid if this is missing, + // and should decrypt correctly - so it's good as is. + val decryptionResult = processMessage("decrypt - encrypted missing checksum") + assertNotNull("Message not returned", decryptionResult.content) + assertNull("Error returned", decryptionResult.exception) + } + + @Test + fun wrongArmorChecksumTest() { + val decryptionResult = processMessage("decrypt - issue 1347 - wrong checksum") + assertNotNull("Error not returned", decryptionResult.exception != null) + assertEquals( + PgpDecryptAndOrVerify.DecryptionErrorType.FORMAT, + decryptionResult.exception?.decryptionErrorType + ) + } + + @Test + fun wrongPassphraseTest() { + val messageInfo = findMessage("decrypt - without a subject") + val wrongPassphrase = Passphrase.fromPassword("this is wrong passphrase for sure") + val privateKeysWithWrongPassPhrases = FLOWCRYPT_COMPATIBILITY_PRIVATE_KEYS.map { + TestKeys.KeyWithPassPhrase(keyRing = it.keyRing, passphrase = wrongPassphrase) + } + val decryptionResult = PgpDecryptAndOrVerify.decryptAndOrVerifyWithResult( + srcInputStream = messageInfo.armored.byteInputStream(), + publicKeys = PGPPublicKeyRingCollection(emptyList()), + secretKeys = PGPSecretKeyRingCollection(privateKeysWithWrongPassPhrases.map { it.keyRing }), + protector = SecretKeyRingProtector.unprotectedKeys() + ) + assertNull("Message returned", decryptionResult.content) + assertNotNull("Error not returned", decryptionResult.exception) + assertTrue( + "Wrong passphrase not detected", + decryptionResult.exception?.decryptionErrorType == PgpDecryptAndOrVerify.DecryptionErrorType.WRONG_PASSPHRASE + ) + } + @Test + fun missingPassphraseTest() { + val messageInfo = findMessage("decrypt - without a subject") + val decryptionResult = PgpDecryptAndOrVerify.decryptAndOrVerifyWithResult( + srcInputStream = messageInfo.armored.byteInputStream(), + publicKeys = PGPPublicKeyRingCollection(emptyList()), + secretKeys = PGPSecretKeyRingCollection(FLOWCRYPT_COMPATIBILITY_PRIVATE_KEYS.map { it.keyRing }), + protector = SecretKeyRingProtector.unprotectedKeys() + ) + assertNull("Message returned", decryptionResult.content) + assertNotNull("Error returned", decryptionResult.exception) + assertTrue( + "Missing passphrase not detected", + decryptionResult.exception?.decryptionErrorType == PgpDecryptAndOrVerify.DecryptionErrorType.WRONG_PASSPHRASE + ) + } + + @Test + fun wrongKeyTest() { + val messageInfo = findMessage("decrypt - without a subject") + val wrongKey = listOf(FLOWCRYPT_COMPATIBILITY_PRIVATE_KEYS[1]) + val decryptionResult = PgpDecryptAndOrVerify.decryptAndOrVerifyWithResult( + messageInfo.armored.byteInputStream(), + publicKeys = PGPPublicKeyRingCollection(emptyList()), + secretKeys = PGPSecretKeyRingCollection(wrongKey.map { it.keyRing }), + protector = SecretKeyRingProtector.unprotectedKeys() + ) + assertNull("Message returned", decryptionResult.content) + assertNotNull("Error not returned", decryptionResult.exception) + assertTrue( + "Key mismatch not detected", + decryptionResult.exception?.decryptionErrorType == PgpDecryptAndOrVerify.DecryptionErrorType.KEY_MISMATCH + ) + } + + @Test + fun singleDecryptionTest() { + val key = "decrypt - [enigmail] encrypted iso-2022-jp, plain text" + val decryptionResult = processMessage(key) + assertNotNull("Message not returned", decryptionResult.content) + findMessage(key).run { + checkContent( + expected = content, + actual = requireNotNull(decryptionResult.content?.toByteArray()), + charset = charset + ) + } } @Test @@ -64,8 +208,8 @@ class PgpDecryptAndOrVerifyTest { destOutputStream = outputStreamForEncryptedSource, pgpPublicKeyRingCollection = PGPPublicKeyRingCollection( listOf( - KeyRingUtils.publicKeyRingFrom(senderPGPSecretKeyRing), - KeyRingUtils.publicKeyRingFrom(receiverPGPSecretKeyRing) + senderPGPSecretKeyRing.certificate, + receiverPGPSecretKeyRing.certificate ) ), doArmor = false @@ -88,7 +232,7 @@ class PgpDecryptAndOrVerifyTest { ) } - Assert.assertEquals( + assertEquals( exception.decryptionErrorType, PgpDecryptAndOrVerify.DecryptionErrorType.KEY_MISMATCH ) @@ -104,8 +248,8 @@ class PgpDecryptAndOrVerifyTest { destOutputStream = outputStreamForEncryptedSource, pgpPublicKeyRingCollection = PGPPublicKeyRingCollection( listOf( - KeyRingUtils.publicKeyRingFrom(senderPGPSecretKeyRing), - KeyRingUtils.publicKeyRingFrom(receiverPGPSecretKeyRing) + senderPGPSecretKeyRing.certificate, + receiverPGPSecretKeyRing.certificate ) ), doArmor = false @@ -125,36 +269,12 @@ class PgpDecryptAndOrVerifyTest { ) } - Assert.assertEquals( + assertEquals( exception.decryptionErrorType, PgpDecryptAndOrVerify.DecryptionErrorType.WRONG_PASSPHRASE ) } - @Test - @Ignore("need to add realization") - fun testDecryptionErrorNoMdc() { - - } - - @Test - @Ignore("need to add realization") - fun testDecryptionErrorBadMdc() { - - } - - @Test - @Ignore("need to add realization") - fun testDecryptionErrorFormat() { - - } - - @Test - @Ignore("need to add realization") - fun testDecryptionErrorOther() { - - } - @Test fun testPatternToDetectEncryptedAtts() { //"(?i)(\\.pgp$)|(\\.gpg$)|(\\.[a-zA-Z0-9]{3,4}\\.asc$)" @@ -176,6 +296,24 @@ class PgpDecryptAndOrVerifyTest { assertNull(RawBlockParser.ENCRYPTED_FILE_REGEX.find("d.ft2ASC")) } + @Test + fun testDecryptMsgUnescapedSpecialCharactersInEncryptedText() { + val text = TestUtil.readResourceAsString("pgp/messages/direct-encrypted-text-special-chars.txt") + val keys = TestKeys.KEYS["rsa1"]!!.listOfKeysWithPassPhrase + + val decryptResult = PgpDecryptAndOrVerify.decryptAndOrVerifyWithResult( + text.toInputStream(), + publicKeys = PGPPublicKeyRingCollection(emptyList()), + secretKeys = PGPSecretKeyRingCollection(keys.map { it.keyRing }), + protector = TestKeys.genRingProtector(keys) + ) + assertEquals(true, decryptResult.isEncrypted) + assertEquals( + "> special & other\n> second line", + String(decryptResult.content?.toByteArray() ?: byteArrayOf()) + ) + } + private fun testDecryptFileSuccess(shouldSrcBeArmored: Boolean) { val srcFile = temporaryFolder.createFileWithRandomData(fileSizeInBytes = FileUtils.ONE_MB) val sourceBytes = srcFile.readBytes() @@ -185,8 +323,8 @@ class PgpDecryptAndOrVerifyTest { destOutputStream = outputStreamForEncryptedSource, pgpPublicKeyRingCollection = PGPPublicKeyRingCollection( listOf( - KeyRingUtils.publicKeyRingFrom(senderPGPSecretKeyRing), - KeyRingUtils.publicKeyRingFrom(receiverPGPSecretKeyRing) + senderPGPSecretKeyRing.certificate, + receiverPGPSecretKeyRing.certificate ) ), doArmor = shouldSrcBeArmored @@ -205,6 +343,33 @@ class PgpDecryptAndOrVerifyTest { Assert.assertArrayEquals(sourceBytes, decryptedBytesForSender) } + private fun processMessage(messageKey: String): PgpDecryptAndOrVerify.DecryptionResult { + val messageInfo = findMessage(messageKey) + val result = PgpDecryptAndOrVerify.decryptAndOrVerifyWithResult( + messageInfo.armored.byteInputStream(), + publicKeys = PGPPublicKeyRingCollection(emptyList()), + secretKeys = PGPSecretKeyRingCollection(FLOWCRYPT_COMPATIBILITY_PRIVATE_KEYS.map { it.keyRing }), + protector = secretKeyRingProtectorFlowcryptCompatibility + ) + return result + } + + private fun checkContent(expected: List, actual: ByteArray, charset: String) { + val actualText = String(actual, Charset.forName(charset)) + for (text in expected) { + assertTrue("Text '$text' not found", actualText.indexOf(text) != -1) + } + } + + private data class MessageInfo( + val key: String, + val content: List, + val quoted: Boolean? = null, + val charset: String = "UTF-8" + ) { + val armored: String by lazy { TestUtil.readResourceAsString("pgp/messages/$key.txt") } + } + companion object { private lateinit var senderPGPSecretKeyRing: PGPSecretKeyRing private lateinit var receiverPGPSecretKeyRing: PGPSecretKeyRing @@ -213,6 +378,90 @@ class PgpDecryptAndOrVerifyTest { private const val SENDER_PASSWORD = "qwerty1234" private const val RECEIVER_PASSWORD = "password1234" + private val FLOWCRYPT_COMPATIBILITY_PRIVATE_KEYS = listOf( + TestKeys.KeyWithPassPhrase( + passphrase = Passphrase.fromPassword("flowcrypt compatibility tests"), + keyRing = requireNotNull(loadSecretKey("key0.txt")), + ), + + TestKeys.KeyWithPassPhrase( + passphrase = Passphrase.fromPassword("flowcrypt compatibility tests 2"), + keyRing = requireNotNull(loadSecretKey("key1.txt")) + ) + ) + + private val secretKeyRingProtectorFlowcryptCompatibility = + TestKeys.genRingProtector(FLOWCRYPT_COMPATIBILITY_PRIVATE_KEYS) + + private fun loadSecretKey(fileName: String): PGPSecretKeyRing? { + return PGPainless.readKeyRing() + .secretKeyRing(TestUtil.readResourceAsString("pgp/keys/$fileName")) + } + + private val MESSAGES = listOf( + MessageInfo( + key = "decrypt - without a subject", + content = listOf("This is a compatibility test email") + ), + + MessageInfo( + key = "decrypt - [security] mdc - missing - error", + content = listOf("Security threat!", "MDC", "Display the message at your own risk."), + ), + + MessageInfo( + key = "decrypt - [security] mdc - modification detected - error", + content = listOf( + "Security threat - opening this message is dangerous because it was modified" + + " in transit." + ), + ), + + MessageInfo( + key = "decrypt - [everdesk] message encrypted for sub but claims encryptedFor-primary,sub", + content = listOf("this is a sample for FlowCrypt compatibility") + ), + + MessageInfo( + key = "decrypt - [gpg] signed fully armored message", + content = listOf( + "this was encrypted with gpg", + "gpg --sign --armor -r flowcrypt.compatibility@gmail.com ./text.txt" + ), + quoted = false + ), + + MessageInfo( + key = "decrypt - encrypted missing checksum", + content = listOf("400 library systems in 177 countries worldwide") + ), + + MessageInfo( + key = "decrypt - [enigmail] encrypted iso-2022-jp pgp-mime", + content = listOf("=E3=82=BE=E3=81=97=E9=80=B8=E7=8F=BE"), // part of "ゾし逸現飲" + charset = "ISO-2022-JP", + ), + + MessageInfo( + key = "decrypt - [enigmail] encrypted iso-2022-jp, plain text", + content = listOf( + // complete string "ゾし逸現飲" + TestUtil.decodeString("=E3=82=BE=E3=81=97=E9=80=B8=E7=8F=BE=E9=A3=B2", "UTF-8") + ), + charset = "ISO-2022-JP", + ), + + MessageInfo( + key = "decrypt - issue 1347 - wrong checksum", + content = listOf("") + ), + ) + + private fun findMessage(key: String): MessageInfo { + return MESSAGES.firstOrNull { it.key == key } + ?: throw IllegalArgumentException("Message '$key' not found") + } + @BeforeClass @JvmStatic fun setUp() { diff --git a/FlowCrypt/src/test/java/com/flowcrypt/email/security/pgp/PgpKeyTest.kt b/FlowCrypt/src/test/java/com/flowcrypt/email/security/pgp/PgpKeyTest.kt index ba95726f26..48d4dce227 100644 --- a/FlowCrypt/src/test/java/com/flowcrypt/email/security/pgp/PgpKeyTest.kt +++ b/FlowCrypt/src/test/java/com/flowcrypt/email/security/pgp/PgpKeyTest.kt @@ -25,24 +25,19 @@ import org.pgpainless.exception.KeyIntegrityException import org.pgpainless.key.OpenPgpV4Fingerprint import org.pgpainless.policy.Policy.HashAlgorithmPolicy import org.pgpainless.util.Passphrase -import java.nio.charset.Charset -import java.nio.charset.StandardCharsets class PgpKeyTest { companion object { - private fun loadResourceAsString( - path: String, - charset: Charset = StandardCharsets.UTF_8 - ): String = TestUtil.readResourceAsString("${PgpKeyTest::class.simpleName}/$path", charset) - @Suppress("SameParameterValue") private fun loadSecretKey(keyFile: String): PGPSecretKeyRing? { - return PGPainless.readKeyRing().secretKeyRing(loadResourceAsString("keys/$keyFile")) + return PGPainless.readKeyRing() + .secretKeyRing(TestUtil.readResourceAsString("pgp/keys/$keyFile")) } @Suppress("SameParameterValue") private fun loadPublicKey(keyFile: String): PGPPublicKeyRing? { - return PGPainless.readKeyRing().publicKeyRing(loadResourceAsString("keys/$keyFile")) + return PGPainless.readKeyRing() + .publicKeyRing(TestUtil.readResourceAsString("pgp/keys/$keyFile")) } } @@ -55,8 +50,8 @@ class PgpKeyTest { usableForSigning = true, isRevoked = false, privateKey = null, - publicKey = loadResourceAsString( - "keys/E76853E128A0D376CAE47C143A30F4CC0A9A8F10.public.gpg-key" + publicKey = TestUtil.readResourceAsString( + "pgp/keys/E76853E128A0D376CAE47C143A30F4CC0A9A8F10.public.gpg-key" ).replace("@@VERSION_NAME@@", BuildConfig.VERSION_NAME), users = listOf("Test "), primaryUserId = "Test ", @@ -100,8 +95,8 @@ class PgpKeyTest { usableForEncryption = false, usableForSigning = false, privateKey = null, - publicKey = loadResourceAsString( - "keys/6D3E09867544EE627F2E928FBEE3A42D9A9C8AC9.public.gpg-key" + publicKey = TestUtil.readResourceAsString( + "pgp/keys/6D3E09867544EE627F2E928FBEE3A42D9A9C8AC9.public.gpg-key" ).replace("@@VERSION_NAME@@", BuildConfig.VERSION_NAME), users = listOf(""), primaryUserId = "", @@ -153,7 +148,7 @@ class PgpKeyTest { @Test fun testPublicKey_Issue1358() { - val keyText = loadResourceAsString("keys/issue-1358.public.gpg-key") + val keyText = TestUtil.readResourceAsString("pgp/keys/issue-1358.public.gpg-key") val actual = PgpKey.parseKeys(source = keyText) assertEquals(1, actual.getAllKeys().size) } @@ -162,7 +157,8 @@ class PgpKeyTest { fun testReadCorruptedPrivateKey() { try { PGPainless.getPolicy().enableKeyParameterValidation = true - val encryptedKeyText = loadResourceAsString("keys/issue-1669-corrupted.private.gpg-key") + val encryptedKeyText = + TestUtil.readResourceAsString("pgp/keys/issue-1669-corrupted.private.gpg-key") val passphrase = Passphrase.fromPassword("123") assertThrows(KeyIntegrityException::class.java) { PgpKey.checkSecretKeyIntegrity(encryptedKeyText, passphrase) @@ -177,7 +173,7 @@ class PgpKeyTest { val policy = PGPainless.getPolicy() val originalSignatureHashAlgorithmPolicy = policy.certificationSignatureHashAlgorithmPolicy try { - val keyWithSHA1Algo = loadResourceAsString("keys/sha1@flowcrypt.test_pub.asc") + val keyWithSHA1Algo = TestUtil.readResourceAsString("pgp/keys/sha1@flowcrypt.test_pub.asc") PGPainless.getPolicy().certificationSignatureHashAlgorithmPolicy = HashAlgorithmPolicy.static2022SignatureHashAlgorithmPolicy() assertFalse(policy.certificationSignatureHashAlgorithmPolicy.isAcceptable(HashAlgorithm.SHA1)) @@ -196,7 +192,7 @@ class PgpKeyTest { val originalSignatureHashAlgorithmPolicy = policy.certificationSignatureHashAlgorithmPolicy try { assertFalse(policy.certificationSignatureHashAlgorithmPolicy.isAcceptable(HashAlgorithm.SHA1)) - val keyWithSHA1Algo = loadResourceAsString("keys/sha1@flowcrypt.test_pub.asc") + val keyWithSHA1Algo = TestUtil.readResourceAsString("pgp/keys/sha1@flowcrypt.test_pub.asc") PGPainless.getPolicy().certificationSignatureHashAlgorithmPolicy = HashAlgorithmPolicy.static2022RevocationSignatureHashAlgorithmPolicy() assertTrue(policy.certificationSignatureHashAlgorithmPolicy.isAcceptable(HashAlgorithm.SHA1)) diff --git a/FlowCrypt/src/test/java/com/flowcrypt/email/security/pgp/PgpMsgTest.kt b/FlowCrypt/src/test/java/com/flowcrypt/email/security/pgp/PgpMsgTest.kt deleted file mode 100644 index c406a85de5..0000000000 --- a/FlowCrypt/src/test/java/com/flowcrypt/email/security/pgp/PgpMsgTest.kt +++ /dev/null @@ -1,945 +0,0 @@ -/* - * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com - * Contributors: denbond7 - */ - -package com.flowcrypt.email.security.pgp - -import com.flowcrypt.email.api.retrofit.response.model.DecryptedAndOrSignedContentMsgBlock -import com.flowcrypt.email.api.retrofit.response.model.MsgBlock -import com.flowcrypt.email.core.msg.MimeUtils -import com.flowcrypt.email.core.msg.RawBlockParser -import com.flowcrypt.email.extensions.kotlin.normalizeEol -import com.flowcrypt.email.extensions.kotlin.removeUtf8Bom -import com.flowcrypt.email.extensions.kotlin.toEscapedHtml -import com.flowcrypt.email.extensions.kotlin.toInputStream -import com.flowcrypt.email.util.TestUtil -import com.google.gson.JsonParser -import jakarta.mail.Session -import jakarta.mail.internet.MimeMessage -import kotlinx.coroutines.runBlocking -import org.bouncycastle.openpgp.PGPPublicKeyRingCollection -import org.bouncycastle.openpgp.PGPSecretKeyRing -import org.bouncycastle.openpgp.PGPSecretKeyRingCollection -import org.jsoup.Jsoup -import org.jsoup.parser.Parser -import org.junit.Assert.assertEquals -import org.junit.Assert.assertNotNull -import org.junit.Assert.assertTrue -import org.junit.Ignore -import org.junit.Test -import org.pgpainless.PGPainless -import org.pgpainless.key.protection.SecretKeyRingProtector -import org.pgpainless.key.protection.UnprotectedKeysProtector -import org.pgpainless.util.Passphrase -import java.nio.charset.Charset -import java.nio.charset.StandardCharsets -import java.util.Base64 -import java.util.Properties - -class PgpMsgTest { - private data class MessageInfo( - val key: String, - val content: List, - val quoted: Boolean? = null, - val charset: String = "UTF-8" - ) { - val armored: String by lazy { loadResourceAsString("messages/$key.txt") } - } - - companion object { - private const val NEXT_MSG_BLOCK_DELIMITER = "\n" - - private val BODY_SPLIT_REGEX = Regex("") - - private const val TEXT_SPECIAL_CHARS = "> special & other\n> second line" - - private val HTML_SPECIAL_CHARS = TEXT_SPECIAL_CHARS.toEscapedHtml() - - private val PRIVATE_KEYS = listOf( - TestKeys.KeyWithPassPhrase( - passphrase = Passphrase.fromPassword("flowcrypt compatibility tests"), - keyRing = requireNotNull(loadSecretKey("key0.txt")), - ), - - TestKeys.KeyWithPassPhrase( - passphrase = Passphrase.fromPassword("flowcrypt compatibility tests 2"), - keyRing = requireNotNull(loadSecretKey("key1.txt")) - ) - ) - - private val secretKeyRingProtector = TestKeys.genRingProtector(PRIVATE_KEYS) - - private val MESSAGES = listOf( - MessageInfo( - key = "decrypt - without a subject", - content = listOf("This is a compatibility test email") - ), - - MessageInfo( - key = "decrypt - [security] mdc - missing - error", - content = listOf("Security threat!", "MDC", "Display the message at your own risk."), - ), - - MessageInfo( - key = "decrypt - [security] mdc - modification detected - error", - content = listOf( - "Security threat - opening this message is dangerous because it was modified" + - " in transit." - ), - ), - - MessageInfo( - key = "decrypt - [everdesk] message encrypted for sub but claims encryptedFor-primary,sub", - content = listOf("this is a sample for FlowCrypt compatibility") - ), - - MessageInfo( - key = "decrypt - [gpg] signed fully armored message", - content = listOf( - "this was encrypted with gpg", - "gpg --sign --armor -r flowcrypt.compatibility@gmail.com ./text.txt" - ), - quoted = false - ), - - MessageInfo( - key = "decrypt - encrypted missing checksum", - content = listOf("400 library systems in 177 countries worldwide") - ), - - MessageInfo( - key = "decrypt - [enigmail] encrypted iso-2022-jp pgp-mime", - content = listOf("=E3=82=BE=E3=81=97=E9=80=B8=E7=8F=BE"), // part of "ゾし逸現飲" - charset = "ISO-2022-JP", - ), - - MessageInfo( - key = "decrypt - [enigmail] encrypted iso-2022-jp, plain text", - content = listOf( - // complete string "ゾし逸現飲" - TestUtil.decodeString("=E3=82=BE=E3=81=97=E9=80=B8=E7=8F=BE=E9=A3=B2", "UTF-8") - ), - charset = "ISO-2022-JP", - ), - - MessageInfo( - key = "decrypt - issue 1347 - wrong checksum", - content = listOf("") - ), - ) - - private fun findMessage(key: String): MessageInfo { - return MESSAGES.firstOrNull { it.key == key } - ?: throw IllegalArgumentException("Message '$key' not found") - } - - private fun loadResourceAsString( - path: String, - charset: Charset = StandardCharsets.UTF_8 - ): String { - return TestUtil.readResourceAsString( - path = "${PgpMsgTest::class.java.simpleName}/$path", - charset = charset - ) - } - - private fun loadSecretKey(keyFile: String): PGPSecretKeyRing? { - return PGPainless.readKeyRing().secretKeyRing(loadResourceAsString("keys/$keyFile")) - } - } - - @Test - fun multipleDecryptionTest() { - val keys = listOf( - "decrypt - without a subject", - "decrypt - [enigmail] encrypted iso-2022-jp pgp-mime", - "decrypt - [enigmail] encrypted iso-2022-jp, plain text", - "decrypt - [gpg] signed fully armored message" - ) - for (key in keys) { - println("Decrypt: '$key'") - val r = processMessage(key) - assertTrue("Message not returned", r.content != null) - val messageInfo = findMessage(key) - checkContent( - expected = messageInfo.content, - actual = r.content!!.toByteArray(), - charset = messageInfo.charset - ) - } - } - - @Test - fun missingMdcTest() { - val r = processMessage("decrypt - [security] mdc - missing - error") - assertTrue("Message is returned when should not", r.content == null) - assertTrue("Error not returned", r.exception != null) - assertTrue( - "Missing MDC not detected", - r.exception?.decryptionErrorType == PgpDecryptAndOrVerify.DecryptionErrorType.NO_MDC - ) - } - - @Test - fun badMdcTest() { - val r = processMessage("decrypt - [security] mdc - modification detected - error") - assertTrue("Message is returned when should not", r.content == null) - assertTrue("Error not returned", r.exception != null) - assertTrue( - "Bad MDC not detected", - r.exception?.decryptionErrorType == PgpDecryptAndOrVerify.DecryptionErrorType.BAD_MDC - ) - } - - @Test - fun decryptionTest3() { - val r = processMessage( - "decrypt - [everdesk] message encrypted for sub but claims encryptedFor-primary,sub" - ) - assertTrue("Message not returned", r.content != null) - assertTrue("Error returned", r.exception == null) - // TODO: Should there be any error? - // https://github.com/FlowCrypt/flowcrypt-android/issues/1214 - } - - @Test - fun missingArmorChecksumTest() { - // This is a test for the message with missing armor checksum - different from MDC. - // Usually the four digits at the and like p3Fc=. - // Such messages are still valid if this is missing, - // and should decrypt correctly - so it's good as is. - val r = processMessage("decrypt - encrypted missing checksum") - assertTrue("Message not returned", r.content != null) - assertTrue("Error returned", r.exception == null) - } - - @Test - fun wrongArmorChecksumTest() { - val r = processMessage("decrypt - issue 1347 - wrong checksum") - assertTrue("Error not returned", r.exception != null) - assertEquals(PgpDecryptAndOrVerify.DecryptionErrorType.FORMAT, r.exception?.decryptionErrorType) - } - - @Test - fun wrongPassphraseTest() { - val messageInfo = findMessage("decrypt - without a subject") - val wrongPassphrase = Passphrase.fromPassword("this is wrong passphrase for sure") - val privateKeysWithWrongPassPhrases = PRIVATE_KEYS.map { - TestKeys.KeyWithPassPhrase(keyRing = it.keyRing, passphrase = wrongPassphrase) - } - val r = PgpDecryptAndOrVerify.decryptAndOrVerifyWithResult( - srcInputStream = messageInfo.armored.byteInputStream(), - publicKeys = PGPPublicKeyRingCollection(emptyList()), - secretKeys = PGPSecretKeyRingCollection(privateKeysWithWrongPassPhrases.map { it.keyRing }), - protector = SecretKeyRingProtector.unprotectedKeys() - ) - assertTrue("Message returned", r.content == null) - assertTrue("Error not returned", r.exception != null) - assertTrue( - "Wrong passphrase not detected", - r.exception?.decryptionErrorType == PgpDecryptAndOrVerify.DecryptionErrorType.WRONG_PASSPHRASE - ) - } - - @Test - fun missingPassphraseTest() { - val messageInfo = findMessage("decrypt - without a subject") - val r = PgpDecryptAndOrVerify.decryptAndOrVerifyWithResult( - srcInputStream = messageInfo.armored.byteInputStream(), - publicKeys = PGPPublicKeyRingCollection(emptyList()), - secretKeys = PGPSecretKeyRingCollection(PRIVATE_KEYS.map { it.keyRing }), - protector = SecretKeyRingProtector.unprotectedKeys() - ) - assertTrue("Message returned", r.content == null) - assertTrue("Error returned", r.exception != null) - assertTrue( - "Missing passphrase not detected", - r.exception?.decryptionErrorType == PgpDecryptAndOrVerify.DecryptionErrorType.WRONG_PASSPHRASE - ) - } - - @Test - fun wrongKeyTest() { - val messageInfo = findMessage("decrypt - without a subject") - val wrongKey = listOf(PRIVATE_KEYS[1]) - val r = PgpDecryptAndOrVerify.decryptAndOrVerifyWithResult( - messageInfo.armored.byteInputStream(), - publicKeys = PGPPublicKeyRingCollection(emptyList()), - secretKeys = PGPSecretKeyRingCollection(wrongKey.map { it.keyRing }), - protector = SecretKeyRingProtector.unprotectedKeys() - ) - assertTrue("Message returned", r.content == null) - assertTrue("Error not returned", r.exception != null) - assertTrue( - "Key mismatch not detected", - r.exception?.decryptionErrorType == PgpDecryptAndOrVerify.DecryptionErrorType.KEY_MISMATCH - ) - } - - // ------------------------------------------------------------------------------------------- - - // Use this one for debugging - @Test - fun singleDecryptionTest() { - val key = "decrypt - [enigmail] encrypted iso-2022-jp, plain text" - val r = processMessage(key) - assertTrue("Message not returned", r.content != null) - val messageInfo = findMessage(key) - checkContent( - expected = messageInfo.content, - actual = r.content!!.toByteArray(), - charset = messageInfo.charset - ) - } - - private fun processMessage(messageKey: String): PgpDecryptAndOrVerify.DecryptionResult { - val messageInfo = findMessage(messageKey) - val result = PgpDecryptAndOrVerify.decryptAndOrVerifyWithResult( - messageInfo.armored.byteInputStream(), - publicKeys = PGPPublicKeyRingCollection(emptyList()), - secretKeys = PGPSecretKeyRingCollection(PRIVATE_KEYS.map { it.keyRing }), - protector = secretKeyRingProtector - ) - if (result.content != null) { - val s = String(result.content!!.toByteArray(), Charset.forName(messageInfo.charset)) - println("=========\n$s\n=========") - } - return result - } - - private fun checkContent(expected: List, actual: ByteArray, charset: String) { - val z = String(actual, Charset.forName(charset)) - for (s in expected) { - assertTrue("Text '$s' not found", z.indexOf(s) != -1) - } - } - - // ------------------------------------------------------------------------------------------- - - @Test - @Ignore("Should be reworked after switching to a new logic that uses RawBlockParser") - fun multipleComplexMessagesTest() { - val testFiles = listOf( - "decrypt - [enigmail] basic html-0.json", - "decrypt - [gnupg v2] thai text-0.json", - "decrypt - [gnupg v2] thai text in html-0.json", - "decrypt - [gpgmail] signed message will get parsed and rendered " + - "(though verification fails, enigmail does the same)-0.json", - "decrypt - protonmail - auto TOFU load matching pubkey first time-0.json", - "decrypt - protonmail - load pubkey into contact + verify detached msg-0.json", - "decrypt - protonmail - load pubkey into contact + verify detached msg-1.json", - "decrypt - protonmail - load pubkey into contact + verify detached msg-2.json", - "decrypt - [symantec] base64 german umlauts-0.json", - "decrypt - [thunderbird] unicode chinese-0.json", - "decrypt - verify encrypted+signed message-0.json", - "verify - Kraken - urldecode signature-0.json" - ) - - for (testFile in testFiles) { - checkComplexMessage(testFile) - } - } - - // Use this one for debugging - @Test - @Ignore("Should be reworked after switching to a new logic that uses RawBlockParser") - fun singleComplexMessageTest() { - val testFile = "verify - Kraken - urldecode signature-0.json" - checkComplexMessage(testFile) - } - - private fun checkComplexMessage(fileName: String) { - println("\n*** Processing '$fileName'") - val json = loadResourceAsString("complex_messages/$fileName") - val rootObject = JsonParser.parseString(json).asJsonObject - val inputMsg = Base64.getDecoder().decode(rootObject["in"].asJsonObject["mimeMsg"].asString) - val out = rootObject["out"].asJsonObject - val expectedBlocks = out["blocks"].asJsonArray - val session = Session.getInstance(Properties()) - val mimeMessage = MimeMessage(session, inputMsg.inputStream()) - val extractedBlocks = PgpMsg.extractMsgBlocksFromPart( - mimeMessage, - PGPPublicKeyRingCollection(emptyList()), - PGPSecretKeyRingCollection(emptyList()), - UnprotectedKeysProtector() - ).toList() - - assertEquals(expectedBlocks.size(), extractedBlocks.size) - - for (i in extractedBlocks.indices) { - println("Checking block #$i of the '$fileName'") - - val expectedBlock = expectedBlocks[i].asJsonObject - val expectedBlockType = MsgBlock.Type.ofSerializedName(expectedBlock["type"].asString) - val expectedContent = - if (expectedBlockType == MsgBlock.Type.DECRYPTED_AND_OR_SIGNED_CONTENT) { - expectedBlock["blocks"].asJsonArray.first().asJsonObject["content"].asString.normalizeEol() - } else { - expectedBlock["content"].asString.normalizeEol() - } - val actualBlock = extractedBlocks[i] - val actualContent = if (actualBlock is DecryptedAndOrSignedContentMsgBlock) { - (actualBlock.blocks.first().content ?: "").normalizeEol().removeUtf8Bom() - } else { - (actualBlock.content ?: "").normalizeEol().removeUtf8Bom() - } - - assertEquals(expectedBlockType, actualBlock.type) - assertEquals(expectedContent, actualContent) - } - } - - // ------------------------------------------------------------------------------------------- - - @Test - fun testParseDecryptMsgUnescapedSpecialCharactersInTextOriginallyTextPlain() { - val mimeText = "MIME-Version: 1.0\n" + - "Date: Fri, 6 Sep 2019 10:48:25 +0000\n" + - "Message-ID: \n" + - "Subject: plain text with special chars\n" + - "From: Human at FlowCrypt \n" + - "To: FlowCrypt Compatibility \n" + - "Content-Type: text/plain; charset=\"UTF-8\"\n" + - "\n" + TEXT_SPECIAL_CHARS - val keys = TestKeys.KEYS["rsa1"]!!.listOfKeysWithPassPhrase - val result = PgpMsg.processMimeMessage( - msg = MimeUtils.mimeTextToMimeMessage(mimeText), - secretKeys = PGPSecretKeyRingCollection(keys.map { it.keyRing }), - protector = SecretKeyRingProtector.unprotectedKeys() - ) - assertEquals(TEXT_SPECIAL_CHARS, result.text) - assertEquals(false, result.verificationResult.hasEncryptedParts) - assertEquals(1, result.blocks.size) - val block = result.blocks[0] - assertEquals(MsgBlock.Type.PLAIN_HTML, block.type) - - checkRenderedBlock( - block, - listOf(RenderedBlock.normal(true, "PLAIN", HTML_SPECIAL_CHARS)) - ) - } - - @Test - fun testParseDecryptMsgUnescapedSpecialCharactersInTextOriginallyTextHtml() { - val mimeText = "MIME-Version: 1.0\n" + - "Date: Fri, 6 Sep 2019 10:48:25 +0000\n" + - "Message-ID: \n" + - "Subject: plain text with special chars\n" + - "From: Human at FlowCrypt \n" + - "To: FlowCrypt Compatibility \n" + - "Content-Type: text/html; charset=\"UTF-8\"\n" + - "\n" + HTML_SPECIAL_CHARS - val keys = TestKeys.KEYS["rsa1"]!!.listOfKeysWithPassPhrase - val result = PgpMsg.processMimeMessage( - msg = MimeUtils.mimeTextToMimeMessage(mimeText), - secretKeys = PGPSecretKeyRingCollection(keys.map { it.keyRing }), - protector = SecretKeyRingProtector.unprotectedKeys() - ) - assertEquals(TEXT_SPECIAL_CHARS, result.text) - assertEquals(false, result.verificationResult.hasEncryptedParts) - assertEquals(1, result.blocks.size) - val block = result.blocks[0] - assertEquals(MsgBlock.Type.PLAIN_HTML, block.type) - - checkRenderedBlock( - block, - listOf(RenderedBlock.normal(true, "PLAIN", HTML_SPECIAL_CHARS)) - ) - } - - @Test - fun testParseDecryptMsgUnescapedSpecialCharactersInEncryptedPgpMime() { - val text = loadResourceAsString("compat/direct-encrypted-pgpmime-special-chars.txt") - val keys = TestKeys.KEYS["rsa1"]!!.listOfKeysWithPassPhrase - val decryptResult = PgpDecryptAndOrVerify.decryptAndOrVerifyWithResult( - srcInputStream = text.toInputStream(), - publicKeys = PGPPublicKeyRingCollection(emptyList()), - secretKeys = PGPSecretKeyRingCollection(keys.map { it.keyRing }), - protector = TestKeys.genRingProtector(keys) - ) - assertEquals(true, decryptResult.isEncrypted) - val result = PgpMsg.processMimeMessage( - decryptResult.content?.toByteArray()?.inputStream()!!, - secretKeys = PGPSecretKeyRingCollection(keys.map { it.keyRing }), - protector = TestKeys.genRingProtector(keys) - ) - assertEquals(TEXT_SPECIAL_CHARS, result.text) - assertEquals(1, result.blocks.size) - val block = result.blocks[0] - assertEquals(MsgBlock.Type.PLAIN_HTML, block.type) - - checkRenderedBlock( - block, - listOf(RenderedBlock.normal(true, "PLAIN", HTML_SPECIAL_CHARS)) - ) - } - - @Test - fun testDecryptMsgUnescapedSpecialCharactersInEncryptedText() { - val text = loadResourceAsString("compat/direct-encrypted-text-special-chars.txt") - val keys = TestKeys.KEYS["rsa1"]!!.listOfKeysWithPassPhrase - - val decryptResult = PgpDecryptAndOrVerify.decryptAndOrVerifyWithResult( - text.toInputStream(), - publicKeys = PGPPublicKeyRingCollection(emptyList()), - secretKeys = PGPSecretKeyRingCollection(keys.map { it.keyRing }), - protector = TestKeys.genRingProtector(keys) - ) - assertEquals(true, decryptResult.isEncrypted) - assertEquals(TEXT_SPECIAL_CHARS, String(decryptResult.content?.toByteArray() ?: byteArrayOf())) - } - - @Test - fun testParseDecryptMsgPlainInlineImage() { - val text = loadResourceAsString("other/plain-inline-image.txt") - val keys = TestKeys.KEYS["rsa1"]!!.listOfKeysWithPassPhrase - val result = PgpMsg.processMimeMessage( - text.toInputStream(), - secretKeys = PGPSecretKeyRingCollection(keys.map { it.keyRing }), - protector = SecretKeyRingProtector.unprotectedKeys() - ) - assertEquals("Below\n[image: image.png]\nAbove", result.text) - assertEquals(false, result.verificationResult.hasEncryptedParts) - assertEquals(2, result.blocks.size) - val block = result.blocks[0] - assertEquals(MsgBlock.Type.PLAIN_HTML, block.type) - val htmlContent = loadResourceAsString("other/plain-inline-image-html-content.txt") - - checkRenderedBlock(block, listOf(RenderedBlock.normal(true, "PLAIN", htmlContent))) - } - - @Test - fun testExtractClearTextFromMsgSignedMessagePreserveNewlines() { - val text = loadResourceAsString("other/signed-message-preserve-newlines.txt") - val blocks = RawBlockParser.detectBlocks(text).toList() - val clearText = PgpSignature.extractClearText(text) - assertEquals( - "Standard message\n\nsigned inline\n\nshould easily verify\nThis is email footer", - clearText - ) - assertEquals(1, blocks.size) - assertEquals(RawBlockParser.RawBlockType.PGP_CLEARSIGN_MSG, blocks[0].type) - } - - @Test - fun testParseDecryptPlainGoogleSecurityAlertMessage() { - val text = loadResourceAsString("other/plain-google-security-alert-20210416-084836-UTC.txt") - val keys = TestKeys.KEYS["rsa1"]!!.listOfKeysWithPassPhrase - val result = PgpMsg.processMimeMessage( - text.toInputStream(), - secretKeys = PGPSecretKeyRingCollection(keys.map { it.keyRing }), - protector = SecretKeyRingProtector.unprotectedKeys() - ) - val textContent = loadResourceAsString( - "other/plain-google-security-alert-20210416-084836-UTC-text-content.txt" - ) - assertEquals(textContent.replace("\n", "\r\n"), result.text) - assertEquals(false, result.verificationResult.hasEncryptedParts) - assertEquals(1, result.blocks.size) - val block = result.blocks[0] - assertEquals(MsgBlock.Type.PLAIN_HTML, block.type) - val htmlContent = loadResourceAsString( - "other/plain-google-security-alert-20210416-084836-UTC-html-content.txt" - ) - - checkRenderedBlock(block, listOf(RenderedBlock.normal(true, "PLAIN", htmlContent))) - } - - @Test - fun testGmailQuotesParsingAndHtmlManipulation() { - val mimeMessageRaw = """ - To: default@flowcrypt.test - From: denbond7@flowcrypt.test - Subject: test - Date: Sun, 17 Mar 2019 11:46:37 +0000 - Message-Id: <1552823197874-dd5800d9-54ca1a01-c548da66@flowcrypt.test> - MIME-Version: 1.0 - Content-Type: multipart/alternative; boundary="000000000000a02340062c4d860a" - - --000000000000a02340062c4d860a - Content-Type: text/plain; charset="UTF-8" - Content-Transfer-Encoding: quoted-printable - - Your Android devices are always getting better thanks to new features and - updates rolling out all the time - - On Wed, Jan 22, 2025 at 5:55=E2=80=AFPM Den wrote: - - > Today, Android 15 starts rolling out to Pixel devices. These updates - > include security features that help keep your sensitive health, financial - > and personal information protected from theft and fraud. - > - > -- - > Regards, - > Den - > - - - --=20 - Regards, - Den - - --000000000000a02340062c4d860a - Content-Type: text/html; charset="UTF-8" - Content-Transfer-Encoding: quoted-printable - -
Your Android devices = - are always getting better thanks to new features and updates rolling out al= - l the time

On Wed, Jan 22, 2025 at 5:55 PM Den= - <denbond7@flowcrypt.test= - > wrote:
Today, Android 15 = - starts rolling out to Pixel devices. These updates include security feature= - s that help keep your sensitive health, financial and personal information = - protected from theft and fraud.

--
Regards,
Den

= - --
= -
Regards,
Den
- - --000000000000a02340062c4d860a-- - """.trimIndent() - - val processedMimeMessageResult = runBlocking { - PgpMsg.processMimeMessage( - MimeMessage(Session.getInstance(Properties()), mimeMessageRaw.toInputStream()), - PGPPublicKeyRingCollection(listOf()), - PGPSecretKeyRingCollection(listOf()), - SecretKeyRingProtector.unprotectedKeys(), - ) - } - - assertEquals(1, processedMimeMessageResult.blocks.size) - - val plainHtmlBlock = processedMimeMessageResult.blocks.first { - it.type == MsgBlock.Type.PLAIN_HTML - } - - val document = Jsoup.parse(requireNotNull(plainHtmlBlock.content), "", Parser.xmlParser()) - assertNotNull(document.select("details").first()) - assertEquals(1, document.select("details").size) - assertNotNull(document.select("summary").first()) - assertEquals(1, document.select("summary").size) - - //check that plain text was generated correctly and quotes for reply will be correct - val documentForPlainVersion = Jsoup.parse( - requireNotNull(PgpMsg.checkAndReturnQuotesFormatIfFound(processedMimeMessageResult.text)), - "", - Parser.xmlParser() - ) - - val quotesForPlainVersion = documentForPlainVersion.select("blockquote") - assertEquals(1, quotesForPlainVersion.size) - assertTrue(quotesForPlainVersion[0].text().startsWith("Today, Android 15")) - } - - @Test - fun testQuotesParsingAndHtmlManipulationForPlainMode() { - val mimeMessageRaw = """ - MIME-Version: 1.0 - Date: Mon, 24 Feb 2025 17:02:47 +0200 - Message-ID: - Subject: Re: Quotes for plain text - From: Den at FlowCrypt - To: DenBond7 - Content-Type: text/plain; charset="UTF-8" - Content-Transfer-Encoding: quoted-printable - - reply 2 - - The top-level build.gradle.kts file (for the Kotlin DSL) or - build.gradle file (for the Groovy DSL) is located in the root project - directory. It typically defines the common versions of plugins used by - modules in your project. - - The following code sample describes the default settings and DSL - elements in the top-level build script after creating a new project - - On Mon, Feb 24, 2025 at 5:02=E2=80=AFPM DenBond7 wrote: - > - > 2 - > - > Creating custom build configurations requires you to make changes to - > one or more build configuration files. These plain-text files use a - > domain-specific language (DSL) to describe and manipulate the build - > logic using Kotlin script, which is a flavor of the Kotlin language. - > You can also use Groovy, which is a dynamic language for the Java - > Virtual Machine (JVM), to configure your builds. - > - > You don't need to know Kotlin script or Groovy to start configuring - > your build because the Android Gradle plugin introduces most of the - > DSL elements you need. To learn more about the Android Gradle plugin - > DSL, read the DSL reference documentation. Kotlin script also relies - > on the underlying Gradle Kotlin DSL - > - > When starting a new project, Android Studio automatically creates some - > of these files for you and populates them based on sensible defaults. - > For an overview of the created files, see Android build structure. - > - > =D0=BF=D0=BD, 24 =D0=BB=D1=8E=D1=82. 2025=E2=80=AF=D1=80. =D0=BE 17:01 De= - n at FlowCrypt =D0=BF=D0=B8=D1=88=D0=B5: - > > - > > reply 1 - > > - > > Build types define certain properties that Gradle uses when building - > > and packaging your app. Build types are typically configured for - > > different stages of your development lifecycle. - > > - > > For example, the debug build type enables debug options and signs the - > > app with the debug key, while the release build type may shrink, - > > obfuscate, and sign your app with a release key for distribution. - > > - > > You must define at least one build type to build your app. Android - > > Studio creates the debug and release build types by default. To start - > > customizing packaging settings for your app, learn how to configure - > > build types. - > > - > > - > > On Mon, Feb 24, 2025 at 5:00=E2=80=AFPM DenBond7 wrote: - > > > - > > > 1 - > > > - > > > The Android build system compiles app resources and source code and - > > > packages them into APKs or Android App Bundles that you can test, - > > > deploy, sign, and distribute. - > > > - > > > In Gradle build overview and Android build structure, we discussed - > > > build concepts and the structure of an Android app. Now it's time to - > > > configure the build. - > > > - > > > - > > > -- - > > > Regards, - > > > Denys Bondarenko - > > - > > - > > - > > -- - > > Regards, - > > Den at FlowCrypt - > - > - > - > -- - > Regards, - > Denys Bondarenko - - - - --=20 - Regards, - Den at FlowCrypt""".trimIndent() - - val processedMimeMessageResult = runBlocking { - PgpMsg.processMimeMessage( - MimeMessage(Session.getInstance(Properties()), mimeMessageRaw.toInputStream()), - PGPPublicKeyRingCollection(listOf()), - PGPSecretKeyRingCollection(listOf()), - SecretKeyRingProtector.unprotectedKeys(), - ) - } - - assertEquals(1, processedMimeMessageResult.blocks.size) - - val plainHtmlBlock = processedMimeMessageResult.blocks.first { - it.type == MsgBlock.Type.PLAIN_HTML - } - - val document = Jsoup.parse(requireNotNull(plainHtmlBlock.content), "", Parser.xmlParser()) - assertNotNull(document.select("details").first()) - assertEquals(1, document.select("details").size) - assertNotNull(document.select("summary").first()) - assertEquals(1, document.select("summary").size) - - val quotes = document.select("blockquote") - assertEquals(3, quotes.size) - assertTrue(quotes[0].text().startsWith("2 Creating custom build configurations requires")) - assertTrue(quotes[1].text().startsWith("reply 1")) - assertTrue(quotes[2].text().startsWith("1 The Android build system")) - - //check that plain text was generated correctly and quotes for reply will be correct - val documentForPlainVersion = Jsoup.parse( - requireNotNull(PgpMsg.checkAndReturnQuotesFormatIfFound(processedMimeMessageResult.text)), - "", - Parser.xmlParser() - ) - - val quotesForPlainVersion = documentForPlainVersion.select("blockquote") - assertEquals(3, quotesForPlainVersion.size) - assertTrue(quotes[0].text().startsWith("2 Creating custom build configurations requires")) - assertTrue(quotes[1].text().startsWith("reply 1")) - assertTrue(quotes[2].text().startsWith("1 The Android build system")) - } - - @Test - fun testQuotesParsingAndHtmlManipulationForEncryptedMessages() { - val mimeMessageRaw = """ - Date: Tue, 4 Mar 2025 15:14:00 +0200 (GMT+02:00) - From: default@flowcrypt.test - To: default@flowcrypt.test - Message-ID: <255254603.1.1741101240147@flowcrypt.test> - Subject: TEst - Mime-Version: 1.0 - Content-Type: multipart/mixed; - boundary="----=_Part_0_124883294.1741101240093" - - ------=_Part_0_124883294.1741101240093 - Content-Type: text/plain; charset=us-ascii - Content-Transfer-Encoding: 7bit - - -----BEGIN PGP MESSAGE----- - Version: PGPainless - - wV4DTxRYvSK3u1MSAQdA0rQBDv6Qe3gj8IWoEkn0r6W7+Uz/zyz0YI6DCLA2h3Ew - bM+5OX93DKqTkaLWbV0VcuN4ACPOb+4nyWIhb/lQq468FO7y2rqMFah0LcTJfTWr - wV4DTxRYvSK3u1MSAQdA/iyqemW8rY+ka18ANSph7TD8u3INnwT6Wt5BhcHzOTQw - hPcE7cge7naa351khAsVRMgXZS4jzxR0WQw3E/truKHcprpCaQis0dgsiTxURTm+ - 0sCWAdMfNsQa1GUZVredhzYCVRg6iiieU1k42vhwxYe8GMO/s+aWQgwR2EhenqKz - 0utgBCc1uP0o0fzIKdyirdCmlrSwk1yAhiU1/mR0p7Yl29vDdhRWjz7UEV1nfbwg - 1CAh7028G0YTFItwnPCCYDYYP1R0pPSffnuVV/BLuEfk0JiXUaa1ar73v5Sepfkm - RKWX0PuM0IIY2+d78pCfr4puci2qwMGDX9hdTvp+qk+QolpFnVP7YUlDZTu5N8Q7 - GYrz0YJBXCvrkCNbu5UcssTRNyYyHB7sDT+XD+kE3uKcMat1cl190Gm6WFYB5I8x - H3jhdxj8HyrazAyI3Lra/rtLIRg0zFbQ8Q7nb96gAzi3KU0gTdXPJzbMetlwF0cT - Q027sUsEQ4L3PBVFNy6SYRRsQiS3o3CVaRkRlOEqsl+ix2S8bASh9bIl7Ode8+jz - l8r4umM+zyWe - =HJxV - -----END PGP MESSAGE----- - - ------=_Part_0_124883294.1741101240093-- - """.trimIndent() - - val privateKey = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + - "Version: PGPainless\n" + - "\n" + - "lIYEYIq7phYJKwYBBAHaRw8BAQdAat45rrh+gvQwWwJw5eScq3Pdxt/8d+lWNVSm\n" + - "kImXcRP+CQMCvWfx3mzDdd5g6c59LcPqADK0p70/7ZmTkp3ZC1YViTprg4tQt/PF\n" + - "QJL+VPCG+BF9bWyFcfxKe+KAnXRTWml5O6xrv6ZkiNmAxoYyO1shzLQWZGVmYXVs\n" + - "dEBmbG93Y3J5cHQudGVzdIh4BBMWCgAgBQJgirumAhsDBRYCAwEABAsJCAcFFQoJ\n" + - "CAsCHgECGQEACgkQIl+AI8INCVcysgD/cu23M07rImuV5gIl98uOnSIR+QnHUD/M\n" + - "I34b7iY/iTQBALMIsqO1PwYl2qKwmXb5lSoMj5SmnzRRE2RwAFW3AiMCnIsEYIq7\n" + - "phIKKwYBBAGXVQEFAQEHQA8q7iPr+0OXqBGBSAL6WNDjzHuBsG7uiu5w8l/A6v8l\n" + - "AwEIB/4JAwK9Z/HebMN13mCOF6Wy/9oZK4d0DW9cNLuQDeRVZejxT8oFMm7G8iGw\n" + - "CGNjIWWcQSvctBZtHwgcMeplCW7tmzkD3Nq/ty50lCwQQd6gZSXMiHUEGBYKAB0F\n" + - "AmCKu6YCGwwFFgIDAQAECwkIBwUVCgkICwIeAQAKCRAiX4Ajwg0JV+sbAQCv4LVM\n" + - "0+AN54ivWa4vPRyYOfSQ1FqsipkYLJce+xwUeAD+LZpEVCypFtGWQVdeSJVxIHx3\n" + - "k40IfHsK0fGgR+NrRAw=\n" + - "=osuI\n" + - "-----END PGP PRIVATE KEY BLOCK-----" - - val secretKeyRing = PgpKey.extractSecretKeyRing(privateKey) - val processedMimeMessageResult = runBlocking { - PgpMsg.processMimeMessage( - MimeMessage(Session.getInstance(Properties()), mimeMessageRaw.toInputStream()), - PGPPublicKeyRingCollection(listOf()), - PGPSecretKeyRingCollection(listOf(secretKeyRing)), - SecretKeyRingProtector.unlockAnyKeyWith(Passphrase.fromPassword("android")), - ) - } - - assertEquals(1, processedMimeMessageResult.blocks.size) - - val plainHtmlBlock = processedMimeMessageResult.blocks.first { - it.type == MsgBlock.Type.PLAIN_HTML - } - - val document = Jsoup.parse(requireNotNull(plainHtmlBlock.content), "", Parser.xmlParser()) - assertNotNull(document.select("details").first()) - assertEquals(1, document.select("details").size) - assertNotNull(document.select("summary").first()) - assertEquals(1, document.select("summary").size) - - val quotes = document.select("blockquote") - assertEquals(3, quotes.size) - assertTrue(quotes[0].text().startsWith("Sender 2")) - assertTrue(quotes[1].text().startsWith("Reply 1")) - assertTrue(quotes[2].text().startsWith("Sender 1")) - - //check that plain text was generated correctly and quotes for reply will be correct - val documentForPlainVersion = Jsoup.parse( - requireNotNull(PgpMsg.checkAndReturnQuotesFormatIfFound(processedMimeMessageResult.text)), - "", - Parser.xmlParser() - ) - - val quotesForPlainVersion = documentForPlainVersion.select("blockquote") - assertEquals(3, quotesForPlainVersion.size) - assertTrue(quotesForPlainVersion[0].text().startsWith("Sender 2")) - assertTrue(quotesForPlainVersion[1].text().startsWith("Reply 1")) - assertTrue(quotesForPlainVersion[2].text().startsWith("Sender 1")) - } - - private data class RenderedBlock( - val rendered: Boolean, - val frameColor: String?, - val htmlContent: String?, - val content: String?, - val error: String? - ) { - companion object { - fun normal(rendered: Boolean, frameColor: String?, htmlContent: String?): RenderedBlock { - return RenderedBlock( - rendered = rendered, - frameColor = frameColor, - htmlContent = htmlContent, - content = null, - error = null - ) - } - - fun error(error: String, content: String): RenderedBlock { - return RenderedBlock( - rendered = false, - frameColor = null, - htmlContent = null, - content = content, - error = error - ) - } - } - } - - private fun checkRenderedBlock(block: MsgBlock, expectedRenderedBlocks: List) { - val parts = block.content!!.split(BODY_SPLIT_REGEX, 3) - val head = parts[0] - assertTrue(head.contains("")) - assertTrue(head.contains("\n \n \n \n \n \n \n \n \n \n\n\n\n
\n \n
\n \n \n \n \n \n \n
\n \n
\n \n \n \n \n \n \n
\n \n \n \n \n
\n \n \n \n \n \n \n
\n \n \"Kraken\"\n \n
\n
\n
\n
\n \n
\n
\n \n
\n \n \n \n \n \n \n
\n \n
\n \n \n \n \n \n \n
\n \n \n \n \n
\n
\n

Hi Richard,

\n


\n

Kraken clients can now begin converting popular currencies more efficiently with the addition of 11 new trading pairs.

\n


\n

This includes expanded options for converting ether (ETH), USDT and Pound Sterling (GBP), with offerings designed to bring more functionality, while enabling clients to avoid added fees.

\n


\n

For example, converting Bitcoin Cash (BCH) to ETH on Kraken previously required two distinct trades. With our new BCH/ETH trading pair, this conversion can be done seamlessly, allowing clients to simply sell BCH directly for ETH.

\n


\n

It’s the latest example of how we continue to strive to make Kraken easier and more convenient for active traders and newer clients alike. 

\n


\n

With the news, Kraken’s total number of trading pairs grows to 155+, a figure that includes our launch of more traditional FX trading pairs just six weeks ago.

\n


\n

What are the new trading pairs?

\n


\n

Ether Pairs

\n


\n
    \n
  • BCH/ETH
  • \n
  • LTC/ETH
  • \n
  • XRP/ETH
  • \n
\n


\n

Pound Sterling Pairs

\n


\n
    \n
  • BCH/GBP
  • \n
  • LTC/GBP
  • \n
  • XRP/GBP
  • \n
\n


\n

Tether Pairs

\n


\n
    \n
  • BCH/USDT
  • \n
  • LTC/USDT
  • \n
  • XRP/USDT
  • \n
  • USDT/JPY
  • \n
  • USDT/CHF
  • \n
\n


\n

For more details such as trading minimums and fees, please see our blog post.

\n


\n

Thank you for choosing Kraken, the trusted and secure digital asset exchange.

\n


\n

The Kraken Team

\n
\n
\n
\n
\n \n
\n
\n \n
\n \n \n \n \n \n \n
\n \n
\n \n \n \n \n \n \n
\n \n \n \n \n
\n
\n

P.S. Download the Kraken Pro app from the App Store or Google Play:

\n
\n
\n
\n
\n \n
\n
\n \n
\n \n \n \n \n \n \n
\n \n
\n \n \n \n \n \n \n
\n \n \n \n \n
\n
\n
\"download \"download
\n
\n
\n
\n
\n \n
\n
\n \n
\n \n \n \n \n \n \n
\n \n
\n \n \n \n \n \n \n
\n \n \n \n \n
\n

\n

\n \n
\n
\n
\n \n
\n
\n \n
\n \n \n \n \n \n \n
\n \n
\n \n \n \n \n \n \n
\n \n \n \n \n
\n
\n \n \n \n \n \n \n
\n
Spread the word
\n
\n \n \n \n \n \n \n
\n \n \n \n \n \n
\n
\n \n \n \n \n \n \n
\n \n \n \n \n \n
\n
\n
\n
\n
\n
\n \n
\n
\n \n
\n \n \n \n \n \n \n
\n \n
\n \n \n \n \n \n \n
\n \n \n \n \n
\n
This communication is a commercial message and its contents are intended for the recipient only and may contain confidential, non-public and/or privileged information. If you have received this communication in error, do not read, duplicate or distribute. Kraken does not make recommendations on the suitability of a particular asset class, strategy, or course of action. Any investment decision you make is solely your responsibility. Please consider your individual position and financial goals before making an independent investment decision. Distributed by: Kraken.com, 237 Kearny St #102, San Francisco, CA 94108.
\n
\n
\n
\n \n
\n
\n \n
\n \n \n \n \n \n \n
\n \n
\n \n \n \n \n \n \n
\n \n \n \n \n
\n
This email contains important updates from Kraken. Unsubscribe
\n
\n
\n
\n \n
\n
\n \n
\n\n\n", - "complete": true, - "signature": "-----BEGIN PGP SIGNATURE-----\n\nwsFcBAABCAAQBQJerDKHCRCjgEL2B9Yj2gAAMKIQAIn3kIuaEmgqFB1tMoOkbk6s\nCHCsS6CIbWef81Yz0scqjtTq6120YAdndkgckNB5zFcyQCClVM0t+yZTth2n2Lzh\n5VY9Ie3QNLoJ7tsPRQJpc2KbpESgvO7GHE5Y/67Cn6LIpdBuGm3E8YQSpQbE4By6\nCOCtWC//zK+4ZwjKqvl0+Po1kApY1pnoVQEdKI+vLmwuYW4b0gbsZk7PQup65Pw4\nL5cG90EMEGMNtJ/3qpUMjIx9s0frG69Zauf1N1+glj4pxH/3HyP4ovxE5RkvSc7V\nsl7KNeHjo1LBPr5oPqxHtoXx0+c0bj9T4BwitJHmBJWUDWETPlFu8wOGGwYfPa7I\nLF4YXLS4yMobnBZUubMwlUVMcdUE98J4Nmrdz8RL9gilRyOEVUuulapelxVJaMg8\ndqXcXG9nlwOcAvDGGkYMQlqMoAgA/Jq8IJaJ5yyvlHEhBDGHfqN2SwjXIImti/A6\nIMKLZiaVRAym61d4rk7av0ejTAptZQdrZXT8bBzIw2qleU4EYPXccHGYpO8AkIhd\nNbcDyEpnamuA4UHn1CTLWVOmJzHZrskxe4cxy/ZbJWhdxeRqDgvvNWB0m8B6XdEj\noW7kpCjdRTTmUY0zP7+6W0htgEQEFDY3G31CoTHIR0XBddCaTSYVk5i1mqvBsSgo\nDbLYaN73VsaW3OUQxyO5\n=3DvUPp\n-----END PGP SIGNATURE-----" - } - ], - "from": "noreply@email2.kraken.com", - "to": [ - "Richardwjepson@gmail.com" - ], - "rawSignedContent": "Content-Type: multipart/alternative; boundary=\"D0A9EA10D1AD99A6B8A85679BE81EA93\"\r\n\r\nThis is multipart MIME message\r\n--D0A9EA10D1AD99A6B8A85679BE81EA93\r\nContent-Transfer-Encoding: quoted-printable\r\nContent-Type: text/plain; charset=utf-8\r\n\r\n Kraken [https://img.kraken.com/e/kraken-logo.png]\r\n[https://www.kraken.com/?kec=3DtGGTabsAAukVFU&utm_source=3Dproduct+anouncem=\r\nent&utm_medium=3Demail&utm_campaign=3Dadditional+pairs+announcement&utm_con=\r\ntent=3Dlogo]\r\n Hi Richard,\r\n\r\n\r\n\r\n\r\nKraken clients can now begin converting popular currencies more efficiently=\r\n with\r\nthe addition of 11 new trading pairs.\r\n\r\n\r\n\r\n\r\nThis includes expanded options for converting ether (ETH), USDT and Pound\r\nSterling (GBP), with offerings designed to bring more functionality, while\r\nenabling clients to avoid added fees.\r\n\r\n\r\n\r\n\r\nFor example, converting Bitcoin Cash (BCH) to ETH on Kraken previously requ=\r\nired\r\ntwo distinct trades. With our new BCH/ETH trading pair, this conversion can=\r\n be\r\ndone seamlessly, allowing clients to simply sell BCH directly for ETH.\r\n\r\n\r\n\r\n\r\nIt=E2=80=99s the latest example of how we continue to strive to make Kraken=\r\n easier and\r\nmore convenient for active traders and newer clients alike.\r\n\r\n\r\n\r\n\r\nWith the news, Kraken=E2=80=99s total number of trading pairs grows to 155+=\r\n, a figure\r\nthat includes our launch of more traditional FX trading pairs\r\n[https://blog.kraken.com/post/4200/eur-gbp-usd-and-more-fx-trading-is-going=\r\n-live-on-kraken/]=20\r\njust six weeks ago.\r\n\r\n\r\n\r\n\r\nWhat are the new trading pairs?\r\n\r\n\r\n\r\n\r\nEther Pairs\r\n\r\n\r\n\r\n\r\n * BCH/ETH\r\n * LTC/ETH\r\n * XRP/ETH\r\n\r\n\r\n\r\n\r\nPound Sterling Pairs\r\n\r\n\r\n\r\n\r\n * BCH/GBP\r\n * LTC/GBP\r\n * XRP/GBP\r\n\r\n\r\n\r\n\r\nTether Pairs\r\n\r\n\r\n\r\n\r\n * BCH/USDT\r\n * LTC/USDT\r\n * XRP/USDT\r\n * USDT/JPY\r\n * USDT/CHF\r\n\r\n\r\n\r\n\r\nFor more details such as trading minimums and fees, please see our blog pos=\r\nt\r\n[https://blog.kraken.com/post/4801/kraken-adds-11-new-pairs-for-trading-pop=\r\nular-currencies]\r\n.\r\n\r\n\r\n\r\n\r\nThank you for choosing Kraken, the trusted and secure digital asset exchang=\r\ne.\r\n\r\n\r\n\r\n\r\nThe Kraken Team\r\n\r\nP.S. Download the Kraken Pro app from the App Store or Google Play:\r\n\r\ndownload kraken pro app ios\r\n[https://blog.kraken.com/wp-content/uploads/2019/10/applestore.png]\r\n[https://krakenpro.app.link] download kraken pro app google\r\n[https://blog.kraken.com/wp-content/uploads/2019/11/google-play-badge-mk.pn=\r\ng]\r\n[https://krakenpro.app.link] =20\r\n\r\nSpread the word [https://img.kraken.com/e/twitter.png]\r\n[https://twitter.com/krakenfx] [https://img.kraken.com/e/facebook.png]\r\n[https://www.facebook.com/KrakenFX/] This communication is a commercial mes=\r\nsage\r\nand its contents are intended for the recipient only and may contain\r\nconfidential, non-public and/or privileged information. If you have receive=\r\nd\r\nthis communication in error, do not read, duplicate or distribute. Kraken d=\r\noes\r\nnot make recommendations on the suitability of a particular asset class,\r\nstrategy, or course of action. Any investment decision you make is solely y=\r\nour\r\nresponsibility. Please consider your individual position and financial goal=\r\ns\r\nbefore making an independent investment decision. Distributed by: Kraken.co=\r\nm,\r\n237 Kearny St #102, San Francisco, CA 94108. This email contains important\r\nupdates from Kraken. Unsubscribe\r\n[https://www.kraken.com/unsubscribe?kec=3DtGGTabsAAukVFU]\r\n--D0A9EA10D1AD99A6B8A85679BE81EA93\r\nContent-Transfer-Encoding: quoted-printable\r\nContent-Type: text/html; charset=utf-8\r\n\r\n\r\n\r\n\r\n\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n\r\n\r\n\r\n
\r\n \r\n
\r\n \r\n \r\n \r\n \r\n \r\n \r\n
\r\n \r\n
\r\n \r\n \r\n \r\n \r\n \r\n \r\n
\r\n \r\n \r\n \r\n \r\n
\r\n \r\n \r\n \r\n \r\n \r\n \r\n
\r\n \r\n 3D\"Kraken\"\r\n \r\n
\r\n
\r\n
\r\n
\r\n \r\n
\r\n
\r\n \r\n
\r\n \r\n \r\n \r\n \r\n \r\n \r\n
\r\n \r\n
\r\n \r\n \r\n \r\n \r\n \r\n \r\n
\r\n \r\n \r\n \r\n \r\n
\r\n
\r\n

Hi Richard,

\r\n


\r\n

Kraken clients can now begin converting =\r\npopular currencies more efficiently with the addition of 11 new trading pai=\r\nrs.

\r\n


\r\n

This includes expanded options for conve=\r\nrting ether (ETH), USDT and Pound Sterling (GBP), with offerings designed t=\r\no bring more functionality, while enabling clients to avoid added fees.

\r\n


\r\n

For example, converting Bitcoin Cash (BC=\r\nH) to ETH on Kraken previously required two distinct trades. With our new B=\r\nCH/ETH trading pair, this conversion can be done seamlessly, allowing clien=\r\nts to simply sell BCH directly for ETH.

\r\n


\r\n

It=E2=80=99s the latest example of how w=\r\ne continue to strive to make Kraken easier and more convenient for active t=\r\nraders and newer clients alike.=C2=A0

\r\n


\r\n

With the news, Kraken=E2=80=99s total nu=\r\nmber of trading pairs grows to 155+, a figure that includes our launch of m=\r\nore traditional FX trading pairs just s=\r\nix weeks ago.

\r\n


\r\n

What are the new trading pairs?<=\r\n/strong>

\r\n


\r\n

Ether Pairs

\r\n


\r\n
    \r\n
  • BCH/ETH
  • \r\n
  • LTC/ETH
  • \r\n
  • XRP/ETH
  • \r\n
\r\n


\r\n

Pound Sterling Pairs

\r\n


\r\n
    \r\n
  • BCH/GBP
  • \r\n
  • LTC/GBP
  • \r\n
  • XRP/GBP
  • \r\n
\r\n


\r\n

Tether Pairs

\r\n


\r\n
    \r\n
  • BCH/USDT
  • \r\n
  • LTC/USDT
  • \r\n
  • XRP/USDT
  • \r\n
  • USDT/JPY
  • \r\n
  • USDT/CHF
  • \r\n
\r\n


\r\n

For m=\r\nore details such as trading minimums and fees, please see our blog post.<=\r\n/span>

\r\n


\r\n

Thank you for choosing Kraken, the trust=\r\ned and secure digital asset exchange.

\r\n


\r\n

The Kraken Team

\r\n
\r\n
\r\n
\r\n
\r\n \r\n
\r\n
\r\n \r\n
\r\n \r\n \r\n \r\n \r\n \r\n \r\n
\r\n \r\n
\r\n \r\n \r\n \r\n \r\n \r\n \r\n
\r\n \r\n \r\n \r\n \r\n
\r\n
\r\n

P.S. Download the Kraken Pro app from th=\r\ne App Store or Google Play:

\r\n
\r\n
\r\n
\r\n
\r\n \r\n
\r\n
\r\n \r\n
\r\n \r\n \r\n \r\n \r\n \r\n \r\n
\r\n \r\n
\r\n \r\n \r\n \r\n \r\n \r\n \r\n
\r\n \r\n \r\n \r\n \r\n
\r\n
\r\n
3D\"down=\r\nload
\r\n
\r\n
\r\n
\r\n
\r\n \r\n
\r\n
\r\n \r\n
\r\n \r\n \r\n \r\n \r\n \r\n \r\n
\r\n \r\n
\r\n \r\n \r\n \r\n \r\n \r\n \r\n
\r\n \r\n \r\n \r\n \r\n
\r\n

\r\n

\r\n \r\n
\r\n
\r\n
\r\n \r\n
\r\n
\r\n \r\n
\r\n \r\n \r\n \r\n \r\n \r\n \r\n
\r\n \r\n
\r\n \r\n \r\n \r\n \r\n \r\n \r\n
\r\n \r\n \r\n \r\n \r\n
\r\n
\r\n \r\n \r\n \r\n \r\n \r\n \r\n
\r\n
Spread the word
\r\n
\r\n \r\n \r\n \r\n \r\n \r\n \r\n
\r\n \r\n \r\n \r\n \r\n \r\n
\r\n
\r\n \r\n \r\n \r\n \r\n \r\n \r\n
\r\n \r\n \r\n \r\n \r\n \r\n
\r\n
\r\n
\r\n
\r\n
\r\n
\r\n \r\n
\r\n
\r\n \r\n
\r\n \r\n \r\n \r\n \r\n \r\n \r\n
\r\n \r\n
\r\n \r\n \r\n \r\n \r\n \r\n \r\n
\r\n \r\n \r\n \r\n \r\n
\r\n
This communication is a commercial message and its contents are =\r\nintended for the recipient only and may contain confidential, non-public an=\r\nd/or privileged information. If you have received this communication in err=\r\nor, do not read, duplicate or distribute. Kraken does not make recommendati=\r\nons on the suitability of a particular asset class, strategy, or course of =\r\naction. Any investment decision you make is solely your responsibility. Ple=\r\nase consider your individual position and financial goals before making an =\r\nindependent investment decision. Distributed by: Kraken.com, 237 Kearny St =\r\n#102, San Francisco, CA 94108.
\r\n
\r\n
\r\n
\r\n \r\n
\r\n
\r\n \r\n
\r\n \r\n \r\n \r\n \r\n \r\n \r\n
\r\n \r\n
\r\n \r\n \r\n \r\n \r\n \r\n \r\n
\r\n \r\n \r\n \r\n \r\n
\r\n
T=\r\nhis email contains important updates from Kraken. Unsubscribe
\r\n
\r\n
\r\n
\r\n \r\n
\r\n
\r\n \r\n
\r\n\r\n\r\n\r\n--D0A9EA10D1AD99A6B8A85679BE81EA93--\r\n" -} -} diff --git a/FlowCrypt/src/test/resources/PgpMsgTest/other/plain-google-security-alert-20210416-084836-UTC.txt b/FlowCrypt/src/test/resources/mime/plain-google-security-alert-20210416-084836-UTC.txt similarity index 98% rename from FlowCrypt/src/test/resources/PgpMsgTest/other/plain-google-security-alert-20210416-084836-UTC.txt rename to FlowCrypt/src/test/resources/mime/plain-google-security-alert-20210416-084836-UTC.txt index 0b7df7b0bf..b37d162595 100644 --- a/FlowCrypt/src/test/resources/PgpMsgTest/other/plain-google-security-alert-20210416-084836-UTC.txt +++ b/FlowCrypt/src/test/resources/mime/plain-google-security-alert-20210416-084836-UTC.txt @@ -1,164 +1,164 @@ -Delivered-To: flowcrypt.compatibility@gmail.com -Received: by 2002:a05:6102:35a:0:0:0:0 with SMTP id e26csp217974vsa; - Fri, 16 Apr 2021 01:48:36 -0700 (PDT) -X-Received: by 2002:a17:90a:2e0d:: with SMTP id q13mr8804494pjd.225.1618562916245; - Fri, 16 Apr 2021 01:48:36 -0700 (PDT) -ARC-Seal: i=1; a=rsa-sha256; t=1618562916; cv=none; - d=google.com; s=arc-20160816; - b=qIy3PJymeTkFTBEwV1jKJ1BKJu2NJ/8sLeoKHdRwZsiG2GjdpEVCZyp+cN6E9YnFU5 - wgNN9iq2VDgdHltm0KRdi3ilG3sRPCZaE6ca0Ey62AxpTQWsjU5Zubac/aYfwDvk/gCj - veNRMP+EM2VJulBrRqMSLfP+OF71M3A0aXXKwhZbb2+PiZpbNmHnTud0wFyAi5iuBVKd - XgBGgpTEXiC8l6YKlprYz5UG9H1PXQG0fd2YvMkar0NQ3ohicckzoLXqdwxRVJynWAgz - 6jKsZcKhiTDo2bN8Owzk6KhHNR1g5x7FNraWFZWp1M1X/IbiUAjBZjIXEgvmJhjWzsVH - niug== -ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=google.com; s=arc-20160816; - h=to:from:subject:message-id:feedback-id:date:mime-version - :dkim-signature; - bh=uuipVF2qU0hHa5vKd2OTA42pQ+zzCQZNwnk2SJnqU04=; - b=0tAIPVtA6BG3s8HAhdFLeMZYNWmSNCabefg3rNdT7w2Fv6lcP4dv5HE/ec6DFfRQ16 - 19nd3mln6x0krMCE5NKjwN813aqa3uKUSMUFCh47ADyQtDZVMDqh/zntJqO74d4MF9+n - g0D6wsi6F/+QPUtiR1ng+yuTmrgc9T7eX5mQffksccFmxfWkmmBLLbbJVGSEodCBA5Ns - I2qj12pFH4sRTZILiikJc9BDKhLAd5TLweaetzd2R0vLEPNi/hOlmOUs4O6R6uU5HF4+ - eZT0Xg00l2bNnMZjtQSionbK2MOMHSuwNih/57f9nx5PkzARKmIyFsbYHKXbjJ+MBY2m - iFUQ== -ARC-Authentication-Results: i=1; mx.google.com; - dkim=pass header.i=@accounts.google.com header.s=20161025 header.b=NQLVJV7G; - spf=pass (google.com: domain of 3y095yagtapafg-jwhdqsuugmflk.yggydw.uge@gaia.bounces.google.com designates 209.85.220.73 as permitted sender) smtp.mailfrom=3Y095YAgTAPAfg-jWhdqSUUgmflk.YggYdW.Uge@gaia.bounces.google.com; - dmarc=pass (p=REJECT sp=REJECT dis=NONE) header.from=accounts.google.com -Return-Path: <3Y095YAgTAPAfg-jWhdqSUUgmflk.YggYdW.Uge@gaia.bounces.google.com> -Received: from mail-sor-f73.google.com (mail-sor-f73.google.com. [209.85.220.73]) - by mx.google.com with SMTPS id t6sor2698897pgc.87.2021.04.16.01.48.36 - for - (Google Transport Security); - Fri, 16 Apr 2021 01:48:36 -0700 (PDT) -Received-SPF: pass (google.com: domain of 3y095yagtapafg-jwhdqsuugmflk.yggydw.uge@gaia.bounces.google.com designates 209.85.220.73 as permitted sender) client-ip=209.85.220.73; -Authentication-Results: mx.google.com; - dkim=pass header.i=@accounts.google.com header.s=20161025 header.b=NQLVJV7G; - spf=pass (google.com: domain of 3y095yagtapafg-jwhdqsuugmflk.yggydw.uge@gaia.bounces.google.com designates 209.85.220.73 as permitted sender) smtp.mailfrom=3Y095YAgTAPAfg-jWhdqSUUgmflk.YggYdW.Uge@gaia.bounces.google.com; - dmarc=pass (p=REJECT sp=REJECT dis=NONE) header.from=accounts.google.com -DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; - d=accounts.google.com; s=20161025; - h=mime-version:date:feedback-id:message-id:subject:from:to; - bh=uuipVF2qU0hHa5vKd2OTA42pQ+zzCQZNwnk2SJnqU04=; - b=NQLVJV7GtfxXZce+hFiep/b28eTKVLz0jnDhZxfcRZ2OYG7zjEJOk8esbs5kM5rnRY - 2IPqEdRvqinIGKG1qlIV98KYs6JBFVzq5o6muGcsG0pvL357iKcQSahNfyV7MsHtQoNV - SKNQ1zmqc9s0OJctTJ/MP2BR3tmDjZ1m3nBI8Ia5s2yeCjIB1fZP/AGek7swM+0w6Ec8 - gZhXdnfc3V4LHL48bsP39tGISJJcC9KFA+qrC5gO3gOj5pV286xG6egFbBQHTpmPu4CO - GHtSny7yoUmVwTah3pRv69fdmXtc5K3ANzSYm/J4VFnbMbLKpTBOSipn6iT9GjLiu9sG - tUlA== -X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; - d=1e100.net; s=20161025; - h=x-gm-message-state:mime-version:date:feedback-id:message-id:subject - :from:to; - bh=uuipVF2qU0hHa5vKd2OTA42pQ+zzCQZNwnk2SJnqU04=; - b=PyXLZ/LrynuP8tOq8tXpmin+TwkH2qzhx1gsFA+O4oYHCSqxir258GKSEFXtakL16q - 5gukI/GKZz/7Os03VXiB/p7dzBFX9xz27o7iBC9xZ6cbzNZcuYjxU/yOgzlHftfqitcZ - /gMY3hThJkdM9uSds4XKsnMKjGK/gLMzo/IY83puL6rOxnoBYTSxsB8JZwISrkfIjX+b - doZTfyib7O1MtHKvSxSh4QfvvgttrnmD8Rt+3RAwFTw01IvBlAY7qHkC08O8dXYZNbHJ - jMJmmkXH97tisSusvDqj/XdkviRtpik+uIqo6tzv6eq6wREyhAsfDj4LcCJTNzg8j9kp - nwuw== -X-Gm-Message-State: AOAM5303wb/4LAKdZ3WEEbwTuugNto+GoqOiN1E4cjkfq1Qk/TwoO3E1 - Fa7Tb1Kr8TK1J0NX8LIDrKmTHQo3asGuMprFyqP3AQ== -X-Google-Smtp-Source: ABdhPJwI8I9oocxzmTc/mMCgQnuSPYs3ajnsCGOY1T6fyuqp5mYFHn8A8kUgPWAEP/l/WhpgogXONK0sECb9/ubzQeWjRA== -MIME-Version: 1.0 -X-Received: by 2002:a63:e541:: with SMTP id z1mr7439195pgj.59.1618562915955; - Fri, 16 Apr 2021 01:48:35 -0700 (PDT) -Date: Fri, 16 Apr 2021 08:48:35 GMT -X-Account-Notification-Type: 127-anexp#nret-fa -Feedback-ID: 127-anexp#nret-fa:account-notifier -X-Notifications: 8bb96f5d67200000 -Message-ID: -Subject: Security alert -From: Google -To: flowcrypt.compatibility@gmail.com -Content-Type: multipart/alternative; boundary="000000000000a6f5a905c0130b1a" - ---000000000000a6f5a905c0130b1a -Content-Type: text/plain; charset="UTF-8"; format=flowed; delsp=yes -Content-Transfer-Encoding: base64 - -W2ltYWdlOiBHb29nbGVdDQpGbG93Q3J5cHQgaU9TIEFwcCB3YXMgZ3JhbnRlZCBhY2Nlc3MgdG8g -eW91ciBHb29nbGUgQWNjb3VudA0KDQoNCmZsb3djcnlwdC5jb21wYXRpYmlsaXR5QGdtYWlsLmNv -bQ0KDQpJZiB5b3UgZGlkIG5vdCBncmFudCBhY2Nlc3MsIHlvdSBzaG91bGQgY2hlY2sgdGhpcyBh -Y3Rpdml0eSBhbmQgc2VjdXJlIHlvdXINCmFjY291bnQuDQpDaGVjayBhY3Rpdml0eQ0KPGh0dHBz -Oi8vYWNjb3VudHMuZ29vZ2xlLmNvbS9BY2NvdW50Q2hvb3Nlcj9FbWFpbD1mbG93Y3J5cHQuY29t -cGF0aWJpbGl0eUBnbWFpbC5jb20mY29udGludWU9aHR0cHM6Ly9teWFjY291bnQuZ29vZ2xlLmNv -bS9hbGVydC9udC8xNjE4NTYyOTE1MDAwP3JmbiUzRDEyNyUyNnJmbmMlM0QxJTI2ZWlkJTNEMzI1 -MzUyMzg0MDcxNDQwNTUxNyUyNmV0JTNEMCUyNmFuZXhwJTNEbnJldC1mYT4NCllvdSBjYW4gYWxz -byBzZWUgc2VjdXJpdHkgYWN0aXZpdHkgYXQNCmh0dHBzOi8vbXlhY2NvdW50Lmdvb2dsZS5jb20v -bm90aWZpY2F0aW9ucw0KWW91IHJlY2VpdmVkIHRoaXMgZW1haWwgdG8gbGV0IHlvdSBrbm93IGFi -b3V0IGltcG9ydGFudCBjaGFuZ2VzIHRvIHlvdXINCkdvb2dsZSBBY2NvdW50IGFuZCBzZXJ2aWNl -cy4NCsKpIDIwMjEgR29vZ2xlIExMQywgMTYwMCBBbXBoaXRoZWF0cmUgUGFya3dheSwgTW91bnRh -aW4gVmlldywgQ0EgOTQwNDMsIFVTQQ0K ---000000000000a6f5a905c0130b1a -Content-Type: text/html; charset="UTF-8" -Content-Transfer-Encoding: quoted-printable - -= -= -
= -
<= -/table> ---000000000000a6f5a905c0130b1a-- +Delivered-To: flowcrypt.compatibility@gmail.com +Received: by 2002:a05:6102:35a:0:0:0:0 with SMTP id e26csp217974vsa; + Fri, 16 Apr 2021 01:48:36 -0700 (PDT) +X-Received: by 2002:a17:90a:2e0d:: with SMTP id q13mr8804494pjd.225.1618562916245; + Fri, 16 Apr 2021 01:48:36 -0700 (PDT) +ARC-Seal: i=1; a=rsa-sha256; t=1618562916; cv=none; + d=google.com; s=arc-20160816; + b=qIy3PJymeTkFTBEwV1jKJ1BKJu2NJ/8sLeoKHdRwZsiG2GjdpEVCZyp+cN6E9YnFU5 + wgNN9iq2VDgdHltm0KRdi3ilG3sRPCZaE6ca0Ey62AxpTQWsjU5Zubac/aYfwDvk/gCj + veNRMP+EM2VJulBrRqMSLfP+OF71M3A0aXXKwhZbb2+PiZpbNmHnTud0wFyAi5iuBVKd + XgBGgpTEXiC8l6YKlprYz5UG9H1PXQG0fd2YvMkar0NQ3ohicckzoLXqdwxRVJynWAgz + 6jKsZcKhiTDo2bN8Owzk6KhHNR1g5x7FNraWFZWp1M1X/IbiUAjBZjIXEgvmJhjWzsVH + niug== +ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=google.com; s=arc-20160816; + h=to:from:subject:message-id:feedback-id:date:mime-version + :dkim-signature; + bh=uuipVF2qU0hHa5vKd2OTA42pQ+zzCQZNwnk2SJnqU04=; + b=0tAIPVtA6BG3s8HAhdFLeMZYNWmSNCabefg3rNdT7w2Fv6lcP4dv5HE/ec6DFfRQ16 + 19nd3mln6x0krMCE5NKjwN813aqa3uKUSMUFCh47ADyQtDZVMDqh/zntJqO74d4MF9+n + g0D6wsi6F/+QPUtiR1ng+yuTmrgc9T7eX5mQffksccFmxfWkmmBLLbbJVGSEodCBA5Ns + I2qj12pFH4sRTZILiikJc9BDKhLAd5TLweaetzd2R0vLEPNi/hOlmOUs4O6R6uU5HF4+ + eZT0Xg00l2bNnMZjtQSionbK2MOMHSuwNih/57f9nx5PkzARKmIyFsbYHKXbjJ+MBY2m + iFUQ== +ARC-Authentication-Results: i=1; mx.google.com; + dkim=pass header.i=@accounts.google.com header.s=20161025 header.b=NQLVJV7G; + spf=pass (google.com: domain of 3y095yagtapafg-jwhdqsuugmflk.yggydw.uge@gaia.bounces.google.com designates 209.85.220.73 as permitted sender) smtp.mailfrom=3Y095YAgTAPAfg-jWhdqSUUgmflk.YggYdW.Uge@gaia.bounces.google.com; + dmarc=pass (p=REJECT sp=REJECT dis=NONE) header.from=accounts.google.com +Return-Path: <3Y095YAgTAPAfg-jWhdqSUUgmflk.YggYdW.Uge@gaia.bounces.google.com> +Received: from mail-sor-f73.google.com (mail-sor-f73.google.com. [209.85.220.73]) + by mx.google.com with SMTPS id t6sor2698897pgc.87.2021.04.16.01.48.36 + for + (Google Transport Security); + Fri, 16 Apr 2021 01:48:36 -0700 (PDT) +Received-SPF: pass (google.com: domain of 3y095yagtapafg-jwhdqsuugmflk.yggydw.uge@gaia.bounces.google.com designates 209.85.220.73 as permitted sender) client-ip=209.85.220.73; +Authentication-Results: mx.google.com; + dkim=pass header.i=@accounts.google.com header.s=20161025 header.b=NQLVJV7G; + spf=pass (google.com: domain of 3y095yagtapafg-jwhdqsuugmflk.yggydw.uge@gaia.bounces.google.com designates 209.85.220.73 as permitted sender) smtp.mailfrom=3Y095YAgTAPAfg-jWhdqSUUgmflk.YggYdW.Uge@gaia.bounces.google.com; + dmarc=pass (p=REJECT sp=REJECT dis=NONE) header.from=accounts.google.com +DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; + d=accounts.google.com; s=20161025; + h=mime-version:date:feedback-id:message-id:subject:from:to; + bh=uuipVF2qU0hHa5vKd2OTA42pQ+zzCQZNwnk2SJnqU04=; + b=NQLVJV7GtfxXZce+hFiep/b28eTKVLz0jnDhZxfcRZ2OYG7zjEJOk8esbs5kM5rnRY + 2IPqEdRvqinIGKG1qlIV98KYs6JBFVzq5o6muGcsG0pvL357iKcQSahNfyV7MsHtQoNV + SKNQ1zmqc9s0OJctTJ/MP2BR3tmDjZ1m3nBI8Ia5s2yeCjIB1fZP/AGek7swM+0w6Ec8 + gZhXdnfc3V4LHL48bsP39tGISJJcC9KFA+qrC5gO3gOj5pV286xG6egFbBQHTpmPu4CO + GHtSny7yoUmVwTah3pRv69fdmXtc5K3ANzSYm/J4VFnbMbLKpTBOSipn6iT9GjLiu9sG + tUlA== +X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; + d=1e100.net; s=20161025; + h=x-gm-message-state:mime-version:date:feedback-id:message-id:subject + :from:to; + bh=uuipVF2qU0hHa5vKd2OTA42pQ+zzCQZNwnk2SJnqU04=; + b=PyXLZ/LrynuP8tOq8tXpmin+TwkH2qzhx1gsFA+O4oYHCSqxir258GKSEFXtakL16q + 5gukI/GKZz/7Os03VXiB/p7dzBFX9xz27o7iBC9xZ6cbzNZcuYjxU/yOgzlHftfqitcZ + /gMY3hThJkdM9uSds4XKsnMKjGK/gLMzo/IY83puL6rOxnoBYTSxsB8JZwISrkfIjX+b + doZTfyib7O1MtHKvSxSh4QfvvgttrnmD8Rt+3RAwFTw01IvBlAY7qHkC08O8dXYZNbHJ + jMJmmkXH97tisSusvDqj/XdkviRtpik+uIqo6tzv6eq6wREyhAsfDj4LcCJTNzg8j9kp + nwuw== +X-Gm-Message-State: AOAM5303wb/4LAKdZ3WEEbwTuugNto+GoqOiN1E4cjkfq1Qk/TwoO3E1 + Fa7Tb1Kr8TK1J0NX8LIDrKmTHQo3asGuMprFyqP3AQ== +X-Google-Smtp-Source: ABdhPJwI8I9oocxzmTc/mMCgQnuSPYs3ajnsCGOY1T6fyuqp5mYFHn8A8kUgPWAEP/l/WhpgogXONK0sECb9/ubzQeWjRA== +MIME-Version: 1.0 +X-Received: by 2002:a63:e541:: with SMTP id z1mr7439195pgj.59.1618562915955; + Fri, 16 Apr 2021 01:48:35 -0700 (PDT) +Date: Fri, 16 Apr 2021 08:48:35 GMT +X-Account-Notification-Type: 127-anexp#nret-fa +Feedback-ID: 127-anexp#nret-fa:account-notifier +X-Notifications: 8bb96f5d67200000 +Message-ID: +Subject: Security alert +From: Google +To: flowcrypt.compatibility@gmail.com +Content-Type: multipart/alternative; boundary="000000000000a6f5a905c0130b1a" + +--000000000000a6f5a905c0130b1a +Content-Type: text/plain; charset="UTF-8"; format=flowed; delsp=yes +Content-Transfer-Encoding: base64 + +W2ltYWdlOiBHb29nbGVdDQpGbG93Q3J5cHQgaU9TIEFwcCB3YXMgZ3JhbnRlZCBhY2Nlc3MgdG8g +eW91ciBHb29nbGUgQWNjb3VudA0KDQoNCmZsb3djcnlwdC5jb21wYXRpYmlsaXR5QGdtYWlsLmNv +bQ0KDQpJZiB5b3UgZGlkIG5vdCBncmFudCBhY2Nlc3MsIHlvdSBzaG91bGQgY2hlY2sgdGhpcyBh +Y3Rpdml0eSBhbmQgc2VjdXJlIHlvdXINCmFjY291bnQuDQpDaGVjayBhY3Rpdml0eQ0KPGh0dHBz +Oi8vYWNjb3VudHMuZ29vZ2xlLmNvbS9BY2NvdW50Q2hvb3Nlcj9FbWFpbD1mbG93Y3J5cHQuY29t +cGF0aWJpbGl0eUBnbWFpbC5jb20mY29udGludWU9aHR0cHM6Ly9teWFjY291bnQuZ29vZ2xlLmNv +bS9hbGVydC9udC8xNjE4NTYyOTE1MDAwP3JmbiUzRDEyNyUyNnJmbmMlM0QxJTI2ZWlkJTNEMzI1 +MzUyMzg0MDcxNDQwNTUxNyUyNmV0JTNEMCUyNmFuZXhwJTNEbnJldC1mYT4NCllvdSBjYW4gYWxz +byBzZWUgc2VjdXJpdHkgYWN0aXZpdHkgYXQNCmh0dHBzOi8vbXlhY2NvdW50Lmdvb2dsZS5jb20v +bm90aWZpY2F0aW9ucw0KWW91IHJlY2VpdmVkIHRoaXMgZW1haWwgdG8gbGV0IHlvdSBrbm93IGFi +b3V0IGltcG9ydGFudCBjaGFuZ2VzIHRvIHlvdXINCkdvb2dsZSBBY2NvdW50IGFuZCBzZXJ2aWNl +cy4NCsKpIDIwMjEgR29vZ2xlIExMQywgMTYwMCBBbXBoaXRoZWF0cmUgUGFya3dheSwgTW91bnRh +aW4gVmlldywgQ0EgOTQwNDMsIFVTQQ0K +--000000000000a6f5a905c0130b1a +Content-Type: text/html; charset="UTF-8" +Content-Transfer-Encoding: quoted-printable + += +
3D"Google"= -
FlowCrypt iOS App was= - granted access to your Google Account
3D""fl= -owcrypt.compatibility@gmail.com

If you did not grant access, you should check this activity and secu= -re your account.
<= -div style=3D"padding-top: 20px; font-size: 12px; line-height: 16px; color: = -#5f6368; letter-spacing: 0.3px; text-align: center">You can also see securi= -ty activity at
https://myaccount.google.com/notifications
You received this email t= -o let you know about important changes to your Google Account and services.= -
= +
= +
<= +/table> +--000000000000a6f5a905c0130b1a-- diff --git a/FlowCrypt/src/test/resources/PgpMsgTest/other/plain-inline-image.txt b/FlowCrypt/src/test/resources/mime/plain-inline-image.txt similarity index 100% rename from FlowCrypt/src/test/resources/PgpMsgTest/other/plain-inline-image.txt rename to FlowCrypt/src/test/resources/mime/plain-inline-image.txt diff --git a/FlowCrypt/src/test/resources/PgpMsgTest/other/plain-google-security-alert-20210416-084836-UTC-html-content.txt b/FlowCrypt/src/test/resources/other/plain-google-security-alert-20210416-084836-UTC-html-content.txt similarity index 100% rename from FlowCrypt/src/test/resources/PgpMsgTest/other/plain-google-security-alert-20210416-084836-UTC-html-content.txt rename to FlowCrypt/src/test/resources/other/plain-google-security-alert-20210416-084836-UTC-html-content.txt diff --git a/FlowCrypt/src/test/resources/PgpMsgTest/other/plain-google-security-alert-20210416-084836-UTC-text-content.txt b/FlowCrypt/src/test/resources/other/plain-google-security-alert-20210416-084836-UTC-text-content.txt similarity index 100% rename from FlowCrypt/src/test/resources/PgpMsgTest/other/plain-google-security-alert-20210416-084836-UTC-text-content.txt rename to FlowCrypt/src/test/resources/other/plain-google-security-alert-20210416-084836-UTC-text-content.txt diff --git a/FlowCrypt/src/test/resources/PgpMsgTest/other/plain-inline-image-html-content.txt b/FlowCrypt/src/test/resources/other/plain-inline-image-html-content.txt similarity index 100% rename from FlowCrypt/src/test/resources/PgpMsgTest/other/plain-inline-image-html-content.txt rename to FlowCrypt/src/test/resources/other/plain-inline-image-html-content.txt diff --git a/FlowCrypt/src/test/resources/PgpKeyTest/keys/6D3E09867544EE627F2E928FBEE3A42D9A9C8AC9.public.gpg-key b/FlowCrypt/src/test/resources/pgp/keys/6D3E09867544EE627F2E928FBEE3A42D9A9C8AC9.public.gpg-key similarity index 100% rename from FlowCrypt/src/test/resources/PgpKeyTest/keys/6D3E09867544EE627F2E928FBEE3A42D9A9C8AC9.public.gpg-key rename to FlowCrypt/src/test/resources/pgp/keys/6D3E09867544EE627F2E928FBEE3A42D9A9C8AC9.public.gpg-key diff --git a/FlowCrypt/src/test/resources/PgpKeyTest/keys/E76853E128A0D376CAE47C143A30F4CC0A9A8F10.public.gpg-key b/FlowCrypt/src/test/resources/pgp/keys/E76853E128A0D376CAE47C143A30F4CC0A9A8F10.public.gpg-key similarity index 100% rename from FlowCrypt/src/test/resources/PgpKeyTest/keys/E76853E128A0D376CAE47C143A30F4CC0A9A8F10.public.gpg-key rename to FlowCrypt/src/test/resources/pgp/keys/E76853E128A0D376CAE47C143A30F4CC0A9A8F10.public.gpg-key diff --git a/FlowCrypt/src/test/resources/PgpKeyTest/keys/issue-1296-0xA96B4C55A800DB83.public.gpg-key b/FlowCrypt/src/test/resources/pgp/keys/issue-1296-0xA96B4C55A800DB83.public.gpg-key similarity index 100% rename from FlowCrypt/src/test/resources/PgpKeyTest/keys/issue-1296-0xA96B4C55A800DB83.public.gpg-key rename to FlowCrypt/src/test/resources/pgp/keys/issue-1296-0xA96B4C55A800DB83.public.gpg-key diff --git a/FlowCrypt/src/test/resources/PgpKeyTest/keys/issue-1296-0xA96B4C55A800DB83.secret-subkeys.gpg-key b/FlowCrypt/src/test/resources/pgp/keys/issue-1296-0xA96B4C55A800DB83.secret-subkeys.gpg-key similarity index 100% rename from FlowCrypt/src/test/resources/PgpKeyTest/keys/issue-1296-0xA96B4C55A800DB83.secret-subkeys.gpg-key rename to FlowCrypt/src/test/resources/pgp/keys/issue-1296-0xA96B4C55A800DB83.secret-subkeys.gpg-key diff --git a/FlowCrypt/src/test/resources/PgpKeyTest/keys/issue-1358.public.gpg-key b/FlowCrypt/src/test/resources/pgp/keys/issue-1358.public.gpg-key similarity index 100% rename from FlowCrypt/src/test/resources/PgpKeyTest/keys/issue-1358.public.gpg-key rename to FlowCrypt/src/test/resources/pgp/keys/issue-1358.public.gpg-key diff --git a/FlowCrypt/src/test/resources/PgpKeyTest/keys/issue-1669-corrupted.private.gpg-key b/FlowCrypt/src/test/resources/pgp/keys/issue-1669-corrupted.private.gpg-key similarity index 100% rename from FlowCrypt/src/test/resources/PgpKeyTest/keys/issue-1669-corrupted.private.gpg-key rename to FlowCrypt/src/test/resources/pgp/keys/issue-1669-corrupted.private.gpg-key diff --git a/FlowCrypt/src/test/resources/PgpMsgTest/keys/key0.txt b/FlowCrypt/src/test/resources/pgp/keys/key0.txt similarity index 100% rename from FlowCrypt/src/test/resources/PgpMsgTest/keys/key0.txt rename to FlowCrypt/src/test/resources/pgp/keys/key0.txt diff --git a/FlowCrypt/src/test/resources/PgpMsgTest/keys/key1.txt b/FlowCrypt/src/test/resources/pgp/keys/key1.txt similarity index 100% rename from FlowCrypt/src/test/resources/PgpMsgTest/keys/key1.txt rename to FlowCrypt/src/test/resources/pgp/keys/key1.txt diff --git a/FlowCrypt/src/test/resources/PgpKeyTest/keys/sha1@flowcrypt.test_pub.asc b/FlowCrypt/src/test/resources/pgp/keys/sha1@flowcrypt.test_pub.asc similarity index 100% rename from FlowCrypt/src/test/resources/PgpKeyTest/keys/sha1@flowcrypt.test_pub.asc rename to FlowCrypt/src/test/resources/pgp/keys/sha1@flowcrypt.test_pub.asc diff --git a/FlowCrypt/src/test/resources/PgpMsgTest/messages/decrypt - [enigmail] encrypted iso-2022-jp pgp-mime.txt b/FlowCrypt/src/test/resources/pgp/messages/decrypt - [enigmail] encrypted iso-2022-jp pgp-mime.txt similarity index 100% rename from FlowCrypt/src/test/resources/PgpMsgTest/messages/decrypt - [enigmail] encrypted iso-2022-jp pgp-mime.txt rename to FlowCrypt/src/test/resources/pgp/messages/decrypt - [enigmail] encrypted iso-2022-jp pgp-mime.txt diff --git a/FlowCrypt/src/test/resources/PgpMsgTest/messages/decrypt - [enigmail] encrypted iso-2022-jp, plain text.txt b/FlowCrypt/src/test/resources/pgp/messages/decrypt - [enigmail] encrypted iso-2022-jp, plain text.txt similarity index 100% rename from FlowCrypt/src/test/resources/PgpMsgTest/messages/decrypt - [enigmail] encrypted iso-2022-jp, plain text.txt rename to FlowCrypt/src/test/resources/pgp/messages/decrypt - [enigmail] encrypted iso-2022-jp, plain text.txt diff --git a/FlowCrypt/src/test/resources/PgpMsgTest/messages/decrypt - [everdesk] message encrypted for sub but claims encryptedFor-primary,sub.txt b/FlowCrypt/src/test/resources/pgp/messages/decrypt - [everdesk] message encrypted for sub but claims encryptedFor-primary,sub.txt similarity index 100% rename from FlowCrypt/src/test/resources/PgpMsgTest/messages/decrypt - [everdesk] message encrypted for sub but claims encryptedFor-primary,sub.txt rename to FlowCrypt/src/test/resources/pgp/messages/decrypt - [everdesk] message encrypted for sub but claims encryptedFor-primary,sub.txt diff --git a/FlowCrypt/src/test/resources/PgpMsgTest/messages/decrypt - [gpg] signed fully armored message.txt b/FlowCrypt/src/test/resources/pgp/messages/decrypt - [gpg] signed fully armored message.txt similarity index 100% rename from FlowCrypt/src/test/resources/PgpMsgTest/messages/decrypt - [gpg] signed fully armored message.txt rename to FlowCrypt/src/test/resources/pgp/messages/decrypt - [gpg] signed fully armored message.txt diff --git a/FlowCrypt/src/test/resources/PgpMsgTest/messages/decrypt - [security] mdc - missing - error.txt b/FlowCrypt/src/test/resources/pgp/messages/decrypt - [security] mdc - missing - error.txt similarity index 100% rename from FlowCrypt/src/test/resources/PgpMsgTest/messages/decrypt - [security] mdc - missing - error.txt rename to FlowCrypt/src/test/resources/pgp/messages/decrypt - [security] mdc - missing - error.txt diff --git a/FlowCrypt/src/test/resources/PgpMsgTest/messages/decrypt - [security] mdc - modification detected - error.txt b/FlowCrypt/src/test/resources/pgp/messages/decrypt - [security] mdc - modification detected - error.txt similarity index 100% rename from FlowCrypt/src/test/resources/PgpMsgTest/messages/decrypt - [security] mdc - modification detected - error.txt rename to FlowCrypt/src/test/resources/pgp/messages/decrypt - [security] mdc - modification detected - error.txt diff --git a/FlowCrypt/src/test/resources/PgpMsgTest/messages/decrypt - encrypted missing checksum.txt b/FlowCrypt/src/test/resources/pgp/messages/decrypt - encrypted missing checksum.txt similarity index 100% rename from FlowCrypt/src/test/resources/PgpMsgTest/messages/decrypt - encrypted missing checksum.txt rename to FlowCrypt/src/test/resources/pgp/messages/decrypt - encrypted missing checksum.txt diff --git a/FlowCrypt/src/test/resources/PgpMsgTest/messages/decrypt - issue 1347 - wrong checksum.txt b/FlowCrypt/src/test/resources/pgp/messages/decrypt - issue 1347 - wrong checksum.txt similarity index 100% rename from FlowCrypt/src/test/resources/PgpMsgTest/messages/decrypt - issue 1347 - wrong checksum.txt rename to FlowCrypt/src/test/resources/pgp/messages/decrypt - issue 1347 - wrong checksum.txt diff --git a/FlowCrypt/src/test/resources/PgpMsgTest/messages/decrypt - without a subject.txt b/FlowCrypt/src/test/resources/pgp/messages/decrypt - without a subject.txt similarity index 100% rename from FlowCrypt/src/test/resources/PgpMsgTest/messages/decrypt - without a subject.txt rename to FlowCrypt/src/test/resources/pgp/messages/decrypt - without a subject.txt diff --git a/FlowCrypt/src/test/resources/PgpMsgTest/compat/direct-encrypted-pgpmime-special-chars.txt b/FlowCrypt/src/test/resources/pgp/messages/direct-encrypted-pgpmime-special-chars.txt similarity index 100% rename from FlowCrypt/src/test/resources/PgpMsgTest/compat/direct-encrypted-pgpmime-special-chars.txt rename to FlowCrypt/src/test/resources/pgp/messages/direct-encrypted-pgpmime-special-chars.txt diff --git a/FlowCrypt/src/test/resources/PgpMsgTest/compat/direct-encrypted-text-special-chars.txt b/FlowCrypt/src/test/resources/pgp/messages/direct-encrypted-text-special-chars.txt similarity index 100% rename from FlowCrypt/src/test/resources/PgpMsgTest/compat/direct-encrypted-text-special-chars.txt rename to FlowCrypt/src/test/resources/pgp/messages/direct-encrypted-text-special-chars.txt diff --git a/FlowCrypt/src/test/resources/PgpMsgTest/other/signed-message-preserve-newlines.txt b/FlowCrypt/src/test/resources/pgp/messages/signed-message-preserve-newlines.txt similarity index 100% rename from FlowCrypt/src/test/resources/PgpMsgTest/other/signed-message-preserve-newlines.txt rename to FlowCrypt/src/test/resources/pgp/messages/signed-message-preserve-newlines.txt
3D"Google"= +
FlowCrypt iOS App was= + granted access to your Google Account
3D""fl= +owcrypt.compatibility@gmail.com

If you did not grant access, you should check this activity and secu= +re your account.
<= +div style=3D"padding-top: 20px; font-size: 12px; line-height: 16px; color: = +#5f6368; letter-spacing: 0.3px; text-align: center">You can also see securi= +ty activity at
https://myaccount.google.com/notifications
You received this email t= +o let you know about important changes to your Google Account and services.= +