Loading k9mail/src/main/AndroidManifest.xml +4 −0 Original line number Diff line number Diff line Loading @@ -16,6 +16,10 @@ <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/> <uses-permission android:name="android.permission.READ_CONTACTS"/> <uses-permission android:name="android.permission.READ_SYNC_SETTINGS"/> <uses-permission android:name="android.permission.MANAGE_ACCOUNTS" /> <uses-permission android:name="android.permission.GET_ACCOUNTS" /> <uses-permission android:name="android.permission.USE_CREDENTIALS" /> <!-- Needed to mark a contact as contacted --> <uses-permission android:name="android.permission.WRITE_CONTACTS"/> Loading k9mail/src/main/java/com/fsck/k9/Account.java +2 −1 Original line number Diff line number Diff line Loading @@ -21,6 +21,7 @@ import android.graphics.Color; import android.net.Uri; import android.util.Log; import com.fsck.k9.account.AndroidAccountOAuth2TokenStore; import com.fsck.k9.activity.setup.AccountSetupCheckSettings.CheckDirection; import com.fsck.k9.helper.Utility; import com.fsck.k9.mail.Address; Loading Loading @@ -1287,7 +1288,7 @@ public class Account implements BaseAccount, StoreConfig { } public Store getRemoteStore() throws MessagingException { return RemoteStore.getInstance(K9.app, this); return RemoteStore.getInstance(K9.app, this, Globals.getOAuth2TokenProvider()); } // It'd be great if this actually went into the store implementation Loading k9mail/src/main/java/com/fsck/k9/Globals.java +15 −0 Original line number Diff line number Diff line Loading @@ -4,14 +4,21 @@ package com.fsck.k9; import android.content.Context; import android.support.annotation.VisibleForTesting; import com.fsck.k9.mail.oauth.OAuth2TokenProvider; public class Globals { private static Context context; private static OAuth2TokenProvider oAuth2TokenProvider; static void setContext(Context context) { Globals.context = context; } static void setOAuth2TokenProvider(OAuth2TokenProvider oAuth2TokenProvider) { Globals.oAuth2TokenProvider = oAuth2TokenProvider; } public static Context getContext() { if (context == null) { throw new IllegalStateException("No context provided"); Loading @@ -19,4 +26,12 @@ public class Globals { return context; } public static OAuth2TokenProvider getOAuth2TokenProvider() { if (oAuth2TokenProvider == null) { throw new IllegalStateException("No OAuth 2.0 Token Provider provided"); } return oAuth2TokenProvider; } } k9mail/src/main/java/com/fsck/k9/K9.java +2 −0 Original line number Diff line number Diff line Loading @@ -26,6 +26,7 @@ import android.text.format.Time; import android.util.Log; import com.fsck.k9.Account.SortType; import com.fsck.k9.account.AndroidAccountOAuth2TokenStore; import com.fsck.k9.activity.MessageCompose; import com.fsck.k9.activity.UpgradeDatabases; import com.fsck.k9.controller.MessagingController; Loading Loading @@ -510,6 +511,7 @@ public class K9 extends Application { super.onCreate(); app = this; Globals.setContext(this); Globals.setOAuth2TokenProvider(new AndroidAccountOAuth2TokenStore(this)); K9MailLib.setDebugStatus(new K9MailLib.DebugStatus() { @Override public boolean enabled() { Loading k9mail/src/main/java/com/fsck/k9/account/AndroidAccountOAuth2TokenStore.java 0 → 100644 +137 −0 Original line number Diff line number Diff line package com.fsck.k9.account; import android.accounts.Account; import android.accounts.AccountManager; import android.accounts.AccountManagerCallback; import android.accounts.AccountManagerFuture; import android.accounts.AuthenticatorException; import android.accounts.OperationCanceledException; import android.app.Activity; import android.content.Context; import android.os.Bundle; import com.fsck.k9.R; import com.fsck.k9.mail.AuthenticationFailedException; import com.fsck.k9.mail.oauth.AuthorizationException; import com.fsck.k9.mail.oauth.OAuth2TokenProvider; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; /** * An interface between the OAuth2 requirements used for authentication and the AccountManager. */ public class AndroidAccountOAuth2TokenStore implements OAuth2TokenProvider { private static final String GMAIL_AUTH_TOKEN_TYPE = "oauth2:https://mail.google.com/"; private static final String GOOGLE_ACCOUNT_TYPE = "com.google"; private Map<String,String> authTokens = new HashMap<>(); private AccountManager accountManager; public AndroidAccountOAuth2TokenStore(Context applicationContext) { this.accountManager = AccountManager.get(applicationContext); } @Override public void authorizeAPI(final String emailAddress, final Activity activity, final OAuth2TokenProviderAuthCallback callback) { Account account = getAccountFromManager(emailAddress); if (account == null) { callback.failure(new AuthorizationException(activity.getString(R.string.xoauth2_account_doesnt_exist))); return; } if (account.name.equals(emailAddress)) { accountManager.getAuthToken(account, GMAIL_AUTH_TOKEN_TYPE, null, activity, new AccountManagerCallback<Bundle>() { @Override public void run(AccountManagerFuture<Bundle> future) { try { Bundle bundle = future.getResult(); Object keyAccountName = bundle.get(AccountManager.KEY_ACCOUNT_NAME); if (keyAccountName == null) { callback.failure(new AuthorizationException(activity.getString( R.string.xoauth2_no_account))); return; } if (keyAccountName.equals(emailAddress)) { callback.success(); } else { callback.failure(new AuthorizationException(activity.getString( R.string.xoauth2_incorrect_auth_info_provided))); } } catch (OperationCanceledException e) { callback.failure(new AuthorizationException(activity.getString( R.string.xoauth2_auth_cancelled_by_user), e)); } catch (IOException e) { callback.failure(new AuthorizationException(activity.getString( R.string.xoauth2_unable_to_contact_auth_server), e)); } catch (AuthenticatorException e) { callback.failure(new AuthorizationException(activity.getString( R.string.xoauth2_error_contacting_auth_server), e)); } } }, null); } } @Override public String getToken(String username, long timeoutMillis) throws AuthenticationFailedException { if(authTokens.get(username) == null) { Account account = getAccountFromManager(username); if (account == null) { throw new AuthenticationFailedException("Account not available"); } fetchNewAuthToken(username, account, timeoutMillis); } return authTokens.get(username); } private Account getAccountFromManager(String username) { android.accounts.Account[] accounts = accountManager.getAccountsByType(GOOGLE_ACCOUNT_TYPE); for (android.accounts.Account account : accounts) { if (account.name.equals(username)) { return account; } } return null; } private void fetchNewAuthToken(String username, Account account, long timeoutMillis) throws AuthenticationFailedException { try { AccountManagerFuture<Bundle> future = accountManager .getAuthToken(account, GMAIL_AUTH_TOKEN_TYPE, false, null, null); Bundle bundle = future.getResult(timeoutMillis, TimeUnit.MILLISECONDS); if (bundle == null) throw new AuthenticationFailedException("No token provided"); if (bundle.get(AccountManager.KEY_ACCOUNT_NAME) == null) throw new AuthenticationFailedException("No account information provided"); if (bundle.get(AccountManager.KEY_ACCOUNT_NAME).equals(username)) authTokens.put(username, bundle.get(AccountManager.KEY_AUTHTOKEN).toString()); else throw new AuthenticationFailedException("Unexpected account information provided"); } catch (Exception e) { throw new AuthenticationFailedException(e.getMessage()); } } @Override public void invalidateToken(String username) { accountManager.invalidateAuthToken("com.google", authTokens.get(username)); authTokens.remove(username); } @Override public List<String> getAccounts() { Account[] accounts = accountManager.getAccountsByType(GOOGLE_ACCOUNT_TYPE); ArrayList<String> accountNames = new ArrayList<>(); for (Account account: accounts) { accountNames.add(account.name); } return accountNames; } } Loading
k9mail/src/main/AndroidManifest.xml +4 −0 Original line number Diff line number Diff line Loading @@ -16,6 +16,10 @@ <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/> <uses-permission android:name="android.permission.READ_CONTACTS"/> <uses-permission android:name="android.permission.READ_SYNC_SETTINGS"/> <uses-permission android:name="android.permission.MANAGE_ACCOUNTS" /> <uses-permission android:name="android.permission.GET_ACCOUNTS" /> <uses-permission android:name="android.permission.USE_CREDENTIALS" /> <!-- Needed to mark a contact as contacted --> <uses-permission android:name="android.permission.WRITE_CONTACTS"/> Loading
k9mail/src/main/java/com/fsck/k9/Account.java +2 −1 Original line number Diff line number Diff line Loading @@ -21,6 +21,7 @@ import android.graphics.Color; import android.net.Uri; import android.util.Log; import com.fsck.k9.account.AndroidAccountOAuth2TokenStore; import com.fsck.k9.activity.setup.AccountSetupCheckSettings.CheckDirection; import com.fsck.k9.helper.Utility; import com.fsck.k9.mail.Address; Loading Loading @@ -1287,7 +1288,7 @@ public class Account implements BaseAccount, StoreConfig { } public Store getRemoteStore() throws MessagingException { return RemoteStore.getInstance(K9.app, this); return RemoteStore.getInstance(K9.app, this, Globals.getOAuth2TokenProvider()); } // It'd be great if this actually went into the store implementation Loading
k9mail/src/main/java/com/fsck/k9/Globals.java +15 −0 Original line number Diff line number Diff line Loading @@ -4,14 +4,21 @@ package com.fsck.k9; import android.content.Context; import android.support.annotation.VisibleForTesting; import com.fsck.k9.mail.oauth.OAuth2TokenProvider; public class Globals { private static Context context; private static OAuth2TokenProvider oAuth2TokenProvider; static void setContext(Context context) { Globals.context = context; } static void setOAuth2TokenProvider(OAuth2TokenProvider oAuth2TokenProvider) { Globals.oAuth2TokenProvider = oAuth2TokenProvider; } public static Context getContext() { if (context == null) { throw new IllegalStateException("No context provided"); Loading @@ -19,4 +26,12 @@ public class Globals { return context; } public static OAuth2TokenProvider getOAuth2TokenProvider() { if (oAuth2TokenProvider == null) { throw new IllegalStateException("No OAuth 2.0 Token Provider provided"); } return oAuth2TokenProvider; } }
k9mail/src/main/java/com/fsck/k9/K9.java +2 −0 Original line number Diff line number Diff line Loading @@ -26,6 +26,7 @@ import android.text.format.Time; import android.util.Log; import com.fsck.k9.Account.SortType; import com.fsck.k9.account.AndroidAccountOAuth2TokenStore; import com.fsck.k9.activity.MessageCompose; import com.fsck.k9.activity.UpgradeDatabases; import com.fsck.k9.controller.MessagingController; Loading Loading @@ -510,6 +511,7 @@ public class K9 extends Application { super.onCreate(); app = this; Globals.setContext(this); Globals.setOAuth2TokenProvider(new AndroidAccountOAuth2TokenStore(this)); K9MailLib.setDebugStatus(new K9MailLib.DebugStatus() { @Override public boolean enabled() { Loading
k9mail/src/main/java/com/fsck/k9/account/AndroidAccountOAuth2TokenStore.java 0 → 100644 +137 −0 Original line number Diff line number Diff line package com.fsck.k9.account; import android.accounts.Account; import android.accounts.AccountManager; import android.accounts.AccountManagerCallback; import android.accounts.AccountManagerFuture; import android.accounts.AuthenticatorException; import android.accounts.OperationCanceledException; import android.app.Activity; import android.content.Context; import android.os.Bundle; import com.fsck.k9.R; import com.fsck.k9.mail.AuthenticationFailedException; import com.fsck.k9.mail.oauth.AuthorizationException; import com.fsck.k9.mail.oauth.OAuth2TokenProvider; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; /** * An interface between the OAuth2 requirements used for authentication and the AccountManager. */ public class AndroidAccountOAuth2TokenStore implements OAuth2TokenProvider { private static final String GMAIL_AUTH_TOKEN_TYPE = "oauth2:https://mail.google.com/"; private static final String GOOGLE_ACCOUNT_TYPE = "com.google"; private Map<String,String> authTokens = new HashMap<>(); private AccountManager accountManager; public AndroidAccountOAuth2TokenStore(Context applicationContext) { this.accountManager = AccountManager.get(applicationContext); } @Override public void authorizeAPI(final String emailAddress, final Activity activity, final OAuth2TokenProviderAuthCallback callback) { Account account = getAccountFromManager(emailAddress); if (account == null) { callback.failure(new AuthorizationException(activity.getString(R.string.xoauth2_account_doesnt_exist))); return; } if (account.name.equals(emailAddress)) { accountManager.getAuthToken(account, GMAIL_AUTH_TOKEN_TYPE, null, activity, new AccountManagerCallback<Bundle>() { @Override public void run(AccountManagerFuture<Bundle> future) { try { Bundle bundle = future.getResult(); Object keyAccountName = bundle.get(AccountManager.KEY_ACCOUNT_NAME); if (keyAccountName == null) { callback.failure(new AuthorizationException(activity.getString( R.string.xoauth2_no_account))); return; } if (keyAccountName.equals(emailAddress)) { callback.success(); } else { callback.failure(new AuthorizationException(activity.getString( R.string.xoauth2_incorrect_auth_info_provided))); } } catch (OperationCanceledException e) { callback.failure(new AuthorizationException(activity.getString( R.string.xoauth2_auth_cancelled_by_user), e)); } catch (IOException e) { callback.failure(new AuthorizationException(activity.getString( R.string.xoauth2_unable_to_contact_auth_server), e)); } catch (AuthenticatorException e) { callback.failure(new AuthorizationException(activity.getString( R.string.xoauth2_error_contacting_auth_server), e)); } } }, null); } } @Override public String getToken(String username, long timeoutMillis) throws AuthenticationFailedException { if(authTokens.get(username) == null) { Account account = getAccountFromManager(username); if (account == null) { throw new AuthenticationFailedException("Account not available"); } fetchNewAuthToken(username, account, timeoutMillis); } return authTokens.get(username); } private Account getAccountFromManager(String username) { android.accounts.Account[] accounts = accountManager.getAccountsByType(GOOGLE_ACCOUNT_TYPE); for (android.accounts.Account account : accounts) { if (account.name.equals(username)) { return account; } } return null; } private void fetchNewAuthToken(String username, Account account, long timeoutMillis) throws AuthenticationFailedException { try { AccountManagerFuture<Bundle> future = accountManager .getAuthToken(account, GMAIL_AUTH_TOKEN_TYPE, false, null, null); Bundle bundle = future.getResult(timeoutMillis, TimeUnit.MILLISECONDS); if (bundle == null) throw new AuthenticationFailedException("No token provided"); if (bundle.get(AccountManager.KEY_ACCOUNT_NAME) == null) throw new AuthenticationFailedException("No account information provided"); if (bundle.get(AccountManager.KEY_ACCOUNT_NAME).equals(username)) authTokens.put(username, bundle.get(AccountManager.KEY_AUTHTOKEN).toString()); else throw new AuthenticationFailedException("Unexpected account information provided"); } catch (Exception e) { throw new AuthenticationFailedException(e.getMessage()); } } @Override public void invalidateToken(String username) { accountManager.invalidateAuthToken("com.google", authTokens.get(username)); authTokens.remove(username); } @Override public List<String> getAccounts() { Account[] accounts = accountManager.getAccountsByType(GOOGLE_ACCOUNT_TYPE); ArrayList<String> accountNames = new ArrayList<>(); for (Account account: accounts) { accountNames.add(account.name); } return accountNames; } }