Loading feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/domain/usecase/ValidateEmailAddress.kt +54 −12 Original line number Diff line number Diff line Loading @@ -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() } } feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/ui/autodiscovery/AutoDiscoveryStringMapper.kt +14 −6 Original line number Diff line number Diff line Loading @@ -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) } } } Loading feature/account/setup/src/main/res/values/strings.xml +3 −1 Original line number Diff line number Diff line Loading @@ -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> Loading feature/account/setup/src/test/kotlin/app/k9mail/feature/account/setup/domain/usecase/ValidateEmailAddressTest.kt +63 −0 Original line number Diff line number Diff line Loading @@ -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 Loading
feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/domain/usecase/ValidateEmailAddress.kt +54 −12 Original line number Diff line number Diff line Loading @@ -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() } }
feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/ui/autodiscovery/AutoDiscoveryStringMapper.kt +14 −6 Original line number Diff line number Diff line Loading @@ -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) } } } Loading
feature/account/setup/src/main/res/values/strings.xml +3 −1 Original line number Diff line number Diff line Loading @@ -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> Loading
feature/account/setup/src/test/kotlin/app/k9mail/feature/account/setup/domain/usecase/ValidateEmailAddressTest.kt +63 −0 Original line number Diff line number Diff line Loading @@ -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