mirror of
https://github.com/M66B/FairEmail.git
synced 2026-04-01 14:46:31 +02:00
Report/show account, folder, message and operation errors
This commit is contained in:
@@ -54,6 +54,7 @@ public class AdapterAccount extends RecyclerView.Adapter<AdapterAccount.ViewHold
|
||||
ImageView ivSync;
|
||||
TextView tvHost;
|
||||
TextView tvUser;
|
||||
TextView tvError;
|
||||
|
||||
ViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
@@ -64,6 +65,7 @@ public class AdapterAccount extends RecyclerView.Adapter<AdapterAccount.ViewHold
|
||||
ivSync = itemView.findViewById(R.id.ivSync);
|
||||
tvHost = itemView.findViewById(R.id.tvHost);
|
||||
tvUser = itemView.findViewById(R.id.tvUser);
|
||||
tvError = itemView.findViewById(R.id.tvError);
|
||||
}
|
||||
|
||||
private void wire() {
|
||||
@@ -80,6 +82,8 @@ public class AdapterAccount extends RecyclerView.Adapter<AdapterAccount.ViewHold
|
||||
ivSync.setVisibility(account.synchronize ? View.VISIBLE : View.INVISIBLE);
|
||||
tvHost.setText(String.format("%s:%d", account.host, account.port));
|
||||
tvUser.setText(account.user);
|
||||
tvError.setText(account.error);
|
||||
tvError.setVisibility(account.error == null ? View.GONE : View.VISIBLE);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -56,6 +56,7 @@ public class AdapterFolder extends RecyclerView.Adapter<AdapterFolder.ViewHolder
|
||||
TextView tvType;
|
||||
TextView tvAfter;
|
||||
ImageView ivSync;
|
||||
TextView tvError;
|
||||
|
||||
ViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
@@ -66,6 +67,7 @@ public class AdapterFolder extends RecyclerView.Adapter<AdapterFolder.ViewHolder
|
||||
tvType = itemView.findViewById(R.id.tvType);
|
||||
tvAfter = itemView.findViewById(R.id.tvAfter);
|
||||
ivSync = itemView.findViewById(R.id.ivSync);
|
||||
tvError = itemView.findViewById(R.id.tvError);
|
||||
}
|
||||
|
||||
private void wire(boolean properties) {
|
||||
@@ -100,6 +102,9 @@ public class AdapterFolder extends RecyclerView.Adapter<AdapterFolder.ViewHolder
|
||||
tvAfter.setVisibility(folder.synchronize ? View.VISIBLE : View.INVISIBLE);
|
||||
|
||||
ivSync.setVisibility(folder.synchronize ? View.VISIBLE : View.INVISIBLE);
|
||||
|
||||
tvError.setText(folder.error);
|
||||
tvError.setVisibility(folder.error == null ? View.GONE : View.VISIBLE);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -52,6 +52,7 @@ public class AdapterMessage extends PagedListAdapter<TupleMessageEx, AdapterMess
|
||||
ImageView ivAttachments;
|
||||
TextView tvSubject;
|
||||
TextView tvCount;
|
||||
TextView tvError;
|
||||
ProgressBar pbLoading;
|
||||
|
||||
ViewHolder(View itemView) {
|
||||
@@ -63,6 +64,7 @@ public class AdapterMessage extends PagedListAdapter<TupleMessageEx, AdapterMess
|
||||
ivAttachments = itemView.findViewById(R.id.ivAttachments);
|
||||
tvSubject = itemView.findViewById(R.id.tvSubject);
|
||||
tvCount = itemView.findViewById(R.id.tvCount);
|
||||
tvError = itemView.findViewById(R.id.tvError);
|
||||
pbLoading = itemView.findViewById(R.id.pbLoading);
|
||||
}
|
||||
|
||||
@@ -98,6 +100,7 @@ public class AdapterMessage extends PagedListAdapter<TupleMessageEx, AdapterMess
|
||||
tvTime.setText(DateUtils.getRelativeTimeSpanString(context, message.received));
|
||||
}
|
||||
|
||||
ivAttachments.setVisibility(message.attachments > 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<TupleMessageEx, AdapterMess
|
||||
tvCount.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
ivAttachments.setVisibility(message.attachments > 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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -344,23 +344,26 @@ public class ServiceSynchronize extends LifecycleService {
|
||||
|
||||
// Listen for connection changes
|
||||
istore.addConnectionListener(new ConnectionAdapter() {
|
||||
List<Thread> folderThreads = new ArrayList<>();
|
||||
Map<Long, IMAPFolder> 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);
|
||||
|
||||
} catch (MessageRemovedException ex) {
|
||||
Log.w(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
|
||||
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);
|
||||
} catch (FolderNotFoundException ex) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// 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();
|
||||
|
||||
Reference in New Issue
Block a user