forked from thunderbird/thunderbird-android
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add support for server settings autodiscovery
This change makes account setup query mail provider's HTTPS server and fills in appropriate settings for store and transport settings. If there is no such file or the file is malformed the account setup proceeds as usual with the manual setup. If the user doesn't want to automatically discover server settings they can initiate manual setup by pressing "Manual Setup" button on the first screen. Currently implemented autodiscovery scheme is Thunderbird Autoconfig but this can be extended to additional methods (e.g. DNS or autodiscover). Resolves thunderbird#865. See: https://wiki.mozilla.org/Thunderbird:Autoconfiguration:ConfigFileFormat
- Loading branch information
Showing
3 changed files
with
281 additions
and
6 deletions.
There are no files selected for viewing
134 changes: 134 additions & 0 deletions
134
app/ui/src/main/java/com/fsck/k9/activity/setup/AccountDiscovery.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,134 @@ | ||
package com.fsck.k9.activity.setup | ||
|
||
import com.fsck.k9.mail.AuthType | ||
import com.fsck.k9.mail.ConnectionSecurity | ||
import com.fsck.k9.mail.ServerSettings | ||
import kotlinx.coroutines.experimental.android.UI | ||
import kotlinx.coroutines.experimental.launch | ||
import org.jetbrains.anko.coroutines.experimental.bg | ||
import org.xmlpull.v1.XmlPullParser | ||
import org.xmlpull.v1.XmlPullParserException | ||
import org.xmlpull.v1.XmlPullParserFactory | ||
import timber.log.Timber | ||
import java.io.IOException | ||
import java.io.InputStream | ||
import java.io.InputStreamReader | ||
import java.net.HttpURLConnection | ||
import java.net.URL | ||
import java.net.URLEncoder | ||
|
||
class AccountDiscovery { | ||
|
||
class DiscoveredSettings(val incoming: ServerSettings, val outgoing: ServerSettings) | ||
|
||
fun discover(email: String, callback: (settings: DiscoveredSettings?) -> Unit) { | ||
launch(UI) { | ||
var settings: DiscoveredSettings? = null | ||
try { | ||
settings = bg { | ||
val url = URL(getAutodiscoveryAddress(email)) | ||
|
||
with(url.openConnection()) { | ||
parseSettings(inputStream, email) | ||
} | ||
}.await() | ||
} catch (e: Exception) { | ||
Timber.e("Autodiscovery error", e) | ||
} | ||
|
||
callback(settings) | ||
} | ||
} | ||
|
||
// address described at: | ||
// https://developer.mozilla.org/en-US/docs/Mozilla/Thunderbird/Autoconfiguration#Configuration_server_at_ISP | ||
internal fun getAutodiscoveryAddress(email: String): String { | ||
val parts = email.split("@") | ||
val domain = parts[1] | ||
return "https://${domain}/.well-known/autoconfig/mail/config-v1.1.xml?emailaddress=" + URLEncoder.encode(email, "UTF-8") | ||
} | ||
|
||
// structure described at: | ||
// https://wiki.mozilla.org/Thunderbird:Autoconfiguration:ConfigFileFormat | ||
internal fun parseSettings(stream: InputStream, email: String): DiscoveredSettings? { | ||
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 DiscoveredSettings(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 | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
111 changes: 111 additions & 0 deletions
111
app/ui/src/test/java/com/fsck/k9/activity/setup/AccountDiscoveryTest.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
package com.fsck.k9.activity.setup | ||
|
||
import com.fsck.k9.RobolectricTest | ||
import junit.framework.Assert.assertEquals | ||
import junit.framework.Assert.assertNotNull | ||
import org.junit.Test | ||
|
||
class AccountDiscoveryTest : RobolectricTest() { | ||
private val accountDiscovery = AccountDiscovery() | ||
|
||
@Test | ||
fun settingsExtract() { | ||
val settings = accountDiscovery.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 = accountDiscovery.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 generatedUrls() { | ||
val email = "test@metacode.biz" | ||
|
||
val actual = accountDiscovery.getAutodiscoveryAddress(email) | ||
|
||
val expected = "https://metacode.biz/.well-known/autoconfig/mail/config-v1.1.xml?emailaddress=test%40metacode.biz" | ||
|
||
assertEquals(expected, actual) | ||
} | ||
} |