Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Unverified Commit a3a343f3 authored by cketti's avatar cketti Committed by GitHub
Browse files

Merge pull request #8568 from cketti/autoconfig_provider_lookup_after_mx

Try autoconfig server at email provider after MX lookup
parents 22c70ebd 625ed5c4
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -54,7 +54,7 @@ class AutoDiscoveryCli : CliktCommand(
        try {
            val providerDiscovery = createProviderAutoconfigDiscovery(okHttpClient, config)
            val ispDbDiscovery = createIspDbAutoconfigDiscovery(okHttpClient)
            val mxDiscovery = createMxLookupAutoconfigDiscovery(okHttpClient)
            val mxDiscovery = createMxLookupAutoconfigDiscovery(okHttpClient, config)

            val runnables = listOf(providerDiscovery, ispDbDiscovery, mxDiscovery)
                .flatMap { it.initDiscovery(emailAddress.toUserEmailAddress()) }
+6 −3
Original line number Diff line number Diff line
@@ -48,7 +48,7 @@ class MxLookupAutoconfigDiscovery internal constructor(

        var latestResult: AutoDiscoveryResult = NoUsableSettingsFound
        for (domainToCheck in listOfNotNull(mxSubDomain, mxBaseDomain)) {
            for (autoconfigUrl in urlProvider.getAutoconfigUrls(domainToCheck)) {
            for (autoconfigUrl in urlProvider.getAutoconfigUrls(domainToCheck, email)) {
                val discoveryResult = autoconfigFetcher.fetchAutoconfig(autoconfigUrl, email)
                if (discoveryResult is Settings) {
                    return discoveryResult.copy(
@@ -85,7 +85,10 @@ class MxLookupAutoconfigDiscovery internal constructor(
    }
}

fun createMxLookupAutoconfigDiscovery(okHttpClient: OkHttpClient): MxLookupAutoconfigDiscovery {
fun createMxLookupAutoconfigDiscovery(
    okHttpClient: OkHttpClient,
    config: AutoconfigUrlConfig,
): MxLookupAutoconfigDiscovery {
    val baseDomainExtractor = OkHttpBaseDomainExtractor()
    val autoconfigFetcher = RealAutoconfigFetcher(
        fetcher = OkHttpFetcher(okHttpClient),
@@ -95,7 +98,7 @@ fun createMxLookupAutoconfigDiscovery(okHttpClient: OkHttpClient): MxLookupAutoc
        mxResolver = SuspendableMxResolver(MiniDnsMxResolver()),
        baseDomainExtractor = baseDomainExtractor,
        subDomainExtractor = RealSubDomainExtractor(baseDomainExtractor),
        urlProvider = IspDbAutoconfigUrlProvider(),
        urlProvider = createPostMxLookupAutoconfigUrlProvider(config),
        autoconfigFetcher = autoconfigFetcher,
    )
}
+39 −0
Original line number Diff line number Diff line
package app.k9mail.autodiscovery.autoconfig

import app.k9mail.core.common.mail.EmailAddress
import app.k9mail.core.common.net.Domain
import okhttp3.HttpUrl

internal class PostMxLookupAutoconfigUrlProvider(
    private val ispDbUrlProvider: AutoconfigUrlProvider,
    private val config: AutoconfigUrlConfig,
) : AutoconfigUrlProvider {
    override fun getAutoconfigUrls(domain: Domain, email: EmailAddress?): List<HttpUrl> {
        return buildList {
            add(createProviderUrl(domain, email))
            addAll(ispDbUrlProvider.getAutoconfigUrls(domain, email))
        }
    }

    private fun createProviderUrl(domain: Domain, email: EmailAddress?): HttpUrl {
        // After an MX lookup only the following provider URL is checked:
        // https://autoconfig.{domain}/mail/config-v1.1.xml?emailaddress={email}
        return HttpUrl.Builder()
            .scheme("https")
            .host("autoconfig.${domain.value}")
            .addEncodedPathSegments("mail/config-v1.1.xml")
            .apply {
                if (email != null && config.includeEmailAddress) {
                    addQueryParameter("emailaddress", email.address)
                }
            }
            .build()
    }
}

internal fun createPostMxLookupAutoconfigUrlProvider(config: AutoconfigUrlConfig): AutoconfigUrlProvider {
    return PostMxLookupAutoconfigUrlProvider(
        ispDbUrlProvider = IspDbAutoconfigUrlProvider(),
        config = config,
    )
}
+3 −0
Original line number Diff line number Diff line
@@ -18,6 +18,9 @@ internal class MockAutoconfigFetcher : AutoconfigFetcher {
    val callCount: Int
        get() = callArguments.size

    val urls: List<String>
        get() = callArguments.map { (url, _) -> url.toString() }

    private val results = mutableListOf<AutoDiscoveryResult>()

    fun addResult(discoveryResult: AutoDiscoveryResult) {
+46 −23
Original line number Diff line number Diff line
@@ -6,17 +6,20 @@ import app.k9mail.core.common.mail.toUserEmailAddress
import app.k9mail.core.common.net.toDomain
import assertk.assertThat
import assertk.assertions.containsExactly
import assertk.assertions.extracting
import assertk.assertions.hasSize
import assertk.assertions.isEqualTo
import kotlin.test.Test
import kotlinx.coroutines.test.runTest
import okhttp3.HttpUrl.Companion.toHttpUrl

class MxLookupAutoconfigDiscoveryTest {
    private val mxResolver = MockMxResolver()
    private val baseDomainExtractor = OkHttpBaseDomainExtractor()
    private val urlProvider = MockAutoconfigUrlProvider()
    private val urlProvider = createPostMxLookupAutoconfigUrlProvider(
        AutoconfigUrlConfig(
            httpsOnly = true,
            includeEmailAddress = true,
        ),
    )
    private val autoconfigFetcher = MockAutoconfigFetcher()
    private val discovery = MxLookupAutoconfigDiscovery(
        mxResolver = SuspendableMxResolver(mxResolver),
@@ -27,11 +30,12 @@ class MxLookupAutoconfigDiscoveryTest {
    )

    @Test
    fun `AutoconfigUrlProvider should be called with MX base domain`() = runTest {
    fun `result from email provider should be used if available`() = runTest {
        val emailAddress = "user@company.example".toUserEmailAddress()
        mxResolver.addResult("mx.emailprovider.example".toDomain())
        urlProvider.addResult(listOf("https://ispdb.invalid/emailprovider.example".toHttpUrl()))
        autoconfigFetcher.addResult(RESULT_ONE)
        autoconfigFetcher.apply {
            addResult(RESULT_ONE)
        }

        val autoDiscoveryRunnables = discovery.initDiscovery(emailAddress)

@@ -41,34 +45,57 @@ class MxLookupAutoconfigDiscoveryTest {

        val discoveryResult = autoDiscoveryRunnables.first().run()

        assertThat(mxResolver.callArguments).containsExactly("company.example".toDomain())
        assertThat(urlProvider.callArguments).extracting { it.first }
            .containsExactly("emailprovider.example".toDomain())
        assertThat(autoconfigFetcher.callCount).isEqualTo(1)
        assertThat(autoconfigFetcher.urls).containsExactly(
            "https://autoconfig.emailprovider.example/mail/config-v1.1.xml?emailaddress=user%40company.example",
        )
        assertThat(discoveryResult).isEqualTo(RESULT_ONE)
    }

    @Test
    fun `AutoconfigUrlProvider should be called with MX base domain and subdomain`() = runTest {
    fun `result from ISPDB should be used if config is not available at email provider`() = runTest {
        val emailAddress = "user@company.example".toUserEmailAddress()
        mxResolver.addResult("mx.something.emailprovider.example".toDomain())
        urlProvider.apply {
            addResult(listOf("https://ispdb.invalid/something.emailprovider.example".toHttpUrl()))
            addResult(listOf("https://ispdb.invalid/emailprovider.example".toHttpUrl()))
        mxResolver.addResult("mx.emailprovider.example".toDomain())
        autoconfigFetcher.apply {
            addResult(NoUsableSettingsFound)
            addResult(RESULT_ONE)
        }

        val autoDiscoveryRunnables = discovery.initDiscovery(emailAddress)

        assertThat(autoDiscoveryRunnables).hasSize(1)
        assertThat(mxResolver.callCount).isEqualTo(0)
        assertThat(autoconfigFetcher.callCount).isEqualTo(0)

        val discoveryResult = autoDiscoveryRunnables.first().run()

        assertThat(autoconfigFetcher.urls).containsExactly(
            "https://autoconfig.emailprovider.example/mail/config-v1.1.xml?emailaddress=user%40company.example",
            "https://autoconfig.thunderbird.net/v1.1/emailprovider.example",
        )
        assertThat(discoveryResult).isEqualTo(RESULT_ONE)
    }

    @Test
    fun `base domain and subdomain should be extracted from MX host if possible`() = runTest {
        val emailAddress = "user@company.example".toUserEmailAddress()
        mxResolver.addResult("mx.something.emailprovider.example".toDomain())
        autoconfigFetcher.apply {
            addResult(NoUsableSettingsFound)
            addResult(NoUsableSettingsFound)
            addResult(NoUsableSettingsFound)
            addResult(NoUsableSettingsFound)
        }

        val autoDiscoveryRunnables = discovery.initDiscovery(emailAddress)
        val discoveryResult = autoDiscoveryRunnables.first().run()

        assertThat(urlProvider.callArguments).extracting { it.first }.containsExactly(
            "something.emailprovider.example".toDomain(),
            "emailprovider.example".toDomain(),
        assertThat(autoconfigFetcher.urls).containsExactly(
            "https://autoconfig.something.emailprovider.example/mail/config-v1.1.xml" +
                "?emailaddress=user%40company.example",
            "https://autoconfig.thunderbird.net/v1.1/something.emailprovider.example",
            "https://autoconfig.emailprovider.example/mail/config-v1.1.xml?emailaddress=user%40company.example",
            "https://autoconfig.thunderbird.net/v1.1/emailprovider.example",
        )
        assertThat(autoconfigFetcher.callCount).isEqualTo(2)
        assertThat(discoveryResult).isEqualTo(NoUsableSettingsFound)
    }

@@ -81,7 +108,6 @@ class MxLookupAutoconfigDiscoveryTest {
        val discoveryResult = autoDiscoveryRunnables.first().run()

        assertThat(mxResolver.callCount).isEqualTo(1)
        assertThat(urlProvider.callCount).isEqualTo(0)
        assertThat(autoconfigFetcher.callCount).isEqualTo(0)
        assertThat(discoveryResult).isEqualTo(NoUsableSettingsFound)
    }
@@ -95,7 +121,6 @@ class MxLookupAutoconfigDiscoveryTest {
        val discoveryResult = autoDiscoveryRunnables.first().run()

        assertThat(mxResolver.callCount).isEqualTo(1)
        assertThat(urlProvider.callCount).isEqualTo(0)
        assertThat(autoconfigFetcher.callCount).isEqualTo(0)
        assertThat(discoveryResult).isEqualTo(NoUsableSettingsFound)
    }
@@ -104,7 +129,6 @@ class MxLookupAutoconfigDiscoveryTest {
    fun `isTrusted should be false when MxLookupResult_isTrusted is false`() = runTest {
        val emailAddress = "user@company.example".toUserEmailAddress()
        mxResolver.addResult("mx.emailprovider.example".toDomain(), isTrusted = false)
        urlProvider.addResult(listOf("https://ispdb.invalid/emailprovider.example".toHttpUrl()))
        autoconfigFetcher.addResult(RESULT_ONE.copy(isTrusted = true))

        val autoDiscoveryRunnables = discovery.initDiscovery(emailAddress)
@@ -117,7 +141,6 @@ class MxLookupAutoconfigDiscoveryTest {
    fun `isTrusted should be false when AutoDiscoveryResult_isTrusted from AutoconfigFetcher is false`() = runTest {
        val emailAddress = "user@company.example".toUserEmailAddress()
        mxResolver.addResult("mx.emailprovider.example".toDomain(), isTrusted = true)
        urlProvider.addResult(listOf("https://ispdb.invalid/emailprovider.example".toHttpUrl()))
        autoconfigFetcher.addResult(RESULT_ONE.copy(isTrusted = false))

        val autoDiscoveryRunnables = discovery.initDiscovery(emailAddress)
Loading