Loading feature/account/accountmanager/build.gradle.kts +1 −0 Original line number Diff line number Diff line Loading @@ -11,5 +11,6 @@ dependencies { implementation(projects.mail.common) implementation(projects.feature.autodiscovery.api) implementation(libs.okhttp) implementation(libs.timber) } legacy/ui/legacy/src/main/java/com/fsck/k9/activity/accountmanager/AccountManagerConstants.kt→feature/account/accountmanager/src/main/kotlin/app/k9mail/feature/account/accountmanager/AccountManagerConstants.kt +1 −1 Original line number Diff line number Diff line Loading @@ -15,7 +15,7 @@ * along with this program. If not, see <https://www.gnu.org/licenses/>. * */ package com.fsck.k9.activity.accountmanager package app.k9mail.feature.account.accountmanager object AccountManagerConstants { const val EELO_ACCOUNT_TYPE = "e.foundation.webdav.eelo" Loading feature/account/accountmanager/src/main/kotlin/app/k9mail/feature/account/accountmanager/autoconfig/EeloAutoConfigHelper.kt 0 → 100644 +47 −0 Original line number Diff line number Diff line /* * Copyright (C) 2024 MURENA SAS * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. * */ package app.k9mail.feature.account.accountmanager.autoconfig import com.fsck.k9.mail.AuthType import com.fsck.k9.mail.ConnectionSecurity object EeloAutoConfigHelper { fun getAuthType(authString: String): AuthType? { if (authString.isEmpty()) { return null } when (authString.uppercase()) { "PASSWORD-CLEARTEXT" -> return AuthType.PLAIN "PASSWORD-ENCRYPTED" -> return AuthType.CRAM_MD5 "OAUTH2" -> return AuthType.XOAUTH2 } return null } fun getConnectionSecurity(securityString: String): ConnectionSecurity? { if (securityString.isEmpty()) { return null } when (securityString.uppercase()) { "SSL" -> return ConnectionSecurity.SSL_TLS_REQUIRED "STARTTLS" -> return ConnectionSecurity.STARTTLS_REQUIRED "PLAIN" -> return ConnectionSecurity.NONE } return null } } feature/account/accountmanager/src/main/kotlin/app/k9mail/feature/account/accountmanager/autoconfig/EeloAutoConfigUrlProvider.kt 0 → 100644 +188 −0 Original line number Diff line number Diff line /* * Copyright (C) 2024 MURENA SAS * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. * */ package app.k9mail.feature.account.accountmanager.autoconfig import app.k9mail.feature.account.accountmanager.providersxml.DiscoveredServerSettings import app.k9mail.feature.account.accountmanager.providersxml.DiscoveryResults import com.fsck.k9.helper.EmailHelper import com.fsck.k9.mail.AuthType import com.fsck.k9.mail.ConnectionSecurity import okhttp3.HttpUrl import okhttp3.OkHttpClient import okhttp3.Request import org.xmlpull.v1.XmlPullParser import org.xmlpull.v1.XmlPullParserException import org.xmlpull.v1.XmlPullParserFactory class EeloAutoConfigUrlProvider( private val email: String = "", private val httpsOnly: Boolean = false, private val includeEmailAddress: Boolean = false, ) { private fun getAutoConfigUrls(): List<HttpUrl> { val domain = EmailHelper.getDomainFromEmailAddress(email) ?: return emptyList() return buildList { add(createProviderUrl(domain, email, useHttps = true)) add(createDomainUrl(domain, email, useHttps = true)) if (!httpsOnly) { add(createProviderUrl(domain, email, useHttps = false)) add(createDomainUrl(domain, email, useHttps = false)) } } } private fun createProviderUrl(domain: String, email: String?, useHttps: Boolean): HttpUrl { // https://autoconfig.{domain}/mail/config-v1.1.xml?emailaddress={email} // http://autoconfig.{domain}/mail/config-v1.1.xml?emailaddress={email} return HttpUrl.Builder() .scheme(if (useHttps) "https" else "http") .host("autoconfig.${domain}") .addEncodedPathSegments("mail/config-v1.1.xml") .apply { if (email != null && includeEmailAddress) { addQueryParameter("emailaddress", email) } } .build() } private fun createDomainUrl(domain: String, email: String?, useHttps: Boolean): HttpUrl { // https://{domain}/.well-known/autoconfig/mail/config-v1.1.xml?emailaddress={email} // http://{domain}/.well-known/autoconfig/mail/config-v1.1.xml?emailaddress={email} return HttpUrl.Builder() .scheme(if (useHttps) "https" else "http") .host(domain) .addEncodedPathSegments(".well-known/autoconfig/mail/config-v1.1.xml") .apply { if (email != null && includeEmailAddress) { addQueryParameter("emailaddress", email) } } .build() } fun fetchFromValidXml(): DiscoveryResults { val urls = getAutoConfigUrls() val incomingSettings: MutableList<DiscoveredServerSettings> = emptyList<DiscoveredServerSettings>().toMutableList() val outgoingSettings: MutableList<DiscoveredServerSettings> = emptyList<DiscoveredServerSettings>().toMutableList() if (urls.isEmpty()) { return DiscoveryResults(incomingSettings, outgoingSettings) } val client = OkHttpClient() // Loop through each URL in the list for (url in urls) { val request = Request.Builder() .url(url) .build() client.newCall(request).execute().use { response -> if (response.code == 404) { // Skip this URL and move to the next one in the list return DiscoveryResults(incomingSettings, outgoingSettings) } if (response.isSuccessful) { val body = response.body?.string() if (body != null && body.contains("<clientConfig")) { // Check if response is a valid XML try { val factory = XmlPullParserFactory.newInstance() val parser = factory.newPullParser() parser.setInput(body.reader()) var eventType = parser.eventType var currentTag: String? var serverType: String? = null var hostname = "" var port = 0 var socketType: ConnectionSecurity? = null var authentication: AuthType? = null var username = "" while (eventType != XmlPullParser.END_DOCUMENT) { when (eventType) { XmlPullParser.START_TAG -> { currentTag = parser.name when (currentTag) { "incomingServer", "outgoingServer" -> serverType = parser.getAttributeValue(null, "type") "hostname" -> hostname = parser.nextText() "port" -> port = parser.nextText().toInt() "socketType" -> socketType = EeloAutoConfigHelper.getConnectionSecurity(parser.nextText()) "authentication" -> authentication = EeloAutoConfigHelper.getAuthType(parser.nextText()) "username" -> username = if (parser.nextText().contains( "EMAILADDRESS", )) email else parser.nextText() } } XmlPullParser.END_TAG -> { when (parser.name) { "incomingServer" -> { serverType?.let { if (socketType != null && authentication != null) { incomingSettings.add( DiscoveredServerSettings( protocol = it, host = hostname, port = port, security = socketType, authType = authentication, username = username ) ) } } } "outgoingServer" -> { serverType?.let { if (socketType != null && authentication != null) { outgoingSettings.add( DiscoveredServerSettings( protocol = it, host = hostname, port = port, security = socketType, authType = authentication, username = username ) ) } } } } } } eventType = parser.next() } } catch (e: XmlPullParserException) { e.printStackTrace() } } } } } return DiscoveryResults(incomingSettings, outgoingSettings) } } feature/account/accountmanager/src/main/kotlin/app/k9mail/feature/account/accountmanager/providersxml/ProvidersXmlDiscovery.kt +42 −0 Original line number Diff line number Diff line Loading @@ -162,6 +162,48 @@ class ProvidersXmlDiscovery( return this.replace("\$email", email).replace("\$user", user).replace("\$domain", domain) } fun providersXmlDiscoveryDiscover(emailId: String): DiscoveryResults { val incomingSettings: MutableList<DiscoveredServerSettings> = emptyList<DiscoveredServerSettings>().toMutableList() val outgoingSettings: MutableList<DiscoveredServerSettings> = emptyList<DiscoveredServerSettings>().toMutableList() val results = discover(emailId) ?: return DiscoveryResults(incomingSettings, outgoingSettings) if (results.incoming.isEmpty() || results.outgoing.isEmpty()) { return DiscoveryResults(incomingSettings, outgoingSettings) } results.incoming.forEach { if (it.username == emailId && it.authType != null) { incomingSettings.add( DiscoveredServerSettings( it.protocol, it.host, it.port, it.security, it.authType, emailId ), ) } } results.outgoing.forEach { if (it.username == emailId && it.authType != null) { outgoingSettings.add( DiscoveredServerSettings( it.protocol, it.host, it.port, it.security, it.authType, emailId ), ) } } return DiscoveryResults(incomingSettings, outgoingSettings) } internal data class Provider( val incomingUriTemplate: String, val incomingUsernameTemplate: String, Loading Loading
feature/account/accountmanager/build.gradle.kts +1 −0 Original line number Diff line number Diff line Loading @@ -11,5 +11,6 @@ dependencies { implementation(projects.mail.common) implementation(projects.feature.autodiscovery.api) implementation(libs.okhttp) implementation(libs.timber) }
legacy/ui/legacy/src/main/java/com/fsck/k9/activity/accountmanager/AccountManagerConstants.kt→feature/account/accountmanager/src/main/kotlin/app/k9mail/feature/account/accountmanager/AccountManagerConstants.kt +1 −1 Original line number Diff line number Diff line Loading @@ -15,7 +15,7 @@ * along with this program. If not, see <https://www.gnu.org/licenses/>. * */ package com.fsck.k9.activity.accountmanager package app.k9mail.feature.account.accountmanager object AccountManagerConstants { const val EELO_ACCOUNT_TYPE = "e.foundation.webdav.eelo" Loading
feature/account/accountmanager/src/main/kotlin/app/k9mail/feature/account/accountmanager/autoconfig/EeloAutoConfigHelper.kt 0 → 100644 +47 −0 Original line number Diff line number Diff line /* * Copyright (C) 2024 MURENA SAS * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. * */ package app.k9mail.feature.account.accountmanager.autoconfig import com.fsck.k9.mail.AuthType import com.fsck.k9.mail.ConnectionSecurity object EeloAutoConfigHelper { fun getAuthType(authString: String): AuthType? { if (authString.isEmpty()) { return null } when (authString.uppercase()) { "PASSWORD-CLEARTEXT" -> return AuthType.PLAIN "PASSWORD-ENCRYPTED" -> return AuthType.CRAM_MD5 "OAUTH2" -> return AuthType.XOAUTH2 } return null } fun getConnectionSecurity(securityString: String): ConnectionSecurity? { if (securityString.isEmpty()) { return null } when (securityString.uppercase()) { "SSL" -> return ConnectionSecurity.SSL_TLS_REQUIRED "STARTTLS" -> return ConnectionSecurity.STARTTLS_REQUIRED "PLAIN" -> return ConnectionSecurity.NONE } return null } }
feature/account/accountmanager/src/main/kotlin/app/k9mail/feature/account/accountmanager/autoconfig/EeloAutoConfigUrlProvider.kt 0 → 100644 +188 −0 Original line number Diff line number Diff line /* * Copyright (C) 2024 MURENA SAS * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. * */ package app.k9mail.feature.account.accountmanager.autoconfig import app.k9mail.feature.account.accountmanager.providersxml.DiscoveredServerSettings import app.k9mail.feature.account.accountmanager.providersxml.DiscoveryResults import com.fsck.k9.helper.EmailHelper import com.fsck.k9.mail.AuthType import com.fsck.k9.mail.ConnectionSecurity import okhttp3.HttpUrl import okhttp3.OkHttpClient import okhttp3.Request import org.xmlpull.v1.XmlPullParser import org.xmlpull.v1.XmlPullParserException import org.xmlpull.v1.XmlPullParserFactory class EeloAutoConfigUrlProvider( private val email: String = "", private val httpsOnly: Boolean = false, private val includeEmailAddress: Boolean = false, ) { private fun getAutoConfigUrls(): List<HttpUrl> { val domain = EmailHelper.getDomainFromEmailAddress(email) ?: return emptyList() return buildList { add(createProviderUrl(domain, email, useHttps = true)) add(createDomainUrl(domain, email, useHttps = true)) if (!httpsOnly) { add(createProviderUrl(domain, email, useHttps = false)) add(createDomainUrl(domain, email, useHttps = false)) } } } private fun createProviderUrl(domain: String, email: String?, useHttps: Boolean): HttpUrl { // https://autoconfig.{domain}/mail/config-v1.1.xml?emailaddress={email} // http://autoconfig.{domain}/mail/config-v1.1.xml?emailaddress={email} return HttpUrl.Builder() .scheme(if (useHttps) "https" else "http") .host("autoconfig.${domain}") .addEncodedPathSegments("mail/config-v1.1.xml") .apply { if (email != null && includeEmailAddress) { addQueryParameter("emailaddress", email) } } .build() } private fun createDomainUrl(domain: String, email: String?, useHttps: Boolean): HttpUrl { // https://{domain}/.well-known/autoconfig/mail/config-v1.1.xml?emailaddress={email} // http://{domain}/.well-known/autoconfig/mail/config-v1.1.xml?emailaddress={email} return HttpUrl.Builder() .scheme(if (useHttps) "https" else "http") .host(domain) .addEncodedPathSegments(".well-known/autoconfig/mail/config-v1.1.xml") .apply { if (email != null && includeEmailAddress) { addQueryParameter("emailaddress", email) } } .build() } fun fetchFromValidXml(): DiscoveryResults { val urls = getAutoConfigUrls() val incomingSettings: MutableList<DiscoveredServerSettings> = emptyList<DiscoveredServerSettings>().toMutableList() val outgoingSettings: MutableList<DiscoveredServerSettings> = emptyList<DiscoveredServerSettings>().toMutableList() if (urls.isEmpty()) { return DiscoveryResults(incomingSettings, outgoingSettings) } val client = OkHttpClient() // Loop through each URL in the list for (url in urls) { val request = Request.Builder() .url(url) .build() client.newCall(request).execute().use { response -> if (response.code == 404) { // Skip this URL and move to the next one in the list return DiscoveryResults(incomingSettings, outgoingSettings) } if (response.isSuccessful) { val body = response.body?.string() if (body != null && body.contains("<clientConfig")) { // Check if response is a valid XML try { val factory = XmlPullParserFactory.newInstance() val parser = factory.newPullParser() parser.setInput(body.reader()) var eventType = parser.eventType var currentTag: String? var serverType: String? = null var hostname = "" var port = 0 var socketType: ConnectionSecurity? = null var authentication: AuthType? = null var username = "" while (eventType != XmlPullParser.END_DOCUMENT) { when (eventType) { XmlPullParser.START_TAG -> { currentTag = parser.name when (currentTag) { "incomingServer", "outgoingServer" -> serverType = parser.getAttributeValue(null, "type") "hostname" -> hostname = parser.nextText() "port" -> port = parser.nextText().toInt() "socketType" -> socketType = EeloAutoConfigHelper.getConnectionSecurity(parser.nextText()) "authentication" -> authentication = EeloAutoConfigHelper.getAuthType(parser.nextText()) "username" -> username = if (parser.nextText().contains( "EMAILADDRESS", )) email else parser.nextText() } } XmlPullParser.END_TAG -> { when (parser.name) { "incomingServer" -> { serverType?.let { if (socketType != null && authentication != null) { incomingSettings.add( DiscoveredServerSettings( protocol = it, host = hostname, port = port, security = socketType, authType = authentication, username = username ) ) } } } "outgoingServer" -> { serverType?.let { if (socketType != null && authentication != null) { outgoingSettings.add( DiscoveredServerSettings( protocol = it, host = hostname, port = port, security = socketType, authType = authentication, username = username ) ) } } } } } } eventType = parser.next() } } catch (e: XmlPullParserException) { e.printStackTrace() } } } } } return DiscoveryResults(incomingSettings, outgoingSettings) } }
feature/account/accountmanager/src/main/kotlin/app/k9mail/feature/account/accountmanager/providersxml/ProvidersXmlDiscovery.kt +42 −0 Original line number Diff line number Diff line Loading @@ -162,6 +162,48 @@ class ProvidersXmlDiscovery( return this.replace("\$email", email).replace("\$user", user).replace("\$domain", domain) } fun providersXmlDiscoveryDiscover(emailId: String): DiscoveryResults { val incomingSettings: MutableList<DiscoveredServerSettings> = emptyList<DiscoveredServerSettings>().toMutableList() val outgoingSettings: MutableList<DiscoveredServerSettings> = emptyList<DiscoveredServerSettings>().toMutableList() val results = discover(emailId) ?: return DiscoveryResults(incomingSettings, outgoingSettings) if (results.incoming.isEmpty() || results.outgoing.isEmpty()) { return DiscoveryResults(incomingSettings, outgoingSettings) } results.incoming.forEach { if (it.username == emailId && it.authType != null) { incomingSettings.add( DiscoveredServerSettings( it.protocol, it.host, it.port, it.security, it.authType, emailId ), ) } } results.outgoing.forEach { if (it.username == emailId && it.authType != null) { outgoingSettings.add( DiscoveredServerSettings( it.protocol, it.host, it.port, it.security, it.authType, emailId ), ) } } return DiscoveryResults(incomingSettings, outgoingSettings) } internal data class Provider( val incomingUriTemplate: String, val incomingUsernameTemplate: String, Loading