summaryrefslogtreecommitdiffstats
path: root/provider_src/com/android/email/mail
diff options
context:
space:
mode:
authorJorge Ruesga <jorge@ruesga.com>2015-05-01 21:35:23 +0200
committerSteve Kondik <steve@cyngn.com>2015-10-18 14:05:32 -0700
commit08ace26ed605946d788ce56f5c9aefc65131a63b (patch)
treef1014dc39087078bf19111b76427ab58ba78ad4b /provider_src/com/android/email/mail
parent3b1b30873e9c07139e2cc9fdaa796151592fea69 (diff)
downloadandroid_packages_apps_Email-08ace26ed605946d788ce56f5c9aefc65131a63b.tar.gz
android_packages_apps_Email-08ace26ed605946d788ce56f5c9aefc65131a63b.tar.bz2
android_packages_apps_Email-08ace26ed605946d788ce56f5c9aefc65131a63b.zip
email: imap push
Change-Id: I8a184a5644e4322ee65d969e14cd47fe119f5df2 Signed-off-by: Jorge Ruesga <jorge@ruesga.com>
Diffstat (limited to 'provider_src/com/android/email/mail')
-rw-r--r--provider_src/com/android/email/mail/store/ImapConnection.java85
-rw-r--r--provider_src/com/android/email/mail/store/ImapFolder.java377
-rw-r--r--provider_src/com/android/email/mail/store/ImapStore.java9
-rw-r--r--provider_src/com/android/email/mail/store/Pop3Store.java4
-rw-r--r--provider_src/com/android/email/mail/store/imap/ImapConstants.java4
-rw-r--r--provider_src/com/android/email/mail/store/imap/ImapList.java2
-rw-r--r--provider_src/com/android/email/mail/store/imap/ImapResponse.java7
-rw-r--r--provider_src/com/android/email/mail/store/imap/ImapResponseParser.java37
-rw-r--r--provider_src/com/android/email/mail/transport/MailTransport.java8
9 files changed, 520 insertions, 13 deletions
diff --git a/provider_src/com/android/email/mail/store/ImapConnection.java b/provider_src/com/android/email/mail/store/ImapConnection.java
index bf4bb2a4c..bef5f346e 100644
--- a/provider_src/com/android/email/mail/store/ImapConnection.java
+++ b/provider_src/com/android/email/mail/store/ImapConnection.java
@@ -36,6 +36,7 @@ import com.android.emailcommon.mail.MessagingException;
import com.android.mail.utils.LogUtils;
import java.io.IOException;
+import java.net.SocketTimeoutException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -50,6 +51,15 @@ class ImapConnection {
// Always check in FALSE
private static final boolean DEBUG_FORCE_SEND_ID = false;
+ // RFC 2177 defines that IDLE connections must be refreshed at least every 29 minutes
+ public static final int PING_IDLE_TIMEOUT = 29 * 60 * 1000;
+
+ // Special timeout for DONE operations
+ public static final int DONE_TIMEOUT = 5 * 1000;
+
+ // Time to wait between the first idle message and triggering the changes
+ private static final int IDLE_OP_READ_TIMEOUT = 500;
+
/** ID capability per RFC 2971*/
public static final int CAPABILITY_ID = 1 << 0;
/** NAMESPACE capability per RFC 2342 */
@@ -58,6 +68,8 @@ class ImapConnection {
public static final int CAPABILITY_STARTTLS = 1 << 2;
/** UIDPLUS capability per RFC 4315 */
public static final int CAPABILITY_UIDPLUS = 1 << 3;
+ /** IDLE capability per RFC 2177 */
+ public static final int CAPABILITY_IDLE = 1 << 4;
/** The capabilities supported; a set of CAPABILITY_* values. */
private int mCapabilities;
@@ -69,6 +81,8 @@ class ImapConnection {
private String mAccessToken;
private String mIdPhrase = null;
+ private boolean mIdling = false;
+
/** # of command/response lines to log upon crash. */
private static final int DISCOURSE_LOGGER_SIZE = 64;
private final DiscourseLogger mDiscourse = new DiscourseLogger(DISCOURSE_LOGGER_SIZE);
@@ -210,10 +224,23 @@ class ImapConnection {
mImapStore = null;
}
+ int getReadTimeout() throws IOException {
+ if (mTransport == null) {
+ return MailTransport.SOCKET_READ_TIMEOUT;
+ }
+ return mTransport.getReadTimeout();
+ }
+
+ void setReadTimeout(int timeout) throws IOException {
+ if (mTransport != null) {
+ mTransport.setReadTimeout(timeout);
+ }
+ }
+
/**
* Returns whether or not the specified capability is supported by the server.
*/
- private boolean isCapable(int capability) {
+ public boolean isCapable(int capability) {
return (mCapabilities & capability) != 0;
}
@@ -235,6 +262,9 @@ class ImapConnection {
if (capabilities.contains(ImapConstants.STARTTLS)) {
mCapabilities |= CAPABILITY_STARTTLS;
}
+ if (capabilities.contains(ImapConstants.IDLE)) {
+ mCapabilities |= CAPABILITY_IDLE;
+ }
}
/**
@@ -273,6 +303,12 @@ class ImapConnection {
*/
String sendCommand(String command, boolean sensitive)
throws MessagingException, IOException {
+ // Don't allow any command other than DONE when idling
+ if (mIdling && !command.equals(ImapConstants.DONE)) {
+ return null;
+ }
+ mIdling = command.equals(ImapConstants.IDLE);
+
LogUtils.d(Logging.LOG_TAG, "sendCommand %s", (sensitive ? IMAP_REDACTED_LOG : command));
open();
return sendCommandInternal(command, sensitive);
@@ -284,7 +320,13 @@ class ImapConnection {
throw new IOException("Null transport");
}
String tag = Integer.toString(mNextCommandTag.incrementAndGet());
- String commandToSend = tag + " " + command;
+ final String commandToSend;
+ if (command.equals(ImapConstants.DONE)) {
+ // Do not send a tag for DONE command
+ commandToSend = command;
+ } else {
+ commandToSend = tag + " " + command;
+ }
mTransport.writeLine(commandToSend, sensitive ? IMAP_REDACTED_LOG : null);
mDiscourse.addSentCommand(sensitive ? IMAP_REDACTED_LOG : commandToSend);
return tag;
@@ -327,6 +369,11 @@ class ImapConnection {
return executeSimpleCommand(command, false);
}
+ List<ImapResponse> executeIdleCommand() throws IOException, MessagingException {
+ mParser.expectIdlingResponse();
+ return executeSimpleCommand(ImapConstants.IDLE, false);
+ }
+
/**
* Read and return all of the responses from the most recent command sent to the server
*
@@ -336,13 +383,35 @@ class ImapConnection {
*/
List<ImapResponse> getCommandResponses() throws IOException, MessagingException {
final List<ImapResponse> responses = new ArrayList<ImapResponse>();
- ImapResponse response;
- do {
- response = mParser.readResponse();
- responses.add(response);
- } while (!response.isTagged());
+ ImapResponse response = null;
+ boolean idling = false;
+ boolean throwSocketTimeoutEx = true;
+ int lastSocketTimeout = getReadTimeout();
+ try {
+ do {
+ response = mParser.readResponse();
+ if (idling) {
+ setReadTimeout(IDLE_OP_READ_TIMEOUT);
+ throwSocketTimeoutEx = false;
+ }
+ responses.add(response);
+ if (response.isIdling()) {
+ idling = true;
+ }
+ } while (idling || !response.isTagged());
+ } catch (SocketTimeoutException ex) {
+ if (throwSocketTimeoutEx) {
+ throw ex;
+ }
+ } finally {
+ mParser.resetIdlingStatus();
+ if (lastSocketTimeout != getReadTimeout()) {
+ setReadTimeout(lastSocketTimeout);
+ }
+ }
- if (!response.isOk()) {
+ // When idling, any response is valid; otherwise it must be OK
+ if (!response.isOk() && !idling) {
final String toString = response.toString();
final String status = response.getStatusOrEmpty().getString();
final String alert = response.getAlertTextOrEmpty().getString();
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) {
diff --git a/provider_src/com/android/email/mail/store/ImapStore.java b/provider_src/com/android/email/mail/store/ImapStore.java
index 5fc83e001..ddabe1c4a 100644
--- a/provider_src/com/android/email/mail/store/ImapStore.java
+++ b/provider_src/com/android/email/mail/store/ImapStore.java
@@ -501,6 +501,14 @@ public class ImapStore extends Store {
connection.destroyResponses();
}
bundle.putInt(EmailServiceProxy.VALIDATE_BUNDLE_RESULT_CODE, result);
+
+ // Shared capabilities (check EmailProxyServices for available shared capabilities)
+ int capabilities = 0;
+ if (connection.isCapable(ImapConnection.CAPABILITY_IDLE)) {
+ capabilities |= EmailServiceProxy.CAPABILITY_PUSH;
+ }
+ bundle.putInt(EmailServiceProxy.SETTINGS_BUNDLE_CAPABILITIES, capabilities);
+
return bundle;
}
@@ -556,6 +564,7 @@ public class ImapStore extends Store {
while ((connection = mConnectionPool.poll()) != null) {
try {
connection.setStore(this);
+ connection.setReadTimeout(MailTransport.SOCKET_READ_TIMEOUT);
connection.executeSimpleCommand(ImapConstants.NOOP);
break;
} catch (MessagingException e) {
diff --git a/provider_src/com/android/email/mail/store/Pop3Store.java b/provider_src/com/android/email/mail/store/Pop3Store.java
index 4ea75ccf3..b0aa9a2eb 100644
--- a/provider_src/com/android/email/mail/store/Pop3Store.java
+++ b/provider_src/com/android/email/mail/store/Pop3Store.java
@@ -186,6 +186,10 @@ public class Pop3Store extends Store {
ioe.getMessage());
}
bundle.putInt(EmailServiceProxy.VALIDATE_BUNDLE_RESULT_CODE, result);
+
+ // No special capabilities
+ bundle.putInt(EmailServiceProxy.SETTINGS_BUNDLE_CAPABILITIES, 0);
+
return bundle;
}
diff --git a/provider_src/com/android/email/mail/store/imap/ImapConstants.java b/provider_src/com/android/email/mail/store/imap/ImapConstants.java
index 9f4d59290..9c94fcf31 100644
--- a/provider_src/com/android/email/mail/store/imap/ImapConstants.java
+++ b/provider_src/com/android/email/mail/store/imap/ImapConstants.java
@@ -46,6 +46,7 @@ public final class ImapConstants {
public static final String COPYUID = "COPYUID";
public static final String CREATE = "CREATE";
public static final String DELETE = "DELETE";
+ public static final String DONE = "DONE";
public static final String EXAMINE = "EXAMINE";
public static final String EXISTS = "EXISTS";
public static final String EXPUNGE = "EXPUNGE";
@@ -58,6 +59,8 @@ public final class ImapConstants {
public static final String FLAGS = "FLAGS";
public static final String FLAGS_SILENT = "FLAGS.SILENT";
public static final String ID = "ID";
+ public static final String IDLE = "IDLE";
+ public static final String IDLING = "idling";
public static final String INBOX = "INBOX";
public static final String INTERNALDATE = "INTERNALDATE";
public static final String LIST = "LIST";
@@ -73,6 +76,7 @@ public final class ImapConstants {
public static final String PREAUTH = "PREAUTH";
public static final String READ_ONLY = "READ-ONLY";
public static final String READ_WRITE = "READ-WRITE";
+ public static final String RECENT = "RECENT";
public static final String RENAME = "RENAME";
public static final String RFC822_SIZE = "RFC822.SIZE";
public static final String SEARCH = "SEARCH";
diff --git a/provider_src/com/android/email/mail/store/imap/ImapList.java b/provider_src/com/android/email/mail/store/imap/ImapList.java
index e28355989..2ddf8227f 100644
--- a/provider_src/com/android/email/mail/store/imap/ImapList.java
+++ b/provider_src/com/android/email/mail/store/imap/ImapList.java
@@ -180,7 +180,7 @@ public class ImapList extends ImapElement {
@Override
public String toString() {
- return mList.toString();
+ return mList != null ? mList.toString() : "[null]";
}
/**
diff --git a/provider_src/com/android/email/mail/store/imap/ImapResponse.java b/provider_src/com/android/email/mail/store/imap/ImapResponse.java
index 9f975f7bf..292ff92b2 100644
--- a/provider_src/com/android/email/mail/store/imap/ImapResponse.java
+++ b/provider_src/com/android/email/mail/store/imap/ImapResponse.java
@@ -77,6 +77,13 @@ public class ImapResponse extends ImapList {
}
/**
+ * @return whether it's an IDLE response.
+ */
+ public boolean isIdling() {
+ return is(0, ImapConstants.IDLING);
+ }
+
+ /**
* @return whether it's an {@code responseType} data response. (i.e. not tagged).
* @param index where {@code responseType} should appear. e.g. 1 for "FETCH"
* @param responseType e.g. "FETCH"
diff --git a/provider_src/com/android/email/mail/store/imap/ImapResponseParser.java b/provider_src/com/android/email/mail/store/imap/ImapResponseParser.java
index 8dd1cf610..5efea3109 100644
--- a/provider_src/com/android/email/mail/store/imap/ImapResponseParser.java
+++ b/provider_src/com/android/email/mail/store/imap/ImapResponseParser.java
@@ -66,6 +66,9 @@ public class ImapResponseParser {
*/
private final ArrayList<ImapResponse> mResponsesToDestroy = new ArrayList<ImapResponse>();
+ private boolean mIdling;
+ private boolean mExpectIdlingResponse;
+
/**
* Exception thrown when we receive BYE. It derives from IOException, so it'll be treated
* in the same way EOF does.
@@ -168,10 +171,17 @@ public class ImapResponseParser {
} catch (RuntimeException e) {
// Parser crash -- log network activities.
onParseError(e);
+ mIdling = false;
throw e;
} catch (IOException e) {
// Network error, or received an unexpected char.
- onParseError(e);
+ // If we are idling don't parse the error, just let the upper layers
+ // handle the exception
+ if (!mIdling) {
+ onParseError(e);
+ } else {
+ mIdling = false;
+ }
throw e;
}
@@ -242,6 +252,14 @@ public class ImapResponseParser {
return ret;
}
+ public void resetIdlingStatus() {
+ mIdling = false;
+ }
+
+ public void expectIdlingResponse() {
+ mExpectIdlingResponse = true;
+ }
+
/**
* Parse and return the response line.
*/
@@ -263,11 +281,26 @@ public class ImapResponseParser {
responseToDestroy = new ImapResponse(null, true);
// If it's continuation request, we don't really care what's in it.
- responseToDestroy.add(new ImapSimpleString(readUntilEol()));
+ // NOTE: specs say the server is supposed to respond to the IDLE command
+ // with a continuation request response. To simplify internal handling,
+ // we'll always construct same response (ignoring the server text response).
+ // Our implementation always returns "+ idling".
+ if (mExpectIdlingResponse) {
+ // Discard the server message and put what we expected
+ readUntilEol();
+ responseToDestroy.add(new ImapSimpleString(ImapConstants.IDLING));
+ } else {
+ responseToDestroy.add(new ImapSimpleString(readUntilEol()));
+ }
// Response has successfully been built. Let's return it.
responseToReturn = responseToDestroy;
responseToDestroy = null;
+
+ mIdling = responseToReturn.isIdling();
+ if (mIdling) {
+ mExpectIdlingResponse = true;
+ }
} else {
// Status response or response data
final String tag;
diff --git a/provider_src/com/android/email/mail/transport/MailTransport.java b/provider_src/com/android/email/mail/transport/MailTransport.java
index 26801f93f..1767f19a7 100644
--- a/provider_src/com/android/email/mail/transport/MailTransport.java
+++ b/provider_src/com/android/email/mail/transport/MailTransport.java
@@ -191,6 +191,14 @@ public class MailTransport {
}
}
+ public int getReadTimeout() throws IOException {
+ return mSocket.getSoTimeout();
+ }
+
+ public void setReadTimeout(int timeout) throws IOException {
+ mSocket.setSoTimeout(timeout);
+ }
+
/**
* Lightweight version of SSLCertificateSocketFactory.verifyHostname, which provides this
* service but is not in the public API.