DO NOT MERGE: New IMAP parser to fix long-lasting problems.

- Almost completely re-wrote ImapResponseParser layer
- We no longer use simple ArrayList and String to represent
imap response.  We have classes for that.  (Type safe!)
These classes are also NPE-free.
(which isn't necessarily a good thing, though)
- A lot of clean-ups and fixes in ImapStore.
- More tests for ImapStore.

Now ImapResponseParser moved to com.android.email.mail.store.imap.parser,
but inside, it's 99% new code.

This CL introduces many new classes, but most of them are small classes
to represent the IMAP response.

Problems that this CL fixes includes:
- Special characters in OK response
- Handling BYE response
- Case sensitivity
- ClassCast/ArrayIndexOutOfBound/NumberFormatException
- Handling NIL/literals at any position

Bug 2480227
Bug 2244049
Bug 2138981
Bug 1351896
Bug 2591435
Bug 2173061
Bug 2370627
Bug 2524881
Bug 2525902
Bug 2538076

Backport of I7116f57fba079b8a5ef8d5439a9b3d9a9af8e6ed

Change-Id: I38b6da7b82110181dc78a2c63c6837c57afa81ae
This commit is contained in:
Makoto Onuki 2010-05-19 17:23:23 -07:00
parent 9d1b9fc784
commit ff0712cb1e
24 changed files with 3192 additions and 1139 deletions

View File

@ -69,6 +69,10 @@ public class FixedLengthInputStream extends InputStream {
return read(b, 0, b.length);
}
public int getLength() {
return mLength;
}
@Override
public String toString() {
return String.format("FixedLengthInputStream(in=%s, length=%d)", mIn.toString(), mLength);

View File

@ -1640,7 +1640,7 @@ public class MessagingController implements Runnable {
remoteFolder.fetch(new Message[] { remoteMessage }, fp, null);
Date localDate = new Date(message.mServerTimeStamp);
Date remoteDate = remoteMessage.getInternalDate();
if (remoteDate.compareTo(localDate) > 0) {
if (remoteDate != null && remoteDate.compareTo(localDate) > 0) {
// 4a. If the remote message is newer than ours we'll just
// delete ours and move on. A sync will get the server message
// if we need to be able to see it.

View File

@ -40,6 +40,7 @@ import android.util.Base64;
import android.util.Log;
import android.widget.TextView;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
@ -55,6 +56,9 @@ import java.util.regex.Pattern;
public class Utility {
public static final Charset UTF_8 = Charset.forName("UTF-8");
public static final Charset ASCII = Charset.forName("US-ASCII");
public static final String[] EMPTY_STRINGS = new String[0];
// "GMT" + "+" or "-" + 4 digits
private static final Pattern DATE_CLEANUP_PATTERN_WRONG_TIMEZONE =
@ -474,26 +478,44 @@ public class Utility {
return cal.getTimeInMillis();
}
/** Converts a String to UTF-8 */
public static byte[] toUtf8(String s) {
private static byte[] encode(Charset charset, String s) {
if (s == null) {
return null;
}
final ByteBuffer buffer = UTF_8.encode(CharBuffer.wrap(s));
final ByteBuffer buffer = charset.encode(CharBuffer.wrap(s));
final byte[] bytes = new byte[buffer.limit()];
buffer.get(bytes);
return bytes;
}
/** Build a String from UTF-8 bytes */
public static String fromUtf8(byte[] b) {
private static String decode(Charset charset, byte[] b) {
if (b == null) {
return null;
}
final CharBuffer cb = Utility.UTF_8.decode(ByteBuffer.wrap(b));
final CharBuffer cb = charset.decode(ByteBuffer.wrap(b));
return new String(cb.array(), 0, cb.length());
}
/** Converts a String to UTF-8 */
public static byte[] toUtf8(String s) {
return encode(UTF_8, s);
}
/** Builds a String from UTF-8 bytes */
public static String fromUtf8(byte[] b) {
return decode(UTF_8, b);
}
/** Converts a String to ASCII bytes */
public static byte[] toAscii(String s) {
return encode(ASCII, s);
}
/** Builds a String from ASCII bytes */
public static String fromAscii(byte[] b) {
return decode(ASCII, b);
}
/**
* @return true if the input is the first (or only) byte in a UTF-8 character
*/
@ -593,4 +615,8 @@ public class Utility {
date = DATE_CLEANUP_PATTERN_WRONG_TIMEZONE.matcher(date).replaceFirst("$1");
return date;
}
public static ByteArrayInputStream streamFromAsciiString(String ascii) {
return new ByteArrayInputStream(toAscii(ascii));
}
}

View File

@ -69,4 +69,17 @@ public class FetchProfile extends ArrayList<Fetchable> {
*/
BODY,
}
/**
* @return the first {@link Part} in this collection, or null if it doesn't contain
* {@link Part}.
*/
public Part getFirstPart() {
for (Fetchable o : this) {
if (o instanceof Part) {
return (Part) o;
}
}
return null;
}
}

View File

@ -20,6 +20,8 @@ import java.util.Date;
import java.util.HashSet;
public abstract class Message implements Part, Body {
public static final Message[] EMPTY_ARRAY = new Message[0];
public enum RecipientType {
TO, CC, BCC,
}
@ -111,7 +113,7 @@ public abstract class Message implements Part, Body {
}
/*
* TODO Refactor Flags at some point to be able to store user defined flags.
* TODO Refactor Flags at some point to be able to store user defined flags.
*/
public Flag[] getFlags() {
return getFlagSet().toArray(new Flag[] {});
@ -149,7 +151,7 @@ public abstract class Message implements Part, Body {
}
public abstract void saveChanges() throws MessagingException;
@Override
public String toString() {
return getClass().getSimpleName() + ':' + mUid;

View File

@ -1,512 +0,0 @@
/*
* Copyright (C) 2008 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;
import com.android.email.Email;
import com.android.email.FixedLengthInputStream;
import com.android.email.PeekableInputStream;
import com.android.email.mail.MessagingException;
import com.android.email.mail.transport.DiscourseLogger;
import com.android.email.mail.transport.LoggingInputStream;
import android.util.Config;
import android.util.Log;
import java.io.IOException;
import java.io.InputStream;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.Locale;
public class ImapResponseParser {
// DEBUG ONLY - Always check in as "false"
private static boolean DEBUG_LOG_RAW_STREAM = false;
// mDateTimeFormat 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 final PeekableInputStream mIn;
private InputStream mActiveLiteral;
/**
* To log network activities when the parser crashes.
*
* <p>We log all bytes received from the server, except for the part sent as literals.
*/
private final DiscourseLogger mDiscourseLogger;
public ImapResponseParser(InputStream in, DiscourseLogger discourseLogger) {
if (DEBUG_LOG_RAW_STREAM && Config.LOGD && Email.DEBUG) {
in = new LoggingInputStream(in);
}
this.mIn = new PeekableInputStream(in);
mDiscourseLogger = discourseLogger;
}
/**
* Read and return one byte from {@link #mIn}, and put it in {@link #mDiscourseLogger}.
* Return -1 when EOF.
*/
private int readByte() throws IOException {
int ret = mIn.read();
if (ret != -1) {
mDiscourseLogger.addReceivedByte(ret);
}
return ret;
}
/**
* Reads the next response available on the stream and returns an
* ImapResponse object that represents it.
* @return the parsed {@link ImapResponse} object.
*/
public ImapResponse readResponse() throws IOException {
try {
ImapResponse response = new ImapResponse();
if (mActiveLiteral != null) {
while (mActiveLiteral.read() != -1)
;
mActiveLiteral = null;
}
int ch = mIn.peek();
if (ch == '*') {
parseUntaggedResponse();
readTokens(response);
} else if (ch == '+') {
response.mCommandContinuationRequested =
parseCommandContinuationRequest();
readTokens(response);
} else {
response.mTag = parseTaggedResponse();
readTokens(response);
}
if (Config.LOGD) {
if (Email.DEBUG) {
Log.d(Email.LOG_TAG, "<<< " + response.toString());
}
}
return response;
} 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;
}
}
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(Email.LOG_TAG, "Exception detected: " + e.getMessage());
mDiscourseLogger.logLastDiscourse();
}
private void readTokens(ImapResponse response) throws IOException {
response.clear();
Object token;
while ((token = readToken()) != null) {
if (response != null) {
response.add(token);
}
if (mActiveLiteral != null) {
break;
}
}
response.mCompleted = token == null;
}
/**
* Reads the next token of the response. The token can be one of: String -
* for NIL, QUOTED, NUMBER, ATOM. InputStream - for LITERAL.
* InputStream.available() returns the total length of the stream.
* ImapResponseList - for PARENTHESIZED LIST. Can contain any of the above
* elements including List.
*
* @return The next token in the response or null if there are no more
* tokens.
* @throws IOException
*/
public Object readToken() throws IOException {
while (true) {
Object token = parseToken();
if (token == null || !(token.equals(")") || token.equals("]"))) {
return token;
}
}
}
private Object parseToken() throws IOException {
if (mActiveLiteral != null) {
while (mActiveLiteral.read() != -1)
;
mActiveLiteral = null;
}
while (true) {
int ch = mIn.peek();
if (ch == '(') {
return parseList('(', ")");
} else if (ch == ')') {
expect(')');
return ")";
} else if (ch == '[') {
return parseList('[', "]");
} else if (ch == ']') {
expect(']');
return "]";
} else if (ch == '"') {
return parseQuoted();
} else if (ch == '{') {
mActiveLiteral = parseLiteral();
return mActiveLiteral;
} else if (ch == ' ') {
expect(' ');
} else if (ch == '\r') {
expect('\r');
expect('\n');
return null;
} else if (ch == '\n') {
expect('\n');
return null;
} else {
return parseAtom();
}
}
}
private boolean parseCommandContinuationRequest() throws IOException {
expect('+');
expect(' ');
return true;
}
// * OK [UIDNEXT 175] Predicted next UID
private void parseUntaggedResponse() throws IOException {
expect('*');
expect(' ');
}
// 3 OK [READ-WRITE] Select completed.
private String parseTaggedResponse() throws IOException {
String tag = readStringUntil(' ');
return tag;
}
/**
* @param opener The char that the list opens with
* @param closer The char that ends the list
* @return a list object containing the elements of the list
* @throws IOException
*/
private ImapList parseList(char opener, String closer) throws IOException {
expect(opener);
ImapList list = new ImapList();
Object token;
while (true) {
token = parseToken();
if (token == null) {
break;
} else if (token instanceof InputStream) {
list.add(token);
break;
} else if (token.equals(closer)) {
break;
} else {
list.add(token);
}
}
return list;
}
private String parseAtom() throws IOException {
StringBuffer sb = new StringBuffer();
int ch;
while (true) {
ch = mIn.peek();
if (ch == -1) {
if (Config.LOGD && Email.DEBUG) {
Log.d(Email.LOG_TAG, "parseAtom(): end of stream reached");
}
throw new IOException("parseAtom(): end of stream reached");
} else 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 == '"' || (ch >= 0x00 && ch <= 0x1f) || ch == 0x7f) {
if (sb.length() == 0) {
throw new IOException(String.format("parseAtom(): (%04x %c)", ch, ch));
}
return sb.toString();
} else {
sb.append((char)readByte());
}
}
}
/**
* A { has been read, read the rest of the size string, the space and then
* notify the listener with an InputStream.
*
* @param mListener
* @throws IOException
*/
private InputStream parseLiteral() throws IOException {
expect('{');
int size = Integer.parseInt(readStringUntil('}'));
expect('\r');
expect('\n');
FixedLengthInputStream fixed = new FixedLengthInputStream(mIn, size);
return fixed;
}
/**
* A " has been read, read to the end of the quoted string and notify the
* listener.
*
* @param mListener
* @throws IOException
*/
private String parseQuoted() throws IOException {
expect('"');
return readStringUntil('"');
}
private String readStringUntil(char end) throws IOException {
StringBuffer sb = new StringBuffer();
int ch;
while ((ch = readByte()) != -1) {
if (ch == end) {
return sb.toString();
} else {
sb.append((char)ch);
}
}
if (Config.LOGD && Email.DEBUG) {
Log.d(Email.LOG_TAG, "readQuotedString(): end of stream reached");
}
throw new IOException("readQuotedString(): end of stream reached");
}
private int expect(char ch) throws IOException {
int d;
if ((d = readByte()) != ch) {
if (d == -1 && Config.LOGD && Email.DEBUG) {
Log.d(Email.LOG_TAG, "expect(): end of stream reached");
}
throw new IOException(String.format("Expected %04x (%c) but got %04x (%c)", (int)ch,
ch, d, (char)d));
}
return d;
}
/**
* Represents an IMAP LIST response and is also the base class for the
* ImapResponse.
*/
public class ImapList extends ArrayList<Object> {
public ImapList getList(int index) {
return (ImapList)get(index);
}
/** Safe version of getList() */
public ImapList getListOrNull(int index) {
if (index < size()) {
Object list = get(index);
if (list instanceof ImapList) {
return (ImapList) list;
}
}
return null;
}
public String getString(int index) {
return (String)get(index);
}
/** Safe version of getString() */
public String getStringOrNull(int index) {
if (index < size()) {
Object string = get(index);
if (string instanceof String) {
return (String) string;
}
}
return null;
}
public InputStream getLiteral(int index) {
return (InputStream)get(index);
}
public int getNumber(int index) {
return Integer.parseInt(getString(index));
}
public Date getDate(int index) throws MessagingException {
try {
return DATE_TIME_FORMAT.parse(getString(index));
} catch (ParseException pe) {
throw new MessagingException("Unable to parse IMAP datetime", pe);
}
}
public Object getKeyedValue(Object key) {
for (int i = 0, count = size(); i < count; i++) {
if (get(i).equals(key)) {
return get(i + 1);
}
}
return null;
}
public ImapList getKeyedList(Object key) {
return (ImapList)getKeyedValue(key);
}
public String getKeyedString(Object key) {
return (String)getKeyedValue(key);
}
public InputStream getKeyedLiteral(Object key) {
return (InputStream)getKeyedValue(key);
}
public int getKeyedNumber(Object key) {
return Integer.parseInt(getKeyedString(key));
}
public Date getKeyedDate(Object key) throws MessagingException {
try {
String value = getKeyedString(key);
if (value == null) {
return null;
}
return DATE_TIME_FORMAT.parse(value);
} catch (ParseException pe) {
throw new MessagingException("Unable to parse IMAP datetime", pe);
}
}
}
/**
* Represents a single response from the IMAP server. Tagged responses will
* have a non-null tag. Untagged responses will have a null tag. The object
* will contain all of the available tokens at the time the response is
* received. In general, it will either contain all of the tokens of the
* response or all of the tokens up until the first LITERAL. If the object
* does not contain the entire response the caller must call more() to
* continue reading the response until more returns false.
*/
public class ImapResponse extends ImapList {
private boolean mCompleted;
boolean mCommandContinuationRequested;
String mTag;
/*
* Return true if this response is completely read and parsed.
*/
public boolean completed() {
return mCompleted;
}
/*
* Nail down the last element that possibly is FixedLengthInputStream literal.
*/
public void nailDown() throws IOException {
int last = size() - 1;
if (last >= 0) {
Object o = get(last);
if (o instanceof FixedLengthInputStream) {
FixedLengthInputStream is = (FixedLengthInputStream) o;
byte[] buffer = new byte[is.available()];
is.read(buffer);
set(last, new String(buffer));
}
}
}
/*
* Append all response elements to this and copy completed flag.
*/
public void appendAll(ImapResponse other) {
addAll(other);
mCompleted = other.mCompleted;
}
public boolean more() throws IOException {
if (mCompleted) {
return false;
}
readTokens(this);
return true;
}
// Convert * [ALERT] blah blah blah into "blah blah blah"
public String getAlertText() {
if (size() > 1) {
ImapList alertList = this.getListOrNull(1);
if (alertList != null) {
String responseCode = alertList.getStringOrNull(0);
if ("ALERT".equalsIgnoreCase(responseCode)) {
StringBuffer sb = new StringBuffer();
for (int i = 2, count = size(); i < count; i++) {
if (i > 2) {
sb.append(' ');
}
sb.append(get(i).toString());
}
return sb.toString();
}
}
}
return null;
}
@Override
public String toString() {
return "#" + mTag + "# " + super.toString();
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,85 @@
/*
* 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.mail.Store;
public final class ImapConstants {
private ImapConstants() {}
public static final String FETCH_FIELD_BODY_PEEK_BARE = "BODY.PEEK";
public static final String FETCH_FIELD_BODY_PEEK = FETCH_FIELD_BODY_PEEK_BARE + "[]";
public static final String FETCH_FIELD_BODY_PEEK_SANE
= String.format("BODY.PEEK[]<0.%d>", Store.FETCH_BODY_SANE_SUGGESTED_SIZE);
public static final String FETCH_FIELD_HEADERS =
"BODY.PEEK[HEADER.FIELDS (date subject from content-type to cc message-id)]";
public static final String FLAG_ANSWERED = "\\ANSWERED";
public static final String FLAG_DELETED = "\\DELETED";
public static final String FLAG_FLAGGED = "\\FLAGGED";
public static final String FLAG_NO_SELECT = "\\NOSELECT";
public static final String FLAG_SEEN = "\\SEEN";
public static final String ALERT = "ALERT";
public static final String APPEND = "APPEND";
public static final String BAD = "BAD";
public static final String BADCHARSET = "BADCHARSET";
public static final String BODY = "BODY";
public static final String BODY_BRACKET_HEADER = "BODY[HEADER";
public static final String BODYSTRUCTURE = "BODYSTRUCTURE";
public static final String BYE = "BYE";
public static final String CAPABILITY = "CAPABILITY";
public static final String CHECK = "CHECK";
public static final String CLOSE = "CLOSE";
public static final String COPY = "COPY";
public static final String CREATE = "CREATE";
public static final String DELETE = "DELETE";
public static final String EXAMINE = "EXAMINE";
public static final String EXISTS = "EXISTS";
public static final String EXPUNGE = "EXPUNGE";
public static final String FETCH = "FETCH";
public static final String FLAGS = "FLAGS";
public static final String INBOX = "INBOX";
public static final String INTERNALDATE = "INTERNALDATE";
public static final String LIST = "LIST";
public static final String LOGIN = "LOGIN";
public static final String LOGOUT = "LOGOUT";
public static final String LSUB = "LSUB";
public static final String NO = "NO";
public static final String NOOP = "NOOP";
public static final String OK = "OK";
public static final String PARSE = "PARSE";
public static final String PERMANENTFLAGS = "PERMANENTFLAGS";
public static final String PREAUTH = "PREAUTH";
public static final String READ_ONLY = "READ-ONLY";
public static final String READ_WRITE = "READ-WRITE";
public static final String RENAME = "RENAME";
public static final String RFC822_SIZE = "RFC822.SIZE";
public static final String SEARCH = "SEARCH";
public static final String SELECT = "SELECT";
public static final String STARTTLS = "STARTTLS";
public static final String STATUS = "STATUS";
public static final String STORE = "STORE";
public static final String SUBSCRIBE = "SUBSCRIBE";
public static final String TRYCREATE = "TRYCREATE";
public static final String UID = "UID";
public static final String UIDNEXT = "UIDNEXT";
public static final String UIDVALIDITY = "UIDVALIDITY";
public static final String UNSEEN = "UNSEEN";
public static final String UNSUBSCRIBE = "UNSUBSCRIBE";
public static final String APPENDUID = "APPENDUID";
public static final String NIL = "NIL";
}

View File

@ -0,0 +1,99 @@
/*
* 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 representing "element"s in IMAP responses.
*
* <p>Class hierarchy:
* <pre>
* ImapElement
* |
* |-- ImapElement.NONE (for 'index out of range')
* |
* |-- ImapList (isList() == true)
* | |
* | |-- ImapList.EMPTY
* | |
* | --- ImapResponse
* |
* --- ImapString (isString() == true)
* |
* |-- ImapString.EMPTY
* |
* |-- ImapSimpleString
* |
* |-- ImapMemoryLiteral
* |
* --- ImapTempFileLiteral
* </pre>
*/
public abstract class ImapElement {
/**
* An element that is returned by {@link ImapList#getElementOrNone} to indicate an index
* is out of range.
*/
public static final ImapElement NONE = new ImapElement() {
@Override public boolean isList() {
return false;
}
@Override public boolean isString() {
return false;
}
@Override public String toString() {
return "[NO ELEMENT]";
}
@Override
public boolean equalsForTest(ImapElement that) {
return super.equalsForTest(that);
}
};
public abstract boolean isList();
public abstract boolean isString();
/**
* Clean up the resources used by the instance.
* It's for removing a temp file used by {@link ImapTempFileLiteral}.
*/
public void destroy() {
}
/**
* Return a string that represents this object; it's purely for the debug purpose. Don't
* mistake it for {@link ImapString#getString}.
*
* Abstract to force subclasses to implement it.
*/
@Override
public abstract String toString();
/**
* The equals implementation that is intended to be used only for unit testing.
* (Because it may be heavy and has a special sense of "equal" for testing.)
*/
public boolean equalsForTest(ImapElement that) {
if (that == null) {
return false;
}
return this.getClass() == that.getClass(); // Has to be the same class.
}
}

View File

@ -0,0 +1,226 @@
/*
* 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 java.util.ArrayList;
/**
* Class represents an IMAP list.
*/
public class ImapList extends ImapElement {
/**
* {@link ImapList} representing an empty list.
*/
public static final ImapList EMPTY = new ImapList() {
@Override void add(ImapElement e) {
throw new RuntimeException();
}
};
private final ArrayList<ImapElement> mList = new ArrayList<ImapElement>();
/* 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() {
for (ImapElement e : mList) {
e.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;
}
}

View File

@ -0,0 +1,66 @@
/*
* 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.Email;
import com.android.email.FixedLengthInputStream;
import com.android.email.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 final 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(Email.LOG_TAG, "");
}
}
@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);
}
}

View File

@ -0,0 +1,138 @@
/*
* 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 {@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;
}
}

View File

@ -0,0 +1,401 @@
/*
* 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.Email;
import com.android.email.FixedLengthInputStream;
import com.android.email.PeekableInputStream;
import com.android.email.mail.MessagingException;
import com.android.email.mail.transport.DiscourseLogger;
import com.android.email.mail.transport.LoggingInputStream;
import android.text.TextUtils;
import android.util.Config;
import android.util.Log;
import java.io.IOException;
import java.io.InputStream;
/**
* 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.
*/
private static final int LITERAL_KEEP_IN_MEMORY_THRESHOLD = 16 * 1024 * 1024;
/** Input stream */
private final PeekableInputStream mIn;
/**
* To log network activities when the parser crashes.
*
* <p>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();
/**
* 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 && Config.LOGD && Email.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 (Config.LOGD && Email.DEBUG) {
Log.d(Email.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;
}
/**
* Reads the next response available on the stream and returns an
* {@link ImapResponse} object that represents it.
*
* @return the parsed {@link ImapResponse} object.
* @exception ByeException when detects BYE.
*/
public ImapResponse readResponse() throws IOException, MessagingException {
final ImapResponse response;
try {
response = parseResponse();
if (Config.LOGD && Email.DEBUG) {
Log.d(Email.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(Email.LOG_TAG, ByeException.MESSAGE);
throw new ByeException();
}
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(Email.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, MessagingException {
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 {
final int ch = peek();
if (ch == '+') { // Continuation request
readByte(); // skip +
expect(' ');
ImapResponse response = new ImapResponse(null, true);
// If it's continuation request, we don't really care what's in it.
response.add(new ImapSimpleString(readUntilEol()));
return response;
}
// Status response or response data
final String tag;
if (ch == '*') {
tag = null;
readByte(); // skip *
expect(' ');
} else {
tag = readUntil(' ');
}
final ImapResponse response = new ImapResponse(tag, false);
final ImapString firstString = parseBareString();
response.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 (response.isStatusResponse()) { // It's a status response
// Is there a response code?
final int next = peek();
if (next == '[') {
response.add(parseList('[', ']'));
if (peek() == ' ') { // Skip following space
readByte();
}
}
String rest = readUntilEol();
if (!TextUtils.isEmpty(rest)) {
// The rest is free-form text.
response.add(new ImapSimpleString(rest));
}
} else { // It's a response data.
parseElements(response, '\0');
}
} else {
expect('\r');
expect('\n');
}
return response;
}
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);
}
}
}

View File

@ -0,0 +1,49 @@
/*
* 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.Utility;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
/**
* Subclass of {@link ImapString} used for non literals.
*/
public class ImapSimpleString extends ImapString {
private final String mString;
/* package */ ImapSimpleString(String string) {
mString = (string != null) ? string : "";
}
@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 + "\"";
}
}

View File

@ -0,0 +1,182 @@
/*
* 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.Email;
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 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(Email.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());
}
}

View File

@ -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 com.android.email.Email;
import com.android.email.FixedLengthInputStream;
import com.android.email.Utility;
import org.apache.commons.io.IOUtils;
import android.util.Log;
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 {
private boolean mDestroyed = false;
/* 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", Email.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();
}
/**
* Because we can't use File.deleteOnExit(), let finalizer clean up the temp files....
*
* TODO: Don't rely on this. Always explicitly call destroy().
*/
@Override
protected void finalize() throws Throwable {
try {
destroy();
} finally {
super.finalize();
}
}
private void checkNotDestroyed() {
if (mDestroyed) {
throw new RuntimeException("Already destroyed");
}
}
@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(Email.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 {
return Utility.fromAscii(IOUtils.toByteArray(getAsStream()));
} catch (IOException e) {
Log.w(Email.LOG_TAG, "ImapTempFileLiteral: Error while reading temp file");
return "";
}
}
@Override
public void destroy() {
try {
if (!mDestroyed && mFile.exists()) {
mFile.delete();
}
} finally {
mDestroyed = true;
}
}
@Override
public String toString() {
return String.format("{%d byte literal(file)}", mSize);
}
public boolean tempFileExistsForTest() {
return mFile.exists();
}
}

View File

@ -1,177 +0,0 @@
/*
* Copyright (C) 2009 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;
import com.android.email.FixedLengthInputStream;
import com.android.email.mail.MessagingException;
import com.android.email.mail.store.ImapResponseParser.ImapList;
import com.android.email.mail.store.ImapResponseParser.ImapResponse;
import com.android.email.mail.transport.DiscourseLogger;
import android.test.AndroidTestCase;
import android.test.suitebuilder.annotation.SmallTest;
import java.io.ByteArrayInputStream;
import java.io.IOException;
/**
* This is a series of unit tests for the ImapStore class. These tests must be locally
* complete - no server(s) required.
*/
@SmallTest
public class ImapResponseParserUnitTests extends AndroidTestCase {
// TODO more comprehensive test for parsing
/**
* Test for parsing literal string
*/
public void testParseLiteral() throws Exception {
ByteArrayInputStream is = new ByteArrayInputStream(
("* STATUS \"INBOX\" (UNSEEN 2)\r\n"
+ "100 OK STATUS completed\r\n"
+ "* STATUS {5}\r\n"
+ "INBOX (UNSEEN 10)\r\n"
+ "101 OK STATUS completed\r\n")
.getBytes());
ImapResponseParser parser = new ImapResponseParser(is, new DiscourseLogger(4));
ImapResponse line1 = parser.readResponse();
assertNull("Line 1 tag", line1.mTag);
assertTrue("Line 1 completed", line1.completed());
assertEquals("Line 1 count", 3, line1.size());
Object line1list = line1.get(2);
assertEquals("Line 1 list count", 2, ((ImapList)line1list).size());
ImapResponse line2 = parser.readResponse();
assertEquals("Line 2 tag", "100", line2.mTag);
assertTrue("Line 2 completed", line2.completed());
assertEquals("Line 2 count", 3, line2.size());
ImapResponse line3 = parser.readResponse();
assertNull("Line 3 tag", line3.mTag);
assertFalse("Line 3 completed", line3.completed());
assertEquals("Line 3 count", 2, line3.size());
assertEquals("Line 3 word 2 class", FixedLengthInputStream.class, line3.get(1).getClass());
line3.nailDown();
assertEquals("Line 3 word 2 nailed down", String.class, line3.get(1).getClass());
assertEquals("Line 3 word 2 value", "INBOX", line3.getString(1));
ImapResponse line4 = parser.readResponse();
assertEquals("Line 4 tag", "", line4.mTag);
assertTrue("Line 4 completed", line4.completed());
assertEquals("Line 4 count", 1, line4.size());
line3.appendAll(line4);
assertNull("Line 3-4 tag", line3.mTag);
assertTrue("Line 3-4 completed", line3.completed());
assertEquals("Line 3-4 count", 3, line3.size());
assertEquals("Line 3-4 word 3 class", ImapList.class, line3.get(2).getClass());
ImapResponse line5 = parser.readResponse();
assertEquals("Line 5 tag", "101", line5.mTag);
assertTrue("Line 5 completed", line5.completed());
assertEquals("Line 5 count", 3, line5.size());
}
/**
* Test for parsing expansion resp-text in OK or related responses
*/
public void testParseResponseText() throws Exception {
ByteArrayInputStream is = new ByteArrayInputStream(
("101 OK STATUS completed\r\n"
+ "102 OK [APPENDUID 2 238257] APPEND completed\r\n")
.getBytes());
ImapResponseParser parser = new ImapResponseParser(is, new DiscourseLogger(4));
ImapResponse line1 = parser.readResponse();
assertEquals("101", line1.mTag);
assertTrue(line1.completed());
assertEquals(3, line1.size()); // "OK STATUS COMPLETED"
ImapResponse line2 = parser.readResponse();
assertEquals("102", line2.mTag);
assertTrue(line2.completed());
assertEquals(4, line2.size()); // "OK [APPENDUID 2 238257] APPEND completed"
Object responseList = line2.get(1);
assertEquals(3, ((ImapList)responseList).size());
}
/**
* Test special parser of [ALERT] responses
*/
public void testAlertText() throws IOException {
ByteArrayInputStream is = new ByteArrayInputStream(
("* OK [AlErT] system going down\r\n"
+ "* OK [ALERT]\r\n"
+ "* OK [SOME-OTHER-TAG]\r\n")
.getBytes());
ImapResponseParser parser = new ImapResponseParser(is, new DiscourseLogger(4));
ImapResponse line1 = parser.readResponse();
assertEquals("system going down", line1.getAlertText());
ImapResponse line2 = parser.readResponse();
assertEquals("", line2.getAlertText());
ImapResponse line3 = parser.readResponse();
assertNull(line3.getAlertText());
}
/**
* Test basic ImapList functionality
* TODO: Add tests for keyed lists
*/
public void testImapList() throws MessagingException {
ByteArrayInputStream is = new ByteArrayInputStream("foo".getBytes());
ImapResponseParser parser = new ImapResponseParser(is, new DiscourseLogger(4));
ImapList list1 = parser.new ImapList();
list1.add("foo");
list1.add("bar");
list1.add("20");
list1.add(is);
list1.add("01-Jan-2009 11:20:39 -0800");
ImapList list2 = parser.new ImapList();
list2.add(list1);
// Test getString(), getStringOrNull(), getList(), getListOrNull, getNumber()
// getLiteral(), and getDate()
assertEquals("foo", list1.getString(0));
assertEquals("foo", list1.getStringOrNull(0));
assertNull(list1.getListOrNull(0));
assertEquals("bar", list1.getString(1));
assertEquals("bar", list1.getStringOrNull(1));
assertNull(list1.getListOrNull(1));
assertEquals("20", list1.getString(2));
assertEquals("20", list1.getStringOrNull(2));
assertEquals(20, list1.getNumber(2));
assertNull(list1.getStringOrNull(3));
assertNotNull(list1.getLiteral(3));
// getDate() is removed by proguard. (aparently it's not used.)
// assertNotNull(list1.getDate(4));
// Test getList() and getListOrNull() with list value
assertEquals(list1, list2.getList(0));
assertEquals(list1, list2.getListOrNull(0));
assertNull(list2.getListOrNull(20));
assertNull(list2.getStringOrNull(20));
}
}

View File

@ -47,6 +47,7 @@ import android.test.suitebuilder.annotation.SmallTest;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.regex.Pattern;
/**
* This is a series of unit tests for the ImapStore class. These tests must be locally
@ -58,6 +59,7 @@ import java.util.HashMap;
* TODO Check if callback is really called
* TODO test for BAD response in various places?
* TODO test for BYE response in various places?
* TODO test for case-insensitivity (e.g. replace FETCH -> FeTCH)
*/
@SmallTest
public class ImapStoreUnitTests extends AndroidTestCase {
@ -94,6 +96,17 @@ public class ImapStoreUnitTests extends AndroidTestCase {
mNextTag = 1;
}
public void testJoinMessageUids() throws Exception {
assertEquals("", ImapStore.joinMessageUids(new Message[] {}));
assertEquals("a", ImapStore.joinMessageUids(new Message[] {
mFolder.createMessage("a")
}));
assertEquals("a,XX", ImapStore.joinMessageUids(new Message[] {
mFolder.createMessage("a"),
mFolder.createMessage("XX"),
}));
}
/**
* Confirms simple non-SSL non-TLS login
*/
@ -124,7 +137,6 @@ public class ImapStoreUnitTests extends AndroidTestCase {
* TODO: Test with SSL required but not supported
* TODO: Test with TLS negotiation (faked)
* TODO: Test with TLS required but not supported
* TODO: Test calling getMessageCount(), getMessages(), etc.
*/
/**
@ -142,7 +154,8 @@ public class ImapStoreUnitTests extends AndroidTestCase {
// x-android-device-model Model (Optional, so not tested here)
// x-android-net-operator Carrier (Unreliable, so not tested here)
// AGUID A device+account UID
String id = mStore.getImapId(getContext(), "user-name", "host-name", "IMAP4rev1 STARTTLS");
String id = ImapStore.getImapId(getContext(),
"user-name", "host-name", "IMAP4rev1 STARTTLS");
HashMap<String, String> map = tokenizeImapId(id);
assertEquals(getContext().getPackageName(), map.get("name"));
assertEquals("android", map.get("os"));
@ -154,7 +167,7 @@ public class ImapStoreUnitTests extends AndroidTestCase {
// variants for release and non-release devices.
// simple API check - non-REL codename, non-empty version
id = mStore.makeCommonImapId("packageName", "version", "codeName",
id = ImapStore.makeCommonImapId("packageName", "version", "codeName",
"model", "id", "vendor", "network-operator");
map = tokenizeImapId(id);
assertEquals("packageName", map.get("name"));
@ -167,7 +180,7 @@ public class ImapStoreUnitTests extends AndroidTestCase {
// simple API check - codename is REL, so use model name.
// also test empty version => 1.0 and empty network operator
id = mStore.makeCommonImapId("packageName", "", "REL",
id = ImapStore.makeCommonImapId("packageName", "", "REL",
"model", "id", "vendor", "");
map = tokenizeImapId(id);
assertEquals("packageName", map.get("name"));
@ -188,7 +201,8 @@ public class ImapStoreUnitTests extends AndroidTestCase {
* The most important goal of the filters is to keep out control chars, (, ), and "
*/
public void testImapIdFiltering() {
String id = mStore.makeCommonImapId("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ",
String id = ImapStore.makeCommonImapId(
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ",
"0123456789", "codeName",
"model", "-_+=;:.,// ",
"v(e)n\"d\ro\nr", // look for bad chars stripped out, leaving OK chars
@ -212,9 +226,9 @@ public class ImapStoreUnitTests extends AndroidTestCase {
ImapStore store2 = (ImapStore) ImapStore.newInstance("imap://user2:password@server:999",
getContext(), null);
String id1a = mStore.getImapId(getContext(), "user1", "host-name", "IMAP4rev1");
String id1b = mStore.getImapId(getContext(), "user1", "host-name", "IMAP4rev1");
String id2 = mStore.getImapId(getContext(), "user2", "host-name", "IMAP4rev1");
String id1a = ImapStore.getImapId(getContext(), "user1", "host-name", "IMAP4rev1");
String id1b = ImapStore.getImapId(getContext(), "user1", "host-name", "IMAP4rev1");
String id2 = ImapStore.getImapId(getContext(), "user2", "host-name", "IMAP4rev1");
String uid1a = tokenizeImapId(id1a).get("AGUID");
String uid1b = tokenizeImapId(id1b).get("AGUID");
@ -286,14 +300,8 @@ public class ImapStoreUnitTests extends AndroidTestCase {
mFolder.open(OpenMode.READ_WRITE, null);
}
/**
* TODO: Test the operation of checkSettings()
* TODO: Test small Store & Folder functions that manage folders & namespace
*/
/**
* Test small Folder functions that don't really do anything in Imap
* TODO: Test all of the small Folder functions.
*/
public void testSmallFolderFunctions() throws MessagingException {
// getPermanentFlags() returns { Flag.DELETED, Flag.SEEN, Flag.FLAGGED }
@ -399,6 +407,10 @@ public class ImapStoreUnitTests extends AndroidTestCase {
FOLDER_ENCODED + " selected. (Success)"});
}
private void expectLogin(MockTransport mockTransport) {
expectLogin(mockTransport, new String[] {"* ID NIL", "OK"});
}
private void expectLogin(MockTransport mockTransport, String[] imapIdResponse) {
expectLogin(mockTransport, imapIdResponse, "OK user authenticated (Success)");
}
@ -425,6 +437,12 @@ public class ImapStoreUnitTests extends AndroidTestCase {
getNextTag(true) + " " + loginResponse);
}
private void expectNoop(MockTransport mockTransport, boolean ok) {
String response = ok ? " OK success" : " NO timeout";
mockTransport.expect(getNextTag(false) + " NOOP",
new String[] {getNextTag(true) + response});
}
/**
* Return a tag for use in setting up expect strings. Typically this is called in pairs,
* first as getNextTag(false) when emitting the command, then as getNextTag(true) when
@ -530,7 +548,10 @@ public class ImapStoreUnitTests extends AndroidTestCase {
// TODO: Test NO response.
}
public void testFetchBodyStructure() throws MessagingException {
/**
* Test for fetching simple BODYSTRUCTURE.
*/
public void testFetchBodyStructureSimple() throws Exception {
final MockTransport mock = openAndInjectMockTransport();
setupOpenFolder(mock);
mFolder.open(OpenMode.READ_WRITE, null);
@ -540,91 +561,155 @@ public class ImapStoreUnitTests extends AndroidTestCase {
fp.add(FetchProfile.Item.STRUCTURE);
mock.expect(getNextTag(false) + " UID FETCH 1 \\(UID BODYSTRUCTURE\\)",
new String[] {
"* 9 FETCH (UID 1 BODYSTRUCTURE ((\"TEXT\" \"PLAIN\" (\"CHARSET\" \"ISO-8859-1\")" +
" CID NIL \"7BIT\" 18 3 NIL NIL NIL)" +
"(\"IMAGE\" \"PNG\"" +
" (\"NAME\" \"device.png\") NIL NIL \"BASE64\" 117840 NIL (\"ATTACHMENT\"" +
"(\"FILENAME\" \"device.png\")) NIL)" +
"(\"TEXT\" \"HTML\"" +
" () NIL NIL \"7BIT\" 100 NIL NIL (\"ATTACHMENT\"" +
"(\"FILENAME\" \"attachment.html\" \"SIZE\" 555)) NIL)" +
"\"MIXED\" (\"BOUNDARY\" \"00032556278a7005e40486d159ca\") NIL NIL))",
"* 9 FETCH (UID 1 BODYSTRUCTURE (\"TEXT\" \"PLAIN\" NIL" +
" NIL NIL NIL 18 3 NIL NIL NIL))",
getNextTag(true) + " OK SUCCESS"
});
mFolder.fetch(new Message[] { message }, fp, null);
// Check mime structure...
Body body = message.getBody();
MoreAsserts.assertEquals(
new String[] {"text/plain"},
message.getHeader("Content-Type")
);
assertNull(message.getHeader("Content-Transfer-Encoding"));
assertNull(message.getHeader("Content-ID"));
MoreAsserts.assertEquals(
new String[] {";\n size=18"},
message.getHeader("Content-Disposition")
);
MoreAsserts.assertEquals(
new String[] {"TEXT"},
message.getHeader("X-Android-Attachment-StoreData")
);
// TODO: Test NO response.
}
/**
* Test for fetching complex muiltipart BODYSTRUCTURE.
*/
public void testFetchBodyStructureMultipart() throws Exception {
final MockTransport mock = openAndInjectMockTransport();
setupOpenFolder(mock);
mFolder.open(OpenMode.READ_WRITE, null);
final Message message = mFolder.createMessage("1");
final FetchProfile fp = new FetchProfile();
fp.add(FetchProfile.Item.STRUCTURE);
mock.expect(getNextTag(false) + " UID FETCH 1 \\(UID BODYSTRUCTURE\\)",
new String[] {
"* 9 FETCH (UID 1 BODYSTRUCTURE ((\"TEXT\" \"PLAIN\" () {20}",
"long content id#@!@#" +
" NIL \"7BIT\" 18 3 NIL NIL NIL)" +
"(\"IMAGE\" \"PNG\" (\"NAME\" {10}",
"device.png) NIL NIL \"BASE64\" {6}",
"117840 NIL (\"ATTACHMENT\" (\"FILENAME\" \"device.png\")) NIL)" +
"(\"TEXT\" \"HTML\" () NIL NIL \"7BIT\" 100 NIL 123 (\"ATTACHMENT\"" +
"(\"FILENAME\" {15}",
"attachment.html \"SIZE\" 555)) NIL)" +
"((\"TEXT\" \"HTML\" NIL NIL \"BASE64\")(\"XXX\" \"YYY\"))" + // Nested
"\"MIXED\" (\"BOUNDARY\" \"00032556278a7005e40486d159ca\") NIL NIL))",
getNextTag(true) + " OK SUCCESS"
});
mFolder.fetch(new Message[] { message }, fp, null);
// Check mime structure...
final Body body = message.getBody();
assertTrue(body instanceof MimeMultipart);
MimeMultipart mimeMultipart = (MimeMultipart) body;
assertEquals(3, mimeMultipart.getCount());
assertEquals(4, mimeMultipart.getCount());
assertEquals("mixed", mimeMultipart.getSubTypeForTest());
Part part0 = mimeMultipart.getBodyPart(0);
Part part1 = mimeMultipart.getBodyPart(1);
Part part2 = mimeMultipart.getBodyPart(2);
assertTrue(part0 instanceof MimeBodyPart);
final Part part1 = mimeMultipart.getBodyPart(0);
final Part part2 = mimeMultipart.getBodyPart(1);
final Part part3 = mimeMultipart.getBodyPart(2);
final Part part4 = mimeMultipart.getBodyPart(3);
assertTrue(part1 instanceof MimeBodyPart);
assertTrue(part2 instanceof MimeBodyPart);
assertTrue(part3 instanceof MimeBodyPart);
assertTrue(part4 instanceof MimeBodyPart);
MimeBodyPart mimePart0 = (MimeBodyPart) part0; // text/plain
MimeBodyPart mimePart1 = (MimeBodyPart) part1; // image/png
MimeBodyPart mimePart2 = (MimeBodyPart) part2; // text/html
final MimeBodyPart mimePart1 = (MimeBodyPart) part1; // text/plain
final MimeBodyPart mimePart2 = (MimeBodyPart) part2; // image/png
final MimeBodyPart mimePart3 = (MimeBodyPart) part3; // text/html
final MimeBodyPart mimePart4 = (MimeBodyPart) part4; // Nested
MoreAsserts.assertEquals(
new String[] {"text/plain;\n CHARSET=\"ISO-8859-1\""},
part0.getHeader("Content-Type")
new String[] {"1"},
part1.getHeader("X-Android-Attachment-StoreData")
);
MoreAsserts.assertEquals(
new String[] {"image/png;\n NAME=\"device.png\""},
new String[] {"2"},
part2.getHeader("X-Android-Attachment-StoreData")
);
MoreAsserts.assertEquals(
new String[] {"3"},
part3.getHeader("X-Android-Attachment-StoreData")
);
MoreAsserts.assertEquals(
new String[] {"text/plain"},
part1.getHeader("Content-Type")
);
MoreAsserts.assertEquals(
new String[] {"text/html"},
new String[] {"image/png;\n NAME=\"device.png\""},
part2.getHeader("Content-Type")
);
MoreAsserts.assertEquals(
new String[] {"text/html"},
part3.getHeader("Content-Type")
);
MoreAsserts.assertEquals(
new String[] {"CID"},
part0.getHeader("Content-ID")
);
assertNull(
new String[] {"long content id#@!@#"},
part1.getHeader("Content-ID")
);
assertNull(
part2.getHeader("Content-ID")
);
assertNull(part2.getHeader("Content-ID"));
assertNull(part3.getHeader("Content-ID"));
MoreAsserts.assertEquals(
new String[] {"7BIT"},
part0.getHeader("Content-Transfer-Encoding")
);
MoreAsserts.assertEquals(
new String[] {"BASE64"},
part1.getHeader("Content-Transfer-Encoding")
);
MoreAsserts.assertEquals(
new String[] {"7BIT"},
new String[] {"BASE64"},
part2.getHeader("Content-Transfer-Encoding")
);
MoreAsserts.assertEquals(
new String[] {"7BIT"},
part3.getHeader("Content-Transfer-Encoding")
);
MoreAsserts.assertEquals(
new String[] {";\n size=18"}, // TODO Is that right?
part0.getHeader("Content-Disposition")
);
MoreAsserts.assertEquals(
new String[] {"attachment;\n filename=\"device.png\";\n size=117840"},
new String[] {";\n size=18"},
part1.getHeader("Content-Disposition")
);
MoreAsserts.assertEquals(
new String[] {"attachment;\n filename=\"attachment.html\";\n size=\"555\""},
new String[] {"attachment;\n filename=\"device.png\";\n size=117840"},
part2.getHeader("Content-Disposition")
);
MoreAsserts.assertEquals(
new String[] {"attachment;\n filename=\"attachment.html\";\n size=\"555\""},
part3.getHeader("Content-Disposition")
);
// TODO Test for quote: If a filename contains ", it should become %22,
// which isn't implemented.
// Check the nested parts.
final Body part4body = part4.getBody();
assertTrue(part4body instanceof MimeMultipart);
MimeMultipart mimeMultipartPart4 = (MimeMultipart) part4body;
assertEquals(2, mimeMultipartPart4.getCount());
// TODO: Test NO response.
final MimeBodyPart mimePart41 = (MimeBodyPart) mimeMultipartPart4.getBodyPart(0);
final MimeBodyPart mimePart42 = (MimeBodyPart) mimeMultipartPart4.getBodyPart(1);
MoreAsserts.assertEquals(new String[] {"4.1"},
mimePart41.getHeader("X-Android-Attachment-StoreData")
);
MoreAsserts.assertEquals(new String[] {"4.2"},
mimePart42.getHeader("X-Android-Attachment-StoreData")
);
}
public void testFetchBodySane() throws MessagingException {
@ -906,7 +991,7 @@ public class ImapStoreUnitTests extends AndroidTestCase {
public void testGetPersonalNamespaces() throws Exception {
MockTransport mock = openAndInjectMockTransport();
expectLogin(mock, new String[] {"* ID NIL", "OK"});
expectLogin(mock);
mock.expect(getNextTag(false) + " LIST \"\" \"\\*\"",
new String[] {
@ -947,11 +1032,70 @@ public class ImapStoreUnitTests extends AndroidTestCase {
assertEquals("!\u65E5\u672C\u8A9E!", ImapStore.decodeFolderName("!&ZeVnLIqe-!"));
}
// TODO test folder open failure
public void testOpen() throws Exception {
MockTransport mock = openAndInjectMockTransport();
expectLogin(mock);
final Folder folder = mStore.getFolder("test");
// Not exist
mock.expect(getNextTag(false) + " SELECT \\\"test\\\"",
new String[] {
getNextTag(true) + " NO no such mailbox"
});
try {
folder.open(OpenMode.READ_WRITE, null);
fail();
} catch (MessagingException expected) {
}
// READ-WRITE
expectNoop(mock, true); // Need it because we reuse the connection.
mock.expect(getNextTag(false) + " SELECT \\\"test\\\"",
new String[] {
"* 1 EXISTS",
getNextTag(true) + " OK [READ-WRITE]"
});
folder.open(OpenMode.READ_WRITE, null);
assertTrue(folder.exists());
assertEquals(1, folder.getMessageCount());
assertEquals(OpenMode.READ_WRITE, folder.getMode());
assertTrue(folder.isOpen());
folder.close(false);
assertFalse(folder.isOpen());
// READ-ONLY
expectNoop(mock, true); // Need it because we reuse the connection.
mock.expect(getNextTag(false) + " SELECT \\\"test\\\"",
new String[] {
"* 2 EXISTS",
getNextTag(true) + " OK [READ-ONLY]"
});
folder.open(OpenMode.READ_WRITE, null);
assertTrue(folder.exists());
assertEquals(2, folder.getMessageCount());
assertEquals(OpenMode.READ_ONLY, folder.getMode());
// Try to re-open as read-write. Should send SELECT again.
expectNoop(mock, true); // Need it because we reuse the connection.
mock.expect(getNextTag(false) + " SELECT \\\"test\\\"",
new String[] {
"* 15 EXISTS",
getNextTag(true) + " OK selected"
});
folder.open(OpenMode.READ_WRITE, null);
assertTrue(folder.exists());
assertEquals(15, folder.getMessageCount());
assertEquals(OpenMode.READ_WRITE, folder.getMode());
}
public void testExists() throws Exception {
MockTransport mock = openAndInjectMockTransport();
expectLogin(mock, new String[] {"* ID NIL", "OK"});
expectLogin(mock);
// Folder exists
Folder folder = mStore.getFolder("\u65E5\u672C\u8A9E");
@ -964,10 +1108,7 @@ public class ImapStoreUnitTests extends AndroidTestCase {
assertTrue(folder.exists());
// Connection verification
mock.expect(getNextTag(false) + " NOOP",
new String[] {
getNextTag(true) + " OK success"
});
expectNoop(mock, true);
// Doesn't exist
folder = mStore.getFolder("no such folder");
@ -981,7 +1122,7 @@ public class ImapStoreUnitTests extends AndroidTestCase {
public void testCreate() throws Exception {
MockTransport mock = openAndInjectMockTransport();
expectLogin(mock, new String[] {"* ID NIL", "OK"});
expectLogin(mock);
// Success
Folder folder = mStore.getFolder("\u65E5\u672C\u8A9E");
@ -996,10 +1137,7 @@ public class ImapStoreUnitTests extends AndroidTestCase {
assertTrue(folder.create(FolderType.HOLDS_MESSAGES));
// Connection verification
mock.expect(getNextTag(false) + " NOOP",
new String[] {
getNextTag(true) + " OK success"
});
expectNoop(mock, true);
// Failure
mock.expect(getNextTag(false) + " CREATE \\\"&ZeVnLIqe-\\\"",
@ -1198,7 +1336,7 @@ public class ImapStoreUnitTests extends AndroidTestCase {
assertFalse(con1.isTransportOpenForTest()); // Transport not open yet.
// Open con1
expectLogin(mock, new String[] {"* ID NIL", "OK"});
expectLogin(mock);
con1.open();
assertTrue(con1.isTransportOpenForTest());
@ -1212,7 +1350,7 @@ public class ImapStoreUnitTests extends AndroidTestCase {
assertNotSame(con1, con2);
// Open con2
expectLogin(mock, new String[] {"* ID NIL", "OK"});
expectLogin(mock);
con2.open();
assertTrue(con1.isTransportOpenForTest());
@ -1244,4 +1382,136 @@ public class ImapStoreUnitTests extends AndroidTestCase {
assertNotSame(con1, con3);
assertNotSame(con2, con3);
}
public void testCheckSettings() throws Exception {
MockTransport mock = openAndInjectMockTransport();
expectLogin(mock);
mStore.checkSettings();
expectLogin(mock, new String[] {"* ID NIL", "OK"}, "NO authentication failed");
try {
mStore.checkSettings();
fail();
} catch (MessagingException expected) {
}
}
// Compatibility tests...
/**
* Getting an ALERT with a % mark in the message, which crashed the old parser.
*/
public void testQuotaAlert() throws Exception {
MockTransport mock = openAndInjectMockTransport();
expectLogin(mock);
// Success
Folder folder = mStore.getFolder("INBOX");
// The following response was copied from an actual bug...
mock.expect(getNextTag(false) + " SELECT \"INBOX\"", new String[] {
"* FLAGS (\\Answered \\Flagged \\Draft \\Deleted \\Seen NonJunk $Forwarded Junk" +
" $Label4 $Label1 $Label2 $Label3 $Label5 $MDNSent Old)",
"* OK [PERMANENTFLAGS (\\Answered \\Flagged \\Draft \\Deleted \\Seen NonJunk" +
" $Forwarded Junk $Label4 $Label1 $Label2 $Label3 $Label5 $MDNSent Old \\*)]",
"* 6406 EXISTS",
"* 0 RECENT",
"* OK [UNSEEN 5338]",
"* OK [UIDVALIDITY 1055957975]",
"* OK [UIDNEXT 449625]",
"* NO [ALERT] Mailbox is at 98% of quota",
getNextTag(true) + " OK [READ-WRITE] Completed"});
folder.open(OpenMode.READ_WRITE, null); // shouldn't crash.
assertEquals(6406, folder.getMessageCount());
}
/**
* Apparently some servers send a size in the wrong format. e.g. 123E
*/
public void testFetchBodyStructureMalformed() throws Exception {
final MockTransport mock = openAndInjectMockTransport();
setupOpenFolder(mock);
mFolder.open(OpenMode.READ_WRITE, null);
final Message message = mFolder.createMessage("1");
final FetchProfile fp = new FetchProfile();
fp.add(FetchProfile.Item.STRUCTURE);
mock.expect(getNextTag(false) + " UID FETCH 1 \\(UID BODYSTRUCTURE\\)",
new String[] {
"* 9 FETCH (UID 1 BODYSTRUCTURE (\"TEXT\" \"PLAIN\" ()" +
" NIL NIL NIL 123E 3))", // 123E isn't a number!
getNextTag(true) + " OK SUCCESS"
});
mFolder.fetch(new Message[] { message }, fp, null);
// Check mime structure...
MoreAsserts.assertEquals(
new String[] {"text/plain"},
message.getHeader("Content-Type")
);
assertNull(message.getHeader("Content-Transfer-Encoding"));
assertNull(message.getHeader("Content-ID"));
// Doesn't have size=xxx
assertNull(message.getHeader("Content-Disposition"));
}
/**
* Folder name with special chars in it.
*
* Gmail puts the folder name in the OK response, which crashed the old parser if there's a
* special char in the folder name.
*/
public void testFolderNameWithSpecialChars() throws Exception {
final String FOLDER_1 = "@u88**%_St";
final String FOLDER_1_QUOTED = Pattern.quote(FOLDER_1);
final String FOLDER_2 = "folder test_06";
MockTransport mock = openAndInjectMockTransport();
expectLogin(mock);
// List folders.
mock.expect(getNextTag(false) + " LIST \"\" \"\\*\"",
new String[] {
"* LIST () \"/\" \"" + FOLDER_1 + "\"",
"* LIST () \"/\" \"" + FOLDER_2 + "\"",
getNextTag(true) + " OK SUCCESS"
});
final Folder[] folders = mStore.getPersonalNamespaces();
ArrayList<String> list = new ArrayList<String>();
for (Folder f : folders) {
list.add(f.getName());
}
MoreAsserts.assertEquals(
new String[] {FOLDER_1, FOLDER_2, "INBOX"},
list.toArray(new String[0])
);
// Try to open the folders.
expectNoop(mock, true);
mock.expect(getNextTag(false) + " SELECT \"" + FOLDER_1_QUOTED + "\"", new String[] {
"* FLAGS (\\Answered \\Flagged \\Draft \\Deleted \\Seen)",
"* OK [PERMANENTFLAGS (\\Answered \\Flagged \\Draft \\Deleted \\Seen \\*)]",
"* 0 EXISTS",
"* 0 RECENT",
"* OK [UNSEEN 0]",
"* OK [UIDNEXT 1]",
getNextTag(true) + " OK [READ-WRITE] " + FOLDER_1});
folders[0].open(OpenMode.READ_WRITE, null);
folders[0].close(false);
expectNoop(mock, true);
mock.expect(getNextTag(false) + " SELECT \"" + FOLDER_2 + "\"", new String[] {
"* FLAGS (\\Answered \\Flagged \\Draft \\Deleted \\Seen)",
"* OK [PERMANENTFLAGS (\\Answered \\Flagged \\Draft \\Deleted \\Seen \\*)]",
"* 0 EXISTS",
"* 0 RECENT",
"* OK [UNSEEN 0]",
"* OK [UIDNEXT 1]",
getNextTag(true) + " OK [READ-WRITE] " + FOLDER_2});
folders[1].open(OpenMode.READ_WRITE, null);
folders[1].close(false);
}
}

View File

@ -0,0 +1,38 @@
/*
* 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.mail.store.imap.ImapElement;
import android.test.suitebuilder.annotation.SmallTest;
import junit.framework.TestCase;
@SmallTest
public class ImapElementTest extends TestCase {
/** Test for {@link ImapElement#NONE} */
public void testNone() {
assertFalse(ImapElement.NONE.isList());
assertFalse(ImapElement.NONE.isString());
assertTrue(ImapElement.NONE.equalsForTest(ImapElement.NONE));
assertFalse(ImapElement.NONE.equalsForTest(null));
assertFalse(ImapElement.NONE.equalsForTest(ImapTestUtils.STRING_1));
assertFalse(ImapElement.NONE.equalsForTest(ImapTestUtils.LIST_1));
}
}

View File

@ -0,0 +1,218 @@
/*
* 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 static com.android.email.mail.store.imap.ImapTestUtils.*;
import com.android.email.mail.store.imap.ImapElement;
import com.android.email.mail.store.imap.ImapList;
import com.android.email.mail.store.imap.ImapSimpleString;
import com.android.email.mail.store.imap.ImapString;
import android.test.suitebuilder.annotation.SmallTest;
import junit.framework.TestCase;
@SmallTest
public class ImapListTest extends TestCase {
/**
* Test for small functions. (isList, isString, isEmpty and size)
*/
public void testBasics() {
ImapList list = new ImapList();
assertTrue(list.isList());
assertFalse(list.isString());
assertTrue(list.isEmpty());
assertEquals(0, list.size());
list.add(STRING_1);
assertFalse(list.isEmpty());
assertEquals(1, list.size());
list.add(STRING_2);
assertEquals(2, list.size());
list.add(LIST_1);
assertEquals(3, list.size());
}
/**
* Test for {@link ImapList#EMPTY}.
*/
public void testEmpty() {
assertTrue(ImapList.EMPTY.isEmpty());
}
public void testIs() {
final ImapString ABC = new ImapSimpleString("AbC");
ImapList list = buildList(ImapList.EMPTY, ABC, LIST_1, ImapString.EMPTY);
assertFalse(list.is(0, "abc"));
assertFalse(list.is(1, "ab"));
assertTrue (list.is(1, "abc"));
assertFalse(list.is(2, "abc"));
assertFalse(list.is(3, "abc"));
assertFalse(list.is(4, "abc"));
assertFalse(list.is(0, "ab", false));
assertFalse(list.is(1, "ab", false));
assertTrue (list.is(1, "abc", false));
assertFalse(list.is(2, "ab", false));
assertFalse(list.is(3, "ab", false));
assertFalse(list.is(4, "ab", false));
assertFalse(list.is(0, "ab", true));
assertTrue (list.is(1, "ab", true));
assertTrue (list.is(1, "abc", true));
assertFalse(list.is(2, "ab", true));
assertFalse(list.is(3, "ab", true));
assertFalse(list.is(4, "ab", true));
// Make sure null is okay
assertFalse(list.is(0, null, false));
// Make sure won't crash with empty list
assertFalse(ImapList.EMPTY.is(0, "abc"));
}
public void testGetElementOrNone() {
ImapList list = buildList(ImapList.EMPTY, STRING_1, LIST_1, ImapString.EMPTY);
assertElement(ImapList.EMPTY, list.getElementOrNone(0));
assertElement(STRING_1, list.getElementOrNone(1));
assertElement(LIST_1, list.getElementOrNone(2));
assertElement(ImapString.EMPTY, list.getElementOrNone(3));
assertElement(ImapElement.NONE, list.getElementOrNone(4)); // Out of index.
// Make sure won't crash with empty list
assertElement(ImapElement.NONE, ImapList.EMPTY.getElementOrNone(0));
}
public void testGetListOrEmpty() {
ImapList list = buildList(ImapList.EMPTY, STRING_1, LIST_1, ImapString.EMPTY);
assertElement(ImapList.EMPTY, list.getListOrEmpty(0));
assertElement(ImapList.EMPTY, list.getListOrEmpty(1));
assertElement(LIST_1, list.getListOrEmpty(2));
assertElement(ImapList.EMPTY, list.getListOrEmpty(3));
assertElement(ImapList.EMPTY, list.getListOrEmpty(4)); // Out of index.
// Make sure won't crash with empty list
assertElement(ImapList.EMPTY, ImapList.EMPTY.getListOrEmpty(0));
}
public void testGetStringOrEmpty() {
ImapList list = buildList(ImapList.EMPTY, STRING_1, LIST_1, ImapString.EMPTY);
assertElement(ImapString.EMPTY, list.getStringOrEmpty(0));
assertElement(STRING_1, list.getStringOrEmpty(1));
assertElement(ImapString.EMPTY, list.getStringOrEmpty(2));
assertElement(ImapString.EMPTY, list.getStringOrEmpty(3));
assertElement(ImapString.EMPTY, list.getStringOrEmpty(4)); // Out of index.
// Make sure won't crash with empty list
assertElement(ImapString.EMPTY, ImapList.EMPTY.getStringOrEmpty(0));
}
public void testGetKeyedElementOrNull() {
final ImapString K1 = new ImapSimpleString("aBCd");
final ImapString K2 = new ImapSimpleString("Def");
final ImapString K3 = new ImapSimpleString("abC");
ImapList list = buildList(
K1, STRING_1,
K2, K3,
K3, STRING_2);
assertElement(null, list.getKeyedElementOrNull("ab", false));
assertElement(STRING_1, list.getKeyedElementOrNull("abcd", false));
assertElement(K3, list.getKeyedElementOrNull("def", false));
assertElement(STRING_2, list.getKeyedElementOrNull("abc", false));
assertElement(STRING_1, list.getKeyedElementOrNull("ab", true));
assertElement(STRING_1, list.getKeyedElementOrNull("abcd", true));
assertElement(K3, list.getKeyedElementOrNull("def", true));
assertElement(STRING_1, list.getKeyedElementOrNull("abc", true));
// Make sure null is okay
assertElement(null, list.getKeyedElementOrNull(null, false));
// Make sure won't crash with empty list
assertNull(ImapList.EMPTY.getKeyedElementOrNull("ab", false));
// Shouldn't crash with a list with an odd number of elements.
assertElement(null, buildList(K1).getKeyedElementOrNull("abcd", false));
}
public void getKeyedListOrEmpty() {
final ImapString K1 = new ImapSimpleString("Key");
ImapList list = buildList(K1, LIST_1);
assertElement(LIST_1, list.getKeyedListOrEmpty("key", false));
assertElement(LIST_1, list.getKeyedListOrEmpty("key", true));
assertElement(ImapList.EMPTY, list.getKeyedListOrEmpty("ke", false));
assertElement(LIST_1, list.getKeyedListOrEmpty("ke", true));
assertElement(ImapList.EMPTY, list.getKeyedListOrEmpty("ke"));
assertElement(LIST_1, list.getKeyedListOrEmpty("key"));
}
public void getKeyedStringOrEmpty() {
final ImapString K1 = new ImapSimpleString("Key");
ImapList list = buildList(K1, STRING_1);
assertElement(STRING_1, list.getKeyedListOrEmpty("key", false));
assertElement(STRING_1, list.getKeyedListOrEmpty("key", true));
assertElement(ImapString.EMPTY, list.getKeyedListOrEmpty("ke", false));
assertElement(STRING_1, list.getKeyedListOrEmpty("ke", true));
assertElement(ImapString.EMPTY, list.getKeyedListOrEmpty("ke"));
assertElement(STRING_1, list.getKeyedListOrEmpty("key"));
}
public void testContains() {
final ImapString K1 = new ImapSimpleString("aBCd");
final ImapString K2 = new ImapSimpleString("Def");
final ImapString K3 = new ImapSimpleString("abC");
ImapList list = buildList(K1, K2, K3);
assertTrue(list.contains("abc"));
assertTrue(list.contains("abcd"));
assertTrue(list.contains("def"));
assertFalse(list.contains(""));
assertFalse(list.contains("a"));
assertFalse(list.contains(null));
// Make sure null is okay
assertFalse(list.contains(null));
// Make sure won't crash with empty list
assertFalse(ImapList.EMPTY.contains(null));
}
public void testFlatten() {
assertEquals("[]", ImapList.EMPTY.flatten());
assertEquals("[aBc]", buildList(STRING_1).flatten());
assertEquals("[[]]", buildList(ImapList.EMPTY).flatten());
assertEquals("[aBc,[,X y z],aBc]",
buildList(STRING_1, buildList(ImapString.EMPTY, STRING_2), STRING_1).flatten());
}
}

View File

@ -0,0 +1,401 @@
/*
* 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 static com.android.email.mail.store.imap.ImapTestUtils.assertElement;
import static com.android.email.mail.store.imap.ImapTestUtils.buildList;
import static com.android.email.mail.store.imap.ImapTestUtils.buildResponse;
import static com.android.email.mail.store.imap.ImapTestUtils.createFixedLengthInputStream;
import com.android.email.Email;
import com.android.email.Utility;
import com.android.email.mail.MessagingException;
import com.android.email.mail.store.imap.ImapMemoryLiteral;
import com.android.email.mail.store.imap.ImapResponse;
import com.android.email.mail.store.imap.ImapResponseParser;
import com.android.email.mail.store.imap.ImapSimpleString;
import com.android.email.mail.store.imap.ImapString;
import com.android.email.mail.store.imap.ImapTempFileLiteral;
import com.android.email.mail.store.imap.ImapResponseParser.ByeException;
import com.android.email.mail.transport.DiscourseLogger;
import android.test.AndroidTestCase;
import android.test.suitebuilder.annotation.SmallTest;
import java.io.ByteArrayInputStream;
import java.io.IOException;
@SmallTest
public class ImapResponseParserTest extends AndroidTestCase {
private static ImapResponseParser generateParser(int literalKeepInMemoryThreshold,
String responses) {
return new ImapResponseParser(new ByteArrayInputStream(Utility.toAscii(responses)),
new DiscourseLogger(4), literalKeepInMemoryThreshold);
}
@Override
protected void setUp() throws Exception {
super.setUp();
Email.setTempDirectory(getContext());
}
public void testExpect() throws Exception {
final ImapResponseParser p = generateParser(100000, "abc");
p.expect('a');
p.expect('b');
try {
p.expect('C');
fail();
} catch (IOException e) {
// OK
}
}
public void testreadUntil() throws Exception {
final ImapResponseParser p = generateParser(100000, "!ab!c!!def!");
assertEquals("", p.readUntil('!'));
assertEquals("ab", p.readUntil('!'));
assertEquals("c", p.readUntil('!'));
assertEquals("", p.readUntil('!'));
assertEquals("def", p.readUntil('!'));
}
public void testBasic() throws Exception {
ImapResponse r;
final ImapResponseParser p = generateParser(100000,
"* STATUS \"INBOX\" (UNSEEN 2)\r\n" +
"100 OK STATUS completed\r\n" +
"+ continuation request+(\r\n" +
"* STATUS {5}\r\n" +
"IN%OX (UNSEEN 10) \"a b c\"\r\n" +
"101 OK STATUS completed %!(\r\n" +
"102 OK 1\r\n" +
"* 1 FETCH\r\n" +
"103 OK\r\n" + // shortest OK
"* a\r\n" // shortest response
);
r = p.readResponse();
assertElement(buildResponse(null, false,
new ImapSimpleString("STATUS"),
new ImapSimpleString("INBOX"),
buildList(
new ImapSimpleString("UNSEEN"),
new ImapSimpleString("2")
)
), r);
r = p.readResponse();
assertElement(buildResponse("100", false,
new ImapSimpleString("OK"),
new ImapSimpleString("STATUS completed") // one string
), r);
r = p.readResponse();
assertElement(buildResponse(null, true,
new ImapSimpleString("continuation request+(") // one string
), r);
r = p.readResponse();
assertElement(buildResponse(null, false,
new ImapSimpleString("STATUS"),
new ImapMemoryLiteral(createFixedLengthInputStream("IN%OX")),
buildList(
new ImapSimpleString("UNSEEN"),
new ImapSimpleString("10")
),
new ImapSimpleString("a b c")
), r);
r = p.readResponse();
assertElement(buildResponse("101", false,
new ImapSimpleString("OK"),
new ImapSimpleString("STATUS completed %!(") // one string
), r);
r = p.readResponse();
assertElement(buildResponse("102", false,
new ImapSimpleString("OK"),
new ImapSimpleString("1")
), r);
r = p.readResponse();
assertElement(buildResponse(null, false,
new ImapSimpleString("1"),
new ImapSimpleString("FETCH")
), r);
r = p.readResponse();
assertElement(buildResponse("103", false,
new ImapSimpleString("OK")
), r);
r = p.readResponse();
assertElement(buildResponse(null, false,
new ImapSimpleString("a")
), r);
}
public void testNil() throws Exception {
ImapResponse r;
final ImapResponseParser p = generateParser(100000,
"* nil nil NIL \"NIL\" {3}\r\n" +
"NIL\r\n"
);
r = p.readResponse();
assertElement(buildResponse(null, false,
ImapString.EMPTY,
ImapString.EMPTY,
ImapString.EMPTY,
new ImapSimpleString("NIL"),
new ImapMemoryLiteral(createFixedLengthInputStream("NIL"))
), r);
}
public void testBareLf() throws Exception {
ImapResponse r;
// Threshold = 3 bytes: use in memory literal.
ImapResponseParser p = generateParser(3,
"* a b\n" + // Bare LF -- should be treated like CRLF
"* x y\r\n"
);
r = p.readResponse();
assertElement(buildResponse(null, false,
new ImapSimpleString("a"),
new ImapSimpleString("b")
), r);
r = p.readResponse();
assertElement(buildResponse(null, false,
new ImapSimpleString("x"),
new ImapSimpleString("y")
), r);
}
public void testLiteral() throws Exception {
ImapResponse r;
// Threshold = 3 bytes: use in memory literal.
ImapResponseParser p = generateParser(3,
"* test {3}\r\n" +
"ABC\r\n"
);
r = p.readResponse();
assertElement(buildResponse(null, false,
new ImapSimpleString("test"),
new ImapMemoryLiteral(createFixedLengthInputStream("ABC"))
), r);
// Threshold = 2 bytes: use temp file literal.
p = generateParser(2,
"* test {3}\r\n" +
"ABC\r\n"
);
r = p.readResponse();
assertElement(buildResponse(null, false,
new ImapSimpleString("test"),
new ImapTempFileLiteral(createFixedLengthInputStream("ABC"))
), r);
// 2 literals in a line
p = generateParser(0,
"* test {3}\r\n" +
"ABC {4}\r\n" +
"wxyz\r\n"
);
r = p.readResponse();
assertElement(buildResponse(null, false,
new ImapSimpleString("test"),
new ImapTempFileLiteral(createFixedLengthInputStream("ABC")),
new ImapTempFileLiteral(createFixedLengthInputStream("wxyz"))
), r);
}
public void testAlert() throws Exception {
ImapResponse r;
final ImapResponseParser p = generateParser(100000,
"* OK [ALERT]\r\n" + // No message
"* OK [ALERT] alert ( message ) %*\r\n" +
"* OK [ABC] not alert\r\n"
);
r = p.readResponse();
assertTrue(r.isOk());
assertTrue(r.getAlertTextOrEmpty().isEmpty());
r = p.readResponse();
assertTrue(r.isOk());
assertEquals("alert ( message ) %*", r.getAlertTextOrEmpty().getString());
r = p.readResponse();
assertTrue(r.isOk());
assertTrue(r.getAlertTextOrEmpty().isEmpty());
}
/**
* If a [ appears in the middle of a string, the following string until the next ']' will
* be considered a part of the string.
*/
public void testBracket() throws Exception {
ImapResponse r;
final ImapResponseParser p = generateParser(100000,
"* AAA BODY[HEADER.FIELDS (\"DATE\" \"SUBJECT\")]\r\n" +
"* BBB B[a b c]d e f\r\n"
);
r = p.readResponse();
assertEquals("BODY[HEADER.FIELDS (\"DATE\" \"SUBJECT\")]",
r.getStringOrEmpty(1).getString());
r = p.readResponse();
assertEquals("B[a b c]d", r.getStringOrEmpty(1).getString());
}
public void testNest() throws Exception {
ImapResponse r;
final ImapResponseParser p = generateParser(100000,
"* A (a B () DEF) (a (ab)) ((() ())) ((a) ab) ((x y ZZ) () [] [A B] (A B C))" +
" ([abc] a[abc])\r\n"
);
r = p.readResponse();
assertElement(buildResponse(null, false,
new ImapSimpleString("A"),
buildList(
new ImapSimpleString("a"),
new ImapSimpleString("B"),
buildList(),
new ImapSimpleString("DEF")
),
buildList(
new ImapSimpleString("a"),
buildList(
new ImapSimpleString("ab")
)
),
buildList(
buildList(
buildList(),
buildList()
)
),
buildList(
buildList(
new ImapSimpleString("a")
),
new ImapSimpleString("ab")
),
buildList(
buildList(
new ImapSimpleString("x"),
new ImapSimpleString("y"),
new ImapSimpleString("ZZ")
),
buildList(),
buildList(),
buildList(
new ImapSimpleString("A"),
new ImapSimpleString("B")
),
buildList(
new ImapSimpleString("A"),
new ImapSimpleString("B"),
new ImapSimpleString("C")
)
),
buildList(
buildList(
new ImapSimpleString("abc")
),
new ImapSimpleString("a[abc]")
)
), r);
}
/**
* Parser shouldn't crash for any response. Should just throw IO/MessagingException.
*/
public void testMalformedResponse() throws Exception {
expectMessagingException("");
expectMessagingException("\r");
expectMessagingException("\r\n");
expectMessagingException("*\r\n");
expectMessagingException("1\r\n");
expectMessagingException("* \r\n");
expectMessagingException("1 \r\n");
expectMessagingException("* A (\r\n");
expectMessagingException("* A )\r\n");
expectMessagingException("* A (()\r\n");
expectMessagingException("* A ())\r\n");
expectMessagingException("* A [\r\n");
expectMessagingException("* A ]\r\n");
expectMessagingException("* A [[]\r\n");
expectMessagingException("* A []]\r\n");
expectMessagingException("* A ([)]\r\n");
expectMessagingException("* A");
expectMessagingException("* {3}");
expectMessagingException("* {3}\r\nab");
}
private static void expectMessagingException(String response) throws Exception {
final ImapResponseParser p = generateParser(100000, response);
try {
p.readResponse();
fail("Didn't throw Exception: response='" + response + "'");
} catch (MessagingException ok) {
return;
} catch (IOException ok) {
return;
}
}
// Compatibility tests...
/**
* OK response with a long message that contains special chars. (including tabs)
*/
public void testOkWithLongMessage() throws Exception {
ImapResponse r;
final ImapResponseParser p = generateParser(100000,
"* OK [CAPABILITY IMAP4 IMAP4rev1 LITERAL+ ID STARTTLS AUTH=PLAIN AUTH=LOGIN" +
"AUTH=CRAM-MD5] server.domain.tld\tCyrus IMAP4 v2.3.8-OS X Server 10.5:"
+" \t\t\t9F33 server ready %%\r\n");
assertTrue(p.readResponse().isOk());
}
/** Make sure literals and strings are interchangeable. */
public void testLiteralStringConversion() throws Exception {
ImapResponse r;
final ImapResponseParser p = generateParser(100000,
"* XXX {5}\r\n" +
"a b c\r\n");
assertEquals("a b c", p.readResponse().getStringOrEmpty(1).getString());
}
public void testByeReceived() throws Exception {
final ImapResponseParser p = generateParser(100000,
"* BYE Autologout timer; idle for too long\r\n");
try {
p.readResponse();
fail("Didn't throw ByeException");
} catch (ByeException ok) {
}
}
}

View File

@ -0,0 +1,142 @@
/*
* 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 static com.android.email.mail.store.imap.ImapTestUtils.*;
import com.android.email.mail.store.imap.ImapConstants;
import com.android.email.mail.store.imap.ImapResponse;
import com.android.email.mail.store.imap.ImapSimpleString;
import android.test.suitebuilder.annotation.SmallTest;
import junit.framework.TestCase;
@SmallTest
public class ImapResponseTest extends TestCase {
public void testIsTagged() {
assertTrue(buildResponse("a", false).isTagged());
assertFalse(buildResponse(null, false).isTagged());
}
public void testIsOk() {
assertTrue(buildResponse(null, false, new ImapSimpleString("OK")).isOk());
assertFalse(buildResponse(null, false, new ImapSimpleString("NO")).isOk());
}
public void testIsDataResponse() {
final ImapResponse OK = buildResponse("tag", false, new ImapSimpleString("OK"));
final ImapResponse SEARCH = buildResponse(null, false, new ImapSimpleString("SEARCH"),
new ImapSimpleString("1"));
final ImapResponse EXISTS = buildResponse(null, false, new ImapSimpleString("3"),
new ImapSimpleString("EXISTS"));
final ImapResponse TAGGED_EXISTS = buildResponse("tag", false, new ImapSimpleString("1"),
new ImapSimpleString("EXISTS"));
assertTrue(SEARCH.isDataResponse(0, ImapConstants.SEARCH));
assertTrue(EXISTS.isDataResponse(1, ImapConstants.EXISTS));
// Falses...
assertFalse(SEARCH.isDataResponse(1, ImapConstants.SEARCH));
assertFalse(EXISTS.isDataResponse(0, ImapConstants.EXISTS));
assertFalse(EXISTS.isDataResponse(1, ImapConstants.FETCH));
// It's tagged, so can't be a data response
assertFalse(TAGGED_EXISTS.isDataResponse(1, ImapConstants.EXISTS));
}
public void testGetResponseCodeOrEmpty() {
assertEquals(
"rescode",
buildResponse("tag", false,
new ImapSimpleString("OK"),
buildList(new ImapSimpleString("rescode"))
).getResponseCodeOrEmpty().getString()
);
assertEquals(
"",
buildResponse("tag", false,
new ImapSimpleString("STATUS"), // Not a status response
buildList(new ImapSimpleString("rescode"))
).getResponseCodeOrEmpty().getString()
);
assertEquals(
"",
buildResponse("tag", false,
new ImapSimpleString("OK"),
new ImapSimpleString("XXX"), // Second element not a list.
buildList(new ImapSimpleString("rescode"))
).getResponseCodeOrEmpty().getString()
);
}
public void testGetAlertTextOrEmpty() {
assertEquals(
"alert text",
buildResponse("tag", false,
new ImapSimpleString("OK"),
buildList(new ImapSimpleString("ALERT")),
new ImapSimpleString("alert text")
).getAlertTextOrEmpty().getString()
);
// Not alert
assertEquals(
"",
buildResponse("tag", false,
new ImapSimpleString("OK"),
buildList(new ImapSimpleString("X")),
new ImapSimpleString("alert text")
).getAlertTextOrEmpty().getString()
);
}
public void testGetStatusResponseTextOrEmpty() {
// Not a status response
assertEquals(
"",
buildResponse("tag", false,
new ImapSimpleString("XXX"),
new ImapSimpleString("!text!")
).getStatusResponseTextOrEmpty().getString()
);
// Second element isn't a list.
assertEquals(
"!text!",
buildResponse("tag", false,
new ImapSimpleString("OK"),
new ImapSimpleString("!text!")
).getStatusResponseTextOrEmpty().getString()
);
// Second element is a list.
assertEquals(
"!text!",
buildResponse("tag", false,
new ImapSimpleString("OK"),
buildList(new ImapSimpleString("XXX")),
new ImapSimpleString("!text!")
).getStatusResponseTextOrEmpty().getString()
);
}
}

View File

@ -0,0 +1,153 @@
/*
* 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 static com.android.email.mail.store.imap.ImapTestUtils.*;
import com.android.email.Email;
import com.android.email.Utility;
import com.android.email.mail.store.imap.ImapMemoryLiteral;
import com.android.email.mail.store.imap.ImapSimpleString;
import com.android.email.mail.store.imap.ImapString;
import com.android.email.mail.store.imap.ImapTempFileLiteral;
import org.apache.commons.io.IOUtils;
import android.test.AndroidTestCase;
import android.test.suitebuilder.annotation.SmallTest;
import java.io.IOException;
import java.util.Date;
import java.util.Locale;
/**
* Test for {@link ImapString} and its subclasses.
*/
@SmallTest
public class ImapStringTest extends AndroidTestCase {
@Override
protected void setUp() throws Exception {
super.setUp();
Email.setTempDirectory(getContext());
}
public void testEmpty() throws Exception {
assertTrue(ImapString.EMPTY.isEmpty());
assertEquals("", ImapString.EMPTY.getString());
assertEquals("", Utility.fromAscii(IOUtils.toByteArray(ImapString.EMPTY.getAsStream())));
assertFalse(ImapString.EMPTY.isNumber());
assertFalse(ImapString.EMPTY.isDate());
assertTrue(ImapString.EMPTY.is(""));
assertTrue(ImapString.EMPTY.startsWith(""));
assertFalse(ImapString.EMPTY.is("a"));
assertFalse(ImapString.EMPTY.startsWith("a"));
assertTrue(new ImapSimpleString(null).isEmpty());
}
public void testBasics() throws Exception {
final ImapSimpleString s = new ImapSimpleString("AbcD");
assertFalse(s.isEmpty());
assertEquals("AbcD", s.getString());
assertEquals("AbcD", Utility.fromAscii(IOUtils.toByteArray(s.getAsStream())));
assertFalse(s.isNumber());
assertFalse(s.isDate());
assertFalse(s.is(null));
assertFalse(s.is(""));
assertTrue(s.is("abcd"));
assertFalse(s.is("abc"));
assertFalse(s.startsWith(null));
assertTrue(s.startsWith(""));
assertTrue(s.startsWith("a"));
assertTrue(s.startsWith("abcd"));
assertFalse(s.startsWith("Z"));
assertFalse(s.startsWith("abcde"));
}
public void testGetNumberOrZero() {
assertEquals(1234, new ImapSimpleString("1234").getNumberOrZero());
assertEquals(-1, new ImapSimpleString("-1").getNumberOrZero());
assertEquals(0, new ImapSimpleString("").getNumberOrZero());
assertEquals(0, new ImapSimpleString("X").getNumberOrZero());
assertEquals(0, new ImapSimpleString("1234E").getNumberOrZero());
// Too large for 32 bit int
assertEquals(0, new ImapSimpleString("99999999999999999999").getNumberOrZero());
}
public void testGetDateOrNull() {
final ImapString date = new ImapSimpleString("01-Jan-2009 11:34:56 -0100");
assertTrue(date.isDate());
Date d = date.getDateOrNull();
assertNotNull(d);
assertEquals("1 Jan 2009 12:34:56 GMT", d.toGMTString());
final ImapString nonDate = new ImapSimpleString("1234");
assertFalse(nonDate.isDate());
assertNull(nonDate.getDateOrNull());
}
/**
* Confirms that getDateOrNull() works fine regardless of the current locale.
*/
public void testGetDateOrNullOnDifferentLocales() throws Exception {
Locale savedLocale = Locale.getDefault();
try {
Locale.setDefault(Locale.US);
checkGetDateOrNullOnDifferentLocales();
Locale.setDefault(Locale.JAPAN);
checkGetDateOrNullOnDifferentLocales();
} finally {
Locale.setDefault(savedLocale);
}
}
private static void checkGetDateOrNullOnDifferentLocales() throws Exception {
ImapSimpleString s = new ImapSimpleString("01-Jan-2009 11:34:56 -0100");
assertEquals("1 Jan 2009 12:34:56 GMT", s.getDateOrNull().toGMTString());
}
/** Test for ImapMemoryLiteral */
public void testImapMemoryLiteral() throws Exception {
final String CONTENT = "abc";
doLiteralTest(new ImapMemoryLiteral(createFixedLengthInputStream(CONTENT)), CONTENT);
}
/** Test for ImapTempFileLiteral */
public void testImapTempFileLiteral() throws Exception {
final String CONTENT = "def";
ImapTempFileLiteral l = new ImapTempFileLiteral(createFixedLengthInputStream(CONTENT));
doLiteralTest(l, CONTENT);
// destroy() should remove the temp file.
assertTrue(l.tempFileExistsForTest());
l.destroy();
assertFalse(l.tempFileExistsForTest());
}
private static void doLiteralTest(ImapString s, String content) throws IOException {
assertEquals(content, s.getString());
assertEquals(content, Utility.fromAscii(IOUtils.toByteArray(s.getAsStream())));
}
}

View File

@ -0,0 +1,92 @@
/*
* 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.email.Utility;
import com.android.email.mail.store.imap.ImapElement;
import com.android.email.mail.store.imap.ImapList;
import com.android.email.mail.store.imap.ImapResponse;
import com.android.email.mail.store.imap.ImapSimpleString;
import com.android.email.mail.store.imap.ImapString;
import java.io.ByteArrayInputStream;
import junit.framework.Assert;
/**
* Utility methods for IMAP tests.
*/
public final class ImapTestUtils {
private ImapTestUtils() {}
// Generic constants used by various tests.
public static final ImapString STRING_1 = new ImapSimpleString("aBc");
public static final ImapString STRING_2 = new ImapSimpleString("X y z");
public static final ImapList LIST_1 = buildList(STRING_1);
public static final ImapList LIST_2 = buildList(STRING_1, STRING_2, LIST_1);
/** @see #assertElement(String, ImapElement, ImapElement) */
public static final void assertElement(ImapElement expected, ImapElement actual) {
assertElement("(no message)", expected, actual);
}
/**
* Compare two {@link ImapElement}s and throws {@link AssertionFailedError} if different.
*
* Note this method used {@link ImapElement#equalsForTest} rather than equals().
*/
public static final void assertElement(String message, ImapElement expected,
ImapElement actual) {
if (expected == null && actual == null) {
return;
}
if (expected != null && expected.equalsForTest(actual)) {
return; // OK
}
Assert.fail(String.format("%s expected=%s\nactual=%s", message, expected, actual));
}
/** Convenience method to build an {@link ImapList} */
public static final ImapList buildList(ImapElement... elements) {
ImapList list = new ImapList();
for (ImapElement e : elements) {
list.add(e);
}
return list;
}
/** Convenience method to build an {@link ImapResponse} */
public static final ImapResponse buildResponse(String tag, boolean isContinuationRequest,
ImapElement... elements) {
ImapResponse res = new ImapResponse(tag, isContinuationRequest);
for (ImapElement e : elements) {
res.add(e);
}
return res;
}
/**
* Convenience method to build an {@link FixedLengthInputStream} from a String, using
* US-ASCII.
*/
public static FixedLengthInputStream createFixedLengthInputStream(String content) {
// Add unnecessary part. FixedLengthInputStream should cut it.
ByteArrayInputStream in = new ByteArrayInputStream(Utility.toAscii(content + "#trailing"));
return new FixedLengthInputStream(in, content.length());
}
}