Repo created
This commit is contained in:
parent
a629de6271
commit
3cef7c5092
2161 changed files with 246605 additions and 2 deletions
15
app/autodiscovery/thunderbird/build.gradle.kts
Normal file
15
app/autodiscovery/thunderbird/build.gradle.kts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
@Suppress("DSL_SCOPE_VIOLATION")
|
||||
plugins {
|
||||
id(ThunderbirdPlugins.Library.jvm)
|
||||
alias(libs.plugins.android.lint)
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api(projects.app.autodiscovery.api)
|
||||
|
||||
compileOnly(libs.xmlpull)
|
||||
implementation(libs.okhttp)
|
||||
|
||||
testImplementation(libs.kxml2)
|
||||
testImplementation(libs.okhttp.mockwebserver)
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
package com.fsck.k9.autodiscovery.thunderbird
|
||||
|
||||
import com.fsck.k9.logging.Timber
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
|
||||
class ThunderbirdAutoconfigFetcher(private val okHttpClient: OkHttpClient) {
|
||||
|
||||
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
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
Timber.d(e, "Error fetching URL: %s", url)
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
package com.fsck.k9.autodiscovery.thunderbird
|
||||
|
||||
import com.fsck.k9.autodiscovery.api.DiscoveredServerSettings
|
||||
import com.fsck.k9.autodiscovery.api.DiscoveryResults
|
||||
import com.fsck.k9.mail.AuthType
|
||||
import com.fsck.k9.mail.ConnectionSecurity
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.io.InputStreamReader
|
||||
import org.xmlpull.v1.XmlPullParser
|
||||
import org.xmlpull.v1.XmlPullParserException
|
||||
import org.xmlpull.v1.XmlPullParserFactory
|
||||
|
||||
/**
|
||||
* Parser for Thunderbird's
|
||||
* [Autoconfig file format](https://wiki.mozilla.org/Thunderbird:Autoconfiguration:ConfigFileFormat)
|
||||
*/
|
||||
class ThunderbirdAutoconfigParser {
|
||||
fun parseSettings(stream: InputStream, email: String): DiscoveryResults? {
|
||||
val factory = XmlPullParserFactory.newInstance()
|
||||
val xpp = factory.newPullParser()
|
||||
|
||||
xpp.setInput(InputStreamReader(stream))
|
||||
|
||||
val incomingServers = mutableListOf<DiscoveredServerSettings>()
|
||||
val outgoingServers = mutableListOf<DiscoveredServerSettings>()
|
||||
var eventType = xpp.eventType
|
||||
while (eventType != XmlPullParser.END_DOCUMENT) {
|
||||
if (eventType == XmlPullParser.START_TAG) {
|
||||
when (xpp.name) {
|
||||
"incomingServer" -> {
|
||||
incomingServers += parseServer(xpp, "incomingServer", email)
|
||||
}
|
||||
"outgoingServer" -> {
|
||||
outgoingServers += parseServer(xpp, "outgoingServer", email)
|
||||
}
|
||||
}
|
||||
}
|
||||
eventType = xpp.next()
|
||||
}
|
||||
return DiscoveryResults(incomingServers, outgoingServers)
|
||||
}
|
||||
|
||||
private fun parseServer(xpp: XmlPullParser, nodeName: String, email: String): DiscoveredServerSettings {
|
||||
val type = xpp.getAttributeValue(null, "type")
|
||||
var host: String? = null
|
||||
var username: String? = null
|
||||
var port: Int? = null
|
||||
var authType: AuthType? = null
|
||||
var connectionSecurity: ConnectionSecurity? = null
|
||||
|
||||
var eventType = xpp.eventType
|
||||
while (!(eventType == XmlPullParser.END_TAG && nodeName == xpp.name)) {
|
||||
if (eventType == XmlPullParser.START_TAG) {
|
||||
when (xpp.name) {
|
||||
"hostname" -> {
|
||||
host = getText(xpp)
|
||||
}
|
||||
"port" -> {
|
||||
port = getText(xpp).toInt()
|
||||
}
|
||||
"username" -> {
|
||||
username = getText(xpp).replace("%EMAILADDRESS%", email)
|
||||
}
|
||||
"authentication" -> {
|
||||
if (authType == null) authType = parseAuthType(getText(xpp))
|
||||
}
|
||||
"socketType" -> {
|
||||
connectionSecurity = parseSocketType(getText(xpp))
|
||||
}
|
||||
}
|
||||
}
|
||||
eventType = xpp.next()
|
||||
}
|
||||
|
||||
return DiscoveredServerSettings(type, host!!, port!!, connectionSecurity!!, authType, username)
|
||||
}
|
||||
|
||||
private fun parseAuthType(authentication: String): AuthType? {
|
||||
return when (authentication) {
|
||||
"password-cleartext" -> AuthType.PLAIN
|
||||
"TLS-client-cert" -> AuthType.EXTERNAL
|
||||
"secure" -> AuthType.CRAM_MD5
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseSocketType(socketType: String): ConnectionSecurity? {
|
||||
return when (socketType) {
|
||||
"plain" -> ConnectionSecurity.NONE
|
||||
"SSL" -> ConnectionSecurity.SSL_TLS_REQUIRED
|
||||
"STARTTLS" -> ConnectionSecurity.STARTTLS_REQUIRED
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(XmlPullParserException::class, IOException::class)
|
||||
private fun getText(xpp: XmlPullParser): String {
|
||||
val eventType = xpp.next()
|
||||
return if (eventType != XmlPullParser.TEXT) "" else xpp.text
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
package com.fsck.k9.autodiscovery.thunderbird
|
||||
|
||||
import com.fsck.k9.helper.EmailHelper
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
|
||||
class ThunderbirdAutoconfigUrlProvider {
|
||||
fun getAutoconfigUrls(email: String): List<HttpUrl> {
|
||||
val domain = EmailHelper.getDomainFromEmailAddress(email)
|
||||
requireNotNull(domain) { "Couldn't extract domain from email address: $email" }
|
||||
|
||||
return listOf(
|
||||
createProviderUrl(domain, email),
|
||||
createDomainUrl(scheme = "https", domain),
|
||||
createDomainUrl(scheme = "http", domain),
|
||||
createIspDbUrl(domain)
|
||||
)
|
||||
}
|
||||
|
||||
private fun createProviderUrl(domain: String?, email: String): HttpUrl {
|
||||
// https://autoconfig.{domain}/mail/config-v1.1.xml?emailaddress={email}
|
||||
return HttpUrl.Builder()
|
||||
.scheme("https")
|
||||
.host("autoconfig.$domain")
|
||||
.addEncodedPathSegments("mail/config-v1.1.xml")
|
||||
.addQueryParameter("emailaddress", email)
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun createDomainUrl(scheme: String, domain: String): HttpUrl {
|
||||
// https://{domain}/.well-known/autoconfig/mail/config-v1.1.xml
|
||||
// http://{domain}/.well-known/autoconfig/mail/config-v1.1.xml
|
||||
return HttpUrl.Builder()
|
||||
.scheme(scheme)
|
||||
.host(domain)
|
||||
.addEncodedPathSegments(".well-known/autoconfig/mail/config-v1.1.xml")
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun createIspDbUrl(domain: String): HttpUrl {
|
||||
// https://autoconfig.thunderbird.net/v1.1/{domain}
|
||||
return "https://autoconfig.thunderbird.net/v1.1/".toHttpUrl()
|
||||
.newBuilder()
|
||||
.addPathSegment(domain)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
package com.fsck.k9.autodiscovery.thunderbird
|
||||
|
||||
import com.fsck.k9.autodiscovery.api.ConnectionSettingsDiscovery
|
||||
import com.fsck.k9.autodiscovery.api.DiscoveryResults
|
||||
|
||||
class ThunderbirdDiscovery(
|
||||
private val urlProvider: ThunderbirdAutoconfigUrlProvider,
|
||||
private val fetcher: ThunderbirdAutoconfigFetcher,
|
||||
private val parser: ThunderbirdAutoconfigParser
|
||||
) : ConnectionSettingsDiscovery {
|
||||
|
||||
override fun discover(email: String): DiscoveryResults? {
|
||||
val autoconfigUrls = urlProvider.getAutoconfigUrls(email)
|
||||
|
||||
return autoconfigUrls
|
||||
.asSequence()
|
||||
.mapNotNull { autoconfigUrl ->
|
||||
fetcher.fetchAutoconfigFile(autoconfigUrl)?.use { inputStream ->
|
||||
parser.parseSettings(inputStream, email)
|
||||
}
|
||||
}
|
||||
.firstOrNull { result ->
|
||||
result.incoming.isNotEmpty() || result.outgoing.isNotEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
override fun toString(): String = "Thunderbird autoconfig"
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
package com.fsck.k9.autodiscovery.thunderbird
|
||||
|
||||
import assertk.assertThat
|
||||
import assertk.assertions.isEqualTo
|
||||
import assertk.assertions.isNull
|
||||
import kotlin.test.assertNotNull
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.mockwebserver.MockResponse
|
||||
import okhttp3.mockwebserver.MockWebServer
|
||||
import org.junit.Test
|
||||
|
||||
class ThunderbirdAutoconfigFetcherTest {
|
||||
private val fetcher = ThunderbirdAutoconfigFetcher(OkHttpClient.Builder().build())
|
||||
|
||||
@Test
|
||||
fun shouldHandleNonexistentUrl() {
|
||||
val nonExistentUrl =
|
||||
"https://autoconfig.domain.invalid/mail/config-v1.1.xml?emailaddress=test%40domain.example".toHttpUrl()
|
||||
|
||||
val inputStream = fetcher.fetchAutoconfigFile(nonExistentUrl)
|
||||
|
||||
assertThat(inputStream).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun shouldHandleEmptyResponse() {
|
||||
val server = MockWebServer().apply {
|
||||
this.enqueue(
|
||||
MockResponse()
|
||||
.setBody("")
|
||||
.setResponseCode(204),
|
||||
)
|
||||
start()
|
||||
}
|
||||
val url = server.url("/empty/")
|
||||
|
||||
val inputStream = fetcher.fetchAutoconfigFile(url)
|
||||
|
||||
assertNotNull(inputStream) { inputStream ->
|
||||
assertThat(inputStream.available()).isEqualTo(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,174 @@
|
|||
package com.fsck.k9.autodiscovery.thunderbird
|
||||
|
||||
import assertk.assertThat
|
||||
import assertk.assertions.isEqualTo
|
||||
import assertk.assertions.isNotNull
|
||||
import com.fsck.k9.autodiscovery.api.DiscoveredServerSettings
|
||||
import com.fsck.k9.autodiscovery.api.DiscoveryResults
|
||||
import com.fsck.k9.mail.AuthType
|
||||
import com.fsck.k9.mail.ConnectionSecurity
|
||||
import org.junit.Test
|
||||
|
||||
class ThunderbirdAutoconfigTest {
|
||||
private val parser = ThunderbirdAutoconfigParser()
|
||||
|
||||
@Test
|
||||
fun settingsExtract() {
|
||||
val input =
|
||||
"""
|
||||
<?xml version="1.0"?>
|
||||
<clientConfig version="1.1">
|
||||
<emailProvider id="metacode.biz">
|
||||
<domain>metacode.biz</domain>
|
||||
|
||||
<incomingServer type="imap">
|
||||
<hostname>imap.googlemail.com</hostname>
|
||||
<port>993</port>
|
||||
<socketType>SSL</socketType>
|
||||
<username>%EMAILADDRESS%</username>
|
||||
<authentication>OAuth2</authentication>
|
||||
<authentication>password-cleartext</authentication>
|
||||
</incomingServer>
|
||||
|
||||
<outgoingServer type="smtp">
|
||||
<hostname>smtp.googlemail.com</hostname>
|
||||
<port>465</port>
|
||||
<socketType>SSL</socketType>
|
||||
<username>%EMAILADDRESS%</username>
|
||||
<authentication>OAuth2</authentication>
|
||||
<authentication>password-cleartext</authentication>
|
||||
<addThisServer>true</addThisServer>
|
||||
</outgoingServer>
|
||||
</emailProvider>
|
||||
</clientConfig>
|
||||
""".trimIndent().byteInputStream()
|
||||
|
||||
val connectionSettings = parser.parseSettings(input, "test@metacode.biz")
|
||||
|
||||
assertThat(connectionSettings).isNotNull()
|
||||
assertThat(connectionSettings!!.incoming).isNotNull()
|
||||
assertThat(connectionSettings.outgoing).isNotNull()
|
||||
with(connectionSettings.incoming.first()) {
|
||||
assertThat(host).isEqualTo("imap.googlemail.com")
|
||||
assertThat(port).isEqualTo(993)
|
||||
assertThat(username).isEqualTo("test@metacode.biz")
|
||||
}
|
||||
with(connectionSettings.outgoing.first()) {
|
||||
assertThat(host).isEqualTo("smtp.googlemail.com")
|
||||
assertThat(port).isEqualTo(465)
|
||||
assertThat(username).isEqualTo("test@metacode.biz")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun multipleServers() {
|
||||
val input =
|
||||
"""
|
||||
<?xml version="1.0"?>
|
||||
<clientConfig version="1.1">
|
||||
<emailProvider id="metacode.biz">
|
||||
<domain>metacode.biz</domain>
|
||||
|
||||
<incomingServer type="imap">
|
||||
<hostname>imap.googlemail.com</hostname>
|
||||
<port>993</port>
|
||||
<socketType>SSL</socketType>
|
||||
<username>%EMAILADDRESS%</username>
|
||||
<authentication>OAuth2</authentication>
|
||||
<authentication>password-cleartext</authentication>
|
||||
</incomingServer>
|
||||
|
||||
<outgoingServer type="smtp">
|
||||
<hostname>first</hostname>
|
||||
<port>465</port>
|
||||
<socketType>SSL</socketType>
|
||||
<username>%EMAILADDRESS%</username>
|
||||
<authentication>OAuth2</authentication>
|
||||
<authentication>password-cleartext</authentication>
|
||||
<addThisServer>true</addThisServer>
|
||||
</outgoingServer>
|
||||
|
||||
<outgoingServer type="smtp">
|
||||
<hostname>second</hostname>
|
||||
<port>465</port>
|
||||
<socketType>SSL</socketType>
|
||||
<username>%EMAILADDRESS%</username>
|
||||
<authentication>OAuth2</authentication>
|
||||
<authentication>password-cleartext</authentication>
|
||||
<addThisServer>true</addThisServer>
|
||||
</outgoingServer>
|
||||
</emailProvider>
|
||||
</clientConfig>
|
||||
""".trimIndent().byteInputStream()
|
||||
|
||||
val discoveryResults = parser.parseSettings(input, "test@metacode.biz")
|
||||
|
||||
assertThat(discoveryResults).isNotNull()
|
||||
assertThat(discoveryResults!!.outgoing).isNotNull()
|
||||
with(discoveryResults.outgoing[0]) {
|
||||
assertThat(host).isEqualTo("first")
|
||||
assertThat(port).isEqualTo(465)
|
||||
assertThat(username).isEqualTo("test@metacode.biz")
|
||||
}
|
||||
with(discoveryResults.outgoing[1]) {
|
||||
assertThat(host).isEqualTo("second")
|
||||
assertThat(port).isEqualTo(465)
|
||||
assertThat(username).isEqualTo("test@metacode.biz")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun invalidResponse() {
|
||||
val input =
|
||||
"""
|
||||
<?xml version="1.0"?>
|
||||
<clientConfig version="1.1">
|
||||
<emailProvider id="metacode.biz">
|
||||
<domain>metacode.biz</domain>
|
||||
""".trimIndent().byteInputStream()
|
||||
|
||||
val connectionSettings = parser.parseSettings(input, "test@metacode.biz")
|
||||
|
||||
assertThat(connectionSettings).isEqualTo(DiscoveryResults(listOf(), listOf()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun incompleteConfiguration() {
|
||||
val input =
|
||||
"""
|
||||
<?xml version="1.0"?>
|
||||
<clientConfig version="1.1">
|
||||
<emailProvider id="metacode.biz">
|
||||
<domain>metacode.biz</domain>
|
||||
|
||||
<incomingServer type="imap">
|
||||
<hostname>imap.googlemail.com</hostname>
|
||||
<port>993</port>
|
||||
<socketType>SSL</socketType>
|
||||
<username>%EMAILADDRESS%</username>
|
||||
<authentication>OAuth2</authentication>
|
||||
<authentication>password-cleartext</authentication>
|
||||
</incomingServer>
|
||||
</emailProvider>
|
||||
</clientConfig>
|
||||
""".trimIndent().byteInputStream()
|
||||
|
||||
val connectionSettings = parser.parseSettings(input, "test@metacode.biz")
|
||||
|
||||
assertThat(connectionSettings).isEqualTo(
|
||||
DiscoveryResults(
|
||||
listOf(
|
||||
DiscoveredServerSettings(
|
||||
protocol = "imap",
|
||||
host = "imap.googlemail.com",
|
||||
port = 993,
|
||||
security = ConnectionSecurity.SSL_TLS_REQUIRED,
|
||||
authType = AuthType.PLAIN,
|
||||
username = "test@metacode.biz"
|
||||
)
|
||||
),
|
||||
listOf()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package com.fsck.k9.autodiscovery.thunderbird
|
||||
|
||||
import assertk.assertThat
|
||||
import assertk.assertions.containsExactly
|
||||
import org.junit.Test
|
||||
|
||||
class ThunderbirdAutoconfigUrlProviderTest {
|
||||
private val urlProvider = ThunderbirdAutoconfigUrlProvider()
|
||||
|
||||
@Test
|
||||
fun `getAutoconfigUrls with ASCII email address`() {
|
||||
val autoconfigUrls = urlProvider.getAutoconfigUrls("test@domain.example")
|
||||
|
||||
assertThat(autoconfigUrls.map { it.toString() }).containsExactly(
|
||||
"https://autoconfig.domain.example/mail/config-v1.1.xml?emailaddress=test%40domain.example",
|
||||
"https://domain.example/.well-known/autoconfig/mail/config-v1.1.xml",
|
||||
"http://domain.example/.well-known/autoconfig/mail/config-v1.1.xml",
|
||||
"https://autoconfig.thunderbird.net/v1.1/domain.example"
|
||||
)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue