Loading cli/autodiscovery-cli/src/main/kotlin/app/k9mail/cli/autodiscovery/AutoDiscoveryCli.kt +1 −1 Original line number Diff line number Diff line Loading @@ -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()) } Loading feature/autodiscovery/autoconfig/src/main/kotlin/app/k9mail/autodiscovery/autoconfig/MxLookupAutoconfigDiscovery.kt +6 −3 Original line number Diff line number Diff line Loading @@ -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( Loading Loading @@ -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), Loading @@ -95,7 +98,7 @@ fun createMxLookupAutoconfigDiscovery(okHttpClient: OkHttpClient): MxLookupAutoc mxResolver = SuspendableMxResolver(MiniDnsMxResolver()), baseDomainExtractor = baseDomainExtractor, subDomainExtractor = RealSubDomainExtractor(baseDomainExtractor), urlProvider = IspDbAutoconfigUrlProvider(), urlProvider = createPostMxLookupAutoconfigUrlProvider(config), autoconfigFetcher = autoconfigFetcher, ) } feature/autodiscovery/autoconfig/src/main/kotlin/app/k9mail/autodiscovery/autoconfig/PostMxLookupAutoconfigUrlProvider.kt 0 → 100644 +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, ) } feature/autodiscovery/autoconfig/src/test/kotlin/app/k9mail/autodiscovery/autoconfig/MockAutoconfigFetcher.kt +3 −0 Original line number Diff line number Diff line Loading @@ -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) { Loading feature/autodiscovery/autoconfig/src/test/kotlin/app/k9mail/autodiscovery/autoconfig/MxLookupAutoconfigDiscoveryTest.kt +46 −23 Original line number Diff line number Diff line Loading @@ -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), Loading @@ -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) Loading @@ -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) } Loading @@ -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) } Loading @@ -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) } Loading @@ -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) Loading @@ -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 Loading
cli/autodiscovery-cli/src/main/kotlin/app/k9mail/cli/autodiscovery/AutoDiscoveryCli.kt +1 −1 Original line number Diff line number Diff line Loading @@ -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()) } Loading
feature/autodiscovery/autoconfig/src/main/kotlin/app/k9mail/autodiscovery/autoconfig/MxLookupAutoconfigDiscovery.kt +6 −3 Original line number Diff line number Diff line Loading @@ -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( Loading Loading @@ -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), Loading @@ -95,7 +98,7 @@ fun createMxLookupAutoconfigDiscovery(okHttpClient: OkHttpClient): MxLookupAutoc mxResolver = SuspendableMxResolver(MiniDnsMxResolver()), baseDomainExtractor = baseDomainExtractor, subDomainExtractor = RealSubDomainExtractor(baseDomainExtractor), urlProvider = IspDbAutoconfigUrlProvider(), urlProvider = createPostMxLookupAutoconfigUrlProvider(config), autoconfigFetcher = autoconfigFetcher, ) }
feature/autodiscovery/autoconfig/src/main/kotlin/app/k9mail/autodiscovery/autoconfig/PostMxLookupAutoconfigUrlProvider.kt 0 → 100644 +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, ) }
feature/autodiscovery/autoconfig/src/test/kotlin/app/k9mail/autodiscovery/autoconfig/MockAutoconfigFetcher.kt +3 −0 Original line number Diff line number Diff line Loading @@ -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) { Loading
feature/autodiscovery/autoconfig/src/test/kotlin/app/k9mail/autodiscovery/autoconfig/MxLookupAutoconfigDiscoveryTest.kt +46 −23 Original line number Diff line number Diff line Loading @@ -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), Loading @@ -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) Loading @@ -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) } Loading @@ -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) } Loading @@ -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) } Loading @@ -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) Loading @@ -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