Merge pull request #6926 from thundernest/autodiscovery_cli
Add auto-discovery command line interface
This commit is contained in:
commit
f53d73630e
24 changed files with 206 additions and 17 deletions
3
autodiscovery
Executable file
3
autodiscovery
Executable 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 "$@"
|
|
@ -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 {
|
||||
|
|
18
cli/autodiscovery-cli/build.gradle.kts
Normal file
18
cli/autodiscovery-cli/build.gradle.kts
Normal 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)
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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("------------------------------")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
package app.k9mail.cli.autodiscovery
|
||||
|
||||
fun main(args: Array<String>) = AutoDiscoveryCli().main(args)
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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>
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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?
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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",
|
||||
)
|
||||
|
|
Loading…
Reference in a new issue