Merge pull request #6987 from thundernest/relaxed_email_address
Change `EmailAddressParser` to support addresses violating the specification
This commit is contained in:
commit
bf73b0a787
20 changed files with 260 additions and 107 deletions
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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": {
|
||||||
|
|
|
@ -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.
|
|
@ -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 {
|
||||||
|
|
|
@ -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")
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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`() {
|
||||||
|
|
|
@ -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(
|
||||||
|
|
Loading…
Reference in a new issue