diff --git a/app/src/main/java/eu/faircode/email/Core.java.orig b/app/src/main/java/eu/faircode/email/Core.java.orig
deleted file mode 100644
index e8a8b93ca9..0000000000
--- a/app/src/main/java/eu/faircode/email/Core.java.orig
+++ /dev/null
@@ -1,6478 +0,0 @@
-package eu.faircode.email;
-
-/*
- This file is part of FairEmail.
-
- FairEmail is free software: you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- FairEmail is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License
- along with FairEmail. If not, see .
-
- Copyright 2018-2023 by Marcel Bokhorst (M66B)
-*/
-
-import static android.os.Process.THREAD_PRIORITY_BACKGROUND;
-import static androidx.core.app.NotificationCompat.DEFAULT_LIGHTS;
-import static androidx.core.app.NotificationCompat.DEFAULT_SOUND;
-import static javax.mail.Folder.READ_WRITE;
-
-import android.app.AlarmManager;
-import android.app.Notification;
-import android.app.NotificationChannel;
-import android.app.NotificationManager;
-import android.app.PendingIntent;
-import android.content.Context;
-import android.content.Intent;
-import android.content.SharedPreferences;
-import android.database.sqlite.SQLiteConstraintException;
-import android.net.Uri;
-import android.os.Build;
-import android.os.Bundle;
-import android.os.OperationCanceledException;
-import android.os.PowerManager;
-import android.os.SystemClock;
-import android.service.notification.StatusBarNotification;
-import android.text.Html;
-import android.text.TextUtils;
-import android.util.Pair;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.core.app.NotificationCompat;
-import androidx.core.app.Person;
-import androidx.core.app.RemoteInput;
-import androidx.core.graphics.drawable.IconCompat;
-import androidx.localbroadcastmanager.content.LocalBroadcastManager;
-import androidx.preference.PreferenceManager;
-
-import com.sun.mail.gimap.GmailFolder;
-import com.sun.mail.gimap.GmailMessage;
-import com.sun.mail.iap.BadCommandException;
-import com.sun.mail.iap.CommandFailedException;
-import com.sun.mail.iap.ConnectionException;
-import com.sun.mail.iap.ProtocolException;
-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.FLAGS;
-import com.sun.mail.imap.protocol.FetchResponse;
-import com.sun.mail.imap.protocol.IMAPProtocol;
-import com.sun.mail.imap.protocol.Status;
-import com.sun.mail.imap.protocol.UID;
-import com.sun.mail.imap.protocol.UIDSet;
-import com.sun.mail.pop3.POP3Folder;
-import com.sun.mail.pop3.POP3Message;
-import com.sun.mail.pop3.POP3Store;
-
-import org.json.JSONArray;
-import org.json.JSONException;
-
-import java.io.BufferedInputStream;
-import java.io.BufferedOutputStream;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.io.UnsupportedEncodingException;
-import java.net.SocketException;
-import java.net.UnknownHostException;
-import java.nio.charset.StandardCharsets;
-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.Comparator;
-import java.util.Date;
-import java.util.HashMap;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Locale;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Properties;
-import java.util.concurrent.Semaphore;
-import java.util.concurrent.TimeUnit;
-
-import javax.mail.Address;
-import javax.mail.FetchProfile;
-import javax.mail.Flags;
-import javax.mail.Folder;
-import javax.mail.FolderClosedException;
-import javax.mail.FolderNotFoundException;
-import javax.mail.Header;
-import javax.mail.Message;
-import javax.mail.MessageRemovedException;
-import javax.mail.MessagingException;
-import javax.mail.Session;
-import javax.mail.Store;
-import javax.mail.StoreClosedException;
-import javax.mail.UIDFolder;
-import javax.mail.internet.AddressException;
-import javax.mail.internet.InternetAddress;
-import javax.mail.internet.MimeMessage;
-import javax.mail.search.AndTerm;
-import javax.mail.search.ComparisonTerm;
-import javax.mail.search.FlagTerm;
-import javax.mail.search.HeaderTerm;
-import javax.mail.search.MessageIDTerm;
-import javax.mail.search.OrTerm;
-import javax.mail.search.ReceivedDateTerm;
-import javax.mail.search.SearchTerm;
-import javax.mail.search.SentDateTerm;
-
-import me.leolin.shortcutbadger.ShortcutBadger;
-
-class Core {
- static final int DEFAULT_CHUNK_SIZE = 50;
-
- private static final int MAX_NOTIFICATION_DISPLAY = 10; // per group
- private static final int MAX_NOTIFICATION_COUNT = 100; // per group
- private static final long SCREEN_ON_DURATION = 3000L; // milliseconds
- private static final int SYNC_BATCH_SIZE = 20;
- private static final int DOWNLOAD_BATCH_SIZE = 20;
- private static final long YIELD_DURATION = 200L; // milliseconds
- private static final long JOIN_WAIT_ALIVE = 5 * 60 * 1000L; // milliseconds
- private static final long JOIN_WAIT_INTERRUPT = 1 * 60 * 1000L; // milliseconds
- private static final long FUTURE_RECEIVED = 30 * 24 * 3600 * 1000L; // milliseconds
- private static final int LOCAL_RETRY_MAX = 2;
- private static final long LOCAL_RETRY_DELAY = 5 * 1000L; // milliseconds
- private static final int TOTAL_RETRY_MAX = LOCAL_RETRY_MAX * 5;
- private static final int MAX_PREVIEW = 5000; // characters
- private static final long EXISTS_RETRY_DELAY = 20 * 1000L; // milliseconds
- private static final int FIND_RETRY_COUNT = 3; // times
- private static final long FIND_RETRY_DELAY = 5 * 1000L; // milliseconds
-
- private static final Map> accountIdentities = new HashMap<>();
-
- static void clearIdentities() {
- synchronized (accountIdentities) {
- accountIdentities.clear();
- }
- }
-
- static List getIdentities(long account, Context context) {
- synchronized (accountIdentities) {
- if (!accountIdentities.containsKey(account))
- accountIdentities.put(account,
- DB.getInstance(context).identity().getSynchronizingIdentities(account));
- return accountIdentities.get(account);
- }
- }
-
- static void processOperations(
- Context context,
- EntityAccount account, EntityFolder folder, List ops,
- EmailService iservice, Folder ifolder,
- State state, long serial)
- throws JSONException, FolderClosedException {
- try {
- Log.i(folder.name + " start process");
-
- Store istore = iservice.getStore();
- DB db = DB.getInstance(context);
-
- SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
- int chunk_size = prefs.getInt("chunk_size", DEFAULT_CHUNK_SIZE);
-
- NotificationManager nm = Helper.getSystemService(context, NotificationManager.class);
-
- int retry = 0;
- boolean group = true;
- Log.i(folder.name + " executing serial=" + serial + " operations=" + ops.size());
- while (retry < LOCAL_RETRY_MAX && ops.size() > 0 &&
- state.isRunning() &&
- state.getSerial() == serial) {
- TupleOperationEx op = ops.get(0);
-
- try {
- Log.i(folder.name +
- " start op=" + op.id + "/" + op.name +
- " folder=" + op.folder +
- " msg=" + op.message +
- " args=" + op.args +
- " group=" + group +
- " retry=" + retry);
-
- if (EntityOperation.HEADERS.equals(op.name) ||
- EntityOperation.RAW.equals(op.name))
- nm.cancel(op.name + ":" + op.message, NotificationHelper.NOTIFICATION_TAGGED);
-
- if (!Objects.equals(folder.id, op.folder))
- throw new IllegalArgumentException("Invalid folder=" + folder.id + "/" + op.folder);
-
- if (account.protocol == EntityAccount.TYPE_IMAP && !folder.local && ifolder != null) {
- try {
- ((IMAPFolder) ifolder).doCommand(new IMAPFolder.ProtocolCommand() {
- @Override
- public Object doCommand(IMAPProtocol protocol) throws ProtocolException {
- long ago = System.currentTimeMillis() - protocol.getTimestamp();
- if (ago > 20000)
- protocol.noop();
- return null;
- }
- });
- } catch (MessagingException ex) {
- throw new FolderClosedException(ifolder, account.name + "/" + folder.name + " unexpectedly closed", ex);
- }
- }
-
- if (account.protocol == EntityAccount.TYPE_POP &&
- EntityFolder.INBOX.equals(folder.type) &&
- ifolder != null && !ifolder.isOpen())
- throw new FolderClosedException(ifolder, account.name + "/" + folder.name + " unexpectedly closed");
-
- // 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);
- Map similar = new HashMap<>();
-
- try {
- // Operations should use database transaction when needed
-
- if (message == null &&
- !EntityOperation.FETCH.equals(op.name) &&
- !EntityOperation.REPORT.equals(op.name) &&
- !EntityOperation.SYNC.equals(op.name) &&
- !EntityOperation.SUBSCRIBE.equals(op.name) &&
- !EntityOperation.PURGE.equals(op.name) &&
- !EntityOperation.EXPUNGE.equals(op.name))
- throw new MessageRemovedException();
-
- // Process similar operations
- boolean skip = false;
- for (int j = 1; j < ops.size(); j++) {
- TupleOperationEx next = ops.get(j);
-
- switch (op.name) {
- case EntityOperation.SEEN:
- case EntityOperation.FLAG:
- if (group &&
- message.uid != null &&
- op.name.equals(next.name) &&
- account.protocol == EntityAccount.TYPE_IMAP) {
- JSONArray jnext = new JSONArray(next.args);
- // Same flag
- if (jargs.getBoolean(0) == jnext.getBoolean(0)) {
- EntityMessage m = db.message().getMessage(next.message);
- if (m != null && m.uid != null)
- similar.put(next, m);
- }
- }
- break;
-
- case EntityOperation.ADD:
- // Same message
- if (Objects.equals(op.message, next.message) &&
- (EntityOperation.ADD.equals(next.name) ||
- EntityOperation.DELETE.equals(next.name)))
- skip = true;
- break;
-
- case EntityOperation.FETCH:
- if (EntityOperation.FETCH.equals(next.name)) {
- JSONArray jnext = new JSONArray(next.args);
- // Same uid, invalidate, delete flag
- if (jargs.getLong(0) == jnext.getLong(0) &&
- jargs.optBoolean(1) == jnext.optBoolean(1) &&
- jargs.optBoolean(2) == jnext.optBoolean(2))
- skip = true;
- }
- break;
-
- case EntityOperation.DOWNLOAD:
- if (EntityOperation.DOWNLOAD.equals(next.name)) {
- JSONArray jnext = new JSONArray(next.args);
- // Same uid
- if (jargs.getLong(0) == jnext.getLong(0))
- skip = true;
- }
- break;
-
- case EntityOperation.MOVE:
- if (group &&
- message.uid != null &&
- op.name.equals(next.name) &&
- account.protocol == EntityAccount.TYPE_IMAP) {
- JSONArray jnext = new JSONArray(next.args);
- // Same target
- if (jargs.getLong(0) == jnext.getLong(0) &&
- jargs.optBoolean(4) == jnext.optBoolean(4)) {
- EntityMessage m = db.message().getMessage(next.message);
- if (m != null && m.uid != null)
- similar.put(next, m);
- }
- }
- if (group &&
- op.name.equals(next.name) &&
- account.protocol == EntityAccount.TYPE_POP) {
- JSONArray jnext = new JSONArray(next.args);
- // Same target
- if (jargs.getLong(0) == jnext.getLong(0)) {
- EntityMessage m = db.message().getMessage(next.message);
- if (m != null)
- similar.put(next, m);
- }
- }
- break;
-
- case EntityOperation.DELETE:
- if (group &&
- message.uid != null &&
- op.name.equals(next.name) &&
- account.protocol == EntityAccount.TYPE_IMAP) {
- EntityMessage m = db.message().getMessage(next.message);
- if (m != null &&
- m.uid != null && m.ui_deleted == message.ui_deleted)
- similar.put(next, m);
- }
- if (group &&
- op.name.equals(next.name) &&
- account.protocol == EntityAccount.TYPE_POP) {
- EntityMessage m = db.message().getMessage(next.message);
- if (m != null)
- similar.put(next, m);
- }
- break;
- }
-
- if (similar.size() >= chunk_size)
- break;
- }
-
- if (skip) {
- Log.i(folder.name +
- " skipping op=" + op.id + "/" + op.name +
- " msg=" + op.message + " args=" + op.args);
- db.operation().deleteOperation(op.id);
- ops.remove(op);
- continue;
- }
-
- List sids = new ArrayList<>();
- for (TupleOperationEx s : similar.keySet())
- sids.add(s.id);
-
- if (similar.size() > 0)
- Log.i(folder.name + " similar=" + TextUtils.join(",", sids));
-
- op.tries++;
-
- // Leave crumb
- Map crumb = new HashMap<>();
- crumb.put("name", op.name);
- crumb.put("args", op.args);
- crumb.put("account", op.account + ":" + account.protocol);
- crumb.put("folder", op.folder + ":" + folder.type);
- if (op.message != null)
- crumb.put("message", Long.toString(op.message));
- crumb.put("tries", Integer.toString(op.tries));
- crumb.put("similar", TextUtils.join(",", sids));
- crumb.put("thread", Thread.currentThread().getName() + ":" + Thread.currentThread().getId());
- Log.breadcrumb("start operation", crumb);
-
- try {
- db.beginTransaction();
-
- db.operation().setOperationError(op.id, null);
-
- if (message != null)
- db.message().setMessageError(message.id, null);
-
- db.operation().setOperationState(op.id, "executing");
- for (TupleOperationEx s : similar.keySet())
- db.operation().setOperationState(s.id, "executing");
-
- db.setTransactionSuccessful();
- } finally {
- db.endTransaction();
- }
-
- if (istore instanceof POP3Store) {
- List messages = new ArrayList<>();
- messages.add(message);
- messages.addAll(similar.values());
-
- switch (op.name) {
- case EntityOperation.DELETE:
- onDelete(context, jargs, account, folder, messages, (POP3Folder) ifolder, (POP3Store) istore, state);
- break;
-
- case EntityOperation.RAW:
- onRaw(context, jargs, folder, message, (POP3Store) istore, (POP3Folder) ifolder);
- break;
-
- case EntityOperation.BODY:
- onBody(context, jargs, folder, message, (POP3Folder) ifolder, (POP3Store) istore);
- break;
-
- case EntityOperation.ATTACHMENT:
- onAttachment(context, jargs, folder, message, (POP3Folder) ifolder, (POP3Store) istore);
- break;
-
- case EntityOperation.SYNC:
- Helper.gc();
- onSynchronizeMessages(context, jargs, account, folder, (POP3Folder) ifolder, (POP3Store) istore, state);
- Helper.gc();
- break;
-
- case EntityOperation.PURGE:
- onPurgeFolder(context, folder);
- break;
-
- default:
- Log.w(folder.name + " ignored=" + op.name);
- }
- } else {
- List messages = new ArrayList<>();
- messages.add(message);
- if (similar.size() == 0)
- ensureUid(context, account, folder, message, op, (IMAPFolder) ifolder);
- else
- messages.addAll(similar.values());
-
- switch (op.name) {
- case EntityOperation.SEEN:
- onSetFlag(context, jargs, folder, messages, (IMAPFolder) ifolder, Flags.Flag.SEEN);
- break;
-
- case EntityOperation.FLAG:
- onSetFlag(context, jargs, folder, messages, (IMAPFolder) ifolder, Flags.Flag.FLAGGED);
- break;
-
- case EntityOperation.ANSWERED:
- onAnswered(context, jargs, folder, message, (IMAPFolder) ifolder);
- break;
-
- case EntityOperation.KEYWORD:
- onKeyword(context, jargs, folder, message, (IMAPFolder) ifolder);
- break;
-
- case EntityOperation.LABEL:
- onLabel(context, jargs, folder, message, (IMAPStore) istore, (IMAPFolder) ifolder, state);
- break;
-
- case EntityOperation.ADD:
- onAdd(context, jargs, account, folder, message, (IMAPStore) istore, (IMAPFolder) ifolder, state);
- break;
-
- case EntityOperation.MOVE:
- onMove(context, jargs, false, account, folder, messages, (IMAPStore) istore, (IMAPFolder) ifolder, state);
- break;
-
- case EntityOperation.COPY:
- onMove(context, jargs, true, account, folder, Arrays.asList(message), (IMAPStore) istore, (IMAPFolder) ifolder, state);
- break;
-
- case EntityOperation.FETCH:
- onFetch(context, jargs, folder, (IMAPStore) istore, (IMAPFolder) ifolder, state);
- break;
-
- case EntityOperation.DELETE:
- onDelete(context, jargs, account, folder, messages, (IMAPFolder) ifolder);
- break;
-
- case EntityOperation.HEADERS:
- onHeaders(context, jargs, folder, message, (IMAPFolder) ifolder);
- break;
-
- case EntityOperation.RAW:
- onRaw(context, jargs, folder, message, (IMAPFolder) ifolder);
- break;
-
- case EntityOperation.BODY:
- onBody(context, jargs, folder, message, (IMAPFolder) ifolder);
- break;
-
- case EntityOperation.ATTACHMENT:
- onAttachment(context, jargs, folder, message, op, (IMAPFolder) ifolder);
- break;
-
- case EntityOperation.EXISTS:
- onExists(context, jargs, account, folder, message, op, (IMAPFolder) ifolder);
- break;
-
- case EntityOperation.REPORT:
- onReport(context, jargs, folder, (IMAPStore) istore, (IMAPFolder) ifolder, state);
- break;
-
- case EntityOperation.SYNC:
- Helper.gc();
- onSynchronizeMessages(context, jargs, account, folder, (IMAPStore) istore, (IMAPFolder) ifolder, state);
- Helper.gc();
- break;
-
- case EntityOperation.SUBSCRIBE:
- onSubscribeFolder(context, jargs, folder, (IMAPFolder) ifolder);
- break;
-
- case EntityOperation.PURGE:
- onPurgeFolder(context, jargs, account, folder, (IMAPFolder) ifolder);
- break;
-
- case EntityOperation.EXPUNGE:
- onExpungeFolder(context, jargs, folder, (IMAPFolder) ifolder);
- break;
-
- case EntityOperation.RULE:
- onRule(context, jargs, message);
- break;
-
- case EntityOperation.DOWNLOAD:
- onDownload(context, jargs, account, folder, message, (IMAPStore) istore, (IMAPFolder) ifolder, state);
- break;
-
- default:
- throw new IllegalArgumentException("Unknown operation=" + op.name);
- }
- }
-
- crumb.put("thread", Thread.currentThread().getName() + ":" + Thread.currentThread().getId());
- Log.breadcrumb("end operation", crumb);
-
- // Operation succeeded
- try {
- db.beginTransaction();
-
- db.operation().deleteOperation(op.id);
- for (TupleOperationEx s : similar.keySet())
- db.operation().deleteOperation(s.id);
-
- db.setTransactionSuccessful();
- } finally {
- db.endTransaction();
- }
-
- ops.remove(op);
- for (TupleOperationEx s : similar.keySet())
- ops.remove(s);
- } catch (Throwable ex) {
- iservice.dump(account.name + "/" + folder.name);
- if (ex instanceof OperationCanceledException ||
- (ex instanceof IllegalArgumentException &&
- ex.getMessage() != null &&
- ex.getMessage().startsWith("Message not found for")))
- Log.i(folder.name, ex);
- else
- Log.e(folder.name, ex);
-
- EntityLog.log(context, folder.name +
- " op=" + op.name +
- " try=" + op.tries +
- " " + ex + "\n" + android.util.Log.getStackTraceString(ex));
-
- try {
- db.beginTransaction();
-
- db.operation().setOperationTries(op.id, op.tries);
-
- op.error = Log.formatThrowable(ex);
- db.operation().setOperationError(op.id, op.error);
-
- if (message != null &&
- !EntityOperation.FETCH.equals(op.name) &&
- !EntityOperation.ATTACHMENT.equals(op.name) &&
- !(ex instanceof IllegalArgumentException))
- db.message().setMessageError(message.id, op.error);
-
- db.setTransactionSuccessful();
- } finally {
- db.endTransaction();
- }
-
- if (similar.size() > 0 && op.tries < TOTAL_RETRY_MAX) {
- // Retry individually
- group = false;
- // Finally will reset state
- continue;
- }
-
- long attachments = (op.message == null ? 0 : db.attachment().countAttachments(op.message));
-
- if (op.tries >= TOTAL_RETRY_MAX ||
- ex instanceof JSONException ||
- ex instanceof OutOfMemoryError ||
- ex instanceof FileNotFoundException ||
- ex instanceof FolderNotFoundException ||
- ex instanceof IllegalArgumentException ||
- ex instanceof SQLiteConstraintException ||
- ex instanceof OperationCanceledException ||
- (!ConnectionHelper.isIoError(ex) &&
- (ex.getCause() instanceof BadCommandException ||
- ex.getCause() instanceof CommandFailedException /* NO */) &&
- // https://sebastian.marsching.com/wiki/Network/Zimbra#Mailbox_Selected_READ-ONLY_Error_in_Thunderbird
- (ex.getMessage() == null ||
- !ex.getMessage().contains("mailbox selected READ-ONLY"))) ||
- MessageHelper.isRemoved(ex) ||
- EntityOperation.HEADERS.equals(op.name) ||
- EntityOperation.RAW.equals(op.name) ||
- (op.tries >= LOCAL_RETRY_MAX &&
- EntityOperation.BODY.equals(op.name)) ||
- EntityOperation.ATTACHMENT.equals(op.name) ||
- ((op.tries >= LOCAL_RETRY_MAX || attachments > 0) &&
- EntityOperation.ADD.equals(op.name)) ||
- (op.tries >= LOCAL_RETRY_MAX &&
- EntityOperation.SYNC.equals(op.name) &&
- (account.protocol == EntityAccount.TYPE_POP ||
- !ConnectionHelper.isIoError(ex)))) {
- // com.sun.mail.iap.BadCommandException: BAD [TOOBIG] Message too large
- // com.sun.mail.iap.CommandFailedException: NO [CANNOT] Cannot APPEND to a SPAM folder
- // com.sun.mail.iap.CommandFailedException: NO [ALERT] Cannot MOVE messages out of the Drafts folder
- // com.sun.mail.iap.CommandFailedException: NO [OVERQUOTA] quota exceeded
- // Drafts: javax.mail.FolderClosedException: * BYE Jakarta Mail Exception:
- // javax.net.ssl.SSLException: Write error: ssl=0x8286cac0: I/O error during system call, Broken pipe
- // Drafts: * BYE Jakarta Mail Exception: java.io.IOException: Connection dropped by server?
- // Sync: BAD Could not parse command
- // Sync: SEARCH not allowed now
- // Sync: BAD Command SEARCH invalid in AUTHENTICATED state (MARKER:xxx)
- // Seen: NO mailbox selected READ-ONLY
- // Fetch: BAD Error in IMAP command FETCH: Invalid messageset (n.nnn + n.nnn secs).
- // Fetch: NO all of the requested messages have been expunged
- // Fetch: BAD parse error: invalid message sequence number:
- // Fetch: NO The specified message set is invalid.
- // Fetch: NO [SERVERBUG] SELECT Server error - Please try again later
- // Fetch: NO [SERVERBUG] UID FETCH Server error - Please try again later
- // Fetch: NO Invalid message number (took nnn ms)
- // Fetch: NO Invalid message sequence ID: nnn
- // Fetch: BAD Internal Server Error
- // Fetch: BAD Error in IMAP command FETCH: Invalid messageset (n.nnn + n .nnn secs).
- // Fetch: NO FETCH sequence parse error in: nnn
- // Fetch: NO [NONEXISTENT] No matching messages
- // Fetch UID: NO Some messages could not be FETCHed (Failure)
- // Fetch UID: NO [LIMIT] UID FETCH Rate limit hit.
- // Fetch UID: NO Server Unavailable. 15
- // Fetch UID: NO [UNAVAILABLE] Failed to open mailbox
- // Fetch UID: NO [TEMPFAIL] SELECT completed
- // Fetch UID: NO Internal error. Try again later... (MARKER:xxx)
- // Fetch UID: BAD Serious error while processing UID FETCH (NioRecvFail (nn/nn))
- // Fetch UID: NO SELECT: libmapper: Internal error: No servers available or value handling error!
- // Fetch UID: BAD Serious error while processing UID FETCH (CassdbDatabaseError (nnn/n))
- // Move: NO Over quota
- // Move: NO No matching messages
- // Move: NO [EXPUNGEISSUED] Some of the requested messages no longer exist (n.nnn + n.nnn + n.nnn secs)
- // Move: BAD parse error: invalid message sequence number:
- // Move: NO MOVE failed or partially completed.
- // Move: NO mailbox selected READ-ONLY
- // Move: NO read only
- // Move: NO COPY failed
- // Move: NO [SERVERBUG] Internal error occurred. Refer to server log for more information.
- // Move: NO STORE: mtd: internal error: Cannot set message attributes.<404, ebox: no such entity: LiteMessage 29215 does not exist>
- // Move: NO mailbox selected READ-ONLY
- // Move: NO System Error (Failure)
- // Move: NO APPEND processing failed.
- // Move: NO Server Unavailable. 15
- // Move: NO [CANNOT] Operation is not supported on mailbox
- // Move: NO [ALREADYEXISTS] Mailbox already exists
- // Copy: NO Client tried to access nonexistent namespace. (Mailbox name should probably be prefixed with: INBOX.) (n.nnn + n.nnn secs).
- // Copy: NO Message not found
- // Add: BAD Data length exceeds limit
- // Add: NO [LIMIT] APPEND Command exceeds the maximum allowed size
- // Add: NO APPEND failed: Unknown flag: SEEN
- // Add: BAD mtd: internal error: APPEND Message too long. 12345678
- // Add: NO [OVERQUOTA] Not enough disk quota (n.nnn + n.nnn + n.nnn secs).
- // Add: NO [OVERQUOTA] Quota exceeded (mailbox for user is full) (n.nnn + n.nnn secs).
- // Add: NO APPEND failed
- // Add: BAD [TOOBIG] Message too large.
- // Add: NO Permission denied
- // Delete: NO [CANNOT] STORE It's not possible to perform specified operation
- // Delete: NO [UNAVAILABLE] EXPUNGE Backend error
- // Delete: NO mailbox selected READ-ONLY
- // Delete: NO Mails not exist!
- // Flags: NO mailbox selected READ-ONLY
- // Flags: BAD Server error: 'NoneType' object has no attribute 'message_id'
- // Keyword: NO STORE completed
- // Keyword: NO [CANNOT] Keyword length too long (n.nnn + n.nnn secs).
- // Search: BAD command syntax error
- // Search (sync): BAD Could not parse command
-
- String msg = "Unrecoverable operation=" + op.name + " tries=" + op.tries + " created=" + new Date(op.created);
-
- EntityLog.log(context, msg +
- " folder=" + folder.id + ":" + folder.name +
- " message=" + (message == null ? null : message.id + ":" + message.subject) +
- " reason=" + Log.formatThrowable(ex, false));
-
- if (ifolder != null && ifolder.isOpen() &&
- (op.tries > 1 ||
- ex.getCause() instanceof BadCommandException ||
- ex.getCause() instanceof CommandFailedException))
- Log.e(new Throwable(msg, ex));
-
- try {
- db.beginTransaction();
-
- // Cleanup operation
- op.cleanup(context, true);
-
- // There is no use in repeating
- db.operation().deleteOperation(op.id);
-
- // Cleanup messages
- if (MessageHelper.isRemoved(ex)) {
- if (message != null &&
- !EntityOperation.SEEN.equals(op.name) &&
- (!EntityOperation.FLAG.equals(op.name) ||
- EntityFolder.FLAGGED.equals(folder.subtype)))
- db.message().deleteMessage(message.id);
- }
-
- db.setTransactionSuccessful();
- } finally {
- db.endTransaction();
- }
-
- ops.remove(op);
-
- if (!MessageHelper.isRemoved(ex)) {
- int resid = context.getResources().getIdentifier(
- "title_op_title_" + op.name,
- "string",
- context.getPackageName());
- String title = (resid == 0 ? null : context.getString(resid));
- if (title != null) {
- NotificationCompat.Builder builder =
- getNotificationError(context, "warning", account, message.id, new Throwable(title, ex));
- if (NotificationHelper.areNotificationsEnabled(nm))
- nm.notify(op.name + ":" + op.message,
- NotificationHelper.NOTIFICATION_TAGGED,
- builder.build());
- }
- }
-
- } else {
- retry++;
- if (retry < LOCAL_RETRY_MAX &&
- state.isRunning() &&
- state.getSerial() == serial)
- try {
- Thread.sleep(LOCAL_RETRY_DELAY);
- } catch (InterruptedException ex1) {
- Log.w(ex1);
- }
- }
- } finally {
- // Reset operation state
- try {
- db.beginTransaction();
-
- db.operation().setOperationState(op.id, null);
- for (TupleOperationEx s : similar.keySet())
- db.operation().setOperationState(s.id, null);
-
- db.setTransactionSuccessful();
- } finally {
- db.endTransaction();
- }
- }
- } finally {
- Log.i(folder.name + " end op=" + op.id + "/" + op.name);
- }
- }
-
- if (ops.size() != 0 && state.getSerial() == serial) {
- List names = new ArrayList<>();
- for (EntityOperation op : ops)
- names.add(op.name);
- state.error(new OperationCanceledException("Processing " + TextUtils.join(",", names)));
- }
- } finally {
- Log.i(folder.name + " end process state=" + state + " pending=" + ops.size());
- }
- }
-
- private static void ensureUid(Context context, EntityAccount account, EntityFolder folder, EntityMessage message, EntityOperation op, IMAPFolder ifolder) throws MessagingException, IOException {
- if (folder.local)
- return;
- if (message == null || message.uid != null)
- return;
-
- if (EntityOperation.ADD.equals(op.name))
- return;
- if (EntityOperation.FETCH.equals(op.name))
- return;
- if (EntityOperation.EXISTS.equals(op.name))
- return;
- if (EntityOperation.DELETE.equals(op.name) && !TextUtils.isEmpty(message.msgid))
- return;
-
- Log.i(folder.name + " ensure uid op=" + op.name + " msgid=" + message.msgid);
-
- if (TextUtils.isEmpty(message.msgid))
- throw new IllegalArgumentException("Message without msgid for " + op.name);
-
- DB db = DB.getInstance(context);
-
- Long uid = findUid(context, account, ifolder, message.msgid);
- if (uid == null) {
- if (EntityOperation.MOVE.equals(op.name) &&
- EntityFolder.DRAFTS.equals(folder.type))
- try {
- long fid = new JSONArray(op.args).optLong(0, -1L);
- EntityFolder target = db.folder().getFolder(fid);
- if (target != null && EntityFolder.TRASH.equals(target.type)) {
- Log.w(folder.name + " deleting id=" + message.id);
- db.message().deleteMessage(message.id);
- }
- } catch (JSONException ex) {
- Log.e(ex);
- }
-
- throw new IllegalArgumentException("Message not found for " + op.name + " folder=" + folder.name);
- }
-
- db.message().setMessageUid(message.id, message.uid);
- message.uid = uid;
- }
-
- private static Long findUid(Context context, EntityAccount account, IMAPFolder ifolder, String msgid) throws MessagingException, IOException {
- String name = ifolder.getFullName();
- Log.i(name + " searching for msgid=" + msgid);
-
- Long uid = null;
-
- Message[] imessages = findMsgId(context, account, ifolder, msgid);
- if (imessages != null)
- for (Message iexisting : imessages)
- try {
- long muid = ifolder.getUID(iexisting);
- if (muid < 0)
- continue;
- Log.i(name + " found uid=" + muid + " for msgid=" + msgid);
- // RFC3501: Unique identifiers are assigned in a strictly ascending fashion
- if (uid == null || muid > uid)
- uid = muid;
- } catch (MessageRemovedException ex) {
- Log.w(ex);
- }
-
- Log.i(name + " got uid=" + uid + " for msgid=" + msgid);
- return uid;
- }
-
- private static Message[] findMsgId(Context context, EntityAccount account, IMAPFolder ifolder, String msgid) throws MessagingException, IOException {
- // https://stackoverflow.com/questions/18891509/how-to-get-message-from-messageidterm-for-yahoo-imap-profile
- if (account.isYahooJp()) {
- Message[] itemps = ifolder.search(new ReceivedDateTerm(ComparisonTerm.GE, new Date()));
- List tmp = new ArrayList<>();
- for (Message itemp : itemps) {
- MessageHelper helper = new MessageHelper((MimeMessage) itemp, context);
- if (msgid.equals(helper.getMessageID()))
- tmp.add(itemp);
- }
- return tmp.toArray(new Message[0]);
- } else
- return ifolder.search(new MessageIDTerm(msgid));
- }
-
- private static Map findMessages(Context context, EntityFolder folder, List messages, POP3Store istore, POP3Folder ifolder) throws MessagingException, IOException {
- Map caps = istore.capabilities();
- boolean hasUidl = caps.containsKey("UIDL");
-
- Message[] imessages = ifolder.getMessages();
-
- if (hasUidl) {
- FetchProfile ifetch = new FetchProfile();
- ifetch.add(UIDFolder.FetchProfileItem.UID);
- ifolder.fetch(imessages, ifetch);
- }
-
- Map result = new HashMap<>();
-
- for (EntityMessage message : messages) {
- result.put(message, null);
- Log.i(folder.name + " POP searching for=" + message.uidl + "/" + message.msgid +
- " messages=" + imessages.length + " uidl=" + hasUidl);
-
- for (Message imessage : imessages) {
- MessageHelper helper = new MessageHelper((MimeMessage) imessage, context);
-
- String uidl = (hasUidl ? ifolder.getUID(imessage) : null);
- String msgid = (TextUtils.isEmpty(uidl) ? helper.getPOP3MessageID() : null);
-
- if ((uidl != null && uidl.equals(message.uidl)) ||
- (msgid != null && msgid.equals(message.msgid))) {
- Log.i(folder.name + " POP found=" + uidl + "/" + msgid);
- result.put(message, imessage);
- }
- }
- }
-
- return result;
- }
-
- private static void onSetFlag(Context context, JSONArray jargs, EntityFolder folder, List messages, IMAPFolder ifolder, Flags.Flag flag) throws MessagingException, JSONException {
- // Mark message (un)seen
- DB db = DB.getInstance(context);
-
- if (flag != Flags.Flag.SEEN &&
- flag != Flags.Flag.ANSWERED &&
- flag != Flags.Flag.FLAGGED &&
- flag != Flags.Flag.DELETED)
- throw new IllegalArgumentException("Invalid flag=" + flag);
-
- if (folder.read_only)
- return;
-
- if (!ifolder.getPermanentFlags().contains(flag)) {
- for (EntityMessage message : messages)
- if (flag == Flags.Flag.SEEN) {
- db.message().setMessageSeen(message.id, false);
- db.message().setMessageUiSeen(message.id, false);
- } else if (flag == Flags.Flag.ANSWERED) {
- db.message().setMessageAnswered(message.id, false);
- db.message().setMessageUiAnswered(message.id, false);
- } else if (flag == Flags.Flag.FLAGGED) {
- db.message().setMessageFlagged(message.id, false);
- db.message().setMessageUiFlagged(message.id, false, null);
- } else if (flag == Flags.Flag.DELETED) {
- db.message().setMessageDeleted(message.id, false);
- db.message().setMessageUiDeleted(message.id, false);
- }
- return;
- }
-
- List uids = new ArrayList<>();
- boolean set = jargs.getBoolean(0);
- for (EntityMessage message : messages) {
- if (message.uid == null)
- if (messages.size() == 1)
- throw new IllegalArgumentException("Set flag: uid missing");
- else
- throw new MessagingException("Set flag: uid missing");
- if (flag == Flags.Flag.SEEN && !message.seen.equals(set))
- uids.add(message.uid);
- else if (flag == Flags.Flag.ANSWERED && !message.answered.equals(set))
- uids.add(message.uid);
- else if (flag == Flags.Flag.FLAGGED && !message.flagged.equals(set))
- uids.add(message.uid);
- else if (flag == Flags.Flag.DELETED && !message.deleted.equals(set))
- uids.add(message.uid);
- }
-
- if (uids.size() == 0)
- return;
-
- Message[] imessages = ifolder.getMessagesByUID(Helper.toLongArray(uids));
- for (Message imessage : imessages)
- if (imessage == null)
- if (messages.size() == 1)
- throw new MessageRemovedException();
- else
- throw new MessagingException("Set flag: message missing");
-
- ifolder.setFlags(imessages, new Flags(flag), set);
-
- for (EntityMessage message : messages)
- if (flag == Flags.Flag.SEEN && !message.seen.equals(set))
- db.message().setMessageSeen(message.id, set);
- else if (flag == Flags.Flag.ANSWERED && !message.answered.equals(set))
- db.message().setMessageAnswered(message.id, set);
- else if (flag == Flags.Flag.FLAGGED && !message.flagged.equals(set))
- db.message().setMessageFlagged(message.id, set);
- else if (flag == Flags.Flag.DELETED && !message.deleted.equals(set))
- db.message().setMessageDeleted(message.id, set);
- }
-
- private static void onAnswered(Context context, JSONArray jargs, EntityFolder folder, EntityMessage message, IMAPFolder ifolder) throws MessagingException, JSONException {
- // Mark message (un)answered
- DB db = DB.getInstance(context);
-
- if (folder.read_only)
- return;
-
- 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;
-
- // This will be fixed when moving the message
- if (message.uid == null)
- 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 onKeyword(Context context, JSONArray jargs, EntityFolder folder, EntityMessage message, IMAPFolder ifolder) throws MessagingException, JSONException {
- // Set/reset user flag
- // https://tools.ietf.org/html/rfc3501#section-2.3.2
-
- String keyword = jargs.getString(0);
- boolean set = jargs.getBoolean(1);
-
- if (TextUtils.isEmpty(keyword))
- throw new IllegalArgumentException("keyword/empty");
-
- if (message.uid == null)
- throw new IllegalArgumentException("keyword/uid");
-
- if (folder.read_only ||
- !ifolder.getPermanentFlags().contains(Flags.Flag.USER))
- return;
-
- Message imessage = ifolder.getMessageByUID(message.uid);
- if (imessage == null)
- throw new MessageRemovedException();
-
- Flags flags = new Flags(keyword);
- imessage.setFlags(flags, set);
- }
-
- private static void onLabel(Context context, JSONArray jargs, EntityFolder folder, EntityMessage message, IMAPStore istore, IMAPFolder ifolder, State state) throws JSONException, MessagingException, IOException {
- // Set/clear Gmail label
- // Gmail does not push label changes
- String label = jargs.getString(0);
- boolean set = jargs.getBoolean(1);
-
- if (TextUtils.isEmpty(label))
- throw new IllegalArgumentException("label/empty");
-
- if (message.uid == null)
- throw new IllegalArgumentException("label/uid");
-
- DB db = DB.getInstance(context);
-
- if (!set && label.equals(folder.name)) {
- if (TextUtils.isEmpty(message.msgid)) {
- Log.w("label/msgid");
- return;
- }
-
- // Prevent deleting message
- EntityFolder archive = db.folder().getFolderByType(message.account, EntityFolder.ARCHIVE);
- if (archive == null) {
- Log.w("label/archive");
- return;
- }
-
- boolean archived;
- Folder iarchive = istore.getFolder(archive.name);
- try {
- iarchive.open(Folder.READ_ONLY);
- Message[] imessages = iarchive.search(new MessageIDTerm(message.msgid));
- archived = (imessages != null && imessages.length > 0);
- } finally {
- if (iarchive.isOpen())
- iarchive.close(false);
- }
-
- if (archived)
- try {
- Message imessage = ifolder.getMessageByUID(message.uid);
- if (imessage == null)
- throw new MessageRemovedException();
- imessage.setFlag(Flags.Flag.DELETED, true);
- expunge(context, ifolder, Arrays.asList(imessage));
- } catch (MessagingException ex) {
- Log.w(ex);
- }
- else {
- Log.w("label/delete folder=" + folder.name);
- return;
- }
- } else {
- try {
- Message imessage = ifolder.getMessageByUID(message.uid);
- if (imessage instanceof GmailMessage)
- ((GmailMessage) imessage).setLabels(new String[]{label}, set);
- } catch (MessagingException ex) {
- Log.w(ex);
- }
- }
-
- try {
- db.beginTransaction();
-
- List messages = db.message().getMessagesByMsgId(message.account, message.msgid);
- if (messages == null)
- return;
-
- for (EntityMessage m : messages) {
- EntityFolder f = db.folder().getFolder(m.folder);
- if (!label.equals(f.name) && m.setLabel(label, set)) {
- Log.i("Set " + label + "=" + set + " id=" + m.id + " folder=" + f.name);
- db.message().setMessageLabels(m.id, DB.Converters.fromStringArray(m.labels));
- }
- }
-
- db.setTransactionSuccessful();
- } finally {
- db.endTransaction();
- }
- }
-
- private static void onAdd(Context context, JSONArray jargs, EntityAccount account, EntityFolder folder, EntityMessage message, IMAPStore istore, IMAPFolder ifolder, State state) throws MessagingException, IOException {
- // Add message
- DB db = DB.getInstance(context);
-
- if (folder.local) {
- Log.i(folder.name + " local add");
- return;
- }
-
- // Drafts can change accounts
- if (jargs.length() == 0 && !folder.id.equals(message.folder))
- throw new IllegalArgumentException("Message folder changed");
-
- // Get arguments
- long target = jargs.optLong(0, folder.id);
- boolean autoread = jargs.optBoolean(1, false);
- boolean copy = jargs.optBoolean(2, false); // Cross account
-
- if (target != folder.id)
- throw new IllegalArgumentException("Invalid folder");
-
- // External draft might have a uid only
- if (TextUtils.isEmpty(message.msgid)) {
- message.msgid = EntityMessage.generateMessageId();
- db.message().setMessageMsgId(message.id, message.msgid);
- }
-
- Properties props = MessageHelper.getSessionProperties(account.unicode);
- Session isession = Session.getInstance(props, null);
- Flags flags = ifolder.getPermanentFlags();
-
- // Get raw message
- MimeMessage imessage;
- File file = message.getRawFile(context);
- if (folder.id.equals(message.folder)) {
- // Pre flight check
- if (!message.content)
- throw new IllegalArgumentException("Message body missing");
-
- if (!BuildConfig.DEBUG) {
- List attachments = db.attachment().getAttachments(message.id);
- for (EntityAttachment attachment : attachments)
- if (EntityAttachment.SMIME_SIGNATURE.equals(attachment.encryption))
- for (EntityAttachment content : attachments)
- if (EntityAttachment.SMIME_CONTENT.equals(content.encryption)) {
- boolean afile = attachment.getFile(context).exists();
- boolean cfile = content.getFile(context).exists();
- if (!attachment.available || !afile || !content.available || !cfile) {
- Log.e("S/MIME vanished" +
- " available=" + attachment.available + "/" + content.available +
- " file=" + afile + "/" + cfile +
- " error=" + attachment.error + "/" + content.error);
- db.attachment().setAvailable(attachment.id, false);
- db.attachment().setAvailable(content.id, false);
- db.attachment().setEncryption(attachment.id, null);
- db.attachment().setEncryption(content.id, null);
- }
- }
- }
-
- imessage = MessageHelper.from(context, message, null, isession, false);
-
- try (OutputStream os = new BufferedOutputStream(new FileOutputStream(file))) {
- imessage.writeTo(os);
- }
- } else {
- // Cross account move
- 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 MimeMessageEx(isession, is, message.msgid);
- }
-
- imessage.removeHeader(MessageHelper.HEADER_CORRELATION_ID);
- imessage.addHeader(MessageHelper.HEADER_CORRELATION_ID, message.msgid);
-
- imessage.saveChanges();
- /*
- javax.mail.internet.ParseException: Unbalanced quoted string
- at javax.mail.internet.HeaderTokenizer.collectString(SourceFile:15)
- at javax.mail.internet.HeaderTokenizer.getNext(SourceFile:20)
- at javax.mail.internet.HeaderTokenizer.next(SourceFile:4)
- at javax.mail.internet.HeaderTokenizer.next(SourceFile:1)
- at javax.mail.internet.ParameterList.(SourceFile:23)
- at javax.mail.internet.ContentType.(SourceFile:17)
- at javax.mail.internet.MimeBodyPart.updateHeaders(SourceFile:12)
- at javax.mail.internet.MimeBodyPart.updateHeaders(SourceFile:1)
- at javax.mail.internet.MimeMultipart.updateHeaders(SourceFile:3)
- at javax.mail.internet.MimeBodyPart.updateHeaders(SourceFile:24)
- at javax.mail.internet.MimeMessage.updateHeaders(SourceFile:1)
- at javax.mail.internet.MimeMessage.saveChanges(SourceFile:3)
- */
-
- if (flags.contains(Flags.Flag.SEEN))
- imessage.setFlag(Flags.Flag.SEEN, message.ui_seen);
- if (flags.contains(Flags.Flag.ANSWERED))
- imessage.setFlag(Flags.Flag.ANSWERED, message.ui_answered);
- if (flags.contains(Flags.Flag.FLAGGED))
- imessage.setFlag(Flags.Flag.FLAGGED, message.ui_flagged);
- if (flags.contains(Flags.Flag.DELETED))
- imessage.setFlag(Flags.Flag.DELETED, message.ui_deleted);
-
- if (flags.contains(Flags.Flag.USER)) {
- if (message.isForwarded()) {
- Flags fwd = new Flags(MessageHelper.FLAG_FORWARDED);
- imessage.setFlags(new Flags(fwd), true);
- }
- }
- }
-
- db.message().setMessageRaw(message.id, true);
-
- // Check size
- if (account.max_size != null) {
- long size = file.length();
- if (size > account.max_size) {
- String msg = "Too large" +
- " size=" + Helper.humanReadableByteCount(size) +
- "/" + Helper.humanReadableByteCount(account.max_size) +
- " host=" + account.host;
- Log.w(msg);
- throw new IllegalArgumentException(msg);
- }
- }
-
- // Handle auto read
- if (flags.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 (flags.contains(Flags.Flag.DRAFT))
- imessage.setFlag(Flags.Flag.DRAFT, EntityFolder.DRAFTS.equals(folder.type));
-
- // Add message
- Long newuid = null;
- if (MessageHelper.hasCapability(ifolder, "UIDPLUS")) {
- // https://tools.ietf.org/html/rfc4315
- AppendUID[] uids = ifolder.appendUIDMessages(new Message[]{imessage});
- if (uids != null && uids.length > 0 && uids[0] != null && uids[0].uid > 0) {
- newuid = uids[0].uid;
- Log.i(folder.name + " appended uid=" + newuid);
- }
- } else
- ifolder.appendMessages(new Message[]{imessage});
-
- if (folder.id.equals(message.folder)) {
- // Prevent deleting message
- db.message().setMessageUid(message.id, null);
-
- // Some providers do not list the new message yet
- try {
- List delete = new ArrayList<>();
-
- if (message.uid != null)
- try {
- Message iprev = ifolder.getMessageByUID(message.uid);
- if (iprev != null) {
- Log.i(folder.name + " found prev uid=" + message.uid + " msgid=" + message.msgid);
- iprev.setFlag(Flags.Flag.DELETED, true);
- delete.add(iprev);
- }
- } catch (Throwable ex) {
- Log.w(ex);
- }
-
- Log.i(folder.name + " searching for added msgid=" + message.msgid);
- Message[] imessages = findMsgId(context, account, ifolder, message.msgid);
- if (imessages != null) {
- Long found = newuid;
-
- for (Message iexisting : imessages)
- try {
- long muid = ifolder.getUID(iexisting);
- if (muid < 0)
- continue;
- Log.i(folder.name + " found added uid=" + muid + " msgid=" + message.msgid);
- if (found == null || muid > found)
- found = muid;
- } catch (MessageRemovedException ex) {
- Log.w(ex);
- }
-
- if (found != null) {
- if (newuid == null || found > newuid)
- newuid = found;
-
- for (Message iexisting : imessages)
- try {
- long muid = ifolder.getUID(iexisting);
- if (muid < 0)
- continue;
- if (muid < newuid &&
- (message.uid == null || message.uid != muid))
- try {
- iexisting.setFlag(Flags.Flag.DELETED, true);
- delete.add(iexisting);
- } catch (MessagingException ex) {
- Log.w(ex);
- }
- } catch (MessageRemovedException ex) {
- Log.w(ex);
- }
- }
- }
-
- expunge(context, ifolder, delete);
-
- } catch (MessagingException ex) {
- Log.w(ex);
- }
-
- if (newuid != null && (message.uid == null || newuid > message.uid))
- try {
- Log.i(folder.name + " Fetching uid=" + newuid);
- JSONArray fargs = new JSONArray();
- fargs.put(newuid);
- onFetch(context, fargs, folder, istore, ifolder, state);
- } catch (Throwable ex) {
- Log.e(ex);
- }
- } else {
- // Lookup added message
- int count = 0;
- Long found = newuid;
- while (found == null && count++ < FIND_RETRY_COUNT) {
- found = findUid(context, account, ifolder, message.msgid);
- if (found == null)
- try {
- Thread.sleep(FIND_RETRY_DELAY);
- } catch (InterruptedException ex) {
- Log.e(ex);
- }
- }
-
- try {
- db.beginTransaction();
-
- if (found == null) {
- db.message().setMessageError(message.id,
- "Message not found in target folder " + account.name + "/" + folder.name);
- db.message().setMessageUiHide(message.id, false);
- } else {
- // Mark source read
- if (autoread)
- EntityOperation.queue(context, message, EntityOperation.SEEN, true);
-
- // Delete source
- if (!copy)
- EntityOperation.queue(context, message, EntityOperation.DELETE);
- }
-
- db.setTransactionSuccessful();
- } finally {
- db.endTransaction();
- }
-
- // Fetch target
- if (found != null)
- try {
- Log.i(folder.name + " Fetching uid=" + found);
- JSONArray fargs = new JSONArray();
- fargs.put(found);
- onFetch(context, fargs, folder, istore, ifolder, state);
- } catch (Throwable ex) {
- Log.e(ex);
- }
- }
- }
-
- private static void onMove(Context context, JSONArray jargs, boolean copy, EntityAccount account, EntityFolder folder, List messages, IMAPStore istore, IMAPFolder ifolder, State state) throws JSONException, MessagingException, IOException {
- // Move message
- DB db = DB.getInstance(context);
-
- // Get arguments
- long id = jargs.getLong(0);
- boolean seen = jargs.optBoolean(1);
- boolean unflag = jargs.optBoolean(3);
- boolean delete = jargs.optBoolean(4);
- boolean create = jargs.optBoolean(5);
-
- Flags flags = ifolder.getPermanentFlags();
-
- // Get target folder
- EntityFolder target = db.folder().getFolder(id);
- if (target == null)
- throw new FolderNotFoundException();
- if (folder.id.equals(target.id))
- throw new IllegalArgumentException("self type=" + folder.type + "/" + target.type);
- if (!target.selectable)
- throw new IllegalArgumentException("not selectable type=" + target.type);
-
- if (create) {
- Folder icreate = istore.getFolder(target.name);
- if (!icreate.exists()) {
- ((IMAPFolder) ifolder).doCommand(new IMAPFolder.ProtocolCommand() {
- @Override
- public Object doCommand(IMAPProtocol protocol) throws ProtocolException {
- protocol.create(target.name);
- return null;
- }
- });
- ifolder.setSubscribed(true);
- db.folder().resetFolderTbc(target.id);
- }
- }
-
- // De-classify
- if (!copy &&
- !EntityFolder.TRASH.equals(target.type) &&
- !EntityFolder.ARCHIVE.equals(target.type))
- for (EntityMessage message : messages)
- MessageClassifier.classify(message, folder, false, context);
-
- IMAPFolder itarget = (IMAPFolder) istore.getFolder(target.name);
-
- // Get source messages
- Map map = new HashMap<>();
- Map msgids = new HashMap<>();
- for (EntityMessage message : messages)
- try {
- if (message.uid == null)
- throw new IllegalArgumentException("move without uid");
- Message imessage = ifolder.getMessageByUID(message.uid);
- if (imessage == null)
- throw new MessageRemovedException("move without message");
- map.put(imessage, message);
- } catch (MessageRemovedException ex) {
- Log.e(ex);
- db.message().deleteMessage(message.id);
- }
-
- // Some servers return different capabilities for different sessions
- // NO [CANNOT] MOVE It's not possible to perform specified operation
- // https://stackoverflow.com/questions/56148668/cannot-delete-emails-of-domain-co-jp-type
- boolean canMove = !account.isYahooJp() &&
- MessageHelper.hasCapability(ifolder, "MOVE");
-
- // Some providers do not support the COPY operation for drafts
- boolean draft = (EntityFolder.DRAFTS.equals(folder.type) || EntityFolder.DRAFTS.equals(target.type));
- boolean duplicate = (copy && !account.isGmail()) || (draft && account.isGmail());
- if (draft || duplicate) {
- Log.i(folder.name + " " + (duplicate ? "copy" : "move") +
- " from " + folder.type + " to " + target.type);
-
- if (!duplicate && account.isSeznam())
- ifolder.copyMessages(map.keySet().toArray(new Message[0]), itarget);
- else {
- List icopies = new ArrayList<>();
- for (Message imessage : map.keySet()) {
- EntityMessage message = map.get(imessage);
-
- Message icopy;
- File file = new File(message.getFile(context).getAbsoluteFile() + ".copy");
- try {
- try (OutputStream os = new BufferedOutputStream(new FileOutputStream(file))) {
- imessage.writeTo(os);
- }
-
- Properties props = MessageHelper.getSessionProperties(account.unicode);
- Session isession = Session.getInstance(props, null);
-
- try (InputStream is = new BufferedInputStream(new FileInputStream(file))) {
- if (duplicate) {
- String msgid = EntityMessage.generateMessageId();
- msgids.put(message, msgid);
- icopy = new MimeMessageEx(isession, is, msgid);
- icopy.saveChanges();
-
- if (!copy) {
- List tmps = db.message().getMessagesByMsgId(message.account, message.msgid);
- for (EntityMessage tmp : tmps)
- if (target.id.equals(tmp.folder)) {
- db.message().setMessageMsgId(tmp.id, msgid);
- break;
- }
- }
- } else
- icopy = new MimeMessage(isession, is);
- }
- } finally {
- file.delete();
- }
-
- for (Flags.Flag flag : imessage.getFlags().getSystemFlags())
- icopy.setFlag(flag, true);
-
- icopies.add(icopy);
- }
-
- itarget.appendMessages(icopies.toArray(new Message[0]));
- }
- } else {
- for (Message imessage : map.keySet()) {
- Log.i((copy ? "Copy" : "Move") + " seen=" + seen + " unflag=" + unflag + " flags=" + imessage.getFlags() + " can=" + canMove);
-
- // Mark read
- if (seen && !imessage.isSet(Flags.Flag.SEEN) && flags.contains(Flags.Flag.SEEN))
- imessage.setFlag(Flags.Flag.SEEN, true);
-
- // Remove star
- if (unflag && imessage.isSet(Flags.Flag.FLAGGED) && flags.contains(Flags.Flag.FLAGGED))
- imessage.setFlag(Flags.Flag.FLAGGED, false);
-
- // Mark not spam
- if (!copy && ifolder.getPermanentFlags().contains(Flags.Flag.USER)) {
- Flags junk = new Flags(MessageHelper.FLAG_JUNK);
- Flags notJunk = new Flags(MessageHelper.FLAG_NOT_JUNK);
- List userFlags = Arrays.asList(imessage.getFlags().getUserFlags());
- if (EntityFolder.JUNK.equals(target.type)) {
- // To junk
- if (userFlags.contains(MessageHelper.FLAG_NOT_JUNK))
- imessage.setFlags(notJunk, false);
- imessage.setFlags(junk, true);
- } else if (EntityFolder.JUNK.equals(folder.type)) {
- // From junk
- if (userFlags.contains(MessageHelper.FLAG_JUNK))
- imessage.setFlags(junk, false);
- imessage.setFlags(notJunk, true);
- }
- }
- }
-
- // https://tools.ietf.org/html/rfc6851
- if (!copy && canMove)
- try {
- ifolder.moveMessages(map.keySet().toArray(new Message[0]), itarget);
- } catch (MessagingException ex) {
- if (!(map.size() == 1 &&
- ex.getCause() instanceof CommandFailedException &&
- ex.getCause().getMessage() != null &&
- ex.getCause().getMessage().contains("[EXPUNGEISSUED]")))
- throw ex;
- }
- else
- ifolder.copyMessages(map.keySet().toArray(new Message[0]), itarget);
- }
-
- // Delete source
- if (!copy && (draft || !canMove)) {
- List deleted = new ArrayList<>();
- for (Message imessage : map.keySet())
- try {
- imessage.setFlag(Flags.Flag.DELETED, true);
- deleted.add(imessage);
- } catch (MessageRemovedException ex) {
- Log.w(ex);
- }
- expunge(context, ifolder, deleted);
- } else {
- int count = MessageHelper.getMessageCount(ifolder);
- db.folder().setFolderTotal(folder.id, count < 0 ? null : count, new Date().getTime());
- }
-
- // Fetch appended/copied when needed
- boolean fetch = (copy || delete ||
- !"connected".equals(target.state) ||
- !MessageHelper.hasCapability(ifolder, "IDLE"));
- if (draft || fetch)
- try {
- Log.i(target.name + " moved message fetch=" + fetch);
- itarget.open(READ_WRITE);
-
- boolean sync = false;
- List ideletes = new ArrayList<>();
- for (EntityMessage message : map.values())
- try {
- String msgid = msgids.get(message);
- if (msgid == null)
- msgid = message.msgid;
-
- if (TextUtils.isEmpty(msgid))
- throw new IllegalArgumentException("move: msgid missing");
-
- Long uid = findUid(context, account, itarget, msgid);
- if (uid == null)
- throw new IllegalArgumentException("move: uid not found");
-
- if (draft || duplicate) {
- Message icopy = itarget.getMessageByUID(uid);
- if (icopy == null)
- throw new IllegalArgumentException("move: gone uid=" + uid);
-
- // Mark read
- if (seen && !icopy.isSet(Flags.Flag.SEEN) && flags.contains(Flags.Flag.SEEN))
- icopy.setFlag(Flags.Flag.SEEN, true);
-
- // Remove star
- if (unflag && icopy.isSet(Flags.Flag.FLAGGED) && flags.contains(Flags.Flag.FLAGGED))
- icopy.setFlag(Flags.Flag.FLAGGED, false);
-
- // Set drafts flag
- if (flags.contains(Flags.Flag.DRAFT))
- icopy.setFlag(Flags.Flag.DRAFT, EntityFolder.DRAFTS.equals(target.type));
- }
-
- if (delete) {
- Log.i(target.name + " Deleting uid=" + uid);
- Message idelete = itarget.getMessageByUID(uid);
- idelete.setFlag(Flags.Flag.DELETED, true);
- ideletes.add(idelete);
- } else if (fetch) {
- Log.i(target.name + " Fetching uid=" + uid);
- JSONArray fargs = new JSONArray();
- fargs.put(uid);
- onFetch(context, fargs, target, istore, itarget, state);
- }
- } catch (Throwable ex) {
- if (ex instanceof IllegalArgumentException)
- Log.i(ex);
- else
- Log.e(ex);
- if (fetch)
- sync = true;
- }
-
- expunge(context, itarget, ideletes);
-
- if (sync)
- EntityOperation.sync(context, target.id, false);
- } catch (Throwable ex) {
- Log.w(ex);
- } finally {
- if (itarget.isOpen())
- itarget.close(false);
- }
-
- // Delete junk contacts
- if (EntityFolder.JUNK.equals(target.type))
- for (EntityMessage message : map.values()) {
- Address[] recipients = (message.reply != null ? message.reply : message.from);
- if (recipients != null)
- for (Address recipient : recipients) {
- String email = ((InternetAddress) recipient).getAddress();
- int count = db.contact().deleteContact(target.account, EntityContact.TYPE_FROM, email);
- Log.i("Deleted contact email=" + email + " count=" + count);
- }
- }
- }
-
- private static void onFetch(Context context, JSONArray jargs, EntityFolder folder, IMAPStore istore, IMAPFolder ifolder, State state) throws JSONException, MessagingException, IOException {
- long uid = jargs.getLong(0);
- boolean invalidate = jargs.optBoolean(1);
- boolean removed = jargs.optBoolean(2);
-
- DB db = DB.getInstance(context);
- EntityAccount account = db.account().getAccount(folder.account);
- if (account == null)
- throw new IllegalArgumentException("account missing");
-
- try {
- if (uid < 0)
- throw new MessageRemovedException(folder.name + " fetch uid=" + uid);
- if (removed)
- throw new MessageRemovedException("removed uid=" + uid);
-
- MimeMessage imessage = (MimeMessage) ifolder.getMessageByUID(uid);
- if (imessage == null)
- throw new MessageRemovedException(folder.name + " fetch not found uid=" + uid);
- // synchronizeMessage will check expunged/deleted
-
- if (invalidate && imessage instanceof IMAPMessage)
- ((IMAPMessage) imessage).invalidateHeaders();
-
- SyncStats stats = new SyncStats();
- boolean download = db.folder().getFolderDownload(folder.id);
- List rules = db.rule().getEnabledRules(folder.id, false);
-
- FetchProfile fp = new FetchProfile();
- fp.add(UIDFolder.FetchProfileItem.UID); // To check if message exists
- fp.add(FetchProfile.Item.FLAGS); // To update existing messages
- if (account.isGmail())
- fp.add(GmailFolder.FetchProfileItem.LABELS);
- ifolder.fetch(new Message[]{imessage}, fp);
-
- EntityMessage message = synchronizeMessage(context, account, folder, istore, ifolder, imessage, false, download, rules, state, stats);
- if (message != null) {
- if (account.isGmail() && EntityFolder.USER.equals(folder.type))
- try {
- JSONArray jlabel = new JSONArray();
- jlabel.put(0, folder.name);
- jlabel.put(1, true);
- onLabel(context, jlabel, folder, message, istore, ifolder, state);
- } catch (Throwable ex1) {
- Log.e(ex1);
- }
-
- if (download) {
- SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
- boolean fast_fetch = prefs.getBoolean("fast_fetch", false);
-
- boolean async = false;
- if (fast_fetch) {
- long maxSize = prefs.getInt("download", MessageHelper.DEFAULT_DOWNLOAD_SIZE);
- if (maxSize == 0)
- maxSize = Long.MAX_VALUE;
- boolean download_eml = prefs.getBoolean("download_eml", false);
-
- if (!message.content)
- if (state.getNetworkState().isUnmetered() || (message.size != null && message.size < maxSize))
- async = true;
-
- List attachments = db.attachment().getAttachments(message.id);
- for (EntityAttachment attachment : attachments)
- if (!attachment.available)
- if (state.getNetworkState().isUnmetered() || (attachment.size != null && attachment.size < maxSize))
- async = true;
-
- if (download_eml &&
- (message.raw == null || !message.raw) &&
- (state.getNetworkState().isUnmetered() || (message.total != null && message.total < maxSize)))
- async = true;
- }
-
- if (async && message.uid != null && !message.ui_hide)
- EntityOperation.queue(context, message, EntityOperation.DOWNLOAD, message.uid);
- else
- downloadMessage(context, account, folder, istore, ifolder, imessage, message.id, state, stats);
- }
- }
-
- if (!stats.isEmpty())
- EntityLog.log(context, EntityLog.Type.Statistics,
- account.name + "/" + folder.name + " fetch stats " + stats);
- } catch (Throwable ex) {
- if (MessageHelper.isRemoved(ex)) {
- Log.i(ex);
-
- if (account.isGmail() && EntityFolder.USER.equals(folder.type)) {
- EntityMessage message = db.message().getMessageByUid(folder.id, uid);
- if (message != null)
- try {
- JSONArray jlabel = new JSONArray();
- jlabel.put(0, folder.name);
- jlabel.put(1, false);
- onLabel(context, jlabel, folder, message, istore, ifolder, state);
- } catch (Throwable ex1) {
- Log.e(ex1);
- }
- }
-
- int count = db.message().deleteMessage(folder.id, uid);
- Log.i(folder.name + " delete local uid=" + uid + " count=" + count);
- } else
- throw ex;
- } finally {
- int count = MessageHelper.getMessageCount(ifolder);
- db.folder().setFolderTotal(folder.id, count < 0 ? null : count, new Date().getTime());
- }
- }
-
- private static void onDelete(Context context, JSONArray jargs, EntityAccount account, EntityFolder folder, List messages, IMAPFolder ifolder) throws MessagingException, IOException {
- // Delete message
- DB db = DB.getInstance(context);
- SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
- boolean perform_expunge = prefs.getBoolean("perform_expunge", true);
-
- if (folder.local) {
- Log.i(folder.name + " local delete");
- for (EntityMessage message : messages)
- db.message().deleteMessage(message.id);
- return;
- }
-
- try {
- if (messages.size() > 1) {
- boolean ui_deleted = messages.get(0).ui_deleted;
-
- List uids = new ArrayList<>();
- for (EntityMessage message : messages) {
- if (message.uid == null)
- throw new MessagingException("Delete: uid missing");
- if (message.ui_deleted != ui_deleted)
- throw new MessagingException("Delete: flag inconsistent");
- uids.add(message.uid);
- }
-
- Message[] idelete = ifolder.getMessagesByUID(Helper.toLongArray(uids));
- for (Message imessage : idelete)
- if (imessage == null)
- throw new MessagingException("Delete: message missing");
-
- EntityLog.log(context, folder.name + " deleting messages=" + uids.size());
-
- if (perform_expunge) {
- ifolder.setFlags(idelete, new Flags(Flags.Flag.DELETED), true);
- expunge(context, ifolder, Arrays.asList(idelete));
- for (EntityMessage message : messages)
- db.message().deleteMessage(message.id);
- } else {
- ifolder.setFlags(idelete, new Flags(Flags.Flag.DELETED), ui_deleted);
- for (EntityMessage message : messages)
- db.message().setMessageDeleted(message.id, message.ui_deleted);
- }
-
- EntityLog.log(context, folder.name + " deleted messages=" + uids.size());
- } else if (messages.size() == 1) {
- List deleted = new ArrayList<>();
-
- EntityMessage message = messages.get(0);
- if (message.uid != null) {
- Message iexisting = ifolder.getMessageByUID(message.uid);
- if (iexisting == null)
- Log.w(folder.name + " existing not found uid=" + message.uid);
- else
- try {
- Log.i(folder.name + " deleting uid=" + message.uid);
- if (perform_expunge)
- iexisting.setFlag(Flags.Flag.DELETED, true);
- else
- iexisting.setFlag(Flags.Flag.DELETED, message.ui_deleted);
- deleted.add(iexisting);
- } catch (MessageRemovedException ignored) {
- Log.w(folder.name + " existing gone uid=" + message.uid);
- }
- }
-
- boolean found = (deleted.size() > 0);
- if (!TextUtils.isEmpty(message.msgid) &&
- (!found || EntityFolder.DRAFTS.equals(folder.type)))
- try {
- Message[] imessages = findMsgId(context, account, ifolder, message.msgid);
- if (imessages != null)
- for (Message iexisting : imessages)
- try {
- long muid = ifolder.getUID(iexisting);
- if (found && muid == message.uid)
- continue;
-
- // Fail safe
- MessageHelper helper = new MessageHelper((MimeMessage) iexisting, context);
- if (!message.msgid.equals(helper.getMessageID()))
- continue;
-
- Log.i(folder.name + " deleting uid=" + muid);
- if (perform_expunge)
- iexisting.setFlag(Flags.Flag.DELETED, true);
- else
- iexisting.setFlag(Flags.Flag.DELETED, message.ui_deleted);
-
- deleted.add(iexisting);
- } catch (MessageRemovedException ex) {
- Log.w(ex);
- }
- } catch (MessagingException ex) {
- Log.w(ex);
- }
-
- if (perform_expunge) {
- if (deleted.size() == 0 || expunge(context, ifolder, deleted))
- db.message().deleteMessage(message.id);
- } else {
- if (deleted.size() > 0)
- db.message().setMessageDeleted(message.id, message.ui_deleted);
- }
- }
- } finally {
- int count = MessageHelper.getMessageCount(ifolder);
- db.folder().setFolderTotal(folder.id, count < 0 ? null : count, new Date().getTime());
- }
- }
-
- private static void onDelete(Context context, JSONArray jargs, EntityAccount account, EntityFolder folder, List messages, POP3Folder ifolder, POP3Store istore, State state) throws MessagingException, IOException {
- // Delete from server
- if (!EntityFolder.INBOX.equals(folder.type) || account.leave_deleted)
- throw new IllegalArgumentException("POP3: invalid DELETE" +
- " folder=" + folder.type + " leave=" + account.leave_deleted);
-
- Map map = findMessages(context, folder, messages, istore, ifolder);
- for (EntityMessage message : messages) {
- Message imessage = map.get(message);
- if (imessage != null) {
- Log.i(folder.name + " POP delete=" + message.uidl + "/" + message.msgid);
- imessage.setFlag(Flags.Flag.DELETED, true);
- }
- }
-
- if (map.size() > 0)
- try {
- Log.i(folder.name + " POP expunge");
- ifolder.close(true);
- ifolder.open(Folder.READ_WRITE);
- } catch (Throwable ex) {
- Log.e(ex);
- state.error(new FolderClosedException(ifolder, "POP", new Exception(ex)));
- }
- }
-
- private static void onHeaders(Context context, JSONArray jargs, EntityFolder folder, EntityMessage message, IMAPFolder ifolder) throws MessagingException, IOException {
- // Download headers
- DB db = DB.getInstance(context);
-
- if (message.headers != null)
- return;
-
- IMAPMessage imessage = (IMAPMessage) ifolder.getMessageByUID(message.uid);
- if (imessage == null)
- throw new MessageRemovedException();
-
- MessageHelper helper = new MessageHelper(imessage, context);
- db.message().setMessageHeaders(message.id, helper.getHeaders());
- }
-
- private static void onRaw(Context context, JSONArray jargs, EntityFolder folder, EntityMessage message, IMAPFolder ifolder) throws MessagingException, IOException, JSONException {
- // Download raw message
- DB db = DB.getInstance(context);
-
- if (message.raw == null || !message.raw) {
- IMAPMessage imessage = (IMAPMessage) ifolder.getMessageByUID(message.uid);
- if (imessage == null)
- throw new MessageRemovedException();
-
- File file = message.getRawFile(context);
- try (OutputStream os = new BufferedOutputStream(new FileOutputStream(file))) {
- imessage.writeTo(os);
- }
-
- db.message().setMessageRaw(message.id, true);
- }
-
- if (jargs.length() > 0) {
- // Cross account move/copy
- long tid = jargs.getLong(0);
- EntityFolder target = db.folder().getFolder(tid);
- if (target == null)
- throw new FolderNotFoundException();
-
- Log.i(folder.name + " queuing ADD id=" + message.id + ":" + target.id);
-
- EntityOperation operation = new EntityOperation();
- operation.account = target.account;
- operation.folder = target.id;
- 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 onRaw(Context context, JSONArray jargs, EntityFolder folder, EntityMessage message, POP3Store istore, POP3Folder ifolder) throws MessagingException, IOException, JSONException {
- // Download raw message
- if (!EntityFolder.INBOX.equals(folder.type))
- throw new IllegalArgumentException("Unexpected folder=" + folder.type);
-
- if (message.raw == null || !message.raw) {
- Map map = findMessages(context, folder, Arrays.asList(message), istore, ifolder);
- if (map.get(message) == null)
- throw new IllegalArgumentException("Message not found msgid=" + message.msgid);
-
- File file = message.getRawFile(context);
- try (OutputStream os = new BufferedOutputStream(new FileOutputStream(file))) {
- map.get(message).writeTo(os);
- }
-
- DB db = DB.getInstance(context);
- db.message().setMessageRaw(message.id, true);
- }
- }
-
- private static void onBody(Context context, JSONArray jargs, EntityFolder folder, EntityMessage message, IMAPFolder ifolder) throws MessagingException, IOException {
- boolean plain_text = jargs.optBoolean(0);
- String charset = (jargs.isNull(1) ? null : jargs.optString(1, null));
-
- if (message.uid == null)
- throw new IllegalArgumentException("uid missing");
-
- // Download message body
- DB db = DB.getInstance(context);
-
- if (message.content && message.isPlainOnly() == plain_text && charset == null)
- return;
-
- // Get message
- Message imessage = ifolder.getMessageByUID(message.uid);
- if (imessage == null)
- throw new MessageRemovedException();
-
- MessageHelper helper = new MessageHelper((MimeMessage) imessage, context);
- MessageHelper.MessageParts parts = helper.getMessageParts();
- String body = parts.getHtml(context, plain_text, charset);
- File file = message.getFile(context);
- Helper.writeText(file, body);
- String text = HtmlHelper.getFullText(body);
- message.preview = HtmlHelper.getPreview(text);
- message.language = HtmlHelper.getLanguage(context, message.subject, text);
- Integer plain_only = parts.isPlainOnly();
- if (plain_text)
- plain_only = 1 | (plain_only == null ? 0 : plain_only & 0x80);
- db.message().setMessageContent(message.id,
- true,
- message.language,
- plain_only,
- message.preview,
- parts.getWarnings(message.warning));
- MessageClassifier.classify(message, folder, true, context);
-
- if (body != null)
- EntityLog.log(context, "Operation body size=" + body.length());
- }
-
- private static void onAttachment(Context context, JSONArray jargs, EntityFolder folder, EntityMessage message, EntityOperation op, IMAPFolder ifolder) throws JSONException, MessagingException, IOException {
- // Download attachment
- DB db = DB.getInstance(context);
-
- long id = jargs.getLong(0);
-
- // Get attachment
- EntityAttachment attachment = db.attachment().getAttachment(id);
- if (attachment == null)
- attachment = db.attachment().getAttachment(message.id, (int) id); // legacy
- if (attachment == null)
- throw new IllegalArgumentException("Local attachment not found");
- if (attachment.subsequence != null)
- throw new IllegalArgumentException("Download of sub attachment");
- if (attachment.available)
- return;
- if (message.uid == null)
- throw new IllegalArgumentException("Attachment/message uid missing");
-
- // Get message
- Message imessage = ifolder.getMessageByUID(message.uid);
- if (imessage == null)
- throw new MessageRemovedException();
-
- // Get message parts
- MessageHelper helper = new MessageHelper((MimeMessage) imessage, context);
- MessageHelper.MessageParts parts = helper.getMessageParts();
-
- // Download attachment
- parts.downloadAttachment(context, attachment);
-
- if (attachment.size != null)
- EntityLog.log(context, "Operation attachment size=" + attachment.size);
- }
-
- private static void onBody(Context context, JSONArray jargs, EntityFolder folder, EntityMessage message, POP3Folder ifolder, POP3Store istore) throws MessagingException, IOException {
- if (!EntityFolder.INBOX.equals(folder.type))
- throw new IllegalArgumentException("Not INBOX");
-
- Map map = findMessages(context, folder, Arrays.asList(message), istore, ifolder);
- if (map.size() == 0)
- throw new MessageRemovedException("Message not found");
-
- SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
- boolean download_plain = prefs.getBoolean("download_plain", false);
-
- MessageHelper helper = new MessageHelper((MimeMessage) map.entrySet().iterator().next().getValue(), context);
- MessageHelper.MessageParts parts = helper.getMessageParts();
-
- String body = parts.getHtml(context, download_plain);
- File file = message.getFile(context);
- Helper.writeText(file, body);
- String text = HtmlHelper.getFullText(body);
- message.preview = HtmlHelper.getPreview(text);
- message.language = HtmlHelper.getLanguage(context, message.subject, text);
-
- DB db = DB.getInstance(context);
- db.message().setMessageContent(message.id,
- true,
- message.language,
- parts.isPlainOnly(download_plain),
- message.preview,
- parts.getWarnings(message.warning));
- }
-
- private static void onAttachment(Context context, JSONArray jargs, EntityFolder folder, EntityMessage message, POP3Folder ifolder, POP3Store istore) throws JSONException, MessagingException, IOException {
- long id = jargs.getLong(0);
-
- if (!EntityFolder.INBOX.equals(folder.type))
- throw new IllegalArgumentException("Not INBOX");
-
- DB db = DB.getInstance(context);
- EntityAttachment attachment = db.attachment().getAttachment(id);
- if (attachment == null)
- throw new IllegalArgumentException("Local attachment not found");
- if (attachment.subsequence != null)
- throw new IllegalArgumentException("Download of sub attachment");
- if (attachment.available)
- return;
-
- Map map = findMessages(context, folder, Arrays.asList(message), istore, ifolder);
- if (map.size() == 0)
- throw new MessageRemovedException("Message not found");
-
- MessageHelper helper = new MessageHelper((MimeMessage) map.entrySet().iterator().next().getValue(), context);
- MessageHelper.MessageParts parts = helper.getMessageParts();
-
- // Download attachment
- parts.downloadAttachment(context, attachment);
-
- if (attachment.size != null)
- EntityLog.log(context, "Operation attachment size=" + attachment.size);
- }
-
- private static void onExists(Context context, JSONArray jargs, EntityAccount account, EntityFolder folder, EntityMessage message, EntityOperation op, IMAPFolder ifolder) throws MessagingException, IOException {
- DB db = DB.getInstance(context);
-
- boolean retry = jargs.optBoolean(0);
-
- if (message.uid != null)
- return;
-
- if (message.msgid == null)
- throw new IllegalArgumentException("exists without msgid");
-
- // Search for message
- Message[] imessages = ifolder.search(new MessageIDTerm(message.msgid));
- if (imessages == null || imessages.length == 0)
- try {
- // Needed for Outlook
- imessages = ifolder.search(
- new AndTerm(
- new SentDateTerm(ComparisonTerm.GE, new Date()),
- new HeaderTerm(MessageHelper.HEADER_CORRELATION_ID, message.msgid)));
- } catch (Throwable ex) {
- Log.e(ex);
- // Seznam: Jakarta Mail Exception: java.io.IOException: Connection dropped by server?
- }
-
- // Some email servers are slow with adding sent messages
- if (retry)
- Log.w(folder.name + " EXISTS retry" +
- " found=" + (imessages == null ? null : imessages.length) +
- " host=" + account.host);
- else if (imessages == null || imessages.length == 0) {
- long next = new Date().getTime() + EXISTS_RETRY_DELAY;
-
- Intent intent = new Intent(context, ServiceSynchronize.class);
- intent.setAction("exists:" + message.id);
- PendingIntent piExists = PendingIntentCompat.getForegroundService(
- context, ServiceSynchronize.PI_EXISTS, intent, PendingIntent.FLAG_UPDATE_CURRENT);
-
- AlarmManager am = Helper.getSystemService(context, AlarmManager.class);
- AlarmManagerCompatEx.setAndAllowWhileIdle(context, am, AlarmManager.RTC_WAKEUP, next, piExists);
- return;
- }
-
- if (imessages != null && imessages.length == 1) {
- String msgid;
- try {
- MessageHelper helper = new MessageHelper((MimeMessage) imessages[0], context);
- msgid = helper.getMessageID();
- } catch (MessagingException ex) {
- Log.e(ex);
- msgid = message.msgid;
- }
- if (Objects.equals(message.msgid, msgid)) {
- db.folder().setFolderAutoAdd(folder.id, false);
- long uid = ifolder.getUID(imessages[0]);
- EntityOperation.queue(context, folder, EntityOperation.FETCH, uid);
- } else {
- db.folder().setFolderAutoAdd(folder.id, true);
- EntityOperation.queue(context, message, EntityOperation.ADD);
- }
- } else {
- db.folder().setFolderAutoAdd(folder.id, true);
- if (imessages != null && imessages.length > 1)
- Log.e(folder.name + " EXISTS messages=" + imessages.length + " retry=" + retry);
- EntityLog.log(context, folder.name +
- " EXISTS messages=" + (imessages == null ? null : imessages.length));
- EntityOperation.queue(context, message, EntityOperation.ADD);
- }
- }
-
- private static void onReport(Context context, JSONArray jargs, EntityFolder folder, IMAPStore istore, IMAPFolder ifolder, State state) throws JSONException, MessagingException {
- String msgid = jargs.getString(0);
- String keyword = jargs.getString(1);
-
- if (TextUtils.isEmpty(msgid))
- throw new IllegalArgumentException("msgid missing");
-
- if (TextUtils.isEmpty(keyword))
- throw new IllegalArgumentException("keyword missing");
-
- if (folder.read_only) {
- Log.w(folder.name + " read-only");
- return;
- }
-
- if (!ifolder.getPermanentFlags().contains(Flags.Flag.USER)) {
- Log.w(folder.name + " has no keywords");
- return;
- }
-
- Message[] imessages = ifolder.search(new MessageIDTerm(msgid));
- if (imessages == null || imessages.length == 0) {
- Log.w(folder.name + " " + msgid + " not found");
- return;
- }
-
- for (Message imessage : imessages) {
- long uid = ifolder.getUID(imessage);
- Log.i("Report uid=" + uid + " keyword=" + keyword);
-
- Flags flags = new Flags(keyword);
- imessage.setFlags(flags, true);
-
- if (BuildConfig.DEBUG)
- try {
- JSONArray fargs = new JSONArray();
- fargs.put(uid);
- onFetch(context, fargs, folder, istore, ifolder, state);
- } catch (Throwable ex) {
- Log.w(ex);
- }
- }
- }
-
- static void onSynchronizeFolders(
- Context context, EntityAccount account, Store istore, State state,
- boolean keep_alive, boolean force) throws MessagingException {
- DB db = DB.getInstance(context);
-
- if (account.protocol != EntityAccount.TYPE_IMAP)
- return;
-
- SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
- boolean sync_folders = prefs.getBoolean("sync_folders", true);
- boolean sync_folders_poll = prefs.getBoolean("sync_folders_poll", false);
- boolean sync_shared_folders = prefs.getBoolean("sync_shared_folders", false);
- boolean sync_added_folders = prefs.getBoolean("sync_added_folders", false);
- Log.i(account.name + " sync folders=" + sync_folders +
- " poll=" + sync_folders_poll +
- " shared=" + sync_shared_folders +
- " added=" + sync_added_folders +
- " keep_alive=" + keep_alive +
- " force=" + force);
-
- if (force)
- sync_folders = true;
- if (keep_alive)
- sync_folders = sync_folders_poll;
- if (!sync_folders)
- sync_shared_folders = false;
-
- // Get folder names
- boolean drafts = false;
- boolean user = false;
- Map local = new HashMap<>();
- List folders = db.folder().getFolders(account.id, false, false);
- for (EntityFolder folder : folders) {
- if (EntityFolder.USER.equals(folder.type))
- user = true;
- if (folder.tbc != null) {
- try {
- // Prefix folder with namespace
- try {
- Folder[] ns = istore.getPersonalNamespaces();
- Folder[] sh = istore.getSharedNamespaces();
- if (ns != null && ns.length == 1 &&
- !(sync_shared_folders && sh != null && sh.length > 0)) {
- String n = ns[0].getFullName();
- // Typically "" or "INBOX"
- if (!TextUtils.isEmpty(n)) {
- n += ns[0].getSeparator();
- if (!folder.name.startsWith(n)) {
- folder.name = n + folder.name;
- db.folder().updateFolder(folder);
- }
- }
- }
- } catch (MessagingException ex) {
- Log.w(ex);
- }
-
- EntityLog.log(context, folder.name + " creating");
- Folder ifolder = istore.getFolder(folder.name);
- if (ifolder.exists())
- EntityLog.log(context, folder.name + " already exists on server");
- else
- try {
- ((IMAPFolder) ifolder).doCommand(new IMAPFolder.ProtocolCommand() {
- @Override
- public Object doCommand(IMAPProtocol protocol) throws ProtocolException {
- protocol.create(folder.name);
- return null;
- }
- });
- ifolder.setSubscribed(true);
- } catch (MessagingException ex) {
- // com.sun.mail.iap.CommandFailedException:
- // K5 NO Client tried to access nonexistent namespace.
- // (Mailbox name should probably be prefixed with: INBOX.) (n.nnn + n.nnn secs).
- // com.sun.mail.iap.CommandFailedException:
- // AN5 NO [OVERQUOTA] Quota exceeded (number of mailboxes exceeded) (n.nnn + n.nnn + n.nnn secs).
- Log.w(ex);
- EntityLog.log(context, folder.name + " creation " +
- ex + "\n" + android.util.Log.getStackTraceString(ex));
- db.account().setAccountError(account.id, Log.formatThrowable(ex));
- }
- local.put(folder.name, folder);
- } finally {
- db.folder().resetFolderTbc(folder.id);
- sync_folders = true;
- }
-
- } else if (folder.rename != null) {
- try {
- EntityLog.log(context, folder.name + " rename into " + folder.rename);
- Folder ifolder = istore.getFolder(folder.name);
- if (ifolder.exists()) {
- // https://tools.ietf.org/html/rfc3501#section-6.3.9
- boolean subscribed = ifolder.isSubscribed();
- if (subscribed)
- ifolder.setSubscribed(false);
-
- Folder itarget = istore.getFolder(folder.rename);
- ifolder.renameTo(itarget);
-
- if (subscribed && folder.selectable)
- try {
- itarget.open(READ_WRITE);
- itarget.setSubscribed(subscribed);
- itarget.close(false);
- } catch (MessagingException ex) {
- Log.w(ex);
- }
-
- db.folder().renameFolder(folder.account, folder.name, folder.rename);
- folder.name = folder.rename;
- }
- } finally {
- db.folder().resetFolderRename(folder.id);
- sync_folders = true;
- }
-
- } else if (folder.tbd != null && folder.tbd) {
- try {
- EntityLog.log(context, folder.name + " deleting server");
- Folder ifolder = istore.getFolder(folder.name);
- if (ifolder.exists()) {
- try {
- ifolder.setSubscribed(false);
- ((IMAPFolder) ifolder).doCommand(new IMAPFolder.ProtocolCommand() {
- @Override
- public Object doCommand(IMAPProtocol protocol) throws ProtocolException {
- protocol.delete(folder.name);
- return null;
- }
- });
- EntityLog.log(context, folder.name + " deleting device");
- db.folder().deleteFolder(folder.id);
- } catch (MessagingException ex) {
- Log.w(ex);
- EntityLog.log(context, folder.name + " deletion " +
- ex + "\n" + android.util.Log.getStackTraceString(ex));
- db.account().setAccountError(account.id, Log.formatThrowable(ex));
- }
- } else
- EntityLog.log(context, folder.name + " does not exist on server anymore");
- } finally {
- db.folder().resetFolderTbd(folder.id);
- sync_folders = true;
- }
-
- } else {
- if (EntityFolder.DRAFTS.equals(folder.type))
- drafts = true;
-
- if (folder.local) {
- if (!EntityFolder.DRAFTS.equals(folder.type)) {
- List ids = db.message().getMessageByFolder(folder.id);
- if (ids == null || ids.size() == 0)
- db.folder().deleteFolder(folder.id);
- }
- } else {
- local.put(folder.name, folder);
- if (folder.selectable && folder.synchronize && folder.initialize != 0)
- sync_folders = true;
- }
- }
-
- String key = "label.color." + folder.name;
- if (folder.color == null)
- prefs.edit().remove(key).apply();
- else
- prefs.edit().putInt(key, folder.color).apply();
- }
- Log.i("Local folder count=" + local.size() + " drafts=" + drafts);
-
- if (!drafts) {
- EntityFolder d = new EntityFolder();
- d.account = account.id;
- d.name = context.getString(R.string.title_folder_local_drafts);
- d.type = EntityFolder.DRAFTS;
- d.local = true;
- d.setProperties();
- d.synchronize = false;
- d.download = false;
- d.sync_days = Integer.MAX_VALUE;
- d.keep_days = Integer.MAX_VALUE;
- db.folder().insertFolder(d);
- }
-
- if (!sync_folders)
- return;
-
- EntityLog.log(context, "Start sync folders account=" + account.name);
-
- // Get default folder
- Folder defaultFolder = istore.getDefaultFolder();
-
- // Get remote folders
- long start = new Date().getTime();
- List> ifolders = new ArrayList<>();
- List subscription = new ArrayList<>();
-
- boolean root = false;
- List personal = new ArrayList<>();
- try {
- Folder[] pnamespaces = istore.getPersonalNamespaces();
- if (pnamespaces != null) {
- personal.addAll(Arrays.asList(pnamespaces));
- for (Folder p : pnamespaces)
- if (defaultFolder.getFullName().equals(p.getFullName())) {
- root = true;
- break;
- }
- }
- } catch (MessagingException ex) {
- Log.e(ex);
- }
-
- if (!root)
- personal.add(defaultFolder);
-
- for (Folder namespace : personal) {
- EntityLog.log(context, "Personal namespace=" + namespace.getFullName());
-
- String pattern = namespace.getFullName() + "*";
- for (Folder ifolder : defaultFolder.list(pattern))
- ifolders.add(new Pair<>(namespace, ifolder));
-
- try {
- Folder[] isubscribed = defaultFolder.listSubscribed(pattern);
- for (Folder ifolder : isubscribed) {
- String fullName = ifolder.getFullName();
- if (TextUtils.isEmpty(fullName)) {
- Log.w("Subscribed folder name empty namespace=" + defaultFolder.getFullName());
- continue;
- }
- subscription.add(fullName);
- Log.i("Subscribed " + fullName);
- }
- } catch (Throwable ex) {
- /*
- 06-21 10:02:38.035 9927 10024 E fairemail: java.lang.NullPointerException: Folder name is null
- 06-21 10:02:38.035 9927 10024 E fairemail: at com.sun.mail.imap.IMAPFolder.(SourceFile:372)
- 06-21 10:02:38.035 9927 10024 E fairemail: at com.sun.mail.imap.IMAPFolder.(SourceFile:411)
- 06-21 10:02:38.035 9927 10024 E fairemail: at com.sun.mail.imap.IMAPStore.newIMAPFolder(SourceFile:1809)
- 06-21 10:02:38.035 9927 10024 E fairemail: at com.sun.mail.imap.DefaultFolder.listSubscribed(SourceFile:89)
- */
- Log.e(account.name, ex);
- }
- }
-
- if (sync_shared_folders) {
- // https://tools.ietf.org/html/rfc2342
- Folder[] shared = istore.getSharedNamespaces();
- EntityLog.log(context, "Shared namespaces=" + shared.length);
-
- for (Folder namespace : shared) {
- EntityLog.log(context, "Shared namespace=" + namespace.getFullName());
-
- String pattern = namespace.getFullName() + "*";
- try {
- for (Folder ifolder : defaultFolder.list(pattern))
- ifolders.add(new Pair<>(namespace, ifolder));
- } catch (FolderNotFoundException ex) {
- Log.w(ex);
- }
-
- try {
- Folder[] isubscribed = namespace.listSubscribed(pattern);
- for (Folder ifolder : isubscribed) {
- String fullName = ifolder.getFullName();
- if (TextUtils.isEmpty(fullName)) {
- Log.e("Subscribed folder name empty namespace=" + namespace.getFullName());
- continue;
- }
- subscription.add(fullName);
- Log.i("Subscribed " + namespace.getFullName() + ":" + fullName);
- }
- } catch (Throwable ex) {
- Log.e(account.name, ex);
- }
- }
- }
-
- long duration = new Date().getTime() - start;
-
- Log.i("Remote folder count=" + ifolders.size() +
- " subscriptions=" + subscription.size() +
- " fetched in " + duration + " ms");
-
- if (ifolders.size() == 0) {
- List ns = new ArrayList<>();
- for (Folder namespace : personal)
- ns.add("'" + namespace.getFullName() + "'");
- Log.e(account.host + " no folders listed" +
- " namespaces=" + TextUtils.join(",", ns));
- return;
- }
-
- // Check if system folders were renamed
- try {
- for (Pair ifolder : ifolders) {
- String fullName = ifolder.second.getFullName();
- if (TextUtils.isEmpty(fullName))
- continue;
-
- String[] attrs = ((IMAPFolder) ifolder.second).getAttributes();
- String type = EntityFolder.getType(attrs, fullName, false);
- if (type != null &&
- !EntityFolder.USER.equals(type) &&
- !EntityFolder.SYSTEM.equals(type)) {
-
- // Rename system folders
- if (!EntityFolder.INBOX.equals(type))
- for (EntityFolder folder : new ArrayList<>(local.values()))
- if (type.equals(folder.type) &&
- !fullName.equals(folder.name) &&
- !local.containsKey(fullName) &&
- !istore.getFolder(folder.name).exists()) {
- Log.e(account.host +
- " renaming " + type + " folder" +
- " from " + folder.name + " to " + fullName);
- local.remove(folder.name);
- local.put(fullName, folder);
- folder.name = fullName;
- db.folder().setFolderName(folder.id, fullName);
- }
-
- // Reselect system folders once
- String key = "unset." + account.id + "." + type;
- boolean unset = prefs.getBoolean(key, false);
- if (!unset) {
- EntityFolder folder = db.folder().getFolderByType(account.id, type);
- if (folder == null) {
- folder = db.folder().getFolderByName(account.id, fullName);
- if (folder != null && !folder.local) {
- Log.e("Reselected " + account.host + " " + type + "=" + fullName);
- folder.type = type;
- folder.setProperties();
- folder.setSpecials(account);
- db.folder().updateFolder(folder);
-
- if (EntityFolder.TRASH.equals(folder.type) &&
- account.swipe_left != null && account.swipe_left > 0) {
- EntityFolder swipe = db.folder().getFolder(account.swipe_left);
- if (swipe == null) {
- Log.e("Reselected " + account.host + " swipe left");
- account.swipe_left = folder.id;
- db.account().setAccountSwipes(account.id,
- account.swipe_left, account.swipe_right);
- }
- }
-
- if (EntityFolder.ARCHIVE.equals(folder.type) &&
- account.swipe_right != null && account.swipe_right > 0) {
- EntityFolder swipe = db.folder().getFolder(account.swipe_right);
- if (swipe == null) {
- Log.e("Reselected " + account.host + " swipe right");
- account.swipe_right = folder.id;
- db.account().setAccountSwipes(account.id,
- account.swipe_left, account.swipe_right);
- }
- }
- }
- }
- }
- }
- }
- } catch (Throwable ex) {
- Log.e(ex);
- }
-
- Map nameFolder = new HashMap<>();
- Map> parentFolders = new HashMap<>();
- for (Pair ifolder : ifolders) {
- String fullName = ifolder.second.getFullName();
- if (TextUtils.isEmpty(fullName)) {
- Log.e("Folder name empty");
- continue;
- }
-
- String[] attrs = ((IMAPFolder) ifolder.second).getAttributes();
- String type = EntityFolder.getType(attrs, fullName, false);
- String subtype = EntityFolder.getSubtype(attrs, fullName);
- boolean subscribed = subscription.contains(fullName);
-
- boolean selectable = true;
- boolean inferiors = true;
- for (String attr : attrs) {
- if (attr.equalsIgnoreCase("\\NoSelect"))
- selectable = false;
- if (attr.equalsIgnoreCase("\\NoInferiors"))
- inferiors = false;
- }
- selectable = selectable && ((ifolder.second.getType() & IMAPFolder.HOLDS_MESSAGES) != 0);
- inferiors = inferiors && ((ifolder.second.getType() & IMAPFolder.HOLDS_FOLDERS) != 0);
-
- if (EntityFolder.INBOX.equals(type))
- selectable = true;
-
- Log.i(account.name + ":" + fullName + " type=" + type + ":" + subtype +
- " subscribed=" + subscribed +
- " selectable=" + selectable +
- " inferiors=" + inferiors +
- " attrs=" + TextUtils.join(" ", attrs));
-
- if (type != null) {
- local.remove(fullName);
-
- EntityFolder folder;
- try {
- db.beginTransaction();
-
- folder = db.folder().getFolderByName(account.id, fullName);
- if (folder == null) {
- EntityFolder parent = null;
- char separator = ifolder.first.getSeparator();
- int sep = fullName.lastIndexOf(separator);
- if (sep > 0)
- parent = db.folder().getFolderByName(account.id, fullName.substring(0, sep));
-
- if (!EntityFolder.USER.equals(type) && !EntityFolder.SYSTEM.equals(type)) {
- EntityFolder has = db.folder().getFolderByType(account.id, type);
- if (has != null)
- type = EntityFolder.USER;
- }
-
- folder = new EntityFolder();
- folder.account = account.id;
- folder.namespace = ifolder.first.getFullName();
- folder.separator = separator;
- folder.name = fullName;
- folder.type = type;
- folder.subtype = type;
- folder.subscribed = subscribed;
- folder.selectable = selectable;
- folder.inferiors = inferiors;
- folder.setProperties();
- folder.setSpecials(account);
-
- if (selectable)
- folder.inheritFrom(parent);
- if (user && sync_added_folders && EntityFolder.USER.equals(type))
- folder.synchronize = true;
-
- folder.id = db.folder().insertFolder(folder);
- Log.i(folder.name + " added type=" + folder.type + " sync=" + folder.synchronize);
- if (folder.synchronize)
- EntityOperation.sync(context, folder.id, false);
- } else {
- Log.i(folder.name + " exists type=" + folder.type);
-
- folder.namespace = ifolder.first.getFullName();
- folder.separator = ifolder.first.getSeparator();
- db.folder().setFolderNamespace(folder.id, folder.namespace, folder.separator);
-
- if (folder.subscribed == null || !folder.subscribed.equals(subscribed))
- db.folder().setFolderSubscribed(folder.id, subscribed);
-
- if (folder.selectable != selectable)
- db.folder().setFolderSelectable(folder.id, selectable);
-
- if (folder.inferiors != inferiors)
- db.folder().setFolderInferiors(folder.id, inferiors);
-
- // Compatibility
- 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);
- else if (EntityFolder.INBOX.equals(type) && !EntityFolder.INBOX.equals(folder.type)) {
- if (db.folder().getFolderByType(folder.account, EntityFolder.INBOX) == null)
- db.folder().setFolderType(folder.id, type);
- }
- db.folder().setFolderSubtype(folder.id, subtype);
- }
-
- db.setTransactionSuccessful();
- } finally {
- db.endTransaction();
- Log.i("End sync folder");
- }
-
- nameFolder.put(folder.name, folder);
- String parentName = folder.getParentName();
- if (!parentFolders.containsKey(parentName))
- parentFolders.put(parentName, new ArrayList<>());
- parentFolders.get(parentName).add(folder);
- }
- }
-
- Log.i("Creating folders parents=" + parentFolders.size());
- for (String parentName : parentFolders.keySet()) {
- EntityFolder parent = nameFolder.get(parentName);
- if (parent == null && parentName != null) {
- parent = db.folder().getFolderByName(account.id, parentName);
- if (parent == null) {
- Log.i("Creating parent name=" + parentName);
- parent = new EntityFolder();
- parent.account = account.id;
- parent.name = parentName;
- parent.type = EntityFolder.SYSTEM;
- parent.subscribed = false;
- parent.selectable = false;
- parent.inferiors = false;
- parent.setProperties();
- parent.display = parentName + "*";
- parent.id = db.folder().insertFolder(parent);
- }
- nameFolder.put(parentName, parent);
- }
- }
-
- Log.i("Updating folders parents=" + parentFolders.size());
- for (String parentName : parentFolders.keySet()) {
- EntityFolder parent = nameFolder.get(parentName);
- for (EntityFolder child : parentFolders.get(parentName)) {
- String rootType = null;
- EntityFolder r = parent;
- while (r != null) {
- rootType = r.type;
- if (!EntityFolder.USER.equals(r.type) && !EntityFolder.SYSTEM.equals(r.type))
- break;
- r = nameFolder.get(r.getParentName());
- }
- if (EntityFolder.USER.equals(rootType) || EntityFolder.SYSTEM.equals(rootType))
- rootType = null;
- db.folder().setFolderInheritedType(child.id, rootType);
- db.folder().setFolderParent(child.id, parent == null ? null : parent.id);
- }
- }
-
- Log.i("Delete local count=" + local.size());
- for (String name : local.keySet()) {
- EntityFolder folder = local.get(name);
- if (EntityFolder.INBOX.equals(folder.type)) {
- Log.e(account.host + " keep inbox");
- continue;
- }
- List childs = parentFolders.get(name);
- if (EntityFolder.USER.equals(folder.type) ||
- childs == null || childs.size() == 0) {
- EntityLog.log(context, name + " delete");
- db.folder().deleteFolder(account.id, name);
- EntityLog.log(context, name + " deleted");
- } else
- Log.w(name + " keep type=" + folder.type);
- }
- }
-
- private static void onSubscribeFolder(Context context, JSONArray jargs, EntityFolder folder, IMAPFolder ifolder)
- throws JSONException, MessagingException {
- boolean subscribe = jargs.getBoolean(0);
- ifolder.setSubscribed(subscribe);
-
- DB db = DB.getInstance(context);
- db.folder().setFolderSubscribed(folder.id, subscribe);
-
- Log.i(folder.name + " subscribed=" + subscribe);
- }
-
- private static void onPurgeFolder(Context context, JSONArray jargs, EntityAccount account, EntityFolder folder, IMAPFolder ifolder) throws MessagingException {
- // Delete all messages from folder
- try {
- DB db = DB.getInstance(context);
- List busy = db.message().getBusyUids(folder.id, new Date().getTime());
-
- Message[] imessages = ifolder.getMessages();
- Log.i(folder.name + " purge=" + imessages.length + " busy=" + busy.size());
-
- FetchProfile fp = new FetchProfile();
- fp.add(UIDFolder.FetchProfileItem.UID);
- ifolder.fetch(imessages, fp);
-
- List idelete = new ArrayList<>();
- for (Message imessage : imessages)
- try {
- long uid = ifolder.getUID(imessage);
- if (!busy.contains(uid))
- idelete.add(imessage);
- } catch (MessageRemovedException ex) {
- Log.w(ex);
- }
-
- EntityLog.log(context, folder.name + " purging=" + idelete.size() + "/" + imessages.length);
- if (account.isYahooJp()) {
- for (Message imessage : new ArrayList<>(idelete))
- try {
- imessage.setFlag(Flags.Flag.DELETED, true);
- } catch (MessagingException mex) {
- Log.w(mex);
- idelete.remove(imessage);
- }
- } else {
- SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
- int chunk_size = prefs.getInt("chunk_size", DEFAULT_CHUNK_SIZE);
-
- Flags flags = new Flags(Flags.Flag.DELETED);
- List iremove = new ArrayList<>();
- for (List list : Helper.chunkList(idelete, chunk_size))
- try {
- ifolder.setFlags(list.toArray(new Message[0]), flags, true);
- } catch (MessagingException ex) {
- Log.w(ex);
- for (Message imessage : list)
- try {
- imessage.setFlag(Flags.Flag.DELETED, true);
- } catch (MessagingException mex) {
- Log.w(mex);
- iremove.add(imessage);
- }
- }
-
- for (Message imessage : iremove)
- idelete.remove(imessage);
- }
- Log.i(folder.name + " purge deleted");
- expunge(context, ifolder, idelete);
- } catch (Throwable ex) {
- Log.e(ex);
- throw ex;
- } finally {
- EntityOperation.sync(context, folder.id, false);
- }
- }
-
- private static void onExpungeFolder(Context context, JSONArray jargs, EntityFolder folder, IMAPFolder ifolder) throws MessagingException {
- Log.i(folder.name + " expunge");
-
- SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
- boolean uid_expunge = prefs.getBoolean("uid_expunge", false);
-
- if (uid_expunge)
- uid_expunge = MessageHelper.hasCapability(ifolder, "UIDPLUS");
-
- if (uid_expunge) {
- DB db = DB.getInstance(context);
-
- List uids = db.message().getDeletedUids(folder.id);
- if (uids == null || uids.size() == 0)
- return;
-
- Log.i(ifolder.getName() + " expunging " + TextUtils.join(",", uids));
- uidExpunge(context, ifolder, uids);
- Log.i(ifolder.getName() + " expunged " + TextUtils.join(",", uids));
- } else
- ifolder.expunge();
- }
-
- private static void onPurgeFolder(Context context, EntityFolder folder) {
- // POP3
- int count = 0;
- int purged = 0;
- do {
- if (count > 0) {
- try {
- Thread.sleep(YIELD_DURATION);
- } catch (InterruptedException ignored) {
- }
- }
-
- DB db = DB.getInstance(context);
- try {
- db.beginTransaction();
- count = db.message().deleteHiddenMessages(folder.id, 100);
- db.setTransactionSuccessful();
- } finally {
- db.endTransaction();
- }
-
- purged += count;
- Log.i(folder.name + " purge count=" + count + "/" + purged);
- } while (count > 0);
- }
-
- private static void onRule(Context context, JSONArray jargs, EntityMessage message) throws JSONException, MessagingException {
- // Deferred rule (download headers, body, etc)
- DB db = DB.getInstance(context);
-
- long id = jargs.getLong(0);
- if (id < 0) {
- List rules = db.rule().getEnabledRules(message.folder, true);
- for (EntityRule rule : rules)
- if (rule.matches(context, message, null, null)) {
- rule.execute(context, message);
- if (rule.stop)
- break;
- }
- } else {
- EntityRule rule = db.rule().getRule(id);
- if (rule == null)
- throw new IllegalArgumentException("Rule not found id=" + id);
-
- if (!message.content)
- throw new IllegalArgumentException("Message without content id=" + rule.id + ":" + rule.name);
-
- rule.execute(context, message);
- }
- }
-
- private static void onDownload(Context context, JSONArray jargs, EntityAccount account, EntityFolder folder, EntityMessage message, IMAPStore istore, IMAPFolder ifolder, State state) throws MessagingException, IOException, JSONException {
- long uid = jargs.getLong(0);
- if (!Objects.equals(uid, message.uid))
- throw new IllegalArgumentException("Different uid" + uid + "/" + message.uid);
-
- MimeMessage imessage = (MimeMessage) ifolder.getMessageByUID(uid);
- downloadMessage(context, account, folder, istore, ifolder, imessage, message.id, state, new SyncStats());
- }
-
- private static void onSynchronizeMessages(
- Context context, JSONArray jargs,
- EntityAccount account, final EntityFolder folder,
- POP3Folder ifolder, POP3Store istore, State state) throws MessagingException, IOException {
- DB db = DB.getInstance(context);
- SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
- boolean sync_quick_pop = prefs.getBoolean("sync_quick_pop", true);
- boolean notify_known = prefs.getBoolean("notify_known", false);
- boolean native_dkim = prefs.getBoolean("native_dkim", false);
- boolean download_eml = prefs.getBoolean("download_eml", false);
- boolean download_plain = prefs.getBoolean("download_plain", false);
- boolean check_blocklist = prefs.getBoolean("check_blocklist", false);
- boolean use_blocklist_pop = prefs.getBoolean("use_blocklist_pop", false);
- boolean pro = ActivityBilling.isPro(context);
-
- boolean force = jargs.optBoolean(5, false);
-
- EntityLog.log(context, account.name + " POP sync type=" + folder.type +
- " quick=" + sync_quick_pop + " force=" + force +
- " connected=" + (ifolder != null));
-
- if (!EntityFolder.INBOX.equals(folder.type)) {
- folder.synchronize = false;
- db.folder().setFolderSynchronize(folder.id, folder.synchronize);
- db.folder().setFolderSyncState(folder.id, null);
- return;
- }
-
- List rules = db.rule().getEnabledRules(folder.id, false);
-
- try {
- db.folder().setFolderSyncState(folder.id, "syncing");
-
- // Get capabilities
- Map caps = istore.capabilities();
- boolean hasUidl = caps.containsKey("UIDL");
- EntityLog.log(context, account.name +
- " POP capabilities= " + caps.keySet() +
- " uidl=" + account.capability_uidl);
-
- if (hasUidl) {
- if (Boolean.FALSE.equals(account.capability_uidl)) {
- hasUidl = false;
- Log.w(account.host + " did not had UIDL before");
- }
- } else {
- account.capability_uidl = false;
- db.account().setAccountUidl(account.id, account.capability_uidl);
- }
-
- // Get messages
- Message[] imessages = ifolder.getMessages();
-
- List ids = db.message().getUidls(folder.id);
- int max = (account.max_messages == null
- ? imessages.length
- : Math.min(imessages.length, Math.abs(account.max_messages)));
- boolean reversed = (account.max_messages != null && account.max_messages < 0);
-
- boolean sync = true;
- if (!hasUidl && sync_quick_pop && !force &&
- imessages.length > 0 && folder.last_sync_count != null &&
- imessages.length == folder.last_sync_count) {
- // Check if last message known as new messages indicator
- MessageHelper helper = new MessageHelper((MimeMessage) imessages[imessages.length - 1], context);
- String msgid = helper.getPOP3MessageID();
- if (msgid != null) {
- int count = db.message().countMessageByMsgId(folder.id, msgid, true);
- if (count == 1) {
- Log.i(account.name + " POP having last msgid=" + msgid);
- sync = false;
- }
- }
- }
-
- EntityLog.log(context, account.name + " POP" +
- " device=" + ids.size() +
- " server=" + imessages.length +
- " max=" + max + "/" + account.max_messages +
- " reversed=" + reversed +
- " last=" + folder.last_sync_count +
- " sync=" + sync +
- " uidl=" + hasUidl);
-
- if (sync) {
- // Index IDs
- Map uidlTuple = new HashMap<>();
- for (TupleUidl id : ids) {
- if (id.uidl != null) {
- if (uidlTuple.containsKey(id.uidl))
- Log.w(account.name + " POP duplicate uidl/msgid=" + id.uidl + "/" + id.msgid);
- uidlTuple.put(id.uidl, id);
- }
- }
-
- Map msgIdTuple = new HashMap<>();
- for (TupleUidl id : ids)
- if (id.msgid != null) {
- if (msgIdTuple.containsKey(id.msgid))
- Log.w(account.name + " POP duplicate msgid/uidl=" + id.msgid + "/" + id.uidl);
- msgIdTuple.put(id.msgid, id);
- }
-
- // Fetch UIDLs
- if (hasUidl) {
- FetchProfile ifetch = new FetchProfile();
- ifetch.add(UIDFolder.FetchProfileItem.UID); // This will fetch all UIDs
- ifolder.fetch(imessages, ifetch);
- }
-
- if (!account.leave_on_device) {
- if (hasUidl) {
- Map known = new HashMap<>();
- for (TupleUidl id : ids)
- if (id.uidl != null)
- known.put(id.uidl, id);
-
- for (Message imessage : imessages) {
- String uidl = ifolder.getUID(imessage);
- if (TextUtils.isEmpty(uidl))
- known.clear(); // better safe than sorry
- else
- known.remove(uidl);
- }
-
- for (TupleUidl uidl : known.values())
- if (!uidl.ui_flagged) {
- EntityLog.log(context, account.name + " POP purging uidl=" + uidl.uidl);
- db.message().deleteMessage(uidl.id);
- }
- } else {
- Map known = new HashMap<>();
- for (TupleUidl id : ids)
- if (id.msgid != null)
- known.put(id.msgid, id);
-
- for (int i = imessages.length - max; i < imessages.length; i++) {
- Message imessage = imessages[i];
- MessageHelper helper = new MessageHelper((MimeMessage) imessage, context);
- String msgid = helper.getPOP3MessageID(); // expensive!
- known.remove(msgid);
- }
-
- for (TupleUidl uidl : known.values())
- if (!uidl.ui_flagged) {
- EntityLog.log(context, account.name + " POP purging msgid=" + uidl.msgid);
- db.message().deleteMessage(uidl.id);
- }
- }
- }
-
- boolean _new = true;
- for (int i = reversed ? 0 : imessages.length - 1; reversed ? i < max : i >= imessages.length - max; i += reversed ? 1 : -1) {
- state.ensureRunning("Sync/POP3");
-
- Message imessage = imessages[i];
- try {
- MessageHelper helper = new MessageHelper((MimeMessage) imessage, context);
-
- String uidl;
- String msgid;
- if (hasUidl) {
- uidl = ifolder.getUID(imessage);
- if (TextUtils.isEmpty(uidl)) {
- EntityLog.log(context, account.name + " POP no uidl");
- continue;
- }
-
- TupleUidl tuple = uidlTuple.get(uidl);
- msgid = (tuple == null ? null : tuple.msgid);
- if (msgid == null) {
- msgid = helper.getMessageID();
- if (TextUtils.isEmpty(msgid))
- msgid = uidl;
- }
- } else {
- uidl = null;
- msgid = helper.getPOP3MessageID();
- }
-
- if (TextUtils.isEmpty(msgid)) {
- EntityLog.log(context, account.name + " POP no msgid uidl=" + uidl);
- continue;
- }
-
- TupleUidl tuple = (hasUidl ? uidlTuple.get(uidl) : msgIdTuple.get(msgid));
- if (tuple != null) {
- if (account.max_messages != null)
- _new = false;
-
- Log.i(account.name + " POP having index=" + i + " " +
- msgid + "=" + msgIdTuple.containsKey(msgid) + "/" +
- uidl + "=" + uidlTuple.containsKey(uidl));
-
- // Restore orphan POP3 moves
- if (tuple.ui_hide &&
- tuple.ui_busy != null &&
- tuple.ui_busy < new Date().getTime())
- db.message().setMessageUiHide(tuple.id, false);
-
- if (download_eml)
- try {
- File raw = EntityMessage.getRawFile(context, tuple.id);
- if (raw.exists())
- continue;
-
- Log.i(account.name + " POP raw " + msgid + "/" + uidl);
- try (OutputStream os = new BufferedOutputStream(new FileOutputStream(raw))) {
- imessage.writeTo(os);
- }
-
- db.message().setMessageRaw(tuple.id, true);
- } catch (Throwable ex) {
- Log.w(ex);
- }
-
- continue;
- }
-
- Long sent = helper.getSent();
- long received = helper.getPOP3Received();
-
- boolean seen = (received <= account.created);
- EntityLog.log(context, account.name + " POP index=" + i + " sync=" + uidl + "/" + msgid +
- " new=" + _new + " seen=" + seen);
-
- String[] authentication = helper.getAuthentication();
- MessageHelper.MessageParts parts = helper.getMessageParts();
-
- EntityMessage message = new EntityMessage();
- message.account = folder.account;
- message.folder = folder.id;
- message.uid = null;
- message.uidl = uidl;
- message.msgid = msgid;
- message.hash = helper.getHash();
- message.references = TextUtils.join(" ", helper.getReferences());
- message.inreplyto = helper.getInReplyTo();
- message.deliveredto = helper.getDeliveredTo();
- message.thread = helper.getThreadId(context, account.id, folder.id, 0, received);
- message.priority = helper.getPriority();
- message.sensitivity = helper.getSensitivity();
- message.auto_submitted = helper.getAutoSubmitted();
- message.receipt_request = helper.getReceiptRequested();
- message.receipt_to = helper.getReceiptTo();
- message.bimi_selector = helper.getBimiSelector();
-
- if (native_dkim && !BuildConfig.PLAY_STORE_RELEASE) {
- List signers = helper.verifyDKIM(context);
- message.signedby = (signers.size() == 0 ? null : TextUtils.join(",", signers));
- }
-
- message.tls = helper.getTLS();
- message.dkim = MessageHelper.getAuthentication("dkim", authentication);
- if (Boolean.TRUE.equals(message.dkim))
- message.dkim = helper.checkDKIMRequirements();
- message.spf = MessageHelper.getAuthentication("spf", authentication);
- if (message.spf == null && helper.getSPF())
- message.spf = true;
- message.dmarc = MessageHelper.getAuthentication("dmarc", authentication);
- message.smtp_from = helper.getMailFrom(authentication);
- message.return_path = helper.getReturnPath();
- message.submitter = helper.getSender();
- message.from = helper.getFrom();
- message.to = helper.getTo();
- message.cc = helper.getCc();
- message.bcc = helper.getBcc();
- message.reply = helper.getReply();
- message.list_post = helper.getListPost();
- message.unsubscribe = helper.getListUnsubscribe();
- message.headers = helper.getHeaders();
- message.infrastructure = helper.getInfrastructure();
- message.subject = helper.getSubject();
- message.size = parts.getBodySize();
- message.total = helper.getSize();
- message.content = false;
- message.encrypt = parts.getEncryption();
- message.ui_encrypt = message.encrypt;
- message.received = received;
- message.sent = sent;
- message.seen = seen;
- message.answered = false;
- message.flagged = false;
- message.flags = null;
- message.keywords = new String[0];
- message.ui_seen = seen;
- message.ui_answered = false;
- message.ui_flagged = false;
- message.ui_hide = false;
- message.ui_found = false;
- message.ui_ignored = !_new;
- message.ui_browsed = false;
-
- if (message.deliveredto != null)
- try {
- Address deliveredto = new InternetAddress(message.deliveredto);
- if (MessageHelper.equalEmail(new Address[]{deliveredto}, message.to))
- message.deliveredto = null;
- } catch (AddressException ex) {
- Log.w(ex);
- }
-
- if (MessageHelper.equalEmail(message.submitter, message.from))
- message.submitter = null;
-
- if (message.size == null && message.total != null)
- message.size = message.total;
-
- EntityIdentity identity = matchIdentity(context, folder, message);
- message.identity = (identity == null ? null : identity.id);
-
- message.sender = MessageHelper.getSortKey(message.from);
- Uri lookupUri = ContactInfo.getLookupUri(message.from);
- message.avatar = (lookupUri == null ? null : lookupUri.toString());
- if (message.avatar == null && notify_known && pro)
- message.ui_ignored = true;
-
- message.from_domain = (message.checkFromDomain(context) == null);
-
- // No reply_domain
- // No MX check
-
- if (check_blocklist && use_blocklist_pop) {
- message.blocklist = DnsBlockList.isJunk(context,
- imessage.getHeader("Received"));
-
- if (message.blocklist == null || !message.blocklist) {
- List senders = new ArrayList<>();
- if (message.reply != null)
- senders.addAll(Arrays.asList(message.reply));
- if (message.from != null)
- senders.addAll(Arrays.asList(message.from));
- message.blocklist = DnsBlockList.isJunk(context, senders);
- }
-
- if (Boolean.TRUE.equals(message.blocklist)) {
- EntityLog.log(context, account.name + " POP blocklist=" +
- MessageHelper.formatAddresses(message.from));
- message.ui_hide = true;
- }
- }
-
- if (message.from != null) {
- EntityContact badboy = null;
- for (Address from : message.from) {
- String email = ((InternetAddress) from).getAddress();
- if (TextUtils.isEmpty(email))
- continue;
-
- badboy = db.contact().getContact(message.account, EntityContact.TYPE_JUNK, email);
- if (badboy != null)
- break;
- }
-
- if (badboy != null) {
- badboy.times_contacted++;
- badboy.last_contacted = new Date().getTime();
- db.contact().updateContact(badboy);
-
- EntityLog.log(context, account.name + " POP blocked=" +
- MessageHelper.formatAddresses(message.from));
-
- message.ui_hide = true;
- }
- }
-
- boolean needsHeaders = EntityRule.needsHeaders(message, rules);
- List headers = (needsHeaders ? helper.getAllHeaders() : null);
- String body = parts.getHtml(context, download_plain);
-
- try {
- db.beginTransaction();
-
- message.id = db.message().insertMessage(message);
- EntityLog.log(context, account.name + " POP added id=" + message.id +
- " uidl/msgid=" + message.uidl + "/" + message.msgid);
-
- int sequence = 1;
- for (EntityAttachment attachment : parts.getAttachments()) {
- Log.i(account.name + " POP attachment seq=" + sequence +
- " name=" + attachment.name + " type=" + attachment.type +
- " cid=" + attachment.cid + " pgp=" + attachment.encryption +
- " size=" + attachment.size);
- attachment.message = message.id;
- attachment.sequence = sequence++;
- attachment.id = db.attachment().insertAttachment(attachment);
- }
-
- runRules(context, headers, body, account, folder, message, rules);
- reportNewMessage(context, account, folder, message);
-
- db.setTransactionSuccessful();
- } finally {
- db.endTransaction();
- }
-
- File file = message.getFile(context);
- Helper.writeText(file, body);
- String text = HtmlHelper.getFullText(body);
- message.preview = HtmlHelper.getPreview(text);
- message.language = HtmlHelper.getLanguage(context, message.subject, text);
- db.message().setMessageContent(message.id,
- true,
- message.language,
- parts.isPlainOnly(download_plain),
- message.preview,
- parts.getWarnings(message.warning));
-
- try {
- for (EntityAttachment attachment : parts.getAttachments())
- if (attachment.subsequence == null)
- parts.downloadAttachment(context, attachment);
- } catch (Throwable ex) {
- Log.w(ex);
- }
-
- if (download_eml)
- try {
- Log.i(account.name + " POP raw " + msgid + "/" + uidl);
-
- File raw = message.getRawFile(context);
- try (OutputStream os = new BufferedOutputStream(new FileOutputStream(raw))) {
- imessage.writeTo(os);
- }
-
- message.raw = true;
- db.message().setMessageRaw(message.id, message.raw);
- } catch (Throwable ex) {
- Log.w(ex);
- }
-
- if (!account.leave_on_server && account.client_delete)
- imessage.setFlag(Flags.Flag.DELETED, true);
-
- EntityContact.received(context, account, folder, message);
- } catch (FolderClosedException ex) {
- throw ex;
- } catch (Throwable ex) {
- Log.e(ex);
- db.folder().setFolderError(folder.id, Log.formatThrowable(ex));
- //if (!(ex instanceof MessagingException))
- throw ex;
-
- /*
- javax.mail.MessagingException: error loading POP3 headers;
- nested exception is:
- java.io.IOException: Unexpected response: ...
- at com.sun.mail.pop3.POP3Message.loadHeaders(SourceFile:15)
- at com.sun.mail.pop3.POP3Message.getHeader(SourceFile:5)
- at eu.faircode.email.MessageHelper.getMessageID(SourceFile:2)
- at eu.faircode.email.Core.onSynchronizeMessages(SourceFile:78)
- at eu.faircode.email.Core.processOperations(SourceFile:89)
- at eu.faircode.email.ServiceSynchronize$19$1$2.run(SourceFile:51)
- at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:462)
- at java.util.concurrent.FutureTask.run(FutureTask.java:266)
- at eu.faircode.email.Helper$PriorityFuture.run(SourceFile:1)
- at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
- at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
- at java.lang.Thread.run(Thread.java:923)
- Caused by: java.io.IOException: Unexpected response: Bd04v8G0fQOraFZwxNDLapHDdRM0xj8oW+4nG4FVG05/WuE/sW8i3xxzx3unQBWtyhU3KDqQSDzz
- at com.sun.mail.pop3.Protocol.readResponse(SourceFile:12)
- at com.sun.mail.pop3.Protocol.multilineCommand(SourceFile:3)
- at com.sun.mail.pop3.Protocol.top(SourceFile:1)
- at com.sun.mail.pop3.POP3Message.loadHeaders(SourceFile:5)
- */
- } finally {
- ((POP3Message) imessage).invalidate(true);
- }
- }
- }
-
- if (account.max_messages != null && !account.leave_on_device) {
- int hidden = db.message().setMessagesUiHide(folder.id, Math.abs(account.max_messages));
- int deleted = db.message().deleteMessagesKeep(folder.id, Math.abs(account.max_messages) + 100);
- EntityLog.log(context, account.name + " POP" +
- " cleanup max=" + account.max_messages + "" +
- " hidden=" + hidden + " deleted=" + deleted);
- }
-
- folder.last_sync_count = imessages.length;
- db.folder().setFolderLastSyncCount(folder.id, folder.last_sync_count);
- db.folder().setFolderLastSync(folder.id, new Date().getTime());
- EntityLog.log(context, account.name + " POP done");
- } finally {
- db.folder().setFolderSyncState(folder.id, null);
- }
- }
-
- private static void onSynchronizeMessages(
- Context context, JSONArray jargs,
- EntityAccount account, final EntityFolder folder,
- IMAPStore istore, final IMAPFolder ifolder, State state)
- throws JSONException, MessagingException, IOException {
- final DB db = DB.getInstance(context);
- try {
- SyncStats stats = new SyncStats();
-
- // Legacy
- if (jargs.length() == 0)
- jargs = folder.getSyncArgs(false);
-
- int sync_days = jargs.getInt(0);
- int keep_days = jargs.getInt(1);
- boolean download = jargs.optBoolean(2, false);
- boolean auto_delete = jargs.optBoolean(3, false);
- int initialize = jargs.optInt(4, folder.initialize);
- boolean force = jargs.optBoolean(5, false);
-
- if (keep_days == sync_days && keep_days != Integer.MAX_VALUE)
- keep_days++;
-
- SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
- boolean sync_quick_imap = prefs.getBoolean("sync_quick_imap", false);
- boolean sync_nodate = prefs.getBoolean("sync_nodate", false);
- boolean sync_unseen = prefs.getBoolean("sync_unseen", false);
- boolean sync_flagged = prefs.getBoolean("sync_flagged", false);
- boolean sync_kept = prefs.getBoolean("sync_kept", true);
- boolean delete_unseen = prefs.getBoolean("delete_unseen", false);
- boolean use_modseq = prefs.getBoolean("use_modseq", true);
- boolean perform_expunge = prefs.getBoolean("perform_expunge", true);
- boolean log = prefs.getBoolean("protocol", false);
-
- if (account.isYahoo() || account.isAol())
- sync_nodate = false;
-
- if (account.isZoho()) {
- sync_unseen = false;
- sync_flagged = false;
- }
-
- Log.i(folder.name + " start sync after=" + sync_days + "/" + keep_days +
- " quick=" + sync_quick_imap + " force=" + force +
- " sync unseen=" + sync_unseen + " flagged=" + sync_flagged +
- " delete unseen=" + delete_unseen + " kept=" + sync_kept);
-
- if (folder.local) {
- folder.synchronize = false;
- db.folder().setFolderSynchronize(folder.id, folder.synchronize);
- db.folder().setFolderSyncState(folder.id, null);
- return;
- }
-
- db.folder().setFolderSyncState(folder.id, "syncing");
-
- String[] userFlags = ifolder.getPermanentFlags().getUserFlags();
- if (userFlags != null && userFlags.length > 0) {
- List keywords = new ArrayList<>(Arrays.asList(userFlags));
- Collections.sort(keywords);
- userFlags = keywords.toArray(new String[0]);
- if (!Arrays.equals(folder.keywords, userFlags)) {
- Log.i(folder.name + " updating flags=" + TextUtils.join(",", userFlags));
- folder.keywords = userFlags;
- db.folder().setFolderKeywords(folder.id, DB.Converters.fromStringArray(userFlags));
- }
- }
-
- // Check uid validity
- try {
- long uidv = ifolder.getUIDValidity();
- if (folder.uidv != null && !folder.uidv.equals(uidv)) {
- Log.w(folder.name + " uid validity changed from " + folder.uidv + " to " + uidv);
- db.message().deleteLocalMessages(folder.id);
- }
- folder.uidv = uidv;
- db.folder().setFolderUidValidity(folder.id, uidv);
- } catch (MessagingException ex) {
- Log.w(folder.name, ex);
- }
-
- // https://tools.ietf.org/html/rfc4551
- // https://wiki.mozilla.org/Thunderbird:IMAP_RFC_4551_Implementation
- Long modseq = null;
- boolean modified = true;
- if (use_modseq)
- try {
- if (MessageHelper.hasCapability(ifolder, "CONDSTORE")) {
- modseq = ifolder.getHighestModSeq();
- if (modseq < 0)
- modseq = null;
- modified = (force || initialize != 0 ||
- folder.modseq == null || !folder.modseq.equals(modseq));
- EntityLog.log(context, folder.name + " modseq=" + modseq + "/" + folder.modseq +
- " force=" + force + " init=" + (initialize != 0) + " modified=" + modified);
- }
- } catch (MessagingException ex) {
- Log.w(folder.name, ex);
- }
-
- // 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
- if (auto_delete) {
- List tbds = db.message().getMessagesBefore(folder.id, sync_time, keep_time, delete_unseen);
- Log.i(folder.name + " local tbd=" + tbds.size());
- EntityFolder trash = db.folder().getFolderByType(folder.account, EntityFolder.TRASH);
- for (Long tbd : tbds) {
- EntityMessage message = db.message().getMessage(tbd);
- if (message != null && trash != null)
- if (EntityFolder.TRASH.equals(folder.type) ||
- EntityFolder.JUNK.equals(folder.type))
- EntityOperation.queue(context, message, EntityOperation.DELETE);
- else
- EntityOperation.queue(context, message, EntityOperation.MOVE, trash.id);
- }
- } else {
- int old = db.message().deleteMessagesBefore(folder.id, sync_time, keep_time, delete_unseen);
- Log.i(folder.name + " local old=" + old);
- }
-
- Message[] imessages;
- long search;
- Long[] ids;
- if (modified || !sync_quick_imap || force) {
- // Get list of local uids
- final List uids = db.message().getUids(folder.id, sync_kept || force ? null : sync_time);
- Log.i(folder.name + " local count=" + uids.size());
-
- if (BuildConfig.DEBUG || log)
- try {
- Status status = (Status) ifolder.doCommand(new IMAPFolder.ProtocolCommand() {
- @Override
- public Object doCommand(IMAPProtocol protocol) throws ProtocolException {
- return protocol.status(ifolder.getFullName(), null);
- }
- });
- EntityLog.log(context, EntityLog.Type.Protocol, folder.name + " status" +
- " total=" + status.total + " unseen=" + status.unseen);
- } catch (Throwable ex) {
- Log.w(ex);
- }
-
- // Reduce list of local uids
- SearchTerm dateTerm = account.use_date
- ? new SentDateTerm(ComparisonTerm.GE, new Date(sync_time))
- : new ReceivedDateTerm(ComparisonTerm.GE, new Date(sync_time));
-
- SearchTerm searchTerm = dateTerm;
- Flags flags = ifolder.getPermanentFlags();
- if (sync_nodate && !account.isOutlook())
- searchTerm = new OrTerm(searchTerm, new ReceivedDateTerm(ComparisonTerm.LT, new Date(365 * 24 * 3600 * 1000L)));
- if (sync_unseen && flags.contains(Flags.Flag.SEEN))
- searchTerm = new OrTerm(searchTerm, new FlagTerm(new Flags(Flags.Flag.SEEN), false));
- if (sync_flagged && flags.contains(Flags.Flag.FLAGGED))
- searchTerm = new OrTerm(searchTerm, new FlagTerm(new Flags(Flags.Flag.FLAGGED), true));
-
- search = SystemClock.elapsedRealtime();
- if (sync_time == 0)
- imessages = ifolder.getMessages();
- else
- try {
- imessages = ifolder.search(searchTerm);
- } catch (MessagingException ex) {
- Log.w(ex);
- // Fallback to date only search
- // BAD Could not parse command
- imessages = ifolder.search(dateTerm);
- }
- if (imessages == null)
- imessages = new Message[0];
-
- for (Message imessage : imessages)
- if (imessage instanceof IMAPMessage)
- ((IMAPMessage) imessage).invalidateHeaders();
-
- stats.search_ms = (SystemClock.elapsedRealtime() - search);
- Log.i(folder.name + " remote count=" + imessages.length + " search=" + stats.search_ms + " ms");
-
- ids = new Long[imessages.length];
-
- if (!modified) {
- Log.i(folder.name + " quick check");
- long fetch = SystemClock.elapsedRealtime();
-
- FetchProfile fp = new FetchProfile();
- fp.add(UIDFolder.FetchProfileItem.UID);
- ifolder.fetch(imessages, fp);
-
- stats.flags = imessages.length;
- stats.flags_ms = (SystemClock.elapsedRealtime() - fetch);
- Log.i(folder.name + " remote fetched=" + stats.flags_ms + " ms");
-
- for (int i = 0; i < imessages.length; i++) {
- state.ensureRunning("Sync/IMAP/check");
-
- try {
- long uid = ifolder.getUID(imessages[i]);
- EntityMessage message = db.message().getMessageByUid(folder.id, uid);
- ids[i] = (message == null ? null : message.id);
- if (message == null || message.ui_hide) {
- Log.i(folder.name + " missing uid=" + uid);
- modified = true;
- break;
- } else
- uids.remove(uid);
- } catch (Throwable ex) {
- Log.w(ex);
- modified = true;
- modseq = null;
- }
- }
-
- if (uids.size() > 0) {
- Log.i(folder.name + " remaining=" + uids.size());
- modified = true;
- }
-
- EntityLog.log(context, folder.name + " modified=" + modified);
- }
-
- if (modified) {
- long fetch = SystemClock.elapsedRealtime();
-
- FetchProfile fp = new FetchProfile();
- fp.add(UIDFolder.FetchProfileItem.UID); // To check if message exists
- fp.add(FetchProfile.Item.FLAGS); // To update existing messages
- if (account.isGmail())
- fp.add(GmailFolder.FetchProfileItem.LABELS);
- ifolder.fetch(imessages, fp);
-
- stats.flags = imessages.length;
- stats.flags_ms = (SystemClock.elapsedRealtime() - fetch);
- Log.i(folder.name + " remote fetched=" + stats.flags_ms + " ms");
-
- // Sort for finding referenced/replied-to messages
- // Sorting on date/time would be better, but requires fetching the headers
- Arrays.sort(imessages, new Comparator() {
- @Override
- public int compare(Message m1, Message m2) {
- try {
- return Long.compare(ifolder.getUID(m1), ifolder.getUID(m2));
- } catch (MessagingException ex) {
- return 0;
- }
- }
- });
-
- List deleted = new ArrayList<>();
- for (int i = 0; i < imessages.length; i++) {
- state.ensureRunning("Sync/IMAP/delete");
-
- try {
- if (perform_expunge && imessages[i].isSet(Flags.Flag.DELETED))
- deleted.add(imessages[i]);
- else
- uids.remove(ifolder.getUID(imessages[i]));
- } catch (MessageRemovedException ex) {
- Log.w(folder.name, ex);
- } catch (FolderClosedException ex) {
- throw ex;
- } catch (Throwable ex) {
- Log.e(folder.name, ex);
- modseq = null;
- EntityLog.log(context, folder.name + " expunge " + Log.formatThrowable(ex, false));
- db.folder().setFolderError(folder.id, Log.formatThrowable(ex));
- }
- }
-
- expunge(context, ifolder, deleted);
-
- if (uids.size() > 0) {
- // This is done outside of JavaMail to prevent changed notifications
- if (!ifolder.isOpen())
- throw new FolderClosedException(ifolder, "UID FETCH");
-
- long getuid = SystemClock.elapsedRealtime();
- MessagingException ex = (MessagingException) ifolder.doCommand(new IMAPFolder.ProtocolCommand() {
- @Override
- public Object doCommand(IMAPProtocol protocol) throws ProtocolException {
- protocol.select(folder.name);
-
- // Build ranges
- List> ranges = new ArrayList<>();
- long first = -1;
- long last = -1;
- for (long uid : uids)
- if (first < 0)
- first = uid;
- else if ((last < 0 ? first : last) + 1 == uid)
- last = uid;
- else {
- ranges.add(new Pair<>(first, last < 0 ? first : last));
- first = uid;
- last = -1;
- }
- if (first > 0)
- ranges.add(new Pair<>(first, last < 0 ? first : last));
-
- // https://datatracker.ietf.org/doc/html/rfc2683#section-3.2.1.5
- int chunk_size = prefs.getInt("chunk_size", DEFAULT_CHUNK_SIZE);
- if (chunk_size < 200 &&
- (account.isGmail() || account.isOutlook()))
- chunk_size = 200;
- List>> chunks = Helper.chunkList(ranges, chunk_size);
-
- Log.i(folder.name + " executing uid fetch count=" + uids.size() +
- " ranges=" + ranges.size() + " chunks=" + chunks.size());
- for (int c = 0; c < chunks.size(); c++) {
- List> chunk = chunks.get(c);
- Log.i(folder.name + " chunk #" + c + " size=" + chunk.size());
-
- StringBuilder sb = new StringBuilder();
- for (Pair range : chunk) {
- if (sb.length() > 0)
- sb.append(',');
- if (range.first.equals(range.second))
- sb.append(range.first);
- else
- sb.append(range.first).append(':').append(range.second);
- }
- String command = "UID FETCH " + sb + " (UID FLAGS)";
- Response[] responses = protocol.command(command, null);
-
- if (responses.length > 0 && responses[responses.length - 1].isOK()) {
- for (Response response : responses)
- if (response instanceof FetchResponse) {
- FetchResponse fr = (FetchResponse) response;
- UID uid = fr.getItem(UID.class);
- FLAGS flags = fr.getItem(FLAGS.class);
- if (uid == null || flags == null)
- continue;
- if (perform_expunge && flags.contains(Flags.Flag.DELETED))
- continue;
-
- uids.remove(uid.uid);
-
- if (force) {
- EntityMessage message = db.message().getMessageByUid(folder.id, uid.uid);
- if (message != null) {
- boolean update = false;
- boolean recent = flags.contains(Flags.Flag.RECENT);
- boolean seen = flags.contains(Flags.Flag.SEEN);
- boolean answered = flags.contains(Flags.Flag.ANSWERED);
- boolean flagged = flags.contains(Flags.Flag.FLAGGED);
- boolean deleted = flags.contains(Flags.Flag.DELETED);
- if (message.recent != recent) {
- update = true;
- message.recent = recent;
- Log.i("UID fetch recent=" + recent);
- }
- if (message.seen != seen) {
- update = true;
- message.seen = seen;
- message.ui_seen = seen;
- Log.i("UID fetch seen=" + seen);
- }
- if (message.answered != answered) {
- update = true;
- message.answered = answered;
- message.ui_answered = answered;
- Log.i("UID fetch answered=" + answered);
- }
- if (message.flagged != flagged) {
- update = true;
- message.flagged = flagged;
- message.ui_flagged = flagged;
- Log.i("UID fetch flagged=" + flagged);
- }
- if (message.deleted != deleted) {
- update = true;
- message.deleted = deleted;
- message.ui_deleted = deleted;
- message.ui_ignored = deleted;
- Log.i("UID fetch deleted=" + deleted);
- }
-
- if (update)
- db.message().updateMessage(message);
- }
- }
- }
- } else {
- for (Response response : responses)
- if (response.isBYE())
- return new MessagingException("UID FETCH", new IOException(response.toString()));
- else if (response.isNO()) {
- Log.e("UID FETCH " + response);
- throw new CommandFailedException(response);
- } else if (response.isBAD()) {
- Log.e("UID FETCH " + response);
- // BAD Error in IMAP command UID FETCH: Too long argument (n.nnn + n.nnn + n.nnn secs).
- if (response.toString().contains("Too long argument")) {
- chunk_size = chunk_size / 2;
- if (chunk_size > 0)
- prefs.edit().putInt("chunk_size", chunk_size).apply();
- }
- throw new BadCommandException(response);
- }
- throw new ProtocolException("UID FETCH failed");
- }
- }
-
- return null;
- }
- });
- if (ex != null)
- throw ex;
-
- stats.uids = uids.size();
- stats.uids_ms = (SystemClock.elapsedRealtime() - getuid);
- Log.i(folder.name + " remote uids=" + stats.uids_ms + " 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, false);
-
- 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);
- if (account.isGmail())
- fp.add(GmailFolder.FetchProfileItem.THRID);
-
- // Add/update local messages
- DutyCycle dc = new DutyCycle(account.name + " sync");
- Log.i(folder.name + " add=" + imessages.length);
- for (int i = imessages.length - 1; i >= 0; i -= SYNC_BATCH_SIZE) {
- state.ensureRunning("Sync/IMAP/sync/fetch");
-
- 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); // already fetched
- 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);
- stats.headers += full.size();
- stats.headers_ms += (SystemClock.elapsedRealtime() - headers);
- Log.i(folder.name + " fetched headers=" + full.size() + " " + stats.headers_ms + " ms");
- }
-
- int free = Log.getFreeMemMb();
- Map crumb = new HashMap<>();
- crumb.put("account", account.id + ":" + account.protocol);
- crumb.put("folder", folder.id + ":" + folder.type);
- crumb.put("start", Integer.toString(from));
- crumb.put("end", Integer.toString(i));
- crumb.put("partial", Boolean.toString(account.partial_fetch));
- Log.breadcrumb("sync", crumb);
- Log.i("Sync " + from + ".." + i + " free=" + free);
-
- for (int j = isub.length - 1; j >= 0; j--) {
- state.ensureRunning("Sync/IMAP/sync");
-
- try {
- dc.start();
-
- // Some providers erroneously return old messages
- if (full.contains(isub[j]))
- try {
- Date received = isub[j].getReceivedDate();
- if (received == null || received.getTime() == 0)
- received = isub[j].getSentDate();
- boolean unseen = (sync_unseen && !isub[j].isSet(Flags.Flag.SEEN));
- boolean flagged = (sync_flagged && isub[j].isSet(Flags.Flag.FLAGGED));
- if (received != null && received.getTime() < keep_time && !unseen && !flagged) {
- long uid = ifolder.getUID(isub[j]);
- Log.i(folder.name + " Skipping old uid=" + uid + " date=" + received);
- ids[from + j] = null;
- continue;
- }
- } catch (Throwable ex) {
- Log.w(ex);
- }
-
- EntityMessage message = synchronizeMessage(
- context,
- account, folder,
- istore, ifolder, (MimeMessage) isub[j],
- false, download && initialize == 0,
- rules, state, stats);
- ids[from + j] = (message == null || message.ui_hide ? null : message.id);
- } 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);
- modseq = null;
- db.folder().setFolderError(folder.id, Log.formatThrowable(ex));
- } else
- throw ex;
- } catch (Throwable ex) {
- Log.e(folder.name, ex);
- modseq = null;
- db.folder().setFolderError(folder.id, Log.formatThrowable(ex));
- } finally {
- // Free memory
- isub[j] = null;
- dc.stop(state.getForeground(), from == 0 && j == 0);
- }
- }
- }
- }
-
- // Delete not synchronized messages without uid
- if (!EntityFolder.isOutgoing(folder.type)) {
- int orphans = db.message().deleteOrphans(folder.id, new Date().getTime());
- Log.i(folder.name + " deleted orphans=" + orphans);
- }
- } else {
- List _ids = new ArrayList<>();
- List _uids = new ArrayList<>();
-
- if (download && initialize == 0) {
- List messages = db.message().getMessagesWithoutContent(
- folder.id, sync_kept || force ? null : sync_time);
- if (messages != null) {
- Log.i(folder.name + " needs content=" + messages.size());
- for (EntityMessage message : messages) {
- _ids.add(message.id);
- _uids.add(message.uid);
- }
- }
- }
-
- // This will result in message changed events
- imessages = ifolder.getMessagesByUID(Helper.toLongArray(_uids));
- ids = _ids.toArray(new Long[0]);
-
- search = SystemClock.elapsedRealtime();
- }
-
- // Update modseq
- folder.modseq = modseq;
- Log.i(folder.name + " set modseq=" + modseq);
- db.folder().setFolderModSeq(folder.id, folder.modseq);
-
- // Update stats
- int count = MessageHelper.getMessageCount(ifolder);
- db.folder().setFolderTotal(folder.id, count < 0 ? null : count);
- account.last_connected = new Date().getTime();
- db.account().setAccountConnected(account.id, account.last_connected);
-
- if (download && initialize == 0) {
- db.folder().setFolderSyncState(folder.id, "downloading");
-
- // Download messages/attachments
- DutyCycle dc = new DutyCycle(account.name + " download");
- Log.i(folder.name + " download=" + imessages.length);
- for (int i = imessages.length - 1; i >= 0; i -= DOWNLOAD_BATCH_SIZE) {
- state.ensureRunning("Sync/IMAP/download/fetch");
-
- int from = Math.max(0, i - DOWNLOAD_BATCH_SIZE + 1);
- Message[] isub = Arrays.copyOfRange(imessages, from, i + 1);
- Arrays.fill(imessages, from, i + 1, null);
- // Fetch on demand
-
- int free = Log.getFreeMemMb();
- Map crumb = new HashMap<>();
- crumb.put("account", account.id + ":" + account.protocol);
- crumb.put("folder", folder.id + ":" + folder.type);
- crumb.put("start", Integer.toString(from));
- crumb.put("end", Integer.toString(i));
- crumb.put("partial", Boolean.toString(account.partial_fetch));
- Log.breadcrumb("download", crumb);
- Log.i("Download " + from + ".." + i + " free=" + free);
-
- for (int j = isub.length - 1; j >= 0; j--) {
- state.ensureRunning("Sync/IMAP/download");
-
- try {
- dc.start();
- if (ids[from + j] != null)
- downloadMessage(
- context,
- account, folder,
- istore, ifolder,
- (MimeMessage) isub[j], ids[from + j],
- state, stats);
- } catch (FolderClosedException ex) {
- throw ex;
- } catch (Throwable ex) {
- Log.e(folder.name, ex);
- } finally {
- // Free memory
- isub[j] = null;
- dc.stop(state.getForeground(), from == 0 && j == 0);
- }
- }
- }
- }
-
- if (state.running && initialize != 0) {
- jargs.put(4, 0);
- folder.initialize = 0;
- db.folder().setFolderInitialize(folder.id, 0);
-
- // Schedule download
- if (download) {
- EntityOperation operation = new EntityOperation();
- operation.account = folder.account;
- operation.folder = folder.id;
- operation.message = null;
- operation.name = EntityOperation.SYNC;
- operation.args = jargs.toString();
- operation.created = new Date().getTime();
- operation.id = db.operation().insertOperation(operation);
- }
- }
-
- db.folder().setFolderLastSync(folder.id, new Date().getTime());
- //db.folder().setFolderError(folder.id, null);
-
- stats.total = (SystemClock.elapsedRealtime() - search);
-
- EntityLog.log(context, EntityLog.Type.Statistics,
- account.name + "/" + folder.name + " sync stats " + stats);
- } finally {
- Log.i(folder.name + " end sync state=" + state);
- db.folder().setFolderSyncState(folder.id, null);
- }
- }
-
- static EntityMessage synchronizeMessage(
- Context context,
- EntityAccount account, EntityFolder folder,
- IMAPStore istore, IMAPFolder ifolder, MimeMessage imessage,
- boolean browsed, boolean download,
- List rules, State state, SyncStats stats) throws MessagingException, IOException {
- DB db = DB.getInstance(context);
- SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
- boolean download_headers = prefs.getBoolean("download_headers", false);
- boolean download_plain = prefs.getBoolean("download_plain", false);
- boolean notify_known = prefs.getBoolean("notify_known", false);
- boolean native_dkim = prefs.getBoolean("native_dkim", false);
- boolean experiments = prefs.getBoolean("experiments", false);
- boolean pro = ActivityBilling.isPro(context);
-
- long uid = ifolder.getUID(imessage);
- if (uid < 0) {
- Log.w(folder.name + " invalid uid=" + uid);
- throw new MessageRemovedException("uid");
- }
-
- if (imessage.isExpunged()) {
- Log.w(folder.name + " expunged uid=" + uid);
- throw new MessageRemovedException("Expunged");
- }
-
- if (imessage.isSet(Flags.Flag.DELETED)) {
- Log.w(folder.name + " deleted uid=" + uid);
- if (expunge(context, ifolder, Arrays.asList(imessage)))
- throw new MessageRemovedException("Deleted");
- }
-
- MessageHelper helper = new MessageHelper(imessage, context);
- boolean recent = helper.getRecent();
- boolean seen = helper.getSeen();
- boolean answered = helper.getAnswered();
- boolean flagged = helper.getFlagged();
- boolean deleted = helper.getDeleted();
- String flags = helper.getFlags();
- String[] keywords = helper.getKeywords();
- String[] labels = helper.getLabels();
- boolean update = false;
- boolean process = false;
- boolean syncSimilar = false;
-
- // 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
- boolean have = false;
- Integer color = null;
- String notes = null;
- Integer notes_color = null;
- if (message == null) {
- String msgid = helper.getMessageID();
- Log.i(folder.name + " searching for " + msgid);
- List dups = db.message().getMessagesByMsgId(folder.account, msgid);
- for (EntityMessage dup : dups) {
- 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 (!EntityFolder.JUNK.equals(dfolder.type))
- have = true;
-
- if (dup.folder.equals(folder.id)) {
- String thread = helper.getThreadId(context, account.id, folder.id, uid, dup.received);
- Log.i(folder.name + " found as id=" + dup.id +
- " uid=" + dup.uid + "/" + uid +
- " msgid=" + msgid + " thread=" + thread);
-
- if (dup.uid == null) {
- Log.i(folder.name + " set uid=" + uid);
- dup.uid = uid;
- if (dup.thread == null)
- dup.thread = thread;
-
- if (EntityFolder.SENT.equals(folder.type) &&
- (folder.auto_add == null || !folder.auto_add)) {
- Long sent = helper.getSent();
- Long received = helper.getReceived();
- if (sent != null)
- dup.sent = sent;
- if (received != null)
- dup.received = received;
- }
-
- dup.error = null;
-
- message = dup;
- process = true;
- } else if (msgid != null && EntityFolder.DRAFTS.equals(folder.type)) {
- try {
- if (dup.uid < uid) {
- MimeMessage existing = (MimeMessage) ifolder.getMessageByUID(dup.uid);
- if (existing != null &&
- msgid.equals(existing.getHeader(MessageHelper.HEADER_CORRELATION_ID, null))) {
- Log.e(folder.name + " late draft" +
- " host=" + account.host + " uid=" + dup.uid + "<" + uid);
- existing.setFlag(Flags.Flag.DELETED, true);
- expunge(context, ifolder, Arrays.asList(existing));
- db.message().setMessageUiHide(dup.id, true);
- }
- } else if (dup.uid > uid) {
- if (msgid.equals(imessage.getHeader(MessageHelper.HEADER_CORRELATION_ID, null))) {
- Log.e(folder.name + " late draft" +
- " host=" + account.host + " uid=" + dup.uid + ">" + uid);
- imessage.setFlag(Flags.Flag.DELETED, true);
- expunge(context, ifolder, Arrays.asList(imessage));
- return null;
- }
- }
- } catch (Throwable ex) {
- Log.e(ex);
- }
- }
- }
-
- if (dup.recent != recent || dup.seen != seen || dup.answered != answered || dup.flagged != flagged)
- syncSimilar = true;
-
- if (dup.flagged && dup.color != null)
- color = dup.color;
- if (dup.notes != null) {
- notes = dup.notes;
- notes_color = dup.notes_color;
- }
- }
- }
-
- if (message == null) {
- Long sent = helper.getSent();
-
- Long received;
- long future = new Date().getTime() + FUTURE_RECEIVED;
- if (account.use_date) {
- received = sent;
- if (received == null || received == 0 || received > future)
- received = helper.getReceived();
- if (received == null || received == 0 || received > future)
- received = helper.getReceivedHeader();
- } else if (account.use_received) {
- received = helper.getReceivedHeader();
- if (received == null || received == 0 || received > future)
- received = helper.getReceived();
- } else {
- received = helper.getReceived();
- if (received == null || received == 0 || received > future)
- received = helper.getReceivedHeader();
- }
- if (received == null || received == 0)
- received = sent;
- if (received == null)
- received = 0L;
-
- String[] authentication = helper.getAuthentication();
- MessageHelper.MessageParts parts = helper.getMessageParts();
-
- message = new EntityMessage();
- message.account = folder.account;
- message.folder = folder.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.hash = helper.getHash();
- message.references = TextUtils.join(" ", helper.getReferences());
- message.inreplyto = helper.getInReplyTo();
- // Local address contains control or whitespace in string ``mailing list someone@example.org''
- message.deliveredto = helper.getDeliveredTo();
- message.thread = helper.getThreadId(context, account.id, folder.id, uid, received);
- if (BuildConfig.DEBUG && message.thread.startsWith("outlook:"))
- message.warning = message.thread;
- message.priority = helper.getPriority();
- message.sensitivity = helper.getSensitivity();
-
- for (String keyword : keywords)
- if (MessageHelper.FLAG_LOW_IMPORTANCE.equals(keyword))
- message.importance = EntityMessage.PRIORITIY_LOW;
- else if (MessageHelper.FLAG_HIGH_IMPORTANCE.equals(keyword))
- message.importance = EntityMessage.PRIORITIY_HIGH;
-
- message.auto_submitted = helper.getAutoSubmitted();
- message.receipt_request = helper.getReceiptRequested();
- message.receipt_to = helper.getReceiptTo();
- message.bimi_selector = helper.getBimiSelector();
-
- if (native_dkim && !BuildConfig.PLAY_STORE_RELEASE) {
- List signers = helper.verifyDKIM(context);
- message.signedby = (signers.size() == 0 ? null : TextUtils.join(",", signers));
- }
-
- message.tls = helper.getTLS();
- message.dkim = MessageHelper.getAuthentication("dkim", authentication);
- if (Boolean.TRUE.equals(message.dkim))
- message.dkim = helper.checkDKIMRequirements();
- message.spf = MessageHelper.getAuthentication("spf", authentication);
- if (message.spf == null && helper.getSPF())
- message.spf = true;
- message.dmarc = MessageHelper.getAuthentication("dmarc", authentication);
- message.smtp_from = helper.getMailFrom(authentication);
- message.return_path = helper.getReturnPath();
- message.submitter = helper.getSender();
- message.from = helper.getFrom();
- message.to = helper.getTo();
- message.cc = helper.getCc();
- message.bcc = helper.getBcc();
- message.reply = helper.getReply();
- message.list_post = helper.getListPost();
- message.unsubscribe = helper.getListUnsubscribe();
- message.autocrypt = helper.getAutocrypt();
- if (download_headers)
- message.headers = helper.getHeaders();
- message.infrastructure = helper.getInfrastructure();
- message.subject = helper.getSubject();
- message.size = parts.getBodySize();
- message.total = helper.getSize();
- message.content = false;
- message.encrypt = parts.getEncryption();
- message.ui_encrypt = message.encrypt;
- message.received = received;
- message.notes = notes;
- message.notes_color = notes_color;
- message.sent = sent;
- message.recent = recent;
- message.seen = seen;
- message.answered = answered;
- message.flagged = flagged;
- message.deleted = deleted;
- message.flags = flags;
- message.keywords = keywords;
- message.labels = labels;
- message.ui_seen = seen;
- message.ui_answered = answered;
- message.ui_flagged = flagged;
- message.ui_deleted = deleted;
- message.ui_hide = false;
- message.ui_found = false;
- message.ui_ignored = (seen || deleted);
- message.ui_browsed = browsed;
-
- if (message.flagged)
- message.color = color;
-
- if (message.deliveredto != null)
- try {
- Address deliveredto = new InternetAddress(message.deliveredto);
- if (MessageHelper.equalEmail(new Address[]{deliveredto}, message.to))
- message.deliveredto = null;
- } catch (AddressException ex) {
- Log.w(ex);
- }
-
- if (MessageHelper.equalEmail(message.submitter, message.from))
- message.submitter = null;
-
- // Borrow reply name from sender name
- if (message.from != null && message.from.length == 1 &&
- message.reply != null && message.reply.length == 1) {
- InternetAddress from = (InternetAddress) message.from[0];
- InternetAddress reply = (InternetAddress) message.reply[0];
- if (TextUtils.isEmpty(reply.getPersonal()) &&
- Objects.equals(from.getAddress(), reply.getAddress()))
- reply.setPersonal(from.getPersonal());
- }
-
- if (helper.isReport() && EntityFolder.DRAFTS.equals(folder.type))
- message.dsn = EntityMessage.DSN_HARD_BOUNCE;
-
- EntityIdentity identity = matchIdentity(context, folder, message);
- message.identity = (identity == null ? null : identity.id);
-
- message.sender = MessageHelper.getSortKey(EntityFolder.isOutgoing(folder.type) ? message.to : message.from);
- Uri lookupUri = ContactInfo.getLookupUri(message.from);
- message.avatar = (lookupUri == null ? null : lookupUri.toString());
- if (message.avatar == null && notify_known && pro)
- message.ui_ignored = true;
-
- message.from_domain = (message.checkFromDomain(context) == null);
-
- // For contact forms
- boolean self = false;
- if (identity != null && message.from != null)
- for (Address from : message.from)
- if (identity.sameAddress(from) || identity.similarAddress(from)) {
- self = true;
- break;
- }
-
- if (!self) {
- String[] warning = message.checkReplyDomain(context);
- message.reply_domain = (warning == null);
- }
-
- boolean check_mx = prefs.getBoolean("check_mx", false);
- if (check_mx)
- try {
- Address[] addresses =
- (message.reply == null || message.reply.length == 0
- ? message.from : message.reply);
- DnsHelper.checkMx(context, addresses);
- message.mx = true;
- } catch (UnknownHostException ex) {
- Log.w(ex);
- message.mx = false;
- } catch (Throwable ex) {
- Log.e(folder.name, ex);
- message.warning = Log.formatThrowable(ex, false);
- }
-
- boolean check_blocklist = prefs.getBoolean("check_blocklist", false);
- if (check_blocklist) {
- if (!have &&
- !EntityFolder.isOutgoing(folder.type) &&
- !EntityFolder.ARCHIVE.equals(folder.type) &&
- !EntityFolder.TRASH.equals(folder.type) &&
- !EntityFolder.JUNK.equals(folder.type) &&
- !message.isNotJunk(context) &&
- !Arrays.asList(message.keywords).contains(MessageHelper.FLAG_NOT_JUNK))
- try {
- message.blocklist = DnsBlockList.isJunk(context,
- imessage.getHeader("Received"));
-
- if (message.blocklist == null || !message.blocklist) {
- List senders = new ArrayList<>();
- if (message.reply != null)
- senders.addAll(Arrays.asList(message.reply));
- if (message.from != null)
- senders.addAll(Arrays.asList(message.from));
- message.blocklist = DnsBlockList.isJunk(context, senders);
- }
- } catch (Throwable ex) {
- Log.w(folder.name, ex);
- }
- }
-
- boolean needsHeaders = EntityRule.needsHeaders(message, rules);
- boolean needsBody = EntityRule.needsBody(message, rules);
- if (needsHeaders || needsBody)
- Log.i(folder.name + " needs headers=" + needsHeaders + " body=" + needsBody);
- List headers = (needsHeaders ? helper.getAllHeaders() : null);
- String body = (needsBody ? parts.getHtml(context, download_plain) : null);
-
- if (experiments && helper.isReport())
- try {
- MessageHelper.Report r = parts.getReport();
- boolean client_id = prefs.getBoolean("client_id", true);
- String we = "dns;" + (client_id ? EmailService.getDefaultEhlo() : "example.com");
- if (r != null && !we.equals(r.reporter)) {
- String label = null;
- if (r.isDeliveryStatus())
- label = (r.isDelivered() ? MessageHelper.FLAG_DELIVERED : MessageHelper.FLAG_NOT_DELIVERED);
- else if (r.isDispositionNotification())
- label = (r.isMdnDisplayed() ? MessageHelper.FLAG_DISPLAYED : MessageHelper.FLAG_NOT_DISPLAYED);
- else if (r.isFeedbackReport())
- label = MessageHelper.FLAG_COMPLAINT;
-
- if (label != null) {
- Map map = new HashMap<>();
-
- EntityFolder s = db.folder().getFolderByType(folder.account, EntityFolder.SENT);
- if (s != null)
- map.put(s.id, s);
-
- List all = new ArrayList<>();
-
- if (message.inreplyto != null) {
- List replied = db.message().getMessagesByMsgId(folder.account, message.inreplyto);
- if (replied != null)
- all.addAll(replied);
- }
- if (r.refid != null && !r.refid.equals(message.inreplyto)) {
- List refs = db.message().getMessagesByMsgId(folder.account, r.refid);
- if (refs != null)
- all.addAll(refs);
- }
-
- for (EntityMessage m : all)
- if (!map.containsKey(m.folder)) {
- EntityFolder f = db.folder().getFolder(m.folder);
- if (f != null)
- map.put(f.id, f);
- }
-
- for (String msgid : new String[]{message.inreplyto, r.refid})
- if (msgid != null)
- for (EntityFolder f : map.values())
- EntityOperation.queue(context, f, EntityOperation.REPORT, msgid, label);
- }
- }
- } catch (Throwable ex) {
- Log.w(ex);
- }
-
- try {
- db.beginTransaction();
-
- message.notifying = EntityMessage.NOTIFYING_IGNORE;
- message.id = db.message().insertMessage(message);
- Log.i(folder.name + " added id=" + message.id + " uid=" + message.uid);
-
- int sequence = 1;
- List attachments = parts.getAttachments();
- for (EntityAttachment attachment : attachments) {
- Log.i(folder.name + " attachment seq=" + sequence + " " + attachment);
- attachment.message = message.id;
- attachment.sequence = sequence++;
- attachment.id = db.attachment().insertAttachment(attachment);
- }
-
- runRules(context, headers, body, account, folder, message, rules);
-
- if (message.blocklist != null && message.blocklist) {
- boolean use_blocklist = prefs.getBoolean("use_blocklist", false);
- if (use_blocklist) {
- EntityLog.log(context, EntityLog.Type.General, message,
- "Block list" +
- " folder=" + folder.name +
- " message=" + message.id +
- "@" + new Date(message.received) +
- ":" + message.subject);
- EntityFolder junk = db.folder().getFolderByType(message.account, EntityFolder.JUNK);
- if (junk != null) {
- EntityOperation.queue(context, message, EntityOperation.MOVE, junk.id, false);
- message.ui_hide = true;
- }
- }
- }
-
- if (download && !message.ui_hide &&
- MessageClassifier.isEnabled(context) && folder.auto_classify_source)
- db.message().setMessageUiHide(message.id, true); // keep local value
-
- db.setTransactionSuccessful();
- } catch (SQLiteConstraintException ex) {
- Log.i(ex);
-
- Map crumb = new HashMap<>();
- crumb.put("folder", message.account + ":" + message.folder + ":" + folder.type);
- crumb.put("message", uid + ":" + message.uid);
- crumb.put("what", ex.getMessage());
- Log.breadcrumb("insert", crumb);
-
- return null;
- } finally {
- db.endTransaction();
- }
-
- if (BuildConfig.DEBUG &&
- message.signedby == null &&
- Boolean.TRUE.equals(message.dkim))
- EntityOperation.queue(context, message, EntityOperation.FLAG, true, android.graphics.Color.RED);
-
- try {
- EntityContact.received(context, account, folder, message);
-
- if (body == null && helper.isReport())
- body = parts.getHtml(context, download_plain);
-
- // Download small messages inline
- if (body != null || (download && !message.ui_hide)) {
- long maxSize;
- if (state == null || state.networkState.isUnmetered())
- maxSize = MessageHelper.SMALL_MESSAGE_SIZE;
- else {
- maxSize = prefs.getInt("download", MessageHelper.DEFAULT_DOWNLOAD_SIZE);
- if (maxSize == 0 || maxSize > MessageHelper.SMALL_MESSAGE_SIZE)
- maxSize = MessageHelper.SMALL_MESSAGE_SIZE;
- }
-
- if (body != null ||
- (message.size != null && message.size < maxSize) ||
- (MessageClassifier.isEnabled(context)) && folder.auto_classify_source)
- try {
- if (body == null)
- body = parts.getHtml(context, download_plain);
- File file = message.getFile(context);
- Helper.writeText(file, body);
- String text = HtmlHelper.getFullText(body);
- message.content = true;
- message.preview = HtmlHelper.getPreview(text);
- message.language = HtmlHelper.getLanguage(context, message.subject, text);
- db.message().setMessageContent(message.id,
- message.content,
- message.language,
- parts.isPlainOnly(download_plain),
- message.preview,
- parts.getWarnings(message.warning));
- MessageClassifier.classify(message, folder, true, context);
-
- if (stats != null && body != null)
- stats.content += body.length();
- Log.i(folder.name + " inline downloaded message id=" + message.id +
- " size=" + message.size + "/" + (body == null ? null : body.length()));
-
- if (TextUtils.isEmpty(body) && parts.hasBody())
- reportEmptyMessage(context, state, account, istore);
- } finally {
- if (!message.ui_hide)
- db.message().setMessageUiHide(message.id, false);
- }
- }
- } finally {
- db.message().setMessageNotifying(message.id, 0);
- }
-
- reportNewMessage(context, account, folder, message);
- } else {
- if (process) {
- EntityIdentity identity = matchIdentity(context, folder, message);
- if (identity != null &&
- (message.identity == null || !message.identity.equals(identity.id))) {
- message.identity = identity.id;
- Log.i(folder.name + " updated id=" + message.id + " identity=" + identity.id);
- }
- }
-
- if (!message.recent.equals(recent)) {
- update = true;
- message.recent = recent;
- Log.i(folder.name + " updated id=" + message.id + " uid=" + message.uid + " recent=" + recent);
- syncSimilar = true;
- }
-
- if ((!message.seen.equals(seen) ||
- (!folder.read_only && !message.ui_seen.equals(seen))) &&
- db.operation().getOperationCount(folder.id, message.id, EntityOperation.SEEN) == 0) {
- update = true;
- message.seen = seen;
- message.ui_seen = seen;
- if (seen)
- message.ui_ignored = true;
- Log.i(folder.name + " updated id=" + message.id + " uid=" + message.uid + " seen=" + seen);
- syncSimilar = true;
- }
-
- if ((!message.answered.equals(answered) ||
- (!folder.read_only && !message.ui_answered.equals(message.answered))) &&
- db.operation().getOperationCount(folder.id, message.id, EntityOperation.ANSWERED) == 0) {
- update = true;
- message.answered = answered;
- message.ui_answered = answered;
- Log.i(folder.name + " updated id=" + message.id + " uid=" + message.uid + " answered=" + answered);
- syncSimilar = true;
- }
-
- if ((!message.flagged.equals(flagged) ||
- (!folder.read_only && !message.ui_flagged.equals(flagged))) &&
- db.operation().getOperationCount(folder.id, message.id, EntityOperation.FLAG) == 0) {
- update = true;
- message.flagged = flagged;
- message.ui_flagged = flagged;
- if (!flagged)
- message.color = null;
- Log.i(folder.name + " updated id=" + message.id + " uid=" + message.uid + " flagged=" + flagged);
- syncSimilar = true;
- }
-
- if ((!message.deleted.equals(deleted) || !message.ui_deleted.equals(deleted)) &&
- db.operation().getOperationCount(folder.id, message.id, EntityOperation.DELETE) == 0) {
- update = true;
- message.deleted = deleted;
- message.ui_deleted = deleted;
- message.ui_ignored = deleted;
- Log.i(folder.name + " updated id=" + message.id + " uid=" + message.uid + " deleted=" + deleted);
- syncSimilar = true;
- }
-
- 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) &&
- !folder.read_only &&
- ifolder.getPermanentFlags().contains(Flags.Flag.USER)) {
- update = true;
- message.keywords = keywords;
- Log.i(folder.name + " updated id=" + message.id + " uid=" + message.uid +
- " keywords=" + TextUtils.join(" ", keywords));
- }
-
- if (!Helper.equal(message.labels, labels)) {
- update = true;
- message.labels = labels;
- Log.i(folder.name + " updated id=" + message.id + " uid=" + message.uid +
- " labels=" + (labels == null ? null : TextUtils.join(" ", labels)));
- }
-
- if (download_headers && message.headers == null) {
- update = true;
- message.headers = helper.getHeaders();
- Log.i(folder.name + " updated id=" + message.id + " headers");
- }
-
- if (message.hash == null || process) {
- update = true;
- message.hash = helper.getHash();
- Log.i(folder.name + " updated id=" + message.id + " uid=" + message.uid + " hash=" + message.hash);
-
- // Update archive to prevent visible > 1
- if (EntityFolder.DRAFTS.equals(folder.type))
- for (EntityMessage dup : db.message().getMessagesByMsgId(message.account, message.msgid))
- db.message().setMessageHash(dup.id, message.hash);
- }
-
- if (message.ui_hide &&
- (message.ui_busy == null || message.ui_busy < new Date().getTime()) &&
- db.operation().getOperationCount(folder.id, message.id) == 0 &&
- db.operation().getOperationCount(folder.id, EntityOperation.PURGE) == 0) {
- update = true;
- message.ui_hide = false;
- Log.i(folder.name + " updated id=" + message.id + " uid=" + message.uid + " unhide");
- }
-
- if (message.ui_browsed != browsed) {
- update = true;
- message.ui_browsed = browsed;
- Log.i(folder.name + " updated id=" + message.id + " uid=" + message.uid + " browsed=" + browsed);
- }
-
- Uri uri = ContactInfo.getLookupUri(message.from);
- if (uri != null) {
- String avatar = uri.toString();
- if (!Objects.equals(message.avatar, avatar)) {
- update = true;
- message.avatar = avatar;
- Log.i(folder.name + " updated id=" + message.id + " uid=" + message.uid + " avatar=" + avatar);
- }
- }
-
- if (update || process) {
- boolean needsHeaders = (process && EntityRule.needsHeaders(message, rules));
- boolean needsBody = (process && EntityRule.needsBody(message, rules));
- if (needsHeaders || needsBody)
- Log.i(folder.name + " needs headers=" + needsHeaders + " body=" + needsBody);
- List headers = (needsHeaders ? helper.getAllHeaders() : null);
- String body = (needsBody ? helper.getMessageParts().getHtml(context, download_plain) : null);
-
- try {
- db.beginTransaction();
-
- EntityMessage existing = db.message().getMessage(message.id);
- if (existing != null) {
- message.revision = existing.revision;
- message.revisions = existing.revisions;
- }
-
- db.message().updateMessage(message);
-
- if (process)
- runRules(context, headers, body, account, folder, message, rules);
-
- db.setTransactionSuccessful();
- } finally {
- db.endTransaction();
- }
- }
-
- if (process) {
- EntityContact.received(context, account, folder, message);
- MessageClassifier.classify(message, folder, true, context);
- } else
- Log.d(folder.name + " unchanged uid=" + uid);
-
- if (process)
- reportNewMessage(context, account, folder, message);
- }
-
- if (syncSimilar && account.isGmail())
- for (EntityMessage similar : db.message().getMessagesBySimilarity(message.account, message.id, message.msgid, message.hash)) {
- if (similar.recent != message.recent) {
- Log.i(folder.name + " Synchronize similar id=" + similar.id + " recent=" + message.recent);
- db.message().setMessageRecent(similar.id, message.recent);
- }
-
- if (similar.seen != message.seen) {
- Log.i(folder.name + " Synchronize similar id=" + similar.id + " seen=" + message.seen);
- db.message().setMessageSeen(similar.id, message.seen);
- db.message().setMessageUiSeen(similar.id, message.seen);
- }
-
- if (similar.answered != message.answered) {
- Log.i(folder.name + " Synchronize similar id=" + similar.id + " answered=" + message.answered);
- db.message().setMessageAnswered(similar.id, message.answered);
- db.message().setMessageUiAnswered(similar.id, message.answered);
- }
-
- if (similar.flagged != flagged) {
- Log.i(folder.name + " Synchronize similar id=" + similar.id + " flagged=" + message.flagged);
- db.message().setMessageFlagged(similar.id, message.flagged);
- db.message().setMessageUiFlagged(similar.id, message.flagged, flagged ? similar.color : null);
- }
- }
-
- 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])));
- }
-
- return message;
- }
-
- private static boolean expunge(Context context, IMAPFolder ifolder, List messages) {
- if (messages.size() == 0)
- return false;
-
- SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
- boolean perform_expunge = prefs.getBoolean("perform_expunge", true);
- boolean uid_expunge = prefs.getBoolean("uid_expunge", false);
-
- if (!perform_expunge)
- return false;
-
- try {
- if (uid_expunge)
- uid_expunge = MessageHelper.hasCapability(ifolder, "UIDPLUS");
- if (MessageHelper.hasCapability(ifolder, "X-UIDONLY"))
- uid_expunge = true;
-
- if (uid_expunge) {
- FetchProfile fp = new FetchProfile();
- fp.add(UIDFolder.FetchProfileItem.UID);
- ifolder.fetch(messages.toArray(new Message[0]), fp);
-
- List uids = new ArrayList<>();
- for (Message m : messages)
- try {
- long uid = ifolder.getUID(m);
- if (uid < 0)
- continue;
- uids.add(uid);
- } catch (MessageRemovedException ex) {
- Log.w(ex);
- }
-
- Log.i(ifolder.getName() + " expunging " + TextUtils.join(",", uids));
- uidExpunge(context, ifolder, uids);
- Log.i(ifolder.getName() + " expunged " + TextUtils.join(",", uids));
- } else {
- Log.i(ifolder.getName() + " expunging all");
- ifolder.expunge();
- Log.i(ifolder.getName() + " expunged all");
- }
-
- return true;
- } catch (MessagingException ex) {
- // NO EXPUNGE failed.
- Log.w(ex);
- return false;
- }
- }
-
- private static void uidExpunge(Context context, IMAPFolder ifolder, List uids) throws MessagingException {
- SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
- int chunk_size = prefs.getInt("chunk_size", DEFAULT_CHUNK_SIZE);
-
- ifolder.doCommand(new IMAPFolder.ProtocolCommand() {
- @Override
- public Object doCommand(IMAPProtocol protocol) throws ProtocolException {
- // https://datatracker.ietf.org/doc/html/rfc4315#section-2.1
- for (List list : Helper.chunkList(uids, chunk_size))
- protocol.uidexpunge(UIDSet.createUIDSets(Helper.toLongArray(list)));
- return null;
- }
- });
- }
-
- private static EntityIdentity matchIdentity(Context context, EntityFolder folder, EntityMessage message) {
- if (EntityFolder.DRAFTS.equals(folder.type))
- return null;
-
- List addresses = new ArrayList<>();
- if (folder.isOutgoing()) {
- if (message.from != null)
- addresses.addAll(Arrays.asList(message.from));
- } else {
- if (message.to != null)
- addresses.addAll(Arrays.asList(message.to));
- if (message.cc != null)
- addresses.addAll(Arrays.asList(message.cc));
- if (message.bcc != null)
- addresses.addAll(Arrays.asList(message.bcc));
- if (message.from != null)
- addresses.addAll(Arrays.asList(message.from));
- }
-
- InternetAddress deliveredto = null;
- if (message.deliveredto != null)
- try {
- deliveredto = new InternetAddress(message.deliveredto);
- } catch (AddressException ex) {
- Log.w(ex);
- }
-
- // Search for matching identity
- List identities = getIdentities(folder.account, context);
- if (identities != null) {
- for (Address address : addresses)
- for (EntityIdentity identity : identities)
- if (identity.sameAddress(address))
- return identity;
-
- for (Address address : addresses)
- for (EntityIdentity identity : identities)
- if (identity.similarAddress(address))
- return identity;
-
- if (deliveredto != null)
- for (EntityIdentity identity : identities)
- if (identity.sameAddress(deliveredto) || identity.similarAddress(deliveredto))
- return identity;
- }
-
- return null;
- }
-
- private static void runRules(
- Context context, List headers, String html,
- EntityAccount account, EntityFolder folder, EntityMessage message,
- List rules) {
-
- if (EntityFolder.INBOX.equals(folder.type)) {
- SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
- String mnemonic = prefs.getString("wipe_mnemonic", null);
- if (mnemonic != null && message.subject != null &&
- message.subject.toLowerCase(Locale.ROOT).contains(mnemonic))
- Helper.clearAll(context);
- }
-
- if (account.protocol == EntityAccount.TYPE_IMAP && folder.read_only)
- return;
-
- boolean pro = ActivityBilling.isPro(context);
-
- DB db = DB.getInstance(context);
- try {
- boolean executed = false;
- if (pro)
- for (EntityRule rule : rules)
- if (rule.matches(context, message, headers, html)) {
- rule.execute(context, message);
- executed = true;
- if (rule.stop)
- break;
- }
-
- if (EntityFolder.INBOX.equals(folder.type))
- if (message.from != null) {
- EntityContact badboy = null;
- for (Address from : message.from) {
- String email = ((InternetAddress) from).getAddress();
- if (TextUtils.isEmpty(email))
- continue;
-
- badboy = db.contact().getContact(message.account, EntityContact.TYPE_JUNK, email);
- if (badboy != null)
- break;
- }
-
- if (badboy != null) {
- badboy.times_contacted++;
- badboy.last_contacted = new Date().getTime();
- db.contact().updateContact(badboy);
-
- EntityFolder junk = db.folder().getFolderByType(message.account, EntityFolder.JUNK);
- if (junk != null) {
- EntityOperation.queue(context, message, EntityOperation.MOVE, junk.id);
- message.ui_hide = true;
- executed = true;
- }
- }
- }
-
- if (executed &&
- !message.hasKeyword(MessageHelper.FLAG_FILTERED))
- EntityOperation.queue(context, message, EntityOperation.KEYWORD, MessageHelper.FLAG_FILTERED, true);
- } catch (Throwable ex) {
- Log.e(ex);
- db.message().setMessageError(message.id, Log.formatThrowable(ex));
- }
- }
-
- private static void reportNewMessage(Context context, EntityAccount account, EntityFolder folder, EntityMessage message) {
- // Prepare scroll to top
- if (!message.ui_seen && !message.ui_hide &&
- message.received > account.created) {
- Intent report = new Intent(ActivityView.ACTION_NEW_MESSAGE);
- report.putExtra("folder", folder.id);
- report.putExtra("type", folder.type);
- report.putExtra("unified", folder.unified);
- Log.i("Report new id=" + message.id +
- " folder=" + folder.type + ":" + folder.name +
- " unified=" + folder.unified);
-
- LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(context);
- lbm.sendBroadcast(report);
- }
- }
-
- private static boolean downloadMessage(
- Context context,
- EntityAccount account, EntityFolder folder,
- IMAPStore istore, IMAPFolder ifolder,
- MimeMessage imessage, long id, State state, SyncStats stats) throws MessagingException, IOException {
- if (state.getNetworkState().isRoaming())
- return false;
-
- if (imessage == null)
- return false;
-
- DB db = DB.getInstance(context);
- EntityMessage message = db.message().getMessage(id);
- if (message == null || message.ui_hide)
- return false;
-
- SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
- long maxSize = prefs.getInt("download", MessageHelper.DEFAULT_DOWNLOAD_SIZE);
- if (maxSize == 0)
- maxSize = Long.MAX_VALUE;
- boolean download_eml = prefs.getBoolean("download_eml", false);
-
- List attachments = db.attachment().getAttachments(message.id);
-
- boolean fetch = false;
- if (!message.content)
- if (state.getNetworkState().isUnmetered() || (message.size != null && message.size < maxSize))
- fetch = true;
-
- if (!fetch)
- for (EntityAttachment attachment : attachments)
- if (!attachment.available)
- if (state.getNetworkState().isUnmetered() || (attachment.size != null && attachment.size < maxSize)) {
- fetch = true;
- break;
- }
-
- if (fetch) {
- Log.i(folder.name + " fetching message id=" + message.id);
-
- // Fetch on demand to prevent OOM
-
- //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);
- //if (account.isGmail()) {
- // fp.add(GmailFolder.FetchProfileItem.THRID);
- // fp.add(GmailFolder.FetchProfileItem.LABELS);
- //}
- //ifolder.fetch(new Message[]{imessage}, fp);
-
- MessageHelper helper = new MessageHelper(imessage, context);
- MessageHelper.MessageParts parts = helper.getMessageParts();
-
- if (!message.content) {
- if (state.getNetworkState().isUnmetered() ||
- (message.size != null && message.size < maxSize)) {
- String body = parts.getHtml(context);
- File file = message.getFile(context);
- Helper.writeText(file, body);
- String text = HtmlHelper.getFullText(body);
- message.preview = HtmlHelper.getPreview(text);
- message.language = HtmlHelper.getLanguage(context, message.subject, text);
- db.message().setMessageContent(message.id,
- true,
- message.language,
- parts.isPlainOnly(),
- message.preview,
- parts.getWarnings(message.warning));
- MessageClassifier.classify(message, folder, true, context);
-
- if (stats != null && body != null)
- stats.content += body.length();
- Log.i(folder.name + " downloaded message id=" + message.id +
- " size=" + message.size + "/" + (body == null ? null : body.length()));
-
- if (TextUtils.isEmpty(body) && parts.hasBody())
- reportEmptyMessage(context, state, account, istore);
- }
- }
-
- for (EntityAttachment attachment : attachments)
- if (!attachment.available &&
- attachment.subsequence == null &&
- TextUtils.isEmpty(attachment.error))
- if (state.getNetworkState().isUnmetered() ||
- (attachment.size != null && attachment.size < maxSize))
- try {
- parts.downloadAttachment(context, attachment);
- if (stats != null && attachment.size != null)
- stats.attachments += attachment.size;
- } catch (Throwable ex) {
- Log.e(folder.name, ex);
- db.attachment().setError(attachment.id, Log.formatThrowable(ex, false));
- }
- }
-
- if (download_eml &&
- (message.raw == null || !message.raw) &&
- (state.getNetworkState().isUnmetered() || (message.total != null && message.total < maxSize))) {
- File file = message.getRawFile(context);
- try (OutputStream os = new BufferedOutputStream(new FileOutputStream(file))) {
- imessage.writeTo(os);
- }
-
- message.raw = true;
- db.message().setMessageRaw(message.id, message.raw);
- }
-
- return fetch;
- }
-
- private static void reportEmptyMessage(Context context, State state, EntityAccount account, IMAPStore istore) {
- try {
- if (istore.hasCapability("ID")) {
- Map id = new LinkedHashMap<>();
- id.put("name", context.getString(R.string.app_name));
- id.put("version", BuildConfig.VERSION_NAME);
- Map sid = istore.id(id);
- if (sid != null) {
- StringBuilder sb = new StringBuilder();
- for (String key : sid.keySet())
- sb.append(" ").append(key).append("=").append(sid.get(key));
- if (!account.partial_fetch)
- Log.w("Empty message" + sb.toString());
- }
- } else {
- if (!account.partial_fetch)
- Log.w("Empty message " + account.host);
- }
- } catch (Throwable ex) {
- Log.w(ex);
- }
-
- // Auto disable partial fetch
- if (account.partial_fetch && false) {
- account.partial_fetch = false;
- DB db = DB.getInstance(context);
- db.account().setAccountPartialFetch(account.id, account.partial_fetch);
- state.error(new StoreClosedException(istore));
- }
- }
-
- static void notifyMessages(Context context, List messages, NotificationData data, boolean foreground) {
- if (messages == null)
- messages = new ArrayList<>();
-
- NotificationManager nm = Helper.getSystemService(context, NotificationManager.class);
- if (nm == null)
- return;
-
- DB db = DB.getInstance(context);
-
- SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
- boolean badge = prefs.getBoolean("badge", true);
- boolean notify_background_only = prefs.getBoolean("notify_background_only", false);
- boolean notify_summary = prefs.getBoolean("notify_summary", false);
- boolean notify_preview = prefs.getBoolean("notify_preview", true);
- boolean notify_preview_only = prefs.getBoolean("notify_preview_only", false);
- boolean notify_screen_on = prefs.getBoolean("notify_screen_on", false);
- boolean wearable_preview = prefs.getBoolean("wearable_preview", false);
- boolean biometrics = prefs.getBoolean("biometrics", false);
- String pin = prefs.getString("pin", null);
- boolean biometric_notify = prefs.getBoolean("biometrics_notify", true);
- boolean pro = ActivityBilling.isPro(context);
-
- boolean redacted = ((biometrics || !TextUtils.isEmpty(pin)) && !biometric_notify);
- if (redacted)
- notify_summary = true;
-
- EntityLog.log(context, EntityLog.Type.Notification, "Notify messages=" + messages.size() +
- " biometrics=" + biometrics + "/" + biometric_notify +
- " summary=" + notify_summary +
- " thread=" + Thread.currentThread().getId());
-
- Map newMessages = new HashMap<>();
-
- Map> groupMessages = new HashMap<>();
- for (long group : data.groupNotifying.keySet())
- groupMessages.put(group, new ArrayList<>());
-
- // Current
- for (TupleMessageEx message : messages) {
- EntityMessage m = db.message().getMessage(message.id);
- if (m == null)
- EntityLog.log(context, EntityLog.Type.Notification, "Notify missing=" + message.id);
-
- if (message.notifying == EntityMessage.NOTIFYING_IGNORE) {
- EntityLog.log(context, EntityLog.Type.Notification, "Notify ignore=" + message.id);
- continue;
- }
-
- // Check if notification channel enabled
- if (message.notifying == 0 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && pro) {
- String channelId = message.getNotificationChannelId();
- if (channelId != null) {
- NotificationChannel channel = nm.getNotificationChannel(channelId);
- if (channel != null && channel.getImportance() == NotificationManager.IMPORTANCE_NONE) {
- db.message().setMessageUiIgnored(message.id, true);
- EntityLog.log(context, EntityLog.Type.Notification, "Notify disabled=" + message.id + " channel=" + channelId);
- continue;
- }
- }
- }
-
- if (notify_preview && notify_preview_only && !message.content) {
- EntityLog.log(context, EntityLog.Type.Notification, "Notify no content id=" + message.id +
- " notifying=" + message.notifying);
- continue;
- }
-
- if (foreground && notify_background_only && message.notifying == 0) {
- EntityLog.log(context, EntityLog.Type.Notification, "Notify foreground=" + message.id +
- " notifying=" + message.notifying);
- if (!message.ui_ignored)
- db.message().setMessageUiIgnored(message.id, true);
- continue;
- }
-
- long group = (pro && message.accountNotify ? message.account : 0);
- if (!message.folderUnified)
- group = -message.folder;
- if (!data.groupNotifying.containsKey(group))
- data.groupNotifying.put(group, new ArrayList<>());
- if (!groupMessages.containsKey(group))
- groupMessages.put(group, new ArrayList<>());
-
- if (message.notifying == 0) {
- // Handle clear notifying on boot/update
- EntityLog.log(context, EntityLog.Type.Notification, "Notify clear=" + message.id +
- " notifying=" + message.notifying);
- data.groupNotifying.get(group).remove(message.id);
- data.groupNotifying.get(group).remove(-message.id);
- } else {
- long id = message.id * message.notifying;
- if (!data.groupNotifying.get(group).contains(id) &&
- !data.groupNotifying.get(group).contains(-id)) {
- EntityLog.log(context, EntityLog.Type.Notification, "Notify database=" + id +
- " notifying=" + message.notifying);
- data.groupNotifying.get(group).add(id);
- }
- }
-
- if (message.ui_seen || message.ui_ignored || message.ui_hide)
- EntityLog.log(context, EntityLog.Type.Notification, "Notify id=" + message.id +
- " seen=" + message.ui_seen +
- " ignored=" + message.ui_ignored +
- " hide=" + message.ui_hide);
- else {
- Integer current = newMessages.get(group);
- newMessages.put(group, current == null ? 1 : current + 1);
-
- // This assumes the messages are properly ordered
- if (groupMessages.get(group).size() < MAX_NOTIFICATION_COUNT)
- groupMessages.get(group).add(message);
- else
- db.message().setMessageUiIgnored(message.id, true);
- }
- }
-
- // Difference
- boolean flash = false;
- for (long group : groupMessages.keySet()) {
- List add = new ArrayList<>();
- List update = new ArrayList<>();
- List remove = new ArrayList<>(data.groupNotifying.get(group));
- EntityLog.log(context, EntityLog.Type.Notification, "Notify group=" + group +
- " size=" + groupMessages.get(group).size() +
- " notifying=" + TextUtils.join(",", data.groupNotifying.get(group)) +
- " existing=" + TextUtils.join(",", remove));
- for (int m = 0; m < groupMessages.get(group).size(); m++) {
- TupleMessageEx message = groupMessages.get(group).get(m);
- if (m >= MAX_NOTIFICATION_DISPLAY) {
- // This is to prevent notification sounds when shifting messages up
- if (!message.ui_silent) {
- EntityLog.log(context, EntityLog.Type.Notification, "Notify silence=" + message.id +
- " notifying=" + message.notifying);
- db.message().setMessageUiSilent(message.id, true);
- }
- continue;
- }
-
- long id = (message.content ? message.id : -message.id);
- if (remove.contains(id)) {
- remove.remove(id);
- EntityLog.log(context, EntityLog.Type.Notification, "Notify existing=" + id +
- " notifying=" + message.notifying);
- } else {
- boolean existing = remove.contains(-id);
- if (existing) {
- if (message.content && notify_preview) {
- EntityLog.log(context, EntityLog.Type.Notification, "Notify preview=" + id +
- " notifying=" + message.notifying);
- add.add(id);
- update.add(id);
- }
- remove.remove(-id);
- } else {
- flash = true;
- add.add(id);
- }
- EntityLog.log(context, EntityLog.Type.Notification, "Notify adding=" + id + " existing=" + existing +
- " notifying=" + message.notifying);
- }
- }
-
- Integer prev = prefs.getInt("new_messages." + group, 0);
- Integer current = newMessages.get(group);
- if (current == null)
- current = 0;
- prefs.edit().putInt("new_messages." + group, current).apply();
-
- if (prev.equals(current) &&
- remove.size() + add.size() == 0) {
- EntityLog.log(context, EntityLog.Type.Notification, "Notify unchanged");
- continue;
- }
-
- // Build notifications
- List notifications = getNotificationUnseen(context,
- group, groupMessages.get(group),
- notify_summary, current - prev, current,
- redacted);
-
- EntityLog.log(context, EntityLog.Type.Notification, "Notify group=" + group +
- " new=" + prev + "/" + current +
- " count=" + notifications.size() +
- " add=" + TextUtils.join(",", add) +
- " update=" + TextUtils.join(",", update) +
- " remove=" + TextUtils.join(",", remove));
-
- for (Long id : remove) {
- String tag = "unseen." + group + "." + Math.abs(id);
- EntityLog.log(context, EntityLog.Type.Notification,
- null, null, id == 0 ? null : Math.abs(id),
- "Notify cancel tag=" + tag + " id=" + id);
- nm.cancel(tag, NotificationHelper.NOTIFICATION_TAGGED);
-
- data.groupNotifying.get(group).remove(id);
- int count = db.message().setMessageNotifying(Math.abs(id), 0);
- if (count != 1) {
- EntityMessage m = db.message().getMessage(Math.abs(id));
- EntityLog.log(context, "Notify remove failed=" + id + "/" + Math.abs(id) +
- " notifying=" + (m == null ? "n/a" : m.notifying) + "/" + 0 + " count=" + count);
- }
- }
-
- if (notifications.size() == 0) {
- String tag = "unseen." + group + "." + 0;
- EntityLog.log(context, EntityLog.Type.Notification,
- "Notify cancel tag=" + tag);
- nm.cancel(tag, NotificationHelper.NOTIFICATION_TAGGED);
- }
-
- for (Long id : add) {
- EntityLog.log(context, EntityLog.Type.Notification, "Notify list add=" + id);
- data.groupNotifying.get(group).add(id);
- data.groupNotifying.get(group).remove(-id);
- int count = db.message().setMessageNotifying(Math.abs(id), (int) Math.signum(id));
- if (count != 1) {
- EntityMessage m = db.message().getMessage(Math.abs(id));
- EntityLog.log(context, "Notify add failed=" + id + "/" + Math.abs(id) +
- " notifying=" + (m == null ? "n/a" : m.notifying) + "/" + ((int) Math.signum(id)) + " count=" + count);
- }
- }
-
- EntityLog.log(context, EntityLog.Type.Notification,
- "Notify notifying end=" + TextUtils.join(",", data.groupNotifying.get(group)));
-
- for (NotificationCompat.Builder builder : notifications) {
- long id = builder.getExtras().getLong("id", 0);
- if ((id == 0 && !prev.equals(current)) || add.contains(id)) {
- // https://developer.android.com/training/wearables/notifications/bridger#non-bridged
- if (id == 0) {
- if (!notify_summary)
- builder.setLocalOnly(true);
- } else {
- if (wearable_preview ? id < 0 : update.contains(id))
- builder.setLocalOnly(true);
- }
-
- String tag = "unseen." + group + "." + Math.abs(id);
- Notification notification = builder.build();
- EntityLog.log(context, EntityLog.Type.Notification,
- null, null, id == 0 ? null : Math.abs(id),
- "Notifying tag=" + tag +
- " id=" + id + " group=" + notification.getGroup() +
- (Build.VERSION.SDK_INT < Build.VERSION_CODES.O
- ? " sdk=" + Build.VERSION.SDK_INT
- : " channel=" + notification.getChannelId()) +
- " sort=" + notification.getSortKey());
- try {
- if (NotificationHelper.areNotificationsEnabled(nm))
- nm.notify(tag, NotificationHelper.NOTIFICATION_TAGGED, notification);
- // https://github.com/leolin310148/ShortcutBadger/wiki/Xiaomi-Device-Support
- if (id == 0 && badge && Helper.isXiaomi())
- ShortcutBadger.applyNotification(context, notification, current);
- } catch (Throwable ex) {
- Log.w(ex);
- }
- }
- }
- }
-
- if (notify_screen_on && flash) {
- Log.i("Notify screen on");
- PowerManager pm = Helper.getSystemService(context, PowerManager.class);
- PowerManager.WakeLock wakeLock = pm.newWakeLock(
- PowerManager.FULL_WAKE_LOCK | PowerManager.ACQUIRE_CAUSES_WAKEUP,
- BuildConfig.APPLICATION_ID + ":notification");
- wakeLock.acquire(SCREEN_ON_DURATION);
- }
- }
-
- private static List getNotificationUnseen(
- Context context,
- long group, List messages,
- boolean notify_summary, int new_messages, int total_messages, boolean redacted) {
- List notifications = new ArrayList<>();
-
- // Android 7+ N https://developer.android.com/training/notify-user/group
- // Android 8+ O https://developer.android.com/training/notify-user/channels
- // Android 7+ N https://android-developers.googleblog.com/2016/06/notifications-in-android-n.html
-
- // Group
- // < 0: folder
- // = 0: unified
- // > 0: account
-
- NotificationManager nm = Helper.getSystemService(context, NotificationManager.class);
- if (messages == null || messages.size() == 0 || nm == null)
- return notifications;
-
- boolean pro = ActivityBilling.isPro(context);
-
- SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
- boolean notify_grouping = prefs.getBoolean("notify_grouping", true);
- boolean notify_private = prefs.getBoolean("notify_private", true);
- boolean notify_newest_first = prefs.getBoolean("notify_newest_first", false);
- MessageHelper.AddressFormat email_format = MessageHelper.getAddressFormat(context);
- boolean prefer_contact = prefs.getBoolean("prefer_contact", false);
- boolean flags = prefs.getBoolean("flags", true);
- boolean notify_messaging = prefs.getBoolean("notify_messaging", false);
- boolean notify_subtext = prefs.getBoolean("notify_subtext", true);
- boolean notify_preview = prefs.getBoolean("notify_preview", true);
- boolean notify_preview_all = prefs.getBoolean("notify_preview_all", false);
- boolean wearable_preview = prefs.getBoolean("wearable_preview", false);
- boolean notify_trash = (prefs.getBoolean("notify_trash", true) || !pro);
- boolean notify_junk = (prefs.getBoolean("notify_junk", false) && pro);
- boolean notify_archive = (prefs.getBoolean("notify_archive", true) || !pro);
- boolean notify_move = (prefs.getBoolean("notify_move", false) && pro);
- boolean notify_reply = (prefs.getBoolean("notify_reply", false) && pro);
- boolean notify_reply_direct = (prefs.getBoolean("notify_reply_direct", false) && pro);
- boolean notify_flag = (prefs.getBoolean("notify_flag", false) && flags && pro);
- boolean notify_seen = (prefs.getBoolean("notify_seen", true) || !pro);
- boolean notify_hide = (prefs.getBoolean("notify_hide", false) && pro);
- boolean notify_snooze = (prefs.getBoolean("notify_snooze", false) && pro);
- boolean notify_remove = prefs.getBoolean("notify_remove", true);
- boolean light = prefs.getBoolean("light", false);
- String sound = prefs.getString("sound", null);
- boolean alert_once = prefs.getBoolean("alert_once", true);
- boolean perform_expunge = prefs.getBoolean("perform_expunge", true);
-
- // Get contact info
- Map messageFrom = new HashMap<>();
- Map messageInfo = new HashMap<>();
- for (int m = 0; m < messages.size() && m < MAX_NOTIFICATION_DISPLAY; m++) {
- TupleMessageEx message = messages.get(m);
- ContactInfo[] info = ContactInfo.get(context,
- message.account, message.folderType,
- message.bimi_selector, message.from);
-
- Address[] modified = (message.from == null
- ? new InternetAddress[0]
- : Arrays.copyOf(message.from, message.from.length));
- for (int i = 0; i < modified.length; i++) {
- String displayName = info[i].getDisplayName();
- if (!TextUtils.isEmpty(displayName)) {
- String email = ((InternetAddress) modified[i]).getAddress();
- String personal = ((InternetAddress) modified[i]).getPersonal();
- if (TextUtils.isEmpty(personal) || prefer_contact)
- try {
- modified[i] = new InternetAddress(email, displayName, StandardCharsets.UTF_8.name());
- } catch (UnsupportedEncodingException ex) {
- Log.w(ex);
- }
- }
- }
-
- messageInfo.put(message.id, info);
- messageFrom.put(message.id, modified);
- }
-
- // Summary notification
- if (notify_summary ||
- (notify_grouping && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)) {
- // Build pending intents
- Intent content;
- if (group < 0) {
- content = new Intent(context, ActivityView.class)
- .setAction("folder:" + (-group) + (notify_remove ? ":" + group : ""));
- if (messages.size() > 0)
- content.putExtra("type", messages.get(0).folderType);
- } else
- content = new Intent(context, ActivityView.class)
- .setAction("unified" + (notify_remove ? ":" + group : ""));
- content.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
- PendingIntent piContent = PendingIntentCompat.getActivity(
- context, ActivityView.PI_UNIFIED, content, PendingIntent.FLAG_UPDATE_CURRENT);
-
- Intent clear = new Intent(context, ServiceUI.class).setAction("clear:" + group);
- PendingIntent piClear = PendingIntentCompat.getService(
- context, ServiceUI.PI_CLEAR, clear, PendingIntent.FLAG_UPDATE_CURRENT);
-
- // Build title
- String title = context.getResources().getQuantityString(
- R.plurals.title_notification_unseen, total_messages, total_messages);
-
- long cgroup = (group >= 0
- ? group
- : (pro && messages.size() > 0 && messages.get(0).accountNotify ? messages.get(0).account : 0));
-
- // Build notification
- NotificationCompat.Builder builder =
- new NotificationCompat.Builder(context, EntityAccount.getNotificationChannelId(cgroup))
- .setSmallIcon(messages.size() > 1
- ? R.drawable.baseline_mail_more_white_24
- : R.drawable.baseline_mail_white_24)
- .setContentTitle(title)
- .setContentIntent(piContent)
- .setNumber(total_messages)
- .setDeleteIntent(piClear)
- .setPriority(NotificationCompat.PRIORITY_DEFAULT)
- .setCategory(notify_summary
- ? NotificationCompat.CATEGORY_EMAIL : NotificationCompat.CATEGORY_STATUS)
- .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
- .setAllowSystemGeneratedContextualActions(false);
-
- if (notify_summary) {
- builder.setOnlyAlertOnce(new_messages <= 0);
-
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O)
- if (new_messages > 0)
- setLightAndSound(builder, light, sound);
- else
- builder.setSound(null);
- } else {
- builder
- .setGroup(Long.toString(group))
- .setGroupSummary(true)
- .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN);
-
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O)
- builder.setSound(null);
- }
-
- if (pro) {
- Integer color = null;
- for (TupleMessageEx message : messages) {
- Integer mcolor = getColor(message);
- if (mcolor == null) {
- color = null;
- break;
- } else if (color == null)
- color = mcolor;
- else if (!color.equals(mcolor)) {
- color = null;
- break;
- }
- }
-
- if (color != null) {
- builder.setColor(color);
- builder.setColorized(true);
- }
- }
-
- // Subtext should not be set, to show number of new messages
-
- if (notify_private) {
- Notification pub = builder.build();
- builder
- .setVisibility(NotificationCompat.VISIBILITY_PRIVATE)
- .setPublicVersion(pub);
- }
-
- if (notify_preview)
- if (redacted)
- builder.setContentText(context.getString(R.string.title_notification_redacted));
- else {
- DateFormat DTF = Helper.getDateTimeInstance(context, SimpleDateFormat.SHORT, SimpleDateFormat.SHORT);
- StringBuilder sb = new StringBuilder();
- for (EntityMessage message : messages) {
- Address[] afrom = messageFrom.get(message.id);
- String from = MessageHelper.formatAddresses(afrom, email_format, false);
- sb.append("").append(Html.escapeHtml(from)).append("");
- if (!TextUtils.isEmpty(message.subject))
- sb.append(": ").append(Html.escapeHtml(message.subject));
- sb.append(" ").append(Html.escapeHtml(DTF.format(message.received)));
- sb.append("
");
- }
-
- // Wearables
- builder.setContentText(title);
-
- // Device
- builder.setStyle(new NotificationCompat.BigTextStyle()
- .bigText(HtmlHelper.fromHtml(sb.toString(), context))
- .setSummaryText(title));
- }
-
- //builder.extend(new NotificationCompat.WearableExtender()
- // .setDismissalId(BuildConfig.APPLICATION_ID));
-
- notifications.add(builder);
- }
-
- if (notify_summary)
- return notifications;
-
- // Message notifications
- for (int m = 0; m < messages.size() && m < MAX_NOTIFICATION_DISPLAY; m++) {
- TupleMessageEx message = messages.get(m);
- ContactInfo[] info = messageInfo.get(message.id);
-
- // Build arguments
- long id = (message.content ? message.id : -message.id);
- Bundle args = new Bundle();
- args.putLong("id", id);
-
- // Build pending intents
- Intent thread = new Intent(context, ActivityView.class);
- thread.setAction("thread:" + message.id);
- thread.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- thread.putExtra("account", message.account);
- thread.putExtra("folder", message.folder);
- thread.putExtra("thread", message.thread);
- thread.putExtra("filter_archive", !EntityFolder.ARCHIVE.equals(message.folderType));
- thread.putExtra("ignore", notify_remove);
- PendingIntent piContent = PendingIntentCompat.getActivity(
- context, ActivityView.PI_THREAD, thread, PendingIntent.FLAG_UPDATE_CURRENT);
-
- Intent ignore = new Intent(context, ServiceUI.class).setAction("ignore:" + message.id);
- PendingIntent piIgnore = PendingIntentCompat.getService(
- context, ServiceUI.PI_IGNORED, ignore, PendingIntent.FLAG_UPDATE_CURRENT);
-
- // Get channel name
- String channelName = EntityAccount.getNotificationChannelId(0);
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && pro) {
- NotificationChannel channel = null;
-
- String channelId = message.getNotificationChannelId();
- if (channelId != null)
- channel = nm.getNotificationChannel(channelId);
-
- if (channel == null)
- channel = nm.getNotificationChannel(EntityFolder.getNotificationChannelId(message.folder));
-
- if (channel == null) {
- if (message.accountNotify)
- channelName = EntityAccount.getNotificationChannelId(message.account);
- } else
- channelName = channel.getId();
- }
-
- String sortKey = String.format(Locale.ROOT, "%13d",
- notify_newest_first ? (10000000000000L - message.received) : message.received);
-
- NotificationCompat.Builder mbuilder =
- new NotificationCompat.Builder(context, channelName)
- .addExtras(args)
- .setSmallIcon(R.drawable.baseline_mail_white_24)
- .setContentIntent(piContent)
- .setWhen(message.received)
- .setShowWhen(true)
- .setSortKey(sortKey)
- .setDeleteIntent(piIgnore)
- .setPriority(NotificationCompat.PRIORITY_DEFAULT)
- .setCategory(NotificationCompat.CATEGORY_EMAIL)
- .setVisibility(notify_private
- ? NotificationCompat.VISIBILITY_PRIVATE
- : NotificationCompat.VISIBILITY_PUBLIC)
- .setOnlyAlertOnce(alert_once)
- .setAllowSystemGeneratedContextualActions(false);
-
- if (message.ui_silent) {
- mbuilder.setSilent(true);
- Log.i("Notify silent=" + message.id);
- }
- if (message.ui_local_only) {
- mbuilder.setLocalOnly(true);
- Log.i("Notify local=" + message.id);
- }
-
- if (notify_messaging) {
- // https://developer.android.com/training/cars/messaging
- String meName = MessageHelper.formatAddresses(message.to, email_format, false);
- String youName = MessageHelper.formatAddresses(message.from, email_format, false);
-
- // Names cannot be empty
- if (TextUtils.isEmpty(meName))
- meName = "-";
- if (TextUtils.isEmpty(youName))
- youName = "-";
-
- Person.Builder me = new Person.Builder().setName(meName);
- Person.Builder you = new Person.Builder().setName(youName);
-
- if (info[0].hasPhoto())
- you.setIcon(IconCompat.createWithBitmap(info[0].getPhotoBitmap()));
-
- if (info[0].hasLookupUri())
- you.setUri(info[0].getLookupUri().toString());
-
- NotificationCompat.MessagingStyle messagingStyle = new NotificationCompat.MessagingStyle(me.build());
-
- if (!TextUtils.isEmpty(message.subject))
- messagingStyle.setConversationTitle(message.subject);
-
- messagingStyle.addMessage(
- notify_preview && message.preview != null ? message.preview : "",
- message.received,
- you.build());
-
- mbuilder.setStyle(messagingStyle);
- }
-
- if (notify_grouping && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
- mbuilder
- .setGroup(Long.toString(group))
- .setGroupSummary(false)
- .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN);
-
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O)
- setLightAndSound(mbuilder, light, sound);
-
- Address[] afrom = messageFrom.get(message.id);
- String from = MessageHelper.formatAddresses(afrom, email_format, false);
- mbuilder.setContentTitle(from);
- if (notify_subtext)
- if (message.folderUnified && EntityFolder.INBOX.equals(message.folderType))
- mbuilder.setSubText(message.accountName);
- else
- mbuilder.setSubText(message.accountName + " - " + message.getFolderName(context));
-
- DB db = DB.getInstance(context);
-
- List wactions = new ArrayList<>();
-
- if (notify_trash &&
- perform_expunge &&
- message.accountProtocol == EntityAccount.TYPE_IMAP &&
- db.folder().getFolderByType(message.account, EntityFolder.TRASH) != null) {
- Intent trash = new Intent(context, ServiceUI.class)
- .setAction("trash:" + message.id)
- .putExtra("group", group);
- PendingIntent piTrash = PendingIntentCompat.getService(
- context, ServiceUI.PI_TRASH, trash, PendingIntent.FLAG_UPDATE_CURRENT);
- NotificationCompat.Action.Builder actionTrash = new NotificationCompat.Action.Builder(
- R.drawable.twotone_delete_24,
- context.getString(R.string.title_advanced_notify_action_trash),
- piTrash)
- .setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_DELETE)
- .setShowsUserInterface(false)
- .setAllowGeneratedReplies(false);
- mbuilder.addAction(actionTrash.build());
-
- wactions.add(actionTrash.build());
- }
-
- if (notify_trash &&
- ((message.accountProtocol == EntityAccount.TYPE_POP && message.accountLeaveDeleted) ||
- (message.accountProtocol == EntityAccount.TYPE_IMAP && !perform_expunge))) {
- Intent delete = new Intent(context, ServiceUI.class)
- .setAction("delete:" + message.id)
- .putExtra("group", group);
- PendingIntent piDelete = PendingIntentCompat.getService(
- context, ServiceUI.PI_DELETE, delete, PendingIntent.FLAG_UPDATE_CURRENT);
- NotificationCompat.Action.Builder actionDelete = new NotificationCompat.Action.Builder(
- R.drawable.twotone_delete_forever_24,
- context.getString(R.string.title_advanced_notify_action_delete),
- piDelete)
- .setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_DELETE)
- .setShowsUserInterface(false)
- .setAllowGeneratedReplies(false);
- mbuilder.addAction(actionDelete.build());
-
- wactions.add(actionDelete.build());
- }
-
- if (notify_junk &&
- message.accountProtocol == EntityAccount.TYPE_IMAP &&
- db.folder().getFolderByType(message.account, EntityFolder.JUNK) != null) {
- Intent junk = new Intent(context, ServiceUI.class)
- .setAction("junk:" + message.id)
- .putExtra("group", group);
- PendingIntent piJunk = PendingIntentCompat.getService(
- context, ServiceUI.PI_JUNK, junk, PendingIntent.FLAG_UPDATE_CURRENT);
- NotificationCompat.Action.Builder actionJunk = new NotificationCompat.Action.Builder(
- R.drawable.twotone_report_24,
- context.getString(R.string.title_advanced_notify_action_junk),
- piJunk)
- .setShowsUserInterface(false)
- .setAllowGeneratedReplies(false);
- mbuilder.addAction(actionJunk.build());
-
- wactions.add(actionJunk.build());
- }
-
- if (notify_archive &&
- message.accountProtocol == EntityAccount.TYPE_IMAP &&
- db.folder().getFolderByType(message.account, EntityFolder.ARCHIVE) != null) {
- Intent archive = new Intent(context, ServiceUI.class)
- .setAction("archive:" + message.id)
- .putExtra("group", group);
- PendingIntent piArchive = PendingIntentCompat.getService(
- context, ServiceUI.PI_ARCHIVE, archive, PendingIntent.FLAG_UPDATE_CURRENT);
- NotificationCompat.Action.Builder actionArchive = new NotificationCompat.Action.Builder(
- R.drawable.twotone_archive_24,
- context.getString(R.string.title_advanced_notify_action_archive),
- piArchive)
- .setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_ARCHIVE)
- .setShowsUserInterface(false)
- .setAllowGeneratedReplies(false);
- mbuilder.addAction(actionArchive.build());
-
- wactions.add(actionArchive.build());
- }
-
- if (notify_move &&
- message.accountProtocol == EntityAccount.TYPE_IMAP) {
- EntityAccount account = db.account().getAccount(message.account);
- if (account != null && account.move_to != null) {
- EntityFolder folder = db.folder().getFolder(account.move_to);
- if (folder != null) {
- Intent move = new Intent(context, ServiceUI.class)
- .setAction("move:" + message.id)
- .putExtra("group", group);
- PendingIntent piMove = PendingIntentCompat.getService(
- context, ServiceUI.PI_MOVE, move, PendingIntent.FLAG_UPDATE_CURRENT);
- NotificationCompat.Action.Builder actionMove = new NotificationCompat.Action.Builder(
- R.drawable.twotone_folder_24,
- folder.getDisplayName(context),
- piMove)
- .setShowsUserInterface(false)
- .setAllowGeneratedReplies(false);
- mbuilder.addAction(actionMove.build());
-
- wactions.add(actionMove.build());
- }
- }
- }
-
- if (notify_reply && message.content) {
- List identities = db.identity().getComposableIdentities(message.account);
- if (identities != null && identities.size() > 0) {
- Intent reply = new Intent(context, ActivityCompose.class)
- .putExtra("action", "reply")
- .putExtra("reference", message.id)
- .putExtra("group", group);
- reply.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- PendingIntent piReply = PendingIntentCompat.getActivity(
- context, ActivityCompose.PI_REPLY, reply, PendingIntent.FLAG_UPDATE_CURRENT);
- NotificationCompat.Action.Builder actionReply = new NotificationCompat.Action.Builder(
- R.drawable.twotone_reply_24,
- context.getString(R.string.title_advanced_notify_action_reply),
- piReply)
- .setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_REPLY)
- .setShowsUserInterface(true)
- .setAllowGeneratedReplies(false);
- mbuilder.addAction(actionReply.build());
- }
- }
-
- if (message.content &&
- message.identity != null &&
- message.from != null && message.from.length > 0 &&
- db.folder().getOutbox() != null) {
- Intent reply = new Intent(context, ServiceUI.class)
- .setAction("reply:" + message.id)
- .putExtra("group", group);
- PendingIntent piReply = PendingIntentCompat.getService(
- context, ServiceUI.PI_REPLY_DIRECT, reply, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE);
- NotificationCompat.Action.Builder actionReply = new NotificationCompat.Action.Builder(
- R.drawable.twotone_reply_24,
- context.getString(R.string.title_advanced_notify_action_reply_direct),
- piReply)
- .setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_REPLY)
- .setShowsUserInterface(false)
- .setAllowGeneratedReplies(false);
- RemoteInput.Builder input = new RemoteInput.Builder("text")
- .setLabel(context.getString(R.string.title_advanced_notify_action_reply));
- actionReply.addRemoteInput(input.build())
- .setAllowGeneratedReplies(false);
- if (notify_reply_direct) {
- mbuilder.addAction(actionReply.build());
- wactions.add(actionReply.build());
- } else
- mbuilder.addInvisibleAction(actionReply.build());
- }
-
- if (notify_flag) {
- Intent flag = new Intent(context, ServiceUI.class)
- .setAction("flag:" + message.id)
- .putExtra("group", group);
- PendingIntent piFlag = PendingIntentCompat.getService(
- context, ServiceUI.PI_FLAG, flag, PendingIntent.FLAG_UPDATE_CURRENT);
- NotificationCompat.Action.Builder actionFlag = new NotificationCompat.Action.Builder(
- R.drawable.baseline_star_24,
- context.getString(R.string.title_advanced_notify_action_flag),
- piFlag)
- .setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_THUMBS_UP)
- .setShowsUserInterface(false)
- .setAllowGeneratedReplies(false);
- mbuilder.addAction(actionFlag.build());
-
- wactions.add(actionFlag.build());
- }
-
- if (true) {
- Intent seen = new Intent(context, ServiceUI.class)
- .setAction("seen:" + message.id)
- .putExtra("group", group);
- PendingIntent piSeen = PendingIntentCompat.getService(
- context, ServiceUI.PI_SEEN, seen, PendingIntent.FLAG_UPDATE_CURRENT);
- NotificationCompat.Action.Builder actionSeen = new NotificationCompat.Action.Builder(
- R.drawable.twotone_visibility_24,
- context.getString(R.string.title_advanced_notify_action_seen),
- piSeen)
- .setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_MARK_AS_READ)
- .setShowsUserInterface(false)
- .setAllowGeneratedReplies(false);
- if (notify_seen) {
- mbuilder.addAction(actionSeen.build());
- wactions.add(actionSeen.build());
- } else
- mbuilder.addInvisibleAction(actionSeen.build());
- }
-
- if (notify_hide) {
- Intent hide = new Intent(context, ServiceUI.class)
- .setAction("hide:" + message.id)
- .putExtra("group", group);
- PendingIntent piHide = PendingIntentCompat.getService(
- context, ServiceUI.PI_HIDE, hide, PendingIntent.FLAG_UPDATE_CURRENT);
- NotificationCompat.Action.Builder actionHide = new NotificationCompat.Action.Builder(
- R.drawable.twotone_visibility_off_24,
- context.getString(R.string.title_advanced_notify_action_hide),
- piHide)
- .setShowsUserInterface(false)
- .setAllowGeneratedReplies(false);
- mbuilder.addAction(actionHide.build());
-
- wactions.add(actionHide.build());
- }
-
- if (notify_snooze) {
- Intent snooze = new Intent(context, ServiceUI.class)
- .setAction("snooze:" + message.id)
- .putExtra("group", group);
- PendingIntent piSnooze = PendingIntentCompat.getService(
- context, ServiceUI.PI_SNOOZE, snooze, PendingIntent.FLAG_UPDATE_CURRENT);
- NotificationCompat.Action.Builder actionSnooze = new NotificationCompat.Action.Builder(
- R.drawable.twotone_timelapse_24,
- context.getString(R.string.title_advanced_notify_action_snooze),
- piSnooze)
- .setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_MUTE)
- .setShowsUserInterface(false)
- .setAllowGeneratedReplies(false);
- mbuilder.addAction(actionSnooze.build());
-
- wactions.add(actionSnooze.build());
- }
-
- if (message.content && notify_preview) {
- // Android will truncate the text
- String preview = message.preview;
- if (notify_preview_all)
- try {
- File file = message.getFile(context);
- preview = HtmlHelper.getFullText(file);
- if (preview != null && preview.length() > MAX_PREVIEW)
- preview = preview.substring(0, MAX_PREVIEW);
- } catch (Throwable ex) {
- Log.e(ex);
- }
-
- // Wearables
- StringBuilder sb = new StringBuilder();
- if (!TextUtils.isEmpty(message.subject))
- sb.append(TextHelper.transliterate(context, message.subject));
- if (wearable_preview && !TextUtils.isEmpty(preview)) {
- if (sb.length() > 0)
- sb.append(" - ");
- sb.append(TextHelper.transliterate(context, preview));
- }
- if (sb.length() > 0)
- mbuilder.setContentText(sb.toString());
-
- // Device
- if (!notify_messaging) {
- StringBuilder sbm = new StringBuilder();
-
- if (message.keywords != null && BuildConfig.DEBUG)
- for (String keyword : message.keywords)
- if (keyword.startsWith("!"))
- sbm.append(Html.escapeHtml(keyword)).append(": ");
-
- if (!TextUtils.isEmpty(message.subject))
- sbm.append("").append(Html.escapeHtml(message.subject)).append("").append("
");
-
- if (!TextUtils.isEmpty(preview))
- sbm.append(Html.escapeHtml(preview));
-
- if (sbm.length() > 0) {
- NotificationCompat.BigTextStyle bigText = new NotificationCompat.BigTextStyle()
- .bigText(HtmlHelper.fromHtml(sbm.toString(), context));
- if (!TextUtils.isEmpty(message.subject))
- bigText.setSummaryText(message.subject);
-
- mbuilder.setStyle(bigText);
- }
- }
- } else {
- if (!TextUtils.isEmpty(message.subject))
- mbuilder.setContentText(TextHelper.transliterate(context, message.subject));
- }
-
- if (info[0].hasPhoto())
- mbuilder.setLargeIcon(info[0].getPhotoBitmap());
-
- if (info[0].hasLookupUri()) {
- Person.Builder you = new Person.Builder()
- .setUri(info[0].getLookupUri().toString());
- mbuilder.addPerson(you.build());
- }
-
- if (pro) {
- Integer color = getColor(message);
- if (color != null) {
- mbuilder.setColor(color);
- mbuilder.setColorized(true);
- }
- }
-
- // https://developer.android.com/training/wearables/notifications
- // https://developer.android.com/reference/androidx/core/app/NotificationCompat.Action.WearableExtender
- mbuilder.extend(new NotificationCompat.WearableExtender()
- .addActions(wactions)
- .setDismissalId(BuildConfig.APPLICATION_ID + ":" + id)
- /* .setBridgeTag(id < 0 ? "header" : "body") */);
-
- // https://developer.android.com/reference/androidx/core/app/NotificationCompat.CarExtender
- mbuilder.extend(new NotificationCompat.CarExtender());
-
- notifications.add(mbuilder);
- }
-
- return notifications;
- }
-
- private static Integer getColor(TupleMessageEx message) {
- if (!message.folderUnified && message.folderColor != null)
- return message.folderColor;
- return message.accountColor;
- }
-
- private static void setLightAndSound(NotificationCompat.Builder builder, boolean light, String sound) {
- int def = 0;
-
- if (light) {
- def |= DEFAULT_LIGHTS;
- Log.i("Notify light enabled");
- }
-
- if (!"".equals(sound)) {
- // Not silent sound
- Uri uri = (sound == null ? null : Uri.parse(sound));
- if (uri != null && !"content".equals(uri.getScheme()))
- uri = null;
- Log.i("Notify sound=" + uri);
-
- if (uri == null)
- def |= DEFAULT_SOUND;
- else
- builder.setSound(uri);
- }
-
- builder.setDefaults(def);
- }
-
- // 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
-
- static NotificationCompat.Builder getNotificationError(Context context, String channel, EntityAccount account, long id, Throwable ex) {
- String title = context.getString(R.string.title_notification_failed, account.name);
- String message = Log.formatThrowable(ex, "\n", false);
-
- // Build pending intent
- Intent intent = new Intent(context, ActivityError.class);
- intent.setAction(channel + ":" + account.id + ":" + id);
- intent.putExtra("title", title);
- intent.putExtra("message", message);
- intent.putExtra("provider", account.provider);
- intent.putExtra("account", account.id);
- intent.putExtra("protocol", account.protocol);
- intent.putExtra("auth_type", account.auth_type);
- intent.putExtra("faq", 22);
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- PendingIntent pi = PendingIntentCompat.getActivity(
- context, ActivityError.PI_ERROR, intent, PendingIntent.FLAG_UPDATE_CURRENT);
-
- // Build notification
- NotificationCompat.Builder builder =
- new NotificationCompat.Builder(context, channel)
- .setSmallIcon(R.drawable.baseline_warning_white_24)
- .setContentTitle(title)
- .setContentText(Log.formatThrowable(ex, false))
- .setContentIntent(pi)
- .setAutoCancel(false)
- .setShowWhen(true)
- .setPriority(NotificationCompat.PRIORITY_MAX)
- .setOnlyAlertOnce(true)
- .setCategory(NotificationCompat.CATEGORY_ERROR)
- .setVisibility(NotificationCompat.VISIBILITY_SECRET)
- .setStyle(new NotificationCompat.BigTextStyle().bigText(message));
-
- return builder;
- }
-
- static class State {
- private int backoff;
- private boolean backingoff = false;
- private ConnectionHelper.NetworkState networkState;
- private Thread thread = new Thread();
- private Semaphore semaphore = new Semaphore(0);
- private boolean started = false;
- private boolean running = true;
- private boolean foreground = false;
- private boolean recoverable = true;
- private Throwable unrecoverable = null;
- private Long lastActivity = null;
-
- private long serial = 0;
-
- State(ConnectionHelper.NetworkState networkState) {
- this.networkState = networkState;
- }
-
- void setNetworkState(ConnectionHelper.NetworkState networkState) {
- this.networkState = networkState;
- }
-
- ConnectionHelper.NetworkState getNetworkState() {
- return networkState;
- }
-
- void setBackoff(int value) {
- this.backoff = value;
- }
-
- int getBackoff() {
- return backoff;
- }
-
- void runnable(Runnable runnable, String name) {
- thread = new Thread(runnable, name);
- thread.setPriority(THREAD_PRIORITY_BACKGROUND);
- }
-
- boolean release() {
- if (!thread.isAlive())
- return false;
-
- semaphore.release();
- yield();
- return true;
- }
-
- boolean acquire(long milliseconds, boolean backingoff) throws InterruptedException {
- try {
- this.backingoff = backingoff;
- return semaphore.tryAcquire(milliseconds, TimeUnit.MILLISECONDS);
- } finally {
- this.backingoff = false;
- }
- }
-
- void error(Throwable ex) {
- if (ex instanceof MessagingException &&
- ("connection failure".equals(ex.getMessage()) ||
- "Not connected".equals(ex.getMessage()) || // POP3
- ex.getCause() instanceof SocketException ||
- ex.getCause() instanceof ConnectionException))
- recoverable = false;
-
- if (ex instanceof ConnectionException)
- // failed to create new store connection
- // BYE, Socket is closed
- recoverable = false;
-
- if (ex instanceof StoreClosedException ||
- ex instanceof FolderClosedException ||
- ex instanceof FolderNotFoundException)
- // Lost folder connection to server
- recoverable = false;
-
- if (ex instanceof IllegalStateException && (
- "Not connected".equals(ex.getMessage()) ||
- "This operation is not allowed on a closed folder".equals(ex.getMessage())))
- recoverable = false;
-
- if (ex instanceof OperationCanceledException)
- recoverable = false;
-
- if (!recoverable)
- unrecoverable = ex;
-
- if (!backingoff) {
- thread.interrupt();
- yield();
- }
- }
-
- void reset() {
- Thread.currentThread().interrupted(); // clear interrupted status
- Log.i("Permits=" + semaphore.drainPermits());
- recoverable = true;
- lastActivity = null;
- }
-
- void nextSerial() {
- serial++;
- }
-
- private void yield() {
- try {
- // Give interrupted thread some time to acquire wake lock
- Thread.sleep(YIELD_DURATION);
- } catch (InterruptedException ignored) {
- }
- }
-
- void start() {
- thread.start();
- started = true;
- }
-
- void stop() {
- running = false;
- semaphore.release();
- }
-
- boolean isAlive() {
- if (!started)
- return true;
- if (!running)
- return false;
- if (thread == null)
- return false;
- return thread.isAlive();
- }
-
- void join() {
- join(thread);
- CoalMine.watch(thread, getClass().getSimpleName() + "#join()");
- }
-
- void ensureRunning(String reason) throws OperationCanceledException {
- if (!recoverable && unrecoverable != null)
- throw new OperationCanceledExceptionEx(reason, unrecoverable);
- if (!running)
- throw new OperationCanceledException(reason);
- }
-
- boolean isRunning() {
- return running;
- }
-
- boolean isRecoverable() {
- return recoverable;
- }
-
- Throwable getUnrecoverable() {
- return unrecoverable;
- }
-
- void join(Thread thread) {
- boolean joined = false;
- boolean interrupted = false;
- String name = thread.getName();
- while (!joined)
- try {
- Log.i("Joining " + name +
- " alive=" + thread.isAlive() +
- " state=" + thread.getState() +
- " interrupted=" + interrupted);
-
- thread.join(interrupted ? JOIN_WAIT_INTERRUPT : JOIN_WAIT_ALIVE);
-
- // https://docs.oracle.com/javase/7/docs/api/java/lang/Thread.State.html
- Thread.State state = thread.getState();
- if (thread.isAlive() &&
- state != Thread.State.NEW &&
- state != Thread.State.TERMINATED) {
- if (interrupted)
- Log.e("Join " + name + " failed" +
- " state=" + state + " interrupted=" + interrupted);
- if (interrupted)
- joined = true; // giving up
- else {
- thread.interrupt();
- interrupted = true;
- }
- } else {
- Log.i("Joined " + name + " " + " state=" + state);
- joined = true;
- }
- } catch (InterruptedException ex) {
- Log.i(new Throwable(name, ex));
- }
- }
-
- synchronized void activity() {
- lastActivity = SystemClock.elapsedRealtime();
- }
-
- long getIdleTime() {
- Long last = lastActivity;
- return (last == null ? 0 : SystemClock.elapsedRealtime() - last);
- }
-
- long getSerial() {
- return serial;
- }
-
- void setForeground(boolean value) {
- this.foreground = value;
- }
-
- boolean getForeground() {
- return this.foreground;
- }
-
- @NonNull
- @Override
- public String toString() {
- return "[running=" + running +
- ",recoverable=" + recoverable +
- ",idle=" + getIdleTime() + "" +
- ",serial=" + serial + "]";
- }
- }
-
- static class OperationCanceledExceptionEx extends OperationCanceledException {
- private Throwable cause;
-
- OperationCanceledExceptionEx(String message, Throwable cause) {
- super(message);
- this.cause = cause;
- }
-
- @Nullable
- @Override
- public Throwable getCause() {
- return this.cause;
- }
- }
-
- private static class SyncStats {
- long search_ms;
- int flags;
- long flags_ms;
- int uids;
- long uids_ms;
- int headers;
- long headers_ms;
- long content;
- long attachments;
- long total;
-
- boolean isEmpty() {
- return (search_ms == 0 &&
- flags == 0 &&
- flags_ms == 0 &&
- uids == 0 &&
- uids_ms == 0 &&
- headers == 0 &&
- headers_ms == 0 &&
- content == 0 &&
- attachments == 0 &&
- total == 0);
- }
-
- @Override
- public String toString() {
- return "search=" + search_ms + " ms" +
- " flags=" + flags + "/" + flags_ms + " ms" +
- " uids=" + uids + "/" + uids_ms + " ms" +
- " headers=" + headers + "/" + headers_ms + " ms" +
- " content=" + Helper.humanReadableByteCount(content) +
- " attachments=" + Helper.humanReadableByteCount(attachments) +
- " total=" + total + " ms";
- }
- }
-
- static class NotificationData {
- private Map> groupNotifying = new HashMap<>();
-
- NotificationData(Context context) {
- // Get existing notifications
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
- try {
- NotificationManager nm = Helper.getSystemService(context, NotificationManager.class);
- for (StatusBarNotification sbn : nm.getActiveNotifications()) {
- String tag = sbn.getTag();
- if (tag != null && tag.startsWith("unseen.")) {
- String[] p = tag.split(("\\."));
- long group = Long.parseLong(p[1]);
- long id = sbn.getNotification().extras.getLong("id", 0);
-
- if (!groupNotifying.containsKey(group))
- groupNotifying.put(group, new ArrayList<>());
-
- if (id > 0) {
- EntityLog.log(context, EntityLog.Type.Notification, null, null, id,
- "Notify restore " + tag + " id=" + id);
- groupNotifying.get(group).add(id);
- }
- }
- }
- } catch (Throwable ex) {
- Log.w(ex);
- /*
- java.lang.RuntimeException: Unable to create service eu.faircode.email.ServiceSynchronize: java.lang.NullPointerException: Attempt to invoke virtual method 'java.util.List android.content.pm.ParceledListSlice.getList()' on a null object reference
- at android.app.ActivityThread.handleCreateService(ActivityThread.java:2944)
- at android.app.ActivityThread.access$1900(ActivityThread.java:154)
- at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1474)
- at android.os.Handler.dispatchMessage(Handler.java:102)
- at android.os.Looper.loop(Looper.java:234)
- at android.app.ActivityThread.main(ActivityThread.java:5526)
- */
- }
- }
- }
-}