Add OAuth check to GetAutoDiscovery
This commit is contained in:
parent
23b0e79a0e
commit
f0672ddd53
5 changed files with 327 additions and 55 deletions
|
@ -2,6 +2,7 @@ package app.k9mail.feature.account.setup
|
|||
|
||||
import app.k9mail.autodiscovery.api.AutoDiscoveryService
|
||||
import app.k9mail.autodiscovery.service.RealAutoDiscoveryService
|
||||
import app.k9mail.core.common.coreCommonModule
|
||||
import app.k9mail.feature.account.setup.domain.DomainContract
|
||||
import app.k9mail.feature.account.setup.domain.usecase.CheckIncomingServerConfig
|
||||
import app.k9mail.feature.account.setup.domain.usecase.CheckOutgoingServerConfig
|
||||
|
@ -29,6 +30,7 @@ import org.koin.core.module.Module
|
|||
import org.koin.dsl.module
|
||||
|
||||
val featureAccountSetupModule: Module = module {
|
||||
includes(coreCommonModule)
|
||||
|
||||
single<OkHttpClient> {
|
||||
OkHttpClient()
|
||||
|
@ -43,6 +45,7 @@ val featureAccountSetupModule: Module = module {
|
|||
single<DomainContract.UseCase.GetAutoDiscovery> {
|
||||
GetAutoDiscovery(
|
||||
service = get(),
|
||||
oauthProvider = get(),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,82 @@
|
|||
package app.k9mail.feature.account.setup.data
|
||||
|
||||
import app.k9mail.autodiscovery.api.AuthenticationType
|
||||
import app.k9mail.autodiscovery.api.AutoDiscoveryResult
|
||||
import app.k9mail.autodiscovery.api.AutoDiscoveryService
|
||||
import app.k9mail.autodiscovery.api.ConnectionSecurity
|
||||
import app.k9mail.autodiscovery.api.ImapServerSettings
|
||||
import app.k9mail.autodiscovery.api.SmtpServerSettings
|
||||
import app.k9mail.core.common.mail.EmailAddress
|
||||
import app.k9mail.core.common.net.toHostname
|
||||
import app.k9mail.core.common.net.toPort
|
||||
import java.io.IOException
|
||||
import java.lang.UnsupportedOperationException
|
||||
import kotlin.random.Random
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
class FakeAutoDiscoveryService : AutoDiscoveryService {
|
||||
override suspend fun discover(email: EmailAddress): AutoDiscoveryResult {
|
||||
val result: AutoDiscoveryResult? = handleFakeResponse(email)
|
||||
return if (result != null) {
|
||||
provideWithDelay(result)
|
||||
} else {
|
||||
AutoDiscoveryResult.UnexpectedException(
|
||||
UnsupportedOperationException("No fake response for $email"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
private suspend fun provideWithDelay(autoDiscoveryResult: AutoDiscoveryResult): AutoDiscoveryResult {
|
||||
delay(Random(0).nextLong(500, 2000))
|
||||
return autoDiscoveryResult
|
||||
}
|
||||
|
||||
private fun handleFakeResponse(emailAddress: EmailAddress): AutoDiscoveryResult? {
|
||||
return if (emailAddress.localPart.contains("empty")) {
|
||||
AutoDiscoveryResult.NoUsableSettingsFound
|
||||
} else if (emailAddress.localPart.contains("test")) {
|
||||
getFakeAutoDiscovery(emailAddress)
|
||||
} else if (emailAddress.localPart.contains("error")) {
|
||||
AutoDiscoveryResult.NetworkError(IOException("Failed to load config"))
|
||||
} else if (emailAddress.localPart.contains("unexpected")) {
|
||||
AutoDiscoveryResult.UnexpectedException(Exception("Unexpected exception"))
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
private fun getFakeAutoDiscovery(emailAddress: EmailAddress): AutoDiscoveryResult.Settings {
|
||||
val hasIncomingOauth = emailAddress.localPart.contains("in")
|
||||
val hasOutgoingOauth = emailAddress.localPart.contains("out")
|
||||
val isTrusted = emailAddress.localPart.contains("trust")
|
||||
|
||||
return AutoDiscoveryResult.Settings(
|
||||
incomingServerSettings = ImapServerSettings(
|
||||
hostname = "imap.${emailAddress.domain}".toHostname(),
|
||||
port = 993.toPort(),
|
||||
connectionSecurity = ConnectionSecurity.TLS,
|
||||
authenticationType = if (hasIncomingOauth) {
|
||||
AuthenticationType.OAuth2
|
||||
} else {
|
||||
AuthenticationType.PasswordEncrypted
|
||||
},
|
||||
username = "username",
|
||||
),
|
||||
outgoingServerSettings = SmtpServerSettings(
|
||||
hostname = "smtp.${emailAddress.domain}".toHostname(),
|
||||
port = 993.toPort(),
|
||||
connectionSecurity = ConnectionSecurity.TLS,
|
||||
authenticationType = if (hasOutgoingOauth) {
|
||||
AuthenticationType.OAuth2
|
||||
} else {
|
||||
AuthenticationType.PasswordEncrypted
|
||||
},
|
||||
username = "username",
|
||||
),
|
||||
isTrusted = isTrusted,
|
||||
source = "fake",
|
||||
)
|
||||
}
|
||||
}
|
|
@ -3,79 +3,75 @@ package app.k9mail.feature.account.setup.domain.usecase
|
|||
import app.k9mail.autodiscovery.api.AuthenticationType
|
||||
import app.k9mail.autodiscovery.api.AutoDiscoveryResult
|
||||
import app.k9mail.autodiscovery.api.AutoDiscoveryService
|
||||
import app.k9mail.autodiscovery.api.ConnectionSecurity
|
||||
import app.k9mail.autodiscovery.api.ImapServerSettings
|
||||
import app.k9mail.autodiscovery.api.SmtpServerSettings
|
||||
import app.k9mail.core.common.mail.toUserEmailAddress
|
||||
import app.k9mail.core.common.net.toHostname
|
||||
import app.k9mail.core.common.net.toPort
|
||||
import app.k9mail.core.common.oauth.OAuthConfigurationProvider
|
||||
import app.k9mail.feature.account.setup.data.FakeAutoDiscoveryService
|
||||
import app.k9mail.feature.account.setup.domain.DomainContract
|
||||
import java.io.IOException
|
||||
import kotlin.random.Random
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
internal class GetAutoDiscovery(
|
||||
private val service: AutoDiscoveryService,
|
||||
private val oauthProvider: OAuthConfigurationProvider,
|
||||
private val fakeService: FakeAutoDiscoveryService = FakeAutoDiscoveryService(),
|
||||
) : DomainContract.UseCase.GetAutoDiscovery {
|
||||
override suspend fun execute(emailAddress: String): AutoDiscoveryResult {
|
||||
val fakeResult: AutoDiscoveryResult? = if (emailAddress.contains("empty")) {
|
||||
AutoDiscoveryResult.NoUsableSettingsFound
|
||||
} else if (emailAddress.contains("test")) {
|
||||
getFakeAutoDiscovery(emailAddress)
|
||||
} else if (emailAddress.contains("error")) {
|
||||
AutoDiscoveryResult.NetworkError(IOException("Failed to load config"))
|
||||
} else if (emailAddress.contains("unexpected")) {
|
||||
AutoDiscoveryResult.UnexpectedException(Exception("Unexpected exception"))
|
||||
val email = emailAddress.toUserEmailAddress()
|
||||
val fakeResult = fakeService.discover(email)
|
||||
if (fakeResult !is AutoDiscoveryResult.UnexpectedException) {
|
||||
return fakeResult
|
||||
}
|
||||
|
||||
val result = service.discover(email)
|
||||
|
||||
return if (result is AutoDiscoveryResult.Settings) {
|
||||
validateOAuthSupport(result)
|
||||
} else {
|
||||
null
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
if (fakeResult != null) {
|
||||
return provideWithDelay(fakeResult)
|
||||
private fun validateOAuthSupport(settings: AutoDiscoveryResult.Settings): AutoDiscoveryResult {
|
||||
if (settings.incomingServerSettings !is ImapServerSettings) {
|
||||
return AutoDiscoveryResult.NoUsableSettingsFound
|
||||
}
|
||||
|
||||
return service.discover(emailAddress.toUserEmailAddress())
|
||||
}
|
||||
val incomingServerSettings = settings.incomingServerSettings as ImapServerSettings
|
||||
val outgoingServerSettings = settings.outgoingServerSettings as SmtpServerSettings
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
private suspend fun provideWithDelay(autoDiscoveryResult: AutoDiscoveryResult): AutoDiscoveryResult {
|
||||
delay(Random(0).nextLong(500, 2000))
|
||||
return autoDiscoveryResult
|
||||
}
|
||||
val incomingAuthenticationType = updateAuthenticationType(
|
||||
authenticationType = incomingServerSettings.authenticationType,
|
||||
hostname = incomingServerSettings.hostname.value,
|
||||
)
|
||||
val outgoingAuthenticationType = updateAuthenticationType(
|
||||
authenticationType = outgoingServerSettings.authenticationType,
|
||||
hostname = outgoingServerSettings.hostname.value,
|
||||
)
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
private fun getFakeAutoDiscovery(emailAddress: String): AutoDiscoveryResult.Settings {
|
||||
val hasIncomingOauth = emailAddress.contains("in")
|
||||
val hasOutgoingOauth = emailAddress.contains("out")
|
||||
val isTrusted = emailAddress.contains("trust")
|
||||
|
||||
return AutoDiscoveryResult.Settings(
|
||||
incomingServerSettings = ImapServerSettings(
|
||||
hostname = "imap.${getHost(emailAddress)}".toHostname(),
|
||||
port = 993.toPort(),
|
||||
connectionSecurity = ConnectionSecurity.TLS,
|
||||
authenticationType = if (hasIncomingOauth) {
|
||||
AuthenticationType.OAuth2
|
||||
} else {
|
||||
AuthenticationType.PasswordEncrypted
|
||||
},
|
||||
username = "username",
|
||||
return settings.copy(
|
||||
incomingServerSettings = incomingServerSettings.copy(
|
||||
authenticationType = incomingAuthenticationType,
|
||||
),
|
||||
outgoingServerSettings = SmtpServerSettings(
|
||||
hostname = "smtp.${getHost(emailAddress)}".toHostname(),
|
||||
port = 993.toPort(),
|
||||
connectionSecurity = ConnectionSecurity.TLS,
|
||||
authenticationType = if (hasOutgoingOauth) {
|
||||
AuthenticationType.OAuth2
|
||||
} else {
|
||||
AuthenticationType.PasswordEncrypted
|
||||
},
|
||||
username = "username",
|
||||
outgoingServerSettings = outgoingServerSettings.copy(
|
||||
authenticationType = outgoingAuthenticationType,
|
||||
),
|
||||
isTrusted = isTrusted,
|
||||
source = "fake",
|
||||
)
|
||||
}
|
||||
|
||||
private fun getHost(emailAddress: String) = emailAddress.split("@").last()
|
||||
private fun updateAuthenticationType(
|
||||
authenticationType: AuthenticationType,
|
||||
hostname: String,
|
||||
): AuthenticationType {
|
||||
return if (authenticationType == AuthenticationType.OAuth2 && !isOAuthSupportedFor(hostname)) {
|
||||
// OAuth2 is not supported for this hostname, downgrade to password cleartext
|
||||
// TODO replace with next supported authentication type, once populated by autodiscovery
|
||||
AuthenticationType.PasswordCleartext
|
||||
} else {
|
||||
authenticationType
|
||||
}
|
||||
}
|
||||
|
||||
private fun isOAuthSupportedFor(hostname: String): Boolean {
|
||||
return oauthProvider.getConfiguration(hostname) != null
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package app.k9mail.feature.account.setup
|
||||
|
||||
import app.k9mail.core.common.oauth.OAuthConfigurationFactory
|
||||
import app.k9mail.feature.account.setup.AccountSetupExternalContract.AccountCreator
|
||||
import app.k9mail.feature.account.setup.AccountSetupExternalContract.AccountCreator.AccountCreatorResult
|
||||
import app.k9mail.feature.account.setup.ui.AccountSetupContract
|
||||
|
@ -34,6 +35,7 @@ class AccountSetupModuleKtTest : KoinTest {
|
|||
single<AccountCreator> {
|
||||
AccountCreator { _ -> AccountCreatorResult.Success("accountUuid") }
|
||||
}
|
||||
single<OAuthConfigurationFactory> { OAuthConfigurationFactory { emptyMap() } }
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
@ -0,0 +1,189 @@
|
|||
package app.k9mail.feature.account.setup.domain.usecase
|
||||
|
||||
import app.k9mail.autodiscovery.api.AuthenticationType
|
||||
import app.k9mail.autodiscovery.api.AutoDiscoveryResult
|
||||
import app.k9mail.autodiscovery.api.AutoDiscoveryService
|
||||
import app.k9mail.autodiscovery.api.ConnectionSecurity
|
||||
import app.k9mail.autodiscovery.api.ImapServerSettings
|
||||
import app.k9mail.autodiscovery.api.IncomingServerSettings
|
||||
import app.k9mail.autodiscovery.api.SmtpServerSettings
|
||||
import app.k9mail.core.common.mail.EmailAddress
|
||||
import app.k9mail.core.common.net.toHostname
|
||||
import app.k9mail.core.common.net.toPort
|
||||
import app.k9mail.core.common.oauth.OAuthConfiguration
|
||||
import app.k9mail.core.common.oauth.OAuthConfigurationProvider
|
||||
import assertk.assertThat
|
||||
import assertk.assertions.isEqualTo
|
||||
import assertk.assertions.isInstanceOf
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
class GetAutoDiscoveryTest {
|
||||
|
||||
@Test
|
||||
fun `should return a valid result`() = runTest {
|
||||
val useCase = GetAutoDiscovery(
|
||||
service = FakeAutoDiscoveryService(SETTINGS_WITH_PASSWORD),
|
||||
oauthProvider = FakeOAuthConfigurationProvider(OAUTH_CONFIGURATION),
|
||||
)
|
||||
|
||||
val result = useCase.execute("user@example.com")
|
||||
|
||||
assertThat(result)
|
||||
.isInstanceOf(AutoDiscoveryResult.Settings::class)
|
||||
.isEqualTo(SETTINGS_WITH_PASSWORD)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should return NoUsableSettingsFound result`() = runTest {
|
||||
val useCase = GetAutoDiscovery(
|
||||
service = FakeAutoDiscoveryService(AutoDiscoveryResult.NoUsableSettingsFound),
|
||||
oauthProvider = FakeOAuthConfigurationProvider(),
|
||||
)
|
||||
|
||||
val result = useCase.execute("user@example.com")
|
||||
|
||||
assertThat(result)
|
||||
.isInstanceOf(AutoDiscoveryResult.NoUsableSettingsFound::class)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should return NoUsableSettingsFound result when server settings not supported`() = runTest {
|
||||
val useCase = GetAutoDiscovery(
|
||||
service = FakeAutoDiscoveryService(SETTINGS_WITH_UNSUPPORTED_SERVER),
|
||||
oauthProvider = FakeOAuthConfigurationProvider(),
|
||||
)
|
||||
|
||||
val result = useCase.execute("user@example.com")
|
||||
|
||||
assertThat(result)
|
||||
.isInstanceOf(AutoDiscoveryResult.NoUsableSettingsFound::class)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should return UnexpectedException result`() = runTest {
|
||||
val autoDiscoveryResult = AutoDiscoveryResult.UnexpectedException(Exception("unexpected exception"))
|
||||
val useCase = GetAutoDiscovery(
|
||||
service = FakeAutoDiscoveryService(autoDiscoveryResult),
|
||||
oauthProvider = FakeOAuthConfigurationProvider(),
|
||||
)
|
||||
|
||||
val result = useCase.execute("user@example.com")
|
||||
|
||||
assertThat(result)
|
||||
.isInstanceOf(AutoDiscoveryResult.UnexpectedException::class)
|
||||
.isEqualTo(autoDiscoveryResult)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should check for oauth support and return when supported`() = runTest {
|
||||
val useCase = GetAutoDiscovery(
|
||||
service = FakeAutoDiscoveryService(SETTINGS_WITH_OAUTH),
|
||||
oauthProvider = FakeOAuthConfigurationProvider(OAUTH_CONFIGURATION),
|
||||
)
|
||||
|
||||
val result = useCase.execute("user@example.com")
|
||||
|
||||
assertThat(result)
|
||||
.isInstanceOf(AutoDiscoveryResult.Settings::class)
|
||||
.isEqualTo(SETTINGS_WITH_OAUTH)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should check for oauth support and change auth type to password when not supported`() = runTest {
|
||||
val useCase = GetAutoDiscovery(
|
||||
service = FakeAutoDiscoveryService(SETTINGS_WITH_OAUTH),
|
||||
oauthProvider = FakeOAuthConfigurationProvider(),
|
||||
)
|
||||
|
||||
val result = useCase.execute("user@example.com")
|
||||
|
||||
assertThat(result)
|
||||
.isInstanceOf(AutoDiscoveryResult.Settings::class)
|
||||
.isEqualTo(
|
||||
SETTINGS_WITH_OAUTH.copy(
|
||||
incomingServerSettings = (SETTINGS_WITH_OAUTH.incomingServerSettings as ImapServerSettings).copy(
|
||||
authenticationType = AuthenticationType.PasswordCleartext,
|
||||
),
|
||||
outgoingServerSettings = (SETTINGS_WITH_OAUTH.outgoingServerSettings as SmtpServerSettings).copy(
|
||||
authenticationType = AuthenticationType.PasswordCleartext,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private class FakeAutoDiscoveryService(
|
||||
private val answer: AutoDiscoveryResult = AutoDiscoveryResult.NoUsableSettingsFound,
|
||||
) : AutoDiscoveryService {
|
||||
override suspend fun discover(email: EmailAddress): AutoDiscoveryResult = answer
|
||||
}
|
||||
|
||||
private class FakeOAuthConfigurationProvider(
|
||||
private val answer: OAuthConfiguration? = null,
|
||||
) : OAuthConfigurationProvider {
|
||||
override fun getConfiguration(hostname: String): OAuthConfiguration? = answer
|
||||
}
|
||||
|
||||
private class UnsupportedServerSettings : IncomingServerSettings
|
||||
|
||||
private companion object {
|
||||
private val SETTINGS_WITH_OAUTH = AutoDiscoveryResult.Settings(
|
||||
incomingServerSettings = ImapServerSettings(
|
||||
hostname = "imap.example.com".toHostname(),
|
||||
port = 993.toPort(),
|
||||
connectionSecurity = ConnectionSecurity.TLS,
|
||||
authenticationType = AuthenticationType.OAuth2,
|
||||
username = "user",
|
||||
),
|
||||
outgoingServerSettings = SmtpServerSettings(
|
||||
hostname = "smtp.example.com".toHostname(),
|
||||
port = 465.toPort(),
|
||||
connectionSecurity = ConnectionSecurity.TLS,
|
||||
authenticationType = AuthenticationType.OAuth2,
|
||||
username = "user",
|
||||
),
|
||||
isTrusted = true,
|
||||
source = "source",
|
||||
)
|
||||
|
||||
private val SETTINGS_WITH_UNSUPPORTED_SERVER = AutoDiscoveryResult.Settings(
|
||||
incomingServerSettings = UnsupportedServerSettings(),
|
||||
outgoingServerSettings = SmtpServerSettings(
|
||||
hostname = "smtp.example.com".toHostname(),
|
||||
port = 465.toPort(),
|
||||
connectionSecurity = ConnectionSecurity.TLS,
|
||||
authenticationType = AuthenticationType.OAuth2,
|
||||
username = "user",
|
||||
),
|
||||
isTrusted = true,
|
||||
source = "source",
|
||||
)
|
||||
|
||||
private val SETTINGS_WITH_PASSWORD = AutoDiscoveryResult.Settings(
|
||||
incomingServerSettings = ImapServerSettings(
|
||||
hostname = "imap.example.com".toHostname(),
|
||||
port = 993.toPort(),
|
||||
connectionSecurity = ConnectionSecurity.TLS,
|
||||
authenticationType = AuthenticationType.PasswordCleartext,
|
||||
username = "user",
|
||||
),
|
||||
outgoingServerSettings = SmtpServerSettings(
|
||||
hostname = "smtp.example.com".toHostname(),
|
||||
port = 465.toPort(),
|
||||
connectionSecurity = ConnectionSecurity.TLS,
|
||||
authenticationType = AuthenticationType.PasswordCleartext,
|
||||
username = "user",
|
||||
),
|
||||
isTrusted = true,
|
||||
source = "source",
|
||||
)
|
||||
|
||||
private val OAUTH_CONFIGURATION = OAuthConfiguration(
|
||||
clientId = "clientId",
|
||||
scopes = listOf("scopes"),
|
||||
authorizationEndpoint = "authorizationEndpoint",
|
||||
tokenEndpoint = "tokenEndpoint",
|
||||
redirectUri = "redirectUri",
|
||||
)
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue