Bringing back XOAuth2

This commit is contained in:
M66B 2019-09-18 16:34:07 +02:00
parent df2005dfc1
commit 27d7eddddc
20 changed files with 486 additions and 196 deletions

2
FAQ.md
View File

@ -251,6 +251,8 @@ The following Android permissions are needed:
* Optional: *read your contacts* (READ_CONTACTS): to autocomplete addresses and to show photos
* Optional: *read the contents of your SD card* (READ_EXTERNAL_STORAGE): to accept files from other, outdated apps, see also [this FAQ](#user-content-faq49)
* Optional: *use fingerprint hardware* (USE_FINGERPRINT) and use *biometric hardware* (USE_BIOMETRIC): to use biometric authentication
* Optional: *find accounts on the device* (GET_ACCOUNTS): to use [OAuth](https://en.wikipedia.org/wiki/OAuth) instead of passwords
* Android 5.1 Lollipop and before: *use accounts on the device* (USE_CREDENTIALS): needed to select accounts (not used/needed on later Android versions)
The following permissions are needed to show the count of unread messages as a badge (see also [this FAQ](#user-content-faq106)):

View File

@ -12,6 +12,12 @@
<uses-permission android:name="com.android.vending.BILLING" />
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
<!-- OAuth -->
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
<uses-permission
android:name="android.permission.USE_CREDENTIALS"
android:maxSdkVersion="22" />
<!-- https://developer.android.com/guide/topics/manifest/uses-feature-element#features-reference -->
<uses-feature
android:name="android.software.app_widgets"

View File

@ -113,6 +113,7 @@ public class ActivitySetup extends ActivityBase implements FragmentManager.OnBac
static final int REQUEST_SOUND = 2;
static final int REQUEST_EXPORT = 3;
static final int REQUEST_IMPORT = 4;
static final int REQUEST_CHOOSE_ACCOUNT = 5;
static final String ACTION_QUICK_SETUP = BuildConfig.APPLICATION_ID + ".ACTION_QUICK_SETUP";
static final String ACTION_VIEW_ACCOUNTS = BuildConfig.APPLICATION_ID + ".ACTION_VIEW_ACCOUNTS";

View File

@ -83,7 +83,6 @@ public class AdapterAccount extends RecyclerView.Adapter<AdapterAccount.ViewHold
private ImageView ivState;
private TextView tvHost;
private TextView tvLast;
private TextView tvAuthorize;
private TextView tvIdentity;
private TextView tvDrafts;
private TextView tvWarning;
@ -106,7 +105,6 @@ public class AdapterAccount extends RecyclerView.Adapter<AdapterAccount.ViewHold
ivState = itemView.findViewById(R.id.ivState);
tvHost = itemView.findViewById(R.id.tvHost);
tvLast = itemView.findViewById(R.id.tvLast);
tvAuthorize = itemView.findViewById(R.id.tvAuthorize);
tvIdentity = itemView.findViewById(R.id.tvIdentity);
tvDrafts = itemView.findViewById(R.id.tvDrafts);
tvWarning = itemView.findViewById(R.id.tvWarning);
@ -165,7 +163,6 @@ public class AdapterAccount extends RecyclerView.Adapter<AdapterAccount.ViewHold
tvLast.setText(context.getString(R.string.title_last_connected,
account.last_connected == null ? "-" : DTF.format(account.last_connected)));
tvAuthorize.setVisibility(account.auth_type == ConnectionHelper.AUTH_TYPE_PASSWORD ? View.GONE : View.VISIBLE);
tvIdentity.setVisibility(account.identities > 0 || !settings ? View.GONE : View.VISIBLE);
tvDrafts.setVisibility(account.drafts || !settings ? View.GONE : View.VISIBLE);

View File

@ -72,7 +72,6 @@ public class AdapterIdentity extends RecyclerView.Adapter<AdapterIdentity.ViewHo
private ImageView ivState;
private TextView tvAccount;
private TextView tvLast;
private TextView tvAuthorize;
private TextView tvError;
private TwoStateOwner powner = new TwoStateOwner(owner, "IdentityPopup");
@ -90,7 +89,6 @@ public class AdapterIdentity extends RecyclerView.Adapter<AdapterIdentity.ViewHo
ivState = itemView.findViewById(R.id.ivState);
tvAccount = itemView.findViewById(R.id.tvAccount);
tvLast = itemView.findViewById(R.id.tvLast);
tvAuthorize = itemView.findViewById(R.id.tvAuthorize);
tvError = itemView.findViewById(R.id.tvError);
}
@ -128,8 +126,6 @@ public class AdapterIdentity extends RecyclerView.Adapter<AdapterIdentity.ViewHo
tvLast.setText(context.getString(R.string.title_last_connected,
identity.last_connected == null ? "-" : DTF.format(identity.last_connected)));
tvAuthorize.setVisibility(identity.auth_type == ConnectionHelper.AUTH_TYPE_PASSWORD ? View.GONE : View.VISIBLE);
tvError.setText(identity.error);
tvError.setVisibility(identity.error == null ? View.GONE : View.VISIBLE);
}

View File

@ -33,9 +33,6 @@ public class ConnectionHelper {
// https://dns.watch/
private static final String DEFAULT_DNS = "84.200.69.80";
static final int AUTH_TYPE_PASSWORD = 1;
static final int AUTH_TYPE_GMAIL = 2;
// Roam like at home
// https://en.wikipedia.org/wiki/European_Union_roaming_regulations
private static final List<String> RLAH_COUNTRY_CODES = Collections.unmodifiableList(Arrays.asList(

View File

@ -105,6 +105,9 @@ public interface DaoAccount {
@Update
void updateAccount(EntityAccount account);
@Query("UPDATE account SET password = :password WHERE password = :old")
int updateAccountPassword(String old, String password);
@Query("UPDATE account SET separator = :separator WHERE id = :id")
int setFolderSeparator(long id, Character separator);
@ -117,9 +120,6 @@ public interface DaoAccount {
@Query("UPDATE account SET last_connected = :last_connected WHERE id = :id")
int setAccountConnected(long id, long last_connected);
@Query("UPDATE account SET password = :password WHERE id = :id")
int setAccountPassword(long id, String password);
@Query("UPDATE account SET `order` = :order WHERE id = :id")
int setAccountOrder(long id, Integer order);
@ -129,9 +129,6 @@ public interface DaoAccount {
@Query("UPDATE account SET error = :error WHERE id = :id")
int setAccountError(long id, String error);
@Query("UPDATE account SET poll_interval = :poll_interval WHERE id = :id")
int setAccountPollInterval(long id, int poll_interval);
@Query("UPDATE account SET `primary` = 0")
void resetPrimary();

View File

@ -67,6 +67,9 @@ public interface DaoIdentity {
@Update
void updateIdentity(EntityIdentity identity);
@Query("UPDATE identity SET password = :password WHERE password = :old")
int updateIdentityPassword(String old, String password);
@Query("UPDATE identity SET synchronize = :synchronize WHERE id = :id")
int setIdentitySynchronize(long id, boolean synchronize);

View File

@ -54,8 +54,6 @@ public class EntityAccount extends EntityOrder implements Serializable {
@PrimaryKey(autoGenerate = true)
public Long id;
@NonNull
public Integer auth_type;
@NonNull
public Boolean pop = false; // obsolete
@NonNull
@ -63,10 +61,12 @@ public class EntityAccount extends EntityOrder implements Serializable {
@NonNull
public Boolean starttls;
@NonNull
public Boolean insecure;
public Boolean insecure = false;
@NonNull
public Integer port;
@NonNull
public Integer auth_type;
@NonNull
public String user;
@NonNull
public String password;
@ -83,14 +83,14 @@ public class EntityAccount extends EntityOrder implements Serializable {
@NonNull
public Boolean primary;
@NonNull
public Boolean notify;
public Boolean notify = false;
@NonNull
public Boolean browse = true;
public Character separator;
public Long swipe_left;
public Long swipe_right;
@NonNull
public Integer poll_interval; // keep-alive interval
public Integer poll_interval = DEFAULT_KEEP_ALIVE_INTERVAL; // keep-alive interval
@NonNull
public Boolean partial_fetch = true;
public String prefix; // namespace, obsolete
@ -146,11 +146,11 @@ public class EntityAccount extends EntityOrder implements Serializable {
JSONObject json = new JSONObject();
json.put("id", id);
json.put("order", order);
json.put("auth_type", auth_type);
json.put("host", host);
json.put("starttls", starttls);
json.put("insecure", insecure);
json.put("port", port);
json.put("auth_type", auth_type);
json.put("user", user);
json.put("password", password);
json.put("realm", realm);
@ -184,11 +184,11 @@ public class EntityAccount extends EntityOrder implements Serializable {
if (json.has("order"))
account.order = json.getInt("order");
account.auth_type = json.getInt("auth_type");
account.host = json.getString("host");
account.starttls = (json.has("starttls") && json.getBoolean("starttls"));
account.insecure = (json.has("insecure") && json.getBoolean("insecure"));
account.port = json.getInt("port");
account.auth_type = json.getInt("auth_type");
account.user = json.getString("user");
account.password = json.getString("password");
if (json.has("realm"))
@ -227,11 +227,11 @@ public class EntityAccount extends EntityOrder implements Serializable {
public boolean equals(Object obj) {
if (obj instanceof EntityAccount) {
EntityAccount other = (EntityAccount) obj;
return (this.auth_type.equals(other.auth_type) &&
this.host.equals(other.host) &&
return (this.host.equals(other.host) &&
this.starttls == other.starttls &&
this.insecure == other.insecure &&
this.port.equals(other.port) &&
this.auth_type.equals(other.auth_type) &&
this.user.equals(other.user) &&
this.password.equals(other.password) &&
Objects.equals(this.realm, other.realm) &&

View File

@ -57,16 +57,16 @@ public class EntityIdentity {
public Integer color;
public String signature;
@NonNull
public Integer auth_type;
@NonNull
public String host; // SMTP
@NonNull
public Boolean starttls;
@NonNull
public Boolean insecure;
public Boolean insecure = false;
@NonNull
public Integer port;
@NonNull
public Integer auth_type;
@NonNull
public String user;
@NonNull
public String password;
@ -113,11 +113,11 @@ public class EntityIdentity {
json.put("signature", signature);
// not account
json.put("auth_type", auth_type);
json.put("host", host);
json.put("starttls", starttls);
json.put("insecure", insecure);
json.put("port", port);
json.put("auth_type", auth_type);
json.put("user", user);
json.put("password", password);
json.put("realm", realm);
@ -151,11 +151,11 @@ public class EntityIdentity {
if (json.has("signature") && !json.isNull("signature"))
identity.signature = json.getString("signature");
identity.auth_type = json.getInt("auth_type");
identity.host = json.getString("host");
identity.starttls = json.getBoolean("starttls");
identity.insecure = (json.has("insecure") && json.getBoolean("insecure"));
identity.port = json.getInt("port");
identity.auth_type = json.getInt("auth_type");
identity.user = json.getString("user");
identity.password = json.getString("password");
if (json.has("realm") && !json.isNull("realm"))
@ -196,11 +196,11 @@ public class EntityIdentity {
Objects.equals(this.display, other.display) &&
Objects.equals(this.color, other.color) &&
Objects.equals(this.signature, other.signature) &&
this.auth_type.equals(other.auth_type) &&
this.host.equals(other.host) &&
this.starttls.equals(other.starttls) &&
this.insecure.equals(other.insecure) &&
this.port.equals(other.port) &&
this.auth_type.equals(other.auth_type) &&
this.user.equals(other.user) &&
this.password.equals(other.password) &&
Objects.equals(this.realm, other.realm) &&

View File

@ -142,6 +142,7 @@ public class FragmentAccount extends FragmentBase {
private long id = -1;
private long copy = -1;
private int auth = MailService.AUTH_TYPE_PASSWORD;
private boolean saving = false;
private int color = Color.TRANSPARENT;
@ -516,6 +517,7 @@ public class FragmentAccount extends FragmentBase {
args.putBoolean("starttls", rgEncryption.getCheckedRadioButtonId() == R.id.radio_starttls);
args.putBoolean("insecure", cbInsecure.isChecked());
args.putString("port", etPort.getText().toString());
args.putInt("auth", auth);
args.putString("user", etUser.getText().toString());
args.putString("password", tilPassword.getEditText().getText().toString());
args.putString("realm", etRealm.getText().toString());
@ -551,6 +553,7 @@ public class FragmentAccount extends FragmentBase {
boolean starttls = args.getBoolean("starttls");
boolean insecure = args.getBoolean("insecure");
String port = args.getString("port");
int auth = args.getInt("auth");
String user = args.getString("user");
String password = args.getString("password");
String realm = args.getString("realm");
@ -581,7 +584,7 @@ public class FragmentAccount extends FragmentBase {
// Check IMAP server / get folders
String protocol = "imap" + (starttls ? "" : "s");
try (MailService iservice = new MailService(context, protocol, realm, insecure, true)) {
iservice.connect(host, Integer.parseInt(port), user, password);
iservice.connect(host, Integer.parseInt(port), auth, user, password);
result.idle = iservice.getStore().hasCapability("IDLE");
@ -709,6 +712,7 @@ public class FragmentAccount extends FragmentBase {
args.putBoolean("starttls", rgEncryption.getCheckedRadioButtonId() == R.id.radio_starttls);
args.putBoolean("insecure", cbInsecure.isChecked());
args.putString("port", etPort.getText().toString());
args.putInt("auth", auth);
args.putString("user", etUser.getText().toString());
args.putString("password", tilPassword.getEditText().getText().toString());
args.putString("realm", etRealm.getText().toString());
@ -762,6 +766,7 @@ public class FragmentAccount extends FragmentBase {
boolean starttls = args.getBoolean("starttls");
boolean insecure = args.getBoolean("insecure");
String port = args.getString("port");
int auth = args.getInt("auth");
String user = args.getString("user").trim();
String password = args.getString("password");
String realm = args.getString("realm");
@ -829,6 +834,8 @@ public class FragmentAccount extends FragmentBase {
return true;
if (!Objects.equals(account.port, Integer.parseInt(port)))
return true;
if (account.auth_type != auth)
return true;
if (!Objects.equals(account.user, user))
return true;
if (!Objects.equals(account.password, password))
@ -907,7 +914,7 @@ public class FragmentAccount extends FragmentBase {
if (check) {
String protocol = "imap" + (starttls ? "" : "s");
try (MailService iservice = new MailService(context, protocol, realm, insecure, true)) {
iservice.connect(host, Integer.parseInt(port), user, password);
iservice.connect(host, Integer.parseInt(port), auth, user, password);
for (Folder ifolder : iservice.getStore().getDefaultFolder().list("*")) {
// Check folder attributes
@ -948,13 +955,15 @@ public class FragmentAccount extends FragmentBase {
if (account == null)
account = new EntityAccount();
account.auth_type = ConnectionHelper.AUTH_TYPE_PASSWORD;
account.host = host;
account.starttls = starttls;
account.insecure = insecure;
account.port = Integer.parseInt(port);
account.user = user;
account.password = password;
account.auth_type = auth;
if (auth == MailService.AUTH_TYPE_PASSWORD) {
account.user = user;
account.password = password;
}
account.realm = realm;
account.name = name;
@ -1139,6 +1148,7 @@ public class FragmentAccount extends FragmentBase {
outState.putInt("fair:provider", spProvider.getSelectedItemPosition());
outState.putString("fair:password", tilPassword.getEditText().getText().toString());
outState.putInt("fair:advanced", grpAdvanced.getVisibility());
outState.putInt("fair:auth", auth);
outState.putInt("fair:color", color);
super.onSaveInstanceState(outState);
}
@ -1210,6 +1220,7 @@ public class FragmentAccount extends FragmentBase {
etInterval.setText(account == null ? "" : Long.toString(account.poll_interval));
cbPartialFetch.setChecked(account == null ? true : account.partial_fetch);
auth = (account == null ? MailService.AUTH_TYPE_PASSWORD : account.auth_type);
color = (account == null || account.color == null ? Color.TRANSPARENT : account.color);
new SimpleTask<EntityAccount>() {
@ -1236,11 +1247,17 @@ public class FragmentAccount extends FragmentBase {
tilPassword.getEditText().setText(savedInstanceState.getString("fair:password"));
grpAdvanced.setVisibility(savedInstanceState.getInt("fair:advanced"));
auth = savedInstanceState.getInt("fair:auth");
color = savedInstanceState.getInt("fair:color");
}
Helper.setViewsEnabled(view, true);
if (auth != MailService.AUTH_TYPE_PASSWORD) {
etUser.setEnabled(false);
tilPassword.setEnabled(false);
}
setColor(color);
cbPrimary.setEnabled(cbSynchronize.isChecked());

View File

@ -130,6 +130,7 @@ public class FragmentIdentity extends FragmentBase {
private long id = -1;
private long copy = -1;
private int auth = MailService.AUTH_TYPE_PASSWORD;
private boolean saving = false;
private int color = Color.TRANSPARENT;
@ -537,6 +538,7 @@ public class FragmentIdentity extends FragmentBase {
args.putBoolean("starttls", rgEncryption.getCheckedRadioButtonId() == R.id.radio_starttls);
args.putBoolean("insecure", cbInsecure.isChecked());
args.putString("port", etPort.getText().toString());
args.putInt("auth", auth);
args.putString("user", etUser.getText().toString());
args.putString("password", tilPassword.getEditText().getText().toString());
args.putString("realm", etRealm.getText().toString());
@ -584,6 +586,7 @@ public class FragmentIdentity extends FragmentBase {
boolean starttls = args.getBoolean("starttls");
boolean insecure = args.getBoolean("insecure");
String port = args.getString("port");
int auth = args.getInt("auth");
String user = args.getString("user").trim();
String password = args.getString("password");
String realm = args.getString("realm");
@ -680,6 +683,8 @@ public class FragmentIdentity extends FragmentBase {
return true;
if (!Objects.equals(identity.port, Integer.parseInt(port)))
return true;
if (identity.auth_type != auth)
return true;
if (!Objects.equals(identity.user, user))
return true;
if (!Objects.equals(identity.password, password))
@ -731,7 +736,7 @@ public class FragmentIdentity extends FragmentBase {
String protocol = (starttls ? "smtp" : "smtps");
try (MailService iservice = new MailService(context, protocol, realm, insecure, true)) {
iservice.setUseIp(use_ip);
iservice.connect(host, Integer.parseInt(port), user, password);
iservice.connect(host, Integer.parseInt(port), auth, user, password);
}
}
@ -748,13 +753,15 @@ public class FragmentIdentity extends FragmentBase {
identity.color = color;
identity.signature = signature;
identity.auth_type = ConnectionHelper.AUTH_TYPE_PASSWORD;
identity.host = host;
identity.starttls = starttls;
identity.insecure = insecure;
identity.port = Integer.parseInt(port);
identity.user = user;
identity.password = password;
identity.auth_type = auth;
if (auth == MailService.AUTH_TYPE_PASSWORD) {
identity.user = user;
identity.password = password;
}
identity.realm = realm;
identity.use_ip = use_ip;
identity.synchronize = synchronize;
@ -853,6 +860,7 @@ public class FragmentIdentity extends FragmentBase {
outState.putInt("fair:provider", spProvider.getSelectedItemPosition());
outState.putString("fair:password", tilPassword.getEditText().getText().toString());
outState.putInt("fair:advanced", grpAdvanced.getVisibility());
outState.putInt("fair:auth", auth);
outState.putInt("fair:color", color);
outState.putString("fair:html", (String) etSignature.getTag());
super.onSaveInstanceState(outState);
@ -902,6 +910,7 @@ public class FragmentIdentity extends FragmentBase {
cbDeliveryReceipt.setChecked(identity == null ? false : identity.delivery_receipt);
cbReadReceipt.setChecked(identity == null ? false : identity.read_receipt);
auth = (identity == null ? MailService.AUTH_TYPE_PASSWORD : identity.auth_type);
color = (identity == null || identity.color == null ? Color.TRANSPARENT : identity.color);
if (identity == null || copy > 0)
@ -924,12 +933,18 @@ public class FragmentIdentity extends FragmentBase {
} else {
tilPassword.getEditText().setText(savedInstanceState.getString("fair:password"));
grpAdvanced.setVisibility(savedInstanceState.getInt("fair:advanced"));
auth = savedInstanceState.getInt("fair:auth");
color = savedInstanceState.getInt("fair:color");
etSignature.setTag(savedInstanceState.getString("fair:html"));
}
Helper.setViewsEnabled(view, true);
if (auth != MailService.AUTH_TYPE_PASSWORD) {
etUser.setEnabled(false);
tilPassword.setEnabled(false);
}
setColor(color);
cbPrimary.setEnabled(cbSynchronize.isChecked());
@ -949,7 +964,6 @@ public class FragmentIdentity extends FragmentBase {
EntityAccount unselected = new EntityAccount();
unselected.id = -1L;
unselected.auth_type = ConnectionHelper.AUTH_TYPE_PASSWORD;
unselected.name = getString(R.string.title_select);
unselected.primary = false;
accounts.add(0, unselected);

View File

@ -49,15 +49,12 @@ import androidx.appcompat.app.AlertDialog;
import androidx.constraintlayout.widget.Group;
import com.google.android.material.textfield.TextInputLayout;
import com.sun.mail.imap.IMAPFolder;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import javax.mail.AuthenticationFailedException;
import javax.mail.Folder;
import static android.app.Activity.RESULT_OK;
@ -256,79 +253,32 @@ public class FragmentQuickSetup extends FragmentBase {
String user = (provider.user == EmailProvider.UserType.EMAIL ? email : dparts[0]);
Log.i("User type=" + provider.user + " name=" + user);
List<EntityFolder> folders = new ArrayList<>();
long now = new Date().getTime();
List<EntityFolder> folders;
{
String protocol = provider.imap.starttls ? "imap" : "imaps";
try (MailService iservice = new MailService(context, protocol, null, false, true)) {
try {
iservice.connect(provider.imap.host, provider.imap.port, user, password);
} catch (AuthenticationFailedException ex) {
if (user.contains("@")) {
Log.w(ex);
user = dparts[0];
Log.i("Retry with user=" + user);
iservice.connect(provider.imap.host, provider.imap.port, user, password);
} else
throw ex;
}
List<EntityFolder> guesses = new ArrayList<>();
for (Folder ifolder : iservice.getStore().getDefaultFolder().list("*")) {
String fullName = ifolder.getFullName();
String[] attrs = ((IMAPFolder) ifolder).getAttributes();
String type = EntityFolder.getType(attrs, fullName, true);
Log.i(fullName + " attrs=" + TextUtils.join(" ", attrs) + " type=" + type);
if (type != null) {
EntityFolder folder = new EntityFolder(fullName, type);
folders.add(folder);
if (EntityFolder.USER.equals(type)) {
String guess = EntityFolder.guessType(fullName);
if (guess != null)
guesses.add(folder);
}
}
}
for (EntityFolder guess : guesses) {
boolean has = false;
String gtype = EntityFolder.guessType(guess.name);
for (EntityFolder folder : folders)
if (folder.type.equals(gtype)) {
has = true;
break;
}
if (!has) {
guess.type = gtype;
Log.i(guess.name + " guessed type=" + gtype);
}
}
boolean inbox = false;
boolean drafts = false;
for (EntityFolder folder : folders)
if (EntityFolder.INBOX.equals(folder.type))
inbox = true;
else if (EntityFolder.DRAFTS.equals(folder.type))
drafts = true;
Log.i("Quick inbox=" + inbox + " drafts=" + drafts);
if (!inbox || !drafts)
throw new IllegalArgumentException(
context.getString(R.string.title_setup_no_settings, dparts[1]));
String aprotocol = provider.imap.starttls ? "imap" : "imaps";
try (MailService iservice = new MailService(context, aprotocol, null, false, true)) {
try {
iservice.connect(provider.imap.host, provider.imap.port, MailService.AUTH_TYPE_PASSWORD, user, password);
} catch (AuthenticationFailedException ex) {
if (user.contains("@")) {
Log.w(ex);
user = dparts[0];
Log.i("Retry with user=" + user);
iservice.connect(provider.imap.host, provider.imap.port, MailService.AUTH_TYPE_PASSWORD, user, password);
} else
throw ex;
}
folders = iservice.getFolders();
if (folders == null)
throw new IllegalArgumentException(
context.getString(R.string.title_setup_no_settings, dparts[1]));
}
{
String protocol = provider.smtp.starttls ? "smtp" : "smtps";
try (MailService iservice = new MailService(context, protocol, null, false, true)) {
iservice.connect(provider.smtp.host, provider.smtp.port, user, password);
}
String iprotocol = provider.smtp.starttls ? "smtp" : "smtps";
try (MailService iservice = new MailService(context, iprotocol, null, false, true)) {
iservice.connect(provider.smtp.host, provider.smtp.port, MailService.AUTH_TYPE_PASSWORD, user, password);
}
if (check)
@ -337,31 +287,26 @@ public class FragmentQuickSetup extends FragmentBase {
DB db = DB.getInstance(context);
try {
db.beginTransaction();
EntityAccount primary = db.account().getPrimaryAccount();
// Create account
EntityAccount account = new EntityAccount();
account.auth_type = ConnectionHelper.AUTH_TYPE_PASSWORD;
account.host = provider.imap.host;
account.starttls = provider.imap.starttls;
account.insecure = false;
account.port = provider.imap.port;
account.auth_type = MailService.AUTH_TYPE_PASSWORD;
account.user = user;
account.password = password;
account.name = provider.name;
account.color = null;
account.synchronize = true;
account.primary = (primary == null);
account.notify = false;
account.browse = true;
account.poll_interval = EntityAccount.DEFAULT_KEEP_ALIVE_INTERVAL;
account.created = now;
account.error = null;
account.last_connected = now;
account.created = new Date().getTime();
account.last_connected = account.created;
account.id = db.account().insertAccount(account);
EntityLog.log(context, "Quick added account=" + account.name);
@ -388,27 +333,15 @@ public class FragmentQuickSetup extends FragmentBase {
identity.email = email;
identity.account = account.id;
identity.display = null;
identity.color = null;
identity.signature = null;
identity.auth_type = ConnectionHelper.AUTH_TYPE_PASSWORD;
identity.host = provider.smtp.host;
identity.starttls = provider.smtp.starttls;
identity.insecure = false;
identity.port = provider.smtp.port;
identity.auth_type = MailService.AUTH_TYPE_PASSWORD;
identity.user = user;
identity.password = password;
identity.synchronize = true;
identity.primary = true;
identity.replyto = null;
identity.bcc = null;
identity.delivery_receipt = false;
identity.read_receipt = false;
identity.error = null;
identity.id = db.identity().insertIdentity(identity);
EntityLog.log(context, "Quick added identity=" + identity.name + " email=" + identity.email);

View File

@ -20,6 +20,12 @@ package eu.faircode.email;
*/
import android.Manifest;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.accounts.AccountManagerCallback;
import android.accounts.AccountManagerFuture;
import android.accounts.AccountsException;
import android.app.Activity;
import android.app.Dialog;
import android.content.ComponentName;
import android.content.Context;
@ -27,12 +33,14 @@ import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.graphics.drawable.Drawable;
import android.net.ConnectivityManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.PowerManager;
import android.provider.ContactsContract;
import android.provider.Settings;
import android.view.LayoutInflater;
import android.view.View;
@ -45,13 +53,20 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.constraintlayout.widget.Group;
import androidx.core.content.ContextCompat;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.Observer;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import androidx.preference.PreferenceManager;
import com.google.android.material.snackbar.Snackbar;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import static android.accounts.AccountManager.newChooseAccountIntent;
public class FragmentSetup extends FragmentBase {
private ViewGroup view;
@ -60,6 +75,7 @@ public class FragmentSetup extends FragmentBase {
private Button btnHelp;
private Button btnQuick;
private Button btnGmail;
private TextView tvAccountDone;
private Button btnAccount;
@ -87,10 +103,6 @@ public class FragmentSetup extends FragmentBase {
private int colorWarning;
private Drawable check;
private static final String[] permissions = new String[]{
Manifest.permission.READ_CONTACTS
};
@Override
@Nullable
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
@ -108,6 +120,7 @@ public class FragmentSetup extends FragmentBase {
btnHelp = view.findViewById(R.id.btnHelp);
btnQuick = view.findViewById(R.id.btnQuick);
btnGmail = view.findViewById(R.id.btnGmail);
tvAccountDone = view.findViewById(R.id.tvAccountDone);
btnAccount = view.findViewById(R.id.btnAccount);
@ -166,6 +179,28 @@ public class FragmentSetup extends FragmentBase {
}
});
btnGmail.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
List<String> permissions = new ArrayList<>();
permissions.add(Manifest.permission.READ_CONTACTS); // profile
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O)
permissions.add(Manifest.permission.GET_ACCOUNTS);
boolean granted = true;
for (String permission : permissions)
if (!hasPermission(permission)) {
granted = false;
break;
}
if (granted)
selectAccount();
else
requestPermissions(permissions.toArray(new String[0]), ActivitySetup.REQUEST_CHOOSE_ACCOUNT);
}
});
btnAccount.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
@ -186,7 +221,8 @@ public class FragmentSetup extends FragmentBase {
@Override
public void onClick(View view) {
btnPermissions.setEnabled(false);
requestPermissions(permissions, ActivitySetup.REQUEST_PERMISSION);
String permission = Manifest.permission.READ_CONTACTS;
requestPermissions(new String[]{permission}, ActivitySetup.REQUEST_PERMISSION);
}
});
@ -226,6 +262,8 @@ public class FragmentSetup extends FragmentBase {
});
// Initialize
btnGmail.setVisibility(Helper.hasValidFingerprint(getContext()) ? View.VISIBLE : View.GONE);
tvAccountDone.setText(null);
tvAccountDone.setCompoundDrawables(null, null, null, null);
tvNoPrimaryDrafts.setVisibility(View.GONE);
@ -248,11 +286,7 @@ public class FragmentSetup extends FragmentBase {
grpWelcome.setVisibility(welcome ? View.VISIBLE : View.GONE);
grpDataSaver.setVisibility(View.GONE);
int[] grantResults = new int[permissions.length];
for (int i = 0; i < permissions.length; i++)
grantResults[i] = ContextCompat.checkSelfPermission(getActivity(), permissions[i]);
checkPermissions(permissions, grantResults, true);
setContactsPermission(hasPermission(Manifest.permission.READ_CONTACTS));
// Create outbox
new SimpleTask<Void>() {
@ -392,25 +426,227 @@ public class FragmentSetup extends FragmentBase {
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
if (requestCode == ActivitySetup.REQUEST_PERMISSION)
checkPermissions(permissions, grantResults, false);
boolean granted = true;
for (int i = 0; i < permissions.length; i++)
if (grantResults[i] == PackageManager.PERMISSION_GRANTED) {
if (Manifest.permission.READ_CONTACTS.equals(permissions[i]))
setContactsPermission(true);
} else
granted = false;
if (requestCode == ActivitySetup.REQUEST_CHOOSE_ACCOUNT)
if (granted)
selectAccount();
}
private void checkPermissions(String[] permissions, @NonNull int[] grantResults, boolean init) {
boolean has = (grantResults.length > 0);
for (int result : grantResults)
if (result != PackageManager.PERMISSION_GRANTED) {
has = false;
break;
}
if (has)
private void setContactsPermission(boolean granted) {
if (granted)
ContactInfo.init(getContext());
tvPermissionsDone.setText(has ? R.string.title_setup_done : R.string.title_setup_to_do);
tvPermissionsDone.setTextColor(has ? textColorPrimary : colorWarning);
tvPermissionsDone.setCompoundDrawablesWithIntrinsicBounds(has ? check : null, null, null, null);
btnPermissions.setEnabled(!has);
tvPermissionsDone.setText(granted ? R.string.title_setup_done : R.string.title_setup_to_do);
tvPermissionsDone.setTextColor(granted ? textColorPrimary : colorWarning);
tvPermissionsDone.setCompoundDrawablesWithIntrinsicBounds(granted ? check : null, null, null, null);
btnPermissions.setEnabled(!granted);
}
private void selectAccount() {
Log.i("Select account");
startActivityForResult(
Helper.getChooser(getContext(), newChooseAccountIntent(
null,
null,
new String[]{"com.google"},
false,
null,
null,
null,
null)),
ActivitySetup.REQUEST_CHOOSE_ACCOUNT);
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
switch (requestCode) {
case ActivitySetup.REQUEST_CHOOSE_ACCOUNT:
if (resultCode == Activity.RESULT_OK && data != null)
onAccountSelected(data);
break;
}
}
private void onAccountSelected(Intent data) {
Log.i("Selected " + data);
Log.logExtras(data);
String name = data.getStringExtra(AccountManager.KEY_ACCOUNT_NAME);
String type = data.getStringExtra(AccountManager.KEY_ACCOUNT_TYPE);
AccountManager am = AccountManager.get(getContext());
Account[] accounts = am.getAccountsByType(type);
Log.i("Accounts=" + accounts.length);
for (final Account account : accounts)
if (name.equals(account.name)) {
Snackbar.make(view, R.string.title_authorizing, Snackbar.LENGTH_LONG).show();
am.getAuthToken(
account,
MailService.getAuthTokenType(type),
new Bundle(),
getActivity(),
new AccountManagerCallback<Bundle>() {
@Override
public void run(AccountManagerFuture<Bundle> future) {
try {
Bundle bundle = future.getResult();
String token = bundle.getString(AccountManager.KEY_AUTHTOKEN);
Log.i("Got token=" + token);
onAuthorized(name, token);
} catch (Throwable ex) {
if (ex instanceof AccountsException || ex instanceof IOException) {
if (getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.RESUMED))
Snackbar.make(view, Helper.formatThrowable(ex), Snackbar.LENGTH_LONG).show();
} else {
Log.e(ex);
Helper.unexpectedError(getFragmentManager(), ex);
}
}
}
},
null);
break;
}
}
private void onAuthorized(String user, String password) {
Bundle args = new Bundle();
args.putString("user", user);
args.putString("password", password);
new SimpleTask<Void>() {
@Override
protected Void onExecute(Context context, Bundle args) throws Throwable {
String user = args.getString("user");
String password = args.getString("password");
if (!user.contains("@"))
throw new IllegalArgumentException(user);
String domain = user.split("@")[1];
EmailProvider provider = EmailProvider.fromDomain(context, domain, EmailProvider.Discover.ALL);
if (provider == null)
throw new IllegalArgumentException(user);
List<EntityFolder> folders;
String aprotocol = provider.imap.starttls ? "imap" : "imaps";
try (MailService iservice = new MailService(context, aprotocol, null, false, true)) {
iservice.connect(provider.imap.host, provider.imap.port, MailService.AUTH_TYPE_GMAIL, user, password);
folders = iservice.getFolders();
if (folders == null)
throw new IllegalArgumentException(domain);
}
String iprotocol = provider.smtp.starttls ? "smtp" : "smtps";
try (MailService iservice = new MailService(context, iprotocol, null, false, true)) {
iservice.connect(provider.smtp.host, provider.smtp.port, MailService.AUTH_TYPE_GMAIL, user, password);
}
DB db = DB.getInstance(context);
try {
db.beginTransaction();
EntityAccount primary = db.account().getPrimaryAccount();
// Create account
EntityAccount account = new EntityAccount();
account.host = provider.imap.host;
account.starttls = provider.imap.starttls;
account.port = provider.imap.port;
account.auth_type = MailService.AUTH_TYPE_GMAIL;
account.user = user;
account.password = password;
account.name = provider.name;
account.synchronize = true;
account.primary = (primary == null);
account.created = new Date().getTime();
account.last_connected = account.created;
account.id = db.account().insertAccount(account);
EntityLog.log(context, "Gmail account=" + account.name);
// Create folders
for (EntityFolder folder : folders) {
folder.account = account.id;
folder.id = db.folder().insertFolder(folder);
EntityLog.log(context, "Gmail folder=" + folder.name + " type=" + folder.type);
}
// Set swipe left/right folder
for (EntityFolder folder : folders)
if (EntityFolder.TRASH.equals(folder.type))
account.swipe_left = folder.id;
else if (EntityFolder.ARCHIVE.equals(folder.type))
account.swipe_right = folder.id;
db.account().updateAccount(account);
String name = user.split("@")[0];
try (Cursor cursor = context.getContentResolver().query(
ContactsContract.Profile.CONTENT_URI,
new String[]{ContactsContract.Profile.DISPLAY_NAME}, null, null, null)) {
if (cursor != null && cursor.moveToFirst()) {
int colDisplay = cursor.getColumnIndex(ContactsContract.Profile.DISPLAY_NAME);
name = cursor.getString(colDisplay);
}
} catch (Throwable ex) {
Log.e(ex);
}
// Create identity
EntityIdentity identity = new EntityIdentity();
identity.name = name;
identity.email = user;
identity.account = account.id;
identity.host = provider.smtp.host;
identity.starttls = provider.smtp.starttls;
identity.port = provider.smtp.port;
identity.auth_type = MailService.AUTH_TYPE_GMAIL;
identity.user = user;
identity.password = password;
identity.synchronize = true;
identity.primary = true;
identity.id = db.identity().insertIdentity(identity);
EntityLog.log(context, "Gmail identity=" + identity.name + " email=" + identity.email);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
ServiceSynchronize.reload(getContext(), "Gmail");
return null;
}
@Override
protected void onExecuted(Bundle args, Void data) {
FragmentQuickSetup.FragmentDialogDone fragment = new FragmentQuickSetup.FragmentDialogDone();
fragment.show(getFragmentManager(), "gmail:done");
}
@Override
protected void onException(Bundle args, Throwable ex) {
Helper.unexpectedError(getFragmentManager(), ex);
}
}.execute(this, args, "setup:gmail");
}
public static class FragmentDialogDoze extends FragmentDialogBase {

View File

@ -1,7 +1,11 @@
package eu.faircode.email;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.content.Context;
import android.text.TextUtils;
import com.sun.mail.imap.IMAPFolder;
import com.sun.mail.imap.IMAPStore;
import com.sun.mail.smtp.SMTPTransport;
import com.sun.mail.util.MailConnectException;
@ -10,13 +14,17 @@ import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import javax.mail.AuthenticationFailedException;
import javax.mail.Folder;
import javax.mail.MessagingException;
import javax.mail.NoSuchProviderException;
import javax.mail.Service;
@ -33,6 +41,9 @@ public class MailService implements AutoCloseable {
private ExecutorService executor = Executors.newCachedThreadPool(Helper.backgroundThreadFactory);
static final int AUTH_TYPE_PASSWORD = 1;
static final int AUTH_TYPE_GMAIL = 2;
private final static int CONNECT_TIMEOUT = 20 * 1000; // milliseconds
private final static int WRITE_TIMEOUT = 60 * 1000; // milliseconds
private final static int READ_TIMEOUT = 60 * 1000; // milliseconds
@ -127,18 +138,56 @@ public class MailService implements AutoCloseable {
}
public void connect(EntityAccount account) throws MessagingException {
connect(account.host, account.port, account.user, account.password);
connect(account.host, account.port, account.auth_type, account.user, account.password);
}
public void connect(EntityIdentity identity) throws MessagingException {
connect(identity.host, identity.port, identity.user, identity.password);
connect(identity.host, identity.port, identity.auth_type, identity.user, identity.password);
}
public void connect(String host, int port, String user, String password) throws MessagingException {
public void connect(String host, int port, int auth, String user, String password) throws MessagingException {
try {
if (auth == AUTH_TYPE_GMAIL)
properties.put("mail." + protocol + ".auth.mechanisms", "XOAUTH2");
//if (BuildConfig.DEBUG)
// throw new MailConnectException(new SocketConnectException("Debug", new Exception(), host, port, 0));
_connect(context, host, port, user, password);
} catch (AuthenticationFailedException ex) {
// Refresh token
if (auth == AUTH_TYPE_GMAIL)
try {
String type = "com.google";
AccountManager am = AccountManager.get(context);
Account[] accounts = am.getAccountsByType(type);
for (Account account : accounts)
if (user.equals(account.name)) {
Log.i("Refreshing token user=" + user);
am.invalidateAuthToken(type, password);
String refreshed = am.blockingGetAuthToken(account, getAuthTokenType(type), true);
if (refreshed == null)
throw new IllegalStateException("no token");
int count = 0;
DB db = DB.getInstance(context);
if ("imap".equals(protocol) || "imaps".equals(protocol))
count = db.account().updateAccountPassword(password, refreshed);
else if ("smtp".equals(protocol) || "smtps".equals(protocol))
count = db.identity().updateIdentityPassword(password, refreshed);
if (count != 1)
throw new IllegalStateException(protocol + "=" + count);
_connect(context, host, port, user, refreshed);
break;
}
} catch (Throwable ex1) {
Log.e(ex1);
throw ex;
}
else
throw ex;
} catch (MailConnectException ex) {
try {
// Some devices resolve IPv6 addresses while not having IPv6 connectivity
@ -213,6 +262,63 @@ public class MailService implements AutoCloseable {
throw new NoSuchProviderException(protocol);
}
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<>();
List<EntityFolder> guesses = new ArrayList<>();
for (Folder ifolder : getStore().getDefaultFolder().list("*")) {
String fullName = ifolder.getFullName();
String[] attrs = ((IMAPFolder) ifolder).getAttributes();
String type = EntityFolder.getType(attrs, fullName, true);
Log.i(fullName + " attrs=" + TextUtils.join(" ", attrs) + " type=" + type);
if (type != null) {
EntityFolder folder = new EntityFolder(fullName, type);
folders.add(folder);
if (EntityFolder.USER.equals(type)) {
String guess = EntityFolder.guessType(fullName);
if (guess != null)
guesses.add(folder);
}
}
}
for (EntityFolder guess : guesses) {
boolean has = false;
String gtype = EntityFolder.guessType(guess.name);
for (EntityFolder folder : folders)
if (folder.type.equals(gtype)) {
has = true;
break;
}
if (!has) {
guess.type = gtype;
Log.i(guess.name + " guessed type=" + gtype);
}
}
boolean inbox = false;
boolean drafts = false;
for (EntityFolder folder : folders)
if (EntityFolder.INBOX.equals(folder.type))
inbox = true;
else if (EntityFolder.DRAFTS.equals(folder.type))
drafts = true;
if (!inbox || !drafts)
return null;
return folders;
}
IMAPStore getStore() {
return (IMAPStore) iservice;
}

View File

@ -105,6 +105,17 @@
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvQuick" />
<Button
android:id="@+id/btnGmail"
style="?android:attr/buttonStyleSmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minWidth="0dp"
android:minHeight="0dp"
android:text="@string/title_setup_gmail"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/btnQuick" />
<!-- account -->
<View
@ -114,7 +125,7 @@
android:layout_marginTop="12dp"
android:background="?attr/colorSeparator"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/btnQuick" />
app:layout_constraintTop_toBottomOf="@id/btnGmail" />
<ImageView
android:id="@+id/one"

View File

@ -134,20 +134,6 @@
app:layout_constraintStart_toEndOf="@+id/ivState"
app:layout_constraintTop_toBottomOf="@id/tvHost" />
<TextView
android:id="@+id/tvAuthorize"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="6dp"
android:layout_marginEnd="6dp"
android:text="@string/title_authorization_required"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
android:textColor="?attr/colorWarning"
android:textIsSelectable="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/vwColor"
app:layout_constraintTop_toBottomOf="@id/tvLast" />
<TextView
android:id="@+id/tvIdentity"
android:layout_width="0dp"
@ -160,7 +146,7 @@
android:textIsSelectable="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/vwColor"
app:layout_constraintTop_toBottomOf="@id/tvAuthorize" />
app:layout_constraintTop_toBottomOf="@id/tvLast" />
<TextView
android:id="@+id/tvDrafts"

View File

@ -136,20 +136,6 @@
app:layout_constraintStart_toEndOf="@+id/ivState"
app:layout_constraintTop_toBottomOf="@id/tvHost" />
<TextView
android:id="@+id/tvAuthorize"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="6dp"
android:layout_marginEnd="6dp"
android:text="@string/title_authorization_required"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
android:textColor="?attr/colorWarning"
android:textIsSelectable="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/vwColor"
app:layout_constraintTop_toBottomOf="@id/tvLast" />
<TextView
android:id="@+id/tvError"
android:layout_width="0dp"
@ -162,7 +148,7 @@
android:textIsSelectable="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/vwColor"
app:layout_constraintTop_toBottomOf="@id/tvAuthorize" />
app:layout_constraintTop_toBottomOf="@id/tvLast" />
<View
android:id="@+id/marginBottom"

View File

@ -135,6 +135,8 @@
<string name="title_setup_quick_smtp">SMTP server to send messages</string>
<string name="title_setup_go">Go</string>
<string name="title_setup_wizard">Wizard</string>
<string name="title_setup_gmail" translatable="false">Gmail</string>
<string name="title_authorizing">Authorizing &#8230;</string>
<string name="title_setup_instructions">Setup instructions</string>
<string name="title_setup_no_settings">No settings found for domain \'%1$s\'</string>
<string name="title_setup_quick_success">An account and an identity have successfully been added</string>
@ -406,7 +408,6 @@
<string name="title_no_idle">This provider does not support push messages. This will delay reception of new messages and increase battery usage.</string>
<string name="title_no_utf8">This provider does not support UTF-8</string>
<string name="title_no_sync">Synchronization errors since %1$s</string>
<string name="title_authorization_required">Authorization required</string>
<string name="title_identity_required">An identity is required to send messages</string>
<string name="title_drafts_required">A drafts folder is required to send messages</string>
<string name="title_account_delete">Delete this account permanently?</string>

View File

@ -2,6 +2,7 @@
<providers>
<provider
name="Gmail"
domain="gmail.com"
link="https://github.com/M66B/FairEmail/blob/master/FAQ.md#user-content-faq6"
order="1"
type="com.google">