diff --git a/app/core/src/main/java/com/fsck/k9/helper/ContactNameProvider.kt b/app/core/src/main/java/com/fsck/k9/helper/ContactNameProvider.kt index 5ed06cdbf..1e354c987 100644 --- a/app/core/src/main/java/com/fsck/k9/helper/ContactNameProvider.kt +++ b/app/core/src/main/java/com/fsck/k9/helper/ContactNameProvider.kt @@ -1,7 +1,7 @@ package com.fsck.k9.helper import app.k9mail.core.android.common.contact.ContactRepository -import app.k9mail.core.common.mail.toEmailAddress +import app.k9mail.core.common.mail.toEmailAddressOrNull interface ContactNameProvider { fun getNameForAddress(address: String): String? @@ -11,6 +11,8 @@ class RealContactNameProvider( private val contactRepository: ContactRepository, ) : ContactNameProvider { override fun getNameForAddress(address: String): String? { - return contactRepository.getContactFor(address.toEmailAddress())?.name + return address.toEmailAddressOrNull()?.let { emailAddress -> + contactRepository.getContactFor(emailAddress)?.name + } } } diff --git a/app/core/src/main/java/com/fsck/k9/helper/MessageHelper.kt b/app/core/src/main/java/com/fsck/k9/helper/MessageHelper.kt index 2fe1f5542..a36dc7790 100644 --- a/app/core/src/main/java/com/fsck/k9/helper/MessageHelper.kt +++ b/app/core/src/main/java/com/fsck/k9/helper/MessageHelper.kt @@ -6,7 +6,7 @@ import android.text.SpannableStringBuilder import android.text.TextUtils import android.text.style.ForegroundColorSpan import app.k9mail.core.android.common.contact.ContactRepository -import app.k9mail.core.common.mail.toEmailAddress +import app.k9mail.core.common.mail.toEmailAddressOrNull import com.fsck.k9.CoreResourceProvider import com.fsck.k9.K9.contactNameColor import com.fsck.k9.K9.isChangeContactNameColor @@ -101,7 +101,7 @@ class MessageHelper( if (!showCorrespondentNames) { return address.address } else if (contactRepository != null) { - val name = contactRepository.getContactFor(address.address.toEmailAddress())?.name + val name = contactRepository.getContactName(address) if (name != null) { return if (changeContactNameColor) { val coloredName = SpannableString(name) @@ -124,6 +124,12 @@ class MessageHelper( } } + private fun ContactRepository.getContactName(address: Address): String? { + return address.address.toEmailAddressOrNull()?.let { emailAddress -> + getContactFor(emailAddress)?.name + } + } + private fun isSpoofAddress(displayName: String): Boolean { return displayName.contains("@") && SPOOF_ADDRESS_PATTERN.matcher(displayName).find() } diff --git a/app/core/src/test/java/com/fsck/k9/helper/MessageHelperTest.kt b/app/core/src/test/java/com/fsck/k9/helper/MessageHelperTest.kt index 4aafc2e72..b34ab5d71 100644 --- a/app/core/src/test/java/com/fsck/k9/helper/MessageHelperTest.kt +++ b/app/core/src/test/java/com/fsck/k9/helper/MessageHelperTest.kt @@ -5,7 +5,7 @@ import android.text.SpannableString import app.k9mail.core.android.common.contact.Contact import app.k9mail.core.android.common.contact.ContactRepository import app.k9mail.core.common.mail.EmailAddress -import app.k9mail.core.common.mail.toEmailAddress +import app.k9mail.core.common.mail.toEmailAddressOrThrow import assertk.assertThat import assertk.assertions.isEqualTo import assertk.assertions.isInstanceOf @@ -143,6 +143,6 @@ class MessageHelperTest : RobolectricTest() { } private companion object { - val EMAIL_ADDRESS = "test@testor.com".toEmailAddress() + val EMAIL_ADDRESS = "test@testor.com".toEmailAddressOrThrow() } } diff --git a/app/k9mail/src/main/java/com/fsck/k9/notification/K9NotificationStrategy.kt b/app/k9mail/src/main/java/com/fsck/k9/notification/K9NotificationStrategy.kt index 77967e14e..005a8e125 100644 --- a/app/k9mail/src/main/java/com/fsck/k9/notification/K9NotificationStrategy.kt +++ b/app/k9mail/src/main/java/com/fsck/k9/notification/K9NotificationStrategy.kt @@ -1,7 +1,7 @@ package com.fsck.k9.notification import app.k9mail.core.android.common.contact.ContactRepository -import app.k9mail.core.common.mail.toEmailAddress +import app.k9mail.core.common.mail.toEmailAddressOrNull import com.fsck.k9.Account import com.fsck.k9.K9 import com.fsck.k9.mail.Flag @@ -88,7 +88,7 @@ class K9NotificationStrategy( } if (account.isNotifyContactsMailOnly && - !contactRepository.hasAnyContactFor(message.from.asList().mapNotNull { it.address.toEmailAddress() }) + !contactRepository.hasAnyContactFor(message.from.asList().mapNotNull { it.address.toEmailAddressOrNull() }) ) { Timber.v("No notification: Message is not from a known contact") return false diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/contacts/ContactPhotoLoader.kt b/app/ui/legacy/src/main/java/com/fsck/k9/contacts/ContactPhotoLoader.kt index ac7012219..363d4890c 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/contacts/ContactPhotoLoader.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/contacts/ContactPhotoLoader.kt @@ -3,8 +3,9 @@ package com.fsck.k9.contacts import android.content.ContentResolver import android.graphics.Bitmap import android.graphics.BitmapFactory +import android.net.Uri import app.k9mail.core.android.common.contact.ContactRepository -import app.k9mail.core.common.mail.toEmailAddress +import app.k9mail.core.common.mail.toEmailAddressOrNull import timber.log.Timber internal class ContactPhotoLoader( @@ -12,7 +13,7 @@ internal class ContactPhotoLoader( private val contactRepository: ContactRepository, ) { fun loadContactPhoto(emailAddress: String): Bitmap? { - val photoUri = contactRepository.getContactFor(emailAddress.toEmailAddress())?.photoUri ?: return null + val photoUri = getPhotoUri(emailAddress) ?: return null return try { contentResolver.openInputStream(photoUri).use { inputStream -> BitmapFactory.decodeStream(inputStream) @@ -22,4 +23,10 @@ internal class ContactPhotoLoader( null } } + + private fun getPhotoUri(email: String): Uri? { + return email.toEmailAddressOrNull()?.let { emailAddress -> + contactRepository.getContactFor(emailAddress)?.photoUri + } + } } diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagedetails/MessageDetailsViewModel.kt b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagedetails/MessageDetailsViewModel.kt index 060a2a5dd..63b46ed53 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagedetails/MessageDetailsViewModel.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagedetails/MessageDetailsViewModel.kt @@ -2,12 +2,13 @@ package com.fsck.k9.ui.messagedetails import android.app.PendingIntent import android.content.res.Resources +import android.net.Uri import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import app.k9mail.core.android.common.contact.CachingRepository import app.k9mail.core.android.common.contact.ContactPermissionResolver import app.k9mail.core.android.common.contact.ContactRepository -import app.k9mail.core.common.mail.toEmailAddress +import app.k9mail.core.common.mail.toEmailAddressOrNull import com.fsck.k9.Account import com.fsck.k9.controller.MessageReference import com.fsck.k9.helper.ClipboardManager @@ -30,6 +31,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.launch +@Suppress("TooManyFunctions") internal class MessageDetailsViewModel( private val resources: Resources, private val messageRepository: MessageRepository, @@ -130,11 +132,17 @@ internal class MessageDetailsViewModel( Participant( displayName = displayName, emailAddress = emailAddress, - contactLookupUri = contactRepository.getContactFor(emailAddress.toEmailAddress())?.uri, + contactLookupUri = getContactLookupUri(emailAddress), ) } } + private fun getContactLookupUri(email: String): Uri? { + return email.toEmailAddressOrNull()?.let { emailAddress -> + contactRepository.getContactFor(emailAddress)?.uri + } + } + private fun Folder.toFolderInfo(): FolderInfoUi { return FolderInfoUi( displayName = folderNameFormatter.displayName(this), diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageTopView.kt b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageTopView.kt index 85d771323..c55613365 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageTopView.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageTopView.kt @@ -18,7 +18,8 @@ import android.widget.LinearLayout import android.widget.ProgressBar import android.widget.TextView import app.k9mail.core.android.common.contact.ContactRepository -import app.k9mail.core.common.mail.toEmailAddress +import app.k9mail.core.common.mail.EmailAddress +import app.k9mail.core.common.mail.toEmailAddressOrNull import com.fsck.k9.Account import com.fsck.k9.Account.ShowPictures import com.fsck.k9.mail.Message @@ -262,15 +263,15 @@ class MessageTopView( return false } val senderEmailAddress = getSenderEmailAddress(message) ?: return false - return contactRepository.hasContactFor(senderEmailAddress.toEmailAddress()) + return contactRepository.hasContactFor(senderEmailAddress) } - private fun getSenderEmailAddress(message: Message): String? { + private fun getSenderEmailAddress(message: Message): EmailAddress? { val from = message.from return if (from == null || from.isEmpty()) { null } else { - from[0].address + from[0].address.toEmailAddressOrNull() } } diff --git a/backend/demo/src/main/resources/contents.json b/backend/demo/src/main/resources/contents.json index f0f75a8e0..137551a3b 100644 --- a/backend/demo/src/main/resources/contents.json +++ b/backend/demo/src/main/resources/contents.json @@ -8,7 +8,8 @@ "thread_1", "thread_2", "inline_image_data_uri", - "inline_image_attachment" + "inline_image_attachment", + "localpart_exceeds_length_limit" ] }, "trash": { diff --git a/backend/demo/src/main/resources/inbox/localpart_exceeds_length_limit.eml b/backend/demo/src/main/resources/inbox/localpart_exceeds_length_limit.eml new file mode 100644 index 000000000..2aa0ecb1f --- /dev/null +++ b/backend/demo/src/main/resources/inbox/localpart_exceeds_length_limit.eml @@ -0,0 +1,9 @@ +MIME-Version: 1.0 +From: Sender (local part exceeds maximum length) +Date: Thu, 15 Jun 2023 18:00:00 +0200 +Message-ID: +Subject: Localpart of email address exceeds 64 characters +To: User +Content-Type: text/plain; charset=UTF-8 + +You should still be able to read this message. diff --git a/cli/autodiscovery-cli/src/main/kotlin/app/k9mail/cli/autodiscovery/AutoDiscoveryCli.kt b/cli/autodiscovery-cli/src/main/kotlin/app/k9mail/cli/autodiscovery/AutoDiscoveryCli.kt index 5419037bc..1cdf47d95 100644 --- a/cli/autodiscovery-cli/src/main/kotlin/app/k9mail/cli/autodiscovery/AutoDiscoveryCli.kt +++ b/cli/autodiscovery-cli/src/main/kotlin/app/k9mail/cli/autodiscovery/AutoDiscoveryCli.kt @@ -6,7 +6,7 @@ import app.k9mail.autodiscovery.autoconfig.AutoconfigUrlConfig import app.k9mail.autodiscovery.autoconfig.createIspDbAutoconfigDiscovery import app.k9mail.autodiscovery.autoconfig.createMxLookupAutoconfigDiscovery import app.k9mail.autodiscovery.autoconfig.createProviderAutoconfigDiscovery -import app.k9mail.core.common.mail.toEmailAddress +import app.k9mail.core.common.mail.toUserEmailAddress import com.github.ajalt.clikt.core.CliktCommand import com.github.ajalt.clikt.parameters.arguments.argument import com.github.ajalt.clikt.parameters.options.flag @@ -57,7 +57,7 @@ class AutoDiscoveryCli : CliktCommand( val mxDiscovery = createMxLookupAutoconfigDiscovery(okHttpClient) val runnables = listOf(providerDiscovery, ispDbDiscovery, mxDiscovery) - .flatMap { it.initDiscovery(emailAddress.toEmailAddress()) } + .flatMap { it.initDiscovery(emailAddress.toUserEmailAddress()) } val serialRunner = SerialRunner(runnables) return runBlocking { diff --git a/core/android/common/src/test/kotlin/app/k9mail/core/android/common/contact/ContactFixture.kt b/core/android/common/src/test/kotlin/app/k9mail/core/android/common/contact/ContactFixture.kt index 89d71831d..35567986a 100644 --- a/core/android/common/src/test/kotlin/app/k9mail/core/android/common/contact/ContactFixture.kt +++ b/core/android/common/src/test/kotlin/app/k9mail/core/android/common/contact/ContactFixture.kt @@ -1,12 +1,12 @@ package app.k9mail.core.android.common.contact import android.net.Uri -import app.k9mail.core.common.mail.toEmailAddress +import app.k9mail.core.common.mail.toEmailAddressOrThrow const val CONTACT_ID = 123L const val CONTACT_NAME = "user name" const val CONTACT_LOOKUP_KEY = "0r1-4F314D4F2F294F29" -val CONTACT_EMAIL_ADDRESS = "user@example.com".toEmailAddress() +val CONTACT_EMAIL_ADDRESS = "user@example.com".toEmailAddressOrThrow() val CONTACT_URI: Uri = Uri.parse("content://com.android.contacts/contacts/lookup/$CONTACT_LOOKUP_KEY/$CONTACT_ID") val CONTACT_PHOTO_URI: Uri = Uri.parse("content://com.android.contacts/display_photo/$CONTACT_ID") diff --git a/core/common/src/main/kotlin/app/k9mail/core/common/mail/EmailAddress.kt b/core/common/src/main/kotlin/app/k9mail/core/common/mail/EmailAddress.kt index c164c02bc..cdf0dfd60 100644 --- a/core/common/src/main/kotlin/app/k9mail/core/common/mail/EmailAddress.kt +++ b/core/common/src/main/kotlin/app/k9mail/core/common/mail/EmailAddress.kt @@ -1,5 +1,12 @@ package app.k9mail.core.common.mail +// See RFC 5321, 4.5.3.1.3. +// The maximum length of 'Path' indirectly limits the length of 'Mailbox'. +internal const val MAXIMUM_EMAIL_ADDRESS_LENGTH = 254 + +// See RFC 5321, 4.5.3.1.1. +internal const val MAXIMUM_LOCAL_PART_LENGTH = 64 + /** * Represents an email address. * @@ -15,6 +22,14 @@ class EmailAddress internal constructor( init { warnings = buildSet { + if (localPart.length > MAXIMUM_LOCAL_PART_LENGTH) { + add(Warning.LocalPartExceedsLengthLimit) + } + + if (address.length > MAXIMUM_EMAIL_ADDRESS_LENGTH) { + add(Warning.EmailAddressExceedsLengthLimit) + } + if (localPart.isEmpty()) { add(Warning.EmptyLocalPart) } @@ -65,6 +80,17 @@ class EmailAddress internal constructor( } enum class Warning { + /** + * The local part exceeds the length limit (see RFC 5321, 4.5.3.1.1.). + */ + LocalPartExceedsLengthLimit, + + /** + * The email address exceeds the length limit (see RFC 5321, 4.5.3.1.3.; The maximum length of 'Path' + * indirectly limits the length of 'Mailbox'). + */ + EmailAddressExceedsLengthLimit, + /** * The local part requires using a quoted string. * @@ -81,10 +107,32 @@ class EmailAddress internal constructor( } companion object { - fun parse(address: String, config: EmailAddressParserConfig = EmailAddressParserConfig.DEFAULT): EmailAddress { + fun parse(address: String, config: EmailAddressParserConfig = EmailAddressParserConfig.RELAXED): EmailAddress { return EmailAddressParser(address, config).parse() } } } -fun String.toEmailAddress() = EmailAddress.parse(this) +/** + * Converts this string to an [EmailAddress] instance using [EmailAddressParserConfig.RELAXED]. + */ +fun String.toEmailAddressOrThrow() = EmailAddress.parse(this, EmailAddressParserConfig.RELAXED) + +/** + * Converts this string to an [EmailAddress] instance using [EmailAddressParserConfig.RELAXED]. + */ +@Suppress("SwallowedException") +fun String.toEmailAddressOrNull(): EmailAddress? { + return try { + EmailAddress.parse(this, EmailAddressParserConfig.RELAXED) + } catch (e: EmailAddressParserException) { + null + } +} + +/** + * Convert this string into an [EmailAddress] instance using [EmailAddressParserConfig.LIMITED]. + * + * Use this when validating the email address a user wants to add to an account/identity. + */ +fun String.toUserEmailAddress() = EmailAddress.parse(this, EmailAddressParserConfig.LIMITED) diff --git a/core/common/src/main/kotlin/app/k9mail/core/common/mail/EmailAddressParser.kt b/core/common/src/main/kotlin/app/k9mail/core/common/mail/EmailAddressParser.kt index 8c0ed4946..fd03a6e72 100644 --- a/core/common/src/main/kotlin/app/k9mail/core/common/mail/EmailAddressParser.kt +++ b/core/common/src/main/kotlin/app/k9mail/core/common/mail/EmailAddressParser.kt @@ -13,13 +13,6 @@ import app.k9mail.core.common.mail.EmailAddressParserError.LocalPartRequiresQuot import app.k9mail.core.common.mail.EmailAddressParserError.QuotedStringInLocalPart import app.k9mail.core.common.mail.EmailAddressParserError.TotalLengthExceeded -// See RFC 5321, 4.5.3.1.3. -// The maximum length of 'Path' indirectly limits the length of 'Mailbox'. -internal const val MAXIMUM_EMAIL_ADDRESS_LENGTH = 254 - -// See RFC 5321, 4.5.3.1.1. -internal const val MAXIMUM_LOCAL_PART_LENGTH = 64 - /** * Parse an email address. * @@ -52,15 +45,23 @@ internal class EmailAddressParser( parserError(ExpectedEndOfInput) } - if (emailAddress.address.length > MAXIMUM_EMAIL_ADDRESS_LENGTH) { + if ( + config.isEmailAddressLengthCheckEnabled && Warning.EmailAddressExceedsLengthLimit in emailAddress.warnings + ) { parserError(TotalLengthExceeded) } - if (!config.allowLocalPartRequiringQuotedString && Warning.QuotedStringInLocalPart in emailAddress.warnings) { + if (config.isLocalPartLengthCheckEnabled && Warning.LocalPartExceedsLengthLimit in emailAddress.warnings) { + parserError(LocalPartLengthExceeded, position = input.lastIndexOf('@')) + } + + if ( + !config.isLocalPartRequiringQuotedStringAllowed && Warning.QuotedStringInLocalPart in emailAddress.warnings + ) { parserError(LocalPartRequiresQuotedString, position = 0) } - if (!config.allowEmptyLocalPart && Warning.EmptyLocalPart in emailAddress.warnings) { + if (!config.isEmptyLocalPartAllowed && Warning.EmptyLocalPart in emailAddress.warnings) { parserError(EmptyLocalPart, position = 1) } @@ -83,7 +84,7 @@ internal class EmailAddressParser( readDotString() } character == DQUOTE -> { - if (config.allowQuotedLocalPart) { + if (config.isQuotedLocalPartAllowed) { readQuotedString() } else { parserError(QuotedStringInLocalPart) @@ -94,10 +95,6 @@ internal class EmailAddressParser( } } - if (localPart.length > MAXIMUM_LOCAL_PART_LENGTH) { - parserError(LocalPartLengthExceeded) - } - return localPart } diff --git a/core/common/src/main/kotlin/app/k9mail/core/common/mail/EmailAddressParserConfig.kt b/core/common/src/main/kotlin/app/k9mail/core/common/mail/EmailAddressParserConfig.kt index 2a19dfbba..325b97975 100644 --- a/core/common/src/main/kotlin/app/k9mail/core/common/mail/EmailAddressParserConfig.kt +++ b/core/common/src/main/kotlin/app/k9mail/core/common/mail/EmailAddressParserConfig.kt @@ -3,39 +3,63 @@ package app.k9mail.core.common.mail /** * Configuration to control the behavior when parsing an email address into [EmailAddress]. * - * @param allowQuotedLocalPart When this is `true`, the parsing step allows email addresses with a local part encoded - * as quoted string, e.g. `"foo bar"@domain.example`. Otherwise, the parser will throw an [EmailAddressParserException] - * as soon as a quoted string is encountered. + * @param isLocalPartLengthCheckEnabled When this is `true` the length of the local part is checked to make sure it + * doesn't exceed the specified limit (see RFC 5321, 4.5.3.1.1.). + * + * @param isEmailAddressLengthCheckEnabled When this is `true` the length of the whole email address is checked to make + * sure it doesn't exceed the specified limit (see RFC 5321, 4.5.3.1.3.; The maximum length of 'Path' indirectly limits + * the length of 'Mailbox'). + * + * @param isQuotedLocalPartAllowed When this is `true`, the parsing step allows email addresses with a local part + * encoded as quoted string, e.g. `"foo bar"@domain.example`. Otherwise, the parser will throw an + * [EmailAddressParserException] as soon as a quoted string is encountered. * Quoted strings in local parts are not widely used. It's recommended to disallow them whenever possible. * - * @param allowLocalPartRequiringQuotedString Email addresses whose local part requires the use of a quoted string are - * only allowed when this is `true`. This is separate from [allowQuotedLocalPart] because one might want to allow email - * addresses that unnecessarily use a quoted string, e.g. `"test"@domain.example` ([allowQuotedLocalPart] = `true`, - * [allowLocalPartRequiringQuotedString] = `false`; [EmailAddress] will not retain the original form and treat this - * address exactly like `test@domain.example`). When allowing this, remember to use the value of [EmailAddress.address] - * instead of retaining the original user input. + * @param isLocalPartRequiringQuotedStringAllowed Email addresses whose local part requires the use of a quoted string + * are only allowed when this is `true`. This is separate from [isQuotedLocalPartAllowed] because one might want to + * allow email addresses that unnecessarily use a quoted string, e.g. `"test"@domain.example` + * ([isQuotedLocalPartAllowed] = `true`, [isLocalPartRequiringQuotedStringAllowed] = `false`; [EmailAddress] will not + * retain the original form and treat this address exactly like `test@domain.example`). When allowing this, remember to + * use the value of [EmailAddress.address] instead of retaining the original user input. * - * The value of this property is ignored if [allowQuotedLocalPart] is `false`. + * The value of this property is ignored if [isQuotedLocalPartAllowed] is `false`. * - * @param allowEmptyLocalPart Email addresses with an empty local part (e.g. `""@domain.example`) are only allowed if - * this value is `true`. + * @param isEmptyLocalPartAllowed Email addresses with an empty local part (e.g. `""@domain.example`) are only allowed + * if this value is `true`. * - * The value of this property is ignored if at least one of [allowQuotedLocalPart] and - * [allowLocalPartRequiringQuotedString] is `false`. + * The value of this property is ignored if at least one of [isQuotedLocalPartAllowed] and + * [isLocalPartRequiringQuotedStringAllowed] is `false`. */ data class EmailAddressParserConfig( - val allowQuotedLocalPart: Boolean, - val allowLocalPartRequiringQuotedString: Boolean, - val allowEmptyLocalPart: Boolean = false, + val isLocalPartLengthCheckEnabled: Boolean, + val isEmailAddressLengthCheckEnabled: Boolean, + val isQuotedLocalPartAllowed: Boolean, + val isLocalPartRequiringQuotedStringAllowed: Boolean, + val isEmptyLocalPartAllowed: Boolean = false, ) { companion object { /** - * This configuration should match what `EmailAddressValidator` currently allows. + * This allows local parts requiring quoted strings and disables length checks for the local part and the + * whole email address. */ - val DEFAULT = EmailAddressParserConfig( - allowQuotedLocalPart = true, - allowLocalPartRequiringQuotedString = true, - allowEmptyLocalPart = false, + val RELAXED = EmailAddressParserConfig( + isLocalPartLengthCheckEnabled = false, + isEmailAddressLengthCheckEnabled = false, + isQuotedLocalPartAllowed = true, + isLocalPartRequiringQuotedStringAllowed = true, + isEmptyLocalPartAllowed = false, + ) + + /** + * This only allows a subset of valid email addresses. Use this when validating the email address a user wants + * to add to an account/identity. + */ + val LIMITED = EmailAddressParserConfig( + isLocalPartLengthCheckEnabled = true, + isEmailAddressLengthCheckEnabled = true, + isQuotedLocalPartAllowed = false, + isLocalPartRequiringQuotedStringAllowed = false, + isEmptyLocalPartAllowed = false, ) } } diff --git a/core/common/src/test/kotlin/app/k9mail/core/common/mail/EmailAddressParserTest.kt b/core/common/src/test/kotlin/app/k9mail/core/common/mail/EmailAddressParserTest.kt index f506ad823..48431066a 100644 --- a/core/common/src/test/kotlin/app/k9mail/core/common/mail/EmailAddressParserTest.kt +++ b/core/common/src/test/kotlin/app/k9mail/core/common/mail/EmailAddressParserTest.kt @@ -15,6 +15,7 @@ import app.k9mail.core.common.mail.EmailAddressParserError.UnexpectedCharacter import assertk.all import assertk.assertFailure import assertk.assertThat +import assertk.assertions.contains import assertk.assertions.hasMessage import assertk.assertions.isEqualTo import assertk.assertions.isInstanceOf @@ -40,7 +41,10 @@ class EmailAddressParserTest { @Test fun `quoted local part`() { - val emailAddress = parseEmailAddress("\"one two\"@domain.example", allowLocalPartRequiringQuotedString = true) + val emailAddress = parseEmailAddress( + address = "\"one two\"@domain.example", + isLocalPartRequiringQuotedStringAllowed = true, + ) assertThat(emailAddress.localPart).isEqualTo("one two") assertThat(emailAddress.domain).isEqualTo(EmailDomain("domain.example")) @@ -51,8 +55,8 @@ class EmailAddressParserTest { assertFailure { parseEmailAddress( address = "\"one two\"@domain.example", - allowQuotedLocalPart = true, - allowLocalPartRequiringQuotedString = false, + isQuotedLocalPartAllowed = true, + isLocalPartRequiringQuotedStringAllowed = false, ) }.isInstanceOf().all { prop(EmailAddressParserException::error).isEqualTo(LocalPartRequiresQuotedString) @@ -65,8 +69,8 @@ class EmailAddressParserTest { fun `unnecessarily quoted local part`() { val emailAddress = parseEmailAddress( address = "\"user\"@domain.example", - allowQuotedLocalPart = true, - allowLocalPartRequiringQuotedString = false, + isQuotedLocalPartAllowed = true, + isLocalPartRequiringQuotedStringAllowed = false, ) assertThat(emailAddress.localPart).isEqualTo("user") @@ -77,7 +81,7 @@ class EmailAddressParserTest { @Test fun `unnecessarily quoted local part not allowed`() { assertFailure { - parseEmailAddress("\"user\"@domain.example", allowQuotedLocalPart = false) + parseEmailAddress("\"user\"@domain.example", isQuotedLocalPartAllowed = false) }.isInstanceOf().all { prop(EmailAddressParserException::error).isEqualTo(QuotedStringInLocalPart) prop(EmailAddressParserException::position).isEqualTo(0) @@ -87,7 +91,10 @@ class EmailAddressParserTest { @Test fun `quoted local part containing double quote character`() { - val emailAddress = parseEmailAddress(""""a\"b"@domain.example""", allowLocalPartRequiringQuotedString = true) + val emailAddress = parseEmailAddress( + address = """"a\"b"@domain.example""", + isLocalPartRequiringQuotedStringAllowed = true, + ) assertThat(emailAddress.localPart).isEqualTo("a\"b") assertThat(emailAddress.domain).isEqualTo(EmailDomain("domain.example")) @@ -96,7 +103,7 @@ class EmailAddressParserTest { @Test fun `empty local part`() { - val emailAddress = parseEmailAddress("\"\"@domain.example", allowEmptyLocalPart = true) + val emailAddress = parseEmailAddress("\"\"@domain.example", isEmptyLocalPartAllowed = true) assertThat(emailAddress.localPart).isEqualTo("") assertThat(emailAddress.domain).isEqualTo(EmailDomain("domain.example")) @@ -108,8 +115,8 @@ class EmailAddressParserTest { assertFailure { parseEmailAddress( address = "\"\"@domain.example", - allowLocalPartRequiringQuotedString = true, - allowEmptyLocalPart = false, + isLocalPartRequiringQuotedStringAllowed = true, + isEmptyLocalPartAllowed = false, ) }.isInstanceOf().all { prop(EmailAddressParserException::error).isEqualTo(EmptyLocalPart) @@ -154,7 +161,7 @@ class EmailAddressParserTest { @Test fun `obsolete syntax`() { assertFailure { - parseEmailAddress("\"quoted\".atom@domain.example", allowLocalPartRequiringQuotedString = true) + parseEmailAddress("\"quoted\".atom@domain.example", isLocalPartRequiringQuotedStringAllowed = true) }.isInstanceOf().all { prop(EmailAddressParserException::error).isEqualTo(UnexpectedCharacter) prop(EmailAddressParserException::position).isEqualTo(8) @@ -187,7 +194,7 @@ class EmailAddressParserTest { @Test fun `quoted local part missing closing double quote`() { assertFailure { - parseEmailAddress("\"invalid@domain.example", allowLocalPartRequiringQuotedString = true) + parseEmailAddress("\"invalid@domain.example", isLocalPartRequiringQuotedStringAllowed = true) }.isInstanceOf().all { prop(EmailAddressParserException::error).isEqualTo(UnexpectedCharacter) prop(EmailAddressParserException::position).isEqualTo(23) @@ -198,7 +205,7 @@ class EmailAddressParserTest { @Test fun `quoted text containing unsupported character`() { assertFailure { - parseEmailAddress("\"ä\"@domain.example", allowLocalPartRequiringQuotedString = true) + parseEmailAddress("\"ä\"@domain.example", isLocalPartRequiringQuotedStringAllowed = true) }.isInstanceOf().all { prop(EmailAddressParserException::error).isEqualTo(InvalidQuotedString) prop(EmailAddressParserException::position).isEqualTo(1) @@ -209,7 +216,7 @@ class EmailAddressParserTest { @Test fun `quoted text containing unsupported escaped character`() { assertFailure { - parseEmailAddress(""""\ä"@domain.example""", allowLocalPartRequiringQuotedString = true) + parseEmailAddress(""""\ä"@domain.example""", isLocalPartRequiringQuotedStringAllowed = true) }.isInstanceOf().all { prop(EmailAddressParserException::error).isEqualTo(InvalidQuotedString) prop(EmailAddressParserException::position).isEqualTo(3) @@ -218,9 +225,12 @@ class EmailAddressParserTest { } @Test - fun `local part exceeds maximum size`() { + fun `local part exceeds maximum size with length check enabled`() { assertFailure { - parseEmailAddress("1xxxxxxxxx2xxxxxxxxx3xxxxxxxxx4xxxxxxxxx5xxxxxxxxx6xxxxxxxxx12345@domain.example") + parseEmailAddress( + address = "1xxxxxxxxx2xxxxxxxxx3xxxxxxxxx4xxxxxxxxx5xxxxxxxxx6xxxxxxxxx12345@domain.example", + isLocalPartLengthCheckEnabled = true, + ) }.isInstanceOf().all { prop(EmailAddressParserException::error).isEqualTo(LocalPartLengthExceeded) prop(EmailAddressParserException::position).isEqualTo(65) @@ -229,13 +239,27 @@ class EmailAddressParserTest { } @Test - fun `email exceeds maximum size`() { + fun `local part exceeds maximum size with length check disabled`() { + val input = "1xxxxxxxxx2xxxxxxxxx3xxxxxxxxx4xxxxxxxxx5xxxxxxxxx6xxxxxxxxx12345@domain.example" + + val emailAddress = parseEmailAddress(address = input, isLocalPartLengthCheckEnabled = false) + + assertThat(emailAddress.localPart) + .isEqualTo("1xxxxxxxxx2xxxxxxxxx3xxxxxxxxx4xxxxxxxxx5xxxxxxxxx6xxxxxxxxx12345") + assertThat(emailAddress.domain).isEqualTo(EmailDomain("domain.example")) + assertThat(emailAddress.address).isEqualTo(input) + assertThat(emailAddress.warnings).contains(EmailAddress.Warning.LocalPartExceedsLengthLimit) + } + + @Test + fun `email exceeds maximum size with length check enabled`() { assertFailure { parseEmailAddress( - "1xxxxxxxxx2xxxxxxxxx3xxxxxxxxx4xxxxxxxxx5xxxxxxxxx6xxxxxxxxx1234@" + + address = "1xxxxxxxxx2xxxxxxxxx3xxxxxxxxx4xxxxxxxxx5xxxxxxxxx6xxxxxxxxx1234@" + "1xxxxxxxxx2xxxxxxxxx3xxxxxxxxx4xxxxxxxxx5xxxxxxxxx6xxxxxxxxx123." + "1xxxxxxxxx2xxxxxxxxx3xxxxxxxxx4xxxxxxxxx5xxxxxxxxx6xxxxxxxxx123." + "1xxxxxxxxx2xxxxxxxxx3xxxxxxxxx4xxxxxxxxx5xxxxxxxxx6xxxxxxxxx12", + isEmailAddressLengthCheckEnabled = true, ) }.isInstanceOf().all { prop(EmailAddressParserException::error).isEqualTo(TotalLengthExceeded) @@ -244,6 +268,28 @@ class EmailAddressParserTest { } } + @Test + fun `email exceeds maximum size with length check disabled`() { + val input = "1xxxxxxxxx2xxxxxxxxx3xxxxxxxxx4xxxxxxxxx5xxxxxxxxx6xxxxxxxxx1234@" + + "1xxxxxxxxx2xxxxxxxxx3xxxxxxxxx4xxxxxxxxx5xxxxxxxxx6xxxxxxxxx123." + + "1xxxxxxxxx2xxxxxxxxx3xxxxxxxxx4xxxxxxxxx5xxxxxxxxx6xxxxxxxxx123." + + "1xxxxxxxxx2xxxxxxxxx3xxxxxxxxx4xxxxxxxxx5xxxxxxxxx6xxxxxxxxx12" + + val emailAddress = parseEmailAddress(address = input, isEmailAddressLengthCheckEnabled = false) + + assertThat(emailAddress.localPart) + .isEqualTo("1xxxxxxxxx2xxxxxxxxx3xxxxxxxxx4xxxxxxxxx5xxxxxxxxx6xxxxxxxxx1234") + assertThat(emailAddress.domain).isEqualTo( + EmailDomain( + "1xxxxxxxxx2xxxxxxxxx3xxxxxxxxx4xxxxxxxxx5xxxxxxxxx6xxxxxxxxx123." + + "1xxxxxxxxx2xxxxxxxxx3xxxxxxxxx4xxxxxxxxx5xxxxxxxxx6xxxxxxxxx123." + + "1xxxxxxxxx2xxxxxxxxx3xxxxxxxxx4xxxxxxxxx5xxxxxxxxx6xxxxxxxxx12", + ), + ) + assertThat(emailAddress.address).isEqualTo(input) + assertThat(emailAddress.warnings).contains(EmailAddress.Warning.EmailAddressExceedsLengthLimit) + } + @Test fun `input contains additional character`() { assertFailure { @@ -257,14 +303,18 @@ class EmailAddressParserTest { private fun parseEmailAddress( address: String, - allowEmptyLocalPart: Boolean = false, - allowLocalPartRequiringQuotedString: Boolean = allowEmptyLocalPart, - allowQuotedLocalPart: Boolean = allowLocalPartRequiringQuotedString, + isLocalPartLengthCheckEnabled: Boolean = false, + isEmailAddressLengthCheckEnabled: Boolean = false, + isEmptyLocalPartAllowed: Boolean = false, + isLocalPartRequiringQuotedStringAllowed: Boolean = isEmptyLocalPartAllowed, + isQuotedLocalPartAllowed: Boolean = isLocalPartRequiringQuotedStringAllowed, ): EmailAddress { val config = EmailAddressParserConfig( - allowQuotedLocalPart, - allowLocalPartRequiringQuotedString, - allowEmptyLocalPart, + isLocalPartLengthCheckEnabled, + isEmailAddressLengthCheckEnabled, + isQuotedLocalPartAllowed, + isLocalPartRequiringQuotedStringAllowed, + isEmptyLocalPartAllowed, ) return EmailAddressParser(address, config).parse() } diff --git a/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/domain/usecase/GetAutoDiscovery.kt b/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/domain/usecase/GetAutoDiscovery.kt index 7c150dca0..88717707d 100644 --- a/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/domain/usecase/GetAutoDiscovery.kt +++ b/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/domain/usecase/GetAutoDiscovery.kt @@ -6,7 +6,7 @@ import app.k9mail.autodiscovery.api.AutoDiscoveryService import app.k9mail.autodiscovery.api.ConnectionSecurity import app.k9mail.autodiscovery.api.ImapServerSettings import app.k9mail.autodiscovery.api.SmtpServerSettings -import app.k9mail.core.common.mail.toEmailAddress +import app.k9mail.core.common.mail.toUserEmailAddress import app.k9mail.core.common.net.toHostname import app.k9mail.core.common.net.toPort import app.k9mail.feature.account.setup.domain.DomainContract @@ -34,7 +34,7 @@ internal class GetAutoDiscovery( return provideWithDelay(fakeResult) } - return service.discover(emailAddress.toEmailAddress())!! + return service.discover(emailAddress.toUserEmailAddress())!! } @Suppress("MagicNumber") diff --git a/feature/autodiscovery/autoconfig/src/test/kotlin/app/k9mail/autodiscovery/autoconfig/AutoconfigDiscoveryTest.kt b/feature/autodiscovery/autoconfig/src/test/kotlin/app/k9mail/autodiscovery/autoconfig/AutoconfigDiscoveryTest.kt index c289283b8..d077ce2e3 100644 --- a/feature/autodiscovery/autoconfig/src/test/kotlin/app/k9mail/autodiscovery/autoconfig/AutoconfigDiscoveryTest.kt +++ b/feature/autodiscovery/autoconfig/src/test/kotlin/app/k9mail/autodiscovery/autoconfig/AutoconfigDiscoveryTest.kt @@ -2,7 +2,7 @@ package app.k9mail.autodiscovery.autoconfig import app.k9mail.autodiscovery.autoconfig.MockAutoconfigFetcher.Companion.RESULT_ONE import app.k9mail.autodiscovery.autoconfig.MockAutoconfigFetcher.Companion.RESULT_TWO -import app.k9mail.core.common.mail.toEmailAddress +import app.k9mail.core.common.mail.toUserEmailAddress import app.k9mail.core.common.net.toDomain import assertk.assertThat import assertk.assertions.containsExactly @@ -13,7 +13,7 @@ import kotlin.test.Test import kotlinx.coroutines.test.runTest import okhttp3.HttpUrl.Companion.toHttpUrl -private val IRRELEVANT_EMAIL_ADDRESS = "irrelevant@domain.example".toEmailAddress() +private val IRRELEVANT_EMAIL_ADDRESS = "irrelevant@domain.example".toUserEmailAddress() class AutoconfigDiscoveryTest { private val urlProvider = MockAutoconfigUrlProvider() @@ -22,7 +22,7 @@ class AutoconfigDiscoveryTest { @Test fun `AutoconfigFetcher and AutoconfigParser should only be called when AutoDiscoveryRunnable is run`() = runTest { - val emailAddress = "user@domain.example".toEmailAddress() + val emailAddress = "user@domain.example".toUserEmailAddress() val autoconfigUrl = "https://autoconfig.domain.invalid/mail/config-v1.1.xml".toHttpUrl() urlProvider.addResult(listOf(autoconfigUrl)) autoconfigFetcher.addResult(RESULT_ONE) diff --git a/feature/autodiscovery/autoconfig/src/test/kotlin/app/k9mail/autodiscovery/autoconfig/MxLookupAutoconfigDiscoveryTest.kt b/feature/autodiscovery/autoconfig/src/test/kotlin/app/k9mail/autodiscovery/autoconfig/MxLookupAutoconfigDiscoveryTest.kt index 2b58f204e..0e12e3c08 100644 --- a/feature/autodiscovery/autoconfig/src/test/kotlin/app/k9mail/autodiscovery/autoconfig/MxLookupAutoconfigDiscoveryTest.kt +++ b/feature/autodiscovery/autoconfig/src/test/kotlin/app/k9mail/autodiscovery/autoconfig/MxLookupAutoconfigDiscoveryTest.kt @@ -2,7 +2,7 @@ package app.k9mail.autodiscovery.autoconfig import app.k9mail.autodiscovery.api.AutoDiscoveryResult.NoUsableSettingsFound import app.k9mail.autodiscovery.autoconfig.MockAutoconfigFetcher.Companion.RESULT_ONE -import app.k9mail.core.common.mail.toEmailAddress +import app.k9mail.core.common.mail.toUserEmailAddress import app.k9mail.core.common.net.toDomain import assertk.assertThat import assertk.assertions.containsExactly @@ -28,7 +28,7 @@ class MxLookupAutoconfigDiscoveryTest { @Test fun `AutoconfigUrlProvider should be called with MX base domain`() = runTest { - val emailAddress = "user@company.example".toEmailAddress() + val emailAddress = "user@company.example".toUserEmailAddress() mxResolver.addResult("mx.emailprovider.example".toDomain()) urlProvider.addResult(listOf("https://ispdb.invalid/emailprovider.example".toHttpUrl())) autoconfigFetcher.addResult(RESULT_ONE) @@ -50,7 +50,7 @@ class MxLookupAutoconfigDiscoveryTest { @Test fun `AutoconfigUrlProvider should be called with MX base domain and subdomain`() = runTest { - val emailAddress = "user@company.example".toEmailAddress() + val emailAddress = "user@company.example".toUserEmailAddress() mxResolver.addResult("mx.something.emailprovider.example".toDomain()) urlProvider.apply { addResult(listOf("https://ispdb.invalid/something.emailprovider.example".toHttpUrl())) @@ -74,7 +74,7 @@ class MxLookupAutoconfigDiscoveryTest { @Test fun `skip Autoconfig lookup when MX lookup does not return a result`() = runTest { - val emailAddress = "user@company.example".toEmailAddress() + val emailAddress = "user@company.example".toUserEmailAddress() mxResolver.addResult(emptyList()) val autoDiscoveryRunnables = discovery.initDiscovery(emailAddress) @@ -88,7 +88,7 @@ class MxLookupAutoconfigDiscoveryTest { @Test fun `skip Autoconfig lookup when base domain of MX record is email domain`() = runTest { - val emailAddress = "user@company.example".toEmailAddress() + val emailAddress = "user@company.example".toUserEmailAddress() mxResolver.addResult("mx.company.example".toDomain()) val autoDiscoveryRunnables = discovery.initDiscovery(emailAddress) @@ -102,7 +102,7 @@ class MxLookupAutoconfigDiscoveryTest { @Test fun `isTrusted should be false when MxLookupResult_isTrusted is false`() = runTest { - val emailAddress = "user@company.example".toEmailAddress() + val emailAddress = "user@company.example".toUserEmailAddress() mxResolver.addResult("mx.emailprovider.example".toDomain(), isTrusted = false) urlProvider.addResult(listOf("https://ispdb.invalid/emailprovider.example".toHttpUrl())) autoconfigFetcher.addResult(RESULT_ONE.copy(isTrusted = true)) @@ -115,7 +115,7 @@ class MxLookupAutoconfigDiscoveryTest { @Test fun `isTrusted should be false when AutoDiscoveryResult_isTrusted from AutoconfigFetcher is false`() = runTest { - val emailAddress = "user@company.example".toEmailAddress() + val emailAddress = "user@company.example".toUserEmailAddress() mxResolver.addResult("mx.emailprovider.example".toDomain(), isTrusted = true) urlProvider.addResult(listOf("https://ispdb.invalid/emailprovider.example".toHttpUrl())) autoconfigFetcher.addResult(RESULT_ONE.copy(isTrusted = false)) diff --git a/feature/autodiscovery/autoconfig/src/test/kotlin/app/k9mail/autodiscovery/autoconfig/ProviderAutoconfigUrlProviderTest.kt b/feature/autodiscovery/autoconfig/src/test/kotlin/app/k9mail/autodiscovery/autoconfig/ProviderAutoconfigUrlProviderTest.kt index 50c0abb85..2a2250dfd 100644 --- a/feature/autodiscovery/autoconfig/src/test/kotlin/app/k9mail/autodiscovery/autoconfig/ProviderAutoconfigUrlProviderTest.kt +++ b/feature/autodiscovery/autoconfig/src/test/kotlin/app/k9mail/autodiscovery/autoconfig/ProviderAutoconfigUrlProviderTest.kt @@ -1,6 +1,6 @@ package app.k9mail.autodiscovery.autoconfig -import app.k9mail.core.common.mail.toEmailAddress +import app.k9mail.core.common.mail.toUserEmailAddress import app.k9mail.core.common.net.toDomain import assertk.assertThat import assertk.assertions.containsExactly @@ -8,7 +8,7 @@ import org.junit.Test class ProviderAutoconfigUrlProviderTest { private val domain = "domain.example".toDomain() - private val email = "test@domain.example".toEmailAddress() + private val email = "test@domain.example".toUserEmailAddress() @Test fun `getAutoconfigUrls with http allowed and email address included`() { diff --git a/feature/autodiscovery/autoconfig/src/test/kotlin/app/k9mail/autodiscovery/autoconfig/RealAutoconfigParserTest.kt b/feature/autodiscovery/autoconfig/src/test/kotlin/app/k9mail/autodiscovery/autoconfig/RealAutoconfigParserTest.kt index e90d2ce40..31448eeba 100644 --- a/feature/autodiscovery/autoconfig/src/test/kotlin/app/k9mail/autodiscovery/autoconfig/RealAutoconfigParserTest.kt +++ b/feature/autodiscovery/autoconfig/src/test/kotlin/app/k9mail/autodiscovery/autoconfig/RealAutoconfigParserTest.kt @@ -8,7 +8,7 @@ import app.k9mail.autodiscovery.api.ImapServerSettings import app.k9mail.autodiscovery.api.SmtpServerSettings import app.k9mail.autodiscovery.autoconfig.AutoconfigParserResult.ParserError import app.k9mail.autodiscovery.autoconfig.AutoconfigParserResult.Settings -import app.k9mail.core.common.mail.toEmailAddress +import app.k9mail.core.common.mail.toUserEmailAddress import app.k9mail.core.common.net.toHostname import app.k9mail.core.common.net.toPort import assertk.assertThat @@ -55,13 +55,13 @@ class RealAutoconfigParserTest { """.trimIndent() - private val irrelevantEmailAddress = "irrelevant@domain.example".toEmailAddress() + private val irrelevantEmailAddress = "irrelevant@domain.example".toUserEmailAddress() @Test fun `minimal data`() { val inputStream = minimalConfig.byteInputStream() - val result = parser.parseSettings(inputStream, email = "user@domain.example".toEmailAddress()) + val result = parser.parseSettings(inputStream, email = "user@domain.example".toUserEmailAddress()) assertThat(result).isNotNull().isEqualTo( Settings( @@ -87,7 +87,7 @@ class RealAutoconfigParserTest { fun `real-world data`() { val inputStream = javaClass.getResourceAsStream("/2022-11-19-googlemail.com.xml")!! - val result = parser.parseSettings(inputStream, email = "test@gmail.com".toEmailAddress()) + val result = parser.parseSettings(inputStream, email = "test@gmail.com".toUserEmailAddress()) assertThat(result).isNotNull().isEqualTo( Settings( @@ -117,7 +117,7 @@ class RealAutoconfigParserTest { element("outgoingServer > username").text("%EMAILDOMAIN%") } - val result = parser.parseSettings(inputStream, email = "user@domain.example".toEmailAddress()) + val result = parser.parseSettings(inputStream, email = "user@domain.example".toUserEmailAddress()) assertThat(result).isNotNull().isEqualTo( Settings( @@ -149,7 +149,7 @@ class RealAutoconfigParserTest { element("incomingServer > username").prepend("") } - val result = parser.parseSettings(inputStream, email = "user@domain.example".toEmailAddress()) + val result = parser.parseSettings(inputStream, email = "user@domain.example".toUserEmailAddress()) assertThat(result).isInstanceOf() .prop(Settings::incomingServerSettings).isEqualTo( @@ -169,7 +169,7 @@ class RealAutoconfigParserTest { element("incomingServer").insertBefore("""""") } - val result = parser.parseSettings(inputStream, email = "user@domain.example".toEmailAddress()) + val result = parser.parseSettings(inputStream, email = "user@domain.example".toUserEmailAddress()) assertThat(result).isInstanceOf() .prop(Settings::incomingServerSettings).isEqualTo( @@ -189,7 +189,7 @@ class RealAutoconfigParserTest { element("outgoingServer").insertBefore("""""") } - val result = parser.parseSettings(inputStream, email = "user@domain.example".toEmailAddress()) + val result = parser.parseSettings(inputStream, email = "user@domain.example".toUserEmailAddress()) assertThat(result).isInstanceOf() .prop(Settings::outgoingServerSettings).isEqualTo( @@ -209,7 +209,7 @@ class RealAutoconfigParserTest { element("incomingServer > authentication").insertBefore("") } - val result = parser.parseSettings(inputStream, email = "user@domain.example".toEmailAddress()) + val result = parser.parseSettings(inputStream, email = "user@domain.example".toUserEmailAddress()) assertThat(result).isInstanceOf() .prop(Settings::incomingServerSettings).isEqualTo(