diff --git a/app/src/main/java/eu/faircode/email/ActivitySetup.java b/app/src/main/java/eu/faircode/email/ActivitySetup.java index aaa6a664ed..4108e2c69c 100644 --- a/app/src/main/java/eu/faircode/email/ActivitySetup.java +++ b/app/src/main/java/eu/faircode/email/ActivitySetup.java @@ -19,8 +19,6 @@ package eu.faircode.email; Copyright 2018-2020 by Marcel Bokhorst (M66B) */ -import android.accounts.Account; -import android.accounts.AccountManager; import android.app.Dialog; import android.app.NotificationChannel; import android.app.NotificationChannelGroup; @@ -103,7 +101,6 @@ import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.PBEKeySpec; import static eu.faircode.email.ServiceAuthenticator.AUTH_TYPE_GMAIL; -import static eu.faircode.email.ServiceAuthenticator.TYPE_GOOGLE; public class ActivitySetup extends ActivityBase implements FragmentManager.OnBackStackChangedListener { private View view; @@ -777,16 +774,8 @@ public class ActivitySetup extends ActivityBase implements FragmentManager.OnBac EntityAccount account = EntityAccount.fromJSON(jaccount); if (account.auth_type == AUTH_TYPE_GMAIL) { - AccountManager am = AccountManager.get(context); - boolean found = false; - for (Account google : am.getAccountsByType(TYPE_GOOGLE)) - if (account.user.equals(google.name)) { - found = true; - break; - } - - if (!found) { - Log.i("Google account not found email=" + account.user); + if (GmailState.getAccount(context, account.user) == null) { + Log.i("Google account not found user=" + account.user); continue; } } diff --git a/app/src/main/java/eu/faircode/email/EmailService.java b/app/src/main/java/eu/faircode/email/EmailService.java index 221242579c..6b34ce7e3c 100644 --- a/app/src/main/java/eu/faircode/email/EmailService.java +++ b/app/src/main/java/eu/faircode/email/EmailService.java @@ -364,10 +364,9 @@ public class EmailService implements AutoCloseable { connect(host, port, auth, user, authenticator, factory); } catch (AuthenticationFailedException ex) { - // Refresh token if (auth == AUTH_TYPE_GMAIL || auth == AUTH_TYPE_OAUTH) { try { - authenticator.expire(); + authenticator.refreshToken(true); connect(host, port, auth, user, authenticator, factory); } catch (Exception ex1) { Log.e(ex1); diff --git a/app/src/main/java/eu/faircode/email/FragmentAccount.java b/app/src/main/java/eu/faircode/email/FragmentAccount.java index 8f7567c407..05292c87cd 100644 --- a/app/src/main/java/eu/faircode/email/FragmentAccount.java +++ b/app/src/main/java/eu/faircode/email/FragmentAccount.java @@ -1334,7 +1334,9 @@ public class FragmentAccount extends FragmentBase { if (account == null) return null; - return ServiceAuthenticator.getGmailToken(context, account.user); + GmailState state = GmailState.jsonDeserialize(account.password); + state.refresh(context, account.user, true); + return state.jsonSerializeString(); } @Override diff --git a/app/src/main/java/eu/faircode/email/FragmentGmail.java b/app/src/main/java/eu/faircode/email/FragmentGmail.java index 874d2d121e..fba50c42f6 100644 --- a/app/src/main/java/eu/faircode/email/FragmentGmail.java +++ b/app/src/main/java/eu/faircode/email/FragmentGmail.java @@ -58,6 +58,7 @@ import java.util.Map; import static android.accounts.AccountManager.newChooseAccountIntent; import static android.app.Activity.RESULT_OK; +import static eu.faircode.email.GmailState.TYPE_GOOGLE; import static eu.faircode.email.ServiceAuthenticator.AUTH_TYPE_GMAIL; public class FragmentGmail extends FragmentBase { @@ -75,8 +76,6 @@ public class FragmentGmail extends FragmentBase { private Group grpError; - private static String TYPE_GOOGLE = "com.google"; - @Override @Nullable public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { @@ -318,11 +317,13 @@ public class FragmentGmail extends FragmentBase { } } - private void onAuthorized(String user, String password) { + private void onAuthorized(String user, String token) { + GmailState state = GmailState.jsonDeserialize(token); + Bundle args = new Bundle(); args.putString("name", etName.getText().toString().trim()); args.putString("user", user); - args.putString("password", password); + args.putString("password", state.jsonSerializeString()); new SimpleTask() { @Override diff --git a/app/src/main/java/eu/faircode/email/FragmentIdentity.java b/app/src/main/java/eu/faircode/email/FragmentIdentity.java index 766e43a417..9a8df2aa2c 100644 --- a/app/src/main/java/eu/faircode/email/FragmentIdentity.java +++ b/app/src/main/java/eu/faircode/email/FragmentIdentity.java @@ -987,7 +987,9 @@ public class FragmentIdentity extends FragmentBase { if (identity == null) return null; - return ServiceAuthenticator.getGmailToken(context, identity.user); + GmailState state = GmailState.jsonDeserialize(identity.password); + state.refresh(context, identity.user, true); + return state.jsonSerializeString(); } @Override diff --git a/app/src/main/java/eu/faircode/email/GmailState.java b/app/src/main/java/eu/faircode/email/GmailState.java new file mode 100644 index 0000000000..5178e729e4 --- /dev/null +++ b/app/src/main/java/eu/faircode/email/GmailState.java @@ -0,0 +1,118 @@ +package eu.faircode.email; + +/* + This file is part of FairEmail. + + FairEmail 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. + + FairEmail 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 FairEmail. If not, see . + + Copyright 2018-2020 by Marcel Bokhorst (M66B) +*/ + +import android.accounts.Account; +import android.accounts.AccountManager; +import android.accounts.AuthenticatorException; +import android.accounts.OperationCanceledException; +import android.content.Context; + +import androidx.annotation.NonNull; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.IOException; +import java.util.Date; +import java.util.Objects; + +public class GmailState { + private String token; + private long acquired; + + static final String TYPE_GOOGLE = "com.google"; + private static final long TOKEN_LIFETIME = 3600 * 1000L; // milliseconds + + private GmailState(String token, long acquired) { + this.token = token; + this.acquired = acquired; + } + + @NonNull + String getAccessToken() throws AuthenticatorException { + if (token == null) + throw new AuthenticatorException("no token"); + return token; + } + + void refresh(@NonNull Context context, @NonNull String user, boolean expired) throws AuthenticatorException, OperationCanceledException, IOException { + if (expired || acquired + TOKEN_LIFETIME < new Date().getTime()) + try { + if (token != null) { + EntityLog.log(context, "Invalidating token user=" + user); + AccountManager am = AccountManager.get(context); + am.invalidateAuthToken(TYPE_GOOGLE, token); + } + token = null; + acquired = 0; + } catch (Throwable ex) { + Log.e(ex); + } + + Account account = getAccount(context, user); + if (account == null) + throw new AuthenticatorException("Account not found for " + user); + + EntityLog.log(context, "Getting token user=" + user); + AccountManager am = AccountManager.get(context); + String newToken = am.blockingGetAuthToken(account, ServiceAuthenticator.getAuthTokenType(TYPE_GOOGLE), true); + + if (newToken != null && !newToken.equals(token)) { + token = newToken; + acquired = new Date().getTime(); + } + + if (token == null) + throw new AuthenticatorException("No token for " + user); + } + + static Account getAccount(Context context, String user) { + AccountManager am = AccountManager.get(context); + Account[] accounts = am.getAccountsByType(TYPE_GOOGLE); + for (Account account : accounts) + if (Objects.equals(account.name, user)) + return account; + return null; + } + + public String jsonSerializeString() { + try { + JSONObject jobject = new JSONObject(); + jobject.put("token", token); + jobject.put("acquired", acquired); + return jobject.toString(); + } catch (JSONException ex) { + Log.e(ex); + return null; + } + } + + static GmailState jsonDeserialize(@NonNull String password) { + try { + JSONObject jobject = new JSONObject(password); + String token = jobject.getString("token"); + long acquired = jobject.getLong("acquired"); + return new GmailState(token, acquired); + } catch (JSONException ex) { + return new GmailState(password, new Date().getTime()); + } + } +} diff --git a/app/src/main/java/eu/faircode/email/ServiceAuthenticator.java b/app/src/main/java/eu/faircode/email/ServiceAuthenticator.java index 7c354543c0..b3c5cf2df1 100644 --- a/app/src/main/java/eu/faircode/email/ServiceAuthenticator.java +++ b/app/src/main/java/eu/faircode/email/ServiceAuthenticator.java @@ -19,8 +19,6 @@ package eu.faircode.email; Copyright 2018-2020 by Marcel Bokhorst (M66B) */ -import android.accounts.Account; -import android.accounts.AccountManager; import android.accounts.AuthenticatorException; import android.accounts.OperationCanceledException; import android.content.Context; @@ -32,8 +30,9 @@ import net.openid.appauth.ClientAuthentication; import net.openid.appauth.ClientSecretPost; import net.openid.appauth.NoClientAuthentication; +import org.json.JSONException; + import java.io.IOException; -import java.util.Date; import java.util.Objects; import java.util.concurrent.Semaphore; @@ -41,22 +40,20 @@ import javax.mail.Authenticator; import javax.mail.MessagingException; import javax.mail.PasswordAuthentication; -class ServiceAuthenticator extends Authenticator { +import static eu.faircode.email.GmailState.TYPE_GOOGLE; + +public class ServiceAuthenticator extends Authenticator { private Context context; private int auth; private String provider; private String user; private String password; private IAuthenticated intf; - private long refreshed; static final int AUTH_TYPE_PASSWORD = 1; static final int AUTH_TYPE_GMAIL = 2; static final int AUTH_TYPE_OAUTH = 3; - static final String TYPE_GOOGLE = "com.google"; - private static final long GMAIL_EXPIRY = 3600 * 1000L; - ServiceAuthenticator( Context context, int auth, String provider, @@ -68,76 +65,52 @@ class ServiceAuthenticator extends Authenticator { this.user = user; this.password = password; this.intf = intf; - this.refreshed = new Date().getTime(); - } - - void expire() { - if (auth == AUTH_TYPE_GMAIL) { - EntityLog.log(context, user + " token expired"); - expireGmailToken(context, password); - password = null; - } } @Override protected PasswordAuthentication getPasswordAuthentication() { String token = password; try { - if (auth == AUTH_TYPE_GMAIL) { - long now = new Date().getTime(); - if (now - refreshed > GMAIL_EXPIRY) - expire(); - - String oldToken = password; - token = getGmailToken(context, user); - password = token; - - if (intf != null && !Objects.equals(oldToken, token)) - intf.onPasswordChanged(password); - } else if (auth == AUTH_TYPE_OAUTH) { - AuthState authState = AuthState.jsonDeserialize(password); - String oldToken = authState.getAccessToken(); - OAuthRefresh(context, provider, authState); - token = authState.getAccessToken(); - password = authState.jsonSerializeString(); - - if (intf != null && !Objects.equals(oldToken, token)) - intf.onPasswordChanged(password); - } + token = refreshToken(false); } catch (Throwable ex) { Log.e(ex); } + Log.i(user + " returning password"); return new PasswordAuthentication(user, token); } - interface IAuthenticated { - void onPasswordChanged(String newPassword); - } + String refreshToken(boolean expired) throws AuthenticatorException, OperationCanceledException, IOException, JSONException, MessagingException { + if (auth == AUTH_TYPE_GMAIL) { + GmailState gmailState = GmailState.jsonDeserialize(password); + gmailState.refresh(context, user, expired); + + String newPassword = gmailState.jsonSerializeString(); + if (!Objects.equals(password, newPassword)) { + password = newPassword; + if (intf != null) + intf.onPasswordChanged(password); + } + + return gmailState.getAccessToken(); + } else if (auth == AUTH_TYPE_OAUTH) { + AuthState authState = AuthState.jsonDeserialize(password); + OAuthRefresh(context, provider, authState); - static String getGmailToken(Context context, String user) throws AuthenticatorException, OperationCanceledException, IOException { - AccountManager am = AccountManager.get(context); - Account[] accounts = am.getAccountsByType(TYPE_GOOGLE); - for (Account account : accounts) - if (user.equals(account.name)) { - Log.i("Getting token user=" + user); - String token = am.blockingGetAuthToken(account, getAuthTokenType(TYPE_GOOGLE), true); - if (token == null) - throw new AuthenticatorException("No token for " + user); - - return token; + String newPassword = authState.jsonSerializeString(); + if (!Objects.equals(password, newPassword)) { + password = newPassword; + if (intf != null) + intf.onPasswordChanged(password); } - throw new AuthenticatorException("Account not found for " + user); + return authState.getAccessToken(); + } else + return password; } - private static void expireGmailToken(Context context, String token) { - try { - AccountManager am = AccountManager.get(context); - am.invalidateAuthToken(TYPE_GOOGLE, token); - } catch (Throwable ex) { - Log.e(ex); - } + interface IAuthenticated { + void onPasswordChanged(String newPassword); } private static void OAuthRefresh(Context context, String id, AuthState authState) throws MessagingException { @@ -178,7 +151,7 @@ class ServiceAuthenticator extends Authenticator { static String getAuthTokenType(String type) { // https://developers.google.com/gmail/imap/xoauth2-protocol - if ("com.google".equals(type)) + if (TYPE_GOOGLE.equals(type)) return "oauth2:https://mail.google.com/"; return null; }