Add Thunderbird autodiscovery code (part 1)
This commit is contained in:
parent
57062fd558
commit
b7652205ae
7 changed files with 308 additions and 0 deletions
|
@ -11,6 +11,7 @@ dependencies {
|
|||
implementation project(":mail:common")
|
||||
|
||||
implementation "com.jakewharton.timber:timber:${versions.timber}"
|
||||
implementation "com.squareup.okhttp3:okhttp:${versions.okhttp}"
|
||||
|
||||
testImplementation project(':app:testing')
|
||||
testImplementation project(":backend:imap")
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
package com.fsck.k9.autodiscovery.thunderbird
|
||||
|
||||
import com.fsck.k9.helper.EmailHelper
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import java.io.InputStream
|
||||
import java.net.URLEncoder
|
||||
|
||||
class ThunderbirdAutoconfigFetcher(private val client: OkHttpClient) {
|
||||
|
||||
fun fetchAutoconfigFile(email: String): InputStream? {
|
||||
val url = getAutodiscoveryAddress(email)
|
||||
val request = Request.Builder().url(url).build()
|
||||
|
||||
return client.newCall(request).execute().body()?.byteStream()
|
||||
}
|
||||
|
||||
companion object {
|
||||
// address described at:
|
||||
// https://developer.mozilla.org/en-US/docs/Mozilla/Thunderbird/Autoconfiguration#Configuration_server_at_ISP
|
||||
internal fun getAutodiscoveryAddress(email: String): String {
|
||||
val domain = EmailHelper.getDomainFromEmailAddress(email)
|
||||
return "https://${domain}/.well-known/autoconfig/mail/config-v1.1.xml?emailaddress=" + URLEncoder.encode(email, "UTF-8")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,102 @@
|
|||
package com.fsck.k9.autodiscovery.thunderbird
|
||||
|
||||
import com.fsck.k9.autodiscovery.ConnectionSettings
|
||||
import com.fsck.k9.mail.AuthType
|
||||
import com.fsck.k9.mail.ConnectionSecurity
|
||||
import com.fsck.k9.mail.ServerSettings
|
||||
import org.xmlpull.v1.XmlPullParser
|
||||
import org.xmlpull.v1.XmlPullParserException
|
||||
import org.xmlpull.v1.XmlPullParserFactory
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.io.InputStreamReader
|
||||
|
||||
class ThunderbirdAutoconfigParser {
|
||||
// structure described at:
|
||||
// https://wiki.mozilla.org/Thunderbird:Autoconfiguration:ConfigFileFormat
|
||||
fun parseSettings(stream: InputStream?, email: String): ConnectionSettings? {
|
||||
if (stream == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
var incoming: ServerSettings? = null
|
||||
var outgoing: ServerSettings? = null
|
||||
|
||||
val factory = XmlPullParserFactory.newInstance()
|
||||
val xpp = factory.newPullParser()
|
||||
|
||||
xpp.setInput(InputStreamReader(stream))
|
||||
|
||||
var eventType = xpp.eventType
|
||||
while (eventType != XmlPullParser.END_DOCUMENT) {
|
||||
if (eventType == XmlPullParser.START_TAG) {
|
||||
if ("incomingServer" == xpp.name) {
|
||||
incoming = parseServer(xpp, "incomingServer", email)
|
||||
} else if ("outgoingServer" == xpp.name) {
|
||||
outgoing = parseServer(xpp, "outgoingServer", email)
|
||||
}
|
||||
|
||||
if (incoming != null && outgoing != null) {
|
||||
return ConnectionSettings(incoming, outgoing)
|
||||
}
|
||||
}
|
||||
eventType = xpp.next()
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun parseServer(xpp: XmlPullParser, nodeName: String, email: String): ServerSettings {
|
||||
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) {
|
||||
if (xpp.name == "hostname") {
|
||||
host = getText(xpp)
|
||||
} else if (xpp.name == "port") {
|
||||
port = getText(xpp).toInt()
|
||||
} else if (xpp.name == "username") {
|
||||
username = getText(xpp).replace("%EMAILADDRESS%", email)
|
||||
} else if (xpp.name == "authentication" && authType == null) {
|
||||
authType = parseAuthType(getText(xpp))
|
||||
} else if (xpp.name == "socketType") {
|
||||
connectionSecurity = parseSocketType(getText(xpp))
|
||||
}
|
||||
}
|
||||
eventType = xpp.next()
|
||||
}
|
||||
|
||||
return ServerSettings(type, host, port!!, connectionSecurity, authType, username, null, null)
|
||||
}
|
||||
|
||||
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,18 @@
|
|||
package com.fsck.k9.autodiscovery.thunderbird
|
||||
|
||||
import com.fsck.k9.autodiscovery.ConnectionSettings
|
||||
import com.fsck.k9.autodiscovery.ConnectionSettingsDiscovery
|
||||
|
||||
class ThunderbirdDiscovery(
|
||||
private val fetcher: ThunderbirdAutoconfigFetcher,
|
||||
private val parser: ThunderbirdAutoconfigParser
|
||||
): ConnectionSettingsDiscovery {
|
||||
|
||||
override fun discover(email: String): ConnectionSettings? {
|
||||
return fetcher.fetchAutoconfigFile(email).use {
|
||||
parser.parseSettings(it, email)
|
||||
}
|
||||
}
|
||||
|
||||
override fun toString(): String = "Thunderbird autoconfig"
|
||||
}
|
|
@ -0,0 +1,143 @@
|
|||
package com.fsck.k9.autodiscovery.thunderbird
|
||||
|
||||
import com.fsck.k9.RobolectricTest
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Test
|
||||
|
||||
class ThunderbirdAutoconfigTest : RobolectricTest() {
|
||||
private val parser = ThunderbirdAutoconfigParser()
|
||||
|
||||
@Test
|
||||
fun settingsExtract() {
|
||||
val settings = parser.parseSettings("""<?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>""".byteInputStream(), "test@metacode.biz")
|
||||
|
||||
assertNotNull(settings)
|
||||
assertNotNull(settings?.outgoing)
|
||||
assertNotNull(settings?.incoming)
|
||||
|
||||
assertEquals("imap.googlemail.com", settings?.incoming?.host)
|
||||
assertEquals(993, settings?.incoming?.port)
|
||||
assertEquals("test@metacode.biz", settings?.incoming?.username)
|
||||
|
||||
assertEquals("smtp.googlemail.com", settings?.outgoing?.host)
|
||||
assertEquals(465, settings?.outgoing?.port)
|
||||
assertEquals("test@metacode.biz", settings?.outgoing?.username)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun pickFirstServer() {
|
||||
val settings = parser.parseSettings("""<?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>""".byteInputStream(), "test@metacode.biz")
|
||||
|
||||
assertNotNull(settings)
|
||||
assertNotNull(settings?.outgoing)
|
||||
assertNotNull(settings?.incoming)
|
||||
|
||||
assertEquals("first", settings?.outgoing?.host)
|
||||
assertEquals(465, settings?.outgoing?.port)
|
||||
assertEquals("test@metacode.biz", settings?.outgoing?.username)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun invalidResponse() {
|
||||
val settings = parser.parseSettings("""<?xml version="1.0"?>
|
||||
<clientConfig version="1.1">
|
||||
<emailProvider id="metacode.biz">
|
||||
<domain>metacode.biz</domain>""".byteInputStream(), "test@metacode.biz")
|
||||
|
||||
assertNull(settings)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun notCompleteConfiguration() {
|
||||
val settings = parser.parseSettings("""<?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>""".byteInputStream(), "test@metacode.biz")
|
||||
|
||||
assertNull(settings)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun generatedUrls() {
|
||||
val email = "test@metacode.biz"
|
||||
|
||||
val actual = ThunderbirdAutoconfigFetcher.getAutodiscoveryAddress(email)
|
||||
|
||||
val expected = "https://metacode.biz/.well-known/autoconfig/mail/config-v1.1.xml?emailaddress=test%40metacode.biz"
|
||||
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
}
|
15
app/k9mail/proguard-rules.pro
vendored
15
app/k9mail/proguard-rules.pro
vendored
|
@ -26,3 +26,18 @@
|
|||
-keepclassmembers class * extends androidx.appcompat.widget.SearchView {
|
||||
public <init>(android.content.Context);
|
||||
}
|
||||
|
||||
# okhttp rules
|
||||
# see: https://github.com/square/okhttp/blob/master/okhttp/src/main/resources/META-INF/proguard/okhttp3.pro
|
||||
|
||||
# JSR 305 annotations are for embedding nullability information.
|
||||
-dontwarn javax.annotation.**
|
||||
|
||||
# A resource is loaded with a relative path so the package of this class must be preserved.
|
||||
-keepnames class okhttp3.internal.publicsuffix.PublicSuffixDatabase
|
||||
|
||||
# Animal Sniffer compileOnly dependency to ensure APIs are compatible with older versions of Java.
|
||||
-dontwarn org.codehaus.mojo.animal_sniffer.*
|
||||
|
||||
# OkHttp platform used only on JVM and when Conscrypt dependency is available.
|
||||
-dontwarn okhttp3.internal.platform.ConscryptPlatform
|
||||
|
|
|
@ -20,6 +20,9 @@ buildscript {
|
|||
'commonsIo': '2.4',
|
||||
'mime4j': '0.8.1',
|
||||
|
||||
// Note: Only use 3.12.x versions until we raise minSdkVersion to 21
|
||||
'okhttp': '3.12.1',
|
||||
|
||||
'androidxTestRunner': '1.1.1',
|
||||
'junit': '4.12',
|
||||
'robolectric': '3.7.1',
|
||||
|
|
Loading…
Reference in a new issue