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