0
0
mirror of https://github.com/thunderbird/thunderbird-android.git synced 2024-09-20 12:12:15 +02:00

Merge pull request #6962 from thundernest/UserInputEmailAddressParser

Add `UserInputEmailAddressParser`
This commit is contained in:
cketti 2023-06-09 18:49:39 +02:00 committed by GitHub
commit f649a6a7d9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 200 additions and 10 deletions

View File

@ -16,4 +16,5 @@ val viewModule = module {
K9WebViewClient(clipboardManager = get(), attachmentResolver, onPageFinishedListener)
}
factory { WebViewClientFactory() }
factory { UserInputEmailAddressParser() }
}

View File

@ -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<Recipient> 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<Recipient> implem
@Override
protected Recipient defaultObject(String completionText) {
Address[] parsedAddresses = Address.parse(completionText);
if (!CharsetUtil.isASCII(completionText)) {
setError(getContext().getString(R.string.recipient_error_non_ascii));
return null;
}
if (parsedAddresses.length == 0 || parsedAddresses[0].getAddress() == null) {
try {
List<Address> parsedAddresses = emailAddressParser.parse(completionText);
if (parsedAddresses.isEmpty()) {
setError(getContext().getString(R.string.recipient_error_parse_failed));
return null;
}
return new Recipient(parsedAddresses[0]);
return new Recipient(parsedAddresses.get(0));
} catch (NonAsciiEmailAddressException e) {
setError(getContext().getString(R.string.recipient_error_non_ascii));
return null;
}
}
public void setLoaderManager(@Nullable LoaderManager loaderManager) {

View File

@ -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<Address> {
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)

View File

@ -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("<user@domain.example>")
assertThat(addresses).containsExactly(Address("user@domain.example"))
}
@Test
fun `simple name and address`() {
val addresses = parser.parse("Name <user@domain.example>")
assertThat(addresses).containsExactly(Address("user@domain.example", "Name"))
}
@Test
fun `name with quoted string and address`() {
val addresses = parser.parse("\"Name\" <user@domain.example>")
assertThat(addresses).containsExactly(Address("user@domain.example", "Name"))
}
@Test
fun `name with multiple words and address`() {
val addresses = parser.parse("Firstname Lastname <user@domain.example>")
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 <user@domain.example>")
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<NonAsciiEmailAddressException>()
}
@Test
fun `address with non-ASCII character in domain part`() {
assertFailure {
parser.parse("user@dömain.example")
}.isInstanceOf<NonAsciiEmailAddressException>()
}
@Test
fun `multiple addresses separated by comma`() {
val addresses = parser.parse("one@domain.example,<two@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("<one@domain.example>, <two@domain.example>")
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 <user")
assertThat(addresses).isEmpty()
}
@Test
@Ignore("Currently not supported")
fun `name followed by address not in angle brackets`() {
val addresses = parser.parse("Firstname Lastname user@domain.example")
assertThat(addresses).containsExactly(Address("user@domain.example", "Firstname LastName"))
}
@Test
@Ignore("Currently not supported")
fun `name containing parenthesis`() {
val addresses = parser.parse("Firstname (Nickname) Lastname <user@domain.example>")
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 <user@domain.example>")
assertThat(addresses).containsExactly(Address("user@domain.example", "Firstname \"Nickname\" LastName"))
}
}

View File

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