Make individual autoconfig steps suspendable and cancellable

This commit is contained in:
cketti 2023-05-24 15:22:03 +02:00
parent 2d1d54cd6d
commit 67d082f01f
5 changed files with 87 additions and 40 deletions

View file

@ -8,14 +8,12 @@ import app.k9mail.core.common.net.toDomain
import com.fsck.k9.helper.EmailHelper
import com.fsck.k9.logging.Timber
import java.io.IOException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.HttpUrl
class AutoconfigDiscovery(
private val urlProvider: AutoconfigUrlProvider,
private val fetcher: AutoconfigFetcher,
private val parser: AutoconfigParser,
private val parser: SuspendableAutoconfigParser,
) : AutoDiscovery {
override fun initDiscovery(email: EmailAddress): List<AutoDiscoveryRunnable> {
@ -27,18 +25,12 @@ class AutoconfigDiscovery(
return autoconfigUrls.map { autoconfigUrl ->
AutoDiscoveryRunnable {
getConfigInBackground(email, autoconfigUrl)
getAutoconfig(email, autoconfigUrl)
}
}
}
private suspend fun getConfigInBackground(email: EmailAddress, autoconfigUrl: HttpUrl): AutoDiscoveryResult? {
return withContext(Dispatchers.IO) {
getAutoconfig(email, autoconfigUrl)
}
}
private fun getAutoconfig(email: EmailAddress, autoconfigUrl: HttpUrl): AutoDiscoveryResult? {
private suspend fun getAutoconfig(email: EmailAddress, autoconfigUrl: HttpUrl): AutoDiscoveryResult? {
return try {
fetcher.fetchAutoconfigFile(autoconfigUrl)?.use { inputStream ->
parser.parseSettings(inputStream, email)

View file

@ -3,26 +3,56 @@ package app.k9mail.autodiscovery.autoconfig
import com.fsck.k9.logging.Timber
import java.io.IOException
import java.io.InputStream
import kotlin.coroutines.resume
import kotlinx.coroutines.suspendCancellableCoroutine
import okhttp3.Call
import okhttp3.Callback
import okhttp3.HttpUrl
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
class AutoconfigFetcher(private val okHttpClient: OkHttpClient) {
fun fetchAutoconfigFile(url: HttpUrl): InputStream? {
suspend fun fetchAutoconfigFile(url: HttpUrl): InputStream? {
return try {
val request = Request.Builder().url(url).build()
val response = okHttpClient.newCall(request).execute()
if (response.isSuccessful) {
response.body?.byteStream()
} else {
null
}
performHttpRequest(url)
} catch (e: IOException) {
Timber.d(e, "Error fetching URL: %s", url)
null
}
}
private suspend fun performHttpRequest(url: HttpUrl): InputStream? {
return suspendCancellableCoroutine { cancellableContinuation ->
val request = Request.Builder()
.url(url)
.build()
val call = okHttpClient.newCall(request)
cancellableContinuation.invokeOnCancellation {
call.cancel()
}
val responseCallback = object : Callback {
override fun onFailure(call: Call, e: IOException) {
cancellableContinuation.cancel(e)
}
override fun onResponse(call: Call, response: Response) {
if (response.isSuccessful) {
val inputStream = response.body?.byteStream()
cancellableContinuation.resume(inputStream)
} else {
// We don't care about the body of error responses.
response.close()
cancellableContinuation.resume(null)
}
}
}
call.enqueue(responseCallback)
}
}
}

View file

@ -9,35 +9,27 @@ import app.k9mail.core.common.net.toDomain
import com.fsck.k9.helper.EmailHelper
import com.fsck.k9.logging.Timber
import java.io.IOException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.HttpUrl
class MxLookupAutoconfigDiscovery(
private val mxResolver: MxResolver,
private val mxResolver: SuspendableMxResolver,
private val baseDomainExtractor: BaseDomainExtractor,
private val subDomainExtractor: SubDomainExtractor,
private val urlProvider: AutoconfigUrlProvider,
private val fetcher: AutoconfigFetcher,
private val parser: AutoconfigParser,
private val parser: SuspendableAutoconfigParser,
) : AutoDiscovery {
override fun initDiscovery(email: EmailAddress): List<AutoDiscoveryRunnable> {
return listOf(
AutoDiscoveryRunnable {
mxLookupInBackground(email)
mxLookupAutoconfig(email)
},
)
}
private suspend fun mxLookupInBackground(email: EmailAddress): AutoDiscoveryResult? {
return withContext(Dispatchers.IO) {
mxLookupAutoconfig(email)
}
}
@Suppress("ReturnCount")
private fun mxLookupAutoconfig(email: EmailAddress): AutoDiscoveryResult? {
private suspend fun mxLookupAutoconfig(email: EmailAddress): AutoDiscoveryResult? {
val domain = requireNotNull(EmailHelper.getDomainFromEmailAddress(email.address)?.toDomain()) {
"Couldn't extract domain from email address: ${email.address}"
}
@ -54,14 +46,19 @@ class MxLookupAutoconfigDiscovery(
// between Outlook.com/Hotmail and Office365 business domains.
val mxSubDomain = getNextSubDomain(mxHostName)?.takeIf { it != mxBaseDomain }
return listOfNotNull(mxSubDomain, mxBaseDomain)
.asSequence()
.flatMap { domainToCheck -> urlProvider.getAutoconfigUrls(domainToCheck) }
.mapNotNull { autoconfigUrl -> getAutoconfig(email, autoconfigUrl) }
.firstOrNull()
for (domainToCheck in listOfNotNull(mxSubDomain, mxBaseDomain)) {
for (autoconfigUrl in urlProvider.getAutoconfigUrls(domainToCheck)) {
val discoveryResult = getAutoconfig(email, autoconfigUrl)
if (discoveryResult != null) {
return discoveryResult
}
}
}
return null
}
private fun mxLookup(domain: Domain): Domain? {
private suspend fun mxLookup(domain: Domain): Domain? {
// Only return the most preferred entry to match Thunderbird's behavior.
return try {
mxResolver.lookup(domain).firstOrNull()
@ -79,7 +76,7 @@ class MxLookupAutoconfigDiscovery(
return subDomainExtractor.extractSubDomain(domain)
}
private fun getAutoconfig(email: EmailAddress, autoconfigUrl: HttpUrl): AutoDiscoveryResult? {
private suspend fun getAutoconfig(email: EmailAddress, autoconfigUrl: HttpUrl): AutoDiscoveryResult? {
return try {
fetcher.fetchAutoconfigFile(autoconfigUrl)?.use { inputStream ->
parser.parseSettings(inputStream, email)

View file

@ -0,0 +1,15 @@
package app.k9mail.autodiscovery.autoconfig
import app.k9mail.autodiscovery.api.AutoDiscoveryResult
import app.k9mail.core.common.mail.EmailAddress
import java.io.InputStream
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runInterruptible
class SuspendableAutoconfigParser(private val autoconfigParser: AutoconfigParser) {
suspend fun parseSettings(inputStream: InputStream, email: EmailAddress): AutoDiscoveryResult? {
return runInterruptible(Dispatchers.IO) {
autoconfigParser.parseSettings(inputStream, email)
}
}
}

View file

@ -0,0 +1,13 @@
package app.k9mail.autodiscovery.autoconfig
import app.k9mail.core.common.net.Domain
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runInterruptible
class SuspendableMxResolver(private val mxResolver: MxResolver) {
suspend fun lookup(domain: Domain): List<Domain> {
return runInterruptible(Dispatchers.IO) {
mxResolver.lookup(domain)
}
}
}