Download all message metadata from server.

When a new SIM is inserted, the following should happen:
1. All local data is synced up to the server
2. All local data is wiped
3. All non-deleted voicemail metadata is downloaded from the server
(this means that there is enough information to create entries for the
voicemails but not that the content/audio files are downloaded).

Also added back LogUtils in all the e-mail library code (everything
under com.android.phone.common.mail).

All newly added files are direct copies except for "Flag" (including
refactorings of the files that use "Flag", converting enums into static
strings) and Utility (which selects only the necessary utility
functions). Note that this means that some of the files have dead code
in them--so later CLs will remove the excess weighte

Bug: 19236241

Change-Id: Ibc49c96667db8ec0de2ab52bdd82bfd0f6993135
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 0e87095..62bc4dc 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -59,6 +59,7 @@
     <uses-permission android:name="android.permission.WAKE_LOCK" />
     <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
     <uses-permission android:name="android.permission.STATUS_BAR" />
+    <uses-permission android:name="android.permission.RECEIVE_SMS" />
     <uses-permission android:name="android.permission.READ_SMS" />
     <uses-permission android:name="android.permission.WRITE_SMS" />
     <uses-permission android:name="android.permission.SEND_SMS" />
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 7887bf6..ab56faf 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -1292,4 +1292,10 @@
 
     <!-- Hint appearing below a selected action on the emergency dialer telling user to tap again to execute the action [CHAR LIMIT=NONE] -->
     <string name="emergency_action_launch_hint">Touch again to open</string>
+
+    <!-- Strings for IMAP -->
+
+    <!-- String used in place of a message that could not be properly decoded (e.g. bad base64
+       data was received.) [CHAR LIMIT=none] -->
+    <string name="message_decode_error">There was an error while decoding the message.</string>
 </resources>
