summaryrefslogtreecommitdiffstats
path: root/provider_src/com/android/email/mail/store/ImapFolder.java
diff options
context:
space:
mode:
Diffstat (limited to 'provider_src/com/android/email/mail/store/ImapFolder.java')
-rw-r--r--provider_src/com/android/email/mail/store/ImapFolder.java377
1 files changed, 375 insertions, 2 deletions
diff --git a/provider_src/com/android/email/mail/store/ImapFolder.java b/provider_src/com/android/email/mail/store/ImapFolder.java
index 2eefdfec3..eb0535d2a 100644
--- a/provider_src/com/android/email/mail/store/ImapFolder.java
+++ b/provider_src/com/android/email/mail/store/ImapFolder.java
@@ -52,6 +52,8 @@ import com.android.emailcommon.utility.Utility;
import com.android.mail.utils.LogUtils;
import com.google.common.annotations.VisibleForTesting;
+import static com.android.emailcommon.Logging.LOG_TAG;
+
import org.apache.commons.io.IOUtils;
import java.io.File;
@@ -60,6 +62,7 @@ import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
+import java.net.SocketTimeoutException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
@@ -68,13 +71,39 @@ import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
+import java.util.Map;
import java.util.TimeZone;
-class ImapFolder extends Folder {
+public class ImapFolder extends Folder {
private final static Flag[] PERMANENT_FLAGS =
{ Flag.DELETED, Flag.SEEN, Flag.FLAGGED, Flag.ANSWERED };
private static final int COPY_BUFFER_SIZE = 16*1024;
+ public interface IdleCallback {
+ /**
+ * Invoked when the connection enters idle mode
+ */
+ public void onIdled();
+ /**
+ * Invoked when a new change is communicated by the server.
+ *
+ * @param needSync whether a sync is required
+ * @param fetchMessages list of message UIDs to update
+ */
+ public void onNewServerChange(boolean needSync, List<String> fetchMessages);
+ /**
+ * Connection to socket timed out. The idle connection needs
+ * to be considered broken when this is called.
+ */
+ public void onTimeout();
+ /**
+ * Something went wrong while waiting for push data.
+ *
+ * @param ex the exception detected
+ */
+ public void onException(MessagingException ex);
+ }
+
private final ImapStore mStore;
private final String mName;
private int mMessageCount = -1;
@@ -86,6 +115,22 @@ class ImapFolder extends Folder {
/** A set of hashes that can be used to track dirtiness */
Object mHash[];
+ private final Object mIdleSync = new Object();
+ private boolean mIdling;
+ private boolean mIdlingCancelled;
+ private boolean mDiscardIdlingConnection;
+ private Thread mIdleReader;
+
+ private static final String[] IDLE_STATUSES = {
+ ImapConstants.UIDVALIDITY, ImapConstants.UIDNEXT
+ };
+ private Map<String, String> mIdleStatuses = new HashMap<>();
+
+ private static class ImapIdleChanges {
+ public boolean mRequiredSync = false;
+ public ArrayList<String> mMessageToFetch = new ArrayList<>();
+ }
+
/*package*/ ImapFolder(ImapStore store, String name) {
mStore = store;
mName = name;
@@ -176,6 +221,159 @@ class ImapFolder extends Folder {
return mName;
}
+ public void startIdling(final IdleCallback callback) throws MessagingException {
+ checkOpen();
+ synchronized (mIdleSync) {
+ if (mIdling) {
+ throw new MessagingException("Folder " + mName + " is in IDLE state already.");
+ }
+ mIdling = true;
+ mIdlingCancelled = false;
+ mDiscardIdlingConnection = false;
+ }
+
+ // Run idle in background
+ mIdleReader = new Thread() {
+ @Override
+ public void run() {
+ try {
+ // Get some info before start idling
+ mIdleStatuses = getStatuses(IDLE_STATUSES);
+
+ // We setup the max time specified in RFC 2177 to re-issue
+ // an idle request to the server
+ mConnection.setReadTimeout(ImapConnection.PING_IDLE_TIMEOUT);
+ mConnection.destroyResponses();
+
+ // Enter now in idle status (we hold a connection with
+ // the server to listen for new changes)
+ synchronized (mIdleSync) {
+ if (mIdlingCancelled) {
+ return;
+ }
+ }
+
+ if (callback != null) {
+ callback.onIdled();
+ }
+ List<ImapResponse> responses = mConnection.executeIdleCommand();
+
+ // Check whether IDLE was successful (first response is an idling response)
+ if (responses.isEmpty() || (mIdling && !responses.get(0).isIdling())) {
+ if (callback != null) {
+ callback.onException(new MessagingException(
+ MessagingException.SERVER_ERROR, "Cannot idle"));
+ }
+ synchronized (mIdleSync) {
+ mIdling = false;
+ }
+ return;
+ }
+
+ // Exit idle if we are still in that state
+ boolean cancelled = false;
+ boolean discardConnection = false;
+ synchronized (mIdleSync) {
+ if (!mIdlingCancelled) {
+ try {
+ mConnection.setReadTimeout(ImapConnection.DONE_TIMEOUT);
+ mConnection.executeSimpleCommand(ImapConstants.DONE);
+ } catch (MessagingException me) {
+ // Ignore this exception caused by messages in the queue
+ }
+ }
+
+ cancelled = mIdlingCancelled;
+ discardConnection = mDiscardIdlingConnection;
+ }
+
+ if (!cancelled && callback != null) {
+ // Notify that new changes exists in the server. Remove
+ // the idling status response since is only relevant for the protocol
+ // We have to enter in idle
+ ImapIdleChanges changes = extractImapChanges(
+ new ArrayList<Object>(responses.subList(1, responses.size())));
+ callback.onNewServerChange(changes.mRequiredSync, changes.mMessageToFetch);
+ }
+
+ if (discardConnection) {
+ // Return the connection to the pool
+ close(false);
+ }
+
+ synchronized (mIdleSync) {
+ mIdling = false;
+ }
+
+ } catch (MessagingException me) {
+ close(false);
+ synchronized (mIdleSync) {
+ mIdling = false;
+ }
+ if (callback != null) {
+ callback.onException(me);
+ }
+
+ } catch (SocketTimeoutException ste) {
+ close(false);
+ synchronized (mIdleSync) {
+ mIdling = false;
+ }
+ if (callback != null) {
+ callback.onTimeout();
+ }
+
+ } catch (IOException ioe) {
+ close(false);
+ synchronized (mIdleSync) {
+ mIdling = false;
+ }
+ if (callback != null) {
+ callback.onException(ioExceptionHandler(mConnection, ioe));
+ }
+
+ }
+ }
+ };
+ mIdleReader.setName("IdleReader " + mStore.getAccount().mId + ":" + mName);
+ mIdleReader.start();
+ }
+
+ public void stopIdling(boolean discardConnection) throws MessagingException {
+ if (!isOpen()) {
+ throw new MessagingException("Folder " + mName + " is not open.");
+ }
+ synchronized (mIdleSync) {
+ if (!mIdling) {
+ throw new MessagingException("Folder " + mName + " isn't in IDLE state.");
+ }
+ try {
+ mIdlingCancelled = true;
+ mDiscardIdlingConnection = discardConnection;
+ // We can read responses here because we can block the buffer. Read commands
+ // are always done by startListener method (blocking idle)
+ mConnection.sendCommand(ImapConstants.DONE, false);
+
+ } catch (MessagingException me) {
+ // Treat IOERROR messaging exception as IOException
+ if (me.getExceptionType() == MessagingException.IOERROR) {
+ close(false);
+ throw me;
+ }
+
+ } catch (IOException ioe) {
+ throw ioExceptionHandler(mConnection, ioe);
+
+ }
+ }
+ }
+
+ public boolean isIdling() {
+ synchronized (mIdleSync) {
+ return mIdling;
+ }
+ }
+
@Override
public boolean exists() throws MessagingException {
if (mExists) {
@@ -373,6 +571,58 @@ class ImapFolder extends Folder {
}
}
+ public Map<String, String> getStatuses(String[] statuses) throws MessagingException {
+ checkOpen();
+ Map<String, String> allReturnStatuses = new HashMap<>();
+ try {
+ String flags = TextUtils.join(" ", statuses);
+ final List<ImapResponse> responses = mConnection.executeSimpleCommand(
+ String.format(Locale.US,
+ ImapConstants.STATUS + " \"%s\" (%s)",
+ ImapStore.encodeFolderName(mName, mStore.mPathPrefix), flags));
+ // S: * STATUS mboxname (MESSAGES 231 UIDNEXT 44292)
+ for (ImapResponse response : responses) {
+ if (response.isDataResponse(0, ImapConstants.STATUS)) {
+ ImapList list = response.getListOrEmpty(2);
+ int count = list.size();
+ for (int i = 0; i < count; i += 2) {
+ String key = list.getStringOrEmpty(i).getString();
+ String value = list.getStringOrEmpty(i + 1).getString();
+ allReturnStatuses.put(key, value);
+ }
+ }
+ }
+ } catch (IOException ioe) {
+ throw ioExceptionHandler(mConnection, ioe);
+ } finally {
+ destroyResponses();
+ }
+ return allReturnStatuses;
+ }
+
+ private List<String> getNewMessagesFromUid(String uid) throws MessagingException {
+ checkOpen();
+ List<String> nextMSNs = new ArrayList<>();
+ try {
+ final List<ImapResponse> responses = mConnection.executeSimpleCommand(
+ ImapConstants.SEARCH + " " + ImapConstants.UID + " " + uid + ":*");
+ // S: * SEARCH 1 2 3
+ for (ImapResponse response : responses) {
+ if (response.isDataResponse(0, ImapConstants.SEARCH)) {
+ int count = response.size();
+ for (int i = 1; i < count; i++) {
+ nextMSNs.add(response.getStringOrEmpty(i).getString());
+ }
+ }
+ }
+ } catch (IOException ioe) {
+ throw ioExceptionHandler(mConnection, ioe);
+ } finally {
+ destroyResponses();
+ }
+ return nextMSNs;
+ }
+
@Override
public void delete(boolean recurse) {
throw new Error("ImapStore.delete() not yet implemented");
@@ -1270,7 +1520,9 @@ class ImapFolder extends Folder {
if (DebugUtils.DEBUG) {
LogUtils.d(Logging.LOG_TAG, "IO Exception detected: ", ioe);
}
- connection.close();
+ if (connection != null) {
+ connection.close();
+ }
if (connection == mConnection) {
mConnection = null; // To prevent close() from returning the connection to the pool.
close(false);
@@ -1278,6 +1530,127 @@ class ImapFolder extends Folder {
return new MessagingException(MessagingException.IOERROR, "IO Error", ioe);
}
+ private ImapIdleChanges extractImapChanges(List<Object> changes) throws MessagingException {
+ // Process the changes and fill the idle changes structure.
+ // Basically we should look for the next commands in this method:
+ //
+ // OK DONE
+ // No more changes
+ // n EXISTS
+ // Indicates that the mailbox changed => ignore
+ // n EXPUNGE
+ // Indicates a message were completely deleted => a full sync is required
+ // n RECENT
+ // New messages waiting in the server => use UIDNEXT to search for the new messages.
+ // If isn't possible to retrieve the new UID messages, then a full sync is required
+ // n FETCH (UID X FLAGS (...))
+ // a message has changed and requires to fetch only X message
+ // (something change on that item). If UID is not present, a conversion
+ // from MSN to UID is required
+
+ final ImapIdleChanges imapIdleChanges = new ImapIdleChanges();
+
+ int count = changes.size();
+ if (Logging.LOGD) {
+ for (int i = 0; i < count; i++) {
+ ImapResponse change = (ImapResponse) changes.get(i);
+ if (Logging.LOGD) {
+ LogUtils.d(Logging.LOG_TAG, "Received: " + change.toString());
+ }
+ }
+ }
+
+ // We can't ask to the server, because the responses will be destroyed. We need
+ // to compute and fetch any related after we have all the responses processed
+ boolean hasNewMessages = false;
+ List<String> msns = new ArrayList<>();
+ for (int i = 0; i < count; i++) {
+ ImapResponse change = (ImapResponse) changes.get(i);
+ if (change.isOk() || change.isNo() || change.isBad()) {
+ // No more processing. DONE included
+ break;
+ }
+ try {
+ ImapElement element = change.getElementOrNone(1);
+ if (element.equals(ImapElement.NONE)) {
+ continue;
+ }
+ if (!element.isString()) {
+ continue;
+ }
+
+ ImapString op = (ImapString) element;
+ if (op.is(ImapConstants.DONE)) {
+ break;
+ } else if (op.is(ImapConstants.EXISTS)) {
+ continue;
+ } else if (op.is(ImapConstants.EXPUNGE)) {
+ imapIdleChanges.mRequiredSync = true;
+ } else if (op.is(ImapConstants.RECENT)) {
+ hasNewMessages = true;
+ } else if (op.is(ImapConstants.FETCH)
+ && change.getElementOrNone(2).isList()) {
+ ImapList messageFlags = (ImapList) change.getElementOrNone(2);
+ String uid = ((ImapString) messageFlags.getKeyedStringOrEmpty(
+ ImapConstants.UID, true)).getString();
+ if (!TextUtils.isEmpty(uid) &&
+ !imapIdleChanges.mMessageToFetch.contains(uid)) {
+ imapIdleChanges.mMessageToFetch.add(uid);
+ } else {
+ msns.add(change.getStringOrEmpty(0).getString());
+ }
+ } else {
+ if (Logging.LOGD) {
+ LogUtils.w(LOG_TAG, "Unrecognized imap change (" + change
+ + ") for mailbox " + mName);
+ }
+ }
+
+ } catch (Exception ex) {
+ if (Logging.LOGD) {
+ LogUtils.e(LOG_TAG, "Failure processing imap change (" + change
+ + ") for mailbox " + mName, ex);
+ }
+ }
+ }
+
+ // Check whether UIDVALIDITY changed - if yes, a full sync request is required
+ // NOTE: This needs to happen after parsing all responses; otherwise
+ // getStatuses will destroy the response
+ Map<String, String> statuses = getStatuses(new String[] { ImapConstants.UIDVALIDITY });
+ String oldUidValidity = mIdleStatuses.get(ImapConstants.UIDVALIDITY);
+ String newUidValidity = statuses.get(ImapConstants.UIDVALIDITY);
+ if (!TextUtils.equals(oldUidValidity, newUidValidity)) {
+ imapIdleChanges.mMessageToFetch.clear();
+ imapIdleChanges.mRequiredSync = true;
+ return imapIdleChanges;
+ }
+
+ // Recover the UIDs of new messages in case we don't do a full sync anyway
+ if (!imapIdleChanges.mRequiredSync) {
+ try {
+ // Retrieve new message UIDs
+ String uidNext = mIdleStatuses.get(ImapConstants.UIDNEXT);
+ if (hasNewMessages && !TextUtils.isEmpty(uidNext)) {
+ msns.addAll(getNewMessagesFromUid(uidNext));
+ }
+
+ // Transform MSNs to UIDs
+ for (String msn : msns) {
+ String[] uids = searchForUids(String.format(Locale.US, "%s:%s", msn, msn));
+ imapIdleChanges.mMessageToFetch.add(uids[0]);
+ }
+ } catch (MessagingException ex) {
+ // Server doesn't support UID. We have to do a full sync (since
+ // we don't know what message changed)
+ imapIdleChanges.mMessageToFetch.clear();
+ imapIdleChanges.mRequiredSync = true;
+ }
+ }
+
+ return imapIdleChanges;
+ }
+
@Override
public boolean equals(Object o) {
if (o instanceof ImapFolder) {