();
+
+ /* package */ void add(ImapElement e) {
+ if (e == null) {
+ throw new RuntimeException("Can't add null");
+ }
+ mList.add(e);
+ }
+
+ @Override
+ public final boolean isString() {
+ return false;
+ }
+
+ @Override
+ public final boolean isList() {
+ return true;
+ }
+
+ public final int size() {
+ return mList.size();
+ }
+
+ public final boolean isEmpty() {
+ return size() == 0;
+ }
+
+ /**
+ * Return true if the element at {@code index} exists, is string, and equals to {@code s}.
+ * (case insensitive)
+ */
+ public final boolean is(int index, String s) {
+ return is(index, s, false);
+ }
+
+ /**
+ * Same as {@link #is(int, String)}, but does the prefix match if {@code prefixMatch}.
+ */
+ public final boolean is(int index, String s, boolean prefixMatch) {
+ if (!prefixMatch) {
+ return getStringOrEmpty(index).is(s);
+ } else {
+ return getStringOrEmpty(index).startsWith(s);
+ }
+ }
+
+ /**
+ * Return the element at {@code index}.
+ * If {@code index} is out of range, returns {@link ImapElement#NONE}.
+ */
+ public final ImapElement getElementOrNone(int index) {
+ return (index >= mList.size()) ? ImapElement.NONE : mList.get(index);
+ }
+
+ /**
+ * Return the element at {@code index} if it's a list.
+ * If {@code index} is out of range or not a list, returns {@link ImapList#EMPTY}.
+ */
+ public final ImapList getListOrEmpty(int index) {
+ ImapElement el = getElementOrNone(index);
+ return el.isList() ? (ImapList) el : EMPTY;
+ }
+
+ /**
+ * Return the element at {@code index} if it's a string.
+ * If {@code index} is out of range or not a string, returns {@link ImapString#EMPTY}.
+ */
+ public final ImapString getStringOrEmpty(int index) {
+ ImapElement el = getElementOrNone(index);
+ return el.isString() ? (ImapString) el : ImapString.EMPTY;
+ }
+
+ /**
+ * Return an element keyed by {@code key}. Return null if not found. {@code key} has to be
+ * at an even index.
+ */
+ /* package */ final ImapElement getKeyedElementOrNull(String key, boolean prefixMatch) {
+ for (int i = 1; i < size(); i += 2) {
+ if (is(i-1, key, prefixMatch)) {
+ return mList.get(i);
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Return an {@link ImapList} keyed by {@code key}.
+ * Return {@link ImapList#EMPTY} if not found.
+ */
+ public final ImapList getKeyedListOrEmpty(String key) {
+ return getKeyedListOrEmpty(key, false);
+ }
+
+ /**
+ * Return an {@link ImapList} keyed by {@code key}.
+ * Return {@link ImapList#EMPTY} if not found.
+ */
+ public final ImapList getKeyedListOrEmpty(String key, boolean prefixMatch) {
+ ImapElement e = getKeyedElementOrNull(key, prefixMatch);
+ return (e != null) ? ((ImapList) e) : ImapList.EMPTY;
+ }
+
+ /**
+ * Return an {@link ImapString} keyed by {@code key}.
+ * Return {@link ImapString#EMPTY} if not found.
+ */
+ public final ImapString getKeyedStringOrEmpty(String key) {
+ return getKeyedStringOrEmpty(key, false);
+ }
+
+ /**
+ * Return an {@link ImapString} keyed by {@code key}.
+ * Return {@link ImapString#EMPTY} if not found.
+ */
+ public final ImapString getKeyedStringOrEmpty(String key, boolean prefixMatch) {
+ ImapElement e = getKeyedElementOrNull(key, prefixMatch);
+ return (e != null) ? ((ImapString) e) : ImapString.EMPTY;
+ }
+
+ /**
+ * Return true if it contains {@code s}.
+ */
+ public final boolean contains(String s) {
+ for (int i = 0; i < size(); i++) {
+ if (getStringOrEmpty(i).is(s)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public void destroy() {
+ if (mList != null) {
+ for (ImapElement e : mList) {
+ e.destroy();
+ }
+ mList = null;
+ }
+ super.destroy();
+ }
+
+ @Override
+ public String toString() {
+ return mList.toString();
+ }
+
+ /**
+ * Return the text representations of the contents concatenated with ",".
+ */
+ public final String flatten() {
+ return flatten(new StringBuilder()).toString();
+ }
+
+ /**
+ * Returns text representations (i.e. getString()) of contents joined together with
+ * "," as the separator.
+ *
+ * Only used for building the capability string passed to vendor policies.
+ *
+ * We can't use toString(), because it's for debugging (meaning the format may change any time),
+ * and it won't expand literals.
+ */
+ private final StringBuilder flatten(StringBuilder sb) {
+ sb.append('[');
+ for (int i = 0; i < mList.size(); i++) {
+ if (i > 0) {
+ sb.append(',');
+ }
+ final ImapElement e = getElementOrNone(i);
+ if (e.isList()) {
+ getListOrEmpty(i).flatten(sb);
+ } else if (e.isString()) {
+ sb.append(getStringOrEmpty(i).getString());
+ }
+ }
+ sb.append(']');
+ return sb;
+ }
+
+ @Override
+ public boolean equalsForTest(ImapElement that) {
+ if (!super.equalsForTest(that)) {
+ return false;
+ }
+ ImapList thatList = (ImapList) that;
+ if (size() != thatList.size()) {
+ return false;
+ }
+ for (int i = 0; i < size(); i++) {
+ if (!mList.get(i).equalsForTest(thatList.getElementOrNone(i))) {
+ return false;
+ }
+ }
+ return true;
+ }
+}
diff --git a/src/com/android/email/mail/store/imap/ImapMemoryLiteral.java b/src/com/android/email/mail/store/imap/ImapMemoryLiteral.java
new file mode 100644
index 000000000..ea62d52d1
--- /dev/null
+++ b/src/com/android/email/mail/store/imap/ImapMemoryLiteral.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.email.mail.store.imap;
+
+import com.android.email.FixedLengthInputStream;
+import com.android.emailcommon.Logging;
+import com.android.emailcommon.utility.Utility;
+
+import android.util.Log;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Subclass of {@link ImapString} used for literals backed by an in-memory byte array.
+ */
+public class ImapMemoryLiteral extends ImapString {
+ private byte[] mData;
+
+ /* package */ ImapMemoryLiteral(FixedLengthInputStream in) throws IOException {
+ // We could use ByteArrayOutputStream and IOUtils.copy, but it'd perform an unnecessary
+ // copy....
+ mData = new byte[in.getLength()];
+ int pos = 0;
+ while (pos < mData.length) {
+ int read = in.read(mData, pos, mData.length - pos);
+ if (read < 0) {
+ break;
+ }
+ pos += read;
+ }
+ if (pos != mData.length) {
+ Log.w(Logging.LOG_TAG, "");
+ }
+ }
+
+ @Override
+ public void destroy() {
+ mData = null;
+ super.destroy();
+ }
+
+ @Override
+ public String getString() {
+ return Utility.fromAscii(mData);
+ }
+
+ @Override
+ public InputStream getAsStream() {
+ return new ByteArrayInputStream(mData);
+ }
+
+ @Override
+ public String toString() {
+ return String.format("{%d byte literal(memory)}", mData.length);
+ }
+}
diff --git a/src/com/android/email/mail/store/imap/ImapResponse.java b/src/com/android/email/mail/store/imap/ImapResponse.java
new file mode 100644
index 000000000..05bf594e6
--- /dev/null
+++ b/src/com/android/email/mail/store/imap/ImapResponse.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.email.mail.store.imap;
+
+
+/**
+ * Class represents an IMAP response.
+ */
+public class ImapResponse extends ImapList {
+ private final String mTag;
+ private final boolean mIsContinuationRequest;
+
+ /* package */ ImapResponse(String tag, boolean isContinuationRequest) {
+ mTag = tag;
+ mIsContinuationRequest = isContinuationRequest;
+ }
+
+ /* package */ static boolean isStatusResponse(String symbol) {
+ return ImapConstants.OK.equalsIgnoreCase(symbol)
+ || ImapConstants.NO.equalsIgnoreCase(symbol)
+ || ImapConstants.BAD.equalsIgnoreCase(symbol)
+ || ImapConstants.PREAUTH.equalsIgnoreCase(symbol)
+ || ImapConstants.BYE.equalsIgnoreCase(symbol);
+ }
+
+ /**
+ * @return whether it's a tagged response.
+ */
+ public boolean isTagged() {
+ return mTag != null;
+ }
+
+ /**
+ * @return whether it's a continuation request.
+ */
+ public boolean isContinuationRequest() {
+ return mIsContinuationRequest;
+ }
+
+ public boolean isStatusResponse() {
+ return isStatusResponse(getStringOrEmpty(0).getString());
+ }
+
+ /**
+ * @return whether it's an OK response.
+ */
+ public boolean isOk() {
+ return is(0, ImapConstants.OK);
+ }
+
+ /**
+ * @return whether it's an BAD response.
+ */
+ public boolean isBad() {
+ return is(0, ImapConstants.BAD);
+ }
+
+ /**
+ * @return whether it's an NO response.
+ */
+ public boolean isNo() {
+ return is(0, ImapConstants.NO);
+ }
+
+ /**
+ * @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"
+ */
+ public final boolean isDataResponse(int index, String responseType) {
+ return !isTagged() && getStringOrEmpty(index).is(responseType);
+ }
+
+ /**
+ * @return Response code (RFC 3501 7.1) if it's a status response.
+ *
+ * e.g. "ALERT" for "* OK [ALERT] System shutdown in 10 minutes"
+ */
+ public ImapString getResponseCodeOrEmpty() {
+ if (!isStatusResponse()) {
+ return ImapString.EMPTY; // Not a status response.
+ }
+ return getListOrEmpty(1).getStringOrEmpty(0);
+ }
+
+ /**
+ * @return Alert message it it has ALERT response code.
+ *
+ * e.g. "System shutdown in 10 minutes" for "* OK [ALERT] System shutdown in 10 minutes"
+ */
+ public ImapString getAlertTextOrEmpty() {
+ if (!getResponseCodeOrEmpty().is(ImapConstants.ALERT)) {
+ return ImapString.EMPTY; // Not an ALERT
+ }
+ // The 3rd element contains all the rest of line.
+ return getStringOrEmpty(2);
+ }
+
+ /**
+ * @return Response text in a status response.
+ */
+ public ImapString getStatusResponseTextOrEmpty() {
+ if (!isStatusResponse()) {
+ return ImapString.EMPTY;
+ }
+ return getStringOrEmpty(getElementOrNone(1).isList() ? 2 : 1);
+ }
+
+ @Override
+ public String toString() {
+ String tag = mTag;
+ if (isContinuationRequest()) {
+ tag = "+";
+ }
+ return "#" + tag + "# " + super.toString();
+ }
+
+ @Override
+ public boolean equalsForTest(ImapElement that) {
+ if (!super.equalsForTest(that)) {
+ return false;
+ }
+ final ImapResponse thatResponse = (ImapResponse) that;
+ if (mTag == null) {
+ if (thatResponse.mTag != null) {
+ return false;
+ }
+ } else {
+ if (!mTag.equals(thatResponse.mTag)) {
+ return false;
+ }
+ }
+ if (mIsContinuationRequest != thatResponse.mIsContinuationRequest) {
+ return false;
+ }
+ return true;
+ }
+}
diff --git a/src/com/android/email/mail/store/imap/ImapResponseParser.java b/src/com/android/email/mail/store/imap/ImapResponseParser.java
new file mode 100644
index 000000000..078cf9f76
--- /dev/null
+++ b/src/com/android/email/mail/store/imap/ImapResponseParser.java
@@ -0,0 +1,450 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.email.mail.store.imap;
+
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.email.FixedLengthInputStream;
+import com.android.email.PeekableInputStream;
+import com.android.email.mail.transport.DiscourseLogger;
+import com.android.email2.ui.MailActivityEmail;
+import com.android.emailcommon.Logging;
+import com.android.emailcommon.mail.MessagingException;
+import com.android.emailcommon.utility.LoggingInputStream;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+
+/**
+ * IMAP response parser.
+ */
+public class ImapResponseParser {
+ private static final boolean DEBUG_LOG_RAW_STREAM = false; // DO NOT RELEASE AS 'TRUE'
+
+ /**
+ * Literal larger than this will be stored in temp file.
+ */
+ public static final int LITERAL_KEEP_IN_MEMORY_THRESHOLD = 2 * 1024 * 1024;
+
+ /** Input stream */
+ private final PeekableInputStream mIn;
+
+ /**
+ * To log network activities when the parser crashes.
+ *
+ * We log all bytes received from the server, except for the part sent as literals.
+ */
+ private final DiscourseLogger mDiscourseLogger;
+
+ private final int mLiteralKeepInMemoryThreshold;
+
+ /** StringBuilder used by readUntil() */
+ private final StringBuilder mBufferReadUntil = new StringBuilder();
+
+ /** StringBuilder used by parseBareString() */
+ private final StringBuilder mParseBareString = new StringBuilder();
+
+ /**
+ * We store all {@link ImapResponse} in it. {@link #destroyResponses()} must be called from
+ * time to time to destroy them and clear it.
+ */
+ private final ArrayList mResponsesToDestroy = new ArrayList();
+
+ /**
+ * Exception thrown when we receive BYE. It derives from IOException, so it'll be treated
+ * in the same way EOF does.
+ */
+ public static class ByeException extends IOException {
+ public static final String MESSAGE = "Received BYE";
+ public ByeException() {
+ super(MESSAGE);
+ }
+ }
+
+ /**
+ * Public constructor for normal use.
+ */
+ public ImapResponseParser(InputStream in, DiscourseLogger discourseLogger) {
+ this(in, discourseLogger, LITERAL_KEEP_IN_MEMORY_THRESHOLD);
+ }
+
+ /**
+ * Constructor for testing to override the literal size threshold.
+ */
+ /* package for test */ ImapResponseParser(InputStream in, DiscourseLogger discourseLogger,
+ int literalKeepInMemoryThreshold) {
+ if (DEBUG_LOG_RAW_STREAM && MailActivityEmail.DEBUG) {
+ in = new LoggingInputStream(in);
+ }
+ mIn = new PeekableInputStream(in);
+ mDiscourseLogger = discourseLogger;
+ mLiteralKeepInMemoryThreshold = literalKeepInMemoryThreshold;
+ }
+
+ private static IOException newEOSException() {
+ final String message = "End of stream reached";
+ if (MailActivityEmail.DEBUG) {
+ Log.d(Logging.LOG_TAG, message);
+ }
+ return new IOException(message);
+ }
+
+ /**
+ * Peek next one byte.
+ *
+ * Throws IOException() if reaches EOF. As long as logical response lines end with \r\n,
+ * we shouldn't see EOF during parsing.
+ */
+ private int peek() throws IOException {
+ final int next = mIn.peek();
+ if (next == -1) {
+ throw newEOSException();
+ }
+ return next;
+ }
+
+ /**
+ * Read and return one byte from {@link #mIn}, and put it in {@link #mDiscourseLogger}.
+ *
+ * Throws IOException() if reaches EOF. As long as logical response lines end with \r\n,
+ * we shouldn't see EOF during parsing.
+ */
+ private int readByte() throws IOException {
+ int next = mIn.read();
+ if (next == -1) {
+ throw newEOSException();
+ }
+ mDiscourseLogger.addReceivedByte(next);
+ return next;
+ }
+
+ /**
+ * Destroy all the {@link ImapResponse}s stored in the internal storage and clear it.
+ *
+ * @see #readResponse()
+ */
+ public void destroyResponses() {
+ for (ImapResponse r : mResponsesToDestroy) {
+ r.destroy();
+ }
+ mResponsesToDestroy.clear();
+ }
+
+ /**
+ * Reads the next response available on the stream and returns an
+ * {@link ImapResponse} object that represents it.
+ *
+ * When this method successfully returns an {@link ImapResponse}, the {@link ImapResponse}
+ * is stored in the internal storage. When the {@link ImapResponse} is no longer used
+ * {@link #destroyResponses} should be called to destroy all the responses in the array.
+ *
+ * @return the parsed {@link ImapResponse} object.
+ * @exception ByeException when detects BYE.
+ */
+ public ImapResponse readResponse() throws IOException, MessagingException {
+ ImapResponse response = null;
+ try {
+ response = parseResponse();
+ if (MailActivityEmail.DEBUG) {
+ Log.d(Logging.LOG_TAG, "<<< " + response.toString());
+ }
+
+ } catch (RuntimeException e) {
+ // Parser crash -- log network activities.
+ onParseError(e);
+ throw e;
+ } catch (IOException e) {
+ // Network error, or received an unexpected char.
+ onParseError(e);
+ throw e;
+ }
+
+ // Handle this outside of try-catch. We don't have to dump protocol log when getting BYE.
+ if (response.is(0, ImapConstants.BYE)) {
+ Log.w(Logging.LOG_TAG, ByeException.MESSAGE);
+ response.destroy();
+ throw new ByeException();
+ }
+ mResponsesToDestroy.add(response);
+ return response;
+ }
+
+ private void onParseError(Exception e) {
+ // Read a few more bytes, so that the log will contain some more context, even if the parser
+ // crashes in the middle of a response.
+ // This also makes sure the byte in question will be logged, no matter where it crashes.
+ // e.g. when parseAtom() peeks and finds at an unexpected char, it throws an exception
+ // before actually reading it.
+ // However, we don't want to read too much, because then it may get into an email message.
+ try {
+ for (int i = 0; i < 4; i++) {
+ int b = readByte();
+ if (b == -1 || b == '\n') {
+ break;
+ }
+ }
+ } catch (IOException ignore) {
+ }
+ Log.w(Logging.LOG_TAG, "Exception detected: " + e.getMessage());
+ mDiscourseLogger.logLastDiscourse();
+ }
+
+ /**
+ * Read next byte from stream and throw it away. If the byte is different from {@code expected}
+ * throw {@link MessagingException}.
+ */
+ /* package for test */ void expect(char expected) throws IOException {
+ final int next = readByte();
+ if (expected != next) {
+ throw new IOException(String.format("Expected %04x (%c) but got %04x (%c)",
+ (int) expected, expected, next, (char) next));
+ }
+ }
+
+ /**
+ * Read bytes until we find {@code end}, and return all as string.
+ * The {@code end} will be read (rather than peeked) and won't be included in the result.
+ */
+ /* package for test */ String readUntil(char end) throws IOException {
+ mBufferReadUntil.setLength(0);
+ for (;;) {
+ final int ch = readByte();
+ if (ch != end) {
+ mBufferReadUntil.append((char) ch);
+ } else {
+ return mBufferReadUntil.toString();
+ }
+ }
+ }
+
+ /**
+ * Read all bytes until \r\n.
+ */
+ /* package */ String readUntilEol() throws IOException {
+ String ret = readUntil('\r');
+ expect('\n'); // TODO Should this really be error?
+ return ret;
+ }
+
+ /**
+ * Parse and return the response line.
+ */
+ private ImapResponse parseResponse() throws IOException, MessagingException {
+ // We need to destroy the response if we get an exception.
+ // So, we first store the response that's being built in responseToDestroy, until it's
+ // completely built, at which point we copy it into responseToReturn and null out
+ // responseToDestroyt.
+ // If responseToDestroy is not null in finally, we destroy it because that means
+ // we got an exception somewhere.
+ ImapResponse responseToDestroy = null;
+ final ImapResponse responseToReturn;
+
+ try {
+ final int ch = peek();
+ if (ch == '+') { // Continuation request
+ readByte(); // skip +
+ expect(' ');
+ responseToDestroy = new ImapResponse(null, true);
+
+ // If it's continuation request, we don't really care what's in it.
+ responseToDestroy.add(new ImapSimpleString(readUntilEol()));
+
+ // Response has successfully been built. Let's return it.
+ responseToReturn = responseToDestroy;
+ responseToDestroy = null;
+ } else {
+ // Status response or response data
+ final String tag;
+ if (ch == '*') {
+ tag = null;
+ readByte(); // skip *
+ expect(' ');
+ } else {
+ tag = readUntil(' ');
+ }
+ responseToDestroy = new ImapResponse(tag, false);
+
+ final ImapString firstString = parseBareString();
+ responseToDestroy.add(firstString);
+
+ // parseBareString won't eat a space after the string, so we need to skip it,
+ // if exists.
+ // If the next char is not ' ', it should be EOL.
+ if (peek() == ' ') {
+ readByte(); // skip ' '
+
+ if (responseToDestroy.isStatusResponse()) { // It's a status response
+
+ // Is there a response code?
+ final int next = peek();
+ if (next == '[') {
+ responseToDestroy.add(parseList('[', ']'));
+ if (peek() == ' ') { // Skip following space
+ readByte();
+ }
+ }
+
+ String rest = readUntilEol();
+ if (!TextUtils.isEmpty(rest)) {
+ // The rest is free-form text.
+ responseToDestroy.add(new ImapSimpleString(rest));
+ }
+ } else { // It's a response data.
+ parseElements(responseToDestroy, '\0');
+ }
+ } else {
+ expect('\r');
+ expect('\n');
+ }
+
+ // Response has successfully been built. Let's return it.
+ responseToReturn = responseToDestroy;
+ responseToDestroy = null;
+ }
+ } finally {
+ if (responseToDestroy != null) {
+ // We get an exception.
+ responseToDestroy.destroy();
+ }
+ }
+
+ return responseToReturn;
+ }
+
+ private ImapElement parseElement() throws IOException, MessagingException {
+ final int next = peek();
+ switch (next) {
+ case '(':
+ return parseList('(', ')');
+ case '[':
+ return parseList('[', ']');
+ case '"':
+ readByte(); // Skip "
+ return new ImapSimpleString(readUntil('"'));
+ case '{':
+ return parseLiteral();
+ case '\r': // CR
+ readByte(); // Consume \r
+ expect('\n'); // Should be followed by LF.
+ return null;
+ case '\n': // LF // There shouldn't be a bare LF, but just in case.
+ readByte(); // Consume \n
+ return null;
+ default:
+ return parseBareString();
+ }
+ }
+
+ /**
+ * Parses an atom.
+ *
+ * Special case: If an atom contains '[', everything until the next ']' will be considered
+ * a part of the atom.
+ * (e.g. "BODY[HEADER.FIELDS ("DATE" ...)]" will become a single ImapString)
+ *
+ * If the value is "NIL", returns an empty string.
+ */
+ private ImapString parseBareString() throws IOException, MessagingException {
+ mParseBareString.setLength(0);
+ for (;;) {
+ final int ch = peek();
+
+ // TODO Can we clean this up? (This condition is from the old parser.)
+ if (ch == '(' || ch == ')' || ch == '{' || ch == ' ' ||
+ // ']' is not part of atom (it's in resp-specials)
+ ch == ']' ||
+ // docs claim that flags are \ atom but atom isn't supposed to
+ // contain
+ // * and some flags contain *
+ // ch == '%' || ch == '*' ||
+ ch == '%' ||
+ // TODO probably should not allow \ and should recognize
+ // it as a flag instead
+ // ch == '"' || ch == '\' ||
+ ch == '"' || (0x00 <= ch && ch <= 0x1f) || ch == 0x7f) {
+ if (mParseBareString.length() == 0) {
+ throw new MessagingException("Expected string, none found.");
+ }
+ String s = mParseBareString.toString();
+
+ // NIL will be always converted into the empty string.
+ if (ImapConstants.NIL.equalsIgnoreCase(s)) {
+ return ImapString.EMPTY;
+ }
+ return new ImapSimpleString(s);
+ } else if (ch == '[') {
+ // Eat all until next ']'
+ mParseBareString.append((char) readByte());
+ mParseBareString.append(readUntil(']'));
+ mParseBareString.append(']'); // readUntil won't include the end char.
+ } else {
+ mParseBareString.append((char) readByte());
+ }
+ }
+ }
+
+ private void parseElements(ImapList list, char end)
+ throws IOException, MessagingException {
+ for (;;) {
+ for (;;) {
+ final int next = peek();
+ if (next == end) {
+ return;
+ }
+ if (next != ' ') {
+ break;
+ }
+ // Skip space
+ readByte();
+ }
+ final ImapElement el = parseElement();
+ if (el == null) { // EOL
+ return;
+ }
+ list.add(el);
+ }
+ }
+
+ private ImapList parseList(char opening, char closing)
+ throws IOException, MessagingException {
+ expect(opening);
+ final ImapList list = new ImapList();
+ parseElements(list, closing);
+ expect(closing);
+ return list;
+ }
+
+ private ImapString parseLiteral() throws IOException, MessagingException {
+ expect('{');
+ final int size;
+ try {
+ size = Integer.parseInt(readUntil('}'));
+ } catch (NumberFormatException nfe) {
+ throw new MessagingException("Invalid length in literal");
+ }
+ expect('\r');
+ expect('\n');
+ FixedLengthInputStream in = new FixedLengthInputStream(mIn, size);
+ if (size > mLiteralKeepInMemoryThreshold) {
+ return new ImapTempFileLiteral(in);
+ } else {
+ return new ImapMemoryLiteral(in);
+ }
+ }
+}
diff --git a/src/com/android/email/mail/store/imap/ImapSimpleString.java b/src/com/android/email/mail/store/imap/ImapSimpleString.java
new file mode 100644
index 000000000..190c5237f
--- /dev/null
+++ b/src/com/android/email/mail/store/imap/ImapSimpleString.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.email.mail.store.imap;
+
+import com.android.emailcommon.utility.Utility;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+
+/**
+ * Subclass of {@link ImapString} used for non literals.
+ */
+public class ImapSimpleString extends ImapString {
+ private String mString;
+
+ /* package */ ImapSimpleString(String string) {
+ mString = (string != null) ? string : "";
+ }
+
+ @Override
+ public void destroy() {
+ mString = null;
+ super.destroy();
+ }
+
+ @Override
+ public String getString() {
+ return mString;
+ }
+
+ @Override
+ public InputStream getAsStream() {
+ return new ByteArrayInputStream(Utility.toAscii(mString));
+ }
+
+ @Override
+ public String toString() {
+ // Purposefully not return just mString, in order to prevent using it instead of getString.
+ return "\"" + mString + "\"";
+ }
+}
diff --git a/src/com/android/email/mail/store/imap/ImapString.java b/src/com/android/email/mail/store/imap/ImapString.java
new file mode 100644
index 000000000..b0ee99d84
--- /dev/null
+++ b/src/com/android/email/mail/store/imap/ImapString.java
@@ -0,0 +1,187 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.email.mail.store.imap;
+
+import com.android.emailcommon.Logging;
+
+import android.util.Log;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+
+/**
+ * Class represents an IMAP "element" that is not a list.
+ *
+ * An atom, quoted string, literal, are all represented by this. Values like OK, STATUS are too.
+ * Also, this class class may contain more arbitrary value like "BODY[HEADER.FIELDS ("DATE")]".
+ * See {@link ImapResponseParser}.
+ */
+public abstract class ImapString extends ImapElement {
+ private static final byte[] EMPTY_BYTES = new byte[0];
+
+ public static final ImapString EMPTY = new ImapString() {
+ @Override public void destroy() {
+ // Don't call super.destroy().
+ // It's a shared object. We don't want the mDestroyed to be set on this.
+ }
+
+ @Override public String getString() {
+ return "";
+ }
+
+ @Override public InputStream getAsStream() {
+ return new ByteArrayInputStream(EMPTY_BYTES);
+ }
+
+ @Override public String toString() {
+ return "";
+ }
+ };
+
+ // This is used only for parsing IMAP's FETCH ENVELOPE command, in which
+ // en_US-like date format is used like "01-Jan-2009 11:20:39 -0800", so this should be
+ // handled by Locale.US
+ private final static SimpleDateFormat DATE_TIME_FORMAT =
+ new SimpleDateFormat("dd-MMM-yyyy HH:mm:ss Z", Locale.US);
+
+ private boolean mIsInteger;
+ private int mParsedInteger;
+ private Date mParsedDate;
+
+ @Override
+ public final boolean isList() {
+ return false;
+ }
+
+ @Override
+ public final boolean isString() {
+ return true;
+ }
+
+ /**
+ * @return true if and only if the length of the string is larger than 0.
+ *
+ * Note: IMAP NIL is considered an empty string. See {@link ImapResponseParser
+ * #parseBareString}.
+ * On the other hand, a quoted/literal string with value NIL (i.e. "NIL" and {3}\r\nNIL) is
+ * treated literally.
+ */
+ public final boolean isEmpty() {
+ return getString().length() == 0;
+ }
+
+ public abstract String getString();
+
+ public abstract InputStream getAsStream();
+
+ /**
+ * @return whether it can be parsed as a number.
+ */
+ public final boolean isNumber() {
+ if (mIsInteger) {
+ return true;
+ }
+ try {
+ mParsedInteger = Integer.parseInt(getString());
+ mIsInteger = true;
+ return true;
+ } catch (NumberFormatException e) {
+ return false;
+ }
+ }
+
+ /**
+ * @return value parsed as a number.
+ */
+ public final int getNumberOrZero() {
+ if (!isNumber()) {
+ return 0;
+ }
+ return mParsedInteger;
+ }
+
+ /**
+ * @return whether it can be parsed as a date using {@link #DATE_TIME_FORMAT}.
+ */
+ public final boolean isDate() {
+ if (mParsedDate != null) {
+ return true;
+ }
+ if (isEmpty()) {
+ return false;
+ }
+ try {
+ mParsedDate = DATE_TIME_FORMAT.parse(getString());
+ return true;
+ } catch (ParseException e) {
+ Log.w(Logging.LOG_TAG, getString() + " can't be parsed as a date.");
+ return false;
+ }
+ }
+
+ /**
+ * @return value it can be parsed as a {@link Date}, or null otherwise.
+ */
+ public final Date getDateOrNull() {
+ if (!isDate()) {
+ return null;
+ }
+ return mParsedDate;
+ }
+
+ /**
+ * @return whether the value case-insensitively equals to {@code s}.
+ */
+ public final boolean is(String s) {
+ if (s == null) {
+ return false;
+ }
+ return getString().equalsIgnoreCase(s);
+ }
+
+
+ /**
+ * @return whether the value case-insensitively starts with {@code s}.
+ */
+ public final boolean startsWith(String prefix) {
+ if (prefix == null) {
+ return false;
+ }
+ final String me = this.getString();
+ if (me.length() < prefix.length()) {
+ return false;
+ }
+ return me.substring(0, prefix.length()).equalsIgnoreCase(prefix);
+ }
+
+ // To force subclasses to implement it.
+ @Override
+ public abstract String toString();
+
+ @Override
+ public final boolean equalsForTest(ImapElement that) {
+ if (!super.equalsForTest(that)) {
+ return false;
+ }
+ ImapString thatString = (ImapString) that;
+ return getString().equals(thatString.getString());
+ }
+}
diff --git a/src/com/android/email/mail/store/imap/ImapTempFileLiteral.java b/src/com/android/email/mail/store/imap/ImapTempFileLiteral.java
new file mode 100644
index 000000000..eda1b568e
--- /dev/null
+++ b/src/com/android/email/mail/store/imap/ImapTempFileLiteral.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.email.mail.store.imap;
+
+import android.util.Log;
+
+import com.android.email.FixedLengthInputStream;
+import com.android.emailcommon.Logging;
+import com.android.emailcommon.TempDirectory;
+import com.android.emailcommon.utility.Utility;
+
+import org.apache.commons.io.IOUtils;
+
+import java.io.ByteArrayInputStream;
+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;
+
+/**
+ * Subclass of {@link ImapString} used for literals backed by a temp file.
+ */
+public class ImapTempFileLiteral extends ImapString {
+ /* package for test */ final File mFile;
+
+ /** Size is purely for toString() */
+ private final int mSize;
+
+ /* package */ ImapTempFileLiteral(FixedLengthInputStream stream) throws IOException {
+ mSize = stream.getLength();
+ mFile = File.createTempFile("imap", ".tmp", TempDirectory.getTempDirectory());
+
+ // Unfortunately, we can't really use deleteOnExit(), because temp filenames are random
+ // so it'd simply cause a memory leak.
+ // deleteOnExit() simply adds filenames to a static list and the list will never shrink.
+ // mFile.deleteOnExit();
+ OutputStream out = new FileOutputStream(mFile);
+ IOUtils.copy(stream, out);
+ out.close();
+ }
+
+ /**
+ * Make sure we delete the temp file.
+ *
+ * We should always be calling {@link ImapResponse#destroy()}, but it's here as a last resort.
+ */
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ destroy();
+ } finally {
+ super.finalize();
+ }
+ }
+
+ @Override
+ public InputStream getAsStream() {
+ checkNotDestroyed();
+ try {
+ return new FileInputStream(mFile);
+ } catch (FileNotFoundException e) {
+ // It's probably possible if we're low on storage and the system clears the cache dir.
+ Log.w(Logging.LOG_TAG, "ImapTempFileLiteral: Temp file not found");
+
+ // Return 0 byte stream as a dummy...
+ return new ByteArrayInputStream(new byte[0]);
+ }
+ }
+
+ @Override
+ public String getString() {
+ checkNotDestroyed();
+ try {
+ byte[] bytes = IOUtils.toByteArray(getAsStream());
+ // Prevent crash from OOM; we've seen this, but only rarely and not reproducibly
+ if (bytes.length > ImapResponseParser.LITERAL_KEEP_IN_MEMORY_THRESHOLD) {
+ throw new IOException();
+ }
+ return Utility.fromAscii(bytes);
+ } catch (IOException e) {
+ Log.w(Logging.LOG_TAG, "ImapTempFileLiteral: Error while reading temp file", e);
+ return "";
+ }
+ }
+
+ @Override
+ public void destroy() {
+ try {
+ if (!isDestroyed() && mFile.exists()) {
+ mFile.delete();
+ }
+ } catch (RuntimeException re) {
+ // Just log and ignore.
+ Log.w(Logging.LOG_TAG, "Failed to remove temp file: " + re.getMessage());
+ }
+ super.destroy();
+ }
+
+ @Override
+ public String toString() {
+ return String.format("{%d byte literal(file)}", mSize);
+ }
+
+ public boolean tempFileExistsForTest() {
+ return mFile.exists();
+ }
+}
diff --git a/src/com/android/email/mail/store/imap/ImapUtility.java b/src/com/android/email/mail/store/imap/ImapUtility.java
new file mode 100644
index 000000000..dc7e98e96
--- /dev/null
+++ b/src/com/android/email/mail/store/imap/ImapUtility.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.email.mail.store.imap;
+
+import com.android.emailcommon.Logging;
+
+import android.util.Log;
+
+import java.util.ArrayList;
+
+/**
+ * Utility methods for use with IMAP.
+ */
+public class ImapUtility {
+ /**
+ * Apply quoting rules per IMAP RFC,
+ * quoted = DQUOTE *QUOTED-CHAR DQUOTE
+ * QUOTED-CHAR = / "\" quoted-specials
+ * quoted-specials = DQUOTE / "\"
+ *
+ * This is used primarily for IMAP login, but might be useful elsewhere.
+ *
+ * NOTE: Not very efficient - you may wish to preflight this, or perhaps it should check
+ * for trouble chars before calling the replace functions.
+ *
+ * @param s The string to be quoted.
+ * @return A copy of the string, having undergone quoting as described above
+ */
+ public static String imapQuoted(String s) {
+
+ // First, quote any backslashes by replacing \ with \\
+ // regex Pattern: \\ (Java string const = \\\\)
+ // Substitute: \\\\ (Java string const = \\\\\\\\)
+ String result = s.replaceAll("\\\\", "\\\\\\\\");
+
+ // Then, quote any double-quotes by replacing " with \"
+ // regex Pattern: " (Java string const = \")
+ // Substitute: \\" (Java string const = \\\\\")
+ result = result.replaceAll("\"", "\\\\\"");
+
+ // return string with quotes around it
+ return "\"" + result + "\"";
+ }
+
+ /**
+ * Gets all of the values in a sequence set per RFC 3501. Any ranges are expanded into a
+ * list of individual numbers. If the set is invalid, an empty array is returned.
+ *
+ * sequence-number = nz-number / "*"
+ * sequence-range = sequence-number ":" sequence-number
+ * sequence-set = (sequence-number / sequence-range) *("," sequence-set)
+ *
+ */
+ public static String[] getImapSequenceValues(String set) {
+ ArrayList list = new ArrayList();
+ if (set != null) {
+ String[] setItems = set.split(",");
+ for (String item : setItems) {
+ if (item.indexOf(':') == -1) {
+ // simple item
+ try {
+ Integer.parseInt(item); // Don't need the value; just ensure it's valid
+ list.add(item);
+ } catch (NumberFormatException e) {
+ Log.d(Logging.LOG_TAG, "Invalid UID value", e);
+ }
+ } else {
+ // range
+ for (String rangeItem : getImapRangeValues(item)) {
+ list.add(rangeItem);
+ }
+ }
+ }
+ }
+ String[] stringList = new String[list.size()];
+ return list.toArray(stringList);
+ }
+
+ /**
+ * Expand the given number range into a list of individual numbers. If the range is not valid,
+ * an empty array is returned.
+ *
+ * sequence-number = nz-number / "*"
+ * sequence-range = sequence-number ":" sequence-number
+ * sequence-set = (sequence-number / sequence-range) *("," sequence-set)
+ *
+ */
+ public static String[] getImapRangeValues(String range) {
+ ArrayList list = new ArrayList();
+ try {
+ if (range != null) {
+ int colonPos = range.indexOf(':');
+ if (colonPos > 0) {
+ int first = Integer.parseInt(range.substring(0, colonPos));
+ int second = Integer.parseInt(range.substring(colonPos + 1));
+ if (first < second) {
+ for (int i = first; i <= second; i++) {
+ list.add(Integer.toString(i));
+ }
+ } else {
+ for (int i = first; i >= second; i--) {
+ list.add(Integer.toString(i));
+ }
+ }
+ }
+ }
+ } catch (NumberFormatException e) {
+ Log.d(Logging.LOG_TAG, "Invalid range value", e);
+ }
+ String[] stringList = new String[list.size()];
+ return list.toArray(stringList);
+ }
+}
diff --git a/src/com/android/email/service/EmailBroadcastProcessorService.java b/src/com/android/email/service/EmailBroadcastProcessorService.java
index abf128ec8..442feac57 100644
--- a/src/com/android/email/service/EmailBroadcastProcessorService.java
+++ b/src/com/android/email/service/EmailBroadcastProcessorService.java
@@ -31,17 +31,15 @@ import android.util.Log;
import com.android.email.NotificationController;
import com.android.email.Preferences;
+import com.android.email.R;
import com.android.email.SecurityPolicy;
import com.android.email.activity.setup.AccountSettings;
-import com.android.email.service.EmailServiceUtils.EmailServiceInfo;
import com.android.emailcommon.Logging;
import com.android.emailcommon.VendorPolicyLoader;
import com.android.emailcommon.provider.Account;
import com.android.emailcommon.provider.EmailContent.AccountColumns;
import com.android.emailcommon.provider.HostAuth;
-import java.util.List;
-
/**
* The service that really handles broadcast intents on a worker thread.
*
@@ -185,7 +183,8 @@ public class EmailBroadcastProcessorService extends IntentService {
while (c.moveToNext()) {
long recvAuthKey = c.getLong(Account.CONTENT_HOST_AUTH_KEY_RECV_COLUMN);
HostAuth recvAuth = HostAuth.restoreHostAuthWithId(context, recvAuthKey);
- if (HostAuth.LEGACY_SCHEME_IMAP.equals(recvAuth.mProtocol)) {
+ String legacyImapProtocol = context.getString(R.string.protocol_legacy_imap);
+ if (legacyImapProtocol.equals(recvAuth.mProtocol)) {
int flags = c.getInt(Account.CONTENT_FLAGS_COLUMN);
flags &= ~Account.FLAGS_DELETE_POLICY_MASK;
flags |= Account.DELETE_POLICY_ON_DELETE << Account.FLAGS_DELETE_POLICY_SHIFT;
diff --git a/src/com/android/email/service/EmailServiceUtils.java b/src/com/android/email/service/EmailServiceUtils.java
index 49c269786..1a071ce71 100644
--- a/src/com/android/email/service/EmailServiceUtils.java
+++ b/src/com/android/email/service/EmailServiceUtils.java
@@ -36,7 +36,6 @@ import android.database.Cursor;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
-import android.os.Debug;
import android.os.IBinder;
import android.os.RemoteException;
import android.provider.CalendarContract;
@@ -178,7 +177,9 @@ public class EmailServiceUtils {
public boolean requiresAccountUpdate;
public boolean offerLoadMore;
public boolean requiresSetup;
+ public boolean hide;
+ @Override
public String toString() {
StringBuilder sb = new StringBuilder("Protocol: ");
sb.append(protocol);
@@ -288,7 +289,6 @@ public class EmailServiceUtils {
protected Void doInBackground(Void... params) {
disableComponent(mContext, LegacyEmailAuthenticatorService.class);
disableComponent(mContext, LegacyEasAuthenticatorService.class);
- disableComponent(mContext, LegacyImap2AuthenticatorService.class);
return null;
}
}
@@ -498,6 +498,7 @@ public class EmailServiceUtils {
continue;
}
info.name = ta.getString(R.styleable.EmailServiceInfo_name);
+ info.hide = ta.getBoolean(R.styleable.EmailServiceInfo_hide, false);
String klass = ta.getString(R.styleable.EmailServiceInfo_serviceClass);
info.intentAction = ta.getString(R.styleable.EmailServiceInfo_intent);
info.defaultSsl = ta.getBoolean(R.styleable.EmailServiceInfo_defaultSsl, false);
diff --git a/src/com/android/email/service/ImapService.java b/src/com/android/email/service/ImapService.java
new file mode 100644
index 000000000..06975419a
--- /dev/null
+++ b/src/com/android/email/service/ImapService.java
@@ -0,0 +1,1296 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.email.service;
+
+import android.app.Service;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.database.Cursor;
+import android.net.TrafficStats;
+import android.net.Uri;
+import android.os.IBinder;
+import android.os.RemoteCallbackList;
+import android.os.RemoteException;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.email.LegacyConversions;
+import com.android.email.NotificationController;
+import com.android.email.mail.Store;
+import com.android.email.provider.Utilities;
+import com.android.email2.ui.MailActivityEmail;
+import com.android.emailcommon.Logging;
+import com.android.emailcommon.TrafficFlags;
+import com.android.emailcommon.internet.MimeUtility;
+import com.android.emailcommon.mail.AuthenticationFailedException;
+import com.android.emailcommon.mail.FetchProfile;
+import com.android.emailcommon.mail.Flag;
+import com.android.emailcommon.mail.Folder;
+import com.android.emailcommon.mail.Folder.FolderType;
+import com.android.emailcommon.mail.Folder.MessageRetrievalListener;
+import com.android.emailcommon.mail.Folder.MessageUpdateCallbacks;
+import com.android.emailcommon.mail.Folder.OpenMode;
+import com.android.emailcommon.mail.Message;
+import com.android.emailcommon.mail.MessagingException;
+import com.android.emailcommon.mail.Part;
+import com.android.emailcommon.provider.Account;
+import com.android.emailcommon.provider.EmailContent;
+import com.android.emailcommon.provider.EmailContent.MailboxColumns;
+import com.android.emailcommon.provider.EmailContent.MessageColumns;
+import com.android.emailcommon.provider.EmailContent.SyncColumns;
+import com.android.emailcommon.provider.Mailbox;
+import com.android.emailcommon.service.EmailServiceCallback;
+import com.android.emailcommon.service.EmailServiceStatus;
+import com.android.emailcommon.service.IEmailServiceCallback;
+import com.android.emailcommon.service.SearchParams;
+import com.android.emailcommon.utility.AttachmentUtilities;
+import com.android.mail.providers.UIProvider.AccountCapabilities;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+
+public class ImapService extends Service {
+ private static final String TAG = "ImapService";
+ private static final int MAX_SMALL_MESSAGE_SIZE = (25 * 1024);
+
+ private static final Flag[] FLAG_LIST_SEEN = new Flag[] { Flag.SEEN };
+ private static final Flag[] FLAG_LIST_FLAGGED = new Flag[] { Flag.FLAGGED };
+ private static final Flag[] FLAG_LIST_ANSWERED = new Flag[] { Flag.ANSWERED };
+
+ /**
+ * Simple cache for last search result mailbox by account and serverId, since the most common
+ * case will be repeated use of the same mailbox
+ */
+ private static long mLastSearchAccountKey = Account.NO_ACCOUNT;
+ private static String mLastSearchServerId = null;
+ private static Mailbox mLastSearchRemoteMailbox = null;
+
+ /**
+ * Cache search results by account; this allows for "load more" support without having to
+ * redo the search (which can be quite slow). SortableMessage is a smallish class, so memory
+ * shouldn't be an issue
+ */
+ private static final HashMap sSearchResults =
+ new HashMap();
+
+ /**
+ * We write this into the serverId field of messages that will never be upsynced.
+ */
+ private static final String LOCAL_SERVERID_PREFIX = "Local-";
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ return Service.START_STICKY;
+ }
+
+ // Callbacks as set up via setCallback
+ private static final RemoteCallbackList mCallbackList =
+ new RemoteCallbackList();
+
+ private static final EmailServiceCallback sCallbackProxy =
+ new EmailServiceCallback(mCallbackList);
+
+ /**
+ * Create our EmailService implementation here.
+ */
+ private final EmailServiceStub mBinder = new EmailServiceStub() {
+
+ @Override
+ public void setCallback(IEmailServiceCallback cb) throws RemoteException {
+ mCallbackList.register(cb);
+ }
+
+ @Override
+ public void loadMore(long messageId) throws RemoteException {
+ // We don't do "loadMore" for IMAP messages; the sync should handle this
+ }
+
+ @Override
+ public int searchMessages(long accountId, SearchParams searchParams, long destMailboxId) {
+ try {
+ return searchMailboxImpl(getApplicationContext(), accountId, searchParams,
+ destMailboxId);
+ } catch (MessagingException e) {
+ }
+ return 0;
+ }
+
+ @Override
+ public int getCapabilities(Account acct) throws RemoteException {
+ return AccountCapabilities.SYNCABLE_FOLDERS |
+ AccountCapabilities.FOLDER_SERVER_SEARCH |
+ AccountCapabilities.UNDO;
+ }
+
+ @Override
+ public void serviceUpdated(String emailAddress) throws RemoteException {
+ // Not needed
+ }
+ };
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ mBinder.init(this, sCallbackProxy);
+ return mBinder;
+ }
+
+ private static void sendMailboxStatus(Mailbox mailbox, int status) {
+ sCallbackProxy.syncMailboxStatus(mailbox.mId, status, 0);
+ }
+
+ /**
+ * Start foreground synchronization of the specified folder. This is called by
+ * synchronizeMailbox or checkMail.
+ * TODO this should use ID's instead of fully-restored objects
+ * @param account
+ * @param folder
+ * @throws MessagingException
+ */
+ public static void synchronizeMailboxSynchronous(Context context, final Account account,
+ final Mailbox folder) throws MessagingException {
+ sendMailboxStatus(folder, EmailServiceStatus.IN_PROGRESS);
+
+ TrafficStats.setThreadStatsTag(TrafficFlags.getSyncFlags(context, account));
+ if ((folder.mFlags & Mailbox.FLAG_HOLDS_MAIL) == 0) {
+ sendMailboxStatus(folder, EmailServiceStatus.SUCCESS);
+ }
+ NotificationController nc = NotificationController.getInstance(context);
+ try {
+ processPendingActionsSynchronous(context, account);
+ synchronizeMailboxGeneric(context, account, folder);
+ // Clear authentication notification for this account
+ nc.cancelLoginFailedNotification(account.mId);
+ sendMailboxStatus(folder, EmailServiceStatus.SUCCESS);
+ } catch (MessagingException e) {
+ if (Logging.LOGD) {
+ Log.v(Logging.LOG_TAG, "synchronizeMailbox", e);
+ }
+ if (e instanceof AuthenticationFailedException) {
+ // Generate authentication notification
+ nc.showLoginFailedNotification(account.mId);
+ }
+ sendMailboxStatus(folder, e.getExceptionType());
+ throw e;
+ }
+ }
+
+ /**
+ * Lightweight record for the first pass of message sync, where I'm just seeing if
+ * the local message requires sync. Later (for messages that need syncing) we'll do a full
+ * readout from the DB.
+ */
+ private static class LocalMessageInfo {
+ private static final int COLUMN_ID = 0;
+ private static final int COLUMN_FLAG_READ = 1;
+ private static final int COLUMN_FLAG_FAVORITE = 2;
+ private static final int COLUMN_FLAG_LOADED = 3;
+ private static final int COLUMN_SERVER_ID = 4;
+ private static final int COLUMN_FLAGS = 7;
+ private static final String[] PROJECTION = new String[] {
+ EmailContent.RECORD_ID,
+ MessageColumns.FLAG_READ, MessageColumns.FLAG_FAVORITE, MessageColumns.FLAG_LOADED,
+ SyncColumns.SERVER_ID, MessageColumns.MAILBOX_KEY, MessageColumns.ACCOUNT_KEY,
+ MessageColumns.FLAGS
+ };
+
+ final long mId;
+ final boolean mFlagRead;
+ final boolean mFlagFavorite;
+ final int mFlagLoaded;
+ final String mServerId;
+ final int mFlags;
+
+ public LocalMessageInfo(Cursor c) {
+ mId = c.getLong(COLUMN_ID);
+ mFlagRead = c.getInt(COLUMN_FLAG_READ) != 0;
+ mFlagFavorite = c.getInt(COLUMN_FLAG_FAVORITE) != 0;
+ mFlagLoaded = c.getInt(COLUMN_FLAG_LOADED);
+ mServerId = c.getString(COLUMN_SERVER_ID);
+ mFlags = c.getInt(COLUMN_FLAGS);
+ // Note: mailbox key and account key not needed - they are projected for the SELECT
+ }
+ }
+
+ /**
+ * Load the structure and body of messages not yet synced
+ * @param account the account we're syncing
+ * @param remoteFolder the (open) Folder we're working on
+ * @param unsyncedMessages an array of Message's we've got headers for
+ * @param toMailbox the destination mailbox we're syncing
+ * @throws MessagingException
+ */
+ static void loadUnsyncedMessages(final Context context, final Account account,
+ Folder remoteFolder, ArrayList messages, final Mailbox toMailbox)
+ throws MessagingException {
+
+ FetchProfile fp = new FetchProfile();
+ fp.add(FetchProfile.Item.STRUCTURE);
+ remoteFolder.fetch(messages.toArray(new Message[messages.size()]), fp, null);
+ for (Message message : messages) {
+ // Build a list of parts we are interested in. Text parts will be downloaded
+ // right now, attachments will be left for later.
+ ArrayList viewables = new ArrayList();
+ ArrayList attachments = new ArrayList();
+ MimeUtility.collectParts(message, viewables, attachments);
+ // Download the viewables immediately
+ for (Part part : viewables) {
+ fp.clear();
+ fp.add(part);
+ remoteFolder.fetch(new Message[] { message }, fp, null);
+ }
+ // Store the updated message locally and mark it fully loaded
+ Utilities.copyOneMessageToProvider(context, message, account, toMailbox,
+ EmailContent.Message.FLAG_LOADED_COMPLETE);
+ }
+ }
+
+ public static void downloadFlagAndEnvelope(final Context context, final Account account,
+ final Mailbox mailbox, Folder remoteFolder, ArrayList unsyncedMessages,
+ HashMap localMessageMap, final ArrayList unseenMessages)
+ throws MessagingException {
+ FetchProfile fp = new FetchProfile();
+ fp.add(FetchProfile.Item.FLAGS);
+ fp.add(FetchProfile.Item.ENVELOPE);
+
+ final HashMap localMapCopy;
+ if (localMessageMap != null)
+ localMapCopy = new HashMap(localMessageMap);
+ else {
+ localMapCopy = new HashMap();
+ }
+
+ remoteFolder.fetch(unsyncedMessages.toArray(new Message[0]), fp,
+ new MessageRetrievalListener() {
+ @Override
+ public void messageRetrieved(Message message) {
+ try {
+ // Determine if the new message was already known (e.g. partial)
+ // And create or reload the full message info
+ LocalMessageInfo localMessageInfo =
+ localMapCopy.get(message.getUid());
+ EmailContent.Message localMessage = null;
+ if (localMessageInfo == null) {
+ localMessage = new EmailContent.Message();
+ } else {
+ localMessage = EmailContent.Message.restoreMessageWithId(
+ context, localMessageInfo.mId);
+ }
+
+ if (localMessage != null) {
+ try {
+ // Copy the fields that are available into the message
+ LegacyConversions.updateMessageFields(localMessage,
+ message, account.mId, mailbox.mId);
+ // Commit the message to the local store
+ Utilities.saveOrUpdate(localMessage, context);
+ // Track the "new" ness of the downloaded message
+ if (!message.isSet(Flag.SEEN) && unseenMessages != null) {
+ unseenMessages.add(localMessage.mId);
+ }
+ } catch (MessagingException me) {
+ Log.e(Logging.LOG_TAG,
+ "Error while copying downloaded message." + me);
+ }
+
+ }
+ }
+ catch (Exception e) {
+ Log.e(Logging.LOG_TAG,
+ "Error while storing downloaded message." + e.toString());
+ }
+ }
+
+ @Override
+ public void loadAttachmentProgress(int progress) {
+ }
+ });
+
+ }
+
+ /**
+ * Synchronizer for IMAP.
+ *
+ * TODO Break this method up into smaller chunks.
+ *
+ * @param account the account to sync
+ * @param mailbox the mailbox to sync
+ * @return results of the sync pass
+ * @throws MessagingException
+ */
+ private static void synchronizeMailboxGeneric(final Context context,
+ final Account account, final Mailbox mailbox) throws MessagingException {
+
+ /*
+ * A list of IDs for messages that were downloaded and did not have the seen flag set.
+ * This serves as the "true" new message count reported to the user via notification.
+ */
+ final ArrayList unseenMessages = new ArrayList();
+
+ ContentResolver resolver = context.getContentResolver();
+
+ // 0. We do not ever sync DRAFTS or OUTBOX (down or up)
+ if (mailbox.mType == Mailbox.TYPE_DRAFTS || mailbox.mType == Mailbox.TYPE_OUTBOX) {
+ return;
+ }
+
+ // 1. Get the message list from the local store and create an index of the uids
+
+ Cursor localUidCursor = null;
+ HashMap localMessageMap = new HashMap();
+
+ try {
+ localUidCursor = resolver.query(
+ EmailContent.Message.CONTENT_URI,
+ LocalMessageInfo.PROJECTION,
+ EmailContent.MessageColumns.ACCOUNT_KEY + "=?" +
+ " AND " + MessageColumns.MAILBOX_KEY + "=?",
+ new String[] {
+ String.valueOf(account.mId),
+ String.valueOf(mailbox.mId)
+ },
+ null);
+ while (localUidCursor.moveToNext()) {
+ LocalMessageInfo info = new LocalMessageInfo(localUidCursor);
+ localMessageMap.put(info.mServerId, info);
+ }
+ } finally {
+ if (localUidCursor != null) {
+ localUidCursor.close();
+ }
+ }
+
+ // 2. Open the remote folder and create the remote folder if necessary
+
+ Store remoteStore = Store.getInstance(account, context);
+ // The account might have been deleted
+ if (remoteStore == null) return;
+ Folder remoteFolder = remoteStore.getFolder(mailbox.mServerId);
+
+ /*
+ * If the folder is a "special" folder we need to see if it exists
+ * on the remote server. It if does not exist we'll try to create it. If we
+ * can't create we'll abort. This will happen on every single Pop3 folder as
+ * designed and on Imap folders during error conditions. This allows us
+ * to treat Pop3 and Imap the same in this code.
+ */
+ if (mailbox.mType == Mailbox.TYPE_TRASH || mailbox.mType == Mailbox.TYPE_SENT
+ || mailbox.mType == Mailbox.TYPE_DRAFTS) {
+ if (!remoteFolder.exists()) {
+ if (!remoteFolder.create(FolderType.HOLDS_MESSAGES)) {
+ return;
+ }
+ }
+ }
+
+ // 3, Open the remote folder. This pre-loads certain metadata like message count.
+ remoteFolder.open(OpenMode.READ_WRITE);
+
+ // 4. Trash any remote messages that are marked as trashed locally.
+ // TODO - this comment was here, but no code was here.
+
+ // 5. Get the remote message count.
+ int remoteMessageCount = remoteFolder.getMessageCount();
+ ContentValues values = new ContentValues();
+ values.put(MailboxColumns.TOTAL_COUNT, remoteMessageCount);
+ mailbox.update(context, values);
+
+ // 6. Determine the limit # of messages to download
+ int visibleLimit = mailbox.mVisibleLimit;
+ if (visibleLimit <= 0) {
+ visibleLimit = MailActivityEmail.VISIBLE_LIMIT_DEFAULT;
+ }
+
+ // 7. Create a list of messages to download
+ Message[] remoteMessages = new Message[0];
+ final ArrayList unsyncedMessages = new ArrayList();
+ HashMap remoteUidMap = new HashMap();
+
+ if (remoteMessageCount > 0) {
+ /*
+ * Message numbers start at 1.
+ */
+ int remoteStart = Math.max(0, remoteMessageCount - visibleLimit) + 1;
+ int remoteEnd = remoteMessageCount;
+ remoteMessages = remoteFolder.getMessages(remoteStart, remoteEnd, null);
+ // TODO Why are we running through the list twice? Combine w/ for loop below
+ for (Message message : remoteMessages) {
+ remoteUidMap.put(message.getUid(), message);
+ }
+
+ /*
+ * Get a list of the messages that are in the remote list but not on the
+ * local store, or messages that are in the local store but failed to download
+ * on the last sync. These are the new messages that we will download.
+ * Note, we also skip syncing messages which are flagged as "deleted message" sentinels,
+ * because they are locally deleted and we don't need or want the old message from
+ * the server.
+ */
+ for (Message message : remoteMessages) {
+ LocalMessageInfo localMessage = localMessageMap.get(message.getUid());
+ // localMessage == null -> message has never been created (not even headers)
+ // mFlagLoaded = UNLOADED -> message created, but none of body loaded
+ // mFlagLoaded = PARTIAL -> message created, a "sane" amt of body has been loaded
+ // mFlagLoaded = COMPLETE -> message body has been completely loaded
+ // mFlagLoaded = DELETED -> message has been deleted
+ // Only the first two of these are "unsynced", so let's retrieve them
+ if (localMessage == null ||
+ (localMessage.mFlagLoaded == EmailContent.Message.FLAG_LOADED_UNLOADED) ||
+ (localMessage.mFlagLoaded == EmailContent.Message.FLAG_LOADED_PARTIAL)) {
+ unsyncedMessages.add(message);
+ }
+ }
+ }
+
+ // 8. Download basic info about the new/unloaded messages (if any)
+ /*
+ * Fetch the flags and envelope only of the new messages. This is intended to get us
+ * critical data as fast as possible, and then we'll fill in the details.
+ */
+ if (unsyncedMessages.size() > 0) {
+ downloadFlagAndEnvelope(context, account, mailbox, remoteFolder, unsyncedMessages,
+ localMessageMap, unseenMessages);
+ }
+
+ // 9. Refresh the flags for any messages in the local store that we didn't just download.
+ FetchProfile fp = new FetchProfile();
+ fp.add(FetchProfile.Item.FLAGS);
+ remoteFolder.fetch(remoteMessages, fp, null);
+ boolean remoteSupportsSeen = false;
+ boolean remoteSupportsFlagged = false;
+ boolean remoteSupportsAnswered = false;
+ for (Flag flag : remoteFolder.getPermanentFlags()) {
+ if (flag == Flag.SEEN) {
+ remoteSupportsSeen = true;
+ }
+ if (flag == Flag.FLAGGED) {
+ remoteSupportsFlagged = true;
+ }
+ if (flag == Flag.ANSWERED) {
+ remoteSupportsAnswered = true;
+ }
+ }
+ // Update SEEN/FLAGGED/ANSWERED (star) flags (if supported remotely - e.g. not for POP3)
+ if (remoteSupportsSeen || remoteSupportsFlagged || remoteSupportsAnswered) {
+ for (Message remoteMessage : remoteMessages) {
+ LocalMessageInfo localMessageInfo = localMessageMap.get(remoteMessage.getUid());
+ if (localMessageInfo == null) {
+ continue;
+ }
+ boolean localSeen = localMessageInfo.mFlagRead;
+ boolean remoteSeen = remoteMessage.isSet(Flag.SEEN);
+ boolean newSeen = (remoteSupportsSeen && (remoteSeen != localSeen));
+ boolean localFlagged = localMessageInfo.mFlagFavorite;
+ boolean remoteFlagged = remoteMessage.isSet(Flag.FLAGGED);
+ boolean newFlagged = (remoteSupportsFlagged && (localFlagged != remoteFlagged));
+ int localFlags = localMessageInfo.mFlags;
+ boolean localAnswered = (localFlags & EmailContent.Message.FLAG_REPLIED_TO) != 0;
+ boolean remoteAnswered = remoteMessage.isSet(Flag.ANSWERED);
+ boolean newAnswered = (remoteSupportsAnswered && (localAnswered != remoteAnswered));
+ if (newSeen || newFlagged || newAnswered) {
+ Uri uri = ContentUris.withAppendedId(
+ EmailContent.Message.CONTENT_URI, localMessageInfo.mId);
+ ContentValues updateValues = new ContentValues();
+ updateValues.put(MessageColumns.FLAG_READ, remoteSeen);
+ updateValues.put(MessageColumns.FLAG_FAVORITE, remoteFlagged);
+ if (remoteAnswered) {
+ localFlags |= EmailContent.Message.FLAG_REPLIED_TO;
+ } else {
+ localFlags &= ~EmailContent.Message.FLAG_REPLIED_TO;
+ }
+ updateValues.put(MessageColumns.FLAGS, localFlags);
+ resolver.update(uri, updateValues, null, null);
+ }
+ }
+ }
+
+ // 10. Remove any messages that are in the local store but no longer on the remote store.
+ HashSet localUidsToDelete = new HashSet(localMessageMap.keySet());
+ localUidsToDelete.removeAll(remoteUidMap.keySet());
+ for (String uidToDelete : localUidsToDelete) {
+ LocalMessageInfo infoToDelete = localMessageMap.get(uidToDelete);
+
+ // Delete associated data (attachment files)
+ // Attachment & Body records are auto-deleted when we delete the Message record
+ AttachmentUtilities.deleteAllAttachmentFiles(context, account.mId,
+ infoToDelete.mId);
+
+ // Delete the message itself
+ Uri uriToDelete = ContentUris.withAppendedId(
+ EmailContent.Message.CONTENT_URI, infoToDelete.mId);
+ resolver.delete(uriToDelete, null, null);
+
+ // Delete extra rows (e.g. synced or deleted)
+ Uri syncRowToDelete = ContentUris.withAppendedId(
+ EmailContent.Message.UPDATED_CONTENT_URI, infoToDelete.mId);
+ resolver.delete(syncRowToDelete, null, null);
+ Uri deletERowToDelete = ContentUris.withAppendedId(
+ EmailContent.Message.UPDATED_CONTENT_URI, infoToDelete.mId);
+ resolver.delete(deletERowToDelete, null, null);
+ }
+
+ loadUnsyncedMessages(context, account, remoteFolder, unsyncedMessages, mailbox);
+
+ // 14. Clean up and report results
+ remoteFolder.close(false);
+ }
+
+ /**
+ * Find messages in the updated table that need to be written back to server.
+ *
+ * Handles:
+ * Read/Unread
+ * Flagged
+ * Append (upload)
+ * Move To Trash
+ * Empty trash
+ * TODO:
+ * Move
+ *
+ * @param account the account to scan for pending actions
+ * @throws MessagingException
+ */
+ private static void processPendingActionsSynchronous(Context context, Account account)
+ throws MessagingException {
+ TrafficStats.setThreadStatsTag(TrafficFlags.getSyncFlags(context, account));
+ String[] accountIdArgs = new String[] { Long.toString(account.mId) };
+
+ // Handle deletes first, it's always better to get rid of things first
+ processPendingDeletesSynchronous(context, account, accountIdArgs);
+
+ // Handle uploads (currently, only to sent messages)
+ processPendingUploadsSynchronous(context, account, accountIdArgs);
+
+ // Now handle updates / upsyncs
+ processPendingUpdatesSynchronous(context, account, accountIdArgs);
+ }
+
+ /**
+ * Get the mailbox corresponding to the remote location of a message; this will normally be
+ * the mailbox whose _id is mailboxKey, except for search results, where we must look it up
+ * by serverId
+ * @param message the message in question
+ * @return the mailbox in which the message resides on the server
+ */
+ private static Mailbox getRemoteMailboxForMessage(Context context,
+ EmailContent.Message message) {
+ // If this is a search result, use the protocolSearchInfo field to get the server info
+ if (!TextUtils.isEmpty(message.mProtocolSearchInfo)) {
+ long accountKey = message.mAccountKey;
+ String protocolSearchInfo = message.mProtocolSearchInfo;
+ if (accountKey == mLastSearchAccountKey &&
+ protocolSearchInfo.equals(mLastSearchServerId)) {
+ return mLastSearchRemoteMailbox;
+ }
+ Cursor c = context.getContentResolver().query(Mailbox.CONTENT_URI,
+ Mailbox.CONTENT_PROJECTION, Mailbox.PATH_AND_ACCOUNT_SELECTION,
+ new String[] {protocolSearchInfo, Long.toString(accountKey)},
+ null);
+ try {
+ if (c.moveToNext()) {
+ Mailbox mailbox = new Mailbox();
+ mailbox.restore(c);
+ mLastSearchAccountKey = accountKey;
+ mLastSearchServerId = protocolSearchInfo;
+ mLastSearchRemoteMailbox = mailbox;
+ return mailbox;
+ } else {
+ return null;
+ }
+ } finally {
+ c.close();
+ }
+ } else {
+ return Mailbox.restoreMailboxWithId(context, message.mMailboxKey);
+ }
+ }
+
+ /**
+ * Scan for messages that are in the Message_Deletes table, look for differences that
+ * we can deal with, and do the work.
+ *
+ * @param account
+ * @param resolver
+ * @param accountIdArgs
+ */
+ private static void processPendingDeletesSynchronous(Context context, Account account,
+ String[] accountIdArgs) {
+ Cursor deletes = context.getContentResolver().query(
+ EmailContent.Message.DELETED_CONTENT_URI,
+ EmailContent.Message.CONTENT_PROJECTION,
+ EmailContent.MessageColumns.ACCOUNT_KEY + "=?", accountIdArgs,
+ EmailContent.MessageColumns.MAILBOX_KEY);
+ long lastMessageId = -1;
+ try {
+ // Defer setting up the store until we know we need to access it
+ Store remoteStore = null;
+ // loop through messages marked as deleted
+ while (deletes.moveToNext()) {
+ boolean deleteFromTrash = false;
+
+ EmailContent.Message oldMessage =
+ EmailContent.getContent(deletes, EmailContent.Message.class);
+
+ if (oldMessage != null) {
+ lastMessageId = oldMessage.mId;
+
+ Mailbox mailbox = getRemoteMailboxForMessage(context, oldMessage);
+ if (mailbox == null) {
+ continue; // Mailbox removed. Move to the next message.
+ }
+ deleteFromTrash = mailbox.mType == Mailbox.TYPE_TRASH;
+
+ // Load the remote store if it will be needed
+ if (remoteStore == null && deleteFromTrash) {
+ remoteStore = Store.getInstance(account, context);
+ }
+
+ // Dispatch here for specific change types
+ if (deleteFromTrash) {
+ // Move message to trash
+ processPendingDeleteFromTrash(context, remoteStore, account, mailbox,
+ oldMessage);
+ }
+ }
+
+ // Finally, delete the update
+ Uri uri = ContentUris.withAppendedId(EmailContent.Message.DELETED_CONTENT_URI,
+ oldMessage.mId);
+ context.getContentResolver().delete(uri, null, null);
+ }
+ } catch (MessagingException me) {
+ // Presumably an error here is an account connection failure, so there is
+ // no point in continuing through the rest of the pending updates.
+ if (MailActivityEmail.DEBUG) {
+ Log.d(Logging.LOG_TAG, "Unable to process pending delete for id="
+ + lastMessageId + ": " + me);
+ }
+ } finally {
+ deletes.close();
+ }
+ }
+
+ /**
+ * Scan for messages that are in Sent, and are in need of upload,
+ * and send them to the server. "In need of upload" is defined as:
+ * serverId == null (no UID has been assigned)
+ * or
+ * message is in the updated list
+ *
+ * Note we also look for messages that are moving from drafts->outbox->sent. They never
+ * go through "drafts" or "outbox" on the server, so we hang onto these until they can be
+ * uploaded directly to the Sent folder.
+ *
+ * @param account
+ * @param resolver
+ * @param accountIdArgs
+ */
+ private static void processPendingUploadsSynchronous(Context context, Account account,
+ String[] accountIdArgs) {
+ ContentResolver resolver = context.getContentResolver();
+ // Find the Sent folder (since that's all we're uploading for now
+ Cursor mailboxes = resolver.query(Mailbox.CONTENT_URI, Mailbox.ID_PROJECTION,
+ MailboxColumns.ACCOUNT_KEY + "=?"
+ + " and " + MailboxColumns.TYPE + "=" + Mailbox.TYPE_SENT,
+ accountIdArgs, null);
+ long lastMessageId = -1;
+ try {
+ // Defer setting up the store until we know we need to access it
+ Store remoteStore = null;
+ while (mailboxes.moveToNext()) {
+ long mailboxId = mailboxes.getLong(Mailbox.ID_PROJECTION_COLUMN);
+ String[] mailboxKeyArgs = new String[] { Long.toString(mailboxId) };
+ // Demand load mailbox
+ Mailbox mailbox = null;
+
+ // First handle the "new" messages (serverId == null)
+ Cursor upsyncs1 = resolver.query(EmailContent.Message.CONTENT_URI,
+ EmailContent.Message.ID_PROJECTION,
+ EmailContent.Message.MAILBOX_KEY + "=?"
+ + " and (" + EmailContent.Message.SERVER_ID + " is null"
+ + " or " + EmailContent.Message.SERVER_ID + "=''" + ")",
+ mailboxKeyArgs,
+ null);
+ try {
+ while (upsyncs1.moveToNext()) {
+ // Load the remote store if it will be needed
+ if (remoteStore == null) {
+ remoteStore = Store.getInstance(account, context);
+ }
+ // Load the mailbox if it will be needed
+ if (mailbox == null) {
+ mailbox = Mailbox.restoreMailboxWithId(context, mailboxId);
+ if (mailbox == null) {
+ continue; // Mailbox removed. Move to the next message.
+ }
+ }
+ // upsync the message
+ long id = upsyncs1.getLong(EmailContent.Message.ID_PROJECTION_COLUMN);
+ lastMessageId = id;
+ processUploadMessage(context, remoteStore, account, mailbox, id);
+ }
+ } finally {
+ if (upsyncs1 != null) {
+ upsyncs1.close();
+ }
+ }
+
+ // Next, handle any updates (e.g. edited in place, although this shouldn't happen)
+ Cursor upsyncs2 = resolver.query(EmailContent.Message.UPDATED_CONTENT_URI,
+ EmailContent.Message.ID_PROJECTION,
+ EmailContent.MessageColumns.MAILBOX_KEY + "=?", mailboxKeyArgs,
+ null);
+ try {
+ while (upsyncs2.moveToNext()) {
+ // Load the remote store if it will be needed
+ if (remoteStore == null) {
+ remoteStore = Store.getInstance(account, context);
+ }
+ // Load the mailbox if it will be needed
+ if (mailbox == null) {
+ mailbox = Mailbox.restoreMailboxWithId(context, mailboxId);
+ if (mailbox == null) {
+ continue; // Mailbox removed. Move to the next message.
+ }
+ }
+ // upsync the message
+ long id = upsyncs2.getLong(EmailContent.Message.ID_PROJECTION_COLUMN);
+ lastMessageId = id;
+ processUploadMessage(context, remoteStore, account, mailbox, id);
+ }
+ } finally {
+ if (upsyncs2 != null) {
+ upsyncs2.close();
+ }
+ }
+ }
+ } catch (MessagingException me) {
+ // Presumably an error here is an account connection failure, so there is
+ // no point in continuing through the rest of the pending updates.
+ if (MailActivityEmail.DEBUG) {
+ Log.d(Logging.LOG_TAG, "Unable to process pending upsync for id="
+ + lastMessageId + ": " + me);
+ }
+ } finally {
+ if (mailboxes != null) {
+ mailboxes.close();
+ }
+ }
+ }
+
+ /**
+ * Scan for messages that are in the Message_Updates table, look for differences that
+ * we can deal with, and do the work.
+ *
+ * @param account
+ * @param resolver
+ * @param accountIdArgs
+ */
+ private static void processPendingUpdatesSynchronous(Context context, Account account,
+ String[] accountIdArgs) {
+ ContentResolver resolver = context.getContentResolver();
+ Cursor updates = resolver.query(EmailContent.Message.UPDATED_CONTENT_URI,
+ EmailContent.Message.CONTENT_PROJECTION,
+ EmailContent.MessageColumns.ACCOUNT_KEY + "=?", accountIdArgs,
+ EmailContent.MessageColumns.MAILBOX_KEY);
+ long lastMessageId = -1;
+ try {
+ // Defer setting up the store until we know we need to access it
+ Store remoteStore = null;
+ // Demand load mailbox (note order-by to reduce thrashing here)
+ Mailbox mailbox = null;
+ // loop through messages marked as needing updates
+ while (updates.moveToNext()) {
+ boolean changeMoveToTrash = false;
+ boolean changeRead = false;
+ boolean changeFlagged = false;
+ boolean changeMailbox = false;
+ boolean changeAnswered = false;
+
+ EmailContent.Message oldMessage =
+ EmailContent.getContent(updates, EmailContent.Message.class);
+ lastMessageId = oldMessage.mId;
+ EmailContent.Message newMessage =
+ EmailContent.Message.restoreMessageWithId(context, oldMessage.mId);
+ if (newMessage != null) {
+ mailbox = Mailbox.restoreMailboxWithId(context, newMessage.mMailboxKey);
+ if (mailbox == null) {
+ continue; // Mailbox removed. Move to the next message.
+ }
+ if (oldMessage.mMailboxKey != newMessage.mMailboxKey) {
+ if (mailbox.mType == Mailbox.TYPE_TRASH) {
+ changeMoveToTrash = true;
+ } else {
+ changeMailbox = true;
+ }
+ }
+ changeRead = oldMessage.mFlagRead != newMessage.mFlagRead;
+ changeFlagged = oldMessage.mFlagFavorite != newMessage.mFlagFavorite;
+ changeAnswered = (oldMessage.mFlags & EmailContent.Message.FLAG_REPLIED_TO) !=
+ (newMessage.mFlags & EmailContent.Message.FLAG_REPLIED_TO);
+ }
+
+ // Load the remote store if it will be needed
+ if (remoteStore == null &&
+ (changeMoveToTrash || changeRead || changeFlagged || changeMailbox ||
+ changeAnswered)) {
+ remoteStore = Store.getInstance(account, context);
+ }
+
+ // Dispatch here for specific change types
+ if (changeMoveToTrash) {
+ // Move message to trash
+ processPendingMoveToTrash(context, remoteStore, account, mailbox, oldMessage,
+ newMessage);
+ } else if (changeRead || changeFlagged || changeMailbox || changeAnswered) {
+ processPendingDataChange(context, remoteStore, mailbox, changeRead,
+ changeFlagged, changeMailbox, changeAnswered, oldMessage, newMessage);
+ }
+
+ // Finally, delete the update
+ Uri uri = ContentUris.withAppendedId(EmailContent.Message.UPDATED_CONTENT_URI,
+ oldMessage.mId);
+ resolver.delete(uri, null, null);
+ }
+
+ } catch (MessagingException me) {
+ // Presumably an error here is an account connection failure, so there is
+ // no point in continuing through the rest of the pending updates.
+ if (MailActivityEmail.DEBUG) {
+ Log.d(Logging.LOG_TAG, "Unable to process pending update for id="
+ + lastMessageId + ": " + me);
+ }
+ } finally {
+ updates.close();
+ }
+ }
+
+ /**
+ * Upsync an entire message. This must also unwind whatever triggered it (either by
+ * updating the serverId, or by deleting the update record, or it's going to keep happening
+ * over and over again.
+ *
+ * Note: If the message is being uploaded into an unexpected mailbox, we *do not* upload.
+ * This is to avoid unnecessary uploads into the trash. Although the caller attempts to select
+ * only the Drafts and Sent folders, this can happen when the update record and the current
+ * record mismatch. In this case, we let the update record remain, because the filters
+ * in processPendingUpdatesSynchronous() will pick it up as a move and handle it (or drop it)
+ * appropriately.
+ *
+ * @param resolver
+ * @param remoteStore
+ * @param account
+ * @param mailbox the actual mailbox
+ * @param messageId
+ */
+ private static void processUploadMessage(Context context, Store remoteStore,
+ Account account, Mailbox mailbox, long messageId)
+ throws MessagingException {
+ EmailContent.Message newMessage =
+ EmailContent.Message.restoreMessageWithId(context, messageId);
+ boolean deleteUpdate = false;
+ if (newMessage == null) {
+ deleteUpdate = true;
+ Log.d(Logging.LOG_TAG, "Upsync failed for null message, id=" + messageId);
+ } else if (mailbox.mType == Mailbox.TYPE_DRAFTS) {
+ deleteUpdate = false;
+ Log.d(Logging.LOG_TAG, "Upsync skipped for mailbox=drafts, id=" + messageId);
+ } else if (mailbox.mType == Mailbox.TYPE_OUTBOX) {
+ deleteUpdate = false;
+ Log.d(Logging.LOG_TAG, "Upsync skipped for mailbox=outbox, id=" + messageId);
+ } else if (mailbox.mType == Mailbox.TYPE_TRASH) {
+ deleteUpdate = false;
+ Log.d(Logging.LOG_TAG, "Upsync skipped for mailbox=trash, id=" + messageId);
+ } else if (newMessage != null && newMessage.mMailboxKey != mailbox.mId) {
+ deleteUpdate = false;
+ Log.d(Logging.LOG_TAG, "Upsync skipped; mailbox changed, id=" + messageId);
+ } else {
+// Log.d(Logging.LOG_TAG, "Upsyc triggered for message id=" + messageId);
+// deleteUpdate = processPendingAppend(context, remoteStore, account, mailbox,
+ //newMessage);
+ }
+ if (deleteUpdate) {
+ // Finally, delete the update (if any)
+ Uri uri = ContentUris.withAppendedId(
+ EmailContent.Message.UPDATED_CONTENT_URI, messageId);
+ context.getContentResolver().delete(uri, null, null);
+ }
+ }
+
+ /**
+ * Upsync changes to read, flagged, or mailbox
+ *
+ * @param remoteStore the remote store for this mailbox
+ * @param mailbox the mailbox the message is stored in
+ * @param changeRead whether the message's read state has changed
+ * @param changeFlagged whether the message's flagged state has changed
+ * @param changeMailbox whether the message's mailbox has changed
+ * @param oldMessage the message in it's pre-change state
+ * @param newMessage the current version of the message
+ */
+ private static void processPendingDataChange(final Context context, Store remoteStore,
+ Mailbox mailbox, boolean changeRead, boolean changeFlagged, boolean changeMailbox,
+ boolean changeAnswered, EmailContent.Message oldMessage,
+ final EmailContent.Message newMessage) throws MessagingException {
+ // New mailbox is the mailbox this message WILL be in (same as the one it WAS in if it isn't
+ // being moved
+ Mailbox newMailbox = mailbox;
+ // Mailbox is the original remote mailbox (the one we're acting on)
+ mailbox = getRemoteMailboxForMessage(context, oldMessage);
+
+ // 0. No remote update if the message is local-only
+ if (newMessage.mServerId == null || newMessage.mServerId.equals("")
+ || newMessage.mServerId.startsWith(LOCAL_SERVERID_PREFIX) || (mailbox == null)) {
+ return;
+ }
+
+ // 1. No remote update for DRAFTS or OUTBOX
+ if (mailbox.mType == Mailbox.TYPE_DRAFTS || mailbox.mType == Mailbox.TYPE_OUTBOX) {
+ return;
+ }
+
+ // 2. Open the remote store & folder
+ Folder remoteFolder = remoteStore.getFolder(mailbox.mServerId);
+ if (!remoteFolder.exists()) {
+ return;
+ }
+ remoteFolder.open(OpenMode.READ_WRITE);
+ if (remoteFolder.getMode() != OpenMode.READ_WRITE) {
+ return;
+ }
+
+ // 3. Finally, apply the changes to the message
+ Message remoteMessage = remoteFolder.getMessage(newMessage.mServerId);
+ if (remoteMessage == null) {
+ return;
+ }
+ if (MailActivityEmail.DEBUG) {
+ Log.d(Logging.LOG_TAG,
+ "Update for msg id=" + newMessage.mId
+ + " read=" + newMessage.mFlagRead
+ + " flagged=" + newMessage.mFlagFavorite
+ + " answered="
+ + ((newMessage.mFlags & EmailContent.Message.FLAG_REPLIED_TO) != 0)
+ + " new mailbox=" + newMessage.mMailboxKey);
+ }
+ Message[] messages = new Message[] { remoteMessage };
+ if (changeRead) {
+ remoteFolder.setFlags(messages, FLAG_LIST_SEEN, newMessage.mFlagRead);
+ }
+ if (changeFlagged) {
+ remoteFolder.setFlags(messages, FLAG_LIST_FLAGGED, newMessage.mFlagFavorite);
+ }
+ if (changeAnswered) {
+ remoteFolder.setFlags(messages, FLAG_LIST_ANSWERED,
+ (newMessage.mFlags & EmailContent.Message.FLAG_REPLIED_TO) != 0);
+ }
+ if (changeMailbox) {
+ Folder toFolder = remoteStore.getFolder(newMailbox.mServerId);
+ if (!remoteFolder.exists()) {
+ return;
+ }
+ // We may need the message id to search for the message in the destination folder
+ remoteMessage.setMessageId(newMessage.mMessageId);
+ // Copy the message to its new folder
+ remoteFolder.copyMessages(messages, toFolder, new MessageUpdateCallbacks() {
+ @Override
+ public void onMessageUidChange(Message message, String newUid) {
+ ContentValues cv = new ContentValues();
+ cv.put(EmailContent.Message.SERVER_ID, newUid);
+ // We only have one message, so, any updates _must_ be for it. Otherwise,
+ // we'd have to cycle through to find the one with the same server ID.
+ context.getContentResolver().update(ContentUris.withAppendedId(
+ EmailContent.Message.CONTENT_URI, newMessage.mId), cv, null, null);
+ }
+ @Override
+ public void onMessageNotFound(Message message) {
+ }
+ });
+ // Delete the message from the remote source folder
+ remoteMessage.setFlag(Flag.DELETED, true);
+ remoteFolder.expunge();
+ }
+ remoteFolder.close(false);
+ }
+
+ /**
+ * Process a pending trash message command.
+ *
+ * @param remoteStore the remote store we're working in
+ * @param account The account in which we are working
+ * @param newMailbox The local trash mailbox
+ * @param oldMessage The message copy that was saved in the updates shadow table
+ * @param newMessage The message that was moved to the mailbox
+ */
+ private static void processPendingMoveToTrash(final Context context, Store remoteStore,
+ Account account, Mailbox newMailbox, EmailContent.Message oldMessage,
+ final EmailContent.Message newMessage) throws MessagingException {
+
+ // 0. No remote move if the message is local-only
+ if (newMessage.mServerId == null || newMessage.mServerId.equals("")
+ || newMessage.mServerId.startsWith(LOCAL_SERVERID_PREFIX)) {
+ return;
+ }
+
+ // 1. Escape early if we can't find the local mailbox
+ // TODO smaller projection here
+ Mailbox oldMailbox = getRemoteMailboxForMessage(context, oldMessage);
+ if (oldMailbox == null) {
+ // can't find old mailbox, it may have been deleted. just return.
+ return;
+ }
+ // 2. We don't support delete-from-trash here
+ if (oldMailbox.mType == Mailbox.TYPE_TRASH) {
+ return;
+ }
+
+ // The rest of this method handles server-side deletion
+
+ // 4. Find the remote mailbox (that we deleted from), and open it
+ Folder remoteFolder = remoteStore.getFolder(oldMailbox.mServerId);
+ if (!remoteFolder.exists()) {
+ return;
+ }
+
+ remoteFolder.open(OpenMode.READ_WRITE);
+ if (remoteFolder.getMode() != OpenMode.READ_WRITE) {
+ remoteFolder.close(false);
+ return;
+ }
+
+ // 5. Find the remote original message
+ Message remoteMessage = remoteFolder.getMessage(oldMessage.mServerId);
+ if (remoteMessage == null) {
+ remoteFolder.close(false);
+ return;
+ }
+
+ // 6. Find the remote trash folder, and create it if not found
+ Folder remoteTrashFolder = remoteStore.getFolder(newMailbox.mServerId);
+ if (!remoteTrashFolder.exists()) {
+ /*
+ * If the remote trash folder doesn't exist we try to create it.
+ */
+ remoteTrashFolder.create(FolderType.HOLDS_MESSAGES);
+ }
+
+ // 7. Try to copy the message into the remote trash folder
+ // Note, this entire section will be skipped for POP3 because there's no remote trash
+ if (remoteTrashFolder.exists()) {
+ /*
+ * Because remoteTrashFolder may be new, we need to explicitly open it
+ */
+ remoteTrashFolder.open(OpenMode.READ_WRITE);
+ if (remoteTrashFolder.getMode() != OpenMode.READ_WRITE) {
+ remoteFolder.close(false);
+ remoteTrashFolder.close(false);
+ return;
+ }
+
+ remoteFolder.copyMessages(new Message[] { remoteMessage }, remoteTrashFolder,
+ new Folder.MessageUpdateCallbacks() {
+ @Override
+ public void onMessageUidChange(Message message, String newUid) {
+ // update the UID in the local trash folder, because some stores will
+ // have to change it when copying to remoteTrashFolder
+ ContentValues cv = new ContentValues();
+ cv.put(EmailContent.Message.SERVER_ID, newUid);
+ context.getContentResolver().update(newMessage.getUri(), cv, null, null);
+ }
+
+ /**
+ * This will be called if the deleted message doesn't exist and can't be
+ * deleted (e.g. it was already deleted from the server.) In this case,
+ * attempt to delete the local copy as well.
+ */
+ @Override
+ public void onMessageNotFound(Message message) {
+ context.getContentResolver().delete(newMessage.getUri(), null, null);
+ }
+ });
+ remoteTrashFolder.close(false);
+ }
+
+ // 8. Delete the message from the remote source folder
+ remoteMessage.setFlag(Flag.DELETED, true);
+ remoteFolder.expunge();
+ remoteFolder.close(false);
+ }
+
+ /**
+ * Process a pending trash message command.
+ *
+ * @param remoteStore the remote store we're working in
+ * @param account The account in which we are working
+ * @param oldMailbox The local trash mailbox
+ * @param oldMessage The message that was deleted from the trash
+ */
+ private static void processPendingDeleteFromTrash(Context context, Store remoteStore,
+ Account account, Mailbox oldMailbox, EmailContent.Message oldMessage)
+ throws MessagingException {
+
+ // 1. We only support delete-from-trash here
+ if (oldMailbox.mType != Mailbox.TYPE_TRASH) {
+ return;
+ }
+
+ // 2. Find the remote trash folder (that we are deleting from), and open it
+ Folder remoteTrashFolder = remoteStore.getFolder(oldMailbox.mServerId);
+ if (!remoteTrashFolder.exists()) {
+ return;
+ }
+
+ remoteTrashFolder.open(OpenMode.READ_WRITE);
+ if (remoteTrashFolder.getMode() != OpenMode.READ_WRITE) {
+ remoteTrashFolder.close(false);
+ return;
+ }
+
+ // 3. Find the remote original message
+ Message remoteMessage = remoteTrashFolder.getMessage(oldMessage.mServerId);
+ if (remoteMessage == null) {
+ remoteTrashFolder.close(false);
+ return;
+ }
+
+ // 4. Delete the message from the remote trash folder
+ remoteMessage.setFlag(Flag.DELETED, true);
+ remoteTrashFolder.expunge();
+ remoteTrashFolder.close(false);
+ }
+
+ /**
+ * A message and numeric uid that's easily sortable
+ */
+ private static class SortableMessage {
+ private final Message mMessage;
+ private final long mUid;
+
+ SortableMessage(Message message, long uid) {
+ mMessage = message;
+ mUid = uid;
+ }
+ }
+
+ private int searchMailboxImpl(final Context context, long accountId, SearchParams searchParams,
+ final long destMailboxId) throws MessagingException {
+ final Account account = Account.restoreAccountWithId(context, accountId);
+ final Mailbox mailbox = Mailbox.restoreMailboxWithId(context, searchParams.mMailboxId);
+ final Mailbox destMailbox = Mailbox.restoreMailboxWithId(context, destMailboxId);
+ if (account == null || mailbox == null || destMailbox == null) {
+ Log.d(Logging.LOG_TAG, "Attempted search for " + searchParams
+ + " but account or mailbox information was missing");
+ return 0;
+ }
+
+ // Tell UI that we're loading messages
+
+ Store remoteStore = Store.getInstance(account, context);
+ Folder remoteFolder = remoteStore.getFolder(mailbox.mServerId);
+ remoteFolder.open(OpenMode.READ_WRITE);
+
+ SortableMessage[] sortableMessages = new SortableMessage[0];
+ if (searchParams.mOffset == 0) {
+ // Get the "bare" messages (basically uid)
+ Message[] remoteMessages = remoteFolder.getMessages(searchParams, null);
+ int remoteCount = remoteMessages.length;
+ if (remoteCount > 0) {
+ sortableMessages = new SortableMessage[remoteCount];
+ int i = 0;
+ for (Message msg : remoteMessages) {
+ sortableMessages[i++] = new SortableMessage(msg, Long.parseLong(msg.getUid()));
+ }
+ // Sort the uid's, most recent first
+ // Note: Not all servers will be nice and return results in the order of request;
+ // those that do will see messages arrive from newest to oldest
+ Arrays.sort(sortableMessages, new Comparator() {
+ @Override
+ public int compare(SortableMessage lhs, SortableMessage rhs) {
+ return lhs.mUid > rhs.mUid ? -1 : lhs.mUid < rhs.mUid ? 1 : 0;
+ }
+ });
+ sSearchResults.put(accountId, sortableMessages);
+ }
+ } else {
+ sortableMessages = sSearchResults.get(accountId);
+ }
+
+ final int numSearchResults = sortableMessages.length;
+ final int numToLoad =
+ Math.min(numSearchResults - searchParams.mOffset, searchParams.mLimit);
+ if (numToLoad <= 0) {
+ return 0;
+ }
+
+ final ArrayList messageList = new ArrayList();
+ for (int i = searchParams.mOffset; i < numToLoad + searchParams.mOffset; i++) {
+ messageList.add(sortableMessages[i].mMessage);
+ }
+ // Get everything in one pass, rather than two (as in sync); this starts getting us
+ // usable results quickly.
+ FetchProfile fp = new FetchProfile();
+ fp.add(FetchProfile.Item.FLAGS);
+ fp.add(FetchProfile.Item.ENVELOPE);
+ fp.add(FetchProfile.Item.STRUCTURE);
+ fp.add(FetchProfile.Item.BODY_SANE);
+ remoteFolder.fetch(messageList.toArray(new Message[0]), fp,
+ new MessageRetrievalListener() {
+ @Override
+ public void messageRetrieved(Message message) {
+ try {
+ // Determine if the new message was already known (e.g. partial)
+ // And create or reload the full message info
+ EmailContent.Message localMessage = new EmailContent.Message();
+ try {
+ // Copy the fields that are available into the message
+ LegacyConversions.updateMessageFields(localMessage,
+ message, account.mId, mailbox.mId);
+ // Commit the message to the local store
+ Utilities.saveOrUpdate(localMessage, context);
+ localMessage.mMailboxKey = destMailboxId;
+ // We load 50k or so; maybe it's complete, maybe not...
+ int flag = EmailContent.Message.FLAG_LOADED_COMPLETE;
+ // We store the serverId of the source mailbox into protocolSearchInfo
+ // This will be used by loadMessageForView, etc. to use the proper remote
+ // folder
+ localMessage.mProtocolSearchInfo = mailbox.mServerId;
+ if (message.getSize() > Store.FETCH_BODY_SANE_SUGGESTED_SIZE) {
+ flag = EmailContent.Message.FLAG_LOADED_PARTIAL;
+ }
+ Utilities.copyOneMessageToProvider(context, message, localMessage, flag);
+ } catch (MessagingException me) {
+ Log.e(Logging.LOG_TAG,
+ "Error while copying downloaded message." + me);
+ }
+ } catch (Exception e) {
+ Log.e(Logging.LOG_TAG,
+ "Error while storing downloaded message." + e.toString());
+ }
+ }
+
+ @Override
+ public void loadAttachmentProgress(int progress) {
+ }
+ });
+ return numSearchResults;
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/email/service/ImapTempFileLiteral.java b/src/com/android/email/service/ImapTempFileLiteral.java
new file mode 100644
index 000000000..cc1dd5410
--- /dev/null
+++ b/src/com/android/email/service/ImapTempFileLiteral.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.email.service;
+
+import android.util.Log;
+
+import com.android.email.FixedLengthInputStream;
+import com.android.email.mail.store.imap.ImapResponse;
+import com.android.email.mail.store.imap.ImapResponseParser;
+import com.android.email.mail.store.imap.ImapString;
+import com.android.emailcommon.Logging;
+import com.android.emailcommon.TempDirectory;
+import com.android.emailcommon.utility.Utility;
+
+import org.apache.commons.io.IOUtils;
+
+import java.io.ByteArrayInputStream;
+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;
+
+/**
+ * Subclass of {@link ImapString} used for literals backed by a temp file.
+ */
+public class ImapTempFileLiteral extends ImapString {
+ /* package for test */ final File mFile;
+
+ /** Size is purely for toString() */
+ private final int mSize;
+
+ /* package */ ImapTempFileLiteral(FixedLengthInputStream stream) throws IOException {
+ mSize = stream.getLength();
+ mFile = File.createTempFile("imap", ".tmp", TempDirectory.getTempDirectory());
+
+ // Unfortunately, we can't really use deleteOnExit(), because temp filenames are random
+ // so it'd simply cause a memory leak.
+ // deleteOnExit() simply adds filenames to a static list and the list will never shrink.
+ // mFile.deleteOnExit();
+ OutputStream out = new FileOutputStream(mFile);
+ IOUtils.copy(stream, out);
+ out.close();
+ }
+
+ /**
+ * Make sure we delete the temp file.
+ *
+ * We should always be calling {@link ImapResponse#destroy()}, but it's here as a last resort.
+ */
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ destroy();
+ } finally {
+ super.finalize();
+ }
+ }
+
+ @Override
+ public InputStream getAsStream() {
+ checkNotDestroyed();
+ try {
+ return new FileInputStream(mFile);
+ } catch (FileNotFoundException e) {
+ // It's probably possible if we're low on storage and the system clears the cache dir.
+ Log.w(Logging.LOG_TAG, "ImapTempFileLiteral: Temp file not found");
+
+ // Return 0 byte stream as a dummy...
+ return new ByteArrayInputStream(new byte[0]);
+ }
+ }
+
+ @Override
+ public String getString() {
+ checkNotDestroyed();
+ try {
+ byte[] bytes = IOUtils.toByteArray(getAsStream());
+ // Prevent crash from OOM; we've seen this, but only rarely and not reproducibly
+ if (bytes.length > ImapResponseParser.LITERAL_KEEP_IN_MEMORY_THRESHOLD) {
+ throw new IOException();
+ }
+ return Utility.fromAscii(bytes);
+ } catch (IOException e) {
+ Log.w(Logging.LOG_TAG, "ImapTempFileLiteral: Error while reading temp file", e);
+ return "";
+ }
+ }
+
+ @Override
+ public void destroy() {
+ try {
+ if (!isDestroyed() && mFile.exists()) {
+ mFile.delete();
+ }
+ } catch (RuntimeException re) {
+ // Just log and ignore.
+ Log.w(Logging.LOG_TAG, "Failed to remove temp file: " + re.getMessage());
+ }
+ super.destroy();
+ }
+
+ @Override
+ public String toString() {
+ return String.format("{%d byte literal(file)}", mSize);
+ }
+
+ public boolean tempFileExistsForTest() {
+ return mFile.exists();
+ }
+}
diff --git a/src/com/android/email/service/LegacyImap2AuthenticatorService.java b/src/com/android/email/service/LegacyImapAuthenticatorService.java
similarity index 90%
rename from src/com/android/email/service/LegacyImap2AuthenticatorService.java
rename to src/com/android/email/service/LegacyImapAuthenticatorService.java
index fb6dbd81f..8480d1e8d 100644
--- a/src/com/android/email/service/LegacyImap2AuthenticatorService.java
+++ b/src/com/android/email/service/LegacyImapAuthenticatorService.java
@@ -19,5 +19,5 @@ package com.android.email.service;
/**
* This service needs to be declared separately from the base service
*/
-public class LegacyImap2AuthenticatorService extends AuthenticatorService {
+public class LegacyImapAuthenticatorService extends AuthenticatorService {
}
diff --git a/src/com/android/email/service/LegacyImapSyncAdapterService.java b/src/com/android/email/service/LegacyImapSyncAdapterService.java
new file mode 100644
index 000000000..1f6b6195e
--- /dev/null
+++ b/src/com/android/email/service/LegacyImapSyncAdapterService.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.email.service;
+
+public class LegacyImapSyncAdapterService extends PopImapSyncAdapterService {
+}
\ No newline at end of file
diff --git a/src/com/android/email/service/Pop3SyncAdapterService.java b/src/com/android/email/service/Pop3SyncAdapterService.java
index 8f2cc8559..a939f41f9 100644
--- a/src/com/android/email/service/Pop3SyncAdapterService.java
+++ b/src/com/android/email/service/Pop3SyncAdapterService.java
@@ -16,212 +16,5 @@
package com.android.email.service;
-import android.accounts.OperationCanceledException;
-import android.app.Service;
-import android.content.AbstractThreadedSyncAdapter;
-import android.content.ContentProviderClient;
-import android.content.ContentResolver;
-import android.content.ContentUris;
-import android.content.ContentValues;
-import android.content.Context;
-import android.content.Intent;
-import android.content.SyncResult;
-import android.database.Cursor;
-import android.net.Uri;
-import android.os.Bundle;
-import android.os.IBinder;
-import android.util.Log;
-
-import com.android.emailcommon.TempDirectory;
-import com.android.emailcommon.mail.MessagingException;
-import com.android.emailcommon.provider.Account;
-import com.android.emailcommon.provider.EmailContent;
-import com.android.emailcommon.provider.EmailContent.AccountColumns;
-import com.android.emailcommon.provider.EmailContent.Message;
-import com.android.emailcommon.provider.Mailbox;
-import com.android.emailcommon.service.EmailServiceProxy;
-
-import java.util.ArrayList;
-
-public class Pop3SyncAdapterService extends Service {
- private static final String TAG = "Pop3SyncAdapterService";
- private static SyncAdapterImpl sSyncAdapter = null;
- private static final Object sSyncAdapterLock = new Object();
-
- public Pop3SyncAdapterService() {
- super();
- }
-
- private static class SyncAdapterImpl extends AbstractThreadedSyncAdapter {
- private Context mContext;
-
- public SyncAdapterImpl(Context context) {
- super(context, true /* autoInitialize */);
- mContext = context;
- }
-
- @Override
- public void onPerformSync(android.accounts.Account account, Bundle extras,
- String authority, ContentProviderClient provider, SyncResult syncResult) {
- try {
- Pop3SyncAdapterService.performSync(mContext, account, extras,
- authority, provider, syncResult);
- } catch (OperationCanceledException e) {
- }
- }
- }
-
- @Override
- public void onCreate() {
- super.onCreate();
- synchronized (sSyncAdapterLock) {
- if (sSyncAdapter == null) {
- sSyncAdapter = new SyncAdapterImpl(getApplicationContext());
- }
- }
- }
-
- @Override
- public IBinder onBind(Intent intent) {
- return sSyncAdapter.getSyncAdapterBinder();
- }
-
- private static void sync(Context context, long mailboxId, SyncResult syncResult,
- boolean uiRefresh) {
- TempDirectory.setTempDirectory(context);
- Mailbox mailbox = Mailbox.restoreMailboxWithId(context, mailboxId);
- if (mailbox == null) return;
- Account account = Account.restoreAccountWithId(context, mailbox.mAccountKey);
- if (account == null) return;
- ContentResolver resolver = context.getContentResolver();
- if ((mailbox.mType != Mailbox.TYPE_OUTBOX) && (mailbox.mType != Mailbox.TYPE_INBOX)) {
- // This is an update to a message in a non-syncing mailbox; delete this from the
- // updates table and return
- resolver.delete(Message.UPDATED_CONTENT_URI, Message.MAILBOX_KEY + "=?",
- new String[] {Long.toString(mailbox.mId)});
- return;
- }
- Log.d(TAG, "Mailbox: " + mailbox.mDisplayName);
-
- Uri mailboxUri = ContentUris.withAppendedId(Mailbox.CONTENT_URI, mailboxId);
- ContentValues values = new ContentValues();
- // Set mailbox sync state
- values.put(Mailbox.UI_SYNC_STATUS,
- uiRefresh ? EmailContent.SYNC_STATUS_USER : EmailContent.SYNC_STATUS_BACKGROUND);
- resolver.update(mailboxUri, values, null, null);
- try {
- try {
- if (mailbox.mType == Mailbox.TYPE_OUTBOX) {
- EmailServiceStub.sendMailImpl(context, account.mId);
- } else {
- Pop3Service.synchronizeMailboxSynchronous(context, account, mailbox);
- }
- } catch (MessagingException e) {
- int cause = e.getExceptionType();
- switch(cause) {
- case MessagingException.IOERROR:
- syncResult.stats.numIoExceptions++;
- break;
- case MessagingException.AUTHENTICATION_FAILED:
- syncResult.stats.numAuthExceptions++;
- break;
- }
- }
- } finally {
- // Always clear our sync state
- values.put(Mailbox.UI_SYNC_STATUS, EmailContent.SYNC_STATUS_NONE);
- resolver.update(mailboxUri, values, null, null);
- }
- }
-
- /**
- * Partial integration with system SyncManager; we initiate manual syncs upon request
- */
- private static void performSync(Context context, android.accounts.Account account,
- Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult)
- throws OperationCanceledException {
- // Find an EmailProvider account with the Account's email address
- Cursor c = null;
- try {
- c = provider.query(com.android.emailcommon.provider.Account.CONTENT_URI,
- Account.CONTENT_PROJECTION, AccountColumns.EMAIL_ADDRESS + "=?",
- new String[] {account.name}, null);
- if (c != null && c.moveToNext()) {
- Account acct = new Account();
- acct.restore(c);
- if (extras.getBoolean(ContentResolver.SYNC_EXTRAS_UPLOAD)) {
- Log.d(TAG, "Upload sync request for " + acct.mDisplayName);
- // See if any boxes have mail...
- ArrayList mailboxesToUpdate;
- Cursor updatesCursor = provider.query(Message.UPDATED_CONTENT_URI,
- new String[] {Message.MAILBOX_KEY},
- Message.ACCOUNT_KEY + "=?",
- new String[] {Long.toString(acct.mId)},
- null);
- try {
- if ((updatesCursor == null) || (updatesCursor.getCount() == 0)) return;
- mailboxesToUpdate = new ArrayList();
- while (updatesCursor.moveToNext()) {
- Long mailboxId = updatesCursor.getLong(0);
- if (!mailboxesToUpdate.contains(mailboxId)) {
- mailboxesToUpdate.add(mailboxId);
- }
- }
- } finally {
- if (updatesCursor != null) {
- updatesCursor.close();
- }
- }
- for (long mailboxId: mailboxesToUpdate) {
- sync(context, mailboxId, syncResult, false);
- }
- } else {
- Log.d(TAG, "Sync request for " + acct.mDisplayName);
- Log.d(TAG, extras.toString());
- long mailboxId = extras.getLong(EmailServiceStub.SYNC_EXTRA_MAILBOX_ID,
- Mailbox.NO_MAILBOX);
- boolean isInbox = false;
- if (mailboxId == Mailbox.NO_MAILBOX) {
- mailboxId = Mailbox.findMailboxOfType(context, acct.mId,
- Mailbox.TYPE_INBOX);
- if (mailboxId == Mailbox.NO_MAILBOX) {
- // Update folders?
- EmailServiceProxy service =
- EmailServiceUtils.getServiceForAccount(context, null, acct.mId);
- service.updateFolderList(acct.mId);
- }
- isInbox = true;
- }
- if (mailboxId == Mailbox.NO_MAILBOX) return;
- boolean uiRefresh =
- extras.getBoolean(ContentResolver.SYNC_EXTRAS_FORCE, false);
- sync(context, mailboxId, syncResult, uiRefresh);
-
- // Outbox is a special case here
- Mailbox mailbox = Mailbox.restoreMailboxWithId(context, mailboxId);
- if (mailbox.mType == Mailbox.TYPE_OUTBOX) {
- return;
- }
-
- // Convert from minutes to seconds
- int syncFrequency = acct.mSyncInterval * 60;
- // Values < 0 are for "never" or "push"; 0 is undefined
- if (syncFrequency <= 0) return;
- Bundle ex = new Bundle();
- if (!isInbox) {
- ex.putLong(EmailServiceStub.SYNC_EXTRA_MAILBOX_ID, mailboxId);
- }
- Log.d(TAG, "Setting periodic sync for " + acct.mDisplayName + ": " +
- syncFrequency + " seconds");
- ContentResolver.addPeriodicSync(account, authority, ex, syncFrequency);
- }
- }
- } catch (Exception e) {
- e.printStackTrace();
- } finally {
- if (c != null) {
- c.close();
- }
- }
- }
+public class Pop3SyncAdapterService extends PopImapSyncAdapterService {
}
\ No newline at end of file
diff --git a/src/com/android/email/service/PopImapSyncAdapterService.java b/src/com/android/email/service/PopImapSyncAdapterService.java
new file mode 100644
index 000000000..387adb7d0
--- /dev/null
+++ b/src/com/android/email/service/PopImapSyncAdapterService.java
@@ -0,0 +1,252 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.email.service;
+
+import android.accounts.OperationCanceledException;
+import android.app.Service;
+import android.content.AbstractThreadedSyncAdapter;
+import android.content.ContentProviderClient;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SyncResult;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.util.Log;
+
+import com.android.email.R;
+import com.android.emailcommon.TempDirectory;
+import com.android.emailcommon.mail.MessagingException;
+import com.android.emailcommon.provider.Account;
+import com.android.emailcommon.provider.EmailContent;
+import com.android.emailcommon.provider.EmailContent.AccountColumns;
+import com.android.emailcommon.provider.EmailContent.Message;
+import com.android.emailcommon.provider.HostAuth;
+import com.android.emailcommon.provider.Mailbox;
+import com.android.emailcommon.service.EmailServiceProxy;
+
+import java.util.ArrayList;
+
+public class PopImapSyncAdapterService extends Service {
+ private static final String TAG = "PopImapSyncAdapterService";
+ private SyncAdapterImpl mSyncAdapter = null;
+ private static final Object sSyncAdapterLock = new Object();
+
+ public PopImapSyncAdapterService() {
+ super();
+ }
+
+ private static class SyncAdapterImpl extends AbstractThreadedSyncAdapter {
+ private Context mContext;
+
+ public SyncAdapterImpl(Context context) {
+ super(context, true /* autoInitialize */);
+ mContext = context;
+ }
+
+ @Override
+ public void onPerformSync(android.accounts.Account account, Bundle extras,
+ String authority, ContentProviderClient provider, SyncResult syncResult) {
+ try {
+ PopImapSyncAdapterService.performSync(mContext, account, extras,
+ authority, provider, syncResult);
+ } catch (OperationCanceledException e) {
+ }
+ }
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ synchronized (sSyncAdapterLock) {
+ mSyncAdapter = new SyncAdapterImpl(getApplicationContext());
+ }
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return mSyncAdapter.getSyncAdapterBinder();
+ }
+
+ /**
+ * @return whether or not this mailbox retrieves its data from the server (as opposed to just
+ * a local mailbox that is never synced).
+ */
+ private static boolean loadsFromServer(Context context, Mailbox m, String protocol) {
+ String legacyImapProtocol = context.getString(R.string.protocol_legacy_imap);
+ if (legacyImapProtocol.equals(protocol)) {
+ // TODO: actually use a sync flag when creating the mailboxes. Right now we use an
+ // approximation for IMAP.
+ return m.mType != Mailbox.TYPE_DRAFTS
+ && m.mType != Mailbox.TYPE_OUTBOX
+ && m.mType != Mailbox.TYPE_SEARCH;
+
+ } else if (HostAuth.LEGACY_SCHEME_POP3.equals(protocol)) {
+ return Mailbox.TYPE_INBOX == m.mType;
+ }
+
+ return false;
+ }
+
+ private static void sync(Context context, long mailboxId, SyncResult syncResult,
+ boolean uiRefresh) {
+ TempDirectory.setTempDirectory(context);
+ Mailbox mailbox = Mailbox.restoreMailboxWithId(context, mailboxId);
+ if (mailbox == null) return;
+ Account account = Account.restoreAccountWithId(context, mailbox.mAccountKey);
+ if (account == null) return;
+ ContentResolver resolver = context.getContentResolver();
+ String protocol = account.getProtocol(context);
+ if ((mailbox.mType != Mailbox.TYPE_OUTBOX) &&
+ !loadsFromServer(context, mailbox, protocol)) {
+ // This is an update to a message in a non-syncing mailbox; delete this from the
+ // updates table and return
+ resolver.delete(Message.UPDATED_CONTENT_URI, Message.MAILBOX_KEY + "=?",
+ new String[] {Long.toString(mailbox.mId)});
+ return;
+ }
+ Log.d(TAG, "Mailbox: " + mailbox.mDisplayName);
+
+ Uri mailboxUri = ContentUris.withAppendedId(Mailbox.CONTENT_URI, mailboxId);
+ ContentValues values = new ContentValues();
+ // Set mailbox sync state
+ values.put(Mailbox.UI_SYNC_STATUS,
+ uiRefresh ? EmailContent.SYNC_STATUS_USER : EmailContent.SYNC_STATUS_BACKGROUND);
+ resolver.update(mailboxUri, values, null, null);
+ try {
+ try {
+ String legacyImapProtocol = context.getString(R.string.protocol_legacy_imap);
+ if (mailbox.mType == Mailbox.TYPE_OUTBOX) {
+ EmailServiceStub.sendMailImpl(context, account.mId);
+ } else if (protocol.equals(legacyImapProtocol)) {
+ ImapService.synchronizeMailboxSynchronous(context, account, mailbox);
+ } else {
+ Pop3Service.synchronizeMailboxSynchronous(context, account, mailbox);
+ }
+ } catch (MessagingException e) {
+ int cause = e.getExceptionType();
+ switch(cause) {
+ case MessagingException.IOERROR:
+ syncResult.stats.numIoExceptions++;
+ break;
+ case MessagingException.AUTHENTICATION_FAILED:
+ syncResult.stats.numAuthExceptions++;
+ break;
+ }
+ }
+ } finally {
+ // Always clear our sync state
+ values.put(Mailbox.UI_SYNC_STATUS, EmailContent.SYNC_STATUS_NONE);
+ resolver.update(mailboxUri, values, null, null);
+ }
+ }
+
+ /**
+ * Partial integration with system SyncManager; we initiate manual syncs upon request
+ */
+ private static void performSync(Context context, android.accounts.Account account,
+ Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult)
+ throws OperationCanceledException {
+ // Find an EmailProvider account with the Account's email address
+ Cursor c = null;
+ try {
+ c = provider.query(com.android.emailcommon.provider.Account.CONTENT_URI,
+ Account.CONTENT_PROJECTION, AccountColumns.EMAIL_ADDRESS + "=?",
+ new String[] {account.name}, null);
+ if (c != null && c.moveToNext()) {
+ Account acct = new Account();
+ acct.restore(c);
+ if (extras.getBoolean(ContentResolver.SYNC_EXTRAS_UPLOAD)) {
+ Log.d(TAG, "Upload sync request for " + acct.mDisplayName);
+ // See if any boxes have mail...
+ ArrayList mailboxesToUpdate;
+ Cursor updatesCursor = provider.query(Message.UPDATED_CONTENT_URI,
+ new String[] {Message.MAILBOX_KEY},
+ Message.ACCOUNT_KEY + "=?",
+ new String[] {Long.toString(acct.mId)},
+ null);
+ try {
+ if ((updatesCursor == null) || (updatesCursor.getCount() == 0)) return;
+ mailboxesToUpdate = new ArrayList();
+ while (updatesCursor.moveToNext()) {
+ Long mailboxId = updatesCursor.getLong(0);
+ if (!mailboxesToUpdate.contains(mailboxId)) {
+ mailboxesToUpdate.add(mailboxId);
+ }
+ }
+ } finally {
+ if (updatesCursor != null) {
+ updatesCursor.close();
+ }
+ }
+ for (long mailboxId: mailboxesToUpdate) {
+ sync(context, mailboxId, syncResult, false);
+ }
+ } else {
+ Log.d(TAG, "Sync request for " + acct.mDisplayName);
+ Log.d(TAG, extras.toString());
+ long mailboxId = extras.getLong(EmailServiceStub.SYNC_EXTRA_MAILBOX_ID,
+ Mailbox.NO_MAILBOX);
+ boolean isInbox = false;
+ if (mailboxId == Mailbox.NO_MAILBOX) {
+ mailboxId = Mailbox.findMailboxOfType(context, acct.mId,
+ Mailbox.TYPE_INBOX);
+ if (mailboxId == Mailbox.NO_MAILBOX) {
+ // Update folders?
+ EmailServiceProxy service =
+ EmailServiceUtils.getServiceForAccount(context, null, acct.mId);
+ service.updateFolderList(acct.mId);
+ }
+ isInbox = true;
+ }
+ if (mailboxId == Mailbox.NO_MAILBOX) return;
+ boolean uiRefresh =
+ extras.getBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, false);
+ sync(context, mailboxId, syncResult, uiRefresh);
+
+ // Outbox is a special case here
+ Mailbox mailbox = Mailbox.restoreMailboxWithId(context, mailboxId);
+ if (mailbox.mType == Mailbox.TYPE_OUTBOX) {
+ return;
+ }
+
+ // Convert from minutes to seconds
+ int syncFrequency = acct.mSyncInterval * 60;
+ // Values < 0 are for "never" or "push"; 0 is undefined
+ if (syncFrequency <= 0) return;
+ Bundle ex = new Bundle();
+ if (!isInbox) {
+ ex.putLong(EmailServiceStub.SYNC_EXTRA_MAILBOX_ID, mailboxId);
+ }
+ Log.d(TAG, "Setting periodic sync for " + acct.mDisplayName + ": " +
+ syncFrequency + " seconds");
+ ContentResolver.addPeriodicSync(account, authority, ex, syncFrequency);
+ }
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ } finally {
+ if (c != null) {
+ c.close();
+ }
+ }
+ }
+}
\ No newline at end of file