Make OAuth work
Now you can authenticate your account using oauth for google hosted accounts (e.g. google.com, gmail.com) The setup ui is still not up to spec. Change-Id: Ib2826653550a823b4d1b8739c1e483746cccbc22
This commit is contained in:
parent
5f4fb9bb14
commit
e8eb6e659b
|
@ -742,14 +742,12 @@ public final class Account extends EmailContent implements AccountColumns, Parce
|
|||
// Also, remember which operation in the array they represent
|
||||
ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();
|
||||
if (mHostAuthRecv != null) {
|
||||
// TODO: This causes problems because it's incompatible with Exchange.
|
||||
// if (mHostAuthRecv.mCredential != null) {
|
||||
// recvCredentialsIndex = index++;
|
||||
// ops.add(ContentProviderOperation.newInsert(mHostAuthRecv.mCredential.mBaseUri)
|
||||
// .withValues(mHostAuthRecv.mCredential.toContentValues())
|
||||
// .build());
|
||||
// }
|
||||
|
||||
if (mHostAuthRecv.mCredential != null) {
|
||||
recvCredentialsIndex = index++;
|
||||
ops.add(ContentProviderOperation.newInsert(mHostAuthRecv.mCredential.mBaseUri)
|
||||
.withValues(mHostAuthRecv.mCredential.toContentValues())
|
||||
.build());
|
||||
}
|
||||
recvIndex = index++;
|
||||
final ContentProviderOperation.Builder b = ContentProviderOperation.newInsert(
|
||||
mHostAuthRecv.mBaseUri);
|
||||
|
@ -762,19 +760,18 @@ public final class Account extends EmailContent implements AccountColumns, Parce
|
|||
ops.add(b.build());
|
||||
}
|
||||
if (mHostAuthSend != null) {
|
||||
// TODO: This causes problems because it's incompatible with Exchange.
|
||||
// if (mHostAuthSend.mCredential != null) {
|
||||
// if (mHostAuthRecv.mCredential != null &&
|
||||
// mHostAuthRecv.mCredential.equals(mHostAuthSend.mCredential)) {
|
||||
if (mHostAuthSend.mCredential != null) {
|
||||
if (mHostAuthRecv.mCredential != null &&
|
||||
mHostAuthRecv.mCredential.equals(mHostAuthSend.mCredential)) {
|
||||
// These two credentials are identical, use the same row.
|
||||
// sendCredentialsIndex = recvCredentialsIndex;
|
||||
// } else {
|
||||
// sendCredentialsIndex = index++;
|
||||
// ops.add(ContentProviderOperation.newInsert(mHostAuthRecv.mCredential.mBaseUri)
|
||||
// .withValues(mHostAuthRecv.mCredential.toContentValues())
|
||||
// .build());
|
||||
// }
|
||||
// }
|
||||
sendCredentialsIndex = recvCredentialsIndex;
|
||||
} else {
|
||||
sendCredentialsIndex = index++;
|
||||
ops.add(ContentProviderOperation.newInsert(mHostAuthSend.mCredential.mBaseUri)
|
||||
.withValues(mHostAuthSend.mCredential.toContentValues())
|
||||
.build());
|
||||
}
|
||||
}
|
||||
sendIndex = index++;
|
||||
final ContentProviderOperation.Builder b = ContentProviderOperation.newInsert(
|
||||
mHostAuthSend.mBaseUri);
|
||||
|
|
|
@ -16,7 +16,7 @@ public class Credential extends EmailContent implements Parcelable {
|
|||
public static final String TABLE_NAME = "Credential";
|
||||
public static Uri CONTENT_URI;
|
||||
|
||||
public static final Credential EMPTY = new Credential(-1, "", "", 0);
|
||||
public static final Credential EMPTY = new Credential(-1, "", "", "", 0);
|
||||
|
||||
public static void initCredential() {
|
||||
CONTENT_URI = Uri.parse(EmailContent.CONTENT_URI + "/credential");
|
||||
|
@ -24,8 +24,12 @@ public class Credential extends EmailContent implements Parcelable {
|
|||
|
||||
public static final String TYPE_OAUTH = "oauth";
|
||||
|
||||
// This is the Id of the oauth provider. It can be used to lookup an oauth provider
|
||||
// from oauth.xml.
|
||||
public String mProviderId;
|
||||
public String mAccessToken;
|
||||
public String mRefreshToken;
|
||||
// This is the wall clock time, in milliseconds since Midnight, Jan 1, 1970.
|
||||
public long mExpiration;
|
||||
|
||||
// Name of the authentication provider.
|
||||
|
@ -58,9 +62,11 @@ public class Credential extends EmailContent implements Parcelable {
|
|||
mBaseUri = CONTENT_URI;
|
||||
}
|
||||
|
||||
public Credential(long id, String accessToken, String refreshToken, long expiration) {
|
||||
public Credential(long id, String providerId, String accessToken, String refreshToken,
|
||||
long expiration) {
|
||||
mBaseUri = CONTENT_URI;
|
||||
mId = id;
|
||||
mProviderId = providerId;
|
||||
mAccessToken = accessToken;
|
||||
mRefreshToken = refreshToken;
|
||||
mExpiration = expiration;
|
||||
|
@ -81,6 +87,7 @@ public class Credential extends EmailContent implements Parcelable {
|
|||
public void restore(Cursor cursor) {
|
||||
mBaseUri = CONTENT_URI;
|
||||
mId = cursor.getLong(CredentialQuery.ID_COLUMN_INDEX);
|
||||
mProviderId = cursor.getString(CredentialQuery.PROVIDER_COLUMN_INDEX);
|
||||
mAccessToken = cursor.getString(CredentialQuery.ACCESS_TOKEN_COLUMN_INDEX);
|
||||
mRefreshToken = cursor.getString(CredentialQuery.REFRESH_TOKEN_COLUMN_INDEX);
|
||||
mExpiration = cursor.getInt(CredentialQuery.EXPIRATION_COLUMN_INDEX);
|
||||
|
@ -114,6 +121,7 @@ public class Credential extends EmailContent implements Parcelable {
|
|||
public void writeToParcel(Parcel dest, int flags) {
|
||||
// mBaseUri is not parceled
|
||||
dest.writeLong(mId);
|
||||
dest.writeString(mProviderId);
|
||||
dest.writeString(mAccessToken);
|
||||
dest.writeString(mRefreshToken);
|
||||
dest.writeLong(mExpiration);
|
||||
|
@ -125,6 +133,7 @@ public class Credential extends EmailContent implements Parcelable {
|
|||
public Credential(Parcel in) {
|
||||
mBaseUri = CONTENT_URI;
|
||||
mId = in.readLong();
|
||||
mProviderId = in.readString();
|
||||
mAccessToken = in.readString();
|
||||
mRefreshToken = in.readString();
|
||||
mExpiration = in.readLong();
|
||||
|
@ -136,7 +145,8 @@ public class Credential extends EmailContent implements Parcelable {
|
|||
return false;
|
||||
}
|
||||
Credential that = (Credential)o;
|
||||
return Utility.areStringsEqual(mAccessToken, that.mAccessToken)
|
||||
return Utility.areStringsEqual(mProviderId, that.mProviderId)
|
||||
&& Utility.areStringsEqual(mAccessToken, that.mAccessToken)
|
||||
&& Utility.areStringsEqual(mRefreshToken, that.mRefreshToken)
|
||||
&& mExpiration == that.mExpiration;
|
||||
}
|
||||
|
@ -149,6 +159,7 @@ public class Credential extends EmailContent implements Parcelable {
|
|||
@Override
|
||||
public ContentValues toContentValues() {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(PROVIDER_COLUMN, mProviderId);
|
||||
values.put(ACCESS_TOKEN_COLUMN, mAccessToken);
|
||||
values.put(REFRESH_TOKEN_COLUMN, mRefreshToken);
|
||||
values.put(EXPIRATION_COLUMN, mExpiration);
|
||||
|
|
|
@ -32,7 +32,7 @@ import com.android.emailcommon.utility.Utility;
|
|||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
|
||||
public final class HostAuth extends EmailContent implements HostAuthColumns, Parcelable {
|
||||
public class HostAuth extends EmailContent implements HostAuthColumns, Parcelable {
|
||||
public static final String TABLE_NAME = "HostAuth";
|
||||
public static Uri CONTENT_URI;
|
||||
|
||||
|
@ -68,6 +68,8 @@ public final class HostAuth extends EmailContent implements HostAuthColumns, Par
|
|||
public byte[] mServerCert = null;
|
||||
public long mCredentialKey;
|
||||
|
||||
public transient Credential mCredential;
|
||||
|
||||
public static final int CONTENT_ID_COLUMN = 0;
|
||||
public static final int CONTENT_PROTOCOL_COLUMN = 1;
|
||||
public static final int CONTENT_ADDRESS_COLUMN = 2;
|
||||
|
@ -80,55 +82,16 @@ public final class HostAuth extends EmailContent implements HostAuthColumns, Par
|
|||
public static final int CONTENT_CREDENTIAL_KEY_COLUMN = 9;
|
||||
|
||||
public static final String[] CONTENT_PROJECTION = new String[] {
|
||||
RECORD_ID, HostAuthColumns.PROTOCOL, HostAuthColumns.ADDRESS, HostAuthColumns.PORT,
|
||||
HostAuthColumns.FLAGS, HostAuthColumns.LOGIN,
|
||||
HostAuthColumns.PASSWORD, HostAuthColumns.DOMAIN, HostAuthColumns.CLIENT_CERT_ALIAS,
|
||||
HostAuthColumns.CREDENTIAL_KEY
|
||||
RECORD_ID, HostAuthColumns.PROTOCOL, HostAuthColumns.ADDRESS, HostAuthColumns.PORT,
|
||||
HostAuthColumns.FLAGS, HostAuthColumns.LOGIN,
|
||||
HostAuthColumns.PASSWORD, HostAuthColumns.DOMAIN, HostAuthColumns.CLIENT_CERT_ALIAS,
|
||||
HostAuthColumns.CREDENTIAL_KEY
|
||||
};
|
||||
|
||||
public HostAuth() {
|
||||
mBaseUri = CONTENT_URI;
|
||||
|
||||
// other defaults policy)
|
||||
mPort = PORT_UNKNOWN;
|
||||
}
|
||||
|
||||
/**
|
||||
* getOrCreateCredential
|
||||
* Return the credential object for this HostAuth, creating it if it does not yet exist.
|
||||
* This should not be called on the main thread.
|
||||
* @param context
|
||||
* @return the credential object for this HostAuth
|
||||
*/
|
||||
public Credential getOrCreateCredential(Context context) {
|
||||
// TODO: This causes problems because it's incompatible with Exchange.
|
||||
// if (mCredential == null) {
|
||||
// if (mCredentialKey >= 0) {
|
||||
// mCredential = Credential.restoreCredentialsWithId(context, mCredentialKey);
|
||||
// } else {
|
||||
// mCredential = new Credential();
|
||||
// }
|
||||
// }
|
||||
// return mCredential;
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* getCredentials
|
||||
* Return the credential object for this HostAuth, or null if it does not exist.
|
||||
* This should not be called on the main thread.
|
||||
* @param context
|
||||
* @return
|
||||
*/
|
||||
public Credential getCredentials(Context context) {
|
||||
// TODO: This causes problems because it's incompatible with Exchange.
|
||||
// if (mCredential == null) {
|
||||
// if (mCredentialKey >= 0) {
|
||||
// mCredential = Credential.restoreCredentialsWithId(context, mCredentialKey);
|
||||
// }
|
||||
// }
|
||||
// return mCredential;
|
||||
return null;
|
||||
mCredentialKey = -1;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -142,7 +105,6 @@ public final class HostAuth extends EmailContent implements HostAuthColumns, Par
|
|||
HostAuth.CONTENT_URI, HostAuth.CONTENT_PROJECTION, id);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the scheme for the specified flags.
|
||||
*/
|
||||
|
@ -151,8 +113,41 @@ public final class HostAuth extends EmailContent implements HostAuthColumns, Par
|
|||
}
|
||||
|
||||
/**
|
||||
* Builds a URI scheme name given the parameters for a {@code HostAuth}.
|
||||
* If a {@code clientAlias} is provided, this indicates that a secure connection must be used.
|
||||
* Returns the credential object for this HostAuth. This will load from the
|
||||
* database if the HosAuth has a valid credential key, or return null if not.
|
||||
*/
|
||||
public Credential getCredential(Context context) {
|
||||
if (mCredential == null) {
|
||||
if (mCredentialKey >= 0) {
|
||||
mCredential = Credential.restoreCredentialsWithId(context, mCredentialKey);
|
||||
}
|
||||
}
|
||||
return mCredential;
|
||||
}
|
||||
|
||||
/**
|
||||
* getOrCreateCredential Return the credential object for this HostAuth,
|
||||
* creating it if it does not yet exist. This should not be called on the
|
||||
* main thread.
|
||||
*
|
||||
* @param context
|
||||
* @return the credential object for this HostAuth
|
||||
*/
|
||||
public Credential getOrCreateCredential(Context context) {
|
||||
if (mCredential == null) {
|
||||
if (mCredentialKey >= 0) {
|
||||
mCredential = Credential.restoreCredentialsWithId(context, mCredentialKey);
|
||||
} else {
|
||||
mCredential = new Credential();
|
||||
}
|
||||
}
|
||||
return mCredential;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a URI scheme name given the parameters for a {@code HostAuth}. If
|
||||
* a {@code clientAlias} is provided, this indicates that a secure
|
||||
* connection must be used.
|
||||
*/
|
||||
public static String getSchemeString(String protocol, int flags, String clientAlias) {
|
||||
String security = "";
|
||||
|
@ -236,6 +231,7 @@ public final class HostAuth extends EmailContent implements HostAuthColumns, Par
|
|||
values.put(HostAuthColumns.CLIENT_CERT_ALIAS, mClientCertAlias);
|
||||
values.put(HostAuthColumns.CREDENTIAL_KEY, mCredentialKey);
|
||||
values.put(HostAuthColumns.ACCOUNT_KEY, 0); // Need something to satisfy the DB
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
|
@ -371,13 +367,18 @@ public final class HostAuth extends EmailContent implements HostAuthColumns, Par
|
|||
dest.writeString(mPassword);
|
||||
dest.writeString(mDomain);
|
||||
dest.writeString(mClientCertAlias);
|
||||
// dest.writeLong(mCredentialKey);
|
||||
// TODO: This causes problems because it's incompatible with Exchange.
|
||||
// if (mCredential == null) {
|
||||
// Credential.EMPTY.writeToParcel(dest, flags);
|
||||
// } else {
|
||||
// mCredential.writeToParcel(dest, flags);
|
||||
// }
|
||||
if ((mFlags & FLAG_OAUTH) != 0) {
|
||||
// TODO: This is nasty, but to be compatible with backward Exchange, we can't make any
|
||||
// change to the parcelable format. But we need Credential objects to be here.
|
||||
// So... only parcel or unparcel Credentials if the OAUTH flag is set. This will never
|
||||
// be set on HostAuth going to or coming from Exchange.
|
||||
dest.writeLong(mCredentialKey);
|
||||
if (mCredential == null) {
|
||||
Credential.EMPTY.writeToParcel(dest, flags);
|
||||
} else {
|
||||
mCredential.writeToParcel(dest, flags);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -394,11 +395,17 @@ public final class HostAuth extends EmailContent implements HostAuthColumns, Par
|
|||
mPassword = in.readString();
|
||||
mDomain = in.readString();
|
||||
mClientCertAlias = in.readString();
|
||||
// mCredentialKey = in.readLong();
|
||||
// mCredential = new Credential(in);
|
||||
// if (mCredential.equals(Credential.EMPTY)) {
|
||||
// mCredential = null;
|
||||
// }
|
||||
if ((mFlags & FLAG_OAUTH) != 0) {
|
||||
// TODO: This is nasty, but to be compatible with backward Exchange, we can't make any
|
||||
// change to the parcelable format. But we need Credential objects to be here.
|
||||
// So... only parcel or unparcel Credentials if the OAUTH flag is set. This will never
|
||||
// be set on HostAuth going to or coming from Exchange.
|
||||
mCredentialKey = in.readLong();
|
||||
mCredential = new Credential(in);
|
||||
if (mCredential.equals(Credential.EMPTY)) {
|
||||
mCredential = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -415,8 +422,7 @@ public final class HostAuth extends EmailContent implements HostAuthColumns, Par
|
|||
&& Utility.areStringsEqual(mLogin, that.mLogin)
|
||||
&& Utility.areStringsEqual(mPassword, that.mPassword)
|
||||
&& Utility.areStringsEqual(mDomain, that.mDomain)
|
||||
&& Utility.areStringsEqual(mClientCertAlias, that.mClientCertAlias)
|
||||
&& mCredentialKey == that.mCredentialKey;
|
||||
&& Utility.areStringsEqual(mClientCertAlias, that.mClientCertAlias);
|
||||
// We don't care about the server certificate for equals
|
||||
}
|
||||
|
||||
|
|
|
@ -33,10 +33,13 @@ import android.content.Loader;
|
|||
import android.database.Cursor;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.ResultReceiver;
|
||||
import android.provider.ContactsContract;
|
||||
import android.text.Editable;
|
||||
import android.text.TextUtils;
|
||||
import android.text.TextWatcher;
|
||||
import android.text.format.DateUtils;
|
||||
import android.view.View;
|
||||
import android.view.View.OnClickListener;
|
||||
import android.widget.Button;
|
||||
|
@ -52,6 +55,7 @@ import com.android.email.service.EmailServiceUtils.EmailServiceInfo;
|
|||
import com.android.emailcommon.Logging;
|
||||
import com.android.emailcommon.VendorPolicyLoader.Provider;
|
||||
import com.android.emailcommon.provider.Account;
|
||||
import com.android.emailcommon.provider.Credential;
|
||||
import com.android.emailcommon.provider.EmailContent;
|
||||
import com.android.emailcommon.provider.HostAuth;
|
||||
import com.android.emailcommon.utility.Utility;
|
||||
|
@ -107,6 +111,16 @@ public class AccountSetupBasics extends AccountSetupActivity
|
|||
|
||||
private static final String STATE_KEY_PROVIDER = "AccountSetupBasics.provider";
|
||||
|
||||
public static final int REQUEST_OAUTH = 1;
|
||||
|
||||
public static final int RESULT_OAUTH_SUCCESS = 0;
|
||||
public static final int RESULT_OAUTH_USER_CANCELED = -1;
|
||||
public static final int RESULT_OAUTH_FAILURE = -2;
|
||||
|
||||
public static final String EXTRA_OAUTH_ACCESS_TOKEN = "accessToken";
|
||||
public static final String EXTRA_OAUTH_REFRESH_TOKEN = "refreshToken";
|
||||
public static final String EXTRA_OAUTH_EXPIRES_IN = "expiresIn";
|
||||
|
||||
// Support for UI
|
||||
private EditText mEmailView;
|
||||
private EditText mPasswordView;
|
||||
|
@ -136,6 +150,22 @@ public class AccountSetupBasics extends AccountSetupActivity
|
|||
fromActivity.startActivity(i);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
if (requestCode == REQUEST_OAUTH && resultCode == RESULT_OAUTH_SUCCESS) {
|
||||
final String accessToken = data.getStringExtra(EXTRA_OAUTH_ACCESS_TOKEN);
|
||||
final String refreshToken = data.getStringExtra(EXTRA_OAUTH_REFRESH_TOKEN);
|
||||
final int expiresInSeconds = data.getIntExtra(EXTRA_OAUTH_EXPIRES_IN, 0);
|
||||
|
||||
finishOAuthSetup(accessToken, refreshToken, expiresInSeconds);
|
||||
} else {
|
||||
// TODO: STOPSHIP: This setup UI is not correct, we need to figure out what to do
|
||||
// in case of errors and have localized strings.
|
||||
Toast.makeText(AccountSetupBasics.this,
|
||||
"Failed to get token", Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This generates setup data that can be used to start a self-contained account creation flow
|
||||
* for exchange accounts.
|
||||
|
@ -400,7 +430,7 @@ public class AccountSetupBasics extends AccountSetupActivity
|
|||
final Intent i = new Intent(this, OAuthAuthenticationActivity.class);
|
||||
i.putExtra(OAuthAuthenticationActivity.EXTRA_EMAIL_ADDRESS, email);
|
||||
i.putExtra(OAuthAuthenticationActivity.EXTRA_PROVIDER, provider.oauth);
|
||||
startActivity(i);
|
||||
startActivityForResult(i, REQUEST_OAUTH);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
@ -504,6 +534,71 @@ public class AccountSetupBasics extends AccountSetupActivity
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finish the oauth setup process.
|
||||
*/
|
||||
private void finishOAuthSetup(final String accessToken, final String refreshToken,
|
||||
int expiresInSeconds) {
|
||||
|
||||
final String email = mEmailView.getText().toString().trim();
|
||||
final String[] emailParts = email.split("@");
|
||||
final String domain = emailParts[1].trim();
|
||||
mProvider = AccountSettingsUtils.findProviderForDomain(this, domain);
|
||||
if (mProvider == null) {
|
||||
// TODO: STOPSHIP: Need better error handling here.
|
||||
Toast.makeText(AccountSetupBasics.this,
|
||||
"No provider, can't proceed", Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
mProvider.expandTemplates(email);
|
||||
|
||||
final Account account = mSetupData.getAccount();
|
||||
final HostAuth recvAuth = account.getOrCreateHostAuthRecv(this);
|
||||
HostAuth.setHostAuthFromString(recvAuth, mProvider.incomingUri);
|
||||
recvAuth.setLogin(mProvider.incomingUsername, null);
|
||||
Credential cred = recvAuth.getOrCreateCredential(this);
|
||||
cred.mProviderId = mProvider.oauth;
|
||||
cred.mAccessToken = accessToken;
|
||||
cred.mRefreshToken = refreshToken;
|
||||
cred.mExpiration = System.currentTimeMillis() +
|
||||
expiresInSeconds * DateUtils.SECOND_IN_MILLIS;
|
||||
// TODO: For now, assume that we will use SSL because that's what
|
||||
// gmail wants. This needs to be parameterized from providers.xml
|
||||
recvAuth.mFlags |= HostAuth.FLAG_SSL;
|
||||
recvAuth.mFlags |= HostAuth.FLAG_OAUTH;
|
||||
|
||||
final EmailServiceInfo info = EmailServiceUtils.getServiceInfo(this,
|
||||
recvAuth.mProtocol);
|
||||
recvAuth.mPort =
|
||||
((recvAuth.mFlags & HostAuth.FLAG_SSL) != 0) ? info.portSsl : info.port;
|
||||
|
||||
final HostAuth sendAuth = account.getOrCreateHostAuthSend(this);
|
||||
HostAuth.setHostAuthFromString(sendAuth, mProvider.outgoingUri);
|
||||
sendAuth.setLogin(mProvider.outgoingUsername, null);
|
||||
sendAuth.mCredential = cred;
|
||||
sendAuth.mFlags |= HostAuth.FLAG_SSL;
|
||||
sendAuth.mFlags |= HostAuth.FLAG_OAUTH;
|
||||
|
||||
// Populate the setup data, assuming that the duplicate account check will succeed
|
||||
populateSetupData(getOwnerName(), email);
|
||||
|
||||
// Stop here if the login credentials duplicate an existing account
|
||||
// Launch an Async task to do the work
|
||||
new DuplicateCheckTask(this, email, true)
|
||||
.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
} catch (URISyntaxException e) {
|
||||
/*
|
||||
* If there is some problem with the URI we give up and go on to manual setup.
|
||||
* Technically speaking, AutoDiscover is OK here, since the user clicked "Next"
|
||||
* to get here. This will not happen in practice because we don't expect to
|
||||
* find any EAS accounts in the providers list.
|
||||
*/
|
||||
onManualSetup(true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Async task that continues the work of finishAutoSetup(). Checks for a duplicate
|
||||
* account and then either alerts the user, or continues.
|
||||
|
@ -579,7 +674,7 @@ public class AccountSetupBasics extends AccountSetupActivity
|
|||
finishAutoSetup();
|
||||
}
|
||||
} else {
|
||||
// Can't use auto setup (although EAS accounts may still be able to AutoDiscover)
|
||||
// Can't use auto setup (although EAS accounts may still be able to AutoDiscover)
|
||||
new DuplicateCheckTask(this, email, false)
|
||||
.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
}
|
||||
|
|
|
@ -1,27 +1,29 @@
|
|||
package com.android.email.activity.setup;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.LoaderManager.LoaderCallbacks;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Bitmap;
|
||||
import android.content.Loader;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.text.TextUtils;
|
||||
import android.webkit.CookieManager;
|
||||
import android.webkit.CookieSyncManager;
|
||||
import android.webkit.WebResourceResponse;
|
||||
import android.webkit.WebView;
|
||||
import android.webkit.WebViewClient;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.android.email.R;
|
||||
import com.android.email.mail.internet.OAuthAuthenticator;
|
||||
import com.android.email.mail.internet.OAuthAuthenticator.AuthenticationResult;
|
||||
import com.android.emailcommon.Logging;
|
||||
import com.android.emailcommon.VendorPolicyLoader.OAuthProvider;
|
||||
import com.android.emailcommon.mail.AuthenticationFailedException;
|
||||
import com.android.emailcommon.mail.MessagingException;
|
||||
import com.android.mail.ui.MailAsyncTaskLoader;
|
||||
import com.android.mail.utils.LogUtils;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URL;
|
||||
import java.io.IOException;
|
||||
|
||||
|
||||
/**
|
||||
|
@ -29,14 +31,20 @@ import java.net.URL;
|
|||
* should obtain an authorization code, which can be used to obtain access and
|
||||
* refresh tokens.
|
||||
*/
|
||||
public class OAuthAuthenticationActivity extends Activity {
|
||||
public class OAuthAuthenticationActivity extends Activity implements
|
||||
LoaderCallbacks<AuthenticationResult> {
|
||||
private final static String TAG = Logging.LOG_TAG;
|
||||
|
||||
public static final String EXTRA_EMAIL_ADDRESS = "email_address";
|
||||
public static final String EXTRA_PROVIDER = "provider";
|
||||
public static final String EXTRA_PROVIDER_ID = "provider_id";
|
||||
public static final String EXTRA_AUTHENTICATION_CODE = "authentication_code";
|
||||
|
||||
WebView mWv;
|
||||
OAuthProvider mProvider;
|
||||
public static final int LOADER_ID_OAUTH_TOKEN = 1;
|
||||
|
||||
private WebView mWv;
|
||||
private OAuthProvider mProvider;
|
||||
private String mAuthenticationCode;
|
||||
|
||||
private class MyWebViewClient extends WebViewClient {
|
||||
|
||||
|
@ -45,7 +53,6 @@ public class OAuthAuthenticationActivity extends Activity {
|
|||
// TODO: This method works for Google's redirect url to https://localhost.
|
||||
// Does it work for the general case? I don't know what redirect url other
|
||||
// providers use, or how the authentication code is returned.
|
||||
LogUtils.d(TAG, "shouldOverrideUrlLoading %s", url);
|
||||
final String deparameterizedUrl;
|
||||
int i = url.lastIndexOf('?');
|
||||
if (i == -1) {
|
||||
|
@ -59,19 +66,19 @@ public class OAuthAuthenticationActivity extends Activity {
|
|||
// Check the params of this uri, they contain success/failure info,
|
||||
// along with the authentication token.
|
||||
final String error = uri.getQueryParameter("error");
|
||||
|
||||
if (error != null) {
|
||||
// TODO display failure screen
|
||||
LogUtils.d(TAG, "error code %s", error);
|
||||
Toast.makeText(OAuthAuthenticationActivity.this,
|
||||
"Couldn't authenticate", Toast.LENGTH_LONG).show();
|
||||
final Intent intent = new Intent();
|
||||
setResult(AccountSetupBasics.RESULT_OAUTH_USER_CANCELED, intent);
|
||||
finish();
|
||||
} else {
|
||||
// TODO use this token to request the access and refresh tokens
|
||||
final String code = uri.getQueryParameter("code");
|
||||
LogUtils.d(TAG, "authorization code %s", code);
|
||||
Toast.makeText(OAuthAuthenticationActivity.this,
|
||||
"OAuth not implemented", Toast.LENGTH_LONG).show();
|
||||
mAuthenticationCode = uri.getQueryParameter("code");
|
||||
Bundle params = new Bundle();
|
||||
params.putString(EXTRA_PROVIDER_ID, mProvider.id);
|
||||
params.putString(EXTRA_AUTHENTICATION_CODE, mAuthenticationCode);
|
||||
getLoaderManager().initLoader(LOADER_ID_OAUTH_TOKEN, params,
|
||||
OAuthAuthenticationActivity.this);
|
||||
}
|
||||
finish();
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
|
@ -96,7 +103,93 @@ public class OAuthAuthenticationActivity extends Activity {
|
|||
final String providerName = i.getStringExtra(EXTRA_PROVIDER);
|
||||
mProvider = AccountSettingsUtils.findOAuthProvider(this, providerName);
|
||||
final Uri uri = AccountSettingsUtils.createOAuthRegistrationRequest(this, mProvider, email);
|
||||
LogUtils.d(Logging.LOG_TAG, "launching '%s'", uri);
|
||||
mWv.loadUrl(uri.toString());
|
||||
|
||||
if (bundle != null) {
|
||||
mAuthenticationCode = bundle.getString(EXTRA_AUTHENTICATION_CODE);
|
||||
} else {
|
||||
mAuthenticationCode = null;
|
||||
}
|
||||
if (mAuthenticationCode != null) {
|
||||
Bundle params = new Bundle();
|
||||
params.putString(EXTRA_PROVIDER_ID, mProvider.id);
|
||||
params.putString(EXTRA_AUTHENTICATION_CODE, mAuthenticationCode);
|
||||
getLoaderManager().initLoader(LOADER_ID_OAUTH_TOKEN, params,
|
||||
OAuthAuthenticationActivity.this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSaveInstanceState(Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
outState.putString(EXTRA_AUTHENTICATION_CODE, mAuthenticationCode);
|
||||
}
|
||||
|
||||
private static class OAuthTokenLoader extends MailAsyncTaskLoader<AuthenticationResult> {
|
||||
private final String mProviderId;
|
||||
private final String mCode;
|
||||
|
||||
public OAuthTokenLoader(Context context, String providerId, String code) {
|
||||
super(context);
|
||||
mProviderId = providerId;
|
||||
mCode = code;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDiscardResult(AuthenticationResult result) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthenticationResult loadInBackground() {
|
||||
try {
|
||||
final OAuthAuthenticator authenticator = new OAuthAuthenticator();
|
||||
final AuthenticationResult result = authenticator.requestAccess(
|
||||
getContext(), mProviderId, mCode);
|
||||
LogUtils.d(Logging.LOG_TAG, "authentication result %s", result);
|
||||
return result;
|
||||
// TODO: do I need a better UI for displaying exceptions?
|
||||
} catch (AuthenticationFailedException e) {
|
||||
} catch (MessagingException e) {
|
||||
} catch (IOException e) {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Loader<AuthenticationResult> onCreateLoader(int id, Bundle data) {
|
||||
if (id == LOADER_ID_OAUTH_TOKEN) {
|
||||
final String providerId = data.getString(EXTRA_PROVIDER_ID);
|
||||
final String code = data.getString(EXTRA_AUTHENTICATION_CODE);
|
||||
return new OAuthTokenLoader(this, providerId, code);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadFinished(Loader<AuthenticationResult> loader,
|
||||
AuthenticationResult data) {
|
||||
if (data == null) {
|
||||
// STOPSHIP: need a better way to display errors. We might get IO or
|
||||
// MessagingExceptions.
|
||||
Toast.makeText(this, "Error getting tokens", Toast.LENGTH_SHORT).show();
|
||||
|
||||
} else {
|
||||
final Intent intent = new Intent();
|
||||
intent.putExtra(AccountSetupBasics.EXTRA_OAUTH_ACCESS_TOKEN,
|
||||
data.mAccessToken);
|
||||
intent.putExtra(AccountSetupBasics.EXTRA_OAUTH_REFRESH_TOKEN,
|
||||
data.mRefreshToken);
|
||||
intent.putExtra(AccountSetupBasics.EXTRA_OAUTH_EXPIRES_IN,
|
||||
data.mExpiresInSeconds);
|
||||
setResult(AccountSetupBasics.RESULT_OAUTH_SUCCESS, intent);
|
||||
}
|
||||
finish();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoaderReset(Loader<AuthenticationResult> loader) {
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -202,4 +202,8 @@ public abstract class Store {
|
|||
public void closeConnections() {
|
||||
// Base implementation does nothing.
|
||||
}
|
||||
|
||||
public Account getAccount() {
|
||||
return mAccount;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,161 @@
|
|||
package com.android.email.mail.internet;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.format.DateUtils;
|
||||
|
||||
import com.android.email.mail.internet.OAuthAuthenticator.AuthenticationResult;
|
||||
import com.android.emailcommon.Logging;
|
||||
import com.android.emailcommon.mail.AuthenticationFailedException;
|
||||
import com.android.emailcommon.mail.MessagingException;
|
||||
import com.android.emailcommon.provider.Account;
|
||||
import com.android.emailcommon.provider.Credential;
|
||||
import com.android.emailcommon.provider.HostAuth;
|
||||
import com.android.mail.utils.LogUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class AuthenticationCache {
|
||||
private static AuthenticationCache sCache;
|
||||
|
||||
// Threshold for refreshing a token. If the token is expected to expire within this amount of
|
||||
// time, we won't even bother attempting to use it and will simply force a refresh.
|
||||
private static final long EXPIRATION_THRESHOLD = 5 * DateUtils.MINUTE_IN_MILLIS;
|
||||
|
||||
private final Map<Long, CacheEntry> mCache;
|
||||
private final OAuthAuthenticator mAuthenticator;
|
||||
|
||||
private class CacheEntry {
|
||||
CacheEntry(long accountId, String providerId, String accessToken, String refreshToken,
|
||||
long expirationTime) {
|
||||
mAccountId = accountId;
|
||||
mProviderId = providerId;
|
||||
mAccessToken = accessToken;
|
||||
mRefreshToken = refreshToken;
|
||||
mExpirationTime = expirationTime;
|
||||
}
|
||||
|
||||
final long mAccountId;
|
||||
String mProviderId;
|
||||
String mAccessToken;
|
||||
String mRefreshToken;
|
||||
long mExpirationTime;
|
||||
}
|
||||
|
||||
public static AuthenticationCache getInstance() {
|
||||
synchronized (AuthenticationCache.class) {
|
||||
if (sCache == null) {
|
||||
sCache = new AuthenticationCache();
|
||||
}
|
||||
return sCache;
|
||||
}
|
||||
}
|
||||
|
||||
private AuthenticationCache() {
|
||||
mCache = new HashMap<Long, CacheEntry>();
|
||||
mAuthenticator = new OAuthAuthenticator();
|
||||
}
|
||||
|
||||
// Gets an access token for the given account. This may be whatever is currently cached, or
|
||||
// it may query the server to get a new one if the old one is expired or nearly expired.
|
||||
public String retrieveAccessToken(Context context, Account account) throws
|
||||
MessagingException, IOException {
|
||||
// Currently, we always use the same OAuth info for both sending and receiving.
|
||||
// If we start to allow different credential objects for sending and receiving, this
|
||||
// will need to be updated.
|
||||
CacheEntry entry = null;
|
||||
synchronized (mCache) {
|
||||
entry = getEntry(context, account);
|
||||
}
|
||||
synchronized (entry) {
|
||||
final long actualExpiration = entry.mExpirationTime - EXPIRATION_THRESHOLD;
|
||||
if (System.currentTimeMillis() > actualExpiration) {
|
||||
// This access token is pretty close to end of life. Don't bother trying to use it,
|
||||
// it might just time out while we're trying to sync. Go ahead and refresh it
|
||||
// immediately.
|
||||
refreshEntry(context, entry);
|
||||
}
|
||||
return entry.mAccessToken;
|
||||
}
|
||||
}
|
||||
|
||||
public String refreshAccessToken(Context context, Account account) throws
|
||||
MessagingException, IOException {
|
||||
CacheEntry entry = getEntry(context, account);
|
||||
synchronized (entry) {
|
||||
refreshEntry(context, entry);
|
||||
return entry.mAccessToken;
|
||||
}
|
||||
}
|
||||
|
||||
private CacheEntry getEntry(Context context, Account account) {
|
||||
CacheEntry entry;
|
||||
if (account.isSaved()) {
|
||||
entry = mCache.get(account.mId);
|
||||
if (entry == null) {
|
||||
LogUtils.d(Logging.LOG_TAG, "initializing entry from database");
|
||||
final HostAuth hostAuth = account.getOrCreateHostAuthRecv(context);
|
||||
final Credential credential = hostAuth.getOrCreateCredential(context);
|
||||
entry = new CacheEntry(account.mId, credential.mProviderId, credential.mAccessToken,
|
||||
credential.mRefreshToken, credential.mExpiration);
|
||||
mCache.put(account.mId, entry);
|
||||
}
|
||||
} else {
|
||||
// This account is not yet saved, just create a temporary entry. Don't store
|
||||
// it in the cache, it won't be findable because we don't yet have an account Id.
|
||||
final HostAuth hostAuth = account.getOrCreateHostAuthRecv(context);
|
||||
final Credential credential = hostAuth.getCredential(context);
|
||||
entry = new CacheEntry(account.mId, credential.mProviderId, credential.mAccessToken,
|
||||
credential.mRefreshToken, credential.mExpiration);
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
|
||||
private void refreshEntry(Context context, CacheEntry entry) throws
|
||||
IOException, MessagingException {
|
||||
LogUtils.d(Logging.LOG_TAG, "AuthenticationCache refreshEntry %d", entry.mAccountId);
|
||||
try {
|
||||
final AuthenticationResult result = mAuthenticator.requestRefresh(context,
|
||||
entry.mProviderId, entry.mRefreshToken);
|
||||
// Don't set the refresh token here, it's not returned by the refresh response,
|
||||
// so setting it here would make it blank.
|
||||
entry.mAccessToken = result.mAccessToken;
|
||||
entry.mExpirationTime = result.mExpiresInSeconds * DateUtils.SECOND_IN_MILLIS +
|
||||
System.currentTimeMillis();
|
||||
saveEntry(context, entry);
|
||||
} catch (AuthenticationFailedException e) {
|
||||
// This is fatal. Clear the tokens and rethrow the exception.
|
||||
LogUtils.d(Logging.LOG_TAG, "authentication failed, clearning");
|
||||
clearEntry(context, entry);
|
||||
throw e;
|
||||
} catch (MessagingException e) {
|
||||
LogUtils.d(Logging.LOG_TAG, "messaging exception");
|
||||
throw e;
|
||||
} catch (IOException e) {
|
||||
LogUtils.d(Logging.LOG_TAG, "IO exception");
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private void saveEntry(Context context, CacheEntry entry) {
|
||||
LogUtils.d(Logging.LOG_TAG, "saveEntry");
|
||||
|
||||
final Account account = Account.restoreAccountWithId(context, entry.mAccountId);
|
||||
final HostAuth hostAuth = account.getOrCreateHostAuthRecv(context);
|
||||
final Credential cred = hostAuth.getOrCreateCredential(context);
|
||||
cred.mProviderId = entry.mProviderId;
|
||||
cred.mAccessToken = entry.mAccessToken;
|
||||
cred.mRefreshToken = entry.mRefreshToken;
|
||||
cred.mExpiration = entry.mExpirationTime;
|
||||
cred.update(context, cred.toContentValues());
|
||||
}
|
||||
|
||||
private void clearEntry(Context context, CacheEntry entry) {
|
||||
LogUtils.d(Logging.LOG_TAG, "clearEntry");
|
||||
entry.mAccessToken = "";
|
||||
entry.mRefreshToken = "";
|
||||
entry.mExpirationTime = 0;
|
||||
saveEntry(context, entry);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,191 @@
|
|||
package com.android.email.mail.internet;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.format.DateUtils;
|
||||
|
||||
import com.android.email.activity.setup.AccountSettingsUtils;
|
||||
import com.android.emailcommon.Logging;
|
||||
import com.android.emailcommon.VendorPolicyLoader.OAuthProvider;
|
||||
import com.android.emailcommon.mail.AuthenticationFailedException;
|
||||
import com.android.emailcommon.mail.MessagingException;
|
||||
import com.android.mail.utils.LogUtils;
|
||||
|
||||
import org.apache.http.HttpResponse;
|
||||
import org.apache.http.HttpStatus;
|
||||
import org.apache.http.client.HttpClient;
|
||||
import org.apache.http.client.entity.UrlEncodedFormEntity;
|
||||
import org.apache.http.client.methods.HttpPost;
|
||||
import org.apache.http.impl.client.DefaultHttpClient;
|
||||
import org.apache.http.message.BasicNameValuePair;
|
||||
import org.apache.http.params.BasicHttpParams;
|
||||
import org.apache.http.params.HttpConnectionParams;
|
||||
import org.apache.http.params.HttpParams;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class OAuthAuthenticator {
|
||||
private static final String TAG = Logging.LOG_TAG;
|
||||
|
||||
public static final String OAUTH_REQUEST_CODE = "code";
|
||||
public static final String OAUTH_REQUEST_REFRESH_TOKEN = "refresh_token";
|
||||
public static final String OAUTH_REQUEST_CLIENT_ID = "client_id";
|
||||
public static final String OAUTH_REQUEST_CLIENT_SECRET = "client_secret";
|
||||
public static final String OAUTH_REQUEST_REDIRECT_URI = "redirect_uri";
|
||||
public static final String OAUTH_REQUEST_GRANT_TYPE = "grant_type";
|
||||
|
||||
public static final String JSON_ACCESS_TOKEN = "access_token";
|
||||
public static final String JSON_REFRESH_TOKEN = "refresh_token";
|
||||
public static final String JSON_EXPIRES_IN = "expires_in";
|
||||
|
||||
|
||||
private static final long CONNECTION_TIMEOUT = 20 * DateUtils.SECOND_IN_MILLIS;
|
||||
private static final long COMMAND_TIMEOUT = 30 * DateUtils.SECOND_IN_MILLIS;
|
||||
|
||||
final HttpClient mClient;
|
||||
|
||||
public static class AuthenticationResult {
|
||||
public AuthenticationResult(final String accessToken, final String refreshToken,
|
||||
final int expiresInSeconds) {
|
||||
mAccessToken = accessToken;
|
||||
mRefreshToken = refreshToken;
|
||||
mExpiresInSeconds = expiresInSeconds;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "result access " + (mAccessToken==null?"null":"[REDACTED]") +
|
||||
" refresh " + (mRefreshToken==null?"null":"[REDACTED]") +
|
||||
" expiresInSeconds " + mExpiresInSeconds;
|
||||
}
|
||||
|
||||
public final String mAccessToken;
|
||||
public final String mRefreshToken;
|
||||
public final int mExpiresInSeconds;
|
||||
}
|
||||
|
||||
public OAuthAuthenticator() {
|
||||
final HttpParams params = new BasicHttpParams();
|
||||
HttpConnectionParams.setConnectionTimeout(params, (int)(CONNECTION_TIMEOUT));
|
||||
HttpConnectionParams.setSoTimeout(params, (int)(COMMAND_TIMEOUT));
|
||||
HttpConnectionParams.setSocketBufferSize(params, 8192);
|
||||
mClient = new DefaultHttpClient(params);
|
||||
}
|
||||
|
||||
public AuthenticationResult requestAccess(final Context context, final String providerId,
|
||||
final String code) throws MessagingException, IOException {
|
||||
final OAuthProvider provider = AccountSettingsUtils.findOAuthProvider(context, providerId);
|
||||
if (provider == null) {
|
||||
LogUtils.e(TAG, "invalid provider %s", providerId);
|
||||
// This shouldn't happen, but if it does, it's a fatal. Throw an authentication failed
|
||||
// exception, this will at least give the user a heads up to set up their account again.
|
||||
throw new AuthenticationFailedException("Invalid provider" + providerId);
|
||||
}
|
||||
|
||||
final HttpPost post = new HttpPost(provider.tokenEndpoint);
|
||||
post.setHeader("Content-Type", "application/x-www-form-urlencoded");
|
||||
final List<BasicNameValuePair> nvp = new ArrayList<BasicNameValuePair>();
|
||||
nvp.add(new BasicNameValuePair(OAUTH_REQUEST_CODE, code));
|
||||
nvp.add(new BasicNameValuePair(OAUTH_REQUEST_CLIENT_ID, provider.clientId));
|
||||
nvp.add(new BasicNameValuePair(OAUTH_REQUEST_CLIENT_SECRET, provider.clientSecret));
|
||||
nvp.add(new BasicNameValuePair(OAUTH_REQUEST_REDIRECT_URI, provider.redirectUri));
|
||||
nvp.add(new BasicNameValuePair(OAUTH_REQUEST_GRANT_TYPE, "authorization_code"));
|
||||
try {
|
||||
post.setEntity(new UrlEncodedFormEntity(nvp));
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
LogUtils.e(TAG, e, "unsupported encoding");
|
||||
// This shouldn't happen, but if it does, it's a fatal. Throw an authentication failed
|
||||
// exception, this will at least give the user a heads up to set up their account again.
|
||||
throw new AuthenticationFailedException("Unsupported encoding", e);
|
||||
}
|
||||
|
||||
return doRequest(post);
|
||||
}
|
||||
|
||||
public AuthenticationResult requestRefresh(final Context context, final String providerId,
|
||||
final String refreshToken) throws MessagingException, IOException {
|
||||
final OAuthProvider provider = AccountSettingsUtils.findOAuthProvider(context, providerId);
|
||||
if (provider == null) {
|
||||
LogUtils.e(TAG, "invalid provider %s", providerId);
|
||||
// This shouldn't happen, but if it does, it's a fatal. Throw an authentication failed
|
||||
// exception, this will at least give the user a heads up to set up their account again.
|
||||
throw new AuthenticationFailedException("Invalid provider" + providerId);
|
||||
}
|
||||
final HttpPost post = new HttpPost(provider.refreshEndpoint);
|
||||
post.setHeader("Content-Type", "application/x-www-form-urlencoded");
|
||||
final List<BasicNameValuePair> nvp = new ArrayList<BasicNameValuePair>();
|
||||
nvp.add(new BasicNameValuePair(OAUTH_REQUEST_REFRESH_TOKEN, refreshToken));
|
||||
nvp.add(new BasicNameValuePair(OAUTH_REQUEST_CLIENT_ID, provider.clientId));
|
||||
nvp.add(new BasicNameValuePair(OAUTH_REQUEST_CLIENT_SECRET, provider.clientSecret));
|
||||
nvp.add(new BasicNameValuePair(OAUTH_REQUEST_GRANT_TYPE, "refresh_token"));
|
||||
try {
|
||||
post.setEntity(new UrlEncodedFormEntity(nvp));
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
LogUtils.e(TAG, e, "unsupported encoding");
|
||||
// This shouldn't happen, but if it does, it's a fatal. Throw an authentication failed
|
||||
// exception, this will at least give the user a heads up to set up their account again.
|
||||
throw new AuthenticationFailedException("Unsuported encoding", e);
|
||||
}
|
||||
|
||||
return doRequest(post);
|
||||
}
|
||||
|
||||
private AuthenticationResult doRequest(HttpPost post) throws MessagingException,
|
||||
IOException {
|
||||
final HttpResponse response;
|
||||
response = mClient.execute(post);
|
||||
final int status = response.getStatusLine().getStatusCode();
|
||||
if (status == HttpStatus.SC_OK) {
|
||||
return parseResponse(response);
|
||||
} else if (status == HttpStatus.SC_FORBIDDEN || status == HttpStatus.SC_UNAUTHORIZED ||
|
||||
status == HttpStatus.SC_BAD_REQUEST) {
|
||||
LogUtils.e(TAG, "HTTP Authentication error getting oauth tokens %d", status);
|
||||
// This is fatal, and we probably should clear our tokens after this.
|
||||
throw new AuthenticationFailedException("Auth error getting auth token");
|
||||
} else {
|
||||
LogUtils.e(TAG, "HTTP Error %d getting oauth tokens", status);
|
||||
// This is probably a transient error, we can try again later.
|
||||
throw new MessagingException("HTTPError " + status + " getting oauth token");
|
||||
}
|
||||
}
|
||||
|
||||
private AuthenticationResult parseResponse(HttpResponse response) throws IOException,
|
||||
MessagingException {
|
||||
final BufferedReader reader = new BufferedReader(new InputStreamReader(
|
||||
response.getEntity().getContent(), "UTF-8"));
|
||||
final StringBuilder builder = new StringBuilder();
|
||||
for (String line = null; (line = reader.readLine()) != null;) {
|
||||
builder.append(line).append("\n");
|
||||
}
|
||||
try {
|
||||
final JSONObject jsonResult = new JSONObject(builder.toString());
|
||||
final String accessToken = jsonResult.getString(JSON_ACCESS_TOKEN);
|
||||
final String expiresIn = jsonResult.getString(JSON_EXPIRES_IN);
|
||||
final String refreshToken;
|
||||
if (jsonResult.has(JSON_REFRESH_TOKEN)) {
|
||||
refreshToken = jsonResult.getString(JSON_REFRESH_TOKEN);
|
||||
} else {
|
||||
refreshToken = null;
|
||||
}
|
||||
try {
|
||||
int expiresInSeconds = Integer.valueOf(expiresIn);
|
||||
return new AuthenticationResult(accessToken, refreshToken, expiresInSeconds);
|
||||
} catch (NumberFormatException e) {
|
||||
LogUtils.e(TAG, e, "Invalid expiration %s", expiresIn);
|
||||
// This indicates a server error, we can try again later.
|
||||
throw new MessagingException("Invalid number format", e);
|
||||
}
|
||||
} catch (JSONException e) {
|
||||
LogUtils.e(TAG, e, "Invalid JSON");
|
||||
// This indicates a server error, we can try again later.
|
||||
throw new MessagingException("Invalid JSON", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -17,7 +17,9 @@
|
|||
package com.android.email.mail.store;
|
||||
|
||||
import android.text.TextUtils;
|
||||
import android.util.Base64;
|
||||
|
||||
import com.android.email.mail.internet.AuthenticationCache;
|
||||
import com.android.email.mail.store.ImapStore.ImapException;
|
||||
import com.android.email.mail.store.imap.ImapConstants;
|
||||
import com.android.email.mail.store.imap.ImapList;
|
||||
|
@ -59,13 +61,14 @@ class ImapConnection {
|
|||
|
||||
/** The capabilities supported; a set of CAPABILITY_* values. */
|
||||
private int mCapabilities;
|
||||
private static final String IMAP_REDACTED_LOG = "[IMAP command redacted]";
|
||||
static final String IMAP_REDACTED_LOG = "[IMAP command redacted]";
|
||||
MailTransport mTransport;
|
||||
private ImapResponseParser mParser;
|
||||
private ImapStore mImapStore;
|
||||
private String mUsername;
|
||||
private String mLoginPhrase;
|
||||
private String mAccessToken;
|
||||
private String mIdPhrase = null;
|
||||
|
||||
/** # of command/response lines to log upon crash. */
|
||||
private static final int DISCOURSE_LOGGER_SIZE = 64;
|
||||
private final DiscourseLogger mDiscourse = new DiscourseLogger(DISCOURSE_LOGGER_SIZE);
|
||||
|
@ -77,23 +80,54 @@ class ImapConnection {
|
|||
*/
|
||||
private final AtomicInteger mNextCommandTag = new AtomicInteger(0);
|
||||
|
||||
|
||||
// Keep others from instantiating directly
|
||||
ImapConnection(ImapStore store, String username, String password) {
|
||||
setStore(store, username, password);
|
||||
ImapConnection(ImapStore store) {
|
||||
setStore(store);
|
||||
}
|
||||
|
||||
void setStore(ImapStore store, String username, String password) {
|
||||
if (username != null && password != null) {
|
||||
mUsername = username;
|
||||
|
||||
// build the LOGIN string once (instead of over-and-over again.)
|
||||
// apply the quoting here around the built-up password
|
||||
mLoginPhrase = ImapConstants.LOGIN + " " + mUsername + " "
|
||||
+ ImapUtility.imapQuoted(password);
|
||||
}
|
||||
void setStore(ImapStore store) {
|
||||
// TODO: maybe we should throw an exception if the connection is not closed here,
|
||||
// if it's not currently closed, then we won't reopen it, so if the credentials have
|
||||
// changed, the connection will not be reestablished.
|
||||
mImapStore = store;
|
||||
mLoginPhrase = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates and returns the phrase to be used for authentication. This will be a LOGIN with
|
||||
* username and password, or an OAUTH authentication string, with username and access token.
|
||||
* Currently, these are the only two auth mechanisms supported.
|
||||
* @return
|
||||
* @throws IOException
|
||||
* @throws AuthenticationFailedException
|
||||
*/
|
||||
String getLoginPhrase() throws MessagingException, IOException {
|
||||
// build the LOGIN string once (instead of over-and-over again.)
|
||||
if (mImapStore.getUseOAuth()) {
|
||||
// We'll recreate the login phrase if it's null, or if the access token
|
||||
// has changed.
|
||||
final String accessToken = AuthenticationCache.getInstance().retrieveAccessToken(
|
||||
mImapStore.getContext(), mImapStore.getAccount());
|
||||
if (mLoginPhrase == null || !TextUtils.equals(mAccessToken, accessToken)) {
|
||||
mAccessToken = accessToken;
|
||||
final String oauthCode = "user=" + mImapStore.getUsername() + '\001' +
|
||||
"auth=Bearer " + mAccessToken + '\001' + '\001';
|
||||
mLoginPhrase = ImapConstants.AUTHENTICATE + " " + ImapConstants.XOAUTH2 + " " +
|
||||
Base64.encodeToString(oauthCode.getBytes(), Base64.NO_WRAP);
|
||||
}
|
||||
} else {
|
||||
if (mLoginPhrase == null) {
|
||||
if (mImapStore.getUsername() != null && mImapStore.getPassword() != null) {
|
||||
// build the LOGIN string once (instead of over-and-over again.)
|
||||
// apply the quoting here around the built-up password
|
||||
mLoginPhrase = ImapConstants.LOGIN + " " + mImapStore.getUsername() + " "
|
||||
+ ImapUtility.imapQuoted(mImapStore.getPassword());
|
||||
}
|
||||
}
|
||||
}
|
||||
return mLoginPhrase;
|
||||
}
|
||||
|
||||
void open() throws IOException, MessagingException {
|
||||
if (mTransport != null && mTransport.isOpen()) {
|
||||
return;
|
||||
|
@ -237,7 +271,8 @@ class ImapConnection {
|
|||
* @return Returns the command tag that was sent
|
||||
*/
|
||||
String sendCommand(String command, boolean sensitive)
|
||||
throws MessagingException, IOException {
|
||||
throws MessagingException, IOException {
|
||||
LogUtils.d(Logging.LOG_TAG, "sendCommand %s", command);
|
||||
open();
|
||||
String tag = Integer.toString(mNextCommandTag.incrementAndGet());
|
||||
String commandToSend = tag + " " + command;
|
||||
|
@ -320,8 +355,10 @@ class ImapConnection {
|
|||
*/
|
||||
List<ImapResponse> executeSimpleCommand(String command, boolean sensitive)
|
||||
throws IOException, MessagingException {
|
||||
sendCommand(command, sensitive);
|
||||
return getCommandResponses();
|
||||
// TODO: It may be nice to catch IOExceptions and close the connection here.
|
||||
// Currently, we expect callers to do that, but if they fail to we'll be in a broken state.
|
||||
sendCommand(command, sensitive);
|
||||
return getCommandResponses();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -336,9 +373,9 @@ class ImapConnection {
|
|||
*/
|
||||
List<ImapResponse> executeComplexCommand(List<String> commands, boolean sensitive)
|
||||
throws IOException, MessagingException {
|
||||
sendComplexCommand(commands, sensitive);
|
||||
return getCommandResponses();
|
||||
}
|
||||
sendComplexCommand(commands, sensitive);
|
||||
return getCommandResponses();
|
||||
}
|
||||
|
||||
/**
|
||||
* Query server for capabilities.
|
||||
|
@ -374,7 +411,8 @@ class ImapConnection {
|
|||
|
||||
// Assign user-agent string (for RFC2971 ID command)
|
||||
String mUserAgent =
|
||||
ImapStore.getImapId(mImapStore.getContext(), mUsername, host, capabilities);
|
||||
ImapStore.getImapId(mImapStore.getContext(), mImapStore.getUsername(), host,
|
||||
capabilities);
|
||||
|
||||
if (mUserAgent != null) {
|
||||
mIdPhrase = ImapConstants.ID + " (" + mUserAgent + ")";
|
||||
|
@ -441,9 +479,13 @@ class ImapConnection {
|
|||
private void doLogin()
|
||||
throws IOException, MessagingException, AuthenticationFailedException {
|
||||
try {
|
||||
// TODO eventually we need to add additional authentication
|
||||
// options such as SASL
|
||||
executeSimpleCommand(mLoginPhrase, true);
|
||||
if (mImapStore.getUseOAuth()) {
|
||||
// SASL authentication can take multiple steps. Currently the only SASL
|
||||
// authentication supported is OAuth.
|
||||
doSASLAuth();
|
||||
} else {
|
||||
executeSimpleCommand(getLoginPhrase(), true);
|
||||
}
|
||||
} catch (ImapException ie) {
|
||||
if (MailActivityEmail.DEBUG) {
|
||||
LogUtils.d(Logging.LOG_TAG, ie, "ImapException");
|
||||
|
@ -455,6 +497,56 @@ class ImapConnection {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs an SASL authentication. Currently, the only type of SASL authentication supported
|
||||
* is OAuth.
|
||||
* @throws MessagingException
|
||||
* @throws IOException
|
||||
*/
|
||||
private void doSASLAuth() throws MessagingException, IOException {
|
||||
LogUtils.d(Logging.LOG_TAG, "doSASLAuth");
|
||||
ImapResponse response = getOAuthResponse();
|
||||
if (!response.isOk()) {
|
||||
// Failed to authenticate. This may be just due to an expired token.
|
||||
LogUtils.d(Logging.LOG_TAG, "failed to authenticate, retrying");
|
||||
destroyResponses();
|
||||
// Clear the login phrase, this will force us to refresh the auth token.
|
||||
mLoginPhrase = null;
|
||||
// Close the transport so that we'll retry the authentication.
|
||||
if (mTransport != null) {
|
||||
mTransport.close();
|
||||
mTransport = null;
|
||||
}
|
||||
response = getOAuthResponse();
|
||||
if (!response.isOk()) {
|
||||
LogUtils.d(Logging.LOG_TAG, "failed to authenticate, giving up");
|
||||
destroyResponses();
|
||||
throw new AuthenticationFailedException("OAuth failed after refresh");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private ImapResponse getOAuthResponse() throws IOException, MessagingException {
|
||||
ImapResponse response;
|
||||
LogUtils.d(Logging.LOG_TAG, "sending command %s", getLoginPhrase());
|
||||
sendCommand(getLoginPhrase(), true);
|
||||
do {
|
||||
response = mParser.readResponse();
|
||||
} while (!response.isTagged() && !response.isContinuationRequest());
|
||||
|
||||
if (response.isContinuationRequest()) {
|
||||
// SASL allows for a challenge/response type authentication, so if it doesn't yet have
|
||||
// enough info, it will send back a continuation request.
|
||||
// Currently, the only type of authentication we support is OAuth. The only case where
|
||||
// it will send a continuation request is when we fail to authenticate. We need to
|
||||
// reply with a CR/LF, and it will then return with a NO response.
|
||||
sendCommand("", true);
|
||||
response = readResponse();
|
||||
}
|
||||
return response;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the path separator per the LIST command in RFC 3501. If the path separator
|
||||
* was obtained while obtaining the namespace or there is no prefix defined, this
|
||||
|
@ -514,4 +606,4 @@ class ImapConnection {
|
|||
void logLastDiscourse() {
|
||||
mDiscourse.logLastDiscourse();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -39,6 +39,7 @@ import com.android.emailcommon.mail.Folder;
|
|||
import com.android.emailcommon.mail.Message;
|
||||
import com.android.emailcommon.mail.MessagingException;
|
||||
import com.android.emailcommon.provider.Account;
|
||||
import com.android.emailcommon.provider.Credential;
|
||||
import com.android.emailcommon.provider.HostAuth;
|
||||
import com.android.emailcommon.provider.Mailbox;
|
||||
import com.android.emailcommon.service.EmailServiceProxy;
|
||||
|
@ -86,6 +87,8 @@ public class ImapStore extends Store {
|
|||
@VisibleForTesting String mPathPrefix;
|
||||
@VisibleForTesting String mPathSeparator;
|
||||
|
||||
private boolean mUseOAuth;
|
||||
|
||||
private final ConcurrentLinkedQueue<ImapConnection> mConnectionPool =
|
||||
new ConcurrentLinkedQueue<ImapConnection>();
|
||||
|
||||
|
@ -118,9 +121,23 @@ public class ImapStore extends Store {
|
|||
mUsername = null;
|
||||
mPassword = null;
|
||||
}
|
||||
final Credential cred = recvAuth.getCredential(context);
|
||||
mUseOAuth = (cred != null);
|
||||
mPathPrefix = recvAuth.mDomain;
|
||||
}
|
||||
|
||||
boolean getUseOAuth() {
|
||||
return mUseOAuth;
|
||||
}
|
||||
|
||||
String getUsername() {
|
||||
return mUsername;
|
||||
}
|
||||
|
||||
String getPassword() {
|
||||
return mPassword;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
Collection<ImapConnection> getConnectionPoolForTest() {
|
||||
return mConnectionPool;
|
||||
|
@ -374,7 +391,7 @@ public class ImapStore extends Store {
|
|||
// using it.
|
||||
ImapConnection connection = getConnection();
|
||||
try {
|
||||
HashMap<String, ImapFolder> mailboxes = new HashMap<String, ImapFolder>();
|
||||
final HashMap<String, ImapFolder> mailboxes = new HashMap<String, ImapFolder>();
|
||||
// Establish a connection to the IMAP server; if necessary
|
||||
// This ensures a valid prefix if the prefix is automatically set by the server
|
||||
connection.executeSimpleCommand(ImapConstants.NOOP);
|
||||
|
@ -420,7 +437,7 @@ public class ImapStore extends Store {
|
|||
return mailboxes.values().toArray(new Folder[] {});
|
||||
} catch (IOException ioe) {
|
||||
connection.close();
|
||||
throw new MessagingException("Unable to get folder list.", ioe);
|
||||
throw new MessagingException("Unable to get folder list", ioe);
|
||||
} catch (AuthenticationFailedException afe) {
|
||||
// We do NOT want this connection pooled, or we will continue to send NOOP and SELECT
|
||||
// commands to the server
|
||||
|
@ -429,6 +446,8 @@ public class ImapStore extends Store {
|
|||
throw afe;
|
||||
} finally {
|
||||
if (connection != null) {
|
||||
// We keep our connection out of the pool as long as we are using it, then
|
||||
// put it back into the pool so it can be reused.
|
||||
poolConnection(connection);
|
||||
}
|
||||
}
|
||||
|
@ -438,7 +457,10 @@ public class ImapStore extends Store {
|
|||
public Bundle checkSettings() throws MessagingException {
|
||||
int result = MessagingException.NO_ERROR;
|
||||
Bundle bundle = new Bundle();
|
||||
ImapConnection connection = new ImapConnection(this, mUsername, mPassword);
|
||||
// TODO: why doesn't this use getConnection()? I guess this is only done during setup,
|
||||
// so there's need to look for a pooled connection?
|
||||
// But then why doesn't it use poolConnection() after it's done?
|
||||
ImapConnection connection = new ImapConnection(this);
|
||||
try {
|
||||
connection.open();
|
||||
connection.close();
|
||||
|
@ -497,10 +519,13 @@ public class ImapStore extends Store {
|
|||
* Gets a connection if one is available from the pool, or creates a new one if not.
|
||||
*/
|
||||
ImapConnection getConnection() {
|
||||
// TODO Why would we ever have (or need to have) more than one active connection?
|
||||
// TODO We set new username/password each time, but we don't actually close the transport
|
||||
// when we do this. So if that information has changed, this connection will fail.
|
||||
ImapConnection connection = null;
|
||||
while ((connection = mConnectionPool.poll()) != null) {
|
||||
try {
|
||||
connection.setStore(this, mUsername, mPassword);
|
||||
connection.setStore(this);
|
||||
connection.executeSimpleCommand(ImapConstants.NOOP);
|
||||
break;
|
||||
} catch (MessagingException e) {
|
||||
|
@ -511,8 +536,9 @@ public class ImapStore extends Store {
|
|||
connection.close();
|
||||
connection = null;
|
||||
}
|
||||
|
||||
if (connection == null) {
|
||||
connection = new ImapConnection(this, mUsername, mPassword);
|
||||
connection = new ImapConnection(this);
|
||||
}
|
||||
return connection;
|
||||
}
|
||||
|
|
|
@ -32,6 +32,7 @@ public final class ImapConstants {
|
|||
|
||||
public static final String ALERT = "ALERT";
|
||||
public static final String APPEND = "APPEND";
|
||||
public static final String AUTHENTICATE = "AUTHENTICATE";
|
||||
public static final String BAD = "BAD";
|
||||
public static final String BADCHARSET = "BADCHARSET";
|
||||
public static final String BODY = "BODY";
|
||||
|
@ -92,6 +93,7 @@ public final class ImapConstants {
|
|||
public static final String UIDVALIDITY = "UIDVALIDITY";
|
||||
public static final String UNSEEN = "UNSEEN";
|
||||
public static final String UNSUBSCRIBE = "UNSUBSCRIBE";
|
||||
public static final String XOAUTH2 = "XOAUTH2";
|
||||
public static final String APPENDUID = "APPENDUID";
|
||||
public static final String NIL = "NIL";
|
||||
}
|
||||
|
|
|
@ -20,6 +20,8 @@ import android.content.Context;
|
|||
import android.util.Base64;
|
||||
|
||||
import com.android.email.mail.Sender;
|
||||
import com.android.email.mail.internet.AuthenticationCache;
|
||||
import com.android.email.mail.store.imap.ImapConstants;
|
||||
import com.android.email2.ui.MailActivityEmail;
|
||||
import com.android.emailcommon.Logging;
|
||||
import com.android.emailcommon.internet.Rfc822Output;
|
||||
|
@ -28,6 +30,7 @@ import com.android.emailcommon.mail.AuthenticationFailedException;
|
|||
import com.android.emailcommon.mail.CertificateValidationException;
|
||||
import com.android.emailcommon.mail.MessagingException;
|
||||
import com.android.emailcommon.provider.Account;
|
||||
import com.android.emailcommon.provider.Credential;
|
||||
import com.android.emailcommon.provider.EmailContent.Message;
|
||||
import com.android.emailcommon.provider.HostAuth;
|
||||
import com.android.emailcommon.utility.EOLConvertingOutputStream;
|
||||
|
@ -46,8 +49,10 @@ public class SmtpSender extends Sender {
|
|||
|
||||
private final Context mContext;
|
||||
private MailTransport mTransport;
|
||||
private Account mAccount;
|
||||
private String mUsername;
|
||||
private String mPassword;
|
||||
private boolean mUseOAuth;
|
||||
|
||||
/**
|
||||
* Static named constructor.
|
||||
|
@ -61,6 +66,7 @@ public class SmtpSender extends Sender {
|
|||
*/
|
||||
public SmtpSender(Context context, Account account) {
|
||||
mContext = context;
|
||||
mAccount = account;
|
||||
HostAuth sendAuth = account.getOrCreateHostAuthSend(context);
|
||||
mTransport = new MailTransport(context, "SMTP", sendAuth);
|
||||
String[] userInfoParts = sendAuth.getLogin();
|
||||
|
@ -68,6 +74,10 @@ public class SmtpSender extends Sender {
|
|||
mUsername = userInfoParts[0];
|
||||
mPassword = userInfoParts[1];
|
||||
}
|
||||
Credential cred = sendAuth.getCredential(context);
|
||||
if (cred != null) {
|
||||
mUseOAuth = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -133,8 +143,15 @@ public class SmtpSender extends Sender {
|
|||
*/
|
||||
boolean authLoginSupported = result.matches(".*AUTH.*LOGIN.*$");
|
||||
boolean authPlainSupported = result.matches(".*AUTH.*PLAIN.*$");
|
||||
boolean authOAuthSupported = result.matches(".*AUTH.*XOAUTH2.*$");
|
||||
|
||||
if (mUsername != null && mUsername.length() > 0 && mPassword != null
|
||||
if (mUseOAuth) {
|
||||
if (!authOAuthSupported) {
|
||||
LogUtils.w(Logging.LOG_TAG, "OAuth requested, but not supported.");
|
||||
throw new MessagingException(MessagingException.OAUTH_NOT_SUPPORTED);
|
||||
}
|
||||
saslAuthOAuth(mUsername);
|
||||
} else if (mUsername != null && mUsername.length() > 0 && mPassword != null
|
||||
&& mPassword.length() > 0) {
|
||||
if (authPlainSupported) {
|
||||
saslAuthPlain(mUsername, mPassword);
|
||||
|
@ -143,11 +160,15 @@ public class SmtpSender extends Sender {
|
|||
saslAuthLogin(mUsername, mPassword);
|
||||
}
|
||||
else {
|
||||
if (MailActivityEmail.DEBUG) {
|
||||
LogUtils.d(Logging.LOG_TAG, "No valid authentication mechanism found.");
|
||||
}
|
||||
LogUtils.w(Logging.LOG_TAG, "No valid authentication mechanism found.");
|
||||
throw new MessagingException(MessagingException.AUTH_REQUIRED);
|
||||
}
|
||||
} else {
|
||||
// TODO: STOPSHIP Currently, if we have no username or password, we skip
|
||||
// the authentication step. We need to figure out if this is intentional and/or
|
||||
// desirable.
|
||||
//LogUtils.w(Logging.LOG_TAG, "No valid username and password found.");
|
||||
//throw new MessagingException(MessagingException.AUTH_REQUIRED);
|
||||
}
|
||||
} catch (SSLException e) {
|
||||
if (MailActivityEmail.DEBUG) {
|
||||
|
@ -308,4 +329,32 @@ public class SmtpSender extends Sender {
|
|||
throw me;
|
||||
}
|
||||
}
|
||||
|
||||
private void saslAuthOAuth(String username) throws MessagingException,
|
||||
AuthenticationFailedException, IOException {
|
||||
final AuthenticationCache cache = AuthenticationCache.getInstance();
|
||||
String accessToken = cache.retrieveAccessToken(mContext, mAccount);
|
||||
try {
|
||||
saslAuthOAuth(username, accessToken);
|
||||
} catch (AuthenticationFailedException e) {
|
||||
accessToken = cache.refreshAccessToken(mContext, mAccount);
|
||||
saslAuthOAuth(username, accessToken);
|
||||
}
|
||||
}
|
||||
|
||||
private void saslAuthOAuth(final String username, final String accessToken) throws IOException,
|
||||
MessagingException {
|
||||
final String authPhrase = "user=" + username + '\001' + "auth=Bearer " + accessToken +
|
||||
'\001' + '\001';
|
||||
byte[] data = Base64.encode(authPhrase.getBytes(), Base64.NO_WRAP);
|
||||
try {
|
||||
executeSensitiveCommand("AUTH XOAUTH2 " + new String(data),
|
||||
"AUTH XOAUTH2 /redacted/");
|
||||
} catch (MessagingException me) {
|
||||
if (me.getMessage().length() > 1 && me.getMessage().charAt(1) == '3') {
|
||||
throw new AuthenticationFailedException(me.getMessage());
|
||||
}
|
||||
throw me;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1182,6 +1182,7 @@ public class EmailProvider extends ContentProvider {
|
|||
case MAILBOX_ID:
|
||||
case ACCOUNT_ID:
|
||||
case HOSTAUTH_ID:
|
||||
case CREDENTIAL_ID:
|
||||
case POLICY_ID:
|
||||
return new MatrixCursorWithCachedColumns(projection, 0);
|
||||
}
|
||||
|
@ -1262,6 +1263,7 @@ public class EmailProvider extends ContentProvider {
|
|||
case MAILBOX:
|
||||
case ACCOUNT:
|
||||
case HOSTAUTH:
|
||||
case CREDENTIAL:
|
||||
case POLICY:
|
||||
c = db.query(tableName, projection,
|
||||
selection, selectionArgs, null, null, sortOrder, limit);
|
||||
|
@ -1277,6 +1279,7 @@ public class EmailProvider extends ContentProvider {
|
|||
case MAILBOX_ID:
|
||||
case ACCOUNT_ID:
|
||||
case HOSTAUTH_ID:
|
||||
case CREDENTIAL_ID:
|
||||
case POLICY_ID:
|
||||
id = uri.getPathSegments().get(1);
|
||||
c = db.query(tableName, projection, whereWithId(id, selection),
|
||||
|
@ -1763,6 +1766,7 @@ public class EmailProvider extends ContentProvider {
|
|||
case MAILBOX_ID:
|
||||
case ACCOUNT_ID:
|
||||
case HOSTAUTH_ID:
|
||||
case CREDENTIAL_ID:
|
||||
case QUICK_RESPONSE_ID:
|
||||
case POLICY_ID:
|
||||
id = uri.getPathSegments().get(1);
|
||||
|
|
Loading…
Reference in New Issue