From 3bf19ca3f61a4cb0123ba80c1d413861736f0443 Mon Sep 17 00:00:00 2001 From: cketti Date: Wed, 7 Jun 2023 18:02:29 +0200 Subject: [PATCH] Add `UserInputEmailAddressParser` Used to parse name and email address pairs entered by the user when composing a message. --- .../main/java/com/fsck/k9/view/KoinModule.kt | 1 + .../com/fsck/k9/view/RecipientSelectView.java | 22 +-- .../k9/view/UserInputEmailAddressParser.kt | 31 ++++ .../view/UserInputEmailAddressParserTest.kt | 153 ++++++++++++++++++ .../main/java/com/fsck/k9/mail/Address.java | 3 +- 5 files changed, 200 insertions(+), 10 deletions(-) create mode 100644 app/ui/legacy/src/main/java/com/fsck/k9/view/UserInputEmailAddressParser.kt create mode 100644 app/ui/legacy/src/test/java/com/fsck/k9/view/UserInputEmailAddressParserTest.kt diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/view/KoinModule.kt b/app/ui/legacy/src/main/java/com/fsck/k9/view/KoinModule.kt index 39f7fd1a65..89a209b7c2 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/view/KoinModule.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/view/KoinModule.kt @@ -16,4 +16,5 @@ val viewModule = module { K9WebViewClient(clipboardManager = get(), attachmentResolver, onPageFinishedListener) } factory { WebViewClientFactory() } + factory { UserInputEmailAddressParser() } } diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/view/RecipientSelectView.java b/app/ui/legacy/src/main/java/com/fsck/k9/view/RecipientSelectView.java index 1cfd241861..6505c34f53 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/view/RecipientSelectView.java +++ b/app/ui/legacy/src/main/java/com/fsck/k9/view/RecipientSelectView.java @@ -33,6 +33,7 @@ import android.widget.ListPopupWindow; import android.widget.ListView; import android.widget.TextView; +import com.fsck.k9.DI; import com.fsck.k9.K9; import com.fsck.k9.ui.R; import com.fsck.k9.activity.AlternateRecipientAdapter; @@ -43,7 +44,6 @@ import com.fsck.k9.mail.Address; import com.fsck.k9.ui.compose.RecipientCircleImageView; import com.fsck.k9.view.RecipientSelectView.Recipient; import com.tokenautocomplete.TokenCompleteTextView; -import org.apache.james.mime4j.util.CharsetUtil; import timber.log.Timber; import de.hdodenhof.circleimageview.CircleImageView; @@ -61,6 +61,8 @@ public class RecipientSelectView extends TokenCompleteTextView implem private static final int LOADER_ID_ALTERNATES = 1; + private final UserInputEmailAddressParser emailAddressParser = DI.get(UserInputEmailAddressParser.class); + private RecipientAdapter adapter; @Nullable private String cryptoProvider; @@ -171,17 +173,19 @@ public class RecipientSelectView extends TokenCompleteTextView implem @Override protected Recipient defaultObject(String completionText) { - Address[] parsedAddresses = Address.parse(completionText); - if (!CharsetUtil.isASCII(completionText)) { + try { + List
parsedAddresses = emailAddressParser.parse(completionText); + + if (parsedAddresses.isEmpty()) { + setError(getContext().getString(R.string.recipient_error_parse_failed)); + return null; + } + + return new Recipient(parsedAddresses.get(0)); + } catch (NonAsciiEmailAddressException e) { setError(getContext().getString(R.string.recipient_error_non_ascii)); return null; } - if (parsedAddresses.length == 0 || parsedAddresses[0].getAddress() == null) { - setError(getContext().getString(R.string.recipient_error_parse_failed)); - return null; - } - - return new Recipient(parsedAddresses[0]); } public void setLoaderManager(@Nullable LoaderManager loaderManager) { diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/view/UserInputEmailAddressParser.kt b/app/ui/legacy/src/main/java/com/fsck/k9/view/UserInputEmailAddressParser.kt new file mode 100644 index 0000000000..a14cb14bdb --- /dev/null +++ b/app/ui/legacy/src/main/java/com/fsck/k9/view/UserInputEmailAddressParser.kt @@ -0,0 +1,31 @@ +package com.fsck.k9.view + +import com.fsck.k9.mail.Address +import org.apache.james.mime4j.util.CharsetUtil + +/** + * Used to parse name & email address pairs entered by the user. + * + * TODO: Build a custom implementation that can deal with typical inputs from users who are not familiar with the + * RFC 5322 address-list syntax. See (ignored) tests in `UserInputEmailAddressParserTest`. + */ +internal class UserInputEmailAddressParser { + + @Throws(NonAsciiEmailAddressException::class) + fun parse(input: String): List
{ + return Address.parseUnencoded(input) + .mapNotNull { address -> + when { + address.isIncomplete() -> null + address.isNonAsciiAddress() -> throw NonAsciiEmailAddressException(address.address) + else -> Address.parse(address.toEncodedString()).firstOrNull() + } + } + } + + private fun Address.isIncomplete() = hostname.isNullOrBlank() + + private fun Address.isNonAsciiAddress() = !CharsetUtil.isASCII(address) +} + +internal class NonAsciiEmailAddressException(message: String) : Exception(message) diff --git a/app/ui/legacy/src/test/java/com/fsck/k9/view/UserInputEmailAddressParserTest.kt b/app/ui/legacy/src/test/java/com/fsck/k9/view/UserInputEmailAddressParserTest.kt new file mode 100644 index 0000000000..f1ecb2d605 --- /dev/null +++ b/app/ui/legacy/src/test/java/com/fsck/k9/view/UserInputEmailAddressParserTest.kt @@ -0,0 +1,153 @@ +package com.fsck.k9.view + +import assertk.assertFailure +import assertk.assertThat +import assertk.assertions.containsExactly +import assertk.assertions.isEmpty +import assertk.assertions.isInstanceOf +import com.fsck.k9.mail.Address +import kotlin.test.Ignore +import kotlin.test.Test + +class UserInputEmailAddressParserTest { + private val parser = UserInputEmailAddressParser() + + @Test + fun `plain email address`() { + val addresses = parser.parse("user@domain.example") + + assertThat(addresses).containsExactly(Address("user@domain.example")) + } + + @Test + fun `email address followed by space`() { + val addresses = parser.parse("user@domain.example ") + + assertThat(addresses).containsExactly(Address("user@domain.example")) + } + + @Test + fun `email address in angle brackets`() { + val addresses = parser.parse("") + + assertThat(addresses).containsExactly(Address("user@domain.example")) + } + + @Test + fun `simple name and address`() { + val addresses = parser.parse("Name ") + + assertThat(addresses).containsExactly(Address("user@domain.example", "Name")) + } + + @Test + fun `name with quoted string and address`() { + val addresses = parser.parse("\"Name\" ") + + assertThat(addresses).containsExactly(Address("user@domain.example", "Name")) + } + + @Test + fun `name with multiple words and address`() { + val addresses = parser.parse("Firstname Lastname ") + + assertThat(addresses).containsExactly(Address("user@domain.example", "Firstname Lastname")) + } + + @Test + fun `name with non-ASCII characters and address`() { + val addresses = parser.parse("Käthe Gehäusegröße ") + + assertThat(addresses).containsExactly(Address("user@domain.example", "Käthe Gehäusegröße")) + } + + @Test + fun `address with non-ASCII character in local part`() { + assertFailure { + parser.parse("müller@domain.example") + }.isInstanceOf() + } + + @Test + fun `address with non-ASCII character in domain part`() { + assertFailure { + parser.parse("user@dömain.example") + }.isInstanceOf() + } + + @Test + fun `multiple addresses separated by comma`() { + val addresses = parser.parse("one@domain.example,") + + assertThat(addresses).containsExactly( + Address("one@domain.example"), + Address("two@domain.example"), + ) + } + + @Test + @Ignore("Currently not supported") + fun `multiple addresses separated by space`() { + val addresses = parser.parse("one@domain.example two@domain.example") + + assertThat(addresses).containsExactly( + Address("one@domain.example"), + Address("two@domain.example"), + ) + } + + @Test + fun `multiple addresses in angle brackets separated by space`() { + val addresses = parser.parse(", ") + + assertThat(addresses).containsExactly( + Address("one@domain.example"), + Address("two@domain.example"), + ) + } + + @Test + fun `incomplete address should not return a result`() { + val addresses = parser.parse("user") + + assertThat(addresses).isEmpty() + } + + @Test + fun `incomplete address ending in @ should not return a result`() { + val addresses = parser.parse("user@") + + assertThat(addresses).isEmpty() + } + + @Test + fun `name and incomplete address should not return a result`() { + val addresses = parser.parse("Name ") + + assertThat(addresses).containsExactly(Address("user@domain.example", "Firstname (Nickname) LastName")) + } + + @Test + @Ignore("Currently not supported") + fun `name containing double quotes in the middle`() { + val addresses = parser.parse("Firstname \"Nickname\" Lastname ") + + assertThat(addresses).containsExactly(Address("user@domain.example", "Firstname \"Nickname\" LastName")) + } +} diff --git a/mail/common/src/main/java/com/fsck/k9/mail/Address.java b/mail/common/src/main/java/com/fsck/k9/mail/Address.java index fd272638fa..8bc3eaa1d2 100644 --- a/mail/common/src/main/java/com/fsck/k9/mail/Address.java +++ b/mail/common/src/main/java/com/fsck/k9/mail/Address.java @@ -115,7 +115,8 @@ public class Address implements Serializable { for (Rfc822Token token : tokens) { String address = token.getAddress(); if (!TextUtils.isEmpty(address)) { - addresses.add(new Address(token.getAddress(), token.getName(), false)); + String name = TextUtils.isEmpty(token.getName()) ? null : token.getName(); + addresses.add(new Address(token.getAddress(), name, false)); } } }