am b4cb510b: Show a dialog to discover the removal setting

* commit 'b4cb510b1cef97580b0f29562aa7edc25257322b':
  Show a dialog to discover the removal setting
diff --git a/Android.mk b/Android.mk
index a1c9cb6..1cf0bb6 100644
--- a/Android.mk
+++ b/Android.mk
@@ -39,7 +39,7 @@
 
 LOCAL_SRC_FILES := $(call all-java-files-under, $(src_dirs)) \
         $(call all-logtags-files-under, $(src_dirs))
-LOCAL_RESOURCE_DIR := $(addprefix $(LOCAL_PATH)/, $(res_dirs)) $(LOCAL_PATH)/res
+LOCAL_RESOURCE_DIR := $(addprefix $(LOCAL_PATH)/, $(res_dirs))
 LOCAL_AAPT_FLAGS := --auto-add-overlay
 LOCAL_AAPT_FLAGS += --extra-packages com.android.ex.chips:com.android.ex.photo
 
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 24fa2b2..130700b 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -111,6 +111,16 @@
             android:label="@string/app_name"
             android:theme="@style/PhotoViewTheme" >
         </activity>
+        <activity
+                android:name=".browse.EmlViewerActivity"
+                android:label="@string/app_name"
+                android:exported="false">
+            <intent-filter>
+                <action android:name="android.intent.action.VIEW" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <data android:mimeType="message/rfc822" />
+            </intent-filter>
+        </activity>
 
         <provider
             android:authorities="com.android.mail.mockprovider"
diff --git a/res/color/folder_item_text_color.xml b/res/color/folder_item_text_color.xml
index ab79a8d..47e27f0 100644
--- a/res/color/folder_item_text_color.xml
+++ b/res/color/folder_item_text_color.xml
@@ -16,7 +16,10 @@
      limitations under the License.
 -->
 
-<selector xmlns:android="http://schemas.android.com/apk/res/android">
+<selector xmlns:android="http://schemas.android.com/apk/res/android"
+          xmlns:app="http://schemas.android.com/apk/res-auto">
+    <item app:state_drag_mode="true" android:state_drag_can_accept="false"
+          android:color="@color/folder_disabled_drop_target_text_color" />
     <item android:state_activated="true" android:color="@android:color/white" />
     <item android:color="@color/dark_gray_text_color" />
 </selector>
diff --git a/res/drawable/folder_item.xml b/res/drawable/folder_item.xml
index c28ae55..4fd24d0 100644
--- a/res/drawable/folder_item.xml
+++ b/res/drawable/folder_item.xml
@@ -16,7 +16,13 @@
      limitations under the License.
 -->
 
-<selector xmlns:android="http://schemas.android.com/apk/res/android">
+<selector xmlns:android="http://schemas.android.com/apk/res/android"
+          xmlns:app="http://schemas.android.com/apk/res-auto">
+    <item app:state_drag_mode="true"
+          android:state_drag_can_accept="true" android:state_drag_hovered="true"
+          android:drawable="@drawable/list_pressed_holo" />
+    <item app:state_drag_mode="true"
+          android:drawable="@drawable/ic_drawer_divider" />
     <item android:state_pressed="true" android:drawable="@drawable/list_pressed_holo" />
     <item android:state_activated="true" android:drawable="@color/mail_app_blue" />
     <item android:state_focused="true" android:drawable="@drawable/list_focused_holo" />
diff --git a/res/layout/eml_viewer_activity.xml b/res/layout/eml_viewer_activity.xml
new file mode 100644
index 0000000..cbdcee2
--- /dev/null
+++ b/res/layout/eml_viewer_activity.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2013 Google Inc.
+     Licensed to 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.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+              android:orientation="vertical"
+              android:layout_width="match_parent"
+              android:layout_height="match_parent">
+    <WebView
+            android:id="@+id/eml_web_view"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+        />
+</LinearLayout>
\ No newline at end of file
diff --git a/res/layout/folder_item.xml b/res/layout/folder_item.xml
index c36d272..334c312 100644
--- a/res/layout/folder_item.xml
+++ b/res/layout/folder_item.xml
@@ -44,10 +44,12 @@
         android:layout_height="wrap_content"
         android:layout_centerVertical="true"
         android:layout_marginRight="@dimen/folder_list_item_right_margin"
-        android:layout_alignParentRight="true">
+        android:layout_alignParentRight="true"
+        android:duplicateParentState="true">
 
         <TextView
             android:id="@+id/unread"
+            android:duplicateParentState="true"
             style="@style/UnreadCount" />
 
         <TextView
@@ -63,6 +65,7 @@
         android:layout_centerVertical="true"
         android:layout_marginLeft="@dimen/folder_list_item_left_margin"
         android:layout_alignParentLeft="true"
+        android:duplicateParentState="true"
         android:visibility="gone" />
 
     <TextView
@@ -75,6 +78,7 @@
         android:layout_alignWithParentIfMissing="true"
         android:layout_marginLeft="@dimen/folder_list_item_left_margin"
         android:layout_marginRight="@dimen/folder_list_item_right_margin"
+        android:duplicateParentState="true"
         android:includeFontPadding="false"
         android:maxLines="2"
         android:ellipsize="end"
diff --git a/res/layout/nested_folder.xml b/res/layout/nested_folder.xml
new file mode 100644
index 0000000..d93765b
--- /dev/null
+++ b/res/layout/nested_folder.xml
@@ -0,0 +1,68 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2013 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.
+-->
+
+<!-- View that displays folders that are contained in a folder. These are shown at the top of
+     the conversation list. Email has them, Gmail doesn't currently. -->
+<com.android.mail.ui.NestedFolderView
+        xmlns:android="http://schemas.android.com/apk/res/android"
+        android:layout_height="wrap_content"
+        android:layout_width="match_parent"
+        android:minHeight="@dimen/folder_list_item_minimum_height"
+        android:background="@drawable/folder_item" >
+
+    <!--This is a rough layout. We don't have UX specs yet, so all the values are hardcoded.
+        Also, it looks totally ugly. The ugliness is intentional.-->
+    <RelativeLayout
+            android:id="@+id/swipeable_content"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center_vertical"
+            >
+
+        <ImageView
+                android:id="@+id/nested_folder_icon"
+                android:layout_height="wrap_content"
+                android:layout_width="wrap_content"
+                android:layout_marginLeft="16dp"
+                android:layout_marginRight="16dp"
+                android:src="@drawable/ic_menu_folders_holo_light"
+                android:layout_alignParentLeft="true"
+                android:contentDescription="@string/folder_icon_desc"
+                android:layout_centerVertical="true"
+                />
+        <TextView
+                android:layout_width="wrap_content"
+                android:id="@+id/nested_folder_name"
+                android:includeFontPadding="false"
+                android:maxLines="2"
+                android:ellipsize="end"
+                android:textColor="@color/folder_item_text_color"
+                android:textAppearance="?android:attr/textAppearanceMedium"
+                android:layout_centerVertical="true"
+                android:layout_height="match_parent"
+                android:layout_toRightOf="@id/nested_folder_icon" />
+
+        <TextView
+                android:layout_width="wrap_content"
+                android:layout_height="match_parent"
+                android:id="@+id/nested_folder_unread"
+                style="@style/UnreadCount"
+                android:layout_alignBaseline="@id/nested_folder_name"
+                android:layout_alignParentRight="true"
+                android:layout_marginRight="16dp"
+                />
+    </RelativeLayout>
+</com.android.mail.ui.NestedFolderView>
\ No newline at end of file
diff --git a/res/layout/secure_conversation_view.xml b/res/layout/secure_conversation_view.xml
index 26b4920..4ea6c42 100644
--- a/res/layout/secure_conversation_view.xml
+++ b/res/layout/secure_conversation_view.xml
@@ -29,13 +29,17 @@
             <include layout="@layout/conversation_view_header"
                 android:id="@+id/conv_header"
                 android:layout_width="match_parent"
-                android:layout_height="wrap_content" />
+                android:layout_height="wrap_content"
+                android:layout_marginLeft="@dimen/conversation_view_margin_side"
+                android:layout_marginRight="@dimen/conversation_view_margin_side" />
 
             <include layout="@layout/conversation_message_header"
                 android:id="@+id/message_header"
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content"
-                android:layout_below="@id/conv_header" />
+                android:layout_below="@id/conv_header"
+                android:layout_marginLeft="@dimen/conversation_view_margin_side"
+                android:layout_marginRight="@dimen/conversation_view_margin_side" />
             <!-- base WebView layer -->
             <com.android.mail.browse.MessageWebView
                 android:id="@+id/webview"
@@ -46,6 +50,8 @@
                 android:id="@+id/message_footer"
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content"
+                android:layout_marginLeft="@dimen/conversation_view_margin_side"
+                android:layout_marginRight="@dimen/conversation_view_margin_side"
                 android:visibility="gone" />
         </LinearLayout>
     </com.android.mail.browse.MessageScrollView>
diff --git a/res/values/attrs.xml b/res/values/attrs.xml
index 70fc10a..0a2d767 100644
--- a/res/values/attrs.xml
+++ b/res/values/attrs.xml
@@ -27,4 +27,7 @@
     <add-resource name="RecipientComposeFieldLayout" type="style" />
     <add-resource name="ComposeBodyStyle" type="style" />
     <add-resource name="ComposeSubjectStyle" type="style" />
+    <declare-styleable name="FolderItemViewDrawableState">
+        <attr name="state_drag_mode" format="boolean" />
+    </declare-styleable>
 </resources>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 20ffd5e..247a875 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -913,4 +913,6 @@
     <string name="drawer_close">Close navigation drawer</string>
 
     <string name="conversation_photo_welcome_text">Touch a sender image to select that conversation.</string>
+    <!-- Content description for the folder icon for nested folders. -->
+    <string name="folder_icon_desc">Folder icon</string>
 </resources>
