diff --git a/app/core/src/main/java/com/fsck/k9/controller/MessagingController.java b/app/core/src/main/java/com/fsck/k9/controller/MessagingController.java index 3b9eac12980de87215529c0ef61b245aa915a7a9..e654e6d06628534aa51bb56f69f617e54e7fc8d0 100644 --- a/app/core/src/main/java/com/fsck/k9/controller/MessagingController.java +++ b/app/core/src/main/java/com/fsck/k9/controller/MessagingController.java @@ -84,6 +84,7 @@ import com.fsck.k9.notification.NotificationController; import com.fsck.k9.notification.NotificationStrategy; import com.fsck.k9.search.LocalSearch; import com.fsck.k9.search.SearchAccount; +import com.fsck.k9.setup.EeloAccountHelper; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import timber.log.Timber; @@ -2570,11 +2571,15 @@ public class MessagingController { } private boolean isAuthenticationProblem(Account account, boolean incoming) { - ServerSettings serverSettings = incoming ? - account.getIncomingServerSettings() : account.getOutgoingServerSettings(); + ServerSettings serverSettings = incoming ? account.getIncomingServerSettings() : account.getOutgoingServerSettings(); - return serverSettings.isMissingCredentials() || - serverSettings.authenticationType == AuthType.XOAUTH2 && account.getOAuthState() == null; + if (serverSettings.isMissingCredentials()) { + Timber.w("account authenticationProblem detected, missing credentials"); + return true; + } + + EeloAccountHelper.INSTANCE.updateOAuthStateIfMissing(context, preferences, account); + return serverSettings.authenticationType == AuthType.XOAUTH2 && account.getOAuthState() == null; } private void actOnMessagesGroupedByAccountAndFolder(List messages, MessageActor actor) { diff --git a/app/core/src/main/java/com/fsck/k9/job/MailSyncWorker.kt b/app/core/src/main/java/com/fsck/k9/job/MailSyncWorker.kt index 32a8bea1a5b881fd8e26dc6c14a3cab627482383..b9f01eb33699c144dc8f6d179853616d202de8ce 100644 --- a/app/core/src/main/java/com/fsck/k9/job/MailSyncWorker.kt +++ b/app/core/src/main/java/com/fsck/k9/job/MailSyncWorker.kt @@ -9,12 +9,13 @@ import com.fsck.k9.K9 import com.fsck.k9.Preferences import com.fsck.k9.controller.MessagingController import com.fsck.k9.mail.AuthType +import com.fsck.k9.setup.EeloAccountHelper import timber.log.Timber class MailSyncWorker( private val messagingController: MessagingController, private val preferences: Preferences, - context: Context, + private val context: Context, parameters: WorkerParameters ) : Worker(context, parameters) { @@ -45,6 +46,7 @@ class MailSyncWorker( return Result.success() } + EeloAccountHelper.updateOAuthStateIfMissing(context, preferences, account) if (account.incomingServerSettings.authenticationType == AuthType.XOAUTH2 && account.oAuthState == null) { Timber.d("Account requires sign-in. Skipping mail sync.") return Result.success() diff --git a/app/core/src/main/java/com/fsck/k9/setup/AccountManagerConstants.kt b/app/core/src/main/java/com/fsck/k9/setup/AccountManagerConstants.kt new file mode 100644 index 0000000000000000000000000000000000000000..1beb26693ffd2787f5391b9f6766a0e3f46be96f --- /dev/null +++ b/app/core/src/main/java/com/fsck/k9/setup/AccountManagerConstants.kt @@ -0,0 +1,26 @@ +/* + * Copyright ECORP SAS 2022 + * 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 . + */ + +package com.fsck.k9.setup + +object AccountManagerConstants { + const val EELO_ACCOUNT_TYPE = "e.foundation.webdav.eelo" + const val GOOGLE_ACCOUNT_TYPE = "e.foundation.webdav.google" + const val ACCOUNT_EMAIL_ADDRESS_KEY = "email_address" + const val MAIL_CONTENT_AUTHORITY = "foundation.e.mail.provider.AppContentProvider" + const val AUTH_TOKEN_TYPE = "oauth2-access-token" + const val KEY_AUTH_STATE = "auth_state" +} diff --git a/app/core/src/main/java/com/fsck/k9/setup/EeloAccountHelper.kt b/app/core/src/main/java/com/fsck/k9/setup/EeloAccountHelper.kt new file mode 100644 index 0000000000000000000000000000000000000000..2af9c115524afdc12987eaa2d262c8b127217583 --- /dev/null +++ b/app/core/src/main/java/com/fsck/k9/setup/EeloAccountHelper.kt @@ -0,0 +1,93 @@ +/* + * Copyright ECORP SAS 2022 + * 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 . + */ + +package com.fsck.k9.setup + +import android.content.Context +import com.fsck.k9.Account +import com.fsck.k9.mail.AuthType +import com.fsck.k9.preferences.AccountManager +import android.accounts.AccountManager as OsAccountManager +import android.accounts.Account as OsAccount +import timber.log.Timber + +object EeloAccountHelper { + + /** + * to support backward-compatibility. + * In previous versions google accounts loaded from accountManager doesn't store oAuthState in the db. + * This method check if the oAuthState is missing or not, if missing, update the oAuthState + */ + fun updateOAuthStateIfMissing(context: Context, accountManager: AccountManager, account: Account?) { + // check params + if (account == null) { + Timber.w("updating OAuthState failed, account is null") + return + } + + // validation + if (account.incomingServerSettings.authenticationType != AuthType.XOAUTH2 || account.oAuthState != null) { + Timber.w("updating oAuthState failed, not oauth2 authType") + return + } + + val osAccountManager = OsAccountManager.get(context) + val googleAccount = retrieveGoogleAccountFromAccountManager(osAccountManager, account.email) ?: return + account.oAuthState = osAccountManager.getUserData(googleAccount, AccountManagerConstants.KEY_AUTH_STATE) + if (account.oAuthState != null) { + accountManager.saveAccount(account) + } + } + + // If token is updated by mail, also update the accountManager + fun updateAccountInAccountManager(context: Context?, account: OsAccount?, authState: String?, accessToken: String?) { + if (context == null || account == null || authState == null || accessToken == null) { + Timber.w("updating account for accountManager failed, invalid param.") + return + } + + val accountManager = OsAccountManager.get(context) + accountManager.setAuthToken(account, AccountManagerConstants.AUTH_TOKEN_TYPE, accessToken) + accountManager.setUserData(account, AccountManagerConstants.KEY_AUTH_STATE, authState) + } + + fun retrieveGoogleAccountFromAccountManager(context: Context?, email: String?): OsAccount? { + if (context == null) { + Timber.w("retrieve google accounts from accountManager failed, null context.") + return null + } + + val accountManager = OsAccountManager.get(context) + return retrieveGoogleAccountFromAccountManager(accountManager, email) + } + + private fun retrieveGoogleAccountFromAccountManager(accountManager: OsAccountManager?, email: String?): OsAccount? { + if (accountManager == null || email == null || email.isEmpty()) { + Timber.w("retrieve google account from accountManager failed, invalid param") + return null + } + + val googleAccounts = accountManager.getAccountsByType(AccountManagerConstants.GOOGLE_ACCOUNT_TYPE) + for (googleAccount in googleAccounts) { + val emailId = accountManager.getUserData(googleAccount, AccountManagerConstants.ACCOUNT_EMAIL_ADDRESS_KEY) + if (email.equals(emailId, ignoreCase = true)) { + return googleAccount + } + } + + return null + } +} \ No newline at end of file diff --git a/app/k9mail/src/main/java/com/fsck/k9/backends/RealOAuth2TokenProvider.kt b/app/k9mail/src/main/java/com/fsck/k9/backends/RealOAuth2TokenProvider.kt index 94d0afc5c940bc1e91bb3ac9053cea1a5f798ad0..ca6fd4e9a02549a2413f2707d5da06ab14270108 100644 --- a/app/k9mail/src/main/java/com/fsck/k9/backends/RealOAuth2TokenProvider.kt +++ b/app/k9mail/src/main/java/com/fsck/k9/backends/RealOAuth2TokenProvider.kt @@ -1,13 +1,11 @@ package com.fsck.k9.backends import android.content.Context -import android.os.Bundle -import android.os.Handler -import android.os.HandlerThread import com.fsck.k9.Account import com.fsck.k9.mail.AuthenticationFailedException import com.fsck.k9.mail.oauth.OAuth2TokenProvider import com.fsck.k9.preferences.AccountManager +import com.fsck.k9.setup.EeloAccountHelper import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit import net.openid.appauth.AuthState @@ -15,29 +13,17 @@ import net.openid.appauth.AuthorizationException import net.openid.appauth.AuthorizationService class RealOAuth2TokenProvider( - context: Context, + private val context: Context, private val accountManager: AccountManager, private val account: Account ) : OAuth2TokenProvider { - companion object { - const val GOOGLE_ACCOUNT_TYPE = "e.foundation.webdav.google" - const val EELO_ACCOUNT_TYPE = "e.foundation.webdav.eelo" - const val ACCOUNT_EMAIL_ADDRESS_KEY = "email_address" - const val AUTH_TOKEN_TYPE = "oauth2-access-token" - } - private val authService = AuthorizationService(context) private var requestFreshToken = false - private val osAccountManager by lazy { - android.accounts.AccountManager.get(context) - } - override fun getToken(email: String, timeoutMillis: Long): String { - getTokenFromAccountManager(email)?.let { - return it - } + val accountManagerAccount = EeloAccountHelper.retrieveGoogleAccountFromAccountManager(context, email) + EeloAccountHelper.updateOAuthStateIfMissing(context, accountManager, account) val latch = CountDownLatch(1) var token: String? = null @@ -68,6 +54,12 @@ class RealOAuth2TokenProvider( requestFreshToken = false account.oAuthState = authState.jsonSerializeString() accountManager.saveAccount(account) + EeloAccountHelper.updateAccountInAccountManager( + context, + accountManagerAccount, + authState.jsonSerializeString(), + authState.accessToken + ) } exception?.let { authException -> @@ -84,40 +76,4 @@ class RealOAuth2TokenProvider( override fun invalidateToken() { requestFreshToken = true } - - private fun getTokenFromAccountManager(emailId: String): String? { - - var authToken: String? = null - val handlerThread = HandlerThread("callbackThread") - - handlerThread.start() - val handler = Handler(handlerThread.looper) - - val accounts: ArrayList = arrayListOf() - for (account in osAccountManager.getAccountsByType(GOOGLE_ACCOUNT_TYPE)) { - accounts.add(account) - } - - for (account in osAccountManager.getAccountsByType(EELO_ACCOUNT_TYPE)) { - accounts.add(account) - } - - for (account in accounts) { - val accountEmailId = osAccountManager.getUserData(account, ACCOUNT_EMAIL_ADDRESS_KEY) - if (emailId == accountEmailId) { - val latch = CountDownLatch(1) - osAccountManager.getAuthToken( - account, AUTH_TOKEN_TYPE, Bundle(), false, - { future -> - authToken = future?.result?.getString(android.accounts.AccountManager.KEY_AUTHTOKEN) - latch.countDown() - }, - handler - ) - latch.await() - } - } - - return authToken - } } diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/accountmanager/AccountManagerConstants.kt b/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/accountmanager/AccountManagerConstants.kt deleted file mode 100644 index a958a5e269eeecf24c1f6cd543b637f7e92f77d2..0000000000000000000000000000000000000000 --- a/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/accountmanager/AccountManagerConstants.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.fsck.k9.activity.setup.accountmanager - -object AccountManagerConstants { - const val EELO_ACCOUNT_TYPE = "e.foundation.webdav.eelo" - const val GOOGLE_ACCOUNT_TYPE = "e.foundation.webdav.google" - const val ACCOUNT_EMAIL_ADDRESS_KEY = "email_address" - const val MAIL_CONTENT_AUTHORITY = "foundation.e.mail.provider.AppContentProvider" -} diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/accountmanager/EeloAccountCreator.java b/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/accountmanager/EeloAccountCreator.java index 07571c3ef35df359b3591c33beb86c9ec712bbf4..780f92a17d84d7c756c3f8eec840c28579ad3f96 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/accountmanager/EeloAccountCreator.java +++ b/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/accountmanager/EeloAccountCreator.java @@ -43,6 +43,7 @@ import com.fsck.k9.mail.ConnectionSecurity; import com.fsck.k9.mail.ServerSettings; import com.fsck.k9.mailstore.SpecialLocalFoldersCreator; import com.fsck.k9.preferences.Protocols; +import com.fsck.k9.setup.AccountManagerConstants; import com.fsck.k9.ui.ConnectionSettings; import timber.log.Timber; @@ -89,7 +90,8 @@ public class EeloAccountCreator { .peek(account -> updateAccountNameIfMissing(context, emailId, account)) .findAny().isPresent(); if (!accountAlreadyPresent) { - createAccount(context, emailId, "", true); + String authState = accountManager.getUserData(googleAccount, AccountManagerConstants.KEY_AUTH_STATE); + createAccount(context, emailId, "", authState); } } } @@ -121,7 +123,7 @@ public class EeloAccountCreator { .noneMatch(account -> emailId.equalsIgnoreCase(account.getEmail())); if (isNewAccount) { String password = accountManager.getPassword(eeloAccount); - createAccount(context, emailId, password, false); + createAccount(context, emailId, password, null); } } } @@ -132,7 +134,7 @@ public class EeloAccountCreator { .forEach(account -> accountRemover.removeAccountAsync(account.getUuid())); } - private static void createAccount(Context context, String emailId, String password, boolean isGoogleAccount) { + private static void createAccount(Context context, String emailId, String password, @Nullable String authState) { Preferences preferences = Preferences.getPreferences(context); Account account = preferences.newAccount(); @@ -146,9 +148,9 @@ public class EeloAccountCreator { connectionSettings = MailAutoConfigDiscovery.retrieveConfigFromApi(emailId); } // providers.xml doesn't have the connection details & can't retrieve details from api - // & it is google account, meaning custom domain for google account is used. + // & it is google account (authState should not be null), meaning custom domain for google account is used. // In this case, provide default gmail configuration. - if (connectionSettings == null && isGoogleAccount) { + if (connectionSettings == null && authState != null) { connectionSettings = providersDefaultGoogleAccountDiscover(emailId); } if (connectionSettings == null) { @@ -160,6 +162,8 @@ public class EeloAccountCreator { ServerSettings outgoingSettings = connectionSettings.getOutgoing().newPassword(password); account.setOutgoingServerSettings(outgoingSettings); + account.setOAuthState(authState); + DeletePolicy deletePolicy = accountCreator.getDefaultDeletePolicy(incomingSettings.type); account.setDeletePolicy(deletePolicy);