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

Issue:
Don't allow passwords containing line breaks
### Checklist

- [X] I have used the search function to see if someone else has already submitted the same bug report.
- [X] I will describe the problem with as much detail as possible.

### App version

6.717

### Where did you get the app from?

None

### Android version

X

### Device model

_No response_

### Steps to reproduce

Start configuring your icloud.com email

### Expected behavior

Password checks and it goes to next step

I did the setup on FairEmail at the same time and it worked. 

### Actual behavior

Server error: unmatch quote

### Logs

```
02-24 22:58:27.993 20341 10889 V RealImapConnection: conn72600286 <<< #null# [OK, [CAPABILITY, XAPPLEPUSHSERVICE, IMAP4, IMAP4rev1, SASL-IR, AUTH=ATOKEN, AUTH=PLAIN], (2409B40-f42d5d1bd981) mr36p00im-tygg11200301.me.com]
02-24 22:58:27.994 20341 10889 D RealImapConnection: Saving [IMAP4, AUTH=PLAIN, XAPPLEPUSHSERVICE, SASL-IR, AUTH=ATOKEN, IMAP4REV1] capabilities for conn72600286
02-24 22:58:27.995 20341 10889 V RealImapConnection: conn72600286>>> 1 AUTHENTICATE PLAIN 

02-24 22:58:28.166 20341 20341 D InsetsController: show(ime(), fromIme=true)
02-24 22:58:28.223 20341 10889 V RealImapConnection: conn72600286<<<#+# []
02-24 22:58:28.651 20341 10889 V ImapResponseParser: conn72600286<<<#1# [NO, [AUTHENTICATIONFAILED], Authentication Failed]


RealImapConnection: conn72600286>>> 2 LOGIN "myusername" "ykkl-xxx-xxxt-kkki
02-24 22:58:28.655 20341 10889 V RealImapConnection: 
02-24 22:58:28.655 20341 10889 V RealImapConnection: "
02-24 22:58:28.848 20341 10889 V ImapResponseParser: conn72600286<<<#2# [BAD, unmatch quote]
```


Discussion hints:
There is some code and tests on GitHub 

https://github.com/Kopano-dev/vmime/blob/431305e11d847d93b6d67da8867d272d872c127a/tests/net/imap/IMAPParserTest.cpp#L68

`AUTH=ATOKEN` maybe means apple token. I have no idea. Seems like they have a difference in the protocol implementation 
Are you using a password manager or some other tool that somehow managed to enter a password containing two line breaks at the end?
Hmm, indeed maybe my copy paste from their website did this
Yeah that was it !
But the password input does not reflect such characters, FairEmail input does. That's how I did see that I needed to use "paste in raw format"

Pull request: Ignore line breaks in single line text inputs
Fixes #7670

Test command: ./gradlew :core:ui:compose:designsystem:testDebugUnitTest --tests "app.k9mail.core.ui.compose.designsystem.atom.textfield.TextInputTextFieldTest" --tests "app.k9mail.core.ui.compose.designsystem.atom.textfield.TextFieldOutlinedKtTest" --tests "app.k9mail.core.ui.compose.designsystem.atom.textfield.TextFieldOutlinedEmailAddressKtTest" --tests "app.k9mail.core.ui.compose.designsystem.atom.textfield.TextFieldOutlinedPasswordKtTest"

Fix patch:
diff --git a/core/ui/compose/designsystem/src/main/kotlin/app/k9mail/core/ui/compose/designsystem/atom/textfield/TextFieldCommon.kt b/core/ui/compose/designsystem/src/main/kotlin/app/k9mail/core/ui/compose/designsystem/atom/textfield/TextFieldCommon.kt
index f34727a973e..7497bc952d6 100644
--- a/core/ui/compose/designsystem/src/main/kotlin/app/k9mail/core/ui/compose/designsystem/atom/textfield/TextFieldCommon.kt
+++ b/core/ui/compose/designsystem/src/main/kotlin/app/k9mail/core/ui/compose/designsystem/atom/textfield/TextFieldCommon.kt
@@ -5,6 +5,12 @@ import androidx.compose.runtime.Composable
 import androidx.compose.ui.tooling.preview.Preview
 import app.k9mail.core.ui.compose.theme.PreviewWithThemes
 
