diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 3a590494f7..de04f36db4 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -164,6 +164,8 @@
+
+
diff --git a/app/src/main/java/eu/faircode/email/AdapterFolder.java b/app/src/main/java/eu/faircode/email/AdapterFolder.java
index 72cd792e97..71c7cb5093 100644
--- a/app/src/main/java/eu/faircode/email/AdapterFolder.java
+++ b/app/src/main/java/eu/faircode/email/AdapterFolder.java
@@ -292,10 +292,6 @@ public class AdapterFolder extends RecyclerView.Adapter ops = db.operation().getOperations(folder.id);
+ Log.i(folder.name + " pending operations=" + ops.size());
+ for (int i = 0; i < ops.size() && state.running(); i++) {
+ EntityOperation op = ops.get(i);
+ try {
+ Log.i(folder.name +
+ " start op=" + op.id + "/" + op.name +
+ " msg=" + op.message +
+ " args=" + op.args);
+
+ // Fetch most recent copy of message
+ EntityMessage message = null;
+ if (op.message != null)
+ message = db.message().getMessage(op.message);
+
+ JSONArray jargs = new JSONArray(op.args);
+
+ try {
+ if (message == null && !EntityOperation.SYNC.equals(op.name))
+ throw new MessageRemovedException();
+
+ db.operation().setOperationError(op.id, null);
+ if (message != null)
+ db.message().setMessageError(message.id, null);
+
+ if (message != null && message.uid == null &&
+ !(EntityOperation.ADD.equals(op.name) ||
+ EntityOperation.DELETE.equals(op.name) ||
+ EntityOperation.SEND.equals(op.name) ||
+ EntityOperation.SYNC.equals(op.name)))
+ throw new IllegalArgumentException(op.name + " without uid " + op.args);
+
+ // Operations should use database transaction when needed
+
+ switch (op.name) {
+ case EntityOperation.SEEN:
+ doSeen(folder, (IMAPFolder) ifolder, message, jargs, context, db);
+ break;
+
+ case EntityOperation.FLAG:
+ doFlag(folder, (IMAPFolder) ifolder, message, jargs, context, db);
+ break;
+
+ case EntityOperation.ANSWERED:
+ doAnswered(folder, (IMAPFolder) ifolder, message, jargs, context, db);
+ break;
+
+ case EntityOperation.KEYWORD:
+ doKeyword(folder, (IMAPFolder) ifolder, message, jargs, context, db);
+ break;
+
+ case EntityOperation.ADD:
+ doAdd(folder, isession, (IMAPStore) istore, (IMAPFolder) ifolder, message, jargs, context, db);
+ break;
+
+ case EntityOperation.MOVE:
+ doMove(folder, isession, (IMAPStore) istore, (IMAPFolder) ifolder, message, jargs, context, db);
+ break;
+
+ case EntityOperation.DELETE:
+ doDelete(folder, (IMAPFolder) ifolder, message, jargs, context, db);
+ break;
+
+ case EntityOperation.HEADERS:
+ doHeaders(folder, (IMAPFolder) ifolder, message, context, db);
+ break;
+
+ case EntityOperation.RAW:
+ doRaw(folder, (IMAPFolder) ifolder, message, jargs, context, db);
+ break;
+
+ case EntityOperation.BODY:
+ doBody(folder, (IMAPFolder) ifolder, message, context, db);
+ break;
+
+ case EntityOperation.ATTACHMENT:
+ doAttachment(folder, op, (IMAPFolder) ifolder, message, jargs, context, db);
+ break;
+
+ case EntityOperation.SYNC:
+ if (folder.account == null)
+ db.folder().setFolderError(folder.id, null);
+ else
+ synchronizeMessages(context, account, folder, (IMAPFolder) ifolder, jargs, state);
+ break;
+
+ default:
+ throw new IllegalArgumentException("Unknown operation=" + op.name);
+ }
+
+ // Operation succeeded
+ db.operation().deleteOperation(op.id);
+ } catch (Throwable ex) {
+ // TODO: SMTP response codes: https://www.ietf.org/rfc/rfc821.txt
+ Log.e(folder.name, ex);
+ reportError(context, account, folder, ex);
+
+ db.operation().setOperationError(op.id, Helper.formatThrowable(ex));
+
+ if (message != null &&
+ !(ex instanceof MessageRemovedException) &&
+ !(ex instanceof FolderClosedException) &&
+ !(ex instanceof IllegalStateException))
+ db.message().setMessageError(message.id, Helper.formatThrowable(ex));
+
+ if (ex instanceof MessageRemovedException ||
+ ex instanceof FolderNotFoundException ||
+ ex instanceof IllegalArgumentException) {
+ Log.w("Unrecoverable", ex);
+
+ // There is no use in repeating
+ db.operation().deleteOperation(op.id);
+
+ // Cleanup
+ if (message != null) {
+ if (ex instanceof MessageRemovedException)
+ db.message().deleteMessage(message.id);
+
+ Long newid = null;
+
+ if (EntityOperation.MOVE.equals(op.name) &&
+ jargs.length() > 2)
+ newid = jargs.getLong(2);
+
+ if ((EntityOperation.ADD.equals(op.name) ||
+ EntityOperation.RAW.equals(op.name)) &&
+ jargs.length() > 0 && !jargs.isNull(0))
+ newid = jargs.getLong(0);
+
+ // Delete temporary copy in target folder
+ if (newid != null) {
+ db.message().deleteMessage(newid);
+ db.message().setMessageUiHide(message.id, false);
+ }
+ }
+
+ continue;
+ } else if (ex instanceof MessagingException) {
+ // Socket timeout is a recoverable condition (send message)
+ if (ex.getCause() instanceof SocketTimeoutException) {
+ Log.w("Recoverable", ex);
+ // No need to inform user
+ return;
+ }
+ }
+
+ throw ex;
+ }
+ } finally {
+ Log.i(folder.name + " end op=" + op.id + "/" + op.name);
+ }
+ }
+ } finally {
+ Log.i(folder.name + " end process state=" + state);
+ }
+ }
+
+ private static void doSeen(EntityFolder folder, IMAPFolder ifolder, EntityMessage message, JSONArray jargs, Context context, DB db) throws MessagingException, JSONException {
+ // Mark message (un)seen
+ if (!ifolder.getPermanentFlags().contains(Flags.Flag.SEEN)) {
+ db.message().setMessageSeen(message.id, false);
+ db.message().setMessageUiSeen(message.id, false);
+ return;
+ }
+
+ boolean seen = jargs.getBoolean(0);
+ if (message.seen.equals(seen))
+ return;
+
+ Message imessage = ifolder.getMessageByUID(message.uid);
+ if (imessage == null)
+ throw new MessageRemovedException();
+
+ imessage.setFlag(Flags.Flag.SEEN, seen);
+
+ db.message().setMessageSeen(message.id, seen);
+ }
+
+ private static void doFlag(EntityFolder folder, IMAPFolder ifolder, EntityMessage message, JSONArray jargs, Context context, DB db) throws MessagingException, JSONException {
+ // Star/unstar message
+ if (!ifolder.getPermanentFlags().contains(Flags.Flag.FLAGGED)) {
+ db.message().setMessageFlagged(message.id, false);
+ db.message().setMessageUiFlagged(message.id, false);
+ return;
+ }
+
+ boolean flagged = jargs.getBoolean(0);
+ if (message.flagged.equals(flagged))
+ return;
+
+ Message imessage = ifolder.getMessageByUID(message.uid);
+ if (imessage == null)
+ throw new MessageRemovedException();
+
+ imessage.setFlag(Flags.Flag.FLAGGED, flagged);
+
+ db.message().setMessageFlagged(message.id, flagged);
+ }
+
+ private static void doAnswered(EntityFolder folder, IMAPFolder ifolder, EntityMessage message, JSONArray jargs, Context context, DB db) throws MessagingException, JSONException {
+ // Mark message (un)answered
+ if (!ifolder.getPermanentFlags().contains(Flags.Flag.ANSWERED)) {
+ db.message().setMessageAnswered(message.id, false);
+ db.message().setMessageUiAnswered(message.id, false);
+ return;
+ }
+
+ boolean answered = jargs.getBoolean(0);
+ if (message.answered.equals(answered))
+ return;
+
+ Message imessage = ifolder.getMessageByUID(message.uid);
+ if (imessage == null)
+ throw new MessageRemovedException();
+
+ imessage.setFlag(Flags.Flag.ANSWERED, answered);
+
+ db.message().setMessageAnswered(message.id, answered);
+ }
+
+ private static void doKeyword(EntityFolder folder, IMAPFolder ifolder, EntityMessage message, JSONArray jargs, Context context, DB db) throws MessagingException, JSONException {
+ // Set/reset user flag
+ if (!ifolder.getPermanentFlags().contains(Flags.Flag.USER)) {
+ db.message().setMessageKeywords(message.id, DB.Converters.fromStringArray(null));
+ return;
+ }
+
+ // https://tools.ietf.org/html/rfc3501#section-2.3.2
+ String keyword = jargs.getString(0);
+ boolean set = jargs.getBoolean(1);
+
+ Message imessage = ifolder.getMessageByUID(message.uid);
+ if (imessage == null)
+ throw new MessageRemovedException();
+
+ Flags flags = new Flags(keyword);
+ imessage.setFlags(flags, set);
+
+ try {
+ db.beginTransaction();
+
+ message = db.message().getMessage(message.id);
+
+ List keywords = new ArrayList<>(Arrays.asList(message.keywords));
+ if (set) {
+ if (!keywords.contains(keyword))
+ keywords.add(keyword);
+ } else
+ keywords.remove(keyword);
+ db.message().setMessageKeywords(message.id, DB.Converters.fromStringArray(keywords.toArray(new String[0])));
+
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
+ }
+ }
+
+ private static void doAdd(EntityFolder folder, Session isession, IMAPStore istore, IMAPFolder ifolder, EntityMessage message, JSONArray jargs, Context context, DB db) throws MessagingException, JSONException, IOException {
+ // Add message
+ if (TextUtils.isEmpty(message.msgid))
+ throw new IllegalArgumentException("Message ID missing");
+
+ // Get message
+ MimeMessage imessage;
+ if (folder.id.equals(message.folder)) {
+ // Pre flight checks
+ if (!message.content)
+ throw new IllegalArgumentException("Message body missing");
+
+ EntityIdentity identity =
+ (message.identity == null ? null : db.identity().getIdentity(message.identity));
+
+ imessage = MessageHelper.from(context, message, isession,
+ identity == null ? false : identity.plain_only);
+ } else {
+ // Cross account move
+ File file = EntityMessage.getRawFile(context, message.id);
+ if (!file.exists())
+ throw new IllegalArgumentException("raw message file not found");
+
+ Log.i(folder.name + " reading " + file);
+ try (InputStream is = new BufferedInputStream(new FileInputStream(file))) {
+ imessage = new MimeMessage(isession, is);
+ }
+ }
+
+ // Handle auto read
+ boolean autoread = false;
+ if (jargs.length() > 1) {
+ autoread = jargs.getBoolean(1);
+ if (ifolder.getPermanentFlags().contains(Flags.Flag.SEEN)) {
+ if (autoread && !imessage.isSet(Flags.Flag.SEEN)) {
+ Log.i(folder.name + " autoread");
+ imessage.setFlag(Flags.Flag.SEEN, true);
+ }
+ }
+ }
+
+ // Handle draft
+ if (EntityFolder.DRAFTS.equals(folder.type))
+ if (ifolder.getPermanentFlags().contains(Flags.Flag.DRAFT))
+ imessage.setFlag(Flags.Flag.DRAFT, true);
+
+ // Add message
+ long uid = append(istore, ifolder, imessage);
+ Log.i(folder.name + " appended id=" + message.id + " uid=" + uid);
+ db.message().setMessageUid(message.id, uid);
+
+ if (folder.id.equals(message.folder)) {
+ // Delete previous message
+ Message[] ideletes = ifolder.search(new MessageIDTerm(message.msgid));
+ for (Message idelete : ideletes) {
+ long duid = ifolder.getUID(idelete);
+ if (duid == uid)
+ Log.i(folder.name + " append confirmed uid=" + duid);
+ else {
+ Log.i(folder.name + " deleting uid=" + duid + " msgid=" + message.msgid);
+ idelete.setFlag(Flags.Flag.DELETED, true);
+ }
+ }
+ ifolder.expunge();
+ } else {
+ // Cross account move
+ if (autoread) {
+ Log.i(folder.name + " queuing SEEN id=" + message.id);
+ EntityOperation.queue(context, db, message, EntityOperation.SEEN, true);
+ }
+
+ Log.i(folder.name + " queuing DELETE id=" + message.id);
+ EntityOperation.queue(context, db, message, EntityOperation.DELETE);
+ }
+ }
+
+ private static void doMove(EntityFolder folder, Session isession, IMAPStore istore, IMAPFolder ifolder, EntityMessage message, JSONArray jargs, Context context, DB db) throws JSONException, MessagingException, IOException {
+ // Move message
+ Message imessage = ifolder.getMessageByUID(message.uid);
+ if (imessage == null)
+ throw new MessageRemovedException();
+
+ // Get parameters
+ boolean autoread = jargs.getBoolean(1);
+
+ // Get target folder
+ long id = jargs.getLong(0);
+ EntityFolder target = db.folder().getFolder(id);
+ if (target == null)
+ throw new FolderNotFoundException();
+ IMAPFolder itarget = (IMAPFolder) istore.getFolder(target.name);
+
+ boolean canMove = istore.hasCapability("MOVE");
+ if (canMove &&
+ !EntityFolder.DRAFTS.equals(folder.type) &&
+ !EntityFolder.DRAFTS.equals(target.type)) {
+ // Autoread
+ if (ifolder.getPermanentFlags().contains(Flags.Flag.SEEN))
+ if (autoread && !imessage.isSet(Flags.Flag.SEEN))
+ imessage.setFlag(Flags.Flag.SEEN, true);
+
+ // Move message to
+ ifolder.moveMessages(new Message[]{imessage}, itarget);
+ } else {
+ Log.w(folder.name + " MOVE by DELETE/APPEND" +
+ " cap=" + canMove + " from=" + folder.type + " to=" + target.type);
+
+ // Serialize source message
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ imessage.writeTo(bos);
+
+ // Deserialize target message
+ ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
+ Message icopy = new MimeMessage(isession, bis);
+
+ // Make sure the message has a message ID
+ if (message.msgid == null) {
+ String msgid = EntityMessage.generateMessageId();
+ Log.i(target.name + " generated message id=" + msgid);
+ icopy.setHeader("Message-ID", msgid);
+ }
+
+ try {
+ // Needed to read flags
+ itarget.open(Folder.READ_WRITE);
+
+ // Auto read
+ if (itarget.getPermanentFlags().contains(Flags.Flag.SEEN))
+ if (autoread && !icopy.isSet(Flags.Flag.SEEN))
+ icopy.setFlag(Flags.Flag.SEEN, true);
+
+ // Move from drafts
+ if (EntityFolder.DRAFTS.equals(folder.type))
+ if (itarget.getPermanentFlags().contains(Flags.Flag.DRAFT))
+ icopy.setFlag(Flags.Flag.DRAFT, false);
+
+ // Move to drafts
+ if (EntityFolder.DRAFTS.equals(target.type))
+ if (itarget.getPermanentFlags().contains(Flags.Flag.DRAFT))
+ icopy.setFlag(Flags.Flag.DRAFT, true);
+
+ // Append target
+ long uid = append(istore, itarget, (MimeMessage) icopy);
+ Log.i(target.name + " appended id=" + message.id + " uid=" + uid);
+
+ // Fixed timing issue of at least Courier based servers
+ itarget.close(false);
+ itarget.open(Folder.READ_WRITE);
+
+ // Some providers, like Gmail, don't honor the appended seen flag
+ if (itarget.getPermanentFlags().contains(Flags.Flag.SEEN)) {
+ boolean seen = (autoread || message.ui_seen);
+ icopy = itarget.getMessageByUID(uid);
+ if (seen != icopy.isSet(Flags.Flag.SEEN)) {
+ Log.i(target.name + " Fixing id=" + message.id + " seen=" + seen);
+ icopy.setFlag(Flags.Flag.SEEN, seen);
+ }
+ }
+
+ // This is not based on an actual case, so this is just a safeguard
+ if (itarget.getPermanentFlags().contains(Flags.Flag.DRAFT)) {
+ boolean draft = EntityFolder.DRAFTS.equals(target.type);
+ icopy = itarget.getMessageByUID(uid);
+ if (draft != icopy.isSet(Flags.Flag.DRAFT)) {
+ Log.i(target.name + " Fixing id=" + message.id + " draft=" + draft);
+ icopy.setFlag(Flags.Flag.DRAFT, draft);
+ }
+ }
+
+ // Delete source
+ imessage.setFlag(Flags.Flag.DELETED, true);
+ ifolder.expunge();
+ } catch (Throwable ex) {
+ if (itarget.isOpen())
+ itarget.close();
+ throw ex;
+ }
+ }
+ }
+
+ private static void doDelete(EntityFolder folder, IMAPFolder ifolder, EntityMessage message, JSONArray jargs, Context context, DB db) throws MessagingException {
+ // Delete message
+ if (TextUtils.isEmpty(message.msgid))
+ throw new IllegalArgumentException("Message ID missing");
+
+ Message[] imessages = ifolder.search(new MessageIDTerm(message.msgid));
+ for (Message imessage : imessages) {
+ Log.i(folder.name + " deleting uid=" + message.uid + " msgid=" + message.msgid);
+ imessage.setFlag(Flags.Flag.DELETED, true);
+ }
+ ifolder.expunge();
+
+ db.message().deleteMessage(message.id);
+ }
+
+ private static void doHeaders(EntityFolder folder, IMAPFolder ifolder, EntityMessage message, Context context, DB db) throws MessagingException {
+ if (message.headers != null)
+ return;
+
+ IMAPMessage imessage = (IMAPMessage) ifolder.getMessageByUID(message.uid);
+ if (imessage == null)
+ throw new MessageRemovedException();
+
+ MessageHelper helper = new MessageHelper(imessage);
+ db.message().setMessageHeaders(message.id, helper.getHeaders());
+ }
+
+ private static void doRaw(EntityFolder folder, IMAPFolder ifolder, EntityMessage message, JSONArray jargs, Context context, DB db) throws MessagingException, IOException, JSONException {
+ if (message.raw == null || !message.raw) {
+ IMAPMessage imessage = (IMAPMessage) ifolder.getMessageByUID(message.uid);
+ if (imessage == null)
+ throw new MessageRemovedException();
+
+ File file = EntityMessage.getRawFile(context, message.id);
+
+ try (OutputStream os = new BufferedOutputStream(new FileOutputStream(file))) {
+ imessage.writeTo(os);
+ db.message().setMessageRaw(message.id, true);
+ }
+ }
+
+ if (jargs.length() > 0) {
+ long target = jargs.getLong(2);
+ jargs.remove(2);
+ Log.i(folder.name + " queuing ADD id=" + message.id + ":" + target);
+
+ EntityOperation operation = new EntityOperation();
+ operation.folder = target;
+ operation.message = message.id;
+ operation.name = EntityOperation.ADD;
+ operation.args = jargs.toString();
+ operation.created = new Date().getTime();
+ operation.id = db.operation().insertOperation(operation);
+ }
+ }
+
+ private static void doBody(EntityFolder folder, IMAPFolder ifolder, EntityMessage message, Context context, DB db) throws MessagingException, IOException {
+ // Download message body
+ if (message.content)
+ return;
+
+ // Get message
+ Message imessage = ifolder.getMessageByUID(message.uid);
+ if (imessage == null)
+ throw new MessageRemovedException();
+
+ MessageHelper helper = new MessageHelper((MimeMessage) imessage);
+ MessageHelper.MessageParts parts = helper.getMessageParts();
+ String body = parts.getHtml(context);
+ String preview = HtmlHelper.getPreview(body);
+ Helper.writeText(EntityMessage.getFile(context, message.id), body);
+ db.message().setMessageContent(message.id, true, preview);
+ db.message().setMessageWarning(message.id, parts.getWarnings(message.warning));
+ }
+
+ private static void doAttachment(EntityFolder folder, EntityOperation op, IMAPFolder ifolder, EntityMessage message, JSONArray jargs, Context context, DB db) throws JSONException, MessagingException, IOException {
+ // Download attachment
+ int sequence = jargs.getInt(0);
+
+ // Get attachment
+ EntityAttachment attachment = db.attachment().getAttachment(op.message, sequence);
+ if (attachment.available)
+ return;
+
+ // Get message
+ Message imessage = ifolder.getMessageByUID(message.uid);
+ if (imessage == null)
+ throw new MessageRemovedException();
+
+ // Download attachment
+ MessageHelper helper = new MessageHelper((MimeMessage) imessage);
+ MessageHelper.MessageParts parts = helper.getMessageParts();
+ parts.downloadAttachment(context, db, attachment.id, sequence);
+ }
+
+ private static long append(IMAPStore istore, IMAPFolder ifolder, MimeMessage imessage) throws MessagingException {
+ if (istore.hasCapability("UIDPLUS")) {
+ AppendUID[] uids = ifolder.appendUIDMessages(new Message[]{imessage});
+ if (uids == null || uids.length == 0)
+ throw new MessageRemovedException("Message not appended");
+ return uids[0].uid;
+ } else {
+ ifolder.appendMessages(new Message[]{imessage});
+
+ long uid = -1;
+ String msgid = imessage.getMessageID();
+ Log.i("Searching for appended msgid=" + msgid);
+ Message[] messages = ifolder.search(new MessageIDTerm(msgid));
+ if (messages != null)
+ for (Message iappended : messages) {
+ long muid = ifolder.getUID(iappended);
+ Log.i("Found appended uid=" + muid);
+ // RFC3501: Unique identifiers are assigned in a strictly ascending fashion
+ if (muid > uid)
+ uid = muid;
+ }
+
+ if (uid < 0)
+ throw new IllegalArgumentException("uid not found");
+
+ return uid;
+ }
+ }
+
+ static void synchronizeFolders(Context context, EntityAccount account, Store istore, State state) throws MessagingException {
+ DB db = DB.getInstance(context);
+ try {
+ db.beginTransaction();
+
+ Log.i("Start sync folders account=" + account.name);
+
+ List names = new ArrayList<>();
+ for (EntityFolder folder : db.folder().getFolders(account.id))
+ if (folder.tbc != null) {
+ Log.i(folder.name + " creating");
+ Folder ifolder = istore.getFolder(folder.name);
+ if (!ifolder.exists())
+ ifolder.create(Folder.HOLDS_MESSAGES);
+ db.folder().resetFolderTbc(folder.id);
+ } else if (folder.tbd != null && folder.tbd) {
+ Log.i(folder.name + " deleting");
+ Folder ifolder = istore.getFolder(folder.name);
+ if (ifolder.exists())
+ ifolder.delete(false);
+ db.folder().deleteFolder(folder.id);
+ } else
+ names.add(folder.name);
+ Log.i("Local folder count=" + names.size());
+
+ Folder defaultFolder = istore.getDefaultFolder();
+ char separator = defaultFolder.getSeparator();
+ EntityLog.log(context, account.name + " folder separator=" + separator);
+
+ Folder[] ifolders = defaultFolder.list("*");
+ Log.i("Remote folder count=" + ifolders.length + " separator=" + separator);
+
+ for (Folder ifolder : ifolders) {
+ String fullName = ifolder.getFullName();
+ String[] attrs = ((IMAPFolder) ifolder).getAttributes();
+ String type = EntityFolder.getType(attrs, fullName);
+
+ EntityLog.log(context, account.name + ":" + fullName +
+ " attrs=" + TextUtils.join(" ", attrs) + " type=" + type);
+
+ if (type != null) {
+ names.remove(fullName);
+
+ int level = EntityFolder.getLevel(separator, fullName);
+ String display = null;
+ if (account.prefix != null && fullName.startsWith(account.prefix + separator))
+ display = fullName.substring(account.prefix.length() + 1);
+
+ EntityFolder folder = db.folder().getFolderByName(account.id, fullName);
+ if (folder == null) {
+ folder = new EntityFolder();
+ folder.account = account.id;
+ folder.name = fullName;
+ folder.display = display;
+ folder.type = (EntityFolder.SYSTEM.equals(type) ? type : EntityFolder.USER);
+ folder.level = level;
+ folder.synchronize = false;
+ folder.poll = ("imap.gmail.com".equals(account.host));
+ folder.sync_days = EntityFolder.DEFAULT_SYNC;
+ folder.keep_days = EntityFolder.DEFAULT_KEEP;
+ db.folder().insertFolder(folder);
+ Log.i(folder.name + " added type=" + folder.type);
+ } else {
+ Log.i(folder.name + " exists type=" + folder.type);
+
+ if (folder.display == null) {
+ if (display != null) {
+ db.folder().setFolderDisplay(folder.id, display);
+ EntityLog.log(context, account.name + ":" + folder.name +
+ " removed prefix display=" + display + " separator=" + separator);
+ }
+ } else {
+ if (account.prefix == null && folder.name.endsWith(separator + folder.display)) {
+ db.folder().setFolderDisplay(folder.id, null);
+ EntityLog.log(context, account.name + ":" + folder.name +
+ " restored prefix display=" + folder.display + " separator=" + separator);
+ }
+ }
+
+ db.folder().setFolderLevel(folder.id, level);
+
+ // Compatibility
+ if ("Inbox_sub".equals(folder.type))
+ db.folder().setFolderType(folder.id, EntityFolder.USER);
+ else if (EntityFolder.USER.equals(folder.type) && EntityFolder.SYSTEM.equals(type))
+ db.folder().setFolderType(folder.id, type);
+ else if (EntityFolder.SYSTEM.equals(folder.type) && EntityFolder.USER.equals(type))
+ db.folder().setFolderType(folder.id, type);
+ }
+ }
+ }
+
+ Log.i("Delete local count=" + names.size());
+ for (String name : names) {
+ Log.i(name + " delete");
+ db.folder().deleteFolder(account.id, name);
+ }
+
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
+ Log.i("End sync folder");
+ }
+ }
+
+ static void synchronizeMessages(Context context, EntityAccount account, final EntityFolder folder, IMAPFolder ifolder, JSONArray jargs, State state) throws JSONException, MessagingException, IOException {
+ final DB db = DB.getInstance(context);
+ try {
+ int sync_days = jargs.getInt(0);
+ int keep_days = jargs.getInt(1);
+ boolean download = jargs.getBoolean(2);
+
+ if (keep_days == sync_days)
+ keep_days++;
+
+ Log.i(folder.name + " start sync after=" + sync_days + "/" + keep_days);
+
+ db.folder().setFolderSyncState(folder.id, "syncing");
+
+ // Get reference times
+ Calendar cal_sync = Calendar.getInstance();
+ cal_sync.add(Calendar.DAY_OF_MONTH, -sync_days);
+ cal_sync.set(Calendar.HOUR_OF_DAY, 0);
+ cal_sync.set(Calendar.MINUTE, 0);
+ cal_sync.set(Calendar.SECOND, 0);
+ cal_sync.set(Calendar.MILLISECOND, 0);
+
+ Calendar cal_keep = Calendar.getInstance();
+ cal_keep.add(Calendar.DAY_OF_MONTH, -keep_days);
+ cal_keep.set(Calendar.HOUR_OF_DAY, 0);
+ cal_keep.set(Calendar.MINUTE, 0);
+ cal_keep.set(Calendar.SECOND, 0);
+ cal_keep.set(Calendar.MILLISECOND, 0);
+
+ long sync_time = cal_sync.getTimeInMillis();
+ if (sync_time < 0)
+ sync_time = 0;
+
+ long keep_time = cal_keep.getTimeInMillis();
+ if (keep_time < 0)
+ keep_time = 0;
+
+ Log.i(folder.name + " sync=" + new Date(sync_time) + " keep=" + new Date(keep_time));
+
+ // Delete old local messages
+ int old = db.message().deleteMessagesBefore(folder.id, keep_time, false);
+ Log.i(folder.name + " local old=" + old);
+
+ // Get list of local uids
+ final List uids = db.message().getUids(folder.id, null);
+ Log.i(folder.name + " local count=" + uids.size());
+
+ // Reduce list of local uids
+ SearchTerm searchTerm = new ReceivedDateTerm(ComparisonTerm.GE, new Date(sync_time));
+ if (ifolder.getPermanentFlags().contains(Flags.Flag.FLAGGED))
+ searchTerm = new OrTerm(searchTerm, new FlagTerm(new Flags(Flags.Flag.FLAGGED), true));
+
+ long search = SystemClock.elapsedRealtime();
+ Message[] imessages = ifolder.search(searchTerm);
+ Log.i(folder.name + " remote count=" + imessages.length +
+ " search=" + (SystemClock.elapsedRealtime() - search) + " ms");
+
+ FetchProfile fp = new FetchProfile();
+ fp.add(UIDFolder.FetchProfileItem.UID);
+ fp.add(FetchProfile.Item.FLAGS);
+ ifolder.fetch(imessages, fp);
+
+ long fetch = SystemClock.elapsedRealtime();
+ Log.i(folder.name + " remote fetched=" + (SystemClock.elapsedRealtime() - fetch) + " ms");
+
+ for (int i = 0; i < imessages.length && state.running(); i++)
+ try {
+ uids.remove(ifolder.getUID(imessages[i]));
+ } catch (MessageRemovedException ex) {
+ Log.w(folder.name, ex);
+ } catch (Throwable ex) {
+ Log.e(folder.name, ex);
+ reportError(context, account, folder, ex);
+ db.folder().setFolderError(folder.id, Helper.formatThrowable(ex, true));
+ }
+
+ if (uids.size() > 0) {
+ ifolder.doCommand(new IMAPFolder.ProtocolCommand() {
+ @Override
+ public Object doCommand(IMAPProtocol protocol) {
+ Log.i("Executing uid fetch count=" + uids.size());
+ Response[] responses = protocol.command(
+ "UID FETCH " + TextUtils.join(",", uids) + " (UID)", null);
+
+ for (int i = 0; i < responses.length; i++) {
+ if (responses[i] instanceof FetchResponse) {
+ FetchResponse fr = (FetchResponse) responses[i];
+ UID uid = fr.getItem(UID.class);
+ if (uid != null)
+ uids.remove(uid.uid);
+ } else {
+ if (responses[i].isOK())
+ Log.i(folder.name + " response=" + responses[i]);
+ else {
+ Log.e(folder.name + " response=" + responses[i]);
+ db.folder().setFolderError(folder.id, responses[i].toString());
+ }
+ }
+ }
+ return null;
+ }
+ });
+
+ long getuid = SystemClock.elapsedRealtime();
+ Log.i(folder.name + " remote uids=" + (SystemClock.elapsedRealtime() - getuid) + " ms");
+ }
+
+ // Delete local messages not at remote
+ Log.i(folder.name + " delete=" + uids.size());
+ for (Long uid : uids) {
+ int count = db.message().deleteMessage(folder.id, uid);
+ Log.i(folder.name + " delete local uid=" + uid + " count=" + count);
+ }
+
+ List rules = db.rule().getEnabledRules(folder.id);
+
+ fp.add(FetchProfile.Item.ENVELOPE);
+ // fp.add(FetchProfile.Item.FLAGS);
+ fp.add(FetchProfile.Item.CONTENT_INFO); // body structure
+ // fp.add(UIDFolder.FetchProfileItem.UID);
+ fp.add(IMAPFolder.FetchProfileItem.HEADERS);
+ // fp.add(IMAPFolder.FetchProfileItem.MESSAGE);
+ fp.add(FetchProfile.Item.SIZE);
+ fp.add(IMAPFolder.FetchProfileItem.INTERNALDATE);
+
+ // Add/update local messages
+ Long[] ids = new Long[imessages.length];
+ Log.i(folder.name + " add=" + imessages.length);
+ for (int i = imessages.length - 1; i >= 0 && state.running(); i -= SYNC_BATCH_SIZE) {
+ int from = Math.max(0, i - SYNC_BATCH_SIZE + 1);
+ Message[] isub = Arrays.copyOfRange(imessages, from, i + 1);
+
+ // Full fetch new/changed messages only
+ List full = new ArrayList<>();
+ for (Message imessage : isub) {
+ long uid = ifolder.getUID(imessage);
+ EntityMessage message = db.message().getMessageByUid(folder.id, uid);
+ if (message == null)
+ full.add(imessage);
+ }
+ if (full.size() > 0) {
+ long headers = SystemClock.elapsedRealtime();
+ ifolder.fetch(full.toArray(new Message[0]), fp);
+ Log.i(folder.name + " fetched headers=" + full.size() +
+ " " + (SystemClock.elapsedRealtime() - headers) + " ms");
+ }
+
+ for (int j = isub.length - 1; j >= 0 && state.running(); j--)
+ try {
+ db.beginTransaction();
+ EntityMessage message = synchronizeMessage(
+ context,
+ folder, ifolder, (IMAPMessage) isub[j],
+ false,
+ rules);
+ ids[from + j] = message.id;
+ db.setTransactionSuccessful();
+ } catch (MessageRemovedException ex) {
+ Log.w(folder.name, ex);
+ } catch (FolderClosedException ex) {
+ throw ex;
+ } catch (IOException ex) {
+ if (ex.getCause() instanceof MessagingException) {
+ Log.w(folder.name, ex);
+ db.folder().setFolderError(folder.id, Helper.formatThrowable(ex, true));
+ } else
+ throw ex;
+ } catch (Throwable ex) {
+ Log.e(folder.name, ex);
+ db.folder().setFolderError(folder.id, Helper.formatThrowable(ex, true));
+ } finally {
+ db.endTransaction();
+ // Reduce memory usage
+ ((IMAPMessage) isub[j]).invalidateHeaders();
+ }
+ }
+
+ // Delete not synchronized messages without uid
+ db.message().deleteOrphans(folder.id);
+
+ // Add local sent messages to remote sent folder
+ if (EntityFolder.SENT.equals(folder.type)) {
+ List orphans = db.message().getSentOrphans(folder.account);
+ Log.i(folder.name + " sent orphans=" + orphans.size() + " account=" + folder.account);
+ for (EntityMessage orphan : orphans) {
+ Log.i(folder.name + " adding orphan id=" + orphan.id + " sent=" + new Date(orphan.sent));
+ orphan.folder = folder.id;
+ db.message().updateMessage(orphan);
+ EntityOperation.queue(context, db, orphan, EntityOperation.ADD);
+ }
+ }
+
+ if (download) {
+ db.folder().setFolderSyncState(folder.id, "downloading");
+
+ //fp.add(IMAPFolder.FetchProfileItem.MESSAGE);
+
+ // Download messages/attachments
+ Log.i(folder.name + " download=" + imessages.length);
+ for (int i = imessages.length - 1; i >= 0 && state.running(); i -= DOWNLOAD_BATCH_SIZE) {
+ int from = Math.max(0, i - DOWNLOAD_BATCH_SIZE + 1);
+
+ Message[] isub = Arrays.copyOfRange(imessages, from, i + 1);
+ // Fetch on demand
+
+ for (int j = isub.length - 1; j >= 0 && state.running(); j--)
+ try {
+ db.beginTransaction();
+ if (ids[from + j] != null)
+ downloadMessage(
+ context,
+ folder, ifolder,
+ (IMAPMessage) isub[j], ids[from + j]);
+ db.setTransactionSuccessful();
+ } catch (FolderClosedException ex) {
+ throw ex;
+ } catch (FolderClosedIOException ex) {
+ throw ex;
+ } catch (Throwable ex) {
+ Log.e(folder.name, ex);
+ } finally {
+ db.endTransaction();
+ // Free memory
+ ((IMAPMessage) isub[j]).invalidateHeaders();
+ }
+ }
+ }
+
+ if (state.running)
+ db.folder().setFolderInitialized(folder.id);
+
+ db.folder().setFolderSync(folder.id, new Date().getTime());
+ db.folder().setFolderError(folder.id, null);
+
+ } finally {
+ Log.i(folder.name + " end sync state=" + state);
+ db.folder().setFolderSyncState(folder.id, null);
+ }
+ }
+
+ static EntityMessage synchronizeMessage(
+ Context context,
+ EntityFolder folder, IMAPFolder ifolder, IMAPMessage imessage,
+ boolean browsed,
+ List rules) throws MessagingException, IOException {
+ long uid = ifolder.getUID(imessage);
+
+ if (imessage.isExpunged()) {
+ Log.i(folder.name + " expunged uid=" + uid);
+ throw new MessageRemovedException();
+ }
+ if (imessage.isSet(Flags.Flag.DELETED)) {
+ Log.i(folder.name + " deleted uid=" + uid);
+ throw new MessageRemovedException();
+ }
+
+ MessageHelper helper = new MessageHelper(imessage);
+ boolean seen = helper.getSeen();
+ boolean answered = helper.getAnsered();
+ boolean flagged = helper.getFlagged();
+ String flags = helper.getFlags();
+ String[] keywords = helper.getKeywords();
+ boolean filter = false;
+
+ DB db = DB.getInstance(context);
+
+ // Find message by uid (fast, no headers required)
+ EntityMessage message = db.message().getMessageByUid(folder.id, uid);
+
+ // Find message by Message-ID (slow, headers required)
+ // - messages in inbox have same id as message sent to self
+ // - messages in archive have same id as original
+ if (message == null) {
+ // Will fetch headers within database transaction
+ String msgid = helper.getMessageID();
+ Log.i(folder.name + " searching for " + msgid);
+ for (EntityMessage dup : db.message().getMessageByMsgId(folder.account, msgid)) {
+ EntityFolder dfolder = db.folder().getFolder(dup.folder);
+ Log.i(folder.name + " found as id=" + dup.id + "/" + dup.uid +
+ " folder=" + dfolder.type + ":" + dup.folder + "/" + folder.type + ":" + folder.id +
+ " msgid=" + dup.msgid + " thread=" + dup.thread);
+
+ if (dup.folder.equals(folder.id) ||
+ (EntityFolder.OUTBOX.equals(dfolder.type) && EntityFolder.SENT.equals(folder.type))) {
+ String thread = helper.getThreadId(uid);
+ Log.i(folder.name + " found as id=" + dup.id +
+ " uid=" + dup.uid + "/" + uid +
+ " msgid=" + msgid + " thread=" + thread);
+ dup.folder = folder.id; // outbox to sent
+
+ if (dup.uid == null) {
+ Log.i(folder.name + " set uid=" + uid);
+ dup.uid = uid;
+ filter = true;
+ } else
+ Log.w(folder.name + " changed uid=" + dup.uid + " -> " + uid);
+
+ dup.msgid = msgid;
+ dup.thread = thread;
+ dup.error = null;
+ db.message().updateMessage(dup);
+ message = dup;
+ }
+ }
+
+ if (message == null)
+ filter = true;
+ }
+
+ if (message == null) {
+ // Build list of addresses
+ Address[] recipients = helper.getTo();
+ Address[] senders = helper.getFrom();
+ if (recipients == null)
+ recipients = new Address[0];
+ if (senders == null)
+ senders = new Address[0];
+ Address[] all = Arrays.copyOf(recipients, recipients.length + senders.length);
+ System.arraycopy(senders, 0, all, recipients.length, senders.length);
+
+ List emails = new ArrayList<>();
+ for (Address address : all) {
+ String to = ((InternetAddress) address).getAddress();
+ if (!TextUtils.isEmpty(to)) {
+ to = to.toLowerCase();
+ emails.add(to);
+ String canonical = Helper.canonicalAddress(to);
+ if (!to.equals(canonical))
+ emails.add(canonical);
+ }
+ }
+ String delivered = helper.getDeliveredTo();
+ if (!TextUtils.isEmpty(delivered)) {
+ delivered = delivered.toLowerCase();
+ emails.add(delivered);
+ String canonical = Helper.canonicalAddress(delivered);
+ if (!delivered.equals(canonical))
+ emails.add(canonical);
+ }
+
+ // Search for identity
+ EntityIdentity identity = null;
+ for (String email : emails) {
+ identity = db.identity().getIdentity(folder.account, email);
+ if (identity != null)
+ break;
+ }
+
+ message = new EntityMessage();
+ message.account = folder.account;
+ message.folder = folder.id;
+ message.identity = (identity == null ? null : identity.id);
+ message.uid = uid;
+
+ message.msgid = helper.getMessageID();
+ if (TextUtils.isEmpty(message.msgid))
+ Log.w("No Message-ID id=" + message.id + " uid=" + message.uid);
+
+ message.references = TextUtils.join(" ", helper.getReferences());
+ message.inreplyto = helper.getInReplyTo();
+ message.deliveredto = helper.getDeliveredTo();
+ message.thread = helper.getThreadId(uid);
+ message.sender = MessageHelper.getSortKey(helper.getFrom());
+ message.from = helper.getFrom();
+ message.to = helper.getTo();
+ message.cc = helper.getCc();
+ message.bcc = helper.getBcc();
+ message.reply = helper.getReply();
+ message.subject = helper.getSubject();
+ message.size = helper.getSize();
+ message.content = false;
+ message.received = helper.getReceived();
+ message.sent = helper.getSent();
+ message.seen = seen;
+ message.answered = answered;
+ message.flagged = flagged;
+ message.flags = flags;
+ message.keywords = keywords;
+ message.ui_seen = seen;
+ message.ui_answered = answered;
+ message.ui_flagged = flagged;
+ message.ui_hide = false;
+ message.ui_found = false;
+ message.ui_ignored = seen;
+ message.ui_browsed = browsed;
+
+ Uri lookupUri = ContactInfo.getLookupUri(context, message.from);
+ message.avatar = (lookupUri == null ? null : lookupUri.toString());
+
+ // Check sender
+ Address sender = helper.getSender();
+ if (sender != null && senders.length > 0) {
+ String[] f = ((InternetAddress) senders[0]).getAddress().split("@");
+ String[] s = ((InternetAddress) sender).getAddress().split("@");
+ if (f.length > 1 && s.length > 1) {
+ if (!f[1].equals(s[1]))
+ message.warning = context.getString(R.string.title_via, s[1]);
+ }
+ }
+
+ message.id = db.message().insertMessage(message);
+
+ Log.i(folder.name + " added id=" + message.id + " uid=" + message.uid);
+
+ int sequence = 1;
+ MessageHelper.MessageParts parts = helper.getMessageParts();
+ for (EntityAttachment attachment : parts.getAttachments()) {
+ Log.i(folder.name + " attachment seq=" + sequence +
+ " name=" + attachment.name + " type=" + attachment.type +
+ " cid=" + attachment.cid + " pgp=" + attachment.encryption);
+ attachment.message = message.id;
+ attachment.sequence = sequence++;
+ attachment.id = db.attachment().insertAttachment(attachment);
+ }
+ } else {
+ boolean update = false;
+
+ if (!message.seen.equals(seen) || !message.seen.equals(message.ui_seen)) {
+ update = true;
+ message.seen = seen;
+ message.ui_seen = seen;
+ Log.i(folder.name + " updated id=" + message.id + " uid=" + message.uid + " seen=" + seen);
+ }
+
+ if (!message.answered.equals(answered) || !message.answered.equals(message.ui_answered)) {
+ update = true;
+ message.answered = answered;
+ message.ui_answered = answered;
+ Log.i(folder.name + " updated id=" + message.id + " uid=" + message.uid + " answered=" + answered);
+ }
+
+ if (!message.flagged.equals(flagged) || !message.flagged.equals(message.ui_flagged)) {
+ update = true;
+ message.flagged = flagged;
+ message.ui_flagged = flagged;
+ Log.i(folder.name + " updated id=" + message.id + " uid=" + message.uid + " flagged=" + flagged);
+ }
+
+ if (!Objects.equals(flags, message.flags)) {
+ update = true;
+ message.flags = flags;
+ Log.i(folder.name + " updated id=" + message.id + " uid=" + message.uid + " flags=" + flags);
+ }
+
+ if (!Helper.equal(message.keywords, keywords)) {
+ update = true;
+ message.keywords = keywords;
+ Log.i(folder.name + " updated id=" + message.id + " uid=" + message.uid +
+ " keywords=" + TextUtils.join(" ", keywords));
+ }
+
+ if (message.ui_hide && db.operation().getOperationCount(folder.id, message.id) == 0) {
+ update = true;
+ message.ui_hide = false;
+ Log.i(folder.name + " updated id=" + message.id + " uid=" + message.uid + " unhide");
+ }
+
+ if (message.ui_browsed) {
+ update = true;
+ message.ui_browsed = false;
+ Log.i(folder.name + " updated id=" + message.id + " uid=" + message.uid + " unbrowse");
+ }
+
+ if (message.avatar == null) {
+ Uri lookupUri = ContactInfo.getLookupUri(context, message.from);
+ if (lookupUri != null) {
+ update = true;
+ message.avatar = lookupUri.toString();
+ Log.i(folder.name + " updated id=" + message.id + " lookup=" + lookupUri);
+ }
+ }
+
+ if (update)
+ db.message().updateMessage(message);
+ else
+ Log.i(folder.name + " unchanged uid=" + uid);
+ }
+
+ if (!folder.isOutgoing() && !EntityFolder.ARCHIVE.equals(folder.type)) {
+ Address[] senders = (message.reply != null ? message.reply : message.from);
+ if (senders != null)
+ for (Address sender : senders) {
+ String email = ((InternetAddress) sender).getAddress();
+ String name = ((InternetAddress) sender).getPersonal();
+ List contacts = db.contact().getContacts(EntityContact.TYPE_FROM, email);
+ if (contacts.size() == 0) {
+ EntityContact contact = new EntityContact();
+ contact.type = EntityContact.TYPE_FROM;
+ contact.email = email;
+ contact.name = name;
+ contact.id = db.contact().insertContact(contact);
+ Log.i("Inserted sender contact=" + contact);
+ } else {
+ EntityContact contact = contacts.get(0);
+ if (name != null && !name.equals(contact.name)) {
+ contact.name = name;
+ db.contact().updateContact(contact);
+ Log.i("Updated sender contact=" + contact);
+ }
+ }
+ }
+ }
+
+ List fkeywords = new ArrayList<>(Arrays.asList(folder.keywords));
+
+ for (String keyword : keywords)
+ if (!fkeywords.contains(keyword)) {
+ Log.i(folder.name + " adding keyword=" + keyword);
+ fkeywords.add(keyword);
+ }
+
+ if (folder.keywords.length != fkeywords.size()) {
+ Collections.sort(fkeywords);
+ db.folder().setFolderKeywords(folder.id, DB.Converters.fromStringArray(fkeywords.toArray(new String[0])));
+ }
+
+ if (filter && Helper.isPro(context))
+ try {
+ for (EntityRule rule : rules)
+ if (rule.matches(context, message, imessage)) {
+ rule.execute(context, db, message);
+ if (rule.stop)
+ break;
+ }
+ } catch (Throwable ex) {
+ Log.e(ex);
+ db.message().setMessageError(message.id, Helper.formatThrowable(ex));
+ }
+
+ return message;
+ }
+
+ static void downloadMessage(
+ Context context,
+ EntityFolder folder, IMAPFolder ifolder,
+ IMAPMessage imessage, long id) throws MessagingException, IOException {
+ DB db = DB.getInstance(context);
+ EntityMessage message = db.message().getMessage(id);
+ if (message == null)
+ return;
+
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
+ long maxSize = prefs.getInt("download", 32768);
+ if (maxSize == 0)
+ maxSize = Long.MAX_VALUE;
+
+ List attachments = db.attachment().getAttachments(message.id);
+ MessageHelper helper = new MessageHelper(imessage);
+ Boolean isMetered = Helper.isMetered(context, false);
+ boolean metered = (isMetered == null || isMetered);
+
+ boolean fetch = false;
+ if (!message.content)
+ if (!metered || (message.size != null && message.size < maxSize))
+ fetch = true;
+
+ if (!fetch)
+ for (EntityAttachment attachment : attachments)
+ if (!attachment.available)
+ if (!metered || (attachment.size != null && attachment.size < maxSize)) {
+ fetch = true;
+ break;
+ }
+
+ if (fetch) {
+ Log.i(folder.name + " fetching message id=" + message.id);
+ FetchProfile fp = new FetchProfile();
+ fp.add(FetchProfile.Item.ENVELOPE);
+ fp.add(FetchProfile.Item.FLAGS);
+ fp.add(FetchProfile.Item.CONTENT_INFO); // body structure
+ fp.add(UIDFolder.FetchProfileItem.UID);
+ fp.add(IMAPFolder.FetchProfileItem.HEADERS);
+ fp.add(IMAPFolder.FetchProfileItem.MESSAGE);
+ fp.add(FetchProfile.Item.SIZE);
+ fp.add(IMAPFolder.FetchProfileItem.INTERNALDATE);
+ ifolder.fetch(new Message[]{imessage}, fp);
+
+ MessageHelper.MessageParts parts = helper.getMessageParts();
+
+ if (!message.content) {
+ if (!metered || (message.size != null && message.size < maxSize)) {
+ String body = parts.getHtml(context);
+ Helper.writeText(EntityMessage.getFile(context, message.id), body);
+ db.message().setMessageContent(message.id, true, HtmlHelper.getPreview(body));
+ db.message().setMessageWarning(message.id, parts.getWarnings(message.warning));
+ Log.i(folder.name + " downloaded message id=" + message.id + " size=" + message.size);
+ }
+ }
+
+ for (EntityAttachment attachment : attachments)
+ if (!attachment.available)
+ if (!metered || (attachment.size != null && attachment.size < maxSize))
+ if (!parts.downloadAttachment(context, db, attachment.id, attachment.sequence))
+ break;
+ }
+ }
+
+ static void reportError(Context context, EntityAccount account, EntityFolder folder, Throwable ex) {
+ // FolderClosedException: can happen when no connectivity
+
+ // IllegalStateException:
+ // - "This operation is not allowed on a closed folder"
+ // - can happen when syncing message
+
+ // ConnectionException
+ // - failed to create new store connection (connectivity)
+
+ // MailConnectException
+ // - on connectivity problems when connecting to store
+
+ String title;
+ if (account == null)
+ title = folder.name;
+ else if (folder == null)
+ title = account.name;
+ else
+ title = account.name + "/" + folder.name;
+
+ String tag = "error:" + (account == null ? 0 : account.id) + ":" + (folder == null ? 0 : folder.id);
+
+ EntityLog.log(context, title + " " + Helper.formatThrowable(ex));
+
+ if ((ex instanceof SendFailedException) || (ex instanceof AlertException)) {
+ NotificationManager nm = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
+ nm.notify(tag, 1, getNotificationError(context, title, ex).build());
+ }
+
+ // connection failure: Too many simultaneous connections
+
+ if (BuildConfig.DEBUG &&
+ !(ex instanceof SendFailedException) &&
+ !(ex instanceof MailConnectException) &&
+ !(ex instanceof FolderClosedException) &&
+ !(ex instanceof IllegalStateException) &&
+ !(ex instanceof AuthenticationFailedException) && // Also: Too many simultaneous connections
+ !(ex instanceof StoreClosedException) &&
+ !(ex instanceof MessageRemovedException) &&
+ !(ex instanceof MessagingException && ex.getCause() instanceof UnknownHostException) &&
+ !(ex instanceof MessagingException && ex.getCause() instanceof ConnectionException) &&
+ !(ex instanceof MessagingException && ex.getCause() instanceof SocketException) &&
+ !(ex instanceof MessagingException && ex.getCause() instanceof SocketTimeoutException) &&
+ !(ex instanceof MessagingException && ex.getCause() instanceof SSLException) &&
+ !(ex instanceof MessagingException && "connection failure".equals(ex.getMessage()))) {
+ NotificationManager nm = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
+ nm.notify(tag, 1, getNotificationError(context, title, ex).build());
+ }
+ }
+
+ static NotificationCompat.Builder getNotificationError(Context context, String title, Throwable ex) {
+ return getNotificationError(context, "error", title, ex, true);
+ }
+
+ static NotificationCompat.Builder getNotificationError(Context context, String channel, String title, Throwable ex, boolean debug) {
+ // Build pending intent
+ Intent intent = new Intent(context, ActivitySetup.class);
+ if (debug)
+ intent.setAction("error");
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ PendingIntent pi = PendingIntent.getActivity(
+ context, ActivitySetup.REQUEST_ERROR, intent, PendingIntent.FLAG_UPDATE_CURRENT);
+
+ // Build notification
+ NotificationCompat.Builder builder = new NotificationCompat.Builder(context, channel);
+
+ builder
+ .setSmallIcon(R.drawable.baseline_warning_white_24)
+ .setContentTitle(context.getString(R.string.title_notification_failed, title))
+ .setContentText(Helper.formatThrowable(ex))
+ .setContentIntent(pi)
+ .setAutoCancel(false)
+ .setShowWhen(true)
+ .setPriority(Notification.PRIORITY_MAX)
+ .setOnlyAlertOnce(true)
+ .setCategory(Notification.CATEGORY_ERROR)
+ .setVisibility(NotificationCompat.VISIBILITY_SECRET);
+
+ builder.setStyle(new NotificationCompat.BigTextStyle()
+ .bigText(Helper.formatThrowable(ex, false, "\n")));
+
+ return builder;
+ }
+
+ static class AlertException extends Throwable {
+ private String alert;
+
+ AlertException(String alert) {
+ this.alert = alert;
+ }
+
+ @Override
+ public String getMessage() {
+ return alert;
+ }
+ }
+
+ static class State {
+ private Thread thread;
+ private Semaphore semaphore = new Semaphore(0);
+ private boolean running = true;
+
+ void runnable(Runnable runnable, String name) {
+ thread = new Thread(runnable, name);
+ thread.setPriority(THREAD_PRIORITY_BACKGROUND);
+ }
+
+ void release() {
+ semaphore.release();
+ yield();
+ }
+
+ void acquire() throws InterruptedException {
+ semaphore.acquire();
+ }
+
+ boolean acquire(long milliseconds) throws InterruptedException {
+ return semaphore.tryAcquire(milliseconds, TimeUnit.MILLISECONDS);
+ }
+
+ void error() {
+ thread.interrupt();
+ yield();
+ }
+
+ private void yield() {
+ try {
+ // Give interrupted thread some time to acquire wake lock
+ Thread.sleep(YIELD_DURATION);
+ } catch (InterruptedException ignored) {
+ }
+ }
+
+ void start() {
+ thread.start();
+ }
+
+ void stop() {
+ running = false;
+ semaphore.release();
+ }
+
+ void join() {
+ join(thread);
+ }
+
+ boolean running() {
+ return running;
+ }
+
+ void join(Thread thread) {
+ boolean joined = false;
+ while (!joined)
+ try {
+ Log.i("Joining " + thread.getName());
+ thread.join();
+ joined = true;
+ Log.i("Joined " + thread.getName());
+ } catch (InterruptedException ex) {
+ Log.w(thread.getName() + " join " + ex.toString());
+ }
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ return "[running=" + running + "]";
+ }
+ }
+}
diff --git a/app/src/main/java/eu/faircode/email/EntityMessage.java b/app/src/main/java/eu/faircode/email/EntityMessage.java
index 5bac7f9205..b24463f8e3 100644
--- a/app/src/main/java/eu/faircode/email/EntityMessage.java
+++ b/app/src/main/java/eu/faircode/email/EntityMessage.java
@@ -175,7 +175,7 @@ public class EntityMessage implements Serializable {
static void snooze(Context context, long id, Long wakeup) {
Intent snoozed = new Intent(context, ServiceSynchronize.class);
snoozed.setAction("snooze:" + id);
- PendingIntent pi = PendingIntent.getService(context, ServiceSynchronize.PI_SNOOZED, snoozed, PendingIntent.FLAG_UPDATE_CURRENT);
+ PendingIntent pi = PendingIntent.getService(context, ServiceUI.PI_SNOOZED, snoozed, PendingIntent.FLAG_UPDATE_CURRENT);
AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
if (wakeup == null) {
diff --git a/app/src/main/java/eu/faircode/email/FragmentCompose.java b/app/src/main/java/eu/faircode/email/FragmentCompose.java
index f28d741318..5083d20035 100644
--- a/app/src/main/java/eu/faircode/email/FragmentCompose.java
+++ b/app/src/main/java/eu/faircode/email/FragmentCompose.java
@@ -2078,10 +2078,6 @@ public class FragmentCompose extends FragmentBase {
if (!attachment.available)
throw new IllegalArgumentException(context.getString(R.string.title_attachments_missing));
- SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
- if (!prefs.getBoolean("enabled", true))
- throw new IllegalStateException(context.getString(R.string.title_sync_disabled));
-
// Delete draft (cannot move to outbox)
EntityOperation.queue(context, db, draft, EntityOperation.DELETE);
@@ -2160,18 +2156,7 @@ public class FragmentCompose extends FragmentBase {
finish();
else if (ex instanceof IllegalArgumentException || ex instanceof AddressException)
Snackbar.make(view, ex.getMessage(), Snackbar.LENGTH_LONG).show();
- else if (ex instanceof IllegalStateException) {
- Snackbar snackbar = Snackbar.make(view, ex.getMessage(), Snackbar.LENGTH_LONG);
- snackbar.setAction(R.string.title_enable, new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
- prefs.edit().putBoolean("enabled", true).apply();
- ServiceSynchronize.reload(getContext(), "compose/disabled");
- }
- });
- snackbar.show();
- } else
+ else
Helper.unexpectedError(getContext(), getViewLifecycleOwner(), ex);
}
};
diff --git a/app/src/main/java/eu/faircode/email/FragmentMessages.java b/app/src/main/java/eu/faircode/email/FragmentMessages.java
index 4a6fc0f928..0029eaed2a 100644
--- a/app/src/main/java/eu/faircode/email/FragmentMessages.java
+++ b/app/src/main/java/eu/faircode/email/FragmentMessages.java
@@ -492,10 +492,6 @@ public class FragmentMessages extends FragmentBase {
protected Boolean onExecute(Context context, Bundle args) {
long fid = args.getLong("folder");
- SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
- if (!prefs.getBoolean("enabled", true))
- throw new IllegalStateException(context.getString(R.string.title_sync_disabled));
-
ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo ni = cm.getActiveNetworkInfo();
boolean internet = (ni != null && ni.isConnected());
@@ -527,7 +523,7 @@ public class FragmentMessages extends FragmentBase {
if (account.ondemand) {
if (internet) {
now = true;
- ServiceSynchronize.sync(context, folder.id);
+ ServiceUI.sync(context, folder.id);
} else
nointernet = true;
} else {
@@ -561,18 +557,7 @@ public class FragmentMessages extends FragmentBase {
if (ex instanceof IllegalArgumentException)
Snackbar.make(view, ex.getMessage(), Snackbar.LENGTH_LONG).show();
- else if (ex instanceof IllegalStateException) {
- Snackbar snackbar = Snackbar.make(view, ex.getMessage(), Snackbar.LENGTH_LONG);
- snackbar.setAction(R.string.title_enable, new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
- prefs.edit().putBoolean("enabled", true).apply();
- ServiceSynchronize.reload(getContext(), "refresh/disabled");
- }
- });
- snackbar.show();
- } else
+ else
Helper.unexpectedError(getContext(), getViewLifecycleOwner(), ex);
}
}.execute(FragmentMessages.this, args, "messages:refresh");
diff --git a/app/src/main/java/eu/faircode/email/Helper.java b/app/src/main/java/eu/faircode/email/Helper.java
index 9f8979f469..92c51eee48 100644
--- a/app/src/main/java/eu/faircode/email/Helper.java
+++ b/app/src/main/java/eu/faircode/email/Helper.java
@@ -997,38 +997,4 @@ public class Helper {
static String sanitizeFilename(String name) {
return (name == null ? null : name.replaceAll("[^a-zA-Z0-9\\.\\-]", "_"));
}
-
- static NotificationCompat.Builder getNotificationError(Context context, String title, Throwable ex) {
- return getNotificationError(context, "error", title, ex, true);
- }
-
- static NotificationCompat.Builder getNotificationError(Context context, String channel, String title, Throwable ex, boolean debug) {
- // Build pending intent
- Intent intent = new Intent(context, ActivitySetup.class);
- if (debug)
- intent.setAction("error");
- intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- PendingIntent pi = PendingIntent.getActivity(
- context, ActivitySetup.REQUEST_ERROR, intent, PendingIntent.FLAG_UPDATE_CURRENT);
-
- // Build notification
- NotificationCompat.Builder builder = new NotificationCompat.Builder(context, channel);
-
- builder
- .setSmallIcon(R.drawable.baseline_warning_white_24)
- .setContentTitle(context.getString(R.string.title_notification_failed, title))
- .setContentText(Helper.formatThrowable(ex))
- .setContentIntent(pi)
- .setAutoCancel(false)
- .setShowWhen(true)
- .setPriority(Notification.PRIORITY_MAX)
- .setOnlyAlertOnce(true)
- .setCategory(Notification.CATEGORY_ERROR)
- .setVisibility(NotificationCompat.VISIBILITY_SECRET);
-
- builder.setStyle(new NotificationCompat.BigTextStyle()
- .bigText(Helper.formatThrowable(ex, false, "\n")));
-
- return builder;
- }
}
diff --git a/app/src/main/java/eu/faircode/email/ServiceSend.java b/app/src/main/java/eu/faircode/email/ServiceSend.java
index 8d6ed4b62e..9cd72b34d0 100644
--- a/app/src/main/java/eu/faircode/email/ServiceSend.java
+++ b/app/src/main/java/eu/faircode/email/ServiceSend.java
@@ -374,11 +374,11 @@ public class ServiceSend extends LifecycleService {
long now = new Date().getTime();
long delayed = now - message.last_attempt;
- if (delayed > IDENTITY_ERROR_AFTER * 60 * 1000L) {
+ if (delayed > IDENTITY_ERROR_AFTER * 60 * 1000L || ex instanceof SendFailedException) {
Log.i("Reporting send error after=" + delayed);
NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
nm.notify("send", message.identity.intValue(),
- Helper.getNotificationError(this, ident.name, ex).build());
+ Core.getNotificationError(this, ident.name, ex).build());
}
throw ex;
diff --git a/app/src/main/java/eu/faircode/email/ServiceSynchronize.java b/app/src/main/java/eu/faircode/email/ServiceSynchronize.java
index 4d9bdfd5df..1a85cc81f9 100644
--- a/app/src/main/java/eu/faircode/email/ServiceSynchronize.java
+++ b/app/src/main/java/eu/faircode/email/ServiceSynchronize.java
@@ -28,7 +28,6 @@ import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
-import android.content.pm.PackageManager;
import android.graphics.Color;
import android.media.RingtoneManager;
import android.net.ConnectivityManager;
@@ -40,69 +39,36 @@ import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.PowerManager;
-import android.os.SystemClock;
import android.preference.PreferenceManager;
import android.text.TextUtils;
import android.util.LongSparseArray;
-import com.sun.mail.iap.ConnectionException;
-import com.sun.mail.iap.Response;
-import com.sun.mail.imap.AppendUID;
import com.sun.mail.imap.IMAPFolder;
import com.sun.mail.imap.IMAPMessage;
import com.sun.mail.imap.IMAPStore;
-import com.sun.mail.imap.protocol.FetchResponse;
-import com.sun.mail.imap.protocol.IMAPProtocol;
-import com.sun.mail.imap.protocol.UID;
-import com.sun.mail.util.FolderClosedIOException;
-import com.sun.mail.util.MailConnectException;
-import org.json.JSONArray;
-import org.json.JSONException;
import org.jsoup.Jsoup;
-import java.io.BufferedInputStream;
-import java.io.BufferedOutputStream;
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.net.SocketException;
-import java.net.SocketTimeoutException;
-import java.net.UnknownHostException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
-import java.util.Arrays;
import java.util.Calendar;
-import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
-import java.util.Objects;
import java.util.Properties;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
-import java.util.concurrent.Semaphore;
-import java.util.concurrent.TimeUnit;
-import javax.mail.Address;
-import javax.mail.AuthenticationFailedException;
import javax.mail.FetchProfile;
-import javax.mail.Flags;
import javax.mail.Folder;
import javax.mail.FolderClosedException;
-import javax.mail.FolderNotFoundException;
import javax.mail.Message;
import javax.mail.MessageRemovedException;
import javax.mail.MessagingException;
import javax.mail.NoSuchProviderException;
-import javax.mail.SendFailedException;
import javax.mail.Session;
import javax.mail.Store;
import javax.mail.StoreClosedException;
@@ -117,17 +83,7 @@ import javax.mail.event.MessageCountAdapter;
import javax.mail.event.MessageCountEvent;
import javax.mail.event.StoreEvent;
import javax.mail.event.StoreListener;
-import javax.mail.internet.InternetAddress;
-import javax.mail.internet.MimeMessage;
-import javax.mail.search.ComparisonTerm;
-import javax.mail.search.FlagTerm;
-import javax.mail.search.MessageIDTerm;
-import javax.mail.search.OrTerm;
-import javax.mail.search.ReceivedDateTerm;
-import javax.mail.search.SearchTerm;
-import javax.net.ssl.SSLException;
-import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;
import androidx.core.content.ContextCompat;
@@ -145,23 +101,12 @@ public class ServiceSynchronize extends LifecycleService {
private static final int CONNECT_BACKOFF_START = 8; // seconds
private static final int CONNECT_BACKOFF_MAX = 64; // seconds (totally 2 minutes)
private static final int CONNECT_BACKOFF_AlARM = 15; // minutes
- private static final int SYNC_BATCH_SIZE = 20;
- private static final int DOWNLOAD_BATCH_SIZE = 20;
private static final long RECONNECT_BACKOFF = 90 * 1000L; // milliseconds
private static final int ACCOUNT_ERROR_AFTER = 90; // minutes
private static final int BACKOFF_ERROR_AFTER = 16; // seconds
private static final long STOP_DELAY = 5000L; // milliseconds
- private static final long YIELD_DURATION = 200L; // milliseconds
- static final int PI_WHY = 1;
- static final int PI_SUMMARY = 2;
- static final int PI_CLEAR = 3;
- static final int PI_SEEN = 4;
- static final int PI_ARCHIVE = 5;
- static final int PI_TRASH = 6;
- static final int PI_IGNORED = 7;
- static final int PI_SNOOZED = 8;
- static final int PI_SCHEDULE = 9;
+ static final int PI_SCHEDULE = 1;
@Override
public void onCreate() {
@@ -317,23 +262,6 @@ public class ServiceSynchronize extends LifecycleService {
try {
final String[] parts = action.split(":");
switch (parts[0]) {
- case "why":
- Intent why = new Intent(Intent.ACTION_VIEW);
- why.setData(Uri.parse(Helper.FAQ_URI + "#user-content-faq2"));
- why.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-
- PackageManager pm = getPackageManager();
- SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
- if (prefs.getBoolean("why", false) || why.resolveActivity(pm) == null) {
- Intent view = new Intent(this, ActivityView.class);
- view.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- startActivity(view);
- } else {
- prefs.edit().putBoolean("why", true).apply();
- startActivity(why);
- }
- break;
-
case "init":
// Network events will manage the service
serviceManager.service_init(intent.getBooleanExtra("boot", false));
@@ -347,99 +275,6 @@ public class ServiceSynchronize extends LifecycleService {
serviceManager.service_reload(intent.getStringExtra("reason"));
break;
- case "synchronize":
- executor.submit(new Runnable() {
- @Override
- public void run() {
- synchronizeOnDemand(Long.parseLong(parts[1]));
- }
- });
- break;
-
- case "summary":
- case "clear":
- case "seen":
- case "archive":
- case "trash":
- case "ignore":
- case "snooze":
- executor.submit(new Runnable() {
- @Override
- public void run() {
- DB db = DB.getInstance(ServiceSynchronize.this);
- try {
- db.beginTransaction();
-
- long id = (parts.length > 1 ? Long.parseLong(parts[1]) : -1);
- EntityMessage message = db.message().getMessage(id);
- if (id > 0 && message == null)
- return;
-
- switch (parts[0]) {
- case "summary":
- db.message().ignoreAll();
-
- Intent view = new Intent(ServiceSynchronize.this, ActivityView.class);
- view.setAction("unified");
- view.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- startActivity(view);
- break;
-
- case "clear":
- db.message().ignoreAll();
- break;
-
- case "seen":
- EntityOperation.queue(ServiceSynchronize.this, db, message, EntityOperation.SEEN, true);
- break;
-
- case "archive":
- EntityFolder archive = db.folder().getFolderByType(message.account, EntityFolder.ARCHIVE);
- if (archive == null)
- archive = db.folder().getFolderByType(message.account, EntityFolder.TRASH);
- if (archive != null)
- EntityOperation.queue(ServiceSynchronize.this, db, message, EntityOperation.MOVE, archive.id);
- break;
-
- case "trash":
- EntityFolder trash = db.folder().getFolderByType(message.account, EntityFolder.TRASH);
- if (trash != null)
- EntityOperation.queue(ServiceSynchronize.this, db, message, EntityOperation.MOVE, trash.id);
- break;
-
- case "ignore":
- db.message().setMessageUiIgnored(message.id, true);
- break;
-
- case "snooze":
- db.message().setMessageSnoozed(message.id, null);
-
- EntityFolder folder = db.folder().getFolder(message.folder);
- if (EntityFolder.OUTBOX.equals(folder.type)) {
- Log.i("Delayed send id=" + message.id);
- EntityOperation.queue(
- ServiceSynchronize.this, db, message, EntityOperation.SEND);
- } else {
- EntityOperation.queue(
- ServiceSynchronize.this, db, message, EntityOperation.SEEN, false);
- db.message().setMessageUiIgnored(message.id, false);
- }
- break;
-
- default:
- Log.w("Unknown action: " + parts[0]);
- }
-
- db.setTransactionSuccessful();
- } catch (Throwable ex) {
- Log.e(ex);
- } finally {
- db.endTransaction();
- }
- }
- });
- break;
-
default:
Log.w("Unknown action: " + action);
}
@@ -457,9 +292,9 @@ public class ServiceSynchronize extends LifecycleService {
stats = new TupleAccountStats();
// Build pending intent
- Intent intent = new Intent(this, ServiceSynchronize.class);
+ Intent intent = new Intent(this, ServiceUI.class);
intent.setAction("why");
- PendingIntent pi = PendingIntent.getService(this, PI_WHY, intent, PendingIntent.FLAG_UPDATE_CURRENT);
+ PendingIntent pi = PendingIntent.getService(this, ServiceUI.PI_WHY, intent, PendingIntent.FLAG_UPDATE_CURRENT);
// Build notification
NotificationCompat.Builder builder = new NotificationCompat.Builder(this, "service");
@@ -510,13 +345,13 @@ public class ServiceSynchronize extends LifecycleService {
messageContact.put(message, ContactInfo.get(this, message.from, false));
// Build pending intent
- Intent summary = new Intent(this, ServiceSynchronize.class);
+ Intent summary = new Intent(this, ServiceUI.class);
summary.setAction("summary");
- PendingIntent piSummary = PendingIntent.getService(this, PI_SUMMARY, summary, PendingIntent.FLAG_UPDATE_CURRENT);
+ PendingIntent piSummary = PendingIntent.getService(this, ServiceUI.PI_SUMMARY, summary, PendingIntent.FLAG_UPDATE_CURRENT);
- Intent clear = new Intent(this, ServiceSynchronize.class);
+ Intent clear = new Intent(this, ServiceUI.class);
clear.setAction("clear");
- PendingIntent piClear = PendingIntent.getService(this, PI_CLEAR, clear, PendingIntent.FLAG_UPDATE_CURRENT);
+ PendingIntent piClear = PendingIntent.getService(this, ServiceUI.PI_CLEAR, clear, PendingIntent.FLAG_UPDATE_CURRENT);
String channelName = (account == 0 ? "notification" : EntityAccount.getNotificationChannelName(account));
@@ -607,22 +442,21 @@ public class ServiceSynchronize extends LifecycleService {
PendingIntent piContent = PendingIntent.getActivity(
this, ActivityView.REQUEST_THREAD, thread, PendingIntent.FLAG_UPDATE_CURRENT);
- Intent ignored = new Intent(this, ServiceSynchronize.class);
+ Intent ignored = new Intent(this, ServiceUI.class);
ignored.setAction("ignore:" + message.id);
- PendingIntent piDelete = PendingIntent.getService(this, PI_IGNORED, ignored, PendingIntent.FLAG_UPDATE_CURRENT);
+ PendingIntent piDelete = PendingIntent.getService(this, ServiceUI.PI_IGNORED, ignored, PendingIntent.FLAG_UPDATE_CURRENT);
- Intent seen = new Intent(this, ServiceSynchronize.class);
+ Intent seen = new Intent(this, ServiceUI.class);
seen.setAction("seen:" + message.id);
- PendingIntent piSeen = PendingIntent.getService(this, PI_SEEN, seen, PendingIntent.FLAG_UPDATE_CURRENT);
+ PendingIntent piSeen = PendingIntent.getService(this, ServiceUI.PI_SEEN, seen, PendingIntent.FLAG_UPDATE_CURRENT);
- Intent archive = new Intent(this, ServiceSynchronize.class);
+ Intent archive = new Intent(this, ServiceUI.class);
archive.setAction("archive:" + message.id);
- PendingIntent piArchive = PendingIntent.getService(this, PI_ARCHIVE, archive, PendingIntent.FLAG_UPDATE_CURRENT);
+ PendingIntent piArchive = PendingIntent.getService(this, ServiceUI.PI_ARCHIVE, archive, PendingIntent.FLAG_UPDATE_CURRENT);
- Intent trash = new Intent(this, ServiceSynchronize.class);
+ Intent trash = new Intent(this, ServiceUI.class);
trash.setAction("trash:" + message.id);
- PendingIntent piTrash = PendingIntent.getService(this, PI_TRASH, trash, PendingIntent.FLAG_UPDATE_CURRENT);
-
+ PendingIntent piTrash = PendingIntent.getService(this, ServiceUI.PI_TRASH, trash, PendingIntent.FLAG_UPDATE_CURRENT);
NotificationCompat.Action.Builder actionSeen = new NotificationCompat.Action.Builder(
R.drawable.baseline_visibility_24,
@@ -713,58 +547,7 @@ public class ServiceSynchronize extends LifecycleService {
return notifications;
}
- private void reportError(EntityAccount account, EntityFolder folder, Throwable ex) {
- // FolderClosedException: can happen when no connectivity
-
- // IllegalStateException:
- // - "This operation is not allowed on a closed folder"
- // - can happen when syncing message
-
- // ConnectionException
- // - failed to create new store connection (connectivity)
-
- // MailConnectException
- // - on connectivity problems when connecting to store
-
- String title;
- if (account == null)
- title = folder.name;
- else if (folder == null)
- title = account.name;
- else
- title = account.name + "/" + folder.name;
-
- String tag = "error:" + (account == null ? 0 : account.id) + ":" + (folder == null ? 0 : folder.id);
-
- EntityLog.log(this, title + " " + Helper.formatThrowable(ex));
-
- if ((ex instanceof SendFailedException) || (ex instanceof AlertException)) {
- NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
- nm.notify(tag, 1, Helper.getNotificationError(this, title, ex).build());
- }
-
- // connection failure: Too many simultaneous connections
-
- if (BuildConfig.DEBUG &&
- !(ex instanceof SendFailedException) &&
- !(ex instanceof MailConnectException) &&
- !(ex instanceof FolderClosedException) &&
- !(ex instanceof IllegalStateException) &&
- !(ex instanceof AuthenticationFailedException) && // Also: Too many simultaneous connections
- !(ex instanceof StoreClosedException) &&
- !(ex instanceof MessageRemovedException) &&
- !(ex instanceof MessagingException && ex.getCause() instanceof UnknownHostException) &&
- !(ex instanceof MessagingException && ex.getCause() instanceof ConnectionException) &&
- !(ex instanceof MessagingException && ex.getCause() instanceof SocketException) &&
- !(ex instanceof MessagingException && ex.getCause() instanceof SocketTimeoutException) &&
- !(ex instanceof MessagingException && ex.getCause() instanceof SSLException) &&
- !(ex instanceof MessagingException && "connection failure".equals(ex.getMessage()))) {
- NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
- nm.notify(tag, 1, Helper.getNotificationError(this, title, ex).build());
- }
- }
-
- private void monitorAccount(final EntityAccount account, final ServiceState state) throws NoSuchProviderException {
+ private void monitorAccount(final EntityAccount account, final Core.State state) throws NoSuchProviderException {
final PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
final PowerManager.WakeLock wlAccount = pm.newWakeLock(
PowerManager.PARTIAL_WAKE_LOCK, BuildConfig.APPLICATION_ID + ":account." + account.id);
@@ -804,7 +587,9 @@ public class ServiceSynchronize extends LifecycleService {
Log.w(account.name + " " + type + ": " + e.getMessage());
EntityLog.log(ServiceSynchronize.this, account.name + " " + type + ": " + e.getMessage());
db.account().setAccountError(account.id, e.getMessage());
- reportError(account, null, new AlertException(e.getMessage()));
+ Core.reportError(
+ ServiceSynchronize.this, account, null,
+ new Core.AlertException(e.getMessage()));
state.error();
} else
Log.i(account.name + " " + type + ": " + e.getMessage());
@@ -896,7 +681,7 @@ public class ServiceSynchronize extends LifecycleService {
.format((account.last_connected))), ex);
NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
nm.notify("receive", account.id.intValue(),
- Helper.getNotificationError(this, "warning", account.name, warning, false)
+ Core.getNotificationError(this, "warning", account.name, warning, false)
.build());
}
}
@@ -912,7 +697,7 @@ public class ServiceSynchronize extends LifecycleService {
EntityLog.log(this, account.name + " connected");
// Update folder list
- synchronizeFolders(account, istore, state);
+ Core.synchronizeFolders(this, account, istore, state);
// Open synchronizing folders
final ExecutorService pollExecutor = Executors.newSingleThreadExecutor(Helper.backgroundThreadFactory);
@@ -965,7 +750,7 @@ public class ServiceSynchronize extends LifecycleService {
EntityMessage message;
try {
db.beginTransaction();
- message = synchronizeMessage(
+ message = Core.synchronizeMessage(
ServiceSynchronize.this,
folder, (IMAPFolder) ifolder, (IMAPMessage) imessage,
false,
@@ -978,7 +763,7 @@ public class ServiceSynchronize extends LifecycleService {
if (db.folder().getFolderDownload(folder.id))
try {
db.beginTransaction();
- downloadMessage(ServiceSynchronize.this,
+ Core.downloadMessage(ServiceSynchronize.this,
folder, (IMAPFolder) ifolder,
(IMAPMessage) imessage, message.id);
db.setTransactionSuccessful();
@@ -1001,7 +786,7 @@ public class ServiceSynchronize extends LifecycleService {
}
} catch (Throwable ex) {
Log.e(folder.name, ex);
- reportError(account, folder, ex);
+ Core.reportError(ServiceSynchronize.this, account, folder, ex);
state.error();
} finally {
wlAccount.release();
@@ -1026,7 +811,7 @@ public class ServiceSynchronize extends LifecycleService {
}
} catch (Throwable ex) {
Log.e(folder.name, ex);
- reportError(account, folder, ex);
+ Core.reportError(ServiceSynchronize.this, account, folder, ex);
db.folder().setFolderError(folder.id, Helper.formatThrowable(ex, true));
state.error();
} finally {
@@ -1054,7 +839,7 @@ public class ServiceSynchronize extends LifecycleService {
EntityMessage message;
try {
db.beginTransaction();
- message = synchronizeMessage(
+ message = Core.synchronizeMessage(
ServiceSynchronize.this,
folder, (IMAPFolder) ifolder, (IMAPMessage) e.getMessage(),
false,
@@ -1067,7 +852,7 @@ public class ServiceSynchronize extends LifecycleService {
if (db.folder().getFolderDownload(folder.id))
try {
db.beginTransaction();
- downloadMessage(ServiceSynchronize.this,
+ Core.downloadMessage(ServiceSynchronize.this,
folder, (IMAPFolder) ifolder,
(IMAPMessage) e.getMessage(), message.id);
db.setTransactionSuccessful();
@@ -1090,7 +875,7 @@ public class ServiceSynchronize extends LifecycleService {
}
} catch (Throwable ex) {
Log.e(folder.name, ex);
- reportError(account, folder, ex);
+ Core.reportError(ServiceSynchronize.this, account, folder, ex);
state.error();
} finally {
wlAccount.release();
@@ -1110,7 +895,7 @@ public class ServiceSynchronize extends LifecycleService {
}
} catch (Throwable ex) {
Log.e(folder.name, ex);
- reportError(account, folder, ex);
+ Core.reportError(ServiceSynchronize.this, account, folder, ex);
db.folder().setFolderError(folder.id, Helper.formatThrowable(ex, true));
state.error();
} finally {
@@ -1194,11 +979,14 @@ public class ServiceSynchronize extends LifecycleService {
db.folder().setFolderError(folder.id, null);
}
- processOperations(account, folder, isession, istore, ifolder, state);
+ Core.processOperations(ServiceSynchronize.this,
+ account, folder,
+ isession, istore, ifolder,
+ state);
} catch (Throwable ex) {
Log.e(folder.name, ex);
- reportError(account, folder, ex);
+ Core.reportError(ServiceSynchronize.this, account, folder, ex);
db.folder().setFolderError(folder.id, Helper.formatThrowable(ex, true));
state.error();
} finally {
@@ -1305,7 +1093,7 @@ public class ServiceSynchronize extends LifecycleService {
Log.i(account.name + " done state=" + state);
} catch (Throwable ex) {
Log.e(account.name, ex);
- reportError(account, null, ex);
+ Core.reportError(ServiceSynchronize.this, account, null, ex);
EntityLog.log(this, account.name + " " + Helper.formatThrowable(ex));
db.account().setAccountError(account.id, Helper.formatThrowable(ex));
@@ -1402,1355 +1190,8 @@ public class ServiceSynchronize extends LifecycleService {
}
}
- private void processOperations(
- EntityAccount account, EntityFolder folder,
- Session isession, Store istore, Folder ifolder,
- ServiceState state)
- throws MessagingException, JSONException, IOException {
- try {
- Log.i(folder.name + " start process");
-
- DB db = DB.getInstance(this);
- List ops = db.operation().getOperations(folder.id);
- Log.i(folder.name + " pending operations=" + ops.size());
- for (int i = 0; i < ops.size() && state.running(); i++) {
- EntityOperation op = ops.get(i);
- try {
- Log.i(folder.name +
- " start op=" + op.id + "/" + op.name +
- " msg=" + op.message +
- " args=" + op.args);
-
- // Fetch most recent copy of message
- EntityMessage message = null;
- if (op.message != null)
- message = db.message().getMessage(op.message);
-
- JSONArray jargs = new JSONArray(op.args);
-
- try {
- if (message == null && !EntityOperation.SYNC.equals(op.name))
- throw new MessageRemovedException();
-
- db.operation().setOperationError(op.id, null);
- if (message != null)
- db.message().setMessageError(message.id, null);
-
- if (message != null && message.uid == null &&
- !(EntityOperation.ADD.equals(op.name) ||
- EntityOperation.DELETE.equals(op.name) ||
- EntityOperation.SEND.equals(op.name) ||
- EntityOperation.SYNC.equals(op.name)))
- throw new IllegalArgumentException(op.name + " without uid " + op.args);
-
- // Operations should use database transaction when needed
-
- switch (op.name) {
- case EntityOperation.SEEN:
- doSeen(folder, (IMAPFolder) ifolder, message, jargs, db);
- break;
-
- case EntityOperation.FLAG:
- doFlag(folder, (IMAPFolder) ifolder, message, jargs, db);
- break;
-
- case EntityOperation.ANSWERED:
- doAnswered(folder, (IMAPFolder) ifolder, message, jargs, db);
- break;
-
- case EntityOperation.KEYWORD:
- doKeyword(folder, (IMAPFolder) ifolder, message, jargs, db);
- break;
-
- case EntityOperation.ADD:
- doAdd(folder, isession, (IMAPStore) istore, (IMAPFolder) ifolder, message, jargs, db);
- break;
-
- case EntityOperation.MOVE:
- doMove(folder, isession, (IMAPStore) istore, (IMAPFolder) ifolder, message, jargs, db);
- break;
-
- case EntityOperation.DELETE:
- doDelete(folder, (IMAPFolder) ifolder, message, jargs, db);
- break;
-
- case EntityOperation.HEADERS:
- doHeaders(folder, (IMAPFolder) ifolder, message, db);
- break;
-
- case EntityOperation.RAW:
- doRaw(folder, (IMAPFolder) ifolder, message, jargs, db);
- break;
-
- case EntityOperation.BODY:
- doBody(folder, (IMAPFolder) ifolder, message, db);
- break;
-
- case EntityOperation.ATTACHMENT:
- doAttachment(folder, op, (IMAPFolder) ifolder, message, jargs, db);
- break;
-
- case EntityOperation.SYNC:
- if (folder.account == null)
- db.folder().setFolderError(folder.id, null);
- else
- synchronizeMessages(account, folder, (IMAPFolder) ifolder, jargs, state);
- break;
-
- default:
- throw new IllegalArgumentException("Unknown operation=" + op.name);
- }
-
- // Operation succeeded
- db.operation().deleteOperation(op.id);
- } catch (Throwable ex) {
- // TODO: SMTP response codes: https://www.ietf.org/rfc/rfc821.txt
- Log.e(folder.name, ex);
- reportError(account, folder, ex);
-
- db.operation().setOperationError(op.id, Helper.formatThrowable(ex));
-
- if (message != null &&
- !(ex instanceof MessageRemovedException) &&
- !(ex instanceof FolderClosedException) &&
- !(ex instanceof IllegalStateException))
- db.message().setMessageError(message.id, Helper.formatThrowable(ex));
-
- if (ex instanceof MessageRemovedException ||
- ex instanceof FolderNotFoundException ||
- ex instanceof IllegalArgumentException) {
- Log.w("Unrecoverable", ex);
-
- // There is no use in repeating
- db.operation().deleteOperation(op.id);
-
- // Cleanup
- if (message != null) {
- if (ex instanceof MessageRemovedException)
- db.message().deleteMessage(message.id);
-
- Long newid = null;
-
- if (EntityOperation.MOVE.equals(op.name) &&
- jargs.length() > 2)
- newid = jargs.getLong(2);
-
- if ((EntityOperation.ADD.equals(op.name) ||
- EntityOperation.RAW.equals(op.name)) &&
- jargs.length() > 0 && !jargs.isNull(0))
- newid = jargs.getLong(0);
-
- // Delete temporary copy in target folder
- if (newid != null) {
- db.message().deleteMessage(newid);
- db.message().setMessageUiHide(message.id, false);
- }
- }
-
- continue;
- } else if (ex instanceof MessagingException) {
- // Socket timeout is a recoverable condition (send message)
- if (ex.getCause() instanceof SocketTimeoutException) {
- Log.w("Recoverable", ex);
- // No need to inform user
- return;
- }
- }
-
- throw ex;
- }
- } finally {
- Log.i(folder.name + " end op=" + op.id + "/" + op.name);
- }
- }
- } finally {
- Log.i(folder.name + " end process state=" + state);
- }
- }
-
- private void doSeen(EntityFolder folder, IMAPFolder ifolder, EntityMessage message, JSONArray jargs, DB db) throws MessagingException, JSONException {
- // Mark message (un)seen
- if (!ifolder.getPermanentFlags().contains(Flags.Flag.SEEN)) {
- db.message().setMessageSeen(message.id, false);
- db.message().setMessageUiSeen(message.id, false);
- return;
- }
-
- boolean seen = jargs.getBoolean(0);
- if (message.seen.equals(seen))
- return;
-
- Message imessage = ifolder.getMessageByUID(message.uid);
- if (imessage == null)
- throw new MessageRemovedException();
-
- imessage.setFlag(Flags.Flag.SEEN, seen);
-
- db.message().setMessageSeen(message.id, seen);
- }
-
- private void doFlag(EntityFolder folder, IMAPFolder ifolder, EntityMessage message, JSONArray jargs, DB db) throws MessagingException, JSONException {
- // Star/unstar message
- if (!ifolder.getPermanentFlags().contains(Flags.Flag.FLAGGED)) {
- db.message().setMessageFlagged(message.id, false);
- db.message().setMessageUiFlagged(message.id, false);
- return;
- }
-
- boolean flagged = jargs.getBoolean(0);
- if (message.flagged.equals(flagged))
- return;
-
- Message imessage = ifolder.getMessageByUID(message.uid);
- if (imessage == null)
- throw new MessageRemovedException();
-
- imessage.setFlag(Flags.Flag.FLAGGED, flagged);
-
- db.message().setMessageFlagged(message.id, flagged);
- }
-
- private void doAnswered(EntityFolder folder, IMAPFolder ifolder, EntityMessage message, JSONArray jargs, DB db) throws MessagingException, JSONException {
- // Mark message (un)answered
- if (!ifolder.getPermanentFlags().contains(Flags.Flag.ANSWERED)) {
- db.message().setMessageAnswered(message.id, false);
- db.message().setMessageUiAnswered(message.id, false);
- return;
- }
-
- boolean answered = jargs.getBoolean(0);
- if (message.answered.equals(answered))
- return;
-
- Message imessage = ifolder.getMessageByUID(message.uid);
- if (imessage == null)
- throw new MessageRemovedException();
-
- imessage.setFlag(Flags.Flag.ANSWERED, answered);
-
- db.message().setMessageAnswered(message.id, answered);
- }
-
- private void doKeyword(EntityFolder folder, IMAPFolder ifolder, EntityMessage message, JSONArray jargs, DB db) throws MessagingException, JSONException {
- // Set/reset user flag
- if (!ifolder.getPermanentFlags().contains(Flags.Flag.USER)) {
- db.message().setMessageKeywords(message.id, DB.Converters.fromStringArray(null));
- return;
- }
-
- // https://tools.ietf.org/html/rfc3501#section-2.3.2
- String keyword = jargs.getString(0);
- boolean set = jargs.getBoolean(1);
-
- Message imessage = ifolder.getMessageByUID(message.uid);
- if (imessage == null)
- throw new MessageRemovedException();
-
- Flags flags = new Flags(keyword);
- imessage.setFlags(flags, set);
-
- try {
- db.beginTransaction();
-
- message = db.message().getMessage(message.id);
-
- List keywords = new ArrayList<>(Arrays.asList(message.keywords));
- if (set) {
- if (!keywords.contains(keyword))
- keywords.add(keyword);
- } else
- keywords.remove(keyword);
- db.message().setMessageKeywords(message.id, DB.Converters.fromStringArray(keywords.toArray(new String[0])));
-
- db.setTransactionSuccessful();
- } finally {
- db.endTransaction();
- }
- }
-
- private void doAdd(EntityFolder folder, Session isession, IMAPStore istore, IMAPFolder ifolder, EntityMessage message, JSONArray jargs, DB db) throws MessagingException, JSONException, IOException {
- // Add message
- if (TextUtils.isEmpty(message.msgid))
- throw new IllegalArgumentException("Message ID missing");
-
- // Get message
- MimeMessage imessage;
- if (folder.id.equals(message.folder)) {
- // Pre flight checks
- if (!message.content)
- throw new IllegalArgumentException("Message body missing");
-
- EntityIdentity identity =
- (message.identity == null ? null : db.identity().getIdentity(message.identity));
-
- imessage = MessageHelper.from(this, message, isession,
- identity == null ? false : identity.plain_only);
- } else {
- // Cross account move
- File file = EntityMessage.getRawFile(this, message.id);
- if (!file.exists())
- throw new IllegalArgumentException("raw message file not found");
-
- Log.i(folder.name + " reading " + file);
- try (InputStream is = new BufferedInputStream(new FileInputStream(file))) {
- imessage = new MimeMessage(isession, is);
- }
- }
-
- // Handle auto read
- boolean autoread = false;
- if (jargs.length() > 1) {
- autoread = jargs.getBoolean(1);
- if (ifolder.getPermanentFlags().contains(Flags.Flag.SEEN)) {
- if (autoread && !imessage.isSet(Flags.Flag.SEEN)) {
- Log.i(folder.name + " autoread");
- imessage.setFlag(Flags.Flag.SEEN, true);
- }
- }
- }
-
- // Handle draft
- if (EntityFolder.DRAFTS.equals(folder.type))
- if (ifolder.getPermanentFlags().contains(Flags.Flag.DRAFT))
- imessage.setFlag(Flags.Flag.DRAFT, true);
-
- // Add message
- long uid = append(istore, ifolder, imessage);
- Log.i(folder.name + " appended id=" + message.id + " uid=" + uid);
- db.message().setMessageUid(message.id, uid);
-
- if (folder.id.equals(message.folder)) {
- // Delete previous message
- Message[] ideletes = ifolder.search(new MessageIDTerm(message.msgid));
- for (Message idelete : ideletes) {
- long duid = ifolder.getUID(idelete);
- if (duid == uid)
- Log.i(folder.name + " append confirmed uid=" + duid);
- else {
- Log.i(folder.name + " deleting uid=" + duid + " msgid=" + message.msgid);
- idelete.setFlag(Flags.Flag.DELETED, true);
- }
- }
- ifolder.expunge();
- } else {
- // Cross account move
- if (autoread) {
- Log.i(folder.name + " queuing SEEN id=" + message.id);
- EntityOperation.queue(this, db, message, EntityOperation.SEEN, true);
- }
-
- Log.i(folder.name + " queuing DELETE id=" + message.id);
- EntityOperation.queue(this, db, message, EntityOperation.DELETE);
- }
- }
-
- private void doMove(EntityFolder folder, Session isession, IMAPStore istore, IMAPFolder ifolder, EntityMessage message, JSONArray jargs, DB db) throws JSONException, MessagingException, IOException {
- // Move message
- Message imessage = ifolder.getMessageByUID(message.uid);
- if (imessage == null)
- throw new MessageRemovedException();
-
- // Get parameters
- boolean autoread = jargs.getBoolean(1);
-
- // Get target folder
- long id = jargs.getLong(0);
- EntityFolder target = db.folder().getFolder(id);
- if (target == null)
- throw new FolderNotFoundException();
- IMAPFolder itarget = (IMAPFolder) istore.getFolder(target.name);
-
- boolean canMove = istore.hasCapability("MOVE");
- if (canMove &&
- !EntityFolder.DRAFTS.equals(folder.type) &&
- !EntityFolder.DRAFTS.equals(target.type)) {
- // Autoread
- if (ifolder.getPermanentFlags().contains(Flags.Flag.SEEN))
- if (autoread && !imessage.isSet(Flags.Flag.SEEN))
- imessage.setFlag(Flags.Flag.SEEN, true);
-
- // Move message to
- ifolder.moveMessages(new Message[]{imessage}, itarget);
- } else {
- Log.w(folder.name + " MOVE by DELETE/APPEND" +
- " cap=" + canMove + " from=" + folder.type + " to=" + target.type);
-
- // Serialize source message
- ByteArrayOutputStream bos = new ByteArrayOutputStream();
- imessage.writeTo(bos);
-
- // Deserialize target message
- ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
- Message icopy = new MimeMessage(isession, bis);
-
- // Make sure the message has a message ID
- if (message.msgid == null) {
- String msgid = EntityMessage.generateMessageId();
- Log.i(target.name + " generated message id=" + msgid);
- icopy.setHeader("Message-ID", msgid);
- }
-
- try {
- // Needed to read flags
- itarget.open(Folder.READ_WRITE);
-
- // Auto read
- if (itarget.getPermanentFlags().contains(Flags.Flag.SEEN))
- if (autoread && !icopy.isSet(Flags.Flag.SEEN))
- icopy.setFlag(Flags.Flag.SEEN, true);
-
- // Move from drafts
- if (EntityFolder.DRAFTS.equals(folder.type))
- if (itarget.getPermanentFlags().contains(Flags.Flag.DRAFT))
- icopy.setFlag(Flags.Flag.DRAFT, false);
-
- // Move to drafts
- if (EntityFolder.DRAFTS.equals(target.type))
- if (itarget.getPermanentFlags().contains(Flags.Flag.DRAFT))
- icopy.setFlag(Flags.Flag.DRAFT, true);
-
- // Append target
- long uid = append(istore, itarget, (MimeMessage) icopy);
- Log.i(target.name + " appended id=" + message.id + " uid=" + uid);
-
- // Fixed timing issue of at least Courier based servers
- itarget.close(false);
- itarget.open(Folder.READ_WRITE);
-
- // Some providers, like Gmail, don't honor the appended seen flag
- if (itarget.getPermanentFlags().contains(Flags.Flag.SEEN)) {
- boolean seen = (autoread || message.ui_seen);
- icopy = itarget.getMessageByUID(uid);
- if (seen != icopy.isSet(Flags.Flag.SEEN)) {
- Log.i(target.name + " Fixing id=" + message.id + " seen=" + seen);
- icopy.setFlag(Flags.Flag.SEEN, seen);
- }
- }
-
- // This is not based on an actual case, so this is just a safeguard
- if (itarget.getPermanentFlags().contains(Flags.Flag.DRAFT)) {
- boolean draft = EntityFolder.DRAFTS.equals(target.type);
- icopy = itarget.getMessageByUID(uid);
- if (draft != icopy.isSet(Flags.Flag.DRAFT)) {
- Log.i(target.name + " Fixing id=" + message.id + " draft=" + draft);
- icopy.setFlag(Flags.Flag.DRAFT, draft);
- }
- }
-
- // Delete source
- imessage.setFlag(Flags.Flag.DELETED, true);
- ifolder.expunge();
- } catch (Throwable ex) {
- if (itarget.isOpen())
- itarget.close();
- throw ex;
- }
- }
- }
-
- private void doDelete(EntityFolder folder, IMAPFolder ifolder, EntityMessage message, JSONArray jargs, DB db) throws MessagingException {
- // Delete message
- if (TextUtils.isEmpty(message.msgid))
- throw new IllegalArgumentException("Message ID missing");
-
- Message[] imessages = ifolder.search(new MessageIDTerm(message.msgid));
- for (Message imessage : imessages) {
- Log.i(folder.name + " deleting uid=" + message.uid + " msgid=" + message.msgid);
- imessage.setFlag(Flags.Flag.DELETED, true);
- }
- ifolder.expunge();
-
- db.message().deleteMessage(message.id);
- }
-
- private void doHeaders(EntityFolder folder, IMAPFolder ifolder, EntityMessage message, DB db) throws MessagingException {
- if (message.headers != null)
- return;
-
- IMAPMessage imessage = (IMAPMessage) ifolder.getMessageByUID(message.uid);
- if (imessage == null)
- throw new MessageRemovedException();
-
- MessageHelper helper = new MessageHelper(imessage);
- db.message().setMessageHeaders(message.id, helper.getHeaders());
- }
-
- private void doRaw(EntityFolder folder, IMAPFolder ifolder, EntityMessage message, JSONArray jargs, DB db) throws MessagingException, IOException, JSONException {
- if (message.raw == null || !message.raw) {
- IMAPMessage imessage = (IMAPMessage) ifolder.getMessageByUID(message.uid);
- if (imessage == null)
- throw new MessageRemovedException();
-
- File file = EntityMessage.getRawFile(this, message.id);
-
- try (OutputStream os = new BufferedOutputStream(new FileOutputStream(file))) {
- imessage.writeTo(os);
- db.message().setMessageRaw(message.id, true);
- }
- }
-
- if (jargs.length() > 0) {
- long target = jargs.getLong(2);
- jargs.remove(2);
- Log.i(folder.name + " queuing ADD id=" + message.id + ":" + target);
-
- EntityOperation operation = new EntityOperation();
- operation.folder = target;
- operation.message = message.id;
- operation.name = EntityOperation.ADD;
- operation.args = jargs.toString();
- operation.created = new Date().getTime();
- operation.id = db.operation().insertOperation(operation);
- }
- }
-
- private void doBody(EntityFolder folder, IMAPFolder ifolder, EntityMessage message, DB db) throws MessagingException, IOException {
- // Download message body
- if (message.content)
- return;
-
- // Get message
- Message imessage = ifolder.getMessageByUID(message.uid);
- if (imessage == null)
- throw new MessageRemovedException();
-
- MessageHelper helper = new MessageHelper((MimeMessage) imessage);
- MessageHelper.MessageParts parts = helper.getMessageParts();
- String body = parts.getHtml(this);
- String preview = HtmlHelper.getPreview(body);
- Helper.writeText(EntityMessage.getFile(this, message.id), body);
- db.message().setMessageContent(message.id, true, preview);
- db.message().setMessageWarning(message.id, parts.getWarnings(message.warning));
- }
-
- private void doAttachment(EntityFolder folder, EntityOperation op, IMAPFolder ifolder, EntityMessage message, JSONArray jargs, DB db) throws JSONException, MessagingException, IOException {
- // Download attachment
- int sequence = jargs.getInt(0);
-
- // Get attachment
- EntityAttachment attachment = db.attachment().getAttachment(op.message, sequence);
- if (attachment.available)
- return;
-
- // Get message
- Message imessage = ifolder.getMessageByUID(message.uid);
- if (imessage == null)
- throw new MessageRemovedException();
-
- // Download attachment
- MessageHelper helper = new MessageHelper((MimeMessage) imessage);
- MessageHelper.MessageParts parts = helper.getMessageParts();
- parts.downloadAttachment(this, db, attachment.id, sequence);
- }
-
- private long append(IMAPStore istore, IMAPFolder ifolder, MimeMessage imessage) throws MessagingException {
- if (istore.hasCapability("UIDPLUS")) {
- AppendUID[] uids = ifolder.appendUIDMessages(new Message[]{imessage});
- if (uids == null || uids.length == 0)
- throw new MessageRemovedException("Message not appended");
- return uids[0].uid;
- } else {
- ifolder.appendMessages(new Message[]{imessage});
-
- long uid = -1;
- String msgid = imessage.getMessageID();
- Log.i("Searching for appended msgid=" + msgid);
- Message[] messages = ifolder.search(new MessageIDTerm(msgid));
- if (messages != null)
- for (Message iappended : messages) {
- long muid = ifolder.getUID(iappended);
- Log.i("Found appended uid=" + muid);
- // RFC3501: Unique identifiers are assigned in a strictly ascending fashion
- if (muid > uid)
- uid = muid;
- }
-
- if (uid < 0)
- throw new IllegalArgumentException("uid not found");
-
- return uid;
- }
- }
-
- private void synchronizeOnDemand(long fid) {
- Log.i("Synchronize on demand folder=" + fid);
-
- PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
- PowerManager.WakeLock wlAccount = pm.newWakeLock(
- PowerManager.PARTIAL_WAKE_LOCK, BuildConfig.APPLICATION_ID + ":sync." + fid);
-
- DB db = DB.getInstance(this);
- EntityFolder folder = null;
- EntityAccount account = null;
-
- Store istore = null;
- try {
- wlAccount.acquire();
-
- folder = db.folder().getFolder(fid);
- account = db.account().getAccount(folder.account);
-
- // Create session
- Properties props = MessageHelper.getSessionProperties(account.auth_type, account.realm, account.insecure);
- final Session isession = Session.getInstance(props, null);
- isession.setDebug(true);
-
- // Connect account
- Log.i(account.name + " connecting");
- db.account().setAccountState(account.id, "connecting");
- istore = isession.getStore(account.getProtocol());
- Helper.connect(this, istore, account);
- db.account().setAccountState(account.id, "connected");
- Log.i(account.name + " connected");
-
- // Synchronize folders
- synchronizeFolders(account, istore, new ServiceState());
-
- // Connect folder
- Log.i(folder.name + " connecting");
- db.folder().setFolderState(folder.id, "connecting");
- Folder ifolder = istore.getFolder(folder.name);
- ifolder.open(Folder.READ_WRITE);
- db.folder().setFolderState(folder.id, "connected");
- db.folder().setFolderError(folder.id, null);
- Log.i(folder.name + " connected");
-
- // Process operations
- processOperations(account, folder, isession, istore, ifolder, new ServiceState());
-
- // Synchronize messages
- synchronizeMessages(account, folder, (IMAPFolder) ifolder, folder.getSyncArgs(), new ServiceState());
-
- } catch (Throwable ex) {
- Log.w(ex);
- reportError(account, folder, ex);
- db.account().setAccountError(account.id, Helper.formatThrowable(ex));
- db.folder().setFolderError(folder.id, Helper.formatThrowable(ex, false));
- } finally {
- if (istore != null) {
- Log.i(account.name + " closing");
- db.account().setAccountState(account.id, "closing");
- db.folder().setFolderState(folder.id, "closing");
-
- try {
- istore.close();
- } catch (MessagingException ex) {
- Log.e(ex);
- }
-
- db.account().setAccountState(account.id, null);
- db.folder().setFolderState(folder.id, null);
- Log.i(account.name + " closed");
- }
-
- wlAccount.release();
- }
- }
-
- private void synchronizeFolders(EntityAccount account, Store istore, ServiceState state) throws MessagingException {
- DB db = DB.getInstance(this);
- try {
- db.beginTransaction();
-
- Log.i("Start sync folders account=" + account.name);
-
- List names = new ArrayList<>();
- for (EntityFolder folder : db.folder().getFolders(account.id))
- if (folder.tbc != null) {
- Log.i(folder.name + " creating");
- Folder ifolder = istore.getFolder(folder.name);
- if (!ifolder.exists())
- ifolder.create(Folder.HOLDS_MESSAGES);
- db.folder().resetFolderTbc(folder.id);
- } else if (folder.tbd != null && folder.tbd) {
- Log.i(folder.name + " deleting");
- Folder ifolder = istore.getFolder(folder.name);
- if (ifolder.exists())
- ifolder.delete(false);
- db.folder().deleteFolder(folder.id);
- } else
- names.add(folder.name);
- Log.i("Local folder count=" + names.size());
-
- Folder defaultFolder = istore.getDefaultFolder();
- char separator = defaultFolder.getSeparator();
- EntityLog.log(this, account.name + " folder separator=" + separator);
-
- Folder[] ifolders = defaultFolder.list("*");
- Log.i("Remote folder count=" + ifolders.length + " separator=" + separator);
-
- for (Folder ifolder : ifolders) {
- String fullName = ifolder.getFullName();
- String[] attrs = ((IMAPFolder) ifolder).getAttributes();
- String type = EntityFolder.getType(attrs, fullName);
-
- EntityLog.log(this, account.name + ":" + fullName +
- " attrs=" + TextUtils.join(" ", attrs) + " type=" + type);
-
- if (type != null) {
- names.remove(fullName);
-
- int level = EntityFolder.getLevel(separator, fullName);
- String display = null;
- if (account.prefix != null && fullName.startsWith(account.prefix + separator))
- display = fullName.substring(account.prefix.length() + 1);
-
- EntityFolder folder = db.folder().getFolderByName(account.id, fullName);
- if (folder == null) {
- folder = new EntityFolder();
- folder.account = account.id;
- folder.name = fullName;
- folder.display = display;
- folder.type = (EntityFolder.SYSTEM.equals(type) ? type : EntityFolder.USER);
- folder.level = level;
- folder.synchronize = false;
- folder.poll = ("imap.gmail.com".equals(account.host));
- folder.sync_days = EntityFolder.DEFAULT_SYNC;
- folder.keep_days = EntityFolder.DEFAULT_KEEP;
- db.folder().insertFolder(folder);
- Log.i(folder.name + " added type=" + folder.type);
- } else {
- Log.i(folder.name + " exists type=" + folder.type);
-
- if (folder.display == null) {
- if (display != null) {
- db.folder().setFolderDisplay(folder.id, display);
- EntityLog.log(this, account.name + ":" + folder.name +
- " removed prefix display=" + display + " separator=" + separator);
- }
- } else {
- if (account.prefix == null && folder.name.endsWith(separator + folder.display)) {
- db.folder().setFolderDisplay(folder.id, null);
- EntityLog.log(this, account.name + ":" + folder.name +
- " restored prefix display=" + folder.display + " separator=" + separator);
- }
- }
-
- db.folder().setFolderLevel(folder.id, level);
-
- // Compatibility
- if ("Inbox_sub".equals(folder.type))
- db.folder().setFolderType(folder.id, EntityFolder.USER);
- else if (EntityFolder.USER.equals(folder.type) && EntityFolder.SYSTEM.equals(type))
- db.folder().setFolderType(folder.id, type);
- else if (EntityFolder.SYSTEM.equals(folder.type) && EntityFolder.USER.equals(type))
- db.folder().setFolderType(folder.id, type);
- }
- }
- }
-
- Log.i("Delete local count=" + names.size());
- for (String name : names) {
- Log.i(name + " delete");
- db.folder().deleteFolder(account.id, name);
- }
-
- db.setTransactionSuccessful();
- } finally {
- db.endTransaction();
- Log.i("End sync folder");
- }
- }
-
- private void synchronizeMessages(EntityAccount account, final EntityFolder folder, IMAPFolder ifolder, JSONArray jargs, ServiceState state) throws JSONException, MessagingException, IOException {
- final DB db = DB.getInstance(this);
- try {
- int sync_days = jargs.getInt(0);
- int keep_days = jargs.getInt(1);
- boolean download = jargs.getBoolean(2);
-
- if (keep_days == sync_days)
- keep_days++;
-
- Log.i(folder.name + " start sync after=" + sync_days + "/" + keep_days);
-
- db.folder().setFolderSyncState(folder.id, "syncing");
-
- // Get reference times
- Calendar cal_sync = Calendar.getInstance();
- cal_sync.add(Calendar.DAY_OF_MONTH, -sync_days);
- cal_sync.set(Calendar.HOUR_OF_DAY, 0);
- cal_sync.set(Calendar.MINUTE, 0);
- cal_sync.set(Calendar.SECOND, 0);
- cal_sync.set(Calendar.MILLISECOND, 0);
-
- Calendar cal_keep = Calendar.getInstance();
- cal_keep.add(Calendar.DAY_OF_MONTH, -keep_days);
- cal_keep.set(Calendar.HOUR_OF_DAY, 0);
- cal_keep.set(Calendar.MINUTE, 0);
- cal_keep.set(Calendar.SECOND, 0);
- cal_keep.set(Calendar.MILLISECOND, 0);
-
- long sync_time = cal_sync.getTimeInMillis();
- if (sync_time < 0)
- sync_time = 0;
-
- long keep_time = cal_keep.getTimeInMillis();
- if (keep_time < 0)
- keep_time = 0;
-
- Log.i(folder.name + " sync=" + new Date(sync_time) + " keep=" + new Date(keep_time));
-
- // Delete old local messages
- int old = db.message().deleteMessagesBefore(folder.id, keep_time, false);
- Log.i(folder.name + " local old=" + old);
-
- // Get list of local uids
- final List uids = db.message().getUids(folder.id, null);
- Log.i(folder.name + " local count=" + uids.size());
-
- // Reduce list of local uids
- SearchTerm searchTerm = new ReceivedDateTerm(ComparisonTerm.GE, new Date(sync_time));
- if (ifolder.getPermanentFlags().contains(Flags.Flag.FLAGGED))
- searchTerm = new OrTerm(searchTerm, new FlagTerm(new Flags(Flags.Flag.FLAGGED), true));
-
- long search = SystemClock.elapsedRealtime();
- Message[] imessages = ifolder.search(searchTerm);
- Log.i(folder.name + " remote count=" + imessages.length +
- " search=" + (SystemClock.elapsedRealtime() - search) + " ms");
-
- FetchProfile fp = new FetchProfile();
- fp.add(UIDFolder.FetchProfileItem.UID);
- fp.add(FetchProfile.Item.FLAGS);
- ifolder.fetch(imessages, fp);
-
- long fetch = SystemClock.elapsedRealtime();
- Log.i(folder.name + " remote fetched=" + (SystemClock.elapsedRealtime() - fetch) + " ms");
-
- for (int i = 0; i < imessages.length && state.running(); i++)
- try {
- uids.remove(ifolder.getUID(imessages[i]));
- } catch (MessageRemovedException ex) {
- Log.w(folder.name, ex);
- } catch (Throwable ex) {
- Log.e(folder.name, ex);
- reportError(account, folder, ex);
- db.folder().setFolderError(folder.id, Helper.formatThrowable(ex, true));
- }
-
- if (uids.size() > 0) {
- ifolder.doCommand(new IMAPFolder.ProtocolCommand() {
- @Override
- public Object doCommand(IMAPProtocol protocol) {
- Log.i("Executing uid fetch count=" + uids.size());
- Response[] responses = protocol.command(
- "UID FETCH " + TextUtils.join(",", uids) + " (UID)", null);
-
- for (int i = 0; i < responses.length; i++) {
- if (responses[i] instanceof FetchResponse) {
- FetchResponse fr = (FetchResponse) responses[i];
- UID uid = fr.getItem(UID.class);
- if (uid != null)
- uids.remove(uid.uid);
- } else {
- if (responses[i].isOK())
- Log.i(folder.name + " response=" + responses[i]);
- else {
- Log.e(folder.name + " response=" + responses[i]);
- db.folder().setFolderError(folder.id, responses[i].toString());
- }
- }
- }
- return null;
- }
- });
-
- long getuid = SystemClock.elapsedRealtime();
- Log.i(folder.name + " remote uids=" + (SystemClock.elapsedRealtime() - getuid) + " ms");
- }
-
- // Delete local messages not at remote
- Log.i(folder.name + " delete=" + uids.size());
- for (Long uid : uids) {
- int count = db.message().deleteMessage(folder.id, uid);
- Log.i(folder.name + " delete local uid=" + uid + " count=" + count);
- }
-
- List rules = db.rule().getEnabledRules(folder.id);
-
- fp.add(FetchProfile.Item.ENVELOPE);
- // fp.add(FetchProfile.Item.FLAGS);
- fp.add(FetchProfile.Item.CONTENT_INFO); // body structure
- // fp.add(UIDFolder.FetchProfileItem.UID);
- fp.add(IMAPFolder.FetchProfileItem.HEADERS);
- // fp.add(IMAPFolder.FetchProfileItem.MESSAGE);
- fp.add(FetchProfile.Item.SIZE);
- fp.add(IMAPFolder.FetchProfileItem.INTERNALDATE);
-
- // Add/update local messages
- Long[] ids = new Long[imessages.length];
- Log.i(folder.name + " add=" + imessages.length);
- for (int i = imessages.length - 1; i >= 0 && state.running(); i -= SYNC_BATCH_SIZE) {
- int from = Math.max(0, i - SYNC_BATCH_SIZE + 1);
- Message[] isub = Arrays.copyOfRange(imessages, from, i + 1);
-
- // Full fetch new/changed messages only
- List full = new ArrayList<>();
- for (Message imessage : isub) {
- long uid = ifolder.getUID(imessage);
- EntityMessage message = db.message().getMessageByUid(folder.id, uid);
- if (message == null)
- full.add(imessage);
- }
- if (full.size() > 0) {
- long headers = SystemClock.elapsedRealtime();
- ifolder.fetch(full.toArray(new Message[0]), fp);
- Log.i(folder.name + " fetched headers=" + full.size() +
- " " + (SystemClock.elapsedRealtime() - headers) + " ms");
- }
-
- for (int j = isub.length - 1; j >= 0 && state.running(); j--)
- try {
- db.beginTransaction();
- EntityMessage message = synchronizeMessage(
- this,
- folder, ifolder, (IMAPMessage) isub[j],
- false,
- rules);
- ids[from + j] = message.id;
- db.setTransactionSuccessful();
- } catch (MessageRemovedException ex) {
- Log.w(folder.name, ex);
- } catch (FolderClosedException ex) {
- throw ex;
- } catch (IOException ex) {
- if (ex.getCause() instanceof MessagingException) {
- Log.w(folder.name, ex);
- db.folder().setFolderError(folder.id, Helper.formatThrowable(ex, true));
- } else
- throw ex;
- } catch (Throwable ex) {
- Log.e(folder.name, ex);
- db.folder().setFolderError(folder.id, Helper.formatThrowable(ex, true));
- } finally {
- db.endTransaction();
- // Reduce memory usage
- ((IMAPMessage) isub[j]).invalidateHeaders();
- }
- }
-
- // Delete not synchronized messages without uid
- db.message().deleteOrphans(folder.id);
-
- // Add local sent messages to remote sent folder
- if (EntityFolder.SENT.equals(folder.type)) {
- List orphans = db.message().getSentOrphans(folder.account);
- Log.i(folder.name + " sent orphans=" + orphans.size() + " account=" + folder.account);
- for (EntityMessage orphan : orphans) {
- Log.i(folder.name + " adding orphan id=" + orphan.id + " sent=" + new Date(orphan.sent));
- orphan.folder = folder.id;
- db.message().updateMessage(orphan);
- EntityOperation.queue(this, db, orphan, EntityOperation.ADD);
- }
- }
-
- if (download) {
- db.folder().setFolderSyncState(folder.id, "downloading");
-
- //fp.add(IMAPFolder.FetchProfileItem.MESSAGE);
-
- // Download messages/attachments
- Log.i(folder.name + " download=" + imessages.length);
- for (int i = imessages.length - 1; i >= 0 && state.running(); i -= DOWNLOAD_BATCH_SIZE) {
- int from = Math.max(0, i - DOWNLOAD_BATCH_SIZE + 1);
-
- Message[] isub = Arrays.copyOfRange(imessages, from, i + 1);
- // Fetch on demand
-
- for (int j = isub.length - 1; j >= 0 && state.running(); j--)
- try {
- db.beginTransaction();
- if (ids[from + j] != null)
- downloadMessage(
- this,
- folder, ifolder,
- (IMAPMessage) isub[j], ids[from + j]);
- db.setTransactionSuccessful();
- } catch (FolderClosedException ex) {
- throw ex;
- } catch (FolderClosedIOException ex) {
- throw ex;
- } catch (Throwable ex) {
- Log.e(folder.name, ex);
- } finally {
- db.endTransaction();
- // Free memory
- ((IMAPMessage) isub[j]).invalidateHeaders();
- }
- }
- }
-
- if (state.running)
- db.folder().setFolderInitialized(folder.id);
-
- db.folder().setFolderSync(folder.id, new Date().getTime());
- db.folder().setFolderError(folder.id, null);
-
- } finally {
- Log.i(folder.name + " end sync state=" + state);
- db.folder().setFolderSyncState(folder.id, null);
- }
- }
-
- static EntityMessage synchronizeMessage(
- Context context,
- EntityFolder folder, IMAPFolder ifolder, IMAPMessage imessage,
- boolean browsed,
- List rules) throws MessagingException, IOException {
- long uid = ifolder.getUID(imessage);
-
- if (imessage.isExpunged()) {
- Log.i(folder.name + " expunged uid=" + uid);
- throw new MessageRemovedException();
- }
- if (imessage.isSet(Flags.Flag.DELETED)) {
- Log.i(folder.name + " deleted uid=" + uid);
- throw new MessageRemovedException();
- }
-
- MessageHelper helper = new MessageHelper(imessage);
- boolean seen = helper.getSeen();
- boolean answered = helper.getAnsered();
- boolean flagged = helper.getFlagged();
- String flags = helper.getFlags();
- String[] keywords = helper.getKeywords();
- boolean filter = false;
-
- DB db = DB.getInstance(context);
-
- // Find message by uid (fast, no headers required)
- EntityMessage message = db.message().getMessageByUid(folder.id, uid);
-
- // Find message by Message-ID (slow, headers required)
- // - messages in inbox have same id as message sent to self
- // - messages in archive have same id as original
- if (message == null) {
- // Will fetch headers within database transaction
- String msgid = helper.getMessageID();
- Log.i(folder.name + " searching for " + msgid);
- for (EntityMessage dup : db.message().getMessageByMsgId(folder.account, msgid)) {
- EntityFolder dfolder = db.folder().getFolder(dup.folder);
- Log.i(folder.name + " found as id=" + dup.id + "/" + dup.uid +
- " folder=" + dfolder.type + ":" + dup.folder + "/" + folder.type + ":" + folder.id +
- " msgid=" + dup.msgid + " thread=" + dup.thread);
-
- if (dup.folder.equals(folder.id) ||
- (EntityFolder.OUTBOX.equals(dfolder.type) && EntityFolder.SENT.equals(folder.type))) {
- String thread = helper.getThreadId(uid);
- Log.i(folder.name + " found as id=" + dup.id +
- " uid=" + dup.uid + "/" + uid +
- " msgid=" + msgid + " thread=" + thread);
- dup.folder = folder.id; // outbox to sent
-
- if (dup.uid == null) {
- Log.i(folder.name + " set uid=" + uid);
- dup.uid = uid;
- filter = true;
- } else
- Log.w(folder.name + " changed uid=" + dup.uid + " -> " + uid);
-
- dup.msgid = msgid;
- dup.thread = thread;
- dup.error = null;
- db.message().updateMessage(dup);
- message = dup;
- }
- }
-
- if (message == null)
- filter = true;
- }
-
- if (message == null) {
- // Build list of addresses
- Address[] recipients = helper.getTo();
- Address[] senders = helper.getFrom();
- if (recipients == null)
- recipients = new Address[0];
- if (senders == null)
- senders = new Address[0];
- Address[] all = Arrays.copyOf(recipients, recipients.length + senders.length);
- System.arraycopy(senders, 0, all, recipients.length, senders.length);
-
- List emails = new ArrayList<>();
- for (Address address : all) {
- String to = ((InternetAddress) address).getAddress();
- if (!TextUtils.isEmpty(to)) {
- to = to.toLowerCase();
- emails.add(to);
- String canonical = Helper.canonicalAddress(to);
- if (!to.equals(canonical))
- emails.add(canonical);
- }
- }
- String delivered = helper.getDeliveredTo();
- if (!TextUtils.isEmpty(delivered)) {
- delivered = delivered.toLowerCase();
- emails.add(delivered);
- String canonical = Helper.canonicalAddress(delivered);
- if (!delivered.equals(canonical))
- emails.add(canonical);
- }
-
- // Search for identity
- EntityIdentity identity = null;
- for (String email : emails) {
- identity = db.identity().getIdentity(folder.account, email);
- if (identity != null)
- break;
- }
-
- message = new EntityMessage();
- message.account = folder.account;
- message.folder = folder.id;
- message.identity = (identity == null ? null : identity.id);
- message.uid = uid;
-
- message.msgid = helper.getMessageID();
- if (TextUtils.isEmpty(message.msgid))
- Log.w("No Message-ID id=" + message.id + " uid=" + message.uid);
-
- message.references = TextUtils.join(" ", helper.getReferences());
- message.inreplyto = helper.getInReplyTo();
- message.deliveredto = helper.getDeliveredTo();
- message.thread = helper.getThreadId(uid);
- message.sender = MessageHelper.getSortKey(helper.getFrom());
- message.from = helper.getFrom();
- message.to = helper.getTo();
- message.cc = helper.getCc();
- message.bcc = helper.getBcc();
- message.reply = helper.getReply();
- message.subject = helper.getSubject();
- message.size = helper.getSize();
- message.content = false;
- message.received = helper.getReceived();
- message.sent = helper.getSent();
- message.seen = seen;
- message.answered = answered;
- message.flagged = flagged;
- message.flags = flags;
- message.keywords = keywords;
- message.ui_seen = seen;
- message.ui_answered = answered;
- message.ui_flagged = flagged;
- message.ui_hide = false;
- message.ui_found = false;
- message.ui_ignored = seen;
- message.ui_browsed = browsed;
-
- Uri lookupUri = ContactInfo.getLookupUri(context, message.from);
- message.avatar = (lookupUri == null ? null : lookupUri.toString());
-
- // Check sender
- Address sender = helper.getSender();
- if (sender != null && senders.length > 0) {
- String[] f = ((InternetAddress) senders[0]).getAddress().split("@");
- String[] s = ((InternetAddress) sender).getAddress().split("@");
- if (f.length > 1 && s.length > 1) {
- if (!f[1].equals(s[1]))
- message.warning = context.getString(R.string.title_via, s[1]);
- }
- }
-
- message.id = db.message().insertMessage(message);
-
- Log.i(folder.name + " added id=" + message.id + " uid=" + message.uid);
-
- int sequence = 1;
- MessageHelper.MessageParts parts = helper.getMessageParts();
- for (EntityAttachment attachment : parts.getAttachments()) {
- Log.i(folder.name + " attachment seq=" + sequence +
- " name=" + attachment.name + " type=" + attachment.type +
- " cid=" + attachment.cid + " pgp=" + attachment.encryption);
- attachment.message = message.id;
- attachment.sequence = sequence++;
- attachment.id = db.attachment().insertAttachment(attachment);
- }
- } else {
- boolean update = false;
-
- if (!message.seen.equals(seen) || !message.seen.equals(message.ui_seen)) {
- update = true;
- message.seen = seen;
- message.ui_seen = seen;
- Log.i(folder.name + " updated id=" + message.id + " uid=" + message.uid + " seen=" + seen);
- }
-
- if (!message.answered.equals(answered) || !message.answered.equals(message.ui_answered)) {
- update = true;
- message.answered = answered;
- message.ui_answered = answered;
- Log.i(folder.name + " updated id=" + message.id + " uid=" + message.uid + " answered=" + answered);
- }
-
- if (!message.flagged.equals(flagged) || !message.flagged.equals(message.ui_flagged)) {
- update = true;
- message.flagged = flagged;
- message.ui_flagged = flagged;
- Log.i(folder.name + " updated id=" + message.id + " uid=" + message.uid + " flagged=" + flagged);
- }
-
- if (!Objects.equals(flags, message.flags)) {
- update = true;
- message.flags = flags;
- Log.i(folder.name + " updated id=" + message.id + " uid=" + message.uid + " flags=" + flags);
- }
-
- if (!Helper.equal(message.keywords, keywords)) {
- update = true;
- message.keywords = keywords;
- Log.i(folder.name + " updated id=" + message.id + " uid=" + message.uid +
- " keywords=" + TextUtils.join(" ", keywords));
- }
-
- if (message.ui_hide && db.operation().getOperationCount(folder.id, message.id) == 0) {
- update = true;
- message.ui_hide = false;
- Log.i(folder.name + " updated id=" + message.id + " uid=" + message.uid + " unhide");
- }
-
- if (message.ui_browsed) {
- update = true;
- message.ui_browsed = false;
- Log.i(folder.name + " updated id=" + message.id + " uid=" + message.uid + " unbrowse");
- }
-
- if (message.avatar == null) {
- Uri lookupUri = ContactInfo.getLookupUri(context, message.from);
- if (lookupUri != null) {
- update = true;
- message.avatar = lookupUri.toString();
- Log.i(folder.name + " updated id=" + message.id + " lookup=" + lookupUri);
- }
- }
-
- if (update)
- db.message().updateMessage(message);
- else
- Log.i(folder.name + " unchanged uid=" + uid);
- }
-
- if (!folder.isOutgoing() && !EntityFolder.ARCHIVE.equals(folder.type)) {
- Address[] senders = (message.reply != null ? message.reply : message.from);
- if (senders != null)
- for (Address sender : senders) {
- String email = ((InternetAddress) sender).getAddress();
- String name = ((InternetAddress) sender).getPersonal();
- List contacts = db.contact().getContacts(EntityContact.TYPE_FROM, email);
- if (contacts.size() == 0) {
- EntityContact contact = new EntityContact();
- contact.type = EntityContact.TYPE_FROM;
- contact.email = email;
- contact.name = name;
- contact.id = db.contact().insertContact(contact);
- Log.i("Inserted sender contact=" + contact);
- } else {
- EntityContact contact = contacts.get(0);
- if (name != null && !name.equals(contact.name)) {
- contact.name = name;
- db.contact().updateContact(contact);
- Log.i("Updated sender contact=" + contact);
- }
- }
- }
- }
-
- List fkeywords = new ArrayList<>(Arrays.asList(folder.keywords));
-
- for (String keyword : keywords)
- if (!fkeywords.contains(keyword)) {
- Log.i(folder.name + " adding keyword=" + keyword);
- fkeywords.add(keyword);
- }
-
- if (folder.keywords.length != fkeywords.size()) {
- Collections.sort(fkeywords);
- db.folder().setFolderKeywords(folder.id, DB.Converters.fromStringArray(fkeywords.toArray(new String[0])));
- }
-
- if (filter && Helper.isPro(context))
- try {
- for (EntityRule rule : rules)
- if (rule.matches(context, message, imessage)) {
- rule.execute(context, db, message);
- if (rule.stop)
- break;
- }
- } catch (Throwable ex) {
- Log.e(ex);
- db.message().setMessageError(message.id, Helper.formatThrowable(ex));
- }
-
- return message;
- }
-
- static void downloadMessage(
- Context context,
- EntityFolder folder, IMAPFolder ifolder,
- IMAPMessage imessage, long id) throws MessagingException, IOException {
- DB db = DB.getInstance(context);
- EntityMessage message = db.message().getMessage(id);
- if (message == null)
- return;
-
- SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
- long maxSize = prefs.getInt("download", 32768);
- if (maxSize == 0)
- maxSize = Long.MAX_VALUE;
-
- List attachments = db.attachment().getAttachments(message.id);
- MessageHelper helper = new MessageHelper(imessage);
- Boolean isMetered = Helper.isMetered(context, false);
- boolean metered = (isMetered == null || isMetered);
-
- boolean fetch = false;
- if (!message.content)
- if (!metered || (message.size != null && message.size < maxSize))
- fetch = true;
-
- if (!fetch)
- for (EntityAttachment attachment : attachments)
- if (!attachment.available)
- if (!metered || (attachment.size != null && attachment.size < maxSize)) {
- fetch = true;
- break;
- }
-
- if (fetch) {
- Log.i(folder.name + " fetching message id=" + message.id);
- FetchProfile fp = new FetchProfile();
- fp.add(FetchProfile.Item.ENVELOPE);
- fp.add(FetchProfile.Item.FLAGS);
- fp.add(FetchProfile.Item.CONTENT_INFO); // body structure
- fp.add(UIDFolder.FetchProfileItem.UID);
- fp.add(IMAPFolder.FetchProfileItem.HEADERS);
- fp.add(IMAPFolder.FetchProfileItem.MESSAGE);
- fp.add(FetchProfile.Item.SIZE);
- fp.add(IMAPFolder.FetchProfileItem.INTERNALDATE);
- ifolder.fetch(new Message[]{imessage}, fp);
-
- MessageHelper.MessageParts parts = helper.getMessageParts();
-
- if (!message.content) {
- if (!metered || (message.size != null && message.size < maxSize)) {
- String body = parts.getHtml(context);
- Helper.writeText(EntityMessage.getFile(context, message.id), body);
- db.message().setMessageContent(message.id, true, HtmlHelper.getPreview(body));
- db.message().setMessageWarning(message.id, parts.getWarnings(message.warning));
- Log.i(folder.name + " downloaded message id=" + message.id + " size=" + message.size);
- }
- }
-
- for (EntityAttachment attachment : attachments)
- if (!attachment.available)
- if (!metered || (attachment.size != null && attachment.size < maxSize))
- if (!parts.downloadAttachment(context, db, attachment.id, attachment.sequence))
- break;
- }
- }
-
private class ServiceManager extends ConnectivityManager.NetworkCallback {
- private ServiceState state;
+ private Core.State state;
private boolean started = false;
private int queued = 0;
private long lastLost = 0;
@@ -2857,12 +1298,12 @@ public class ServiceSynchronize extends LifecycleService {
private void start() {
EntityLog.log(ServiceSynchronize.this, "Main start");
- state = new ServiceState();
+ state = new Core.State();
state.runnable(new Runnable() {
PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
PowerManager.WakeLock wl = pm.newWakeLock(
PowerManager.PARTIAL_WAKE_LOCK, BuildConfig.APPLICATION_ID + ":main");
- private List threadState = new ArrayList<>();
+ private List threadState = new ArrayList<>();
@Override
public void run() {
@@ -2892,7 +1333,7 @@ public class ServiceSynchronize extends LifecycleService {
account.deleteNotificationChannel(ServiceSynchronize.this);
Log.i(account.host + "/" + account.user + " run");
- final ServiceState astate = new ServiceState();
+ final Core.State astate = new Core.State();
astate.runnable(new Runnable() {
@Override
public void run() {
@@ -2921,9 +1362,9 @@ public class ServiceSynchronize extends LifecycleService {
}
// Stop monitoring accounts
- for (ServiceState astate : threadState)
+ for (Core.State astate : threadState)
astate.stop();
- for (ServiceState astate : threadState)
+ for (Core.State astate : threadState)
astate.join();
threadState.clear();
@@ -3091,96 +1532,4 @@ public class ServiceSynchronize extends LifecycleService {
.setAction("reload")
.putExtra("reason", reason));
}
-
- public static void sync(Context context, long folder) {
- ContextCompat.startForegroundService(context,
- new Intent(context, ServiceSynchronize.class)
- .setAction("synchronize:" + folder));
- }
-
- private class ServiceState {
- private Thread thread;
- private Semaphore semaphore = new Semaphore(0);
- private boolean running = true;
-
- void runnable(Runnable runnable, String name) {
- thread = new Thread(runnable, name);
- thread.setPriority(THREAD_PRIORITY_BACKGROUND);
- }
-
- void release() {
- semaphore.release();
- yield();
- }
-
- void acquire() throws InterruptedException {
- semaphore.acquire();
- }
-
- boolean acquire(long milliseconds) throws InterruptedException {
- return semaphore.tryAcquire(milliseconds, TimeUnit.MILLISECONDS);
- }
-
- void error() {
- thread.interrupt();
- yield();
- }
-
- private void yield() {
- try {
- // Give interrupted thread some time to acquire wake lock
- Thread.sleep(YIELD_DURATION);
- } catch (InterruptedException ignored) {
- }
- }
-
- void start() {
- thread.start();
- }
-
- void stop() {
- running = false;
- semaphore.release();
- }
-
- void join() {
- join(thread);
- }
-
- boolean running() {
- return running;
- }
-
- void join(Thread thread) {
- boolean joined = false;
- while (!joined)
- try {
- Log.i("Joining " + thread.getName());
- thread.join();
- joined = true;
- Log.i("Joined " + thread.getName());
- } catch (InterruptedException ex) {
- Log.w(thread.getName() + " join " + ex.toString());
- }
- }
-
- @NonNull
- @Override
- public String toString() {
- return "[running=" + running + "]";
- }
- }
-
- private class AlertException extends Throwable {
- private String alert;
-
- AlertException(String alert) {
- this.alert = alert;
- }
-
- @Override
- public String getMessage() {
- return alert;
- }
- }
}
diff --git a/app/src/main/java/eu/faircode/email/ServiceUI.java b/app/src/main/java/eu/faircode/email/ServiceUI.java
new file mode 100644
index 0000000000..815cea3286
--- /dev/null
+++ b/app/src/main/java/eu/faircode/email/ServiceUI.java
@@ -0,0 +1,294 @@
+package eu.faircode.email;
+
+import android.app.IntentService;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import android.preference.PreferenceManager;
+
+import com.sun.mail.imap.IMAPFolder;
+
+import java.util.Properties;
+
+import javax.mail.Folder;
+import javax.mail.MessagingException;
+import javax.mail.Session;
+import javax.mail.Store;
+
+import androidx.annotation.Nullable;
+
+public class ServiceUI extends IntentService {
+ static final int PI_WHY = 1;
+ static final int PI_SUMMARY = 2;
+ static final int PI_CLEAR = 3;
+ static final int PI_SEEN = 4;
+ static final int PI_ARCHIVE = 5;
+ static final int PI_TRASH = 6;
+ static final int PI_IGNORED = 7;
+ static final int PI_SNOOZED = 8;
+
+ public ServiceUI() {
+ this(ServiceUI.class.getName());
+ }
+
+ public ServiceUI(String name) {
+ super(name);
+ }
+
+ @Override
+ protected void onHandleIntent(@Nullable Intent intent) {
+ if (intent == null)
+ return;
+
+ // TODO: wakelock?
+
+ String action = intent.getAction();
+ if (action == null)
+ return;
+
+ String[] parts = action.split(":");
+ long id = (parts.length > 1 ? Long.parseLong(parts[1]) : -1);
+
+ switch (parts[0]) {
+ case "why":
+ onWhy();
+ break;
+
+ case "summary":
+ onSummary();
+ break;
+
+ case "clear":
+ onClear();
+ break;
+
+ case "seen":
+ onSeen(id);
+ break;
+
+ case "archive":
+ onArchive(id);
+ break;
+
+ case "trash":
+ onTrash(id);
+ break;
+
+ case "ignore":
+ onIgnore(id);
+ break;
+
+ case "snooze":
+ onSnooze(id);
+ break;
+
+ case "synchronize":
+ // AlarmManager.RTC_WAKEUP
+ onSyncOndemand(id);
+ break;
+
+ default:
+ Log.w("Unknown action: " + parts[0]);
+ }
+ }
+
+ private void onWhy() {
+ Intent why = new Intent(Intent.ACTION_VIEW);
+ why.setData(Uri.parse(Helper.FAQ_URI + "#user-content-faq2"));
+ why.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+
+ PackageManager pm = getPackageManager();
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
+ if (prefs.getBoolean("why", false) || why.resolveActivity(pm) == null) {
+ Intent view = new Intent(this, ActivityView.class);
+ view.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ startActivity(view);
+ } else {
+ prefs.edit().putBoolean("why", true).apply();
+ startActivity(why);
+ }
+ }
+
+ private void onSummary() {
+ DB.getInstance(this).message().ignoreAll();
+
+ Intent view = new Intent(this, ActivityView.class);
+ view.setAction("unified");
+ view.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ startActivity(view);
+ }
+
+ private void onClear() {
+ DB.getInstance(this).message().ignoreAll();
+ }
+
+ private void onSeen(long id) {
+ DB db = DB.getInstance(this);
+ try {
+ db.beginTransaction();
+
+ EntityMessage message = db.message().getMessage(id);
+ if (message != null)
+ EntityOperation.queue(this, db, message, EntityOperation.SEEN, true);
+
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
+ }
+ }
+
+ private void onArchive(long id) {
+ DB db = DB.getInstance(this);
+ try {
+ db.beginTransaction();
+
+ EntityMessage message = db.message().getMessage(id);
+ if (message != null) {
+ EntityFolder archive = db.folder().getFolderByType(message.account, EntityFolder.ARCHIVE);
+ if (archive == null)
+ archive = db.folder().getFolderByType(message.account, EntityFolder.TRASH);
+ if (archive != null)
+ EntityOperation.queue(this, db, message, EntityOperation.MOVE, archive.id);
+ }
+
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
+ }
+ }
+
+ private void onTrash(long id) {
+ DB db = DB.getInstance(this);
+ try {
+ db.beginTransaction();
+
+ EntityMessage message = db.message().getMessage(id);
+ if (message != null) {
+ EntityFolder trash = db.folder().getFolderByType(message.account, EntityFolder.TRASH);
+ if (trash != null)
+ EntityOperation.queue(this, db, message, EntityOperation.MOVE, trash.id);
+ }
+
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
+ }
+ }
+
+ private void onIgnore(long id) {
+ DB db = DB.getInstance(this);
+ try {
+ db.beginTransaction();
+
+ EntityMessage message = db.message().getMessage(id);
+ if (message != null)
+ db.message().setMessageUiIgnored(message.id, true);
+
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
+ }
+ }
+
+ private void onSnooze(long id) {
+ DB db = DB.getInstance(this);
+ try {
+ db.beginTransaction();
+
+ EntityMessage message = db.message().getMessage(id);
+ if (message != null) {
+ db.message().setMessageSnoozed(message.id, null);
+
+ EntityFolder folder = db.folder().getFolder(message.folder);
+ if (EntityFolder.OUTBOX.equals(folder.type)) {
+ Log.i("Delayed send id=" + message.id);
+ EntityOperation.queue(
+ this, db, message, EntityOperation.SEND);
+ } else {
+ EntityOperation.queue(
+ this, db, message, EntityOperation.SEEN, false);
+ db.message().setMessageUiIgnored(message.id, false);
+ }
+ }
+
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
+ }
+ }
+
+ private void onSyncOndemand(long fid) {
+ Log.i("Synchronize on demand folder=" + fid);
+
+ DB db = DB.getInstance(this);
+ EntityFolder folder = null;
+ EntityAccount account = null;
+
+ Store istore = null;
+ try {
+ folder = db.folder().getFolder(fid);
+ account = db.account().getAccount(folder.account);
+
+ // Create session
+ Properties props = MessageHelper.getSessionProperties(account.auth_type, account.realm, account.insecure);
+ final Session isession = Session.getInstance(props, null);
+ isession.setDebug(true);
+
+ // Connect account
+ Log.i(account.name + " connecting");
+ db.account().setAccountState(account.id, "connecting");
+ istore = isession.getStore(account.getProtocol());
+ Helper.connect(this, istore, account);
+ db.account().setAccountState(account.id, "connected");
+ Log.i(account.name + " connected");
+
+ // Synchronize folders
+ Core.synchronizeFolders(this, account, istore, new Core.State());
+
+ // Connect folder
+ Log.i(folder.name + " connecting");
+ db.folder().setFolderState(folder.id, "connecting");
+ Folder ifolder = istore.getFolder(folder.name);
+ ifolder.open(Folder.READ_WRITE);
+ db.folder().setFolderState(folder.id, "connected");
+ db.folder().setFolderError(folder.id, null);
+ Log.i(folder.name + " connected");
+
+ // Process operations
+ Core.processOperations(this, account, folder, isession, istore, ifolder, new Core.State());
+
+ // Synchronize messages
+ Core.synchronizeMessages(this, account, folder, (IMAPFolder) ifolder, folder.getSyncArgs(), new Core.State());
+
+ } catch (Throwable ex) {
+ Log.w(ex);
+ Core.reportError(this, account, folder, ex);
+ db.account().setAccountError(account.id, Helper.formatThrowable(ex));
+ db.folder().setFolderError(folder.id, Helper.formatThrowable(ex, false));
+ } finally {
+ if (istore != null) {
+ Log.i(account.name + " closing");
+ db.account().setAccountState(account.id, "closing");
+ db.folder().setFolderState(folder.id, "closing");
+
+ try {
+ istore.close();
+ } catch (MessagingException ex) {
+ Log.e(ex);
+ }
+
+ db.account().setAccountState(account.id, null);
+ db.folder().setFolderState(folder.id, null);
+ Log.i(account.name + " closed");
+ }
+ }
+ }
+
+ public static void sync(Context context, long folder) {
+ context.startService(
+ new Intent(context, ServiceUI.class)
+ .setAction("synchronize:" + folder));
+ }
+}
diff --git a/app/src/main/java/eu/faircode/email/ViewModelBrowse.java b/app/src/main/java/eu/faircode/email/ViewModelBrowse.java
index be6216aafc..ff7a076544 100644
--- a/app/src/main/java/eu/faircode/email/ViewModelBrowse.java
+++ b/app/src/main/java/eu/faircode/email/ViewModelBrowse.java
@@ -306,7 +306,7 @@ public class ViewModelBrowse extends ViewModel {
Log.i("Boundary sync uid=" + uid);
EntityMessage message = db.message().getMessageByUid(folder.id, uid);
if (message == null) {
- message = ServiceSynchronize.synchronizeMessage(state.context,
+ message = Core.synchronizeMessage(state.context,
folder, state.ifolder, (IMAPMessage) isub[j],
true,
new ArrayList());
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 243fe4feb2..93c5f90374 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -256,7 +256,6 @@
This provider does not support UIDPLUS
This provider does not support UTF-8
Synchronization errors since %1$s
- Synchronization is disabled
Synchronization will be performed on the next account connection
A drafts folder is required to send messages
Delete this account permanently?