diff --git a/FAQ.md b/FAQ.md
index 5da37d2063..52f50506b8 100644
--- a/FAQ.md
+++ b/FAQ.md
@@ -10,9 +10,10 @@ Frequently Asked Questions
* Full network access (INTERNET): to send and receive email
* View network connections (ACCESS_NETWORK_STATE): to monitor internet connectivity changes
* Run at startup (RECEIVE_BOOT_COMPLETED): to start monitoring on device start
+* In-app billing (BILLING): to allow in-app purchases
+* Foreground service (FOREGROUND_SERVICE): to run a foreground service on Android 9 Pie and later, see also the next question
* Optional: read your contacts (READ_CONTACTS): to autocomplete addresses
-* ... (BILLING): to offer in-app purchases
-* ... (FOREGROUND_SERVICE): to run a foreground service on Android 9 Pie and later, see also the next question
+* Optional: find accounts on the device (GET_ACCOUNTS): to use [OAuth](https://en.wikipedia.org/wiki/OAuth) instead of passwords
**(2) Why is there a permanent notification shown?**
diff --git a/app/schemas/eu.faircode.email.DB/5.json b/app/schemas/eu.faircode.email.DB/5.json
new file mode 100644
index 0000000000..e51cbf47dd
--- /dev/null
+++ b/app/schemas/eu.faircode.email.DB/5.json
@@ -0,0 +1,844 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 5,
+ "identityHash": "df70f524a0c744041e89eea7e7052e73",
+ "entities": [
+ {
+ "tableName": "identity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `name` TEXT NOT NULL, `email` TEXT NOT NULL, `replyto` TEXT, `account` INTEGER NOT NULL, `host` TEXT NOT NULL, `port` INTEGER NOT NULL, `starttls` INTEGER NOT NULL, `user` TEXT NOT NULL, `password` TEXT NOT NULL, `auth_type` INTEGER NOT NULL, `primary` INTEGER NOT NULL, `synchronize` INTEGER NOT NULL, `store_sent` INTEGER NOT NULL, `state` TEXT, `error` TEXT, FOREIGN KEY(`account`) REFERENCES `account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "email",
+ "columnName": "email",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "replyto",
+ "columnName": "replyto",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "account",
+ "columnName": "account",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "host",
+ "columnName": "host",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "port",
+ "columnName": "port",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "starttls",
+ "columnName": "starttls",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "user",
+ "columnName": "user",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "password",
+ "columnName": "password",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "auth_type",
+ "columnName": "auth_type",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "primary",
+ "columnName": "primary",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "synchronize",
+ "columnName": "synchronize",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "store_sent",
+ "columnName": "store_sent",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "state",
+ "columnName": "state",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "error",
+ "columnName": "error",
+ "affinity": "TEXT",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [
+ {
+ "name": "index_identity_account",
+ "unique": false,
+ "columnNames": [
+ "account"
+ ],
+ "createSql": "CREATE INDEX `index_identity_account` ON `${TABLE_NAME}` (`account`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "account",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "account"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "account",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `name` TEXT, `host` TEXT NOT NULL, `port` INTEGER NOT NULL, `user` TEXT NOT NULL, `password` TEXT NOT NULL, `auth_type` INTEGER NOT NULL, `primary` INTEGER NOT NULL, `synchronize` INTEGER NOT NULL, `store_sent` INTEGER NOT NULL, `poll_interval` INTEGER NOT NULL, `seen_until` INTEGER, `state` TEXT, `error` TEXT)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "host",
+ "columnName": "host",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "port",
+ "columnName": "port",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "user",
+ "columnName": "user",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "password",
+ "columnName": "password",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "auth_type",
+ "columnName": "auth_type",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "primary",
+ "columnName": "primary",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "synchronize",
+ "columnName": "synchronize",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "store_sent",
+ "columnName": "store_sent",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "poll_interval",
+ "columnName": "poll_interval",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "seen_until",
+ "columnName": "seen_until",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "state",
+ "columnName": "state",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "error",
+ "columnName": "error",
+ "affinity": "TEXT",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "folder",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `account` INTEGER, `name` TEXT NOT NULL, `type` TEXT NOT NULL, `synchronize` INTEGER NOT NULL, `after` INTEGER NOT NULL, `state` TEXT, `error` TEXT, FOREIGN KEY(`account`) REFERENCES `account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "account",
+ "columnName": "account",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "type",
+ "columnName": "type",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "synchronize",
+ "columnName": "synchronize",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "after",
+ "columnName": "after",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "state",
+ "columnName": "state",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "error",
+ "columnName": "error",
+ "affinity": "TEXT",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [
+ {
+ "name": "index_folder_account_name",
+ "unique": true,
+ "columnNames": [
+ "account",
+ "name"
+ ],
+ "createSql": "CREATE UNIQUE INDEX `index_folder_account_name` ON `${TABLE_NAME}` (`account`, `name`)"
+ },
+ {
+ "name": "index_folder_account",
+ "unique": false,
+ "columnNames": [
+ "account"
+ ],
+ "createSql": "CREATE INDEX `index_folder_account` ON `${TABLE_NAME}` (`account`)"
+ },
+ {
+ "name": "index_folder_name",
+ "unique": false,
+ "columnNames": [
+ "name"
+ ],
+ "createSql": "CREATE INDEX `index_folder_name` ON `${TABLE_NAME}` (`name`)"
+ },
+ {
+ "name": "index_folder_type",
+ "unique": false,
+ "columnNames": [
+ "type"
+ ],
+ "createSql": "CREATE INDEX `index_folder_type` ON `${TABLE_NAME}` (`type`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "account",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "account"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "message",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `account` INTEGER, `folder` INTEGER NOT NULL, `identity` INTEGER, `replying` INTEGER, `uid` INTEGER, `msgid` TEXT, `references` TEXT, `inreplyto` TEXT, `thread` TEXT, `from` TEXT, `to` TEXT, `cc` TEXT, `bcc` TEXT, `reply` TEXT, `subject` TEXT, `sent` INTEGER, `received` INTEGER NOT NULL, `stored` INTEGER NOT NULL, `seen` INTEGER NOT NULL, `ui_seen` INTEGER NOT NULL, `ui_hide` INTEGER NOT NULL, `error` TEXT, FOREIGN KEY(`account`) REFERENCES `account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`folder`) REFERENCES `folder`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`identity`) REFERENCES `identity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`replying`) REFERENCES `message`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "account",
+ "columnName": "account",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "folder",
+ "columnName": "folder",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "identity",
+ "columnName": "identity",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "replying",
+ "columnName": "replying",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "uid",
+ "columnName": "uid",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "msgid",
+ "columnName": "msgid",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "references",
+ "columnName": "references",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "inreplyto",
+ "columnName": "inreplyto",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "thread",
+ "columnName": "thread",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "from",
+ "columnName": "from",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "to",
+ "columnName": "to",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "cc",
+ "columnName": "cc",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "bcc",
+ "columnName": "bcc",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "reply",
+ "columnName": "reply",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "subject",
+ "columnName": "subject",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "sent",
+ "columnName": "sent",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "received",
+ "columnName": "received",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "stored",
+ "columnName": "stored",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "seen",
+ "columnName": "seen",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "ui_seen",
+ "columnName": "ui_seen",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "ui_hide",
+ "columnName": "ui_hide",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "error",
+ "columnName": "error",
+ "affinity": "TEXT",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [
+ {
+ "name": "index_message_account",
+ "unique": false,
+ "columnNames": [
+ "account"
+ ],
+ "createSql": "CREATE INDEX `index_message_account` ON `${TABLE_NAME}` (`account`)"
+ },
+ {
+ "name": "index_message_folder",
+ "unique": false,
+ "columnNames": [
+ "folder"
+ ],
+ "createSql": "CREATE INDEX `index_message_folder` ON `${TABLE_NAME}` (`folder`)"
+ },
+ {
+ "name": "index_message_identity",
+ "unique": false,
+ "columnNames": [
+ "identity"
+ ],
+ "createSql": "CREATE INDEX `index_message_identity` ON `${TABLE_NAME}` (`identity`)"
+ },
+ {
+ "name": "index_message_replying",
+ "unique": false,
+ "columnNames": [
+ "replying"
+ ],
+ "createSql": "CREATE INDEX `index_message_replying` ON `${TABLE_NAME}` (`replying`)"
+ },
+ {
+ "name": "index_message_folder_uid",
+ "unique": true,
+ "columnNames": [
+ "folder",
+ "uid"
+ ],
+ "createSql": "CREATE UNIQUE INDEX `index_message_folder_uid` ON `${TABLE_NAME}` (`folder`, `uid`)"
+ },
+ {
+ "name": "index_message_msgid_folder",
+ "unique": true,
+ "columnNames": [
+ "msgid",
+ "folder"
+ ],
+ "createSql": "CREATE UNIQUE INDEX `index_message_msgid_folder` ON `${TABLE_NAME}` (`msgid`, `folder`)"
+ },
+ {
+ "name": "index_message_thread",
+ "unique": false,
+ "columnNames": [
+ "thread"
+ ],
+ "createSql": "CREATE INDEX `index_message_thread` ON `${TABLE_NAME}` (`thread`)"
+ },
+ {
+ "name": "index_message_received",
+ "unique": false,
+ "columnNames": [
+ "received"
+ ],
+ "createSql": "CREATE INDEX `index_message_received` ON `${TABLE_NAME}` (`received`)"
+ },
+ {
+ "name": "index_message_ui_seen",
+ "unique": false,
+ "columnNames": [
+ "ui_seen"
+ ],
+ "createSql": "CREATE INDEX `index_message_ui_seen` ON `${TABLE_NAME}` (`ui_seen`)"
+ },
+ {
+ "name": "index_message_ui_hide",
+ "unique": false,
+ "columnNames": [
+ "ui_hide"
+ ],
+ "createSql": "CREATE INDEX `index_message_ui_hide` ON `${TABLE_NAME}` (`ui_hide`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "account",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "account"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ },
+ {
+ "table": "folder",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "folder"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ },
+ {
+ "table": "identity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "identity"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ },
+ {
+ "table": "message",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "replying"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "attachment",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `message` INTEGER NOT NULL, `sequence` INTEGER NOT NULL, `name` TEXT, `type` TEXT NOT NULL, `size` INTEGER, `progress` INTEGER, `available` INTEGER NOT NULL, FOREIGN KEY(`message`) REFERENCES `message`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "message",
+ "columnName": "message",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "sequence",
+ "columnName": "sequence",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "type",
+ "columnName": "type",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "size",
+ "columnName": "size",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "progress",
+ "columnName": "progress",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "available",
+ "columnName": "available",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [
+ {
+ "name": "index_attachment_message",
+ "unique": false,
+ "columnNames": [
+ "message"
+ ],
+ "createSql": "CREATE INDEX `index_attachment_message` ON `${TABLE_NAME}` (`message`)"
+ },
+ {
+ "name": "index_attachment_message_sequence",
+ "unique": true,
+ "columnNames": [
+ "message",
+ "sequence"
+ ],
+ "createSql": "CREATE UNIQUE INDEX `index_attachment_message_sequence` ON `${TABLE_NAME}` (`message`, `sequence`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "message",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "message"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "operation",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `folder` INTEGER NOT NULL, `message` INTEGER NOT NULL, `name` TEXT NOT NULL, `args` TEXT NOT NULL, `created` INTEGER NOT NULL, FOREIGN KEY(`folder`) REFERENCES `folder`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`message`) REFERENCES `message`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "folder",
+ "columnName": "folder",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "message",
+ "columnName": "message",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "args",
+ "columnName": "args",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "created",
+ "columnName": "created",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [
+ {
+ "name": "index_operation_folder",
+ "unique": false,
+ "columnNames": [
+ "folder"
+ ],
+ "createSql": "CREATE INDEX `index_operation_folder` ON `${TABLE_NAME}` (`folder`)"
+ },
+ {
+ "name": "index_operation_message",
+ "unique": false,
+ "columnNames": [
+ "message"
+ ],
+ "createSql": "CREATE INDEX `index_operation_message` ON `${TABLE_NAME}` (`message`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "folder",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "folder"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ },
+ {
+ "table": "message",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "message"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "answer",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `name` TEXT NOT NULL, `text` TEXT NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "text",
+ "columnName": "text",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [],
+ "foreignKeys": []
+ }
+ ],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"df70f524a0c744041e89eea7e7052e73\")"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index c7ebaaf611..dfee57e383 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -6,6 +6,7 @@
+
diff --git a/app/src/main/java/eu/faircode/email/ActivitySetup.java b/app/src/main/java/eu/faircode/email/ActivitySetup.java
index da28645027..5f41476cc4 100644
--- a/app/src/main/java/eu/faircode/email/ActivitySetup.java
+++ b/app/src/main/java/eu/faircode/email/ActivitySetup.java
@@ -37,6 +37,9 @@ import androidx.localbroadcastmanager.content.LocalBroadcastManager;
public class ActivitySetup extends ActivityBase implements FragmentManager.OnBackStackChangedListener {
boolean hasAccount;
+ static final int REQUEST_PERMISSION = 1;
+ static final int REQUEST_CHOOSE_ACCOUNT = 2;
+
static final String ACTION_EDIT_ACCOUNT = BuildConfig.APPLICATION_ID + ".EDIT_ACCOUNT";
static final String ACTION_EDIT_IDENTITY = BuildConfig.APPLICATION_ID + ".EDIT_IDENTITY";
diff --git a/app/src/main/java/eu/faircode/email/DB.java b/app/src/main/java/eu/faircode/email/DB.java
index a8e520c4b0..523c11ea43 100644
--- a/app/src/main/java/eu/faircode/email/DB.java
+++ b/app/src/main/java/eu/faircode/email/DB.java
@@ -45,7 +45,7 @@ import androidx.sqlite.db.SupportSQLiteDatabase;
// https://developer.android.com/topic/libraries/architecture/room.html
@Database(
- version = 4,
+ version = 5,
entities = {
EntityIdentity.class,
EntityAccount.class,
@@ -135,6 +135,14 @@ public abstract class DB extends RoomDatabase {
db.execSQL("CREATE TABLE IF NOT EXISTS `answer` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `name` TEXT NOT NULL, `text` TEXT NOT NULL)");
}
})
+ .addMigrations(new Migration(4, 5) {
+ @Override
+ public void migrate(SupportSQLiteDatabase db) {
+ Log.i(Helper.TAG, "DB migration from version " + startVersion + " to " + endVersion);
+ db.execSQL("ALTER TABLE `account` ADD COLUMN `auth_type` INTEGER NOT NULL DEFAULT 1");
+ db.execSQL("ALTER TABLE `identity` ADD COLUMN `auth_type` INTEGER NOT NULL DEFAULT 1");
+ }
+ })
.build();
}
diff --git a/app/src/main/java/eu/faircode/email/EntityAccount.java b/app/src/main/java/eu/faircode/email/EntityAccount.java
index 5adf6ba61b..e356c8d2e1 100644
--- a/app/src/main/java/eu/faircode/email/EntityAccount.java
+++ b/app/src/main/java/eu/faircode/email/EntityAccount.java
@@ -43,6 +43,8 @@ public class EntityAccount {
@NonNull
public String password;
@NonNull
+ public Integer auth_type;
+ @NonNull
public Boolean primary;
@NonNull
public Boolean synchronize;
diff --git a/app/src/main/java/eu/faircode/email/EntityIdentity.java b/app/src/main/java/eu/faircode/email/EntityIdentity.java
index 860e47d30a..2d6caa9156 100644
--- a/app/src/main/java/eu/faircode/email/EntityIdentity.java
+++ b/app/src/main/java/eu/faircode/email/EntityIdentity.java
@@ -59,6 +59,8 @@ public class EntityIdentity {
@NonNull
public String password;
@NonNull
+ public Integer auth_type;
+ @NonNull
public Boolean primary;
@NonNull
public Boolean synchronize;
diff --git a/app/src/main/java/eu/faircode/email/FragmentAccount.java b/app/src/main/java/eu/faircode/email/FragmentAccount.java
index 0ba12219c2..20fd54c3f0 100644
--- a/app/src/main/java/eu/faircode/email/FragmentAccount.java
+++ b/app/src/main/java/eu/faircode/email/FragmentAccount.java
@@ -19,8 +19,17 @@ package eu.faircode.email;
Copyright 2018 by Marcel Bokhorst (M66B)
*/
+import android.Manifest;
+import android.accounts.Account;
+import android.accounts.AccountManager;
+import android.accounts.AccountManagerCallback;
+import android.accounts.AccountManagerFuture;
+import android.app.Activity;
import android.content.Context;
import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.text.Html;
@@ -53,6 +62,7 @@ import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
+import java.util.Properties;
import javax.mail.Folder;
import javax.mail.MessagingException;
@@ -62,14 +72,18 @@ 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.Observer;
+import static android.accounts.AccountManager.newChooseAccountIntent;
+
public class FragmentAccount extends FragmentEx {
private ViewGroup view;
private EditText etName;
private Spinner spProvider;
private EditText etHost;
private EditText etPort;
+ private Button btnAuthorize;
private EditText etUser;
private TextInputLayout tilPassword;
private TextView tvLink;
@@ -91,6 +105,18 @@ public class FragmentAccount extends FragmentEx {
private Group grpInstructions;
private Group grpFolders;
+ private long id = -1;
+ private int auth_type = Helper.AUTH_TYPE_PASSWORD;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ // Get arguments
+ Bundle args = getArguments();
+ id = (args == null ? -1 : args.getLong("id", -1));
+ }
+
@Override
@Nullable
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
@@ -98,14 +124,11 @@ public class FragmentAccount extends FragmentEx {
view = (ViewGroup) inflater.inflate(R.layout.fragment_account, container, false);
- // Get arguments
- Bundle args = getArguments();
- final long id = (args == null ? -1 : args.getLong("id", -1));
-
// Get controls
spProvider = view.findViewById(R.id.spProvider);
etName = view.findViewById(R.id.etName);
etHost = view.findViewById(R.id.etHost);
+ btnAuthorize = view.findViewById(R.id.btnAuthorize);
etPort = view.findViewById(R.id.etPort);
etUser = view.findViewById(R.id.etUser);
tilPassword = view.findViewById(R.id.tilPassword);
@@ -143,12 +166,23 @@ public class FragmentAccount extends FragmentEx {
tvLink.setText(Html.fromHtml("" + provider.link + ""));
grpInstructions.setVisibility(provider.link == null ? View.GONE : View.VISIBLE);
- if (provider.imap_port != 0) {
+ if (provider.imap_port > 0) {
etName.setText(provider.name);
etHost.setText(provider.imap_host);
etPort.setText(Integer.toString(provider.imap_port));
- etUser.requestFocus();
+ if (provider.type == null)
+ etUser.requestFocus();
}
+
+ if (provider.type == null) {
+ btnAuthorize.setVisibility(View.GONE);
+ if (auth_type != Helper.AUTH_TYPE_PASSWORD) {
+ auth_type = Helper.AUTH_TYPE_PASSWORD;
+ etUser.setText(null);
+ tilPassword.getEditText().setText(null);
+ }
+ } else
+ btnAuthorize.setVisibility(View.VISIBLE);
}
@Override
@@ -156,6 +190,19 @@ public class FragmentAccount extends FragmentEx {
}
});
+ btnAuthorize.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ String permission = Manifest.permission.GET_ACCOUNTS;
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O &&
+ ContextCompat.checkSelfPermission(getContext(), permission) != PackageManager.PERMISSION_GRANTED) {
+ Log.i(Helper.TAG, "Requesting " + permission);
+ requestPermissions(new String[]{permission}, ActivitySetup.REQUEST_PERMISSION);
+ } else
+ selectAccount();
+ }
+ });
+
cbSynchronize.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton compoundButton, boolean checked) {
@@ -181,6 +228,7 @@ public class FragmentAccount extends FragmentEx {
args.putString("port", etPort.getText().toString());
args.putString("user", etUser.getText().toString());
args.putString("password", tilPassword.getEditText().getText().toString());
+ args.putInt("auth_type", auth_type);
args.putBoolean("synchronize", cbSynchronize.isChecked());
args.putBoolean("primary", cbPrimary.isChecked());
@@ -192,6 +240,7 @@ public class FragmentAccount extends FragmentEx {
String port = args.getString("port");
String user = args.getString("user");
String password = args.getString("password");
+ int auth_type = args.getInt("auth_type");
if (TextUtils.isEmpty(host))
throw new Throwable(getContext().getString(R.string.title_no_host));
@@ -204,7 +253,10 @@ public class FragmentAccount extends FragmentEx {
// Check IMAP server / get folders
List folders = new ArrayList<>();
- Session isession = Session.getInstance(MessageHelper.getSessionProperties(), null);
+ Properties props = MessageHelper.getSessionProperties(auth_type);
+
+ Session isession = Session.getInstance(props, null);
+ isession.setDebug(true);
IMAPStore istore = null;
try {
istore = (IMAPStore) isession.getStore("imaps");
@@ -379,6 +431,7 @@ public class FragmentAccount extends FragmentEx {
args.putString("port", etPort.getText().toString());
args.putString("user", etUser.getText().toString());
args.putString("password", tilPassword.getEditText().getText().toString());
+ args.putInt("auth_type", auth_type);
args.putBoolean("synchronize", cbSynchronize.isChecked());
args.putBoolean("primary", cbPrimary.isChecked());
args.putString("poll_interval", etInterval.getText().toString());
@@ -396,6 +449,7 @@ public class FragmentAccount extends FragmentEx {
String port = args.getString("port");
String user = args.getString("user");
String password = args.getString("password");
+ int auth_type = args.getInt("auth_type");
boolean synchronize = args.getBoolean("synchronize");
boolean primary = args.getBoolean("primary");
String poll_interval = args.getString("poll_interval");
@@ -421,7 +475,7 @@ public class FragmentAccount extends FragmentEx {
// Check IMAP server
if (synchronize) {
- Session isession = Session.getInstance(MessageHelper.getSessionProperties(), null);
+ Session isession = Session.getInstance(MessageHelper.getSessionProperties(auth_type), null);
IMAPStore istore = null;
try {
istore = (IMAPStore) isession.getStore("imaps");
@@ -451,6 +505,7 @@ public class FragmentAccount extends FragmentEx {
account.port = Integer.parseInt(port);
account.user = user;
account.password = password;
+ account.auth_type = auth_type;
account.synchronize = synchronize;
account.primary = (account.synchronize && primary);
account.store_sent = false;
@@ -582,6 +637,7 @@ public class FragmentAccount extends FragmentEx {
// Initialize
Helper.setViewsEnabled(view, false);
+ btnAuthorize.setVisibility(View.GONE);
tilPassword.setPasswordVisibilityToggleEnabled(id < 0);
tvLink.setMovementMethod(LinkMovementMethod.getInstance());
btnCheck.setEnabled(false);
@@ -599,6 +655,7 @@ public class FragmentAccount extends FragmentEx {
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putInt("provider", spProvider.getSelectedItemPosition());
+ outState.putInt("auth_type", auth_type);
outState.putString("password", tilPassword.getEditText().getText().toString());
outState.putInt("instructions", grpInstructions.getVisibility());
}
@@ -607,10 +664,6 @@ public class FragmentAccount extends FragmentEx {
public void onActivityCreated(@Nullable final Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
- // Get arguments
- Bundle args = getArguments();
- long id = (args == null ? -1 : args.getLong("id", -1));
-
// Observe
DB.getInstance(getContext()).account().liveAccount(id).observe(getViewLifecycleOwner(), new Observer() {
boolean once = false;
@@ -640,6 +693,7 @@ public class FragmentAccount extends FragmentEx {
etInterval.setText(account == null ? "9" : Integer.toString(account.poll_interval));
} else {
int provider = savedInstanceState.getInt("provider");
+ auth_type = savedInstanceState.getInt("auth_type");
spProvider.setTag(provider);
spProvider.setSelection(provider);
tilPassword.getEditText().setText(savedInstanceState.getString("password"));
@@ -648,6 +702,7 @@ public class FragmentAccount extends FragmentEx {
Helper.setViewsEnabled(view, true);
+ btnAuthorize.setVisibility(auth_type == Helper.AUTH_TYPE_PASSWORD ? View.GONE : View.VISIBLE);
cbPrimary.setEnabled(cbSynchronize.isChecked());
btnCheck.setVisibility(cbSynchronize.isChecked() ? View.VISIBLE : View.GONE);
@@ -660,4 +715,70 @@ public class FragmentAccount extends FragmentEx {
}
});
}
+
+ void selectAccount() {
+ Log.i(Helper.TAG, "Select account");
+ Provider provider = (Provider) spProvider.getSelectedItem();
+ if (provider.type != null)
+ startActivityForResult(newChooseAccountIntent(
+ null,
+ null,
+ new String[]{provider.type},
+ null,
+ null,
+ null,
+ null), ActivitySetup.REQUEST_CHOOSE_ACCOUNT);
+ }
+
+ @Override
+ public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
+ if (requestCode == ActivitySetup.REQUEST_PERMISSION)
+ if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED)
+ selectAccount();
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ Log.i(Helper.TAG, "Activity result request=" + requestCode + " result=" + resultCode + " data=" + data);
+ if (resultCode == Activity.RESULT_OK)
+ if (requestCode == ActivitySetup.REQUEST_CHOOSE_ACCOUNT) {
+ String name = data.getStringExtra(AccountManager.KEY_ACCOUNT_NAME);
+ String type = data.getStringExtra(AccountManager.KEY_ACCOUNT_TYPE);
+
+ String authTokenType = null;
+ if ("com.google".equals(type))
+ authTokenType = "oauth2:https://mail.google.com/";
+
+ AccountManager am = AccountManager.get(getContext());
+ Account[] accounts = am.getAccountsByType(type);
+ Log.i(Helper.TAG, "Accounts=" + accounts.length);
+ for (final Account account : accounts)
+ if (name.equals(account.name)) {
+ am.getAuthToken(
+ account,
+ authTokenType,
+ 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(Helper.TAG, "Got token");
+
+ auth_type = Helper.AUTH_TYPE_GMAIL;
+ etUser.setText(account.name);
+ tilPassword.getEditText().setText(token);
+ } catch (Throwable ex) {
+ Log.e(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
+ Toast.makeText(getContext(), Helper.formatThrowable(ex), Toast.LENGTH_LONG).show();
+ }
+ }
+ },
+ null);
+ break;
+ }
+ }
+ }
}
diff --git a/app/src/main/java/eu/faircode/email/FragmentCompose.java b/app/src/main/java/eu/faircode/email/FragmentCompose.java
index d4190a25de..f565b459f6 100644
--- a/app/src/main/java/eu/faircode/email/FragmentCompose.java
+++ b/app/src/main/java/eu/faircode/email/FragmentCompose.java
@@ -804,7 +804,7 @@ public class FragmentCompose extends FragmentEx {
text = db.answer().getAnswer(answer).text;
draft.subject = context.getString(R.string.title_subject_reply, ref.subject);
body = String.format("%s
%s %s:
%s",
- Html.escapeHtml(text).replaceAll("\\r?\\n", "
"),
+ text.replaceAll("\\r?\\n", "
"),
Html.escapeHtml(new Date().toString()),
Html.escapeHtml(MessageHelper.getFormattedAddresses(draft.to, true)),
HtmlHelper.sanitize(context, ref.read(context), true));
diff --git a/app/src/main/java/eu/faircode/email/FragmentIdentity.java b/app/src/main/java/eu/faircode/email/FragmentIdentity.java
index 7a459e9ad6..1f87de2717 100644
--- a/app/src/main/java/eu/faircode/email/FragmentIdentity.java
+++ b/app/src/main/java/eu/faircode/email/FragmentIdentity.java
@@ -79,6 +79,17 @@ public class FragmentIdentity extends FragmentEx {
private ProgressBar pbWait;
private Group grpInstructions;
+ private long id = -1;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ // Get arguments
+ Bundle args = getArguments();
+ id = (args == null ? -1 : args.getLong("id", -1));
+ }
+
@Override
@Nullable
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
@@ -86,10 +97,6 @@ public class FragmentIdentity extends FragmentEx {
view = (ViewGroup) inflater.inflate(R.layout.fragment_identity, container, false);
- // Get arguments
- Bundle args = getArguments();
- final long id = (args == null ? -1 : args.getLong("id", -1));
-
// Get controls
etName = view.findViewById(R.id.etName);
etEmail = view.findViewById(R.id.etEmail);
@@ -214,6 +221,7 @@ public class FragmentIdentity extends FragmentEx {
args.putString("email", etEmail.getText().toString());
args.putString("replyto", etReplyTo.getText().toString());
args.putLong("account", account == null ? -1 : account.id);
+ args.putInt("auth_type", account == null ? Helper.AUTH_TYPE_PASSWORD : account.auth_type);
args.putString("host", etHost.getText().toString());
args.putBoolean("starttls", cbStartTls.isChecked());
args.putString("port", etPort.getText().toString());
@@ -236,6 +244,7 @@ public class FragmentIdentity extends FragmentEx {
String port = args.getString("port");
String user = args.getString("user");
String password = args.getString("password");
+ int auth_type = args.getInt("auth_type");
boolean synchronize = args.getBoolean("synchronize");
boolean primary = args.getBoolean("primary");
boolean store_sent = args.getBoolean("store_sent");
@@ -260,7 +269,7 @@ public class FragmentIdentity extends FragmentEx {
// Check SMTP server
if (synchronize) {
- Properties props = MessageHelper.getSessionProperties();
+ Properties props = MessageHelper.getSessionProperties(auth_type);
Session isession = Session.getInstance(props, null);
Transport itransport = isession.getTransport(starttls ? "smtp" : "smtps");
try {
@@ -287,6 +296,7 @@ public class FragmentIdentity extends FragmentEx {
identity.starttls = starttls;
identity.user = user;
identity.password = password;
+ identity.auth_type = auth_type;
identity.synchronize = synchronize;
identity.primary = (identity.synchronize && primary);
identity.store_sent = store_sent;
@@ -394,10 +404,6 @@ public class FragmentIdentity extends FragmentEx {
public void onActivityCreated(@Nullable final Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
- // Get arguments
- Bundle args = getArguments();
- long id = (args == null ? -1 : args.getLong("id", -1));
-
final DB db = DB.getInstance(getContext());
// Observe identity
@@ -447,7 +453,7 @@ public class FragmentIdentity extends FragmentEx {
EntityAccount unselected = new EntityAccount();
unselected.id = -1L;
- unselected.name = "";
+ unselected.name = getString(R.string.title_select);
unselected.primary = false;
accounts.add(0, unselected);
diff --git a/app/src/main/java/eu/faircode/email/Helper.java b/app/src/main/java/eu/faircode/email/Helper.java
index 0e106335f7..3792df3c0d 100644
--- a/app/src/main/java/eu/faircode/email/Helper.java
+++ b/app/src/main/java/eu/faircode/email/Helper.java
@@ -52,6 +52,9 @@ import javax.mail.internet.InternetAddress;
public class Helper {
static final String TAG = "fairemail";
+ static final int AUTH_TYPE_PASSWORD = 1;
+ static final int AUTH_TYPE_GMAIL = 2;
+
static int resolveColor(Context context, int attr) {
int[] attrs = new int[]{attr};
TypedArray a = context.getTheme().obtainStyledAttributes(attrs);
diff --git a/app/src/main/java/eu/faircode/email/MessageHelper.java b/app/src/main/java/eu/faircode/email/MessageHelper.java
index 857c316c61..377f67100a 100644
--- a/app/src/main/java/eu/faircode/email/MessageHelper.java
+++ b/app/src/main/java/eu/faircode/email/MessageHelper.java
@@ -57,7 +57,7 @@ public class MessageHelper {
private MimeMessage imessage;
private String raw = null;
- static Properties getSessionProperties() {
+ static Properties getSessionProperties(int auth_type) {
Properties props = new Properties();
// https://javaee.github.io/javamail/docs/api/com/sun/mail/imap/package-summary.html#properties
@@ -97,6 +97,12 @@ public class MessageHelper {
props.put("mail.mime.address.strict", "false");
props.put("mail.mime.decodetext.strict", "false");
+ // https://javaee.github.io/javamail/OAuth2
+ if (auth_type == Helper.AUTH_TYPE_GMAIL) {
+ props.put("mail.imaps.auth.mechanisms", "XOAUTH2");
+ props.put("mail.smtps.auth.mechanisms", "XOAUTH2");
+ }
+
return props;
}
diff --git a/app/src/main/java/eu/faircode/email/Provider.java b/app/src/main/java/eu/faircode/email/Provider.java
index 597031554e..aeceedd40f 100644
--- a/app/src/main/java/eu/faircode/email/Provider.java
+++ b/app/src/main/java/eu/faircode/email/Provider.java
@@ -35,6 +35,7 @@ import java.util.Locale;
public class Provider {
public String name;
public String link;
+ public String type;
public String imap_host;
public int imap_port;
public String smtp_host;
@@ -62,6 +63,7 @@ public class Provider {
provider = new Provider();
provider.name = xml.getAttributeValue(null, "name");
provider.link = xml.getAttributeValue(null, "link");
+ provider.type = xml.getAttributeValue(null, "type");
} else if ("imap".equals(xml.getName())) {
provider.imap_host = xml.getAttributeValue(null, "host");
provider.imap_port = xml.getAttributeIntValue(null, "port", 0);
diff --git a/app/src/main/java/eu/faircode/email/SearchDataSource.java b/app/src/main/java/eu/faircode/email/SearchDataSource.java
index f836e3909d..9c3f82e5ad 100644
--- a/app/src/main/java/eu/faircode/email/SearchDataSource.java
+++ b/app/src/main/java/eu/faircode/email/SearchDataSource.java
@@ -117,7 +117,7 @@ public class SearchDataSource extends PositionalDataSource imple
folder = db.folder().getFolder(fid);
account = db.account().getAccount(folder.account);
- Properties props = MessageHelper.getSessionProperties();
+ Properties props = MessageHelper.getSessionProperties(account.auth_type);
Session isession = Session.getInstance(props, null);
Log.i(Helper.TAG, "SDS connecting account=" + account.name);
diff --git a/app/src/main/java/eu/faircode/email/ServiceSynchronize.java b/app/src/main/java/eu/faircode/email/ServiceSynchronize.java
index 7c0be6f9d1..7d4dde4d09 100644
--- a/app/src/main/java/eu/faircode/email/ServiceSynchronize.java
+++ b/app/src/main/java/eu/faircode/email/ServiceSynchronize.java
@@ -73,6 +73,7 @@ import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import javax.mail.Address;
+import javax.mail.AuthenticationFailedException;
import javax.mail.FetchProfile;
import javax.mail.Flags;
import javax.mail.Folder;
@@ -390,7 +391,7 @@ public class ServiceSynchronize extends LifecycleService {
if (debug)
System.setProperty("mail.socket.debug", "true");
- Properties props = MessageHelper.getSessionProperties();
+ Properties props = MessageHelper.getSessionProperties(account.auth_type);
final Session isession = Session.getInstance(props, null);
isession.setDebug(debug);
// adb -t 1 logcat | grep "fairemail\|System.out"
@@ -455,6 +456,8 @@ public class ServiceSynchronize extends LifecycleService {
// Initiate connection
Log.i(Helper.TAG, account.name + " connect");
+ for (EntityFolder folder : db.folder().getFolders(account.id))
+ db.folder().setFolderState(folder.id, null);
db.account().setAccountState(account.id, "connecting");
istore.connect(account.host, account.port, account.user, account.password);
@@ -470,9 +473,6 @@ public class ServiceSynchronize extends LifecycleService {
throw new IllegalStateException("synchronize folders", ex);
}
- for (EntityFolder folder : db.folder().getFolders(account.id))
- db.folder().setFolderState(folder.id, null);
-
// Synchronize folders
for (final EntityFolder folder : db.folder().getFolders(account.id, true)) {
Log.i(Helper.TAG, account.name + " sync folder " + folder.name);
@@ -752,6 +752,9 @@ public class ServiceSynchronize extends LifecycleService {
reportError(account.name, null, ex);
db.account().setAccountError(account.id, Helper.formatThrowable(ex));
+
+ if (ex instanceof AuthenticationFailedException)
+ break;
} finally {
// Close store
Log.i(Helper.TAG, account.name + " closing");
@@ -840,7 +843,7 @@ public class ServiceSynchronize extends LifecycleService {
doDelete(folder, ifolder, message, jargs, db);
else if (EntityOperation.SEND.equals(op.name))
- doSend(isession, message, db);
+ doSend(message, db);
else if (EntityOperation.ATTACHMENT.equals(op.name))
doAttachment(folder, op, ifolder, message, jargs, db);
@@ -960,23 +963,27 @@ public class ServiceSynchronize extends LifecycleService {
db.message().deleteMessage(message.id);
}
- private void doSend(Session isession, EntityMessage message, DB db) throws MessagingException, IOException {
+ private void doSend(EntityMessage message, DB db) throws MessagingException, IOException {
// Send message
EntityIdentity ident = db.identity().getIdentity(message.identity);
- EntityMessage reply = (message.replying == null ? null : db.message().getMessage(message.replying));
- List attachments = db.attachment().getAttachments(message.id);
-
if (!ident.synchronize) {
// Message will remain in outbox
return;
}
+ // Create session
+ Properties props = MessageHelper.getSessionProperties(ident.auth_type);
+ final Session isession = Session.getInstance(props, null);
+
// Create message
MimeMessage imessage;
+ EntityMessage reply = (message.replying == null ? null : db.message().getMessage(message.replying));
+ List attachments = db.attachment().getAttachments(message.id);
if (reply == null)
imessage = MessageHelper.from(this, message, attachments, isession);
else
imessage = MessageHelper.from(this, message, reply, attachments, isession);
+
if (ident.replyto != null)
imessage.setReplyTo(new Address[]{new InternetAddress(ident.replyto)});
@@ -1564,16 +1571,12 @@ public class ServiceSynchronize extends LifecycleService {
public void onReceive(Context context, Intent intent) {
Log.v(Helper.TAG, outbox.name + " run operations");
- // Create session
- Properties props = MessageHelper.getSessionProperties();
- final Session isession = Session.getInstance(props, null);
-
executor.submit(new Runnable() {
@Override
public void run() {
try {
Log.i(Helper.TAG, outbox.name + " start operations");
- processOperations(outbox, isession, null, null);
+ processOperations(outbox, null, null, null);
} catch (Throwable ex) {
Log.e(Helper.TAG, outbox.name + " " + ex + "\n" + Log.getStackTraceString(ex));
reportError(null, outbox.name, ex);
diff --git a/app/src/main/res/layout/fragment_account.xml b/app/src/main/res/layout/fragment_account.xml
index 4483010208..9d1e331f37 100644
--- a/app/src/main/res/layout/fragment_account.xml
+++ b/app/src/main/res/layout/fragment_account.xml
@@ -108,6 +108,15 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvPort" />
+
+
+ app:layout_constraintTop_toBottomOf="@id/btnAuthorize" />
Instead of Chrome Custom Tabs
Debug
+ Select …
Your name
Your email address
Reply to address
@@ -86,7 +87,7 @@
Port number
User name
Password
- Authorize
+ Select account
Instructions
Store sent messages (enable if needed only)
Poll/keep-alive interval (minutes)
diff --git a/app/src/main/res/xml/providers.xml b/app/src/main/res/xml/providers.xml
index eae4c178cc..c8e951392e 100644
--- a/app/src/main/res/xml/providers.xml
+++ b/app/src/main/res/xml/providers.xml
@@ -2,7 +2,8 @@
+ link="https://support.google.com/mail/answer/7126229"
+ type="com.google">