From 02dfb75542e8183462d8698ad8fd9f82cd0df918 Mon Sep 17 00:00:00 2001 From: M66B Date: Sat, 21 Dec 2019 16:03:57 +0100 Subject: [PATCH] Added/storing auth provider id --- app/schemas/eu.faircode.email.DB/124.json | 2024 +++++++++++++++++ app/src/main/java/eu/faircode/email/DB.java | 10 +- .../java/eu/faircode/email/EmailProvider.java | 11 + .../java/eu/faircode/email/EntityAccount.java | 1 + .../eu/faircode/email/EntityIdentity.java | 1 + .../eu/faircode/email/FragmentAccount.java | 19 +- .../java/eu/faircode/email/FragmentGmail.java | 4 +- .../eu/faircode/email/FragmentIdentity.java | 12 +- .../java/eu/faircode/email/FragmentOAuth.java | 364 ++- .../java/eu/faircode/email/FragmentPop.java | 2 +- .../eu/faircode/email/FragmentQuickSetup.java | 6 +- .../java/eu/faircode/email/FragmentSetup.java | 1 + .../java/eu/faircode/email/MailService.java | 42 +- app/src/main/res/xml/providers.xml | 2 + 14 files changed, 2286 insertions(+), 213 deletions(-) create mode 100644 app/schemas/eu.faircode.email.DB/124.json diff --git a/app/schemas/eu.faircode.email.DB/124.json b/app/schemas/eu.faircode.email.DB/124.json new file mode 100644 index 0000000000..ad12b4e109 --- /dev/null +++ b/app/schemas/eu.faircode.email.DB/124.json @@ -0,0 +1,2024 @@ +{ + "formatVersion": 1, + "database": { + "version": 124, + "identityHash": "d8d88f3b2f4188b4097e505a9d5e732a", + "entities": [ + { + "tableName": "identity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `name` TEXT NOT NULL, `email` TEXT NOT NULL, `account` INTEGER NOT NULL, `display` TEXT, `color` INTEGER, `signature` TEXT, `host` TEXT NOT NULL, `starttls` INTEGER NOT NULL, `insecure` INTEGER NOT NULL, `port` INTEGER NOT NULL, `auth_type` INTEGER NOT NULL, `provider` TEXT, `user` TEXT NOT NULL, `password` TEXT NOT NULL, `realm` TEXT, `fingerprint` TEXT, `use_ip` INTEGER NOT NULL, `synchronize` INTEGER NOT NULL, `primary` INTEGER NOT NULL, `sender_extra` INTEGER NOT NULL, `sender_extra_regex` TEXT, `replyto` TEXT, `bcc` TEXT, `plain_only` INTEGER NOT NULL, `encrypt` INTEGER NOT NULL, `delivery_receipt` INTEGER NOT NULL, `read_receipt` INTEGER NOT NULL, `store_sent` INTEGER NOT NULL, `sent_folder` INTEGER, `sign_key` INTEGER, `sign_key_alias` TEXT, `tbd` INTEGER, `state` TEXT, `error` TEXT, `last_connected` INTEGER, 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": "account", + "columnName": "account", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "display", + "columnName": "display", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "signature", + "columnName": "signature", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "host", + "columnName": "host", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "starttls", + "columnName": "starttls", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "insecure", + "columnName": "insecure", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "port", + "columnName": "port", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "auth_type", + "columnName": "auth_type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "provider", + "columnName": "provider", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "user", + "columnName": "user", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "password", + "columnName": "password", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "realm", + "columnName": "realm", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "fingerprint", + "columnName": "fingerprint", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "use_ip", + "columnName": "use_ip", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "synchronize", + "columnName": "synchronize", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "primary", + "columnName": "primary", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sender_extra", + "columnName": "sender_extra", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sender_extra_regex", + "columnName": "sender_extra_regex", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "replyto", + "columnName": "replyto", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bcc", + "columnName": "bcc", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "plain_only", + "columnName": "plain_only", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "encrypt", + "columnName": "encrypt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "delivery_receipt", + "columnName": "delivery_receipt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "read_receipt", + "columnName": "read_receipt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "store_sent", + "columnName": "store_sent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sent_folder", + "columnName": "sent_folder", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sign_key", + "columnName": "sign_key", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sign_key_alias", + "columnName": "sign_key_alias", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tbd", + "columnName": "tbd", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "error", + "columnName": "error", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "last_connected", + "columnName": "last_connected", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_identity_account", + "unique": false, + "columnNames": [ + "account" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_identity_account` ON `${TABLE_NAME}` (`account`)" + }, + { + "name": "index_identity_account_email", + "unique": false, + "columnNames": [ + "account", + "email" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_identity_account_email` ON `${TABLE_NAME}` (`account`, `email`)" + } + ], + "foreignKeys": [ + { + "table": "account", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "account" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "account", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`order` INTEGER, `id` INTEGER PRIMARY KEY AUTOINCREMENT, `pop` INTEGER NOT NULL, `host` TEXT NOT NULL, `starttls` INTEGER NOT NULL, `insecure` INTEGER NOT NULL, `port` INTEGER NOT NULL, `auth_type` INTEGER NOT NULL, `provider` TEXT, `user` TEXT NOT NULL, `password` TEXT NOT NULL, `realm` TEXT, `fingerprint` TEXT, `name` TEXT, `signature` TEXT, `color` INTEGER, `synchronize` INTEGER NOT NULL, `ondemand` INTEGER NOT NULL, `primary` INTEGER NOT NULL, `notify` INTEGER NOT NULL, `browse` INTEGER NOT NULL, `auto_seen` INTEGER NOT NULL, `separator` INTEGER, `swipe_left` INTEGER, `swipe_right` INTEGER, `move_to` INTEGER, `poll_interval` INTEGER NOT NULL, `partial_fetch` INTEGER NOT NULL, `ignore_size` INTEGER NOT NULL, `use_date` INTEGER NOT NULL, `prefix` TEXT, `created` INTEGER, `tbd` INTEGER, `state` TEXT, `warning` TEXT, `error` TEXT, `last_connected` INTEGER)", + "fields": [ + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "protocol", + "columnName": "pop", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "host", + "columnName": "host", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "starttls", + "columnName": "starttls", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "insecure", + "columnName": "insecure", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "port", + "columnName": "port", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "auth_type", + "columnName": "auth_type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "provider", + "columnName": "provider", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "user", + "columnName": "user", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "password", + "columnName": "password", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "realm", + "columnName": "realm", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "fingerprint", + "columnName": "fingerprint", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "signature", + "columnName": "signature", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "synchronize", + "columnName": "synchronize", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ondemand", + "columnName": "ondemand", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "primary", + "columnName": "primary", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notify", + "columnName": "notify", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "browse", + "columnName": "browse", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "auto_seen", + "columnName": "auto_seen", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "separator", + "columnName": "separator", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "swipe_left", + "columnName": "swipe_left", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "swipe_right", + "columnName": "swipe_right", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "move_to", + "columnName": "move_to", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "poll_interval", + "columnName": "poll_interval", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "partial_fetch", + "columnName": "partial_fetch", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ignore_size", + "columnName": "ignore_size", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "use_date", + "columnName": "use_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "prefix", + "columnName": "prefix", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "created", + "columnName": "created", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "tbd", + "columnName": "tbd", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "warning", + "columnName": "warning", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "error", + "columnName": "error", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "last_connected", + "columnName": "last_connected", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "folder", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`order` INTEGER, `id` INTEGER PRIMARY KEY AUTOINCREMENT, `account` INTEGER, `parent` INTEGER, `uidv` INTEGER, `name` TEXT NOT NULL, `type` TEXT NOT NULL, `level` INTEGER NOT NULL, `synchronize` INTEGER NOT NULL, `poll` INTEGER NOT NULL, `download` INTEGER NOT NULL, `subscribed` INTEGER, `sync_days` INTEGER NOT NULL, `keep_days` INTEGER NOT NULL, `auto_delete` INTEGER NOT NULL, `display` TEXT, `color` INTEGER, `hide` INTEGER NOT NULL, `collapsed` INTEGER NOT NULL, `unified` INTEGER NOT NULL, `navigation` INTEGER NOT NULL, `notify` INTEGER NOT NULL, `total` INTEGER, `keywords` TEXT, `initialize` INTEGER NOT NULL, `tbc` INTEGER, `tbd` INTEGER, `rename` TEXT, `state` TEXT, `sync_state` TEXT, `read_only` INTEGER NOT NULL, `selectable` INTEGER NOT NULL, `error` TEXT, `last_sync` INTEGER, FOREIGN KEY(`account`) REFERENCES `account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "account", + "columnName": "account", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "parent", + "columnName": "parent", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "uidv", + "columnName": "uidv", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "level", + "columnName": "level", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "synchronize", + "columnName": "synchronize", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "download", + "columnName": "download", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subscribed", + "columnName": "subscribed", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sync_days", + "columnName": "sync_days", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "keep_days", + "columnName": "keep_days", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "auto_delete", + "columnName": "auto_delete", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "display", + "columnName": "display", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hide", + "columnName": "hide", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "collapsed", + "columnName": "collapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unified", + "columnName": "unified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "navigation", + "columnName": "navigation", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notify", + "columnName": "notify", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "total", + "columnName": "total", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "keywords", + "columnName": "keywords", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "initialize", + "columnName": "initialize", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "tbc", + "columnName": "tbc", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "tbd", + "columnName": "tbd", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "rename", + "columnName": "rename", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sync_state", + "columnName": "sync_state", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "read_only", + "columnName": "read_only", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "selectable", + "columnName": "selectable", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "error", + "columnName": "error", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "last_sync", + "columnName": "last_sync", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_folder_account_name", + "unique": true, + "columnNames": [ + "account", + "name" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_folder_account_name` ON `${TABLE_NAME}` (`account`, `name`)" + }, + { + "name": "index_folder_account", + "unique": false, + "columnNames": [ + "account" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_folder_account` ON `${TABLE_NAME}` (`account`)" + }, + { + "name": "index_folder_name", + "unique": false, + "columnNames": [ + "name" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_folder_name` ON `${TABLE_NAME}` (`name`)" + }, + { + "name": "index_folder_type", + "unique": false, + "columnNames": [ + "type" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_folder_type` ON `${TABLE_NAME}` (`type`)" + }, + { + "name": "index_folder_unified", + "unique": false, + "columnNames": [ + "unified" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_folder_unified` ON `${TABLE_NAME}` (`unified`)" + } + ], + "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 NOT NULL, `folder` INTEGER NOT NULL, `identity` INTEGER, `extra` TEXT, `replying` INTEGER, `forwarding` INTEGER, `uid` INTEGER, `msgid` TEXT, `references` TEXT, `deliveredto` TEXT, `inreplyto` TEXT, `thread` TEXT, `priority` INTEGER, `receipt` INTEGER, `receipt_request` INTEGER, `receipt_to` TEXT, `dkim` INTEGER, `spf` INTEGER, `dmarc` INTEGER, `mx` INTEGER, `avatar` TEXT, `sender` TEXT, `from` TEXT, `to` TEXT, `cc` TEXT, `bcc` TEXT, `reply` TEXT, `list_post` TEXT, `unsubscribe` TEXT, `headers` TEXT, `raw` INTEGER, `subject` TEXT, `size` INTEGER, `total` INTEGER, `attachments` INTEGER NOT NULL, `content` INTEGER NOT NULL, `plain_only` INTEGER, `encrypt` INTEGER, `preview` TEXT, `signature` INTEGER NOT NULL, `sent` INTEGER, `received` INTEGER NOT NULL, `stored` INTEGER NOT NULL, `seen` INTEGER NOT NULL, `answered` INTEGER NOT NULL, `flagged` INTEGER NOT NULL, `flags` TEXT, `keywords` TEXT, `notifying` INTEGER NOT NULL, `ui_seen` INTEGER NOT NULL, `ui_answered` INTEGER NOT NULL, `ui_flagged` INTEGER NOT NULL, `ui_hide` INTEGER NOT NULL, `ui_found` INTEGER NOT NULL, `ui_ignored` INTEGER NOT NULL, `ui_browsed` INTEGER NOT NULL, `ui_busy` INTEGER, `ui_snoozed` INTEGER, `color` INTEGER, `revision` INTEGER, `revisions` INTEGER, `warning` TEXT, `error` TEXT, `last_attempt` INTEGER, 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 SET NULL , FOREIGN KEY(`replying`) REFERENCES `message`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL , FOREIGN KEY(`forwarding`) REFERENCES `message`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "account", + "columnName": "account", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "folder", + "columnName": "folder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "identity", + "columnName": "identity", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "extra", + "columnName": "extra", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "replying", + "columnName": "replying", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "forwarding", + "columnName": "forwarding", + "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": "deliveredto", + "columnName": "deliveredto", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inreplyto", + "columnName": "inreplyto", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "thread", + "columnName": "thread", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "priority", + "columnName": "priority", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "receipt", + "columnName": "receipt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "receipt_request", + "columnName": "receipt_request", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "receipt_to", + "columnName": "receipt_to", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "dkim", + "columnName": "dkim", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "spf", + "columnName": "spf", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "dmarc", + "columnName": "dmarc", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "mx", + "columnName": "mx", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "avatar", + "columnName": "avatar", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sender", + "columnName": "sender", + "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": "list_post", + "columnName": "list_post", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "unsubscribe", + "columnName": "unsubscribe", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "headers", + "columnName": "headers", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "raw", + "columnName": "raw", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "total", + "columnName": "total", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "plain_only", + "columnName": "plain_only", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "encrypt", + "columnName": "encrypt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "preview", + "columnName": "preview", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "signature", + "columnName": "signature", + "affinity": "INTEGER", + "notNull": true + }, + { + "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": "answered", + "columnName": "answered", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "flagged", + "columnName": "flagged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "flags", + "columnName": "flags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "keywords", + "columnName": "keywords", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "notifying", + "columnName": "notifying", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ui_seen", + "columnName": "ui_seen", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ui_answered", + "columnName": "ui_answered", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ui_flagged", + "columnName": "ui_flagged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ui_hide", + "columnName": "ui_hide", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ui_found", + "columnName": "ui_found", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ui_ignored", + "columnName": "ui_ignored", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ui_browsed", + "columnName": "ui_browsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ui_busy", + "columnName": "ui_busy", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "ui_snoozed", + "columnName": "ui_snoozed", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "revision", + "columnName": "revision", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "revisions", + "columnName": "revisions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "warning", + "columnName": "warning", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "error", + "columnName": "error", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "last_attempt", + "columnName": "last_attempt", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_message_account", + "unique": false, + "columnNames": [ + "account" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_message_account` ON `${TABLE_NAME}` (`account`)" + }, + { + "name": "index_message_folder", + "unique": false, + "columnNames": [ + "folder" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_message_folder` ON `${TABLE_NAME}` (`folder`)" + }, + { + "name": "index_message_identity", + "unique": false, + "columnNames": [ + "identity" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_message_identity` ON `${TABLE_NAME}` (`identity`)" + }, + { + "name": "index_message_folder_uid", + "unique": true, + "columnNames": [ + "folder", + "uid" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_message_folder_uid` ON `${TABLE_NAME}` (`folder`, `uid`)" + }, + { + "name": "index_message_msgid", + "unique": false, + "columnNames": [ + "msgid" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_message_msgid` ON `${TABLE_NAME}` (`msgid`)" + }, + { + "name": "index_message_thread", + "unique": false, + "columnNames": [ + "thread" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_message_thread` ON `${TABLE_NAME}` (`thread`)" + }, + { + "name": "index_message_sender", + "unique": false, + "columnNames": [ + "sender" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_message_sender` ON `${TABLE_NAME}` (`sender`)" + }, + { + "name": "index_message_received", + "unique": false, + "columnNames": [ + "received" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_message_received` ON `${TABLE_NAME}` (`received`)" + }, + { + "name": "index_message_subject", + "unique": false, + "columnNames": [ + "subject" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_message_subject` ON `${TABLE_NAME}` (`subject`)" + }, + { + "name": "index_message_ui_seen", + "unique": false, + "columnNames": [ + "ui_seen" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_message_ui_seen` ON `${TABLE_NAME}` (`ui_seen`)" + }, + { + "name": "index_message_ui_flagged", + "unique": false, + "columnNames": [ + "ui_flagged" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_message_ui_flagged` ON `${TABLE_NAME}` (`ui_flagged`)" + }, + { + "name": "index_message_ui_hide", + "unique": false, + "columnNames": [ + "ui_hide" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_message_ui_hide` ON `${TABLE_NAME}` (`ui_hide`)" + }, + { + "name": "index_message_ui_found", + "unique": false, + "columnNames": [ + "ui_found" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_message_ui_found` ON `${TABLE_NAME}` (`ui_found`)" + }, + { + "name": "index_message_ui_ignored", + "unique": false, + "columnNames": [ + "ui_ignored" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_message_ui_ignored` ON `${TABLE_NAME}` (`ui_ignored`)" + }, + { + "name": "index_message_ui_browsed", + "unique": false, + "columnNames": [ + "ui_browsed" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_message_ui_browsed` ON `${TABLE_NAME}` (`ui_browsed`)" + }, + { + "name": "index_message_ui_snoozed", + "unique": false, + "columnNames": [ + "ui_snoozed" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_message_ui_snoozed` ON `${TABLE_NAME}` (`ui_snoozed`)" + } + ], + "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": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "identity" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "message", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "replying" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "message", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "forwarding" + ], + "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, `disposition` TEXT, `cid` TEXT, `encryption` INTEGER, `size` INTEGER, `progress` INTEGER, `available` INTEGER NOT NULL, `error` TEXT, 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": "disposition", + "columnName": "disposition", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "cid", + "columnName": "cid", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "encryption", + "columnName": "encryption", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "progress", + "columnName": "progress", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "available", + "columnName": "available", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "error", + "columnName": "error", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_attachment_message", + "unique": false, + "columnNames": [ + "message" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_attachment_message` ON `${TABLE_NAME}` (`message`)" + }, + { + "name": "index_attachment_message_sequence", + "unique": true, + "columnNames": [ + "message", + "sequence" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_attachment_message_sequence` ON `${TABLE_NAME}` (`message`, `sequence`)" + }, + { + "name": "index_attachment_message_cid", + "unique": false, + "columnNames": [ + "message", + "cid" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_attachment_message_cid` ON `${TABLE_NAME}` (`message`, `cid`)" + } + ], + "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, `account` INTEGER, `folder` INTEGER NOT NULL, `message` INTEGER, `name` TEXT NOT NULL, `args` TEXT NOT NULL, `created` INTEGER NOT NULL, `state` TEXT, `error` TEXT, 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": "account", + "columnName": "account", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "folder", + "columnName": "folder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "message", + "columnName": "message", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "args", + "columnName": "args", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "created", + "columnName": "created", + "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_operation_account", + "unique": false, + "columnNames": [ + "account" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_operation_account` ON `${TABLE_NAME}` (`account`)" + }, + { + "name": "index_operation_folder", + "unique": false, + "columnNames": [ + "folder" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_operation_folder` ON `${TABLE_NAME}` (`folder`)" + }, + { + "name": "index_operation_message", + "unique": false, + "columnNames": [ + "message" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_operation_message` ON `${TABLE_NAME}` (`message`)" + }, + { + "name": "index_operation_name", + "unique": false, + "columnNames": [ + "name" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_operation_name` ON `${TABLE_NAME}` (`name`)" + }, + { + "name": "index_operation_state", + "unique": false, + "columnNames": [ + "state" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_operation_state` ON `${TABLE_NAME}` (`state`)" + } + ], + "foreignKeys": [ + { + "table": "folder", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "folder" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "message", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "message" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "contact", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `account` INTEGER NOT NULL, `type` INTEGER NOT NULL, `email` TEXT NOT NULL, `name` TEXT, `avatar` TEXT, `times_contacted` INTEGER NOT NULL, `first_contacted` INTEGER NOT NULL, `last_contacted` INTEGER NOT NULL, `state` INTEGER NOT NULL, 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": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "avatar", + "columnName": "avatar", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "times_contacted", + "columnName": "times_contacted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "first_contacted", + "columnName": "first_contacted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "last_contacted", + "columnName": "last_contacted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_contact_account_type_email", + "unique": true, + "columnNames": [ + "account", + "type", + "email" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_contact_account_type_email` ON `${TABLE_NAME}` (`account`, `type`, `email`)" + }, + { + "name": "index_contact_email", + "unique": false, + "columnNames": [ + "email" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_contact_email` ON `${TABLE_NAME}` (`email`)" + }, + { + "name": "index_contact_name", + "unique": false, + "columnNames": [ + "name" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_contact_name` ON `${TABLE_NAME}` (`name`)" + }, + { + "name": "index_contact_avatar", + "unique": false, + "columnNames": [ + "avatar" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_contact_avatar` ON `${TABLE_NAME}` (`avatar`)" + }, + { + "name": "index_contact_times_contacted", + "unique": false, + "columnNames": [ + "times_contacted" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_contact_times_contacted` ON `${TABLE_NAME}` (`times_contacted`)" + }, + { + "name": "index_contact_last_contacted", + "unique": false, + "columnNames": [ + "last_contacted" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_contact_last_contacted` ON `${TABLE_NAME}` (`last_contacted`)" + }, + { + "name": "index_contact_state", + "unique": false, + "columnNames": [ + "state" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_contact_state` ON `${TABLE_NAME}` (`state`)" + } + ], + "foreignKeys": [ + { + "table": "account", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "account" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "certificate", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `fingerprint` TEXT NOT NULL, `email` TEXT NOT NULL, `subject` TEXT, `after` INTEGER, `before` INTEGER, `data` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "fingerprint", + "columnName": "fingerprint", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "after", + "columnName": "after", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "before", + "columnName": "before", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "data", + "columnName": "data", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_certificate_fingerprint_email", + "unique": true, + "columnNames": [ + "fingerprint", + "email" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_certificate_fingerprint_email` ON `${TABLE_NAME}` (`fingerprint`, `email`)" + }, + { + "name": "index_certificate_email", + "unique": false, + "columnNames": [ + "email" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_certificate_email` ON `${TABLE_NAME}` (`email`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "answer", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `name` TEXT NOT NULL, `hide` INTEGER NOT NULL, `text` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hide", + "columnName": "hide", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rule", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `folder` INTEGER NOT NULL, `name` TEXT NOT NULL, `order` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `stop` INTEGER NOT NULL, `condition` TEXT NOT NULL, `action` TEXT NOT NULL, `applied` INTEGER NOT NULL, FOREIGN KEY(`folder`) REFERENCES `folder`(`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": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "stop", + "columnName": "stop", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "condition", + "columnName": "condition", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "action", + "columnName": "action", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "applied", + "columnName": "applied", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_rule_folder", + "unique": false, + "columnNames": [ + "folder" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_rule_folder` ON `${TABLE_NAME}` (`folder`)" + }, + { + "name": "index_rule_order", + "unique": false, + "columnNames": [ + "order" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_rule_order` ON `${TABLE_NAME}` (`order`)" + } + ], + "foreignKeys": [ + { + "table": "folder", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "folder" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "log", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `time` INTEGER NOT NULL, `data` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "data", + "columnName": "data", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_log_time", + "unique": false, + "columnNames": [ + "time" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_log_time` ON `${TABLE_NAME}` (`time`)" + } + ], + "foreignKeys": [] + } + ], + "views": [], + "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, 'd8d88f3b2f4188b4097e505a9d5e732a')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/eu/faircode/email/DB.java b/app/src/main/java/eu/faircode/email/DB.java index d87b1c6a8c..51e0c253be 100644 --- a/app/src/main/java/eu/faircode/email/DB.java +++ b/app/src/main/java/eu/faircode/email/DB.java @@ -56,7 +56,7 @@ import io.requery.android.database.sqlite.RequerySQLiteOpenHelperFactory; // https://developer.android.com/topic/libraries/architecture/room.html @Database( - version = 123, + version = 124, entities = { EntityIdentity.class, EntityAccount.class, @@ -1196,6 +1196,14 @@ public abstract class DB extends RoomDatabase { db.execSQL("ALTER TABLE `identity` ADD COLUMN `fingerprint` TEXT"); } }) + .addMigrations(new Migration(123, 124) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase db) { + Log.i("DB migration from version " + startVersion + " to " + endVersion); + db.execSQL("ALTER TABLE `account` ADD COLUMN `provider` TEXT"); + db.execSQL("ALTER TABLE `identity` ADD COLUMN `provider` TEXT"); + } + }) .build(); } diff --git a/app/src/main/java/eu/faircode/email/EmailProvider.java b/app/src/main/java/eu/faircode/email/EmailProvider.java index 0a2345f298..394600da9e 100644 --- a/app/src/main/java/eu/faircode/email/EmailProvider.java +++ b/app/src/main/java/eu/faircode/email/EmailProvider.java @@ -36,6 +36,7 @@ import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlPullParserFactory; +import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStreamReader; import java.net.HttpURLConnection; @@ -57,6 +58,7 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; public class EmailProvider { + public String id; public String name; public List domain; public int order; @@ -107,6 +109,7 @@ public class EmailProvider { result = new ArrayList<>(); else if ("provider".equals(name)) { provider = new EmailProvider(); + provider.id = xml.getAttributeValue(null, "id"); provider.name = xml.getAttributeValue(null, "name"); String domain = xml.getAttributeValue(null, "domain"); if (domain != null) @@ -170,6 +173,14 @@ public class EmailProvider { return result; } + static EmailProvider getProvider(Context context, String id) throws FileNotFoundException { + for (EmailProvider provider : loadProfiles(context)) + if (id.equals(provider.id)) + return provider; + + throw new FileNotFoundException("provider id=" + id); + } + @NonNull static EmailProvider fromDomain(Context context, String domain, Discover discover) throws IOException { return fromEmail(context, domain, discover); diff --git a/app/src/main/java/eu/faircode/email/EntityAccount.java b/app/src/main/java/eu/faircode/email/EntityAccount.java index 211f137e12..49a6c1ed74 100644 --- a/app/src/main/java/eu/faircode/email/EntityAccount.java +++ b/app/src/main/java/eu/faircode/email/EntityAccount.java @@ -72,6 +72,7 @@ public class EntityAccount extends EntityOrder implements Serializable { public Integer port; @NonNull public Integer auth_type; // immutable + public String provider; @NonNull public String user; @NonNull diff --git a/app/src/main/java/eu/faircode/email/EntityIdentity.java b/app/src/main/java/eu/faircode/email/EntityIdentity.java index 287ce93476..31c877368b 100644 --- a/app/src/main/java/eu/faircode/email/EntityIdentity.java +++ b/app/src/main/java/eu/faircode/email/EntityIdentity.java @@ -72,6 +72,7 @@ public class EntityIdentity { public Integer port; @NonNull public Integer auth_type; + public String provider; @NonNull public String user; @NonNull diff --git a/app/src/main/java/eu/faircode/email/FragmentAccount.java b/app/src/main/java/eu/faircode/email/FragmentAccount.java index 15daa2dd0e..f3f86ebede 100644 --- a/app/src/main/java/eu/faircode/email/FragmentAccount.java +++ b/app/src/main/java/eu/faircode/email/FragmentAccount.java @@ -143,6 +143,7 @@ public class FragmentAccount extends FragmentBase { private long id = -1; private long copy = -1; private int auth = MailService.AUTH_TYPE_PASSWORD; + private String provider = null; private boolean saving = false; private static final int REQUEST_COLOR = 1; @@ -500,6 +501,7 @@ public class FragmentAccount extends FragmentBase { args.putBoolean("insecure", cbInsecure.isChecked()); args.putString("port", etPort.getText().toString()); args.putInt("auth", auth); + args.putString("provider", provider); args.putString("user", etUser.getText().toString()); args.putString("password", tilPassword.getEditText().getText().toString()); args.putString("realm", etRealm.getText().toString()); @@ -537,6 +539,7 @@ public class FragmentAccount extends FragmentBase { boolean insecure = args.getBoolean("insecure"); String port = args.getString("port"); int auth = args.getInt("auth"); + String provider = args.getString("provider"); String user = args.getString("user"); String password = args.getString("password"); String realm = args.getString("realm"); @@ -568,7 +571,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, true)) { - iservice.connect(host, Integer.parseInt(port), auth, user, password, fingerprint); + iservice.connect(host, Integer.parseInt(port), auth, provider, user, password, fingerprint); result.idle = iservice.hasCapability("IDLE"); @@ -707,6 +710,7 @@ public class FragmentAccount extends FragmentBase { args.putBoolean("insecure", cbInsecure.isChecked()); args.putString("port", etPort.getText().toString()); args.putInt("auth", auth); + args.putString("provider", provider); args.putString("user", etUser.getText().toString()); args.putString("password", tilPassword.getEditText().getText().toString()); args.putString("realm", etRealm.getText().toString()); @@ -767,6 +771,7 @@ public class FragmentAccount extends FragmentBase { boolean insecure = args.getBoolean("insecure"); String port = args.getString("port"); int auth = args.getInt("auth"); + String provider = args.getString("provider"); String user = args.getString("user").trim(); String password = args.getString("password"); String realm = args.getString("realm"); @@ -942,7 +947,7 @@ public class FragmentAccount extends FragmentBase { if (check) { String protocol = "imap" + (starttls ? "" : "s"); try (MailService iservice = new MailService(context, protocol, realm, insecure, true, true)) { - iservice.connect(host, Integer.parseInt(port), auth, user, password, fingerprint); + iservice.connect(host, Integer.parseInt(port), auth, provider, user, password, fingerprint); for (Folder ifolder : iservice.getStore().getDefaultFolder().list("*")) { // Check folder attributes @@ -989,6 +994,7 @@ public class FragmentAccount extends FragmentBase { account.user = user; account.password = password; } + account.provider = provider; account.realm = realm; account.fingerprint = fingerprint; @@ -1213,6 +1219,7 @@ public class FragmentAccount extends FragmentBase { outState.putString("fair:password", tilPassword.getEditText().getText().toString()); outState.putInt("fair:advanced", grpAdvanced.getVisibility()); outState.putInt("fair:auth", auth); + outState.putString("fair:authprovider", provider); super.onSaveInstanceState(outState); } @@ -1300,6 +1307,7 @@ public class FragmentAccount extends FragmentBase { cbUseDate.setChecked(account == null ? false : account.use_date); auth = (account == null ? MailService.AUTH_TYPE_PASSWORD : account.auth_type); + provider = (account == null ? null : account.provider); new SimpleTask() { @Override @@ -1319,13 +1327,14 @@ public class FragmentAccount extends FragmentBase { } }.execute(FragmentAccount.this, new Bundle(), "account:primary"); } else { - int provider = savedInstanceState.getInt("fair:provider"); - spProvider.setTag(provider); - spProvider.setSelection(provider); + int p = savedInstanceState.getInt("fair:provider"); + spProvider.setTag(p); + spProvider.setSelection(p); tilPassword.getEditText().setText(savedInstanceState.getString("fair:password")); grpAdvanced.setVisibility(savedInstanceState.getInt("fair:advanced")); auth = savedInstanceState.getInt("fair:auth"); + provider = savedInstanceState.getString("fair:authprovider"); } Helper.setViewsEnabled(view, true); diff --git a/app/src/main/java/eu/faircode/email/FragmentGmail.java b/app/src/main/java/eu/faircode/email/FragmentGmail.java index bb6a184d55..d9487a5815 100644 --- a/app/src/main/java/eu/faircode/email/FragmentGmail.java +++ b/app/src/main/java/eu/faircode/email/FragmentGmail.java @@ -284,7 +284,7 @@ public class FragmentGmail extends FragmentBase { String aprotocol = provider.imap.starttls ? "imap" : "imaps"; try (MailService iservice = new MailService(context, aprotocol, null, false, true, true)) { - iservice.connect(provider.imap.host, provider.imap.port, MailService.AUTH_TYPE_GMAIL, user, password, null); + iservice.connect(provider.imap.host, provider.imap.port, MailService.AUTH_TYPE_GMAIL, null, user, password, null); folders = iservice.getFolders(); @@ -294,7 +294,7 @@ public class FragmentGmail extends FragmentBase { String iprotocol = provider.smtp.starttls ? "smtp" : "smtps"; try (MailService iservice = new MailService(context, iprotocol, null, false, true, true)) { - iservice.connect(provider.smtp.host, provider.smtp.port, MailService.AUTH_TYPE_GMAIL, user, password, null); + iservice.connect(provider.smtp.host, provider.smtp.port, MailService.AUTH_TYPE_GMAIL, null, user, password, null); } DB db = DB.getInstance(context); diff --git a/app/src/main/java/eu/faircode/email/FragmentIdentity.java b/app/src/main/java/eu/faircode/email/FragmentIdentity.java index 8502cc7f36..c9a24c8e71 100644 --- a/app/src/main/java/eu/faircode/email/FragmentIdentity.java +++ b/app/src/main/java/eu/faircode/email/FragmentIdentity.java @@ -125,6 +125,7 @@ public class FragmentIdentity extends FragmentBase { private long copy = -1; private long account = -1; private int auth = MailService.AUTH_TYPE_PASSWORD; + private String provider = null; private boolean saving = false; private static final int REQUEST_COLOR = 1; @@ -253,7 +254,8 @@ public class FragmentIdentity extends FragmentBase { } // Copy account credentials - auth = (account.auth_type == null ? MailService.AUTH_TYPE_PASSWORD : account.auth_type); + auth = account.auth_type; + provider = account.provider; etEmail.setText(account.user); etUser.setText(account.user); tilPassword.getEditText().setText(account.password); @@ -519,6 +521,7 @@ public class FragmentIdentity extends FragmentBase { args.putBoolean("insecure", cbInsecure.isChecked()); args.putString("port", etPort.getText().toString()); args.putInt("auth", auth); + args.putString("provider", provider); args.putString("user", etUser.getText().toString()); args.putString("password", tilPassword.getEditText().getText().toString()); args.putString("realm", etRealm.getText().toString()); @@ -567,6 +570,7 @@ public class FragmentIdentity extends FragmentBase { boolean insecure = args.getBoolean("insecure"); String port = args.getString("port"); int auth = args.getInt("auth"); + String provider = args.getString("provider"); String user = args.getString("user").trim(); String password = args.getString("password"); String realm = args.getString("realm"); @@ -741,7 +745,7 @@ public class FragmentIdentity extends FragmentBase { String protocol = (starttls ? "smtp" : "smtps"); try (MailService iservice = new MailService(context, protocol, realm, insecure, true, true)) { iservice.setUseIp(use_ip); - iservice.connect(host, Integer.parseInt(port), auth, user, password, fingerprint); + iservice.connect(host, Integer.parseInt(port), auth, provider, user, password, fingerprint); } } @@ -775,6 +779,7 @@ public class FragmentIdentity extends FragmentBase { identity.user = user; identity.password = password; } + identity.provider = provider; identity.realm = realm; identity.fingerprint = fingerprint; identity.use_ip = use_ip; @@ -881,6 +886,7 @@ public class FragmentIdentity extends FragmentBase { outState.putString("fair:password", tilPassword.getEditText().getText().toString()); outState.putInt("fair:advanced", grpAdvanced.getVisibility()); outState.putInt("fair:auth", auth); + outState.putString("fair:authprovider", provider); outState.putString("fair:html", (String) etSignature.getTag()); super.onSaveInstanceState(outState); } @@ -941,6 +947,7 @@ public class FragmentIdentity extends FragmentBase { etBcc.setText(identity == null ? null : identity.bcc); auth = (identity == null ? MailService.AUTH_TYPE_PASSWORD : identity.auth_type); + provider = (identity == null ? null : identity.provider); if (identity == null || copy > 0) new SimpleTask() { @@ -963,6 +970,7 @@ public class FragmentIdentity extends FragmentBase { tilPassword.getEditText().setText(savedInstanceState.getString("fair:password")); grpAdvanced.setVisibility(savedInstanceState.getInt("fair:advanced")); auth = savedInstanceState.getInt("fair:auth"); + provider = savedInstanceState.getString("fair:authprovider"); etSignature.setTag(savedInstanceState.getString("fair:html")); } diff --git a/app/src/main/java/eu/faircode/email/FragmentOAuth.java b/app/src/main/java/eu/faircode/email/FragmentOAuth.java index 97ae48341a..7da9c7ce8f 100644 --- a/app/src/main/java/eu/faircode/email/FragmentOAuth.java +++ b/app/src/main/java/eu/faircode/email/FragmentOAuth.java @@ -75,6 +75,7 @@ import java.util.Map; import static android.app.Activity.RESULT_OK; public class FragmentOAuth extends FragmentBase { + private String id; private String name; private ViewGroup view; @@ -97,6 +98,7 @@ public class FragmentOAuth extends FragmentBase { super.onCreate(savedInstanceState); Bundle args = getArguments(); + id = args.getString("id"); name = args.getString("name"); } @@ -133,7 +135,7 @@ public class FragmentOAuth extends FragmentBase { tvGrantHint.setText(getString(R.string.title_setup_oauth_rationale, name)); pbOAuth.setVisibility(View.GONE); tvAuthorized.setVisibility(View.GONE); - tvGmailHint.setVisibility("Gmail".equals(name) ? View.VISIBLE : View.GONE); + tvGmailHint.setVisibility("gmail".equals(id) ? View.VISIBLE : View.GONE); hideError(); return view; @@ -186,61 +188,56 @@ public class FragmentOAuth extends FragmentBase { pbOAuth.setVisibility(View.VISIBLE); hideError(); - for (EmailProvider provider : EmailProvider.loadProfiles(getContext())) - if (provider.name.equals(name) && provider.oauth != null) { - AppAuthConfiguration appAuthConfig = new AppAuthConfiguration.Builder() - .setBrowserMatcher(new BrowserMatcher() { - @Override - public boolean matches(@NonNull BrowserDescriptor descriptor) { - BrowserMatcher sbrowser = new VersionedBrowserMatcher( - Browsers.SBrowser.PACKAGE_NAME, - Browsers.SBrowser.SIGNATURE_SET, - true, - VersionRange.atMost("5.3")); - return !sbrowser.matches(descriptor); - } - }) - .build(); - - AuthorizationService authService = new AuthorizationService(getContext(), appAuthConfig); - - AuthorizationServiceConfiguration serviceConfig = new AuthorizationServiceConfiguration( - Uri.parse(provider.oauth.authorizationEndpoint), - Uri.parse(provider.oauth.tokenEndpoint)); - - AuthState authState = new AuthState(serviceConfig); - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext()); - prefs.edit().putString("oauth." + provider.name, authState.jsonSerializeString()).apply(); - - Map params = new HashMap<>(); - if ("Gmail".equals(provider.name)) - params.put("access_type", "offline"); - - AuthorizationRequest.Builder authRequestBuilder = - new AuthorizationRequest.Builder( - serviceConfig, - provider.oauth.clientId, - ResponseTypeValues.CODE, - Uri.parse(provider.oauth.redirectUri)) - .setScopes(provider.oauth.scopes) - .setState(provider.name) - .setAdditionalParameters(params); - - if ("Gmail".equals(provider.name) && BuildConfig.DEBUG) - authRequestBuilder.setPrompt("consent"); - - AuthorizationRequest authRequest = authRequestBuilder.build(); - - Log.i("OAuth request provider=" + provider.name); - if (BuildConfig.DEBUG) - Log.i("OAuth uri=" + authRequest.toUri()); - Intent authIntent = authService.getAuthorizationRequestIntent(authRequest); - startActivityForResult(authIntent, ActivitySetup.REQUEST_OAUTH); - - return; - } - - throw new IllegalArgumentException("Unknown provider=" + name); + EmailProvider provider = EmailProvider.getProvider(getContext(), id); + + AppAuthConfiguration appAuthConfig = new AppAuthConfiguration.Builder() + .setBrowserMatcher(new BrowserMatcher() { + @Override + public boolean matches(@NonNull BrowserDescriptor descriptor) { + BrowserMatcher sbrowser = new VersionedBrowserMatcher( + Browsers.SBrowser.PACKAGE_NAME, + Browsers.SBrowser.SIGNATURE_SET, + true, + VersionRange.atMost("5.3")); + return !sbrowser.matches(descriptor); + } + }) + .build(); + + AuthorizationService authService = new AuthorizationService(getContext(), appAuthConfig); + + AuthorizationServiceConfiguration serviceConfig = new AuthorizationServiceConfiguration( + Uri.parse(provider.oauth.authorizationEndpoint), + Uri.parse(provider.oauth.tokenEndpoint)); + + AuthState authState = new AuthState(serviceConfig); + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext()); + prefs.edit().putString("oauth." + provider.id, authState.jsonSerializeString()).apply(); + + Map params = new HashMap<>(); + if ("gmail".equals(provider.id)) + params.put("access_type", "offline"); + + AuthorizationRequest.Builder authRequestBuilder = + new AuthorizationRequest.Builder( + serviceConfig, + provider.oauth.clientId, + ResponseTypeValues.CODE, + Uri.parse(provider.oauth.redirectUri)) + .setScopes(provider.oauth.scopes) + .setState(provider.id) + .setAdditionalParameters(params); + + if ("gmail".equals(provider.id) && BuildConfig.DEBUG) + authRequestBuilder.setPrompt("consent"); + + AuthorizationRequest authRequest = authRequestBuilder.build(); + + Log.i("OAuth request provider=" + provider.id); + if (BuildConfig.DEBUG) + Log.i("OAuth uri=" + authRequest.toUri()); + Intent authIntent = authService.getAuthorizationRequestIntent(authRequest); + startActivityForResult(authIntent, ActivitySetup.REQUEST_OAUTH); } catch (Throwable ex) { showError(ex); btnOAuth.setEnabled(true); @@ -256,54 +253,51 @@ public class FragmentOAuth extends FragmentBase { tvAuthorized.setVisibility(View.VISIBLE); - for (final EmailProvider provider : EmailProvider.loadProfiles(getContext())) - if (provider.name.equals(auth.state)) { - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext()); - final AuthState authState = AuthState.jsonDeserialize(prefs.getString("oauth." + provider.name, null)); - prefs.edit().remove("oauth." + provider.name).apply(); - - Log.i("OAuth get token provider=" + provider.name); - authState.update(auth, null); - if (BuildConfig.DEBUG) - Log.i("OAuth response=" + authState.jsonSerializeString()); - - AuthorizationService authService = new AuthorizationService(getContext()); - - ClientAuthentication clientAuth; - if (provider.oauth.clientSecret == null) - clientAuth = NoClientAuthentication.INSTANCE; - else - clientAuth = new ClientSecretPost(provider.oauth.clientSecret); - - authService.performTokenRequest( - auth.createTokenExchangeRequest(), - clientAuth, - new AuthorizationService.TokenResponseCallback() { - @Override - public void onTokenRequestCompleted(TokenResponse access, AuthorizationException error) { - try { - if (access == null) - throw error; - - Log.i("OAuth got token provider=" + provider.name); - authState.update(access, null); - if (BuildConfig.DEBUG) - Log.i("OAuth response=" + authState.jsonSerializeString()); - - if (TextUtils.isEmpty(access.refreshToken)) - throw new IllegalStateException("No refresh token"); - - onOAuthorized(access.accessToken, authState); - } catch (Throwable ex) { - showError(ex); - } - } - }); - - return; - } + final EmailProvider provider = EmailProvider.getProvider(getContext(), auth.state); + + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext()); + String json = prefs.getString("oauth." + provider.id, null); + prefs.edit().remove("oauth." + provider.id).apply(); + + final AuthState authState = AuthState.jsonDeserialize(json); + + Log.i("OAuth get token provider=" + provider.id); + authState.update(auth, null); + if (BuildConfig.DEBUG) + Log.i("OAuth response=" + authState.jsonSerializeString()); - throw new IllegalArgumentException("Unknown state=" + auth.state); + AuthorizationService authService = new AuthorizationService(getContext()); + + ClientAuthentication clientAuth; + if (provider.oauth.clientSecret == null) + clientAuth = NoClientAuthentication.INSTANCE; + else + clientAuth = new ClientSecretPost(provider.oauth.clientSecret); + + authService.performTokenRequest( + auth.createTokenExchangeRequest(), + clientAuth, + new AuthorizationService.TokenResponseCallback() { + @Override + public void onTokenRequestCompleted(TokenResponse access, AuthorizationException error) { + try { + if (access == null) + throw error; + + Log.i("OAuth got token provider=" + provider.id); + authState.update(access, null); + if (BuildConfig.DEBUG) + Log.i("OAuth response=" + authState.jsonSerializeString()); + + if (TextUtils.isEmpty(access.refreshToken)) + throw new IllegalStateException("No refresh token"); + + onOAuthorized(access.accessToken, authState); + } catch (Throwable ex) { + showError(ex); + } + } + }); } catch (Throwable ex) { showError(ex); btnOAuth.setEnabled(true); @@ -313,6 +307,7 @@ public class FragmentOAuth extends FragmentBase { private void onOAuthorized(String accessToken, AuthState state) { Bundle args = new Bundle(); + args.putString("id", id); args.putString("name", name); args.putString("token", accessToken); args.putString("state", state.jsonSerializeString()); @@ -320,6 +315,7 @@ public class FragmentOAuth extends FragmentBase { new SimpleTask() { @Override protected Void onExecute(Context context, Bundle args) throws Throwable { + String id = args.getString("id"); String name = args.getString("name"); String token = args.getString("token"); String state = args.getString("state"); @@ -327,7 +323,7 @@ public class FragmentOAuth extends FragmentBase { String primaryEmail = null; List> identities = new ArrayList<>(); - if ("Gmail".equals(name)) { + if ("gmail".equals(id)) { // https://developers.google.com/gmail/api/v1/reference/users/getProfile URL url = new URL("https://www.googleapis.com/gmail/v1/users/me/settings/sendAs"); Log.i("Fetching " + url); @@ -366,7 +362,7 @@ public class FragmentOAuth extends FragmentBase { } } - } else if ("Outlook/Office365".equals(name)) { + } else if ("outlook".equals(id)) { // https://docs.microsoft.com/en-us/graph/api/user-get?view=graph-rest-1.0&tabs=http#http-request URL url = new URL("https://graph.microsoft.com/v1.0/me?$select=displayName,otherMails"); Log.i("Fetching " + url); @@ -403,7 +399,7 @@ public class FragmentOAuth extends FragmentBase { } } } else - throw new IllegalArgumentException("Unknown provider=" + name); + throw new IllegalArgumentException("Unknown provider=" + id); if (TextUtils.isEmpty(primaryEmail) || identities.size() == 0) throw new IllegalArgumentException("Primary email address not found"); @@ -412,101 +408,101 @@ public class FragmentOAuth extends FragmentBase { for (Pair identity : identities) Log.i("OAuth identity=" + identity.first + "/" + identity.second); - for (EmailProvider provider : EmailProvider.loadProfiles(context)) - if (provider.name.equals(name)) { + EmailProvider provider = EmailProvider.getProvider(context, id); - List folders; + List folders; - Log.i("OAuth checking IMAP provider=" + provider.name); - String aprotocol = provider.imap.starttls ? "imap" : "imaps"; - try (MailService iservice = new MailService(context, aprotocol, null, false, true, true)) { - iservice.connect(provider.imap.host, provider.imap.port, MailService.AUTH_TYPE_OAUTH, primaryEmail, state, null); + Log.i("OAuth checking IMAP provider=" + provider.id); + String aprotocol = provider.imap.starttls ? "imap" : "imaps"; + try (MailService iservice = new MailService(context, aprotocol, null, false, true, true)) { + iservice.connect(provider.imap.host, provider.imap.port, MailService.AUTH_TYPE_OAUTH, provider.id, primaryEmail, state, null); - folders = iservice.getFolders(); + folders = iservice.getFolders(); - if (folders == null) - throw new IllegalArgumentException(context.getString(R.string.title_setup_no_system_folders)); - } + if (folders == null) + throw new IllegalArgumentException(context.getString(R.string.title_setup_no_system_folders)); + } - Log.i("OAuth checking SMTP provider=" + provider.name); - String iprotocol = provider.smtp.starttls ? "smtp" : "smtps"; - try (MailService iservice = new MailService(context, iprotocol, null, false, true, true)) { - iservice.connect(provider.smtp.host, provider.smtp.port, MailService.AUTH_TYPE_OAUTH, primaryEmail, state, null); - } + Log.i("OAuth checking SMTP provider=" + provider.id); + String iprotocol = provider.smtp.starttls ? "smtp" : "smtps"; + try (MailService iservice = new MailService(context, iprotocol, null, false, true, true)) { + iservice.connect(provider.smtp.host, provider.smtp.port, MailService.AUTH_TYPE_OAUTH, provider.id, primaryEmail, state, null); + } - Log.i("OAuth passed provider=" + provider.name); + Log.i("OAuth passed provider=" + provider.id); - DB db = DB.getInstance(context); - try { - db.beginTransaction(); + DB db = DB.getInstance(context); + try { + db.beginTransaction(); - EntityAccount primary = db.account().getPrimaryAccount(); + EntityAccount primary = db.account().getPrimaryAccount(); - // Create account - EntityAccount account = new EntityAccount(); + // 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_OAUTH; - account.user = primaryEmail; - account.password = state; + account.host = provider.imap.host; + account.starttls = provider.imap.starttls; + account.port = provider.imap.port; + account.auth_type = MailService.AUTH_TYPE_OAUTH; + account.provider = provider.id; + account.user = primaryEmail; + account.password = state; - account.name = provider.name; + account.name = provider.name; - account.synchronize = true; - account.primary = (primary == null); + account.synchronize = true; + account.primary = (primary == null); - account.created = new Date().getTime(); - account.last_connected = account.created; + account.created = new Date().getTime(); + account.last_connected = account.created; - account.id = db.account().insertAccount(account); - args.putLong("account", account.id); - EntityLog.log(context, "OAuth account=" + account.name); + account.id = db.account().insertAccount(account); + args.putLong("account", account.id); + EntityLog.log(context, "OAuth account=" + account.name); - // Create folders - for (EntityFolder folder : folders) { - folder.account = account.id; - folder.id = db.folder().insertFolder(folder); - EntityLog.log(context, "OAuth folder=" + folder.name + " type=" + folder.type); - } + // Create folders + for (EntityFolder folder : folders) { + folder.account = account.id; + folder.id = db.folder().insertFolder(folder); + EntityLog.log(context, "OAuth 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); - - // Create identities - for (Pair identity : identities) { - EntityIdentity ident = new EntityIdentity(); - ident.name = identity.second; - ident.email = identity.first; - ident.account = account.id; - - ident.host = provider.smtp.host; - ident.starttls = provider.smtp.starttls; - ident.port = provider.smtp.port; - ident.auth_type = MailService.AUTH_TYPE_OAUTH; - ident.user = primaryEmail; - ident.password = state; - ident.synchronize = true; - ident.primary = ident.user.equals(ident.email); - - ident.id = db.identity().insertIdentity(ident); - EntityLog.log(context, "OAuth identity=" + ident.name + " email=" + ident.email); - } + // 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); + + // Create identities + for (Pair identity : identities) { + EntityIdentity ident = new EntityIdentity(); + ident.name = identity.second; + ident.email = identity.first; + ident.account = account.id; + + ident.host = provider.smtp.host; + ident.starttls = provider.smtp.starttls; + ident.port = provider.smtp.port; + ident.auth_type = MailService.AUTH_TYPE_OAUTH; + ident.provider = provider.id; + ident.user = primaryEmail; + ident.password = state; + ident.synchronize = true; + ident.primary = ident.user.equals(ident.email); + + ident.id = db.identity().insertIdentity(ident); + EntityLog.log(context, "OAuth identity=" + ident.name + " email=" + ident.email); + } - db.setTransactionSuccessful(); - } finally { - db.endTransaction(); - } + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } - ServiceSynchronize.eval(context, "OAuth"); - } + ServiceSynchronize.eval(context, "OAuth"); return null; } @@ -542,7 +538,7 @@ public class FragmentOAuth extends FragmentBase { grpError.setVisibility(View.VISIBLE); - if ("Gmail".equals(name)) + if ("gmail".equals(id)) tvGmailDraftsHint.setVisibility(View.VISIBLE); new Handler().post(new Runnable() { diff --git a/app/src/main/java/eu/faircode/email/FragmentPop.java b/app/src/main/java/eu/faircode/email/FragmentPop.java index 02e823baa0..ec6bce5f26 100644 --- a/app/src/main/java/eu/faircode/email/FragmentPop.java +++ b/app/src/main/java/eu/faircode/email/FragmentPop.java @@ -278,7 +278,7 @@ public class FragmentPop extends FragmentBase { if (check) { String protocol = "pop3" + (starttls ? "" : "s"); try (MailService iservice = new MailService(context, protocol, null, insecure, true, true)) { - iservice.connect(host, Integer.parseInt(port), MailService.AUTH_TYPE_PASSWORD, user, password, null); + iservice.connect(host, Integer.parseInt(port), MailService.AUTH_TYPE_PASSWORD, null, user, password, null); } } diff --git a/app/src/main/java/eu/faircode/email/FragmentQuickSetup.java b/app/src/main/java/eu/faircode/email/FragmentQuickSetup.java index c198b1d354..eb1b1018c1 100644 --- a/app/src/main/java/eu/faircode/email/FragmentQuickSetup.java +++ b/app/src/main/java/eu/faircode/email/FragmentQuickSetup.java @@ -250,13 +250,13 @@ public class FragmentQuickSetup extends FragmentBase { String aprotocol = provider.imap.starttls ? "imap" : "imaps"; try (MailService iservice = new MailService(context, aprotocol, null, false, true, true)) { try { - iservice.connect(provider.imap.host, provider.imap.port, MailService.AUTH_TYPE_PASSWORD, user, password, null); + iservice.connect(provider.imap.host, provider.imap.port, MailService.AUTH_TYPE_PASSWORD, null, user, password, null); } catch (AuthenticationFailedException ex) { if (!user.equals(username)) { Log.w(ex); user = username; Log.i("Retry with user=" + user); - iservice.connect(provider.imap.host, provider.imap.port, MailService.AUTH_TYPE_PASSWORD, user, password, null); + iservice.connect(provider.imap.host, provider.imap.port, MailService.AUTH_TYPE_PASSWORD, null, user, password, null); } else throw ex; } @@ -270,7 +270,7 @@ public class FragmentQuickSetup extends FragmentBase { String iprotocol = provider.smtp.starttls ? "smtp" : "smtps"; try (MailService iservice = new MailService(context, iprotocol, null, false, true, true)) { iservice.setUseIp(provider.useip); - iservice.connect(provider.smtp.host, provider.smtp.port, MailService.AUTH_TYPE_PASSWORD, user, password, null); + iservice.connect(provider.smtp.host, provider.smtp.port, MailService.AUTH_TYPE_PASSWORD, null, user, password, null); } if (check) diff --git a/app/src/main/java/eu/faircode/email/FragmentSetup.java b/app/src/main/java/eu/faircode/email/FragmentSetup.java index cfecaa6364..9b3396c40f 100644 --- a/app/src/main/java/eu/faircode/email/FragmentSetup.java +++ b/app/src/main/java/eu/faircode/email/FragmentSetup.java @@ -174,6 +174,7 @@ public class FragmentSetup extends FragmentBase { popupMenu.getMenu() .add(Menu.NONE, -1, order++, getString(R.string.title_setup_oauth, provider.name)) .setIntent(new Intent(ActivitySetup.ACTION_QUICK_OAUTH) + .putExtra("id", provider.id) .putExtra("name", provider.name)); popupMenu.getMenu().add(Menu.NONE, R.string.title_setup_activesync, order++, R.string.title_setup_activesync); diff --git a/app/src/main/java/eu/faircode/email/MailService.java b/app/src/main/java/eu/faircode/email/MailService.java index 43518619b1..9164c96690 100644 --- a/app/src/main/java/eu/faircode/email/MailService.java +++ b/app/src/main/java/eu/faircode/email/MailService.java @@ -17,6 +17,9 @@ import com.sun.mail.util.MailConnectException; import net.openid.appauth.AuthState; import net.openid.appauth.AuthorizationException; import net.openid.appauth.AuthorizationService; +import net.openid.appauth.ClientAuthentication; +import net.openid.appauth.ClientSecretPost; +import net.openid.appauth.NoClientAuthentication; import org.bouncycastle.asn1.x509.GeneralName; import org.jetbrains.annotations.NotNull; @@ -200,7 +203,7 @@ public class MailService implements AutoCloseable { } public void connect(EntityAccount account) throws MessagingException { - String password = connect(account.host, account.port, account.auth_type, account.user, account.password, account.fingerprint); + String password = connect(account.host, account.port, account.auth_type, account.provider, account.user, account.password, account.fingerprint); if (password != null) { DB db = DB.getInstance(context); int count = db.account().setAccountPassword(account.id, account.password); @@ -209,7 +212,7 @@ public class MailService implements AutoCloseable { } public void connect(EntityIdentity identity) throws MessagingException { - String password = connect(identity.host, identity.port, identity.auth_type, identity.user, identity.password, identity.fingerprint); + String password = connect(identity.host, identity.port, identity.auth_type, identity.provider, identity.user, identity.password, identity.fingerprint); if (password != null) { DB db = DB.getInstance(context); int count = db.identity().setIdentityPassword(identity.id, identity.password); @@ -217,7 +220,7 @@ public class MailService implements AutoCloseable { } } - public String connect(String host, int port, int auth, String user, String password, String fingerprint) throws MessagingException { + public String connect(String host, int port, int auth, String provider, String user, String password, String fingerprint) throws MessagingException { SSLSocketFactoryService factory = null; try { factory = new SSLSocketFactoryService(host, insecure, fingerprint); @@ -240,7 +243,7 @@ public class MailService implements AutoCloseable { // new SocketConnectException("Debug", new Exception("Test"), host, port, 0)); if (auth == AUTH_TYPE_OAUTH) { - AuthState authState = OAuthRefresh(context, password); + AuthState authState = OAuthRefresh(context, provider, password); _connect(context, host, port, user, authState.getAccessToken(), factory); return authState.jsonSerializeString(); } else { @@ -272,7 +275,7 @@ public class MailService implements AutoCloseable { throw new AuthenticationFailedException(ex.getMessage(), ex1); } else if (auth == AUTH_TYPE_OAUTH) { - AuthState authState = OAuthRefresh(context, password); + AuthState authState = OAuthRefresh(context, provider, password); _connect(context, host, port, user, authState.getAccessToken(), factory); return authState.jsonSerializeString(); } else @@ -377,24 +380,33 @@ public class MailService implements AutoCloseable { AuthorizationException error; } - static AuthState OAuthRefresh(Context context, String json) throws MessagingException { + private static AuthState OAuthRefresh(Context context, String id, String json) throws MessagingException { try { AuthState authState = AuthState.jsonDeserialize(json); - Semaphore semaphore = new Semaphore(0); + ClientAuthentication clientAuth; + EmailProvider provider = EmailProvider.getProvider(context, id); + if (provider.oauth.clientSecret == null) + clientAuth = NoClientAuthentication.INSTANCE; + else + clientAuth = new ClientSecretPost(provider.oauth.clientSecret); ErrorHolder holder = new ErrorHolder(); + Semaphore semaphore = new Semaphore(0); Log.i("OAuth refresh"); AuthorizationService authService = new AuthorizationService(context); - authState.performActionWithFreshTokens(authService, new AuthState.AuthStateAction() { - @Override - public void execute(String accessToken, String idToken, AuthorizationException error) { - if (error != null) - holder.error = error; - semaphore.release(); - } - }); + authState.performActionWithFreshTokens( + authService, + clientAuth, + new AuthState.AuthStateAction() { + @Override + public void execute(String accessToken, String idToken, AuthorizationException error) { + if (error != null) + holder.error = error; + semaphore.release(); + } + }); semaphore.acquire(); Log.i("OAuth refreshed"); diff --git a/app/src/main/res/xml/providers.xml b/app/src/main/res/xml/providers.xml index 7a33726346..315aaaa802 100644 --- a/app/src/main/res/xml/providers.xml +++ b/app/src/main/res/xml/providers.xml @@ -3,6 +3,7 @@ @@ -28,6 +29,7 @@