diff options
Diffstat (limited to 'provider_src/com/android/email/mail/store')
8 files changed, 512 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; |