Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 26add5ab authored by Mohammed Althaf T's avatar Mohammed Althaf T 😊
Browse files

mail: add autoconfig support

parent 13c825c3
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -11,5 +11,6 @@ dependencies {
    implementation(projects.mail.common)
    implementation(projects.feature.autodiscovery.api)

    implementation(libs.okhttp)
    implementation(libs.timber)
}
+1 −1
Original line number Diff line number Diff line
@@ -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"
+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
    }
}
+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)
    }
}
+42 −0
Original line number Diff line number Diff line
@@ -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