Merge branch '6.8-MAINT'

This commit is contained in:
cketti 2024-03-11 14:12:59 +01:00
commit 45d79a9a16
14 changed files with 340 additions and 110 deletions

View file

@ -0,0 +1,7 @@
- Added DNSSEC support when looking for server settings during setup
- Made a change to prevent some software keyboards from capitalizing/auto-correcting email addresses in account setup
- Fixed a crash when a very long subject was used
- Fixed displaying OAuth 2.0 error messages
- Fixed rare crash when downloading an attachment
- Added code to disallow line breaks in single line text inputs
- Updated translations

View file

@ -39,6 +39,8 @@
public <init>(android.content.Context);
}
-keep class com.fsck.k9.mail.oauth.XOAuth2Response { *; }
# okhttp rules
# see: https://github.com/square/okhttp/blob/master/okhttp/src/main/resources/META-INF/proguard/okhttp3.pro

View file

@ -18,7 +18,6 @@ public class AttachmentDownloadDialogFragment extends DialogFragment {
private static final String ARG_MESSAGE = "message";
private ProgressDialog dialog;
private MessagingListener messagingListener;
private MessagingController messagingController;
@ -42,6 +41,14 @@ public class AttachmentDownloadDialogFragment extends DialogFragment {
final SizeUnit sizeUnit = SizeUnit.getAppropriateFor(size);
ProgressDialog dialog = new ProgressDialog(getActivity());
dialog.setMessage(message);
dialog.setMax(sizeUnit.valueInSizeUnit(size));
dialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
dialog.setProgress(0);
dialog.setProgressNumberFormat("%1d/%2d " + sizeUnit.shortName);
dialog.show();
messagingListener = new SimpleMessagingListener() {
@Override
public void updateProgress(int progress) {
@ -52,14 +59,6 @@ public class AttachmentDownloadDialogFragment extends DialogFragment {
messagingController = MessagingController.getInstance(getActivity());
messagingController.addListener(messagingListener);
dialog = new ProgressDialog(getActivity());
dialog.setMessage(message);
dialog.setMax(sizeUnit.valueInSizeUnit(size));
dialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
dialog.setProgress(0);
dialog.setProgressNumberFormat("%1d/%2d " + sizeUnit.shortName);
dialog.show();
return dialog;
}

View file

@ -5,6 +5,15 @@
Locale-specific versions are kept in res/raw-<locale qualifier>/changelog.xml.
-->
<changelog>
<release version="6.801" versioncode="38001" date="2024-03-11">
<change>Added DNSSEC support when looking for server settings during setup</change>
<change>Made a change to prevent some software keyboards from capitalizing/auto-correcting email addresses in account setup</change>
<change>Fixed a crash when a very long subject was used</change>
<change>Fixed displaying OAuth 2.0 error messages</change>
<change>Fixed rare crash when downloading an attachment</change>
<change>Added code to disallow line breaks in single line text inputs</change>
<change>Updated translations</change>
</release>
<release version="6.800" versioncode="38000" date="2024-02-29">
<change>New and improved account setup</change>
<change>Added option to return to the message list after marking a message as unread in the message view</change>

View file

@ -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,

View file

@ -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),

View file

@ -22,13 +22,14 @@ fun TextFieldOutlinedEmailAddress(
) {
MaterialOutlinedTextField(
value = value,
onValueChange = onValueChange,
onValueChange = stripLineBreaks(onValueChange),
modifier = modifier,
enabled = isEnabled,
label = selectLabel(label, isRequired),
readOnly = isReadOnly,
isError = hasError,
keyboardOptions = KeyboardOptions(
autoCorrect = false,
keyboardType = KeyboardType.Email,
),
singleLine = true,

View file

@ -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),

View file

@ -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")
}
}

View file

@ -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")
}
}

View file

@ -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),

View file

@ -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,
)
},
),
)
}
}

View file