diff --git a/src/com/android/phone/common/mail/Address.java b/src/com/android/phone/common/mail/Address.java
new file mode 100644
index 0000000..5928b63
--- /dev/null
+++ b/src/com/android/phone/common/mail/Address.java
@@ -0,0 +1,544 @@
+/*
+ * Copyright (C) 2015 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.phone.common.mail;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.Html;
+import android.text.TextUtils;
+import android.text.util.Rfc822Token;
+import android.text.util.Rfc822Tokenizer;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.phone.common.mail.utils.LogUtils;
+
+import org.apache.james.mime4j.codec.EncoderUtil;
+import org.apache.james.mime4j.decoder.DecoderUtil;
+
+import java.util.ArrayList;
+import java.util.regex.Pattern;
+
+/**
+ * This class represent email address.
+ *
+ * RFC822 email address may have following format.
+ *   "name" <address> (comment)
+ *   "name" <address>
+ *   name <address>
+ *   address
+ * Name and comment part should be MIME/base64 encoded in header if necessary.
+ *
+ */
+public class Address implements Parcelable {
+    public static final String ADDRESS_DELIMETER = ",";
+    /**
+     *  Address part, in the form local_part@domain_part. No surrounding angle brackets.
+     */
+    private String mAddress;
+
+    /**
+     * Name part. No surrounding double quote, and no MIME/base64 encoding.
+     * This must be null if Address has no name part.
+     */
+    private String mPersonal;
+
+    /**
+     * When personal is set, it will return the first token of the personal
+     * string. Otherwise, it will return the e-mail address up to the '@' sign.
+     */
+    private String mSimplifiedName;
+
+    // Regex that matches address surrounded by '<>' optionally. '^<?([^>]+)>?$'
+    private static final Pattern REMOVE_OPTIONAL_BRACKET = Pattern.compile("^<?([^>]+)>?$");
+    // Regex that matches personal name surrounded by '""' optionally. '^"?([^"]+)"?$'
+    private static final Pattern REMOVE_OPTIONAL_DQUOTE = Pattern.compile("^\"?([^\"]*)\"?$");
+    // Regex that matches escaped character '\\([\\"])'
+    private static final Pattern UNQUOTE = Pattern.compile("\\\\([\\\\\"])");
+
+    // TODO: LOCAL_PART and DOMAIN_PART_PART are too permissive and can be improved.
+    // TODO: Fix this to better constrain comments.
+    /** Regex for the local part of an email address. */
+    private static final String LOCAL_PART = "[^@]+";
+    /** Regex for each part of the domain part, i.e. the thing between the dots. */
+    private static final String DOMAIN_PART_PART = "[[\\w][\\d]\\-\\(\\)\\[\\]]+";
+    /** Regex for the domain part, which is two or more {@link #DOMAIN_PART_PART} separated by . */
+    private static final String DOMAIN_PART =
+            "(" + DOMAIN_PART_PART + "\\.)+" + DOMAIN_PART_PART;
+
+    /** Pattern to check if an email address is valid. */
+    private static final Pattern EMAIL_ADDRESS =
+            Pattern.compile("\\A" + LOCAL_PART + "@" + DOMAIN_PART + "\\z");
+
+    private static final Address[] EMPTY_ADDRESS_ARRAY = new Address[0];
+
+    // delimiters are chars that do not appear in an email address, used by fromHeader
+    private static final char LIST_DELIMITER_EMAIL = '\1';
+    private static final char LIST_DELIMITER_PERSONAL = '\2';
+
+    private static final String LOG_TAG = "Email Address";
+
+    @VisibleForTesting
+    public Address(String address) {
+        setAddress(address);
+    }
+
+    public Address(String address, String personal) {
+        setPersonal(personal);
+        setAddress(address);
+    }
+
+    /**
+     * Returns a simplified string for this e-mail address.
+     * When a name is known, it will return the first token of that name. Otherwise, it will
+     * return the e-mail address up to the '@' sign.
+     */
+    public String getSimplifiedName() {
+        if (mSimplifiedName == null) {
+            if (TextUtils.isEmpty(mPersonal) && !TextUtils.isEmpty(mAddress)) {
+                int atSign = mAddress.indexOf('@');
+                mSimplifiedName = (atSign != -1) ? mAddress.substring(0, atSign) : "";
+            } else if (!TextUtils.isEmpty(mPersonal)) {
+
+                // TODO: use Contacts' NameSplitter for more reliable first-name extraction
+
+                int end = mPersonal.indexOf(' ');
+                while (end > 0 && mPersonal.charAt(end - 1) == ',') {
+                    end--;
+                }
+                mSimplifiedName = (end < 1) ? mPersonal : mPersonal.substring(0, end);
+
+            } else {
+                LogUtils.w(LOG_TAG, "Unable to get a simplified name");
+                mSimplifiedName = "";
+            }
+        }
+        return mSimplifiedName;
+    }
+
+    public static synchronized Address getEmailAddress(String rawAddress) {
+        if (TextUtils.isEmpty(rawAddress)) {
+            return null;
+        }
+        String name, address;
+        final Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(rawAddress);
+        if (tokens.length > 0) {
+            final String tokenizedName = tokens[0].getName();
+            name = tokenizedName != null ? Html.fromHtml(tokenizedName.trim()).toString()
+                    : "";
+            address = Html.fromHtml(tokens[0].getAddress()).toString();
+        } else {
+            name = "";
+            address = rawAddress == null ?
+                    "" : Html.fromHtml(rawAddress).toString();
+        }
+        return new Address(address, name);
+    }
+
+    public String getAddress() {
+        return mAddress;
+    }
+
+    public void setAddress(String address) {
+        mAddress = REMOVE_OPTIONAL_BRACKET.matcher(address).replaceAll("$1");
+    }
+
+    /**
+     * Get name part as UTF-16 string. No surrounding double quote, and no MIME/base64 encoding.
+     *
+     * @return Name part of email address. Returns null if it is omitted.
+     */
+    public String getPersonal() {
+        return mPersonal;
+    }
+
+    /**
+     * Set personal part from UTF-16 string. Optional surrounding double quote will be removed.
+     * It will be also unquoted and MIME/base64 decoded.
+     *
+     * @param personal name part of email address as UTF-16 string. Null is acceptable.
+     */
+    public void setPersonal(String personal) {
+        mPersonal = decodeAddressPersonal(personal);
+    }
+
+    /**
+     * Decodes name from UTF-16 string. Optional surrounding double quote will be removed.
+     * It will be also unquoted and MIME/base64 decoded.
+     *
+     * @param personal name part of email address as UTF-16 string. Null is acceptable.
+     */
+    public static String decodeAddressPersonal(String personal) {
+        if (personal != null) {
+            personal = REMOVE_OPTIONAL_DQUOTE.matcher(personal).replaceAll("$1");
+            personal = UNQUOTE.matcher(personal).replaceAll("$1");
+            personal = DecoderUtil.decodeEncodedWords(personal);
+            if (personal.length() == 0) {
+                personal = null;
+            }
+        }
+        return personal;
+    }
+
+    /**
+     * This method is used to check that all the addresses that the user
+     * entered in a list (e.g. To:) are valid, so that none is dropped.
+     */
+    @VisibleForTesting
+    public static boolean isAllValid(String addressList) {
+        // This code mimics the parse() method below.
+        // I don't know how to better avoid the code-duplication.
+        if (addressList != null && addressList.length() > 0) {
+            Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(addressList);
+            for (int i = 0, length = tokens.length; i < length; ++i) {
+                Rfc822Token token = tokens[i];
+                String address = token.getAddress();
+                if (!TextUtils.isEmpty(address) && !isValidAddress(address)) {
+                    return false;
+                }
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Parse a comma-delimited list of addresses in RFC822 format and return an
+     * array of Address objects.
+     *
+     * @param addressList Address list in comma-delimited string.
+     * @return An array of 0 or more Addresses.
+     */
+    public static Address[] parse(String addressList) {
+        if (addressList == null || addressList.length() == 0) {
+            return EMPTY_ADDRESS_ARRAY;
+        }
+        Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(addressList);
+        ArrayList<Address> addresses = new ArrayList<Address>();
+        for (int i = 0, length = tokens.length; i < length; ++i) {
+            Rfc822Token token = tokens[i];
+            String address = token.getAddress();
+            if (!TextUtils.isEmpty(address)) {
+                if (isValidAddress(address)) {
+                    String name = token.getName();
+                    if (TextUtils.isEmpty(name)) {
+                        name = null;
+                    }
+                    addresses.add(new Address(address, name));
+                }
+            }
+        }
+        return addresses.toArray(new Address[addresses.size()]);
+    }
+
+    /**
+     * Checks whether a string email address is valid.
+     * E.g. name@domain.com is valid.
+     */
+    @VisibleForTesting
+    static boolean isValidAddress(final String address) {
+        return EMAIL_ADDRESS.matcher(address).find();
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (o instanceof Address) {
+            // It seems that the spec says that the "user" part is case-sensitive,
+            // while the domain part in case-insesitive.
+            // So foo@yahoo.com and Foo@yahoo.com are different.
+            // This may seem non-intuitive from the user POV, so we
+            // may re-consider it if it creates UI trouble.
+            // A problem case is "replyAll" sending to both
+            // a@b.c and to A@b.c, which turn out to be the same on the server.
+            // Leave unchanged for now (i.e. case-sensitive).
+            return getAddress().equals(((Address) o).getAddress());
+        }
+        return super.equals(o);
+    }
+
+    @Override
+    public int hashCode() {
+        return getAddress().hashCode();
+    }
+
+    /**
+     * Get human readable address string.
+     * Do not use this for email header.
+     *
+     * @return Human readable address string.  Not quoted and not encoded.
+     */
+    @Override
+    public String toString() {
+        if (mPersonal != null && !mPersonal.equals(mAddress)) {
+            if (mPersonal.matches(".*[\\(\\)<>@,;:\\\\\".\\[\\]].*")) {
+                return ensureQuotedString(mPersonal) + " <" + mAddress + ">";
+            } else {
+                return mPersonal + " <" + mAddress + ">";
+            }
+        } else {
+            return mAddress;
+        }
+    }
+
+    /**
+     * Ensures that the given string starts and ends with the double quote character. The string is
+     * not modified in any way except to add the double quote character to start and end if it's not
+     * already there.
+     *
+     * sample -> "sample"
+     * "sample" -> "sample"
+     * ""sample"" -> "sample"
+     * "sample"" -> "sample"
+     * sa"mp"le -> "sa"mp"le"
+     * "sa"mp"le" -> "sa"mp"le"
+     * (empty string) -> ""
+     * " -> ""
+     */
+    private static String ensureQuotedString(String s) {
+        if (s == null) {
+            return null;
+        }
+        if (!s.matches("^\".*\"$")) {
+            return "\"" + s + "\"";
+        } else {
+            return s;
+        }
+    }
+
+    /**
+     * Get human readable comma-delimited address string.
+     *
+     * @param addresses Address array
+     * @return Human readable comma-delimited address string.
+     */
+    @VisibleForTesting
+    public static String toString(Address[] addresses) {
+        return toString(addresses, ADDRESS_DELIMETER);
+    }
+
+    /**
+     * Get human readable address strings joined with the specified separator.
+     *
+     * @param addresses Address array
+     * @param separator Separator
+     * @return Human readable comma-delimited address string.
+     */
+    public static String toString(Address[] addresses, String separator) {
+        if (addresses == null || addresses.length == 0) {
+            return null;
+        }
+        if (addresses.length == 1) {
+            return addresses[0].toString();
+        }
+        StringBuilder sb = new StringBuilder(addresses[0].toString());
+        for (int i = 1; i < addresses.length; i++) {
+            sb.append(separator);
+            // TODO: investigate why this .trim() is needed.
+            sb.append(addresses[i].toString().trim());
+        }
+        return sb.toString();
+    }
+
+    /**
+     * Get RFC822/MIME compatible address string.
+     *
+     * @return RFC822/MIME compatible address string.
+     * It may be surrounded by double quote or quoted and MIME/base64 encoded if necessary.
+     */
+    public String toHeader() {
+        if (mPersonal != null) {
+            return EncoderUtil.encodeAddressDisplayName(mPersonal) + " <" + mAddress + ">";
+        } else {
+            return mAddress;
+        }
+    }
+
+    /**
+     * Get RFC822/MIME compatible comma-delimited address string.
+     *
+     * @param addresses Address array
+     * @return RFC822/MIME compatible comma-delimited address string.
+     * it may be surrounded by double quoted or quoted and MIME/base64 encoded if necessary.
+     */
+    public static String toHeader(Address[] addresses) {
+        if (addresses == null || addresses.length == 0) {
+            return null;
+        }
+        if (addresses.length == 1) {
+            return addresses[0].toHeader();
+        }
+        StringBuilder sb = new StringBuilder(addresses[0].toHeader());
+        for (int i = 1; i < addresses.length; i++) {
+            // We need space character to be able to fold line.
+            sb.append(", ");
+            sb.append(addresses[i].toHeader());
+        }
+        return sb.toString();
+    }
+
+    /**
+     * Get Human friendly address string.
+     *
+     * @return the personal part of this Address, or the address part if the
+     * personal part is not available
+     */
+    @VisibleForTesting
+    public String toFriendly() {
+        if (mPersonal != null && mPersonal.length() > 0) {
+            return mPersonal;
+        } else {
+            return mAddress;
+        }
+    }
+
+    /**
+     * Creates a comma-delimited list of addresses in the "friendly" format (see toFriendly() for
+     * details on the per-address conversion).
+     *
+     * @param addresses Array of Address[] values
+     * @return A comma-delimited string listing all of the addresses supplied.  Null if source
+     * was null or empty.
+     */
+    @VisibleForTesting
+    public static String toFriendly(Address[] addresses) {
+        if (addresses == null || addresses.length == 0) {
+            return null;
+        }
+        if (addresses.length == 1) {
+            return addresses[0].toFriendly();
+        }
+        StringBuilder sb = new StringBuilder(addresses[0].toFriendly());
+        for (int i = 1; i < addresses.length; i++) {
+            sb.append(", ");
+            sb.append(addresses[i].toFriendly());
+        }
+        return sb.toString();
+    }
+
+    /**
+     * Returns exactly the same result as Address.toString(Address.fromHeader(addressList)).
+     */
+    @VisibleForTesting
+    public static String fromHeaderToString(String addressList) {
+        return toString(fromHeader(addressList));
+    }
+
+    /**
+     * Returns exactly the same result as Address.toHeader(Address.parse(addressList)).
+     */
+    @VisibleForTesting
+    public static String parseToHeader(String addressList) {
+        return Address.toHeader(Address.parse(addressList));
+    }
+
+    /**
+     * Returns null if the addressList has 0 addresses, otherwise returns the first address.
+     * The same as Address.fromHeader(addressList)[0] for non-empty list.
+     * This is an utility method that offers some performance optimization opportunities.
+     */
+    @VisibleForTesting
+    public static Address firstAddress(String addressList) {
+        Address[] array = fromHeader(addressList);
+        return array.length > 0 ? array[0] : null;
+    }
+
+    /**
+     * This method exists to convert an address list formatted in a deprecated legacy format to the
+     * standard RFC822 header format. {@link #fromHeader(String)} is capable of reading the legacy
+     * format and the RFC822 format. {@link #toHeader()} always produces the RFC822 format.
+     *
+     * This implementation is brute-force, and could be replaced with a more efficient version
+     * if desired.
+     */
+    public static String reformatToHeader(String addressList) {
+        return toHeader(fromHeader(addressList));
+    }
+
+    /**
+     * @param addressList a CSV of RFC822 addresses or the deprecated legacy string format
+     * @return array of addresses parsed from <code>addressList</code>
+     */
+    @VisibleForTesting
+    public static Address[] fromHeader(String addressList) {
+        if (addressList == null || addressList.length() == 0) {
+            return EMPTY_ADDRESS_ARRAY;
+        }
+        // IF we're CSV, just parse
+        if ((addressList.indexOf(LIST_DELIMITER_PERSONAL) == -1) &&
+                (addressList.indexOf(LIST_DELIMITER_EMAIL) == -1)) {
+            return Address.parse(addressList);
+        }
+        // Otherwise, do backward-compatible unpack
+        ArrayList<Address> addresses = new ArrayList<Address>();
+        int length = addressList.length();
+        int pairStartIndex = 0;
+        int pairEndIndex;
+
+        /* addressEndIndex is only re-scanned (indexOf()) when a LIST_DELIMITER_PERSONAL
+           is used, not for every email address; i.e. not for every iteration of the while().
+           This reduces the theoretical complexity from quadratic to linear,
+           and provides some speed-up in practice by removing redundant scans of the string.
+        */
+        int addressEndIndex = addressList.indexOf(LIST_DELIMITER_PERSONAL);
+
+        while (pairStartIndex < length) {
+            pairEndIndex = addressList.indexOf(LIST_DELIMITER_EMAIL, pairStartIndex);
+            if (pairEndIndex == -1) {
+                pairEndIndex = length;
+            }
+            Address address;
+            if (addressEndIndex == -1 || pairEndIndex <= addressEndIndex) {
+                // in this case the DELIMITER_PERSONAL is in a future pair,
+                // so don't use personal, and don't update addressEndIndex
+                address = new Address(addressList.substring(pairStartIndex, pairEndIndex), null);
+            } else {
+                address = new Address(addressList.substring(pairStartIndex, addressEndIndex),
+                        addressList.substring(addressEndIndex + 1, pairEndIndex));
+                // only update addressEndIndex when we use the LIST_DELIMITER_PERSONAL
+                addressEndIndex = addressList.indexOf(LIST_DELIMITER_PERSONAL, pairEndIndex + 1);
+            }
+            addresses.add(address);
+            pairStartIndex = pairEndIndex + 1;
+        }
+        return addresses.toArray(new Address[addresses.size()]);
+    }
+
+    public static final Creator<Address> CREATOR = new Creator<Address>() {
+        @Override
+        public Address createFromParcel(Parcel parcel) {
+            return new Address(parcel);
+        }
+
+        @Override
+        public Address[] newArray(int size) {
+            return new Address[size];
+        }
+    };
+
+    public Address(Parcel in) {
+        setPersonal(in.readString());
+        setAddress(in.readString());
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel out, int flags) {
+        out.writeString(mPersonal);
+        out.writeString(mAddress);
+    }
+}
diff --git a/src/com/android/phone/common/mail/Base64Body.java b/src/com/android/phone/common/mail/Base64Body.java
new file mode 100644
index 0000000..757b4b7
--- /dev/null
+++ b/src/com/android/phone/common/mail/Base64Body.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2015 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.phone.common.mail;
+
+import android.util.Base64;
+import android.util.Base64OutputStream;
+
+import org.apache.commons.io.IOUtils;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+public class Base64Body implements Body {
+    private final InputStream mSource;
+    // Because we consume the input stream, we can only write out once
+    private boolean mAlreadyWritten;
+
+    public Base64Body(InputStream source) {
+        mSource = source;
+    }
+
+    @Override
+    public InputStream getInputStream() throws MessagingException {
+        return mSource;
+    }
+
+    /**
+     * This method consumes the input stream, so can only be called once
+     * @param out Stream to write to
+     * @throws IllegalStateException If called more than once
+     * @throws IOException
+     * @throws MessagingException
+     */
+    @Override
+    public void writeTo(OutputStream out)
+            throws IllegalStateException, IOException, MessagingException {
+        if (mAlreadyWritten) {
+            throw new IllegalStateException("Base64Body can only be written once");
+        }
+        mAlreadyWritten = true;
+        try {
+            final Base64OutputStream b64out = new Base64OutputStream(out, Base64.DEFAULT);
+            IOUtils.copyLarge(mSource, b64out);
+        } finally {
+            mSource.close();
+        }
+    }
+}
diff --git a/src/com/android/phone/common/mail/Body.java b/src/com/android/phone/common/mail/Body.java
new file mode 100644
index 0000000..8625ef2
--- /dev/null
+++ b/src/com/android/phone/common/mail/Body.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2015 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.phone.common.mail;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+public interface Body {
+    public InputStream getInputStream() throws MessagingException;
+    public void writeTo(OutputStream out) throws IOException, MessagingException;
+}
diff --git a/src/com/android/phone/common/mail/BodyPart.java b/src/com/android/phone/common/mail/BodyPart.java
new file mode 100644
index 0000000..78140c8
--- /dev/null
+++ b/src/com/android/phone/common/mail/BodyPart.java
@@ -0,0 +1,24 @@
+/*

+ * Copyright (C) 2015 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.phone.common.mail;

+

+public abstract class BodyPart implements Part {

+    protected Multipart mParent;

+

+    public Multipart getParent() {

+        return mParent;

+    }

+}

diff --git a/src/com/android/phone/common/mail/FetchProfile.java b/src/com/android/phone/common/mail/FetchProfile.java
new file mode 100644
index 0000000..01c1fcf
--- /dev/null
+++ b/src/com/android/phone/common/mail/FetchProfile.java
@@ -0,0 +1,84 @@
+/*

+ * Copyright (C) 2015 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.phone.common.mail;

+

+import java.util.ArrayList;

+

+/**

+ * <pre>

+ * A FetchProfile is a list of items that should be downloaded in bulk for a set of messages.

+ * FetchProfile can contain the following objects:

+ *      FetchProfile.Item:      Described below.

+ *      Message:                Indicates that the body of the entire message should be fetched.

+ *                              Synonymous with FetchProfile.Item.BODY.

+ *      Part:                   Indicates that the given Part should be fetched. The provider

+ *                              is expected have previously created the given BodyPart and stored

+ *                              any information it needs to download the content.

+ * </pre>

+ */

+public class FetchProfile extends ArrayList<Fetchable> {

+    /**

+     * Default items available for pre-fetching. It should be expected that any

+     * item fetched by using these items could potentially include all of the

+     * previous items.

+     */

+    public enum Item implements Fetchable {

+        /**

+         * Download the flags of the message.

+         */

+        FLAGS,

+

+        /**

+         * Download the envelope of the message. This should include at minimum

+         * the size and the following headers: date, subject, from, content-type, to, cc

+         */

+        ENVELOPE,

+

+        /**

+         * Download the structure of the message. This maps directly to IMAP's BODYSTRUCTURE

+         * and may map to other providers.

+         * The provider should, if possible, fill in a properly formatted MIME structure in

+         * the message without actually downloading any message data. If the provider is not

+         * capable of this operation it should specifically set the body of the message to null

+         * so that upper levels can detect that a full body download is needed.

+         */

+        STRUCTURE,

+

+        /**

+         * A sane portion of the entire message, cut off at a provider determined limit.

+         * This should generaly be around 50kB.

+         */

+        BODY_SANE,

+

+        /**

+         * The entire message.

+         */

+        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;

+    }

+}

diff --git a/src/com/android/phone/common/mail/Fetchable.java b/src/com/android/phone/common/mail/Fetchable.java
new file mode 100644
index 0000000..829c672
--- /dev/null
+++ b/src/com/android/phone/common/mail/Fetchable.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2015 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.phone.common.mail;
+
+/**
+ * Interface for classes that can be added to {@link FetchProfile}.
+ * i.e. {@link Part} and its subclasses, and {@link FetchProfile.Item}.
+ */
+public interface Fetchable {
+}
diff --git a/src/com/android/phone/common/mail/Flag.java b/src/com/android/phone/common/mail/Flag.java
new file mode 100644
index 0000000..aa5d7e9
--- /dev/null
+++ b/src/com/android/phone/common/mail/Flag.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2015 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.phone.common.mail;
+
+/**
+ * Flags that can be applied to Messages.
+ */
+public class Flag {
+    // If adding new flags: ALL FLAGS MUST BE UPPER CASE.
+    public static final String DELETED = "deleted";
+    public static final String SEEN = "seen";
+    public static final String ANSWERED = "answered";
+    public static final String FLAGGED = "flagged";
+    public static final String DRAFT = "draft";
+    public static final String RECENT = "recent";
+}
diff --git a/src/com/android/phone/common/mail/MailTransport.java b/src/com/android/phone/common/mail/MailTransport.java
index 5e3f7f5..c66130c 100644
--- a/src/com/android/phone/common/mail/MailTransport.java
+++ b/src/com/android/phone/common/mail/MailTransport.java
@@ -16,9 +16,9 @@
 package com.android.phone.common.mail;
 
 import android.content.Context;
-import android.util.Log;
 
 import com.android.phone.common.mail.store.ImapStore;
+import com.android.phone.common.mail.utils.LogUtils;
 
 import java.net.SocketAddress;
 
@@ -87,9 +87,7 @@
      * an SSL connection if indicated.
      */
     public void open() throws MessagingException, CertificateValidationException {
-        if (Log.isLoggable(TAG, Log.DEBUG)) {
-            Log.d(TAG, "*** IMAP open " + mHost + ":" + String.valueOf(mPort));
-        }
+        LogUtils.d(TAG, "*** IMAP open " + mHost + ":" + String.valueOf(mPort));
 
         try {
             SocketAddress socketAddress = new InetSocketAddress(mHost, mPort);
@@ -108,19 +106,13 @@
             mOut = new BufferedOutputStream(mSocket.getOutputStream(), 512);
             mSocket.setSoTimeout(SOCKET_READ_TIMEOUT);
         } catch (SSLException e) {
-            if (Log.isLoggable(TAG, Log.DEBUG)) {
-                Log.d(TAG, e.toString());
-            }
+            LogUtils.d(TAG, e.toString());
             throw new CertificateValidationException(e.getMessage(), e);
         } catch (IOException ioe) {
-            if (Log.isLoggable(TAG, Log.DEBUG)) {
-                Log.d(TAG, ioe.toString());
-            }
+            LogUtils.d(TAG, ioe.toString());
             throw new MessagingException(MessagingException.IOERROR, ioe.toString());
         } catch (IllegalArgumentException iae) {
-            if (Log.isLoggable(TAG, Log.DEBUG)) {
-                Log.d(TAG, iae.toString());
-            }
+            LogUtils.d(TAG, iae.toString());
             throw new MessagingException(MessagingException.UNSPECIFIED_EXCEPTION, iae.toString());
         }
     }
@@ -203,12 +195,10 @@
      * Writes a single line to the server using \r\n termination.
      */
     public void writeLine(String s, String sensitiveReplacement) throws IOException {
-        if (Log.isLoggable(TAG, Log.DEBUG)) {
-            if (sensitiveReplacement != null) {
-                Log.d(TAG, ">>> " + sensitiveReplacement);
-            } else {
-                Log.d(TAG, ">>> " + s);
-            }
+        if (sensitiveReplacement != null) {
+            LogUtils.d(TAG, ">>> " + sensitiveReplacement);
+        } else {
+            LogUtils.d(TAG, ">>> " + s);
         }
 
         OutputStream out = getOutputStream();
@@ -235,12 +225,12 @@
                 sb.append((char)d);
             }
         }
-        if (d == -1 && Log.isLoggable(TAG, Log.DEBUG)) {
-            Log.d(TAG, "End of stream reached while trying to read line.");
+        if (d == -1) {
+            LogUtils.d(TAG, "End of stream reached while trying to read line.");
         }
         String ret = sb.toString();
-        if (loggable && Log.isLoggable(TAG, Log.DEBUG)) {
-            Log.d(TAG, "<<< " + ret);
+        if (loggable) {
+            LogUtils.d(TAG, "<<< " + ret);
         }
         return ret;
     }
diff --git a/src/com/android/phone/common/mail/MeetingInfo.java b/src/com/android/phone/common/mail/MeetingInfo.java
new file mode 100644
index 0000000..9fbafe7
--- /dev/null
+++ b/src/com/android/phone/common/mail/MeetingInfo.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2015 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.phone.common.mail;
+
+public class MeetingInfo {
+    // Predefined tags; others can be added
+    public static final String MEETING_DTSTAMP = "DTSTAMP";
+    public static final String MEETING_UID = "UID";
+    public static final String MEETING_ORGANIZER_EMAIL = "ORGMAIL";
+    public static final String MEETING_DTSTART = "DTSTART";
+    public static final String MEETING_DTEND = "DTEND";
+    public static final String MEETING_TITLE = "TITLE";
+    public static final String MEETING_LOCATION = "LOC";
+    public static final String MEETING_RESPONSE_REQUESTED = "RESPONSE";
+    public static final String MEETING_ALL_DAY = "ALLDAY";
+}
diff --git a/src/com/android/phone/common/mail/Message.java b/src/com/android/phone/common/mail/Message.java
index 8f52b83..0a5ed35 100644
--- a/src/com/android/phone/common/mail/Message.java
+++ b/src/com/android/phone/common/mail/Message.java
@@ -17,26 +17,77 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 
+import java.util.Date;
 import java.util.HashSet;
 
-/**
- * Object to represent an email message.
- */
-public class Message {
-    public static final String FLAG_SEEN = "seen";
-    public static final String FLAG_DELETED = "deleted";
-    public static final String FLAG_FLAGGED = "flagged";
-    public static final String FLAG_ANSWERED = "answered";
+public abstract class Message implements Part, Body {
+    public static final Message[] EMPTY_ARRAY = new Message[0];
+
+    public static final String RECIPIENT_TYPE_TO = "to";
+    public static final String RECIPIENT_TYPE_CC = "cc";
+    public static final String RECIPIENT_TYPE_BCC = "bcc";
+    public enum RecipientType {
+        TO, CC, BCC,
+    }
 
     protected String mUid;
+
     private HashSet<String> mFlags = null;
 
+    protected Date mInternalDate;
+
     public String getUid() {
         return mUid;
     }
 
     public void setUid(String uid) {
-        mUid = uid;
+        this.mUid = uid;
+    }
+
+    public abstract String getSubject() throws MessagingException;
+
+    public abstract void setSubject(String subject) throws MessagingException;
+
+    public Date getInternalDate() {
+        return mInternalDate;
+    }
+
+    public void setInternalDate(Date internalDate) {
+        this.mInternalDate = internalDate;
+    }
+
+    public abstract Date getReceivedDate() throws MessagingException;
+
+    public abstract Date getSentDate() throws MessagingException;
+
+    public abstract void setSentDate(Date sentDate) throws MessagingException;
+
+    public abstract Address[] getRecipients(String type) throws MessagingException;
+
+    public abstract void setRecipients(String type, Address[] addresses)
+            throws MessagingException;
+
+    public void setRecipient(String type, Address address) throws MessagingException {
+        setRecipients(type, new Address[] {
+            address
+        });
+    }
+
+    public abstract Address[] getFrom() throws MessagingException;
+
+    public abstract void setFrom(Address from) throws MessagingException;
+
+    public abstract Address[] getReplyTo() throws MessagingException;
+
+    public abstract void setReplyTo(Address[] from) throws MessagingException;
+
+    // Always use these instead of getHeader("Message-ID") or setHeader("Message-ID");
+    public abstract void setMessageId(String messageId) throws MessagingException;
+    public abstract String getMessageId() throws MessagingException;
+
+    @Override
+    public boolean isMimeType(String mimeType) throws MessagingException {
+        return getContentType().startsWith(mimeType);
     }
 
     private HashSet<String> getFlagSet() {
@@ -46,6 +97,13 @@
         return mFlags;
     }
 
+    /*
+     * TODO Refactor Flags at some point to be able to store user defined flags.
+     */
+    public String[] getFlags() {
+        return getFlagSet().toArray(new String[] {});
+    }
+
     /**
      * Set/clear a flag directly, without involving overrides of {@link #setFlag} in subclasses.
      * Only used for testing.
@@ -64,7 +122,7 @@
     }
 
     /**
-     * This method calls setFlag(Flag, boolean)
+     * This method calls setFlag(String, boolean)
      * @param flags
      * @param set
      */
@@ -73,4 +131,15 @@
             setFlag(flag, set);
         }
     }
+
+    public boolean isSet(String flag) {
+        return getFlagSet().contains(flag);
+    }
+
+    public abstract void saveChanges() throws MessagingException;
+
+    @Override
+    public String toString() {
+        return getClass().getSimpleName() + ':' + mUid;
+    }
 }
diff --git a/src/com/android/phone/common/mail/MessageDateComparator.java b/src/com/android/phone/common/mail/MessageDateComparator.java
new file mode 100644
index 0000000..5d8baa7
--- /dev/null
+++ b/src/com/android/phone/common/mail/MessageDateComparator.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2015 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.phone.common.mail;
+
+import java.util.Comparator;
+
+public class MessageDateComparator implements Comparator<Message> {
+    @Override
+    public int compare(Message o1, Message o2) {
+        try {
+            if (o1.getSentDate() == null) {
+                return 1;
+            } else if (o2.getSentDate() == null) {
+                return -1;
+            } else
+                return o2.getSentDate().compareTo(o1.getSentDate());
+        } catch (Exception e) {
+            return 0;
+        }
+    }
+}
diff --git a/src/com/android/phone/common/mail/Multipart.java b/src/com/android/phone/common/mail/Multipart.java
new file mode 100644
index 0000000..77963c8
--- /dev/null
+++ b/src/com/android/phone/common/mail/Multipart.java
@@ -0,0 +1,62 @@
+/*

+ * Copyright (C) 2015 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.phone.common.mail;

+

+import java.util.ArrayList;

+

+public abstract class Multipart implements Body {

+    protected Part mParent;

+

+    protected ArrayList<BodyPart> mParts = new ArrayList<BodyPart>();

+

+    protected String mContentType;

+

+    public void addBodyPart(BodyPart part) throws MessagingException {

+        mParts.add(part);

+    }

+

+    public void addBodyPart(BodyPart part, int index) throws MessagingException {

+        mParts.add(index, part);

+    }

+

+    public BodyPart getBodyPart(int index) throws MessagingException {

+        return mParts.get(index);

+    }

+

+    public String getContentType() throws MessagingException {

+        return mContentType;

+    }

+

+    public int getCount() throws MessagingException {

+        return mParts.size();

+    }

+

+    public boolean removeBodyPart(BodyPart part) throws MessagingException {

+        return mParts.remove(part);

+    }

+

+    public void removeBodyPart(int index) throws MessagingException {

+        mParts.remove(index);

+    }

+

+    public Part getParent() throws MessagingException {

+        return mParent;

+    }

+

+    public void setParent(Part parent) throws MessagingException {

+        this.mParent = parent;

+    }

+}

diff --git a/src/com/android/phone/common/mail/PackedString.java b/src/com/android/phone/common/mail/PackedString.java
new file mode 100644
index 0000000..6cbba9e
--- /dev/null
+++ b/src/com/android/phone/common/mail/PackedString.java
@@ -0,0 +1,175 @@
+/*
+ * Copyright (C) 2015 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.phone.common.mail;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * A utility class for creating and modifying Strings that are tagged and packed together.
+ *
+ * Uses non-printable (control chars) for internal delimiters;  Intended for regular displayable
+ * strings only, so please use base64 or other encoding if you need to hide any binary data here.
+ *
+ * Binary compatible with Address.pack() format, which should migrate to use this code.
+ */
+public class PackedString {
+
+    /**
+     * Packing format is:
+     *   element : [ value ] or [ value TAG-DELIMITER tag ]
+     *   packed-string : [ element ] [ ELEMENT-DELIMITER [ element ] ]*
+     */
+    private static final char DELIMITER_ELEMENT = '\1';
+    private static final char DELIMITER_TAG = '\2';
+
+    private String mString;
+    private HashMap<String, String> mExploded;
+    private static final HashMap<String, String> EMPTY_MAP = new HashMap<String, String>();
+
+    /**
+     * Create a packed string using an already-packed string (e.g. from database)
+     * @param string packed string
+     */
+    public PackedString(String string) {
+        mString = string;
+        mExploded = null;
+    }
+
+    /**
+     * Get the value referred to by a given tag.  If the tag does not exist, return null.
+     * @param tag identifier of string of interest
+     * @return returns value, or null if no string is found
+     */
+    public String get(String tag) {
+        if (mExploded == null) {
+            mExploded = explode(mString);
+        }
+        return mExploded.get(tag);
+    }
+
+    /**
+     * Return a map of all of the values referred to by a given tag.  This is a shallow
+     * copy, don't edit the values.
+     * @return a map of the values in the packed string
+     */
+    public Map<String, String> unpack() {
+        if (mExploded == null) {
+            mExploded = explode(mString);
+        }
+        return new HashMap<String,String>(mExploded);
+    }
+
+    /**
+     * Read out all values into a map.
+     */
+    private static HashMap<String, String> explode(String packed) {
+        if (packed == null || packed.length() == 0) {
+            return EMPTY_MAP;
+        }
+        HashMap<String, String> map = new HashMap<String, String>();
+
+        int length = packed.length();
+        int elementStartIndex = 0;
+        int elementEndIndex = 0;
+        int tagEndIndex = packed.indexOf(DELIMITER_TAG);
+
+        while (elementStartIndex < length) {
+            elementEndIndex = packed.indexOf(DELIMITER_ELEMENT, elementStartIndex);
+            if (elementEndIndex == -1) {
+                elementEndIndex = length;
+            }
+            String tag;
+            String value;
+            if (tagEndIndex == -1 || elementEndIndex <= tagEndIndex) {
+                // in this case the DELIMITER_PERSONAL is in a future pair (or not found)
+                // so synthesize a positional tag for the value, and don't update tagEndIndex
+                value = packed.substring(elementStartIndex, elementEndIndex);
+                tag = Integer.toString(map.size());
+            } else {
+                value = packed.substring(elementStartIndex, tagEndIndex);
+                tag = packed.substring(tagEndIndex + 1, elementEndIndex);
+                // scan forward for next tag, if any
+                tagEndIndex = packed.indexOf(DELIMITER_TAG, elementEndIndex + 1);
+            }
+            map.put(tag, value);
+            elementStartIndex = elementEndIndex + 1;
+        }
+
+        return map;
+    }
+
+    /**
+     * Builder class for creating PackedString values.  Can also be used for editing existing
+     * PackedString representations.
+     */
+    static public class Builder {
+        HashMap<String, String> mMap;
+
+        /**
+         * Create a builder that's empty (for filling)
+         */
+        public Builder() {
+            mMap = new HashMap<String, String>();
+        }
+
+        /**
+         * Create a builder using the values of an existing PackedString (for editing).
+         */
+        public Builder(String packed) {
+            mMap = explode(packed);
+        }
+
+        /**
+         * Add a tagged value
+         * @param tag identifier of string of interest
+         * @param value the value to record in this position.  null to delete entry.
+         */
+        public void put(String tag, String value) {
+            if (value == null) {
+                mMap.remove(tag);
+            } else {
+                mMap.put(tag, value);
+            }
+        }
+
+        /**
+         * Get the value referred to by a given tag.  If the tag does not exist, return null.
+         * @param tag identifier of string of interest
+         * @return returns value, or null if no string is found
+         */
+        public String get(String tag) {
+            return mMap.get(tag);
+        }
+
+        /**
+         * Pack the values and return a single, encoded string
+         */
+        @Override
+        public String toString() {
+            StringBuilder sb = new StringBuilder();
+            for (Map.Entry<String,String> entry : mMap.entrySet()) {
+                if (sb.length() > 0) {
+                    sb.append(DELIMITER_ELEMENT);
+                }
+                sb.append(entry.getValue());
+                sb.append(DELIMITER_TAG);
+                sb.append(entry.getKey());
+            }
+            return sb.toString();
+        }
+    }
+}
diff --git a/src/com/android/phone/common/mail/Part.java b/src/com/android/phone/common/mail/Part.java
new file mode 100644
index 0000000..d064e2a
--- /dev/null
+++ b/src/com/android/phone/common/mail/Part.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2015 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.phone.common.mail;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+public interface Part extends Fetchable {
+    public void addHeader(String name, String value) throws MessagingException;
+
+    public void removeHeader(String name) throws MessagingException;
+
+    public void setHeader(String name, String value) throws MessagingException;
+
+    public Body getBody() throws MessagingException;
+
+    public String getContentType() throws MessagingException;
+
+    public String getDisposition() throws MessagingException;
+
+    public String getContentId() throws MessagingException;
+
+    public String[] getHeader(String name) throws MessagingException;
+
+    public void setExtendedHeader(String name, String value) throws MessagingException;
+
+    public String getExtendedHeader(String name) throws MessagingException;
+
+    public int getSize() throws MessagingException;
+
+    public boolean isMimeType(String mimeType) throws MessagingException;
+
+    public String getMimeType() throws MessagingException;
+
+    public void setBody(Body body) throws MessagingException;
+
+    public void writeTo(OutputStream out) throws IOException, MessagingException;
+}
diff --git a/src/com/android/phone/common/mail/TempDirectory.java b/src/com/android/phone/common/mail/TempDirectory.java
new file mode 100644
index 0000000..8fa0e44
--- /dev/null
+++ b/src/com/android/phone/common/mail/TempDirectory.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2015 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.phone.common.mail;
+
+import android.content.Context;
+
+import java.io.File;
+
+/**
+ * TempDirectory caches the directory used for caching file.  It is set up during application
+ * initialization.
+ */
+public class TempDirectory {
+    private static File sTempDirectory = null;
+
+    public static void setTempDirectory(Context context) {
+        sTempDirectory = context.getCacheDir();
+    }
+
+    public static File getTempDirectory() {
+        if (sTempDirectory == null) {
+            throw new RuntimeException(
+                    "TempDirectory not set.  " +
+                    "If in a unit test, call Email.setTempDirectory(context) in setUp().");
+        }
+        return sTempDirectory;
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/phone/common/mail/internet/BinaryTempFileBody.java b/src/com/android/phone/common/mail/internet/BinaryTempFileBody.java
new file mode 100644
index 0000000..4990bad
--- /dev/null
+++ b/src/com/android/phone/common/mail/internet/BinaryTempFileBody.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2015 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.phone.common.mail.internet;
+
+import com.android.phone.common.mail.Body;
+import com.android.phone.common.mail.MessagingException;
+import com.android.phone.common.mail.TempDirectory;
+
+import org.apache.commons.io.IOUtils;
+
+import android.util.Base64;
+import android.util.Base64OutputStream;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/**
+ * A Body that is backed by a temp file. The Body exposes a getOutputStream method that allows
+ * the user to write to the temp file. After the write the body is available via getInputStream
+ * and writeTo one time. After writeTo is called, or the InputStream returned from
+ * getInputStream is closed the file is deleted and the Body should be considered disposed of.
+ */
+public class BinaryTempFileBody implements Body {
+    private File mFile;
+
+    /**
+     * An alternate way to put data into a BinaryTempFileBody is to simply supply an already-
+     * created file.  Note that this file will be deleted after it is read.
+     * @param filePath The file containing the data to be stored on disk temporarily
+     */
+    public void setFile(String filePath) {
+        mFile = new File(filePath);
+    }
+
+    public OutputStream getOutputStream() throws IOException {
+        mFile = File.createTempFile("body", null, TempDirectory.getTempDirectory());
+        mFile.deleteOnExit();
+        return new FileOutputStream(mFile);
+    }
+
+    @Override
+    public InputStream getInputStream() throws MessagingException {
+        try {
+            return new BinaryTempFileBodyInputStream(new FileInputStream(mFile));
+        }
+        catch (IOException ioe) {
+            throw new MessagingException("Unable to open body", ioe);
+        }
+    }
+
+    @Override
+    public void writeTo(OutputStream out) throws IOException, MessagingException {
+        InputStream in = getInputStream();
+        Base64OutputStream base64Out = new Base64OutputStream(
+            out, Base64.CRLF | Base64.NO_CLOSE);
+        IOUtils.copy(in, base64Out);
+        base64Out.close();
+        mFile.delete();
+    }
+
+    class BinaryTempFileBodyInputStream extends FilterInputStream {
+        public BinaryTempFileBodyInputStream(InputStream in) {
+            super(in);
+        }
+
+        @Override
+        public void close() throws IOException {
+            super.close();
+            mFile.delete();
+        }
+    }
+}
diff --git a/src/com/android/phone/common/mail/internet/MimeBodyPart.java b/src/com/android/phone/common/mail/internet/MimeBodyPart.java
new file mode 100644
index 0000000..286a4f2
--- /dev/null
+++ b/src/com/android/phone/common/mail/internet/MimeBodyPart.java
@@ -0,0 +1,207 @@
+/*
+ * Copyright (C) 2015 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.phone.common.mail.internet;
+
+import com.android.phone.common.mail.Body;
+import com.android.phone.common.mail.BodyPart;
+import com.android.phone.common.mail.MessagingException;
+
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.util.regex.Pattern;
+
+/**
+ * TODO this is a close approximation of Message, need to update along with
+ * Message.
+ */
+public class MimeBodyPart extends BodyPart {
+    protected MimeHeader mHeader = new MimeHeader();
+    protected MimeHeader mExtendedHeader;
+    protected Body mBody;
+    protected int mSize;
+
+    // regex that matches content id surrounded by "<>" optionally.
+    private static final Pattern REMOVE_OPTIONAL_BRACKETS = Pattern.compile("^<?([^>]+)>?$");
+    // regex that matches end of line.
+    private static final Pattern END_OF_LINE = Pattern.compile("\r?\n");
+
+    public MimeBodyPart() throws MessagingException {
+        this(null);
+    }
+
+    public MimeBodyPart(Body body) throws MessagingException {
+        this(body, null);
+    }
+
+    public MimeBodyPart(Body body, String mimeType) throws MessagingException {
+        if (mimeType != null) {
+            setHeader(MimeHeader.HEADER_CONTENT_TYPE, mimeType);
+        }
+        setBody(body);
+    }
+
+    protected String getFirstHeader(String name) throws MessagingException {
+        return mHeader.getFirstHeader(name);
+    }
+
+    @Override
+    public void addHeader(String name, String value) throws MessagingException {
+        mHeader.addHeader(name, value);
+    }
+
+    @Override
+    public void setHeader(String name, String value) throws MessagingException {
+        mHeader.setHeader(name, value);
+    }
+
+    @Override
+    public String[] getHeader(String name) throws MessagingException {
+        return mHeader.getHeader(name);
+    }
+
+    @Override
+    public void removeHeader(String name) throws MessagingException {
+        mHeader.removeHeader(name);
+    }
+
+    @Override
+    public Body getBody() throws MessagingException {
+        return mBody;
+    }
+
+    @Override
+    public void setBody(Body body) throws MessagingException {
+        this.mBody = body;
+        if (body instanceof com.android.phone.common.mail.Multipart) {
+            com.android.phone.common.mail.Multipart multipart =
+                ((com.android.phone.common.mail.Multipart)body);
+            multipart.setParent(this);
+            setHeader(MimeHeader.HEADER_CONTENT_TYPE, multipart.getContentType());
+        }
+        else if (body instanceof TextBody) {
+            String contentType = String.format("%s;\n charset=utf-8", getMimeType());
+            String name = MimeUtility.getHeaderParameter(getContentType(), "name");
+            if (name != null) {
+                contentType += String.format(";\n name=\"%s\"", name);
+            }
+            setHeader(MimeHeader.HEADER_CONTENT_TYPE, contentType);
+            setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, "base64");
+        }
+    }
+
+    @Override
+    public String getContentType() throws MessagingException {
+        String contentType = getFirstHeader(MimeHeader.HEADER_CONTENT_TYPE);
+        if (contentType == null) {
+            return "text/plain";
+        } else {
+            return contentType;
+        }
+    }
+
+    @Override
+    public String getDisposition() throws MessagingException {
+        String contentDisposition = getFirstHeader(MimeHeader.HEADER_CONTENT_DISPOSITION);
+        if (contentDisposition == null) {
+            return null;
+        } else {
+            return contentDisposition;
+        }
+    }
+
+    @Override
+    public String getContentId() throws MessagingException {
+        String contentId = getFirstHeader(MimeHeader.HEADER_CONTENT_ID);
+        if (contentId == null) {
+            return null;
+        } else {
+            // remove optionally surrounding brackets.
+            return REMOVE_OPTIONAL_BRACKETS.matcher(contentId).replaceAll("$1");
+        }
+    }
+
+    @Override
+    public String getMimeType() throws MessagingException {
+        return MimeUtility.getHeaderParameter(getContentType(), null);
+    }
+
+    @Override
+    public boolean isMimeType(String mimeType) throws MessagingException {
+        return getMimeType().equals(mimeType);
+    }
+
+    public void setSize(int size) {
+        this.mSize = size;
+    }
+
+    @Override
+    public int getSize() throws MessagingException {
+        return mSize;
+    }
+
+    /**
+     * Set extended header
+     *
+     * @param name Extended header name
+     * @param value header value - flattened by removing CR-NL if any
+     * remove header if value is null
+     * @throws MessagingException
+     */
+    @Override
+    public void setExtendedHeader(String name, String value) throws MessagingException {
+        if (value == null) {
+            if (mExtendedHeader != null) {
+                mExtendedHeader.removeHeader(name);
+            }
+            return;
+        }
+        if (mExtendedHeader == null) {
+            mExtendedHeader = new MimeHeader();
+        }
+        mExtendedHeader.setHeader(name, END_OF_LINE.matcher(value).replaceAll(""));
+    }
+
+    /**
+     * Get extended header
+     *
+     * @param name Extended header name
+     * @return header value - null if header does not exist
+     * @throws MessagingException
+     */
+    @Override
+    public String getExtendedHeader(String name) throws MessagingException {
+        if (mExtendedHeader == null) {
+            return null;
+        }
+        return mExtendedHeader.getFirstHeader(name);
+    }
+
+    /**
+     * Write the MimeMessage out in MIME format.
+     */
+    @Override
+    public void writeTo(OutputStream out) throws IOException, MessagingException {
+        BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out), 1024);
+        mHeader.writeTo(out);
+        writer.write("\r\n");
+        writer.flush();
+        if (mBody != null) {
+            mBody.writeTo(out);
+        }
+    }
+}
diff --git a/src/com/android/phone/common/mail/internet/MimeHeader.java b/src/com/android/phone/common/mail/internet/MimeHeader.java
new file mode 100644
index 0000000..7e6a07f
--- /dev/null
+++ b/src/com/android/phone/common/mail/internet/MimeHeader.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2015 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.phone.common.mail.internet;
+
+import com.android.phone.common.mail.MessagingException;
+
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.util.ArrayList;
+
+public class MimeHeader {
+    /**
+     * Application specific header that contains Store specific information about an attachment.
+     * In IMAP this contains the IMAP BODYSTRUCTURE part id so that the ImapStore can later
+     * retrieve the attachment at will from the server.
+     * The info is recorded from this header on LocalStore.appendMessage and is put back
+     * into the MIME data by LocalStore.fetch.
+     */
+    public static final String HEADER_ANDROID_ATTACHMENT_STORE_DATA = "X-Android-Attachment-StoreData";
+
+    public static final String HEADER_CONTENT_TYPE = "Content-Type";
+    public static final String HEADER_CONTENT_TRANSFER_ENCODING = "Content-Transfer-Encoding";
+    public static final String HEADER_CONTENT_DISPOSITION = "Content-Disposition";
+    public static final String HEADER_CONTENT_ID = "Content-ID";
+
+    /**
+     * Fields that should be omitted when writing the header using writeTo()
+     */
+    private static final String[] WRITE_OMIT_FIELDS = {
+//        HEADER_ANDROID_ATTACHMENT_DOWNLOADED,
+//        HEADER_ANDROID_ATTACHMENT_ID,
+        HEADER_ANDROID_ATTACHMENT_STORE_DATA
+    };
+
+    protected final ArrayList<Field> mFields = new ArrayList<Field>();
+
+    public void clear() {
+        mFields.clear();
+    }
+
+    public String getFirstHeader(String name) throws MessagingException {
+        String[] header = getHeader(name);
+        if (header == null) {
+            return null;
+        }
+        return header[0];
+    }
+
+    public void addHeader(String name, String value) throws MessagingException {
+        mFields.add(new Field(name, value));
+    }
+
+    public void setHeader(String name, String value) throws MessagingException {
+        if (name == null || value == null) {
+            return;
+        }
+        removeHeader(name);
+        addHeader(name, value);
+    }
+
+    public String[] getHeader(String name) throws MessagingException {
+        ArrayList<String> values = new ArrayList<String>();
+        for (Field field : mFields) {
+            if (field.name.equalsIgnoreCase(name)) {
+                values.add(field.value);
+            }
+        }
+        if (values.size() == 0) {
+            return null;
+        }
+        return values.toArray(new String[] {});
+    }
+
+    public void removeHeader(String name) throws MessagingException {
+        ArrayList<Field> removeFields = new ArrayList<Field>();
+        for (Field field : mFields) {
+            if (field.name.equalsIgnoreCase(name)) {
+                removeFields.add(field);
+            }
+        }
+        mFields.removeAll(removeFields);
+    }
+
+    /**
+     * Write header into String
+     *
+     * @return CR-NL separated header string except the headers in writeOmitFields
+     * null if header is empty
+     */
+    public String writeToString() {
+        if (mFields.size() == 0) {
+            return null;
+        }
+        StringBuilder builder = new StringBuilder();
+        for (Field field : mFields) {
+            if (!arrayContains(WRITE_OMIT_FIELDS, field.name)) {
+                builder.append(field.name + ": " + field.value + "\r\n");
+            }
+        }
+        return builder.toString();
+    }
+
+    public void writeTo(OutputStream out) throws IOException, MessagingException {
+        BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out), 1024);
+        for (Field field : mFields) {
+            if (!arrayContains(WRITE_OMIT_FIELDS, field.name)) {
+                writer.write(field.name + ": " + field.value + "\r\n");
+            }
+        }
+        writer.flush();
+    }
+
+    private static class Field {
+        final String name;
+        final String value;
+
+        public Field(String name, String value) {
+            this.name = name;
+            this.value = value;
+        }
+
+        @Override
+        public String toString() {
+            return name + "=" + value;
+        }
+    }
+
+    @Override
+    public String toString() {
+        return (mFields == null) ? null : mFields.toString();
+    }
+
+    public final static boolean arrayContains(Object[] a, Object o) {
+        int index = arrayIndex(a, o);
+        return (index >= 0);
+    }
+
+    public final static int arrayIndex(Object[] a, Object o) {
+        for (int i = 0, count = a.length; i < count; i++) {
+            if (a[i].equals(o)) {
+                return i;
+            }
+        }
+        return -1;
+    }
+}
diff --git a/src/com/android/phone/common/mail/internet/MimeMessage.java b/src/com/android/phone/common/mail/internet/MimeMessage.java
new file mode 100644
index 0000000..f4c6c88
--- /dev/null
+++ b/src/com/android/phone/common/mail/internet/MimeMessage.java
@@ -0,0 +1,675 @@
+/*
+ * Copyright (C) 2015 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.phone.common.mail.internet;
+
+import com.android.phone.common.mail.Address;
+import com.android.phone.common.mail.Body;
+import com.android.phone.common.mail.BodyPart;
+import com.android.phone.common.mail.Message;
+import com.android.phone.common.mail.MessagingException;
+import com.android.phone.common.mail.Multipart;
+import com.android.phone.common.mail.Part;
+import com.android.phone.common.mail.utils.LogUtils;
+
+import org.apache.james.mime4j.BodyDescriptor;
+import org.apache.james.mime4j.ContentHandler;
+import org.apache.james.mime4j.EOLConvertingInputStream;
+import org.apache.james.mime4j.MimeStreamParser;
+import org.apache.james.mime4j.field.DateTimeField;
+import org.apache.james.mime4j.field.Field;
+
+import android.text.TextUtils;
+
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+import java.util.Stack;
+import java.util.regex.Pattern;
+
+/**
+ * An implementation of Message that stores all of its metadata in RFC 822 and
+ * RFC 2045 style headers.
+ *
+ * NOTE:  Automatic generation of a local message-id is becoming unwieldy and should be removed.
+ * It would be better to simply do it explicitly on local creation of new outgoing messages.
+ */
+public class MimeMessage extends Message {
+    private MimeHeader mHeader;
+    private MimeHeader mExtendedHeader;
+
+    // NOTE:  The fields here are transcribed out of headers, and values stored here will supersede
+    // the values found in the headers.  Use caution to prevent any out-of-phase errors.  In
+    // particular, any adds/changes/deletes here must be echoed by changes in the parse() function.
+    private Address[] mFrom;
+    private Address[] mTo;
+    private Address[] mCc;
+    private Address[] mBcc;
+    private Address[] mReplyTo;
+    private Date mSentDate;
+    private Body mBody;
+    protected int mSize;
+    private boolean mInhibitLocalMessageId = false;
+    private boolean mComplete = true;
+
+    // Shared random source for generating local message-id values
+    private static final java.util.Random sRandom = new java.util.Random();
+
+    // In MIME, en_US-like date format should be used. In other words "MMM" should be encoded to
+    // "Jan", not the other localized format like "Ene" (meaning January in locale es).
+    // This conversion is used when generating outgoing MIME messages. Incoming MIME date
+    // headers are parsed by org.apache.james.mime4j.field.DateTimeField which does not have any
+    // localization code.
+    private static final SimpleDateFormat DATE_FORMAT =
+        new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z", Locale.US);
+
+    // regex that matches content id surrounded by "<>" optionally.
+    private static final Pattern REMOVE_OPTIONAL_BRACKETS = Pattern.compile("^<?([^>]+)>?$");
+    // regex that matches end of line.
+    private static final Pattern END_OF_LINE = Pattern.compile("\r?\n");
+
+    public MimeMessage() {
+        mHeader = null;
+    }
+
+    /**
+     * Generate a local message id.  This is only used when none has been assigned, and is
+     * installed lazily.  Any remote (typically server-assigned) message id takes precedence.
+     * @return a long, locally-generated message-ID value
+     */
+    private static String generateMessageId() {
+        final StringBuilder sb = new StringBuilder();
+        sb.append("<");
+        for (int i = 0; i < 24; i++) {
+            // We'll use a 5-bit range (0..31)
+            final int value = sRandom.nextInt() & 31;
+            final char c = "0123456789abcdefghijklmnopqrstuv".charAt(value);
+            sb.append(c);
+        }
+        sb.append(".");
+        sb.append(Long.toString(System.currentTimeMillis()));
+        sb.append("@email.android.com>");
+        return sb.toString();
+    }
+
+    /**
+     * Parse the given InputStream using Apache Mime4J to build a MimeMessage.
+     *
+     * @param in InputStream providing message content
+     * @throws IOException
+     * @throws MessagingException
+     */
+    public MimeMessage(InputStream in) throws IOException, MessagingException {
+        parse(in);
+    }
+
+    private MimeStreamParser init() {
+        // Before parsing the input stream, clear all local fields that may be superceded by
+        // the new incoming message.
+        getMimeHeaders().clear();
+        mInhibitLocalMessageId = true;
+        mFrom = null;
+        mTo = null;
+        mCc = null;
+        mBcc = null;
+        mReplyTo = null;
+        mSentDate = null;
+        mBody = null;
+
+        final MimeStreamParser parser = new MimeStreamParser();
+        parser.setContentHandler(new MimeMessageBuilder());
+        return parser;
+    }
+
+    protected void parse(InputStream in) throws IOException, MessagingException {
+        final MimeStreamParser parser = init();
+        parser.parse(new EOLConvertingInputStream(in));
+        mComplete = !parser.getPrematureEof();
+    }
+
+    public void parse(InputStream in, EOLConvertingInputStream.Callback callback)
+            throws IOException, MessagingException {
+        final MimeStreamParser parser = init();
+        parser.parse(new EOLConvertingInputStream(in, getSize(), callback));
+        mComplete = !parser.getPrematureEof();
+    }
+
+    /**
+     * Return the internal mHeader value, with very lazy initialization.
+     * The goal is to save memory by not creating the headers until needed.
+     */
+    private MimeHeader getMimeHeaders() {
+        if (mHeader == null) {
+            mHeader = new MimeHeader();
+        }
+        return mHeader;
+    }
+
+    @Override
+    public Date getReceivedDate() throws MessagingException {
+        return null;
+    }
+
+    @Override
+    public Date getSentDate() throws MessagingException {
+        if (mSentDate == null) {
+            try {
+                DateTimeField field = (DateTimeField)Field.parse("Date: "
+                        + MimeUtility.unfoldAndDecode(getFirstHeader("Date")));
+                mSentDate = field.getDate();
+                // TODO: We should make it more clear what exceptions can be thrown here,
+                // and whether they reflect a normal or error condition.
+            } catch (Exception e) {
+                LogUtils.v(LogUtils.TAG, "Message missing Date header");
+            }
+        }
+        if (mSentDate == null) {
+            // If we still don't have a date, fall back to "Delivery-date"
+            try {
+                DateTimeField field = (DateTimeField)Field.parse("Date: "
+                        + MimeUtility.unfoldAndDecode(getFirstHeader("Delivery-date")));
+                mSentDate = field.getDate();
+                // TODO: We should make it more clear what exceptions can be thrown here,
+                // and whether they reflect a normal or error condition.
+            } catch (Exception e) {
+                LogUtils.v(LogUtils.TAG, "Message also missing Delivery-Date header");
+            }
+        }
+        return mSentDate;
+    }
+
+    @Override
+    public void setSentDate(Date sentDate) throws MessagingException {
+        setHeader("Date", DATE_FORMAT.format(sentDate));
+        this.mSentDate = sentDate;
+    }
+
+    @Override
+    public String getContentType() throws MessagingException {
+        final String contentType = getFirstHeader(MimeHeader.HEADER_CONTENT_TYPE);
+        if (contentType == null) {
+            return "text/plain";
+        } else {
+            return contentType;
+        }
+    }
+
+    @Override
+    public String getDisposition() throws MessagingException {
+        return getFirstHeader(MimeHeader.HEADER_CONTENT_DISPOSITION);
+    }
+
+    @Override
+    public String getContentId() throws MessagingException {
+        final String contentId = getFirstHeader(MimeHeader.HEADER_CONTENT_ID);
+        if (contentId == null) {
+            return null;
+        } else {
+            // remove optionally surrounding brackets.
+            return REMOVE_OPTIONAL_BRACKETS.matcher(contentId).replaceAll("$1");
+        }
+    }
+
+    public boolean isComplete() {
+        return mComplete;
+    }
+
+    @Override
+    public String getMimeType() throws MessagingException {
+        return MimeUtility.getHeaderParameter(getContentType(), null);
+    }
+
+    @Override
+    public int getSize() throws MessagingException {
+        return mSize;
+    }
+
+    /**
+     * Returns a list of the given recipient type from this message. If no addresses are
+     * found the method returns an empty array.
+     */
+    @Override
+    public Address[] getRecipients(String type) throws MessagingException {
+        if (type == RECIPIENT_TYPE_TO) {
+            if (mTo == null) {
+                mTo = Address.parse(MimeUtility.unfold(getFirstHeader("To")));
+            }
+            return mTo;
+        } else if (type == RECIPIENT_TYPE_CC) {
+            if (mCc == null) {
+                mCc = Address.parse(MimeUtility.unfold(getFirstHeader("CC")));
+            }
+            return mCc;
+        } else if (type == RECIPIENT_TYPE_BCC) {
+            if (mBcc == null) {
+                mBcc = Address.parse(MimeUtility.unfold(getFirstHeader("BCC")));
+            }
+            return mBcc;
+        } else {
+            throw new MessagingException("Unrecognized recipient type.");
+        }
+    }
+
+    @Override
+    public void setRecipients(String type, Address[] addresses) throws MessagingException {
+        final int TO_LENGTH = 4;  // "To: "
+        final int CC_LENGTH = 4;  // "Cc: "
+        final int BCC_LENGTH = 5; // "Bcc: "
+        if (type == RECIPIENT_TYPE_TO) {
+            if (addresses == null || addresses.length == 0) {
+                removeHeader("To");
+                this.mTo = null;
+            } else {
+                setHeader("To", MimeUtility.fold(Address.toHeader(addresses), TO_LENGTH));
+                this.mTo = addresses;
+            }
+        } else if (type == RECIPIENT_TYPE_CC) {
+            if (addresses == null || addresses.length == 0) {
+                removeHeader("CC");
+                this.mCc = null;
+            } else {
+                setHeader("CC", MimeUtility.fold(Address.toHeader(addresses), CC_LENGTH));
+                this.mCc = addresses;
+            }
+        } else if (type == RECIPIENT_TYPE_BCC) {
+            if (addresses == null || addresses.length == 0) {
+                removeHeader("BCC");
+                this.mBcc = null;
+            } else {
+                setHeader("BCC", MimeUtility.fold(Address.toHeader(addresses), BCC_LENGTH));
+                this.mBcc = addresses;
+            }
+        } else {
+            throw new MessagingException("Unrecognized recipient type.");
+        }
+    }
+
+    /**
+     * Returns the unfolded, decoded value of the Subject header.
+     */
+    @Override
+    public String getSubject() throws MessagingException {
+        return MimeUtility.unfoldAndDecode(getFirstHeader("Subject"));
+    }
+
+    @Override
+    public void setSubject(String subject) throws MessagingException {
+        final int HEADER_NAME_LENGTH = 9;     // "Subject: "
+        setHeader("Subject", MimeUtility.foldAndEncode2(subject, HEADER_NAME_LENGTH));
+    }
+
+    @Override
+    public Address[] getFrom() throws MessagingException {
+        if (mFrom == null) {
+            String list = MimeUtility.unfold(getFirstHeader("From"));
+            if (list == null || list.length() == 0) {
+                list = MimeUtility.unfold(getFirstHeader("Sender"));
+            }
+            mFrom = Address.parse(list);
+        }
+        return mFrom;
+    }
+
+    @Override
+    public void setFrom(Address from) throws MessagingException {
+        final int FROM_LENGTH = 6;  // "From: "
+        if (from != null) {
+            setHeader("From", MimeUtility.fold(from.toHeader(), FROM_LENGTH));
+            this.mFrom = new Address[] {
+                    from
+                };
+        } else {
+            this.mFrom = null;
+        }
+    }
+
+    @Override
+    public Address[] getReplyTo() throws MessagingException {
+        if (mReplyTo == null) {
+            mReplyTo = Address.parse(MimeUtility.unfold(getFirstHeader("Reply-to")));
+        }
+        return mReplyTo;
+    }
+
+    @Override
+    public void setReplyTo(Address[] replyTo) throws MessagingException {
+        final int REPLY_TO_LENGTH = 10;  // "Reply-to: "
+        if (replyTo == null || replyTo.length == 0) {
+            removeHeader("Reply-to");
+            mReplyTo = null;
+        } else {
+            setHeader("Reply-to", MimeUtility.fold(Address.toHeader(replyTo), REPLY_TO_LENGTH));
+            mReplyTo = replyTo;
+        }
+    }
+
+    /**
+     * Set the mime "Message-ID" header
+     * @param messageId the new Message-ID value
+     * @throws MessagingException
+     */
+    @Override
+    public void setMessageId(String messageId) throws MessagingException {
+        setHeader("Message-ID", messageId);
+    }
+
+    /**
+     * Get the mime "Message-ID" header.  This value will be preloaded with a locally-generated
+     * random ID, if the value has not previously been set.  Local generation can be inhibited/
+     * overridden by explicitly clearing the headers, removing the message-id header, etc.
+     * @return the Message-ID header string, or null if explicitly has been set to null
+     */
+    @Override
+    public String getMessageId() throws MessagingException {
+        String messageId = getFirstHeader("Message-ID");
+        if (messageId == null && !mInhibitLocalMessageId) {
+            messageId = generateMessageId();
+            setMessageId(messageId);
+        }
+        return messageId;
+    }
+
+    @Override
+    public void saveChanges() throws MessagingException {
+        throw new MessagingException("saveChanges not yet implemented");
+    }
+
+    @Override
+    public Body getBody() throws MessagingException {
+        return mBody;
+    }
+
+    @Override
+    public void setBody(Body body) throws MessagingException {
+        this.mBody = body;
+        if (body instanceof Multipart) {
+            final Multipart multipart = ((Multipart)body);
+            multipart.setParent(this);
+            setHeader(MimeHeader.HEADER_CONTENT_TYPE, multipart.getContentType());
+            setHeader("MIME-Version", "1.0");
+        }
+        else if (body instanceof TextBody) {
+            setHeader(MimeHeader.HEADER_CONTENT_TYPE, String.format("%s;\n charset=utf-8",
+                    getMimeType()));
+            setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, "base64");
+        }
+    }
+
+    protected String getFirstHeader(String name) throws MessagingException {
+        return getMimeHeaders().getFirstHeader(name);
+    }
+
+    @Override
+    public void addHeader(String name, String value) throws MessagingException {
+        getMimeHeaders().addHeader(name, value);
+    }
+
+    @Override
+    public void setHeader(String name, String value) throws MessagingException {
+        getMimeHeaders().setHeader(name, value);
+    }
+
+    @Override
+    public String[] getHeader(String name) throws MessagingException {
+        return getMimeHeaders().getHeader(name);
+    }
+
+    @Override
+    public void removeHeader(String name) throws MessagingException {
+        getMimeHeaders().removeHeader(name);
+        if ("Message-ID".equalsIgnoreCase(name)) {
+            mInhibitLocalMessageId = true;
+        }
+    }
+
+    /**
+     * Set extended header
+     *
+     * @param name Extended header name
+     * @param value header value - flattened by removing CR-NL if any
+     * remove header if value is null
+     * @throws MessagingException
+     */
+    @Override
+    public void setExtendedHeader(String name, String value) throws MessagingException {
+        if (value == null) {
+            if (mExtendedHeader != null) {
+                mExtendedHeader.removeHeader(name);
+            }
+            return;
+        }
+        if (mExtendedHeader == null) {
+            mExtendedHeader = new MimeHeader();
+        }
+        mExtendedHeader.setHeader(name, END_OF_LINE.matcher(value).replaceAll(""));
+    }
+
+    /**
+     * Get extended header
+     *
+     * @param name Extended header name
+     * @return header value - null if header does not exist
+     * @throws MessagingException
+     */
+    @Override
+    public String getExtendedHeader(String name) throws MessagingException {
+        if (mExtendedHeader == null) {
+            return null;
+        }
+        return mExtendedHeader.getFirstHeader(name);
+    }
+
+    /**
+     * Set entire extended headers from String
+     *
+     * @param headers Extended header and its value - "CR-NL-separated pairs
+     * if null or empty, remove entire extended headers
+     * @throws MessagingException
+     */
+    public void setExtendedHeaders(String headers) throws MessagingException {
+        if (TextUtils.isEmpty(headers)) {
+            mExtendedHeader = null;
+        } else {
+            mExtendedHeader = new MimeHeader();
+            for (final String header : END_OF_LINE.split(headers)) {
+                final String[] tokens = header.split(":", 2);
+                if (tokens.length != 2) {
+                    throw new MessagingException("Illegal extended headers: " + headers);
+                }
+                mExtendedHeader.setHeader(tokens[0].trim(), tokens[1].trim());
+            }
+        }
+    }
+
+    /**
+     * Get entire extended headers as String
+     *
+     * @return "CR-NL-separated extended headers - null if extended header does not exist
+     */
+    public String getExtendedHeaders() {
+        if (mExtendedHeader != null) {
+            return mExtendedHeader.writeToString();
+        }
+        return null;
+    }
+
+    /**
+     * Write message header and body to output stream
+     *
+     * @param out Output steam to write message header and body.
+     */
+    @Override
+    public void writeTo(OutputStream out) throws IOException, MessagingException {
+        final BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out), 1024);
+        // Force creation of local message-id
+        getMessageId();
+        getMimeHeaders().writeTo(out);
+        // mExtendedHeader will not be write out to external output stream,
+        // because it is intended to internal use.
+        writer.write("\r\n");
+        writer.flush();
+        if (mBody != null) {
+            mBody.writeTo(out);
+        }
+    }
+
+    @Override
+    public InputStream getInputStream() throws MessagingException {
+        return null;
+    }
+
+    class MimeMessageBuilder implements ContentHandler {
+        private final Stack<Object> stack = new Stack<Object>();
+
+        public MimeMessageBuilder() {
+        }
+
+        private void expect(Class<?> c) {
+            if (!c.isInstance(stack.peek())) {
+                throw new IllegalStateException("Internal stack error: " + "Expected '"
+                        + c.getName() + "' found '" + stack.peek().getClass().getName() + "'");
+            }
+        }
+
+        @Override
+        public void startMessage() {
+            if (stack.isEmpty()) {
+                stack.push(MimeMessage.this);
+            } else {
+                expect(Part.class);
+                try {
+                    final MimeMessage m = new MimeMessage();
+                    ((Part)stack.peek()).setBody(m);
+                    stack.push(m);
+                } catch (MessagingException me) {
+                    throw new Error(me);
+                }
+            }
+        }
+
+        @Override
+        public void endMessage() {
+            expect(MimeMessage.class);
+            stack.pop();
+        }
+
+        @Override
+        public void startHeader() {
+            expect(Part.class);
+        }
+
+        @Override
+        public void field(String fieldData) {
+            expect(Part.class);
+            try {
+                final String[] tokens = fieldData.split(":", 2);
+                ((Part)stack.peek()).addHeader(tokens[0], tokens[1].trim());
+            } catch (MessagingException me) {
+                throw new Error(me);
+            }
+        }
+
+        @Override
+        public void endHeader() {
+            expect(Part.class);
+        }
+
+        @Override
+        public void startMultipart(BodyDescriptor bd) {
+            expect(Part.class);
+
+            final Part e = (Part)stack.peek();
+            try {
+                final MimeMultipart multiPart = new MimeMultipart(e.getContentType());
+                e.setBody(multiPart);
+                stack.push(multiPart);
+            } catch (MessagingException me) {
+                throw new Error(me);
+            }
+        }
+
+        @Override
+        public void body(BodyDescriptor bd, InputStream in) throws IOException {
+            expect(Part.class);
+            final Body body = MimeUtility.decodeBody(in, bd.getTransferEncoding());
+            try {
+                ((Part)stack.peek()).setBody(body);
+            } catch (MessagingException me) {
+                throw new Error(me);
+            }
+        }
+
+        @Override
+        public void endMultipart() {
+            stack.pop();
+        }
+
+        @Override
+        public void startBodyPart() {
+            expect(MimeMultipart.class);
+
+            try {
+                final MimeBodyPart bodyPart = new MimeBodyPart();
+                ((MimeMultipart)stack.peek()).addBodyPart(bodyPart);
+                stack.push(bodyPart);
+            } catch (MessagingException me) {
+                throw new Error(me);
+            }
+        }
+
+        @Override
+        public void endBodyPart() {
+            expect(BodyPart.class);
+            stack.pop();
+        }
+
+        @Override
+        public void epilogue(InputStream is) throws IOException {
+            expect(MimeMultipart.class);
+            final StringBuilder sb = new StringBuilder();
+            int b;
+            while ((b = is.read()) != -1) {
+                sb.append((char)b);
+            }
+            // TODO: why is this commented out?
+            // ((Multipart) stack.peek()).setEpilogue(sb.toString());
+        }
+
+        @Override
+        public void preamble(InputStream is) throws IOException {
+            expect(MimeMultipart.class);
+            final StringBuilder sb = new StringBuilder();
+            int b;
+            while ((b = is.read()) != -1) {
+                sb.append((char)b);
+            }
+            try {
+                ((MimeMultipart)stack.peek()).setPreamble(sb.toString());
+            } catch (MessagingException me) {
+                throw new Error(me);
+            }
+        }
+
+        @Override
+        public void raw(InputStream is) throws IOException {
+            throw new UnsupportedOperationException("Not supported");
+        }
+    }
+}
diff --git a/src/com/android/phone/common/mail/internet/MimeMultipart.java b/src/com/android/phone/common/mail/internet/MimeMultipart.java
new file mode 100644
index 0000000..3e118e4
--- /dev/null
+++ b/src/com/android/phone/common/mail/internet/MimeMultipart.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2015 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.phone.common.mail.internet;
+
+import com.android.phone.common.mail.BodyPart;
+import com.android.phone.common.mail.MessagingException;
+import com.android.phone.common.mail.Multipart;
+
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+
+public class MimeMultipart extends Multipart {
+    protected String mPreamble;
+
+    protected String mContentType;
+
+    protected String mBoundary;
+
+    protected String mSubType;
+
+    public MimeMultipart() throws MessagingException {
+        mBoundary = generateBoundary();
+        setSubType("mixed");
+    }
+
+    public MimeMultipart(String contentType) throws MessagingException {
+        this.mContentType = contentType;
+        try {
+            mSubType = MimeUtility.getHeaderParameter(contentType, null).split("/")[1];
+            mBoundary = MimeUtility.getHeaderParameter(contentType, "boundary");
+            if (mBoundary == null) {
+                throw new MessagingException("MultiPart does not contain boundary: " + contentType);
+            }
+        } catch (Exception e) {
+            throw new MessagingException(
+                    "Invalid MultiPart Content-Type; must contain subtype and boundary. ("
+                            + contentType + ")", e);
+        }
+    }
+
+    public String generateBoundary() {
+        StringBuffer sb = new StringBuffer();
+        sb.append("----");
+        for (int i = 0; i < 30; i++) {
+            sb.append(Integer.toString((int)(Math.random() * 35), 36));
+        }
+        return sb.toString().toUpperCase();
+    }
+
+    public String getPreamble() throws MessagingException {
+        return mPreamble;
+    }
+
+    public void setPreamble(String preamble) throws MessagingException {
+        this.mPreamble = preamble;
+    }
+
+    @Override
+    public String getContentType() throws MessagingException {
+        return mContentType;
+    }
+
+    public void setSubType(String subType) throws MessagingException {
+        this.mSubType = subType;
+        mContentType = String.format("multipart/%s; boundary=\"%s\"", subType, mBoundary);
+    }
+
+    @Override
+    public void writeTo(OutputStream out) throws IOException, MessagingException {
+        BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out), 1024);
+
+        if (mPreamble != null) {
+            writer.write(mPreamble + "\r\n");
+        }
+
+        for (int i = 0, count = mParts.size(); i < count; i++) {
+            BodyPart bodyPart = mParts.get(i);
+            writer.write("--" + mBoundary + "\r\n");
+            writer.flush();
+            bodyPart.writeTo(out);
+            writer.write("\r\n");
+        }
+
+        writer.write("--" + mBoundary + "--\r\n");
+        writer.flush();
+    }
+
+    @Override
+    public InputStream getInputStream() throws MessagingException {
+        return null;
+    }
+
+    public String getSubTypeForTest() {
+        return mSubType;
+    }
+}
diff --git a/src/com/android/phone/common/mail/internet/MimeUtility.java b/src/com/android/phone/common/mail/internet/MimeUtility.java
new file mode 100644
index 0000000..ba5036f
--- /dev/null
+++ b/src/com/android/phone/common/mail/internet/MimeUtility.java
@@ -0,0 +1,416 @@
+/*
+ * Copyright (C) 2015 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.phone.common.mail.internet;
+
+import android.text.TextUtils;
+import android.util.Base64;
+import android.util.Base64DataException;
+import android.util.Base64InputStream;
+import android.util.Log;
+
+import com.android.phone.common.mail.Body;
+import com.android.phone.common.mail.BodyPart;
+import com.android.phone.common.mail.Message;
+import com.android.phone.common.mail.MessagingException;
+import com.android.phone.common.mail.Multipart;
+import com.android.phone.common.mail.Part;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.james.mime4j.codec.EncoderUtil;
+import org.apache.james.mime4j.decoder.DecoderUtil;
+import org.apache.james.mime4j.decoder.QuotedPrintableInputStream;
+import org.apache.james.mime4j.util.CharsetUtil;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class MimeUtility {
+    private static final String LOG_TAG = "Email";
+
+    public static final String MIME_TYPE_RFC822 = "message/rfc822";
+    private final static Pattern PATTERN_CR_OR_LF = Pattern.compile("\r|\n");
+
+    /**
+     * Replace sequences of CRLF+WSP with WSP.  Tries to preserve original string
+     * object whenever possible.
+     */
+    public static String unfold(String s) {
+        if (s == null) {
+            return null;
+        }
+        Matcher patternMatcher = PATTERN_CR_OR_LF.matcher(s);
+        if (patternMatcher.find()) {
+            patternMatcher.reset();
+            s = patternMatcher.replaceAll("");
+        }
+        return s;
+    }
+
+    public static String decode(String s) {
+        if (s == null) {
+            return null;
+        }
+        return DecoderUtil.decodeEncodedWords(s);
+    }
+
+    public static String unfoldAndDecode(String s) {
+        return decode(unfold(s));
+    }
+
+    // TODO implement proper foldAndEncode
+    // NOTE: When this really works, we *must* remove all calls to foldAndEncode2() to prevent
+    // duplication of encoding.
+    public static String foldAndEncode(String s) {
+        return s;
+    }
+
+    /**
+     * INTERIM version of foldAndEncode that will be used only by Subject: headers.
+     * This is safer than implementing foldAndEncode() (see above) and risking unknown damage
+     * to other headers.
+     *
+     * TODO: Copy this code to foldAndEncode(), get rid of this function, confirm all working OK.
+     *
+     * @param s original string to encode and fold
+     * @param usedCharacters number of characters already used up by header name
+
+     * @return the String ready to be transmitted
+     */
+    public static String foldAndEncode2(String s, int usedCharacters) {
+        // james.mime4j.codec.EncoderUtil.java
+        // encode:  encodeIfNecessary(text, usage, numUsedInHeaderName)
+        // Usage.TEXT_TOKENlooks like the right thing for subjects
+        // use WORD_ENTITY for address/names
+
+        String encoded = EncoderUtil.encodeIfNecessary(s, EncoderUtil.Usage.TEXT_TOKEN,
+                usedCharacters);
+
+        return fold(encoded, usedCharacters);
+    }
+
+    /**
+     * INTERIM:  From newer version of org.apache.james (but we don't want to import
+     * the entire MimeUtil class).
+     *
+     * Splits the specified string into a multiple-line representation with
+     * lines no longer than 76 characters (because the line might contain
+     * encoded words; see <a href='http://www.faqs.org/rfcs/rfc2047.html'>RFC
+     * 2047</a> section 2). If the string contains non-whitespace sequences
+     * longer than 76 characters a line break is inserted at the whitespace
+     * character following the sequence resulting in a line longer than 76
+     * characters.
+     *
+     * @param s
+     *            string to split.
+     * @param usedCharacters
+     *            number of characters already used up. Usually the number of
+     *            characters for header field name plus colon and one space.
+     * @return a multiple-line representation of the given string.
+     */
+    public static String fold(String s, int usedCharacters) {
+        final int maxCharacters = 76;
+
+        final int length = s.length();
+        if (usedCharacters + length <= maxCharacters)
+            return s;
+
+        StringBuilder sb = new StringBuilder();
+
+        int lastLineBreak = -usedCharacters;
+        int wspIdx = indexOfWsp(s, 0);
+        while (true) {
+            if (wspIdx == length) {
+                sb.append(s.substring(Math.max(0, lastLineBreak)));
+                return sb.toString();
+            }
+
+            int nextWspIdx = indexOfWsp(s, wspIdx + 1);
+
+            if (nextWspIdx - lastLineBreak > maxCharacters) {
+                sb.append(s.substring(Math.max(0, lastLineBreak), wspIdx));
+                sb.append("\r\n");
+                lastLineBreak = wspIdx;
+            }
+
+            wspIdx = nextWspIdx;
+        }
+    }
+
+    /**
+     * INTERIM:  From newer version of org.apache.james (but we don't want to import
+     * the entire MimeUtil class).
+     *
+     * Search for whitespace.
+     */
+    private static int indexOfWsp(String s, int fromIndex) {
+        final int len = s.length();
+        for (int index = fromIndex; index < len; index++) {
+            char c = s.charAt(index);
+            if (c == ' ' || c == '\t')
+                return index;
+        }
+        return len;
+    }
+
+    /**
+     * Returns the named parameter of a header field. If name is null the first
+     * parameter is returned, or if there are no additional parameters in the
+     * field the entire field is returned. Otherwise the named parameter is
+     * searched for in a case insensitive fashion and returned. If the parameter
+     * cannot be found the method returns null.
+     *
+     * TODO: quite inefficient with the inner trimming & splitting.
+     * TODO: Also has a latent bug: uses "startsWith" to match the name, which can false-positive.
+     * TODO: The doc says that for a null name you get the first param, but you get the header.
+     *    Should probably just fix the doc, but if other code assumes that behavior, fix the code.
+     * TODO: Need to decode %-escaped strings, as in: filename="ab%22d".
+     *       ('+' -> ' ' conversion too? check RFC)
+     *
+     * @param header
+     * @param name
+     * @return the entire header (if name=null), the found parameter, or null
+     */
+    public static String getHeaderParameter(String header, String name) {
+        if (header == null) {
+            return null;
+        }
+        String[] parts = unfold(header).split(";");
+        if (name == null) {
+            return parts[0].trim();
+        }
+        String lowerCaseName = name.toLowerCase();
+        for (String part : parts) {
+            if (part.trim().toLowerCase().startsWith(lowerCaseName)) {
+                String[] parameterParts = part.split("=", 2);
+                if (parameterParts.length < 2) {
+                    return null;
+                }
+                String parameter = parameterParts[1].trim();
+                if (parameter.startsWith("\"") && parameter.endsWith("\"")) {
+                    return parameter.substring(1, parameter.length() - 1);
+                } else {
+                    return parameter;
+                }
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Reads the Part's body and returns a String based on any charset conversion that needed
+     * to be done.
+     * @param part The part containing a body
+     * @return a String containing the converted text in the body, or null if there was no text
+     * or an error during conversion.
+     */
+    public static String getTextFromPart(Part part) {
+        try {
+            if (part != null && part.getBody() != null) {
+                InputStream in = part.getBody().getInputStream();
+                String mimeType = part.getMimeType();
+                if (mimeType != null && MimeUtility.mimeTypeMatches(mimeType, "text/*")) {
+                    /*
+                     * Now we read the part into a buffer for further processing. Because
+                     * the stream is now wrapped we'll remove any transfer encoding at this point.
+                     */
+                    ByteArrayOutputStream out = new ByteArrayOutputStream();
+                    IOUtils.copy(in, out);
+                    in.close();
+                    in = null;      // we want all of our memory back, and close might not release
+
+                    /*
+                     * We've got a text part, so let's see if it needs to be processed further.
+                     */
+                    String charset = getHeaderParameter(part.getContentType(), "charset");
+                    if (charset != null) {
+                        /*
+                         * See if there is conversion from the MIME charset to the Java one.
+                         */
+                        charset = CharsetUtil.toJavaCharset(charset);
+                    }
+                    /*
+                     * No encoding, so use us-ascii, which is the standard.
+                     */
+                    if (charset == null) {
+                        charset = "ASCII";
+                    }
+                    /*
+                     * Convert and return as new String
+                     */
+                    String result = out.toString(charset);
+                    out.close();
+                    return result;
+                }
+            }
+
+        }
+        catch (OutOfMemoryError oom) {
+            /*
+             * If we are not able to process the body there's nothing we can do about it. Return
+             * null and let the upper layers handle the missing content.
+             */
+            Log.e(LOG_TAG, "Unable to getTextFromPart " + oom.toString());
+        }
+        catch (Exception e) {
+            /*
+             * If we are not able to process the body there's nothing we can do about it. Return
+             * null and let the upper layers handle the missing content.
+             */
+            Log.e(LOG_TAG, "Unable to getTextFromPart " + e.toString());
+        }
+        return null;
+    }
+
+    /**
+     * Returns true if the given mimeType matches the matchAgainst specification.  The comparison
+     * ignores case and the matchAgainst string may include "*" for a wildcard (e.g. "image/*").
+     *
+     * @param mimeType A MIME type to check.
+     * @param matchAgainst A MIME type to check against. May include wildcards.
+     * @return true if the mimeType matches
+     */
+    public static boolean mimeTypeMatches(String mimeType, String matchAgainst) {
+        Pattern p = Pattern.compile(matchAgainst.replaceAll("\\*", "\\.\\*"),
+                Pattern.CASE_INSENSITIVE);
+        return p.matcher(mimeType).matches();
+    }
+
+    /**
+     * Returns true if the given mimeType matches any of the matchAgainst specifications.  The
+     * comparison ignores case and the matchAgainst strings may include "*" for a wildcard
+     * (e.g. "image/*").
+     *
+     * @param mimeType A MIME type to check.
+     * @param matchAgainst An array of MIME types to check against. May include wildcards.
+     * @return true if the mimeType matches any of the matchAgainst strings
+     */
+    public static boolean mimeTypeMatches(String mimeType, String[] matchAgainst) {
+        for (String matchType : matchAgainst) {
+            if (mimeTypeMatches(mimeType, matchType)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Given an input stream and a transfer encoding, return a wrapped input stream for that
+     * encoding (or the original if none is required)
+     * @param in the input stream
+     * @param contentTransferEncoding the content transfer encoding
+     * @return a properly wrapped stream
+     */
+    public static InputStream getInputStreamForContentTransferEncoding(InputStream in,
+            String contentTransferEncoding) {
+        if (contentTransferEncoding != null) {
+            contentTransferEncoding =
+                MimeUtility.getHeaderParameter(contentTransferEncoding, null);
+            if ("quoted-printable".equalsIgnoreCase(contentTransferEncoding)) {
+                in = new QuotedPrintableInputStream(in);
+            }
+            else if ("base64".equalsIgnoreCase(contentTransferEncoding)) {
+                in = new Base64InputStream(in, Base64.DEFAULT);
+            }
+        }
+        return in;
+    }
+
+    /**
+     * Removes any content transfer encoding from the stream and returns a Body.
+     */
+    public static Body decodeBody(InputStream in, String contentTransferEncoding)
+            throws IOException {
+        /*
+         * We'll remove any transfer encoding by wrapping the stream.
+         */
+        in = getInputStreamForContentTransferEncoding(in, contentTransferEncoding);
+        BinaryTempFileBody tempBody = new BinaryTempFileBody();
+        OutputStream out = tempBody.getOutputStream();
+        try {
+            IOUtils.copy(in, out);
+        } catch (Base64DataException bde) {
+            // TODO Need to fix this somehow
+            //String warning = "\n\n" + Email.getMessageDecodeErrorString();
+            //out.write(warning.getBytes());
+        } finally {
+            out.close();
+        }
+        return tempBody;
+    }
+
+    /**
+     * Recursively scan a Part (usually a Message) and sort out which of its children will be
+     * "viewable" and which will be attachments.
+     *
+     * @param part The part to be broken down
+     * @param viewables This arraylist will be populated with all parts that appear to be
+     * the "message" (e.g. text/plain & text/html)
+     * @param attachments This arraylist will be populated with all parts that appear to be
+     * attachments (including inlines)
+     * @throws MessagingException
+     */
+    public static void collectParts(Part part, ArrayList<Part> viewables,
+            ArrayList<Part> attachments) throws MessagingException {
+        String disposition = part.getDisposition();
+        String dispositionType = MimeUtility.getHeaderParameter(disposition, null);
+        // If a disposition is not specified, default to "inline"
+        boolean inline =
+                TextUtils.isEmpty(dispositionType) || "inline".equalsIgnoreCase(dispositionType);
+        // The lower-case mime type
+        String mimeType = part.getMimeType().toLowerCase();
+
+        if (part.getBody() instanceof Multipart) {
+            // If the part is Multipart but not alternative it's either mixed or
+            // something we don't know about, which means we treat it as mixed
+            // per the spec. We just process its pieces recursively.
+            MimeMultipart mp = (MimeMultipart)part.getBody();
+            boolean foundHtml = false;
+            if (mp.getSubTypeForTest().equals("alternative")) {
+                for (int i = 0; i < mp.getCount(); i++) {
+                    if (mp.getBodyPart(i).isMimeType("text/html")) {
+                        foundHtml = true;
+                        break;
+                    }
+                }
+            }
+            for (int i = 0; i < mp.getCount(); i++) {
+                // See if we have text and html
+                BodyPart bp = mp.getBodyPart(i);
+                // If there's html, don't bother loading text
+                if (foundHtml && bp.isMimeType("text/plain")) {
+                    continue;
+                }
+                collectParts(bp, viewables, attachments);
+            }
+        } else if (part.getBody() instanceof Message) {
+            // If the part is an embedded message we just continue to process
+            // it, pulling any viewables or attachments into the running list.
+            Message message = (Message)part.getBody();
+            collectParts(message, viewables, attachments);
+        } else if (inline && (mimeType.startsWith("text") || (mimeType.startsWith("image")))) {
+            // We'll treat text and images as viewables
+            viewables.add(part);
+        } else {
+            // Everything else is an attachment.
+            attachments.add(part);
+        }
+    }
+}
diff --git a/src/com/android/phone/common/mail/internet/TextBody.java b/src/com/android/phone/common/mail/internet/TextBody.java
new file mode 100644
index 0000000..77bbd65
--- /dev/null
+++ b/src/com/android/phone/common/mail/internet/TextBody.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2015 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.phone.common.mail.internet;
+
+import android.util.Base64;
+
+import com.android.phone.common.mail.Body;
+import com.android.phone.common.mail.MessagingException;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.UnsupportedEncodingException;
+
+public class TextBody implements Body {
+    String mBody;
+
+    public TextBody(String body) {
+        this.mBody = body;
+    }
+
+    @Override
+    public void writeTo(OutputStream out) throws IOException, MessagingException {
+        byte[] bytes = mBody.getBytes("UTF-8");
+        out.write(Base64.encode(bytes, Base64.CRLF));
+    }
+
+    /**
+     * Get the text of the body in it's unencoded format.
+     * @return
+     */
+    public String getText() {
+        return mBody;
+    }
+
+    /**
+     * Returns an InputStream that reads this body's text in UTF-8 format.
+     */
+    @Override
+    public InputStream getInputStream() throws MessagingException {
+        try {
+            byte[] b = mBody.getBytes("UTF-8");
+            return new ByteArrayInputStream(b);
+        }
+        catch (UnsupportedEncodingException usee) {
+            return null;
+        }
+    }
+}
diff --git a/src/com/android/phone/common/mail/store/ImapConnection.java b/src/com/android/phone/common/mail/store/ImapConnection.java
index 3cae5f8..ace7029 100644
--- a/src/com/android/phone/common/mail/store/ImapConnection.java
+++ b/src/com/android/phone/common/mail/store/ImapConnection.java
@@ -16,7 +16,6 @@
 package com.android.phone.common.mail.store;
 
 import android.text.TextUtils;
-import android.util.Log;
 
 import com.android.phone.common.mail.AuthenticationFailedException;
 import com.android.phone.common.mail.CertificateValidationException;
@@ -26,6 +25,7 @@
 import com.android.phone.common.mail.store.imap.ImapResponse;
 import com.android.phone.common.mail.store.imap.ImapResponseParser;
 import com.android.phone.common.mail.store.imap.ImapUtility;
+import com.android.phone.common.mail.utils.LogUtils;
 import com.android.phone.common.mail.store.ImapStore.ImapException;
 
 import java.io.IOException;
@@ -104,14 +104,10 @@
             // LOGIN
             doLogin();
         } catch (SSLException e) {
-            if (Log.isLoggable(TAG, Log.DEBUG)) {
-                Log.d(TAG, "SSLException ", e);
-            }
+            LogUtils.d(TAG, "SSLException ", e);
             throw new CertificateValidationException(e.getMessage(), e);
         } catch (IOException ioe) {
-            if (Log.isLoggable(TAG, Log.DEBUG)) {
-                Log.d(TAG, "IOException", ioe);
-            }
+            LogUtils.d(TAG, "IOException", ioe);
             throw ioe;
         } finally {
             destroyResponses();
@@ -139,9 +135,7 @@
         try {
             executeSimpleCommand(getLoginPhrase(), true);
         } catch (ImapException ie) {
-            if (Log.isLoggable(TAG, Log.DEBUG)) {
-                Log.d(TAG, "ImapException", ie);
-            }
+            LogUtils.d(TAG, "ImapException", ie);
             final String status = ie.getStatus();
             final String code = ie.getResponseCode();
             final String alertText = ie.getAlertText();
@@ -176,6 +170,10 @@
         }
     }
 
+    ImapResponse readResponse() throws IOException, MessagingException {
+        return mParser.readResponse();
+    }
+
     List<ImapResponse> executeSimpleCommand(String command)
             throws IOException, MessagingException{
         return executeSimpleCommand(command, false);
diff --git a/src/com/android/phone/common/mail/store/ImapFolder.java b/src/com/android/phone/common/mail/store/ImapFolder.java
index 1665767..5e9bc6b 100644
--- a/src/com/android/phone/common/mail/store/ImapFolder.java
+++ b/src/com/android/phone/common/mail/store/ImapFolder.java
@@ -15,31 +15,69 @@
  */
 package com.android.phone.common.mail.store;
 
+import android.content.Context;
 import android.text.TextUtils;
-import android.util.Log;
+import android.util.Base64DataException;
 
-import com.android.phone.common.mail.AuthenticationFailedException;
-import com.android.phone.common.mail.Message;
-import com.android.phone.common.mail.MessagingException;
+import com.android.phone.common.mail.store.ImapStore.ImapException;
+import com.android.phone.common.mail.store.ImapStore.ImapMessage;
 import com.android.phone.common.mail.store.imap.ImapConstants;
+import com.android.phone.common.mail.store.imap.ImapElement;
+import com.android.phone.common.mail.store.imap.ImapList;
 import com.android.phone.common.mail.store.imap.ImapResponse;
 import com.android.phone.common.mail.store.imap.ImapString;
+import com.android.phone.common.mail.store.imap.ImapUtility;
+import com.android.phone.common.mail.AuthenticationFailedException;
+import com.android.phone.common.mail.Body;
+import com.android.phone.common.mail.FetchProfile;
+import com.android.phone.common.mail.Flag;
+import com.android.phone.common.mail.Message;
+import com.android.phone.common.mail.MessagingException;
+import com.android.phone.common.mail.Part;
+import com.android.phone.common.mail.internet.BinaryTempFileBody;
+import com.android.phone.common.mail.internet.MimeBodyPart;
+import com.android.phone.common.mail.internet.MimeHeader;
+import com.android.phone.common.mail.internet.MimeMultipart;
+import com.android.phone.common.mail.internet.MimeUtility;
+import com.android.phone.common.mail.utility.CountingOutputStream;
+import com.android.phone.common.mail.utility.EOLConvertingOutputStream;
+import com.android.phone.common.mail.utils.Utility;
+import com.android.phone.common.mail.utils.LogUtils;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.phone.common.R;
 
+import org.apache.commons.io.IOUtils;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
 import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Locale;
+import java.util.TimeZone;
 
-/**
- * Represents one folder on the IMAP server.
- */
 public class ImapFolder {
-    private final String TAG = "ImapFolder";
-    private ImapStore mStore;
-    private String mName;
+    private static final String TAG = "ImapFolder";
+    private final static String[] PERMANENT_FLAGS =
+        { Flag.DELETED, Flag.SEEN, Flag.FLAGGED, Flag.ANSWERED };
+    private static final int COPY_BUFFER_SIZE = 16*1024;
+
+    private final ImapStore mStore;
+    private final String mName;
+    private int mMessageCount = -1;
+    private ImapConnection mConnection;
     private String mMode;
     private boolean mExists;
-    private ImapConnection mConnection;
-    private int mMessageCount;
+    /** A set of hashes that can be used to track dirtiness */
+    Object mHash[];
 
     public static final String MODE_READ_ONLY = "mode_read_only";
     public static final String MODE_READ_WRITE = "mode_read_write";
@@ -49,6 +87,14 @@
         mName = name;
     }
 
+    /**
+     * Callback for each message retrieval.
+     */
+    public interface MessageRetrievalListener {
+        public void messageRetrieved(Message message);
+        public void loadAttachmentProgress(int progress);
+    }
+
     private void destroyResponses() {
         if (mConnection != null) {
             mConnection.destroyResponses();
@@ -110,6 +156,524 @@
         return mExists && mConnection != null;
     }
 
+    public String getMode() {
+        return mMode;
+    }
+
+    public void close(boolean expunge) {
+        if (expunge) {
+            try {
+                expunge();
+            } catch (MessagingException e) {
+                LogUtils.e(TAG, e, "Messaging Exception");
+            }
+        }
+        mMessageCount = -1;
+        synchronized (this) {
+            mStore.closeConnection();
+            mConnection = null;
+        }
+    }
+
+    public int getMessageCount() {
+        return mMessageCount;
+    }
+
+    String[] getSearchUids(List<ImapResponse> responses) {
+        // S: * SEARCH 2 3 6
+        final ArrayList<String> uids = new ArrayList<String>();
+        for (ImapResponse response : responses) {
+            if (!response.isDataResponse(0, ImapConstants.SEARCH)) {
+                continue;
+            }
+            // Found SEARCH response data
+            for (int i = 1; i < response.size(); i++) {
+                ImapString s = response.getStringOrEmpty(i);
+                if (s.isString()) {
+                    uids.add(s.getString());
+                }
+            }
+        }
+        return uids.toArray(Utility.EMPTY_STRINGS);
+    }
+
+    @VisibleForTesting
+    String[] searchForUids(String searchCriteria) throws MessagingException {
+        checkOpen();
+        try {
+            try {
+                final String command = ImapConstants.UID_SEARCH + " " + searchCriteria;
+                final String[] result = getSearchUids(mConnection.executeSimpleCommand(command));
+                LogUtils.d(TAG, "searchForUids '" + searchCriteria + "' results: " +
+                        result.length);
+                return result;
+            } catch (ImapException me) {
+                LogUtils.d(TAG, "ImapException in search: " + searchCriteria, me);
+                return Utility.EMPTY_STRINGS; // Not found
+            } catch (IOException ioe) {
+                LogUtils.d(TAG, "IOException in search: " + searchCriteria, ioe);
+                throw ioExceptionHandler(mConnection, ioe);
+            }
+        } finally {
+            destroyResponses();
+        }
+    }
+
+
+    public Message getMessage(String uid) throws MessagingException {
+        checkOpen();
+
+        final String[] uids = searchForUids(ImapConstants.UID + " " + uid);
+        for (int i = 0; i < uids.length; i++) {
+            if (uids[i].equals(uid)) {
+                return new ImapMessage(uid, this);
+            }
+        }
+        return null;
+    }
+
+    @VisibleForTesting
+    protected static boolean isAsciiString(String str) {
+        int len = str.length();
+        for (int i = 0; i < len; i++) {
+            char c = str.charAt(i);
+            if (c >= 128) return false;
+        }
+        return true;
+    }
+
+    public Message[] getMessages(String[] uids) throws MessagingException {
+        if (uids == null) {
+            uids = searchForUids("1:* NOT DELETED");
+        }
+        return getMessagesInternal(uids);
+    }
+
+    public Message[] getMessagesInternal(String[] uids) {
+        final ArrayList<Message> messages = new ArrayList<Message>(uids.length);
+        for (int i = 0; i < uids.length; i++) {
+            final String uid = uids[i];
+            final ImapMessage message = new ImapMessage(uid, this);
+            messages.add(message);
+        }
+        return messages.toArray(Message.EMPTY_ARRAY);
+    }
+
+    public void fetch(Message[] messages, FetchProfile fp,
+            MessageRetrievalListener listener) throws MessagingException {
+        try {
+            fetchInternal(messages, fp, listener);
+        } catch (RuntimeException e) { // Probably a parser error.
+            LogUtils.w(TAG, "Exception detected: " + e.getMessage());
+            throw e;
+        }
+    }
+
+    public void fetchInternal(Message[] messages, FetchProfile fp,
+            MessageRetrievalListener listener) throws MessagingException {
+        if (messages.length == 0) {
+            return;
+        }
+        checkOpen();
+        HashMap<String, Message> messageMap = new HashMap<String, Message>();
+        for (Message m : messages) {
+            messageMap.put(m.getUid(), m);
+        }
+
+        /*
+         * Figure out what command we are going to run:
+         * FLAGS     - UID FETCH (FLAGS)
+         * ENVELOPE  - UID FETCH (INTERNALDATE UID RFC822.SIZE FLAGS BODY.PEEK[
+         *                            HEADER.FIELDS (date subject from content-type to cc)])
+         * STRUCTURE - UID FETCH (BODYSTRUCTURE)
+         * BODY_SANE - UID FETCH (BODY.PEEK[]<0.N>) where N = max bytes returned
+         * BODY      - UID FETCH (BODY.PEEK[])
+         * Part      - UID FETCH (BODY.PEEK[ID]) where ID = mime part ID
+         */
+
+        final LinkedHashSet<String> fetchFields = new LinkedHashSet<String>();
+
+        fetchFields.add(ImapConstants.UID);
+        if (fp.contains(FetchProfile.Item.FLAGS)) {
+            fetchFields.add(ImapConstants.FLAGS);
+        }
+        if (fp.contains(FetchProfile.Item.ENVELOPE)) {
+            fetchFields.add(ImapConstants.INTERNALDATE);
+            fetchFields.add(ImapConstants.RFC822_SIZE);
+            fetchFields.add(ImapConstants.FETCH_FIELD_HEADERS);
+        }
+        if (fp.contains(FetchProfile.Item.STRUCTURE)) {
+            fetchFields.add(ImapConstants.BODYSTRUCTURE);
+        }
+
+        if (fp.contains(FetchProfile.Item.BODY_SANE)) {
+            fetchFields.add(ImapConstants.FETCH_FIELD_BODY_PEEK_SANE);
+        }
+        if (fp.contains(FetchProfile.Item.BODY)) {
+            fetchFields.add(ImapConstants.FETCH_FIELD_BODY_PEEK);
+        }
+
+        // TODO Why are we only fetching the first part given?
+        final Part fetchPart = fp.getFirstPart();
+        if (fetchPart != null) {
+            final String[] partIds =
+                    fetchPart.getHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA);
+            // TODO Why can a single part have more than one Id? And why should we only fetch
+            // the first id if there are more than one?
+            if (partIds != null) {
+                fetchFields.add(ImapConstants.FETCH_FIELD_BODY_PEEK_BARE
+                        + "[" + partIds[0] + "]");
+            }
+        }
+
+        try {
+            mConnection.sendCommand(String.format(Locale.US,
+                    ImapConstants.UID_FETCH + " %s (%s)", ImapStore.joinMessageUids(messages),
+                    Utility.combine(fetchFields.toArray(new String[fetchFields.size()]), ' ')
+                    ), false);
+            ImapResponse response;
+            do {
+                response = null;
+                try {
+                    response = mConnection.readResponse();
+
+                    if (!response.isDataResponse(1, ImapConstants.FETCH)) {
+                        continue; // Ignore
+                    }
+                    final ImapList fetchList = response.getListOrEmpty(2);
+                    final String uid = fetchList.getKeyedStringOrEmpty(ImapConstants.UID)
+                            .getString();
+                    if (TextUtils.isEmpty(uid)) continue;
+
+                    ImapMessage message = (ImapMessage) messageMap.get(uid);
+                    if (message == null) continue;
+
+                    if (fp.contains(FetchProfile.Item.FLAGS)) {
+                        final ImapList flags =
+                            fetchList.getKeyedListOrEmpty(ImapConstants.FLAGS);
+                        for (int i = 0, count = flags.size(); i < count; i++) {
+                            final ImapString flag = flags.getStringOrEmpty(i);
+                            if (flag.is(ImapConstants.FLAG_DELETED)) {
+                                message.setFlagInternal(Flag.DELETED, true);
+                            } else if (flag.is(ImapConstants.FLAG_ANSWERED)) {
+                                message.setFlagInternal(Flag.ANSWERED, true);
+                            } else if (flag.is(ImapConstants.FLAG_SEEN)) {
+                                message.setFlagInternal(Flag.SEEN, true);
+                            } else if (flag.is(ImapConstants.FLAG_FLAGGED)) {
+                                message.setFlagInternal(Flag.FLAGGED, true);
+                            }
+                        }
+                    }
+                    if (fp.contains(FetchProfile.Item.ENVELOPE)) {
+                        final Date internalDate = fetchList.getKeyedStringOrEmpty(
+                                ImapConstants.INTERNALDATE).getDateOrNull();
+                        final int size = fetchList.getKeyedStringOrEmpty(
+                                ImapConstants.RFC822_SIZE).getNumberOrZero();
+                        final String header = fetchList.getKeyedStringOrEmpty(
+                                ImapConstants.BODY_BRACKET_HEADER, true).getString();
+
+                        message.setInternalDate(internalDate);
+                        message.setSize(size);
+                        message.parse(Utility.streamFromAsciiString(header));
+                    }
+                    if (fp.contains(FetchProfile.Item.STRUCTURE)) {
+                        ImapList bs = fetchList.getKeyedListOrEmpty(
+                                ImapConstants.BODYSTRUCTURE);
+                        if (!bs.isEmpty()) {
+                            try {
+                                parseBodyStructure(bs, message, ImapConstants.TEXT);
+                            } catch (MessagingException e) {
+                                LogUtils.v(TAG, e, "Error handling message");
+                                message.setBody(null);
+                            }
+                        }
+                    }
+                    if (fp.contains(FetchProfile.Item.BODY)
+                            || fp.contains(FetchProfile.Item.BODY_SANE)) {
+                        // Body is keyed by "BODY[]...".
+                        // Previously used "BODY[..." but this can be confused with "BODY[HEADER..."
+                        // TODO Should we accept "RFC822" as well??
+                        ImapString body = fetchList.getKeyedStringOrEmpty("BODY[]", true);
+                        InputStream bodyStream = body.getAsStream();
+                        message.parse(bodyStream);
+                    }
+                    if (fetchPart != null) {
+                        InputStream bodyStream =
+                                fetchList.getKeyedStringOrEmpty("BODY[", true).getAsStream();
+                        String encodings[] = fetchPart.getHeader(
+                                MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING);
+
+                        String contentTransferEncoding = null;
+                        if (encodings != null && encodings.length > 0) {
+                            contentTransferEncoding = encodings[0];
+                        } else {
+                            // According to http://tools.ietf.org/html/rfc2045#section-6.1
+                            // "7bit" is the default.
+                            contentTransferEncoding = "7bit";
+                        }
+
+                        try {
+                            // TODO Don't create 2 temp files.
+                            // decodeBody creates BinaryTempFileBody, but we could avoid this
+                            // if we implement ImapStringBody.
+                            // (We'll need to share a temp file.  Protect it with a ref-count.)
+                            fetchPart.setBody(decodeBody(mStore.getContext(), bodyStream,
+                                    contentTransferEncoding, fetchPart.getSize(), listener));
+                        } catch(Exception e) {
+                            // TODO: Figure out what kinds of exceptions might actually be thrown
+                            // from here. This blanket catch-all is because we're not sure what to
+                            // do if we don't have a contentTransferEncoding, and we don't have
+                            // time to figure out what exceptions might be thrown.
+                            LogUtils.e(TAG, "Error fetching body %s", e);
+                        }
+                    }
+
+                    if (listener != null) {
+                        listener.messageRetrieved(message);
+                    }
+                } finally {
+                    destroyResponses();
+                }
+            } while (!response.isTagged());
+        } catch (IOException ioe) {
+            throw ioExceptionHandler(mConnection, ioe);
+        }
+    }
+
+    /**
+     * Removes any content transfer encoding from the stream and returns a Body.
+     * This code is taken/condensed from MimeUtility.decodeBody
+     */
+    private static Body decodeBody(Context context,InputStream in, String contentTransferEncoding,
+            int size, MessageRetrievalListener listener) throws IOException {
+        // Get a properly wrapped input stream
+        in = MimeUtility.getInputStreamForContentTransferEncoding(in, contentTransferEncoding);
+        BinaryTempFileBody tempBody = new BinaryTempFileBody();
+        OutputStream out = tempBody.getOutputStream();
+        try {
+            byte[] buffer = new byte[COPY_BUFFER_SIZE];
+            int n = 0;
+            int count = 0;
+            while (-1 != (n = in.read(buffer))) {
+                out.write(buffer, 0, n);
+                count += n;
+                if (listener != null) {
+                    if (size == 0) {
+                        // We don't know how big the file is, so just fake it.
+                        listener.loadAttachmentProgress((int)Math.ceil(100 * (1-1.0/count)));
+                    } else {
+                        listener.loadAttachmentProgress(count * 100 / size);
+                    }
+                }
+            }
+        } catch (Base64DataException bde) {
+            String warning = "\n\n" + context.getString(R.string.message_decode_error);
+            out.write(warning.getBytes());
+        } finally {
+            out.close();
+        }
+        return tempBody;
+    }
+
+    public String[] getPermanentFlags() {
+        return PERMANENT_FLAGS;
+    }
+
+    /**
+     * Handle any untagged responses that the caller doesn't care to handle themselves.
+     * @param responses
+     */
+    private void handleUntaggedResponses(List<ImapResponse> responses) {
+        for (ImapResponse response : responses) {
+            handleUntaggedResponse(response);
+        }
+    }
+
+    /**
+     * Handle an untagged response that the caller doesn't care to handle themselves.
+     * @param response
+     */
+    private void handleUntaggedResponse(ImapResponse response) {
+        if (response.isDataResponse(1, ImapConstants.EXISTS)) {
+            mMessageCount = response.getStringOrEmpty(0).getNumberOrZero();
+        }
+    }
+
+    private static void parseBodyStructure(ImapList bs, Part part, String id)
+            throws MessagingException {
+        if (bs.getElementOrNone(0).isList()) {
+            /*
+             * This is a multipart/*
+             */
+            MimeMultipart mp = new MimeMultipart();
+            for (int i = 0, count = bs.size(); i < count; i++) {
+                ImapElement e = bs.getElementOrNone(i);
+                if (e.isList()) {
+                    /*
+                     * For each part in the message we're going to add a new BodyPart and parse
+                     * into it.
+                     */
+                    MimeBodyPart bp = new MimeBodyPart();
+                    if (id.equals(ImapConstants.TEXT)) {
+                        parseBodyStructure(bs.getListOrEmpty(i), bp, Integer.toString(i + 1));
+
+                    } else {
+                        parseBodyStructure(bs.getListOrEmpty(i), bp, id + "." + (i + 1));
+                    }
+                    mp.addBodyPart(bp);
+
+                } else {
+                    if (e.isString()) {
+                        mp.setSubType(bs.getStringOrEmpty(i).getString().toLowerCase(Locale.US));
+                    }
+                    break; // Ignore the rest of the list.
+                }
+            }
+            part.setBody(mp);
+        } else {
+            /*
+             * This is a body. We need to add as much information as we can find out about
+             * it to the Part.
+             */
+
+            /*
+             body type
+             body subtype
+             body parameter parenthesized list
+             body id
+             body description
+             body encoding
+             body size
+             */
+
+            final ImapString type = bs.getStringOrEmpty(0);
+            final ImapString subType = bs.getStringOrEmpty(1);
+            final String mimeType =
+                    (type.getString() + "/" + subType.getString()).toLowerCase(Locale.US);
+
+            final ImapList bodyParams = bs.getListOrEmpty(2);
+            final ImapString cid = bs.getStringOrEmpty(3);
+            final ImapString encoding = bs.getStringOrEmpty(5);
+            final int size = bs.getStringOrEmpty(6).getNumberOrZero();
+
+            if (MimeUtility.mimeTypeMatches(mimeType, MimeUtility.MIME_TYPE_RFC822)) {
+                // A body type of type MESSAGE and subtype RFC822
+                // contains, immediately after the basic fields, the
+                // envelope structure, body structure, and size in
+                // text lines of the encapsulated message.
+                // [MESSAGE, RFC822, [NAME, filename.eml], NIL, NIL, 7BIT, 5974, NIL,
+                //     [INLINE, [FILENAME*0, Fwd: Xxx..., FILENAME*1, filename.eml]], NIL]
+                /*
+                 * This will be caught by fetch and handled appropriately.
+                 */
+                throw new MessagingException("BODYSTRUCTURE " + MimeUtility.MIME_TYPE_RFC822
+                        + " not yet supported.");
+            }
+
+            /*
+             * Set the content type with as much information as we know right now.
+             */
+            final StringBuilder contentType = new StringBuilder(mimeType);
+
+            /*
+             * If there are body params we might be able to get some more information out
+             * of them.
+             */
+            for (int i = 1, count = bodyParams.size(); i < count; i += 2) {
+
+                // TODO We need to convert " into %22, but
+                // because MimeUtility.getHeaderParameter doesn't recognize it,
+                // we can't fix it for now.
+                contentType.append(String.format(";\n %s=\"%s\"",
+                        bodyParams.getStringOrEmpty(i - 1).getString(),
+                        bodyParams.getStringOrEmpty(i).getString()));
+            }
+
+            part.setHeader(MimeHeader.HEADER_CONTENT_TYPE, contentType.toString());
+
+            // Extension items
+            final ImapList bodyDisposition;
+
+            if (type.is(ImapConstants.TEXT) && bs.getElementOrNone(9).isList()) {
+                // If media-type is TEXT, 9th element might be: [body-fld-lines] := number
+                // So, if it's not a list, use 10th element.
+                // (Couldn't find evidence in the RFC if it's ALWAYS 10th element.)
+                bodyDisposition = bs.getListOrEmpty(9);
+            } else {
+                bodyDisposition = bs.getListOrEmpty(8);
+            }
+
+            final StringBuilder contentDisposition = new StringBuilder();
+
+            if (bodyDisposition.size() > 0) {
+                final String bodyDisposition0Str =
+                        bodyDisposition.getStringOrEmpty(0).getString().toLowerCase(Locale.US);
+                if (!TextUtils.isEmpty(bodyDisposition0Str)) {
+                    contentDisposition.append(bodyDisposition0Str);
+                }
+
+                final ImapList bodyDispositionParams = bodyDisposition.getListOrEmpty(1);
+                if (!bodyDispositionParams.isEmpty()) {
+                    /*
+                     * If there is body disposition information we can pull some more
+                     * information about the attachment out.
+                     */
+                    for (int i = 1, count = bodyDispositionParams.size(); i < count; i += 2) {
+
+                        // TODO We need to convert " into %22.  See above.
+                        contentDisposition.append(String.format(Locale.US, ";\n %s=\"%s\"",
+                                bodyDispositionParams.getStringOrEmpty(i - 1)
+                                        .getString().toLowerCase(Locale.US),
+                                bodyDispositionParams.getStringOrEmpty(i).getString()));
+                    }
+                }
+            }
+
+            if ((size > 0)
+                    && (MimeUtility.getHeaderParameter(contentDisposition.toString(), "size")
+                            == null)) {
+                contentDisposition.append(String.format(Locale.US, ";\n size=%d", size));
+            }
+
+            if (contentDisposition.length() > 0) {
+                /*
+                 * Set the content disposition containing at least the size. Attachment
+                 * handling code will use this down the road.
+                 */
+                part.setHeader(MimeHeader.HEADER_CONTENT_DISPOSITION,
+                        contentDisposition.toString());
+            }
+
+            /*
+             * Set the Content-Transfer-Encoding header. Attachment code will use this
+             * to parse the body.
+             */
+            if (!encoding.isEmpty()) {
+                part.setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING,
+                        encoding.getString());
+            }
+
+            /*
+             * Set the Content-ID header.
+             */
+            if (!cid.isEmpty()) {
+                part.setHeader(MimeHeader.HEADER_CONTENT_ID, cid.getString());
+            }
+
+            if (size > 0) {
+                if (part instanceof ImapMessage) {
+                    ((ImapMessage) part).setSize(size);
+                } else if (part instanceof MimeBodyPart) {
+                    ((MimeBodyPart) part).setSize(size);
+                } else {
+                    throw new MessagingException("Unknown part type " + part.toString());
+                }
+            }
+            part.setHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA, id);
+        }
+
+    }
+
     public Message[] expunge() throws MessagingException {
         checkOpen();
         try {
@@ -131,13 +695,13 @@
             StringBuilder flagList = new StringBuilder();
             for (int i = 0, count = flags.length; i < count; i++) {
                 String flag = flags[i];
-                if (Message.FLAG_SEEN.equals(flag)) {
+                if (flag == Flag.SEEN) {
                     flagList.append(" " + ImapConstants.FLAG_SEEN);
-                } else if (Message.FLAG_DELETED.equals(flag)) {
+                } else if (flag == Flag.DELETED) {
                     flagList.append(" " + ImapConstants.FLAG_DELETED);
-                } else if (Message.FLAG_FLAGGED.equals(flag)) {
+                } else if (flag == Flag.FLAGGED) {
                     flagList.append(" " + ImapConstants.FLAG_FLAGGED);
-                } else if (Message.FLAG_ANSWERED.equals(flag)) {
+                } else if (flag == Flag.ANSWERED) {
                     flagList.append(" " + ImapConstants.FLAG_ANSWERED);
                 }
             }
@@ -146,7 +710,7 @@
         try {
             mConnection.executeSimpleCommand(String.format(Locale.US,
                     ImapConstants.UID_STORE + " %s %s" + ImapConstants.FLAGS_SILENT + " (%s)",
-                    TextUtils.join(",", messages),
+                    ImapStore.joinMessageUids(messages),
                     value ? "+" : "-",
                     allFlags));
 
@@ -190,51 +754,14 @@
         mExists = true;
     }
 
-    /**
-     * Handle any untagged responses that the caller doesn't care to handle themselves.
-     * @param responses
-     */
-    private void handleUntaggedResponses(List<ImapResponse> responses) {
-        for (ImapResponse response : responses) {
-            handleUntaggedResponse(response);
-        }
-    }
-
-    /**
-     * Handle an untagged response that the caller doesn't care to handle themselves.
-     * @param response
-     */
-    private void handleUntaggedResponse(ImapResponse response) {
-        if (response.isDataResponse(1, ImapConstants.EXISTS)) {
-            mMessageCount = response.getStringOrEmpty(0).getNumberOrZero();
-        }
-    }
-
     private void checkOpen() throws MessagingException {
         if (!isOpen()) {
             throw new MessagingException("Folder " + mName + " is not open.");
         }
     }
 
-    public void close(boolean expunge) {
-        if (expunge) {
-            try {
-                expunge();
-            } catch (MessagingException e) {
-                Log.e(TAG, "MessagingException: ", e);
-            }
-        }
-        mMessageCount = -1;
-        synchronized (this) {
-            mStore.closeConnection();
-            mConnection = null;
-        }
-    }
-
     private MessagingException ioExceptionHandler(ImapConnection connection, IOException ioe) {
-        if (Log.isLoggable(TAG, Log.DEBUG)) {
-            Log.d(TAG, "IO Exception detected: ", ioe);
-        }
+        LogUtils.d(TAG, "IO Exception detected: ", ioe);
         connection.close();
         if (connection == mConnection) {
             mConnection = null; // To prevent close() from returning the connection to the pool.
@@ -242,4 +769,8 @@
         }
         return new MessagingException(MessagingException.IOERROR, "IO Error", ioe);
     }
-}
+
+    public Message createMessage(String uid) {
+        return new ImapMessage(uid, this);
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/phone/common/mail/store/ImapStore.java b/src/com/android/phone/common/mail/store/ImapStore.java
index 90d2941..1c91e76 100644
--- a/src/com/android/phone/common/mail/store/ImapStore.java
+++ b/src/com/android/phone/common/mail/store/ImapStore.java
@@ -18,11 +18,15 @@
 
 import android.content.Context;
 
+import com.android.phone.common.mail.internet.MimeMessage;
 import com.android.phone.common.mail.MailTransport;
 import com.android.phone.common.mail.Message;
 import com.android.phone.common.mail.MessagingException;
 import com.android.phone.common.mail.store.ImapFolder;
 
+import java.io.IOException;
+import java.io.InputStream;
+
 public class ImapStore {
     /**
      * A global suggestion to Store implementors on how much of the body
@@ -53,8 +57,8 @@
         mTransport = new MailTransport(context, serverName, port, flags);
     }
 
-    public ImapFolder getFolder(String name) {
-        return new ImapFolder(this, name);
+    public Context getContext() {
+        return mContext;
     }
 
     public String getUsername() {
@@ -70,6 +74,50 @@
         return mTransport.clone();
     }
 
+    /**
+     * Returns UIDs of Messages joined with "," as the separator.
+     */
+    static String joinMessageUids(Message[] messages) {
+        StringBuilder sb = new StringBuilder();
+        boolean notFirst = false;
+        for (Message m : messages) {
+            if (notFirst) {
+                sb.append(',');
+            }
+            sb.append(m.getUid());
+            notFirst = true;
+        }
+        return sb.toString();
+    }
+
+    static class ImapMessage extends MimeMessage {
+        private ImapFolder mFolder;
+
+        ImapMessage(String uid, ImapFolder folder) {
+            mUid = uid;
+            mFolder = folder;
+        }
+
+        public void setSize(int size) {
+            mSize = size;
+        }
+
+        @Override
+        public void parse(InputStream in) throws IOException, MessagingException {
+            super.parse(in);
+        }
+
+        public void setFlagInternal(String flag, boolean set) throws MessagingException {
+            super.setFlag(flag, set);
+        }
+
+        @Override
+        public void setFlag(String flag, boolean set) throws MessagingException {
+            super.setFlag(flag, set);
+            mFolder.setFlags(new Message[] { this }, new String[] { flag }, set);
+        }
+    }
+
     static class ImapException extends MessagingException {
         private static final long serialVersionUID = 1L;
 
diff --git a/src/com/android/phone/common/mail/store/imap/ImapConstants.java b/src/com/android/phone/common/mail/store/imap/ImapConstants.java
index 0f1f526..cde1616 100644
--- a/src/com/android/phone/common/mail/store/imap/ImapConstants.java
+++ b/src/com/android/phone/common/mail/store/imap/ImapConstants.java
@@ -18,18 +18,21 @@
 
 import com.android.phone.common.mail.store.ImapStore;
 
+import java.util.Locale;
+
 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>", ImapStore.FETCH_BODY_SANE_SUGGESTED_SIZE);
+    public static final String FETCH_FIELD_BODY_PEEK_SANE = String.format(
+            Locale.US, "BODY.PEEK[]<0.%d>", ImapStore.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 ALERT = "ALERT";
     public static final String APPEND = "APPEND";
+    public static final String AUTHENTICATE = "AUTHENTICATE";
     public static final String BAD = "BAD";
     public static final String BADCHARSET = "BADCHARSET";
     public static final String BODY = "BODY";
@@ -40,6 +43,7 @@
     public static final String CHECK = "CHECK";
     public static final String CLOSE = "CLOSE";
     public static final String COPY = "COPY";
+    public static final String COPYUID = "COPYUID";
     public static final String CREATE = "CREATE";
     public static final String DELETE = "DELETE";
     public static final String EXAMINE = "EXAMINE";
@@ -60,6 +64,7 @@
     public static final String LOGIN = "LOGIN";
     public static final String LOGOUT = "LOGOUT";
     public static final String LSUB = "LSUB";
+    public static final String NAMESPACE = "NAMESPACE";
     public static final String NO = "NO";
     public static final String NOOP = "NOOP";
     public static final String OK = "OK";
@@ -84,9 +89,11 @@
     public static final String UID_SEARCH = "UID SEARCH";
     public static final String UID_STORE = "UID STORE";
     public static final String UIDNEXT = "UIDNEXT";
+    public static final String UIDPLUS = "UIDPLUS";
     public static final String UIDVALIDITY = "UIDVALIDITY";
     public static final String UNSEEN = "UNSEEN";
     public static final String UNSUBSCRIBE = "UNSUBSCRIBE";
+    public static final String XOAUTH2 = "XOAUTH2";
     public static final String APPENDUID = "APPENDUID";
     public static final String NIL = "NIL";
 
diff --git a/src/com/android/phone/common/mail/store/imap/ImapResponseParser.java b/src/com/android/phone/common/mail/store/imap/ImapResponseParser.java
index 1bf1565..d0413df 100644
--- a/src/com/android/phone/common/mail/store/imap/ImapResponseParser.java
+++ b/src/com/android/phone/common/mail/store/imap/ImapResponseParser.java
@@ -20,8 +20,8 @@
 import android.util.Log;
 
 import com.android.phone.common.mail.FixedLengthInputStream;
-import com.android.phone.common.mail.PeekableInputStream;
 import com.android.phone.common.mail.MessagingException;
+import com.android.phone.common.mail.PeekableInputStream;
 
 import java.io.IOException;
 import java.io.InputStream;
diff --git a/src/com/android/phone/common/mail/store/imap/ImapTempFileLiteral.java b/src/com/android/phone/common/mail/store/imap/ImapTempFileLiteral.java
index 2b1113e..67d5026 100644
--- a/src/com/android/phone/common/mail/store/imap/ImapTempFileLiteral.java
+++ b/src/com/android/phone/common/mail/store/imap/ImapTempFileLiteral.java
@@ -13,16 +13,16 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
 package com.android.phone.common.mail.store.imap;
 
-import android.content.Context;
-import android.util.Log;
-
 import com.android.phone.common.mail.FixedLengthInputStream;
+import com.android.phone.common.mail.TempDirectory;
+import com.android.phone.common.mail.utils.Utility;
+import com.android.phone.common.mail.utils.LogUtils;
+
+import org.apache.commons.io.IOUtils;
 
 import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileNotFoundException;
@@ -51,70 +51,11 @@
         // deleteOnExit() simply adds filenames to a static list and the list will never shrink.
         // mFile.deleteOnExit();
         OutputStream out = new FileOutputStream(mFile);
-        StreamUtils.copy(stream, out);
+        IOUtils.copy(stream, out);
         out.close();
     }
 
     /**
-     * Copies utility methods for working with byte arrays and I/O streams from guava library.
-     */
-    public static class StreamUtils {
-        private static final int BUF_SIZE = 0x1000; // 4K
-        /**
-         * Copies all bytes from the input stream to the output stream.
-         * Does not close or flush either stream.
-         *
-         * @param from the input stream to read from
-         * @param to the output stream to write to
-         * @return the number of bytes copied
-         * @throws IOException if an I/O error occurs
-         */
-        public static long copy(InputStream from, OutputStream to) throws IOException {
-            checkNotNull(from);
-            checkNotNull(to);
-            byte[] buf = new byte[BUF_SIZE];
-            long total = 0;
-            while (true) {
-              int r = from.read(buf);
-              if (r == -1) {
-                break;
-              }
-              to.write(buf, 0, r);
-              total += r;
-            }
-            return total;
-        }
-
-        /**
-         * Reads all bytes from an input stream into a byte array.
-         * Does not close the stream.
-         *
-         * @param in the input stream to read from
-         * @return a byte array containing all the bytes from the stream
-         * @throws IOException if an I/O error occurs
-         */
-        public static byte[] toByteArray(InputStream in) throws IOException {
-            ByteArrayOutputStream out = new ByteArrayOutputStream();
-            copy(in, out);
-            return out.toByteArray();
-        }
-
-        /**
-         * Ensures that an object reference passed as a parameter to the calling method is not null.
-         *
-         * @param reference an object reference
-         * @return the non-null reference that was validated
-         * @throws NullPointerException if {@code reference} is null
-         */
-        public static <T> T checkNotNull(T reference) {
-            if (reference == null) {
-                throw new NullPointerException();
-            }
-            return reference;
-        }
-    }
-
-    /**
      * Make sure we delete the temp file.
      *
      * We should always be calling {@link ImapResponse#destroy()}, but it's here as a last resort.
@@ -135,7 +76,7 @@
             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(TAG, "ImapTempFileLiteral: Temp file not found");
+            LogUtils.w(TAG, "ImapTempFileLiteral: Temp file not found");
 
             // Return 0 byte stream as a dummy...
             return new ByteArrayInputStream(new byte[0]);
@@ -146,14 +87,14 @@
     public String getString() {
         checkNotDestroyed();
         try {
-            byte[] bytes = StreamUtils.toByteArray(getAsStream());
+            byte[] bytes = IOUtils.toByteArray(getAsStream());
             // Prevent crash from OOM; we've seen this, but only rarely and not reproducibly
             if (bytes.length > ImapResponseParser.LITERAL_KEEP_IN_MEMORY_THRESHOLD) {
                 throw new IOException();
             }
-            return new String(bytes, "US-ASCII");
+            return Utility.fromAscii(bytes);
         } catch (IOException e) {
-            Log.w(TAG, "ImapTempFileLiteral: Error while reading temp file", e);
+            LogUtils.w(TAG, "ImapTempFileLiteral: Error while reading temp file", e);
             return "";
         }
     }
@@ -166,7 +107,7 @@
             }
         } catch (RuntimeException re) {
             // Just log and ignore.
-            Log.w(TAG, "Failed to remove temp file: " + re.getMessage());
+            LogUtils.w(TAG, "Failed to remove temp file: " + re.getMessage());
         }
         super.destroy();
     }
@@ -179,25 +120,4 @@
     public boolean tempFileExistsForTest() {
         return mFile.exists();
     }
-
-   /**
-    * TempDirectory caches the directory used for caching file.  It is set up during application
-    * initialization.
-    */
-   public static class TempDirectory {
-       private static File sTempDirectory = null;
-
-       public static void setTempDirectory(Context context) {
-           sTempDirectory = context.getCacheDir();
-       }
-
-       public static File getTempDirectory() {
-           if (sTempDirectory == null) {
-               throw new RuntimeException(
-                       "TempDirectory not set.  " +
-                       "If in a unit test, call Email.setTempDirectory(context) in setUp().");
-           }
-           return sTempDirectory;
-       }
-   }
 }
\ No newline at end of file
diff --git a/src/com/android/phone/common/mail/store/imap/ImapUtility.java b/src/com/android/phone/common/mail/store/imap/ImapUtility.java
index 28c8b6f..8551d44 100644
--- a/src/com/android/phone/common/mail/store/imap/ImapUtility.java
+++ b/src/com/android/phone/common/mail/store/imap/ImapUtility.java
@@ -15,10 +15,15 @@
  */
 package com.android.phone.common.mail.store.imap;
 
+import com.android.phone.common.mail.utils.LogUtils;
+
+import java.util.ArrayList;
+
 /**
  * Utility methods for use with IMAP.
  */
 public class ImapUtility {
+    public static final String TAG = "ImapUtility";
     /**
      * Apply quoting rules per IMAP RFC,
      * quoted          = DQUOTE *QUOTED-CHAR DQUOTE
@@ -48,4 +53,73 @@
         // return string with quotes around it
         return "\"" + result + "\"";
     }
+
+    /**
+     * Gets all of the values in a sequence set per RFC 3501. Any ranges are expanded into a
+     * list of individual numbers. If the set is invalid, an empty array is returned.
+     * <pre>
+     * sequence-number = nz-number / "*"
+     * sequence-range  = sequence-number ":" sequence-number
+     * sequence-set    = (sequence-number / sequence-range) *("," sequence-set)
+     * </pre>
+     */
+    public static String[] getImapSequenceValues(String set) {
+        ArrayList<String> list = new ArrayList<String>();
+        if (set != null) {
+            String[] setItems = set.split(",");
+            for (String item : setItems) {
+                if (item.indexOf(':') == -1) {
+                    // simple item
+                    try {
+                        Integer.parseInt(item); // Don't need the value; just ensure it's valid
+                        list.add(item);
+                    } catch (NumberFormatException e) {
+                        LogUtils.d(TAG, "Invalid UID value", e);
+                    }
+                } else {
+                    // range
+                    for (String rangeItem : getImapRangeValues(item)) {
+                        list.add(rangeItem);
+                    }
+                }
+            }
+        }
+        String[] stringList = new String[list.size()];
+        return list.toArray(stringList);
+    }
+
+    /**
+     * Expand the given number range into a list of individual numbers. If the range is not valid,
+     * an empty array is returned.
+     * <pre>
+     * sequence-number = nz-number / "*"
+     * sequence-range  = sequence-number ":" sequence-number
+     * sequence-set    = (sequence-number / sequence-range) *("," sequence-set)
+     * </pre>
+     */
+    public static String[] getImapRangeValues(String range) {
+        ArrayList<String> list = new ArrayList<String>();
+        try {
+            if (range != null) {
+                int colonPos = range.indexOf(':');
+                if (colonPos > 0) {
+                    int first  = Integer.parseInt(range.substring(0, colonPos));
+                    int second = Integer.parseInt(range.substring(colonPos + 1));
+                    if (first < second) {
+                        for (int i = first; i <= second; i++) {
+                            list.add(Integer.toString(i));
+                        }
+                    } else {
+                        for (int i = first; i >= second; i--) {
+                            list.add(Integer.toString(i));
+                        }
+                    }
+                }
+            }
+        } catch (NumberFormatException e) {
+            LogUtils.d(TAG, "Invalid range value", e);
+        }
+        String[] stringList = new String[list.size()];
+        return list.toArray(stringList);
+    }
 }
\ No newline at end of file
diff --git a/src/com/android/phone/common/mail/utility/CountingOutputStream.java b/src/com/android/phone/common/mail/utility/CountingOutputStream.java
new file mode 100644
index 0000000..f631c3e
--- /dev/null
+++ b/src/com/android/phone/common/mail/utility/CountingOutputStream.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2015 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.phone.common.mail.utility;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+/**
+ * A simple pass-thru OutputStream that also counts how many bytes are written to it and
+ * makes that count available to callers.
+ */
+public class CountingOutputStream extends OutputStream {
+    private long mCount;
+    private final OutputStream mOutputStream;
+
+    public CountingOutputStream(OutputStream outputStream) {
+        mOutputStream = outputStream;
+    }
+
+    public long getCount() {
+        return mCount;
+    }
+
+    @Override
+    public void write(byte[] buffer, int offset, int count) throws IOException {
+        mOutputStream.write(buffer, offset, count);
+        mCount += count;
+    }
+
+    @Override
+    public void write(int oneByte) throws IOException {
+        mOutputStream.write(oneByte);
+        mCount++;
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/phone/common/mail/utility/EOLConvertingOutputStream.java b/src/com/android/phone/common/mail/utility/EOLConvertingOutputStream.java
new file mode 100644
index 0000000..1d55152
--- /dev/null
+++ b/src/com/android/phone/common/mail/utility/EOLConvertingOutputStream.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2015 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.phone.common.mail.utility;
+
+import java.io.FilterOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+
+public class EOLConvertingOutputStream extends FilterOutputStream {
+    int lastChar;
+
+    public EOLConvertingOutputStream(OutputStream out) {
+        super(out);
+    }
+
+    @Override
+    public void write(int oneByte) throws IOException {
+        if (oneByte == '\n') {
+            if (lastChar != '\r') {
+                super.write('\r');
+            }
+        }
+        super.write(oneByte);
+        lastChar = oneByte;
+    }
+
+    @Override
+    public void flush() throws IOException {
+        if (lastChar == '\r') {
+            super.write('\n');
+            lastChar = '\n';
+        }
+        super.flush();
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/phone/common/mail/utils/LogUtils.java b/src/com/android/phone/common/mail/utils/LogUtils.java
index b45c84d..711af9b 100644
--- a/src/com/android/phone/common/mail/utils/LogUtils.java
+++ b/src/com/android/phone/common/mail/utils/LogUtils.java
@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2015, Google Inc.
+ * Copyright (c) 2015 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.
diff --git a/src/com/android/phone/common/mail/utils/Utility.java b/src/com/android/phone/common/mail/utils/Utility.java
new file mode 100644
index 0000000..d71b25e
--- /dev/null
+++ b/src/com/android/phone/common/mail/utils/Utility.java
@@ -0,0 +1,80 @@
+/**
+ * Copyright (c) 2015 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.phone.common.mail.utils;
+
+import java.io.ByteArrayInputStream;
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+import java.nio.charset.Charset;
+
+/**
+ * Simple utility methods used in email functions.
+ */
+public class Utility {
+    public static final Charset ASCII = Charset.forName("US-ASCII");
+
+    public static final String[] EMPTY_STRINGS = new String[0];
+
+    /**
+     * Returns a concatenated string containing the output of every Object's
+     * toString() method, each separated by the given separator character.
+     */
+    public static String combine(Object[] parts, char separator) {
+        if (parts == null) {
+            return null;
+        }
+        StringBuilder sb = new StringBuilder();
+        for (int i = 0; i < parts.length; i++) {
+            sb.append(parts[i].toString());
+            if (i < parts.length - 1) {
+                sb.append(separator);
+            }
+        }
+        return sb.toString();
+    }
+
+    /** 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);
+    }
+
+    private static byte[] encode(Charset charset, String s) {
+        if (s == null) {
+            return null;
+        }
+        final ByteBuffer buffer = charset.encode(CharBuffer.wrap(s));
+        final byte[] bytes = new byte[buffer.limit()];
+        buffer.get(bytes);
+        return bytes;
+    }
+
+    private static String decode(Charset charset, byte[] b) {
+        if (b == null) {
+            return null;
+        }
+        final CharBuffer cb = charset.decode(ByteBuffer.wrap(b));
+        return new String(cb.array(), 0, cb.length());
+    }
+
+    public static ByteArrayInputStream streamFromAsciiString(String ascii) {
+        return new ByteArrayInputStream(toAscii(ascii));
+    }
+}
diff --git a/src/com/android/phone/vvm/omtp/OmtpVvmSyncService.java b/src/com/android/phone/vvm/omtp/OmtpVvmSyncService.java
index a27e424..edc9bec 100644
--- a/src/com/android/phone/vvm/omtp/OmtpVvmSyncService.java
+++ b/src/com/android/phone/vvm/omtp/OmtpVvmSyncService.java
@@ -35,6 +35,7 @@
 import android.net.Uri;
 import android.os.Bundle;
 import android.os.IBinder;
+import android.provider.VoicemailContract;
 import android.provider.VoicemailContract.Voicemails;
 import android.telecom.Voicemail;
 
@@ -68,6 +69,12 @@
     }
 
     public class OmtpVvmSyncAdapter extends AbstractThreadedSyncAdapter {
+        /**
+         * Sync triggers should pass this extra to clear the database and freshly populate from the
+         * server.
+         */
+        public static final String SYNC_EXTRAS_CLEAR_AND_RELOAD = "extra_clear_and_reload";
+
         private Context mContext;
         private ContentResolver mContentResolver;
 
@@ -80,6 +87,8 @@
         @Override
         public void onPerformSync(Account account, Bundle extras, String authority,
                 ContentProviderClient provider, SyncResult syncResult) {
+            ImapHelper imapHelper = new ImapHelper(mContext, account);
+
             if (extras.getBoolean(ContentResolver.SYNC_EXTRAS_UPLOAD, false)) {
                 List<Voicemail> readVoicemails = new ArrayList<Voicemail>();
                 List<Voicemail> deletedVoicemails = new ArrayList<Voicemail>();
@@ -106,7 +115,6 @@
                 } finally {
                     cursor.close();
                 }
-                ImapHelper imapHelper = new ImapHelper(mContext, account);
                 if (imapHelper.markMessagesAsDeleted(deletedVoicemails)) {
                     // We want to delete selectively instead of all the voicemails for this provider
                     // in case the state changed since the IMAP query was completed.
@@ -117,6 +125,19 @@
                     markReadInDatabase(readVoicemails);
                 }
             }
+
+            if (extras.getBoolean(SYNC_EXTRAS_CLEAR_AND_RELOAD, false)) {
+                // Fetch voicemails first before deleting local copy, the fetching may take awhile.
+                List<Voicemail> voicemails = imapHelper.fetchAllVoicemails();
+
+                // Deleting current local messages ensure that we start with a fresh copy
+                // and also don't need to deal with comparing between local and server.
+                VoicemailContract.Voicemails.deleteAll(mContext);
+
+                if (voicemails != null) {
+                    VoicemailContract.Voicemails.insert(mContext, voicemails);
+                }
+            }
         }
 
         /**
diff --git a/src/com/android/phone/vvm/omtp/imap/ImapHelper.java b/src/com/android/phone/vvm/omtp/imap/ImapHelper.java
index acaf81c..22b919b 100644
--- a/src/com/android/phone/vvm/omtp/imap/ImapHelper.java
+++ b/src/com/android/phone/vvm/omtp/imap/ImapHelper.java
@@ -19,15 +19,24 @@
 import android.accounts.AccountManager;
 import android.content.Context;
 import android.telecom.Voicemail;
-import android.util.Log;
 
+import com.android.phone.common.mail.Address;
+import com.android.phone.common.mail.BodyPart;
+import com.android.phone.common.mail.FetchProfile;
+import com.android.phone.common.mail.Flag;
 import com.android.phone.common.mail.Message;
 import com.android.phone.common.mail.MessagingException;
+import com.android.phone.common.mail.Multipart;
+import com.android.phone.common.mail.TempDirectory;
+import com.android.phone.common.mail.internet.MimeMessage;
 import com.android.phone.common.mail.store.ImapFolder;
 import com.android.phone.common.mail.store.ImapStore;
 import com.android.phone.common.mail.store.imap.ImapConstants;
+import com.android.phone.common.mail.utils.LogUtils;
 import com.android.phone.vvm.omtp.OmtpConstants;
 
+import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
 
 /**
@@ -38,9 +47,13 @@
 
     private ImapFolder mFolder;
     private ImapStore mImapStore;
+    private Context mContext;
 
     public ImapHelper(Context context, Account account) {
         try {
+            mContext = context;
+            TempDirectory.setTempDirectory(context);
+
             AccountManager accountManager = AccountManager.get(context);
             String username = accountManager.getUserData(account, OmtpConstants.IMAP_USER_NAME);
             String password = accountManager.getUserData(account, OmtpConstants.IMAP_PASSWORD);
@@ -50,20 +63,20 @@
             // TODO: determine the security protocol (e.g. ssl, tls, none, etc.)
             mImapStore = new ImapStore(
                     context, username, password, port, serverName,
-                    ImapStore.FLAG_SSL | ImapStore.FLAG_TLS);
+                    ImapStore.FLAG_TLS);
         } catch (NumberFormatException e) {
-            Log.e(TAG, "Could not parse port number", e);
+            LogUtils.e(TAG, e, "Could not parse port number");
         }
     }
 
     /** The caller thread will block until the method returns. */
     public boolean markMessagesAsRead(List<Voicemail> voicemails) {
-        return setFlags(voicemails, Message.FLAG_SEEN);
+        return setFlags(voicemails, Flag.SEEN);
     }
 
     /** The caller thread will block until the method returns. */
     public boolean markMessagesAsDeleted(List<Voicemail> voicemails) {
-        return setFlags(voicemails, Message.FLAG_DELETED);
+        return setFlags(voicemails, Flag.DELETED);
     }
 
     /**
@@ -79,26 +92,177 @@
         }
         try {
             mFolder = openImapFolder(ImapFolder.MODE_READ_WRITE);
-            if (mFolder != null ) {
+            if (mFolder != null) {
                 mFolder.setFlags(convertToImapMessages(voicemails), flags, true);
                 return true;
             }
             return false;
         } catch (MessagingException e) {
-            Log.e(TAG, "Messaging exception: ", e);
+            LogUtils.e(TAG, e, "Messaging exception");
             return false;
         } finally {
             closeImapFolder();
         }
     }
 
+    /**
+     * Fetch a list of voicemails from the server.
+     *
+     * @return A list of voicemail objects containing data about voicemails stored on the server.
+     */
+    public List<Voicemail> fetchAllVoicemails() {
+        List<Voicemail> result = new ArrayList<Voicemail>();
+        Message[] messages;
+        try {
+            mFolder = openImapFolder(ImapFolder.MODE_READ_WRITE);
+            if (mFolder == null) {
+                // This means we were unable to successfully open the folder.
+                return null;
+            }
+
+            // This method retrieves lightweight messages containing only the uid of the message.
+            messages = mFolder.getMessages(null);
+
+            for (Message message : messages) {
+                // Get the voicemail details.
+                Voicemail voicemail = fetchVoicemail(message);
+                if (voicemail != null) {
+                    result.add(voicemail);
+                }
+            }
+            return result;
+        } catch (MessagingException e) {
+            LogUtils.e(TAG, e, "Messaging Exception");
+            return null;
+        } finally {
+            closeImapFolder();
+        }
+    }
+
+    /**
+     * Fetches the structure of the given message and returns the voicemail parsed from it.
+     *
+     * @throws MessagingException if fetching the structure of the message fails
+     */
+    private Voicemail fetchVoicemail(Message message)
+            throws MessagingException {
+        LogUtils.d(TAG, "Fetching message structure for " + message.getUid());
+
+        MessageStructureFetchedListener listener = new MessageStructureFetchedListener();
+
+        FetchProfile fetchProfile = new FetchProfile();
+        fetchProfile.addAll(Arrays.asList(FetchProfile.Item.FLAGS, FetchProfile.Item.ENVELOPE,
+                FetchProfile.Item.STRUCTURE));
+
+        // The IMAP folder fetch method will call "messageRetrieved" on the listener when the
+        // message is successfully retrieved.
+        mFolder.fetch(new Message[] {message}, fetchProfile, listener);
+        return listener.getVoicemail();
+    }
+
+    /**
+     * Listener for the message structure being fetched.
+     */
+    private final class MessageStructureFetchedListener
+            implements ImapFolder.MessageRetrievalListener {
+        private Voicemail mVoicemail;
+
+        public MessageStructureFetchedListener() {
+        }
+
+        public Voicemail getVoicemail() {
+            return mVoicemail;
+        }
+
+        @Override
+        public void messageRetrieved(Message message) {
+            LogUtils.d(TAG, "Fetched message structure for " + message.getUid());
+            LogUtils.d(TAG, "Message retrieved: " + message);
+            try {
+                mVoicemail = getVoicemailFromMessage(message);
+                if (mVoicemail == null) {
+                    LogUtils.d(TAG, "This voicemail does not have an attachment...");
+                    return;
+                }
+            } catch (MessagingException e) {
+                LogUtils.e(TAG, e, "Messaging Exception");
+                closeImapFolder();
+            }
+        }
+
+        @Override
+        public void loadAttachmentProgress(int progress) {}
+
+        /**
+         * Convert an IMAP message to a voicemail object.
+         *
+         * @param message The IMAP message.
+         * @return The voicemail object corresponding to an IMAP message.
+         * @throws MessagingException
+         */
+        private Voicemail getVoicemailFromMessage(Message message) throws MessagingException {
+            if (!message.getMimeType().startsWith("multipart/")) {
+                LogUtils.w(TAG, "Ignored non multi-part message");
+                return null;
+            }
+            Multipart multipart = (Multipart) message.getBody();
+
+            LogUtils.d(TAG, "Num body parts: " + multipart.getCount());
+
+            for (int i = 0; i < multipart.getCount(); ++i) {
+                BodyPart bodyPart = multipart.getBodyPart(i);
+                String bodyPartMimeType = bodyPart.getMimeType().toLowerCase();
+
+                LogUtils.d(TAG, "bodyPart mime type: " + bodyPartMimeType);
+
+                if (bodyPartMimeType.startsWith("audio/")) {
+                    // Found an audio attachment, this is a valid voicemail.
+                    long time = message.getSentDate().getTime();
+                    String number = getNumber(message.getFrom());
+                    boolean isRead = Arrays.asList(message.getFlags()).contains(Flag.SEEN);
+
+                    return Voicemail.createForInsertion(time, number)
+                            .setSourcePackage(mContext.getPackageName())
+                            .setSourceData(message.getUid())
+                            .setIsRead(isRead)
+                            .build();
+                }
+            }
+            // No attachment found, this is not a voicemail.
+            return null;
+        }
+
+        /**
+         * The "from" field of a visual voicemail IMAP message is the number of the caller who left
+         * the message. Extract this number from the list of "from" addresses.
+         *
+         * @param fromAddresses A list of addresses that comprise the "from" line.
+         * @return The number of the voicemail sender.
+         */
+        private String getNumber(Address[] fromAddresses) {
+            if (fromAddresses != null && fromAddresses.length > 0) {
+                if (fromAddresses.length != 1) {
+                    LogUtils.w(TAG, "More than one from addresses found. Using the first one.");
+                }
+                String sender = fromAddresses[0].getAddress();
+                int atPos = sender.indexOf('@');
+                if (atPos != -1) {
+                    // Strip domain part of the address.
+                    sender = sender.substring(0, atPos);
+                }
+                return sender;
+            }
+            return null;
+        }
+    }
+
     private ImapFolder openImapFolder(String modeReadWrite) {
         try {
             ImapFolder folder = new ImapFolder(mImapStore, ImapConstants.INBOX);
             folder.open(modeReadWrite);
             return folder;
         } catch (MessagingException e) {
-            Log.e(TAG, "Messaging Exception:", e);
+            LogUtils.e(TAG, e, "Messaging Exception");
         }
         return null;
     }
@@ -106,7 +270,7 @@
     private Message[] convertToImapMessages(List<Voicemail> voicemails) {
         Message[] messages = new Message[voicemails.size()];
         for (int i = 0; i < voicemails.size(); ++i) {
-            messages[i] = new Message();
+            messages[i] = new MimeMessage();
             messages[i].setUid(voicemails.get(i).getSourceData());
         }
         return messages;
diff --git a/src/com/android/phone/vvm/omtp/sms/OmtpMessageReceiver.java b/src/com/android/phone/vvm/omtp/sms/OmtpMessageReceiver.java
index 9181e50..07cdafa 100644
--- a/src/com/android/phone/vvm/omtp/sms/OmtpMessageReceiver.java
+++ b/src/com/android/phone/vvm/omtp/sms/OmtpMessageReceiver.java
@@ -32,6 +32,7 @@
 import com.android.phone.PhoneUtils;
 import com.android.phone.vvm.omtp.OmtpConstants;
 import com.android.phone.vvm.omtp.OmtpVvmSyncAccountManager;
+import com.android.phone.vvm.omtp.OmtpVvmSyncService.OmtpVvmSyncAdapter;
 
 import java.io.UnsupportedEncodingException;
 
@@ -133,12 +134,15 @@
                     VoicemailContract.Status.CONFIGURATION_STATE_OK,
                     VoicemailContract.Status.DATA_CHANNEL_STATE_OK,
                     VoicemailContract.Status.NOTIFICATION_CHANNEL_STATE_OK);
+
+            Bundle bundle = new Bundle();
+            bundle.putBoolean(OmtpVvmSyncAdapter.SYNC_EXTRAS_CLEAR_AND_RELOAD, true);
+            bundle.putBoolean(ContentResolver.SYNC_EXTRAS_UPLOAD, true);
+            ContentResolver.requestSync(account, VoicemailContract.AUTHORITY, bundle);
         }
 
         // Save the IMAP credentials in the corresponding account object so they are
         // persistent and can be retrieved.
         vvmAccountSyncManager.setAccountCredentialsFromStatusMessage(account, message);
-
-        ContentResolver.requestSync(account, VoicemailContract.AUTHORITY, new Bundle());
     }
 }