Repo created
This commit is contained in:
parent
75dc487a7a
commit
39c29d175b
6317 changed files with 388324 additions and 2 deletions
13
feature/autodiscovery/service/build.gradle.kts
Normal file
13
feature/autodiscovery/service/build.gradle.kts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
plugins {
|
||||
id(ThunderbirdPlugins.Library.jvm)
|
||||
alias(libs.plugins.android.lint)
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api(projects.feature.autodiscovery.autoconfig)
|
||||
|
||||
implementation(libs.kotlinx.coroutines.core)
|
||||
|
||||
testImplementation(libs.kotlinx.coroutines.test)
|
||||
testImplementation(libs.kxml2)
|
||||
}
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
package app.k9mail.autodiscovery.service
|
||||
|
||||
import app.k9mail.autodiscovery.api.AutoDiscoveryResult
|
||||
import app.k9mail.autodiscovery.api.AutoDiscoveryResult.NetworkError
|
||||
import app.k9mail.autodiscovery.api.AutoDiscoveryResult.NoUsableSettingsFound
|
||||
import app.k9mail.autodiscovery.api.AutoDiscoveryResult.Settings
|
||||
import app.k9mail.autodiscovery.api.AutoDiscoveryResult.UnexpectedException
|
||||
import app.k9mail.autodiscovery.api.AutoDiscoveryRunnable
|
||||
import kotlin.coroutines.cancellation.CancellationException
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineStart.LAZY
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
|
||||
/**
|
||||
* Runs a list of [AutoDiscoveryRunnable]s with descending priority in parallel and returns the result with the highest
|
||||
* priority.
|
||||
*
|
||||
* As soon as an [AutoDiscoveryRunnable] returns a [Settings] result, runnables with a lower priority are canceled.
|
||||
*/
|
||||
internal class PriorityParallelRunner(
|
||||
private val runnables: List<AutoDiscoveryRunnable>,
|
||||
private val coroutineDispatcher: CoroutineDispatcher = Dispatchers.IO,
|
||||
) {
|
||||
suspend fun run(): AutoDiscoveryResult {
|
||||
return coroutineScope {
|
||||
val deferredList = buildList(capacity = runnables.size) {
|
||||
// Create coroutines in reverse order. So ones with lower priority are created first.
|
||||
for (runnable in runnables.reversed()) {
|
||||
val lowerPriorityCoroutines = toList()
|
||||
|
||||
val deferred = async(coroutineDispatcher, start = LAZY) {
|
||||
runnable.run().also { discoveryResult ->
|
||||
if (discoveryResult is Settings) {
|
||||
// We've got a positive result, so cancel all coroutines with lower priority.
|
||||
lowerPriorityCoroutines.cancelAll()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
add(deferred)
|
||||
}
|
||||
}.asReversed()
|
||||
|
||||
for (deferred in deferredList) {
|
||||
deferred.start()
|
||||
}
|
||||
|
||||
@Suppress("SwallowedException", "TooGenericExceptionCaught")
|
||||
val discoveryResults = deferredList.map { deferred ->
|
||||
try {
|
||||
deferred.await()
|
||||
} catch (e: CancellationException) {
|
||||
null
|
||||
} catch (e: Exception) {
|
||||
UnexpectedException(e)
|
||||
}
|
||||
}
|
||||
|
||||
val settingsResult = discoveryResults.firstOrNull { it is Settings }
|
||||
if (settingsResult != null) {
|
||||
settingsResult
|
||||
} else {
|
||||
val networkError = discoveryResults.firstOrNull { it is NetworkError }
|
||||
val networkErrorCount = discoveryResults.count { it is NetworkError }
|
||||
if (networkError != null && networkErrorCount == discoveryResults.size) {
|
||||
networkError
|
||||
} else {
|
||||
NoUsableSettingsFound
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun List<Deferred<AutoDiscoveryResult?>>.cancelAll() {
|
||||
for (deferred in this) {
|
||||
deferred.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
package app.k9mail.autodiscovery.service
|
||||
|
||||
import app.k9mail.autodiscovery.api.AutoDiscovery
|
||||
import app.k9mail.autodiscovery.api.AutoDiscoveryRegistry
|
||||
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 okhttp3.OkHttpClient
|
||||
|
||||
class RealAutoDiscoveryRegistry(
|
||||
private val autoDiscoveries: List<AutoDiscovery> = emptyList(),
|
||||
) : AutoDiscoveryRegistry {
|
||||
|
||||
override fun getAutoDiscoveries(): List<AutoDiscovery> = autoDiscoveries
|
||||
|
||||
companion object {
|
||||
private val defaultAutoconfigUrlConfig = AutoconfigUrlConfig(
|
||||
httpsOnly = false,
|
||||
includeEmailAddress = false,
|
||||
)
|
||||
|
||||
fun createDefaultAutoDiscoveries(
|
||||
okHttpClient: OkHttpClient,
|
||||
autoconfigUrlConfig: AutoconfigUrlConfig = defaultAutoconfigUrlConfig,
|
||||
): List<AutoDiscovery> {
|
||||
return listOf(
|
||||
createProviderAutoconfigDiscovery(
|
||||
okHttpClient = okHttpClient,
|
||||
config = autoconfigUrlConfig,
|
||||
),
|
||||
createIspDbAutoconfigDiscovery(
|
||||
okHttpClient = okHttpClient,
|
||||
),
|
||||
createMxLookupAutoconfigDiscovery(
|
||||
okHttpClient = okHttpClient,
|
||||
config = autoconfigUrlConfig,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package app.k9mail.autodiscovery.service
|
||||
|
||||
import app.k9mail.autodiscovery.api.AutoDiscoveryRegistry
|
||||
import app.k9mail.autodiscovery.api.AutoDiscoveryResult
|
||||
import app.k9mail.autodiscovery.api.AutoDiscoveryService
|
||||
import net.thunderbird.core.common.mail.EmailAddress
|
||||
|
||||
/**
|
||||
* Uses Thunderbird's Autoconfig mechanism to find mail server settings for a given email address.
|
||||
*/
|
||||
class RealAutoDiscoveryService(
|
||||
private val autoDiscoveryRegistry: AutoDiscoveryRegistry,
|
||||
) : AutoDiscoveryService {
|
||||
|
||||
override suspend fun discover(email: EmailAddress): AutoDiscoveryResult {
|
||||
val runner = PriorityParallelRunner(
|
||||
runnables = autoDiscoveryRegistry.getAutoDiscoveries().flatMap { it.initDiscovery(email) },
|
||||
)
|
||||
return runner.run()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,181 @@
|
|||
package app.k9mail.autodiscovery.service
|
||||
|
||||
import app.k9mail.autodiscovery.api.AuthenticationType.PasswordCleartext
|
||||
import app.k9mail.autodiscovery.api.AutoDiscoveryResult
|
||||
import app.k9mail.autodiscovery.api.AutoDiscoveryResult.NoUsableSettingsFound
|
||||
import app.k9mail.autodiscovery.api.AutoDiscoveryRunnable
|
||||
import app.k9mail.autodiscovery.api.ConnectionSecurity.StartTLS
|
||||
import app.k9mail.autodiscovery.api.ConnectionSecurity.TLS
|
||||
import app.k9mail.autodiscovery.api.ImapServerSettings
|
||||
import app.k9mail.autodiscovery.api.SmtpServerSettings
|
||||
import assertk.assertThat
|
||||
import assertk.assertions.isEqualTo
|
||||
import assertk.assertions.isFalse
|
||||
import assertk.assertions.isNull
|
||||
import assertk.assertions.isTrue
|
||||
import kotlin.test.Test
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import net.thunderbird.core.common.net.toHostname
|
||||
import net.thunderbird.core.common.net.toPort
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class PriorityParallelRunnerTest {
|
||||
@Test
|
||||
fun `first runnable returning a success result should cancel remaining runnables`() = runTest {
|
||||
var runnableTwoStarted = false
|
||||
var runnableThreeStarted = false
|
||||
var runnableTwoCompleted = false
|
||||
var runnableThreeCompleted = false
|
||||
val runnableOne = AutoDiscoveryRunnable {
|
||||
delay(100)
|
||||
DISCOVERY_RESULT_ONE
|
||||
}
|
||||
val runnableTwo = AutoDiscoveryRunnable {
|
||||
runnableTwoStarted = true
|
||||
delay(200)
|
||||
runnableTwoCompleted = true
|
||||
DISCOVERY_RESULT_TWO
|
||||
}
|
||||
val runnableThree = AutoDiscoveryRunnable {
|
||||
runnableThreeStarted = true
|
||||
delay(200)
|
||||
runnableThreeCompleted = false
|
||||
DISCOVERY_RESULT_TWO
|
||||
}
|
||||
val runner = PriorityParallelRunner(
|
||||
runnables = listOf(runnableOne, runnableTwo, runnableThree),
|
||||
coroutineDispatcher = StandardTestDispatcher(testScheduler),
|
||||
)
|
||||
|
||||
var result: AutoDiscoveryResult? = null
|
||||
launch {
|
||||
result = runner.run()
|
||||
}
|
||||
|
||||
testScheduler.advanceTimeBy(50)
|
||||
|
||||
assertThat(result).isNull()
|
||||
assertThat(runnableTwoStarted).isTrue()
|
||||
assertThat(runnableTwoCompleted).isFalse()
|
||||
assertThat(runnableThreeStarted).isTrue()
|
||||
assertThat(runnableThreeCompleted).isFalse()
|
||||
|
||||
testScheduler.advanceUntilIdle()
|
||||
|
||||
assertThat(result).isEqualTo(DISCOVERY_RESULT_ONE)
|
||||
assertThat(runnableTwoCompleted).isFalse()
|
||||
assertThat(runnableThreeCompleted).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `highest priority result should be used even if it takes longer to be produced`() = runTest {
|
||||
var runnableTwoCompleted = false
|
||||
val runnableOne = AutoDiscoveryRunnable {
|
||||
delay(100)
|
||||
DISCOVERY_RESULT_ONE
|
||||
}
|
||||
val runnableTwo = AutoDiscoveryRunnable {
|
||||
runnableTwoCompleted = true
|
||||
DISCOVERY_RESULT_TWO
|
||||
}
|
||||
val runner = PriorityParallelRunner(
|
||||
runnables = listOf(runnableOne, runnableTwo),
|
||||
coroutineDispatcher = StandardTestDispatcher(testScheduler),
|
||||
)
|
||||
|
||||
var result: AutoDiscoveryResult? = null
|
||||
launch {
|
||||
result = runner.run()
|
||||
}
|
||||
|
||||
testScheduler.advanceTimeBy(50)
|
||||
|
||||
assertThat(result).isNull()
|
||||
assertThat(runnableTwoCompleted).isTrue()
|
||||
|
||||
testScheduler.advanceUntilIdle()
|
||||
|
||||
assertThat(result).isEqualTo(DISCOVERY_RESULT_ONE)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `wait for higher priority runnable to complete`() = runTest {
|
||||
var runnableOneCompleted = false
|
||||
var runnableTwoCompleted = false
|
||||
val runnableOne = AutoDiscoveryRunnable {
|
||||
delay(100)
|
||||
runnableOneCompleted = true
|
||||
NO_DISCOVERY_RESULT
|
||||
}
|
||||
val runnableTwo = AutoDiscoveryRunnable {
|
||||
runnableTwoCompleted = true
|
||||
DISCOVERY_RESULT_TWO
|
||||
}
|
||||
val runner = PriorityParallelRunner(
|
||||
runnables = listOf(runnableOne, runnableTwo),
|
||||
coroutineDispatcher = StandardTestDispatcher(testScheduler),
|
||||
)
|
||||
|
||||
var result: AutoDiscoveryResult? = null
|
||||
launch {
|
||||
result = runner.run()
|
||||
}
|
||||
|
||||
testScheduler.advanceTimeBy(50)
|
||||
|
||||
assertThat(result).isNull()
|
||||
assertThat(runnableOneCompleted).isFalse()
|
||||
assertThat(runnableTwoCompleted).isTrue()
|
||||
|
||||
testScheduler.advanceTimeBy(100)
|
||||
|
||||
assertThat(result).isEqualTo(DISCOVERY_RESULT_TWO)
|
||||
assertThat(runnableOneCompleted).isTrue()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val NO_DISCOVERY_RESULT: AutoDiscoveryResult = NoUsableSettingsFound
|
||||
|
||||
private val DISCOVERY_RESULT_ONE = AutoDiscoveryResult.Settings(
|
||||
ImapServerSettings(
|
||||
hostname = "imap.domain.example".toHostname(),
|
||||
port = 993.toPort(),
|
||||
connectionSecurity = TLS,
|
||||
authenticationTypes = listOf(PasswordCleartext),
|
||||
username = "user@domain.example",
|
||||
),
|
||||
SmtpServerSettings(
|
||||
hostname = "smtp.domain.example".toHostname(),
|
||||
port = 587.toPort(),
|
||||
connectionSecurity = StartTLS,
|
||||
authenticationTypes = listOf(PasswordCleartext),
|
||||
username = "user@domain.example",
|
||||
),
|
||||
isTrusted = true,
|
||||
source = "result 1",
|
||||
)
|
||||
|
||||
private val DISCOVERY_RESULT_TWO = AutoDiscoveryResult.Settings(
|
||||
ImapServerSettings(
|
||||
hostname = "imap.domain.example".toHostname(),
|
||||
port = 143.toPort(),
|
||||
connectionSecurity = StartTLS,
|
||||
authenticationTypes = listOf(PasswordCleartext),
|
||||
username = "user@domain.example",
|
||||
),
|
||||
SmtpServerSettings(
|
||||
hostname = "smtp.domain.example".toHostname(),
|
||||
port = 465.toPort(),
|
||||
connectionSecurity = TLS,
|
||||
authenticationTypes = listOf(PasswordCleartext),
|
||||
username = "user@domain.example",
|
||||
),
|
||||
isTrusted = true,
|
||||
source = "result 2",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
package app.k9mail.autodiscovery.service
|
||||
|
||||
import app.k9mail.autodiscovery.api.AutoDiscovery
|
||||
import app.k9mail.autodiscovery.api.AutoDiscoveryRunnable
|
||||
import assertk.assertThat
|
||||
import assertk.assertions.isEqualTo
|
||||
import kotlin.test.Test
|
||||
import net.thunderbird.core.common.mail.EmailAddress
|
||||
|
||||
class RealAutoDiscoveryRegistryTest {
|
||||
|
||||
@Test
|
||||
fun `getAutoDiscoveries should return given discoveries`() {
|
||||
val autoDiscoveries = listOf(
|
||||
TestAutoDiscovery(),
|
||||
TestAutoDiscovery(),
|
||||
)
|
||||
val testSubject = RealAutoDiscoveryRegistry(autoDiscoveries)
|
||||
|
||||
val result = testSubject.getAutoDiscoveries()
|
||||
|
||||
assertThat(result).isEqualTo(autoDiscoveries)
|
||||
}
|
||||
|
||||
private class TestAutoDiscovery : AutoDiscovery {
|
||||
override fun initDiscovery(email: EmailAddress): List<AutoDiscoveryRunnable> {
|
||||
return emptyList()
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue