Extract common functionality to AutoconfigFetcher

This commit is contained in:
cketti 2023-06-01 16:28:16 +02:00
parent 9ae4c89c4e
commit 08786a37a1
8 changed files with 100 additions and 144 deletions

View file

@ -1,21 +1,14 @@
package app.k9mail.autodiscovery.autoconfig
import app.k9mail.autodiscovery.api.AutoDiscovery
import app.k9mail.autodiscovery.api.AutoDiscoveryResult
import app.k9mail.autodiscovery.api.AutoDiscoveryRunnable
import app.k9mail.autodiscovery.autoconfig.HttpFetchResult.ErrorResponse
import app.k9mail.autodiscovery.autoconfig.HttpFetchResult.SuccessResponse
import app.k9mail.core.common.mail.EmailAddress
import app.k9mail.core.common.mail.toDomain
import com.fsck.k9.logging.Timber
import java.io.IOException
import okhttp3.HttpUrl
import okhttp3.OkHttpClient
class AutoconfigDiscovery internal constructor(
private val urlProvider: AutoconfigUrlProvider,
private val fetcher: HttpFetcher,
private val parser: SuspendableAutoconfigParser,
private val autoconfigFetcher: AutoconfigFetcher,
) : AutoDiscovery {
override fun initDiscovery(email: EmailAddress): List<AutoDiscoveryRunnable> {
@ -25,29 +18,10 @@ class AutoconfigDiscovery internal constructor(
return autoconfigUrls.map { autoconfigUrl ->
AutoDiscoveryRunnable {
getAutoconfig(email, autoconfigUrl)
autoconfigFetcher.fetchAutoconfig(autoconfigUrl, email)
}
}
}
private suspend fun getAutoconfig(email: EmailAddress, autoconfigUrl: HttpUrl): AutoDiscoveryResult? {
return try {
when (val fetchResult = fetcher.fetch(autoconfigUrl)) {
is SuccessResponse -> {
fetchResult.inputStream.use { inputStream ->
parser.parseSettings(inputStream, email)
}
}
is ErrorResponse -> null
}
} catch (e: AutoconfigParserException) {
Timber.d(e, "Failed to parse config from URL: %s", autoconfigUrl)
null
} catch (e: IOException) {
Timber.d(e, "Error fetching Autoconfig from URL: %s", autoconfigUrl)
null
}
}
}
fun createProviderAutoconfigDiscovery(
@ -67,7 +41,9 @@ private fun createAutoconfigDiscovery(
okHttpClient: OkHttpClient,
urlProvider: AutoconfigUrlProvider,
): AutoconfigDiscovery {
val fetcher = OkHttpFetcher(okHttpClient)
val parser = SuspendableAutoconfigParser(RealAutoconfigParser())
return AutoconfigDiscovery(urlProvider, fetcher, parser)
val autoconfigFetcher = RealAutoconfigFetcher(
fetcher = OkHttpFetcher(okHttpClient),
parser = SuspendableAutoconfigParser(RealAutoconfigParser()),
)
return AutoconfigDiscovery(urlProvider, autoconfigFetcher)
}

View file

@ -0,0 +1,12 @@
package app.k9mail.autodiscovery.autoconfig
import app.k9mail.autodiscovery.api.AutoDiscoveryResult
import app.k9mail.core.common.mail.EmailAddress
import okhttp3.HttpUrl
/**
* Fetches and parses Autoconfig settings.
*/
internal interface AutoconfigFetcher {
suspend fun fetchAutoconfig(autoconfigUrl: HttpUrl, email: EmailAddress): AutoDiscoveryResult?
}

View file

@ -3,14 +3,11 @@ package app.k9mail.autodiscovery.autoconfig
import app.k9mail.autodiscovery.api.AutoDiscovery
import app.k9mail.autodiscovery.api.AutoDiscoveryResult
import app.k9mail.autodiscovery.api.AutoDiscoveryRunnable
import app.k9mail.autodiscovery.autoconfig.HttpFetchResult.ErrorResponse
import app.k9mail.autodiscovery.autoconfig.HttpFetchResult.SuccessResponse
import app.k9mail.core.common.mail.EmailAddress
import app.k9mail.core.common.mail.toDomain
import app.k9mail.core.common.net.Domain
import com.fsck.k9.logging.Timber
import java.io.IOException
import okhttp3.HttpUrl
import okhttp3.OkHttpClient
class MxLookupAutoconfigDiscovery internal constructor(
@ -18,8 +15,7 @@ class MxLookupAutoconfigDiscovery internal constructor(
private val baseDomainExtractor: BaseDomainExtractor,
private val subDomainExtractor: SubDomainExtractor,
private val urlProvider: AutoconfigUrlProvider,
private val fetcher: HttpFetcher,
private val parser: SuspendableAutoconfigParser,
private val autoconfigFetcher: AutoconfigFetcher,
) : AutoDiscovery {
override fun initDiscovery(email: EmailAddress): List<AutoDiscoveryRunnable> {
@ -48,7 +44,7 @@ class MxLookupAutoconfigDiscovery internal constructor(
for (domainToCheck in listOfNotNull(mxSubDomain, mxBaseDomain)) {
for (autoconfigUrl in urlProvider.getAutoconfigUrls(domainToCheck)) {
val discoveryResult = getAutoconfig(email, autoconfigUrl)
val discoveryResult = autoconfigFetcher.fetchAutoconfig(autoconfigUrl, email)
if (discoveryResult != null) {
return discoveryResult
}
@ -75,35 +71,19 @@ class MxLookupAutoconfigDiscovery internal constructor(
private fun getNextSubDomain(domain: Domain): Domain? {
return subDomainExtractor.extractSubDomain(domain)
}
private suspend fun getAutoconfig(email: EmailAddress, autoconfigUrl: HttpUrl): AutoDiscoveryResult? {
return try {
when (val fetchResult = fetcher.fetch(autoconfigUrl)) {
is SuccessResponse -> {
fetchResult.inputStream.use { inputStream ->
parser.parseSettings(inputStream, email)
}
}
is ErrorResponse -> null
}
} catch (e: AutoconfigParserException) {
Timber.d(e, "Failed to parse config from URL: %s", autoconfigUrl)
null
} catch (e: IOException) {
Timber.d(e, "Error fetching Autoconfig from URL: %s", autoconfigUrl)
null
}
}
}
fun createMxLookupAutoconfigDiscovery(okHttpClient: OkHttpClient): MxLookupAutoconfigDiscovery {
val baseDomainExtractor = OkHttpBaseDomainExtractor()
val autoconfigFetcher = RealAutoconfigFetcher(
fetcher = OkHttpFetcher(okHttpClient),
parser = SuspendableAutoconfigParser(RealAutoconfigParser()),
)
return MxLookupAutoconfigDiscovery(
mxResolver = SuspendableMxResolver(MiniDnsMxResolver()),
baseDomainExtractor = baseDomainExtractor,
subDomainExtractor = RealSubDomainExtractor(baseDomainExtractor),
urlProvider = IspDbAutoconfigUrlProvider(),
fetcher = OkHttpFetcher(okHttpClient),
parser = SuspendableAutoconfigParser(RealAutoconfigParser()),
autoconfigFetcher = autoconfigFetcher,
)
}

View file

@ -0,0 +1,33 @@
package app.k9mail.autodiscovery.autoconfig
import app.k9mail.autodiscovery.api.AutoDiscoveryResult
import app.k9mail.autodiscovery.autoconfig.HttpFetchResult.ErrorResponse
import app.k9mail.autodiscovery.autoconfig.HttpFetchResult.SuccessResponse
import app.k9mail.core.common.mail.EmailAddress
import com.fsck.k9.logging.Timber
import java.io.IOException
import okhttp3.HttpUrl
internal class RealAutoconfigFetcher(
private val fetcher: HttpFetcher,
private val parser: SuspendableAutoconfigParser,
) : AutoconfigFetcher {
override suspend fun fetchAutoconfig(autoconfigUrl: HttpUrl, email: EmailAddress): AutoDiscoveryResult? {
return try {
when (val fetchResult = fetcher.fetch(autoconfigUrl)) {
is SuccessResponse -> {
fetchResult.inputStream.use { inputStream ->
parser.parseSettings(inputStream, email)
}
}
is ErrorResponse -> null
}
} catch (e: AutoconfigParserException) {
Timber.d(e, "Failed to parse config from URL: %s", autoconfigUrl)
null
} catch (e: IOException) {
Timber.d(e, "Error fetching Autoconfig from URL: %s", autoconfigUrl)
null
}
}
}

View file

@ -1,5 +1,7 @@
package app.k9mail.autodiscovery.autoconfig
import app.k9mail.autodiscovery.autoconfig.MockAutoconfigFetcher.Companion.RESULT_ONE
import app.k9mail.autodiscovery.autoconfig.MockAutoconfigFetcher.Companion.RESULT_TWO
import app.k9mail.core.common.mail.toEmailAddress
import app.k9mail.core.common.net.toDomain
import assertk.assertThat
@ -15,47 +17,37 @@ private val IRRELEVANT_EMAIL_ADDRESS = "irrelevant@domain.example".toEmailAddres
class AutoconfigDiscoveryTest {
private val urlProvider = MockAutoconfigUrlProvider()
private val fetcher = MockHttpFetcher()
private val parser = MockAutoconfigParser()
private val discovery = AutoconfigDiscovery(urlProvider, fetcher, SuspendableAutoconfigParser(parser))
private val autoconfigFetcher = MockAutoconfigFetcher()
private val discovery = AutoconfigDiscovery(urlProvider, autoconfigFetcher)
@Test
fun `AutoconfigFetcher and AutoconfigParser should only be called when AutoDiscoveryRunnable is run`() = runTest {
val emailAddress = "user@domain.example".toEmailAddress()
val autoconfigUrl = "https://autoconfig.domain.invalid/mail/config-v1.1.xml".toHttpUrl()
urlProvider.addResult(listOf(autoconfigUrl))
fetcher.addSuccessResult("data")
parser.addResult(MockAutoconfigParser.RESULT_ONE)
autoconfigFetcher.addResult(RESULT_ONE)
val autoDiscoveryRunnables = discovery.initDiscovery(emailAddress)
assertThat(autoDiscoveryRunnables).hasSize(1)
assertThat(urlProvider.callArguments).containsExactly("domain.example".toDomain() to emailAddress)
assertThat(fetcher.callCount).isEqualTo(0)
assertThat(parser.callCount).isEqualTo(0)
assertThat(autoconfigFetcher.callCount).isEqualTo(0)
val discoveryResult = autoDiscoveryRunnables.first().run()
assertThat(fetcher.callArguments).containsExactly(autoconfigUrl)
assertThat(parser.callArguments).containsExactly("data" to emailAddress)
assertThat(discoveryResult).isEqualTo(MockAutoconfigParser.RESULT_ONE)
assertThat(autoconfigFetcher.callArguments).containsExactly(autoconfigUrl to emailAddress)
assertThat(discoveryResult).isEqualTo(RESULT_ONE)
}
@Test
fun `Two Autoconfig URLs should return two AutoDiscoveryRunnables`() = runTest {
urlProvider.addResult(
listOf(
"https://autoconfig.domain1.invalid/mail/config-v1.1.xml".toHttpUrl(),
"https://autoconfig.domain2.invalid/mail/config-v1.1.xml".toHttpUrl(),
),
)
fetcher.apply {
addSuccessResult("data1")
addSuccessResult("data2")
}
parser.apply {
addResult(MockAutoconfigParser.RESULT_ONE)
addResult(MockAutoconfigParser.RESULT_TWO)
val urlOne = "https://autoconfig.domain1.invalid/mail/config-v1.1.xml".toHttpUrl()
val urlTwo = "https://autoconfig.domain2.invalid/mail/config-v1.1.xml".toHttpUrl()
urlProvider.addResult(listOf(urlOne, urlTwo))
autoconfigFetcher.apply {
addResult(RESULT_ONE)
addResult(RESULT_TWO)
}
val autoDiscoveryRunnables = discovery.initDiscovery(IRRELEVANT_EMAIL_ADDRESS)
@ -64,14 +56,14 @@ class AutoconfigDiscoveryTest {
val discoveryResultOne = autoDiscoveryRunnables[0].run()
assertThat(parser.callArguments).extracting { it.first }.containsExactly("data1")
assertThat(discoveryResultOne).isEqualTo(MockAutoconfigParser.RESULT_ONE)
assertThat(autoconfigFetcher.callArguments).extracting { it.first }.containsExactly(urlOne)
assertThat(discoveryResultOne).isEqualTo(RESULT_ONE)
parser.callArguments.clear()
autoconfigFetcher.callArguments.clear()
val discoveryResultTwo = autoDiscoveryRunnables[1].run()
assertThat(parser.callArguments).extracting { it.first }.containsExactly("data2")
assertThat(discoveryResultTwo).isEqualTo(MockAutoconfigParser.RESULT_TWO)
assertThat(autoconfigFetcher.callArguments).extracting { it.first }.containsExactly(urlTwo)
assertThat(discoveryResultTwo).isEqualTo(RESULT_TWO)
}
}

View file

@ -10,10 +10,10 @@ import app.k9mail.autodiscovery.api.SmtpServerSettings
import app.k9mail.core.common.mail.EmailAddress
import app.k9mail.core.common.net.toHostname
import app.k9mail.core.common.net.toPort
import java.io.InputStream
import okhttp3.HttpUrl
internal class MockAutoconfigParser : AutoconfigParser {
val callArguments = mutableListOf<Pair<String, EmailAddress>>()
internal class MockAutoconfigFetcher : AutoconfigFetcher {
val callArguments = mutableListOf<Pair<HttpUrl, EmailAddress>>()
val callCount: Int
get() = callArguments.size
@ -24,11 +24,12 @@ internal class MockAutoconfigParser : AutoconfigParser {
results.add(discoveryResult)
}
override fun parseSettings(inputStream: InputStream, email: EmailAddress): AutoDiscoveryResult? {
val data = String(inputStream.readBytes())
callArguments.add(data to email)
override suspend fun fetchAutoconfig(autoconfigUrl: HttpUrl, email: EmailAddress): AutoDiscoveryResult? {
callArguments.add(autoconfigUrl to email)
check(results.isNotEmpty()) { "parseSettings($data, $email) called but no result provided" }
check(results.isNotEmpty()) {
"MockAutoconfigFetcher.fetchAutoconfig($autoconfigUrl) called but no result provided"
}
return results.removeAt(0)
}

View file

@ -1,31 +0,0 @@
package app.k9mail.autodiscovery.autoconfig
import okhttp3.HttpUrl
internal class MockHttpFetcher : HttpFetcher {
val callArguments = mutableListOf<HttpUrl>()
val callCount: Int
get() = callArguments.size
private val results = mutableListOf<HttpFetchResult>()
fun addSuccessResult(data: String) {
val result = HttpFetchResult.SuccessResponse(
inputStream = data.byteInputStream(),
)
results.add(result)
}
fun addErrorResult(code: Int) {
results.add(HttpFetchResult.ErrorResponse(code))
}
override suspend fun fetch(url: HttpUrl): HttpFetchResult {
callArguments.add(url)
check(results.isNotEmpty()) { "MockHttpFetcher.fetch($url) called but no result provided" }
return results.removeAt(0)
}
}

View file

@ -1,5 +1,6 @@
package app.k9mail.autodiscovery.autoconfig
import app.k9mail.autodiscovery.autoconfig.MockAutoconfigFetcher.Companion.RESULT_ONE
import app.k9mail.core.common.mail.toEmailAddress
import app.k9mail.core.common.net.toDomain
import assertk.assertThat
@ -16,15 +17,13 @@ class MxLookupAutoconfigDiscoveryTest {
private val mxResolver = MockMxResolver()
private val baseDomainExtractor = OkHttpBaseDomainExtractor()
private val urlProvider = MockAutoconfigUrlProvider()
private val fetcher = MockHttpFetcher()
private val parser = MockAutoconfigParser()
private val autoconfigFetcher = MockAutoconfigFetcher()
private val discovery = MxLookupAutoconfigDiscovery(
mxResolver = SuspendableMxResolver(mxResolver),
baseDomainExtractor = baseDomainExtractor,
subDomainExtractor = RealSubDomainExtractor(baseDomainExtractor),
urlProvider = urlProvider,
fetcher = fetcher,
parser = SuspendableAutoconfigParser(parser),
autoconfigFetcher = autoconfigFetcher,
)
@Test
@ -32,24 +31,21 @@ class MxLookupAutoconfigDiscoveryTest {
val emailAddress = "user@company.example".toEmailAddress()
mxResolver.addResult("mx.emailprovider.example".toDomain())
urlProvider.addResult(listOf("https://ispdb.invalid/emailprovider.example".toHttpUrl()))
fetcher.addSuccessResult("data")
parser.addResult(MockAutoconfigParser.RESULT_ONE)
autoconfigFetcher.addResult(RESULT_ONE)
val autoDiscoveryRunnables = discovery.initDiscovery(emailAddress)
assertThat(autoDiscoveryRunnables).hasSize(1)
assertThat(mxResolver.callCount).isEqualTo(0)
assertThat(fetcher.callCount).isEqualTo(0)
assertThat(parser.callCount).isEqualTo(0)
assertThat(autoconfigFetcher.callCount).isEqualTo(0)
val discoveryResult = autoDiscoveryRunnables.first().run()
assertThat(mxResolver.callArguments).containsExactly("company.example".toDomain())
assertThat(urlProvider.callArguments).extracting { it.first }
.containsExactly("emailprovider.example".toDomain())
assertThat(fetcher.callCount).isEqualTo(1)
assertThat(parser.callCount).isEqualTo(1)
assertThat(discoveryResult).isEqualTo(MockAutoconfigParser.RESULT_ONE)
assertThat(autoconfigFetcher.callCount).isEqualTo(1)
assertThat(discoveryResult).isEqualTo(RESULT_ONE)
}
@Test
@ -60,9 +56,9 @@ class MxLookupAutoconfigDiscoveryTest {
addResult(listOf("https://ispdb.invalid/something.emailprovider.example".toHttpUrl()))
addResult(listOf("https://ispdb.invalid/emailprovider.example".toHttpUrl()))
}
fetcher.apply {
addErrorResult(404)
addErrorResult(404)
autoconfigFetcher.apply {
addResult(null)
addResult(null)
}
val autoDiscoveryRunnables = discovery.initDiscovery(emailAddress)
@ -72,8 +68,7 @@ class MxLookupAutoconfigDiscoveryTest {
"something.emailprovider.example".toDomain(),
"emailprovider.example".toDomain(),
)
assertThat(fetcher.callCount).isEqualTo(2)
assertThat(parser.callCount).isEqualTo(0)
assertThat(autoconfigFetcher.callCount).isEqualTo(2)
assertThat(discoveryResult).isNull()
}
@ -87,8 +82,7 @@ class MxLookupAutoconfigDiscoveryTest {
assertThat(mxResolver.callCount).isEqualTo(1)
assertThat(urlProvider.callCount).isEqualTo(0)
assertThat(fetcher.callCount).isEqualTo(0)
assertThat(parser.callCount).isEqualTo(0)
assertThat(autoconfigFetcher.callCount).isEqualTo(0)
assertThat(discoveryResult).isNull()
}
@ -102,8 +96,7 @@ class MxLookupAutoconfigDiscoveryTest {
assertThat(mxResolver.callCount).isEqualTo(1)
assertThat(urlProvider.callCount).isEqualTo(0)
assertThat(fetcher.callCount).isEqualTo(0)
assertThat(parser.callCount).isEqualTo(0)
assertThat(autoconfigFetcher.callCount).isEqualTo(0)
assertThat(discoveryResult).isNull()
}
}