From f0d89470afd93f120892ce394eb0691082dda575 Mon Sep 17 00:00:00 2001 From: denbond7 Date: Mon, 24 Feb 2025 12:02:44 +0200 Subject: [PATCH 1/7] wip --- .../com/google/api/services/gmail/model/ThreadExt.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/extensions/com/google/api/services/gmail/model/ThreadExt.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/extensions/com/google/api/services/gmail/model/ThreadExt.kt index a6e8f6f964..378ff0df71 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/extensions/com/google/api/services/gmail/model/ThreadExt.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/extensions/com/google/api/services/gmail/model/ThreadExt.kt @@ -20,9 +20,10 @@ import jakarta.mail.internet.InternetAddress */ fun Thread.getUniqueRecipients(account: String): List { return mutableListOf().apply { - if (messages == null || messages.isEmpty()) { + if (messages.isNullOrEmpty()) { return@apply } + val fromHeaderName = "From" val filteredHeaders = if (messages.size > 1) { @@ -42,7 +43,7 @@ fun Thread.getUniqueRecipients(account: String): List { } else emptyList() }.ifEmpty { //otherwise we will use all recipients - messages.flatMap { message -> message.filterHeadersWithName(fromHeaderName) } + messages.flatMap { it.filterHeadersWithName(fromHeaderName) } } } else { messages.first().filterHeadersWithName(fromHeaderName) From 39258c8b83480f2934642ff9bfe3cdda97ceb92f Mon Sep 17 00:00:00 2001 From: denbond7 Date: Mon, 24 Feb 2025 13:47:03 +0200 Subject: [PATCH 2/7] wip --- .../flowcrypt/email/security/pgp/PgpMsg.kt | 71 ++++++++++++++++++- 1 file changed, 70 insertions(+), 1 deletion(-) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/security/pgp/PgpMsg.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/security/pgp/PgpMsg.kt index 0cbe27de11..db8d09a445 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/security/pgp/PgpMsg.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/security/pgp/PgpMsg.kt @@ -1214,7 +1214,7 @@ object PgpMsg { MsgBlock.Type.PLAIN_TEXT -> { val html = fmtMsgContentBlockAsHtml( - content.toEscapedHtml(), + checkAndReturnQuotesFormatIfFound(content) ?: content.toEscapedHtml(), if (block.isOpenPGPMimeSigned) FrameColor.GRAY else FrameColor.PLAIN ) msgContentAsHtml.append(html) @@ -1269,6 +1269,13 @@ object PgpMsg { msgContentAsText.append("[image: ${alt}]\n") } + /*blockquote:nth-child(even){ *//*for first blockquote *//* + border-left: 1px solid red; margin: 0px 0px 0px 10px; padding:10px 0px 0px 10px; + } + blockquote:nth-child(odd){ *//*for second blockquote *//* + border-left: 1px solid #31a217; margin: 0px 0px 0px 10px; padding:10px 0px 0px 10px; + }*/ + return FormattedContentBlockResult( text = msgContentAsText.toString().trim(), contentBlock = MsgBlockFactory.fromContent( @@ -1304,6 +1311,68 @@ object PgpMsg { ) } + private fun checkAndReturnQuotesFormatIfFound(content: String): String? { + return buildQuotes(originalContent = content, unwrapContent = false)?.outerHtml() + } + + private fun buildQuotes(originalContent: String, unwrapContent: Boolean = true): Element? { + val content = if (unwrapContent) { + //remove > at the beginning of all lines to define next quotes level + val patternQuotesSign = "^>([^\\S\\r\\n])?".toRegex(RegexOption.MULTILINE) + originalContent.replace(patternQuotesSign, "") + } else { + originalContent + } + + val newLineStringPattern = "\\r\\n|\\r|\\n" + val patternQuotes = "(^>.*\$($newLineStringPattern))+".toRegex(RegexOption.MULTILINE) + val patternNewLine = "($newLineStringPattern)".toRegex(RegexOption.MULTILINE) + val br = "
" + val tagDiv = "div" + val tagBlockquote = "blockquote" + + val matchingResult = patternQuotes.find(content)?.groups?.firstOrNull() + ?: return Element(tagDiv).apply { + append(content.replace(patternNewLine, br)) + }.takeIf { unwrapContent } + val quotes = matchingResult.value + + return Element(tagDiv).apply { + //append text before quotes. Replace CRLF with
to transform to HTML. + if (matchingResult.range.first > 0) { + append(content.substring(0, matchingResult.range.first).replace(patternNewLine, br)) + } + + //append quotes + if (!unwrapContent) { + appendChild( + Element(tagDiv).apply { + attr("class", "gmail_quote") + appendChild( + Element(tagBlockquote).apply { + buildQuotes(quotes)?.let { appendChild(it) } + } + ) + } + ) + } else { + appendChild( + Element(tagBlockquote).apply { + buildQuotes(quotes)?.let { appendChild(it) } + } + ) + } + + //append text after quotes. Replace CRLF with
to transform to HTML. + if (matchingResult.range.last < content.length) { + append( + content.substring(matchingResult.range.last + 1, content.length) + .replace(patternNewLine, br) + ) + } + } + } + /** * replace content of images: */ From 0d6738931f864ec4400d5bd40ba18d931a3ea4de Mon Sep 17 00:00:00 2001 From: denbond7 Date: Tue, 25 Feb 2025 18:15:07 +0200 Subject: [PATCH 3/7] wip --- .../flowcrypt/email/security/pgp/PgpMsg.kt | 29 ++++++++++++++----- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/security/pgp/PgpMsg.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/security/pgp/PgpMsg.kt index db8d09a445..cfd3e43fdb 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/security/pgp/PgpMsg.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/security/pgp/PgpMsg.kt @@ -59,6 +59,7 @@ import org.json.JSONObject import org.jsoup.Jsoup import org.jsoup.nodes.Document import org.jsoup.nodes.Element +import org.jsoup.nodes.Entities import org.jsoup.nodes.TextNode import org.jsoup.parser.Parser import org.owasp.html.HtmlPolicyBuilder @@ -1326,21 +1327,19 @@ object PgpMsg { val newLineStringPattern = "\\r\\n|\\r|\\n" val patternQuotes = "(^>.*\$($newLineStringPattern))+".toRegex(RegexOption.MULTILINE) - val patternNewLine = "($newLineStringPattern)".toRegex(RegexOption.MULTILINE) - val br = "
" val tagDiv = "div" val tagBlockquote = "blockquote" val matchingResult = patternQuotes.find(content)?.groups?.firstOrNull() ?: return Element(tagDiv).apply { - append(content.replace(patternNewLine, br)) + append(prepareHtmlFromGivenText(content)) }.takeIf { unwrapContent } val quotes = matchingResult.value return Element(tagDiv).apply { - //append text before quotes. Replace CRLF with
to transform to HTML. + //prepend text before quotes if (matchingResult.range.first > 0) { - append(content.substring(0, matchingResult.range.first).replace(patternNewLine, br)) + prepend(prepareHtmlFromGivenText(content.substring(0, matchingResult.range.first))) } //append quotes @@ -1363,16 +1362,30 @@ object PgpMsg { ) } - //append text after quotes. Replace CRLF with
to transform to HTML. + //append text after quotes if (matchingResult.range.last < content.length) { append( - content.substring(matchingResult.range.last + 1, content.length) - .replace(patternNewLine, br) + prepareHtmlFromGivenText(content.substring(matchingResult.range.last + 1, content.length)) ) } } } + private fun prepareHtmlFromGivenText(content: String): String { + val newLineStringPattern = "\\r\\n|\\r|\\n" + val patternNewLine = "($newLineStringPattern)".toRegex() + val patternEscapedEmailAddress = "<(\\S+@\\S+)>".toRegex() + val emailAddressReplacement = "\$1" + val br = "
" + return Entities + //escape given text to fit HTML standard + .escape(content) + //Prepare for email addresses. + .replace(patternEscapedEmailAddress, emailAddressReplacement) + //Replace CRLF with
to transform to HTML. + .replace(patternNewLine, br) + } + /** * replace content of images: */ From d9401be69cb2eae910a66abf66c913067e3797b0 Mon Sep 17 00:00:00 2001 From: denbond7 Date: Tue, 25 Feb 2025 18:47:28 +0200 Subject: [PATCH 4/7] wip --- .../flowcrypt/email/security/pgp/PgpMsg.kt | 30 +++++++++++++------ 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/security/pgp/PgpMsg.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/security/pgp/PgpMsg.kt index cfd3e43fdb..7e0cc6cc54 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/security/pgp/PgpMsg.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/security/pgp/PgpMsg.kt @@ -1326,7 +1326,12 @@ object PgpMsg { } val newLineStringPattern = "\\r\\n|\\r|\\n" - val patternQuotes = "(^>.*\$($newLineStringPattern))+".toRegex(RegexOption.MULTILINE) + val beforeQuotesHeaderStringPattern = "^.*:($newLineStringPattern){1,2}" + val patternQuotes = (if (unwrapContent) { + "(^>.*\$($newLineStringPattern))+" + } else { + "($beforeQuotesHeaderStringPattern)(^>.*\$($newLineStringPattern))+" + }).toRegex(RegexOption.MULTILINE) val tagDiv = "div" val tagBlockquote = "blockquote" @@ -1343,23 +1348,30 @@ object PgpMsg { } //append quotes - if (!unwrapContent) { + if (unwrapContent) { + appendChild( + Element(tagBlockquote).apply { + buildQuotes(quotes)?.let { appendChild(it) } + } + ) + } else { appendChild( Element(tagDiv).apply { attr("class", "gmail_quote") + //for better UI experience we need to extract the quote header of the first quote + //and add it separately + val quotesHeader = + quotes.replace("(^>.*\$($newLineStringPattern))+".toRegex(RegexOption.MULTILINE), "") + append(prepareHtmlFromGivenText(quotesHeader)) + appendChild( Element(tagBlockquote).apply { - buildQuotes(quotes)?.let { appendChild(it) } + //here we should pass clear quotes and drop the first quote header + buildQuotes(quotes.replaceFirst(quotesHeader, ""))?.let { appendChild(it) } } ) } ) - } else { - appendChild( - Element(tagBlockquote).apply { - buildQuotes(quotes)?.let { appendChild(it) } - } - ) } //append text after quotes From 211706522a81274bfb29ddd1fb84adc8eee716d0 Mon Sep 17 00:00:00 2001 From: denbond7 Date: Tue, 25 Feb 2025 18:53:03 +0200 Subject: [PATCH 5/7] wip --- .../src/main/java/com/flowcrypt/email/security/pgp/PgpMsg.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/security/pgp/PgpMsg.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/security/pgp/PgpMsg.kt index 7e0cc6cc54..7bc68ce4c1 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/security/pgp/PgpMsg.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/security/pgp/PgpMsg.kt @@ -531,7 +531,7 @@ object PgpMsg { if (dirtyHtml == null) return null val originalDocument = Jsoup.parse(dirtyHtml, "", Parser.xmlParser()) - originalDocument.select("div.gmail_quote").firstOrNull()?.let { element -> + originalDocument.select("div.gmail_quote,div.flowcrypt_quote").firstOrNull()?.let { element -> //we wrap Gmail quote with 'details' tag val generation = Element("details").apply { appendChild(Element("summary")) @@ -1357,7 +1357,7 @@ object PgpMsg { } else { appendChild( Element(tagDiv).apply { - attr("class", "gmail_quote") + attr("class", "flowcrypt_quote") //for better UI experience we need to extract the quote header of the first quote //and add it separately val quotesHeader = From d7c73b498108cd6efd2ff5a28ee0434923a33b83 Mon Sep 17 00:00:00 2001 From: denbond7 Date: Thu, 27 Feb 2025 17:31:16 +0200 Subject: [PATCH 6/7] Added PgpMsgTest.testQuotesParsingAndHtmlManipulationForPlainMode().| #2961 --- .../email/security/pgp/PgpMsgTest.kt | 127 ++++++++++++++++++ 1 file changed, 127 insertions(+) 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 index d09452f491..8b8cc7b8f6 100644 --- a/FlowCrypt/src/test/java/com/flowcrypt/email/security/pgp/PgpMsgTest.kt +++ b/FlowCrypt/src/test/java/com/flowcrypt/email/security/pgp/PgpMsgTest.kt @@ -615,6 +615,133 @@ class PgpMsgTest { assertEquals(1, document.select("summary").size) } + @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")) + } + private data class RenderedBlock( val rendered: Boolean, val frameColor: String?, From 18e167acbea7dfa7a9b2729d20bdbd5efc7d5634 Mon Sep 17 00:00:00 2001 From: denbond7 Date: Thu, 27 Feb 2025 17:39:21 +0200 Subject: [PATCH 7/7] Removed unused code --- .../main/java/com/flowcrypt/email/security/pgp/PgpMsg.kt | 7 ------- 1 file changed, 7 deletions(-) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/security/pgp/PgpMsg.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/security/pgp/PgpMsg.kt index 7bc68ce4c1..5d7e9c23f9 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/security/pgp/PgpMsg.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/security/pgp/PgpMsg.kt @@ -1270,13 +1270,6 @@ object PgpMsg { msgContentAsText.append("[image: ${alt}]\n") } - /*blockquote:nth-child(even){ *//*for first blockquote *//* - border-left: 1px solid red; margin: 0px 0px 0px 10px; padding:10px 0px 0px 10px; - } - blockquote:nth-child(odd){ *//*for second blockquote *//* - border-left: 1px solid #31a217; margin: 0px 0px 0px 10px; padding:10px 0px 0px 10px; - }*/ - return FormattedContentBlockResult( text = msgContentAsText.toString().trim(), contentBlock = MsgBlockFactory.fromContent(