Implement mail sending in EAS 14
* Mail is sent entirely differently in EAS 14...
* While we're at it, clean up Serializer
Bug: 4500720
Change-Id: I0eeb7fd28d32c0c7ac8790140721244eb0d4f65c
diff --git a/src/com/android/exchange/EasOutboxService.java b/src/com/android/exchange/EasOutboxService.java
index 5958e03..147bdb5 100644
--- a/src/com/android/exchange/EasOutboxService.java
+++ b/src/com/android/exchange/EasOutboxService.java
@@ -29,7 +29,13 @@
import com.android.emailcommon.provider.Mailbox;
import com.android.emailcommon.service.EmailServiceStatus;
import com.android.emailcommon.utility.Utility;
+import com.android.exchange.CommandStatusException.CommandStatus;
+import com.android.exchange.adapter.Parser;
+import com.android.exchange.adapter.Parser.EmptyStreamException;
+import com.android.exchange.adapter.Serializer;
+import com.android.exchange.adapter.Tags;
+import org.apache.http.HttpEntity;
import org.apache.http.HttpStatus;
import org.apache.http.entity.InputStreamEntity;
@@ -44,17 +50,26 @@
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
public class EasOutboxService extends EasSyncService {
public static final int SEND_FAILED = 1;
public static final String MAILBOX_KEY_AND_NOT_SEND_FAILED =
MessageColumns.MAILBOX_KEY + "=? and (" + SyncColumns.SERVER_ID + " is null or " +
- SyncColumns.SERVER_ID + "!=" + SEND_FAILED + ')';
+ SyncColumns.SERVER_ID + "!=" + SEND_FAILED + ')';
public static final String[] BODY_SOURCE_PROJECTION =
new String[] {BodyColumns.SOURCE_MESSAGE_KEY};
public static final String WHERE_MESSAGE_KEY = Body.MESSAGE_KEY + "=?";
+ // This is a normal email (i.e. not one of the other types)
+ public static final int MODE_NORMAL = 0;
+ // This is a smart reply email
+ public static final int MODE_SMART_REPLY = 1;
+ // This is a smart forward email
+ public static final int MODE_SMART_FORWARD = 2;
+
// This needs to be long enough to send the longest reasonable message, without being so long
// as to effectively "hang" sending of mail. The standard 30 second timeout isn't long enough
// for pictures and the like. For now, we'll use 15 minutes, in the knowledge that any socket
@@ -65,6 +80,122 @@
super(_context, _mailbox);
}
+ /**
+ * Our own HttpEntity subclass that is able to insert opaque data (in this case the MIME
+ * representation of the message body as stored in a temporary file) into the serializer stream
+ */
+ private static class SendMailEntity extends InputStreamEntity {
+ private final Context mContext;
+ private final FileInputStream mFileStream;
+ private final long mFileLength;
+ private final int mSendTag;
+ private final Message mMessage;
+
+ private static final int[] MODE_TAGS = new int[] {Tags.COMPOSE_SEND_MAIL,
+ Tags.COMPOSE_SMART_REPLY, Tags.COMPOSE_SMART_FORWARD};
+
+ public SendMailEntity(Context context, FileInputStream instream, long length, int tag,
+ Message message) {
+ super(instream, length);
+ mContext = context;
+ mFileStream = instream;
+ mFileLength = length;
+ mSendTag = tag;
+ mMessage = message;
+ }
+
+ /**
+ * We always return -1 because we don't know the actual length of the POST data (this
+ * causes HttpClient to send the data in "chunked" mode)
+ */
+ @Override
+ public long getContentLength() {
+ return -1;
+ }
+
+ @Override
+ public void writeTo(OutputStream outstream) throws IOException {
+ // Not sure if this is possible; the check is taken from the superclass
+ if (outstream == null) {
+ throw new IllegalArgumentException("Output stream may not be null");
+ }
+
+ // We'll serialize directly into the output stream
+ Serializer s = new Serializer(outstream);
+ // Send the appropriate initial tag
+ s.start(mSendTag);
+ // The Message-Id for this message (note that we cannot use the messageId stored in
+ // the message, as EAS 14 limits the length to 40 chars and we use 70+)
+ s.data(Tags.COMPOSE_CLIENT_ID, "SendMail-" + System.nanoTime());
+ // We always save sent mail
+ s.tag(Tags.COMPOSE_SAVE_IN_SENT_ITEMS);
+
+ // If we're using smart reply/forward, we need info about the original message
+ if (mSendTag != Tags.COMPOSE_SEND_MAIL) {
+ OriginalMessageInfo info = getOriginalMessageInfo(mContext, mMessage.mId);
+ if (info != null) {
+ s.start(Tags.COMPOSE_SOURCE);
+ s.data(Tags.COMPOSE_ITEM_ID, info.mItemId);
+ s.data(Tags.COMPOSE_FOLDER_ID, info.mCollectionId);
+ s.end(); // Tags.COMPOSE_SOURCE
+ }
+ }
+
+ // Start the MIME tag; this is followed by "opaque" data (byte array)
+ s.start(Tags.COMPOSE_MIME);
+ // Send opaque data from the file stream
+ s.opaque(mFileStream, (int)mFileLength);
+ // And we're done
+ s.end().end().done();
+ }
+ }
+
+ private static class SendMailParser extends Parser {
+ private final int mStartTag;
+ private int mStatus;
+
+ public SendMailParser(InputStream in, int startTag) throws IOException {
+ super(in);
+ mStartTag = startTag;
+ }
+
+ public int getStatus() {
+ return mStatus;
+ }
+
+ /**
+ * The only useful info in the SendMail response is the status; we capture and save it
+ */
+ @Override
+ public boolean parse() throws IOException {
+ if (nextTag(START_DOCUMENT) != mStartTag) {
+ throw new IOException();
+ }
+ while (nextTag(START_DOCUMENT) != END_DOCUMENT) {
+ if (tag == Tags.COMPOSE_STATUS) {
+ mStatus = getValueInt();
+ } else {
+ skipTag();
+ }
+ }
+ return true;
+ }
+ }
+
+ /**
+ * For OriginalMessageInfo, we use the terminology of EAS for the serverId and mailboxId of the
+ * original message
+ */
+ protected static class OriginalMessageInfo {
+ final String mItemId;
+ final String mCollectionId;
+
+ OriginalMessageInfo(String itemId, String collectionId) {
+ mItemId = itemId;
+ mCollectionId = collectionId;
+ }
+ }
+
private void sendCallback(long msgId, String subject, int status) {
try {
ExchangeService.callback().sendMessageStatus(msgId, subject, status, 0);
@@ -79,6 +210,56 @@
}
/**
+ * Get information about the original message that is referenced by the message to be sent; this
+ * information will exist for replies and forwards
+ *
+ * @param context the caller's context
+ * @param msgId the id of the message we're sending
+ * @return a data structure with the serverId and mailboxId of the original message, or null if
+ * either or both of those pieces of information can't be found
+ */
+ private static OriginalMessageInfo getOriginalMessageInfo(Context context, long msgId) {
+ // Note: itemId and collectionId are the terms used by EAS to refer to the serverId and
+ // mailboxId of a Message
+ String itemId = null;
+ String collectionId = null;
+
+ // First, we need to get the id of the reply/forward message
+ String[] cols = Utility.getRowColumns(context, Body.CONTENT_URI,
+ BODY_SOURCE_PROJECTION, WHERE_MESSAGE_KEY,
+ new String[] {Long.toString(msgId)});
+ if (cols != null) {
+ long refId = Long.parseLong(cols[0]);
+ // Then, we need the serverId and mailboxKey of the message
+ cols = Utility.getRowColumns(context, Message.CONTENT_URI, refId,
+ SyncColumns.SERVER_ID, MessageColumns.MAILBOX_KEY);
+ if (cols != null) {
+ itemId = cols[0];
+ long boxId = Long.parseLong(cols[1]);
+ // Then, we need the serverId of the mailbox
+ cols = Utility.getRowColumns(context, Mailbox.CONTENT_URI, boxId,
+ MailboxColumns.SERVER_ID);
+ if (cols != null) {
+ collectionId = cols[0];
+ }
+ }
+ }
+ // We need both itemId (serverId) and collectionId (mailboxId) to process a smart reply or
+ // a smart forward
+ if (itemId != null && collectionId != null) {
+ return new OriginalMessageInfo(itemId, collectionId);
+ }
+ return null;
+ }
+
+ private void sendFailed(long msgId, int result) {
+ ContentValues cv = new ContentValues();
+ cv.put(SyncColumns.SERVER_ID, SEND_FAILED);
+ Message.update(mContext, Message.CONTENT_URI, msgId, cv);
+ sendCallback(msgId, null, result);
+ }
+
+ /**
* Send a single message via EAS
* Note that we mark messages SEND_FAILED when there is a permanent failure, rather than an
* IOException, which is handled by ExchangeService with retries, backoffs, etc.
@@ -89,44 +270,27 @@
*/
int sendMessage(File cacheDir, long msgId) throws IOException, MessagingException {
int result;
+ // Say we're starting to send this message
sendCallback(msgId, null, EmailServiceStatus.IN_PROGRESS);
+ // Create a temporary file (this will hold the outgoing message in RFC822 (MIME) format)
File tmpFile = File.createTempFile("eas_", "tmp", cacheDir);
- // Write the output to a temporary file
try {
- String[] cols = Utility.getRowColumns(mContext, Message.CONTENT_URI, msgId,
- MessageColumns.FLAGS, MessageColumns.SUBJECT);
- int flags = Integer.parseInt(cols[0]);
- String subject = cols[1];
+ // Get the message and fail quickly if not found
+ Message msg = Message.restoreMessageWithId(mContext, msgId);
+ if (msg == null) return EmailServiceStatus.MESSAGE_NOT_FOUND;
+ // See what kind of outgoing messge this is
+ int flags = msg.mFlags;
boolean reply = (flags & Message.FLAG_TYPE_REPLY) != 0;
boolean forward = (flags & Message.FLAG_TYPE_FORWARD) != 0;
- // The reference message and mailbox are called item and collection in EAS
- String itemId = null;
- String collectionId = null;
- if (reply || forward) {
- // First, we need to get the id of the reply/forward message
- cols = Utility.getRowColumns(mContext, Body.CONTENT_URI, BODY_SOURCE_PROJECTION,
- WHERE_MESSAGE_KEY, new String[] {Long.toString(msgId)});
- if (cols != null) {
- long refId = Long.parseLong(cols[0]);
- // Then, we need the serverId and mailboxKey of the message
- cols = Utility.getRowColumns(mContext, Message.CONTENT_URI, refId,
- SyncColumns.SERVER_ID, MessageColumns.MAILBOX_KEY);
- if (cols != null) {
- itemId = cols[0];
- long boxId = Long.parseLong(cols[1]);
- // Then, we need the serverId of the mailbox
- cols = Utility.getRowColumns(mContext, Mailbox.CONTENT_URI, boxId,
- MailboxColumns.SERVER_ID);
- if (cols != null) {
- collectionId = cols[0];
- }
- }
- }
- }
+ // The reference message and mailbox are called item and collection in EAS
+ OriginalMessageInfo referenceInfo = null;
+ if (reply || forward) {
+ referenceInfo = getOriginalMessageInfo(mContext, msgId);
+ }
// Generally, we use SmartReply/SmartForward if we've got a good reference
- boolean smartSend = itemId != null && collectionId != null;
+ boolean smartSend = referenceInfo != null;
// But we won't use SmartForward if the account isn't set up for it (currently, we only
// use SmartForward for EAS 12.0 or later to avoid creating eml files that are
// potentially difficult for the recipient to handle)
@@ -134,48 +298,98 @@
smartSend = false;
}
- // Write the message in rfc822 format to the temporary file
- FileOutputStream fileStream = new FileOutputStream(tmpFile);
- Rfc822Output.writeTo(mContext, msgId, fileStream, smartSend, true);
- fileStream.close();
+ // Write the message to the temporary file
+ FileOutputStream fileOutputStream = new FileOutputStream(tmpFile);
+ Rfc822Output.writeTo(mContext, msgId, fileOutputStream, smartSend, true);
+ fileOutputStream.close();
- // Now, get an input stream to our temporary file and create an entity with it
- FileInputStream inputStream = new FileInputStream(tmpFile);
- InputStreamEntity inputEntity =
- new InputStreamEntity(inputStream, tmpFile.length());
+ // Sending via EAS14 is a whole 'nother kettle of fish
+ boolean isEas14 = (Double.parseDouble(mAccount.mProtocolVersion) >=
+ Eas.SUPPORTED_PROTOCOL_EX2010_DOUBLE);
+ // Get an input stream to our temporary file and create an entity with it
+ FileInputStream fileStream = new FileInputStream(tmpFile);
+ long fileLength = tmpFile.length();
+ // The type of entity depends on whether we're using EAS 14
+ HttpEntity inputEntity;
+ // For EAS 14, we need to save the wbxml tag we're using
+ int modeTag = 0;
+ if (isEas14) {
+ int mode = !smartSend ? MODE_NORMAL : reply ? MODE_SMART_REPLY : MODE_SMART_FORWARD;
+ modeTag = SendMailEntity.MODE_TAGS[mode];
+ inputEntity = new SendMailEntity(mContext, fileStream, fileLength, modeTag, msg);
+ } else {
+ inputEntity = new InputStreamEntity(fileStream, fileLength);
+ }
// Create the appropriate command and POST it to the server
String cmd = "SendMail";
if (smartSend) {
- cmd = generateSmartSendCmd(reply, itemId, collectionId);
+ // In EAS 14, we don't send itemId and collectionId in the command
+ if (isEas14) {
+ cmd = reply ? "SmartReply" : "SmartForward";
+ } else {
+ cmd = generateSmartSendCmd(reply, referenceInfo.mItemId,
+ referenceInfo.mCollectionId);
+ }
}
- cmd += "&SaveInSent=T";
+ // If we're not EAS 14, add our save-in-sent setting here
+ if (!isEas14) {
+ cmd += "&SaveInSent=T";
+ }
userLog("Send cmd: " + cmd);
+
+ // Finally, post SendMail to the server
EasResponse resp = sendHttpClientPost(cmd, inputEntity, SEND_MAIL_TIMEOUT);
try {
- inputStream.close();
+ fileStream.close();
int code = resp.getStatus();
if (code == HttpStatus.SC_OK) {
+ // HTTP OK before EAS 14 is a thumbs up; in EAS 14, we've actually got to parse
+ // the reply
+ if (isEas14) {
+ try {
+ // Try to parse the result
+ SendMailParser p = new SendMailParser(resp.getInputStream(), modeTag);
+ // If we get here, the SendMail failed; go figure
+ p.parse();
+ // The parser holds the status
+ int status = p.getStatus();
+ userLog("SendMail error, status: " + status);
+ // The only "interesting" failure is a security error. Note that an
+ // auth error would have manifest with an HTTP error code
+ if (CommandStatus.isNeedsProvisioning(status)) {
+ result = EmailServiceStatus.SECURITY_FAILURE;
+ } else {
+ // We mark the result as SUCCESS on a non-security failure since the
+ // message itself will be marked failed and we don't want to block
+ // other messages
+ result = EmailServiceStatus.SUCCESS;
+ }
+ sendFailed(msgId, result);
+ return result;
+ } catch (EmptyStreamException e) {
+ // This is actually fine; an empty stream means SendMail succeeded
+ }
+ }
+
+ // If we're here, the SendMail command succeeded
userLog("Deleting message...");
+ // Delete the message from the Outbox and send callback
mContentResolver.delete(ContentUris.withAppendedId(Message.CONTENT_URI, msgId),
null, null);
result = EmailServiceStatus.SUCCESS;
- sendCallback(-1, subject, EmailServiceStatus.SUCCESS);
+ sendCallback(-1, msg.mSubject, EmailServiceStatus.SUCCESS);
} else {
userLog("Message sending failed, code: " + code);
- ContentValues cv = new ContentValues();
- cv.put(SyncColumns.SERVER_ID, SEND_FAILED);
- Message.update(mContext, Message.CONTENT_URI, msgId, cv);
- // We mark the result as SUCCESS on a non-auth failure since the message itself
- // is already marked failed and we don't want to stop other messages from trying
- // to send.
if (isAuthError(code)) {
result = EmailServiceStatus.LOGIN_FAILED;
} else {
+ // We mark the result as SUCCESS on a non-auth failure since the message
+ // itself will be marked failed and we don't want to block other messages
result = EmailServiceStatus.SUCCESS;
}
- sendCallback(msgId, null, result);
+ sendFailed(msgId, result);
}
} finally {
resp.close();
@@ -199,12 +413,14 @@
File cacheDir = mContext.getCacheDir();
try {
mDeviceId = ExchangeService.getDeviceId(mContext);
+ // Get a cursor to Outbox messages
Cursor c = mContext.getContentResolver().query(Message.CONTENT_URI,
Message.ID_COLUMN_PROJECTION, MAILBOX_KEY_AND_NOT_SEND_FAILED,
new String[] {Long.toString(mMailbox.mId)}, null);
- try {
+ try {
+ // Loop through the messages, sending each one
while (c.moveToNext()) {
- long msgId = c.getLong(0);
+ long msgId = c.getLong(Message.ID_COLUMNS_ID_COLUMN);
if (msgId != 0) {
if (Utility.hasUnloadedAttachments(mContext, msgId)) {
// We'll just have to wait on this...
@@ -216,6 +432,9 @@
if (result == EmailServiceStatus.LOGIN_FAILED) {
mExitStatus = EXIT_LOGIN_FAILURE;
return;
+ } else if (result == EmailServiceStatus.SECURITY_FAILURE) {
+ mExitStatus = EXIT_SECURITY_FAILURE;
+ return;
} else if (result == EmailServiceStatus.REMOTE_EXCEPTION) {
mExitStatus = EXIT_EXCEPTION;
return;
@@ -223,7 +442,7 @@
}
}
} finally {
- c.close();
+ c.close();
}
mExitStatus = EXIT_DONE;
} catch (IOException e) {
diff --git a/src/com/android/exchange/EasSyncService.java b/src/com/android/exchange/EasSyncService.java
index b73cf5e..2bfd45a 100644
--- a/src/com/android/exchange/EasSyncService.java
+++ b/src/com/android/exchange/EasSyncService.java
@@ -1419,9 +1419,10 @@
String us = makeUriString(cmd, extra);
HttpPost method = new HttpPost(URI.create(us));
- // Send the proper Content-Type header
+ // Send the proper Content-Type header; it's always wbxml except for messages when
+ // the EAS protocol version is < 14.0
// If entity is null (e.g. for attachments), don't set this header
- if (msg) {
+ if (msg && (mProtocolVersionDouble < Eas.SUPPORTED_PROTOCOL_EX2010_DOUBLE)) {
method.setHeader("Content-Type", "message/rfc822");
} else if (entity != null) {
method.setHeader("Content-Type", "application/vnd.ms-sync.wbxml");
diff --git a/src/com/android/exchange/adapter/Parser.java b/src/com/android/exchange/adapter/Parser.java
index 4ef1994..dde69c9 100644
--- a/src/com/android/exchange/adapter/Parser.java
+++ b/src/com/android/exchange/adapter/Parser.java
@@ -484,6 +484,10 @@
for (int i = 0; i < length; i++) {
bytes[i] = (byte)readByte();
}
+ if (logging) {
+ name = tagTable[startTag - TAG_BASE];
+ log(name + ": (opaque:" + length + ") ");
+ }
break;
default:
diff --git a/src/com/android/exchange/adapter/Serializer.java b/src/com/android/exchange/adapter/Serializer.java
index ec1122d..dd2033d 100644
--- a/src/com/android/exchange/adapter/Serializer.java
+++ b/src/com/android/exchange/adapter/Serializer.java
@@ -19,7 +19,7 @@
* IN THE SOFTWARE. */
//Contributors: Jonathan Cox, Bogdan Onoiu, Jerry Tian
-//Simplified for Google, Inc. by Marc Blank
+// Greatly simplified for Google, Inc. by Marc Blank
package com.android.exchange.adapter;
@@ -31,49 +31,47 @@
import java.io.ByteArrayOutputStream;
import java.io.IOException;
+import java.io.InputStream;
import java.io.OutputStream;
-import java.util.Hashtable;
public class Serializer {
-
private static final String TAG = "Serializer";
- private boolean logging = Log.isLoggable(TAG, Log.VERBOSE);
-
+ private static final int BUFFER_SIZE = 16*1024;
private static final int NOT_PENDING = -1;
- ByteArrayOutputStream out = new ByteArrayOutputStream();
- ByteArrayOutputStream buf = new ByteArrayOutputStream();
+ private final OutputStream mOutput;
+ private int mPendingTag = NOT_PENDING;
+ private int mDepth;
+ private String[] mNameStack = new String[20];
+ private int mTagPage = 0;
+ private boolean mLogging = Log.isLoggable(TAG, Log.VERBOSE);
- String pending;
- int pendingTag = NOT_PENDING;
- int depth;
- String name;
- String[] nameStack = new String[20];
-
- Hashtable<String, Object> tagTable = new Hashtable<String, Object>();
-
- private int tagPage;
-
- public Serializer() {
- this(true);
+ public Serializer() throws IOException {
+ this(new ByteArrayOutputStream(), true);
}
- public Serializer(boolean startDocument, boolean _logging) {
- this(true);
- logging = _logging;
+ public Serializer(OutputStream os) throws IOException {
+ this(os, true);
}
- public Serializer(boolean startDocument) {
+ public Serializer(boolean startDocument) throws IOException {
+ this(new ByteArrayOutputStream(), startDocument);
+ }
+
+ /**
+ * Base constructor
+ * @param outputStream the stream we're serializing to
+ * @param startDocument whether or not to start a document
+ * @param _logging whether or not to log our output
+ * @throws IOException
+ */
+ public Serializer(OutputStream outputStream, boolean startDocument) throws IOException {
super();
+ mOutput = outputStream;
if (startDocument) {
- try {
- startDocument();
- //logging = Eas.PARSER_LOG;
- } catch (IOException e) {
- // Nothing to be done
- }
+ startDocument();
} else {
- out.write(0);
+ mOutput.write(0);
}
}
@@ -89,58 +87,57 @@
}
public void done() throws IOException {
- if (depth != 0) {
+ if (mDepth != 0) {
throw new IOException("Done received with unclosed tags");
}
- writeInteger(out, 0);
- out.write(buf.toByteArray());
- out.flush();
+ mOutput.flush();
}
public void startDocument() throws IOException{
- out.write(0x03); // version 1.3
- out.write(0x01); // unknown or missing public identifier
- out.write(106);
+ mOutput.write(0x03); // version 1.3
+ mOutput.write(0x01); // unknown or missing public identifier
+ mOutput.write(106); // UTF-8
+ mOutput.write(0); // 0 length string array
}
public void checkPendingTag(boolean degenerated) throws IOException {
- if (pendingTag == NOT_PENDING)
+ if (mPendingTag == NOT_PENDING)
return;
- int page = pendingTag >> Tags.PAGE_SHIFT;
- int tag = pendingTag & Tags.PAGE_MASK;
- if (page != tagPage) {
- tagPage = page;
- buf.write(Wbxml.SWITCH_PAGE);
- buf.write(page);
+ int page = mPendingTag >> Tags.PAGE_SHIFT;
+ int tag = mPendingTag & Tags.PAGE_MASK;
+ if (page != mTagPage) {
+ mTagPage = page;
+ mOutput.write(Wbxml.SWITCH_PAGE);
+ mOutput.write(page);
}
- buf.write(degenerated ? tag : tag | 64);
- if (logging) {
+ mOutput.write(degenerated ? tag : tag | 64);
+ if (mLogging) {
String name = Tags.pages[page][tag - 5];
- nameStack[depth] = name;
+ mNameStack[mDepth] = name;
log("<" + name + '>');
}
- pendingTag = NOT_PENDING;
+ mPendingTag = NOT_PENDING;
}
public Serializer start(int tag) throws IOException {
checkPendingTag(false);
- pendingTag = tag;
- depth++;
+ mPendingTag = tag;
+ mDepth++;
return this;
}
public Serializer end() throws IOException {
- if (pendingTag >= 0) {
+ if (mPendingTag >= 0) {
checkPendingTag(true);
} else {
- buf.write(Wbxml.END);
- if (logging) {
- log("</" + nameStack[depth] + '>');
+ mOutput.write(Wbxml.END);
+ if (mLogging) {
+ log("</" + mNameStack[mDepth] + '>');
}
}
- depth--;
+ mDepth--;
return this;
}
@@ -160,28 +157,39 @@
return this;
}
- @Override
- public String toString() {
- return out.toString();
- }
-
- public byte[] toByteArray() {
- return out.toByteArray();
- }
-
public Serializer text(String text) throws IOException {
if (text == null) {
- Log.e(TAG, "Writing null text for pending tag: " + pendingTag);
+ Log.e(TAG, "Writing null text for pending tag: " + mPendingTag);
}
checkPendingTag(false);
- buf.write(Wbxml.STR_I);
- writeLiteralString(buf, text);
- if (logging) {
+ mOutput.write(Wbxml.STR_I);
+ writeLiteralString(mOutput, text);
+ if (mLogging) {
log(text);
}
return this;
}
+ public Serializer opaque(InputStream is, int length) throws IOException {
+ checkPendingTag(false);
+ mOutput.write(Wbxml.OPAQUE);
+ writeInteger(mOutput, length);
+ if (mLogging) {
+ log("Opaque, length: " + length);
+ }
+ // Now write out the opaque data in batches
+ byte[] buffer = new byte[BUFFER_SIZE];
+ while (length > 0) {
+ int bytesRead = is.read(buffer, 0, (int)Math.min(BUFFER_SIZE, length));
+ if (bytesRead == -1) {
+ break;
+ }
+ mOutput.write(buffer, 0, bytesRead);
+ length -= bytesRead;
+ }
+ return this;
+ }
+
void writeInteger(OutputStream out, int i) throws IOException {
byte[] buf = new byte[5];
int idx = 0;
@@ -195,7 +203,7 @@
out.write(buf[--idx] | 0x80);
}
out.write(buf[0]);
- if (logging) {
+ if (mLogging) {
log(Integer.toString(i));
}
}
@@ -212,4 +220,20 @@
data(tag, value);
}
}
+
+ @Override
+ public String toString() {
+ if (mOutput instanceof ByteArrayOutputStream) {
+ return ((ByteArrayOutputStream)mOutput).toString();
+ }
+ throw new IllegalStateException();
+ }
+
+ public byte[] toByteArray() {
+ if (mOutput instanceof ByteArrayOutputStream) {
+ return ((ByteArrayOutputStream)mOutput).toByteArray();
+ }
+ throw new IllegalStateException();
+ }
+
}
diff --git a/src/com/android/exchange/adapter/Tags.java b/src/com/android/exchange/adapter/Tags.java
index cb4ee89..f5fe60c 100644
--- a/src/com/android/exchange/adapter/Tags.java
+++ b/src/com/android/exchange/adapter/Tags.java
@@ -557,6 +557,23 @@
public static final int ITEMS_CONVERSATION_ID = ITEMS_PAGE + 0x18;
public static final int ITEMS_MOVE_ALWAYS = ITEMS_PAGE + 0x19;
+ public static final int COMPOSE_PAGE = COMPOSE << PAGE_SHIFT;
+ public static final int COMPOSE_SEND_MAIL = COMPOSE_PAGE + 5;
+ public static final int COMPOSE_SMART_FORWARD = COMPOSE_PAGE + 6;
+ public static final int COMPOSE_SMART_REPLY = COMPOSE_PAGE + 7;
+ public static final int COMPOSE_SAVE_IN_SENT_ITEMS = COMPOSE_PAGE + 8;
+ public static final int COMPOSE_REPLACE_MIME = COMPOSE_PAGE + 9;
+ // There no tag for COMPOSE_PAGE + 0xA
+ public static final int COMPOSE_SOURCE = COMPOSE_PAGE + 0xB;
+ public static final int COMPOSE_FOLDER_ID = COMPOSE_PAGE + 0xC;
+ public static final int COMPOSE_ITEM_ID = COMPOSE_PAGE + 0xD;
+ public static final int COMPOSE_LONG_ID = COMPOSE_PAGE + 0xE;
+ public static final int COMPOSE_INSTANCE_ID = COMPOSE_PAGE + 0xF;
+ public static final int COMPOSE_MIME = COMPOSE_PAGE + 0x10;
+ public static final int COMPOSE_CLIENT_ID = COMPOSE_PAGE + 0x11;
+ public static final int COMPOSE_STATUS = COMPOSE_PAGE + 0x12;
+ public static final int COMPOSE_ACCOUNT_ID = COMPOSE_PAGE + 0x13;
+
public static final int EMAIL2_PAGE = EMAIL2 << PAGE_SHIFT;
public static final int EMAIL2_UM_CALLER_ID = EMAIL2_PAGE + 5;
public static final int EMAIL2_UM_USER_NOTES = EMAIL2_PAGE + 6;
@@ -769,6 +786,10 @@
},
{
// 0x15 ComposeMail
+ "SendMail", "SmartForward", "SmartReply", "SaveInSentItems", "ReplaceMime",
+ "--unused--", "ComposeSource", "ComposeFolderId", "ComposeItemId", "ComposeLongId",
+ "ComposeInstanceId", "ComposeMime", "ComposeClientId", "ComposeStatus",
+ "ComposeAccountId"
},
{
// 0x16 Email2