Use EmailAddressParser
for validating email address in account setup
This commit is contained in:
parent
907e315f7d
commit
3fb37a0873
4 changed files with 134 additions and 19 deletions
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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")
|
||||
|
|
Loading…
Reference in a new issue