Use EmailAddressParser for validating email address in account setup

This commit is contained in:
cketti 2023-11-07 16:54:59 +01:00
parent 907e315f7d
commit 3fb37a0873
4 changed files with 134 additions and 19 deletions

View file

@ -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)
}
!EMAIL_ADDRESS.matches(emailAddress) -> ValidationResult.Failure(
ValidateEmailAddressError.InvalidEmailAddress,
)
return try {
val parsedEmailAddress = emailAddress.toUserEmailAddress()
else -> ValidationResult.Success
if (parsedEmailAddress.warnings.isEmpty()) {
ValidationResult.Success
} else {
ValidationResult.Failure(ValidateEmailAddressError.NotAllowed)
}
} catch (e: EmailAddressParserException) {
Timber.v(e, "Error parsing email address: %s", emailAddress)
val validationError = when (e.error) {
EmailAddressParserError.AddressLiteralsNotSupported,
EmailAddressParserError.LocalPartLengthExceeded,
EmailAddressParserError.DnsLabelLengthExceeded,
EmailAddressParserError.DomainLengthExceeded,
EmailAddressParserError.TotalLengthExceeded,
EmailAddressParserError.QuotedStringInLocalPart,
EmailAddressParserError.LocalPartRequiresQuotedString,
EmailAddressParserError.EmptyLocalPart,
-> {
ValidateEmailAddressError.NotAllowed
}
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()
}
}

View file

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

View file

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

View file

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