@ -1,23 +1,29 @@
package com.fsck.k9.mail.internet
import org.apache.james.mime4j.util.MimeUtil
object MimeHeaderEncoder {
@JvmStatic
fun encode(name: String, value: String): String {
// TODO: Fold long text that provides enough opportunities for folding and doesn't contain any characters that
// need to be encoded.
return if (hasToBeEncoded(name, value)) {
EncoderUtil.encodeEncodedWord(value)
// Number of characters already used up on the first line (header field name + colon + space)
val usedCharacters = name.length + COLON_PLUS_SPACE_LENGTH
return if (hasToBeEncoded(value, usedCharacters)) {
MimeUtil.fold(EncoderUtil.encodeEncodedWord(value), usedCharacters)
} else {
value
}
}
private fun hasToBeEncoded(name: String, value: String): Boolean {
return exceedsRecommendedLineLength(name, value) || charactersNeedEncoding(value)
private fun hasToBeEncoded(value: String, usedCharacters: Int): Boolean {
return exceedsRecommendedLineLength(value, usedCharacters) || charactersNeedEncoding(value)
}
private fun exceedsRecommendedLineLength(name: String, value: String): Boolean {
return name.length + COLON_PLUS_SPACE_LENGTH + value.length > RECOMMENDED_MAX_LINE_LENGTH
private fun exceedsRecommendedLineLength(value: String, usedCharacters: Int): Boolean {
return usedCharacters + value.length > RECOMMENDED_MAX_LINE_LENGTH
}
private fun charactersNeedEncoding(text: String): Boolean {

View file

@ -0,0 +1,75 @@
package com.fsck.k9.mail.internet
import assertk.Assert
import assertk.all
import assertk.assertThat
import assertk.assertions.each
import assertk.assertions.isEqualTo
import assertk.assertions.isLessThanOrEqualTo
import assertk.assertions.length
import assertk.fail
import kotlin.test.Test
class MimeHeaderEncoderTest {
@Test
fun `short subject containing only ASCII characters should not be encoded`() {
val result = MimeHeaderEncoder.encode(name = "Subject", value = "Hello World!")
assertThat(result).isEqualTo("Hello World!")
}
@Test
fun `short subject containing non-ASCII characters should be encoded`() {
val result = MimeHeaderEncoder.encode(name = "Subject", value = "Gänseblümchen")
assertThat(result).isEqualTo("=?UTF-8?Q?G=C3=A4nsebl=C3=BCmchen?=")
}
@Test
fun `subject with recommended line length should not be folded`() {
val subject = "a".repeat(RECOMMENDED_MAX_LINE_LENGTH - "Subject: ".length)
val result = MimeHeaderEncoder.encode(name = "Subject", value = subject)
assertThat(result).isEqualTo(subject)
}
@Test
fun `subject exceeding recommended line length should be folded`() {
val subject = "a".repeat(34) + " " + "a".repeat(35)
val result = MimeHeaderEncoder.encode(name = "Subject", value = subject)
assertThat(result).all {
transform { it.lines() }.each {
it.length().isLessThanOrEqualTo(RECOMMENDED_MAX_LINE_LENGTH)
}
isValidHeader(name = "Subject")
decodesTo(subject)
}
}
@Test
fun `subject exceeding maximum line length should be encoded`() {
val subject = "a".repeat(999)
val result = MimeHeaderEncoder.encode(name = "Subject", value = subject)
assertThat(result).all {
isValidHeader(name = "Subject")
decodesTo(subject)
}
}
private fun Assert<String>.isValidHeader(name: String) = given { value ->
try {
MimeHeaderChecker.checkHeader(name, value)
} catch (e: MimeHeaderParserException) {
fail(AssertionError("Not a valid RFC5322 header", e))
}
}
private fun Assert<String>.decodesTo(expected: String) = given { value ->
assertThat(MimeUtility.unfoldAndDecode(value)).isEqualTo(expected)
}
}