+private val LINE_BREAK = "[\\r\\n]".toRegex()
+
+internal fun stripLineBreaks(onValueChange: (String) -> Unit): (String) -> Unit = { value ->
+    onValueChange(value.replace(LINE_BREAK, replacement = ""))
+}
+
 internal fun selectLabel(
     label: String?,
     isRequired: Boolean,
diff --git a/core/ui/compose/designsystem/src/main/kotlin/app/k9mail/core/ui/compose/designsystem/atom/textfield/TextFieldOutlined.kt b/core/ui/compose/designsystem/src/main/kotlin/app/k9mail/core/ui/compose/designsystem/atom/textfield/TextFieldOutlined.kt
index f91b9245947..b272c29442a 100644
--- a/core/ui/compose/designsystem/src/main/kotlin/app/k9mail/core/ui/compose/designsystem/atom/textfield/TextFieldOutlined.kt
+++ b/core/ui/compose/designsystem/src/main/kotlin/app/k9mail/core/ui/compose/designsystem/atom/textfield/TextFieldOutlined.kt
@@ -26,7 +26,7 @@ fun TextFieldOutlined(
 ) {
     MaterialOutlinedTextField(
         value = value,
-        onValueChange = onValueChange,
+        onValueChange = if (isSingleLine) stripLineBreaks(onValueChange) else onValueChange,
         modifier = modifier,
         enabled = isEnabled,
         label = selectLabel(label, isRequired),
diff --git a/core/ui/compose/designsystem/src/main/kotlin/app/k9mail/core/ui/compose/designsystem/atom/textfield/TextFieldOutlinedEmailAddress.kt b/core/ui/compose/designsystem/src/main/kotlin/app/k9mail/core/ui/compose/designsystem/atom/textfield/TextFieldOutlinedEmailAddress.kt
index 2f06322a441..4a354205b6c 100644
--- a/core/ui/compose/designsystem/src/main/kotlin/app/k9mail/core/ui/compose/designsystem/atom/textfield/TextFieldOutlinedEmailAddress.kt
+++ b/core/ui/compose/designsystem/src/main/kotlin/app/k9mail/core/ui/compose/designsystem/atom/textfield/TextFieldOutlinedEmailAddress.kt
@@ -22,7 +22,7 @@ fun TextFieldOutlinedEmailAddress(
 ) {
     MaterialOutlinedTextField(
         value = value,
-        onValueChange = onValueChange,
+        onValueChange = stripLineBreaks(onValueChange),
         modifier = modifier,
         enabled = isEnabled,
         label = selectLabel(label, isRequired),
diff --git a/core/ui/compose/designsystem/src/main/kotlin/app/k9mail/core/ui/compose/designsystem/atom/textfield/TextFieldOutlinedPassword.kt b/core/ui/compose/designsystem/src/main/kotlin/app/k9mail/core/ui/compose/designsystem/atom/textfield/TextFieldOutlinedPassword.kt
index b07f850fe2b..e779b69aab9 100644
--- a/core/ui/compose/designsystem/src/main/kotlin/app/k9mail/core/ui/compose/designsystem/atom/textfield/TextFieldOutlinedPassword.kt
+++ b/core/ui/compose/designsystem/src/main/kotlin/app/k9mail/core/ui/compose/designsystem/atom/textfield/TextFieldOutlinedPassword.kt
@@ -35,7 +35,7 @@ fun TextFieldOutlinedPassword(
 
     MaterialOutlinedTextField(
         value = value,
-        onValueChange = onValueChange,
+        onValueChange = stripLineBreaks(onValueChange),
         modifier = modifier,
         enabled = isEnabled,
         label = selectLabel(label, isRequired),
@@ -70,7 +70,7 @@ fun TextFieldOutlinedPassword(
 ) {
     MaterialOutlinedTextField(
         value = value,
-        onValueChange = onValueChange,
+        onValueChange = stripLineBreaks(onValueChange),
         modifier = modifier,
         enabled = isEnabled,
         label = selectLabel(label, isRequired),


Test patch:
diff --git a/core/ui/compose/designsystem/src/test/kotlin/app/k9mail/core/ui/compose/designsystem/atom/textfield/TextFieldOutlinedEmailAddressKtTest.kt b/core/ui/compose/designsystem/src/test/kotlin/app/k9mail/core/ui/compose/designsystem/atom/textfield/TextFieldOutlinedEmailAddressKtTest.kt
new file mode 100644
index 00000000000..f519ae90154
--- /dev/null
+++ b/core/ui/compose/designsystem/src/test/kotlin/app/k9mail/core/ui/compose/designsystem/atom/textfield/TextFieldOutlinedEmailAddressKtTest.kt
@@ -0,0 +1,50 @@
+package app.k9mail.core.ui.compose.designsystem.atom.textfield
+
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performClick
+import androidx.compose.ui.test.performTextInput
+import app.k9mail.core.ui.compose.testing.ComposeTest
+import assertk.assertThat
+import assertk.assertions.isEqualTo
+import org.junit.Test
+
+private const val TEST_TAG = "TextFieldOutlinedEmailAddress"
+
+class TextFieldOutlinedEmailAddressKtTest : ComposeTest() {
+
+    @Test
+    fun `should call onValueChange when value changes`() = runComposeTest {
+        var value = "initial"
+        setContent {
+            TextFieldOutlinedEmailAddress(
+                value = value,
+                onValueChange = { value = it },
+                modifier = Modifier.testTag(TEST_TAG),
+            )
+        }
+
+        onNodeWithTag(TEST_TAG).performClick()
+        onNodeWithTag(TEST_TAG).performTextInput(" + added text")
+
+        assertThat(value).isEqualTo("initial + added text")
+    }
+
+    @Test
+    fun `should strip line breaks before onValueChange is called`() = runComposeTest {
+        var value = ""
+        setContent {
+            TextFieldOutlinedEmailAddress(
+                value = value,
+                onValueChange = { value = it },
+                modifier = Modifier.testTag(TEST_TAG),
+            )
+        }
+
+        onNodeWithTag(TEST_TAG).performClick()
+        onNodeWithTag(TEST_TAG).performTextInput("one\n two")
+
+        assertThat(value).isEqualTo("one two")
+    }
+}
diff --git a/core/ui/compose/designsystem/src/test/kotlin/app/k9mail/core/ui/compose/designsystem/atom/textfield/TextFieldOutlinedKtTest.kt b/core/ui/compose/designsystem/src/test/kotlin/app/k9mail/core/ui/compose/designsystem/atom/textfield/TextFieldOutlinedKtTest.kt
new file mode 100644
index 00000000000..bc5c0c2014f
--- /dev/null
+++ b/core/ui/compose/designsystem/src/test/kotlin/app/k9mail/core/ui/compose/designsystem/atom/textfield/TextFieldOutlinedKtTest.kt
@@ -0,0 +1,88 @@
+package app.k9mail.core.ui.compose.designsystem.atom.textfield
+
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performClick
+import androidx.compose.ui.test.performTextInput
+import app.k9mail.core.ui.compose.testing.ComposeTest
+import assertk.assertThat
+import assertk.assertions.isEqualTo
+import org.junit.Test
+
+private const val TEST_TAG = "TextFieldOutlined"
+
+class TextFieldOutlinedKtTest : ComposeTest() {
+
+    @Test
+    fun `should call onValueChange when value changes with isSingleLine = false`() = runComposeTest {
+        var value = "initial"
+        setContent {
+            TextFieldOutlined(
+                value = value,
+                onValueChange = { value = it },
+                isSingleLine = false,
+                modifier = Modifier.testTag(TEST_TAG),
+            )
+        }
+
+        onNodeWithTag(TEST_TAG).performClick()
+        onNodeWithTag(TEST_TAG).performTextInput(" + added text")
+
+        assertThat(value).isEqualTo("initial + added text")
+    }
+
+    @Test
+    fun `should call onValueChange when value changes with isSingleLine = true`() = runComposeTest {
+        var value = "initial"
+        setContent {
+            TextFieldOutlined(
+                value = value,
+                onValueChange = { value = it },
+                isSingleLine = true,
+                modifier = Modifier.testTag(TEST_TAG),
+            )
+        }
+
+        onNodeWithTag(TEST_TAG).performClick()
+        onNodeWithTag(TEST_TAG).performTextInput(" + added text")
+
+        assertThat(value).isEqualTo("initial + added text")
+    }
+
+    @Test
+    fun `should allow line breaks when isSingleLine = false`() = runComposeTest {
+        var value = ""
+        setContent {
+            TextFieldOutlined(
+                value = value,
+                onValueChange = { value = it },
+                isSingleLine = false,
+                modifier = Modifier.testTag(TEST_TAG),
+            )
+        }
+
+        onNodeWithTag(TEST_TAG).performClick()
+        onNodeWithTag(TEST_TAG).performTextInput("one\ntwo")
+
+        assertThat(value).isEqualTo("one\ntwo")
+    }
+
+    @Test
+    fun `should strip line breaks before onValueChange is called when isSingleLine = true`() = runComposeTest {
+        var value = ""
+        setContent {
+            TextFieldOutlined(
+                value = value,
+                onValueChange = { value = it },
+                isSingleLine = true,
+                modifier = Modifier.testTag(TEST_TAG),
+            )
+        }
+
+        onNodeWithTag(TEST_TAG).performClick()
+        onNodeWithTag(TEST_TAG).performTextInput("one\n two")
+
+        assertThat(value).isEqualTo("one two")
+    }
+}
diff --git a/core/ui/compose/designsystem/src/test/kotlin/app/k9mail/core/ui/compose/designsystem/atom/textfield/TextFieldOutlinedPasswordKtTest.kt b/core/ui/compose/designsystem/src/test/kotlin/app/k9mail/core/ui/compose/designsystem/atom/textfield/TextFieldOutlinedPasswordKtTest.kt
index ffddca54e6c..36ae93bf736 100644
--- a/core/ui/compose/designsystem/src/test/kotlin/app/k9mail/core/ui/compose/designsystem/atom/textfield/TextFieldOutlinedPasswordKtTest.kt
+++ b/core/ui/compose/designsystem/src/test/kotlin/app/k9mail/core/ui/compose/designsystem/atom/textfield/TextFieldOutlinedPasswordKtTest.kt
@@ -1,18 +1,24 @@
 package app.k9mail.core.ui.compose.designsystem.atom.textfield
 
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.test.SemanticsNodeInteraction
 import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
 import androidx.compose.ui.test.assertIsDisplayed
 import androidx.compose.ui.test.onNodeWithContentDescription
+import androidx.compose.ui.test.onNodeWithTag
 import androidx.compose.ui.test.onNodeWithText
 import androidx.compose.ui.test.performClick
+import androidx.compose.ui.test.performTextInput
 import app.k9mail.core.ui.compose.designsystem.R
 import app.k9mail.core.ui.compose.testing.ComposeTest
 import assertk.assertThat
+import assertk.assertions.isEqualTo
 import assertk.assertions.isTrue
 import org.junit.Test
 
 private const val PASSWORD = "Password input"
+private const val TEST_TAG = "TextFieldOutlinedPassword"
 
 class TextFieldOutlinedPasswordKtTest : ComposeTest() {
 
@@ -131,6 +137,78 @@ class TextFieldOutlinedPasswordKtTest : ComposeTest() {
         onNodeWithText(PASSWORD).assertDoesNotExist()
     }
 
+    @Test
+    fun `variant 1 should call onValueChange when value changes`() = runComposeTest {
+        var value = "initial"
+        setContent {
+            TextFieldOutlinedPassword(
+                value = value,
+                onValueChange = { value = it },
+                modifier = Modifier.testTag(TEST_TAG),
+            )
+        }
+
+        onNodeWithTag(TEST_TAG).performClick()
+        onNodeWithTag(TEST_TAG).performTextInput(" + added text")
+
+        assertThat(value).isEqualTo("initial + added text")
+    }
+
+    @Test
+    fun `variant 2 should call onValueChange when value changes`() = runComposeTest {
+        var value = "initial"
+        setContent {
+            TextFieldOutlinedPassword(
+                value = value,
+                onValueChange = { value = it },
+                isPasswordVisible = false,
+                onPasswordVisibilityToggleClicked = {},
+                modifier = Modifier.testTag(TEST_TAG),
+            )
+        }
+
+        onNodeWithTag(TEST_TAG).performClick()
+        onNodeWithTag(TEST_TAG).performTextInput(" + added text")
+
+        assertThat(value).isEqualTo("initial + added text")
+    }
+
+    @Test
+    fun `variant 1 should strip line breaks before onValueChange is called`() = runComposeTest {
+        var value = ""
+        setContent {
+            TextFieldOutlinedPassword(
+                value = value,
+                onValueChange = { value = it },
+                modifier = Modifier.testTag(TEST_TAG),
+            )
+        }
+
+        onNodeWithTag(TEST_TAG).performClick()
+        onNodeWithTag(TEST_TAG).performTextInput("one\n two")
+
+        assertThat(value).isEqualTo("one two")
+    }
+
+    @Test
+    fun `variant 2 should strip line breaks before onValueChange is called`() = runComposeTest {
+        var value = ""
+        setContent {
+            TextFieldOutlinedPassword(
+                value = value,
+                onValueChange = { value = it },
+                isPasswordVisible = false,
+                onPasswordVisibilityToggleClicked = {},
+                modifier = Modifier.testTag(TEST_TAG),
+            )
+        }
+
+        onNodeWithTag(TEST_TAG).performClick()
+        onNodeWithTag(TEST_TAG).performTextInput("one\n two")
+
+        assertThat(value).isEqualTo("one two")
+    }
+
     private fun SemanticsNodeInteractionsProvider.onShowPasswordNode(): SemanticsNodeInteraction {
         return onNodeWithContentDescription(
             getString(R.string.designsystem_atom_password_textfield_show_password),
diff --git a/core/ui/compose/designsystem/src/test/kotlin/app/k9mail/core/ui/compose/designsystem/atom/textfield/TextInputTextFieldTest.kt b/core/ui/compose/designsystem/src/test/kotlin/app/k9mail/core/ui/compose/designsystem/atom/textfield/TextInputTextFieldTest.kt
deleted file mode 100644
index af6eff666d0..00000000000
--- a/core/ui/compose/designsystem/src/test/kotlin/app/k9mail/core/ui/compose/designsystem/atom/textfield/TextInputTextFieldTest.kt
+++ /dev/null
@@ -1,91 +0,0 @@
-package app.k9mail.core.ui.compose.designsystem.atom.textfield
-
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.test.onNodeWithTag
-import androidx.compose.ui.test.performClick
-import androidx.compose.ui.test.performTextInput
-import app.k9mail.core.ui.compose.testing.ComposeTest
-import assertk.assertThat
-import assertk.assertions.isEqualTo
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.robolectric.ParameterizedRobolectricTestRunner
-
-data class TextInputTextFieldTestData(
-    val name: String,
-    val input: String,
-    val content: @Composable (
-        value: String,
-        onValueChange: (String) -> Unit,
-        modifier: Modifier,
-    ) -> Unit,
-)
-
-@RunWith(ParameterizedRobolectricTestRunner::class)
-class TextInputTextFieldTest(
-    data: TextInputTextFieldTestData,
-) : ComposeTest() {
-
-    private val testSubjectName = data.name
-    private val testSubject = data.content
-    private val testInput = data.input
-
-    @Test
-    fun `should call onValueChange when value changes`() = runComposeTest {
-        var value = testInput
-        setContent {
-            testSubject(
-                value,
-                { value = it },
-                Modifier.testTag(testSubjectName),
-            )
-        }
-
-        onNodeWithTag(testSubjectName).performClick()
-        onNodeWithTag(testSubjectName).performTextInput(" + added text")
-
-        assertThat(value).isEqualTo("$testInput + added text")
-    }
-
-    companion object {
-        @JvmStatic
-        @ParameterizedRobolectricTestRunner.Parameters(name = "{0}")
-        fun data(): List<TextInputTextFieldTestData> = listOf(
-            TextInputTextFieldTestData(
-                name = "TextFieldOutlined",
-                input = "value",
-                content = { value, onValueChange: (String) -> Unit, modifier ->
-                    TextFieldOutlined(
-                        value = value,
-                        onValueChange = onValueChange,
-                        modifier = modifier,
-                    )
-                },
-            ),
-            TextInputTextFieldTestData(
-                name = "TextFieldOutlinedPassword",
-                input = "value",
-                content = { value, onValueChange: (String) -> Unit, modifier ->
-                    TextFieldOutlinedPassword(
-                        value = value,
-                        onValueChange = onValueChange,
-                        modifier = modifier,
-                    )
-                },
-            ),
-            TextInputTextFieldTestData(
-                name = "TextFieldOutlinedEmail",
-                input = "value",
-                content = { value, onValueChange: (String) -> Unit, modifier ->
-                    TextFieldOutlinedEmailAddress(
-                        value = value,
-                        onValueChange = onValueChange,
-                        modifier = modifier,
-                    )
-                },
-            ),
-        )
-    }
-}