diff --git a/app/schemas/eu.faircode.email.DB/1.json b/app/schemas/eu.faircode.email.DB/1.json index 9d1e88fc..7e6cce59 100644 --- a/app/schemas/eu.faircode.email.DB/1.json +++ b/app/schemas/eu.faircode.email.DB/1.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 1, - "identityHash": "959322c3e61ed9307a830e355bb2b8ab", + "identityHash": "9fd2cb9e7b45bf1dbddced278a737dfa", "entities": [ { "tableName": "identity", @@ -113,7 +113,7 @@ }, { "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, `primary` INTEGER NOT NULL, `synchronize` INTEGER NOT NULL, `seen_until` INTEGER)", + "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, `primary` INTEGER NOT NULL, `synchronize` INTEGER NOT NULL, `seen_until` INTEGER, `error` TEXT)", "fields": [ { "fieldPath": "id", @@ -168,6 +168,12 @@ "columnName": "seen_until", "affinity": "INTEGER", "notNull": false + }, + { + "fieldPath": "error", + "columnName": "error", + "affinity": "TEXT", + "notNull": false } ], "primaryKey": { @@ -181,7 +187,7 @@ }, { "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, FOREIGN KEY(`account`) REFERENCES `account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "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, `error` TEXT, FOREIGN KEY(`account`) REFERENCES `account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", "fields": [ { "fieldPath": "id", @@ -218,6 +224,12 @@ "columnName": "after", "affinity": "INTEGER", "notNull": true + }, + { + "fieldPath": "error", + "columnName": "error", + "affinity": "TEXT", + "notNull": false } ], "primaryKey": { @@ -277,7 +289,7 @@ }, { "tableName": "message", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `account` INTEGER, `folder` INTEGER NOT NULL, `identity` INTEGER, `replying` INTEGER, `uid` INTEGER, `msgid` TEXT, `references` TEXT, `inreplyto` TEXT, `thread` TEXT, `from` TEXT, `to` TEXT, `cc` TEXT, `bcc` TEXT, `reply` TEXT, `subject` TEXT, `body` TEXT, `sent` INTEGER, `received` INTEGER NOT NULL, `seen` INTEGER NOT NULL, `ui_seen` INTEGER NOT NULL, `ui_hide` INTEGER NOT NULL, 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 )", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `account` INTEGER, `folder` INTEGER NOT NULL, `identity` INTEGER, `replying` INTEGER, `uid` INTEGER, `msgid` TEXT, `references` TEXT, `inreplyto` TEXT, `thread` TEXT, `from` TEXT, `to` TEXT, `cc` TEXT, `bcc` TEXT, `reply` TEXT, `subject` TEXT, `body` TEXT, `sent` INTEGER, `received` INTEGER NOT NULL, `seen` INTEGER NOT NULL, `ui_seen` INTEGER NOT NULL, `ui_hide` INTEGER NOT NULL, `error` TEXT, FOREIGN KEY(`account`) REFERENCES `account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`folder`) REFERENCES `folder`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`identity`) REFERENCES `identity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`replying`) REFERENCES `message`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", "fields": [ { "fieldPath": "id", @@ -410,6 +422,12 @@ "columnName": "ui_hide", "affinity": "INTEGER", "notNull": true + }, + { + "fieldPath": "error", + "columnName": "error", + "affinity": "TEXT", + "notNull": false } ], "primaryKey": { @@ -634,7 +652,7 @@ }, { "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, 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 )", + "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, `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", @@ -664,6 +682,12 @@ "fieldPath": "args", "columnName": "args", "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "error", + "columnName": "error", + "affinity": "TEXT", "notNull": false } ], @@ -675,12 +699,12 @@ }, "indices": [ { - "name": "index_operation_message", + "name": "index_operation_folder", "unique": false, "columnNames": [ - "message" + "folder" ], - "createSql": "CREATE INDEX `index_operation_message` ON `${TABLE_NAME}` (`message`)" + "createSql": "CREATE INDEX `index_operation_folder` ON `${TABLE_NAME}` (`folder`)" }, { "name": "index_operation_message", @@ -719,7 +743,7 @@ ], "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, \"959322c3e61ed9307a830e355bb2b8ab\")" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"9fd2cb9e7b45bf1dbddced278a737dfa\")" ] } } \ No newline at end of file diff --git a/app/src/main/java/eu/faircode/email/AdapterAccount.java b/app/src/main/java/eu/faircode/email/AdapterAccount.java index 5e5eda3d..93998d6e 100644 --- a/app/src/main/java/eu/faircode/email/AdapterAccount.java +++ b/app/src/main/java/eu/faircode/email/AdapterAccount.java @@ -54,6 +54,7 @@ public class AdapterAccount extends RecyclerView.Adapter 0 ? View.VISIBLE : View.GONE); tvSubject.setText(message.subject); String extra = (debug ? (message.ui_hide ? "HIDDEN " : "") + message.uid + "/" + message.id + " " : ""); @@ -109,7 +112,8 @@ public class AdapterMessage extends PagedListAdapter 0 ? View.VISIBLE : View.GONE); + tvError.setText(message.error); + tvError.setVisibility(message.error == null ? View.GONE : View.VISIBLE); int typeface = (message.unseen > 0 ? Typeface.BOLD : Typeface.NORMAL); tvFrom.setTypeface(null, typeface); diff --git a/app/src/main/java/eu/faircode/email/DaoOperation.java b/app/src/main/java/eu/faircode/email/DaoOperation.java index 6d513ddf..aa065b24 100644 --- a/app/src/main/java/eu/faircode/email/DaoOperation.java +++ b/app/src/main/java/eu/faircode/email/DaoOperation.java @@ -25,6 +25,7 @@ import androidx.room.Dao; import androidx.room.Insert; import androidx.room.OnConflictStrategy; import androidx.room.Query; +import androidx.room.Update; @Dao public interface DaoOperation { @@ -37,6 +38,9 @@ public interface DaoOperation { @Query("SELECT COUNT(id) FROM operation WHERE folder = :folder") int getOperationCount(long folder); + @Update + void updateOperation(EntityOperation operation); + @Query("DELETE FROM operation WHERE id = :id") void deleteOperation(long id); diff --git a/app/src/main/java/eu/faircode/email/EntityAccount.java b/app/src/main/java/eu/faircode/email/EntityAccount.java index b3f313a7..5197dbe5 100644 --- a/app/src/main/java/eu/faircode/email/EntityAccount.java +++ b/app/src/main/java/eu/faircode/email/EntityAccount.java @@ -47,6 +47,7 @@ public class EntityAccount { @NonNull public Boolean synchronize; public Long seen_until; + public String error; @Override public boolean equals(Object obj) { @@ -58,7 +59,8 @@ public class EntityAccount { this.user.equals(other.user) && this.password.equals(other.password) && this.primary.equals(other.primary) && - this.synchronize.equals(other.synchronize)); + this.synchronize.equals(other.synchronize) && + (this.error == null ? other.error == null : this.error.equals(other.error))); } else return false; } diff --git a/app/src/main/java/eu/faircode/email/EntityFolder.java b/app/src/main/java/eu/faircode/email/EntityFolder.java index ccefb2dc..62d840e2 100644 --- a/app/src/main/java/eu/faircode/email/EntityFolder.java +++ b/app/src/main/java/eu/faircode/email/EntityFolder.java @@ -46,6 +46,19 @@ import static androidx.room.ForeignKey.CASCADE; public class EntityFolder implements Serializable { static final String TABLE_NAME = "folder"; + @PrimaryKey(autoGenerate = true) + public Long id; + public Long account; // Outbox = null + @NonNull + public String name; + @NonNull + public String type; + @NonNull + public Boolean synchronize; + @NonNull + public Integer after; // days + public String error; + static final String INBOX = "Inbox"; static final String OUTBOX = "Outbox"; static final String ARCHIVE = "All"; @@ -92,18 +105,6 @@ public class EntityFolder implements Serializable { SENT ); - @PrimaryKey(autoGenerate = true) - public Long id; - public Long account; // Outbox = null - @NonNull - public String name; - @NonNull - public String type; - @NonNull - public Boolean synchronize; - @NonNull - public Integer after; // days - @Override public boolean equals(Object obj) { if (obj instanceof EntityFolder) { @@ -112,7 +113,8 @@ public class EntityFolder implements Serializable { this.name.equals(other.name) && this.type.equals(other.type) && this.synchronize.equals(other.synchronize) && - this.after.equals(other.after)); + this.after.equals(other.after) && + (this.error == null ? other.error == null : this.error.equals(other.error))); } else return false; } diff --git a/app/src/main/java/eu/faircode/email/EntityMessage.java b/app/src/main/java/eu/faircode/email/EntityMessage.java index 052b3881..c6771274 100644 --- a/app/src/main/java/eu/faircode/email/EntityMessage.java +++ b/app/src/main/java/eu/faircode/email/EntityMessage.java @@ -82,6 +82,7 @@ public class EntityMessage { public Boolean ui_seen; @NonNull public Boolean ui_hide; + public String error; @Override public boolean equals(Object obj) { @@ -107,7 +108,8 @@ public class EntityMessage { this.received.equals(other.received) && this.seen.equals(other.seen) && this.ui_seen.equals(other.ui_seen) && - this.ui_hide.equals(other.ui_hide)); + this.ui_hide.equals(other.ui_hide) && + (this.error == null ? other.error == null : this.error.equals(other.error))); } return false; } diff --git a/app/src/main/java/eu/faircode/email/EntityOperation.java b/app/src/main/java/eu/faircode/email/EntityOperation.java index 91785667..4c5ec85c 100644 --- a/app/src/main/java/eu/faircode/email/EntityOperation.java +++ b/app/src/main/java/eu/faircode/email/EntityOperation.java @@ -59,7 +59,9 @@ public class EntityOperation { public Long message; @NonNull public String name; + @NonNull public String args; + public String error; public static final String SEEN = "seen"; public static final String ADD = "add"; @@ -123,4 +125,17 @@ public class EntityOperation { queue.clear(); } } + + @Override + public boolean equals(Object obj) { + if (obj instanceof EntityOperation) { + EntityOperation other = (EntityOperation) obj; + return (this.folder.equals(other.folder) && + this.message.equals(other.message) && + this.name.equals(other.name) && + this.args.equals(other.args) && + (this.error == null ? other.error == null : this.error.equals(other.error))); + } else + return false; + } } diff --git a/app/src/main/java/eu/faircode/email/ServiceSynchronize.java b/app/src/main/java/eu/faircode/email/ServiceSynchronize.java index 60a43bc6..130786ad 100644 --- a/app/src/main/java/eu/faircode/email/ServiceSynchronize.java +++ b/app/src/main/java/eu/faircode/email/ServiceSynchronize.java @@ -344,23 +344,26 @@ public class ServiceSynchronize extends LifecycleService { // Listen for connection changes istore.addConnectionListener(new ConnectionAdapter() { - List folderThreads = new ArrayList<>(); Map mapFolder = new HashMap<>(); @Override public void opened(ConnectionEvent e) { Log.i(Helper.TAG, account.name + " opened"); - try { - DB db = DB.getInstance(ServiceSynchronize.this); + DB db = DB.getInstance(ServiceSynchronize.this); + account.error = null; + db.account().updateAccount(account); + + try { synchronizeFolders(account, fstore); for (final EntityFolder folder : db.folder().getFolders(account.id, true)) { Log.i(Helper.TAG, account.name + " sync folder " + folder.name); - Thread thread = new Thread(new Runnable() { + new Thread(new Runnable() { @Override public void run() { IMAPFolder ifolder = null; + DB db = DB.getInstance(ServiceSynchronize.this); try { Log.i(Helper.TAG, folder.name + " start"); @@ -371,19 +374,25 @@ public class ServiceSynchronize extends LifecycleService { mapFolder.put(folder.id, ifolder); } + folder.error = null; + db.folder().updateFolder(folder); + monitorFolder(account, folder, fstore, ifolder, state); - } catch (FolderNotFoundException ex) { - Log.w(Helper.TAG, folder.name + " " + ex + "\n" + Log.getStackTraceString(ex)); + } catch (Throwable ex) { Log.e(Helper.TAG, folder.name + " " + ex + "\n" + Log.getStackTraceString(ex)); reportError(account.name, folder.name, ex); + folder.error = Helper.formatThrowable(ex); + db.folder().updateFolder(folder); + // Cascade up - try { - fstore.close(); - } catch (MessagingException e1) { - Log.w(Helper.TAG, account.name + " " + e1 + "\n" + Log.getStackTraceString(e1)); - } + if (!(ex instanceof FolderNotFoundException)) + try { + fstore.close(); + } catch (MessagingException e1) { + Log.w(Helper.TAG, account.name + " " + e1 + "\n" + Log.getStackTraceString(e1)); + } } finally { if (ifolder != null && ifolder.isOpen()) { try { @@ -395,15 +404,14 @@ public class ServiceSynchronize extends LifecycleService { Log.i(Helper.TAG, folder.name + " stop"); } } - }, "sync.folder." + folder.id); - folderThreads.add(thread); - thread.start(); + }, "sync.folder." + folder.id).start(); } IntentFilter f = new IntentFilter(ACTION_PROCESS_FOLDER); f.addDataType("account/" + account.id); LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(ServiceSynchronize.this); lbm.registerReceiver(processReceiver, f); + Log.i(Helper.TAG, "listen process folder"); for (final EntityFolder folder : db.folder().getFolders(account.id)) if (!EntityFolder.OUTBOX.equals(folder.type)) @@ -415,6 +423,9 @@ public class ServiceSynchronize extends LifecycleService { Log.e(Helper.TAG, account.name + " " + ex + "\n" + Log.getStackTraceString(ex)); reportError(account.name, null, ex); + account.error = Helper.formatThrowable(ex); + db.account().updateAccount(account); + // Cascade up try { fstore.close(); @@ -490,8 +501,6 @@ public class ServiceSynchronize extends LifecycleService { } processOperations(folder, fstore, ifolder); - } catch (FolderNotFoundException ex) { - Log.w(Helper.TAG, folder.name + " " + ex + "\n" + Log.getStackTraceString(ex)); } catch (Throwable ex) { Log.e(Helper.TAG, folder.name + " " + ex + "\n" + Log.getStackTraceString(ex)); reportError(account.name, folder.name, ex); @@ -538,7 +547,11 @@ public class ServiceSynchronize extends LifecycleService { Log.i(Helper.TAG, account.name + " not running anymore"); } catch (Throwable ex) { - Log.w(Helper.TAG, account.name + " " + ex + "\n" + Log.getStackTraceString(ex)); + Log.e(Helper.TAG, account.name + " " + ex + "\n" + Log.getStackTraceString(ex)); + reportError(account.name, null, ex); + + account.error = Helper.formatThrowable(ex); + DB.getInstance(this).account().updateAccount(account); } finally { if (istore != null) { try { @@ -633,6 +646,9 @@ public class ServiceSynchronize extends LifecycleService { Log.e(Helper.TAG, folder.name + " " + ex + "\n" + Log.getStackTraceString(ex)); reportError(account.name, folder.name, ex); + folder.error = Helper.formatThrowable(ex); + DB.getInstance(ServiceSynchronize.this).folder().updateFolder(folder); + // Cascade up try { istore.close(); @@ -665,6 +681,9 @@ public class ServiceSynchronize extends LifecycleService { Log.e(Helper.TAG, folder.name + " " + ex + "\n" + Log.getStackTraceString(ex)); reportError(account.name, folder.name, ex); + folder.error = Helper.formatThrowable(ex); + DB.getInstance(ServiceSynchronize.this).folder().updateFolder(folder); + // Cascade up try { istore.close(); @@ -699,9 +718,10 @@ public class ServiceSynchronize extends LifecycleService { " msg=" + op.message + " args=" + op.args); - JSONArray jargs = new JSONArray(op.args); - EntityMessage message = db.message().getMessage(op.message); try { + JSONArray jargs = new JSONArray(op.args); + EntityMessage message = db.message().getMessage(op.message); + if (EntityOperation.SEEN.equals(op.name)) doSeen(folder, ifolder, jargs, message); @@ -727,32 +747,28 @@ public class ServiceSynchronize extends LifecycleService { // Operation succeeded db.operation().deleteOperation(op.id); + } catch (Throwable ex) { + op.error = Helper.formatThrowable(ex); + db.operation().updateOperation(op); + + if (ex instanceof MessageRemovedException || + ex instanceof FolderNotFoundException || + ex instanceof SMTPSendFailedException) { + Log.w(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex)); + + // There is no use in repeating + db.operation().deleteOperation(op.id); + continue; + } else if (ex instanceof MessagingException) { + // Socket timeout is a recoverable condition (send message) + if (ex.getCause() instanceof SocketTimeoutException) { + Log.w(Helper.TAG, "Recoverable " + ex); + // No need to inform user + return; + } + } - } catch (MessageRemovedException ex) { - Log.w(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex)); - - // There is no use in repeating - db.operation().deleteOperation(op.id); - } catch (FolderNotFoundException ex) { - Log.w(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex)); - - // There is no use in repeating - db.operation().deleteOperation(op.id); - } catch (SMTPSendFailedException ex) { - // TODO: response codes: https://www.ietf.org/rfc/rfc821.txt - Log.w(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex)); - - // There is probably no use in repeating - db.operation().deleteOperation(op.id); throw ex; - } catch (MessagingException ex) { - // Socket timeout is a recoverable condition (send message) - if (ex.getCause() instanceof SocketTimeoutException) { - Log.w(Helper.TAG, "Recoverable " + ex); - // No need to inform user - return; - } else - throw ex; } } finally { Log.i(Helper.TAG, folder.name + " end op=" + op.id + "/" + op.name); @@ -894,10 +910,17 @@ public class ServiceSynchronize extends LifecycleService { itransport.connect(ident.host, ident.port, ident.user, ident.password); // Send message - Address[] to = imessage.getAllRecipients(); - itransport.sendMessage(imessage, to); - Log.i(Helper.TAG, "Sent via " + ident.host + "/" + ident.user + - " to " + TextUtils.join(", ", to)); + try { + Address[] to = imessage.getAllRecipients(); + itransport.sendMessage(imessage, to); + Log.i(Helper.TAG, "Sent via " + ident.host + "/" + ident.user + + " to " + TextUtils.join(", ", to)); + } catch (SMTPSendFailedException ex) { + // TODO: response codes: https://www.ietf.org/rfc/rfc821.txt + message.error = Helper.formatThrowable(ex); + db.message().updateMessage(message); + throw ex; + } try { db.beginTransaction(); diff --git a/app/src/main/res/layout/item_account.xml b/app/src/main/res/layout/item_account.xml index ae7567ce..0df784e8 100644 --- a/app/src/main/res/layout/item_account.xml +++ b/app/src/main/res/layout/item_account.xml @@ -65,6 +65,19 @@ app:layout_constraintStart_toEndOf="@id/tvHost" app:layout_constraintTop_toBottomOf="@id/ivSync" /> + + + app:layout_constraintTop_toBottomOf="@id/tvError" /> \ No newline at end of file diff --git a/app/src/main/res/layout/item_folder.xml b/app/src/main/res/layout/item_folder.xml index 37c3993b..d95bbf1e 100644 --- a/app/src/main/res/layout/item_folder.xml +++ b/app/src/main/res/layout/item_folder.xml @@ -71,6 +71,19 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@id/ivMessages" /> + + + app:layout_constraintTop_toBottomOf="@id/tvError" /> \ No newline at end of file diff --git a/app/src/main/res/layout/item_message.xml b/app/src/main/res/layout/item_message.xml index c92dd762..5f70c54b 100644 --- a/app/src/main/res/layout/item_message.xml +++ b/app/src/main/res/layout/item_message.xml @@ -65,6 +65,19 @@ app:layout_constraintBottom_toBottomOf="@id/tvSubject" app:layout_constraintEnd_toEndOf="parent" /> + + + app:layout_constraintTop_toBottomOf="@id/tvError" /> \ No newline at end of file