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:
parent
9d1b9fc784
commit
ff0712cb1e
|
@ -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);
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
@ -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";
|
||||
}
|
|
@ -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.
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 + "\"";
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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())));
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue