Use service authenticator

This commit is contained in:
M66B 2020-10-25 22:20:48 +01:00
parent b5443dd4f9
commit 480eaa18f6
14 changed files with 314 additions and 192 deletions

2
FAQ.md
View File

@ -1080,7 +1080,7 @@ This requires contact/account permissions and internet connectivity.
The error *... Authentication failed ... Account not found ...* means that a previously authorized Gmail account was removed from the device.
The errors *... Authentication failed ... No token on refresh ...* means that the Android account manager failed to refresh the authorization of a Gmail account.
The errors *... Authentication failed ... No token ...* means that the Android account manager failed to refresh the authorization of a Gmail account.
The error *... Authentication failed ... Invalid credentials ... network error ...*
means that the Android account manager was not able to refresh the authorization of a Gmail account due to problems with the internet connection

View File

@ -102,6 +102,9 @@ import javax.crypto.SecretKeyFactory;
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;
private DrawerLayout drawerLayout;
@ -773,10 +776,10 @@ public class ActivitySetup extends ActivityBase implements FragmentManager.OnBac
JSONObject jaccount = (JSONObject) jaccounts.get(a);
EntityAccount account = EntityAccount.fromJSON(jaccount);
if (account.auth_type == EmailService.AUTH_TYPE_GMAIL) {
if (account.auth_type == AUTH_TYPE_GMAIL) {
AccountManager am = AccountManager.get(context);
boolean found = false;
for (Account google : am.getAccountsByType(EmailService.TYPE_GOOGLE))
for (Account google : am.getAccountsByType(TYPE_GOOGLE))
if (account.user.equals(google.name)) {
found = true;
break;

View File

@ -64,6 +64,8 @@ import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.List;
import static eu.faircode.email.ServiceAuthenticator.AUTH_TYPE_PASSWORD;
public class AdapterAccount extends RecyclerView.Adapter<AdapterAccount.ViewHolder> {
private Fragment parentFragment;
private boolean settings;
@ -152,7 +154,7 @@ public class AdapterAccount extends RecyclerView.Adapter<AdapterAccount.ViewHold
ivSync.setContentDescription(context.getString(account.synchronize ? R.string.title_legend_synchronize_on : R.string.title_legend_synchronize_off));
ivOAuth.setVisibility(
settings && account.auth_type != EmailService.AUTH_TYPE_PASSWORD ? View.VISIBLE : View.GONE);
settings && account.auth_type != AUTH_TYPE_PASSWORD ? View.VISIBLE : View.GONE);
ivPrimary.setVisibility(account.primary ? View.VISIBLE : View.GONE);
ivNotify.setVisibility(account.notify ? View.VISIBLE : View.GONE);

View File

@ -55,6 +55,8 @@ import java.util.Comparator;
import java.util.List;
import java.util.Locale;
import static eu.faircode.email.ServiceAuthenticator.AUTH_TYPE_PASSWORD;
public class AdapterIdentity extends RecyclerView.Adapter<AdapterIdentity.ViewHolder> {
private Fragment parentFragment;
private Context context;
@ -123,7 +125,7 @@ public class AdapterIdentity extends RecyclerView.Adapter<AdapterIdentity.ViewHo
ivSync.setImageResource(identity.synchronize ? R.drawable.twotone_sync_24 : R.drawable.twotone_sync_disabled_24);
ivSync.setContentDescription(context.getString(identity.synchronize ? R.string.title_legend_synchronize_on : R.string.title_legend_synchronize_off));
ivOAuth.setVisibility(identity.auth_type == EmailService.AUTH_TYPE_PASSWORD ? View.GONE : View.VISIBLE);
ivOAuth.setVisibility(identity.auth_type == AUTH_TYPE_PASSWORD ? View.GONE : View.VISIBLE);
ivPrimary.setVisibility(identity.primary ? View.VISIBLE : View.GONE);
ivGroup.setVisibility(identity.self ? View.GONE : View.VISIBLE);
tvName.setText(identity.getDisplayName());

View File

@ -40,6 +40,8 @@ import javax.mail.internet.InternetAddress;
import io.requery.android.database.sqlite.RequerySQLiteOpenHelperFactory;
import io.requery.android.database.sqlite.SQLiteDatabase;
import static eu.faircode.email.ServiceAuthenticator.AUTH_TYPE_PASSWORD;
/*
This file is part of FairEmail.
@ -1343,8 +1345,8 @@ public abstract class DB extends RoomDatabase {
int previous_version = prefs.getInt("previous_version", -1);
if (previous_version <= 848 && Helper.isPlayStoreInstall()) {
// JavaMail didn't check server certificates
db.execSQL("UPDATE account SET insecure = 1 WHERE auth_type = " + EmailService.AUTH_TYPE_PASSWORD);
db.execSQL("UPDATE identity SET insecure = 1 WHERE auth_type = " + EmailService.AUTH_TYPE_PASSWORD);
db.execSQL("UPDATE account SET insecure = 1 WHERE auth_type = " + AUTH_TYPE_PASSWORD);
db.execSQL("UPDATE identity SET insecure = 1 WHERE auth_type = " + AUTH_TYPE_PASSWORD);
}
}
})

View File

@ -1,9 +1,24 @@
package eu.faircode.email;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.accounts.AuthenticatorException;
import android.accounts.OperationCanceledException;
/*
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 <http://www.gnu.org/licenses/>.
Copyright 2018-2020 by Marcel Bokhorst (M66B)
*/
import android.content.Context;
import android.content.SharedPreferences;
import android.security.KeyChain;
@ -19,13 +34,6 @@ import com.sun.mail.smtp.SMTPTransport;
import com.sun.mail.util.MailConnectException;
import com.sun.mail.util.SocketConnectException;
import net.openid.appauth.AuthState;
import net.openid.appauth.AuthorizationException;
import net.openid.appauth.AuthorizationService;
import net.openid.appauth.ClientAuthentication;
import net.openid.appauth.ClientSecretPost;
import net.openid.appauth.NoClientAuthentication;
import org.bouncycastle.asn1.DEROctetString;
import org.bouncycastle.asn1.x509.Extension;
import org.bouncycastle.asn1.x509.GeneralName;
@ -63,10 +71,10 @@ import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Semaphore;
import java.util.regex.Pattern;
import javax.mail.AuthenticationFailedException;
import javax.mail.Authenticator;
import javax.mail.Folder;
import javax.mail.MessagingException;
import javax.mail.NoSuchProviderException;
@ -86,6 +94,9 @@ import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;
import static eu.faircode.email.ServiceAuthenticator.AUTH_TYPE_GMAIL;
import static eu.faircode.email.ServiceAuthenticator.AUTH_TYPE_OAUTH;
// IMAP standards: https://imapwiki.org/Specs
public class EmailService implements AutoCloseable {
@ -105,12 +116,6 @@ public class EmailService implements AutoCloseable {
private ExecutorService executor = Helper.getBackgroundExecutor(0, "mail");
static final String TYPE_GOOGLE = "com.google";
static final int AUTH_TYPE_PASSWORD = 1;
static final int AUTH_TYPE_GMAIL = 2;
static final int AUTH_TYPE_OAUTH = 3;
static final int PURPOSE_CHECK = 1;
static final int PURPOSE_USE = 2;
static final int PURPOSE_SEARCH = 3;
@ -278,35 +283,50 @@ public class EmailService implements AutoCloseable {
}
public void connect(EntityAccount account) throws MessagingException {
String password = connect(
connect(
account.host, account.port,
account.auth_type, account.provider,
account.user, account.password,
new ServiceAuthenticator.IAuthenticated() {
@Override
public void onPasswordChanged(String newPassword) {
DB db = DB.getInstance(context);
int count = db.account().setAccountPassword(account.id, newPassword);
EntityLog.log(context, account.name + " token refreshed=" + count);
}
},
account.certificate_alias, account.fingerprint);
if (password != null) {
DB db = DB.getInstance(context);
int count = db.account().setAccountPassword(account.id, password);
Log.i(account.name + " token refreshed=" + count);
}
}
public void connect(EntityIdentity identity) throws MessagingException {
String password = connect(
connect(
identity.host, identity.port,
identity.auth_type, identity.provider,
identity.user, identity.password,
new ServiceAuthenticator.IAuthenticated() {
@Override
public void onPasswordChanged(String newPassword) {
DB db = DB.getInstance(context);
int count = db.identity().setIdentityPassword(identity.id, newPassword);
EntityLog.log(context, identity.email + " token refreshed=" + count);
}
},
identity.certificate_alias, identity.fingerprint);
if (password != null) {
DB db = DB.getInstance(context);
int count = db.identity().setIdentityPassword(identity.id, password);
Log.i(identity.email + " token refreshed=" + count);
}
}
public String connect(
public void connect(
String host, int port,
int auth, String provider, String user, String password,
String certificate, String fingerprint) throws MessagingException {
connect(host, port, auth, provider, user, password, null, certificate, fingerprint);
}
private void connect(
String host, int port,
int auth, String provider, String user, String password,
ServiceAuthenticator.IAuthenticated intf,
String certificate, String fingerprint) throws MessagingException {
SSLSocketFactoryService factory = null;
try {
PrivateKey key = null;
@ -332,35 +352,27 @@ public class EmailService implements AutoCloseable {
Log.e("Trust issues", ex);
}
properties.put("mail." + protocol + ".forcepasswordrefresh", "true");
ServiceAuthenticator authenticator = new ServiceAuthenticator(context, auth, provider, user, password, intf);
try {
if (auth == AUTH_TYPE_GMAIL || auth == AUTH_TYPE_OAUTH)
properties.put("mail." + protocol + ".auth.mechanisms", "XOAUTH2");
if (auth == AUTH_TYPE_OAUTH) {
if ("imap.mail.yahoo.com".equals(host))
properties.put("mail." + protocol + ".yahoo.guid", "FAIRMAIL_V1");
AuthState authState = OAuthRefresh(context, provider, password);
connect(host, port, auth, user, authState.getAccessToken(), factory);
return authState.jsonSerializeString();
} else {
connect(host, port, auth, user, password, factory);
return null;
}
if (auth == AUTH_TYPE_OAUTH && "imap.mail.yahoo.com".equals(host))
properties.put("mail." + protocol + ".yahoo.guid", "FAIRMAIL_V1");
connect(host, port, auth, user, authenticator, factory);
} catch (AuthenticationFailedException ex) {
// Refresh token
if (auth == AUTH_TYPE_GMAIL)
if (auth == AUTH_TYPE_GMAIL || auth == AUTH_TYPE_OAUTH) {
try {
String token = GmailRefresh(context, user, password);
connect(host, port, auth, user, token, factory);
return token;
authenticator.expire();
connect(host, port, auth, user, authenticator, factory);
} catch (Exception ex1) {
Log.e(ex1);
throw new AuthenticationFailedException(ex.getMessage(), ex1);
}
else if (auth == AUTH_TYPE_OAUTH) {
AuthState authState = OAuthRefresh(context, provider, password);
connect(host, port, auth, user, authState.getAccessToken(), factory);
return authState.jsonSerializeString();
} else if (purpose == PURPOSE_CHECK) {
String msg = ex.getMessage();
if (msg != null)
@ -407,7 +419,7 @@ public class EmailService implements AutoCloseable {
private void connect(
String host, int port, int auth,
String user, String password,
String user, Authenticator authenticator,
SSLSocketFactoryService factory) throws MessagingException {
InetAddress main = null;
boolean require_id = (purpose == PURPOSE_CHECK &&
@ -421,7 +433,7 @@ public class EmailService implements AutoCloseable {
main = InetAddress.getByName(host);
EntityLog.log(context, "Connecting to " + main);
_connect(main, port, require_id, user, password, factory);
_connect(main, port, require_id, user, authenticator, factory);
} catch (UnknownHostException ex) {
throw new MessagingException(ex.getMessage(), ex);
} catch (MessagingException ex) {
@ -486,7 +498,7 @@ public class EmailService implements AutoCloseable {
try {
EntityLog.log(context, "Falling back to " + iaddr);
_connect(iaddr, port, require_id, user, password, factory);
_connect(iaddr, port, require_id, user, authenticator, factory);
return;
} catch (MessagingException ex1) {
ex = ex1;
@ -505,9 +517,9 @@ public class EmailService implements AutoCloseable {
private void _connect(
InetAddress address, int port, boolean require_id,
String user, String password,
String user, Authenticator authenticator,
SSLSocketFactoryService factory) throws MessagingException {
isession = Session.getInstance(properties, null);
isession = Session.getInstance(properties, authenticator);
isession.setDebug(debug || log);
if (debug || log)
@ -538,13 +550,13 @@ public class EmailService implements AutoCloseable {
if ("pop3".equals(protocol) || "pop3s".equals(protocol)) {
iservice = isession.getStore(protocol);
iservice.connect(address.getHostAddress(), port, user, password);
iservice.connect(address.getHostAddress(), port, user, null);
} else if ("imap".equals(protocol) || "imaps".equals(protocol) || "gimaps".equals(protocol)) {
iservice = isession.getStore(protocol);
if (listener != null)
((IMAPStore) iservice).addStoreListener(listener);
iservice.connect(address.getHostAddress(), port, user, password);
iservice.connect(address.getHostAddress(), port, user, null);
// https://www.ietf.org/rfc/rfc2971.txt
IMAPStore istore = (IMAPStore) getStore();
@ -580,13 +592,13 @@ public class EmailService implements AutoCloseable {
iservice = isession.getTransport(protocol);
try {
iservice.connect(address.getHostAddress(), port, user, password);
iservice.connect(address.getHostAddress(), port, user, null);
} catch (MessagingException ex) {
if (ehlo == null && ConnectionHelper.isSyntacticallyInvalid(ex)) {
properties.put("mail." + protocol + ".localhost", useip ? hdomain : haddr);
Log.i("Fallback localhost=" + properties.getProperty("mail." + protocol + ".localhost"));
try {
iservice.connect(address.getHostAddress(), port, user, password);
iservice.connect(address.getHostAddress(), port, user, null);
} catch (MessagingException ex1) {
if (ConnectionHelper.isSyntacticallyInvalid(ex1))
Log.e("Used localhost=" + haddr + "/" + hdomain);
@ -605,75 +617,6 @@ public class EmailService implements AutoCloseable {
return TextUtils.join(".", c);
}
private static class ErrorHolder {
AuthorizationException error;
}
private static String GmailRefresh(Context context, String user, String password) 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("Refreshing token user=" + user);
if (password != null)
am.invalidateAuthToken(TYPE_GOOGLE, password);
String token = am.blockingGetAuthToken(account, getAuthTokenType(TYPE_GOOGLE), true);
if (token == null)
throw new AuthenticatorException("No token on refresh for " + user);
return token;
}
throw new AuthenticatorException("Account not found for " + user);
}
private static AuthState OAuthRefresh(Context context, String id, String json) throws MessagingException {
try {
AuthState authState = AuthState.jsonDeserialize(json);
ClientAuthentication clientAuth;
EmailProvider provider = EmailProvider.getProvider(context, id);
if (provider.oauth.clientSecret == null)
clientAuth = NoClientAuthentication.INSTANCE;
else
clientAuth = new ClientSecretPost(provider.oauth.clientSecret);
ErrorHolder holder = new ErrorHolder();
Semaphore semaphore = new Semaphore(0);
Log.i("OAuth refresh");
AuthorizationService authService = new AuthorizationService(context);
authState.performActionWithFreshTokens(
authService,
clientAuth,
new AuthState.AuthStateAction() {
@Override
public void execute(String accessToken, String idToken, AuthorizationException error) {
if (error != null)
holder.error = error;
semaphore.release();
}
});
semaphore.acquire();
Log.i("OAuth refreshed");
if (holder.error != null)
throw holder.error;
return authState;
} catch (Exception ex) {
throw new MessagingException("OAuth refresh", ex);
}
}
static String getAuthTokenType(String type) {
// https://developers.google.com/gmail/imap/xoauth2-protocol
if ("com.google".equals(type))
return "oauth2:https://mail.google.com/";
return null;
}
List<EntityFolder> getFolders() throws MessagingException {
List<EntityFolder> folders = new ArrayList<>();

View File

@ -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.NotificationManager;
import android.content.Context;
import android.content.Intent;
@ -74,6 +72,9 @@ import javax.mail.Folder;
import static android.app.Activity.RESULT_OK;
import static com.google.android.material.textfield.TextInputLayout.END_ICON_NONE;
import static com.google.android.material.textfield.TextInputLayout.END_ICON_PASSWORD_TOGGLE;
import static eu.faircode.email.ServiceAuthenticator.AUTH_TYPE_GMAIL;
import static eu.faircode.email.ServiceAuthenticator.AUTH_TYPE_OAUTH;
import static eu.faircode.email.ServiceAuthenticator.AUTH_TYPE_PASSWORD;
public class FragmentAccount extends FragmentBase {
private ViewGroup view;
@ -153,7 +154,7 @@ public class FragmentAccount extends FragmentBase {
private long id = -1;
private long copy = -1;
private int auth = EmailService.AUTH_TYPE_PASSWORD;
private int auth = AUTH_TYPE_PASSWORD;
private String provider = null;
private String certificate = null;
private boolean saving = false;
@ -261,7 +262,7 @@ public class FragmentAccount extends FragmentBase {
public void onItemSelected(AdapterView<?> adapterView, View view, int position, long itemid) {
EmailProvider provider = (EmailProvider) adapterView.getSelectedItem();
tvGmailHint.setVisibility(
auth == EmailService.AUTH_TYPE_PASSWORD && "gmail".equals(provider.id)
auth == AUTH_TYPE_PASSWORD && "gmail".equals(provider.id)
? View.VISIBLE : View.GONE);
grpServer.setVisibility(position > 0 ? View.VISIBLE : View.GONE);
grpAuthorize.setVisibility(position > 0 ? View.VISIBLE : View.GONE);
@ -1333,16 +1334,7 @@ public class FragmentAccount extends FragmentBase {
if (account == null)
return null;
AccountManager am = AccountManager.get(context);
Account[] accounts = am.getAccountsByType(EmailService.TYPE_GOOGLE);
for (Account google : accounts)
if (account.user.equals(google.name))
return am.blockingGetAuthToken(
google,
EmailService.getAuthTokenType(EmailService.TYPE_GOOGLE),
true);
return null;
return ServiceAuthenticator.getGmailToken(context, account.user);
}
@Override
@ -1449,7 +1441,7 @@ public class FragmentAccount extends FragmentBase {
boolean found = false;
for (int pos = 2; pos < providers.size(); pos++) {
EmailProvider provider = providers.get(pos);
if ((provider.oauth != null) == (account.auth_type == EmailService.AUTH_TYPE_OAUTH) &&
if ((provider.oauth != null) == (account.auth_type == AUTH_TYPE_OAUTH) &&
provider.imap.host.equals(account.host) &&
provider.imap.port == account.port &&
provider.imap.starttls == (account.encryption == EmailService.ENCRYPTION_STARTTLS)) {
@ -1514,7 +1506,7 @@ public class FragmentAccount extends FragmentBase {
else
rgDate.check(R.id.radio_server_time);
auth = (account == null ? EmailService.AUTH_TYPE_PASSWORD : account.auth_type);
auth = (account == null ? AUTH_TYPE_PASSWORD : account.auth_type);
provider = (account == null ? null : account.provider);
new SimpleTask<EntityAccount>() {
@ -1550,13 +1542,13 @@ public class FragmentAccount extends FragmentBase {
Helper.setViewsEnabled(view, true);
if (auth != EmailService.AUTH_TYPE_PASSWORD) {
if (auth != AUTH_TYPE_PASSWORD) {
etUser.setEnabled(false);
tilPassword.setEnabled(false);
btnCertificate.setEnabled(false);
}
if (account == null || account.auth_type != EmailService.AUTH_TYPE_GMAIL)
if (account == null || account.auth_type != AUTH_TYPE_GMAIL)
Helper.hide((btnOAuth));
cbOnDemand.setEnabled(cbSynchronize.isChecked());

View File

@ -56,6 +56,8 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import static eu.faircode.email.ServiceAuthenticator.AUTH_TYPE_PASSWORD;
public class FragmentAccounts extends FragmentBase {
private boolean settings;
@ -267,7 +269,7 @@ public class FragmentAccounts extends FragmentBase {
boolean authorized = true;
for (TupleAccountEx account : accounts)
if (account.auth_type != EmailService.AUTH_TYPE_PASSWORD &&
if (account.auth_type != AUTH_TYPE_PASSWORD &&
!Helper.hasPermissions(getContext(), Helper.getOAuthPermissions())) {
authorized = false;
}

View File

@ -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.ServiceAuthenticator.AUTH_TYPE_GMAIL;
public class FragmentGmail extends FragmentBase {
private ViewGroup view;
@ -264,7 +265,7 @@ public class FragmentGmail extends FragmentBase {
Log.i("Requesting token name=" + account.name);
am.getAuthToken(
account,
EmailService.getAuthTokenType(type),
ServiceAuthenticator.getAuthTokenType(type),
new Bundle(),
getActivity(),
new AccountManagerCallback<Bundle>() {
@ -369,7 +370,7 @@ public class FragmentGmail extends FragmentBase {
EmailService.PURPOSE_CHECK, true)) {
iservice.connect(
provider.imap.host, provider.imap.port,
EmailService.AUTH_TYPE_GMAIL, null,
AUTH_TYPE_GMAIL, null,
user, password,
null, null);
@ -384,7 +385,7 @@ public class FragmentGmail extends FragmentBase {
EmailService.PURPOSE_CHECK, true)) {
iservice.connect(
provider.smtp.host, provider.smtp.port,
EmailService.AUTH_TYPE_GMAIL, null,
AUTH_TYPE_GMAIL, null,
user, password,
null, null);
max_size = iservice.getMaxSize();
@ -402,7 +403,7 @@ public class FragmentGmail extends FragmentBase {
account.host = provider.imap.host;
account.encryption = aencryption;
account.port = provider.imap.port;
account.auth_type = EmailService.AUTH_TYPE_GMAIL;
account.auth_type = AUTH_TYPE_GMAIL;
account.user = user;
account.password = password;
@ -451,7 +452,7 @@ public class FragmentGmail extends FragmentBase {
identity.host = provider.smtp.host;
identity.encryption = iencryption;
identity.port = provider.smtp.port;
identity.auth_type = EmailService.AUTH_TYPE_GMAIL;
identity.auth_type = AUTH_TYPE_GMAIL;
identity.user = user;
identity.password = password;
identity.synchronize = true;

View File

@ -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.content.Context;
import android.content.Intent;
import android.graphics.Color;
@ -68,6 +66,9 @@ import javax.mail.internet.InternetAddress;
import static android.app.Activity.RESULT_OK;
import static com.google.android.material.textfield.TextInputLayout.END_ICON_NONE;
import static com.google.android.material.textfield.TextInputLayout.END_ICON_PASSWORD_TOGGLE;
import static eu.faircode.email.ServiceAuthenticator.AUTH_TYPE_GMAIL;
import static eu.faircode.email.ServiceAuthenticator.AUTH_TYPE_OAUTH;
import static eu.faircode.email.ServiceAuthenticator.AUTH_TYPE_PASSWORD;
public class FragmentIdentity extends FragmentBase {
private ViewGroup view;
@ -131,7 +132,7 @@ public class FragmentIdentity extends FragmentBase {
private long id = -1;
private long copy = -1;
private long account = -1;
private int auth = EmailService.AUTH_TYPE_PASSWORD;
private int auth = AUTH_TYPE_PASSWORD;
private String provider = null;
private String certificate = null;
private String signature = null;
@ -498,9 +499,9 @@ public class FragmentIdentity extends FragmentBase {
etRealm.setText(account.realm);
cbTrust.setChecked(false);
etUser.setEnabled(auth == EmailService.AUTH_TYPE_PASSWORD);
tilPassword.setEnabled(auth == EmailService.AUTH_TYPE_PASSWORD);
btnCertificate.setEnabled(auth == EmailService.AUTH_TYPE_PASSWORD);
etUser.setEnabled(auth == AUTH_TYPE_PASSWORD);
tilPassword.setEnabled(auth == AUTH_TYPE_PASSWORD);
btnCertificate.setEnabled(auth == AUTH_TYPE_PASSWORD);
}
private void setProvider(EmailProvider provider) {
@ -986,16 +987,7 @@ public class FragmentIdentity extends FragmentBase {
if (identity == null)
return null;
AccountManager am = AccountManager.get(context);
Account[] accounts = am.getAccountsByType(EmailService.TYPE_GOOGLE);
for (Account google : accounts)
if (identity.user.equals(google.name))
return am.blockingGetAuthToken(
google,
EmailService.getAuthTokenType(EmailService.TYPE_GOOGLE),
true);
return null;
return ServiceAuthenticator.getGmailToken(context, identity.user);
}
@Override
@ -1137,7 +1129,7 @@ public class FragmentIdentity extends FragmentBase {
etBcc.setText(identity == null ? null : identity.bcc);
cbUnicode.setChecked(identity != null && identity.unicode);
auth = (identity == null ? EmailService.AUTH_TYPE_PASSWORD : identity.auth_type);
auth = (identity == null ? AUTH_TYPE_PASSWORD : identity.auth_type);
provider = (identity == null ? null : identity.provider);
if (identity == null || copy > 0)
@ -1171,13 +1163,13 @@ public class FragmentIdentity extends FragmentBase {
Helper.setViewsEnabled(view, true);
if (auth != EmailService.AUTH_TYPE_PASSWORD) {
if (auth != AUTH_TYPE_PASSWORD) {
etUser.setEnabled(false);
tilPassword.setEnabled(false);
btnCertificate.setEnabled(false);
}
if (identity == null || identity.auth_type != EmailService.AUTH_TYPE_GMAIL)
if (identity == null || identity.auth_type != AUTH_TYPE_GMAIL)
Helper.hide(btnOAuth);
cbPrimary.setEnabled(cbSynchronize.isChecked());
@ -1197,7 +1189,7 @@ public class FragmentIdentity extends FragmentBase {
if (identity != null)
for (int pos = 1; pos < providers.size(); pos++) {
EmailProvider provider = providers.get(pos);
if ((provider.oauth != null) == (identity.auth_type == EmailService.AUTH_TYPE_OAUTH) &&
if ((provider.oauth != null) == (identity.auth_type == AUTH_TYPE_OAUTH) &&
provider.smtp.host.equals(identity.host) &&
provider.smtp.port == identity.port &&
provider.smtp.starttls == (identity.encryption == EmailService.ENCRYPTION_STARTTLS)) {
@ -1226,7 +1218,7 @@ public class FragmentIdentity extends FragmentBase {
EntityAccount unselected = new EntityAccount();
unselected.id = -1L;
unselected.auth_type = EmailService.AUTH_TYPE_PASSWORD;
unselected.auth_type = AUTH_TYPE_PASSWORD;
unselected.name = getString(R.string.title_select);
unselected.primary = false;
accounts.add(0, unselected);

View File

@ -77,6 +77,7 @@ import java.util.Map;
import javax.mail.AuthenticationFailedException;
import static android.app.Activity.RESULT_OK;
import static eu.faircode.email.ServiceAuthenticator.AUTH_TYPE_OAUTH;
public class FragmentOAuth extends FragmentBase {
private String id;
@ -434,7 +435,7 @@ public class FragmentOAuth extends FragmentBase {
EmailService.PURPOSE_CHECK, true)) {
iservice.connect(
provider.imap.host, provider.imap.port,
EmailService.AUTH_TYPE_OAUTH, provider.id,
AUTH_TYPE_OAUTH, provider.id,
unique_name, state,
null, null);
username = unique_name;
@ -486,7 +487,7 @@ public class FragmentOAuth extends FragmentBase {
EmailService.PURPOSE_CHECK, true)) {
iservice.connect(
provider.imap.host, provider.imap.port,
EmailService.AUTH_TYPE_OAUTH, provider.id,
AUTH_TYPE_OAUTH, provider.id,
username, state,
null, null);
@ -503,7 +504,7 @@ public class FragmentOAuth extends FragmentBase {
EmailService.PURPOSE_CHECK, true)) {
iservice.connect(
provider.smtp.host, provider.smtp.port,
EmailService.AUTH_TYPE_OAUTH, provider.id,
AUTH_TYPE_OAUTH, provider.id,
username, state,
null, null);
max_size = iservice.getMaxSize();
@ -523,7 +524,7 @@ public class FragmentOAuth extends FragmentBase {
account.host = provider.imap.host;
account.encryption = aencryption;
account.port = provider.imap.port;
account.auth_type = EmailService.AUTH_TYPE_OAUTH;
account.auth_type = AUTH_TYPE_OAUTH;
account.provider = provider.id;
account.user = username;
account.password = state;
@ -579,7 +580,7 @@ public class FragmentOAuth extends FragmentBase {
ident.host = provider.smtp.host;
ident.encryption = iencryption;
ident.port = provider.smtp.port;
ident.auth_type = EmailService.AUTH_TYPE_OAUTH;
ident.auth_type = AUTH_TYPE_OAUTH;
ident.provider = provider.id;
ident.user = username;
ident.password = state;

View File

@ -61,6 +61,7 @@ import java.util.Objects;
import static android.app.Activity.RESULT_OK;
import static com.google.android.material.textfield.TextInputLayout.END_ICON_NONE;
import static com.google.android.material.textfield.TextInputLayout.END_ICON_PASSWORD_TOGGLE;
import static eu.faircode.email.ServiceAuthenticator.AUTH_TYPE_PASSWORD;
public class FragmentPop extends FragmentBase {
private ViewGroup view;
@ -383,7 +384,7 @@ public class FragmentPop extends FragmentBase {
EmailService.PURPOSE_CHECK, true)) {
iservice.connect(
host, Integer.parseInt(port),
EmailService.AUTH_TYPE_PASSWORD, null,
AUTH_TYPE_PASSWORD, null,
user, password,
null, null);
}
@ -408,7 +409,7 @@ public class FragmentPop extends FragmentBase {
account.encryption = encryption;
account.insecure = insecure;
account.port = Integer.parseInt(port);
account.auth_type = EmailService.AUTH_TYPE_PASSWORD;
account.auth_type = AUTH_TYPE_PASSWORD;
account.user = user;
account.password = password;

View File

@ -54,6 +54,8 @@ import java.util.List;
import javax.mail.AuthenticationFailedException;
import static eu.faircode.email.ServiceAuthenticator.AUTH_TYPE_PASSWORD;
public class FragmentQuickSetup extends FragmentBase {
private ViewGroup view;
private ScrollView scroll;
@ -303,7 +305,7 @@ public class FragmentQuickSetup extends FragmentBase {
try {
iservice.connect(
provider.imap.host, provider.imap.port,
EmailService.AUTH_TYPE_PASSWORD, null,
AUTH_TYPE_PASSWORD, null,
user, password,
null, imap_fingerprint);
} catch (EmailService.UntrustedException ex) {
@ -311,7 +313,7 @@ public class FragmentQuickSetup extends FragmentBase {
imap_fingerprint = ex.getFingerprint();
iservice.connect(
provider.imap.host, provider.imap.port,
EmailService.AUTH_TYPE_PASSWORD, null,
AUTH_TYPE_PASSWORD, null,
user, password,
null, imap_fingerprint);
} else
@ -328,7 +330,7 @@ public class FragmentQuickSetup extends FragmentBase {
Log.i("Retry with user=" + user);
iservice.connect(
provider.imap.host, provider.imap.port,
EmailService.AUTH_TYPE_PASSWORD, null,
AUTH_TYPE_PASSWORD, null,
user, password,
null, null);
} catch (Throwable ex1) {
@ -353,7 +355,7 @@ public class FragmentQuickSetup extends FragmentBase {
iservice.setUseIp(provider.useip, null);
iservice.connect(
provider.smtp.host, provider.smtp.port,
EmailService.AUTH_TYPE_PASSWORD, null,
AUTH_TYPE_PASSWORD, null,
user, password,
null, smtp_fingerprint);
max_size = iservice.getMaxSize();
@ -382,7 +384,7 @@ public class FragmentQuickSetup extends FragmentBase {
account.host = provider.imap.host;
account.encryption = aencryption;
account.port = provider.imap.port;
account.auth_type = EmailService.AUTH_TYPE_PASSWORD;
account.auth_type = AUTH_TYPE_PASSWORD;
account.user = user;
account.password = password;
account.fingerprint = imap_fingerprint;
@ -432,7 +434,7 @@ public class FragmentQuickSetup extends FragmentBase {
identity.host = provider.smtp.host;
identity.encryption = iencryption;
identity.port = provider.smtp.port;
identity.auth_type = EmailService.AUTH_TYPE_PASSWORD;
identity.auth_type = AUTH_TYPE_PASSWORD;
identity.user = user;
identity.password = password;
identity.fingerprint = smtp_fingerprint;

View File

@ -0,0 +1,179 @@
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 <http://www.gnu.org/licenses/>.
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 net.openid.appauth.AuthState;
import net.openid.appauth.AuthorizationException;
import net.openid.appauth.AuthorizationService;
import net.openid.appauth.ClientAuthentication;
import net.openid.appauth.ClientSecretPost;
import net.openid.appauth.NoClientAuthentication;
import java.io.IOException;
import java.util.Objects;
import java.util.concurrent.Semaphore;
import javax.mail.Authenticator;
import javax.mail.MessagingException;
import javax.mail.PasswordAuthentication;
class ServiceAuthenticator extends Authenticator {
private Context context;
private int auth;
private String provider;
private String user;
private String password;
private IAuthenticated intf;
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";
ServiceAuthenticator(
Context context,
int auth, String provider,
String user, String password,
IAuthenticated intf) {
this.context = context.getApplicationContext();
this.auth = auth;
this.provider = provider;
this.user = user;
this.password = password;
this.intf = intf;
}
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) {
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);
}
} catch (Throwable ex) {
Log.e(ex);
}
Log.i(user + " returning password");
return new PasswordAuthentication(user, token);
}
interface IAuthenticated {
void onPasswordChanged(String newPassword);
}
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;
}
throw new AuthenticatorException("Account not found for " + user);
}
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);
}
}
private static void OAuthRefresh(Context context, String id, AuthState authState) throws MessagingException {
try {
ClientAuthentication clientAuth;
EmailProvider provider = EmailProvider.getProvider(context, id);
if (provider.oauth.clientSecret == null)
clientAuth = NoClientAuthentication.INSTANCE;
else
clientAuth = new ClientSecretPost(provider.oauth.clientSecret);
ErrorHolder holder = new ErrorHolder();
Semaphore semaphore = new Semaphore(0);
Log.i("OAuth refresh");
AuthorizationService authService = new AuthorizationService(context);
authState.performActionWithFreshTokens(
authService,
clientAuth,
new AuthState.AuthStateAction() {
@Override
public void execute(String accessToken, String idToken, AuthorizationException error) {
if (error != null)
holder.error = error;
semaphore.release();
}
});
semaphore.acquire();
Log.i("OAuth refreshed");
if (holder.error != null)
throw holder.error;
} catch (Exception ex) {
throw new MessagingException("OAuth refresh", ex);
}
}
static String getAuthTokenType(String type) {
// https://developers.google.com/gmail/imap/xoauth2-protocol
if ("com.google".equals(type))
return "oauth2:https://mail.google.com/";
return null;
}
private static class ErrorHolder {
AuthorizationException error;
}
}