diff --git a/FAQ.md b/FAQ.md
index fa83e93bb5..1481eea646 100644
--- a/FAQ.md
+++ b/FAQ.md
@@ -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)):
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index daab975f8e..11b052ab47 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -12,6 +12,12 @@
+
+
+
+
0 || !settings ? View.GONE : View.VISIBLE);
tvDrafts.setVisibility(account.drafts || !settings ? View.GONE : View.VISIBLE);
diff --git a/app/src/main/java/eu/faircode/email/AdapterIdentity.java b/app/src/main/java/eu/faircode/email/AdapterIdentity.java
index fe5b86d06b..18ce3cd561 100644
--- a/app/src/main/java/eu/faircode/email/AdapterIdentity.java
+++ b/app/src/main/java/eu/faircode/email/AdapterIdentity.java
@@ -72,7 +72,6 @@ public class AdapterIdentity extends RecyclerView.Adapter RLAH_COUNTRY_CODES = Collections.unmodifiableList(Arrays.asList(
diff --git a/app/src/main/java/eu/faircode/email/DaoAccount.java b/app/src/main/java/eu/faircode/email/DaoAccount.java
index 7921be8b73..4107cc56f0 100644
--- a/app/src/main/java/eu/faircode/email/DaoAccount.java
+++ b/app/src/main/java/eu/faircode/email/DaoAccount.java
@@ -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();
diff --git a/app/src/main/java/eu/faircode/email/DaoIdentity.java b/app/src/main/java/eu/faircode/email/DaoIdentity.java
index e59d6b8112..32f6fbc6cf 100644
--- a/app/src/main/java/eu/faircode/email/DaoIdentity.java
+++ b/app/src/main/java/eu/faircode/email/DaoIdentity.java
@@ -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);
diff --git a/app/src/main/java/eu/faircode/email/EntityAccount.java b/app/src/main/java/eu/faircode/email/EntityAccount.java
index 5b14043c45..3bbdcd45f4 100644
--- a/app/src/main/java/eu/faircode/email/EntityAccount.java
+++ b/app/src/main/java/eu/faircode/email/EntityAccount.java
@@ -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) &&
diff --git a/app/src/main/java/eu/faircode/email/EntityIdentity.java b/app/src/main/java/eu/faircode/email/EntityIdentity.java
index 595340ae2c..7d55c6494c 100644
--- a/app/src/main/java/eu/faircode/email/EntityIdentity.java
+++ b/app/src/main/java/eu/faircode/email/EntityIdentity.java
@@ -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) &&
diff --git a/app/src/main/java/eu/faircode/email/FragmentAccount.java b/app/src/main/java/eu/faircode/email/FragmentAccount.java
index 4ca223c6d1..7dcb840f77 100644
--- a/app/src/main/java/eu/faircode/email/FragmentAccount.java
+++ b/app/src/main/java/eu/faircode/email/FragmentAccount.java
@@ -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() {
@@ -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());
diff --git a/app/src/main/java/eu/faircode/email/FragmentIdentity.java b/app/src/main/java/eu/faircode/email/FragmentIdentity.java
index d2386019bf..94813eb1a6 100644
--- a/app/src/main/java/eu/faircode/email/FragmentIdentity.java
+++ b/app/src/main/java/eu/faircode/email/FragmentIdentity.java
@@ -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);
diff --git a/app/src/main/java/eu/faircode/email/FragmentQuickSetup.java b/app/src/main/java/eu/faircode/email/FragmentQuickSetup.java
index 9dca020ad8..6473ac5355 100644
--- a/app/src/main/java/eu/faircode/email/FragmentQuickSetup.java
+++ b/app/src/main/java/eu/faircode/email/FragmentQuickSetup.java
@@ -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 folders = new ArrayList<>();
- long now = new Date().getTime();
-
- {
- 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 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;
+ List folders;
+
+ 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;
+ }
- Log.i("Quick inbox=" + inbox + " drafts=" + drafts);
+ folders = iservice.getFolders();
- if (!inbox || !drafts)
- throw new IllegalArgumentException(
- context.getString(R.string.title_setup_no_settings, dparts[1]));
- }
+ 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);
diff --git a/app/src/main/java/eu/faircode/email/FragmentSetup.java b/app/src/main/java/eu/faircode/email/FragmentSetup.java
index 6cd9bf25cf..6129378f25 100644
--- a/app/src/main/java/eu/faircode/email/FragmentSetup.java
+++ b/app/src/main/java/eu/faircode/email/FragmentSetup.java
@@ -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 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() {
@@ -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 setContactsPermission(boolean granted) {
+ if (granted)
+ ContactInfo.init(getContext());
+
+ 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 checkPermissions(String[] permissions, @NonNull int[] grantResults, boolean init) {
- boolean has = (grantResults.length > 0);
- for (int result : grantResults)
- if (result != PackageManager.PERMISSION_GRANTED) {
- has = false;
+ 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() {
+ @Override
+ public void run(AccountManagerFuture 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;
}
+ }
- if (has)
- ContactInfo.init(getContext());
+ private void onAuthorized(String user, String password) {
+ Bundle args = new Bundle();
+ args.putString("user", user);
+ args.putString("password", password);
+
+ new SimpleTask() {
+ @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 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);
+ }
- 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);
+ 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 {
diff --git a/app/src/main/java/eu/faircode/email/MailService.java b/app/src/main/java/eu/faircode/email/MailService.java
index 67930b86c2..7a07ef78fc 100644
--- a/app/src/main/java/eu/faircode/email/MailService.java
+++ b/app/src/main/java/eu/faircode/email/MailService.java
@@ -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 getFolders() throws MessagingException {
+ List folders = new ArrayList<>();
+ List 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;
}
diff --git a/app/src/main/res/layout/fragment_setup.xml b/app/src/main/res/layout/fragment_setup.xml
index 3aba026c05..67d678da4a 100644
--- a/app/src/main/res/layout/fragment_setup.xml
+++ b/app/src/main/res/layout/fragment_setup.xml
@@ -105,6 +105,17 @@
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvQuick" />
+
+
+ app:layout_constraintTop_toBottomOf="@id/btnGmail" />
-
-
+ app:layout_constraintTop_toBottomOf="@id/tvLast" />
-
-
+ app:layout_constraintTop_toBottomOf="@id/tvLast" />
SMTP server to send messages
Go
Wizard
+ Gmail
+ Authorizing …
Setup instructions
No settings found for domain \'%1$s\'
An account and an identity have successfully been added
@@ -406,7 +408,6 @@
This provider does not support push messages. This will delay reception of new messages and increase battery usage.
This provider does not support UTF-8
Synchronization errors since %1$s
- Authorization required
An identity is required to send messages
A drafts folder is required to send messages
Delete this account permanently?
diff --git a/app/src/main/res/xml/providers.xml b/app/src/main/res/xml/providers.xml
index ba8e3e5d68..d5ea6b4816 100644
--- a/app/src/main/res/xml/providers.xml
+++ b/app/src/main/res/xml/providers.xml
@@ -2,6 +2,7 @@