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); public <init>(android.content.Context);
} }
-keep class com.fsck.k9.mail.oauth.XOAuth2Response { *; }
# okhttp rules # okhttp rules
# see: https://github.com/square/okhttp/blob/master/okhttp/src/main/resources/META-INF/proguard/okhttp3.pro # 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 static final String ARG_MESSAGE = "message";
private ProgressDialog dialog;
private MessagingListener messagingListener; private MessagingListener messagingListener;
private MessagingController messagingController; private MessagingController messagingController;
@ -42,6 +41,14 @@ public class AttachmentDownloadDialogFragment extends DialogFragment {
final SizeUnit sizeUnit = SizeUnit.getAppropriateFor(size); 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() { messagingListener = new SimpleMessagingListener() {
@Override @Override
public void updateProgress(int progress) { public void updateProgress(int progress) {
@ -52,14 +59,6 @@ public class AttachmentDownloadDialogFragment extends DialogFragment {
messagingController = MessagingController.getInstance(getActivity()); messagingController = MessagingController.getInstance(getActivity());
messagingController.addListener(messagingListener); 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; return dialog;
} }

View file

@ -5,6 +5,15 @@
Locale-specific versions are kept in res/raw-<locale qualifier>/changelog.xml. Locale-specific versions are kept in res/raw-<locale qualifier>/changelog.xml.
--> -->
<changelog> <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"> <release version="6.800" versioncode="38000" date="2024-02-29">
<change>New and improved account setup</change> <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> <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 androidx.compose.ui.tooling.preview.Preview
import app.k9mail.core.ui.compose.theme.PreviewWithThemes 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( internal fun selectLabel(
label: String?, label: String?,
isRequired: Boolean, isRequired: Boolean,

View file

@ -26,7 +26,7 @@ fun TextFieldOutlined(
) { ) {
MaterialOutlinedTextField( MaterialOutlinedTextField(
value = value, value = value,
onValueChange = onValueChange, onValueChange = if (isSingleLine) stripLineBreaks(onValueChange) else onValueChange,
modifier = modifier, modifier = modifier,
enabled = isEnabled, enabled = isEnabled,
label = selectLabel(label, isRequired), label = selectLabel(label, isRequired),

View file

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

View file

@ -35,7 +35,7 @@ fun TextFieldOutlinedPassword(
MaterialOutlinedTextField( MaterialOutlinedTextField(
value = value, value = value,
onValueChange = onValueChange, onValueChange = stripLineBreaks(onValueChange),
modifier = modifier, modifier = modifier,
enabled = isEnabled, enabled = isEnabled,
label = selectLabel(label, isRequired), label = selectLabel(label, isRequired),
@ -70,7 +70,7 @@ fun TextFieldOutlinedPassword(
) { ) {
MaterialOutlinedTextField( MaterialOutlinedTextField(
value = value, value = value,
onValueChange = onValueChange, onValueChange = stripLineBreaks(onValueChange),
modifier = modifier, modifier = modifier,
enabled = isEnabled, enabled = isEnabled,
label = selectLabel(label, isRequired), 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 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.SemanticsNodeInteraction
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick 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.designsystem.R
import app.k9mail.core.ui.compose.testing.ComposeTest import app.k9mail.core.ui.compose.testing.ComposeTest
import assertk.assertThat import assertk.assertThat
import assertk.assertions.isEqualTo
import assertk.assertions.isTrue import assertk.assertions.isTrue
import org.junit.Test import org.junit.Test
private const val PASSWORD = "Password input" private const val PASSWORD = "Password input"
private const val TEST_TAG = "TextFieldOutlinedPassword"
class TextFieldOutlinedPasswordKtTest : ComposeTest() { class TextFieldOutlinedPasswordKtTest : ComposeTest() {
@ -131,6 +137,78 @@ class TextFieldOutlinedPasswordKtTest : ComposeTest() {
onNodeWithText(PASSWORD).assertDoesNotExist() 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 { private fun SemanticsNodeInteractionsProvider.onShowPasswordNode(): SemanticsNodeInteraction {
return onNodeWithContentDescription( return onNodeWithContentDescription(
getString(R.string.designsystem_atom_password_textfield_show_password), 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 package com.fsck.k9.mail.internet
import org.apache.james.mime4j.util.MimeUtil
object MimeHeaderEncoder { object MimeHeaderEncoder {
@JvmStatic @JvmStatic
fun encode(name: String, value: String): String { fun encode(name: String, value: String): String {
// TODO: Fold long text that provides enough opportunities for folding and doesn't contain any characters that // TODO: Fold long text that provides enough opportunities for folding and doesn't contain any characters that
// need to be encoded. // 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 { } else {
value value
} }
} }
private fun hasToBeEncoded(name: String, value: String): Boolean { private fun hasToBeEncoded(value: String, usedCharacters: Int): Boolean {
return exceedsRecommendedLineLength(name, value) || charactersNeedEncoding(value) return exceedsRecommendedLineLength(value, usedCharacters) || charactersNeedEncoding(value)
} }
private fun exceedsRecommendedLineLength(name: String, value: String): Boolean { private fun exceedsRecommendedLineLength(value: String, usedCharacters: Int): Boolean {
return name.length + COLON_PLUS_SPACE_LENGTH + value.length > RECOMMENDED_MAX_LINE_LENGTH return usedCharacters + value.length > RECOMMENDED_MAX_LINE_LENGTH
} }
private fun charactersNeedEncoding(text: String): Boolean { 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)
}
}