Merge pull request #6987 from thundernest/relaxed_email_address

Change `EmailAddressParser` to support addresses violating the specification
This commit is contained in:
cketti 2023-06-16 14:59:44 +02:00 committed by GitHub
commit bf73b0a787
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 260 additions and 107 deletions

View file

@ -1,7 +1,7 @@
package com.fsck.k9.helper package com.fsck.k9.helper
import app.k9mail.core.android.common.contact.ContactRepository import app.k9mail.core.android.common.contact.ContactRepository
import app.k9mail.core.common.mail.toEmailAddress import app.k9mail.core.common.mail.toEmailAddressOrNull
interface ContactNameProvider { interface ContactNameProvider {
fun getNameForAddress(address: String): String? fun getNameForAddress(address: String): String?
@ -11,6 +11,8 @@ class RealContactNameProvider(
private val contactRepository: ContactRepository, private val contactRepository: ContactRepository,
) : ContactNameProvider { ) : ContactNameProvider {
override fun getNameForAddress(address: String): String? { override fun getNameForAddress(address: String): String? {
return contactRepository.getContactFor(address.toEmailAddress())?.name return address.toEmailAddressOrNull()?.let { emailAddress ->
contactRepository.getContactFor(emailAddress)?.name
}
} }
} }

View file

@ -6,7 +6,7 @@ import android.text.SpannableStringBuilder
import android.text.TextUtils import android.text.TextUtils
import android.text.style.ForegroundColorSpan import android.text.style.ForegroundColorSpan
import app.k9mail.core.android.common.contact.ContactRepository 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.CoreResourceProvider
import com.fsck.k9.K9.contactNameColor import com.fsck.k9.K9.contactNameColor
import com.fsck.k9.K9.isChangeContactNameColor import com.fsck.k9.K9.isChangeContactNameColor
@ -101,7 +101,7 @@ class MessageHelper(
if (!showCorrespondentNames) { if (!showCorrespondentNames) {
return address.address return address.address
} else if (contactRepository != null) { } else if (contactRepository != null) {
val name = contactRepository.getContactFor(address.address.toEmailAddress())?.name val name = contactRepository.getContactName(address)
if (name != null) { if (name != null) {
return if (changeContactNameColor) { return if (changeContactNameColor) {
val coloredName = SpannableString(name) 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 { private fun isSpoofAddress(displayName: String): Boolean {
return displayName.contains("@") && SPOOF_ADDRESS_PATTERN.matcher(displayName).find() return displayName.contains("@") && SPOOF_ADDRESS_PATTERN.matcher(displayName).find()
} }

View file

@ -5,7 +5,7 @@ import android.text.SpannableString
import app.k9mail.core.android.common.contact.Contact import app.k9mail.core.android.common.contact.Contact
import app.k9mail.core.android.common.contact.ContactRepository import app.k9mail.core.android.common.contact.ContactRepository
import app.k9mail.core.common.mail.EmailAddress 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.assertThat
import assertk.assertions.isEqualTo import assertk.assertions.isEqualTo
import assertk.assertions.isInstanceOf import assertk.assertions.isInstanceOf
@ -143,6 +143,6 @@ class MessageHelperTest : RobolectricTest() {
} }
private companion object { private companion object {
val EMAIL_ADDRESS = "test@testor.com".toEmailAddress() val EMAIL_ADDRESS = "test@testor.com".toEmailAddressOrThrow()
} }
} }

View file

@ -1,7 +1,7 @@
package com.fsck.k9.notification package com.fsck.k9.notification
import app.k9mail.core.android.common.contact.ContactRepository 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.Account
import com.fsck.k9.K9 import com.fsck.k9.K9
import com.fsck.k9.mail.Flag import com.fsck.k9.mail.Flag
@ -88,7 +88,7 @@ class K9NotificationStrategy(
} }
if (account.isNotifyContactsMailOnly && 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") Timber.v("No notification: Message is not from a known contact")
return false return false

View file

@ -3,8 +3,9 @@ package com.fsck.k9.contacts
import android.content.ContentResolver import android.content.ContentResolver
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.net.Uri
import app.k9mail.core.android.common.contact.ContactRepository 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 import timber.log.Timber
internal class ContactPhotoLoader( internal class ContactPhotoLoader(
@ -12,7 +13,7 @@ internal class ContactPhotoLoader(
private val contactRepository: ContactRepository, private val contactRepository: ContactRepository,
) { ) {
fun loadContactPhoto(emailAddress: String): Bitmap? { fun loadContactPhoto(emailAddress: String): Bitmap? {
val photoUri = contactRepository.getContactFor(emailAddress.toEmailAddress())?.photoUri ?: return null val photoUri = getPhotoUri(emailAddress) ?: return null
return try { return try {
contentResolver.openInputStream(photoUri).use { inputStream -> contentResolver.openInputStream(photoUri).use { inputStream ->
BitmapFactory.decodeStream(inputStream) BitmapFactory.decodeStream(inputStream)
@ -22,4 +23,10 @@ internal class ContactPhotoLoader(
null null
} }
} }
private fun getPhotoUri(email: String): Uri? {
return email.toEmailAddressOrNull()?.let { emailAddress ->
contactRepository.getContactFor(emailAddress)?.photoUri
}
}
} }

View file

@ -2,12 +2,13 @@ package com.fsck.k9.ui.messagedetails
import android.app.PendingIntent import android.app.PendingIntent
import android.content.res.Resources import android.content.res.Resources
import android.net.Uri
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import app.k9mail.core.android.common.contact.CachingRepository import app.k9mail.core.android.common.contact.CachingRepository
import app.k9mail.core.android.common.contact.ContactPermissionResolver import app.k9mail.core.android.common.contact.ContactPermissionResolver
import app.k9mail.core.android.common.contact.ContactRepository 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.Account
import com.fsck.k9.controller.MessageReference import com.fsck.k9.controller.MessageReference
import com.fsck.k9.helper.ClipboardManager import com.fsck.k9.helper.ClipboardManager
@ -30,6 +31,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@Suppress("TooManyFunctions")
internal class MessageDetailsViewModel( internal class MessageDetailsViewModel(
private val resources: Resources, private val resources: Resources,
private val messageRepository: MessageRepository, private val messageRepository: MessageRepository,
@ -130,11 +132,17 @@ internal class MessageDetailsViewModel(
Participant( Participant(
displayName = displayName, displayName = displayName,
emailAddress = emailAddress, 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 { private fun Folder.toFolderInfo(): FolderInfoUi {
return FolderInfoUi( return FolderInfoUi(
displayName = folderNameFormatter.displayName(this), displayName = folderNameFormatter.displayName(this),

View file

@ -18,7 +18,8 @@ import android.widget.LinearLayout
import android.widget.ProgressBar import android.widget.ProgressBar
import android.widget.TextView import android.widget.TextView
import app.k9mail.core.android.common.contact.ContactRepository 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
import com.fsck.k9.Account.ShowPictures import com.fsck.k9.Account.ShowPictures
import com.fsck.k9.mail.Message import com.fsck.k9.mail.Message
@ -262,15 +263,15 @@ class MessageTopView(
return false return false
} }
val senderEmailAddress = getSenderEmailAddress(message) ?: 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 val from = message.from
return if (from == null || from.isEmpty()) { return if (from == null || from.isEmpty()) {
null null
} else { } else {
from[0].address from[0].address.toEmailAddressOrNull()
} }
} }

View file

@ -8,7 +8,8 @@
"thread_1", "thread_1",
"thread_2", "thread_2",
"inline_image_data_uri", "inline_image_data_uri",
"inline_image_attachment" "inline_image_attachment",
"localpart_exceeds_length_limit"
] ]
}, },
"trash": { "trash": {

View file

@ -0,0 +1,9 @@
MIME-Version: 1.0
From: Sender <aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeeeffffffffffggggg@k9mail.example> (local part exceeds maximum length)
Date: Thu, 15 Jun 2023 18:00:00 +0200
Message-ID: <localpart@k9mail.example>
Subject: Localpart of email address exceeds 64 characters
To: User <user@k9mail.example>
Content-Type: text/plain; charset=UTF-8
You should still be able to read this message.

View file

@ -6,7 +6,7 @@ import app.k9mail.autodiscovery.autoconfig.AutoconfigUrlConfig
import app.k9mail.autodiscovery.autoconfig.createIspDbAutoconfigDiscovery import app.k9mail.autodiscovery.autoconfig.createIspDbAutoconfigDiscovery
import app.k9mail.autodiscovery.autoconfig.createMxLookupAutoconfigDiscovery import app.k9mail.autodiscovery.autoconfig.createMxLookupAutoconfigDiscovery
import app.k9mail.autodiscovery.autoconfig.createProviderAutoconfigDiscovery 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.core.CliktCommand
import com.github.ajalt.clikt.parameters.arguments.argument import com.github.ajalt.clikt.parameters.arguments.argument
import com.github.ajalt.clikt.parameters.options.flag import com.github.ajalt.clikt.parameters.options.flag
@ -57,7 +57,7 @@ class AutoDiscoveryCli : CliktCommand(
val mxDiscovery = createMxLookupAutoconfigDiscovery(okHttpClient) val mxDiscovery = createMxLookupAutoconfigDiscovery(okHttpClient)
val runnables = listOf(providerDiscovery, ispDbDiscovery, mxDiscovery) val runnables = listOf(providerDiscovery, ispDbDiscovery, mxDiscovery)
.flatMap { it.initDiscovery(emailAddress.toEmailAddress()) } .flatMap { it.initDiscovery(emailAddress.toUserEmailAddress()) }
val serialRunner = SerialRunner(runnables) val serialRunner = SerialRunner(runnables)
return runBlocking { return runBlocking {

View file

@ -1,12 +1,12 @@
package app.k9mail.core.android.common.contact package app.k9mail.core.android.common.contact
import android.net.Uri 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_ID = 123L
const val CONTACT_NAME = "user name" const val CONTACT_NAME = "user name"
const val CONTACT_LOOKUP_KEY = "0r1-4F314D4F2F294F29" 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_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") val CONTACT_PHOTO_URI: Uri = Uri.parse("content://com.android.contacts/display_photo/$CONTACT_ID")

View file

@ -1,5 +1,12 @@
package app.k9mail.core.common.mail 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. * Represents an email address.
* *
@ -15,6 +22,14 @@ class EmailAddress internal constructor(
init { init {
warnings = buildSet { 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()) { if (localPart.isEmpty()) {
add(Warning.EmptyLocalPart) add(Warning.EmptyLocalPart)
} }
@ -65,6 +80,17 @@ class EmailAddress internal constructor(
} }
enum class Warning { 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. * The local part requires using a quoted string.
* *
@ -81,10 +107,32 @@ class EmailAddress internal constructor(
} }
companion object { 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() 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)

View file

@ -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.QuotedStringInLocalPart
import app.k9mail.core.common.mail.EmailAddressParserError.TotalLengthExceeded 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. * Parse an email address.
* *
@ -52,15 +45,23 @@ internal class EmailAddressParser(
parserError(ExpectedEndOfInput) parserError(ExpectedEndOfInput)
} }
if (emailAddress.address.length > MAXIMUM_EMAIL_ADDRESS_LENGTH) { if (
config.isEmailAddressLengthCheckEnabled && Warning.EmailAddressExceedsLengthLimit in emailAddress.warnings
) {
parserError(TotalLengthExceeded) 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) parserError(LocalPartRequiresQuotedString, position = 0)
} }
if (!config.allowEmptyLocalPart && Warning.EmptyLocalPart in emailAddress.warnings) { if (!config.isEmptyLocalPartAllowed && Warning.EmptyLocalPart in emailAddress.warnings) {
parserError(EmptyLocalPart, position = 1) parserError(EmptyLocalPart, position = 1)
} }
@ -83,7 +84,7 @@ internal class EmailAddressParser(
readDotString() readDotString()
} }
character == DQUOTE -> { character == DQUOTE -> {
if (config.allowQuotedLocalPart) { if (config.isQuotedLocalPartAllowed) {
readQuotedString() readQuotedString()
} else { } else {
parserError(QuotedStringInLocalPart) parserError(QuotedStringInLocalPart)
@ -94,10 +95,6 @@ internal class EmailAddressParser(
} }
} }
if (localPart.length > MAXIMUM_LOCAL_PART_LENGTH) {
parserError(LocalPartLengthExceeded)
}
return localPart return localPart
} }

View file

@ -3,39 +3,63 @@ package app.k9mail.core.common.mail
/** /**
* Configuration to control the behavior when parsing an email address into [EmailAddress]. * 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 * @param isLocalPartLengthCheckEnabled When this is `true` the length of the local part is checked to make sure it
* as quoted string, e.g. `"foo bar"@domain.example`. Otherwise, the parser will throw an [EmailAddressParserException] * doesn't exceed the specified limit (see RFC 5321, 4.5.3.1.1.).
* as soon as a quoted string is encountered. *
* @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. * 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 * @param isLocalPartRequiringQuotedStringAllowed Email addresses whose local part requires the use of a quoted string
* only allowed when this is `true`. This is separate from [allowQuotedLocalPart] because one might want to allow email * are only allowed when this is `true`. This is separate from [isQuotedLocalPartAllowed] because one might want to
* addresses that unnecessarily use a quoted string, e.g. `"test"@domain.example` ([allowQuotedLocalPart] = `true`, * allow email addresses that unnecessarily use a quoted string, e.g. `"test"@domain.example`
* [allowLocalPartRequiringQuotedString] = `false`; [EmailAddress] will not retain the original form and treat this * ([isQuotedLocalPartAllowed] = `true`, [isLocalPartRequiringQuotedStringAllowed] = `false`; [EmailAddress] will not
* address exactly like `test@domain.example`). When allowing this, remember to use the value of [EmailAddress.address] * retain the original form and treat this address exactly like `test@domain.example`). When allowing this, remember to
* instead of retaining the original user input. * 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 * @param isEmptyLocalPartAllowed Email addresses with an empty local part (e.g. `""@domain.example`) are only allowed
* this value is `true`. * if this value is `true`.
* *
* The value of this property is ignored if at least one of [allowQuotedLocalPart] and * The value of this property is ignored if at least one of [isQuotedLocalPartAllowed] and
* [allowLocalPartRequiringQuotedString] is `false`. * [isLocalPartRequiringQuotedStringAllowed] is `false`.
*/ */
data class EmailAddressParserConfig( data class EmailAddressParserConfig(
val allowQuotedLocalPart: Boolean, val isLocalPartLengthCheckEnabled: Boolean,
val allowLocalPartRequiringQuotedString: Boolean, val isEmailAddressLengthCheckEnabled: Boolean,
val allowEmptyLocalPart: Boolean = false, val isQuotedLocalPartAllowed: Boolean,
val isLocalPartRequiringQuotedStringAllowed: Boolean,
val isEmptyLocalPartAllowed: Boolean = false,
) { ) {
companion object { 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( val RELAXED = EmailAddressParserConfig(
allowQuotedLocalPart = true, isLocalPartLengthCheckEnabled = false,
allowLocalPartRequiringQuotedString = true, isEmailAddressLengthCheckEnabled = false,
allowEmptyLocalPart = 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,
) )
} }
} }

View file

@ -15,6 +15,7 @@ import app.k9mail.core.common.mail.EmailAddressParserError.UnexpectedCharacter
import assertk.all import assertk.all
import assertk.assertFailure import assertk.assertFailure
import assertk.assertThat import assertk.assertThat
import assertk.assertions.contains
import assertk.assertions.hasMessage import assertk.assertions.hasMessage
import assertk.assertions.isEqualTo import assertk.assertions.isEqualTo
import assertk.assertions.isInstanceOf import assertk.assertions.isInstanceOf
@ -40,7 +41,10 @@ class EmailAddressParserTest {
@Test @Test
fun `quoted local part`() { 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.localPart).isEqualTo("one two")
assertThat(emailAddress.domain).isEqualTo(EmailDomain("domain.example")) assertThat(emailAddress.domain).isEqualTo(EmailDomain("domain.example"))
@ -51,8 +55,8 @@ class EmailAddressParserTest {
assertFailure { assertFailure {
parseEmailAddress( parseEmailAddress(
address = "\"one two\"@domain.example", address = "\"one two\"@domain.example",
allowQuotedLocalPart = true, isQuotedLocalPartAllowed = true,
allowLocalPartRequiringQuotedString = false, isLocalPartRequiringQuotedStringAllowed = false,
) )
}.isInstanceOf<EmailAddressParserException>().all { }.isInstanceOf<EmailAddressParserException>().all {
prop(EmailAddressParserException::error).isEqualTo(LocalPartRequiresQuotedString) prop(EmailAddressParserException::error).isEqualTo(LocalPartRequiresQuotedString)
@ -65,8 +69,8 @@ class EmailAddressParserTest {
fun `unnecessarily quoted local part`() { fun `unnecessarily quoted local part`() {
val emailAddress = parseEmailAddress( val emailAddress = parseEmailAddress(
address = "\"user\"@domain.example", address = "\"user\"@domain.example",
allowQuotedLocalPart = true, isQuotedLocalPartAllowed = true,
allowLocalPartRequiringQuotedString = false, isLocalPartRequiringQuotedStringAllowed = false,
) )
assertThat(emailAddress.localPart).isEqualTo("user") assertThat(emailAddress.localPart).isEqualTo("user")
@ -77,7 +81,7 @@ class EmailAddressParserTest {
@Test @Test
fun `unnecessarily quoted local part not allowed`() { fun `unnecessarily quoted local part not allowed`() {
assertFailure { assertFailure {
parseEmailAddress("\"user\"@domain.example", allowQuotedLocalPart = false) parseEmailAddress("\"user\"@domain.example", isQuotedLocalPartAllowed = false)
}.isInstanceOf<EmailAddressParserException>().all { }.isInstanceOf<EmailAddressParserException>().all {
prop(EmailAddressParserException::error).isEqualTo(QuotedStringInLocalPart) prop(EmailAddressParserException::error).isEqualTo(QuotedStringInLocalPart)
prop(EmailAddressParserException::position).isEqualTo(0) prop(EmailAddressParserException::position).isEqualTo(0)
@ -87,7 +91,10 @@ class EmailAddressParserTest {
@Test @Test
fun `quoted local part containing double quote character`() { 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.localPart).isEqualTo("a\"b")
assertThat(emailAddress.domain).isEqualTo(EmailDomain("domain.example")) assertThat(emailAddress.domain).isEqualTo(EmailDomain("domain.example"))
@ -96,7 +103,7 @@ class EmailAddressParserTest {
@Test @Test
fun `empty local part`() { fun `empty local part`() {
val emailAddress = parseEmailAddress("\"\"@domain.example", allowEmptyLocalPart = true) val emailAddress = parseEmailAddress("\"\"@domain.example", isEmptyLocalPartAllowed = true)
assertThat(emailAddress.localPart).isEqualTo("") assertThat(emailAddress.localPart).isEqualTo("")
assertThat(emailAddress.domain).isEqualTo(EmailDomain("domain.example")) assertThat(emailAddress.domain).isEqualTo(EmailDomain("domain.example"))
@ -108,8 +115,8 @@ class EmailAddressParserTest {
assertFailure { assertFailure {
parseEmailAddress( parseEmailAddress(
address = "\"\"@domain.example", address = "\"\"@domain.example",
allowLocalPartRequiringQuotedString = true, isLocalPartRequiringQuotedStringAllowed = true,
allowEmptyLocalPart = false, isEmptyLocalPartAllowed = false,
) )
}.isInstanceOf<EmailAddressParserException>().all { }.isInstanceOf<EmailAddressParserException>().all {
prop(EmailAddressParserException::error).isEqualTo(EmptyLocalPart) prop(EmailAddressParserException::error).isEqualTo(EmptyLocalPart)
@ -154,7 +161,7 @@ class EmailAddressParserTest {
@Test @Test
fun `obsolete syntax`() { fun `obsolete syntax`() {
assertFailure { assertFailure {
parseEmailAddress("\"quoted\".atom@domain.example", allowLocalPartRequiringQuotedString = true) parseEmailAddress("\"quoted\".atom@domain.example", isLocalPartRequiringQuotedStringAllowed = true)
}.isInstanceOf<EmailAddressParserException>().all { }.isInstanceOf<EmailAddressParserException>().all {
prop(EmailAddressParserException::error).isEqualTo(UnexpectedCharacter) prop(EmailAddressParserException::error).isEqualTo(UnexpectedCharacter)
prop(EmailAddressParserException::position).isEqualTo(8) prop(EmailAddressParserException::position).isEqualTo(8)
@ -187,7 +194,7 @@ class EmailAddressParserTest {
@Test @Test
fun `quoted local part missing closing double quote`() { fun `quoted local part missing closing double quote`() {
assertFailure { assertFailure {
parseEmailAddress("\"invalid@domain.example", allowLocalPartRequiringQuotedString = true) parseEmailAddress("\"invalid@domain.example", isLocalPartRequiringQuotedStringAllowed = true)
}.isInstanceOf<EmailAddressParserException>().all { }.isInstanceOf<EmailAddressParserException>().all {
prop(EmailAddressParserException::error).isEqualTo(UnexpectedCharacter) prop(EmailAddressParserException::error).isEqualTo(UnexpectedCharacter)
prop(EmailAddressParserException::position).isEqualTo(23) prop(EmailAddressParserException::position).isEqualTo(23)
@ -198,7 +205,7 @@ class EmailAddressParserTest {
@Test @Test
fun `quoted text containing unsupported character`() { fun `quoted text containing unsupported character`() {
assertFailure { assertFailure {
parseEmailAddress("\"ä\"@domain.example", allowLocalPartRequiringQuotedString = true) parseEmailAddress("\"ä\"@domain.example", isLocalPartRequiringQuotedStringAllowed = true)
}.isInstanceOf<EmailAddressParserException>().all { }.isInstanceOf<EmailAddressParserException>().all {
prop(EmailAddressParserException::error).isEqualTo(InvalidQuotedString) prop(EmailAddressParserException::error).isEqualTo(InvalidQuotedString)
prop(EmailAddressParserException::position).isEqualTo(1) prop(EmailAddressParserException::position).isEqualTo(1)
@ -209,7 +216,7 @@ class EmailAddressParserTest {
@Test @Test
fun `quoted text containing unsupported escaped character`() { fun `quoted text containing unsupported escaped character`() {
assertFailure { assertFailure {
parseEmailAddress(""""\ä"@domain.example""", allowLocalPartRequiringQuotedString = true) parseEmailAddress(""""\ä"@domain.example""", isLocalPartRequiringQuotedStringAllowed = true)
}.isInstanceOf<EmailAddressParserException>().all { }.isInstanceOf<EmailAddressParserException>().all {
prop(EmailAddressParserException::error).isEqualTo(InvalidQuotedString) prop(EmailAddressParserException::error).isEqualTo(InvalidQuotedString)
prop(EmailAddressParserException::position).isEqualTo(3) prop(EmailAddressParserException::position).isEqualTo(3)
@ -218,9 +225,12 @@ class EmailAddressParserTest {
} }
@Test @Test
fun `local part exceeds maximum size`() { fun `local part exceeds maximum size with length check enabled`() {
assertFailure { assertFailure {
parseEmailAddress("1xxxxxxxxx2xxxxxxxxx3xxxxxxxxx4xxxxxxxxx5xxxxxxxxx6xxxxxxxxx12345@domain.example") parseEmailAddress(
address = "1xxxxxxxxx2xxxxxxxxx3xxxxxxxxx4xxxxxxxxx5xxxxxxxxx6xxxxxxxxx12345@domain.example",
isLocalPartLengthCheckEnabled = true,
)
}.isInstanceOf<EmailAddressParserException>().all { }.isInstanceOf<EmailAddressParserException>().all {
prop(EmailAddressParserException::error).isEqualTo(LocalPartLengthExceeded) prop(EmailAddressParserException::error).isEqualTo(LocalPartLengthExceeded)
prop(EmailAddressParserException::position).isEqualTo(65) prop(EmailAddressParserException::position).isEqualTo(65)
@ -229,13 +239,27 @@ class EmailAddressParserTest {
} }
@Test @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 { assertFailure {
parseEmailAddress( parseEmailAddress(
"1xxxxxxxxx2xxxxxxxxx3xxxxxxxxx4xxxxxxxxx5xxxxxxxxx6xxxxxxxxx1234@" + address = "1xxxxxxxxx2xxxxxxxxx3xxxxxxxxx4xxxxxxxxx5xxxxxxxxx6xxxxxxxxx1234@" +
"1xxxxxxxxx2xxxxxxxxx3xxxxxxxxx4xxxxxxxxx5xxxxxxxxx6xxxxxxxxx123." + "1xxxxxxxxx2xxxxxxxxx3xxxxxxxxx4xxxxxxxxx5xxxxxxxxx6xxxxxxxxx123." +
"1xxxxxxxxx2xxxxxxxxx3xxxxxxxxx4xxxxxxxxx5xxxxxxxxx6xxxxxxxxx123." + "1xxxxxxxxx2xxxxxxxxx3xxxxxxxxx4xxxxxxxxx5xxxxxxxxx6xxxxxxxxx123." +
"1xxxxxxxxx2xxxxxxxxx3xxxxxxxxx4xxxxxxxxx5xxxxxxxxx6xxxxxxxxx12", "1xxxxxxxxx2xxxxxxxxx3xxxxxxxxx4xxxxxxxxx5xxxxxxxxx6xxxxxxxxx12",
isEmailAddressLengthCheckEnabled = true,
) )
}.isInstanceOf<EmailAddressParserException>().all { }.isInstanceOf<EmailAddressParserException>().all {
prop(EmailAddressParserException::error).isEqualTo(TotalLengthExceeded) 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 @Test
fun `input contains additional character`() { fun `input contains additional character`() {
assertFailure { assertFailure {
@ -257,14 +303,18 @@ class EmailAddressParserTest {
private fun parseEmailAddress( private fun parseEmailAddress(
address: String, address: String,
allowEmptyLocalPart: Boolean = false, isLocalPartLengthCheckEnabled: Boolean = false,
allowLocalPartRequiringQuotedString: Boolean = allowEmptyLocalPart, isEmailAddressLengthCheckEnabled: Boolean = false,
allowQuotedLocalPart: Boolean = allowLocalPartRequiringQuotedString, isEmptyLocalPartAllowed: Boolean = false,
isLocalPartRequiringQuotedStringAllowed: Boolean = isEmptyLocalPartAllowed,
isQuotedLocalPartAllowed: Boolean = isLocalPartRequiringQuotedStringAllowed,
): EmailAddress { ): EmailAddress {
val config = EmailAddressParserConfig( val config = EmailAddressParserConfig(
allowQuotedLocalPart, isLocalPartLengthCheckEnabled,
allowLocalPartRequiringQuotedString, isEmailAddressLengthCheckEnabled,
allowEmptyLocalPart, isQuotedLocalPartAllowed,
isLocalPartRequiringQuotedStringAllowed,
isEmptyLocalPartAllowed,
) )
return EmailAddressParser(address, config).parse() return EmailAddressParser(address, config).parse()
} }

View file

@ -6,7 +6,7 @@ import app.k9mail.autodiscovery.api.AutoDiscoveryService
import app.k9mail.autodiscovery.api.ConnectionSecurity import app.k9mail.autodiscovery.api.ConnectionSecurity
import app.k9mail.autodiscovery.api.ImapServerSettings import app.k9mail.autodiscovery.api.ImapServerSettings
import app.k9mail.autodiscovery.api.SmtpServerSettings 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.toHostname
import app.k9mail.core.common.net.toPort import app.k9mail.core.common.net.toPort
import app.k9mail.feature.account.setup.domain.DomainContract import app.k9mail.feature.account.setup.domain.DomainContract
@ -34,7 +34,7 @@ internal class GetAutoDiscovery(
return provideWithDelay(fakeResult) return provideWithDelay(fakeResult)
} }
return service.discover(emailAddress.toEmailAddress())!! return service.discover(emailAddress.toUserEmailAddress())!!
} }
@Suppress("MagicNumber") @Suppress("MagicNumber")

View file

@ -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_ONE
import app.k9mail.autodiscovery.autoconfig.MockAutoconfigFetcher.Companion.RESULT_TWO 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 app.k9mail.core.common.net.toDomain
import assertk.assertThat import assertk.assertThat
import assertk.assertions.containsExactly import assertk.assertions.containsExactly
@ -13,7 +13,7 @@ import kotlin.test.Test
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import okhttp3.HttpUrl.Companion.toHttpUrl 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 { class AutoconfigDiscoveryTest {
private val urlProvider = MockAutoconfigUrlProvider() private val urlProvider = MockAutoconfigUrlProvider()
@ -22,7 +22,7 @@ class AutoconfigDiscoveryTest {
@Test @Test
fun `AutoconfigFetcher and AutoconfigParser should only be called when AutoDiscoveryRunnable is run`() = runTest { 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() val autoconfigUrl = "https://autoconfig.domain.invalid/mail/config-v1.1.xml".toHttpUrl()
urlProvider.addResult(listOf(autoconfigUrl)) urlProvider.addResult(listOf(autoconfigUrl))
autoconfigFetcher.addResult(RESULT_ONE) autoconfigFetcher.addResult(RESULT_ONE)

View file

@ -2,7 +2,7 @@ package app.k9mail.autodiscovery.autoconfig
import app.k9mail.autodiscovery.api.AutoDiscoveryResult.NoUsableSettingsFound import app.k9mail.autodiscovery.api.AutoDiscoveryResult.NoUsableSettingsFound
import app.k9mail.autodiscovery.autoconfig.MockAutoconfigFetcher.Companion.RESULT_ONE 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 app.k9mail.core.common.net.toDomain
import assertk.assertThat import assertk.assertThat
import assertk.assertions.containsExactly import assertk.assertions.containsExactly
@ -28,7 +28,7 @@ class MxLookupAutoconfigDiscoveryTest {
@Test @Test
fun `AutoconfigUrlProvider should be called with MX base domain`() = runTest { 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()) mxResolver.addResult("mx.emailprovider.example".toDomain())
urlProvider.addResult(listOf("https://ispdb.invalid/emailprovider.example".toHttpUrl())) urlProvider.addResult(listOf("https://ispdb.invalid/emailprovider.example".toHttpUrl()))
autoconfigFetcher.addResult(RESULT_ONE) autoconfigFetcher.addResult(RESULT_ONE)
@ -50,7 +50,7 @@ class MxLookupAutoconfigDiscoveryTest {
@Test @Test
fun `AutoconfigUrlProvider should be called with MX base domain and subdomain`() = runTest { 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()) mxResolver.addResult("mx.something.emailprovider.example".toDomain())
urlProvider.apply { urlProvider.apply {
addResult(listOf("https://ispdb.invalid/something.emailprovider.example".toHttpUrl())) addResult(listOf("https://ispdb.invalid/something.emailprovider.example".toHttpUrl()))
@ -74,7 +74,7 @@ class MxLookupAutoconfigDiscoveryTest {
@Test @Test
fun `skip Autoconfig lookup when MX lookup does not return a result`() = runTest { 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()) mxResolver.addResult(emptyList())
val autoDiscoveryRunnables = discovery.initDiscovery(emailAddress) val autoDiscoveryRunnables = discovery.initDiscovery(emailAddress)
@ -88,7 +88,7 @@ class MxLookupAutoconfigDiscoveryTest {
@Test @Test
fun `skip Autoconfig lookup when base domain of MX record is email domain`() = runTest { 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()) mxResolver.addResult("mx.company.example".toDomain())
val autoDiscoveryRunnables = discovery.initDiscovery(emailAddress) val autoDiscoveryRunnables = discovery.initDiscovery(emailAddress)
@ -102,7 +102,7 @@ class MxLookupAutoconfigDiscoveryTest {
@Test @Test
fun `isTrusted should be false when MxLookupResult_isTrusted is false`() = runTest { 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) mxResolver.addResult("mx.emailprovider.example".toDomain(), isTrusted = false)
urlProvider.addResult(listOf("https://ispdb.invalid/emailprovider.example".toHttpUrl())) urlProvider.addResult(listOf("https://ispdb.invalid/emailprovider.example".toHttpUrl()))
autoconfigFetcher.addResult(RESULT_ONE.copy(isTrusted = true)) autoconfigFetcher.addResult(RESULT_ONE.copy(isTrusted = true))
@ -115,7 +115,7 @@ class MxLookupAutoconfigDiscoveryTest {
@Test @Test
fun `isTrusted should be false when AutoDiscoveryResult_isTrusted from AutoconfigFetcher is false`() = runTest { 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) mxResolver.addResult("mx.emailprovider.example".toDomain(), isTrusted = true)
urlProvider.addResult(listOf("https://ispdb.invalid/emailprovider.example".toHttpUrl())) urlProvider.addResult(listOf("https://ispdb.invalid/emailprovider.example".toHttpUrl()))
autoconfigFetcher.addResult(RESULT_ONE.copy(isTrusted = false)) autoconfigFetcher.addResult(RESULT_ONE.copy(isTrusted = false))

View file

@ -1,6 +1,6 @@
package app.k9mail.autodiscovery.autoconfig 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 app.k9mail.core.common.net.toDomain
import assertk.assertThat import assertk.assertThat
import assertk.assertions.containsExactly import assertk.assertions.containsExactly
@ -8,7 +8,7 @@ import org.junit.Test
class ProviderAutoconfigUrlProviderTest { class ProviderAutoconfigUrlProviderTest {
private val domain = "domain.example".toDomain() private val domain = "domain.example".toDomain()
private val email = "test@domain.example".toEmailAddress() private val email = "test@domain.example".toUserEmailAddress()
@Test @Test
fun `getAutoconfigUrls with http allowed and email address included`() { fun `getAutoconfigUrls with http allowed and email address included`() {

View file

@ -8,7 +8,7 @@ import app.k9mail.autodiscovery.api.ImapServerSettings
import app.k9mail.autodiscovery.api.SmtpServerSettings import app.k9mail.autodiscovery.api.SmtpServerSettings
import app.k9mail.autodiscovery.autoconfig.AutoconfigParserResult.ParserError import app.k9mail.autodiscovery.autoconfig.AutoconfigParserResult.ParserError
import app.k9mail.autodiscovery.autoconfig.AutoconfigParserResult.Settings 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.toHostname
import app.k9mail.core.common.net.toPort import app.k9mail.core.common.net.toPort
import assertk.assertThat import assertk.assertThat
@ -55,13 +55,13 @@ class RealAutoconfigParserTest {
</clientConfig> </clientConfig>
""".trimIndent() """.trimIndent()
private val irrelevantEmailAddress = "irrelevant@domain.example".toEmailAddress() private val irrelevantEmailAddress = "irrelevant@domain.example".toUserEmailAddress()
@Test @Test
fun `minimal data`() { fun `minimal data`() {
val inputStream = minimalConfig.byteInputStream() 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( assertThat(result).isNotNull().isEqualTo(
Settings( Settings(
@ -87,7 +87,7 @@ class RealAutoconfigParserTest {
fun `real-world data`() { fun `real-world data`() {
val inputStream = javaClass.getResourceAsStream("/2022-11-19-googlemail.com.xml")!! 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( assertThat(result).isNotNull().isEqualTo(
Settings( Settings(
@ -117,7 +117,7 @@ class RealAutoconfigParserTest {
element("outgoingServer > username").text("%EMAILDOMAIN%") 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( assertThat(result).isNotNull().isEqualTo(
Settings( Settings(
@ -149,7 +149,7 @@ class RealAutoconfigParserTest {
element("incomingServer > username").prepend("<!-- comment -->") element("incomingServer > username").prepend("<!-- comment -->")
} }
val result = parser.parseSettings(inputStream, email = "user@domain.example".toEmailAddress()) val result = parser.parseSettings(inputStream, email = "user@domain.example".toUserEmailAddress())
assertThat(result).isInstanceOf<Settings>() assertThat(result).isInstanceOf<Settings>()
.prop(Settings::incomingServerSettings).isEqualTo( .prop(Settings::incomingServerSettings).isEqualTo(
@ -169,7 +169,7 @@ class RealAutoconfigParserTest {
element("incomingServer").insertBefore("""<incomingServer type="smtp"/>""") element("incomingServer").insertBefore("""<incomingServer type="smtp"/>""")
} }
val result = parser.parseSettings(inputStream, email = "user@domain.example".toEmailAddress()) val result = parser.parseSettings(inputStream, email = "user@domain.example".toUserEmailAddress())
assertThat(result).isInstanceOf<Settings>() assertThat(result).isInstanceOf<Settings>()
.prop(Settings::incomingServerSettings).isEqualTo( .prop(Settings::incomingServerSettings).isEqualTo(
@ -189,7 +189,7 @@ class RealAutoconfigParserTest {
element("outgoingServer").insertBefore("""<outgoingServer type="imap"/>""") element("outgoingServer").insertBefore("""<outgoingServer type="imap"/>""")
} }
val result = parser.parseSettings(inputStream, email = "user@domain.example".toEmailAddress()) val result = parser.parseSettings(inputStream, email = "user@domain.example".toUserEmailAddress())
assertThat(result).isInstanceOf<Settings>() assertThat(result).isInstanceOf<Settings>()
.prop(Settings::outgoingServerSettings).isEqualTo( .prop(Settings::outgoingServerSettings).isEqualTo(
@ -209,7 +209,7 @@ class RealAutoconfigParserTest {
element("incomingServer > authentication").insertBefore("<authentication></authentication>") element("incomingServer > authentication").insertBefore("<authentication></authentication>")
} }
val result = parser.parseSettings(inputStream, email = "user@domain.example".toEmailAddress()) val result = parser.parseSettings(inputStream, email = "user@domain.example".toUserEmailAddress())
assertThat(result).isInstanceOf<Settings>() assertThat(result).isInstanceOf<Settings>()
.prop(Settings::incomingServerSettings).isEqualTo( .prop(Settings::incomingServerSettings).isEqualTo(