diff --git a/FAQ.md b/FAQ.md index 3315bd74..b8b1a491 100644 --- a/FAQ.md +++ b/FAQ.md @@ -28,12 +28,13 @@ Most, if not all, other email apps don't show a notification with the "side effe The low priority status bar notification shows the number of pending operations, which can be: -* SEEN: mark message as seen/unseen in remote folder -* ADD: add message to remote folder -* MOVE: move message to another remote folder -* DELETE: delete message from remote folder -* SEND: send message -* ATTACHMENT download attachment +* seen: mark message as seen/unseen in remote folder +* add: add message to remote folder +* move: move message to another remote folder +* delete: delete message from remote folder +* send: send message +* attachment download attachment +* headers: download message headers **(4) What is a valid security certificate?** diff --git a/app/schemas/eu.faircode.email.DB/9.json b/app/schemas/eu.faircode.email.DB/9.json new file mode 100644 index 00000000..d7566f89 --- /dev/null +++ b/app/schemas/eu.faircode.email.DB/9.json @@ -0,0 +1,905 @@ +{ + "formatVersion": 1, + "database": { + "version": 9, + "identityHash": "67fade7db3a87ec2ef27dcec483c456f", + "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, `headers` 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, `ui_found` 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": "headers", + "columnName": "headers", + "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": "ui_found", + "columnName": "ui_found", + "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`)" + }, + { + "name": "index_message_ui_found", + "unique": false, + "columnNames": [ + "ui_found" + ], + "createSql": "CREATE INDEX `index_message_ui_found` ON `${TABLE_NAME}` (`ui_found`)" + } + ], + "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": [] + }, + { + "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 `index_log_time` ON `${TABLE_NAME}` (`time`)" + } + ], + "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, \"67fade7db3a87ec2ef27dcec483c456f\")" + ] + } +} \ 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 29deb4cf..b8956093 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 = 8, + version = 9, entities = { EntityIdentity.class, EntityAccount.class, @@ -168,6 +168,13 @@ public abstract class DB extends RoomDatabase { db.execSQL("CREATE INDEX `index_message_ui_found` ON `message` (`ui_found`)"); } }) + .addMigrations(new Migration(8, 9) { + @Override + public void migrate(SupportSQLiteDatabase db) { + Log.i(Helper.TAG, "DB migration from version " + startVersion + " to " + endVersion); + db.execSQL("ALTER TABLE `message` ADD COLUMN `headers` TEXT"); + } + }) .build(); } diff --git a/app/src/main/java/eu/faircode/email/DaoMessage.java b/app/src/main/java/eu/faircode/email/DaoMessage.java index d8318182..a9c88c59 100644 --- a/app/src/main/java/eu/faircode/email/DaoMessage.java +++ b/app/src/main/java/eu/faircode/email/DaoMessage.java @@ -167,6 +167,9 @@ public interface DaoMessage { @Query("UPDATE message SET ui_found = 0 WHERE folder = :folder") int resetFound(long folder); + @Query("UPDATE message SET headers = :headers WHERE id = :id") + int setMessageHeaders(long id, String headers); + @Query("DELETE FROM message WHERE id = :id") int deleteMessage(long id); diff --git a/app/src/main/java/eu/faircode/email/EntityMessage.java b/app/src/main/java/eu/faircode/email/EntityMessage.java index 5ef6663b..1d721eff 100644 --- a/app/src/main/java/eu/faircode/email/EntityMessage.java +++ b/app/src/main/java/eu/faircode/email/EntityMessage.java @@ -87,6 +87,7 @@ public class EntityMessage implements Serializable { public Address[] cc; public Address[] bcc; public Address[] reply; + public String headers; public String subject; public Long sent; // compose = null @NonNull diff --git a/app/src/main/java/eu/faircode/email/EntityOperation.java b/app/src/main/java/eu/faircode/email/EntityOperation.java index 4922eaca..53d3c402 100644 --- a/app/src/main/java/eu/faircode/email/EntityOperation.java +++ b/app/src/main/java/eu/faircode/email/EntityOperation.java @@ -71,6 +71,7 @@ public class EntityOperation { public static final String DELETE = "delete"; public static final String SEND = "send"; public static final String ATTACHMENT = "attachment"; + public static final String HEADERS = "headers"; private static List queue = new ArrayList<>(); diff --git a/app/src/main/java/eu/faircode/email/FragmentMessage.java b/app/src/main/java/eu/faircode/email/FragmentMessage.java index ff5af432..2206bf64 100644 --- a/app/src/main/java/eu/faircode/email/FragmentMessage.java +++ b/app/src/main/java/eu/faircode/email/FragmentMessage.java @@ -120,6 +120,8 @@ public class FragmentMessage extends FragmentEx { private TextView tvReplyTo; private TextView tvCc; private TextView tvBcc; + private TextView tvRawHeaders; + private ProgressBar pbRawHeaders; private RecyclerView rvAttachment; private TextView tvError; private View vSeparatorBody; @@ -132,12 +134,15 @@ public class FragmentMessage extends FragmentEx { private Group grpHeader; private Group grpThread; private Group grpAddresses; + private Group grpRawHeaders; private Group grpAttachments; private Group grpError; private Group grpMessage; private TupleMessageEx message = null; private boolean free = false; + private boolean addresses = false; + private boolean headers = false; private AdapterAttachment adapter; private OpenPgpServiceConnection openPgpConnection = null; @@ -205,6 +210,8 @@ public class FragmentMessage extends FragmentEx { tvReplyTo = view.findViewById(R.id.tvReplyTo); tvCc = view.findViewById(R.id.tvCc); tvBcc = view.findViewById(R.id.tvBcc); + tvRawHeaders = view.findViewById(R.id.tvRawHeaders); + pbRawHeaders = view.findViewById(R.id.pbRawHeaders); rvAttachment = view.findViewById(R.id.rvAttachment); tvError = view.findViewById(R.id.tvError); vSeparatorBody = view.findViewById(R.id.vSeparatorBody); @@ -217,6 +224,7 @@ public class FragmentMessage extends FragmentEx { grpHeader = view.findViewById(R.id.grpHeader); grpThread = view.findViewById(R.id.grpThread); grpAddresses = view.findViewById(R.id.grpAddresses); + grpRawHeaders = view.findViewById(R.id.grpRawHeaders); grpAttachments = view.findViewById(R.id.grpAttachments); grpError = view.findViewById(R.id.grpError); grpMessage = view.findViewById(R.id.grpMessage); @@ -295,11 +303,10 @@ public class FragmentMessage extends FragmentEx { grpThread.setVisibility(View.GONE); grpAddresses.setVisibility(View.GONE); + pbRawHeaders.setVisibility(View.GONE); + grpRawHeaders.setVisibility(View.GONE); grpAttachments.setVisibility(View.GONE); grpError.setVisibility(View.GONE); - - tvCc.setTag(grpAddresses.getVisibility()); - tvError.setTag(grpError.getVisibility()); } }); @@ -317,9 +324,10 @@ public class FragmentMessage extends FragmentEx { RecyclerView.Adapter adapter = rvAttachment.getAdapter(); grpThread.setVisibility(View.VISIBLE); - grpAddresses.setVisibility((int) tvCc.getTag()); + grpAddresses.setVisibility(addresses ? View.VISIBLE : View.GONE); + pbRawHeaders.setVisibility(headers && message.headers == null ? View.VISIBLE : View.GONE); + grpRawHeaders.setVisibility(headers ? View.VISIBLE : View.GONE); grpAttachments.setVisibility(adapter != null && adapter.getItemCount() > 0 ? View.VISIBLE : View.GONE); - grpError.setVisibility((int) tvError.getTag()); return true; } @@ -354,6 +362,8 @@ public class FragmentMessage extends FragmentEx { // Initialize grpHeader.setVisibility(View.GONE); grpAddresses.setVisibility(View.GONE); + pbRawHeaders.setVisibility(View.GONE); + grpRawHeaders.setVisibility(View.GONE); grpAttachments.setVisibility(View.GONE); btnImages.setVisibility(View.GONE); grpMessage.setVisibility(View.GONE); @@ -372,9 +382,6 @@ public class FragmentMessage extends FragmentEx { adapter = new AdapterAttachment(getContext(), getViewLifecycleOwner(), true); rvAttachment.setAdapter(adapter); - tvCc.setTag(View.GONE); - tvError.setTag(View.GONE); - return view; } @@ -389,10 +396,8 @@ public class FragmentMessage extends FragmentEx { super.onSaveInstanceState(outState); outState.putSerializable("message", message); outState.putBoolean("free", free); - if (free) { - outState.putInt("tag_cc", (int) tvCc.getTag()); - outState.putInt("tag_error", (int) tvError.getTag()); - } + outState.putBoolean("headers", headers); + outState.putBoolean("addresses", addresses); } @Override @@ -413,13 +418,13 @@ public class FragmentMessage extends FragmentEx { tvCc.setText(MessageHelper.getFormattedAddresses(message.cc, true)); tvBcc.setText(MessageHelper.getFormattedAddresses(message.bcc, true)); + tvRawHeaders.setText(message.headers); + tvError.setText(message.error); } else { free = savedInstanceState.getBoolean("free"); - if (free) { - tvCc.setTag(savedInstanceState.getInt("tag_cc")); - tvError.setTag(savedInstanceState.getInt("tag_error")); - } + headers = savedInstanceState.getBoolean("headers"); + addresses = savedInstanceState.getBoolean("addresses"); } if (tvBody.getTag() == null) { @@ -449,14 +454,11 @@ public class FragmentMessage extends FragmentEx { grpHeader.setVisibility(free ? View.GONE : View.VISIBLE); vSeparatorBody.setVisibility(free ? View.GONE : View.VISIBLE); - if (free) { - grpThread.setVisibility(View.GONE); - grpAddresses.setVisibility((int) tvCc.getTag()); - grpError.setVisibility((int) tvError.getTag()); - } else { - grpThread.setVisibility(!free ? View.VISIBLE : View.GONE); - grpError.setVisibility(free || message.error == null ? View.GONE : View.VISIBLE); - } + grpAddresses.setVisibility(!free && addresses ? View.VISIBLE : View.GONE); + grpThread.setVisibility(free ? View.GONE : View.VISIBLE); + pbRawHeaders.setVisibility(!free && headers && message.headers == null ? View.VISIBLE : View.GONE); + grpRawHeaders.setVisibility(free || !headers ? View.GONE : View.VISIBLE); + grpError.setVisibility(message.error == null ? View.GONE : View.VISIBLE); final DB db = DB.getInstance(getContext()); @@ -475,6 +477,10 @@ public class FragmentMessage extends FragmentEx { FragmentMessage.this.message = message; setSeen(); + // Headers can be downloaded + tvRawHeaders.setText(message.headers); + pbRawHeaders.setVisibility(!free && headers && message.headers == null ? View.VISIBLE : View.GONE); + // Message count can be changed getActivity().invalidateOptionsMenu(); @@ -565,6 +571,9 @@ public class FragmentMessage extends FragmentEx { menu.findItem(R.id.menu_addresses).setVisible(!free); menu.findItem(R.id.menu_thread).setVisible(message.count > 1); menu.findItem(R.id.menu_forward).setVisible(!inOutbox); + menu.findItem(R.id.menu_show_headers).setChecked(headers); + menu.findItem(R.id.menu_show_headers).setEnabled(message.uid != null); + menu.findItem(R.id.menu_show_headers).setVisible(!free); menu.findItem(R.id.menu_reply_all).setVisible(message.cc != null && !inOutbox); menu.findItem(R.id.menu_decrypt).setVisible(!inOutbox); } @@ -587,6 +596,9 @@ public class FragmentMessage extends FragmentEx { case R.id.menu_show_html: onMenuShowHtml(); return true; + case R.id.menu_show_headers: + onMenuShowHeaders(); + return true; case R.id.menu_answer: onMenuAnswer(); return true; @@ -599,7 +611,8 @@ public class FragmentMessage extends FragmentEx { } private void onMenuAddresses() { - grpAddresses.setVisibility(grpAddresses.getVisibility() == View.GONE ? View.VISIBLE : View.GONE); + addresses = !addresses; + grpAddresses.setVisibility(addresses ? View.VISIBLE : View.GONE); } private void onMenuThread() { @@ -628,6 +641,30 @@ public class FragmentMessage extends FragmentEx { .putExtra("reference", message.id)); } + private void onMenuShowHeaders() { + headers = !headers; + getActivity().invalidateOptionsMenu(); + pbRawHeaders.setVisibility(headers && message.headers == null ? View.VISIBLE : View.GONE); + grpRawHeaders.setVisibility(headers ? View.VISIBLE : View.GONE); + + if (headers && message.headers == null) { + Bundle args = new Bundle(); + args.putLong("id", message.id); + + new SimpleTask() { + @Override + protected Void onLoad(Context context, Bundle args) throws Throwable { + Long id = args.getLong("id"); + DB db = DB.getInstance(context); + EntityMessage message = db.message().getMessage(id); + EntityOperation.queue(db, message, EntityOperation.HEADERS); + EntityOperation.process(context); + return null; + } + }.load(this, args); + } + } + private void onMenuShowHtml() { new SimpleTask() { @Override diff --git a/app/src/main/java/eu/faircode/email/ServiceSynchronize.java b/app/src/main/java/eu/faircode/email/ServiceSynchronize.java index e152c314..89e129fc 100644 --- a/app/src/main/java/eu/faircode/email/ServiceSynchronize.java +++ b/app/src/main/java/eu/faircode/email/ServiceSynchronize.java @@ -66,6 +66,7 @@ import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Calendar; import java.util.Date; +import java.util.Enumeration; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -80,6 +81,7 @@ import javax.mail.Flags; import javax.mail.Folder; import javax.mail.FolderClosedException; import javax.mail.FolderNotFoundException; +import javax.mail.Header; import javax.mail.Message; import javax.mail.MessageRemovedException; import javax.mail.MessagingException; @@ -841,7 +843,8 @@ public class ServiceSynchronize extends LifecycleService { if (message.uid == null && (EntityOperation.SEEN.equals(op.name) || EntityOperation.DELETE.equals(op.name) || - EntityOperation.MOVE.equals(op.name))) + EntityOperation.MOVE.equals(op.name) || + EntityOperation.HEADERS.equals(op.name))) throw new IllegalArgumentException(op.name + " without uid"); JSONArray jargs = new JSONArray(op.args); @@ -864,6 +867,9 @@ public class ServiceSynchronize extends LifecycleService { else if (EntityOperation.ATTACHMENT.equals(op.name)) doAttachment(folder, op, ifolder, message, jargs, db); + else if (EntityOperation.HEADERS.equals(op.name)) + doHeaders(folder, ifolder, message, db); + else throw new MessagingException("Unknown operation name=" + op.name); @@ -1124,6 +1130,17 @@ public class ServiceSynchronize extends LifecycleService { } } + private void doHeaders(EntityFolder folder, IMAPFolder ifolder, EntityMessage message, DB db) throws MessagingException { + Message imessage = ifolder.getMessageByUID(message.uid); + Enumeration
headers = imessage.getAllHeaders(); + StringBuilder sb = new StringBuilder(); + while (headers.hasMoreElements()) { + Header header = headers.nextElement(); + sb.append(header.getName()).append(": ").append(header.getValue()).append("\n"); + } + db.message().setMessageHeaders(message.id, sb.toString()); + } + private void synchronizeFolders(EntityAccount account, IMAPStore istore, ServiceState state) throws MessagingException { try { Log.v(Helper.TAG, "Start sync folders"); diff --git a/app/src/main/res/layout/fragment_message.xml b/app/src/main/res/layout/fragment_message.xml index bdde3d66..bd89e953 100644 --- a/app/src/main/res/layout/fragment_message.xml +++ b/app/src/main/res/layout/fragment_message.xml @@ -185,8 +185,9 @@ app:layout_constraintStart_toEndOf="@id/tvBccTitle" app:layout_constraintTop_toBottomOf="@id/tvCc" /> + + + + + + + + + + + Mark unread Forward Reply to all + Show headers Show original Trash