Merge pull request #6938 from thundernest/email_address
Add proper email address parser
This commit is contained in:
commit
2daea4dc30
24 changed files with 1065 additions and 50 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.EmailAddress
|
import app.k9mail.core.common.mail.toEmailAddress
|
||||||
|
|
||||||
interface ContactNameProvider {
|
interface ContactNameProvider {
|
||||||
fun getNameForAddress(address: String): String?
|
fun getNameForAddress(address: String): String?
|
||||||
|
@ -11,6 +11,6 @@ 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(EmailAddress(address))?.name
|
return contactRepository.getContactFor(address.toEmailAddress())?.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.EmailAddress
|
import app.k9mail.core.common.mail.toEmailAddress
|
||||||
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(EmailAddress(address.address))?.name
|
val name = contactRepository.getContactFor(address.address.toEmailAddress())?.name
|
||||||
if (name != null) {
|
if (name != null) {
|
||||||
return if (changeContactNameColor) {
|
return if (changeContactNameColor) {
|
||||||
val coloredName = SpannableString(name)
|
val coloredName = SpannableString(name)
|
||||||
|
|
|
@ -5,6 +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 assertk.assertThat
|
import assertk.assertThat
|
||||||
import assertk.assertions.isEqualTo
|
import assertk.assertions.isEqualTo
|
||||||
import assertk.assertions.isInstanceOf
|
import assertk.assertions.isInstanceOf
|
||||||
|
@ -142,6 +143,6 @@ class MessageHelperTest : RobolectricTest() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
val EMAIL_ADDRESS = EmailAddress("test@testor.com")
|
val EMAIL_ADDRESS = "test@testor.com".toEmailAddress()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.EmailAddress
|
import app.k9mail.core.common.mail.toEmailAddress
|
||||||
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 { EmailAddress(it.address) })
|
!contactRepository.hasAnyContactFor(message.from.asList().mapNotNull { it.address.toEmailAddress() })
|
||||||
) {
|
) {
|
||||||
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
|
||||||
|
|
|
@ -4,7 +4,7 @@ import android.content.ContentResolver
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
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.toEmailAddress
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
internal class ContactPhotoLoader(
|
internal class ContactPhotoLoader(
|
||||||
|
@ -12,7 +12,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(emailAddress))?.photoUri ?: return null
|
val photoUri = contactRepository.getContactFor(emailAddress.toEmailAddress())?.photoUri ?: return null
|
||||||
return try {
|
return try {
|
||||||
contentResolver.openInputStream(photoUri).use { inputStream ->
|
contentResolver.openInputStream(photoUri).use { inputStream ->
|
||||||
BitmapFactory.decodeStream(inputStream)
|
BitmapFactory.decodeStream(inputStream)
|
||||||
|
|
|
@ -7,7 +7,7 @@ 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.EmailAddress
|
import app.k9mail.core.common.mail.toEmailAddress
|
||||||
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
|
||||||
|
@ -130,7 +130,7 @@ internal class MessageDetailsViewModel(
|
||||||
Participant(
|
Participant(
|
||||||
displayName = displayName,
|
displayName = displayName,
|
||||||
emailAddress = emailAddress,
|
emailAddress = emailAddress,
|
||||||
contactLookupUri = contactRepository.getContactFor(EmailAddress(emailAddress))?.uri,
|
contactLookupUri = contactRepository.getContactFor(emailAddress.toEmailAddress())?.uri,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,7 @@ 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.EmailAddress
|
import app.k9mail.core.common.mail.toEmailAddress
|
||||||
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,7 +262,7 @@ class MessageTopView(
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
val senderEmailAddress = getSenderEmailAddress(message) ?: return false
|
val senderEmailAddress = getSenderEmailAddress(message) ?: return false
|
||||||
return contactRepository.hasContactFor(EmailAddress(senderEmailAddress))
|
return contactRepository.hasContactFor(senderEmailAddress.toEmailAddress())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getSenderEmailAddress(message: Message): String? {
|
private fun getSenderEmailAddress(message: Message): String? {
|
||||||
|
|
|
@ -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.EmailAddress
|
import app.k9mail.core.common.mail.toEmailAddress
|
||||||
|
|
||||||
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 = EmailAddress("user@example.com")
|
val CONTACT_EMAIL_ADDRESS = "user@example.com".toEmailAddress()
|
||||||
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")
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,81 @@
|
||||||
|
package app.k9mail.core.common.mail
|
||||||
|
|
||||||
|
import app.k9mail.core.common.mail.EmailAddressParserError.UnexpectedCharacter
|
||||||
|
import app.k9mail.core.common.mail.EmailAddressParserError.UnexpectedEndOfInput
|
||||||
|
|
||||||
|
@Suppress("UnnecessaryAbstractClass")
|
||||||
|
internal abstract class AbstractParser(val input: String, startIndex: Int = 0, val endIndex: Int = input.length) {
|
||||||
|
protected var currentIndex = startIndex
|
||||||
|
|
||||||
|
val position: Int
|
||||||
|
get() = currentIndex
|
||||||
|
|
||||||
|
fun endReached() = currentIndex >= endIndex
|
||||||
|
|
||||||
|
fun peek(): Char {
|
||||||
|
if (currentIndex >= endIndex) {
|
||||||
|
parserError(UnexpectedEndOfInput)
|
||||||
|
}
|
||||||
|
|
||||||
|
return input[currentIndex]
|
||||||
|
}
|
||||||
|
|
||||||
|
fun read(): Char {
|
||||||
|
if (currentIndex >= endIndex) {
|
||||||
|
parserError(UnexpectedEndOfInput)
|
||||||
|
}
|
||||||
|
|
||||||
|
return input[currentIndex].also { currentIndex++ }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun expect(character: Char) {
|
||||||
|
if (!endReached() && peek() == character) {
|
||||||
|
currentIndex++
|
||||||
|
} else {
|
||||||
|
parserError(UnexpectedCharacter, message = "Expected '$character' (${character.code})")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("SameParameterValue")
|
||||||
|
protected inline fun expect(displayInError: String, predicate: (Char) -> Boolean) {
|
||||||
|
if (!endReached() && predicate(peek())) {
|
||||||
|
skip()
|
||||||
|
} else {
|
||||||
|
parserError(UnexpectedCharacter, message = "Expected $displayInError")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("NOTHING_TO_INLINE")
|
||||||
|
protected inline fun skip() {
|
||||||
|
currentIndex++
|
||||||
|
}
|
||||||
|
|
||||||
|
protected inline fun skipWhile(crossinline predicate: (Char) -> Boolean) {
|
||||||
|
while (!endReached() && predicate(input[currentIndex])) {
|
||||||
|
currentIndex++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected inline fun readString(block: () -> Unit): String {
|
||||||
|
val startIndex = currentIndex
|
||||||
|
block()
|
||||||
|
return input.substring(startIndex, currentIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected inline fun <P : AbstractParser, T> withParser(parser: P, block: P.() -> T): T {
|
||||||
|
try {
|
||||||
|
return block(parser)
|
||||||
|
} finally {
|
||||||
|
currentIndex = parser.position
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("NOTHING_TO_INLINE")
|
||||||
|
protected inline fun parserError(
|
||||||
|
error: EmailAddressParserError,
|
||||||
|
position: Int = currentIndex,
|
||||||
|
message: String = error.message,
|
||||||
|
): Nothing {
|
||||||
|
throw EmailAddressParserException(message, error, input, position)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,10 +1,90 @@
|
||||||
package app.k9mail.core.common.mail
|
package app.k9mail.core.common.mail
|
||||||
|
|
||||||
@JvmInline
|
/**
|
||||||
value class EmailAddress(val address: String) {
|
* Represents an email address.
|
||||||
|
*
|
||||||
|
* This class currently doesn't support internationalized domain names (RFC 5891) or non-ASCII local parts (RFC 6532).
|
||||||
|
*/
|
||||||
|
class EmailAddress internal constructor(
|
||||||
|
val localPart: String,
|
||||||
|
val domain: EmailDomain,
|
||||||
|
) {
|
||||||
|
val encodedLocalPart: String = if (localPart.isDotString) localPart else quoteString(localPart)
|
||||||
|
|
||||||
|
val warnings: Set<Warning>
|
||||||
|
|
||||||
init {
|
init {
|
||||||
require(address.isNotBlank()) { "Email address must not be blank" }
|
warnings = buildSet {
|
||||||
|
if (localPart.isEmpty()) {
|
||||||
|
add(Warning.EmptyLocalPart)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!localPart.isDotString) {
|
||||||
|
add(Warning.QuotedStringInLocalPart)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun String.toEmailAddress() = EmailAddress(this)
|
val address: String
|
||||||
|
get() = "$encodedLocalPart@$domain"
|
||||||
|
|
||||||
|
val normalizedAddress: String
|
||||||
|
get() = "$encodedLocalPart@${domain.normalized}"
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return address
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (other == null || this::class != other::class) return false
|
||||||
|
|
||||||
|
other as EmailAddress
|
||||||
|
|
||||||
|
if (localPart != other.localPart) return false
|
||||||
|
return domain == other.domain
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = localPart.hashCode()
|
||||||
|
result = 31 * result + domain.hashCode()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun quoteString(input: String): String {
|
||||||
|
return buildString {
|
||||||
|
append(DQUOTE)
|
||||||
|
for (character in input) {
|
||||||
|
if (!character.isQtext) {
|
||||||
|
append(BACKSLASH)
|
||||||
|
}
|
||||||
|
append(character)
|
||||||
|
}
|
||||||
|
append(DQUOTE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class Warning {
|
||||||
|
/**
|
||||||
|
* The local part requires using a quoted string.
|
||||||
|
*
|
||||||
|
* This is valid, but very uncommon. Using such a local part should be avoided whenever possible.
|
||||||
|
*/
|
||||||
|
QuotedStringInLocalPart,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The local part is the empty string.
|
||||||
|
*
|
||||||
|
* Even if you want to allow quoted strings, you probably don't want to allow this.
|
||||||
|
*/
|
||||||
|
EmptyLocalPart,
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun parse(address: String, config: EmailAddressParserConfig = EmailAddressParserConfig.DEFAULT): EmailAddress {
|
||||||
|
return EmailAddressParser(address, config).parse()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun String.toEmailAddress() = EmailAddress.parse(this)
|
||||||
|
|
|
@ -0,0 +1,167 @@
|
||||||
|
package app.k9mail.core.common.mail
|
||||||
|
|
||||||
|
import app.k9mail.core.common.mail.EmailAddress.Warning
|
||||||
|
import app.k9mail.core.common.mail.EmailAddressParserError.AddressLiteralsNotSupported
|
||||||
|
import app.k9mail.core.common.mail.EmailAddressParserError.EmptyLocalPart
|
||||||
|
import app.k9mail.core.common.mail.EmailAddressParserError.ExpectedEndOfInput
|
||||||
|
import app.k9mail.core.common.mail.EmailAddressParserError.InvalidDomainPart
|
||||||
|
import app.k9mail.core.common.mail.EmailAddressParserError.InvalidDotString
|
||||||
|
import app.k9mail.core.common.mail.EmailAddressParserError.InvalidLocalPart
|
||||||
|
import app.k9mail.core.common.mail.EmailAddressParserError.InvalidQuotedString
|
||||||
|
import app.k9mail.core.common.mail.EmailAddressParserError.LocalPartLengthExceeded
|
||||||
|
import app.k9mail.core.common.mail.EmailAddressParserError.LocalPartRequiresQuotedString
|
||||||
|
import app.k9mail.core.common.mail.EmailAddressParserError.QuotedStringInLocalPart
|
||||||
|
import app.k9mail.core.common.mail.EmailAddressParserError.TotalLengthExceeded
|
||||||
|
|
||||||
|
// See RFC 5321, 4.5.3.1.3.
|
||||||
|
// The maximum length of 'Path' indirectly limits the length of 'Mailbox'.
|
||||||
|
internal const val MAXIMUM_EMAIL_ADDRESS_LENGTH = 254
|
||||||
|
|
||||||
|
// See RFC 5321, 4.5.3.1.1.
|
||||||
|
internal const val MAXIMUM_LOCAL_PART_LENGTH = 64
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse an email address.
|
||||||
|
*
|
||||||
|
* This class currently doesn't support internationalized domain names (RFC 5891) or non-ASCII local parts (RFC 6532).
|
||||||
|
*
|
||||||
|
* From RFC 5321:
|
||||||
|
* ```
|
||||||
|
* Mailbox = Local-part "@" ( Domain / address-literal )
|
||||||
|
*
|
||||||
|
* Local-part = Dot-string / Quoted-string
|
||||||
|
* Dot-string = Atom *("." Atom)
|
||||||
|
* Quoted-string = DQUOTE *QcontentSMTP DQUOTE
|
||||||
|
* QcontentSMTP = qtextSMTP / quoted-pairSMTP
|
||||||
|
* qtextSMTP = %d32-33 / %d35-91 / %d93-126
|
||||||
|
* quoted-pairSMTP = %d92 %d32-126
|
||||||
|
*
|
||||||
|
* Domain - see DomainParser
|
||||||
|
* address-literal - We intentionally don't support address literals
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
internal class EmailAddressParser(
|
||||||
|
input: String,
|
||||||
|
private val config: EmailAddressParserConfig,
|
||||||
|
) : AbstractParser(input) {
|
||||||
|
|
||||||
|
fun parse(): EmailAddress {
|
||||||
|
val emailAddress = readEmailAddress()
|
||||||
|
|
||||||
|
if (!endReached()) {
|
||||||
|
parserError(ExpectedEndOfInput)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (emailAddress.address.length > MAXIMUM_EMAIL_ADDRESS_LENGTH) {
|
||||||
|
parserError(TotalLengthExceeded)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!config.allowLocalPartRequiringQuotedString && Warning.QuotedStringInLocalPart in emailAddress.warnings) {
|
||||||
|
parserError(LocalPartRequiresQuotedString, position = 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!config.allowEmptyLocalPart && Warning.EmptyLocalPart in emailAddress.warnings) {
|
||||||
|
parserError(EmptyLocalPart, position = 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return emailAddress
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun readEmailAddress(): EmailAddress {
|
||||||
|
val localPart = readLocalPart()
|
||||||
|
|
||||||
|
expect(AT)
|
||||||
|
val domain = readDomainPart()
|
||||||
|
|
||||||
|
return EmailAddress(localPart, domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun readLocalPart(): String {
|
||||||
|
val character = peek()
|
||||||
|
val localPart = when {
|
||||||
|
character.isAtext -> {
|
||||||
|
readDotString()
|
||||||
|
}
|
||||||
|
character == DQUOTE -> {
|
||||||
|
if (config.allowQuotedLocalPart) {
|
||||||
|
readQuotedString()
|
||||||
|
} else {
|
||||||
|
parserError(QuotedStringInLocalPart)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
parserError(InvalidLocalPart)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (localPart.length > MAXIMUM_LOCAL_PART_LENGTH) {
|
||||||
|
parserError(LocalPartLengthExceeded)
|
||||||
|
}
|
||||||
|
|
||||||
|
return localPart
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun readDotString(): String {
|
||||||
|
return buildString {
|
||||||
|
appendAtom()
|
||||||
|
|
||||||
|
while (!endReached() && peek() == DOT) {
|
||||||
|
expect(DOT)
|
||||||
|
append(DOT)
|
||||||
|
appendAtom()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun StringBuilder.appendAtom() {
|
||||||
|
val startIndex = currentIndex
|
||||||
|
skipWhile { it.isAtext }
|
||||||
|
|
||||||
|
if (startIndex == currentIndex) {
|
||||||
|
parserError(InvalidDotString)
|
||||||
|
}
|
||||||
|
|
||||||
|
append(input, startIndex, currentIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun readQuotedString(): String {
|
||||||
|
return buildString {
|
||||||
|
expect(DQUOTE)
|
||||||
|
|
||||||
|
while (!endReached()) {
|
||||||
|
val character = peek()
|
||||||
|
when {
|
||||||
|
character.isQtext -> append(read())
|
||||||
|
character == BACKSLASH -> {
|
||||||
|
expect(BACKSLASH)
|
||||||
|
val escapedCharacter = read()
|
||||||
|
if (!escapedCharacter.isQuotedChar) {
|
||||||
|
parserError(InvalidQuotedString)
|
||||||
|
}
|
||||||
|
append(escapedCharacter)
|
||||||
|
}
|
||||||
|
|
||||||
|
character == DQUOTE -> break
|
||||||
|
else -> parserError(InvalidQuotedString)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(DQUOTE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun readDomainPart(): EmailDomain {
|
||||||
|
val character = peek()
|
||||||
|
return when {
|
||||||
|
character.isLetDig -> readDomain()
|
||||||
|
character == '[' -> parserError(AddressLiteralsNotSupported)
|
||||||
|
else -> parserError(InvalidDomainPart)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun readDomain(): EmailDomain {
|
||||||
|
return withParser(EmailDomainParser(input, currentIndex)) {
|
||||||
|
readDomain()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
package app.k9mail.core.common.mail
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration to control the behavior when parsing an email address into [EmailAddress].
|
||||||
|
*
|
||||||
|
* @param allowQuotedLocalPart When this is `true`, the parsing step allows email addresses with a local part encoded
|
||||||
|
* as quoted string, e.g. `"foo bar"@domain.example`. Otherwise, the parser will throw an [EmailAddressParserException]
|
||||||
|
* as soon as a quoted string is encountered.
|
||||||
|
* Quoted strings in local parts are not widely used. It's recommended to disallow them whenever possible.
|
||||||
|
*
|
||||||
|
* @param allowLocalPartRequiringQuotedString Email addresses whose local part requires the use of a quoted string are
|
||||||
|
* only allowed when this is `true`. This is separate from [allowQuotedLocalPart] because one might want to allow email
|
||||||
|
* addresses that unnecessarily use a quoted string, e.g. `"test"@domain.example` ([allowQuotedLocalPart] = `true`,
|
||||||
|
* [allowLocalPartRequiringQuotedString] = `false`; [EmailAddress] will not retain the original form and treat this
|
||||||
|
* address exactly like `test@domain.example`). When allowing this, remember to use the value of [EmailAddress.address]
|
||||||
|
* instead of retaining the original user input.
|
||||||
|
*
|
||||||
|
* The value of this property is ignored if [allowQuotedLocalPart] is `false`.
|
||||||
|
*
|
||||||
|
* @param allowEmptyLocalPart Email addresses with an empty local part (e.g. `""@domain.example`) are only allowed if
|
||||||
|
* this value is `true`.
|
||||||
|
*
|
||||||
|
* The value of this property is ignored if at least one of [allowQuotedLocalPart] and
|
||||||
|
* [allowLocalPartRequiringQuotedString] is `false`.
|
||||||
|
*/
|
||||||
|
data class EmailAddressParserConfig(
|
||||||
|
val allowQuotedLocalPart: Boolean,
|
||||||
|
val allowLocalPartRequiringQuotedString: Boolean,
|
||||||
|
val allowEmptyLocalPart: Boolean = false,
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
/**
|
||||||
|
* This configuration should match what `EmailAddressValidator` currently allows.
|
||||||
|
*/
|
||||||
|
val DEFAULT = EmailAddressParserConfig(
|
||||||
|
allowQuotedLocalPart = true,
|
||||||
|
allowLocalPartRequiringQuotedString = true,
|
||||||
|
allowEmptyLocalPart = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
package app.k9mail.core.common.mail
|
||||||
|
|
||||||
|
enum class EmailAddressParserError(internal val message: String) {
|
||||||
|
UnexpectedEndOfInput("End of input reached unexpectedly"),
|
||||||
|
ExpectedEndOfInput("Expected end of input"),
|
||||||
|
InvalidLocalPart("Expected 'Dot-string' or 'Quoted-string'"),
|
||||||
|
InvalidDotString("Expected 'Dot-string'"),
|
||||||
|
InvalidQuotedString("Expected 'Quoted-string'"),
|
||||||
|
InvalidDomainPart("Expected 'Domain' or 'address-literal'"),
|
||||||
|
AddressLiteralsNotSupported("Address literals are not supported"),
|
||||||
|
|
||||||
|
LocalPartLengthExceeded("Local part exceeds maximum length of $MAXIMUM_LOCAL_PART_LENGTH characters"),
|
||||||
|
DnsLabelLengthExceeded("DNS labels exceeds maximum length of $MAXIMUM_DNS_LABEL_LENGTH characters"),
|
||||||
|
DomainLengthExceeded("Domain exceeds maximum length of $MAXIMUM_DOMAIN_LENGTH characters"),
|
||||||
|
TotalLengthExceeded("The email address exceeds the maximum length of $MAXIMUM_EMAIL_ADDRESS_LENGTH characters"),
|
||||||
|
|
||||||
|
QuotedStringInLocalPart("Quoted string in local part is not allowed by config"),
|
||||||
|
LocalPartRequiresQuotedString("Local part requiring the use of a quoted string is not allowed by config"),
|
||||||
|
EmptyLocalPart("Empty local part is not allowed by config"),
|
||||||
|
|
||||||
|
UnexpectedCharacter("Caller needs to provide message"),
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
package app.k9mail.core.common.mail
|
||||||
|
|
||||||
|
class EmailAddressParserException internal constructor(
|
||||||
|
message: String,
|
||||||
|
val error: EmailAddressParserError,
|
||||||
|
val input: String,
|
||||||
|
val position: Int,
|
||||||
|
) : RuntimeException(message)
|
|
@ -0,0 +1,51 @@
|
||||||
|
package app.k9mail.core.common.mail
|
||||||
|
|
||||||
|
import app.k9mail.core.common.net.Domain
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The domain part of an email address.
|
||||||
|
*
|
||||||
|
* @param value String representation of the email domain with the original capitalization.
|
||||||
|
*/
|
||||||
|
class EmailDomain internal constructor(val value: String) {
|
||||||
|
/**
|
||||||
|
* The normalized (converted to lower case) string representation of this email domain.
|
||||||
|
*/
|
||||||
|
val normalized: String = value.lowercase()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns this email domain with the original capitalization.
|
||||||
|
*
|
||||||
|
* @see value
|
||||||
|
*/
|
||||||
|
override fun toString(): String = value
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compares the normalized string representations of two [EmailDomain] instances.
|
||||||
|
*/
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (other == null || this::class != other::class) return false
|
||||||
|
|
||||||
|
other as EmailDomain
|
||||||
|
|
||||||
|
return normalized == other.normalized
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
return normalized.hashCode()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/**
|
||||||
|
* Parses the string representation of an email domain.
|
||||||
|
*
|
||||||
|
* @throws EmailAddressParserException in case of an error.
|
||||||
|
*/
|
||||||
|
fun parse(domain: String): EmailDomain {
|
||||||
|
return EmailDomainParser(domain).parseDomain()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun EmailDomain.toDomain() = Domain(value)
|
|
@ -0,0 +1,92 @@
|
||||||
|
package app.k9mail.core.common.mail
|
||||||
|
|
||||||
|
import app.k9mail.core.common.mail.EmailAddressParserError.DnsLabelLengthExceeded
|
||||||
|
import app.k9mail.core.common.mail.EmailAddressParserError.DomainLengthExceeded
|
||||||
|
import app.k9mail.core.common.mail.EmailAddressParserError.ExpectedEndOfInput
|
||||||
|
|
||||||
|
// See RFC 1035, 2.3.4.
|
||||||
|
// For the string representation used in emails (labels separated by dots, no final dot allowed), we end up with a
|
||||||
|
// maximum of 253 characters.
|
||||||
|
internal const val MAXIMUM_DOMAIN_LENGTH = 253
|
||||||
|
|
||||||
|
// See RFC 1035, 2.3.4.
|
||||||
|
internal const val MAXIMUM_DNS_LABEL_LENGTH = 63
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parser for domain names in email addresses.
|
||||||
|
*
|
||||||
|
* From RFC 5321:
|
||||||
|
* ```
|
||||||
|
* Domain = sub-domain *("." sub-domain)
|
||||||
|
* sub-domain = Let-dig [Ldh-str]
|
||||||
|
* Let-dig = ALPHA / DIGIT
|
||||||
|
* Ldh-str = *( ALPHA / DIGIT / "-" ) Let-dig
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
internal class EmailDomainParser(
|
||||||
|
input: String,
|
||||||
|
startIndex: Int = 0,
|
||||||
|
endIndex: Int = input.length,
|
||||||
|
) : AbstractParser(input, startIndex, endIndex) {
|
||||||
|
|
||||||
|
fun parseDomain(): EmailDomain {
|
||||||
|
val domain = readDomain()
|
||||||
|
|
||||||
|
if (!endReached()) {
|
||||||
|
parserError(ExpectedEndOfInput)
|
||||||
|
}
|
||||||
|
|
||||||
|
return domain
|
||||||
|
}
|
||||||
|
|
||||||
|
fun readDomain(): EmailDomain {
|
||||||
|
val domain = readString {
|
||||||
|
expectSubDomain()
|
||||||
|
|
||||||
|
while (!endReached() && peek() == DOT) {
|
||||||
|
expect(DOT)
|
||||||
|
expectSubDomain()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (domain.length > MAXIMUM_DOMAIN_LENGTH) {
|
||||||
|
parserError(DomainLengthExceeded)
|
||||||
|
}
|
||||||
|
|
||||||
|
return EmailDomain(domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun expectSubDomain() {
|
||||||
|
val startIndex = currentIndex
|
||||||
|
|
||||||
|
expectLetDig()
|
||||||
|
|
||||||
|
var requireLetDig = false
|
||||||
|
while (!endReached()) {
|
||||||
|
val character = peek()
|
||||||
|
when {
|
||||||
|
character == HYPHEN -> {
|
||||||
|
requireLetDig = true
|
||||||
|
expect(HYPHEN)
|
||||||
|
}
|
||||||
|
character.isLetDig -> {
|
||||||
|
requireLetDig = false
|
||||||
|
expectLetDig()
|
||||||
|
}
|
||||||
|
else -> break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requireLetDig) {
|
||||||
|
expectLetDig()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentIndex - startIndex > MAXIMUM_DNS_LABEL_LENGTH) {
|
||||||
|
parserError(DnsLabelLengthExceeded)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun expectLetDig() {
|
||||||
|
expect("'Let-dig'") { it.isLetDig }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,66 @@
|
||||||
|
@file:Suppress("MagicNumber")
|
||||||
|
|
||||||
|
package app.k9mail.core.common.mail
|
||||||
|
|
||||||
|
internal const val DQUOTE = '"'
|
||||||
|
internal const val DOT = '.'
|
||||||
|
internal const val AT = '@'
|
||||||
|
internal const val BACKSLASH = '\\'
|
||||||
|
internal const val HYPHEN = '-'
|
||||||
|
|
||||||
|
internal val ATEXT_EXTRA = charArrayOf(
|
||||||
|
'!', '#', '$', '%', '&', '\'', '*', '+', '-', '/', '=', '?', '^', '_', '`', '{', '|', '}', '~',
|
||||||
|
)
|
||||||
|
|
||||||
|
// RFC 5234: ALPHA = %x41-5A / %x61-7A ; A-Z / a-z
|
||||||
|
internal val Char.isALPHA
|
||||||
|
get() = this in 'A'..'Z' || this in 'a'..'z'
|
||||||
|
|
||||||
|
// RFC 5234: DIGIT = %x30-39 ; 0-9
|
||||||
|
internal val Char.isDIGIT
|
||||||
|
get() = this in '0'..'9'
|
||||||
|
|
||||||
|
// RFC 5322:
|
||||||
|
// atext = ALPHA / DIGIT / ; Printable US-ASCII
|
||||||
|
// "!" / "#" / ; characters not including
|
||||||
|
// "$" / "%" / ; specials. Used for atoms.
|
||||||
|
// "&" / "'" /
|
||||||
|
// "*" / "+" /
|
||||||
|
// "-" / "/" /
|
||||||
|
// "=" / "?" /
|
||||||
|
// "^" / "_" /
|
||||||
|
// "`" / "{" /
|
||||||
|
// "|" / "}" /
|
||||||
|
// "~"
|
||||||
|
internal val Char.isAtext
|
||||||
|
get() = isALPHA || isDIGIT || this in ATEXT_EXTRA
|
||||||
|
|
||||||
|
// RFC 5321: qtextSMTP = %d32-33 / %d35-91 / %d93-126
|
||||||
|
internal val Char.isQtext
|
||||||
|
get() = code.let { it in 32..33 || it in 35..91 || it in 93..126 }
|
||||||
|
|
||||||
|
// RFC 5321: second character of quoted-pairSMTP = %d92 %d32-126
|
||||||
|
internal val Char.isQuotedChar
|
||||||
|
get() = code in 32..126
|
||||||
|
|
||||||
|
// RFC 5321:
|
||||||
|
// Dot-string = Atom *("." Atom)
|
||||||
|
// Atom = 1*atext
|
||||||
|
internal val String.isDotString: Boolean
|
||||||
|
get() {
|
||||||
|
if (isEmpty() || this[0] == DOT || this[lastIndex] == DOT) return false
|
||||||
|
for (i in 0..lastIndex) {
|
||||||
|
val character = this[i]
|
||||||
|
when {
|
||||||
|
character == DOT -> if (this[i - 1] == DOT) return false
|
||||||
|
character.isAtext -> Unit
|
||||||
|
else -> return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// RFC 5321: Let-dig = ALPHA / DIGIT
|
||||||
|
internal val Char.isLetDig
|
||||||
|
get() = isALPHA || isDIGIT
|
|
@ -0,0 +1,271 @@
|
||||||
|
package app.k9mail.core.common.mail
|
||||||
|
|
||||||
|
import app.k9mail.core.common.mail.EmailAddressParserError.AddressLiteralsNotSupported
|
||||||
|
import app.k9mail.core.common.mail.EmailAddressParserError.EmptyLocalPart
|
||||||
|
import app.k9mail.core.common.mail.EmailAddressParserError.ExpectedEndOfInput
|
||||||
|
import app.k9mail.core.common.mail.EmailAddressParserError.InvalidDomainPart
|
||||||
|
import app.k9mail.core.common.mail.EmailAddressParserError.InvalidDotString
|
||||||
|
import app.k9mail.core.common.mail.EmailAddressParserError.InvalidLocalPart
|
||||||
|
import app.k9mail.core.common.mail.EmailAddressParserError.InvalidQuotedString
|
||||||
|
import app.k9mail.core.common.mail.EmailAddressParserError.LocalPartLengthExceeded
|
||||||
|
import app.k9mail.core.common.mail.EmailAddressParserError.LocalPartRequiresQuotedString
|
||||||
|
import app.k9mail.core.common.mail.EmailAddressParserError.QuotedStringInLocalPart
|
||||||
|
import app.k9mail.core.common.mail.EmailAddressParserError.TotalLengthExceeded
|
||||||
|
import app.k9mail.core.common.mail.EmailAddressParserError.UnexpectedCharacter
|
||||||
|
import assertk.all
|
||||||
|
import assertk.assertFailure
|
||||||
|
import assertk.assertThat
|
||||||
|
import assertk.assertions.hasMessage
|
||||||
|
import assertk.assertions.isEqualTo
|
||||||
|
import assertk.assertions.isInstanceOf
|
||||||
|
import assertk.assertions.prop
|
||||||
|
import kotlin.test.Test
|
||||||
|
|
||||||
|
class EmailAddressParserTest {
|
||||||
|
@Test
|
||||||
|
fun `simple address`() {
|
||||||
|
val emailAddress = parseEmailAddress("alice@domain.example")
|
||||||
|
|
||||||
|
assertThat(emailAddress.localPart).isEqualTo("alice")
|
||||||
|
assertThat(emailAddress.domain).isEqualTo(EmailDomain("domain.example"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `local part containing dot`() {
|
||||||
|
val emailAddress = parseEmailAddress("alice.lastname@domain.example")
|
||||||
|
|
||||||
|
assertThat(emailAddress.localPart).isEqualTo("alice.lastname")
|
||||||
|
assertThat(emailAddress.domain).isEqualTo(EmailDomain("domain.example"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `quoted local part`() {
|
||||||
|
val emailAddress = parseEmailAddress("\"one two\"@domain.example", allowLocalPartRequiringQuotedString = true)
|
||||||
|
|
||||||
|
assertThat(emailAddress.localPart).isEqualTo("one two")
|
||||||
|
assertThat(emailAddress.domain).isEqualTo(EmailDomain("domain.example"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `quoted local part not allowed`() {
|
||||||
|
assertFailure {
|
||||||
|
parseEmailAddress(
|
||||||
|
address = "\"one two\"@domain.example",
|
||||||
|
allowQuotedLocalPart = true,
|
||||||
|
allowLocalPartRequiringQuotedString = false,
|
||||||
|
)
|
||||||
|
}.isInstanceOf<EmailAddressParserException>().all {
|
||||||
|
prop(EmailAddressParserException::error).isEqualTo(LocalPartRequiresQuotedString)
|
||||||
|
prop(EmailAddressParserException::position).isEqualTo(0)
|
||||||
|
hasMessage("Local part requiring the use of a quoted string is not allowed by config")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `unnecessarily quoted local part`() {
|
||||||
|
val emailAddress = parseEmailAddress(
|
||||||
|
address = "\"user\"@domain.example",
|
||||||
|
allowQuotedLocalPart = true,
|
||||||
|
allowLocalPartRequiringQuotedString = false,
|
||||||
|
)
|
||||||
|
|
||||||
|
assertThat(emailAddress.localPart).isEqualTo("user")
|
||||||
|
assertThat(emailAddress.domain).isEqualTo(EmailDomain("domain.example"))
|
||||||
|
assertThat(emailAddress.address).isEqualTo("user@domain.example")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `unnecessarily quoted local part not allowed`() {
|
||||||
|
assertFailure {
|
||||||
|
parseEmailAddress("\"user\"@domain.example", allowQuotedLocalPart = false)
|
||||||
|
}.isInstanceOf<EmailAddressParserException>().all {
|
||||||
|
prop(EmailAddressParserException::error).isEqualTo(QuotedStringInLocalPart)
|
||||||
|
prop(EmailAddressParserException::position).isEqualTo(0)
|
||||||
|
hasMessage("Quoted string in local part is not allowed by config")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `quoted local part containing double quote character`() {
|
||||||
|
val emailAddress = parseEmailAddress(""""a\"b"@domain.example""", allowLocalPartRequiringQuotedString = true)
|
||||||
|
|
||||||
|
assertThat(emailAddress.localPart).isEqualTo("a\"b")
|
||||||
|
assertThat(emailAddress.domain).isEqualTo(EmailDomain("domain.example"))
|
||||||
|
assertThat(emailAddress.address).isEqualTo(""""a\"b"@domain.example""")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `empty local part`() {
|
||||||
|
val emailAddress = parseEmailAddress("\"\"@domain.example", allowEmptyLocalPart = true)
|
||||||
|
|
||||||
|
assertThat(emailAddress.localPart).isEqualTo("")
|
||||||
|
assertThat(emailAddress.domain).isEqualTo(EmailDomain("domain.example"))
|
||||||
|
assertThat(emailAddress.address).isEqualTo("\"\"@domain.example")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `empty local part not allowed`() {
|
||||||
|
assertFailure {
|
||||||
|
parseEmailAddress(
|
||||||
|
address = "\"\"@domain.example",
|
||||||
|
allowLocalPartRequiringQuotedString = true,
|
||||||
|
allowEmptyLocalPart = false,
|
||||||
|
)
|
||||||
|
}.isInstanceOf<EmailAddressParserException>().all {
|
||||||
|
prop(EmailAddressParserException::error).isEqualTo(EmptyLocalPart)
|
||||||
|
prop(EmailAddressParserException::position).isEqualTo(1)
|
||||||
|
hasMessage("Empty local part is not allowed by config")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `IPv4 address literal`() {
|
||||||
|
assertFailure {
|
||||||
|
parseEmailAddress("user@[255.0.100.23]")
|
||||||
|
}.isInstanceOf<EmailAddressParserException>().all {
|
||||||
|
prop(EmailAddressParserException::error).isEqualTo(AddressLiteralsNotSupported)
|
||||||
|
prop(EmailAddressParserException::position).isEqualTo(5)
|
||||||
|
hasMessage("Address literals are not supported")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `IPv6 address literal`() {
|
||||||
|
assertFailure {
|
||||||
|
parseEmailAddress("user@[IPv6:2001:0db8:0000:0000:0000:ff00:0042:8329]")
|
||||||
|
}.isInstanceOf<EmailAddressParserException>().all {
|
||||||
|
prop(EmailAddressParserException::error).isEqualTo(AddressLiteralsNotSupported)
|
||||||
|
prop(EmailAddressParserException::position).isEqualTo(5)
|
||||||
|
hasMessage("Address literals are not supported")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `domain part starts with unsupported value`() {
|
||||||
|
assertFailure {
|
||||||
|
parseEmailAddress("user@ä")
|
||||||
|
}.isInstanceOf<EmailAddressParserException>().all {
|
||||||
|
prop(EmailAddressParserException::error).isEqualTo(InvalidDomainPart)
|
||||||
|
prop(EmailAddressParserException::position).isEqualTo(5)
|
||||||
|
hasMessage("Expected 'Domain' or 'address-literal'")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `obsolete syntax`() {
|
||||||
|
assertFailure {
|
||||||
|
parseEmailAddress("\"quoted\".atom@domain.example", allowLocalPartRequiringQuotedString = true)
|
||||||
|
}.isInstanceOf<EmailAddressParserException>().all {
|
||||||
|
prop(EmailAddressParserException::error).isEqualTo(UnexpectedCharacter)
|
||||||
|
prop(EmailAddressParserException::position).isEqualTo(8)
|
||||||
|
hasMessage("Expected '@' (64)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `local part starting with dot`() {
|
||||||
|
assertFailure {
|
||||||
|
parseEmailAddress(".invalid@domain.example")
|
||||||
|
}.isInstanceOf<EmailAddressParserException>().all {
|
||||||
|
prop(EmailAddressParserException::error).isEqualTo(InvalidLocalPart)
|
||||||
|
prop(EmailAddressParserException::position).isEqualTo(0)
|
||||||
|
hasMessage("Expected 'Dot-string' or 'Quoted-string'")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `local part ending with dot`() {
|
||||||
|
assertFailure {
|
||||||
|
parseEmailAddress("invalid.@domain.example")
|
||||||
|
}.isInstanceOf<EmailAddressParserException>().all {
|
||||||
|
prop(EmailAddressParserException::error).isEqualTo(InvalidDotString)
|
||||||
|
prop(EmailAddressParserException::position).isEqualTo(8)
|
||||||
|
hasMessage("Expected 'Dot-string'")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `quoted local part missing closing double quote`() {
|
||||||
|
assertFailure {
|
||||||
|
parseEmailAddress("\"invalid@domain.example", allowLocalPartRequiringQuotedString = true)
|
||||||
|
}.isInstanceOf<EmailAddressParserException>().all {
|
||||||
|
prop(EmailAddressParserException::error).isEqualTo(UnexpectedCharacter)
|
||||||
|
prop(EmailAddressParserException::position).isEqualTo(23)
|
||||||
|
hasMessage("Expected '\"' (34)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `quoted text containing unsupported character`() {
|
||||||
|
assertFailure {
|
||||||
|
parseEmailAddress("\"ä\"@domain.example", allowLocalPartRequiringQuotedString = true)
|
||||||
|
}.isInstanceOf<EmailAddressParserException>().all {
|
||||||
|
prop(EmailAddressParserException::error).isEqualTo(InvalidQuotedString)
|
||||||
|
prop(EmailAddressParserException::position).isEqualTo(1)
|
||||||
|
hasMessage("Expected 'Quoted-string'")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `quoted text containing unsupported escaped character`() {
|
||||||
|
assertFailure {
|
||||||
|
parseEmailAddress(""""\ä"@domain.example""", allowLocalPartRequiringQuotedString = true)
|
||||||
|
}.isInstanceOf<EmailAddressParserException>().all {
|
||||||
|
prop(EmailAddressParserException::error).isEqualTo(InvalidQuotedString)
|
||||||
|
prop(EmailAddressParserException::position).isEqualTo(3)
|
||||||
|
hasMessage("Expected 'Quoted-string'")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `local part exceeds maximum size`() {
|
||||||
|
assertFailure {
|
||||||
|
parseEmailAddress("1xxxxxxxxx2xxxxxxxxx3xxxxxxxxx4xxxxxxxxx5xxxxxxxxx6xxxxxxxxx12345@domain.example")
|
||||||
|
}.isInstanceOf<EmailAddressParserException>().all {
|
||||||
|
prop(EmailAddressParserException::error).isEqualTo(LocalPartLengthExceeded)
|
||||||
|
prop(EmailAddressParserException::position).isEqualTo(65)
|
||||||
|
hasMessage("Local part exceeds maximum length of 64 characters")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `email exceeds maximum size`() {
|
||||||
|
assertFailure {
|
||||||
|
parseEmailAddress(
|
||||||
|
"1xxxxxxxxx2xxxxxxxxx3xxxxxxxxx4xxxxxxxxx5xxxxxxxxx6xxxxxxxxx1234@" +
|
||||||
|
"1xxxxxxxxx2xxxxxxxxx3xxxxxxxxx4xxxxxxxxx5xxxxxxxxx6xxxxxxxxx123." +
|
||||||
|
"1xxxxxxxxx2xxxxxxxxx3xxxxxxxxx4xxxxxxxxx5xxxxxxxxx6xxxxxxxxx123." +
|
||||||
|
"1xxxxxxxxx2xxxxxxxxx3xxxxxxxxx4xxxxxxxxx5xxxxxxxxx6xxxxxxxxx12",
|
||||||
|
)
|
||||||
|
}.isInstanceOf<EmailAddressParserException>().all {
|
||||||
|
prop(EmailAddressParserException::error).isEqualTo(TotalLengthExceeded)
|
||||||
|
prop(EmailAddressParserException::position).isEqualTo(255)
|
||||||
|
hasMessage("The email address exceeds the maximum length of 254 characters")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `input contains additional character`() {
|
||||||
|
assertFailure {
|
||||||
|
parseEmailAddress("test@domain.example#")
|
||||||
|
}.isInstanceOf<EmailAddressParserException>().all {
|
||||||
|
prop(EmailAddressParserException::error).isEqualTo(ExpectedEndOfInput)
|
||||||
|
prop(EmailAddressParserException::position).isEqualTo(19)
|
||||||
|
hasMessage("Expected end of input")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseEmailAddress(
|
||||||
|
address: String,
|
||||||
|
allowEmptyLocalPart: Boolean = false,
|
||||||
|
allowLocalPartRequiringQuotedString: Boolean = allowEmptyLocalPart,
|
||||||
|
allowQuotedLocalPart: Boolean = allowLocalPartRequiringQuotedString,
|
||||||
|
): EmailAddress {
|
||||||
|
val config = EmailAddressParserConfig(
|
||||||
|
allowQuotedLocalPart,
|
||||||
|
allowLocalPartRequiringQuotedString,
|
||||||
|
allowEmptyLocalPart,
|
||||||
|
)
|
||||||
|
return EmailAddressParser(address, config).parse()
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,32 +1,60 @@
|
||||||
package app.k9mail.core.common.mail
|
package app.k9mail.core.common.mail
|
||||||
|
|
||||||
import assertk.assertFailure
|
import app.k9mail.core.common.mail.EmailAddress.Warning
|
||||||
import assertk.assertThat
|
import assertk.assertThat
|
||||||
import assertk.assertions.hasMessage
|
import assertk.assertions.containsExactlyInAnyOrder
|
||||||
|
import assertk.assertions.isEmpty
|
||||||
import assertk.assertions.isEqualTo
|
import assertk.assertions.isEqualTo
|
||||||
import assertk.assertions.isInstanceOf
|
import assertk.assertions.isSameAs
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
|
|
||||||
internal class EmailAddressTest {
|
class EmailAddressTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `should reject blank email address`() {
|
fun `simple email address`() {
|
||||||
assertFailure {
|
val domain = EmailDomain("DOMAIN.example")
|
||||||
EmailAddress("")
|
val emailAddress = EmailAddress(localPart = "user", domain = domain)
|
||||||
}.isInstanceOf<IllegalArgumentException>()
|
|
||||||
.hasMessage("Email address must not be blank")
|
assertThat(emailAddress.localPart).isEqualTo("user")
|
||||||
|
assertThat(emailAddress.encodedLocalPart).isEqualTo("user")
|
||||||
|
assertThat(emailAddress.domain).isSameAs(domain)
|
||||||
|
assertThat(emailAddress.address).isEqualTo("user@DOMAIN.example")
|
||||||
|
assertThat(emailAddress.normalizedAddress).isEqualTo("user@domain.example")
|
||||||
|
assertThat(emailAddress.toString()).isEqualTo("user@DOMAIN.example")
|
||||||
|
assertThat(emailAddress.warnings).isEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `should return email address`() {
|
fun `local part that requires use of quoted string`() {
|
||||||
val emailAddress = EmailAddress(EMAIL_ADDRESS)
|
val emailAddress = EmailAddress(localPart = "foo bar", domain = EmailDomain("domain.example"))
|
||||||
|
|
||||||
val address = emailAddress.address
|
assertThat(emailAddress.localPart).isEqualTo("foo bar")
|
||||||
|
assertThat(emailAddress.encodedLocalPart).isEqualTo("\"foo bar\"")
|
||||||
assertThat(address).isEqualTo(EMAIL_ADDRESS)
|
assertThat(emailAddress.address).isEqualTo("\"foo bar\"@domain.example")
|
||||||
|
assertThat(emailAddress.normalizedAddress).isEqualTo("\"foo bar\"@domain.example")
|
||||||
|
assertThat(emailAddress.toString()).isEqualTo("\"foo bar\"@domain.example")
|
||||||
|
assertThat(emailAddress.warnings).containsExactlyInAnyOrder(Warning.QuotedStringInLocalPart)
|
||||||
}
|
}
|
||||||
|
|
||||||
private companion object {
|
@Test
|
||||||
private const val EMAIL_ADDRESS = "email@example.com"
|
fun `empty local part`() {
|
||||||
|
val emailAddress = EmailAddress(localPart = "", domain = EmailDomain("domain.example"))
|
||||||
|
|
||||||
|
assertThat(emailAddress.localPart).isEqualTo("")
|
||||||
|
assertThat(emailAddress.encodedLocalPart).isEqualTo("\"\"")
|
||||||
|
assertThat(emailAddress.address).isEqualTo("\"\"@domain.example")
|
||||||
|
assertThat(emailAddress.normalizedAddress).isEqualTo("\"\"@domain.example")
|
||||||
|
assertThat(emailAddress.toString()).isEqualTo("\"\"@domain.example")
|
||||||
|
assertThat(emailAddress.warnings).containsExactlyInAnyOrder(
|
||||||
|
Warning.QuotedStringInLocalPart,
|
||||||
|
Warning.EmptyLocalPart,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `equals() does case-insensitive domain comparison`() {
|
||||||
|
val emailAddress1 = EmailAddress(localPart = "user", domain = EmailDomain("domain.example"))
|
||||||
|
val emailAddress2 = EmailAddress(localPart = "user", domain = EmailDomain("DOMAIN.example"))
|
||||||
|
|
||||||
|
assertThat(emailAddress2).isEqualTo(emailAddress1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,88 @@
|
||||||
|
package app.k9mail.core.common.mail
|
||||||
|
|
||||||
|
import app.k9mail.core.common.mail.EmailAddressParserError.DnsLabelLengthExceeded
|
||||||
|
import app.k9mail.core.common.mail.EmailAddressParserError.DomainLengthExceeded
|
||||||
|
import app.k9mail.core.common.mail.EmailAddressParserError.ExpectedEndOfInput
|
||||||
|
import app.k9mail.core.common.mail.EmailAddressParserError.UnexpectedCharacter
|
||||||
|
import assertk.all
|
||||||
|
import assertk.assertFailure
|
||||||
|
import assertk.assertThat
|
||||||
|
import assertk.assertions.hasMessage
|
||||||
|
import assertk.assertions.isEqualTo
|
||||||
|
import assertk.assertions.isInstanceOf
|
||||||
|
import assertk.assertions.prop
|
||||||
|
import kotlin.test.Test
|
||||||
|
|
||||||
|
class EmailDomainParserTest {
|
||||||
|
@Test
|
||||||
|
fun `simple domain`() {
|
||||||
|
val emailDomain = parseEmailDomain("DOMAIN.example")
|
||||||
|
|
||||||
|
assertThat(emailDomain.value).isEqualTo("DOMAIN.example")
|
||||||
|
assertThat(emailDomain.normalized).isEqualTo("domain.example")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `label starting with hyphen`() {
|
||||||
|
assertFailure {
|
||||||
|
parseEmailDomain("-domain.example")
|
||||||
|
}.isInstanceOf<EmailAddressParserException>().all {
|
||||||
|
prop(EmailAddressParserException::error).isEqualTo(UnexpectedCharacter)
|
||||||
|
prop(EmailAddressParserException::position).isEqualTo(0)
|
||||||
|
hasMessage("Expected 'Let-dig'")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `label ending with hyphen`() {
|
||||||
|
assertFailure {
|
||||||
|
parseEmailDomain("domain-.example")
|
||||||
|
}.isInstanceOf<EmailAddressParserException>().all {
|
||||||
|
prop(EmailAddressParserException::error).isEqualTo(UnexpectedCharacter)
|
||||||
|
prop(EmailAddressParserException::position).isEqualTo(7)
|
||||||
|
hasMessage("Expected 'Let-dig'")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `label exceeds maximum size`() {
|
||||||
|
assertFailure {
|
||||||
|
parseEmailDomain("1xxxxxxxxx2xxxxxxxxx3xxxxxxxxx4xxxxxxxxx5xxxxxxxxx6xxxxxxxxx1234.example")
|
||||||
|
}.isInstanceOf<EmailAddressParserException>().all {
|
||||||
|
prop(EmailAddressParserException::error).isEqualTo(DnsLabelLengthExceeded)
|
||||||
|
prop(EmailAddressParserException::position).isEqualTo(64)
|
||||||
|
hasMessage("DNS labels exceeds maximum length of 63 characters")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `domain exceeds maximum size`() {
|
||||||
|
assertFailure {
|
||||||
|
parseEmailDomain(
|
||||||
|
"1xxxxxxxxx2xxxxxxxxx3xxxxxxxxx4xxxxxxxxx5xxxxxxxxx6xxxxxxxxx123." +
|
||||||
|
"1xxxxxxxxx2xxxxxxxxx3xxxxxxxxx4xxxxxxxxx5xxxxxxxxx6xxxxxxxxx123." +
|
||||||
|
"1xxxxxxxxx2xxxxxxxxx3xxxxxxxxx4xxxxxxxxx5xxxxxxxxx6xxxxxxxxx123." +
|
||||||
|
"1xxxxxxxxx2xxxxxxxxx3xxxxxxxxx4xxxxxxxxx5xxxxxxxxx6xxxxxxxxx12",
|
||||||
|
)
|
||||||
|
}.isInstanceOf<EmailAddressParserException>().all {
|
||||||
|
prop(EmailAddressParserException::error).isEqualTo(DomainLengthExceeded)
|
||||||
|
prop(EmailAddressParserException::position).isEqualTo(254)
|
||||||
|
hasMessage("Domain exceeds maximum length of 253 characters")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `input contains additional character`() {
|
||||||
|
assertFailure {
|
||||||
|
parseEmailDomain("domain.example#")
|
||||||
|
}.isInstanceOf<EmailAddressParserException>().all {
|
||||||
|
prop(EmailAddressParserException::error).isEqualTo(ExpectedEndOfInput)
|
||||||
|
prop(EmailAddressParserException::position).isEqualTo(14)
|
||||||
|
hasMessage("Expected end of input")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseEmailDomain(domain: String): EmailDomain {
|
||||||
|
return EmailDomainParser(domain).parseDomain()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
package app.k9mail.core.common.mail
|
||||||
|
|
||||||
|
import assertk.assertThat
|
||||||
|
import assertk.assertions.isEqualTo
|
||||||
|
import kotlin.test.Test
|
||||||
|
|
||||||
|
class EmailDomainTest {
|
||||||
|
@Test
|
||||||
|
fun `simple domain`() {
|
||||||
|
val domain = EmailDomain("DOMAIN.example")
|
||||||
|
|
||||||
|
assertThat(domain.value).isEqualTo("DOMAIN.example")
|
||||||
|
assertThat(domain.normalized).isEqualTo("domain.example")
|
||||||
|
assertThat(domain.toString()).isEqualTo("DOMAIN.example")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `equals() does case-insensitive comparison`() {
|
||||||
|
val domain1 = EmailDomain("domain.example")
|
||||||
|
val domain2 = EmailDomain("DOMAIN.example")
|
||||||
|
|
||||||
|
assertThat(domain2).isEqualTo(domain1)
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,8 +4,8 @@ import app.k9mail.autodiscovery.api.AutoDiscovery
|
||||||
import app.k9mail.autodiscovery.api.AutoDiscoveryResult
|
import app.k9mail.autodiscovery.api.AutoDiscoveryResult
|
||||||
import app.k9mail.autodiscovery.api.AutoDiscoveryRunnable
|
import app.k9mail.autodiscovery.api.AutoDiscoveryRunnable
|
||||||
import app.k9mail.core.common.mail.EmailAddress
|
import app.k9mail.core.common.mail.EmailAddress
|
||||||
|
import app.k9mail.core.common.mail.toDomain
|
||||||
import app.k9mail.core.common.net.toDomain
|
import app.k9mail.core.common.net.toDomain
|
||||||
import com.fsck.k9.helper.EmailHelper
|
|
||||||
import com.fsck.k9.logging.Timber
|
import com.fsck.k9.logging.Timber
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import okhttp3.HttpUrl
|
import okhttp3.HttpUrl
|
||||||
|
@ -18,9 +18,7 @@ class AutoconfigDiscovery internal constructor(
|
||||||
) : AutoDiscovery {
|
) : AutoDiscovery {
|
||||||
|
|
||||||
override fun initDiscovery(email: EmailAddress): List<AutoDiscoveryRunnable> {
|
override fun initDiscovery(email: EmailAddress): List<AutoDiscoveryRunnable> {
|
||||||
val domain = requireNotNull(EmailHelper.getDomainFromEmailAddress(email.address)?.toDomain()) {
|
val domain = email.domain.toDomain()
|
||||||
"Couldn't extract domain from email address: $email"
|
|
||||||
}
|
|
||||||
|
|
||||||
val autoconfigUrls = urlProvider.getAutoconfigUrls(domain, email)
|
val autoconfigUrls = urlProvider.getAutoconfigUrls(domain, email)
|
||||||
|
|
||||||
|
|
|
@ -4,9 +4,9 @@ import app.k9mail.autodiscovery.api.AutoDiscovery
|
||||||
import app.k9mail.autodiscovery.api.AutoDiscoveryResult
|
import app.k9mail.autodiscovery.api.AutoDiscoveryResult
|
||||||
import app.k9mail.autodiscovery.api.AutoDiscoveryRunnable
|
import app.k9mail.autodiscovery.api.AutoDiscoveryRunnable
|
||||||
import app.k9mail.core.common.mail.EmailAddress
|
import app.k9mail.core.common.mail.EmailAddress
|
||||||
|
import app.k9mail.core.common.mail.toDomain
|
||||||
import app.k9mail.core.common.net.Domain
|
import app.k9mail.core.common.net.Domain
|
||||||
import app.k9mail.core.common.net.toDomain
|
import app.k9mail.core.common.net.toDomain
|
||||||
import com.fsck.k9.helper.EmailHelper
|
|
||||||
import com.fsck.k9.logging.Timber
|
import com.fsck.k9.logging.Timber
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import okhttp3.HttpUrl
|
import okhttp3.HttpUrl
|
||||||
|
@ -31,9 +31,7 @@ class MxLookupAutoconfigDiscovery internal constructor(
|
||||||
|
|
||||||
@Suppress("ReturnCount")
|
@Suppress("ReturnCount")
|
||||||
private suspend fun mxLookupAutoconfig(email: EmailAddress): AutoDiscoveryResult? {
|
private suspend fun mxLookupAutoconfig(email: EmailAddress): AutoDiscoveryResult? {
|
||||||
val domain = requireNotNull(EmailHelper.getDomainFromEmailAddress(email.address)?.toDomain()) {
|
val domain = email.domain.toDomain()
|
||||||
"Couldn't extract domain from email address: ${email.address}"
|
|
||||||
}
|
|
||||||
|
|
||||||
val mxHostName = mxLookup(domain) ?: return null
|
val mxHostName = mxLookup(domain) ?: return null
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,6 @@ import app.k9mail.core.common.net.Hostname
|
||||||
import app.k9mail.core.common.net.Port
|
import app.k9mail.core.common.net.Port
|
||||||
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 com.fsck.k9.helper.EmailHelper
|
|
||||||
import com.fsck.k9.logging.Timber
|
import com.fsck.k9.logging.Timber
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.io.InputStreamReader
|
import java.io.InputStreamReader
|
||||||
|
@ -37,7 +36,7 @@ private typealias ServerSettingsFactory<T> = (
|
||||||
internal class RealAutoconfigParser : AutoconfigParser {
|
internal class RealAutoconfigParser : AutoconfigParser {
|
||||||
override fun parseSettings(inputStream: InputStream, email: EmailAddress): AutoDiscoveryResult? {
|
override fun parseSettings(inputStream: InputStream, email: EmailAddress): AutoDiscoveryResult? {
|
||||||
return try {
|
return try {
|
||||||
ClientConfigParser(inputStream, email.address).parse()
|
ClientConfigParser(inputStream, email).parse()
|
||||||
} catch (e: XmlPullParserException) {
|
} catch (e: XmlPullParserException) {
|
||||||
throw AutoconfigParserException("Error parsing Autoconfig XML", e)
|
throw AutoconfigParserException("Error parsing Autoconfig XML", e)
|
||||||
}
|
}
|
||||||
|
@ -47,10 +46,10 @@ internal class RealAutoconfigParser : AutoconfigParser {
|
||||||
@Suppress("TooManyFunctions")
|
@Suppress("TooManyFunctions")
|
||||||
private class ClientConfigParser(
|
private class ClientConfigParser(
|
||||||
private val inputStream: InputStream,
|
private val inputStream: InputStream,
|
||||||
private val email: String,
|
private val email: EmailAddress,
|
||||||
) {
|
) {
|
||||||
private val localPart = requireNotNull(EmailHelper.getLocalPartFromEmailAddress(email)) { "Invalid email address" }
|
private val localPart = email.localPart
|
||||||
private val domain = requireNotNull(EmailHelper.getDomainFromEmailAddress(email)) { "Invalid email address" }
|
private val domain = email.domain.normalized
|
||||||
|
|
||||||
private val pullParser: XmlPullParser = XmlPullParserFactory.newInstance().newPullParser().apply {
|
private val pullParser: XmlPullParser = XmlPullParserFactory.newInstance().newPullParser().apply {
|
||||||
setInput(InputStreamReader(inputStream))
|
setInput(InputStreamReader(inputStream))
|
||||||
|
@ -288,7 +287,7 @@ private class ClientConfigParser(
|
||||||
private fun String.replaceVariables(): String {
|
private fun String.replaceVariables(): String {
|
||||||
return replace("%EMAILDOMAIN%", domain)
|
return replace("%EMAILDOMAIN%", domain)
|
||||||
.replace("%EMAILLOCALPART%", localPart)
|
.replace("%EMAILLOCALPART%", localPart)
|
||||||
.replace("%EMAILADDRESS%", email)
|
.replace("%EMAILADDRESS%", email.address)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createImapServerSettings(
|
private fun createImapServerSettings(
|
||||||
|
|
Loading…
Reference in a new issue