diff --git a/app/autodiscovery/src/main/java/com/fsck/k9/autodiscovery/ConnectionSettings.kt b/app/autodiscovery/src/main/java/com/fsck/k9/autodiscovery/ConnectionSettings.kt new file mode 100644 index 000000000..9b2c722f4 --- /dev/null +++ b/app/autodiscovery/src/main/java/com/fsck/k9/autodiscovery/ConnectionSettings.kt @@ -0,0 +1,5 @@ +package com.fsck.k9.autodiscovery + +import com.fsck.k9.mail.ServerSettings + +data class ConnectionSettings(val incoming: ServerSettings, val outgoing: ServerSettings) diff --git a/app/autodiscovery/src/main/java/com/fsck/k9/autodiscovery/ConnectionSettingsDiscovery.kt b/app/autodiscovery/src/main/java/com/fsck/k9/autodiscovery/ConnectionSettingsDiscovery.kt new file mode 100644 index 000000000..2842756f1 --- /dev/null +++ b/app/autodiscovery/src/main/java/com/fsck/k9/autodiscovery/ConnectionSettingsDiscovery.kt @@ -0,0 +1,5 @@ +package com.fsck.k9.autodiscovery + +interface ConnectionSettingsDiscovery { + fun discover(email: String): ConnectionSettings? +} diff --git a/app/autodiscovery/src/main/java/com/fsck/k9/autodiscovery/KoinModule.kt b/app/autodiscovery/src/main/java/com/fsck/k9/autodiscovery/KoinModule.kt new file mode 100644 index 000000000..18f98aa93 --- /dev/null +++ b/app/autodiscovery/src/main/java/com/fsck/k9/autodiscovery/KoinModule.kt @@ -0,0 +1,10 @@ +package com.fsck.k9.autodiscovery + +import com.fsck.k9.autodiscovery.providersxml.ProvidersXmlDiscovery +import com.fsck.k9.autodiscovery.providersxml.ProvidersXmlProvider +import org.koin.dsl.module.applicationContext + +val autodiscoveryModule = applicationContext { + factory { ProvidersXmlProvider(get()) } + factory { ProvidersXmlDiscovery(get(), get()) } +} diff --git a/app/autodiscovery/src/main/java/com/fsck/k9/autodiscovery/providersxml/ProvidersXmlDiscovery.java b/app/autodiscovery/src/main/java/com/fsck/k9/autodiscovery/providersxml/ProvidersXmlDiscovery.java new file mode 100644 index 000000000..b8294a06c --- /dev/null +++ b/app/autodiscovery/src/main/java/com/fsck/k9/autodiscovery/providersxml/ProvidersXmlDiscovery.java @@ -0,0 +1,132 @@ +package com.fsck.k9.autodiscovery.providersxml; + + +import java.net.URI; +import java.net.URISyntaxException; + +import android.content.res.XmlResourceParser; + +import com.fsck.k9.autodiscovery.ConnectionSettings; +import com.fsck.k9.autodiscovery.ConnectionSettingsDiscovery; +import com.fsck.k9.backend.BackendManager; +import com.fsck.k9.helper.UrlEncodingHelper; +import com.fsck.k9.mail.ServerSettings; +import org.xmlpull.v1.XmlPullParser; +import timber.log.Timber; + + +public class ProvidersXmlDiscovery implements ConnectionSettingsDiscovery { + private final BackendManager backendManager; + private final ProvidersXmlProvider xmlProvider; + + + public ProvidersXmlDiscovery(BackendManager backendManager, ProvidersXmlProvider xmlProvider) { + this.backendManager = backendManager; + this.xmlProvider = xmlProvider; + } + + private String[] splitEmail(String email) { + String[] retParts = new String[2]; + String[] emailParts = email.split("@"); + retParts[0] = (emailParts.length > 0) ? emailParts[0] : ""; + retParts[1] = (emailParts.length > 1) ? emailParts[1] : ""; + return retParts; + } + + @Override + public ConnectionSettings discover(String email) { + String password = ""; + String[] emailParts = splitEmail(email); + String user = emailParts[0]; + String domain = emailParts[1]; + Provider mProvider = findProviderForDomain(domain); + if (mProvider == null) { + return null; + } + try { + String userEnc = UrlEncodingHelper.encodeUtf8(user); + String passwordEnc = UrlEncodingHelper.encodeUtf8(password); + + String incomingUsername = mProvider.incomingUsernameTemplate; + incomingUsername = incomingUsername.replaceAll("\\$email", email); + incomingUsername = incomingUsername.replaceAll("\\$user", userEnc); + incomingUsername = incomingUsername.replaceAll("\\$domain", domain); + + URI incomingUriTemplate = mProvider.incomingUriTemplate; + URI incomingUri = new URI(incomingUriTemplate.getScheme(), incomingUsername + ":" + passwordEnc, + incomingUriTemplate.getHost(), incomingUriTemplate.getPort(), null, null, null); + + String outgoingUsername = mProvider.outgoingUsernameTemplate; + + URI outgoingUriTemplate = mProvider.outgoingUriTemplate; + + + URI outgoingUri; + if (outgoingUsername != null) { + outgoingUsername = outgoingUsername.replaceAll("\\$email", email); + outgoingUsername = outgoingUsername.replaceAll("\\$user", userEnc); + outgoingUsername = outgoingUsername.replaceAll("\\$domain", domain); + outgoingUri = new URI(outgoingUriTemplate.getScheme(), outgoingUsername + ":" + + passwordEnc, outgoingUriTemplate.getHost(), outgoingUriTemplate.getPort(), null, + null, null); + + } else { + outgoingUri = new URI(outgoingUriTemplate.getScheme(), + null, outgoingUriTemplate.getHost(), outgoingUriTemplate.getPort(), null, + null, null); + } + + ServerSettings incomingSettings = backendManager.decodeStoreUri(incomingUri.toString()); + ServerSettings outgoingSettings = backendManager.decodeTransportUri(outgoingUri.toString()); + return new ConnectionSettings(incomingSettings, outgoingSettings); + } catch (URISyntaxException use) { + return null; + } + } + + private Provider findProviderForDomain(String domain) { + try { + XmlResourceParser xml = xmlProvider.getXml(); + int xmlEventType; + Provider provider = null; + while ((xmlEventType = xml.next()) != XmlPullParser.END_DOCUMENT) { + if (xmlEventType == XmlPullParser.START_TAG && + "provider".equals(xml.getName()) && + domain.equalsIgnoreCase(xml.getAttributeValue(null, "domain"))) { + provider = new Provider(); + provider.id = xml.getAttributeValue(null, "id"); + provider.label = xml.getAttributeValue(null, "label"); + provider.domain = xml.getAttributeValue(null, "domain"); + } else if (xmlEventType == XmlPullParser.START_TAG && + "incoming".equals(xml.getName()) && + provider != null) { + provider.incomingUriTemplate = new URI(xml.getAttributeValue(null, "uri")); + provider.incomingUsernameTemplate = xml.getAttributeValue(null, "username"); + } else if (xmlEventType == XmlPullParser.START_TAG && + "outgoing".equals(xml.getName()) && + provider != null) { + provider.outgoingUriTemplate = new URI(xml.getAttributeValue(null, "uri")); + provider.outgoingUsernameTemplate = xml.getAttributeValue(null, "username"); + } else if (xmlEventType == XmlPullParser.END_TAG && + "provider".equals(xml.getName()) && + provider != null) { + return provider; + } + } + } catch (Exception e) { + Timber.e(e, "Error while trying to load provider settings."); + } + return null; + } + + + static class Provider { + String id; + String label; + String domain; + URI incomingUriTemplate; + String incomingUsernameTemplate; + URI outgoingUriTemplate; + String outgoingUsernameTemplate; + } +} diff --git a/app/autodiscovery/src/main/java/com/fsck/k9/autodiscovery/providersxml/ProvidersXmlProvider.kt b/app/autodiscovery/src/main/java/com/fsck/k9/autodiscovery/providersxml/ProvidersXmlProvider.kt new file mode 100644 index 000000000..38ff0c907 --- /dev/null +++ b/app/autodiscovery/src/main/java/com/fsck/k9/autodiscovery/providersxml/ProvidersXmlProvider.kt @@ -0,0 +1,11 @@ +package com.fsck.k9.autodiscovery.providersxml + +import android.content.Context +import android.content.res.XmlResourceParser +import com.fsck.k9.autodiscovery.R + +class ProvidersXmlProvider(private val context: Context) { + fun getXml(): XmlResourceParser { + return context.resources.getXml(R.xml.providers) + } +} diff --git a/app/ui/src/main/res/xml/providers.xml b/app/autodiscovery/src/main/res/xml/providers.xml similarity index 100% rename from app/ui/src/main/res/xml/providers.xml rename to app/autodiscovery/src/main/res/xml/providers.xml diff --git a/app/autodiscovery/src/test/java/com/fsck/k9/autodiscovery/providersxml/ProvidersXmlDiscoveryTest.kt b/app/autodiscovery/src/test/java/com/fsck/k9/autodiscovery/providersxml/ProvidersXmlDiscoveryTest.kt new file mode 100644 index 000000000..20c1e640a --- /dev/null +++ b/app/autodiscovery/src/test/java/com/fsck/k9/autodiscovery/providersxml/ProvidersXmlDiscoveryTest.kt @@ -0,0 +1,53 @@ +package com.fsck.k9.autodiscovery.providersxml + +import com.fsck.k9.RobolectricTest +import com.fsck.k9.backend.BackendManager +import com.fsck.k9.backend.imap.ImapStoreUriDecoder +import com.fsck.k9.mail.AuthType +import com.fsck.k9.mail.ConnectionSecurity +import com.fsck.k9.mail.transport.smtp.SmtpTransportUriDecoder +import com.google.common.truth.Truth.assertThat +import com.nhaarman.mockito_kotlin.doAnswer +import com.nhaarman.mockito_kotlin.mock +import org.junit.Test +import org.mockito.ArgumentMatchers.anyString +import org.robolectric.RuntimeEnvironment + + +class ProvidersXmlDiscoveryTest : RobolectricTest() { + val backendManager = mock { + on { decodeStoreUri(anyString()) } doAnswer { mock -> ImapStoreUriDecoder.decode(mock.getArgument(0)) } + on { decodeTransportUri(anyString()) } doAnswer { mock -> + SmtpTransportUriDecoder.decodeSmtpUri(mock.getArgument(0)) + } + } + val xmlProvider = ProvidersXmlProvider(RuntimeEnvironment.application) + val providersXmlDiscovery = ProvidersXmlDiscovery(backendManager, xmlProvider) + + + @Test + fun discover_withGmailDomain_shouldReturnCorrectSettings() { + val connectionSettings = providersXmlDiscovery.discover("user@gmail.com") + + assertThat(connectionSettings).isNotNull() + with(connectionSettings!!.incoming) { + assertThat(host).isEqualTo("imap.gmail.com") + assertThat(connectionSecurity).isEqualTo(ConnectionSecurity.SSL_TLS_REQUIRED) + assertThat(authenticationType).isEqualTo(AuthType.PLAIN) + assertThat(username).isEqualTo("user@gmail.com") + } + with(connectionSettings.outgoing) { + assertThat(host).isEqualTo("smtp.gmail.com") + assertThat(connectionSecurity).isEqualTo(ConnectionSecurity.SSL_TLS_REQUIRED) + assertThat(authenticationType).isEqualTo(AuthType.PLAIN) + assertThat(username).isEqualTo("user@gmail.com") + } + } + + @Test + fun discover_withUnknownDomain_shouldReturnNull() { + val connectionSettings = providersXmlDiscovery.discover("user@not.present.in.providers.xml.example") + + assertThat(connectionSettings).isNull() + } +} diff --git a/app/autodiscovery/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/app/autodiscovery/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 000000000..1f0955d45 --- /dev/null +++ b/app/autodiscovery/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1 @@ +mock-maker-inline diff --git a/app/ui/build.gradle b/app/ui/build.gradle index eee1ee098..932869bd8 100644 --- a/app/ui/build.gradle +++ b/app/ui/build.gradle @@ -9,6 +9,7 @@ dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${versions.kotlin}" implementation project(":app:core") + implementation project(":app:autodiscovery") implementation project(":mail:common") //TODO: Remove AccountSetupIncoming's dependency on these diff --git a/app/ui/src/main/java/com/fsck/k9/UiKoinModules.kt b/app/ui/src/main/java/com/fsck/k9/UiKoinModules.kt index 11415e278..4f4fc9cee 100644 --- a/app/ui/src/main/java/com/fsck/k9/UiKoinModules.kt +++ b/app/ui/src/main/java/com/fsck/k9/UiKoinModules.kt @@ -2,6 +2,7 @@ package com.fsck.k9 import com.fsck.k9.account.accountModule import com.fsck.k9.activity.activityModule +import com.fsck.k9.autodiscovery.autodiscoveryModule import com.fsck.k9.contacts.contactsModule import com.fsck.k9.fragment.fragmentModule import com.fsck.k9.ui.endtoend.endToEndUiModule @@ -15,5 +16,6 @@ val uiModules = listOf( endToEndUiModule, fragmentModule, contactsModule, - accountModule + accountModule, + autodiscoveryModule ) diff --git a/app/ui/src/main/java/com/fsck/k9/activity/setup/AccountSetupBasics.java b/app/ui/src/main/java/com/fsck/k9/activity/setup/AccountSetupBasics.java index cb004d833..7e8f3707c 100644 --- a/app/ui/src/main/java/com/fsck/k9/activity/setup/AccountSetupBasics.java +++ b/app/ui/src/main/java/com/fsck/k9/activity/setup/AccountSetupBasics.java @@ -1,12 +1,8 @@ package com.fsck.k9.activity.setup; -import java.net.URI; -import java.net.URISyntaxException; - import android.content.Context; import android.content.Intent; -import android.content.res.XmlResourceParser; import android.os.Bundle; import android.text.Editable; import android.text.InputType; @@ -27,8 +23,10 @@ import com.fsck.k9.Preferences; import com.fsck.k9.account.AccountCreator; import com.fsck.k9.activity.K9Activity; import com.fsck.k9.activity.setup.AccountSetupCheckSettings.CheckDirection; +import com.fsck.k9.autodiscovery.ConnectionSettings; +import com.fsck.k9.autodiscovery.providersxml.ProvidersXmlDiscovery; import com.fsck.k9.backend.BackendManager; -import com.fsck.k9.helper.UrlEncodingHelper; +import com.fsck.k9.helper.EmailHelper; import com.fsck.k9.helper.Utility; import com.fsck.k9.mail.AuthType; import com.fsck.k9.mail.ConnectionSecurity; @@ -53,6 +51,7 @@ public class AccountSetupBasics extends K9Activity private final BackendManager backendManager = DI.get(BackendManager.class); + private final ProvidersXmlDiscovery providersXmlDiscovery = DI.get(ProvidersXmlDiscovery.class); private EditText mEmailView; private EditText mPasswordView; @@ -236,67 +235,30 @@ public class AccountSetupBasics extends K9Activity return name; } - private void finishAutoSetup(Provider provider) { + private void finishAutoSetup(ConnectionSettings connectionSettings) { String email = mEmailView.getText().toString(); String password = mPasswordView.getText().toString(); - String[] emailParts = splitEmail(email); - String user = emailParts[0]; - String domain = emailParts[1]; - try { - String userEnc = UrlEncodingHelper.encodeUtf8(user); - String passwordEnc = UrlEncodingHelper.encodeUtf8(password); - String incomingUsername = provider.incomingUsernameTemplate; - incomingUsername = incomingUsername.replaceAll("\\$email", email); - incomingUsername = incomingUsername.replaceAll("\\$user", userEnc); - incomingUsername = incomingUsername.replaceAll("\\$domain", domain); - - URI incomingUriTemplate = provider.incomingUriTemplate; - URI incomingUri = new URI(incomingUriTemplate.getScheme(), incomingUsername + ":" + passwordEnc, - incomingUriTemplate.getHost(), incomingUriTemplate.getPort(), null, null, null); - - String outgoingUsername = provider.outgoingUsernameTemplate; - - URI outgoingUriTemplate = provider.outgoingUriTemplate; - - - URI outgoingUri; - if (outgoingUsername != null) { - outgoingUsername = outgoingUsername.replaceAll("\\$email", email); - outgoingUsername = outgoingUsername.replaceAll("\\$user", userEnc); - outgoingUsername = outgoingUsername.replaceAll("\\$domain", domain); - outgoingUri = new URI(outgoingUriTemplate.getScheme(), outgoingUsername + ":" - + passwordEnc, outgoingUriTemplate.getHost(), outgoingUriTemplate.getPort(), null, - null, null); - - } else { - outgoingUri = new URI(outgoingUriTemplate.getScheme(), - null, outgoingUriTemplate.getHost(), outgoingUriTemplate.getPort(), null, - null, null); - - - } - if (mAccount == null) { - mAccount = Preferences.getPreferences(this).newAccount(); - mAccount.setChipColor(AccountCreator.pickColor(this)); - } - mAccount.setName(getOwnerName()); - mAccount.setEmail(email); - mAccount.setStoreUri(incomingUri.toString()); - mAccount.setTransportUri(outgoingUri.toString()); - - ServerSettings incomingSettings = backendManager.decodeStoreUri(incomingUri.toString()); - mAccount.setDeletePolicy(AccountCreator.getDefaultDeletePolicy(incomingSettings.type)); - - // Check incoming here. Then check outgoing in onActivityResult() - AccountSetupCheckSettings.actionCheckSettings(this, mAccount, CheckDirection.INCOMING); - } catch (URISyntaxException use) { - /* - * If there is some problem with the URI we give up and go on to - * manual setup. - */ - onManualSetup(); + if (mAccount == null) { + mAccount = Preferences.getPreferences(this).newAccount(); + mAccount.setChipColor(AccountCreator.pickColor(this)); } + + mAccount.setName(getOwnerName()); + mAccount.setEmail(email); + + ServerSettings incomingServerSettings = connectionSettings.getIncoming().newPassword(password); + String storeUri = backendManager.createStoreUri(incomingServerSettings); + mAccount.setStoreUri(storeUri); + + ServerSettings outgoingServerSettings = connectionSettings.getOutgoing().newPassword(password); + String transportUri = backendManager.createTransportUri(outgoingServerSettings); + mAccount.setTransportUri(transportUri); + + mAccount.setDeletePolicy(AccountCreator.getDefaultDeletePolicy(incomingServerSettings.type)); + + // Check incoming here. Then check outgoing in onActivityResult() + AccountSetupCheckSettings.actionCheckSettings(this, mAccount, CheckDirection.INCOMING); } private void onNext() { @@ -308,12 +270,10 @@ public class AccountSetupBasics extends K9Activity } String email = mEmailView.getText().toString(); - String[] emailParts = splitEmail(email); - String domain = emailParts[1]; - Provider provider = findProviderForDomain(domain); - if (provider != null) { - finishAutoSetup(provider); + ConnectionSettings connectionSettings = providersXmlDiscovery.discover(email); + if (connectionSettings != null) { + finishAutoSetup(connectionSettings); } else { // We don't have default settings for this account, start the manual setup process. onManualSetup(); @@ -340,8 +300,7 @@ public class AccountSetupBasics extends K9Activity private void onManualSetup() { String email = mEmailView.getText().toString(); - String[] emailParts = splitEmail(email); - String domain = emailParts[1]; + String domain = EmailHelper.getDomainFromEmailAddress(email); String password = null; String clientCertificateAlias = null; @@ -385,74 +344,4 @@ public class AccountSetupBasics extends K9Activity onManualSetup(); } } - - /** - * Attempts to get the given attribute as a String resource first, and if it fails - * returns the attribute as a simple String value. - * @param xml - * @param name - * @return - */ - private String getXmlAttribute(XmlResourceParser xml, String name) { - int resId = xml.getAttributeResourceValue(null, name, 0); - if (resId == 0) { - return xml.getAttributeValue(null, name); - } else { - return getString(resId); - } - } - - private Provider findProviderForDomain(String domain) { - try { - XmlResourceParser xml = getResources().getXml(R.xml.providers); - int xmlEventType; - Provider provider = null; - while ((xmlEventType = xml.next()) != XmlResourceParser.END_DOCUMENT) { - if (xmlEventType == XmlResourceParser.START_TAG - && "provider".equals(xml.getName()) - && domain.equalsIgnoreCase(getXmlAttribute(xml, "domain"))) { - provider = new Provider(); - provider.id = getXmlAttribute(xml, "id"); - provider.label = getXmlAttribute(xml, "label"); - provider.domain = getXmlAttribute(xml, "domain"); - } else if (xmlEventType == XmlResourceParser.START_TAG - && "incoming".equals(xml.getName()) - && provider != null) { - provider.incomingUriTemplate = new URI(getXmlAttribute(xml, "uri")); - provider.incomingUsernameTemplate = getXmlAttribute(xml, "username"); - } else if (xmlEventType == XmlResourceParser.START_TAG - && "outgoing".equals(xml.getName()) - && provider != null) { - provider.outgoingUriTemplate = new URI(getXmlAttribute(xml, "uri")); - provider.outgoingUsernameTemplate = getXmlAttribute(xml, "username"); - } else if (xmlEventType == XmlResourceParser.END_TAG - && "provider".equals(xml.getName()) - && provider != null) { - return provider; - } - } - } catch (Exception e) { - Timber.e(e, "Error while trying to load provider settings."); - } - return null; - } - - private String[] splitEmail(String email) { - String[] retParts = new String[2]; - String[] emailParts = email.split("@"); - retParts[0] = (emailParts.length > 0) ? emailParts[0] : ""; - retParts[1] = (emailParts.length > 1) ? emailParts[1] : ""; - return retParts; - } - - - static class Provider { - public String id; - public String label; - public String domain; - public URI incomingUriTemplate; - public String incomingUsernameTemplate; - public URI outgoingUriTemplate; - public String outgoingUsernameTemplate; - } }