diff --git a/src/com/android/emailcommon/TempDirectory.java b/src/com/android/emailcommon/TempDirectory.java
new file mode 100644
index 0000000..252488c
--- /dev/null
+++ b/src/com/android/emailcommon/TempDirectory.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2011 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.emailcommon;
+
+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;
+    }
+}
diff --git a/src/com/android/emailcommon/internet/BinaryTempFileBody.java b/src/com/android/emailcommon/internet/BinaryTempFileBody.java
new file mode 100644
index 0000000..f0821ed
--- /dev/null
+++ b/src/com/android/emailcommon/internet/BinaryTempFileBody.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.emailcommon.internet;
+
+import com.android.emailcommon.TempDirectory;
+import com.android.emailcommon.mail.Body;
+import com.android.emailcommon.mail.MessagingException;
+
+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);
+    }
+
+    public InputStream getInputStream() throws MessagingException {
+        try {
+            return new BinaryTempFileBodyInputStream(new FileInputStream(mFile));
+        }
+        catch (IOException ioe) {
+            throw new MessagingException("Unable to open body", ioe);
+        }
+    }
+
+    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/emailcommon/internet/MimeBodyPart.java b/src/com/android/emailcommon/internet/MimeBodyPart.java
new file mode 100644
index 0000000..01efd55
--- /dev/null
+++ b/src/com/android/emailcommon/internet/MimeBodyPart.java
@@ -0,0 +1,193 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.emailcommon.internet;
+
+import com.android.emailcommon.mail.Body;
+import com.android.emailcommon.mail.BodyPart;
+import com.android.emailcommon.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);
+    }
+
+    public void addHeader(String name, String value) throws MessagingException {
+        mHeader.addHeader(name, value);
+    }
+
+    public void setHeader(String name, String value) throws MessagingException {
+        mHeader.setHeader(name, value);
+    }
+
+    public String[] getHeader(String name) throws MessagingException {
+        return mHeader.getHeader(name);
+    }
+
+    public void removeHeader(String name) throws MessagingException {
+        mHeader.removeHeader(name);
+    }
+
+    public Body getBody() throws MessagingException {
+        return mBody;
+    }
+
+    public void setBody(Body body) throws MessagingException {
+        this.mBody = body;
+        if (body instanceof com.android.emailcommon.mail.Multipart) {
+            com.android.emailcommon.mail.Multipart multipart =
+                ((com.android.emailcommon.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");
+        }
+    }
+
+    public String getContentType() throws MessagingException {
+        String contentType = getFirstHeader(MimeHeader.HEADER_CONTENT_TYPE);
+        if (contentType == null) {
+            return "text/plain";
+        } else {
+            return contentType;
+        }
+    }
+
+    public String getDisposition() throws MessagingException {
+        String contentDisposition = getFirstHeader(MimeHeader.HEADER_CONTENT_DISPOSITION);
+        if (contentDisposition == null) {
+            return null;
+        } else {
+            return contentDisposition;
+        }
+    }
+
+    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");
+        }
+    }
+
+    public String getMimeType() throws MessagingException {
+        return MimeUtility.getHeaderParameter(getContentType(), null);
+    }
+
+    public boolean isMimeType(String mimeType) throws MessagingException {
+        return getMimeType().equals(mimeType);
+    }
+
+    public void setSize(int size) {
+        this.mSize = size;
+    }
+
+    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
+     */
+    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 
+     */
+    public String getExtendedHeader(String name) throws MessagingException {
+        if (mExtendedHeader == null) {
+            return null;
+        }
+        return mExtendedHeader.getFirstHeader(name);
+    }
+
+    /**
+     * Write the MimeMessage out in MIME format.
+     */
+    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/emailcommon/internet/MimeHeader.java b/src/com/android/emailcommon/internet/MimeHeader.java
new file mode 100644
index 0000000..e9b0212
--- /dev/null
+++ b/src/com/android/emailcommon/internet/MimeHeader.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.emailcommon.internet;
+
+import com.android.emailcommon.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.appendMessages and is put back
+     * into the MIME data by LocalStore.fetch.
+     */
+    public static final String HEADER_ANDROID_ATTACHMENT_STORE_DATA = "X-Android-Attachment-StoreData";
+    /**
+     * Application specific header that is used to tag body parts for quoted/forwarded messages.
+     */
+    public static final String HEADER_ANDROID_BODY_QUOTED_PART = "X-Android-Body-Quoted-Part";
+
+    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/emailcommon/internet/MimeMessage.java b/src/com/android/emailcommon/internet/MimeMessage.java
new file mode 100644
index 0000000..b3ee70e
--- /dev/null
+++ b/src/com/android/emailcommon/internet/MimeMessage.java
@@ -0,0 +1,644 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.emailcommon.internet;
+
+import com.android.emailcommon.mail.Address;
+import com.android.emailcommon.mail.Body;
+import com.android.emailcommon.mail.BodyPart;
+import com.android.emailcommon.mail.Message;
+import com.android.emailcommon.mail.MessagingException;
+import com.android.emailcommon.mail.Multipart;
+import com.android.emailcommon.mail.Part;
+
+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 supercede
+    // 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 String generateMessageId() {
+        StringBuffer sb = new StringBuffer();
+        sb.append("<");
+        for (int i = 0; i < 24; i++) {
+            // We'll use a 5-bit range (0..31)
+            int value = sRandom.nextInt() & 31;
+            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
+     * @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;
+
+        MimeStreamParser parser = new MimeStreamParser();
+        parser.setContentHandler(new MimeMessageBuilder());
+        return parser;
+    }
+
+    protected void parse(InputStream in) throws IOException, MessagingException {
+        MimeStreamParser parser = init();
+        parser.parse(new EOLConvertingInputStream(in));
+        mComplete = !parser.getPrematureEof();
+    }
+
+    public void parse(InputStream in, EOLConvertingInputStream.Callback callback)
+            throws IOException, MessagingException {
+        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();
+            } catch (Exception e) {
+
+            }
+        }
+        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 {
+        String contentType = getFirstHeader(MimeHeader.HEADER_CONTENT_TYPE);
+        if (contentType == null) {
+            return "text/plain";
+        } else {
+            return contentType;
+        }
+    }
+
+    public String getDisposition() throws MessagingException {
+        String contentDisposition = getFirstHeader(MimeHeader.HEADER_CONTENT_DISPOSITION);
+        if (contentDisposition == null) {
+            return null;
+        } else {
+            return contentDisposition;
+        }
+    }
+
+    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");
+        }
+    }
+
+    public boolean isComplete() {
+        return mComplete;
+    }
+
+    public String getMimeType() throws MessagingException {
+        return MimeUtility.getHeaderParameter(getContentType(), null);
+    }
+
+    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(RecipientType type) throws MessagingException {
+        if (type == RecipientType.TO) {
+            if (mTo == null) {
+                mTo = Address.parse(MimeUtility.unfold(getFirstHeader("To")));
+            }
+            return mTo;
+        } else if (type == RecipientType.CC) {
+            if (mCc == null) {
+                mCc = Address.parse(MimeUtility.unfold(getFirstHeader("CC")));
+            }
+            return mCc;
+        } else if (type == RecipientType.BCC) {
+            if (mBcc == null) {
+                mBcc = Address.parse(MimeUtility.unfold(getFirstHeader("BCC")));
+            }
+            return mBcc;
+        } else {
+            throw new MessagingException("Unrecognized recipient type.");
+        }
+    }
+
+    @Override
+    public void setRecipients(RecipientType 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 == RecipientType.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 == RecipientType.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 == RecipientType.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) {
+            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
+     */
+    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
+     */
+    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 (String header : END_OF_LINE.split(headers)) {
+                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.
+     */
+    public void writeTo(OutputStream out) throws IOException, MessagingException {
+        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);
+        }
+    }
+
+    public InputStream getInputStream() throws MessagingException {
+        return null;
+    }
+
+    class MimeMessageBuilder implements ContentHandler {
+        private 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() + "'");
+            }
+        }
+
+        public void startMessage() {
+            if (stack.isEmpty()) {
+                stack.push(MimeMessage.this);
+            } else {
+                expect(Part.class);
+                try {
+                    MimeMessage m = new MimeMessage();
+                    ((Part)stack.peek()).setBody(m);
+                    stack.push(m);
+                } catch (MessagingException me) {
+                    throw new Error(me);
+                }
+            }
+        }
+
+        public void endMessage() {
+            expect(MimeMessage.class);
+            stack.pop();
+        }
+
+        public void startHeader() {
+            expect(Part.class);
+        }
+
+        public void field(String fieldData) {
+            expect(Part.class);
+            try {
+                String[] tokens = fieldData.split(":", 2);
+                ((Part)stack.peek()).addHeader(tokens[0], tokens[1].trim());
+            } catch (MessagingException me) {
+                throw new Error(me);
+            }
+        }
+
+        public void endHeader() {
+            expect(Part.class);
+        }
+
+        public void startMultipart(BodyDescriptor bd) {
+            expect(Part.class);
+
+            Part e = (Part)stack.peek();
+            try {
+                MimeMultipart multiPart = new MimeMultipart(e.getContentType());
+                e.setBody(multiPart);
+                stack.push(multiPart);
+            } catch (MessagingException me) {
+                throw new Error(me);
+            }
+        }
+
+        public void body(BodyDescriptor bd, InputStream in) throws IOException {
+            expect(Part.class);
+            Body body = MimeUtility.decodeBody(in, bd.getTransferEncoding());
+            try {
+                ((Part)stack.peek()).setBody(body);
+            } catch (MessagingException me) {
+                throw new Error(me);
+            }
+        }
+
+        public void endMultipart() {
+            stack.pop();
+        }
+
+        public void startBodyPart() {
+            expect(MimeMultipart.class);
+
+            try {
+                MimeBodyPart bodyPart = new MimeBodyPart();
+                ((MimeMultipart)stack.peek()).addBodyPart(bodyPart);
+                stack.push(bodyPart);
+            } catch (MessagingException me) {
+                throw new Error(me);
+            }
+        }
+
+        public void endBodyPart() {
+            expect(BodyPart.class);
+            stack.pop();
+        }
+
+        public void epilogue(InputStream is) throws IOException {
+            expect(MimeMultipart.class);
+            StringBuffer sb = new StringBuffer();
+            int b;
+            while ((b = is.read()) != -1) {
+                sb.append((char)b);
+            }
+            // ((Multipart) stack.peek()).setEpilogue(sb.toString());
+        }
+
+        public void preamble(InputStream is) throws IOException {
+            expect(MimeMultipart.class);
+            StringBuffer sb = new StringBuffer();
+            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);
+            }
+        }
+
+        public void raw(InputStream is) throws IOException {
+            throw new UnsupportedOperationException("Not supported");
+        }
+    }
+}
diff --git a/src/com/android/emailcommon/internet/MimeMultipart.java b/src/com/android/emailcommon/internet/MimeMultipart.java
new file mode 100644
index 0000000..e6977ee
--- /dev/null
+++ b/src/com/android/emailcommon/internet/MimeMultipart.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.emailcommon.internet;
+
+import com.android.emailcommon.mail.BodyPart;
+import com.android.emailcommon.mail.MessagingException;
+import com.android.emailcommon.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);
+    }
+
+    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();
+    }
+
+    public InputStream getInputStream() throws MessagingException {
+        return null;
+    }
+
+    public String getSubTypeForTest() {
+        return mSubType;
+    }
+}
diff --git a/src/com/android/emailcommon/internet/MimeUtility.java b/src/com/android/emailcommon/internet/MimeUtility.java
new file mode 100644
index 0000000..a4cada9
--- /dev/null
+++ b/src/com/android/emailcommon/internet/MimeUtility.java
@@ -0,0 +1,453 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.emailcommon.internet;
+
+import android.text.TextUtils;
+import android.util.Base64;
+import android.util.Base64DataException;
+import android.util.Base64InputStream;
+import android.util.Log;
+
+import com.android.emailcommon.mail.Body;
+import com.android.emailcommon.mail.BodyPart;
+import com.android.emailcommon.mail.Message;
+import com.android.emailcommon.mail.MessagingException;
+import com.android.emailcommon.mail.Multipart;
+import com.android.emailcommon.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;
+    }
+
+    public static Part findFirstPartByMimeType(Part part, String mimeType)
+            throws MessagingException {
+        if (part.getBody() instanceof Multipart) {
+            Multipart multipart = (Multipart)part.getBody();
+            for (int i = 0, count = multipart.getCount(); i < count; i++) {
+                BodyPart bodyPart = multipart.getBodyPart(i);
+                Part ret = findFirstPartByMimeType(bodyPart, mimeType);
+                if (ret != null) {
+                    return ret;
+                }
+            }
+        }
+        else if (part.getMimeType().equalsIgnoreCase(mimeType)) {
+            return part;
+        }
+        return null;
+    }
+
+    public static Part findPartByContentId(Part part, String contentId) throws Exception {
+        if (part.getBody() instanceof Multipart) {
+            Multipart multipart = (Multipart)part.getBody();
+            for (int i = 0, count = multipart.getCount(); i < count; i++) {
+                BodyPart bodyPart = multipart.getBodyPart(i);
+                Part ret = findPartByContentId(bodyPart, contentId);
+                if (ret != null) {
+                    return ret;
+                }
+            }
+        }
+        String cid = part.getContentId();
+        if (contentId.equals(cid)) {
+            return part;
+        }
+        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/emailcommon/internet/TextBody.java b/src/com/android/emailcommon/internet/TextBody.java
new file mode 100644
index 0000000..09c265c
--- /dev/null
+++ b/src/com/android/emailcommon/internet/TextBody.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.emailcommon.internet;
+
+import com.android.emailcommon.mail.Body;
+import com.android.emailcommon.mail.MessagingException;
+
+import android.util.Base64;
+
+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;
+    }
+
+    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.
+     */
+    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/emailcommon/mail/Address.java b/src/com/android/emailcommon/mail/Address.java
new file mode 100644
index 0000000..d87e8c2
--- /dev/null
+++ b/src/com/android/emailcommon/mail/Address.java
@@ -0,0 +1,455 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.emailcommon.mail;
+
+import android.text.TextUtils;
+import android.text.util.Rfc822Token;
+import android.text.util.Rfc822Tokenizer;
+
+import com.google.common.annotations.VisibleForTesting;
+
+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 {
+    /**
+     *  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;
+
+    // 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("\\\\([\\\\\"])");
+
+    private static final Address[] EMPTY_ADDRESS_ARRAY = new Address[0];
+
+    // delimiters are chars that do not appear in an email address, used by pack/unpack
+    private static final char LIST_DELIMITER_EMAIL = '\1';
+    private static final char LIST_DELIMITER_PERSONAL = '\2';
+
+    public Address(String address, String personal) {
+        setAddress(address);
+        setPersonal(personal);
+    }
+
+    public Address(String address) {
+        setAddress(address);
+    }
+
+    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 name 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) {
+        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;
+            }
+        }
+        mPersonal = 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.
+     */
+    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[] {});
+    }
+
+    /**
+     * Checks whether a string email address is valid.
+     * E.g. name@domain.com is valid.
+     */
+    @VisibleForTesting
+    static boolean isValidAddress(String address) {
+        // Note: Some email provider may violate the standard, so here we only check that
+        // address consists of two part that are separated by '@', and domain part contains
+        // at least one '.'.
+        int len = address.length();
+        int firstAt = address.indexOf('@');
+        int lastAt = address.lastIndexOf('@');
+        int firstDot = address.indexOf('.', lastAt + 1);
+        int lastDot = address.lastIndexOf('.');
+        return firstAt > 0 && firstAt == lastAt && lastAt + 1 < firstDot
+            && firstDot <= lastDot && lastDot < len - 1;
+    }
+
+    @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);
+    }
+
+    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 quoteString(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.
+     *
+     * TODO: Rename this, because "quoteString()" can mean so many different things.
+     *
+     * sample -> "sample"
+     * "sample" -> "sample"
+     * ""sample"" -> "sample"
+     * "sample"" -> "sample"
+     * sa"mp"le -> "sa"mp"le"
+     * "sa"mp"le" -> "sa"mp"le"
+     * (empty string) -> ""
+     * " -> ""
+     */
+    public static String quoteString(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.
+     */
+    public static String toString(Address[] addresses) {
+        return toString(addresses, ",");
+    }
+
+    /**
+     * 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();
+        }
+        StringBuffer sb = new StringBuffer(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();
+        }
+        StringBuffer sb = new StringBuffer(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
+     */
+    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.
+     */
+    public static String toFriendly(Address[] addresses) {
+        if (addresses == null || addresses.length == 0) {
+            return null;
+        }
+        if (addresses.length == 1) {
+            return addresses[0].toFriendly();
+        }
+        StringBuffer sb = new StringBuffer(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.unpack(packedList)).
+     */
+    public static String unpackToString(String packedList) {
+        return toString(unpack(packedList));
+    }
+
+    /**
+     * Returns exactly the same result as Address.pack(Address.parse(textList)).
+     */
+    public static String parseAndPack(String textList) {
+        return Address.pack(Address.parse(textList));
+    }
+
+    /**
+     * Returns null if the packedList has 0 addresses, otherwise returns the first address.
+     * The same as Address.unpack(packedList)[0] for non-empty list.
+     * This is an utility method that offers some performance optimization opportunities.
+     */
+    public static Address unpackFirst(String packedList) {
+        Address[] array = unpack(packedList);
+        return array.length > 0 ? array[0] : null;
+    }
+
+    /**
+     * Convert a packed list of addresses to a form suitable for use in an RFC822 header.
+     * This implementation is brute-force, and could be replaced with a more efficient version
+     * if desired.
+     */
+    public static String packedToHeader(String packedList) {
+        return toHeader(unpack(packedList));
+    }
+
+    /**
+     * Unpacks an address list that is either CSV of RFC822 addresses OR (for backward
+     * compatibility) previously packed with pack()
+     * @param addressList string packed with pack() or CSV of RFC822 addresses
+     * @return array of addresses resulting from unpack
+     */
+    public static Address[] unpack(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-compatibile unpack
+        ArrayList<Address> addresses = new ArrayList<Address>();
+        int length = addressList.length();
+        int pairStartIndex = 0;
+        int pairEndIndex = 0;
+
+        /* 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(EMPTY_ADDRESS_ARRAY);
+    }
+
+    /**
+     * Generate a String containing RFC822 addresses separated by commas
+     * NOTE: We used to "pack" these addresses in an app-specific format, but no longer do so
+     */
+    public static String pack(Address[] addresses) {
+        return Address.toHeader(addresses);
+    }
+
+    /**
+     * Produces the same result as pack(array), but only packs one (this) address.
+     */
+    public String pack() {
+        final String address = getAddress();
+        final String personal = getPersonal();
+        if (personal == null) {
+            return address;
+        } else {
+            return address + LIST_DELIMITER_PERSONAL + personal;
+        }
+    }
+}
diff --git a/src/com/android/emailcommon/mail/AuthenticationFailedException.java b/src/com/android/emailcommon/mail/AuthenticationFailedException.java
new file mode 100644
index 0000000..af8d96c
--- /dev/null
+++ b/src/com/android/emailcommon/mail/AuthenticationFailedException.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.emailcommon.mail;
+
+
+public class AuthenticationFailedException extends MessagingException {
+    public static final long serialVersionUID = -1;
+
+    public AuthenticationFailedException(String message) {
+        super(MessagingException.AUTHENTICATION_FAILED, message);
+    }
+
+    public AuthenticationFailedException(int exceptionType, String message) {
+        super(exceptionType, message);
+    }
+
+    public AuthenticationFailedException(String message, Throwable throwable) {
+        super(MessagingException.AUTHENTICATION_FAILED, message, throwable);
+    }
+}
diff --git a/src/com/android/emailcommon/mail/Body.java b/src/com/android/emailcommon/mail/Body.java
new file mode 100644
index 0000000..841ab42
--- /dev/null
+++ b/src/com/android/emailcommon/mail/Body.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.emailcommon.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/unified_src/com/android/mail/utils/LogTag.java b/src/com/android/emailcommon/mail/BodyPart.java
similarity index 61%
copy from unified_src/com/android/mail/utils/LogTag.java
copy to src/com/android/emailcommon/mail/BodyPart.java
index 01e2cf8..f698a13 100644
--- a/unified_src/com/android/mail/utils/LogTag.java
+++ b/src/com/android/emailcommon/mail/BodyPart.java
@@ -1,28 +1,25 @@
-/**
- * Copyright (c) 2012, Google Inc.
- *
- * 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.mail.utils;
-
-public class LogTag {
-    private static String LOG_TAG = "UnifiedEmail";
-
-    /**
-     * Get the log tag to apply to logging.
-     */
-    public static String getLogTag() {
-        return LOG_TAG;
-    }
-}
+/*

+ * Copyright (C) 2008 The Android Open Source Project

+ *

+ * Licensed under the Apache License, Version 2.0 (the "License");

+ * you may not use this file except in compliance with the License.

+ * You may obtain a copy of the License at

+ *

+ *      http://www.apache.org/licenses/LICENSE-2.0

+ *

+ * Unless required by applicable law or agreed to in writing, software

+ * distributed under the License is distributed on an "AS IS" BASIS,

+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.

+ * See the License for the specific language governing permissions and

+ * limitations under the License.

+ */

+

+package com.android.emailcommon.mail;

+

+public abstract class BodyPart implements Part {

+    protected Multipart mParent;

+

+    public Multipart getParent() {

+        return mParent;

+    }

+}

diff --git a/src/com/android/emailcommon/mail/CertificateValidationException.java b/src/com/android/emailcommon/mail/CertificateValidationException.java
new file mode 100644
index 0000000..83c6224
--- /dev/null
+++ b/src/com/android/emailcommon/mail/CertificateValidationException.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.emailcommon.mail;
+
+
+public class CertificateValidationException extends MessagingException {
+    public static final long serialVersionUID = -1;
+
+    public CertificateValidationException(String message) {
+        super(MessagingException.CERTIFICATE_VALIDATION_ERROR, message);
+    }
+
+    public CertificateValidationException(String message, Throwable throwable) {
+        super(MessagingException.CERTIFICATE_VALIDATION_ERROR, message, throwable);
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/emailcommon/mail/FetchProfile.java b/src/com/android/emailcommon/mail/FetchProfile.java
new file mode 100644
index 0000000..bfa48d3
--- /dev/null
+++ b/src/com/android/emailcommon/mail/FetchProfile.java
@@ -0,0 +1,85 @@
+/*

+ * Copyright (C) 2008 The Android Open Source Project

+ *

+ * Licensed under the Apache License, Version 2.0 (the "License");

+ * you may not use this file except in compliance with the License.

+ * You may obtain a copy of the License at

+ *

+ *      http://www.apache.org/licenses/LICENSE-2.0

+ *

+ * Unless required by applicable law or agreed to in writing, software

+ * distributed under the License is distributed on an "AS IS" BASIS,

+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.

+ * See the License for the specific language governing permissions and

+ * limitations under the License.

+ */

+

+package com.android.emailcommon.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/unified_src/com/android/mail/utils/LogTag.java b/src/com/android/emailcommon/mail/Fetchable.java
similarity index 61%
copy from unified_src/com/android/mail/utils/LogTag.java
copy to src/com/android/emailcommon/mail/Fetchable.java
index 01e2cf8..4314f93 100644
--- a/unified_src/com/android/mail/utils/LogTag.java
+++ b/src/com/android/emailcommon/mail/Fetchable.java
@@ -1,11 +1,11 @@
-/**
- * Copyright (c) 2012, Google Inc.
+/*
+ * Copyright (C) 2010 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
  *
- *     http://www.apache.org/licenses/LICENSE-2.0
+ *      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,
@@ -14,15 +14,11 @@
  * limitations under the License.
  */
 
-package com.android.mail.utils;
+package com.android.emailcommon.mail;
 
-public class LogTag {
-    private static String LOG_TAG = "UnifiedEmail";
-
-    /**
-     * Get the log tag to apply to logging.
-     */
-    public static String getLogTag() {
-        return LOG_TAG;
-    }
+/**
+ * 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/emailcommon/mail/Flag.java b/src/com/android/emailcommon/mail/Flag.java
new file mode 100644
index 0000000..bcdcb8b
--- /dev/null
+++ b/src/com/android/emailcommon/mail/Flag.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.emailcommon.mail;
+
+/**
+ * Flags that can be applied to Messages.
+ */
+public enum Flag {
+    
+    // If adding new flags: ALL FLAGS MUST BE UPPER CASE.
+
+    DELETED,
+    SEEN,
+    ANSWERED,
+    FLAGGED,
+    DRAFT,
+    RECENT,
+
+    /*
+     * The following flags are for internal library use only.
+     * TODO Eventually we should creates a Flags class that extends ArrayList that allows
+     * these flags and Strings to represent user defined flags. At that point the below
+     * flags should become user defined flags.
+     */
+    /**
+     * Delete and remove from the LocalStore immediately.
+     */
+    X_DESTROYED,
+
+    /**
+     * Sending of an unsent message failed. It will be retried. Used to show status.
+     */
+    X_SEND_FAILED,
+
+    /**
+     * Sending of an unsent message is in progress.
+     */
+    X_SEND_IN_PROGRESS,
+
+    /**
+     * Indicates that a message is fully downloaded from the server and can be viewed normally.
+     * This does not include attachments, which are never downloaded fully.
+     */
+    X_DOWNLOADED_FULL,
+
+    /**
+     * Indicates that a message is partially downloaded from the server and can be viewed but
+     * more content is available on the server.
+     * This does not include attachments, which are never downloaded fully.
+     */
+    X_DOWNLOADED_PARTIAL,
+    
+    /**
+     * General purpose flag that can be used by any remote store.  The flag will be 
+     * saved and restored by the LocalStore.
+     */
+    X_STORE_1,
+    
+    /**
+     * General purpose flag that can be used by any remote store.  The flag will be 
+     * saved and restored by the LocalStore.
+     */
+    X_STORE_2,
+    
+}
diff --git a/src/com/android/emailcommon/mail/Folder.java b/src/com/android/emailcommon/mail/Folder.java
new file mode 100644
index 0000000..c58988d
--- /dev/null
+++ b/src/com/android/emailcommon/mail/Folder.java
@@ -0,0 +1,209 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.emailcommon.mail;
+
+import com.android.emailcommon.service.SearchParams;
+import com.google.common.annotations.VisibleForTesting;
+
+
+public abstract class Folder {
+    public enum OpenMode {
+        READ_WRITE, READ_ONLY,
+    }
+
+    public enum FolderType {
+        HOLDS_FOLDERS, HOLDS_MESSAGES,
+    }
+
+    /**
+     * Identifiers of "special" folders.
+     */
+    public enum FolderRole {
+        INBOX,      // NOTE:  The folder's name must be INBOX
+        TRASH,
+        SENT,
+        DRAFTS,
+
+        OUTBOX,     // Local folders only - not used in remote Stores
+        OTHER,      // this folder has no specific role
+        UNKNOWN     // the role of this folder is unknown
+    }
+
+    /**
+     * Callback for each message retrieval.
+     *
+     * Not all {@link Folder} implementations may invoke it.
+     */
+    public interface MessageRetrievalListener {
+        public void messageRetrieved(Message message);
+        public void loadAttachmentProgress(int progress);
+    }
+
+    /**
+     * Forces an open of the MailProvider. If the provider is already open this
+     * function returns without doing anything.
+     *
+     * @param mode READ_ONLY or READ_WRITE
+     * @param callbacks Pointer to callbacks class.  This may be used by the folder between this
+     * time and when close() is called.  This is only used for remote stores - should be null
+     * for LocalStore.LocalFolder.
+     */
+    public abstract void open(OpenMode mode)
+            throws MessagingException;
+
+    /**
+     * Forces a close of the MailProvider. Any further access will attempt to
+     * reopen the MailProvider.
+     *
+     * @param expunge If true all deleted messages will be expunged.
+     */
+    public abstract void close(boolean expunge) throws MessagingException;
+
+    /**
+     * @return True if further commands are not expected to have to open the
+     *         connection.
+     */
+    @VisibleForTesting
+    public abstract boolean isOpen();
+
+    /**
+     * Returns the mode the folder was opened with. This may be different than the mode the open
+     * was requested with.
+     */
+    public abstract OpenMode getMode() throws MessagingException;
+
+    /**
+     * Reports if the Store is able to create folders of the given type.
+     * Does not actually attempt to create a folder.
+     * @param type
+     * @return true if can create, false if cannot create
+     */
+    public abstract boolean canCreate(FolderType type);
+
+    /**
+     * Attempt to create the given folder remotely using the given type.
+     * @return true if created, false if cannot create (e.g. server side)
+     */
+    public abstract boolean create(FolderType type) throws MessagingException;
+
+    public abstract boolean exists() throws MessagingException;
+
+    /**
+     * Returns the number of messages in the selected folder.
+     */
+    public abstract int getMessageCount() throws MessagingException;
+
+    public abstract int getUnreadMessageCount() throws MessagingException;
+
+    public abstract Message getMessage(String uid) throws MessagingException;
+
+    /**
+     * Fetches the given list of messages. The specified listener is notified as
+     * each fetch completes. Messages are downloaded as (as) lightweight (as
+     * possible) objects to be filled in with later requests. In most cases this
+     * means that only the UID is downloaded.
+     */
+    public abstract Message[] getMessages(int start, int end, MessageRetrievalListener listener)
+            throws MessagingException;
+
+    public abstract Message[] getMessages(SearchParams params,MessageRetrievalListener listener)
+            throws MessagingException;
+
+    public abstract Message[] getMessages(String[] uids, MessageRetrievalListener listener)
+            throws MessagingException;
+
+    /**
+     * Return a set of messages based on the state of the flags.
+     * Note: Not typically implemented in remote stores, so not abstract.
+     *
+     * @param setFlags The flags that should be set for a message to be selected (can be null)
+     * @param clearFlags The flags that should be clear for a message to be selected (can be null)
+     * @param listener
+     * @return A list of messages matching the desired flag states.
+     * @throws MessagingException
+     */
+    public Message[] getMessages(Flag[] setFlags, Flag[] clearFlags,
+            MessageRetrievalListener listener) throws MessagingException {
+        throw new MessagingException("Not implemented");
+    }
+
+    public abstract void appendMessages(Message[] messages) throws MessagingException;
+
+    /**
+     * Copies the given messages to the destination folder.
+     */
+    public abstract void copyMessages(Message[] msgs, Folder folder,
+            MessageUpdateCallbacks callbacks) throws MessagingException;
+
+    public abstract void setFlags(Message[] messages, Flag[] flags, boolean value)
+            throws MessagingException;
+
+    public abstract Message[] expunge() throws MessagingException;
+
+    public abstract void fetch(Message[] messages, FetchProfile fp,
+            MessageRetrievalListener listener) throws MessagingException;
+
+    public abstract void delete(boolean recurse) throws MessagingException;
+
+    public abstract String getName();
+
+    public abstract Flag[] getPermanentFlags() throws MessagingException;
+
+    /**
+     * This method returns a string identifying the name of a "role" folder
+     * (such as inbox, draft, sent, or trash).  Stores that do not implement this
+     * feature can be used - the account UI will provide default strings.  To
+     * let the server identify specific folder roles, simply override this method.
+     *
+     * @return The server- or protocol- specific role for this folder.  If some roles are known
+     * but this is not one of them, return FolderRole.OTHER.  If roles are unsupported here,
+     * return FolderRole.UNKNOWN.
+     */
+    public FolderRole getRole() {
+        return FolderRole.UNKNOWN;
+    }
+
+    /**
+     * Create an empty message of the appropriate type for the Folder.
+     */
+    public abstract Message createMessage(String uid) throws MessagingException;
+
+    /**
+     * Callback interface by which a folder can report UID changes caused by certain operations.
+     */
+    public interface MessageUpdateCallbacks {
+        /**
+         * The operation caused the message's UID to change
+         * @param message The message for which the UID changed
+         * @param newUid The new UID for the message
+         */
+        public void onMessageUidChange(Message message, String newUid) throws MessagingException;
+
+        /**
+         * The operation could not be completed because the message doesn't exist
+         * (for example, it was already deleted from the server side.)
+         * @param message The message that does not exist
+         * @throws MessagingException
+         */
+        public void onMessageNotFound(Message message) throws MessagingException;
+    }
+
+    @Override
+    public String toString() {
+        return getName();
+    }
+}
diff --git a/src/com/android/emailcommon/mail/MeetingInfo.java b/src/com/android/emailcommon/mail/MeetingInfo.java
new file mode 100644
index 0000000..87637f7
--- /dev/null
+++ b/src/com/android/emailcommon/mail/MeetingInfo.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.emailcommon.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/emailcommon/mail/Message.java b/src/com/android/emailcommon/mail/Message.java
new file mode 100644
index 0000000..09aef87
--- /dev/null
+++ b/src/com/android/emailcommon/mail/Message.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.emailcommon.mail;
+
+import java.util.Date;
+import java.util.HashSet;
+
+public abstract class Message implements Part, Body {
+    public static final Message[] EMPTY_ARRAY = new Message[0];
+
+    public enum RecipientType {
+        TO, CC, BCC,
+    }
+
+    protected String mUid;
+
+    private HashSet<Flag> mFlags = null;
+
+    protected Date mInternalDate;
+
+    protected Folder mFolder;
+
+    public String getUid() {
+        return mUid;
+    }
+
+    public void setUid(String uid) {
+        this.mUid = uid;
+    }
+
+    public Folder getFolder() {
+        return mFolder;
+    }
+
+    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(RecipientType type) throws MessagingException;
+
+    public abstract void setRecipients(RecipientType type, Address[] addresses)
+            throws MessagingException;
+
+    public void setRecipient(RecipientType 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;
+
+    public abstract Body getBody() throws MessagingException;
+
+    public abstract String getContentType() throws MessagingException;
+
+    public abstract void addHeader(String name, String value) throws MessagingException;
+
+    public abstract void setHeader(String name, String value) throws MessagingException;
+
+    public abstract String[] getHeader(String name) throws MessagingException;
+
+    public abstract void removeHeader(String name) 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;
+
+    public abstract void setBody(Body body) throws MessagingException;
+
+    public boolean isMimeType(String mimeType) throws MessagingException {
+        return getContentType().startsWith(mimeType);
+    }
+
+    private HashSet<Flag> getFlagSet() {
+        if (mFlags == null) {
+            mFlags = new HashSet<Flag>();
+        }
+        return mFlags;
+    }
+
+    /*
+     * TODO Refactor Flags at some point to be able to store user defined flags.
+     */
+    public Flag[] getFlags() {
+        return getFlagSet().toArray(new Flag[] {});
+    }
+
+    /**
+     * Set/clear a flag directly, without involving overrides of {@link #setFlag} in subclasses.
+     * Only used for testing.
+     */
+    public final void setFlagDirectlyForTest(Flag flag, boolean set) throws MessagingException {
+        if (set) {
+            getFlagSet().add(flag);
+        } else {
+            getFlagSet().remove(flag);
+        }
+    }
+
+    public void setFlag(Flag flag, boolean set) throws MessagingException {
+        setFlagDirectlyForTest(flag, set);
+    }
+
+    /**
+     * This method calls setFlag(Flag, boolean)
+     * @param flags
+     * @param set
+     */
+    public void setFlags(Flag[] flags, boolean set) throws MessagingException {
+        for (Flag flag : flags) {
+            setFlag(flag, set);
+        }
+    }
+
+    public boolean isSet(Flag 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/emailcommon/mail/MessageDateComparator.java b/src/com/android/emailcommon/mail/MessageDateComparator.java
new file mode 100644
index 0000000..0b1a551
--- /dev/null
+++ b/src/com/android/emailcommon/mail/MessageDateComparator.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.emailcommon.mail;
+
+import java.util.Comparator;
+
+public class MessageDateComparator implements Comparator<Message> {
+    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/emailcommon/mail/MessagingException.java b/src/com/android/emailcommon/mail/MessagingException.java
new file mode 100644
index 0000000..4a8ceba
--- /dev/null
+++ b/src/com/android/emailcommon/mail/MessagingException.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.emailcommon.mail;
+
+
+/**
+ * This exception is used for most types of failures that occur during server interactions.
+ *
+ * Data passed through this exception should be considered non-localized.  Any strings should
+ * either be internal-only (for debugging) or server-generated.
+ *
+ * TO DO: Does it make sense to further collapse AuthenticationFailedException and
+ * CertificateValidationException and any others into this?
+ */
+public class MessagingException extends Exception {
+    public static final long serialVersionUID = -1;
+
+    public static final int NO_ERROR = -1;
+    /** Any exception that does not specify a specific issue */
+    public static final int UNSPECIFIED_EXCEPTION = 0;
+    /** Connection or IO errors */
+    public static final int IOERROR = 1;
+    /** The configuration requested TLS but the server did not support it. */
+    public static final int TLS_REQUIRED = 2;
+    /** Authentication is required but the server did not support it. */
+    public static final int AUTH_REQUIRED = 3;
+    /** General security failures */
+    public static final int GENERAL_SECURITY = 4;
+    /** Authentication failed */
+    public static final int AUTHENTICATION_FAILED = 5;
+    /** Attempt to create duplicate account */
+    public static final int DUPLICATE_ACCOUNT = 6;
+    /** Required security policies reported - advisory only */
+    public static final int SECURITY_POLICIES_REQUIRED = 7;
+   /** Required security policies not supported */
+    public static final int SECURITY_POLICIES_UNSUPPORTED = 8;
+   /** The protocol (or protocol version) isn't supported */
+    public static final int PROTOCOL_VERSION_UNSUPPORTED = 9;
+    /** The server's SSL certificate couldn't be validated */
+    public static final int CERTIFICATE_VALIDATION_ERROR = 10;
+    /** Authentication failed during autodiscover */
+    public static final int AUTODISCOVER_AUTHENTICATION_FAILED = 11;
+    /** Autodiscover completed with a result (non-error) */
+    public static final int AUTODISCOVER_AUTHENTICATION_RESULT = 12;
+    /** Ambiguous failure; server error or bad credentials */
+    public static final int AUTHENTICATION_FAILED_OR_SERVER_ERROR = 13;
+    /** The server refused access */
+    public static final int ACCESS_DENIED = 14;
+    /** The server refused access */
+    public static final int ATTACHMENT_NOT_FOUND = 15;
+    /** A client SSL certificate is required for connections to the server */
+    public static final int CLIENT_CERTIFICATE_REQUIRED = 16;
+    /** The client SSL certificate specified is invalid */
+    public static final int CLIENT_CERTIFICATE_ERROR = 17;
+
+    protected int mExceptionType;
+    // Exception type-specific data
+    protected Object mExceptionData;
+
+    public MessagingException(String message, Throwable throwable) {
+        this(UNSPECIFIED_EXCEPTION, message, throwable);
+    }
+
+    public MessagingException(int exceptionType, String message, Throwable throwable) {
+        super(message, throwable);
+        mExceptionType = exceptionType;
+        mExceptionData = null;
+    }
+
+    /**
+     * Constructs a MessagingException with an exceptionType and a null message.
+     * @param exceptionType The exception type to set for this exception.
+     */
+    public MessagingException(int exceptionType) {
+        this(exceptionType, null, null);
+    }
+
+    /**
+     * Constructs a MessagingException with a message.
+     * @param message the message for this exception
+     */
+    public MessagingException(String message) {
+        this(UNSPECIFIED_EXCEPTION, message, null);
+    }
+
+    /**
+     * Constructs a MessagingException with an exceptionType and a message.
+     * @param exceptionType The exception type to set for this exception.
+     */
+    public MessagingException(int exceptionType, String message) {
+        this(exceptionType, message, null);
+    }
+
+    /**
+     * Constructs a MessagingException with an exceptionType, a message, and data
+     * @param exceptionType The exception type to set for this exception.
+     * @param message the message for the exception (or null)
+     * @param data exception-type specific data for the exception (or null)
+     */
+    public MessagingException(int exceptionType, String message, Object data) {
+        super(message);
+        mExceptionType = exceptionType;
+        mExceptionData = data;
+    }
+
+    /**
+     * Return the exception type.  Will be OTHER_EXCEPTION if not explicitly set.
+     *
+     * @return Returns the exception type.
+     */
+    public int getExceptionType() {
+        return mExceptionType;
+    }
+    /**
+     * Return the exception data.  Will be null if not explicitly set.
+     *
+     * @return Returns the exception data.
+     */
+    public Object getExceptionData() {
+        return mExceptionData;
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/emailcommon/mail/Multipart.java b/src/com/android/emailcommon/mail/Multipart.java
new file mode 100644
index 0000000..4a1a067
--- /dev/null
+++ b/src/com/android/emailcommon/mail/Multipart.java
@@ -0,0 +1,63 @@
+/*

+ * Copyright (C) 2008 The Android Open Source Project

+ *

+ * Licensed under the Apache License, Version 2.0 (the "License");

+ * you may not use this file except in compliance with the License.

+ * You may obtain a copy of the License at

+ *

+ *      http://www.apache.org/licenses/LICENSE-2.0

+ *

+ * Unless required by applicable law or agreed to in writing, software

+ * distributed under the License is distributed on an "AS IS" BASIS,

+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.

+ * See the License for the specific language governing permissions and

+ * limitations under the License.

+ */

+

+package com.android.emailcommon.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/emailcommon/mail/PackedString.java b/src/com/android/emailcommon/mail/PackedString.java
new file mode 100644
index 0000000..de5fe46
--- /dev/null
+++ b/src/com/android/emailcommon/mail/PackedString.java
@@ -0,0 +1,176 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.emailcommon.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/emailcommon/mail/Part.java b/src/com/android/emailcommon/mail/Part.java
new file mode 100644
index 0000000..eeb233c
--- /dev/null
+++ b/src/com/android/emailcommon/mail/Part.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.emailcommon.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/emailcommon/service/SearchParams.java b/src/com/android/emailcommon/service/SearchParams.java
new file mode 100644
index 0000000..3b9d6c9
--- /dev/null
+++ b/src/com/android/emailcommon/service/SearchParams.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2011 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.emailcommon.service;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.google.common.base.Objects;
+
+public class SearchParams implements Parcelable {
+
+    private static final int DEFAULT_LIMIT = 10; // Need input on what this number should be
+    private static final int DEFAULT_OFFSET = 0;
+
+    // The id of the mailbox to be searched; if -1, all mailboxes MUST be searched
+    public final long mMailboxId;
+    // If true, all subfolders of the specified mailbox MUST be searched
+    public boolean mIncludeChildren = true;
+    // The search terms (the search MUST only select messages whose contents include all of the
+    // search terms in the query)
+    public final String mFilter;
+    // The maximum number of results to be created by this search
+    public int mLimit = DEFAULT_LIMIT;
+    // If zero, specifies a "new" search; otherwise, asks for a continuation of the previous
+    // query(ies) starting with the mOffset'th match (0 based)
+    public int mOffset = DEFAULT_OFFSET;
+    // The total number of results for this search
+    public int mTotalCount = 0;
+    // The id of the "search" mailbox being used
+    public long mSearchMailboxId;
+
+    /**
+     * Error codes returned by the searchMessages API
+     */
+    public static class SearchParamsError {
+        public static final int CANT_SEARCH_ALL_MAILBOXES = -1;
+        public static final int CANT_SEARCH_CHILDREN = -2;
+    }
+
+    public SearchParams(long mailboxId, String filter) {
+        mMailboxId = mailboxId;
+        mFilter = filter;
+    }
+
+    public SearchParams(long mailboxId, String filter, long searchMailboxId) {
+        mMailboxId = mailboxId;
+        mFilter = filter;
+        mSearchMailboxId = searchMailboxId;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (o == this) {
+            return true;
+        }
+        if ((o == null) || !(o instanceof SearchParams)) {
+            return false;
+        }
+
+        SearchParams os = (SearchParams) o;
+        return mMailboxId == os.mMailboxId
+                && mIncludeChildren == os.mIncludeChildren
+                && mFilter.equals(os.mFilter)
+                && mLimit == os.mLimit
+                && mOffset == os.mOffset;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hashCode(mMailboxId, mFilter, mOffset);
+    }
+
+    @Override
+    public String toString() {
+        return "[SearchParams " + mMailboxId + ":" + mFilter + " (" + mOffset + ", " + mLimit + "]";
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    /**
+     * Supports Parcelable
+     */
+    public static final Parcelable.Creator<SearchParams> CREATOR
+        = new Parcelable.Creator<SearchParams>() {
+        @Override
+        public SearchParams createFromParcel(Parcel in) {
+            return new SearchParams(in);
+        }
+
+        @Override
+        public SearchParams[] newArray(int size) {
+            return new SearchParams[size];
+        }
+    };
+
+    /**
+     * Supports Parcelable
+     */
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeLong(mMailboxId);
+        dest.writeInt(mIncludeChildren ? 1 : 0);
+        dest.writeString(mFilter);
+        dest.writeInt(mLimit);
+        dest.writeInt(mOffset);
+    }
+
+    /**
+     * Supports Parcelable
+     */
+    public SearchParams(Parcel in) {
+        mMailboxId = in.readLong();
+        mIncludeChildren = in.readInt() == 1;
+        mFilter = in.readString();
+        mLimit = in.readInt();
+        mOffset = in.readInt();
+    }
+}
diff --git a/src/com/android/emailcommon/utility/ConversionUtilities.java b/src/com/android/emailcommon/utility/ConversionUtilities.java
new file mode 100644
index 0000000..0dbb520
--- /dev/null
+++ b/src/com/android/emailcommon/utility/ConversionUtilities.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2011 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.emailcommon.utility;
+
+import com.android.emailcommon.internet.MimeHeader;
+import com.android.emailcommon.internet.MimeUtility;
+import com.android.emailcommon.mail.MessagingException;
+import com.android.emailcommon.mail.Part;
+
+import android.text.TextUtils;
+
+import java.util.ArrayList;
+
+public class ConversionUtilities {
+    /**
+     * Values for HEADER_ANDROID_BODY_QUOTED_PART to tag body parts
+     */
+    public static final String BODY_QUOTED_PART_REPLY = "quoted-reply";
+    public static final String BODY_QUOTED_PART_FORWARD = "quoted-forward";
+    public static final String BODY_QUOTED_PART_INTRO = "quoted-intro";
+
+    /**
+     * Helper function to append text to a StringBuffer, creating it if necessary.
+     * Optimization:  The majority of the time we are *not* appending - we should have a path
+     * that deals with single strings.
+     */
+    private static StringBuffer appendTextPart(StringBuffer sb, String newText) {
+        if (newText == null) {
+            return sb;
+        }
+        else if (sb == null) {
+            sb = new StringBuffer(newText);
+        } else {
+            if (sb.length() > 0) {
+                sb.append('\n');
+            }
+            sb.append(newText);
+        }
+        return sb;
+    }
+
+    /**
+     * Plain-Old-Data class to return parsed body data from
+     * {@link ConversionUtilities#parseBodyFields}
+     */
+    public static class BodyFieldData {
+        public String textContent;
+        public String htmlContent;
+        public String textReply;
+        public String htmlReply;
+        public String introText;
+        public String snippet;
+        public boolean isQuotedReply;
+        public boolean isQuotedForward;
+    }
+
+    /**
+     * Parse body text (plain and/or HTML) from MimeMessage to {@link BodyFieldData}.
+     */
+    public static BodyFieldData parseBodyFields(ArrayList<Part> viewables)
+    throws MessagingException {
+        final BodyFieldData data = new BodyFieldData();
+        StringBuffer sbHtml = null;
+        StringBuffer sbText = null;
+        StringBuffer sbHtmlReply = null;
+        StringBuffer sbTextReply = null;
+        StringBuffer sbIntroText = null;
+
+        for (Part viewable : viewables) {
+            String text = MimeUtility.getTextFromPart(viewable);
+            String[] replyTags = viewable.getHeader(MimeHeader.HEADER_ANDROID_BODY_QUOTED_PART);
+            String replyTag = null;
+            if (replyTags != null && replyTags.length > 0) {
+                replyTag = replyTags[0];
+            }
+            // Deploy text as marked by the various tags
+            boolean isHtml = "text/html".equalsIgnoreCase(viewable.getMimeType());
+
+            if (replyTag != null) {
+                data.isQuotedReply = BODY_QUOTED_PART_REPLY.equalsIgnoreCase(replyTag);
+                data.isQuotedForward = BODY_QUOTED_PART_FORWARD.equalsIgnoreCase(replyTag);
+                boolean isQuotedIntro = BODY_QUOTED_PART_INTRO.equalsIgnoreCase(replyTag);
+
+                if (data.isQuotedReply || data.isQuotedForward) {
+                    if (isHtml) {
+                        sbHtmlReply = appendTextPart(sbHtmlReply, text);
+                    } else {
+                        sbTextReply = appendTextPart(sbTextReply, text);
+                    }
+                    continue;
+                }
+                if (isQuotedIntro) {
+                    sbIntroText = appendTextPart(sbIntroText, text);
+                    continue;
+                }
+            }
+
+            // Most of the time, just process regular body parts
+            if (isHtml) {
+                sbHtml = appendTextPart(sbHtml, text);
+            } else {
+                sbText = appendTextPart(sbText, text);
+            }
+        }
+
+        // write the combined data to the body part
+        if (!TextUtils.isEmpty(sbText)) {
+            String text = sbText.toString();
+            data.textContent = text;
+            data.snippet = TextUtilities.makeSnippetFromPlainText(text);
+        }
+        if (!TextUtils.isEmpty(sbHtml)) {
+            String text = sbHtml.toString();
+            data.htmlContent = text;
+            if (data.snippet == null) {
+                data.snippet = TextUtilities.makeSnippetFromHtmlText(text);
+            }
+        }
+        if (sbHtmlReply != null && sbHtmlReply.length() != 0) {
+            data.htmlReply = sbHtmlReply.toString();
+        }
+        if (sbTextReply != null && sbTextReply.length() != 0) {
+            data.textReply = sbTextReply.toString();
+        }
+        if (sbIntroText != null && sbIntroText.length() != 0) {
+            data.introText = sbIntroText.toString();
+        }
+        return data;
+    }
+}
diff --git a/src/com/android/emailcommon/utility/TextUtilities.java b/src/com/android/emailcommon/utility/TextUtilities.java
new file mode 100755
index 0000000..0aa9190
--- /dev/null
+++ b/src/com/android/emailcommon/utility/TextUtilities.java
@@ -0,0 +1,728 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.emailcommon.utility;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import android.graphics.Color;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.SpannableStringBuilder;
+import android.text.TextUtils;
+import android.text.style.BackgroundColorSpan;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.StringTokenizer;
+
+public class TextUtilities {
+    // Highlight color is yellow, as in other apps.
+    // TODO Push for this to be a global (style-related?) constant
+    public static final int HIGHLIGHT_COLOR_INT = Color.YELLOW;
+    // We AND off the "alpha" from the color (i.e. 0xFFFFFF00 -> 0x00FFFF00)
+    /*package*/ static final String HIGHLIGHT_COLOR_STRING =
+        '#' + Integer.toHexString(HIGHLIGHT_COLOR_INT & 0x00FFFFFF);
+
+    // This is how many chars we'll allow in a snippet
+    private static final int MAX_SNIPPET_LENGTH = 200;
+    // For some reason, isWhitespace() returns false with the following...
+    /*package*/ static final char NON_BREAKING_SPACE_CHARACTER = (char)160;
+
+    // Tags whose content must be stripped as well
+    static final String[] STRIP_TAGS =
+        new String[] {"title", "script", "style", "applet", "head"};
+    // The number of characters we peel off for testing against STRIP_TAGS; this should be the
+    // maximum size of the strings in STRIP_TAGS
+    static final int MAX_STRIP_TAG_LENGTH = 6;
+
+    static final Map<String, Character> ESCAPE_STRINGS;
+    static {
+        // HTML character entity references as defined in HTML 4
+        // see http://www.w3.org/TR/REC-html40/sgml/entities.html
+        ESCAPE_STRINGS = new HashMap<String, Character>(252);
+
+        ESCAPE_STRINGS.put("&nbsp", '\u00A0');
+        ESCAPE_STRINGS.put("&iexcl", '\u00A1');
+        ESCAPE_STRINGS.put("&cent", '\u00A2');
+        ESCAPE_STRINGS.put("&pound", '\u00A3');
+        ESCAPE_STRINGS.put("&curren", '\u00A4');
+        ESCAPE_STRINGS.put("&yen", '\u00A5');
+        ESCAPE_STRINGS.put("&brvbar", '\u00A6');
+        ESCAPE_STRINGS.put("&sect", '\u00A7');
+        ESCAPE_STRINGS.put("&uml", '\u00A8');
+        ESCAPE_STRINGS.put("&copy", '\u00A9');
+        ESCAPE_STRINGS.put("&ordf", '\u00AA');
+        ESCAPE_STRINGS.put("&laquo", '\u00AB');
+        ESCAPE_STRINGS.put("&not", '\u00AC');
+        ESCAPE_STRINGS.put("&shy", '\u00AD');
+        ESCAPE_STRINGS.put("&reg", '\u00AE');
+        ESCAPE_STRINGS.put("&macr", '\u00AF');
+        ESCAPE_STRINGS.put("&deg", '\u00B0');
+        ESCAPE_STRINGS.put("&plusmn", '\u00B1');
+        ESCAPE_STRINGS.put("&sup2", '\u00B2');
+        ESCAPE_STRINGS.put("&sup3", '\u00B3');
+        ESCAPE_STRINGS.put("&acute", '\u00B4');
+        ESCAPE_STRINGS.put("&micro", '\u00B5');
+        ESCAPE_STRINGS.put("&para", '\u00B6');
+        ESCAPE_STRINGS.put("&middot", '\u00B7');
+        ESCAPE_STRINGS.put("&cedil", '\u00B8');
+        ESCAPE_STRINGS.put("&sup1", '\u00B9');
+        ESCAPE_STRINGS.put("&ordm", '\u00BA');
+        ESCAPE_STRINGS.put("&raquo", '\u00BB');
+        ESCAPE_STRINGS.put("&frac14", '\u00BC');
+        ESCAPE_STRINGS.put("&frac12", '\u00BD');
+        ESCAPE_STRINGS.put("&frac34", '\u00BE');
+        ESCAPE_STRINGS.put("&iquest", '\u00BF');
+        ESCAPE_STRINGS.put("&Agrave", '\u00C0');
+        ESCAPE_STRINGS.put("&Aacute", '\u00C1');
+        ESCAPE_STRINGS.put("&Acirc", '\u00C2');
+        ESCAPE_STRINGS.put("&Atilde", '\u00C3');
+        ESCAPE_STRINGS.put("&Auml", '\u00C4');
+        ESCAPE_STRINGS.put("&Aring", '\u00C5');
+        ESCAPE_STRINGS.put("&AElig", '\u00C6');
+        ESCAPE_STRINGS.put("&Ccedil", '\u00C7');
+        ESCAPE_STRINGS.put("&Egrave", '\u00C8');
+        ESCAPE_STRINGS.put("&Eacute", '\u00C9');
+        ESCAPE_STRINGS.put("&Ecirc", '\u00CA');
+        ESCAPE_STRINGS.put("&Euml", '\u00CB');
+        ESCAPE_STRINGS.put("&Igrave", '\u00CC');
+        ESCAPE_STRINGS.put("&Iacute", '\u00CD');
+        ESCAPE_STRINGS.put("&Icirc", '\u00CE');
+        ESCAPE_STRINGS.put("&Iuml", '\u00CF');
+        ESCAPE_STRINGS.put("&ETH", '\u00D0');
+        ESCAPE_STRINGS.put("&Ntilde", '\u00D1');
+        ESCAPE_STRINGS.put("&Ograve", '\u00D2');
+        ESCAPE_STRINGS.put("&Oacute", '\u00D3');
+        ESCAPE_STRINGS.put("&Ocirc", '\u00D4');
+        ESCAPE_STRINGS.put("&Otilde", '\u00D5');
+        ESCAPE_STRINGS.put("&Ouml", '\u00D6');
+        ESCAPE_STRINGS.put("&times", '\u00D7');
+        ESCAPE_STRINGS.put("&Oslash", '\u00D8');
+        ESCAPE_STRINGS.put("&Ugrave", '\u00D9');
+        ESCAPE_STRINGS.put("&Uacute", '\u00DA');
+        ESCAPE_STRINGS.put("&Ucirc", '\u00DB');
+        ESCAPE_STRINGS.put("&Uuml", '\u00DC');
+        ESCAPE_STRINGS.put("&Yacute", '\u00DD');
+        ESCAPE_STRINGS.put("&THORN", '\u00DE');
+        ESCAPE_STRINGS.put("&szlig", '\u00DF');
+        ESCAPE_STRINGS.put("&agrave", '\u00E0');
+        ESCAPE_STRINGS.put("&aacute", '\u00E1');
+        ESCAPE_STRINGS.put("&acirc", '\u00E2');
+        ESCAPE_STRINGS.put("&atilde", '\u00E3');
+        ESCAPE_STRINGS.put("&auml", '\u00E4');
+        ESCAPE_STRINGS.put("&aring", '\u00E5');
+        ESCAPE_STRINGS.put("&aelig", '\u00E6');
+        ESCAPE_STRINGS.put("&ccedil", '\u00E7');
+        ESCAPE_STRINGS.put("&egrave", '\u00E8');
+        ESCAPE_STRINGS.put("&eacute", '\u00E9');
+        ESCAPE_STRINGS.put("&ecirc", '\u00EA');
+        ESCAPE_STRINGS.put("&euml", '\u00EB');
+        ESCAPE_STRINGS.put("&igrave", '\u00EC');
+        ESCAPE_STRINGS.put("&iacute", '\u00ED');
+        ESCAPE_STRINGS.put("&icirc", '\u00EE');
+        ESCAPE_STRINGS.put("&iuml", '\u00EF');
+        ESCAPE_STRINGS.put("&eth", '\u00F0');
+        ESCAPE_STRINGS.put("&ntilde", '\u00F1');
+        ESCAPE_STRINGS.put("&ograve", '\u00F2');
+        ESCAPE_STRINGS.put("&oacute", '\u00F3');
+        ESCAPE_STRINGS.put("&ocirc", '\u00F4');
+        ESCAPE_STRINGS.put("&otilde", '\u00F5');
+        ESCAPE_STRINGS.put("&ouml", '\u00F6');
+        ESCAPE_STRINGS.put("&divide", '\u00F7');
+        ESCAPE_STRINGS.put("&oslash", '\u00F8');
+        ESCAPE_STRINGS.put("&ugrave", '\u00F9');
+        ESCAPE_STRINGS.put("&uacute", '\u00FA');
+        ESCAPE_STRINGS.put("&ucirc", '\u00FB');
+        ESCAPE_STRINGS.put("&uuml", '\u00FC');
+        ESCAPE_STRINGS.put("&yacute", '\u00FD');
+        ESCAPE_STRINGS.put("&thorn", '\u00FE');
+        ESCAPE_STRINGS.put("&yuml", '\u00FF');
+        ESCAPE_STRINGS.put("&fnof", '\u0192');
+        ESCAPE_STRINGS.put("&Alpha", '\u0391');
+        ESCAPE_STRINGS.put("&Beta", '\u0392');
+        ESCAPE_STRINGS.put("&Gamma", '\u0393');
+        ESCAPE_STRINGS.put("&Delta", '\u0394');
+        ESCAPE_STRINGS.put("&Epsilon", '\u0395');
+        ESCAPE_STRINGS.put("&Zeta", '\u0396');
+        ESCAPE_STRINGS.put("&Eta", '\u0397');
+        ESCAPE_STRINGS.put("&Theta", '\u0398');
+        ESCAPE_STRINGS.put("&Iota", '\u0399');
+        ESCAPE_STRINGS.put("&Kappa", '\u039A');
+        ESCAPE_STRINGS.put("&Lambda", '\u039B');
+        ESCAPE_STRINGS.put("&Mu", '\u039C');
+        ESCAPE_STRINGS.put("&Nu", '\u039D');
+        ESCAPE_STRINGS.put("&Xi", '\u039E');
+        ESCAPE_STRINGS.put("&Omicron", '\u039F');
+        ESCAPE_STRINGS.put("&Pi", '\u03A0');
+        ESCAPE_STRINGS.put("&Rho", '\u03A1');
+        ESCAPE_STRINGS.put("&Sigma", '\u03A3');
+        ESCAPE_STRINGS.put("&Tau", '\u03A4');
+        ESCAPE_STRINGS.put("&Upsilon", '\u03A5');
+        ESCAPE_STRINGS.put("&Phi", '\u03A6');
+        ESCAPE_STRINGS.put("&Chi", '\u03A7');
+        ESCAPE_STRINGS.put("&Psi", '\u03A8');
+        ESCAPE_STRINGS.put("&Omega", '\u03A9');
+        ESCAPE_STRINGS.put("&alpha", '\u03B1');
+        ESCAPE_STRINGS.put("&beta", '\u03B2');
+        ESCAPE_STRINGS.put("&gamma", '\u03B3');
+        ESCAPE_STRINGS.put("&delta", '\u03B4');
+        ESCAPE_STRINGS.put("&epsilon", '\u03B5');
+        ESCAPE_STRINGS.put("&zeta", '\u03B6');
+        ESCAPE_STRINGS.put("&eta", '\u03B7');
+        ESCAPE_STRINGS.put("&theta", '\u03B8');
+        ESCAPE_STRINGS.put("&iota", '\u03B9');
+        ESCAPE_STRINGS.put("&kappa", '\u03BA');
+        ESCAPE_STRINGS.put("&lambda", '\u03BB');
+        ESCAPE_STRINGS.put("&mu", '\u03BC');
+        ESCAPE_STRINGS.put("&nu", '\u03BD');
+        ESCAPE_STRINGS.put("&xi", '\u03BE');
+        ESCAPE_STRINGS.put("&omicron", '\u03BF');
+        ESCAPE_STRINGS.put("&pi", '\u03C0');
+        ESCAPE_STRINGS.put("&rho", '\u03C1');
+        ESCAPE_STRINGS.put("&sigmaf", '\u03C2');
+        ESCAPE_STRINGS.put("&sigma", '\u03C3');
+        ESCAPE_STRINGS.put("&tau", '\u03C4');
+        ESCAPE_STRINGS.put("&upsilon", '\u03C5');
+        ESCAPE_STRINGS.put("&phi", '\u03C6');
+        ESCAPE_STRINGS.put("&chi", '\u03C7');
+        ESCAPE_STRINGS.put("&psi", '\u03C8');
+        ESCAPE_STRINGS.put("&omega", '\u03C9');
+        ESCAPE_STRINGS.put("&thetasym", '\u03D1');
+        ESCAPE_STRINGS.put("&upsih", '\u03D2');
+        ESCAPE_STRINGS.put("&piv", '\u03D6');
+        ESCAPE_STRINGS.put("&bull", '\u2022');
+        ESCAPE_STRINGS.put("&hellip", '\u2026');
+        ESCAPE_STRINGS.put("&prime", '\u2032');
+        ESCAPE_STRINGS.put("&Prime", '\u2033');
+        ESCAPE_STRINGS.put("&oline", '\u203E');
+        ESCAPE_STRINGS.put("&frasl", '\u2044');
+        ESCAPE_STRINGS.put("&weierp", '\u2118');
+        ESCAPE_STRINGS.put("&image", '\u2111');
+        ESCAPE_STRINGS.put("&real", '\u211C');
+        ESCAPE_STRINGS.put("&trade", '\u2122');
+        ESCAPE_STRINGS.put("&alefsym", '\u2135');
+        ESCAPE_STRINGS.put("&larr", '\u2190');
+        ESCAPE_STRINGS.put("&uarr", '\u2191');
+        ESCAPE_STRINGS.put("&rarr", '\u2192');
+        ESCAPE_STRINGS.put("&darr", '\u2193');
+        ESCAPE_STRINGS.put("&harr", '\u2194');
+        ESCAPE_STRINGS.put("&crarr", '\u21B5');
+        ESCAPE_STRINGS.put("&lArr", '\u21D0');
+        ESCAPE_STRINGS.put("&uArr", '\u21D1');
+        ESCAPE_STRINGS.put("&rArr", '\u21D2');
+        ESCAPE_STRINGS.put("&dArr", '\u21D3');
+        ESCAPE_STRINGS.put("&hArr", '\u21D4');
+        ESCAPE_STRINGS.put("&forall", '\u2200');
+        ESCAPE_STRINGS.put("&part", '\u2202');
+        ESCAPE_STRINGS.put("&exist", '\u2203');
+        ESCAPE_STRINGS.put("&empty", '\u2205');
+        ESCAPE_STRINGS.put("&nabla", '\u2207');
+        ESCAPE_STRINGS.put("&isin", '\u2208');
+        ESCAPE_STRINGS.put("&notin", '\u2209');
+        ESCAPE_STRINGS.put("&ni", '\u220B');
+        ESCAPE_STRINGS.put("&prod", '\u220F');
+        ESCAPE_STRINGS.put("&sum", '\u2211');
+        ESCAPE_STRINGS.put("&minus", '\u2212');
+        ESCAPE_STRINGS.put("&lowast", '\u2217');
+        ESCAPE_STRINGS.put("&radic", '\u221A');
+        ESCAPE_STRINGS.put("&prop", '\u221D');
+        ESCAPE_STRINGS.put("&infin", '\u221E');
+        ESCAPE_STRINGS.put("&ang", '\u2220');
+        ESCAPE_STRINGS.put("&and", '\u2227');
+        ESCAPE_STRINGS.put("&or", '\u2228');
+        ESCAPE_STRINGS.put("&cap", '\u2229');
+        ESCAPE_STRINGS.put("&cup", '\u222A');
+        ESCAPE_STRINGS.put("&int", '\u222B');
+        ESCAPE_STRINGS.put("&there4", '\u2234');
+        ESCAPE_STRINGS.put("&sim", '\u223C');
+        ESCAPE_STRINGS.put("&cong", '\u2245');
+        ESCAPE_STRINGS.put("&asymp", '\u2248');
+        ESCAPE_STRINGS.put("&ne", '\u2260');
+        ESCAPE_STRINGS.put("&equiv", '\u2261');
+        ESCAPE_STRINGS.put("&le", '\u2264');
+        ESCAPE_STRINGS.put("&ge", '\u2265');
+        ESCAPE_STRINGS.put("&sub", '\u2282');
+        ESCAPE_STRINGS.put("&sup", '\u2283');
+        ESCAPE_STRINGS.put("&nsub", '\u2284');
+        ESCAPE_STRINGS.put("&sube", '\u2286');
+        ESCAPE_STRINGS.put("&supe", '\u2287');
+        ESCAPE_STRINGS.put("&oplus", '\u2295');
+        ESCAPE_STRINGS.put("&otimes", '\u2297');
+        ESCAPE_STRINGS.put("&perp", '\u22A5');
+        ESCAPE_STRINGS.put("&sdot", '\u22C5');
+        ESCAPE_STRINGS.put("&lceil", '\u2308');
+        ESCAPE_STRINGS.put("&rceil", '\u2309');
+        ESCAPE_STRINGS.put("&lfloor", '\u230A');
+        ESCAPE_STRINGS.put("&rfloor", '\u230B');
+        ESCAPE_STRINGS.put("&lang", '\u2329');
+        ESCAPE_STRINGS.put("&rang", '\u232A');
+        ESCAPE_STRINGS.put("&loz", '\u25CA');
+        ESCAPE_STRINGS.put("&spades", '\u2660');
+        ESCAPE_STRINGS.put("&clubs", '\u2663');
+        ESCAPE_STRINGS.put("&hearts", '\u2665');
+        ESCAPE_STRINGS.put("&diams", '\u2666');
+        ESCAPE_STRINGS.put("&quot", '\u0022');
+        ESCAPE_STRINGS.put("&amp", '\u0026');
+        ESCAPE_STRINGS.put("&lt", '\u003C');
+        ESCAPE_STRINGS.put("&gt", '\u003E');
+        ESCAPE_STRINGS.put("&OElig", '\u0152');
+        ESCAPE_STRINGS.put("&oelig", '\u0153');
+        ESCAPE_STRINGS.put("&Scaron", '\u0160');
+        ESCAPE_STRINGS.put("&scaron", '\u0161');
+        ESCAPE_STRINGS.put("&Yuml", '\u0178');
+        ESCAPE_STRINGS.put("&circ", '\u02C6');
+        ESCAPE_STRINGS.put("&tilde", '\u02DC');
+        ESCAPE_STRINGS.put("&ensp", '\u2002');
+        ESCAPE_STRINGS.put("&emsp", '\u2003');
+        ESCAPE_STRINGS.put("&thinsp", '\u2009');
+        ESCAPE_STRINGS.put("&zwnj", '\u200C');
+        ESCAPE_STRINGS.put("&zwj", '\u200D');
+        ESCAPE_STRINGS.put("&lrm", '\u200E');
+        ESCAPE_STRINGS.put("&rlm", '\u200F');
+        ESCAPE_STRINGS.put("&ndash", '\u2013');
+        ESCAPE_STRINGS.put("&mdash", '\u2014');
+        ESCAPE_STRINGS.put("&lsquo", '\u2018');
+        ESCAPE_STRINGS.put("&rsquo", '\u2019');
+        ESCAPE_STRINGS.put("&sbquo", '\u201A');
+        ESCAPE_STRINGS.put("&ldquo", '\u201C');
+        ESCAPE_STRINGS.put("&rdquo", '\u201D');
+        ESCAPE_STRINGS.put("&bdquo", '\u201E');
+        ESCAPE_STRINGS.put("&dagger", '\u2020');
+        ESCAPE_STRINGS.put("&Dagger", '\u2021');
+        ESCAPE_STRINGS.put("&permil", '\u2030');
+        ESCAPE_STRINGS.put("&lsaquo", '\u2039');
+        ESCAPE_STRINGS.put("&rsaquo", '\u203A');
+        ESCAPE_STRINGS.put("&euro", '\u20AC');
+    }
+
+    /**
+     * Code to generate a short 'snippet' from either plain text or html text
+     *
+     * If the sync protocol can get plain text, that's great, but we'll still strip out extraneous
+     * whitespace.  If it's HTML, we'll 1) strip out tags, 2) turn entities into the appropriate
+     * characters, and 3) strip out extraneous whitespace, all in one pass
+     *
+     * Why not use an existing class?  The best answer is performance; yet another answer is
+     * correctness (e.g. Html.textFromHtml simply doesn't generate well-stripped text).  But
+     * performance is key; we frequently sync text that is 10K or (much) longer, yet we really only
+     * care about a small amount of text for the snippet.  So it's critically important that we just
+     * stop when we've gotten enough; existing methods that exist will go through the entire
+     * incoming string, at great (and useless, in this case) expense.
+     */
+
+    public static String makeSnippetFromHtmlText(String text) {
+        return makeSnippetFromText(text, true);
+    }
+
+    public static String makeSnippetFromPlainText(String text) {
+        return makeSnippetFromText(text, false);
+    }
+
+    /**
+     * Find the end of this tag; there are two alternatives: <tag .../> or <tag ...> ... </tag>
+     * @param htmlText some HTML text
+     * @param tag the HTML tag
+     * @param startPos the start position in the HTML text where the tag starts
+     * @return the position just before the end of the tag or -1 if not found
+     */
+    /*package*/ static int findTagEnd(String htmlText, String tag, int startPos) {
+        if (tag.endsWith(" ")) {
+            tag = tag.substring(0, tag.length() - 1);
+        }
+        int length = htmlText.length();
+        char prevChar = 0;
+        for (int i = startPos; i < length; i++) {
+            char c = htmlText.charAt(i);
+            if (c == '>') {
+               if (prevChar == '/') {
+                   return i - 1;
+               }
+               break;
+            }
+            prevChar = c;
+        }
+        // We didn't find /> at the end of the tag so find </tag>
+        return htmlText.indexOf("/" + tag, startPos);
+    }
+
+    public static String makeSnippetFromText(String text, boolean stripHtml) {
+        // Handle null and empty string
+        if (TextUtils.isEmpty(text)) return "";
+
+        final int length = text.length();
+        // Use char[] instead of StringBuilder purely for performance; fewer method calls, etc.
+        char[] buffer = new char[MAX_SNIPPET_LENGTH];
+        // skipCount is an array of a single int; that int is set inside stripHtmlEntity and is
+        // used to determine how many characters can be "skipped" due to the transformation of the
+        // entity to a single character.  When Java allows multiple return values, we can make this
+        // much cleaner :-)
+        int[] skipCount = new int[1];
+        int bufferCount = 0;
+        // Start with space as last character to avoid leading whitespace
+        char last = ' ';
+        // Indicates whether we're in the middle of an HTML tag
+        boolean inTag = false;
+
+        // Walk through the text until we're done with the input OR we've got a large enough snippet
+        for (int i = 0; i < length && bufferCount < MAX_SNIPPET_LENGTH; i++) {
+            char c = text.charAt(i);
+            if (stripHtml && !inTag && (c == '<')) {
+                // Find tags to strip; they will begin with <! or !- or </ or <letter
+                if (i < (length - 1)) {
+                    char peek = text.charAt(i + 1);
+                    if (peek == '!' || peek == '-' || peek == '/' || Character.isLetter(peek)) {
+                        inTag = true;
+                        // Strip content of title, script, style and applet tags
+                        if (i < (length - (MAX_STRIP_TAG_LENGTH + 2))) {
+                            String tag = text.substring(i + 1, i + MAX_STRIP_TAG_LENGTH + 1);
+                            String tagLowerCase = tag.toLowerCase();
+                            boolean stripContent = false;
+                            for (String stripTag: STRIP_TAGS) {
+                                if (tagLowerCase.startsWith(stripTag)) {
+                                    stripContent = true;
+                                    tag = tag.substring(0, stripTag.length());
+                                    break;
+                                }
+                            }
+                            if (stripContent) {
+                                // Look for the end of this tag
+                                int endTagPosition = findTagEnd(text, tag, i);
+                                if (endTagPosition < 0) {
+                                    break;
+                                } else {
+                                    i = endTagPosition;
+                                }
+                            }
+                        }
+                    }
+                }
+            } else if (stripHtml && inTag && (c == '>')) {
+                // Terminate stripping here
+                inTag = false;
+                continue;
+            }
+
+            if (inTag) {
+                // We just skip by everything while we're in a tag
+                continue;
+            } else if (stripHtml && (c == '&')) {
+                // Handle a possible HTML entity here
+                // We always get back a character to use; we also get back a "skip count",
+                // indicating how many characters were eaten from the entity
+                c = stripHtmlEntity(text, i, skipCount);
+                i += skipCount[0];
+            }
+
+            if (Character.isWhitespace(c) || (c == NON_BREAKING_SPACE_CHARACTER)) {
+                // The idea is to find the content in the message, not the whitespace, so we'll
+                // turn any combination of contiguous whitespace into a single space
+                if (last == ' ') {
+                    continue;
+                } else {
+                    // Make every whitespace character a simple space
+                    c = ' ';
+                }
+            } else if ((c == '-' || c == '=') && (last == c)) {
+                // Lots of messages (especially digests) have whole lines of --- or ===
+                // We'll get rid of those duplicates here
+                continue;
+            }
+
+            // After all that, maybe we've got a character for our snippet
+            buffer[bufferCount++] = c;
+            last = c;
+        }
+
+        // Lose trailing space and return our snippet
+        if ((bufferCount > 0) && (last == ' ')) {
+            bufferCount--;
+        }
+        return new String(buffer, 0, bufferCount);
+    }
+
+    static /*package*/ char stripHtmlEntity(String text, int pos, int[] skipCount) {
+        int length = text.length();
+        // Ugly, but we store our skip count in this array; we can't use a static here, because
+        // multiple threads might be calling in
+        skipCount[0] = 0;
+        // All entities are <= 8 characters long, so that's how far we'll look for one (+ & and ;)
+        int end = pos + 10;
+        String entity = null;
+        // Isolate the entity
+        for (int i = pos; (i < length) && (i < end); i++) {
+            if (text.charAt(i) == ';') {
+                entity = text.substring(pos, i);
+                break;
+            }
+        }
+        if (entity == null) {
+            // This wasn't really an HTML entity
+            return '&';
+        } else {
+            // Skip count is the length of the entity
+            Character mapping = ESCAPE_STRINGS.get(entity);
+            int entityLength = entity.length();
+            if (mapping != null) {
+                skipCount[0] = entityLength;
+                return mapping;
+            } else if ((entityLength > 2) && (entity.charAt(1) == '#')) {
+                // &#nn; means ascii nn (decimal) and &#xnn means ascii nn (hex)
+                char c = '?';
+                try {
+                    int i;
+                    if ((entity.charAt(2) == 'x') && (entityLength > 3)) {
+                        i = Integer.parseInt(entity.substring(3), 16);
+                    } else {
+                        i = Integer.parseInt(entity.substring(2));
+                    }
+                    c = (char)i;
+                } catch (NumberFormatException e) {
+                    // We'll just return the ? in this case
+                }
+                skipCount[0] = entityLength;
+                return c;
+            }
+        }
+        // Worst case, we return the original start character, ampersand
+        return '&';
+    }
+
+    /**
+     * Given a string of HTML text and a query containing any number of search terms, returns
+     * an HTML string in which those search terms are highlighted (intended for use in a WebView)
+     *
+     * @param text the HTML text to process
+     * @param query the search terms
+     * @return HTML text with the search terms highlighted
+     */
+    @VisibleForTesting
+    public static String highlightTermsInHtml(String text, String query) {
+        try {
+            return highlightTerms(text, query, true).toString();
+        } catch (IOException e) {
+            // Can't happen, but we must catch this
+            return text;
+        }
+    }
+
+    /**
+     * Given a string of plain text and a query containing any number of search terms, returns
+     * a CharSequence in which those search terms are highlighted (intended for use in a TextView)
+     *
+     * @param text the text to process
+     * @param query the search terms
+     * @return a CharSequence with the search terms highlighted
+     */
+    public static CharSequence highlightTermsInText(String text, String query) {
+        try {
+            return highlightTerms(text, query, false);
+        } catch (IOException e) {
+            // Can't happen, but we must catch this
+            return text;
+        }
+    }
+
+    static class SearchTerm {
+        final String mTerm;
+        final String mTermLowerCase;
+        final int mLength;
+        int mMatchLength = 0;
+        int mMatchStart = -1;
+
+        SearchTerm(String term, boolean html) {
+            mTerm = term;
+            mTermLowerCase = term.toLowerCase();
+            mLength = term.length();
+        }
+    }
+
+    /**
+     * Generate a version of the incoming text in which all search terms in a query are highlighted.
+     * If the input is HTML, we return a StringBuilder with additional markup as required
+     * If the input is text, we return a SpannableStringBuilder with additional spans as required
+     *
+     * @param text the text to be processed
+     * @param query the query, which can contain multiple terms separated by whitespace
+     * @param html whether or not the text to be processed is HTML
+     * @return highlighted text
+     *
+     * @throws IOException as Appendable requires this
+     */
+    public static CharSequence highlightTerms(String text, String query, boolean html)
+            throws IOException {
+        // Handle null and empty string
+        if (TextUtils.isEmpty(text)) return "";
+        final int length = text.length();
+
+        // Break up the query into search terms
+        ArrayList<SearchTerm> terms = new ArrayList<SearchTerm>();
+        if (query != null) {
+            StringTokenizer st = new StringTokenizer(query);
+            while (st.hasMoreTokens()) {
+                terms.add(new SearchTerm(st.nextToken(), html));
+            }
+        }
+
+        // Our appendable depends on whether we're building HTML text (for webview) or spannable
+        // text (for UI)
+        final Appendable sb = html ? new StringBuilder() : new SpannableStringBuilder();
+        // Indicates whether we're in the middle of an HTML tag
+        boolean inTag = false;
+        // The position of the last input character copied to output
+        int lastOut = -1;
+
+        // Walk through the text until we're done with the input
+        // Just copy any HTML tags directly into the output; search for terms in the remaining text
+        for (int i = 0; i < length; i++) {
+            char chr = text.charAt(i);
+            if (html) {
+                if (!inTag && (chr == '<')) {
+                    // Find tags; they will begin with <! or !- or </ or <letter
+                    if (i < (length - 1)) {
+                        char peek = text.charAt(i + 1);
+                        if (peek == '!' || peek == '-' || peek == '/' || Character.isLetter(peek)) {
+                            inTag = true;
+                            // Skip content of title, script, style and applet tags
+                            if (i < (length - (MAX_STRIP_TAG_LENGTH + 2))) {
+                                String tag = text.substring(i + 1, i + MAX_STRIP_TAG_LENGTH + 1);
+                                String tagLowerCase = tag.toLowerCase();
+                                boolean stripContent = false;
+                                for (String stripTag: STRIP_TAGS) {
+                                    if (tagLowerCase.startsWith(stripTag)) {
+                                        stripContent = true;
+                                        tag = tag.substring(0, stripTag.length());
+                                        break;
+                                    }
+                                }
+                                if (stripContent) {
+                                    // Look for the end of this tag
+                                    int endTagPosition = findTagEnd(text, tag, i);
+                                    if (endTagPosition < 0) {
+                                        sb.append(text.substring(i));
+                                        break;
+                                    } else {
+                                        sb.append(text.substring(i, endTagPosition - 1));
+                                        i = endTagPosition - 1;
+                                        chr = text.charAt(i);
+                                    }
+                                }
+                            }
+                        }
+                    }
+                } else if (inTag && (chr == '>')) {
+                    inTag = false;
+                }
+
+                if (inTag) {
+                    sb.append(chr);
+                    continue;
+                }
+            }
+
+            // After all that, we've got some "body" text
+            char chrLowerCase = Character.toLowerCase(chr);
+            // Whether or not the current character should be appended to the output; we inhibit
+            // this while any search terms match
+            boolean appendNow = true;
+            // Look through search terms for matches
+            for (SearchTerm t: terms) {
+                if (chrLowerCase == t.mTermLowerCase.charAt(t.mMatchLength)) {
+                    if (t.mMatchLength++ == 0) {
+                        // New match start
+                        t.mMatchStart = i;
+                    }
+                    if (t.mMatchLength == t.mLength) {
+                        String matchText = text.substring(t.mMatchStart, t.mMatchStart + t.mLength);
+                        // Completed match; add highlight and reset term
+                        if (t.mMatchStart <= lastOut) {
+                            matchText = text.substring(lastOut + 1, i + 1);
+                        }
+                        /*else*/
+                        if (matchText.length() == 0) {} else
+                        if (html) {
+                            sb.append("<span style=\"background-color: " + HIGHLIGHT_COLOR_STRING +
+                                    "\">");
+                            sb.append(matchText);
+                            sb.append("</span>");
+                        } else {
+                            SpannableString highlightSpan = new SpannableString(matchText);
+                            highlightSpan.setSpan(new BackgroundColorSpan(HIGHLIGHT_COLOR_INT), 0,
+                                    highlightSpan.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+                            sb.append(highlightSpan);
+                        }
+                        lastOut = t.mMatchStart + t.mLength - 1;
+                        t.mMatchLength = 0;
+                        t.mMatchStart = -1;
+                    }
+                    appendNow = false;
+                } else {
+                    if (t.mMatchStart >= 0) {
+                        // We're no longer matching; check for other matches in progress
+                        int leastOtherStart = -1;
+                        for (SearchTerm ot: terms) {
+                            // Save away the lowest match start for other search terms
+                            if ((ot != t) && (ot.mMatchStart >= 0) && ((leastOtherStart < 0) ||
+                                    (ot.mMatchStart <= leastOtherStart))) {
+                                leastOtherStart = ot.mMatchStart;
+                            }
+                        }
+                        int matchEnd = t.mMatchStart + t.mMatchLength;
+                        if (leastOtherStart < 0 || leastOtherStart > matchEnd) {
+                            // Append the whole thing
+                            if (t.mMatchStart > lastOut) {
+                                sb.append(text.substring(t.mMatchStart, matchEnd));
+                                lastOut = matchEnd;
+                            }
+                        } else if (leastOtherStart == t.mMatchStart) {
+                            // Ok to append the current char
+                        } else if (leastOtherStart < t.mMatchStart) {
+                            // We're already covered by another search term, so don't append
+                            appendNow = false;
+                        } else if (t.mMatchStart > lastOut) {
+                            // Append the piece of our term that's not already covered
+                            sb.append(text.substring(t.mMatchStart, leastOtherStart));
+                            lastOut = leastOtherStart;
+                        }
+                    }
+                    // Reset this term
+                    t.mMatchLength = 0;
+                    t.mMatchStart = -1;
+                }
+            }
+
+            if (appendNow) {
+                sb.append(chr);
+                lastOut = i;
+            }
+        }
+
+        return (CharSequence)sb;
+   }
+
+    /**
+     * Determine whether two Strings (either of which might be null) are the same; this is true
+     * when both are null or both are Strings that are equal.
+     */
+    public static boolean stringOrNullEquals(String a, String b) {
+        if (a == null && b == null) return true;
+        if (a != null && b != null && a.equals(b)) return true;
+        return false;
+    }
+
+}
diff --git a/src/com/android/mail/MailIntentService.java b/src/com/android/mail/MailIntentService.java
index 065e5fd..2fa0717 100644
--- a/src/com/android/mail/MailIntentService.java
+++ b/src/com/android/mail/MailIntentService.java
@@ -66,7 +66,7 @@
             final Account account = intent.getParcelableExtra(Utils.EXTRA_ACCOUNT);
             final Folder folder = intent.getParcelableExtra(Utils.EXTRA_FOLDER);
 
-            NotificationUtils.clearFolderNotification(this, account, folder);
+            NotificationUtils.clearFolderNotification(this, account, folder, true /* markSeen */);
         } else if (ACTION_RESEND_NOTIFICATIONS.equals(action)) {
             final Uri accountUri = intent.getParcelableExtra(Utils.EXTRA_ACCOUNT_URI);
             final Uri folderUri = intent.getParcelableExtra(Utils.EXTRA_FOLDER_URI);
diff --git a/src/com/android/mail/MailLogService.java b/src/com/android/mail/MailLogService.java
index 4fd63bb..12b4a22 100644
--- a/src/com/android/mail/MailLogService.java
+++ b/src/com/android/mail/MailLogService.java
@@ -23,7 +23,6 @@
 import android.app.Service;
 import android.content.Intent;
 import android.os.IBinder;
-import android.util.Log;
 import android.util.Pair;
 
 import java.io.FileDescriptor;
@@ -143,7 +142,7 @@
      * @return true if this service is functioning at the current log level. False otherwise.
      */
     public static boolean isLoggingLevelHighEnough() {
-        return LogUtils.isLoggable(LOG_TAG, Log.DEBUG);
+        return LogUtils.isLoggable(LOG_TAG, LogUtils.DEBUG);
     }
 
     /**
diff --git a/src/com/android/mail/adapter/DrawerItem.java b/src/com/android/mail/adapter/DrawerItem.java
index aa5b08b..06a9365 100644
--- a/src/com/android/mail/adapter/DrawerItem.java
+++ b/src/com/android/mail/adapter/DrawerItem.java
@@ -31,7 +31,19 @@
 import android.view.ViewGroup;
 import android.widget.TextView;
 
-/** An account, a system folder, a recent folder, or a header (a resource string) */
+/**
+ * An element that is shown in the {@link com.android.mail.ui.FolderListFragment}. This class is
+ * only used for elements that are shown in the {@link com.android.mail.ui.DrawerFragment}.
+ * This class is an enumeration of a few element types: Account, a folder, a recent folder,
+ * or a header (a resource string). A {@link DrawerItem} can only be one type and can never
+ * switch types. Items are created using methods like
+ * {@link DrawerItem#ofAccount(com.android.mail.ui.ControllableActivity,
+ com.android.mail.providers.Account, int, boolean)},
+ * {@link DrawerItem#ofWaitView(com.android.mail.ui.ControllableActivity)}, etc.
+ *
+ * Once created, the item can create a view using {@link #getView(int, android.view.View,
+ android.view.ViewGroup)}.
+ */
 public class DrawerItem {
     private static final String LOG_TAG = LogTag.getLogTag();
     // TODO(viki): Remove this: http://b/8478715
@@ -205,7 +217,7 @@
      * @param activity the underlying activity
      * @return a drawer item with an indeterminate progress indicator.
      */
-    public static DrawerItem forWaitView(ControllableActivity activity) {
+    public static DrawerItem ofWaitView(ControllableActivity activity) {
         return new DrawerItem(
                 VIEW_WAITING_FOR_SYNC, activity, null, INERT_HEADER, null, -1, false, -1);
     }
@@ -214,20 +226,24 @@
         return "[DrawerItem VIEW_WAITING_FOR_SYNC ]";
     }
 
+    /**
+     * Returns a view for the given item. The method signature is identical to that required by a
+     * {@link android.widget.ListAdapter#getView(int, android.view.View, android.view.ViewGroup)}.
+     */
     public View getView(int position, View convertView, ViewGroup parent) {
         final View result;
         switch (mType) {
             case VIEW_FOLDER:
-                result = getFolderView(position, convertView, parent);
+                result = getFolderView(convertView, parent);
                 break;
             case VIEW_HEADER:
-                result = getHeaderView(position, convertView, parent);
+                result = getHeaderView(convertView, parent);
                 break;
             case VIEW_ACCOUNT:
-                result = getAccountView(position, convertView, parent);
+                result = getAccountView(convertView, parent);
                 break;
             case VIEW_WAITING_FOR_SYNC:
-                result = getEmptyView(position, convertView, parent);
+                result = getEmptyView(convertView, parent);
                 break;
             default:
                 LogUtils.wtf(LOG_TAG, "DrawerItem.getView(%d) for an invalid type!", mType);
@@ -246,8 +262,9 @@
     }
 
     /**
-     * Returns whether this view is enabled or not.
-     * @return
+     * Returns whether this view is enabled or not. An enabled view is one that accepts user taps
+     * and acts upon them.
+     * @return true if this view is enabled, false otherwise.
      */
     public boolean isItemEnabled() {
         return mIsEnabled;
@@ -277,9 +294,16 @@
     /**
      * Returns whether this view is highlighted or not.
      *
-     * @param currentFolder
-     * @param currentType
-     * @return
+     * @param currentFolder The current folder, according to the
+     *                      {@link com.android.mail.ui.FolderListFragment}
+     * @param currentType The type of the current folder. We want to only highlight a folder once.
+     *                    A folder might be in two places at once: in "All Folders", and in
+     *                    "Recent Folder". Valid types of selected folders are :
+     *                    {@link DrawerItem#FOLDER_INBOX}, {@link DrawerItem#FOLDER_RECENT} or
+     *                    {@link DrawerItem#FOLDER_OTHER}, or {@link DrawerItem#UNSET}.
+
+     * @return true if this DrawerItem results in a view that is highlighted (this DrawerItem is
+     *              the current folder.
      */
     public boolean isHighlighted(Folder currentFolder, int currentType){
         switch (mType) {
@@ -306,12 +330,12 @@
 
     /**
      * Return a view for an account object.
-     * @param position a zero indexed position in to the list.
+     *
      * @param convertView a view, possibly null, to be recycled.
      * @param parent the parent viewgroup to attach to.
      * @return a view to display at this position.
      */
-    private View getAccountView(int position, View convertView, ViewGroup parent) {
+    private View getAccountView(View convertView, ViewGroup parent) {
         final AccountItemView accountItemView;
         if (convertView != null) {
             accountItemView = (AccountItemView) convertView;
@@ -326,12 +350,13 @@
     }
 
     /**
-     * Returns a text divider between sections.
+     * Returns a text divider between divisions.
+     *
      * @param convertView a previous view, perhaps null
      * @param parent the parent of this view
      * @return a text header at the given position.
      */
-    private View getHeaderView(int position, View convertView, ViewGroup parent) {
+    private View getHeaderView(View convertView, ViewGroup parent) {
         final TextView headerView;
         if (convertView != null) {
             headerView = (TextView) convertView;
@@ -346,12 +371,12 @@
     /**
      * Return a folder: either a parent folder or a normal (child or flat)
      * folder.
-     * @param position a zero indexed position into the top level list.
+     *
      * @param convertView a view, possibly null, to be recycled.
      * @param parent the parent hosting this view.
      * @return a view showing a folder at the given position.
      */
-    private View getFolderView(int position, View convertView, ViewGroup parent) {
+    private View getFolderView(View convertView, ViewGroup parent) {
         final FolderItemView folderItemView;
         if (convertView != null) {
             folderItemView = (FolderItemView) convertView;
@@ -367,12 +392,12 @@
 
     /**
      * Return a view for the 'Waiting for sync' item with the indeterminate progress indicator.
-     * @param position a zero indexed position into the top level list.
+     *
      * @param convertView a view, possibly null, to be recycled.
      * @param parent the parent hosting this view.
      * @return a view for "Waiting for sync..." at given position.
      */
-    private View getEmptyView(int position, View convertView, ViewGroup parent) {
+    private View getEmptyView(View convertView, ViewGroup parent) {
         final ViewGroup emptyView;
         if (convertView != null) {
             emptyView = (ViewGroup) convertView;
diff --git a/src/com/android/mail/browse/ConversationCursor.java b/src/com/android/mail/browse/ConversationCursor.java
index b71efa5..56100a8 100644
--- a/src/com/android/mail/browse/ConversationCursor.java
+++ b/src/com/android/mail/browse/ConversationCursor.java
@@ -37,7 +37,6 @@
 import android.os.SystemClock;
 import android.support.v4.util.SparseArrayCompat;
 import android.text.TextUtils;
-import android.util.Log;
 
 import com.android.mail.content.ThreadSafeCursorWrapper;
 import com.android.mail.providers.Conversation;
@@ -103,7 +102,7 @@
      */
     private static final int URI_COLUMN_INDEX = UIProvider.CONVERSATION_URI_COLUMN;
 
-    private static final boolean DEBUG_DUPLICATE_KEYS = false;
+    private static final boolean DEBUG_DUPLICATE_KEYS = true;
 
     /** The resolver for the cursor instantiator's context */
     private final ContentResolver mResolver;
@@ -561,7 +560,7 @@
 
         final Cursor result = mResolver.query(uri, qProjection, null, null, null);
         if (result == null) {
-            Log.w(LOG_TAG, "doQuery returning null cursor, uri: " + uri);
+            LogUtils.w(LOG_TAG, "doQuery returning null cursor, uri: " + uri);
         } else if (DEBUG) {
             time = System.currentTimeMillis() - time;
             LogUtils.i(LOG_TAG, "ConversationCursor query: %s, %dms, %d results",
diff --git a/src/com/android/mail/browse/ConversationItemView.java b/src/com/android/mail/browse/ConversationItemView.java
index a13fdad..346b645 100644
--- a/src/com/android/mail/browse/ConversationItemView.java
+++ b/src/com/android/mail/browse/ConversationItemView.java
@@ -1402,10 +1402,10 @@
 
     @Override
     public boolean performClick() {
-        boolean handled = super.performClick();
-        SwipeableListView list = getListView();
+        final boolean handled = super.performClick();
+        final SwipeableListView list = getListView();
         if (list != null && list.getAdapter() != null) {
-            int pos = list.findConversation(this, mHeader.conversation);
+            final int pos = list.findConversation(this, mHeader.conversation);
             list.performItemClick(this, pos, mHeader.conversation.id);
         }
         return handled;
diff --git a/src/com/android/mail/browse/ConversationMessage.java b/src/com/android/mail/browse/ConversationMessage.java
new file mode 100644
index 0000000..dbc9b07
--- /dev/null
+++ b/src/com/android/mail/browse/ConversationMessage.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2013 Google Inc.
+ * Licensed to 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.mail.browse;
+
+import android.database.Cursor;
+import android.net.Uri;
+
+import com.android.emailcommon.internet.MimeMessage;
+import com.android.emailcommon.mail.MessagingException;
+import com.android.mail.browse.MessageCursor.ConversationController;
+import com.android.mail.content.CursorCreator;
+import com.android.mail.providers.Attachment;
+import com.android.mail.providers.Conversation;
+import com.android.mail.providers.Message;
+import com.android.mail.ui.ConversationUpdater;
+import com.google.common.base.Objects;
+
+/**
+ * A message created as part of a conversation view. Sometimes, like during star/unstar, it's
+ * handy to have the owning {@link com.android.mail.providers.Conversation} for context.
+ *
+ * <p>This class must remain separate from the {@link MessageCursor} from whence it came,
+ * because cursors can be closed by their Loaders at any time. The
+ * {@link ConversationController} intermediate is used to obtain the currently opened cursor.
+ *
+ * <p>(N.B. This is a {@link android.os.Parcelable}, so try not to add non-transient fields here.
+ * Parcelable state belongs either in {@link com.android.mail.providers.Message} or
+ * {@link com.android.mail.ui.ConversationViewState.MessageViewState}. The
+ * assumption is that this class never needs the state of its extra context saved.)
+ */
+public final class ConversationMessage extends Message {
+
+    private transient ConversationController mController;
+
+    private ConversationMessage(Cursor cursor) {
+        super(cursor);
+    }
+
+    public ConversationMessage(MimeMessage mimeMessage) throws MessagingException {
+        super(mimeMessage);
+        // TODO - synthesize conversation
+    }
+
+    public void setController(ConversationController controller) {
+        mController = controller;
+    }
+
+    public Conversation getConversation() {
+        return mController.getConversation();
+    }
+
+    /**
+     * Returns a hash code based on this message's identity, contents and current state.
+     * This is a separate method from hashCode() to allow for an instance of this class to be
+     * a functional key in a hash-based data structure.
+     *
+     */
+    public int getStateHashCode() {
+        return Objects.hashCode(uri, read, starred, getAttachmentsStateHashCode());
+    }
+
+    private int getAttachmentsStateHashCode() {
+        int hash = 0;
+        for (Attachment a : getAttachments()) {
+            final Uri uri = a.getIdentifierUri();
+            hash += (uri != null ? uri.hashCode() : 0);
+        }
+        return hash;
+    }
+
+    public boolean isConversationStarred() {
+        final MessageCursor c = mController.getMessageCursor();
+        return c != null && c.isConversationStarred();
+    }
+
+    public void star(boolean newStarred) {
+        final ConversationUpdater listController = mController.getListController();
+        if (listController != null) {
+            listController.starMessage(this, newStarred);
+        }
+    }
+
+    /**
+     * Public object that knows how to construct Messages given Cursors.
+     */
+    public static final CursorCreator<ConversationMessage> FACTORY =
+            new CursorCreator<ConversationMessage>() {
+                @Override
+                public ConversationMessage createFromCursor(Cursor c) {
+                    return new ConversationMessage(c);
+                }
+
+                @Override
+                public String toString() {
+                    return "ConversationMessage CursorCreator";
+                }
+            };
+
+}
diff --git a/src/com/android/mail/browse/ConversationOverlayItem.java b/src/com/android/mail/browse/ConversationOverlayItem.java
index ee43363..cae878f 100644
--- a/src/com/android/mail/browse/ConversationOverlayItem.java
+++ b/src/com/android/mail/browse/ConversationOverlayItem.java
@@ -25,7 +25,6 @@
 import android.widget.Adapter;
 import android.widget.CursorAdapter;
 
-import com.android.mail.browse.MessageCursor.ConversationMessage;
 import com.android.mail.ui.ConversationViewFragment;
 import com.android.mail.utils.LogUtils;
 
diff --git a/src/com/android/mail/browse/ConversationViewAdapter.java b/src/com/android/mail/browse/ConversationViewAdapter.java
index 8f84c0c..97e7642 100644
--- a/src/com/android/mail/browse/ConversationViewAdapter.java
+++ b/src/com/android/mail/browse/ConversationViewAdapter.java
@@ -30,7 +30,6 @@
 import com.android.mail.FormattedDateBuilder;
 import com.android.mail.R;
 import com.android.mail.browse.ConversationViewHeader.ConversationViewHeaderCallbacks;
-import com.android.mail.browse.MessageCursor.ConversationMessage;
 import com.android.mail.browse.MessageHeaderView.MessageHeaderViewCallbacks;
 import com.android.mail.browse.SuperCollapsedBlock.OnClickListener;
 import com.android.mail.providers.Address;
diff --git a/src/com/android/mail/browse/EmlViewerActivity.java b/src/com/android/mail/browse/EmlViewerActivity.java
new file mode 100644
index 0000000..58ce69e
--- /dev/null
+++ b/src/com/android/mail/browse/EmlViewerActivity.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2013 Google Inc.
+ * Licensed to 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.mail.browse;
+
+import android.app.Activity;
+import android.content.ContentResolver;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.webkit.WebView;
+
+import com.android.emailcommon.TempDirectory;
+import com.android.emailcommon.internet.MimeMessage;
+import com.android.emailcommon.mail.MessagingException;
+import com.android.mail.R;
+import com.android.mail.utils.LogTag;
+import com.android.mail.utils.LogUtils;
+import com.android.mail.utils.MimeType;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+
+public class EmlViewerActivity extends Activity {
+    private static final String LOG_TAG = LogTag.getLogTag();
+
+    private WebView mWebView;
+
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.eml_viewer_activity);
+        mWebView = (WebView) findViewById(R.id.eml_web_view);
+
+        final Intent intent = getIntent();
+        final String action = intent.getAction();
+        final String type = intent.getType();
+
+        if (Intent.ACTION_VIEW.equals(action) &&
+                MimeType.EML_ATTACHMENT_CONTENT_TYPE.equals(type)) {
+            openEmlFile(intent.getData());
+        } else {
+            LogUtils.wtf(LOG_TAG,
+                    "Entered EmlViewerActivity with wrong intent action or type: %s, %s",
+                    action, type);
+            finish(); // we should not be here. bail out. bail out.
+        }
+    }
+
+    private void openEmlFile(Uri uri) {
+        TempDirectory.setTempDirectory(this);
+        final ContentResolver resolver = getContentResolver();
+        final InputStream stream;
+        try {
+            stream = resolver.openInputStream(uri);
+        } catch (FileNotFoundException e) {
+            // TODO handle exception
+            return;
+        }
+
+        final MimeMessage mimeMessage;
+        final ConversationMessage convMessage;
+        try {
+            mimeMessage = new MimeMessage(stream);
+            convMessage = new ConversationMessage(mimeMessage);
+        } catch (IOException e) {
+            // TODO handle exception
+            return;
+        } catch (MessagingException e) {
+            // TODO handle exception
+            return;
+        }
+
+        mWebView.loadDataWithBaseURL("", convMessage.getBodyAsHtml(), "text/html", "utf-8", null);
+    }
+}
diff --git a/src/com/android/mail/browse/MessageCursor.java b/src/com/android/mail/browse/MessageCursor.java
index 0858448..5a4146a 100644
--- a/src/com/android/mail/browse/MessageCursor.java
+++ b/src/com/android/mail/browse/MessageCursor.java
@@ -20,19 +20,15 @@
 import android.database.Cursor;
 import android.net.Uri;
 import android.os.Bundle;
-import android.os.Parcelable;
 
-import com.android.mail.content.CursorCreator;
 import com.android.mail.content.ObjectCursor;
 import com.android.mail.providers.Account;
 import com.android.mail.providers.Attachment;
 import com.android.mail.providers.Conversation;
-import com.android.mail.providers.Message;
 import com.android.mail.providers.UIProvider.CursorExtraKeys;
 import com.android.mail.providers.UIProvider.CursorStatus;
 import com.android.mail.ui.ConversationUpdater;
 
-import com.google.common.base.Objects;
 import com.google.common.collect.Lists;
 
 import java.util.List;
@@ -41,7 +37,7 @@
  * MessageCursor contains the messages within a conversation; the public methods within should
  * only be called by the UI thread, as cursor position isn't guaranteed to be maintained
  */
-public class MessageCursor extends ObjectCursor<MessageCursor.ConversationMessage> {
+public class MessageCursor extends ObjectCursor<ConversationMessage> {
     /**
      * The current controller that this cursor can use to reference the owning {@link Conversation},
      * and a current {@link ConversationUpdater}. Since this cursor will survive a rotation, but
@@ -59,83 +55,6 @@
         Account getAccount();
     }
 
-    /**
-     * A message created as part of a conversation view. Sometimes, like during star/unstar, it's
-     * handy to have the owning {@link Conversation} for context.
-     *
-     * <p>This class must remain separate from the {@link MessageCursor} from whence it came,
-     * because cursors can be closed by their Loaders at any time. The
-     * {@link ConversationController} intermediate is used to obtain the currently opened cursor.
-     *
-     * <p>(N.B. This is a {@link Parcelable}, so try not to add non-transient fields here.
-     * Parcelable state belongs either in {@link Message} or {@link MessageViewState}. The
-     * assumption is that this class never needs the state of its extra context saved.)
-     */
-    public static final class ConversationMessage extends Message {
-
-        private transient ConversationController mController;
-
-        private ConversationMessage(Cursor cursor) {
-            super(cursor);
-        }
-
-        public void setController(ConversationController controller) {
-            mController = controller;
-        }
-
-        public Conversation getConversation() {
-            return mController.getConversation();
-        }
-
-        /**
-         * Returns a hash code based on this message's identity, contents and current state.
-         * This is a separate method from hashCode() to allow for an instance of this class to be
-         * a functional key in a hash-based data structure.
-         *
-         */
-        public int getStateHashCode() {
-            return Objects.hashCode(uri, read, starred, getAttachmentsStateHashCode());
-        }
-
-        private int getAttachmentsStateHashCode() {
-            int hash = 0;
-            for (Attachment a : getAttachments()) {
-                final Uri uri = a.getIdentifierUri();
-                hash += (uri != null ? uri.hashCode() : 0);
-            }
-            return hash;
-        }
-
-        public boolean isConversationStarred() {
-            final MessageCursor c = mController.getMessageCursor();
-            return c != null && c.isConversationStarred();
-        }
-
-        public void star(boolean newStarred) {
-            final ConversationUpdater listController = mController.getListController();
-            if (listController != null) {
-                listController.starMessage(this, newStarred);
-            }
-        }
-
-        /**
-         * Public object that knows how to construct Messages given Cursors.
-         */
-        public static final CursorCreator<ConversationMessage> FACTORY =
-                new CursorCreator<ConversationMessage>() {
-            @Override
-            public ConversationMessage createFromCursor(Cursor c) {
-                return new ConversationMessage(c);
-            }
-
-            @Override
-            public String toString() {
-                return "ConversationMessage CursorCreator";
-            }
-        };
-
-    }
-
     public MessageCursor(Cursor inner) {
         super(inner, ConversationMessage.FACTORY);
     }
diff --git a/src/com/android/mail/browse/MessageHeaderView.java b/src/com/android/mail/browse/MessageHeaderView.java
index d0a23e2..1083002 100644
--- a/src/com/android/mail/browse/MessageHeaderView.java
+++ b/src/com/android/mail/browse/MessageHeaderView.java
@@ -50,7 +50,6 @@
 import com.android.mail.FormattedDateBuilder;
 import com.android.mail.R;
 import com.android.mail.browse.ConversationViewAdapter.MessageHeaderItem;
-import com.android.mail.browse.MessageCursor.ConversationMessage;
 import com.android.mail.compose.ComposeActivity;
 import com.android.mail.perf.Timer;
 import com.android.mail.preferences.MailPrefs;
diff --git a/src/com/android/mail/compose/ComposeActivity.java b/src/com/android/mail/compose/ComposeActivity.java
index 914c065..98fad35 100644
--- a/src/com/android/mail/compose/ComposeActivity.java
+++ b/src/com/android/mail/compose/ComposeActivity.java
@@ -211,6 +211,8 @@
     private static final String MIME_TYPE_PHOTO = "image/*";
     private static final String MIME_TYPE_VIDEO = "video/*";
 
+    private static final String KEY_INNER_SAVED_STATE = "compose_state";
+
     /**
      * A single thread for running tasks in the background.
      */
@@ -264,7 +266,7 @@
     private RecipientTextWatcher mBccListener;
     private Uri mRefMessageUri;
     private boolean mShowQuotedText = false;
-    private Bundle mSavedInstanceState;
+    private Bundle mInnerSavedState;
 
 
     // Array of the outstanding send or save tasks.  Access is synchronized
@@ -370,12 +372,13 @@
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         setContentView(R.layout.compose);
-        mSavedInstanceState = savedInstanceState;
+        mInnerSavedState = (savedInstanceState != null) ?
+                savedInstanceState.getBundle(KEY_INNER_SAVED_STATE) : null;
         checkValidAccounts();
     }
 
     private void finishCreate() {
-        Bundle savedInstanceState = mSavedInstanceState;
+        final Bundle savedState = mInnerSavedState;
         findViews();
         Intent intent = getIntent();
         Message message;
@@ -384,13 +387,13 @@
         int action;
         // Check for any of the possibly supplied accounts.;
         Account account = null;
-        if (hadSavedInstanceStateMessage(savedInstanceState)) {
-            action = savedInstanceState.getInt(EXTRA_ACTION, COMPOSE);
-            account = savedInstanceState.getParcelable(Utils.EXTRA_ACCOUNT);
-            message = (Message) savedInstanceState.getParcelable(EXTRA_MESSAGE);
+        if (hadSavedInstanceStateMessage(savedState)) {
+            action = savedState.getInt(EXTRA_ACTION, COMPOSE);
+            account = savedState.getParcelable(Utils.EXTRA_ACCOUNT);
+            message = (Message) savedState.getParcelable(EXTRA_MESSAGE);
 
-            previews = savedInstanceState.getParcelableArrayList(EXTRA_ATTACHMENT_PREVIEWS);
-            mRefMessage = (Message) savedInstanceState.getParcelable(EXTRA_IN_REFERENCE_TO_MESSAGE);
+            previews = savedState.getParcelableArrayList(EXTRA_ATTACHMENT_PREVIEWS);
+            mRefMessage = (Message) savedState.getParcelable(EXTRA_IN_REFERENCE_TO_MESSAGE);
         } else {
             account = obtainAccount(intent);
             action = intent.getIntExtra(EXTRA_ACTION, COMPOSE);
@@ -439,7 +442,7 @@
         } else if (message != null && action != EDIT_DRAFT) {
             initFromDraftMessage(message);
             initQuotedTextFromRefMessage(mRefMessage, action);
-            showCcBcc(savedInstanceState);
+            showCcBcc(savedState);
             mShowQuotedText = message.appendRefMessageContent;
         } else if (action == EDIT_DRAFT) {
             initFromDraftMessage(message);
@@ -484,7 +487,7 @@
         }
 
         mComposeMode = action;
-        finishSetup(action, intent, savedInstanceState);
+        finishSetup(action, intent, savedState);
     }
 
     private void checkValidAccounts() {
@@ -593,8 +596,8 @@
         updateHideOrShowCcBcc();
         updateHideOrShowQuotedText(mShowQuotedText);
 
-        mRespondedInline = mSavedInstanceState != null ?
-                mSavedInstanceState.getBoolean(EXTRA_RESPONDED_INLINE) : false;
+        mRespondedInline = mInnerSavedState != null ?
+                mInnerSavedState.getBoolean(EXTRA_RESPONDED_INLINE) : false;
         if (mRespondedInline) {
             mQuotedTextView.setVisibility(View.GONE);
         }
@@ -686,12 +689,6 @@
     }
 
     @Override
-    protected void onStop() {
-        super.onStop();
-        mSavedInstanceState = null;
-    }
-
-    @Override
     protected final void onActivityResult(int request, int result, Intent data) {
         if (request == RESULT_PICK_ATTACHMENT && result == RESULT_OK) {
             addAttachmentAndUpdateView(data);
@@ -716,10 +713,10 @@
             clearChangeListeners();
         }
         super.onRestoreInstanceState(savedInstanceState);
-        if (savedInstanceState != null) {
-            if (savedInstanceState.containsKey(EXTRA_FOCUS_SELECTION_START)) {
-                int selectionStart = savedInstanceState.getInt(EXTRA_FOCUS_SELECTION_START);
-                int selectionEnd = savedInstanceState.getInt(EXTRA_FOCUS_SELECTION_END);
+        if (mInnerSavedState != null) {
+            if (mInnerSavedState.containsKey(EXTRA_FOCUS_SELECTION_START)) {
+                int selectionStart = mInnerSavedState.getInt(EXTRA_FOCUS_SELECTION_START);
+                int selectionEnd = mInnerSavedState.getInt(EXTRA_FOCUS_SELECTION_END);
                 // There should be a focus and it should be an EditText since we
                 // only save these extras if these conditions are true.
                 EditText focusEditText = (EditText) getCurrentFocus();
@@ -737,6 +734,12 @@
     @Override
     public final void onSaveInstanceState(Bundle state) {
         super.onSaveInstanceState(state);
+        final Bundle inner = new Bundle();
+        saveState(inner);
+        state.putBundle(KEY_INNER_SAVED_STATE, inner);
+    }
+
+    private void saveState(Bundle state) {
         // We have no accounts so there is nothing to compose, and therefore, nothing to save.
         if (mAccounts == null || mAccounts.length == 0) {
             return;
@@ -1818,8 +1821,8 @@
          */
         mSave = menu.findItem(R.id.save);
         String action = getIntent() != null ? getIntent().getAction() : null;
-        enableSave(mSavedInstanceState != null ?
-                mSavedInstanceState.getBoolean(EXTRA_SAVE_ENABLED)
+        enableSave(mInnerSavedState != null ?
+                mInnerSavedState.getBoolean(EXTRA_SAVE_ENABLED)
                     : (Intent.ACTION_SEND.equals(action)
                             || Intent.ACTION_SEND_MULTIPLE.equals(action)
                             || Intent.ACTION_SENDTO.equals(action)
@@ -3175,7 +3178,7 @@
                 if (data != null && data.moveToFirst()) {
                     mRefMessage = new Message(data);
                 }
-                finishSetup(mComposeMode, getIntent(), mSavedInstanceState);
+                finishSetup(mComposeMode, getIntent(), mInnerSavedState);
                 break;
             case LOADER_ACCOUNT_CURSOR:
                 if (data != null && data.moveToFirst()) {
diff --git a/src/com/android/mail/preferences/AccountPreferences.java b/src/com/android/mail/preferences/AccountPreferences.java
index 4471942..0f205df 100644
--- a/src/com/android/mail/preferences/AccountPreferences.java
+++ b/src/com/android/mail/preferences/AccountPreferences.java
@@ -19,8 +19,6 @@
 
 import android.content.Context;
 
-import com.android.mail.MailIntentService;
-
 /**
  * Preferences relevant to one specific account.
  */
@@ -91,6 +89,6 @@
 
     public void setNotificationsEnabled(final boolean enabled) {
         getEditor().putBoolean(PreferenceKeys.NOTIFICATIONS_ENABLED, enabled).apply();
-        MailIntentService.broadcastBackupDataChanged(getContext());
+        notifyBackupPreferenceChanged();
     }
 }
diff --git a/src/com/android/mail/preferences/FolderPreferences.java b/src/com/android/mail/preferences/FolderPreferences.java
index e058613..33023e2 100644
--- a/src/com/android/mail/preferences/FolderPreferences.java
+++ b/src/com/android/mail/preferences/FolderPreferences.java
@@ -25,7 +25,6 @@
 import android.net.Uri;
 import android.provider.Settings;
 
-import com.android.mail.MailIntentService;
 import com.android.mail.providers.Account;
 import com.android.mail.providers.Folder;
 import com.android.mail.providers.UIProvider.AccountCapabilities;
@@ -215,7 +214,7 @@
 
     public void setNotificationsEnabled(final boolean enabled) {
         getEditor().putBoolean(PreferenceKeys.NOTIFICATIONS_ENABLED, enabled).apply();
-        MailIntentService.broadcastBackupDataChanged(getContext());
+        notifyBackupPreferenceChanged();
     }
 
     public String getNotificationRingtoneUri() {
@@ -225,7 +224,7 @@
 
     public void setNotificationRingtoneUri(final String uri) {
         getEditor().putString(PreferenceKeys.NOTIFICATION_RINGTONE, uri).apply();
-        MailIntentService.broadcastBackupDataChanged(getContext());
+        notifyBackupPreferenceChanged();
     }
 
     public boolean isNotificationVibrateEnabled() {
@@ -234,7 +233,7 @@
 
     public void setNotificationVibrateEnabled(final boolean enabled) {
         getEditor().putBoolean(PreferenceKeys.NOTIFICATION_VIBRATE, enabled).apply();
-        MailIntentService.broadcastBackupDataChanged(getContext());
+        notifyBackupPreferenceChanged();
     }
 
     public boolean isEveryMessageNotificationEnabled() {
@@ -244,7 +243,7 @@
 
     public void setEveryMessageNotificationEnabled(final boolean enabled) {
         getEditor().putBoolean(PreferenceKeys.NOTIFICATION_NOTIFY_EVERY_MESSAGE, enabled).apply();
-        MailIntentService.broadcastBackupDataChanged(getContext());
+        notifyBackupPreferenceChanged();
     }
 
     public Set<String> getNotificationActions(final Account account) {
diff --git a/src/com/android/mail/preferences/MailPrefs.java b/src/com/android/mail/preferences/MailPrefs.java
index 461bc5d..c605fb5 100644
--- a/src/com/android/mail/preferences/MailPrefs.java
+++ b/src/com/android/mail/preferences/MailPrefs.java
@@ -20,7 +20,6 @@
 import android.content.Context;
 import android.content.SharedPreferences;
 
-import com.android.mail.MailIntentService;
 import com.android.mail.providers.Account;
 import com.android.mail.providers.UIProvider;
 import com.android.mail.widget.BaseWidgetProvider;
@@ -35,7 +34,7 @@
  */
 public final class MailPrefs extends VersionedPrefs {
 
-    public static final boolean SHOW_EXPERIMENTAL_PREFS = false;
+    public static final boolean SHOW_EXPERIMENTAL_PREFS = true;
 
     private static final String PREFS_NAME = "UnifiedEmail";
 
@@ -164,7 +163,7 @@
 
     public void setDefaultReplyAll(final boolean replyAll) {
         getEditor().putBoolean(PreferenceKeys.DEFAULT_REPLY_ALL, replyAll).apply();
-        MailIntentService.broadcastBackupDataChanged(getContext());
+        notifyBackupPreferenceChanged();
     }
 
     /**
@@ -185,7 +184,7 @@
      */
     public void setRemovalAction(final String removalAction) {
         getEditor().putString(PreferenceKeys.REMOVAL_ACTION, removalAction).apply();
-        MailIntentService.broadcastBackupDataChanged(getContext());
+        notifyBackupPreferenceChanged();
     }
 
     /**
@@ -198,7 +197,7 @@
 
     public void setConversationListSwipeEnabled(final boolean enabled) {
         getEditor().putBoolean(PreferenceKeys.CONVERSATION_LIST_SWIPE, enabled).apply();
-        MailIntentService.broadcastBackupDataChanged(getContext());
+        notifyBackupPreferenceChanged();
     }
 
     /**
diff --git a/src/com/android/mail/preferences/VersionedPrefs.java b/src/com/android/mail/preferences/VersionedPrefs.java
index b5dd9c9..c450892 100644
--- a/src/com/android/mail/preferences/VersionedPrefs.java
+++ b/src/com/android/mail/preferences/VersionedPrefs.java
@@ -20,10 +20,12 @@
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.Lists;
 
+import android.app.backup.BackupManager;
 import android.content.Context;
 import android.content.SharedPreferences;
 import android.content.SharedPreferences.Editor;
 
+import com.android.mail.MailIntentService;
 import com.android.mail.utils.LogTag;
 import com.android.mail.utils.LogUtils;
 
@@ -250,4 +252,11 @@
 
         return false;
     }
+
+    /**
+     * Notifies {@link BackupManager} that we have new data to back up.
+     */
+    protected void notifyBackupPreferenceChanged() {
+        MailIntentService.broadcastBackupDataChanged(getContext());
+    }
 }
diff --git a/src/com/android/mail/providers/Conversation.java b/src/com/android/mail/providers/Conversation.java
index d873ec6..6e9546b 100644
--- a/src/com/android/mail/providers/Conversation.java
+++ b/src/com/android/mail/providers/Conversation.java
@@ -172,8 +172,6 @@
 
     private transient boolean viewed;
 
-    private ArrayList<Folder> cachedDisplayableFolders;
-
     private static String sSubjectAndSnippet;
 
     // Constituents of convFlags below
@@ -518,33 +516,9 @@
     }
 
     public void setRawFolders(FolderList folders) {
-        clearCachedFolders();
         rawFolders = folders;
     }
 
-    private void clearCachedFolders() {
-        cachedDisplayableFolders = null;
-    }
-
-    public ArrayList<Folder> getRawFoldersForDisplay(final Uri ignoreFolderUri,
-            final int ignoreFolderType) {
-        if (cachedDisplayableFolders == null) {
-            cachedDisplayableFolders = new ArrayList<Folder>();
-            for (Folder folder : rawFolders.folders) {
-                // skip the ignoreFolder
-                if (ignoreFolderUri != null && ignoreFolderUri.equals(folder.uri)) {
-                    continue;
-                }
-                // Skip the ignoreFolderType
-                if (ignoreFolderType >= 0 && folder.isType(ignoreFolderType)) {
-                    continue;
-                }
-                cachedDisplayableFolders.add(folder);
-            }
-        }
-        return cachedDisplayableFolders;
-    }
-
     @Override
     public boolean equals(Object o) {
         if (o instanceof Conversation) {
diff --git a/src/com/android/mail/providers/Message.java b/src/com/android/mail/providers/Message.java
index ff007eb..cc9ea3f 100644
--- a/src/com/android/mail/providers/Message.java
+++ b/src/com/android/mail/providers/Message.java
@@ -29,10 +29,16 @@
 import android.text.util.Rfc822Token;
 import android.text.util.Rfc822Tokenizer;
 
+import com.android.emailcommon.internet.MimeMessage;
+import com.android.emailcommon.internet.MimeUtility;
+import com.android.emailcommon.mail.MessagingException;
+import com.android.emailcommon.mail.Part;
+import com.android.emailcommon.utility.ConversionUtilities;
 import com.android.mail.providers.UIProvider.MessageColumns;
 import com.android.mail.utils.Utils;
 import com.google.common.base.Objects;
 
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 import java.util.regex.Pattern;
@@ -354,6 +360,44 @@
         }
     }
 
+    public Message(MimeMessage mimeMessage) throws MessagingException {
+        // Set message header values.
+        setFrom(com.android.emailcommon.mail.Address.pack(mimeMessage.getFrom()));
+        setTo(com.android.emailcommon.mail.Address.pack(mimeMessage.getRecipients(
+                com.android.emailcommon.mail.Message.RecipientType.TO)));
+        setCc(com.android.emailcommon.mail.Address.pack(mimeMessage.getRecipients(
+                com.android.emailcommon.mail.Message.RecipientType.CC)));
+        setBcc(com.android.emailcommon.mail.Address.pack(mimeMessage.getRecipients(
+                com.android.emailcommon.mail.Message.RecipientType.BCC)));
+        setReplyTo(com.android.emailcommon.mail.Address.pack(mimeMessage.getReplyTo()));
+        subject = mimeMessage.getSubject();
+        dateReceivedMs = mimeMessage.getSentDate().getTime();
+
+        // for now, always set defaults
+        alwaysShowImages = false;
+        viaDomain = null;
+        draftType = UIProvider.DraftType.NOT_A_DRAFT;
+        isSending = false;
+        starred = false;
+        spamWarningString = null;
+        messageFlags = 0;
+        hasAttachments = false;
+
+        // body values (snippet/bodyText/bodyHtml)
+        // Now process body parts & attachments
+        ArrayList<Part> viewables = new ArrayList<Part>();
+        ArrayList<Part> attachments = new ArrayList<Part>();
+        MimeUtility.collectParts(mimeMessage, viewables, attachments);
+
+        ConversionUtilities.BodyFieldData data =
+                ConversionUtilities.parseBodyFields(viewables);
+
+        snippet = data.snippet;
+        bodyText = data.textContent;
+        bodyHtml = data.htmlContent;
+        // TODO - attachments?
+    }
+
     public boolean isFlaggedReplied() {
         return (messageFlags & UIProvider.MessageFlags.REPLIED) ==
                 UIProvider.MessageFlags.REPLIED;
diff --git a/src/com/android/mail/providers/UIProvider.java b/src/com/android/mail/providers/UIProvider.java
index a41f8a0..34a1355 100644
--- a/src/com/android/mail/providers/UIProvider.java
+++ b/src/com/android/mail/providers/UIProvider.java
@@ -577,11 +577,6 @@
          */
         public static final String QUERY = "query";
 
-        /**
-         * If specified, the query results will be limited to this folder.
-         */
-        public static final String FOLDER = "folder";
-
         private SearchQueryParameters() {}
     }
 
@@ -901,6 +896,13 @@
         ConversationColumns.REMOTE
     };
 
+    /**
+     * This integer corresponds to the number of rows of queries that specify the
+     * {@link UIProvider#CONVERSATION_PROJECTION} projection will fit in a single
+     * {@link android.database.CursorWindow}
+     */
+    public static final int CONVERSATION_PROJECTION_QUERY_CURSOR_WINDOW_LIMT = 2000;
+
     // These column indexes only work when the caller uses the
     // default CONVERSATION_PROJECTION defined above.
     public static final int CONVERSATION_ID_COLUMN = 0;
diff --git a/src/com/android/mail/ui/AbstractActivityController.java b/src/com/android/mail/ui/AbstractActivityController.java
index 20eaf7e..c1c8808 100644
--- a/src/com/android/mail/ui/AbstractActivityController.java
+++ b/src/com/android/mail/ui/AbstractActivityController.java
@@ -66,8 +66,8 @@
 import com.android.mail.browse.ConversationCursor;
 import com.android.mail.browse.ConversationCursor.ConversationOperation;
 import com.android.mail.browse.ConversationItemViewModel;
+import com.android.mail.browse.ConversationMessage;
 import com.android.mail.browse.ConversationPagerController;
-import com.android.mail.browse.MessageCursor.ConversationMessage;
 import com.android.mail.browse.SelectedConversationsActionMenu;
 import com.android.mail.browse.SyncErrorDialogFragment;
 import com.android.mail.compose.ComposeActivity;
@@ -269,8 +269,9 @@
     protected ActionableToastBar mToastBar;
     protected ConversationPagerController mPagerController;
 
-    // this is split out from the general loader dispatcher because its loader doesn't return a
+    // This is split out from the general loader dispatcher because its loader doesn't return a
     // basic Cursor
+    /** Handles loader callbacks to create a convesation cursor. */
     private final ConversationListLoaderCallbacks mListCursorCallbacks =
             new ConversationListLoaderCallbacks();
 
@@ -287,17 +288,101 @@
     private final VeiledAddressMatcher mVeiledMatcher;
 
     protected static final String LOG_TAG = LogTag.getLogTag();
-    /** Constants used to differentiate between the types of loaders. */
+
+    // Loader constants: Accounts
+    /**
+     * The list of accounts. This loader is started early in the application life-cycle since
+     * the list of accounts is central to all other data the application needs: unread counts for
+     * folders, critical UI settings like show/hide checkboxes, ...
+     * The loader is started when the application is created: both in
+     * {@link #onCreate(Bundle)} and in {@link #onActivityResult(int, int, Intent)}. It is never
+     * destroyed since the cursor is needed through the life of the application. When the list of
+     * accounts changes, we notify {@link #mAllAccountObservers}.
+     */
     private static final int LOADER_ACCOUNT_CURSOR = 0;
-    private static final int LOADER_FOLDER_CURSOR = 2;
-    private static final int LOADER_RECENT_FOLDERS = 3;
-    private static final int LOADER_CONVERSATION_LIST = 4;
-    private static final int LOADER_ACCOUNT_INBOX = 5;
-    private static final int LOADER_SEARCH = 6;
+
+    /**
+     * The current account. This loader is started when we have an account. The mail application
+     * <b>needs</b> a valid account to function. As soon as we set {@link #mAccount},
+     * we start a loader to observe for changes on the current account.
+     * The loader is always restarted when an account is set in {@link #setAccount(Account)}.
+     * When the current account object changes, we notify {@link #mAccountObservers}.
+     * A possible performance improvement would be to listen purely on
+     * {@link #LOADER_ACCOUNT_CURSOR}. The current account is guaranteed to be in the list,
+     * and would avoid two updates when a single setting on the current account changes.
+     */
     private static final int LOADER_ACCOUNT_UPDATE_CURSOR = 7;
-    /** Loader for showing the initial folder/conversation at app start. */
+
+    // Loader constants: Folders
+    /** The current folder. This loader watches for updates to the current folder in a manner
+     * analogous to the {@link #LOADER_ACCOUNT_UPDATE_CURSOR}. Updates to the current folder
+     * might be due to server-side changes (unread count), or local changes (sync window or sync
+     * status change).
+     * The change of current folder calls {@link #updateFolder(Folder)}.
+     * This is responsible for restarting a loader using the URI of the provided folder. When the
+     * loader returns, the current folder is updated and consumers, if any, are notified.
+     * When the current folder changes, we notify {@link #mFolderObservable}
+     */
+    private static final int LOADER_FOLDER_CURSOR = 2;
+    /**
+     * The list of recent folders. Recent folders are shown in the DrawerFragment. The recent
+     * folders are tied to the current account being viewed. When the account is changed,
+     * we restart this loader to retrieve the recent accounts. Recents are pre-populated for
+     * phones historically, when they were displayed in the spinner. On the tablet,
+     * they showed in the {@link FolderListFragment} and were not-populated.  The code to
+     * pre-populate the recents is somewhat convoluted: when the loader returns a short list of
+     * recent folders, it issues an update on the Recent Folder URI. The underlying provider then
+     * does the appropriate thing to populate recent folders, and notify of a change on the cursor.
+     * Recent folders are needed for the life of the current account.
+     * When the recent folders change, we notify {@link #mRecentFolderObservers}.
+     */
+    private static final int LOADER_RECENT_FOLDERS = 3;
+    /**
+     * The primary inbox for the current account. The mechanism to load the default inbox for the
+     * current account is (sadly) different from loading other folders. The method
+     * {@link #loadAccountInbox()} is called, and it restarts this loader. When the loader returns
+     * a valid cursor, we create a folder, call {@link #onFolderChanged{Folder)} eventually
+     * calling {@link #updateFolder(Folder)} which starts a loader {@link #LOADER_FOLDER_CURSOR}
+     * over the current folder.
+     * When we have a valid cursor, we destroy this loader, This convoluted flow is historical.
+     */
+    private static final int LOADER_ACCOUNT_INBOX = 5;
+    /**
+     * The fake folder of search results for a term. When we search for a term,
+     * a new activity is created with {@link Intent#ACTION_SEARCH}. For this new activity,
+     * we start a loader which returns conversations that match the user-provided query.
+     * We destroy the loader when we obtain a valid cursor since subsequent searches will create
+     * a new activity.
+     */
+    private static final int LOADER_SEARCH = 6;
+    /**
+     * The initial folder at app start. When the application is launched from an intent that
+     * specifies the initial folder (notifications/widgets/shortcuts),
+     * then we extract the folder URI from the intent, but we cannot trust the folder object. Since
+     * shortcuts and widgets persist past application update, they might have incorrect
+     * information encoded in them. So, to obtain a {@link Folder} object from a {@link Uri},
+     * we need to start another loader. Upon obtaining a valid cursor, the loader is destroyed.
+     * An additional complication arises if we have to view a specific conversation within this
+     * folder. This is the case when launching the app from a single conversation notification
+     * or tapping on a specific conversation in the widget. In these cases, the conversation is
+     * saved in {@link #mConversationToShow} and is retrieved when the loader returns.
+     */
     public static final int LOADER_FIRST_FOLDER = 8;
 
+    // Loader constants: Conversations
+    /** The conversation cursor over the current conversation list. This loader provides
+     * a cursor over conversation entries from a folder to display a conversation
+     * list.
+     * This loader is started when the user switches folders (in {@link #updateFolder(Folder)},
+     * or when the controller is told that a folder/account change is imminent
+     * (in {@link #preloadConvList(Account, Folder)}. The loader is maintained for the life of
+     * the current folder. When the user switches folders, the old loader is destroyed and a new
+     * one is created.
+     *
+     * When the conversation list changes, we notify {@link #mConversationListObservable}.
+     */
+    private static final int LOADER_CONVERSATION_LIST = 4;
+
     /**
      * Guaranteed to be the last loader ID used by the activity. Loaders are owned by Activity or
      * fragments, and within an activity, loader IDs need to be unique. A hack to ensure that the
@@ -316,7 +401,9 @@
      */
     public static final int LAST_FRAGMENT_LOADER_ID = 1000;
 
+    /** Code returned after an account has been added. */
     private static final int ADD_ACCOUNT_REQUEST_CODE = 1;
+    /** Code returned when the user has to enter the new password on an existing account. */
     private static final int REAUTHENTICATE_REQUEST_CODE = 2;
 
     /** The pending destructive action to be carried out before swapping the conversation cursor.*/
@@ -554,12 +641,11 @@
         LogUtils.d(LOG_TAG, "AAC.switchToDefaultAccount(%s)", account);
         final boolean firstLoad = mAccount == null;
         final boolean switchToDefaultInbox = !firstLoad && account.uri.equals(mAccount.uri);
-        // if the active account has been clicked in the drawer, go to default inbox
+        // If the active account has been clicked in the drawer, go to default inbox
         if (switchToDefaultInbox) {
             loadAccountInbox();
             return;
         }
-
         changeAccount(account);
     }
 
@@ -695,8 +781,13 @@
     }
 
     /**
-     * Load the conversation list early for the given folder.
-     * @param nextFolder
+     * Load the conversation list early for the given folder. This happens when some UI element
+     * (usually the drawer) instructs the controller that an account change or folder change is
+     * imminent. While the UI element is animating, the controller can preload the conversation
+     * list for the default inbox of the account provided here or to the folder provided here.
+     *
+     * @param nextAccount The account which the app will switch to shortly, possibly null.
+     * @param nextFolder The folder which the app will switch to shortly, possibly null.
      */
     protected void preloadConvList(Account nextAccount, Folder nextFolder) {
         // Fire off the conversation list loader for this account already with a fake
@@ -718,6 +809,11 @@
         lm.initLoader(LOADER_CONVERSATION_LIST, args, mListCursorCallbacks);
     }
 
+    /**
+     * Initiates the async request to create a fake search folder, which returns conversations that
+     * match the query term provided by the user. Returns immediately.
+     * @param intent Intent that the app was started with. This intent contains the search query.
+     */
     private void fetchSearchFolder(Intent intent) {
         final Bundle args = new Bundle();
         args.putString(ConversationListContext.EXTRA_SEARCH_QUERY, intent
@@ -906,6 +1002,17 @@
         mFolderListFolder = folder;
     }
 
+    /**
+     * The mail activity calls other activities for two specific reasons:
+     * <ul>
+     *     <li>To add an account. And receives the result {@link #ADD_ACCOUNT_REQUEST_CODE}</li>
+     *     <li>To update the password on a current account. The result {@link
+     *     #REAUTHENTICATE_REQUEST_CODE} is received.</li>
+     * </ul>
+     * @param requestCode
+     * @param resultCode
+     * @param data
+     */
     @Override
     public void onActivityResult(int requestCode, int resultCode, Intent data) {
         switch (requestCode) {
@@ -1018,6 +1125,44 @@
         mHandler.post(mLogServiceChecker);
     }
 
+    /**
+     * The application can be started from the following entry points:
+     * <ul>
+     *     <li>Launcher: you tap on the Gmail icon in the launcher. This is what most users think of
+     *         as “Starting the app”.</li>
+     *     <li>Shortcut: Users can make a shortcut to take them directly to a label.</li>
+     *     <li>Widget: Shows the contents of a synced label, and allows:
+     *     <ul>
+     *         <li>Viewing the list (tapping on the title)</li>
+     *         <li>Composing a new message (tapping on the new message icon in the title. This
+     *         launches the {@link ComposeActivity}.
+     *         </li>
+     *         <li>Viewing a single message (tapping on a list element)</li>
+     *     </ul>
+     *
+     *     </li>
+     *     <li>Tapping on a notification:
+     *     <ul>
+     *         <li>Shows message list if more than one message</li>
+     *         <li>Shows the conversation if the notification is for a single message</li>
+     *     </ul>
+     *     </li>
+     *     <li>...and most importantly, the activity life cycle can tear down the application and
+     *     restart it:
+     *     <ul>
+     *         <li>Rotate the application: it is destroyed and recreated.</li>
+     *         <li>Navigate away, and return from recent applications.</li>
+     *     </ul>
+     *     </li>
+     *     <li>Add a new account: fires off an intent to add an account,
+     *     and returns in {@link #onActivityResult(int, int, android.content.Intent)} .</li>
+     *     <li>Re-authenticate your account: again returns in onActivityResult().</li>
+     *     <li>Composing can happen from many entry points: third party applications fire off an
+     *     intent to compose email, and launch directly into the {@link ComposeActivity}
+     *     .</li>
+     * </ul>
+     * {@inheritDoc}
+     */
     @Override
     public boolean onCreate(Bundle savedState) {
         initializeActionBar();
@@ -1080,7 +1225,7 @@
         // Sync the toggle state after onRestoreInstanceState has occurred.
         mDrawerToggle.syncState();
 
-        mHideMenuItems = isDrawerEnabled() ? mDrawerContainer.isDrawerOpen(mDrawerPullout) : false;
+        mHideMenuItems = isDrawerEnabled() && mDrawerContainer.isDrawerOpen(mDrawerPullout);
     }
 
     @Override
@@ -2115,6 +2260,10 @@
      * Handle an intent to open the app. This method is called only when there is no saved state,
      * so we need to set state that wasn't set before. It is correct to change the viewmode here
      * since it has not been previously set.
+     *
+     * This method is called for a subset of the reasons mentioned in
+     * {@link #onCreate(android.os.Bundle)}. Notably, this is called when launching the app from
+     * notifications, widgets, and shortcuts.
      * @param intent intent passed to the activity.
      */
     private void handleIntent(Intent intent) {
@@ -3144,6 +3293,9 @@
         return in != null && in.isVisible() && mActivity.hasWindowFocus();
     }
 
+    /**
+     * This class handles callbacks that create a {@link ConversationCursor}.
+     */
     private class ConversationListLoaderCallbacks implements
         LoaderManager.LoaderCallbacks<ConversationCursor> {
 
@@ -3401,6 +3553,7 @@
             }
             switch (loader.getId()) {
                 case LOADER_ACCOUNT_CURSOR:
+                    // We have received an update on the list of accounts.
                     if (data == null) {
                         // Nothing useful to do if we have no valid data.
                         break;
@@ -3435,13 +3588,10 @@
                     break;
                 case LOADER_ACCOUNT_UPDATE_CURSOR:
                     // We have received an update for current account.
-
-                    // Make sure that this is an update for the current account
                     if (data != null && data.moveToFirst()) {
                         final Account updatedAccount = data.getModel();
-
+                        // Make sure that this is an update for the current account
                         if (updatedAccount.uri.equals(mAccount.uri)) {
-                            // Keep a reference to the previous settings object
                             final Settings previousSettings = mAccount.settings;
 
                             // Update the controller's reference to the current account
@@ -3468,6 +3618,7 @@
 
         @Override
         public void onLoaderReset(Loader<ObjectCursor<Account>> loader) {
+            // Do nothing. In onLoadFinished() we copy the relevant data from the cursor.
         }
     }
 
diff --git a/src/com/android/mail/ui/AbstractConversationViewFragment.java b/src/com/android/mail/ui/AbstractConversationViewFragment.java
index 5a6399c..48afab3 100644
--- a/src/com/android/mail/ui/AbstractConversationViewFragment.java
+++ b/src/com/android/mail/ui/AbstractConversationViewFragment.java
@@ -38,11 +38,11 @@
 import android.os.Bundle;
 import android.os.Handler;
 import android.provider.Browser;
-import android.util.Log;
 import android.view.Menu;
 import android.view.MenuInflater;
 import android.view.MenuItem;
 import android.view.View;
+import android.webkit.WebSettings;
 import android.webkit.WebView;
 import android.webkit.WebViewClient;
 
@@ -52,10 +52,10 @@
 import com.android.mail.R;
 import com.android.mail.SenderInfoLoader;
 import com.android.mail.browse.ConversationAccountController;
+import com.android.mail.browse.ConversationMessage;
 import com.android.mail.browse.ConversationViewHeader.ConversationViewHeaderCallbacks;
 import com.android.mail.browse.MessageCursor;
 import com.android.mail.browse.MessageCursor.ConversationController;
-import com.android.mail.browse.MessageCursor.ConversationMessage;
 import com.android.mail.browse.MessageHeaderView.MessageHeaderViewCallbacks;
 import com.android.mail.content.ObjectCursor;
 import com.android.mail.content.ObjectCursorLoader;
@@ -421,7 +421,7 @@
             LogUtils.e(LOG_TAG,
                     "ACVF ignoring onOptionsItemSelected b/c userVisibleHint is false. f=%s", this);
             if (LogUtils.isLoggable(LOG_TAG, LogUtils.DEBUG)) {
-                Log.e(LOG_TAG, Utils.dumpFragment(this));  // the dump has '%' chars in it...
+                LogUtils.e(LOG_TAG, Utils.dumpFragment(this));  // the dump has '%' chars in it...
             }
             return false;
         }
@@ -881,4 +881,21 @@
         return (mAccount.enableMessageTransforms > 0) &&
                 !mHasConversationTransformBeenReverted;
     }
+
+    public void setTextZoom(WebSettings settings) {
+        final Resources resources = getResources();
+        final float fontScale = resources.getConfiguration().fontScale;
+        final int desiredFontSizePx = resources.getInteger(
+                R.integer.conversation_desired_font_size_px);
+        final int unstyledFontSizePx = resources.getInteger(
+                R.integer.conversation_unstyled_font_size_px);
+
+        int textZoom = settings.getTextZoom();
+        // apply a correction to the default body text style to get regular text to the size we want
+        textZoom = textZoom * desiredFontSizePx / unstyledFontSizePx;
+        // then apply any system font scaling
+        textZoom = (int) (textZoom * fontScale);
+        settings.setTextZoom(textZoom);
+
+    }
 }
diff --git a/src/com/android/mail/ui/AbstractMailActivity.java b/src/com/android/mail/ui/AbstractMailActivity.java
index 33a7da2..43f3b23 100644
--- a/src/com/android/mail/ui/AbstractMailActivity.java
+++ b/src/com/android/mail/ui/AbstractMailActivity.java
@@ -39,7 +39,7 @@
 
     private final UiHandler mUiHandler = new UiHandler();
 
-    private static final boolean STRICT_MODE = false;
+    private static final boolean STRICT_MODE = true;
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
diff --git a/src/com/android/mail/ui/AccountController.java b/src/com/android/mail/ui/AccountController.java
index f2b36f9..3d8ab2f 100644
--- a/src/com/android/mail/ui/AccountController.java
+++ b/src/com/android/mail/ui/AccountController.java
@@ -19,6 +19,7 @@
 
 import android.database.DataSetObservable;
 import android.database.DataSetObserver;
+import android.widget.ListView;
 
 import com.android.mail.providers.Account;
 import com.android.mail.providers.AccountObserver;
@@ -108,4 +109,10 @@
      * @return <code>true</code> if the drawer pull action is enabled, <code>false</code> otherwise
      */
     boolean isDrawerPullEnabled();
+
+    /**
+     * @return the choice mode to use in the {@link ListView} in the default folder list (subclasses
+     * of {@link FolderListFragment} may override this
+     */
+    int getFolderListViewChoiceMode();
 }
diff --git a/src/com/android/mail/ui/ActivityController.java b/src/com/android/mail/ui/ActivityController.java
index ed30f29..993d5d4 100644
--- a/src/com/android/mail/ui/ActivityController.java
+++ b/src/com/android/mail/ui/ActivityController.java
@@ -38,11 +38,28 @@
  * An Activity controller knows how to combine views and listeners into a functioning activity.
  * ActivityControllers are delegates that implement methods by calling underlying views to modify,
  * or respond to user action.
+ *
+ * There are two ways of adding methods to this interface:
+ * <ul>
+ *     <li>When the methods pertain to a single logical grouping: consider adding a new
+ *     interface and putting all the methods in that interface. As an example,
+ *     look at {@link AccountController}. The controller implements this,
+ *     and returns itself in
+ *     {@link com.android.mail.ui.ControllableActivity#getAccountController()}. This allows
+ *     for account-specific methods to be added without creating new methods in this interface
+ *     .</li>
+ *     <li>Methods that relate to an activity can be added directly. As an example,
+ *     look at {@link #onActivityResult(int, int, android.content.Intent)} which is identical to
+ *     its declaration in {@link android.app.Activity}.</li>
+ *     <li>Everything else. As an example, look at {@link #isDrawerEnabled()}. Try to avoid
+ *     this path because an implementation has to provided in many classes:
+ *     {@link MailActivity}, {@link FolderSelectionActivity}, and the controllers.</li>
+ * </ul>
  */
 public interface ActivityController extends LayoutListener,
         ModeChangeListener, ConversationListCallbacks,
-        FolderChangeListener, ConversationSetObserver, ConversationListener,
-        FolderListFragment.FolderListSelectionListener, HelpCallback, UndoListener,
+        FolderChangeListener, ConversationSetObserver, ConversationListener, FolderSelector,
+        HelpCallback, UndoListener,
         ConversationUpdater, ErrorListener, FolderController, AccountController,
         ConversationPositionTracker.Callbacks, ConversationListFooterView.FooterViewClickListener,
         RecentFolderController, UpOrBackController {
diff --git a/src/com/android/mail/ui/AnimatedAdapter.java b/src/com/android/mail/ui/AnimatedAdapter.java
index fd387a6..1eba5a0 100644
--- a/src/com/android/mail/ui/AnimatedAdapter.java
+++ b/src/com/android/mail/ui/AnimatedAdapter.java
@@ -23,6 +23,7 @@
 import android.animation.AnimatorSet;
 import android.animation.ObjectAnimator;
 import android.content.Context;
+import android.content.res.Resources;
 import android.database.Cursor;
 import android.os.Bundle;
 import android.os.Handler;
@@ -37,6 +38,7 @@
 import com.android.mail.browse.ConversationItemView;
 import com.android.mail.browse.ConversationItemViewCoordinates;
 import com.android.mail.browse.SwipeableConversationItemView;
+import com.android.mail.content.ObjectCursor;
 import com.android.mail.providers.Account;
 import com.android.mail.providers.AccountObserver;
 import com.android.mail.providers.Conversation;
@@ -46,10 +48,13 @@
 import com.android.mail.ui.SwipeableListView.ListItemsRemovedListener;
 import com.android.mail.utils.LogTag;
 import com.android.mail.utils.LogUtils;
+
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Maps;
 
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Iterator;
@@ -134,10 +139,11 @@
 
     /**
      * The next action to perform. Do not read or write this. All accesses should
-     * be in {@link #performAndSetNextAction(DestructiveAction)} which commits the
-     * previous action, if any.
+     * be in {@link #performAndSetNextAction(SwipeableListView.ListItemsRemovedListener)} which
+     * commits the previous action, if any.
      */
     private ListItemsRemovedListener mPendingDestruction;
+
     /**
      * A destructive action that refreshes the list and performs no other action.
      */
@@ -169,33 +175,38 @@
         }
     };
 
-    private final List<ConversationSpecialItemView> mSpecialViews;
-    private final SparseArray<ConversationSpecialItemView> mSpecialViewPositions;
+    /**
+     * A list of all views that are not conversations. These include temporary views from
+     * {@link #mFleetingViews} and child folders from {@link #mFolderViews}.
+     */
+    private final SparseArray<ConversationSpecialItemView> mSpecialViews;
 
     private final SparseArray<ConversationItemViewCoordinates> mCoordinatesCache =
             new SparseArray<ConversationItemViewCoordinates>();
 
-    private final void setAccount(Account newAccount) {
+    /**
+     * Temporary views insert at specific positions relative to conversations. These can be
+     * related to showing new features (on-boarding) or showing information about new mailboxes
+     * that have been added by the system.
+     */
+    private final List<ConversationSpecialItemView> mFleetingViews;
+
+    /** List of all child folders for this folder. */
+    private List<NestedFolderView> mFolderViews;
+
+    private void setAccount(Account newAccount) {
         mAccount = newAccount;
         mPriorityMarkersEnabled = mAccount.settings.priorityArrowsEnabled;
         mSwipeEnabled = mAccount.supportsCapability(UIProvider.AccountCapabilities.UNDO);
     }
 
-    /**
-     * Used only for debugging.
-     */
     private static final String LOG_TAG = LogTag.getLogTag();
     private static final int INCREASE_WAIT_COUNT = 2;
 
     public AnimatedAdapter(Context context, ConversationCursor cursor,
             ConversationSelectionSet batch, ControllableActivity activity,
-            SwipeableListView listView) {
-        this(context, cursor, batch, activity, listView, null);
-    }
-
-    public AnimatedAdapter(Context context, ConversationCursor cursor,
-            ConversationSelectionSet batch, ControllableActivity activity,
-            SwipeableListView listView, final List<ConversationSpecialItemView> specialViews) {
+            SwipeableListView listView, final List<ConversationSpecialItemView> specialViews,
+            final ObjectCursor<Folder> childFolders) {
         super(context, -1, cursor, UIProvider.CONVERSATION_PROJECTION, null, 0);
         mContext = context;
         mBatchConversations = batch;
@@ -203,27 +214,61 @@
         mActivity = activity;
         mShowFooter = false;
         mListView = listView;
+        mFolderViews = getNestedFolders(childFolders);
+
         mHandler = new Handler();
         if (sDismissAllShortDelay == -1) {
-            sDismissAllShortDelay =
-                    context.getResources()
-                        .getInteger(R.integer.dismiss_all_leavebehinds_short_delay);
-            sDismissAllLongDelay =
-                    context.getResources()
-                        .getInteger(R.integer.dismiss_all_leavebehinds_long_delay);
+            final Resources r = context.getResources();
+            sDismissAllShortDelay = r.getInteger(R.integer.dismiss_all_leavebehinds_short_delay);
+            sDismissAllLongDelay = r.getInteger(R.integer.dismiss_all_leavebehinds_long_delay);
         }
-        mSpecialViews =
-                specialViews == null ? new ArrayList<ConversationSpecialItemView>(0)
-                        : new ArrayList<ConversationSpecialItemView>(specialViews);
-        mSpecialViewPositions = new SparseArray<ConversationSpecialItemView>(mSpecialViews.size());
+        if (specialViews != null) {
+            mFleetingViews = new ArrayList<ConversationSpecialItemView>(specialViews);
+        } else {
+            mFleetingViews = new ArrayList<ConversationSpecialItemView>(0);
+        }
+        /** Total number of special views */
+        final int size = mFleetingViews.size() + mFolderViews.size();
+        mSpecialViews = new SparseArray<ConversationSpecialItemView>(size);
 
-        for (final ConversationSpecialItemView view : mSpecialViews) {
+        // Only set the adapter in teaser views. Folder views don't care about the adapter.
+        for (final ConversationSpecialItemView view : mFleetingViews) {
             view.setAdapter(this);
         }
-
         updateSpecialViews();
     }
 
+    /**
+     * Returns a list containing views for all the nested folders.
+     * @param cursor cursor containing the folders nested within the current folder
+     * @return a list, possibly empty of the views representing the folders.
+     */
+    private List<NestedFolderView> getNestedFolders (final ObjectCursor<Folder> cursor) {
+        if (cursor == null || !cursor.moveToFirst()) {
+            // The cursor has nothing valid.  Return an empty list.
+            return ImmutableList.of();
+        }
+
+        final LayoutInflater inflater = LayoutInflater.from(mContext);
+        final List<NestedFolderView> folders = new ArrayList<NestedFolderView>(cursor.getCount());
+        do {
+            final NestedFolderView view =
+                    (NestedFolderView) inflater.inflate(R.layout.nested_folder, null);
+            view.setFolder(cursor.getModel());
+            folders.add(view);
+        } while (cursor.moveToNext());
+        return folders;
+    }
+
+    /**
+     * Updates the list of folders for the current list with the cursor provided here.
+     * @param childFolders A cursor containing child folders for the current folder.
+     */
+    public void updateNestedFolders (ObjectCursor<Folder> childFolders) {
+        mFolderViews = getNestedFolders(childFolders);
+        notifyDataSetChanged();
+    }
+
     public void cancelDismissCounter() {
         cancelLeaveBehindFadeInAnimation();
         mHandler.removeCallbacks(mCountDown);
@@ -245,8 +290,8 @@
 
     @Override
     public int getCount() {
-        // mSpecialViewPositions only contains the views that are currently being displayed
-        final int specialViewCount = mSpecialViewPositions.size();
+        // mSpecialViews only contains the views that are currently being displayed
+        final int specialViewCount = mSpecialViews.size();
 
         final int count = super.getCount() + specialViewCount;
         return mShowFooter ? count + 1 : count;
@@ -339,7 +384,7 @@
             // types. In a future release, use position/id map to try to make
             // this cleaner / faster to determine if the view is animating.
             return TYPE_VIEW_DONT_RECYCLE;
-        } else if (mSpecialViewPositions.get(position) != null) {
+        } else if (mSpecialViews.get(position) != null) {
             // Don't recycle the special views
             return TYPE_VIEW_DONT_RECYCLE;
         }
@@ -412,12 +457,12 @@
         }
 
         // Check if this is a special view
-        final View specialView = (View) mSpecialViewPositions.get(position);
+        final View specialView = (View) mSpecialViews.get(position);
         if (specialView != null) {
             return specialView;
         }
 
-        ConversationCursor cursor = (ConversationCursor) getItem(position);
+        final ConversationCursor cursor = (ConversationCursor) getItem(position);
         final Conversation conv = cursor.getConversation();
 
         // Notify the provider of this change in the position of Conversation cursor
@@ -616,7 +661,7 @@
     @Override
     public long getItemId(int position) {
         if (mShowFooter && position == getCount() - 1
-                || mSpecialViewPositions.get(position) != null) {
+                || mSpecialViews.get(position) != null) {
             return -1;
         }
         final int cursorPos = position - getPositionOffset(position);
@@ -668,9 +713,7 @@
 
     @Override
     public View newView(Context context, Cursor cursor, ViewGroup parent) {
-        SwipeableConversationItemView view = new SwipeableConversationItemView(context,
-                mAccount.name);
-        return view;
+        return new SwipeableConversationItemView(context, mAccount.name);
     }
 
     @Override
@@ -700,8 +743,8 @@
     public Object getItem(int position) {
         if (mShowFooter && position == getCount() - 1) {
             return mFooter;
-        } else if (mSpecialViewPositions.get(position) != null) {
-            return mSpecialViewPositions.get(position);
+        } else if (mSpecialViews.get(position) != null) {
+            return mSpecialViews.get(position);
         }
         return super.getItem(position - getPositionOffset(position));
     }
@@ -739,7 +782,7 @@
      * @param next The next action that is to be performed, possibly null (if no next action is
      * needed).
      */
-    private final void performAndSetNextAction(ListItemsRemovedListener next) {
+    private void performAndSetNextAction(ListItemsRemovedListener next) {
         if (mPendingDestruction != null) {
             mPendingDestruction.onListItemsRemoved();
         }
@@ -763,28 +806,21 @@
 
     @Override
     public boolean areAllItemsEnabled() {
-        // The animating positions are not enabled.
+        // The animating items and some special views are not enabled.
         return false;
     }
 
     @Override
     public boolean isEnabled(final int position) {
-        if (mSpecialViewPositions.get(position) != null) {
-            // This is a special view
-            return false;
+        final ConversationSpecialItemView view = mSpecialViews.get(position);
+        if (view != null) {
+            final boolean enabled = view.acceptsUserTaps();
+            LogUtils.d(LOG_TAG, "AA.isEnabled(%d) = %b", position, enabled);
+            return enabled;
         }
-
         return !isPositionDeleting(position) && !isPositionUndoing(position);
     }
 
-    public void showFooter() {
-        setFooterVisibility(true);
-    }
-
-    public void hideFooter() {
-        setFooterVisibility(false);
-    }
-
     public void setFooterVisibility(boolean show) {
         if (mShowFooter != show) {
             mShowFooter = show;
@@ -836,8 +872,8 @@
     public void onRestoreInstanceState(Bundle outState) {
         if (outState.containsKey(LAST_DELETING_ITEMS)) {
             final long[] lastDeleting = outState.getLongArray(LAST_DELETING_ITEMS);
-            for (int i = 0; i < lastDeleting.length; i++) {
-                mLastDeletingItems.add(lastDeleting[i]);
+            for (final long aLastDeleting : lastDeleting) {
+                mLastDeletingItems.add(aLastDeleting);
             }
         }
         if (outState.containsKey(LEAVE_BEHIND_ITEM_DATA)) {
@@ -910,24 +946,40 @@
         }
     }
 
+    /**
+     * Updates special (non-conversation view) when either {@link #mFolderViews} or
+     * {@link #mFleetingViews} changed
+     */
     private void updateSpecialViews() {
-        mSpecialViewPositions.clear();
+        // We recreate all the special views using mFolderViews and mFleetingViews (in that order).
+        mSpecialViews.clear();
 
-        for (int i = 0; i < mSpecialViews.size(); i++) {
-            final ConversationSpecialItemView specialView = mSpecialViews.get(i);
+        int folderCount = 0;
+        // Nested folders are added initially. They don't specify positions: we put them at the
+        // very top.
+        for (final NestedFolderView view : mFolderViews) {
+            mSpecialViews.put(folderCount, view);
+            folderCount++;
+        }
+
+        // Fleeting (temporary) views go after this. They specify a position,which is 0-indexed and
+        // has to be adjusted for the number of folders above it.
+        for (final ConversationSpecialItemView specialView : mFleetingViews) {
             specialView.onUpdate(mAccount.name, mFolder, getConversationCursor());
 
             if (specialView.getShouldDisplayInList()) {
-                int position = specialView.getPosition();
+                // If the special view asks for position 0, it wants to be at the top. However,
+                // if there are already 3 folders above it, the real position it needs is 0+3 (4th
+                // from top, since everything is 0-indexed).
+                int position = (specialView.getPosition() + folderCount);
 
                 // insert the special view into the position, but if there is
                 // already an item occupying that position, move that item back
                 // one position, and repeat
                 ConversationSpecialItemView insert = specialView;
                 while (insert != null) {
-                    final ConversationSpecialItemView kickedOut = mSpecialViewPositions.get(
-                            position);
-                    mSpecialViewPositions.put(position, insert);
+                    final ConversationSpecialItemView kickedOut = mSpecialViews.get(position);
+                    mSpecialViews.put(position, insert);
                     insert = kickedOut;
                     position++;
                 }
@@ -968,9 +1020,8 @@
     public int getPositionOffset(final int position) {
         int offset = 0;
 
-        for (int i = 0; i < mSpecialViewPositions.size(); i++) {
-            final int key = mSpecialViewPositions.keyAt(i);
-            final ConversationSpecialItemView specialView = mSpecialViewPositions.get(key);
+        for (int i = 0, size = mSpecialViews.size(); i < size; i++) {
+            final int key = mSpecialViews.keyAt(i);
             if (key <= position) {
                 offset++;
             }
@@ -980,14 +1031,15 @@
     }
 
     public void cleanup() {
-        for (final ConversationSpecialItemView view : mSpecialViews) {
+        // Only clean up teaser views. Folder views don't care about clean up.
+        for (final ConversationSpecialItemView view : mFleetingViews) {
             view.cleanup();
         }
     }
 
     public void onConversationSelected() {
-        for (int i = 0; i < mSpecialViews.size(); i++) {
-            final ConversationSpecialItemView specialView = mSpecialViews.get(i);
+        // Only notify teaser views. Folder views don't care about selected conversations.
+        for (final ConversationSpecialItemView specialView : mFleetingViews) {
             specialView.onConversationSelected();
         }
     }
diff --git a/src/com/android/mail/ui/ControllableActivity.java b/src/com/android/mail/ui/ControllableActivity.java
index fae8665..9bd563b 100644
--- a/src/com/android/mail/ui/ControllableActivity.java
+++ b/src/com/android/mail/ui/ControllableActivity.java
@@ -72,7 +72,7 @@
      * fragment so that activity controllers can track the last folder list
      * pushed for hierarchical folders.
      */
-    FolderListFragment.FolderListSelectionListener getFolderListSelectionListener();
+    FolderSelector getFolderSelector();
 
     /**
      * Get the folder currently being accessed by the activity.
diff --git a/src/com/android/mail/ui/ConversationCursorLoader.java b/src/com/android/mail/ui/ConversationCursorLoader.java
index 530d991..8d9df0b 100644
--- a/src/com/android/mail/ui/ConversationCursorLoader.java
+++ b/src/com/android/mail/ui/ConversationCursorLoader.java
@@ -20,11 +20,11 @@
 import android.app.Activity;
 import android.content.AsyncTaskLoader;
 import android.net.Uri;
-import android.util.Log;
 
 import com.android.mail.browse.ConversationCursor;
 import com.android.mail.providers.Account;
 import com.android.mail.providers.UIProvider.AccountCapabilities;
+import com.android.mail.utils.LogUtils;
 
 import java.util.ArrayList;
 
@@ -58,16 +58,16 @@
 
     private static void dumpLoaders() {
         if (DEBUG) {
-            Log.d(TAG, "Loaders: ");
+            LogUtils.d(TAG, "Loaders: ");
             for (ConversationCursorLoader loader: sLoaders) {
-                Log.d(TAG, " >> " + loader.mName + " (" + loader.mUri + ")");
+                LogUtils.d(TAG, " >> " + loader.mName + " (" + loader.mUri + ")");
             }
         }
     }
 
     private void addLoader() {
         if (DEBUG) {
-            Log.d(TAG, "Add loader: " + mUri);
+            LogUtils.d(TAG, "Add loader: " + mUri);
             sLoaders.add(this);
             if (sLoaders.size() > 1) {
                 dumpLoaders();
@@ -90,7 +90,7 @@
             mConversationCursor.disable();
             mClosed = true;
             if (DEBUG) {
-                Log.d(TAG, "Reset loader/disable cursor: " + mName);
+                LogUtils.d(TAG, "Reset loader/disable cursor: " + mName);
                 sLoaders.remove(this);
                 if (!sLoaders.isEmpty()) {
                     dumpLoaders();
@@ -98,7 +98,7 @@
             }
         } else {
             if (DEBUG) {
-                Log.d(TAG, "Reset loader/retain cursor: " + mName);
+                LogUtils.d(TAG, "Reset loader/retain cursor: " + mName);
                 mRetained = true;
             }
         }
@@ -120,12 +120,12 @@
             mConversationCursor.load();
             addLoader();
             if (DEBUG) {
-                Log.d(TAG, "Restarting reset loader: " + mName);
+                LogUtils.d(TAG, "Restarting reset loader: " + mName);
             }
         } else if (mRetained) {
             mRetained = false;
             if (DEBUG) {
-                Log.d(TAG, "Resuming retained loader: " + mName);
+                LogUtils.d(TAG, "Resuming retained loader: " + mName);
             }
         }
         forceLoad();
diff --git a/src/com/android/mail/ui/ConversationListFragment.java b/src/com/android/mail/ui/ConversationListFragment.java
index 16fc684..b11cadd 100644
--- a/src/com/android/mail/ui/ConversationListFragment.java
+++ b/src/com/android/mail/ui/ConversationListFragment.java
@@ -21,15 +21,15 @@
 
 import android.app.Activity;
 import android.app.ListFragment;
+import android.app.LoaderManager;
 import android.content.Context;
+import android.content.Loader;
 import android.content.res.Resources;
 import android.database.DataSetObserver;
-import android.graphics.PixelFormat;
-import android.graphics.Rect;
+import android.net.Uri;
 import android.os.Bundle;
 import android.os.Handler;
 import android.text.format.DateUtils;
-import android.util.TypedValue;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
@@ -46,6 +46,8 @@
 import com.android.mail.browse.ConversationItemViewModel;
 import com.android.mail.browse.ConversationListFooterView;
 import com.android.mail.browse.ToggleableItem;
+import com.android.mail.content.ObjectCursor;
+import com.android.mail.content.ObjectCursorLoader;
 import com.android.mail.providers.Account;
 import com.android.mail.providers.AccountObserver;
 import com.android.mail.providers.Conversation;
@@ -147,6 +149,52 @@
     private int mConversationCursorHash;
 
     /**
+     * If the current list is for a folder with children, this set of loader callbacks will
+     * create a loader for all the child folders, and will return an {@link ObjectCursor} over the
+     * list.
+     */
+    private final class ChildFolderLoads
+            implements LoaderManager.LoaderCallbacks<ObjectCursor<Folder>> {
+        /** Load all child folders for the current folder. */
+        private static final int LOADER_CHIDREN = 0;
+        public static final String CHILD_URI = "arg-child-uri";
+        private final String[] projection = UIProvider.FOLDERS_PROJECTION;
+
+        @Override
+        public Loader<ObjectCursor<Folder>> onCreateLoader(int id, Bundle args) {
+            if (id != LOADER_CHIDREN) {
+                throw new IllegalStateException("ChildFolderLoads loading ID=" + id);
+            }
+            final Uri childUri = Uri.parse(args.getString(CHILD_URI));
+            return new ObjectCursorLoader<Folder>(
+                    getActivity(), childUri, projection, Folder.FACTORY);
+        }
+
+        @Override
+        public void onLoadFinished(Loader<ObjectCursor<Folder>> loader, ObjectCursor<Folder> data) {
+            if (data != null && data.getCount() >= 0 && mListAdapter != null) {
+                mListAdapter.updateNestedFolders(data);
+            }
+        }
+
+        @Override
+        public void onLoaderReset(Loader<ObjectCursor<Folder>> loader) {
+            // Do nothing.
+        }
+    }
+
+    /** Callbacks to handle creating a loader and receiving child folders from it. */
+    private final ChildFolderLoads mChildCallback = new ChildFolderLoads();
+
+    /**
+     * Include all the folders at the cursor provided here in the conversation list.
+     * @param cursor The cursor containing child folders for the current folder.
+     */
+    private void showChildFolders(ObjectCursor<Folder> cursor) {
+
+    }
+
+    /**
      * Constructor needs to be public to handle orientation changes and activity
      * lifecycle events.
      */
@@ -265,26 +313,35 @@
         mFooterView.setClickListener(mActivity);
         mConversationListView.setActivity(mActivity);
         final ConversationCursor conversationCursor = getConversationListCursor();
+        final LoaderManager manager = getLoaderManager();
+
+        // If this a parent folder, load all the child folders.
+        if (mViewContext.folder.hasChildren) {
+            final Uri childUri = mViewContext.folder.childFoldersListUri;
+            final Bundle args = new Bundle();
+            args.putString(ChildFolderLoads.CHILD_URI, childUri.toString());
+            manager.initLoader(ChildFolderLoads.LOADER_CHIDREN, args, mChildCallback);
+        }
 
         final ConversationListHelper helper = mActivity.getConversationListHelper();
         final List<ConversationSpecialItemView> specialItemViews = helper != null ?
                 ImmutableList.copyOf(helper.makeConversationListSpecialViews(
-                        getActivity(), mAccount, mActivity.getFolderListSelectionListener()))
+                        activity, mAccount, mActivity.getFolderSelector()))
                 : null;
         if (specialItemViews != null) {
             // Attach to the LoaderManager
             for (final ConversationSpecialItemView view : specialItemViews) {
-                view.bindLoaderManager(getLoaderManager());
+                view.bindLoaderManager(manager);
             }
         }
 
         mListAdapter = new AnimatedAdapter(mActivity.getApplicationContext(), conversationCursor,
-                        mActivity.getSelectedSet(), mActivity, mListView, specialItemViews);
+                        mActivity.getSelectedSet(), mActivity, mListView, specialItemViews, null);
         mListAdapter.addFooter(mFooterView);
         mListView.setAdapter(mListAdapter);
         mSelectedSet = mActivity.getSelectedSet();
         mListView.setSelectionSet(mSelectedSet);
-        mListAdapter.hideFooter();
+        mListAdapter.setFooterVisibility(false);
         mFolderObserver = new FolderObserver(){
             @Override
             public void onChanged(Folder newFolder) {
@@ -521,19 +578,22 @@
      */
     @Override
     public void onListItemClick(ListView l, View view, int position, long id) {
-        // Ignore anything that is not a conversation item. Could be a footer.
-        // If we are using a keyboard, the highlighted item is the parent;
-        // otherwise, this is a direct call from the ConverationItemView
-        if (!(view instanceof ToggleableItem)) {
-            return;
-        }
-        boolean showSenderImage = (mAccount.settings.convListIcon ==
-                ConversationListIcon.SENDER_IMAGE);
-        if (!showSenderImage && !mSelectedSet.isEmpty()) {
-            ToggleableItem v = (ToggleableItem) view;
-            v.toggleSelectedState();
+        if (view instanceof NestedFolderView) {
+            final FolderSelector selector = mActivity.getFolderSelector();
+            selector.onFolderSelected(((NestedFolderView) view).getFolder());
+        } else if (view instanceof ToggleableItem) {
+            final boolean showSenderImage =
+                    (mAccount.settings.convListIcon == ConversationListIcon.SENDER_IMAGE);
+            if (!showSenderImage && !mSelectedSet.isEmpty()) {
+                ((ToggleableItem) view).toggleSelectedState();
+            } else {
+                viewConversation(position);
+            }
         } else {
-            viewConversation(position);
+            // Ignore anything that is not a conversation item. Could be a footer.
+            // If we are using a keyboard, the highlighted item is the parent;
+            // otherwise, this is a direct call from the ConverationItemView
+            return;
         }
         // When a new list item is clicked, commit any existing leave behind
         // items. Wait until we have opened the desired conversation to cause
diff --git a/src/com/android/mail/ui/ConversationListHelper.java b/src/com/android/mail/ui/ConversationListHelper.java
index 05c27d0..f7a6862 100644
--- a/src/com/android/mail/ui/ConversationListHelper.java
+++ b/src/com/android/mail/ui/ConversationListHelper.java
@@ -21,7 +21,6 @@
 import android.content.Context;
 
 import com.android.mail.providers.Account;
-import com.android.mail.ui.FolderListFragment.FolderListSelectionListener;
 
 import java.util.ArrayList;
 
@@ -30,7 +29,7 @@
      * Creates a list of newly created special views.
      */
     public ArrayList<ConversationSpecialItemView> makeConversationListSpecialViews(Context context,
-            Account account, FolderListSelectionListener listener) {
+            Account account, FolderSelector listener) {
         // TODO: Move conversation photo teaser view here once
         // getConversationListIcon() is moved out of Persistence
         return Lists.newArrayList();
diff --git a/src/com/android/mail/ui/ConversationPhotoTeaserView.java b/src/com/android/mail/ui/ConversationPhotoTeaserView.java
index 84b710d..9d5f437 100644
--- a/src/com/android/mail/ui/ConversationPhotoTeaserView.java
+++ b/src/com/android/mail/ui/ConversationPhotoTeaserView.java
@@ -135,6 +135,12 @@
     }
 
     @Override
+    public boolean acceptsUserTaps() {
+        // No, we don't allow user taps.
+        return false;
+    }
+
+    @Override
     public void dismiss() {
         setDismissed();
         startDestroyAnimation();
diff --git a/src/com/android/mail/ui/ConversationSpecialItemView.java b/src/com/android/mail/ui/ConversationSpecialItemView.java
index 097641e..e0b221e 100644
--- a/src/com/android/mail/ui/ConversationSpecialItemView.java
+++ b/src/com/android/mail/ui/ConversationSpecialItemView.java
@@ -34,8 +34,17 @@
      */
     void onUpdate(String account, Folder folder, ConversationCursor cursor);
 
+    /**
+     * Returns whether this view is to be displayed in the list or not. A view can be added freely
+     * and it might decide to disable itself by returning false here.
+     * @return true if this view should be displayed, false otherwise.
+     */
     boolean getShouldDisplayInList();
 
+    /**
+     * Returns the position (0 indexed) where this element expects to be inserted.
+     * @return
+     */
     int getPosition();
 
     void setAdapter(AnimatedAdapter adapter);
@@ -51,4 +60,7 @@
      * Called when a regular conversation item was clicked.
      */
     void onConversationSelected();
+
+    /** Returns whether this special view is enabled (= accepts user taps). */
+    boolean acceptsUserTaps();
 }
diff --git a/src/com/android/mail/ui/ConversationUpdater.java b/src/com/android/mail/ui/ConversationUpdater.java
index 4c363c0..cb6bad6 100644
--- a/src/com/android/mail/ui/ConversationUpdater.java
+++ b/src/com/android/mail/ui/ConversationUpdater.java
@@ -23,7 +23,7 @@
 
 import com.android.mail.browse.ConfirmDialogFragment;
 import com.android.mail.browse.ConversationCursor;
-import com.android.mail.browse.MessageCursor.ConversationMessage;
+import com.android.mail.browse.ConversationMessage;
 import com.android.mail.providers.Conversation;
 import com.android.mail.providers.ConversationInfo;
 import com.android.mail.providers.Folder;
diff --git a/src/com/android/mail/ui/ConversationViewFragment.java b/src/com/android/mail/ui/ConversationViewFragment.java
index c119861..cac5b64 100644
--- a/src/com/android/mail/ui/ConversationViewFragment.java
+++ b/src/com/android/mail/ui/ConversationViewFragment.java
@@ -49,6 +49,7 @@
 import com.android.mail.R;
 import com.android.mail.browse.ConversationContainer;
 import com.android.mail.browse.ConversationContainer.OverlayPosition;
+import com.android.mail.browse.ConversationMessage;
 import com.android.mail.browse.ConversationOverlayItem;
 import com.android.mail.browse.ConversationViewAdapter;
 import com.android.mail.browse.ConversationViewAdapter.MessageFooterItem;
@@ -58,7 +59,6 @@
 import com.android.mail.browse.ConversationWebView;
 import com.android.mail.browse.MailWebView.ContentSizeChangeListener;
 import com.android.mail.browse.MessageCursor;
-import com.android.mail.browse.MessageCursor.ConversationMessage;
 import com.android.mail.browse.MessageHeaderView;
 import com.android.mail.browse.ScrollIndicatorsView;
 import com.android.mail.browse.SuperCollapsedBlock;
@@ -380,18 +380,7 @@
 
         settings.setJavaScriptEnabled(true);
 
-        final float fontScale = getResources().getConfiguration().fontScale;
-        final int desiredFontSizePx = getResources()
-                .getInteger(R.integer.conversation_desired_font_size_px);
-        final int unstyledFontSizePx = getResources()
-                .getInteger(R.integer.conversation_unstyled_font_size_px);
-
-        int textZoom = settings.getTextZoom();
-        // apply a correction to the default body text style to get regular text to the size we want
-        textZoom = textZoom * desiredFontSizePx / unstyledFontSizePx;
-        // then apply any system font scaling
-        textZoom = (int) (textZoom * fontScale);
-        settings.setTextZoom(textZoom);
+        setTextZoom(settings);
 
         mViewsCreated = true;
         mWebViewLoadedData = false;
@@ -677,6 +666,10 @@
                 }
                 prevCollapsedMsg = msg;
                 prevSafeForImages = safeForImages;
+
+                // This line puts the from address in the address cache so that
+                // we get the sender image for it if it's in a super-collapsed block.
+                getAddress(msg.getFrom());
                 continue;
             }
 
diff --git a/src/com/android/mail/ui/DrawerFragment.java b/src/com/android/mail/ui/DrawerFragment.java
index 85436e4..c1b9d5c 100644
--- a/src/com/android/mail/ui/DrawerFragment.java
+++ b/src/com/android/mail/ui/DrawerFragment.java
@@ -17,14 +17,34 @@
 
 package com.android.mail.ui;
 
+import android.widget.ListView;
+
 /**
  * A drawer that is shown in one pane mode, as a pull-out from the left.  All the
  * implementation is inherited from the FolderListFragment.
+ *
+ * The drawer shows a list of accounts, the recent folders, and a list of top-level folders for
+ * the given account. This fragment is created using no arguments, it gets all its state from the
+ * controller in {@link #onActivityCreated(android.os.Bundle)}. In particular, it gets the current
+ * account, the list of accounts, and the current folder from the {@link ControllableActivity}.
+ *
+ * Once it has this information, the drawer sets itself up to observe for changes and allows the
+ * user to change folders and accounts.
+ *
+ * The drawer is always instantiated through XML resources: in one_pane_activity.xml and in
+ * two_pane_activity.xml
  */
 public class DrawerFragment extends FolderListFragment {
     public DrawerFragment() {
         super();
-        // Drawer is always sectioned.
-        mIsSectioned = true;
+        // Drawer is always divided: it shows groups for inboxes, recent folders and all other
+        // folders.
+        mIsDivided = true;
+    }
+
+    @Override
+    protected int getListViewChoiceMode() {
+        // Always let one item be selected
+        return ListView.CHOICE_MODE_SINGLE;
     }
 }
diff --git a/src/com/android/mail/ui/FolderDisplayer.java b/src/com/android/mail/ui/FolderDisplayer.java
index 4c4a68c..2e12074 100644
--- a/src/com/android/mail/ui/FolderDisplayer.java
+++ b/src/com/android/mail/ui/FolderDisplayer.java
@@ -32,7 +32,7 @@
 
 /**
  * Used to generate folder display information given a raw folders string.
- * (The raw folders string can be obtained from {@link Conversation#rawFolders}.)
+ * (The raw folders string can be obtained from {@link Conversation#getRawFolders()}.)
  *
  */
 public class FolderDisplayer {
@@ -51,16 +51,26 @@
     }
 
     /**
-     * Configure the FolderDisplayer object by parsing the rawFolders string.
+     * Configure the FolderDisplayer object by filtering and copying from the list of raw folders.
      *
-     * @param foldersString string containing serialized folders to display.
+     * @param conv {@link Conversation} containing the folders to display.
      * @param ignoreFolderUri (optional) folder to omit from the displayed set
      * @param ignoreFolderType -1, or the {@link FolderType} to omit from the displayed set
      */
     public void loadConversationFolders(Conversation conv, final Uri ignoreFolderUri,
             final int ignoreFolderType) {
         mFoldersSortedSet.clear();
-        mFoldersSortedSet.addAll(conv.getRawFoldersForDisplay(ignoreFolderUri, ignoreFolderType));
+        for (Folder folder : conv.getRawFolders()) {
+            // Skip the ignoreFolderType
+            if (ignoreFolderType >= 0 && folder.isType(ignoreFolderType)) {
+                continue;
+            }
+            // skip the ignoreFolder
+            if (ignoreFolderUri != null && ignoreFolderUri.equals(folder.uri)) {
+                continue;
+            }
+            mFoldersSortedSet.add(folder);
+        }
     }
 
     /**
diff --git a/src/com/android/mail/ui/FolderItemView.java b/src/com/android/mail/ui/FolderItemView.java
index 329f59f..ef0cfee 100644
--- a/src/com/android/mail/ui/FolderItemView.java
+++ b/src/com/android/mail/ui/FolderItemView.java
@@ -15,47 +15,29 @@
  */
 package com.android.mail.ui;
 
-import com.android.mail.R;
-
+import android.content.Context;
+import android.graphics.Color;
+import android.util.AttributeSet;
+import android.view.DragEvent;
+import android.view.View;
 import android.widget.ImageView;
+import android.widget.RelativeLayout;
 import android.widget.TextView;
 
+import com.android.mail.R;
 import com.android.mail.providers.Account;
 import com.android.mail.providers.Folder;
 import com.android.mail.utils.LogTag;
 import com.android.mail.utils.LogUtils;
 import com.android.mail.utils.Utils;
 
-import android.content.Context;
-import android.content.res.ColorStateList;
-import android.content.res.Resources;
-import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
-import android.graphics.Color;
-import android.graphics.drawable.Drawable;
-import android.util.AttributeSet;
-import android.view.DragEvent;
-import android.view.View;
-import android.widget.RelativeLayout;
-
 /**
  * The view for each folder in the folder list.
  */
 public class FolderItemView extends RelativeLayout {
     private final String LOG_TAG = LogTag.getLogTag();
-    // Static colors
-    private static int NON_DROPPABLE_TARGET_TEXT_COLOR;
 
-    // Static bitmap
-    private static Bitmap SHORTCUT_ICON;
-
-    // These are fine to be static, as these Drawables only have one state
-    private static Drawable DROPPABLE_HOVER_BACKGROUND;
-    private static Drawable DRAG_STEADY_STATE_BACKGROUND;
-
-    private Drawable mBackground;
-    private ColorStateList mInitialFolderTextColor;
-    private ColorStateList mInitialUnreadCountTextColor;
+    private static final int[] STATE_DRAG_MODE = {R.attr.state_drag_mode};
 
     private Folder mFolder;
     private TextView mFolderTextView;
@@ -64,6 +46,8 @@
     private DropHandler mDropHandler;
     private ImageView mFolderParentIcon;
 
+    private boolean mIsDragMode;
+
     /**
      * A delegate for a handler to handle a drop of an item.
      */
@@ -91,28 +75,17 @@
 
     public FolderItemView(Context context, AttributeSet attrs, int defStyle) {
         super(context, attrs, defStyle);
+
+        mIsDragMode = false;
     }
 
     @Override
     protected void onFinishInflate() {
         super.onFinishInflate();
-        if (SHORTCUT_ICON == null) {
-            final Resources res = getResources();
-            SHORTCUT_ICON = BitmapFactory.decodeResource(
-                    res, R.mipmap.ic_launcher_shortcut_folder);
-            DROPPABLE_HOVER_BACKGROUND =
-                    res.getDrawable(R.drawable.folder_drag_target);
-            DRAG_STEADY_STATE_BACKGROUND =
-                    res.getDrawable(R.drawable.folder_no_hover);
-            NON_DROPPABLE_TARGET_TEXT_COLOR =
-                    res.getColor(R.color.folder_disabled_drop_target_text_color);
-        }
+
         mFolderTextView = (TextView)findViewById(R.id.name);
         mUnreadCountTextView = (TextView)findViewById(R.id.unread);
         mUnseenCountTextView = (TextView)findViewById(R.id.unseen);
-        mBackground = getBackground();
-        mInitialFolderTextColor = mFolderTextView.getTextColors();
-        mInitialUnreadCountTextColor = mUnreadCountTextView.getTextColors();
         mFolderParentIcon = (ImageView) findViewById(R.id.folder_parent_icon);
     }
 
@@ -221,43 +194,20 @@
     public boolean onDragEvent(DragEvent event) {
         switch (event.getAction()) {
             case DragEvent.ACTION_DRAG_STARTED:
-                // If this folder is not a drop target, dim the text.
-                if (!isDroppableTarget(event)) {
-                    // Make sure we update this at the time we drop on the target.
-                    mInitialFolderTextColor = mFolderTextView.getTextColors();
-                    mInitialUnreadCountTextColor = mUnreadCountTextView.getTextColors();
-                    mFolderTextView.setTextColor(NON_DROPPABLE_TARGET_TEXT_COLOR);
-                    mUnreadCountTextView.setTextColor(NON_DROPPABLE_TARGET_TEXT_COLOR);
-                }
-                // Set the background to a steady state background.
-                setBackgroundDrawable(DRAG_STEADY_STATE_BACKGROUND);
-                return true;
-
+                // Set drag mode state to true now that we have entered drag mode.
+                // This change updates the states of icons and text colors.
+                // Additional drawable states are updated by the framework
+                // based on the DragEvent.
+                setDragMode(true);
             case DragEvent.ACTION_DRAG_ENTERED:
-                // Change background color to indicate this folder is the drop target.
-                if (isDroppableTarget(event)) {
-                    setBackgroundDrawable(DROPPABLE_HOVER_BACKGROUND);
-                    return true;
-                }
-                break;
-
             case DragEvent.ACTION_DRAG_EXITED:
-                // If this is a droppable target, make sure that it is set back to steady state,
-                // when the drag leaves the view.
-                if (isDroppableTarget(event)) {
-                    setBackgroundDrawable(DRAG_STEADY_STATE_BACKGROUND);
-                    return true;
-                }
-                break;
-
+                // All of these states return based on isDroppableTarget's return value.
+                // If modifying, watch the switch's drop-through effects.
+                return isDroppableTarget(event);
             case DragEvent.ACTION_DRAG_ENDED:
-                // Reset the text of the non draggable views back to the color it had been..
-                if (!isDroppableTarget(event)) {
-                    mFolderTextView.setTextColor(mInitialFolderTextColor);
-                    mUnreadCountTextView.setTextColor(mInitialUnreadCountTextColor);
-                }
-                // Restore the background of the view.
-                setBackgroundDrawable(mBackground);
+                // Set drag mode to false since we're leaving drag mode.
+                // Updates all the states of icons and text colors back to non-drag values.
+                setDragMode(false);
                 return true;
 
             case DragEvent.ACTION_DRAG_LOCATION:
@@ -273,4 +223,18 @@
         }
         return false;
     }
+
+    @Override
+    protected int[] onCreateDrawableState(int extraSpace) {
+        final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
+        if (mIsDragMode) {
+            mergeDrawableStates(drawableState, STATE_DRAG_MODE);
+        }
+        return drawableState;
+    }
+
+    private void setDragMode(boolean isDragMode) {
+        mIsDragMode = isDragMode;
+        refreshDrawableState();
+    }
 }
diff --git a/src/com/android/mail/ui/FolderListFragment.java b/src/com/android/mail/ui/FolderListFragment.java
index 6e10fa3..120240c 100644
--- a/src/com/android/mail/ui/FolderListFragment.java
+++ b/src/com/android/mail/ui/FolderListFragment.java
@@ -21,7 +21,6 @@
 import android.app.ListFragment;
 import android.app.LoaderManager;
 import android.content.Loader;
-import android.database.Cursor;
 import android.net.Uri;
 import android.os.Bundle;
 import android.view.LayoutInflater;
@@ -55,7 +54,33 @@
 import java.util.List;
 
 /**
- * The folder list UI component.
+ * This fragment shows the list of folders and the list of accounts. Prior to June 2013,
+ * the mail application had a spinner in the top action bar. Now, the list of accounts is displayed
+ * in a drawer along with the list of folders.
+ *
+ * This class has the following use-cases:
+ * <ul>
+ *     <li>
+ *         Show a list of accounts and a divided list of folders. In this case, the list shows
+ *         Accounts, Inboxes, Recent Folders, All folders.
+ *         Tapping on Accounts takes the user to the default Inbox for that account. Tapping on
+ *         folders switches folders.
+ *         This is created through XML resources as a {@link DrawerFragment}. Since it is created
+ *         through resources, it receives all arguments through callbacks.
+ *     </li>
+ *     <li>
+ *         Show a list of folders for a specific level. At the top-level, this shows Inbox, Sent,
+ *         Drafts, Starred, and any user-created folders. For providers that allow nested folders,
+ *         this will only show the folders at the top-level.
+ *         <br /> Tapping on a parent folder creates a new fragment with the child folders at
+ *         that level.
+ *     </li>
+ *     <li>
+ *         Shows a list of folders that can be turned into widgets/shortcuts. This is used by the
+ *         {@link FolderSelectionActivity} to allow the user to create a shortcut or widget for
+ *         any folder for a given account.
+ *     </li>
+ * </ul>
  */
 public class FolderListFragment extends ListFragment implements
         LoaderManager.LoaderCallbacks<ObjectCursor<Folder>> {
@@ -66,16 +91,21 @@
     private ListView mListView;
     /** URI that points to the list of folders for the current account. */
     private Uri mFolderListUri;
-    /** True if you want a sectioned FolderList, false otherwise. */
-    protected boolean mIsSectioned;
+    /**
+     * True if you want a divided FolderList. A divided folder list shows the following groups:
+     * Inboxes, Recent Folders, All folders.
+     *
+     * An undivided FolderList shows all folders without any divisions and without recent folders.
+     */
+    protected boolean mIsDivided;
     /** True if the folder list belongs to a folder selection activity (one account only) */
     private boolean mHideAccounts;
     /** An {@link ArrayList} of {@link FolderType}s to exclude from displaying. */
     private ArrayList<Integer> mExcludedFolderTypes;
     /** Object that changes folders on our behalf. */
-    private FolderListSelectionListener mFolderChanger;
+    private FolderSelector mFolderChanger;
     /** Object that changes accounts on our behalf */
-    private AccountController mAccountChanger;
+    private AccountController mAccountController;
 
     /** The currently selected folder (the folder being viewed).  This is never null. */
     private Uri mSelectedFolderUri = Uri.EMPTY;
@@ -87,11 +117,13 @@
     /** Parent of the current folder, or null if the current folder is not a child. */
     private Folder mParentFolder;
 
-    private static final int FOLDER_LOADER_ID = 0;
+    private static final int FOLDER_LIST_LOADER_ID = 0;
+    /** Loader id for the full list of folders in the account */
+    private static final int FULL_FOLDER_LIST_LOADER_ID = 1;
     /** Key to store {@link #mParentFolder}. */
     private static final String ARG_PARENT_FOLDER = "arg-parent-folder";
-    /** Key to store {@link #mIsSectioned} */
-    private static final String ARG_IS_SECTIONED = "arg-is-sectioned";
+    /** Key to store {@link #mIsDivided} */
+    private static final String ARG_IS_DIVIDED = "arg-is-divided";
     /** Key to store {@link #mFolderListUri}. */
     private static final String ARG_FOLDER_LIST_URI = "arg-folder-list-uri";
     /** Key to store {@link #mExcludedFolderTypes} */
@@ -161,9 +193,9 @@
      */
     public static FolderListFragment ofDrawer() {
         final FolderListFragment fragment = new FolderListFragment();
-        // The drawer is always sectioned
-        final boolean isSectioned = true;
-        fragment.setArguments(getBundleFromArgs(TYPE_DRAWER, null, null, isSectioned, null, false));
+        /** The drawer is always divided: see comments on {@link #mIsDivided} above. */
+        final boolean isDivided = true;
+        fragment.setArguments(getBundleFromArgs(TYPE_DRAWER, null, null, isDivided, null, false));
         return fragment;
     }
 
@@ -175,10 +207,10 @@
      */
     public static FolderListFragment ofTree(Folder folder, final boolean hideAccounts) {
         final FolderListFragment fragment = new FolderListFragment();
-        // Trees are never sectioned.
-        final boolean isSectioned = false;
+        /** Trees are never divided: see comments on {@link #mIsDivided} above. */
+        final boolean isDivided = false;
         fragment.setArguments(getBundleFromArgs(TYPE_TREE, folder, folder.childFoldersListUri,
-                isSectioned, null, hideAccounts));
+                isDivided, null, hideAccounts));
         return fragment;
     }
 
@@ -192,10 +224,10 @@
     public static FolderListFragment ofTopLevelTree(Uri folderListUri,
             final ArrayList<Integer> excludedFolderTypes, final boolean hideAccounts) {
         final FolderListFragment fragment = new FolderListFragment();
-        // Trees are never sectioned.
-        final boolean isSectioned = false;
+        /** Trees are never divided: see comments on {@link #mIsDivided} above. */
+        final boolean isDivided = false;
         fragment.setArguments(getBundleFromArgs(TYPE_TREE, null, folderListUri,
-                isSectioned, excludedFolderTypes, hideAccounts));
+                isDivided, excludedFolderTypes, hideAccounts));
         return fragment;
     }
 
@@ -203,14 +235,14 @@
      * Construct a bundle that represents the state of this fragment.
      * @param type the type of FLF: {@link #TYPE_DRAWER} or {@link #TYPE_TREE}
      * @param parentFolder non-null for trees, the parent of this list
-     * @param isSectioned true if this drawer is sectioned, false otherwise
+     * @param isDivided true if this drawer is divided, false otherwise
      * @param folderListUri the URI which contains all the list of folders
      * @param excludedFolderTypes if non-null, this indicates folders to exclude in lists.
-     * @return Bundle containing parentFolder, sectioned list boolean and
+     * @return Bundle containing parentFolder, divided list boolean and
      *         excluded folder types
      */
     private static Bundle getBundleFromArgs(int type, Folder parentFolder, Uri folderListUri,
-            boolean isSectioned, final ArrayList<Integer> excludedFolderTypes,
+            boolean isDivided, final ArrayList<Integer> excludedFolderTypes,
             final boolean hideAccounts) {
         final Bundle args = new Bundle();
         args.putInt(ARG_TYPE, type);
@@ -220,7 +252,7 @@
         if (folderListUri != null) {
             args.putString(ARG_FOLDER_LIST_URI, folderListUri.toString());
         }
-        args.putBoolean(ARG_IS_SECTIONED, isSectioned);
+        args.putBoolean(ARG_IS_DIVIDED, isDivided);
         if (excludedFolderTypes != null) {
             args.putIntegerArrayList(ARG_EXCLUDED_FOLDER_TYPES, excludedFolderTypes);
         }
@@ -264,7 +296,7 @@
             mCursorAdapter = new HierarchicalFolderListAdapter(null, mParentFolder);
             selectedFolder = mActivity.getHierarchyFolder();
         } else {
-            mCursorAdapter = new FolderListAdapter(mIsSectioned);
+            mCursorAdapter = new FolderListAdapter(mIsDivided);
             selectedFolder = currentFolder;
         }
         // Is the selected folder fresher than the one we have restored from a bundle?
@@ -280,7 +312,7 @@
                 setSelectedAccount(newAccount);
             }
         };
-        mFolderChanger = mActivity.getFolderListSelectionListener();
+        mFolderChanger = mActivity.getFolderSelector();
         if (accountController != null) {
             // Current account and its observer.
             setSelectedAccount(mAccountObserver.initialize(accountController));
@@ -292,7 +324,7 @@
                 }
             };
             mAllAccountsObserver.initialize(accountController);
-            mAccountChanger = accountController;
+            mAccountController = accountController;
 
             // Observer for when the drawer is closed
             mDrawerObserver = new DrawerClosedObserver() {
@@ -301,12 +333,11 @@
                     // First, check if there's a folder to change to
                     if (mNextFolder != null) {
                         mFolderChanger.onFolderSelected(mNextFolder);
-                        // Wait for an update to the current folder. When we get the next folder,
-                        // then we null it out.
+                        mNextFolder = null;
                     }
                     // Next, check if there's an account to change to
                     if (mNextAccount != null) {
-                        mAccountChanger.switchToDefaultInboxOrChangeAccount(mNextAccount);
+                        mAccountController.switchToDefaultInboxOrChangeAccount(mNextAccount);
                         mNextAccount = null;
                     }
                 }
@@ -319,6 +350,8 @@
             return;
         }
 
+        mListView.setChoiceMode(getListViewChoiceMode());
+
         setListAdapter(mCursorAdapter);
     }
 
@@ -337,7 +370,7 @@
         } else {
             mFolderListUri = Uri.parse(folderUri);
         }
-        mIsSectioned = args.getBoolean(ARG_IS_SECTIONED);
+        mIsDivided = args.getBoolean(ARG_IS_DIVIDED);
         mExcludedFolderTypes = args.getIntegerArrayList(ARG_EXCLUDED_FOLDER_TYPES);
         mType = args.getInt(ARG_TYPE);
         mHideAccounts = args.getBoolean(ARG_HIDE_ACCOUNTS, false);
@@ -346,13 +379,10 @@
     @Override
     public View onCreateView(LayoutInflater inflater, ViewGroup container,
             Bundle savedState) {
-        final Bundle args = getArguments();
-        if (args != null) {
-            mHideAccounts = args.getBoolean(ARG_HIDE_ACCOUNTS, false);
-        }
+        setInstanceFromBundle(getArguments());
+
         final View rootView = inflater.inflate(R.layout.folder_list, null);
         mListView = (ListView) rootView.findViewById(android.R.id.list);
-        mListView.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
         mListView.setEmptyView(null);
         mListView.setDivider(null);
         if (savedState != null && savedState.containsKey(BUNDLE_LIST_STATE)) {
@@ -438,7 +468,7 @@
         // Switching accounts takes you to the default inbox for that account.
         mSelectedFolderType = DrawerItem.FOLDER_INBOX;
         mNextAccount = account;
-        mAccountChanger.closeDrawer(true, mNextAccount, getDefaultInbox(mNextAccount));
+        mAccountController.closeDrawer(true, mNextAccount, getDefaultInbox(mNextAccount));
     }
 
     /**
@@ -465,7 +495,7 @@
                         mListView.setItemChecked(defaultInboxPosition, true);
                     }
                     // ... and close the drawer (no new target folders/accounts)
-                    mAccountChanger.closeDrawer(false, mNextAccount,
+                    mAccountController.closeDrawer(false, mNextAccount,
                             getDefaultInbox(mNextAccount));
                 } else {
                     changeAccount(account);
@@ -502,10 +532,10 @@
             // Go to the conversation list for this folder.
             if (!folder.uri.equals(mSelectedFolderUri)) {
                 mNextFolder = folder;
-                mAccountChanger.closeDrawer(true, nextAccount, folder);
+                mAccountController.closeDrawer(true, nextAccount, folder);
             } else {
                 // Clicked on same folder, just close drawer
-                mAccountChanger.closeDrawer(false, nextAccount, folder);
+                mAccountController.closeDrawer(false, nextAccount, folder);
             }
         }
     }
@@ -514,10 +544,10 @@
     public Loader<ObjectCursor<Folder>> onCreateLoader(int id, Bundle args) {
         mListView.setEmptyView(null);
         final Uri folderListUri;
-        if (mType == TYPE_TREE) {
+        if (id == FOLDER_LIST_LOADER_ID && mType == TYPE_TREE) {
             // Folder trees, they specify a URI at construction time.
             folderListUri = mFolderListUri;
-        } else if (mType == TYPE_DRAWER) {
+        } else if (id == FOLDER_LIST_LOADER_ID && mType == TYPE_DRAWER) {
             // Drawers should have a valid account
             if (mCurrentAccount != null) {
                 folderListUri = mCurrentAccount.folderListUri;
@@ -525,6 +555,8 @@
                 LogUtils.wtf(LOG_TAG, "FLF.onCreateLoader() for Drawer with null account");
                 return null;
             }
+        } else if (id == FULL_FOLDER_LIST_LOADER_ID) {
+            folderListUri = mCurrentAccount.fullFolderListUri;
         } else {
             LogUtils.wtf(LOG_TAG, "FLF.onCreateLoader() with weird type");
             return null;
@@ -536,14 +568,22 @@
     @Override
     public void onLoadFinished(Loader<ObjectCursor<Folder>> loader, ObjectCursor<Folder> data) {
         if (mCursorAdapter != null) {
-            mCursorAdapter.setCursor(data);
+            if (loader.getId() == FOLDER_LIST_LOADER_ID) {
+                mCursorAdapter.setCursor(data);
+            } else if (loader.getId() == FULL_FOLDER_LIST_LOADER_ID) {
+                mCursorAdapter.setFullFolderListCursor(data);
+            }
         }
     }
 
     @Override
     public void onLoaderReset(Loader<ObjectCursor<Folder>> loader) {
         if (mCursorAdapter != null) {
-            mCursorAdapter.setCursor(null);
+            if (loader.getId() == FOLDER_LIST_LOADER_ID) {
+                mCursorAdapter.setCursor(null);
+            } else if (loader.getId() == FULL_FOLDER_LIST_LOADER_ID) {
+                mCursorAdapter.setFullFolderListCursor(null);
+            }
         }
     }
 
@@ -565,6 +605,8 @@
     private interface FolderListFragmentCursorAdapter extends ListAdapter {
         /** Update the folder list cursor with the cursor given here. */
         void setCursor(ObjectCursor<Folder> cursor);
+        /** Update the full folder list cursor with the cursor given here. */
+        void setFullFolderListCursor(ObjectCursor<Folder> cursor);
         /**
          * Given an item, find the type of the item, which should only be {@link
          * DrawerItem#VIEW_FOLDER} or {@link DrawerItem#VIEW_ACCOUNT}
@@ -591,7 +633,7 @@
         private final RecentFolderObserver mRecentFolderObserver = new RecentFolderObserver() {
             @Override
             public void onChanged() {
-                if (!isCursorInvalid(mCursor)) {
+                if (!isCursorInvalid()) {
                     recalculateList();
                 }
             }
@@ -600,12 +642,15 @@
         private static final int NO_HEADER_RESOURCE = -1;
         /** Cache of most recently used folders */
         private final RecentFolderList mRecentFolders;
-        /** True if the list is sectioned, false otherwise */
-        private final boolean mIsSectioned;
+        /** True if the list is divided, false otherwise. See the comment on
+         * {@link FolderListFragment#mIsDivided} for more information */
+        private final boolean mIsDivided;
         /** All the items */
         private List<DrawerItem> mItemList = new ArrayList<DrawerItem>();
         /** Cursor into the folder list. This might be null. */
         private ObjectCursor<Folder> mCursor = null;
+        /** Cursor into the full folder list. This might be null. */
+        private ObjectCursor<Folder> mFullFolderListCursor = null;
         /** Watcher for tracking and receiving unread counts for mail */
         private FolderWatcher mFolderWatcher = null;
         private boolean mRegistered = false;
@@ -613,13 +658,14 @@
         /**
          * Creates a {@link FolderListAdapter}.This is a list of all the accounts and folders.
          *
-         * @param isSectioned true if folder list is flat, false if sectioned by label group
+         * @param isDivided true if folder list is flat, false if divided by label group. See
+         *                   the comments on {@link #mIsDivided} for more information
          */
-        public FolderListAdapter(boolean isSectioned) {
+        public FolderListAdapter(boolean isDivided) {
             super();
-            mIsSectioned = isSectioned;
+            mIsDivided = isDivided;
             final RecentFolderController controller = mActivity.getRecentFolderController();
-            if (controller != null && mIsSectioned) {
+            if (controller != null && mIsDivided) {
                 mRecentFolders = mRecentFolderObserver.initialize(controller);
             } else {
                 mRecentFolders = null;
@@ -630,9 +676,9 @@
 
         @Override
         public void notifyAllAccountsChanged() {
-            if (!mRegistered && mAccountChanger != null) {
+            if (!mRegistered && mAccountController != null) {
                 // TODO(viki): Round-about way of setting the watcher. http://b/8750610
-                mAccountChanger.setFolderWatcher(mFolderWatcher);
+                mAccountController.setFolderWatcher(mFolderWatcher);
                 mRegistered = true;
             }
             mFolderWatcher.updateAccountList(getAllAccounts());
@@ -764,14 +810,14 @@
             // If we are waiting for folder initialization, we don't have any kinds of folders,
             // just the "Waiting for initialization" item. Note, this should only be done
             // when we're waiting for account initialization or initial sync.
-            if (isCursorInvalid(mCursor)) {
+            if (isCursorInvalid()) {
                 if(!mCurrentAccount.isAccountReady()) {
-                    itemList.add(DrawerItem.forWaitView(mActivity));
+                    itemList.add(DrawerItem.ofWaitView(mActivity));
                 }
                 return;
             }
 
-            if (!mIsSectioned) {
+            if (!mIsDivided) {
                 // Adapter for a flat list. Everything is a FOLDER_OTHER, and there are no headers.
                 do {
                     final Folder f = mCursor.getModel();
@@ -784,10 +830,9 @@
                 return;
             }
 
-            // Otherwise, this is an adapter for a sectioned list.
+            // Otherwise, this is an adapter for a divided list.
             final List<DrawerItem> allFoldersList = new ArrayList<DrawerItem>();
             final List<DrawerItem> inboxFolders = new ArrayList<DrawerItem>();
-            boolean currentFolderFound = false;
             do {
                 final Folder f = mCursor.getModel();
                 if (!isFolderTypeExcluded(f)) {
@@ -798,42 +843,60 @@
                         allFoldersList.add(DrawerItem.ofFolder(
                                 mActivity, f, DrawerItem.FOLDER_OTHER, mCursor.getPosition()));
                     }
-                    if (f.equals(mCurrentFolderForUnreadCheck)) {
-                        currentFolderFound = true;
-                    }
                 }
             } while (mCursor.moveToNext());
 
-            if (!currentFolderFound && mCurrentFolderForUnreadCheck != null
-                    && mCurrentAccount != null && mAccountChanger != null
-                    && mAccountChanger.isDrawerPullEnabled()) {
-                LogUtils.d(LOG_TAG, "Current folder (%1$s) has disappeared for %2$s",
-                        mCurrentFolderForUnreadCheck.name, mCurrentAccount.name);
-                changeAccount(mCurrentAccount);
+            // If we have the full folder list, verify that the current folder exists
+            boolean currentFolderFound = false;
+            if (mFullFolderListCursor != null) {
+                final String folderName = mCurrentFolderForUnreadCheck == null
+                        ? "null" : mCurrentFolderForUnreadCheck.name;
+                LogUtils.d(LOG_TAG, "Checking if full folder list contains %s", folderName);
+
+                if (mFullFolderListCursor.moveToFirst()) {
+                    LogUtils.d(LOG_TAG, "Cursor for %s seems reasonably valid", folderName);
+                    do {
+                        final Folder f = mFullFolderListCursor.getModel();
+                        if (!isFolderTypeExcluded(f)) {
+                            if (f.equals(mCurrentFolderForUnreadCheck)) {
+                                LogUtils.d(LOG_TAG, "Found %s !", folderName);
+                                currentFolderFound = true;
+                            }
+                        }
+                    } while (mFullFolderListCursor.moveToNext());
+                }
+
+                if (!currentFolderFound && mCurrentFolderForUnreadCheck != null
+                        && mCurrentAccount != null && mAccountController != null
+                        && mAccountController.isDrawerPullEnabled()) {
+                    LogUtils.d(LOG_TAG, "Current folder (%1$s) has disappeared for %2$s",
+                            mCurrentFolderForUnreadCheck.name, mCurrentAccount.name);
+                    changeAccount(mCurrentAccount);
+                }
             }
 
-            // Add all inboxes (sectioned included) before recents.
-            addFolderSection(itemList, inboxFolders, R.string.inbox_folders_heading);
+            // Add all inboxes (sectioned Inboxes included) before recent folders.
+            addFolderDivision(itemList, inboxFolders, R.string.inbox_folders_heading);
 
-            // Add most recently folders (in alphabetical order) next.
+            // Add recent folders next.
             addRecentsToList(itemList);
 
-            // Add the remaining provider folders followed by all labels.
-            addFolderSection(itemList, allFoldersList,  R.string.all_folders_heading);
+            // Add the remaining folders.
+            addFolderDivision(itemList, allFoldersList, R.string.all_folders_heading);
         }
 
         /**
-         * Given a list of folders as {@link DrawerItem}s, add them to the item
-         * list as needed. Passing in a non-0 integer for the resource will
-         * enable a header
+         * Given a list of folders as {@link DrawerItem}s, add them as a group.
+         * Passing in a non-0 integer for the resource will enable a header.
          *
          * @param destination List of drawer items to populate
          * @param source List of drawer items representing folders to add to the drawer
          * @param headerStringResource
          *            {@link FolderListAdapter#NO_HEADER_RESOURCE} if no header
-         *            is required, or res-id otherwise
+         *            is required, or res-id otherwise. The integer is interpreted as the string
+         *            for the header's title.
          */
-        private void addFolderSection(List<DrawerItem> destination, List<DrawerItem> source,
+        private void addFolderDivision(List<DrawerItem> destination, List<DrawerItem> source,
                 int headerStringResource) {
             if (source.size() > 0) {
                 if(headerStringResource != NO_HEADER_RESOURCE) {
@@ -875,10 +938,9 @@
 
         /**
          * Check if the cursor provided is valid.
-         * @param mCursor
          * @return True if cursor is invalid, false otherwise
          */
-        private boolean isCursorInvalid(Cursor mCursor) {
+        private boolean isCursorInvalid() {
             return mCursor == null || mCursor.isClosed()|| mCursor.getCount() <= 0
                     || !mCursor.moveToFirst();
         }
@@ -890,6 +952,12 @@
         }
 
         @Override
+        public void setFullFolderListCursor(final ObjectCursor<Folder> cursor) {
+            mFullFolderListCursor = cursor;
+            recalculateList();
+        }
+
+        @Override
         public Object getItem(int position) {
             // Is there an attempt made to access outside of the drawer item list?
             if (position >= mItemList.size()) {
@@ -940,7 +1008,7 @@
     }
 
     private class HierarchicalFolderListAdapter extends ArrayAdapter<Folder>
-            implements FolderListFragmentCursorAdapter{
+            implements FolderListFragmentCursorAdapter {
 
         private static final int PARENT = 0;
         private static final int CHILD = 1;
@@ -1015,6 +1083,11 @@
         }
 
         @Override
+        public void setFullFolderListCursor(final ObjectCursor<Folder> cursor) {
+            // Not necessary in HierarchicalFolderListAdapter
+        }
+
+        @Override
         public void destroy() {
             // Do nothing.
         }
@@ -1065,12 +1138,6 @@
             LogUtils.e(LOG_TAG, "FolderListFragment.setSelectedFolder(null) called!");
             return;
         }
-        // Is this the folder we changed to previously?  If not, ignore the update
-        if (mNextFolder != null && !folder.uri.equals(mNextFolder.uri)) {
-            // Update to a folder that we don't care about.  Ignore
-            return;
-        }
-        mNextFolder = null;
 
         final boolean viewChanged =
                 !FolderItemView.areSameViews(folder, mCurrentFolderForUnreadCheck);
@@ -1110,8 +1177,10 @@
             // comment on {@link AbstractActivityController#restartOptionalLoader} to see why we
             // don't just do restartLoader.
             final LoaderManager manager = getLoaderManager();
-            manager.destroyLoader(FOLDER_LOADER_ID);
-            manager.restartLoader(FOLDER_LOADER_ID, Bundle.EMPTY, this);
+            manager.destroyLoader(FOLDER_LIST_LOADER_ID);
+            manager.restartLoader(FOLDER_LIST_LOADER_ID, Bundle.EMPTY, this);
+            manager.destroyLoader(FULL_FOLDER_LIST_LOADER_ID);
+            manager.restartLoader(FULL_FOLDER_LIST_LOADER_ID, Bundle.EMPTY, this);
             // An updated cursor causes the entire list to refresh. No need to refresh the list.
             // But we do need to blank out the current folder, since the account might not be
             // synced.
@@ -1122,14 +1191,11 @@
             // non-null account -> null account transition.
             LogUtils.e(LOG_TAG, "FLF.setSelectedAccount(null) called! Destroying existing loader.");
             final LoaderManager manager = getLoaderManager();
-            manager.destroyLoader(FOLDER_LOADER_ID);
+            manager.destroyLoader(FOLDER_LIST_LOADER_ID);
+            manager.destroyLoader(FULL_FOLDER_LIST_LOADER_ID);
         }
     }
 
-    public interface FolderListSelectionListener {
-        public void onFolderSelected(Folder folder);
-    }
-
     /**
      * Get whether the FolderListFragment is currently showing the hierarchy
      * under a single parent.
@@ -1154,4 +1220,11 @@
 
         return false;
     }
+
+    /**
+     * @return the choice mode to use for the {@link ListView}
+     */
+    protected int getListViewChoiceMode() {
+        return mAccountController.getFolderListViewChoiceMode();
+    }
 }
diff --git a/src/com/android/mail/ui/FolderSelectionActivity.java b/src/com/android/mail/ui/FolderSelectionActivity.java
index be2608e..8b8d0a8 100644
--- a/src/com/android/mail/ui/FolderSelectionActivity.java
+++ b/src/com/android/mail/ui/FolderSelectionActivity.java
@@ -27,17 +27,16 @@
 import android.database.DataSetObservable;
 import android.database.DataSetObserver;
 import android.os.Bundle;
-import android.util.Log;
 import android.view.DragEvent;
 import android.view.View;
 import android.view.View.OnClickListener;
 import android.widget.Button;
+import android.widget.ListView;
 
 import com.android.mail.R;
 import com.android.mail.providers.Account;
 import com.android.mail.providers.Folder;
 import com.android.mail.providers.FolderWatcher;
-import com.android.mail.ui.FolderListFragment.FolderListSelectionListener;
 import com.android.mail.ui.ViewMode.ModeChangeListener;
 import com.android.mail.utils.LogTag;
 import com.android.mail.utils.LogUtils;
@@ -53,7 +52,7 @@
  */
 public class FolderSelectionActivity extends Activity implements OnClickListener,
         DialogInterface.OnClickListener, FolderChangeListener, ControllableActivity,
-        FolderListSelectionListener {
+        FolderSelector {
     public static final String EXTRA_ACCOUNT_SHORTCUT = "account-shortcut";
 
     private static final String LOG_TAG = LogTag.getLogTag();
@@ -110,13 +109,14 @@
         @Override
         public void changeAccount(Account account) {
             // Never gets called, so do nothing here.
-            Log.wtf(LOG_TAG, "FolderSelectionActivity.changeAccount() called when NOT expected.");
+            LogUtils.wtf(LOG_TAG,
+                    "FolderSelectionActivity.changeAccount() called when NOT expected.");
         }
 
         @Override
         public void switchToDefaultInboxOrChangeAccount(Account account) {
             // Never gets called, so do nothing here.
-            Log.wtf(LOG_TAG,"FolderSelectionActivity.switchToDefaultInboxOrChangeAccount() " +
+            LogUtils.wtf(LOG_TAG,"FolderSelectionActivity.switchToDefaultInboxOrChangeAccount() " +
                     "called when NOT expected.");
         }
 
@@ -149,6 +149,11 @@
             // Unsupported
             return false;
         }
+
+        @Override
+        public int getFolderListViewChoiceMode() {
+            return ListView.CHOICE_MODE_NONE;
+        }
     };
 
     @Override
@@ -358,7 +363,7 @@
     }
 
     @Override
-    public FolderListSelectionListener getFolderListSelectionListener() {
+    public FolderSelector getFolderSelector() {
         return this;
     }
 
diff --git a/src/com/android/mail/ui/FolderSelector.java b/src/com/android/mail/ui/FolderSelector.java
new file mode 100644
index 0000000..6523a34
--- /dev/null
+++ b/src/com/android/mail/ui/FolderSelector.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2013 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.mail.ui;
+
+import com.android.mail.providers.Folder;
+
+/**
+ * Interface that permits elements to implement selecting a folder.
+ * The single method {@link #onFolderSelected(com.android.mail.providers.Folder)} defines what
+ * happens when a folder is selected.
+ */
+public interface FolderSelector {
+    /**
+     * Selects the folder provided as an argument here.  This corresponds to the user
+     * selecting a folder in the UI element, either for creating a widget/shortcut (as in the
+     * case of {@link FolderSelectionActivity} or for viewing the contents of
+     * the folder (as in the case of {@link AbstractActivityController}.
+     * @param folder
+     */
+    public void onFolderSelected(Folder folder);
+}
diff --git a/src/com/android/mail/ui/MailActivity.java b/src/com/android/mail/ui/MailActivity.java
index c02f8a2..2cf2256 100644
--- a/src/com/android/mail/ui/MailActivity.java
+++ b/src/com/android/mail/ui/MailActivity.java
@@ -36,7 +36,6 @@
 
 import com.android.mail.compose.ComposeActivity;
 import com.android.mail.providers.Folder;
-import com.android.mail.ui.FolderListFragment.FolderListSelectionListener;
 import com.android.mail.ui.ViewMode.ModeChangeListener;
 import com.android.mail.utils.StorageLowState;
 import com.android.mail.utils.Utils;
@@ -50,8 +49,6 @@
  * conversation list or a conversation view).
  */
 public class MailActivity extends AbstractMailActivity implements ControllableActivity {
-    // TODO(viki) This class lacks: Sync Window Upgrade dialog
-
     /**
      * The activity controller to which we delegate most Activity lifecycle events.
      */
@@ -310,7 +307,7 @@
     }
 
     @Override
-    public FolderListSelectionListener getFolderListSelectionListener() {
+    public FolderSelector getFolderSelector() {
         return mController;
     }
 
diff --git a/src/com/android/mail/ui/NestedFolderView.java b/src/com/android/mail/ui/NestedFolderView.java
new file mode 100644
index 0000000..e9c3afb
--- /dev/null
+++ b/src/com/android/mail/ui/NestedFolderView.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright (C) 2013 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.mail.ui;
+
+import com.android.mail.R;
+import com.android.mail.browse.ConversationCursor;
+import com.android.mail.providers.Folder;
+import com.android.mail.utils.LogTag;
+import com.android.mail.utils.LogUtils;
+
+import android.app.LoaderManager;
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+/**
+ * For folders that might contain other folders, we show the nested folders within this view.
+ * Tapping on this opens the folder.
+ */
+public class NestedFolderView extends LinearLayout implements ConversationSpecialItemView,
+        SwipeableItemView {
+    protected static final String LOG_TAG = LogTag.getLogTag();
+    /**
+     * The actual view that is displayed and is perhaps swiped away. We don't allow swiping,
+     * but this is required by the {@link SwipeableItemView} interface.
+     */
+    private View mSwipeableContent;
+    /** The folder this view represents */
+    private Folder mFolder;
+
+    public NestedFolderView(Context context) {
+        super(context);
+    }
+
+    public NestedFolderView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public NestedFolderView(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+        mSwipeableContent = findViewById(R.id.swipeable_content);
+    }
+
+    @Override
+    public void onUpdate(String s, Folder folder, ConversationCursor conversationCursor) {
+        // Do nothing. We don't care about the change to the conversation cursor here.
+        // Nested folders only care if they were removed from the parent folder,
+        // so supposing we should check for that here.
+    }
+
+    /**
+     * Sets the folder associated with this view. This method is meant to be called infrequently,
+     * since we assume that a new view will be created when the unread count changes.
+     * @param folder the folder that this view represents.
+     */
+    public void setFolder(Folder folder) {
+        mFolder = folder;
+        // Since we assume that setFolder will be called infrequently (once currently),
+        // we don't bother saving the textviews for folder name and folder unread count.  If we
+        // find that setFolder gets called repeatedly, it might be prudent to remember the
+        // references to these textviews, making setFolder slightly faster.
+        TextView t = (TextView) findViewById(R.id.nested_folder_name);
+        t.setText(folder.name);
+        t = (TextView) findViewById(R.id.nested_folder_unread);
+        t.setText("" + folder.unreadCount);
+    }
+
+    /**
+     * Returns the folder associated with this view
+     * @return a folder that this view represents.
+     */
+    public Folder getFolder() {
+        return mFolder;
+    }
+
+    @Override
+    public boolean getShouldDisplayInList() {
+        // Nested folders once created are always displayed in the list.
+        return true;
+    }
+
+    @Override
+    public int getPosition() {
+        // We only have one element, and that's always at the top for now.
+        return 0;
+    }
+
+    @Override
+    public void setAdapter(AnimatedAdapter animatedAdapter) {
+        // Do nothing, since the adapter creates these views.
+    }
+
+    @Override
+    public void bindLoaderManager(LoaderManager loaderManager) {
+        // Do nothing. We don't need the loader manager.
+    }
+
+    @Override
+    public void cleanup() {
+        // Do nothing.
+    }
+
+    @Override
+    public void onConversationSelected() {
+        // Do nothing. We don't care if conversations are selected.
+    }
+
+    @Override
+    public boolean acceptsUserTaps() {
+        return true;
+    }
+
+    @Override
+    public SwipeableView getSwipeableView() {
+        return SwipeableView.from(mSwipeableContent);
+    }
+
+    @Override
+    public boolean canChildBeDismissed() {
+        // The folders can never be dismissed, return false.
+        return false;
+    }
+
+    @Override
+    public void dismiss() {
+        /** How did this happen? We returned false in {@link #canChildBeDismissed()} so this
+         * method should never be called. */
+        LogUtils.wtf(LOG_TAG, "NestedFolderView.dismiss() called. Not expected.");
+    }
+
+    @Override
+    public float getMinAllowScrollDistance() {
+        return -1;
+    }
+}
diff --git a/src/com/android/mail/ui/OnePaneController.java b/src/com/android/mail/ui/OnePaneController.java
index 527513a..1ab18f3 100644
--- a/src/com/android/mail/ui/OnePaneController.java
+++ b/src/com/android/mail/ui/OnePaneController.java
@@ -24,6 +24,7 @@
 import android.net.Uri;
 import android.os.Bundle;
 import android.support.v4.widget.DrawerLayout;
+import android.widget.ListView;
 
 import com.android.mail.ConversationListContext;
 import com.android.mail.R;
@@ -200,7 +201,8 @@
         final int transition = mConversationListNeverShown
                 ? FragmentTransaction.TRANSIT_FRAGMENT_FADE
                 : FragmentTransaction.TRANSIT_FRAGMENT_OPEN;
-        Fragment conversationListFragment = ConversationListFragment.newInstance(listContext);
+        final Fragment conversationListFragment =
+                ConversationListFragment.newInstance(listContext);
 
         if (!inInbox(mAccount, listContext)) {
             // Maintain fragment transaction history so we can get back to the
@@ -374,6 +376,12 @@
             mActivity.finish();
         } else if (mViewMode.isListMode() && !inInbox(mAccount, mConvListContext)) {
             if (mLastFolderListTransactionId != INVALID_ID) {
+                // Set the hierarchy folder to what it will be once we go up
+                final Folder hierarchyFolder = getHierarchyFolder();
+                if (hierarchyFolder != null && hierarchyFolder.parent != null) {
+                    setHierarchyFolder(hierarchyFolder.parent);
+                }
+
                 // If the user got here by navigating via the folder list, back
                 // should bring them back to the folder list.
                 mViewMode.enterFolderListMode();
@@ -391,22 +399,11 @@
     }
 
     private void goUpFolderHierarchy(Folder current) {
-        Folder top = current.parent;
+        final Folder top = current.parent;
         if (top != null) {
             // FIXME: This is silly. we worked so hard to add folder fragments to the back stack.
             // it should either just pop back, or should not use the back stack at all.
-
-            setHierarchyFolder(top);
-            // Replace this fragment with a new FolderListFragment
-            // showing this folder's children if we are not already
-            // looking at the child view for this folder.
-            mLastFolderListTransactionId = replaceFragmentWithBack(
-                    FolderListFragment.ofTree(top, false),
-                    FragmentTransaction.TRANSIT_FRAGMENT_OPEN, TAG_FOLDER_LIST, R.id.content_pane);
-        } else {
-            // Otherwise, clear the selected folder and go back to whatever the
-            // last folder list displayed was.
-            // TODO(viki): Load folder list for parent folder.
+            onFolderSelected(top);
         }
     }
 
@@ -424,18 +421,8 @@
 
     @Override
     public void onFolderSelected(Folder folder) {
-        if (folder.hasChildren && !folder.equals(getHierarchyFolder())) {
-            mViewMode.enterFolderListMode();
-            setHierarchyFolder(folder);
-            // Replace this fragment with a new FolderListFragment
-            // showing this folder's children if we are not already
-            // looking at the child view for this folder.
-            mLastFolderListTransactionId = replaceFragmentWithBack(
-                    FolderListFragment.ofTree(folder, false),
-                    FragmentTransaction.TRANSIT_FRAGMENT_OPEN, TAG_FOLDER_LIST, R.id.content_pane);
-        } else {
-            super.onFolderSelected(folder);
-        }
+        setHierarchyFolder(folder);
+        super.onFolderSelected(folder);
     }
 
     private static boolean isTransactionIdValid(int id) {
@@ -511,10 +498,6 @@
         onConversationListVisibilityChanged(true);
     }
 
-    private void safelyPopBackStack(boolean inLoaderCallbacks) {
-        safelyPopBackStack(-1, inLoaderCallbacks);
-    }
-
     /**
      * Pop to a specified point in the fragment back stack without causing IllegalStateExceptions
      * from committing a fragment transaction "at the wrong time".
@@ -652,4 +635,10 @@
         // The drawer is enabled for one pane mode
         return true;
     }
+
+    @Override
+    public int getFolderListViewChoiceMode() {
+        // By default, we do not want to allow any item to be selected in the folder list
+        return ListView.CHOICE_MODE_NONE;
+    }
 }
diff --git a/src/com/android/mail/ui/SecureConversationViewFragment.java b/src/com/android/mail/ui/SecureConversationViewFragment.java
index 3a74d35..f995f07 100644
--- a/src/com/android/mail/ui/SecureConversationViewFragment.java
+++ b/src/com/android/mail/ui/SecureConversationViewFragment.java
@@ -18,7 +18,7 @@
 package com.android.mail.ui;
 
 import android.content.Loader;
-import android.database.Cursor;
+import android.content.res.Resources;
 import android.net.Uri;
 import android.os.Bundle;
 import android.view.LayoutInflater;
@@ -30,11 +30,11 @@
 import android.webkit.WebViewClient;
 
 import com.android.mail.R;
+import com.android.mail.browse.ConversationMessage;
 import com.android.mail.browse.ConversationViewAdapter;
 import com.android.mail.browse.ConversationViewAdapter.MessageHeaderItem;
 import com.android.mail.browse.ConversationViewHeader;
 import com.android.mail.browse.MessageCursor;
-import com.android.mail.browse.MessageCursor.ConversationMessage;
 import com.android.mail.browse.MessageFooterView;
 import com.android.mail.browse.MessageHeaderView;
 import com.android.mail.browse.MessageHeaderView.MessageHeaderViewCallbacks;
@@ -52,6 +52,11 @@
 public class SecureConversationViewFragment extends AbstractConversationViewFragment implements
         MessageHeaderViewCallbacks {
     private static final String LOG_TAG = LogTag.getLogTag();
+
+    private static final String BEGIN_HTML =
+            "<body style=\"margin: 0 %spx;\"><div style=\"margin: 16px 0; font-size: 80%%\">";
+    private static final String END_HTML = "</div></body>";
+
     private MessageWebView mWebView;
     private ConversationViewHeader mConversationHeaderView;
     private MessageHeaderView mMessageHeaderView;
@@ -59,6 +64,8 @@
     private ConversationMessage mMessage;
     private MessageScrollView mScrollView;
 
+    private int mSideMarginInWebPx;
+
     private final WebViewClient mWebViewClient = new AbstractConversationWebViewClient() {
         @Override
         public void onPageFinished(WebView view, String url) {
@@ -96,7 +103,7 @@
     public void onActivityCreated(Bundle savedInstanceState) {
         super.onActivityCreated(savedInstanceState);
         mConversationHeaderView.setCallbacks(this, this);
-        mConversationHeaderView.setFoldersVisible(false);
+        mConversationHeaderView.setFolders(mConversation);
         mConversationHeaderView.setSubject(mConversation.subject);
         mMessageHeaderView.initialize(mDateBuilder, this, mAddressCache);
         mMessageHeaderView.setExpandMode(MessageHeaderView.POPUP_MODE);
@@ -109,6 +116,11 @@
         mMessageFooterView.initialize(getLoaderManager(), getFragmentManager());
         getLoaderManager().initLoader(MESSAGE_LOADER, null, getMessageLoaderCallbacks());
         showLoadingStatus();
+
+        final Resources r = getResources();
+        mSideMarginInWebPx = (int) ((r.getDimensionPixelOffset(
+                R.dimen.conversation_view_margin_side) + r.getDimensionPixelOffset(
+                R.dimen.conversation_message_content_margin_side)) / r.getDisplayMetrics().density);
     }
 
     @Override
@@ -128,6 +140,8 @@
         settings.setJavaScriptEnabled(false);
         settings.setLayoutAlgorithm(LayoutAlgorithm.NORMAL);
 
+        setTextZoom(settings);
+
         settings.setSupportZoom(true);
         settings.setBuiltInZoomControls(true);
         settings.setDisplayZoomControls(false);
@@ -238,10 +252,17 @@
             LogUtils.e(LOG_TAG, "unable to open message cursor");
             return;
         }
-        final ConversationMessage m = messageCursor.getMessage();
         mMessage = messageCursor.getMessage();
         mWebView.getSettings().setBlockNetworkImage(!mMessage.alwaysShowImages);
-        mWebView.loadDataWithBaseURL(mBaseUri, m.getBodyAsHtml(), "text/html", "utf-8", null);
+
+        // Add formatting to message body
+        // At this point, only adds margins.
+        StringBuilder dataBuilder = new StringBuilder(
+                String.format(BEGIN_HTML, mSideMarginInWebPx));
+        dataBuilder.append(mMessage.getBodyAsHtml());
+        dataBuilder.append(END_HTML);
+
+        mWebView.loadDataWithBaseURL(mBaseUri, dataBuilder.toString(), "text/html", "utf-8", null);
         final ConversationViewAdapter adapter = new ConversationViewAdapter(mActivity, null, null,
                 null, null, null, null, null, null);
         final MessageHeaderItem item = adapter.newMessageHeaderItem(mMessage, true,
diff --git a/src/com/android/mail/ui/SwipeHelper.java b/src/com/android/mail/ui/SwipeHelper.java
index 4effb17..7b8b1da 100644
--- a/src/com/android/mail/ui/SwipeHelper.java
+++ b/src/com/android/mail/ui/SwipeHelper.java
@@ -26,7 +26,6 @@
 import android.content.Context;
 import android.content.res.Resources;
 import android.graphics.RectF;
-import android.util.Log;
 import android.view.MotionEvent;
 import android.view.VelocityTracker;
 import android.view.View;
@@ -34,6 +33,7 @@
 
 import com.android.mail.R;
 import com.android.mail.browse.ConversationItemView;
+import com.android.mail.utils.LogUtils;
 import com.android.mail.utils.Utils;
 
 import java.util.ArrayList;
@@ -59,10 +59,7 @@
     private static int MAX_ESCAPE_ANIMATION_DURATION;
     private static int MAX_DISMISS_VELOCITY;
     private static int SNAP_ANIM_LEN;
-    private static int DISMISS_ANIMATION_DURATION;
     private static float MIN_SWIPE;
-    private static float MIN_VERT;
-    private static float MIN_LOCK;
 
     public static float ALPHA_FADE_START = 0f; // fraction of thumbnail width
                                                  // where fade starts
@@ -70,7 +67,6 @@
     static final float ALPHA_FADE_END = 0.7f; // fraction of thumbnail width
                                               // beyond which alpha->0
     private static final float FACTOR = 1.2f;
-    private float mMinAlpha = 0.5f;
 
     /* Dead region where swipe cannot be initiated. */
     private final static int DEAD_REGION_FOR_SWIPE = 56;
@@ -104,10 +100,7 @@
             MAX_ESCAPE_ANIMATION_DURATION = res.getInteger(R.integer.max_escape_animation_duration);
             MAX_DISMISS_VELOCITY = res.getInteger(R.integer.max_dismiss_velocity);
             SNAP_ANIM_LEN = res.getInteger(R.integer.snap_animation_duration);
-            DISMISS_ANIMATION_DURATION = res.getInteger(R.integer.dismiss_animation_duration);
             MIN_SWIPE = res.getDimension(R.dimen.min_swipe);
-            MIN_VERT = res.getDimension(R.dimen.min_vert);
-            MIN_LOCK = res.getDimension(R.dimen.min_lock);
         }
     }
 
@@ -155,10 +148,6 @@
                 v.getMeasuredHeight();
     }
 
-    public void setMinAlpha(float minAlpha) {
-        mMinAlpha = minAlpha;
-    }
-
     private float getAlphaForOffset(View view) {
         float viewSize = getSize(view);
         final float fadeSize = ALPHA_FADE_END * viewSize;
@@ -169,7 +158,8 @@
         } else if (pos < viewSize * (1.0f - ALPHA_FADE_START)) {
             result = 1.0f + (viewSize * ALPHA_FADE_START + pos) / fadeSize;
         }
-        return Math.max(mMinAlpha, result);
+        float minAlpha = 0.5f;
+        return Math.max(minAlpha, result);
     }
 
     private float getTextAlphaForOffset(View view) {
@@ -197,7 +187,7 @@
     public static void invalidateGlobalRegion(View view, RectF childBounds) {
         //childBounds.offset(view.getTranslationX(), view.getTranslationY());
         if (DEBUG_INVALIDATE)
-            Log.v(TAG, "-------------");
+            LogUtils.v(TAG, "-------------");
         while (view.getParent() != null && view.getParent() instanceof View) {
             view = (View) view.getParent();
             view.getMatrix().mapRect(childBounds);
@@ -206,7 +196,7 @@
                             (int) Math.ceil(childBounds.right),
                             (int) Math.ceil(childBounds.bottom));
             if (DEBUG_INVALIDATE) {
-                Log.v(TAG, "INVALIDATE(" + (int) Math.floor(childBounds.left)
+                LogUtils.v(TAG, "INVALIDATE(" + (int) Math.floor(childBounds.left)
                         + "," + (int) Math.floor(childBounds.top)
                         + "," + (int) Math.ceil(childBounds.right)
                         + "," + (int) Math.ceil(childBounds.bottom));
@@ -221,6 +211,12 @@
                 mLastY = ev.getY();
                 mDragging = false;
                 View view = mCallback.getChildAtPosition(ev);
+                if (view instanceof NestedFolderView) {
+                    // We don't want to allow nested folders to swipe at all. This would give the
+                    // false hope that they might be deleted by swiping away. Instead, treat them
+                    // like a plain list view element that doesn't allow any swipe gesture.
+                    return false;
+                }
                 if (view instanceof SwipeableItemView) {
                     mCurrView = (SwipeableItemView) view;
                 }
@@ -305,46 +301,6 @@
         anim.start();
     }
 
-    private void dismissChildren(final Collection<ConversationItemView> views, float velocity,
-            AnimatorListenerAdapter listener) {
-        final View animView = mCurrView.getSwipeableView().getView();
-        final boolean canAnimViewBeDismissed = mCallback.canChildBeDismissed(mCurrView);
-        float newPos = determinePos(animView, velocity);
-        int duration = DISMISS_ANIMATION_DURATION;
-        ArrayList<Animator> animations = new ArrayList<Animator>();
-        ObjectAnimator anim;
-        for (final ConversationItemView view : views) {
-            Utils.enableHardwareLayer(view);
-            anim = createDismissAnimation(view, newPos, duration);
-            anim.addUpdateListener(new AnimatorUpdateListener() {
-                @Override
-                public void onAnimationUpdate(ValueAnimator animation) {
-                    if (FADE_OUT_DURING_SWIPE && canAnimViewBeDismissed) {
-                        view.setAlpha(getAlphaForOffset(view));
-                    }
-                    invalidateGlobalRegion(view);
-                }
-            });
-            anim.addListener(new AnimatorListenerAdapter() {
-                @Override
-                public void onAnimationEnd(Animator animation) {
-                    view.setLayerType(View.LAYER_TYPE_NONE, null);
-                }
-            });
-            animations.add(anim);
-        }
-        AnimatorSet transitionSet = new AnimatorSet();
-        transitionSet.playTogether(animations);
-        transitionSet.addListener(listener);
-        transitionSet.start();
-    }
-
-    public void dismissChildren(ConversationItemView first,
-            final Collection<ConversationItemView> views, AnimatorListenerAdapter listener) {
-        mCurrView = first;
-        dismissChildren(views, 0f, listener);
-    }
-
     private static int determineDuration(View animView, float newPos, float velocity) {
         int duration = MAX_ESCAPE_ANIMATION_DURATION;
         if (velocity != 0) {
@@ -423,11 +379,6 @@
                     // If the user has gone vertical and not gone horizontalish AT
                     // LEAST minBeforeLock, switch to scroll. Otherwise, cancel
                     // the swipe.
-                    if (!mDragging && deltaY > MIN_VERT && (Math.abs(deltaX)) < MIN_LOCK
-                            && deltaY > (FACTOR * Math.abs(deltaX))) {
-                        mCallback.onScroll();
-                        return false;
-                    }
                     float minDistance = MIN_SWIPE;
                     if (Math.abs(deltaX) < minDistance) {
                         // Don't start the drag until at least X distance has
@@ -482,7 +433,7 @@
                             && (velocity > 0) == (mCurrAnimView.getTranslationX() > 0)
                             && translation > 0.05 * currAnimViewSize;
                     if (LOG_SWIPE_DISMISS_VELOCITY) {
-                        Log.v(TAG, "Swipe/Dismiss: " + velocity + "/" + escapeVelocity + "/"
+                        LogUtils.v(TAG, "Swipe/Dismiss: " + velocity + "/" + escapeVelocity + "/"
                                 + perpendicularVelocity + ", x: " + translation + "/"
                                 + currAnimViewSize);
                     }
diff --git a/src/com/android/mail/ui/SwipeableItemView.java b/src/com/android/mail/ui/SwipeableItemView.java
index 9bf7f34..3a4a13f 100644
--- a/src/com/android/mail/ui/SwipeableItemView.java
+++ b/src/com/android/mail/ui/SwipeableItemView.java
@@ -29,6 +29,11 @@
 
     public void dismiss();
 
+    /**
+     * Returns the minimum allowed displacement in the Y axis that is considered a scroll. After
+     * this displacement, all future events are considered scroll events rather than swipes.
+     * @return
+     */
     public float getMinAllowScrollDistance();
 
     public static class SwipeableView {
diff --git a/src/com/android/mail/ui/SwipeableListView.java b/src/com/android/mail/ui/SwipeableListView.java
index 517cc15..987e52a 100644
--- a/src/com/android/mail/ui/SwipeableListView.java
+++ b/src/com/android/mail/ui/SwipeableListView.java
@@ -160,7 +160,7 @@
     public View getChildAtPosition(MotionEvent ev) {
         // find the view under the pointer, accounting for GONE views
         final int count = getChildCount();
-        int touchY = (int) ev.getY();
+        final int touchY = (int) ev.getY();
         int childIdx = 0;
         View slidingChild;
         for (; childIdx < count; childIdx++) {
@@ -331,15 +331,15 @@
 
     @Override
     public boolean performItemClick(View view, int pos, long id) {
-        int previousPosition = getCheckedItemPosition();
-        boolean selectionSetEmpty = mConvSelectionSet.isEmpty();
+        final int previousPosition = getCheckedItemPosition();
+        final boolean selectionSetEmpty = mConvSelectionSet.isEmpty();
 
         // Superclass method modifies the selection set
-        boolean handled = super.performItemClick(view, pos, id);
+        final boolean handled = super.performItemClick(view, pos, id);
 
         // If we are in CAB mode with no checkboxes then a click shouldn't
         // activate the new item, it should only add it to the selection set
-        boolean showSenderImage = mAccount != null
+        final boolean showSenderImage = mAccount != null
                 && (mAccount.settings.convListIcon == ConversationListIcon.SENDER_IMAGE);
         if (!showSenderImage && !selectionSetEmpty && previousPosition != -1) {
             setItemChecked(previousPosition, true);
diff --git a/src/com/android/mail/ui/TwoPaneController.java b/src/com/android/mail/ui/TwoPaneController.java
index e02a5ba..f67f30a 100644
--- a/src/com/android/mail/ui/TwoPaneController.java
+++ b/src/com/android/mail/ui/TwoPaneController.java
@@ -24,6 +24,7 @@
 import android.support.v4.widget.DrawerLayout;
 import android.view.Gravity;
 import android.widget.FrameLayout;
+import android.widget.ListView;
 
 import com.android.mail.ConversationListContext;
 import com.android.mail.R;
@@ -69,7 +70,8 @@
         FragmentTransaction fragmentTransaction = mActivity.getFragmentManager().beginTransaction();
         // Use cross fading animation.
         fragmentTransaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
-        Fragment conversationListFragment = ConversationListFragment.newInstance(mConvListContext);
+        final Fragment conversationListFragment =
+                ConversationListFragment.newInstance(mConvListContext);
         fragmentTransaction.replace(R.id.conversation_list_pane, conversationListFragment,
                 TAG_CONVERSATION_LIST);
         fragmentTransaction.commitAllowingStateLoss();
@@ -166,26 +168,18 @@
             mViewMode.enterConversationListMode();
         }
 
-        if (folder.hasChildren && !folder.equals(getHierarchyFolder())) {
-            // Replace this fragment with a new FolderListFragment
-            // showing this folder's children if we are not already looking
-            // at the child view for this folder.
-            createFolderTree(folder);
+        if (folder.hasChildren) {
             // Show the up affordance when digging into child folders.
             mActionBarView.setBackButton();
-        } else {
-            setHierarchyFolder(folder);
         }
+        setHierarchyFolder(folder);
         super.onFolderSelected(folder);
     }
 
     private void goUpFolderHierarchy(Folder current) {
-        Folder parent = current.parent;
-        if (parent.parent != null) {
-            createFolderTree(parent.parent);
-            // Show the up affordance when digging into child folders.
-            mActionBarView.setBackButton();
-        } else {
+        // If the current folder is a child, up should show the parent folder.
+        final Folder parent = current.parent;
+        if (parent != null) {
             onFolderSelected(parent);
         }
     }
@@ -573,4 +567,10 @@
     public boolean isDrawerEnabled() {
         return mLayout.isDrawerEnabled();
     }
+
+    @Override
+    public int getFolderListViewChoiceMode() {
+        // By default, we want to allow one item to be selected in the folder list
+        return ListView.CHOICE_MODE_SINGLE;
+    }
 }
diff --git a/src/com/android/mail/utils/FragmentStatePagerAdapter2.java b/src/com/android/mail/utils/FragmentStatePagerAdapter2.java
index 7d0914a..a8cba20 100644
--- a/src/com/android/mail/utils/FragmentStatePagerAdapter2.java
+++ b/src/com/android/mail/utils/FragmentStatePagerAdapter2.java
@@ -25,7 +25,6 @@
 import android.support.v13.app.FragmentStatePagerAdapter;
 import android.support.v4.util.SparseArrayCompat;
 import android.support.v4.view.PagerAdapter;
-import android.util.Log;
 import android.view.View;
 import android.view.ViewGroup;
 
@@ -90,7 +89,7 @@
         }
 
         Fragment fragment = getItem(position);
-        if (DEBUG) Log.v(TAG, "Adding item #" + position + ": f=" + fragment);
+        if (DEBUG) LogUtils.v(TAG, "Adding item #" + position + ": f=" + fragment);
         if (mEnableSavedStates && mSavedState.size() > position) {
             Fragment.SavedState fss = mSavedState.get(position);
             if (fss != null) {
@@ -113,7 +112,7 @@
         if (mCurTransaction == null) {
             mCurTransaction = mFragmentManager.beginTransaction();
         }
-        if (DEBUG) Log.v(TAG, "Removing item #" + position + ": f=" + object
+        if (DEBUG) LogUtils.v(TAG, "Removing item #" + position + ": f=" + object
                 + " v=" + ((Fragment)object).getView());
         if (mEnableSavedStates) {
             while (mSavedState.size() <= position) {
@@ -199,7 +198,7 @@
                         setItemVisible(f, false);
                         mFragments.put(index, f);
                     } else {
-                        Log.w(TAG, "Bad fragment at key " + key);
+                        LogUtils.w(TAG, "Bad fragment at key " + key);
                     }
                 }
             }
diff --git a/unified_src/com/android/mail/utils/LogTag.java b/src/com/android/mail/utils/LogTag.java
similarity index 68%
rename from unified_src/com/android/mail/utils/LogTag.java
rename to src/com/android/mail/utils/LogTag.java
index 01e2cf8..76a598b 100644
--- a/unified_src/com/android/mail/utils/LogTag.java
+++ b/src/com/android/mail/utils/LogTag.java
@@ -17,12 +17,20 @@
 package com.android.mail.utils;
 
 public class LogTag {
-    private static String LOG_TAG = "UnifiedEmail";
+    private static String sLogTag = "UnifiedEmail";
 
     /**
      * Get the log tag to apply to logging.
      */
     public static String getLogTag() {
-        return LOG_TAG;
+        return sLogTag;
+    }
+
+    /**
+     * Sets the app-wide log tag to be used in most log messages, and for enabling logging
+     * verbosity. This should be called at most once, during app start-up.
+     */
+    public static void setLogTag(final String logTag) {
+        sLogTag = logTag;
     }
 }
diff --git a/src/com/android/mail/utils/LogUtils.java b/src/com/android/mail/utils/LogUtils.java
index f4fb230..229f4ee 100644
--- a/src/com/android/mail/utils/LogUtils.java
+++ b/src/com/android/mail/utils/LogUtils.java
@@ -25,7 +25,7 @@
 
 public class LogUtils {
 
-    public static final String TAG = "UnifiedEmail";
+    public static final String TAG = LogTag.getLogTag();
 
     // "GMT" + "+" or "-" + 4 digits
     private static final Pattern DATE_CLEANUP_PATTERN_WRONG_TIMEZONE =
@@ -61,7 +61,7 @@
      * production releases.  This should be set to DEBUG for production releases, and VERBOSE for
      * internal builds.
      */
-    private static final int MAX_ENABLED_LOG_LEVEL = DEBUG;
+    private static final int MAX_ENABLED_LOG_LEVEL = VERBOSE;
 
     private static Boolean sDebugLoggingEnabledForTests = null;
 
@@ -69,7 +69,7 @@
      * Enable debug logging for unit tests.
      */
     @VisibleForTesting
-    static void setDebugLoggingEnabledForTests(boolean enabled) {
+    public static void setDebugLoggingEnabledForTests(boolean enabled) {
         setDebugLoggingEnabledForTestsInternal(enabled);
     }
 
@@ -95,7 +95,15 @@
         if (sDebugLoggingEnabledForTests != null) {
             return sDebugLoggingEnabledForTests.booleanValue();
         }
-        return Log.isLoggable(tag, Log.DEBUG);
+        return Log.isLoggable(tag, Log.DEBUG) || Log.isLoggable(TAG, Log.DEBUG);
+    }
+
+    /**
+     * Returns a String for the specified content provider uri.  This will do
+     * sanitation of the uri to remove PII if debug logging is not enabled.
+     */
+    public static String contentUriToString(final Uri uri) {
+        return contentUriToString(TAG, uri);
     }
 
     /**
@@ -128,39 +136,6 @@
         }
     }
 
-   /* TODO: what is the correct behavior for base case and the Gmail case? Seems like this
-    * belongs in override code in UnifiedGmail.
-    *Converts the specified set of labels to a string, and removes any PII as necessary
-    * public static String labelSetToString(Set<String> labelSet) {
-        if (isDebugLoggingEnabled() || labelSet == null) {
-            return labelSet != null ? labelSet.toString() : "";
-        } else {
-            final StringBuilder builder = new StringBuilder("[");
-            int i = 0;
-            for(String label : labelSet) {
-                if (i > 0) {
-                    builder.append(", ");
-                }
-                builder.append(sanitizeLabelName(label));
-                i++;
-            }
-            builder.append(']');
-            return builder.toString();
-        }
-    }
-
-    private static String sanitizeLabelName(String canonicalName) {
-        if (TextUtils.isEmpty(canonicalName)) {
-            return "";
-        }
-
-        if (Gmail.isSystemLabel(canonicalName)) {
-            return canonicalName;
-        }
-
-        return USER_LABEL_PREFIX + String.valueOf(canonicalName.hashCode());
-    }*/
-
     /**
      * Checks to see whether or not a log for the specified tag is loggable at the specified level.
      */
@@ -168,7 +143,7 @@
         if (MAX_ENABLED_LOG_LEVEL > level) {
             return false;
         }
-        return Log.isLoggable(tag, level);
+        return Log.isLoggable(tag, level) || Log.isLoggable(TAG, level);
     }
 
     /**
diff --git a/src/com/android/mail/utils/MimeType.java b/src/com/android/mail/utils/MimeType.java
index 8f0ca96..aec9382 100644
--- a/src/com/android/mail/utils/MimeType.java
+++ b/src/com/android/mail/utils/MimeType.java
@@ -15,8 +15,6 @@
  */
 package com.android.mail.utils;
 
-import com.android.mail.utils.LogTag;
-
 import android.content.Context;
 import android.content.Intent;
 import android.content.pm.PackageManager;
@@ -43,20 +41,12 @@
     static final String GENERIC_MIMETYPE = "application/octet-stream";
 
     @VisibleForTesting
-    static final String EML_ATTACHMENT_CONTENT_TYPE = "application/eml";
+    public static final String EML_ATTACHMENT_CONTENT_TYPE = "message/rfc822";
     private static final String NULL_ATTACHMENT_CONTENT_TYPE = "null";
     private static final Set<String> UNACCEPTABLE_ATTACHMENT_TYPES = ImmutableSet.of(
             "application/zip", "application/x-gzip", "application/x-bzip2",
             "application/x-compress", "application/x-compressed", "application/x-tar");
 
-    private static Set<String> sGviewSupportedTypes = ImmutableSet.of(
-            "application/pdf",
-            "application/vnd.ms-powerpoint",
-            "image/tiff",
-            "application/msword",
-            "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
-            "application/vnd.openxmlformats-officedocument.presentationml.presentation");
-
     /**
      * Returns whether or not an attachment of the specified type is installable (e.g. an apk).
      */
@@ -120,16 +110,6 @@
         return UNACCEPTABLE_ATTACHMENT_TYPES.contains(contentType);
     }
 
-    /* TODO: what do we want to do about GSF keys for the unified app?
-    public static boolean isPreviewable(Context context, String contentType) {
-        final String supportedTypes = Gservices.getString(
-                context.getContentResolver(), GservicesKeys.GMAIL_GVIEW_SUPPORTED_TYPES);
-        if (supportedTypes != null) {
-            sGviewSupportedTypes = ImmutableSet.of(TextUtils.split(supportedTypes, ","));
-        }
-        return sGviewSupportedTypes.contains(contentType);
-    }*/
-
     /**
      * Extract and return filename's extension, converted to lower case, and not including the "."
      *
@@ -151,7 +131,7 @@
      * Returns the mime type of the attachment based on its name and
      * original mime type. This is an workaround for bugs where Gmail
      * server doesn't set content-type for certain types correctly.
-     * 1) EML files -> "application/eml".
+     * 1) EML files -> "message/rfc822".
      * @param name name of the attachment.
      * @param mimeType original mime type of the attachment.
      * @return the inferred mime type of the attachment.
@@ -174,7 +154,7 @@
             if (!TextUtils.isEmpty(type)) {
                 return type;
             } if (extension.equals("eml")) {
-                // Extension is ".eml", return mime type "application/eml"
+                // Extension is ".eml", return mime type "message/rfc822"
                 return EML_ATTACHMENT_CONTENT_TYPE;
             } else {
                 // Extension is not ".eml", just return original mime type.
diff --git a/src/com/android/mail/utils/NotificationUtils.java b/src/com/android/mail/utils/NotificationUtils.java
index de4d887..1e2ffaf 100644
--- a/src/com/android/mail/utils/NotificationUtils.java
+++ b/src/com/android/mail/utils/NotificationUtils.java
@@ -32,13 +32,9 @@
 import android.provider.ContactsContract.CommonDataKinds.Email;
 import android.provider.ContactsContract.Contacts.Photo;
 import android.support.v4.app.NotificationCompat;
-import android.text.Html;
-import android.text.Spannable;
 import android.text.SpannableString;
 import android.text.SpannableStringBuilder;
-import android.text.Spanned;
 import android.text.TextUtils;
-import android.text.TextUtils.SimpleStringSplitter;
 import android.text.style.CharacterStyle;
 import android.text.style.TextAppearanceSpan;
 import android.util.Pair;
@@ -53,6 +49,7 @@
 import com.android.mail.preferences.FolderPreferences;
 import com.android.mail.preferences.MailPrefs;
 import com.android.mail.providers.Account;
+import com.android.mail.providers.Address;
 import com.android.mail.providers.Conversation;
 import com.android.mail.providers.Folder;
 import com.android.mail.providers.Message;
@@ -63,17 +60,15 @@
 import com.google.android.common.html.parser.HtmlDocument;
 import com.google.android.common.html.parser.HtmlTree;
 import com.google.common.base.Objects;
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Lists;
-import com.google.common.collect.Maps;
 import com.google.common.collect.Sets;
 
 import java.io.ByteArrayInputStream;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
-import java.util.Deque;
 import java.util.List;
-import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 
@@ -88,11 +83,6 @@
     private static TextAppearanceSpan sNotificationUnreadStyleSpan;
     private static CharacterStyle sNotificationReadStyleSpan;
 
-    private static final Map<Integer, Integer> sPriorityToLength = Maps.newHashMap();
-    private static final SimpleStringSplitter SENDER_LIST_SPLITTER =
-            new SimpleStringSplitter(Utils.SENDER_LIST_SEPARATOR);
-    private static String[] sSenderFragments = new String[8];
-
     /** A factory that produces a plain text converter that removes elided text. */
     private static final HtmlTree.PlainTextConverterFactory MESSAGE_CONVERTER_FACTORY =
             new HtmlTree.PlainTextConverterFactory() {
@@ -565,14 +555,15 @@
             if (unreadCount > 0) {
                 // How can I order this properly?
                 if (cursor.moveToNext()) {
-                    Intent notificationIntent = createViewConversationIntent(context, account,
-                            folder, null);
+                    final Intent notificationIntent;
 
-                    // Launch directly to the conversation, if the
-                    // number of unseen conversations == 1
+                    // Launch directly to the conversation, if there is only 1 unseen conversation
                     if (unseenCount == 1) {
                         notificationIntent = createViewConversationIntent(context, account, folder,
                                 cursor);
+                    } else {
+                        notificationIntent = createViewConversationIntent(context, account, folder,
+                                null);
                     }
 
                     if (notificationIntent == null) {
@@ -1205,47 +1196,10 @@
     }
 
     /**
-     * Adds a fragment with given style to a string builder.
-     *
-     * @param builder the current string builder
-     * @param fragment the fragment to be added
-     * @param style the style of the fragment
-     * @param withSpaces whether to add the whole fragment or to divide it into
-     *            smaller ones
+     * Clears the notifications for the specified account/folder.
      */
-    private static void addStyledFragment(SpannableStringBuilder builder, String fragment,
-            CharacterStyle style, boolean withSpaces) {
-        if (withSpaces) {
-            int pos = builder.length();
-            builder.append(fragment);
-            builder.setSpan(CharacterStyle.wrap(style), pos, builder.length(),
-                    Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
-        } else {
-            int start = 0;
-            while (true) {
-                int pos = fragment.substring(start).indexOf(' ');
-                if (pos == -1) {
-                    addStyledFragment(builder, fragment.substring(start), style, true);
-                    break;
-                } else {
-                    pos += start;
-                    if (start < pos) {
-                        addStyledFragment(builder, fragment.substring(start, pos), style, true);
-                        builder.append(' ');
-                    }
-                    start = pos + 1;
-                    if (start >= fragment.length()) {
-                        break;
-                    }
-                }
-            }
-        }
-    }
-
-    /**
-     * Clears the notifications for the specified account/folder/conversation.
-     */
-    public static void clearFolderNotification(Context context, Account account, Folder folder) {
+    public static void clearFolderNotification(Context context, Account account, Folder folder,
+            final boolean markSeen) {
         LogUtils.v(LOG_TAG, "NotificationUtils: Clearing all notifications for %s/%s", account.name,
                 folder.name);
         final NotificationMap notificationMap = getNotificationMap(context);
@@ -1253,7 +1207,43 @@
         notificationMap.remove(key);
         notificationMap.saveNotificationMap(context);
 
-        markSeen(context, folder);
+        final NotificationManager notificationManager =
+                (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
+        notificationManager.cancel(getNotificationId(account.name, folder));
+
+        if (markSeen) {
+            markSeen(context, folder);
+        }
+    }
+
+    /**
+     * Clears all notifications for the specified account.
+     */
+    public static void clearAccountNotifications(final Context context, final String account) {
+        LogUtils.v(LOG_TAG, "NotificationUtils: Clearing all notifications for %s", account);
+        final NotificationMap notificationMap = getNotificationMap(context);
+
+        // Find all NotificationKeys for this account
+        final ImmutableList.Builder<NotificationKey> keyBuilder = ImmutableList.builder();
+
+        for (final NotificationKey key : notificationMap.keySet()) {
+            if (account.equals(key.account.name)) {
+                keyBuilder.add(key);
+            }
+        }
+
+        final List<NotificationKey> notificationKeys = keyBuilder.build();
+
+        final NotificationManager notificationManager =
+                (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
+
+        for (final NotificationKey notificationKey : notificationKeys) {
+            final Folder folder = notificationKey.folder;
+            notificationManager.cancel(getNotificationId(account, folder));
+            notificationMap.remove(notificationKey);
+        }
+
+        notificationMap.saveNotificationMap(context);
     }
 
     private static ArrayList<Long> findContacts(Context context, Collection<String> addresses) {
@@ -1371,10 +1361,14 @@
         final EmailAddress address = EmailAddress.getEmailAddress(sender);
 
         String displayableSender = address.getName();
-        // If that fails, default to the sender address.
-        if (TextUtils.isEmpty(displayableSender)) {
-            displayableSender = address.getAddress();
+
+        if (!TextUtils.isEmpty(displayableSender)) {
+            return Address.decodeAddressName(displayableSender);
         }
+
+        // If that fails, default to the sender address.
+        displayableSender = address.getAddress();
+
         // If we were unable to tokenize a name or address,
         // just use whatever was in the sender.
         if (TextUtils.isEmpty(displayableSender)) {
@@ -1450,14 +1444,7 @@
         private static final HtmlDocument.Node ELIDED_TEXT_REPLACEMENT_NODE =
                 HtmlDocument.createSelfTerminatingTag(HTML4.BR_ELEMENT, null, null, null);
 
-        private static final String STYLE_ELEMENT_ATTRIBUTE_CLASS_VALUE = "style";
-
         private int mEndNodeElidedTextBlock = -1;
-        /**
-         * A stack of the end tag numbers for <style /> tags. We don't want to
-         * include anything between these.
-         */
-        private Deque<Integer> mStyleNodeEnds = Lists.newLinkedList();
 
         @Override
         public void addNode(HtmlDocument.Node n, int nodeNum, int endNum) {
@@ -1487,8 +1474,6 @@
                             break;
                         }
                     }
-                } else if (STYLE_ELEMENT_ATTRIBUTE_CLASS_VALUE.equals(htmlElement.getName())) {
-                    mStyleNodeEnds.push(endNum);
                 }
 
                 if (foundElidedTextTag) {
@@ -1496,13 +1481,7 @@
                 }
             }
 
-            if (!mStyleNodeEnds.isEmpty() && mStyleNodeEnds.peek() == nodeNum) {
-                mStyleNodeEnds.pop();
-            }
-
-            if (mStyleNodeEnds.isEmpty()) {
-                super.addNode(n, nodeNum, endNum);
-            }
+            super.addNode(n, nodeNum, endNum);
         }
     }
 
diff --git a/src/com/google/android/common/html/parser/HtmlTree.java b/src/com/google/android/common/html/parser/HtmlTree.java
index 24ce526..35615f0 100644
--- a/src/com/google/android/common/html/parser/HtmlTree.java
+++ b/src/com/google/android/common/html/parser/HtmlTree.java
@@ -18,6 +18,7 @@
 import com.google.android.common.base.CharMatcher;
 import com.google.android.common.base.Preconditions;
 import com.google.android.common.base.X;
+import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.ImmutableSet;
 
 import java.util.ArrayList;
@@ -46,6 +47,8 @@
  * @author jlim@google.com (Jing Yee Lim)
  */
 public class HtmlTree {
+  // http://www.w3.org/TR/html4/struct/text.html#h-9.1
+  private static final CharMatcher HTML_WHITESPACE = CharMatcher.anyOf(" \t\f\u200b\r\n");
 
   /**
    * An interface that allows clients to provide their own implementation
@@ -88,6 +91,7 @@
   /** A factory that produces converters of the default implementation. */
   private static final PlainTextConverterFactory DEFAULT_CONVERTER_FACTORY =
       new PlainTextConverterFactory() {
+        @Override
         public PlainTextConverter createInstance() {
           return new DefaultPlainTextConverter();
         }
@@ -150,6 +154,35 @@
   }
 
   /**
+   * Returns number of matching open tag node, or {@code endTagNodeNum} itself
+   * if it does not point to a closing tag.
+   */
+  public int findOpenTag(int endTagNodeNum) {
+    X.assertTrue(endTagNodeNum >= 0 && endTagNodeNum < nodes.size());
+    return begins.get(endTagNodeNum);
+  }
+
+  /**
+   * Returns number of matching closing tag node, or {@code openTagNodeNum} itself
+   * if it does not point to an open tag or points to an open tag with no closing one.
+   */
+  public int findEndTag(int openTagNodeNum) {
+    X.assertTrue(openTagNodeNum >= 0 && openTagNodeNum < nodes.size());
+    return ends.get(openTagNodeNum);
+  }
+
+  /**
+   * Returns number of matching open/closing tag node, or {@code tagNodeNum} itself
+   * if it does not point to an open/closing tag (e.g text node or comment).
+   */
+  public int findPairedTag(int tagNodeNum) {
+    X.assertTrue(tagNodeNum >= 0 && tagNodeNum < nodes.size());
+    int openNodeNum = begins.get(tagNodeNum);
+    int endNodeNum = ends.get(tagNodeNum);
+    return tagNodeNum == openNodeNum ? endNodeNum : openNodeNum;
+  }
+
+  /**
    * Gets the entire html.
    */
   public String getHtml() {
@@ -238,13 +271,13 @@
 
       if (node instanceof HtmlDocument.Tag) {
         if (HTML4.TEXTAREA_ELEMENT.equals(
-            ((HtmlDocument.Tag)node).getElement())) {
+            ((HtmlDocument.Tag) node).getElement())) {
           stack++;
         }
       }
       if (node instanceof HtmlDocument.EndTag) {
         if (HTML4.TEXTAREA_ELEMENT.equals(
-            ((HtmlDocument.EndTag)node).getElement())) {
+            ((HtmlDocument.EndTag) node).getElement())) {
           if (stack == 0) {
             balanced = false;
           } else {
@@ -435,7 +468,7 @@
       if (ch == '\n') {
         return true;
       }
-      if (i < textPos && !Character.isWhitespace(ch)) {
+      if (i < textPos && !HTML_WHITESPACE.matches(ch)) {
         return false;
       }
     }
@@ -556,6 +589,7 @@
    * Encapsulates the logic for outputting plain text with respect to text
    * segments, white space separators, line breaks, and quote marks.
    */
+  @VisibleForTesting
   static final class PlainTextPrinter {
     /**
      * Separators are whitespace inserted between segments of text. The
@@ -813,7 +847,9 @@
     private final PlainTextPrinter printer = new PlainTextPrinter();
 
     private int preDepth = 0;
+    private int styleDepth = 0;
 
+    @Override
     public void addNode(HtmlDocument.Node n, int nodeNum, int endNum) {
       if (n instanceof HtmlDocument.Text) {        // A string node
 
@@ -823,6 +859,8 @@
         if (preDepth > 0) {
           printer.appendPreText(str);
 
+        } else if (styleDepth > 0) {
+          // Append nothing
         } else {
           printer.appendNormalText(str);
         }
@@ -855,6 +893,8 @@
 
         } else if (HTML4.PRE_ELEMENT.equals(element)) {
           preDepth++;
+        } else if (HTML4.STYLE_ELEMENT.equals(element)) {
+          styleDepth++;
         }
 
       } else if (n instanceof HtmlDocument.EndTag) {
@@ -876,14 +916,18 @@
 
         } else if (HTML4.PRE_ELEMENT.equals(element)) {
           preDepth--;
+        } else if (HTML4.STYLE_ELEMENT.equals(element)) {
+          styleDepth--;
         }
       }
     }
 
+    @Override
     public final int getPlainTextLength() {
       return printer.getTextLength();
     }
 
+    @Override
     public final String getPlainText() {
       return printer.getText();
     }
@@ -909,6 +953,7 @@
   }
 
   /**
+   * Adds a html start tag, there must followed later by a call to addEndTag()
    * to add the matching end tag
    */
   void addStartTag(HtmlDocument.Tag t) {
@@ -930,7 +975,6 @@
       ends.set(parent, nodenum);
     }
 
-    //is this the right pop?
     parent = stack.pop();
   }
 
@@ -951,7 +995,6 @@
 
   /** Adds a node */
   private void addNode(HtmlDocument.Node n, int begin, int end) {
-
     nodes.add(n);
     begins.add(begin);
     ends.add(end);
diff --git a/tests/src/com/android/mail/utils/MimeTypeTest.java b/tests/src/com/android/mail/utils/MimeTypeTest.java
new file mode 100644
index 0000000..9a8cd32
--- /dev/null
+++ b/tests/src/com/android/mail/utils/MimeTypeTest.java
@@ -0,0 +1,37 @@
+// Copyright 2011 Google Inc. All Rights Reserved.
+
+package com.android.mail.utils;
+
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
+
+@SmallTest
+public class MimeTypeTest extends AndroidTestCase {
+
+    private static final String TEST_MIME_TYPE = "test/mimetype";
+    public void testInferMimeType() {
+        // eml file
+        assertEquals(MimeType.EML_ATTACHMENT_CONTENT_TYPE,
+                MimeType.inferMimeType("filename.eml", MimeType.GENERIC_MIMETYPE));
+
+        // mpeg4 video files
+        assertEquals("video/mp4", MimeType.inferMimeType("video.mp4", MimeType.GENERIC_MIMETYPE));
+
+        // file with no extension, should return the mimetype that was specified
+        assertEquals(TEST_MIME_TYPE, MimeType.inferMimeType("filename", TEST_MIME_TYPE));
+
+        // file with extension, and empty mimetype, where an mimetype can be derived
+        // from the extension.
+        assertEquals("video/mp4", MimeType.inferMimeType("video.mp4", ""));
+
+        // file with extension, and empty mimetype, where an mimetype can not be derived
+        // from the extension.
+        assertEquals(MimeType.GENERIC_MIMETYPE, MimeType.inferMimeType("video.foo", ""));
+
+        // rtf files, with a generic mimetype
+        assertEquals("text/rtf", MimeType.inferMimeType("filename.rtf", MimeType.GENERIC_MIMETYPE));
+
+        // rtf files, with a specified mimetype
+        assertEquals("application/rtf", MimeType.inferMimeType("filename.rtf", "application/rtf"));
+    }
+}