Repository: thunderbird/thunderbird-android (language: kotlin)

Issue:
Wrap long lines when displaying message (text size gets too small)
K9 doesn't warp links, which makes some e-mails very wide:
![screenshot_20180208-131048](https://user-images.githubusercontent.com/5685512/36012611-7e192f60-0d5f-11e8-9f28-c7e2559efeac.png)
***
compared to:
![screenshot_20180208-131057](https://user-images.githubusercontent.com/5685512/36012615-7fa039fa-0d5f-11e8-8a5a-b14fc63a9502.png)



Discussion hints:
another example, this time not links, but a large number of underscores (I already decoded the email from quoted printable):
[mail.zip](https://github.com/k9mail/k-9/files/1709841/mail.zip)

The lines marked with red on the right, have been correctly wrapped by K9.
The divider at the top (this e-mail was forwarded by k9 to myself again. it's the divider between your own message and the forwarded message that K9 adds) stretches to the same exact full width, but the underscores are wider than full width and therefore stretch the viewport

![screenshot_20180209-060913](https://user-images.githubusercontent.com/5685512/36013123-b30be084-0d62-11e8-87c8-7f47e8001b75.png)
We could add
`word-wrap: break-word;`
to the HTML of each mail to achieve this, I think.

https://www.w3schools.com/cssref/css3_pr_word-wrap.asp

@cketti Sorry to bother you, but what do you think?
Any updates on this? Solving this issue would greatly improve the UX, as currently I need to zoom in every time to read the text if the email contains even a remotely lengthy non-breakable string, which gets increasingly likely in a long chain of replies.
Side note: it seems to me that some word-breaking is performed, but it isn't applied to fixed-width text, or long strings like URLs, so replies in long chains are sometimes ridiculously malformed (to the point where the text becomes vertical instead of horizontal, i.e. 1 character per line).
@cketti any idea what a good fix for this might be?
+1
@cketti If you point me in the direction of a fix, I can work on this.
Really need this.  Every day we get emails with long links and it makes the rest of the email hard/impossible to read.
What is the expected target behavior here? If you go to Settings > General > Display, at the bottom there is a setting "Auto-fit messages". If you disable it, then a bugzilla email with a long pushloghtml link will show zoomed in, but you might actually have to zoom out or scroll a bit to read all content. 

If you have the same email on Thunderbird desktop it simply has a horizontal scrollbar. So I guess this would be about changing the default for auto-fit?


Further notes from conversation with Ryan: We should do something about images as well, otherwise they either clip or make it zoom out. Then let's see if we can change the default setting on beta with a feature flag and see how the reception is.

https://searchfox.org/comm-central/source/mail/themes/shared/mail/messageBody.css sets max-width: 100% on images if shrinktofit is enabled, which appears to be the case by default. It also forces overflow:auto on the root node.


(Side note it would be cool if we had a system to change defaults, so that we can migrate users without having to think about the implications every time. But that is for another issue.)
This could be a good first bug, though requires some very targeted exploratory testing with different emails in order to find edge cases. So far we have:

* Long link in one line
* Normal text, then a paragraph with an image that exceeds 100% width
Good starting points are:
* legacy/ui/legacy/src/main/java/com/fsck/k9/view/MessageWebView.kt
* legacy/core/src/main/java/com/fsck/k9/message/html/DisplayHtml.kt
To add yet another use case to consider here: When zooming in, the text becomes larger but the lines don't wrap. So there is a fair portion of horizontal scrolling involved.
Please, assign this to me.
> Please, assign this to me.

Feel free to work on it, considering the comments and code pointers added earlier.
> Good starting points are:
> 
> * legacy/ui/legacy/src/main/java/com/fsck/k9/view/MessageWebView.kt
> * legacy/core/src/main/java/com/fsck/k9/message/html/DisplayHtml.kt

DisplayHtml doesn't affect non-null HTML text."

Pull request: Propose a solution for #3172, initially covering long URL on mobile phone screens
Fixes #3172

This update improves readability by ensuring that long words and links in email messages wrap properly in the WebView, avoiding horizontal scrolling. In DisplayHtml.kt, the cssStylePre method now includes CSS rules for <a> tags (word-break and overflow-wrap). Additionally, the HTML and CSS generation methods have been refactored to use multiline string literals for better message clarity.

Test command: ./gradlew :legacy:core:testDebugUnitTest --tests "com.fsck.k9.message.html.DisplayHtmlTest"

Fix patch:
diff --git a/legacy/core/src/main/java/com/fsck/k9/message/html/DisplayHtml.kt b/legacy/core/src/main/java/com/fsck/k9/message/html/DisplayHtml.kt
index a912b0940db..d098b20e486 100644
--- a/legacy/core/src/main/java/com/fsck/k9/message/html/DisplayHtml.kt
+++ b/legacy/core/src/main/java/com/fsck/k9/message/html/DisplayHtml.kt
@@ -1,26 +1,65 @@
 package com.fsck.k9.message.html
 
 import app.k9mail.html.cleaner.HtmlHeadProvider
+import org.intellij.lang.annotations.Language
 
 class DisplayHtml(private val settings: HtmlSettings) : HtmlHeadProvider {
     override val headHtml: String
         get() {
-            return """<meta name="viewport" content="width=device-width"/>""" +
-                cssStylePre() +
-                cssStyleSignature()
+            @Language("HTML")
+            val html = """
+                <meta name="viewport" content="width=device-width"/>
+                ${cssStyleGlobal()}
+                ${cssStylePre()}
+                ${cssStyleSignature()}
+            """.trimIndent()
+
+            return html
         }
 
     fun wrapStatusMessage(status: CharSequence): String {
-        return wrapMessageContent("<div style=\"text-align:center; color: grey;\">$status</div>")
+        @Language("HTML")
+        val html = """
+            <div style="text-align:center; color: grey;">$status</div>
+        """.trimIndent()
+
+        return wrapMessageContent(html)
     }
 
+    @Language("HTML")
     fun wrapMessageContent(messageContent: CharSequence): String {
         // Include a meta tag so the WebView will not use a fixed viewport width of 980 px
-        return "<html dir=\"auto\"><head><meta name=\"viewport\" content=\"width=device-width\"/>" +
-            cssStylePre() +
-            "</head><body>" +
-            messageContent +
-            "</body></html>"
+        return """
+            <html dir="auto">
+                <head>
+                    $headHtml
+                </head>
+                <body>
+                    $messageContent
+                </body>
+            </html>
+        """.trimIndent()
+    }
+
+    /**
+     * Dynamically generates a CSS style block that applies global rules to all elements (`*` selector).
+     *
+     * The style enforces word-breaking and overflow wrapping to prevent content overflow
+     * and ensures long text strings break correctly without causing horizontal scrolling.
+     *
+     * @return A `<style>` element string that can be dynamically injected into the HTML `<head>`
+     * to apply these global styles when rendering messages.
+     */
+    @Language("HTML")
+    private fun cssStyleGlobal(): String {
+        return """
+            <style type="text/css">
+                * {
+                    word-break: break-word;
+                    overflow-wrap: break-word;
+                }
+            </style>
+        """.trimIndent()
     }
 
     /**
@@ -31,15 +70,30 @@ class DisplayHtml(private val settings: HtmlSettings) : HtmlHeadProvider {
      * @return A `<style>` element that can be dynamically included in the HTML `<head>` element when messages are
      * displayed.
      */
+    @Language("HTML")
     private fun cssStylePre(): String {
         val font = if (settings.useFixedWidthFont) "monospace" else "sans-serif"
 
-        return "<style type=\"text/css\"> pre." + EmailTextToHtml.K9MAIL_CSS_CLASS +
-            " {white-space: pre-wrap; word-wrap:break-word; " +
-            "font-family: " + font + "; margin-top: 0px}</style>"
+        return """
+            <style type="text/css">
+                pre.${EmailTextToHtml.K9MAIL_CSS_CLASS} {
+                    white-space: pre-wrap;
+                    word-wrap: break-word;
+                    font-family: $font;
+                    margin-top: 0px;
+                }
+            </style>
+        """.trimIndent()
     }
 
+    @Language("HTML")
     private fun cssStyleSignature(): String {
-        return """<style type="text/css">.k9mail-signature { opacity: 0.5 }</style>"""
+        return """
+            <style type="text/css">
+                .k9mail-signature {
+                    opacity: 0.5;
+                }
+            </style>
+        """.trimIndent()
     }
 }


Test patch:
diff --git a/legacy/core/src/test/java/com/fsck/k9/message/html/DisplayHtmlTest.kt b/legacy/core/src/test/java/com/fsck/k9/message/html/DisplayHtmlTest.kt
index 69620ae002c..f26b972da6a 100644
--- a/legacy/core/src/test/java/com/fsck/k9/message/html/DisplayHtmlTest.kt
+++ b/legacy/core/src/test/java/com/fsck/k9/message/html/DisplayHtmlTest.kt
@@ -2,13 +2,15 @@ package com.fsck.k9.message.html
 
 import assertk.Assert
 import assertk.assertThat
+import assertk.assertions.contains
 import assertk.assertions.hasSize
 import assertk.assertions.isEqualTo
 import org.jsoup.Jsoup
 import org.junit.Test
 
 class DisplayHtmlTest {
-    val displayHtml = DisplayHtml(HtmlSettings(useDarkMode = false, useFixedWidthFont = false))
+    val htmlSettings = HtmlSettings(useDarkMode = false, useFixedWidthFont = false)
+    val displayHtml = DisplayHtml(htmlSettings)
 
     @Test
     fun wrapMessageContent_addsViewportMetaElement() {
@@ -25,10 +27,45 @@ class DisplayHtmlTest {
     }
 
     @Test
-    fun wrapMessageContent_addsPreCSS() {
+    fun wrapMessageContent_addsPreCSSStyles() {
         val html = displayHtml.wrapMessageContent("Some text")
 
-        assertThat(html).containsHtmlElement("head > style")
+        assertThat(html).containsHtmlElement("head > style", 3)
+    }
+
+    @Test
+    fun wrapMessageContent_addsGlobalStyleRules() {
+        val html = displayHtml.wrapMessageContent("test")
+
+        assertThat(html).containsStyleRulesFor(
+            selector = "*",
+            "word-break: break-word;",
+            "overflow-wrap: break-word;",
+        )
+    }
+
+    @Test
+    fun wrapMessageContent_addsPreCSS() {
+        val html = displayHtml.wrapMessageContent("test")
+        val expectedFont = if (htmlSettings.useFixedWidthFont) "monospace" else "sans-serif"
+
+        assertThat(html).containsStyleRulesFor(
+            selector = "pre.${EmailTextToHtml.K9MAIL_CSS_CLASS}",
+            "white-space: pre-wrap;",
+            "word-wrap: break-word;",
+            "font-family: $expectedFont;",
+            "margin-top: 0px;",
+        )
+    }
+
+    @Test
+    fun wrapMessageContent_addsSignatureStyleRules() {
+        val html = displayHtml.wrapMessageContent("test")
+
+        assertThat(html).containsStyleRulesFor(
+            selector = ".k9mail-signature",
+            "opacity: 0.5;",
+        )
     }
 
     @Test
@@ -40,8 +77,26 @@ class DisplayHtmlTest {
         assertThat(html).bodyText().isEqualTo(content)
     }
 
-    private fun Assert<String>.containsHtmlElement(cssQuery: String) = given { actual ->
-        assertThat(actual).htmlElements(cssQuery).hasSize(1)
+    private fun Assert<String>.containsStyleRulesFor(selector: String, vararg expectedRules: String) = given { html ->
+        val styleContent = Jsoup.parse(html)
+            .select("style")
+            .joinToString("\n") { it.data() }
+
+        val selectorPattern = Regex.escape(selector).replace("\\*", "\\\\*")
+        val selectorBlock = Regex("$selectorPattern\\s*\\{([^}]*)\\}", RegexOption.MULTILINE)
+            .find(styleContent)
+            ?.groupValues?.get(1)
+            ?.trim()
+
+        requireNotNull(selectorBlock) { "No style block found for selector: $selector" }
+
+        expectedRules.forEach { rule ->
+            assertThat(selectorBlock).contains(rule)
+        }
+    }
+
+    private fun Assert<String>.containsHtmlElement(cssQuery: String, expectedCount: Int = 1) = given { actual ->
+        assertThat(actual).htmlElements(cssQuery).hasSize(expectedCount)
     }
 
     private fun Assert<String>.htmlElements(cssQuery: String) = transform { html ->