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

Issue:
Crash when creating a new email account with a & ( Ampersand ) 
### 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.711(37011)

### Where did you get the app from?

F-Droid

### Android version

eOs 1.16 / android 12

### Device model

Fairphone 3

### Steps to reproduce

1. Go to the start screen (first launch) 
2. Click on start and type an email adresse with a & in it like you&me@email.org 
3. the app crash 

### Expected behavior

The app should go to the next screen

### Actual behavior

The app crash

Here in the file the logs from Logcat 
[k9-crash.txt](https://github.com/thunderbird/thunderbird-android/files/13270305/k9-crash.txt)


### Logs

_No response_


Discussion hints:
nan

Pull request: Use `EmailAddressParser` for validating email address in account setup
Fixes #7327

Test command: ./gradlew :feature:account:setup:testDebugUnitTest --tests "app.k9mail.feature.account.setup.domain.usecase.ValidateEmailAddressTest"

Fix patch:
diff --git a/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/domain/usecase/ValidateEmailAddress.kt b/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/domain/usecase/ValidateEmailAddress.kt
index 3370de6bedf..061b91daf6b 100644
--- a/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/domain/usecase/ValidateEmailAddress.kt
+++ b/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/domain/usecase/ValidateEmailAddress.kt
@@ -2,30 +2,72 @@ package app.k9mail.feature.account.setup.domain.usecase
 
 import app.k9mail.core.common.domain.usecase.validation.ValidationError
 import app.k9mail.core.common.domain.usecase.validation.ValidationResult
+import app.k9mail.core.common.mail.EmailAddressParserError
+import app.k9mail.core.common.mail.EmailAddressParserException
+import app.k9mail.core.common.mail.toEmailAddressOrNull
+import app.k9mail.core.common.mail.toUserEmailAddress
 import app.k9mail.feature.account.setup.domain.DomainContract.UseCase
+import com.fsck.k9.logging.Timber
 
+/**
+ * Validate an email address that the user wants to add to an account.
+ *
+ * This only allows a subset of all valid email addresses. We currently don't support international email addresses
+ * and don't allow quoted local parts, or email addresses exceeding length restrictions.
+ *
+ * Note: Do NOT use this to validate recipients in incoming or outgoing messages. Use [String.toEmailAddressOrNull]
+ * instead.
+ */
 class ValidateEmailAddress : UseCase.ValidateEmailAddress {
 
-    // TODO replace by new email validation
     override fun execute(emailAddress: String): ValidationResult {
-        return when {
-            emailAddress.isBlank() -> ValidationResult.Failure(ValidateEmailAddressError.EmptyEmailAddress)
+        if (emailAddress.isBlank()) {
+            return ValidationResult.Failure(ValidateEmailAddressError.EmptyEmailAddress)
+        }
+
+        return try {
+            val parsedEmailAddress = emailAddress.toUserEmailAddress()
+
+            if (parsedEmailAddress.warnings.isEmpty()) {
+                ValidationResult.Success
+            } else {
+                ValidationResult.Failure(ValidateEmailAddressError.NotAllowed)
+            }
+        } catch (e: EmailAddressParserException) {
+            Timber.v(e, "Error parsing email address: %s", emailAddress)
 
-            !EMAIL_ADDRESS.matches(emailAddress) -> ValidationResult.Failure(
-                ValidateEmailAddressError.InvalidEmailAddress,
-            )
+            val validationError = when (e.error) {
+                EmailAddressParserError.AddressLiteralsNotSupported,
+                EmailAddressParserError.LocalPartLengthExceeded,
+                EmailAddressParserError.DnsLabelLengthExceeded,
+                EmailAddressParserError.DomainLengthExceeded,
+                EmailAddressParserError.TotalLengthExceeded,
+                EmailAddressParserError.QuotedStringInLocalPart,
+                EmailAddressParserError.LocalPartRequiresQuotedString,
+                EmailAddressParserError.EmptyLocalPart,
+                -> {
+                    ValidateEmailAddressError.NotAllowed
+                }
 
-            else -> ValidationResult.Success
+                else -> {
+                    if ('@' in emailAddress) {
+                        // We currently don't support or recognize international email addresses. So if the string
+                        // contains an "@" character, we assume it's a valid email address that we don't support.
+                        ValidateEmailAddressError.InvalidOrNotSupported
+                    } else {
+                        ValidateEmailAddressError.InvalidEmailAddress
+                    }
+                }
+            }
+
+            ValidationResult.Failure(validationError)
         }
     }
 
     sealed interface ValidateEmailAddressError : ValidationError {
         object EmptyEmailAddress : ValidateEmailAddressError
+        object NotAllowed : ValidateEmailAddressError
+        object InvalidOrNotSupported : ValidateEmailAddressError
         object InvalidEmailAddress : ValidateEmailAddressError
     }
-
-    private companion object {
-        val EMAIL_ADDRESS =
-            "[a-zA-Z0-9+._%\\-]{1,256}@[a-zA-Z0-9][a-zA-Z0-9\\-]{0,64}(\\.[a-zA-Z0-9][a-zA-Z0-9\\-]{0,25})+".toRegex()
-    }
 }
diff --git a/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/ui/autodiscovery/AutoDiscoveryStringMapper.kt b/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/ui/autodiscovery/AutoDiscoveryStringMapper.kt
index 9ec6756f5f9..e7117c2bb1b 100644
--- a/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/ui/autodiscovery/AutoDiscoveryStringMapper.kt
+++ b/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/ui/autodiscovery/AutoDiscoveryStringMapper.kt
@@ -39,13 +39,21 @@ internal fun ValidationError.toResourceString(resources: Resources): String {
 
 private fun ValidateEmailAddress.ValidateEmailAddressError.toEmailAddressErrorString(resources: Resources): String {
     return when (this) {
-        is ValidateEmailAddress.ValidateEmailAddressError.EmptyEmailAddress -> resources.getString(
-            R.string.account_setup_auto_discovery_validation_error_email_address_required,
-        )
+        ValidateEmailAddress.ValidateEmailAddressError.EmptyEmailAddress -> {
+            resources.getString(R.string.account_setup_auto_discovery_validation_error_email_address_required)
+        }
 
-        is ValidateEmailAddress.ValidateEmailAddressError.InvalidEmailAddress -> resources.getString(
-            R.string.account_setup_auto_discovery_validation_error_email_address_invalid,
-        )
+        ValidateEmailAddress.ValidateEmailAddressError.NotAllowed -> {
+            resources.getString(R.string.account_setup_auto_discovery_validation_error_email_address_not_allowed)
+        }
+
+        ValidateEmailAddress.ValidateEmailAddressError.InvalidOrNotSupported -> {
+            resources.getString(R.string.account_setup_auto_discovery_validation_error_email_address_not_supported)
+        }
+
+        ValidateEmailAddress.ValidateEmailAddressError.InvalidEmailAddress -> {
+            resources.getString(R.string.account_setup_auto_discovery_validation_error_email_address_invalid)
+        }
     }
 }
 
diff --git a/feature/account/setup/src/main/res/values/strings.xml b/feature/account/setup/src/main/res/values/strings.xml
index 1291caca638..fa429793dbe 100644
--- a/feature/account/setup/src/main/res/values/strings.xml
+++ b/feature/account/setup/src/main/res/values/strings.xml
@@ -7,7 +7,9 @@
     <string name="account_setup_error_unknown">Unknown error</string>
 
     <string name="account_setup_auto_discovery_validation_error_email_address_required">Email address is required.</string>
-    <string name="account_setup_auto_discovery_validation_error_email_address_invalid">Email address is invalid.</string>
+    <string name="account_setup_auto_discovery_validation_error_email_address_not_allowed">This email address is not allowed.</string>
+    <string name="account_setup_auto_discovery_validation_error_email_address_not_supported">This email address is not supported.</string>
+    <string name="account_setup_auto_discovery_validation_error_email_address_invalid">This is not recognized as a valid email address.</string>
 
     <string name="account_setup_auto_discovery_connection_security_start_tls">StartTLS</string>
     <string name="account_setup_auto_discovery_connection_security_ssl">SSL/TLS</string>


Test patch:
diff --git a/feature/account/setup/src/test/kotlin/app/k9mail/feature/account/setup/domain/usecase/ValidateEmailAddressTest.kt b/feature/account/setup/src/test/kotlin/app/k9mail/feature/account/setup/domain/usecase/ValidateEmailAddressTest.kt
index c00d35ed701..431cfffdccc 100644
--- a/feature/account/setup/src/test/kotlin/app/k9mail/feature/account/setup/domain/usecase/ValidateEmailAddressTest.kt
+++ b/feature/account/setup/src/test/kotlin/app/k9mail/feature/account/setup/domain/usecase/ValidateEmailAddressTest.kt
@@ -27,6 +27,69 @@ class ValidateEmailAddressTest {
             .isInstanceOf<ValidateEmailAddressError.EmptyEmailAddress>()
     }
 
+    @Test
+    fun `should fail when email address is using unnecessary quoting in local part`() {
+        val result = testSubject.execute("\"local-part\"@domain.example")
+
+        assertThat(result).isInstanceOf<ValidationResult.Failure>()
+            .prop(ValidationResult.Failure::error)
+            .isInstanceOf<ValidateEmailAddressError.NotAllowed>()
+    }
+
+    @Test
+    fun `should fail when email address requires quoted local part`() {
+        val result = testSubject.execute("\"local part\"@domain.example")
+
+        assertThat(result).isInstanceOf<ValidationResult.Failure>()
+            .prop(ValidationResult.Failure::error)
+            .isInstanceOf<ValidateEmailAddressError.NotAllowed>()
+    }
+
+    @Test
+    fun `should fail when local part is empty`() {
+        val result = testSubject.execute("\"\"@domain.example")
+
+        assertThat(result).isInstanceOf<ValidationResult.Failure>()
+            .prop(ValidationResult.Failure::error)
+            .isInstanceOf<ValidateEmailAddressError.NotAllowed>()
+    }
+
+    @Test
+    fun `should fail when domain part contains IPv4 literal`() {
+        val result = testSubject.execute("user@[255.0.100.23]")
+
+        assertThat(result).isInstanceOf<ValidationResult.Failure>()
+            .prop(ValidationResult.Failure::error)
+            .isInstanceOf<ValidateEmailAddressError.NotAllowed>()
+    }
+
+    @Test
+    fun `should fail when domain part contains IPv6 literal`() {
+        val result = testSubject.execute("user@[IPv6:2001:0db8:0000:0000:0000:ff00:0042:8329]")
+
+        assertThat(result).isInstanceOf<ValidationResult.Failure>()
+            .prop(ValidationResult.Failure::error)
+            .isInstanceOf<ValidateEmailAddressError.NotAllowed>()
+    }
+
+    @Test
+    fun `should fail when local part contains non-ASCII character`() {
+        val result = testSubject.execute("töst@domain.example")
+
+        assertThat(result).isInstanceOf<ValidationResult.Failure>()
+            .prop(ValidationResult.Failure::error)
+            .isInstanceOf<ValidateEmailAddressError.InvalidOrNotSupported>()
+    }
+
+    @Test
+    fun `should fail when domain contains non-ASCII character`() {
+        val result = testSubject.execute("test@dömain.example")
+
+        assertThat(result).isInstanceOf<ValidationResult.Failure>()
+            .prop(ValidationResult.Failure::error)
+            .isInstanceOf<ValidateEmailAddressError.InvalidOrNotSupported>()
+    }
+
     @Test
     fun `should fail when email address is invalid`() {
         val result = testSubject.execute("test")