diff --git a/app/k9mail/src/main/java/com/fsck/k9/backends/KoinModule.kt b/app/k9mail/src/main/java/com/fsck/k9/backends/KoinModule.kt index 36099da2e..99a976f83 100644 --- a/app/k9mail/src/main/java/com/fsck/k9/backends/KoinModule.kt +++ b/app/k9mail/src/main/java/com/fsck/k9/backends/KoinModule.kt @@ -6,6 +6,7 @@ import com.fsck.k9.BuildConfig import com.fsck.k9.backend.BackendManager import com.fsck.k9.backend.imap.BackendIdleRefreshManager import com.fsck.k9.backend.imap.SystemAlarmManager +import com.fsck.k9.mail.oauth.OAuth2TokenProviderFactory import com.fsck.k9.mail.store.imap.IdleRefreshManager import org.koin.core.qualifier.named import org.koin.dsl.module @@ -34,6 +35,7 @@ val backendsModule = module { single { BackendIdleRefreshManager(alarmManager = get()) } single { Pop3BackendFactory(get(), get()) } single(named("ClientIdAppName")) { BuildConfig.CLIENT_ID_APP_NAME } + single { RealOAuth2TokenProviderFactory(context = get()) } developmentModuleAdditions() } diff --git a/app/k9mail/src/main/java/com/fsck/k9/backends/RealOAuth2TokenProviderFactory.kt b/app/k9mail/src/main/java/com/fsck/k9/backends/RealOAuth2TokenProviderFactory.kt new file mode 100644 index 000000000..58401172d --- /dev/null +++ b/app/k9mail/src/main/java/com/fsck/k9/backends/RealOAuth2TokenProviderFactory.kt @@ -0,0 +1,14 @@ +package com.fsck.k9.backends + +import android.content.Context +import com.fsck.k9.mail.oauth.AuthStateStorage +import com.fsck.k9.mail.oauth.OAuth2TokenProvider +import com.fsck.k9.mail.oauth.OAuth2TokenProviderFactory + +class RealOAuth2TokenProviderFactory( + private val context: Context, +) : OAuth2TokenProviderFactory { + override fun create(authStateStorage: AuthStateStorage): OAuth2TokenProvider { + return RealOAuth2TokenProvider(context, authStateStorage) + } +} diff --git a/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/AccountSetupModule.kt b/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/AccountSetupModule.kt index c3beee349..fd67b0127 100644 --- a/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/AccountSetupModule.kt +++ b/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/AccountSetupModule.kt @@ -55,7 +55,7 @@ val featureAccountSetupModule: Module = module { ValidateServerSettings( imapValidator = ImapServerSettingsValidator( trustedSocketFactory = get(), - oAuth2TokenProvider = null, // TODO + oAuth2TokenProviderFactory = get(), clientIdAppName = "null", ), pop3Validator = Pop3ServerSettingsValidator( @@ -63,7 +63,7 @@ val featureAccountSetupModule: Module = module { ), smtpValidator = SmtpServerSettingsValidator( trustedSocketFactory = get(), - oAuth2TokenProvider = null, // TODO + oAuth2TokenProviderFactory = get(), ), ) } diff --git a/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/domain/usecase/ValidateServerSettings.kt b/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/domain/usecase/ValidateServerSettings.kt index 1bcf7ef7f..d802e8270 100644 --- a/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/domain/usecase/ValidateServerSettings.kt +++ b/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/domain/usecase/ValidateServerSettings.kt @@ -17,9 +17,9 @@ internal class ValidateServerSettings( override suspend fun execute(settings: ServerSettings): ServerSettingsValidationResult = withContext(coroutineDispatcher) { return@withContext when (settings.type) { - "imap" -> imapValidator.checkServerSettings(settings) - "pop3" -> pop3Validator.checkServerSettings(settings) - "smtp" -> smtpValidator.checkServerSettings(settings) + "imap" -> imapValidator.checkServerSettings(settings, authStateStorage = null) + "pop3" -> pop3Validator.checkServerSettings(settings, authStateStorage = null) + "smtp" -> smtpValidator.checkServerSettings(settings, authStateStorage = null) else -> { throw IllegalArgumentException("Unsupported server type: ${settings.type}") } diff --git a/feature/account/setup/src/test/kotlin/app/k9mail/feature/account/setup/AccountSetupModuleKtTest.kt b/feature/account/setup/src/test/kotlin/app/k9mail/feature/account/setup/AccountSetupModuleKtTest.kt index 09487d977..0287162e6 100644 --- a/feature/account/setup/src/test/kotlin/app/k9mail/feature/account/setup/AccountSetupModuleKtTest.kt +++ b/feature/account/setup/src/test/kotlin/app/k9mail/feature/account/setup/AccountSetupModuleKtTest.kt @@ -9,6 +9,8 @@ import app.k9mail.feature.account.setup.ui.incoming.AccountIncomingConfigContrac import app.k9mail.feature.account.setup.ui.options.AccountOptionsContract import app.k9mail.feature.account.setup.ui.outgoing.AccountOutgoingConfigContract import app.k9mail.feature.account.setup.ui.validation.AccountValidationContract +import com.fsck.k9.mail.oauth.OAuth2TokenProvider +import com.fsck.k9.mail.oauth.OAuth2TokenProviderFactory import com.fsck.k9.mail.ssl.TrustedSocketFactory import okhttp3.OkHttpClient import org.junit.Test @@ -37,6 +39,14 @@ class AccountSetupModuleKtTest : KoinTest { AccountCreator { _ -> AccountCreatorResult.Success("accountUuid") } } single { OAuthConfigurationFactory { emptyMap() } } + single { + OAuth2TokenProviderFactory { _ -> + object : OAuth2TokenProvider { + override fun getToken(timeoutMillis: Long) = TODO() + override fun invalidateToken() = TODO() + } + } + } } @Test diff --git a/feature/account/setup/src/test/kotlin/app/k9mail/feature/account/setup/domain/usecase/ValidateServerSettingsTest.kt b/feature/account/setup/src/test/kotlin/app/k9mail/feature/account/setup/domain/usecase/ValidateServerSettingsTest.kt index c5b888eb5..0311bbc05 100644 --- a/feature/account/setup/src/test/kotlin/app/k9mail/feature/account/setup/domain/usecase/ValidateServerSettingsTest.kt +++ b/feature/account/setup/src/test/kotlin/app/k9mail/feature/account/setup/domain/usecase/ValidateServerSettingsTest.kt @@ -15,9 +15,9 @@ class ValidateServerSettingsTest { @Test fun `should check with imap validator when protocol is imap`() = runTest { val testSubject = ValidateServerSettings( - imapValidator = { ServerSettingsValidationResult.Success }, - pop3Validator = { ServerSettingsValidationResult.NetworkError(IOException("Failed POP3")) }, - smtpValidator = { ServerSettingsValidationResult.NetworkError(IOException("Failed SMTP")) }, + imapValidator = { _, _ -> ServerSettingsValidationResult.Success }, + pop3Validator = { _, _ -> ServerSettingsValidationResult.NetworkError(IOException("Failed POP3")) }, + smtpValidator = { _, _ -> ServerSettingsValidationResult.NetworkError(IOException("Failed SMTP")) }, ) val result = testSubject.execute(IMAP_SERVER_SETTINGS) @@ -29,9 +29,9 @@ class ValidateServerSettingsTest { fun `should check with imap validator when protocol is imap and return failure`() = runTest { val failure = ServerSettingsValidationResult.ServerError("Failed") val testSubject = ValidateServerSettings( - imapValidator = { failure }, - pop3Validator = { ServerSettingsValidationResult.NetworkError(IOException("Failed POP3")) }, - smtpValidator = { ServerSettingsValidationResult.NetworkError(IOException("Failed SMTP")) }, + imapValidator = { _, _ -> failure }, + pop3Validator = { _, _ -> ServerSettingsValidationResult.NetworkError(IOException("Failed POP3")) }, + smtpValidator = { _, _ -> ServerSettingsValidationResult.NetworkError(IOException("Failed SMTP")) }, ) val result = testSubject.execute(IMAP_SERVER_SETTINGS) @@ -42,9 +42,9 @@ class ValidateServerSettingsTest { @Test fun `should check with pop3 validator when protocol is pop3`() = runTest { val testSubject = ValidateServerSettings( - imapValidator = { ServerSettingsValidationResult.NetworkError(IOException("Failed IMAP")) }, - pop3Validator = { ServerSettingsValidationResult.Success }, - smtpValidator = { ServerSettingsValidationResult.NetworkError(IOException("Failed SMTP")) }, + imapValidator = { _, _ -> ServerSettingsValidationResult.NetworkError(IOException("Failed IMAP")) }, + pop3Validator = { _, _ -> ServerSettingsValidationResult.Success }, + smtpValidator = { _, _ -> ServerSettingsValidationResult.NetworkError(IOException("Failed SMTP")) }, ) val result = testSubject.execute(POP3_SERVER_SETTINGS) @@ -56,9 +56,9 @@ class ValidateServerSettingsTest { fun `should check with pop3 validator when protocol is pop3 and return failure`() = runTest { val failure = ServerSettingsValidationResult.ServerError("Failed POP3") val testSubject = ValidateServerSettings( - imapValidator = { ServerSettingsValidationResult.NetworkError(IOException("Failed IMAP")) }, - pop3Validator = { failure }, - smtpValidator = { ServerSettingsValidationResult.NetworkError(IOException("Failed SMTP")) }, + imapValidator = { _, _ -> ServerSettingsValidationResult.NetworkError(IOException("Failed IMAP")) }, + pop3Validator = { _, _ -> failure }, + smtpValidator = { _, _ -> ServerSettingsValidationResult.NetworkError(IOException("Failed SMTP")) }, ) val result = testSubject.execute(POP3_SERVER_SETTINGS) @@ -69,9 +69,9 @@ class ValidateServerSettingsTest { @Test fun `should check with smtp validator when protocol is smtp`() = runTest { val testSubject = ValidateServerSettings( - imapValidator = { ServerSettingsValidationResult.NetworkError(IOException("Failed IMAP")) }, - pop3Validator = { ServerSettingsValidationResult.NetworkError(IOException("Failed POP3")) }, - smtpValidator = { ServerSettingsValidationResult.Success }, + imapValidator = { _, _ -> ServerSettingsValidationResult.NetworkError(IOException("Failed IMAP")) }, + pop3Validator = { _, _ -> ServerSettingsValidationResult.NetworkError(IOException("Failed POP3")) }, + smtpValidator = { _, _ -> ServerSettingsValidationResult.Success }, ) val result = testSubject.execute(SMTP_SERVER_SETTINGS) @@ -83,9 +83,9 @@ class ValidateServerSettingsTest { fun `should check with smtp validator when protocol is smtp and return failure`() = runTest { val failure = ServerSettingsValidationResult.ServerError("Failed SMTP") val testSubject = ValidateServerSettings( - imapValidator = { ServerSettingsValidationResult.NetworkError(IOException("Failed IMAP")) }, - pop3Validator = { ServerSettingsValidationResult.NetworkError(IOException("Failed POP3")) }, - smtpValidator = { failure }, + imapValidator = { _, _ -> ServerSettingsValidationResult.NetworkError(IOException("Failed IMAP")) }, + pop3Validator = { _, _ -> ServerSettingsValidationResult.NetworkError(IOException("Failed POP3")) }, + smtpValidator = { _, _ -> failure }, ) val result = testSubject.execute(SMTP_SERVER_SETTINGS) diff --git a/mail/common/src/main/java/com/fsck/k9/mail/oauth/OAuth2TokenProviderFactory.kt b/mail/common/src/main/java/com/fsck/k9/mail/oauth/OAuth2TokenProviderFactory.kt new file mode 100644 index 000000000..b7a8a8844 --- /dev/null +++ b/mail/common/src/main/java/com/fsck/k9/mail/oauth/OAuth2TokenProviderFactory.kt @@ -0,0 +1,9 @@ +package com.fsck.k9.mail.oauth + +/** + * Creates an instance of [OAuth2TokenProvider] that uses a given [AuthStateStorage] to retrieve and store the + * (implementation-specific) authorization state. + */ +fun interface OAuth2TokenProviderFactory { + fun create(authStateStorage: AuthStateStorage): OAuth2TokenProvider +} diff --git a/mail/common/src/main/java/com/fsck/k9/mail/server/ServerSettingsValidator.kt b/mail/common/src/main/java/com/fsck/k9/mail/server/ServerSettingsValidator.kt index b4b3673fe..fc40167c2 100644 --- a/mail/common/src/main/java/com/fsck/k9/mail/server/ServerSettingsValidator.kt +++ b/mail/common/src/main/java/com/fsck/k9/mail/server/ServerSettingsValidator.kt @@ -1,10 +1,14 @@ package com.fsck.k9.mail.server import com.fsck.k9.mail.ServerSettings +import com.fsck.k9.mail.oauth.AuthStateStorage /** * Validate [ServerSettings] by trying to connect to the server and log in. */ fun interface ServerSettingsValidator { - fun checkServerSettings(serverSettings: ServerSettings): ServerSettingsValidationResult + fun checkServerSettings( + serverSettings: ServerSettings, + authStateStorage: AuthStateStorage?, + ): ServerSettingsValidationResult } diff --git a/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/ImapServerSettingsValidator.kt b/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/ImapServerSettingsValidator.kt index 6d33d7e7e..71be1ff19 100644 --- a/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/ImapServerSettingsValidator.kt +++ b/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/ImapServerSettingsValidator.kt @@ -4,7 +4,9 @@ import com.fsck.k9.mail.AuthenticationFailedException import com.fsck.k9.mail.CertificateValidationException import com.fsck.k9.mail.MessagingException import com.fsck.k9.mail.ServerSettings +import com.fsck.k9.mail.oauth.AuthStateStorage import com.fsck.k9.mail.oauth.OAuth2TokenProvider +import com.fsck.k9.mail.oauth.OAuth2TokenProviderFactory import com.fsck.k9.mail.server.ServerSettingsValidationResult import com.fsck.k9.mail.server.ServerSettingsValidator import com.fsck.k9.mail.ssl.TrustedSocketFactory @@ -12,17 +14,21 @@ import java.io.IOException class ImapServerSettingsValidator( private val trustedSocketFactory: TrustedSocketFactory, - private val oAuth2TokenProvider: OAuth2TokenProvider?, + private val oAuth2TokenProviderFactory: OAuth2TokenProviderFactory?, private val clientIdAppName: String, ) : ServerSettingsValidator { @Suppress("TooGenericExceptionCaught") - override fun checkServerSettings(serverSettings: ServerSettings): ServerSettingsValidationResult { + override fun checkServerSettings( + serverSettings: ServerSettings, + authStateStorage: AuthStateStorage?, + ): ServerSettingsValidationResult { val config = object : ImapStoreConfig { override val logLabel = "check" override fun isSubscribedFoldersOnly() = false override fun clientIdAppName() = clientIdAppName } + val oAuth2TokenProvider = createOAuth2TokenProviderOrNull(authStateStorage) val store = RealImapStore(serverSettings, config, trustedSocketFactory, oAuth2TokenProvider) return try { @@ -48,4 +54,10 @@ class ImapServerSettingsValidator( ServerSettingsValidationResult.UnknownError(e) } } + + private fun createOAuth2TokenProviderOrNull(authStateStorage: AuthStateStorage?): OAuth2TokenProvider? { + return authStateStorage?.let { + oAuth2TokenProviderFactory?.create(it) + } + } } diff --git a/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/ImapServerSettingsValidatorTest.kt b/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/ImapServerSettingsValidatorTest.kt index 404f7c7b3..eafe2e42f 100644 --- a/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/ImapServerSettingsValidatorTest.kt +++ b/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/ImapServerSettingsValidatorTest.kt @@ -11,6 +11,8 @@ import com.fsck.k9.mail.ConnectionSecurity import com.fsck.k9.mail.ServerSettings import com.fsck.k9.mail.helpers.FakeTrustManager import com.fsck.k9.mail.helpers.SimpleTrustedSocketFactory +import com.fsck.k9.mail.oauth.AuthStateStorage +import com.fsck.k9.mail.oauth.OAuth2TokenProvider import com.fsck.k9.mail.server.ServerSettingsValidationResult import com.fsck.k9.mail.store.imap.mockserver.MockImapServer import java.net.UnknownHostException @@ -18,19 +20,22 @@ import kotlin.test.Test private const val USERNAME = "user" private const val PASSWORD = "password" +private const val AUTHORIZATION_STATE = "auth state" +private const val AUTHORIZATION_TOKEN = "auth-token" private val CLIENT_CERTIFICATE_ALIAS: String? = null private const val CLIENT_ID = "clientId" class ImapServerSettingsValidatorTest { private val fakeTrustManager = FakeTrustManager() + private val trustedSocketFactory = SimpleTrustedSocketFactory(fakeTrustManager) private val serverSettingsValidator = ImapServerSettingsValidator( - trustedSocketFactory = SimpleTrustedSocketFactory(fakeTrustManager), - oAuth2TokenProvider = null, + trustedSocketFactory = trustedSocketFactory, + oAuth2TokenProviderFactory = null, clientIdAppName = CLIENT_ID, ) @Test - fun `valid server settings should return Success`() { + fun `valid server settings with password should return Success`() { val server = startServer { output("* OK IMAP4rev1 server ready") expect("1 CAPABILITY") @@ -64,7 +69,56 @@ class ImapServerSettingsValidatorTest { ), ) - val result = serverSettingsValidator.checkServerSettings(serverSettings) + val result = serverSettingsValidator.checkServerSettings(serverSettings, authStateStorage = null) + + assertThat(result).isInstanceOf() + server.verifyConnectionClosed() + server.verifyInteractionCompleted() + } + + @Test + fun `valid server settings with OAuth should return Success`() { + val serverSettingsValidator = ImapServerSettingsValidator( + trustedSocketFactory = trustedSocketFactory, + oAuth2TokenProviderFactory = { authStateStorage -> + assertThat(authStateStorage.getAuthorizationState()).isEqualTo(AUTHORIZATION_STATE) + FakeOAuth2TokenProvider() + }, + clientIdAppName = CLIENT_ID, + ) + val server = startServer { + output("* OK IMAP4rev1 server ready") + expect("1 CAPABILITY") + output("* CAPABILITY IMAP4rev1 SASL-IR AUTH=PLAIN AUTH=OAUTHBEARER") + output("1 OK CAPABILITY Completed") + expect("2 AUTHENTICATE OAUTHBEARER bixhPXVzZXIsAWF1dGg9QmVhcmVyIGF1dGgtdG9rZW4BAQ==") + output("2 OK [CAPABILITY IMAP4rev1 SASL-IR AUTH=PLAIN AUTH=OAUTHBEARER NAMESPACE ID] LOGIN completed") + expect("3 ID (\"name\" \"$CLIENT_ID\")") + output("* ID NIL") + output("3 OK ID completed") + expect("4 NAMESPACE") + output("* NAMESPACE ((\"\" \"/\")) NIL NIL") + output("4 OK command completed") + } + val serverSettings = ServerSettings( + type = "imap", + host = server.host, + port = server.port, + connectionSecurity = ConnectionSecurity.NONE, + authenticationType = AuthType.XOAUTH2, + username = USERNAME, + password = PASSWORD, + clientCertificateAlias = CLIENT_CERTIFICATE_ALIAS, + extra = ImapStoreSettings.createExtra( + autoDetectNamespace = true, + pathPrefix = null, + useCompression = false, + sendClientId = true, + ), + ) + val authStateStorage = FakeAuthStateStorage(authorizationState = AUTHORIZATION_STATE) + + val result = serverSettingsValidator.checkServerSettings(serverSettings, authStateStorage) assertThat(result).isInstanceOf() server.verifyConnectionClosed() @@ -93,7 +147,7 @@ class ImapServerSettingsValidatorTest { clientCertificateAlias = CLIENT_CERTIFICATE_ALIAS, ) - val result = serverSettingsValidator.checkServerSettings(serverSettings) + val result = serverSettingsValidator.checkServerSettings(serverSettings, authStateStorage = null) assertThat(result).isInstanceOf() .prop(ServerSettingsValidationResult.AuthenticationError::serverMessage).isEqualTo("Authentication failed") @@ -120,7 +174,7 @@ class ImapServerSettingsValidatorTest { clientCertificateAlias = CLIENT_CERTIFICATE_ALIAS, ) - val result = serverSettingsValidator.checkServerSettings(serverSettings) + val result = serverSettingsValidator.checkServerSettings(serverSettings, authStateStorage = null) assertThat(result).isInstanceOf() server.verifyConnectionClosed() @@ -150,7 +204,7 @@ class ImapServerSettingsValidatorTest { clientCertificateAlias = CLIENT_CERTIFICATE_ALIAS, ) - val result = serverSettingsValidator.checkServerSettings(serverSettings) + val result = serverSettingsValidator.checkServerSettings(serverSettings, authStateStorage = null) assertThat(result).isInstanceOf() .prop(ServerSettingsValidationResult.CertificateError::certificateChain).hasSize(1) @@ -171,7 +225,7 @@ class ImapServerSettingsValidatorTest { clientCertificateAlias = CLIENT_CERTIFICATE_ALIAS, ) - val result = serverSettingsValidator.checkServerSettings(serverSettings) + val result = serverSettingsValidator.checkServerSettings(serverSettings, authStateStorage = null) assertThat(result).isInstanceOf() .prop(ServerSettingsValidationResult.NetworkError::exception) @@ -192,7 +246,7 @@ class ImapServerSettingsValidatorTest { ) assertFailure { - serverSettingsValidator.checkServerSettings(serverSettings) + serverSettingsValidator.checkServerSettings(serverSettings, authStateStorage = null) }.isInstanceOf() } @@ -203,3 +257,25 @@ class ImapServerSettingsValidatorTest { } } } + +class FakeOAuth2TokenProvider : OAuth2TokenProvider { + override fun getToken(timeoutMillis: Long): String { + return AUTHORIZATION_TOKEN + } + + override fun invalidateToken() { + throw UnsupportedOperationException("not implemented") + } +} + +class FakeAuthStateStorage( + private var authorizationState: String? = null, +) : AuthStateStorage { + override fun getAuthorizationState(): String? { + return authorizationState + } + + override fun updateAuthorizationState(authorizationState: String?) { + this.authorizationState = authorizationState + } +} diff --git a/mail/protocols/pop3/src/main/java/com/fsck/k9/mail/store/pop3/Pop3ServerSettingsValidator.kt b/mail/protocols/pop3/src/main/java/com/fsck/k9/mail/store/pop3/Pop3ServerSettingsValidator.kt index 6d6152b00..4dae41e92 100644 --- a/mail/protocols/pop3/src/main/java/com/fsck/k9/mail/store/pop3/Pop3ServerSettingsValidator.kt +++ b/mail/protocols/pop3/src/main/java/com/fsck/k9/mail/store/pop3/Pop3ServerSettingsValidator.kt @@ -4,6 +4,7 @@ import com.fsck.k9.mail.AuthenticationFailedException import com.fsck.k9.mail.CertificateValidationException import com.fsck.k9.mail.MessagingException import com.fsck.k9.mail.ServerSettings +import com.fsck.k9.mail.oauth.AuthStateStorage import com.fsck.k9.mail.server.ServerSettingsValidationResult import com.fsck.k9.mail.server.ServerSettingsValidator import com.fsck.k9.mail.ssl.TrustedSocketFactory @@ -14,7 +15,10 @@ class Pop3ServerSettingsValidator( ) : ServerSettingsValidator { @Suppress("TooGenericExceptionCaught") - override fun checkServerSettings(serverSettings: ServerSettings): ServerSettingsValidationResult { + override fun checkServerSettings( + serverSettings: ServerSettings, + authStateStorage: AuthStateStorage?, + ): ServerSettingsValidationResult { val store = Pop3Store(serverSettings, trustedSocketFactory) return try { diff --git a/mail/protocols/pop3/src/test/java/com/fsck/k9/mail/store/pop3/Pop3ServerSettingsValidatorTest.kt b/mail/protocols/pop3/src/test/java/com/fsck/k9/mail/store/pop3/Pop3ServerSettingsValidatorTest.kt index bf66b7da6..3200c4ee1 100644 --- a/mail/protocols/pop3/src/test/java/com/fsck/k9/mail/store/pop3/Pop3ServerSettingsValidatorTest.kt +++ b/mail/protocols/pop3/src/test/java/com/fsck/k9/mail/store/pop3/Pop3ServerSettingsValidatorTest.kt @@ -54,7 +54,7 @@ class Pop3ServerSettingsValidatorTest { clientCertificateAlias = CLIENT_CERTIFICATE_ALIAS, ) - val result = serverSettingsValidator.checkServerSettings(serverSettings) + val result = serverSettingsValidator.checkServerSettings(serverSettings, authStateStorage = null) assertThat(result).isInstanceOf() server.verifyConnectionClosed() @@ -90,7 +90,7 @@ class Pop3ServerSettingsValidatorTest { clientCertificateAlias = CLIENT_CERTIFICATE_ALIAS, ) - val result = serverSettingsValidator.checkServerSettings(serverSettings) + val result = serverSettingsValidator.checkServerSettings(serverSettings, authStateStorage = null) assertThat(result).isInstanceOf() .prop(ServerSettingsValidationResult.AuthenticationError::serverMessage) @@ -116,7 +116,7 @@ class Pop3ServerSettingsValidatorTest { clientCertificateAlias = CLIENT_CERTIFICATE_ALIAS, ) - val result = serverSettingsValidator.checkServerSettings(serverSettings) + val result = serverSettingsValidator.checkServerSettings(serverSettings, authStateStorage = null) assertThat(result).isInstanceOf() .prop(ServerSettingsValidationResult.ServerError::serverMessage) @@ -151,7 +151,7 @@ class Pop3ServerSettingsValidatorTest { clientCertificateAlias = CLIENT_CERTIFICATE_ALIAS, ) - val result = serverSettingsValidator.checkServerSettings(serverSettings) + val result = serverSettingsValidator.checkServerSettings(serverSettings, authStateStorage = null) assertThat(result).isInstanceOf() .prop(ServerSettingsValidationResult.CertificateError::certificateChain).hasSize(1) @@ -172,7 +172,7 @@ class Pop3ServerSettingsValidatorTest { clientCertificateAlias = CLIENT_CERTIFICATE_ALIAS, ) - val result = serverSettingsValidator.checkServerSettings(serverSettings) + val result = serverSettingsValidator.checkServerSettings(serverSettings, authStateStorage = null) assertThat(result).isInstanceOf() .prop(ServerSettingsValidationResult.NetworkError::exception) @@ -193,7 +193,7 @@ class Pop3ServerSettingsValidatorTest { ) assertFailure { - serverSettingsValidator.checkServerSettings(serverSettings) + serverSettingsValidator.checkServerSettings(serverSettings, authStateStorage = null) }.isInstanceOf() } diff --git a/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/SmtpServerSettingsValidator.kt b/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/SmtpServerSettingsValidator.kt index f50b3f96a..79d4f8c63 100644 --- a/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/SmtpServerSettingsValidator.kt +++ b/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/SmtpServerSettingsValidator.kt @@ -4,7 +4,9 @@ import com.fsck.k9.mail.AuthenticationFailedException import com.fsck.k9.mail.CertificateValidationException import com.fsck.k9.mail.MessagingException import com.fsck.k9.mail.ServerSettings +import com.fsck.k9.mail.oauth.AuthStateStorage import com.fsck.k9.mail.oauth.OAuth2TokenProvider +import com.fsck.k9.mail.oauth.OAuth2TokenProviderFactory import com.fsck.k9.mail.server.ServerSettingsValidationResult import com.fsck.k9.mail.server.ServerSettingsValidator import com.fsck.k9.mail.ssl.TrustedSocketFactory @@ -12,11 +14,15 @@ import java.io.IOException class SmtpServerSettingsValidator( private val trustedSocketFactory: TrustedSocketFactory, - private val oAuth2TokenProvider: OAuth2TokenProvider?, + private val oAuth2TokenProviderFactory: OAuth2TokenProviderFactory?, ) : ServerSettingsValidator { @Suppress("TooGenericExceptionCaught") - override fun checkServerSettings(serverSettings: ServerSettings): ServerSettingsValidationResult { + override fun checkServerSettings( + serverSettings: ServerSettings, + authStateStorage: AuthStateStorage?, + ): ServerSettingsValidationResult { + val oAuth2TokenProvider = createOAuth2TokenProviderOrNull(authStateStorage) val smtpTransport = SmtpTransport(serverSettings, trustedSocketFactory, oAuth2TokenProvider) return try { @@ -42,4 +48,10 @@ class SmtpServerSettingsValidator( ServerSettingsValidationResult.UnknownError(e) } } + + private fun createOAuth2TokenProviderOrNull(authStateStorage: AuthStateStorage?): OAuth2TokenProvider? { + return authStateStorage?.let { + oAuth2TokenProviderFactory?.create(it) + } + } } diff --git a/mail/protocols/smtp/src/test/java/com/fsck/k9/mail/transport/smtp/SmtpServerSettingsValidatorTest.kt b/mail/protocols/smtp/src/test/java/com/fsck/k9/mail/transport/smtp/SmtpServerSettingsValidatorTest.kt index 67a5039c5..e73357d8a 100644 --- a/mail/protocols/smtp/src/test/java/com/fsck/k9/mail/transport/smtp/SmtpServerSettingsValidatorTest.kt +++ b/mail/protocols/smtp/src/test/java/com/fsck/k9/mail/transport/smtp/SmtpServerSettingsValidatorTest.kt @@ -11,6 +11,8 @@ import com.fsck.k9.mail.ConnectionSecurity import com.fsck.k9.mail.ServerSettings import com.fsck.k9.mail.helpers.FakeTrustManager import com.fsck.k9.mail.helpers.SimpleTrustedSocketFactory +import com.fsck.k9.mail.oauth.AuthStateStorage +import com.fsck.k9.mail.oauth.OAuth2TokenProvider import com.fsck.k9.mail.server.ServerSettingsValidationResult import com.fsck.k9.mail.transport.mockServer.MockSmtpServer import java.net.UnknownHostException @@ -18,17 +20,20 @@ import kotlin.test.Test private const val USERNAME = "user" private const val PASSWORD = "password" +private const val AUTHORIZATION_STATE = "auth state" +private const val AUTHORIZATION_TOKEN = "auth-token" private val CLIENT_CERTIFICATE_ALIAS: String? = null class SmtpServerSettingsValidatorTest { private val fakeTrustManager = FakeTrustManager() + private val trustedSocketFactory = SimpleTrustedSocketFactory(fakeTrustManager) private val serverSettingsValidator = SmtpServerSettingsValidator( - trustedSocketFactory = SimpleTrustedSocketFactory(fakeTrustManager), - oAuth2TokenProvider = null, + trustedSocketFactory = trustedSocketFactory, + oAuth2TokenProviderFactory = null, ) @Test - fun `valid server settings should return Success`() { + fun `valid server settings with password should return Success`() { val server = MockSmtpServer().apply { output("220 localhost Simple Mail Transfer Service Ready") expect("EHLO [127.0.0.1]") @@ -53,7 +58,48 @@ class SmtpServerSettingsValidatorTest { clientCertificateAlias = CLIENT_CERTIFICATE_ALIAS, ) - val result = serverSettingsValidator.checkServerSettings(serverSettings) + val result = serverSettingsValidator.checkServerSettings(serverSettings, authStateStorage = null) + + assertThat(result).isInstanceOf() + server.verifyConnectionClosed() + server.verifyInteractionCompleted() + } + + @Test + fun `valid server settings with OAuth should return Success`() { + val serverSettingsValidator = SmtpServerSettingsValidator( + trustedSocketFactory = trustedSocketFactory, + oAuth2TokenProviderFactory = { authStateStorage -> + assertThat(authStateStorage.getAuthorizationState()).isEqualTo(AUTHORIZATION_STATE) + FakeOAuth2TokenProvider() + }, + ) + val server = MockSmtpServer().apply { + output("220 localhost Simple Mail Transfer Service Ready") + expect("EHLO [127.0.0.1]") + output("250-localhost Hello client.localhost") + output("250-ENHANCEDSTATUSCODES") + output("250-AUTH PLAIN LOGIN OAUTHBEARER") + output("250 HELP") + expect("AUTH OAUTHBEARER bixhPXVzZXIsAWF1dGg9QmVhcmVyIGF1dGgtdG9rZW4BAQ==") + output("235 2.7.0 Authentication successful") + expect("QUIT") + closeConnection() + } + server.start() + val serverSettings = ServerSettings( + type = "smtp", + host = server.host, + port = server.port, + connectionSecurity = ConnectionSecurity.NONE, + authenticationType = AuthType.XOAUTH2, + username = USERNAME, + password = null, + clientCertificateAlias = CLIENT_CERTIFICATE_ALIAS, + ) + val authStateStorage = FakeAuthStateStorage(authorizationState = AUTHORIZATION_STATE) + + val result = serverSettingsValidator.checkServerSettings(serverSettings, authStateStorage) assertThat(result).isInstanceOf() server.verifyConnectionClosed() @@ -86,7 +132,7 @@ class SmtpServerSettingsValidatorTest { clientCertificateAlias = CLIENT_CERTIFICATE_ALIAS, ) - val result = serverSettingsValidator.checkServerSettings(serverSettings) + val result = serverSettingsValidator.checkServerSettings(serverSettings, authStateStorage = null) assertThat(result).isInstanceOf() .prop(ServerSettingsValidationResult.AuthenticationError::serverMessage).isEqualTo("Authentication failed") @@ -112,7 +158,7 @@ class SmtpServerSettingsValidatorTest { clientCertificateAlias = CLIENT_CERTIFICATE_ALIAS, ) - val result = serverSettingsValidator.checkServerSettings(serverSettings) + val result = serverSettingsValidator.checkServerSettings(serverSettings, authStateStorage = null) assertThat(result).isInstanceOf() server.verifyConnectionClosed() @@ -144,7 +190,7 @@ class SmtpServerSettingsValidatorTest { clientCertificateAlias = CLIENT_CERTIFICATE_ALIAS, ) - val result = serverSettingsValidator.checkServerSettings(serverSettings) + val result = serverSettingsValidator.checkServerSettings(serverSettings, authStateStorage = null) assertThat(result).isInstanceOf() .prop(ServerSettingsValidationResult.CertificateError::certificateChain).hasSize(1) @@ -165,7 +211,7 @@ class SmtpServerSettingsValidatorTest { clientCertificateAlias = CLIENT_CERTIFICATE_ALIAS, ) - val result = serverSettingsValidator.checkServerSettings(serverSettings) + val result = serverSettingsValidator.checkServerSettings(serverSettings, authStateStorage = null) assertThat(result).isInstanceOf() .prop(ServerSettingsValidationResult.NetworkError::exception) @@ -186,7 +232,29 @@ class SmtpServerSettingsValidatorTest { ) assertFailure { - serverSettingsValidator.checkServerSettings(serverSettings) + serverSettingsValidator.checkServerSettings(serverSettings, authStateStorage = null) }.isInstanceOf() } } + +class FakeOAuth2TokenProvider : OAuth2TokenProvider { + override fun getToken(timeoutMillis: Long): String { + return AUTHORIZATION_TOKEN + } + + override fun invalidateToken() { + throw UnsupportedOperationException("not implemented") + } +} + +class FakeAuthStateStorage( + private var authorizationState: String? = null, +) : AuthStateStorage { + override fun getAuthorizationState(): String? { + return authorizationState + } + + override fun updateAuthorizationState(authorizationState: String?) { + this.authorizationState = authorizationState + } +}