Merge pull request #6926 from thundernest/autodiscovery_cli

Add auto-discovery command line interface
This commit is contained in:
cketti 2023-05-26 16:20:40 +02:00 committed by GitHub
commit f53d73630e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 206 additions and 17 deletions

3
autodiscovery Executable file
View file

@ -0,0 +1,3 @@
#!/bin/sh
./gradlew --quiet ":cli:autodiscovery-cli:installDist" < /dev/null && ./cli/autodiscovery-cli/build/install/autodiscovery-cli/bin/autodiscovery-cli "$@"

View file

@ -1,3 +1,5 @@
import org.gradle.jvm.tasks.Jar
plugins {
`java-library`
id("org.jetbrains.kotlin.jvm")
@ -8,6 +10,13 @@ java {
targetCompatibility = ThunderbirdProjectConfig.javaCompatibilityVersion
}
tasks.withType<Jar> {
// We want to avoid ending up with multiple JARs having the same name, e.g. "common.jar".
// To do this, we use the modified project path as base name, e.g. ":core:common" -> "core.common".
val projectDotPath = project.path.split(":").filter { it.isNotEmpty() }.joinToString(separator = ".")
archiveBaseName.set(projectDotPath)
}
configureKotlinJavaCompatibility()
dependencies {

View file

@ -0,0 +1,18 @@
plugins {
id(ThunderbirdPlugins.App.jvm)
}
version = "unspecified"
application {
mainClass.set("app.k9mail.cli.autodiscovery.MainKt")
}
dependencies {
implementation(projects.feature.autodiscovery.api)
implementation(projects.feature.autodiscovery.autoconfig)
implementation(libs.kotlinx.coroutines.core)
implementation(libs.clikt)
implementation(libs.kxml2)
}

View file

@ -0,0 +1,69 @@
package app.k9mail.cli.autodiscovery
import app.k9mail.autodiscovery.api.AutoDiscoveryResult
import app.k9mail.autodiscovery.autoconfig.AutoconfigUrlConfig
import app.k9mail.autodiscovery.autoconfig.createIspDbAutoconfigDiscovery
import app.k9mail.autodiscovery.autoconfig.createMxLookupAutoconfigDiscovery
import app.k9mail.autodiscovery.autoconfig.createProviderAutoconfigDiscovery
import app.k9mail.core.common.mail.toEmailAddress
import com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.parameters.arguments.argument
import com.github.ajalt.clikt.parameters.options.flag
import com.github.ajalt.clikt.parameters.options.option
import kotlin.time.DurationUnit.MILLISECONDS
import kotlin.time.toDuration
import kotlinx.coroutines.runBlocking
import okhttp3.OkHttpClient.Builder
import org.koin.core.time.measureDurationForResult
class AutoDiscoveryCli : CliktCommand(
help = "Performs the auto-discovery steps used by Thunderbird for Android to find mail server settings",
) {
private val httpsOnly by option(help = "Only perform Autoconfig lookups using HTTPS").flag()
private val includeEmailAddress by option(help = "Include email address in Autoconfig lookups").flag()
private val emailAddress by argument(name = "email", help = "Email address")
override fun run() {
echo("Attempting to find mail server settings for <$emailAddress>…")
echo()
val config = AutoconfigUrlConfig(
httpsOnly = httpsOnly,
includeEmailAddress = includeEmailAddress,
)
val (discoveryResult, durationInMillis) = measureDurationForResult {
runAutoDiscovery(config)
}
if (discoveryResult == null) {
echo("Couldn't find any mail server settings.")
} else {
echo("Found the following mail server settings:")
AutoDiscoveryResultFormatter(::echo).output(discoveryResult)
}
echo()
echo("Duration: ${durationInMillis.toDuration(MILLISECONDS)}")
}
private fun runAutoDiscovery(config: AutoconfigUrlConfig): AutoDiscoveryResult? {
val okHttpClient = Builder().build()
try {
val providerDiscovery = createProviderAutoconfigDiscovery(okHttpClient, config)
val ispDbDiscovery = createIspDbAutoconfigDiscovery(okHttpClient)
val mxDiscovery = createMxLookupAutoconfigDiscovery(okHttpClient)
val runnables = listOf(providerDiscovery, ispDbDiscovery, mxDiscovery)
.flatMap { it.initDiscovery(emailAddress.toEmailAddress()) }
val serialRunner = SerialRunner(runnables)
return runBlocking {
serialRunner.run()
}
} finally {
okHttpClient.dispatcher.executorService.shutdown()
}
}
}

View file

@ -0,0 +1,28 @@
package app.k9mail.cli.autodiscovery
import app.k9mail.autodiscovery.api.AutoDiscoveryResult
import app.k9mail.autodiscovery.api.ImapServerSettings
import app.k9mail.autodiscovery.api.SmtpServerSettings
internal class AutoDiscoveryResultFormatter(private val echo: (String) -> Unit) {
fun output(discoveryResult: AutoDiscoveryResult) {
val incomingServer = requireNotNull(discoveryResult.incomingServerSettings as? ImapServerSettings)
val outgoingServer = requireNotNull(discoveryResult.outgoingServerSettings as? SmtpServerSettings)
echo("------------------------------")
echo("Incoming server:")
echo(" Hostname: ${incomingServer.hostname.value}")
echo(" Port: ${incomingServer.port.value}")
echo(" Connection security: ${incomingServer.connectionSecurity}")
echo(" Authentication type: ${incomingServer.authenticationType}")
echo(" Username: ${incomingServer.username}")
echo("")
echo("Outgoing server:")
echo(" Hostname: ${outgoingServer.hostname.value}")
echo(" Port: ${outgoingServer.port.value}")
echo(" Connection security: ${outgoingServer.connectionSecurity}")
echo(" Authentication type: ${outgoingServer.authenticationType}")
echo(" Username: ${outgoingServer.username}")
echo("------------------------------")
}
}

View file

@ -0,0 +1,3 @@
package app.k9mail.cli.autodiscovery
fun main(args: Array<String>) = AutoDiscoveryCli().main(args)

View file

@ -0,0 +1,20 @@
package app.k9mail.cli.autodiscovery
import app.k9mail.autodiscovery.api.AutoDiscoveryResult
import app.k9mail.autodiscovery.api.AutoDiscoveryRunnable
/**
* Run a list of [AutoDiscoveryRunnable] one after the other until one returns a non-`null` result.
*/
class SerialRunner(private val runnables: List<AutoDiscoveryRunnable>) {
suspend fun run(): AutoDiscoveryResult? {
for (runnable in runnables) {
val discoveryResult = runnable.run()
if (discoveryResult != null) {
return discoveryResult
}
}
return null
}
}

View file

@ -5,9 +5,9 @@ plugins {
dependencies {
api(projects.feature.autodiscovery.api)
api(libs.okhttp)
implementation(libs.kotlinx.coroutines.core)
implementation(libs.okhttp)
implementation(libs.minidns.hla)
compileOnly(libs.xmlpull)

View file

@ -9,8 +9,9 @@ import com.fsck.k9.helper.EmailHelper
import com.fsck.k9.logging.Timber
import java.io.IOException
import okhttp3.HttpUrl
import okhttp3.OkHttpClient
class AutoconfigDiscovery(
class AutoconfigDiscovery internal constructor(
private val urlProvider: AutoconfigUrlProvider,
private val fetcher: AutoconfigFetcher,
private val parser: SuspendableAutoconfigParser,
@ -44,3 +45,25 @@ class AutoconfigDiscovery(
}
}
}
fun createProviderAutoconfigDiscovery(
okHttpClient: OkHttpClient,
config: AutoconfigUrlConfig,
): AutoconfigDiscovery {
val urlProvider = ProviderAutoconfigUrlProvider(config)
return createAutoconfigDiscovery(okHttpClient, urlProvider)
}
fun createIspDbAutoconfigDiscovery(okHttpClient: OkHttpClient): AutoconfigDiscovery {
val urlProvider = IspDbAutoconfigUrlProvider()
return createAutoconfigDiscovery(okHttpClient, urlProvider)
}
private fun createAutoconfigDiscovery(
okHttpClient: OkHttpClient,
urlProvider: AutoconfigUrlProvider,
): AutoconfigDiscovery {
val fetcher = AutoconfigFetcher(okHttpClient)
val parser = SuspendableAutoconfigParser(AutoconfigParser())
return AutoconfigDiscovery(urlProvider, fetcher, parser)
}

View file

@ -12,7 +12,7 @@ import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
class AutoconfigFetcher(private val okHttpClient: OkHttpClient) {
internal class AutoconfigFetcher(private val okHttpClient: OkHttpClient) {
suspend fun fetchAutoconfigFile(url: HttpUrl): InputStream? {
return try {

View file

@ -39,7 +39,7 @@ private typealias ServerSettingsFactory<T> = (
*
* See [https://github.com/thundernest/autoconfig](https://github.com/thundernest/autoconfig)
*/
class AutoconfigParser {
internal class AutoconfigParser {
fun parseSettings(stream: InputStream, email: EmailAddress): AutoDiscoveryResult? {
return try {
ClientConfigParser(stream, email.address).parse()

View file

@ -4,6 +4,6 @@ import app.k9mail.core.common.mail.EmailAddress
import app.k9mail.core.common.net.Domain
import okhttp3.HttpUrl
interface AutoconfigUrlProvider {
internal interface AutoconfigUrlProvider {
fun getAutoconfigUrls(domain: Domain, email: EmailAddress? = null): List<HttpUrl>
}

View file

@ -7,6 +7,6 @@ import app.k9mail.core.common.net.Domain
*
* An implementation needs to respect the [Public Suffix List](https://publicsuffix.org/).
*/
interface BaseDomainExtractor {
internal interface BaseDomainExtractor {
fun extractBaseDomain(domain: Domain): Domain
}

View file

@ -5,7 +5,7 @@ import app.k9mail.core.common.net.Domain
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl
class IspDbAutoconfigUrlProvider : AutoconfigUrlProvider {
internal class IspDbAutoconfigUrlProvider : AutoconfigUrlProvider {
override fun getAutoconfigUrls(domain: Domain, email: EmailAddress?): List<HttpUrl> {
return listOf(createIspDbUrl(domain))
}

View file

@ -5,7 +5,7 @@ import app.k9mail.core.common.net.toDomain
import org.minidns.hla.ResolverApi
import org.minidns.record.MX
class MiniDnsMxResolver : MxResolver {
internal class MiniDnsMxResolver : MxResolver {
override fun lookup(domain: Domain): List<Domain> {
val result = ResolverApi.INSTANCE.resolve(domain.value, MX::class.java)
return result.answersOrEmptySet

View file

@ -10,8 +10,9 @@ import com.fsck.k9.helper.EmailHelper
import com.fsck.k9.logging.Timber
import java.io.IOException
import okhttp3.HttpUrl
import okhttp3.OkHttpClient
class MxLookupAutoconfigDiscovery(
class MxLookupAutoconfigDiscovery internal constructor(
private val mxResolver: SuspendableMxResolver,
private val baseDomainExtractor: BaseDomainExtractor,
private val subDomainExtractor: SubDomainExtractor,
@ -90,3 +91,15 @@ class MxLookupAutoconfigDiscovery(
}
}
}
fun createMxLookupAutoconfigDiscovery(okHttpClient: OkHttpClient): MxLookupAutoconfigDiscovery {
val baseDomainExtractor = OkHttpBaseDomainExtractor()
return MxLookupAutoconfigDiscovery(
mxResolver = SuspendableMxResolver(MiniDnsMxResolver()),
baseDomainExtractor = baseDomainExtractor,
subDomainExtractor = RealSubDomainExtractor(baseDomainExtractor),
urlProvider = IspDbAutoconfigUrlProvider(),
fetcher = AutoconfigFetcher(okHttpClient),
parser = SuspendableAutoconfigParser(AutoconfigParser()),
)
}

View file

@ -5,6 +5,6 @@ import app.k9mail.core.common.net.Domain
/**
* Look up MX records for a domain.
*/
interface MxResolver {
internal interface MxResolver {
fun lookup(domain: Domain): List<Domain>
}

View file

@ -4,7 +4,7 @@ import app.k9mail.core.common.net.Domain
import app.k9mail.core.common.net.toDomain
import okhttp3.HttpUrl
class OkHttpBaseDomainExtractor : BaseDomainExtractor {
internal class OkHttpBaseDomainExtractor : BaseDomainExtractor {
override fun extractBaseDomain(domain: Domain): Domain {
return domain.value.toHttpUrlOrNull().topPrivateDomain()?.toDomain() ?: domain
}

View file

@ -4,7 +4,7 @@ import app.k9mail.core.common.mail.EmailAddress
import app.k9mail.core.common.net.Domain
import okhttp3.HttpUrl
class ProviderAutoconfigUrlProvider(private val config: AutoconfigUrlConfig) : AutoconfigUrlProvider {
internal class ProviderAutoconfigUrlProvider(private val config: AutoconfigUrlConfig) : AutoconfigUrlProvider {
override fun getAutoconfigUrls(domain: Domain, email: EmailAddress?): List<HttpUrl> {
return buildList {
add(createProviderUrl(domain, email, useHttps = true))

View file

@ -3,7 +3,7 @@ package app.k9mail.autodiscovery.autoconfig
import app.k9mail.core.common.net.Domain
import app.k9mail.core.common.net.toDomain
class RealSubDomainExtractor(private val baseDomainExtractor: BaseDomainExtractor) : SubDomainExtractor {
internal class RealSubDomainExtractor(private val baseDomainExtractor: BaseDomainExtractor) : SubDomainExtractor {
@Suppress("ReturnCount")
override fun extractSubDomain(domain: Domain): Domain? {
val baseDomain = baseDomainExtractor.extractBaseDomain(domain)

View file

@ -7,6 +7,6 @@ import app.k9mail.core.common.net.Domain
*
* An implementation needs to respect the [Public Suffix List](https://publicsuffix.org/).
*/
interface SubDomainExtractor {
internal interface SubDomainExtractor {
fun extractSubDomain(domain: Domain): Domain?
}

View file

@ -6,7 +6,7 @@ import java.io.InputStream
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runInterruptible
class SuspendableAutoconfigParser(private val autoconfigParser: AutoconfigParser) {
internal class SuspendableAutoconfigParser(private val autoconfigParser: AutoconfigParser) {
suspend fun parseSettings(inputStream: InputStream, email: EmailAddress): AutoDiscoveryResult? {
return runInterruptible(Dispatchers.IO) {
autoconfigParser.parseSettings(inputStream, email)

View file

@ -4,7 +4,7 @@ import app.k9mail.core.common.net.Domain
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runInterruptible
class SuspendableMxResolver(private val mxResolver: MxResolver) {
internal class SuspendableMxResolver(private val mxResolver: MxResolver) {
suspend fun lookup(domain: Domain): List<Domain> {
return runInterruptible(Dispatchers.IO) {
mxResolver.lookup(domain)

View file

@ -82,4 +82,7 @@ include(
include(":plugins:openpgp-api-lib:openpgp-api")
include(":cli:html-cleaner-cli")
include(
":cli:autodiscovery-cli",
":cli:html-cleaner-cli",
)