am fa997535: Change translation of "me"

* commit 'fa99753541048801cec35d7b945fb65e838d8103':
  Change translation of "me"
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..91a8b3c 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" >
+            <intent-filter>
+                <action android:name="android.intent.action.VIEW" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <data android:mimeType="message/rfc822" />
+                <data android:mimeType="application/eml" />
+            </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..4999b6f
--- /dev/null
+++ b/res/layout/eml_viewer_activity.xml
@@ -0,0 +1,24 @@
+<?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.
+-->
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+             android:id="@+id/eml_root"
+             android:layout_width="match_parent"
+             android:layout_height="match_parent"
+             android:background="@android:color/white" >
+</FrameLayout>
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/menu-sw600dp-land/conversation_actions.xml b/res/menu-sw600dp-land/conversation_actions.xml
index f463841..3a74345 100644
--- a/res/menu-sw600dp-land/conversation_actions.xml
+++ b/res/menu-sw600dp-land/conversation_actions.xml
@@ -84,6 +84,11 @@
         android:icon="@drawable/ic_menu_folders_holo_light" />
 
     <item
+        android:id="@+id/move_to_inbox"
+        android:showAsAction="never"
+        android:title="@string/menu_move_to_inbox" />
+
+    <item
         android:id="@+id/mark_important"
         android:showAsAction="never"
         android:title="@string/mark_important" />
diff --git a/res/menu-sw600dp/general_prefs_fragment_menu.xml b/res/menu-sw600dp/general_prefs_fragment_menu.xml
new file mode 100644
index 0000000..37b73f2
--- /dev/null
+++ b/res/menu-sw600dp/general_prefs_fragment_menu.xml
@@ -0,0 +1,36 @@
+<?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.
+-->
+
+<menu xmlns:android="http://schemas.android.com/apk/res/android" >
+
+    <item
+            android:id="@+id/add_new_account"
+            android:title="@string/add_account"
+            android:showAsAction="ifRoom|withText" />
+
+    <item
+            android:id="@+id/clear_picture_approvals_menu_item"
+            android:showAsAction="never"
+            android:title="@string/clear_display_images_whitelist_title"/>
+    <item
+            android:id="@+id/feedback_menu_item"
+            android:icon="@android:drawable/ic_menu_send"
+            android:title="@string/feedback"/>
+
+    <!-- TODO add help menu item, once help support has been moved to UnifiedEmail -->
+</menu>
\ No newline at end of file
diff --git a/res/menu-sw600dp/settings_fragment_menu.xml b/res/menu-sw600dp/settings_fragment_menu.xml
new file mode 100644
index 0000000..afff6a9
--- /dev/null
+++ b/res/menu-sw600dp/settings_fragment_menu.xml
@@ -0,0 +1,32 @@
+<?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.
+-->
+
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <item
+            android:id="@+id/add_new_account"
+            android:title="@string/add_account"
+            android:showAsAction="ifRoom|withText" />
+
+    <item
+            android:id="@+id/feedback_menu_item"
+            android:icon="@android:drawable/ic_menu_send"
+            android:title="@string/feedback" />
+
+    <!-- TODO add help menu item, once help support has been moved to UnifiedEmail -->
+</menu>
diff --git a/res/menu/conversation_actions.xml b/res/menu/conversation_actions.xml
index e70d6db..9d63506 100644
--- a/res/menu/conversation_actions.xml
+++ b/res/menu/conversation_actions.xml
@@ -72,6 +72,11 @@
         android:showAsAction="never"
         android:icon="@drawable/ic_menu_folders_holo_light" />
 
+    <item
+        android:id="@+id/move_to_inbox"
+        android:showAsAction="never"
+        android:title="@string/menu_move_to_inbox" />
+
     <!-- Always available -->
     <item
         android:id="@+id/mark_important"
diff --git a/res/menu/conversation_list_selection_actions_menu.xml b/res/menu/conversation_list_selection_actions_menu.xml
index f635a4f..b866497 100644
--- a/res/menu/conversation_list_selection_actions_menu.xml
+++ b/res/menu/conversation_list_selection_actions_menu.xml
@@ -78,6 +78,11 @@
         android:title="@string/menu_change_folders"
         android:icon="@drawable/ic_menu_folders_holo_light" />
 
+    <item
+        android:id="@+id/move_to_inbox"
+        android:showAsAction="never"
+        android:title="@string/menu_move_to_inbox" />
+
     <item android:id="@+id/star"
         android:title="@string/add_star"
         android:showAsAction="never" />
diff --git a/res/menu/general_prefs_fragment_menu.xml b/res/menu/general_prefs_fragment_menu.xml
new file mode 100644
index 0000000..8e92d5c
--- /dev/null
+++ b/res/menu/general_prefs_fragment_menu.xml
@@ -0,0 +1,31 @@
+<?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.
+-->
+
+<menu xmlns:android="http://schemas.android.com/apk/res/android" >
+
+    <item
+        android:id="@+id/clear_picture_approvals_menu_item"
+        android:showAsAction="never"
+        android:title="@string/clear_display_images_whitelist_title"/>
+    <item
+        android:id="@+id/feedback_menu_item"
+        android:icon="@android:drawable/ic_menu_send"
+        android:title="@string/feedback"/>
+
+    <!-- TODO add help menu item, once help support has been moved to UnifiedEmail -->
+</menu>
\ No newline at end of file
diff --git a/res/menu/settings_fragment_menu.xml b/res/menu/settings_fragment_menu.xml
new file mode 100644
index 0000000..a39e681
--- /dev/null
+++ b/res/menu/settings_fragment_menu.xml
@@ -0,0 +1,27 @@
+<?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.
+-->
+
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <item
+            android:id="@+id/feedback_menu_item"
+            android:icon="@android:drawable/ic_menu_send"
+            android:title="@string/feedback" />
+
+    <!-- TODO add help menu item, once help support has been moved to UnifiedEmail -->
+</menu>
diff --git a/res/menu/settings_menu.xml b/res/menu/settings_menu.xml
new file mode 100644
index 0000000..4129ba2
--- /dev/null
+++ b/res/menu/settings_menu.xml
@@ -0,0 +1,32 @@
+<?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.
+-->
+
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <item
+            android:id="@+id/add_new_account"
+            android:title="@string/add_account"
+            android:showAsAction="always" />
+
+    <item
+            android:id="@+id/feedback_menu_item"
+            android:icon="@android:drawable/ic_menu_send"
+            android:title="@string/feedback" />
+
+    <!-- TODO add help menu item, once help support has been moved to UnifiedEmail -->
+</menu>
diff --git a/res/values-af/strings.xml b/res/values-af/strings.xml
index cc35af9..e114906 100644
--- a/res/values-af/strings.xml
+++ b/res/values-af/strings.xml
@@ -361,6 +361,10 @@
     <item msgid="732746454445519134">"Wys ouer gesprek nadat jy uitvee"</item>
     <item msgid="2189929276292165301">"Wys gespreklys nadat jy uitvee"</item>
   </string-array>
+    <string name="clear_display_images_whitelist_title" msgid="7120575487854245735">"Vee prentgoedkeurings uit"</string>
+    <string name="clear_display_images_whitelist_dialog_title" msgid="3190704164490442683">"Vee prentgoedkeurings uit?"</string>
+    <string name="clear_display_images_whitelist_dialog_message" msgid="1169152185612117654">"Hou op om inlynprente vanaf afsenders wat jy voorheen toegelaat het, te wys."</string>
+    <string name="sender_whitelist_cleared" msgid="917434007919176024">"Prente sal nie outomaties gewys word nie."</string>
     <string name="whats_new_dialog_title" msgid="4230806739326698666">"Wat\'s nuut"</string>
     <string name="notification_action_reply" msgid="6015299134424685297">"Antwoord"</string>
     <string name="notification_action_reply_all" msgid="20020468410400912">"Antwoord almal"</string>
@@ -411,4 +415,5 @@
     <string name="drawer_open" msgid="6074646853178471940">"Maak navigasielaai oop"</string>
     <string name="drawer_close" msgid="2764774620737876943">"Maak navigasielaai toe"</string>
     <string name="conversation_photo_welcome_text" msgid="4274875219447670662">"Raak \'n stuurderbeeld om daardie gesprek te kies."</string>
+    <string name="folder_icon_desc" msgid="1500547397347480618">"Vouer-ikoon"</string>
 </resources>
diff --git a/res/values-am/strings.xml b/res/values-am/strings.xml
index aee9994..dd77126 100644
--- a/res/values-am/strings.xml
+++ b/res/values-am/strings.xml
@@ -361,6 +361,10 @@
     <item msgid="732746454445519134">"ከሰረዙ በኋላ ይበልጥ የቆዩ ውይይትን ያሳዩ"</item>
     <item msgid="2189929276292165301">"ከሰረዙ በኋላ የውይይት ዝርዝርን ያሳዩ"</item>
   </string-array>
+    <string name="clear_display_images_whitelist_title" msgid="7120575487854245735">"የስዕል ጽድቆችን አጽዳ"</string>
+    <string name="clear_display_images_whitelist_dialog_title" msgid="3190704164490442683">"የስዕል ጽድቆች ይጽዱ?"</string>
+    <string name="clear_display_images_whitelist_dialog_message" msgid="1169152185612117654">"ከዚህ ቀደም ከፈቀዱላቸው ላኪዎች የሚመጡ የውስጠ-መስመር ምስሎችን ማሳየት ይቁም።"</string>
+    <string name="sender_whitelist_cleared" msgid="917434007919176024">"ስዕሎች በራስ-ሰር አይታዩም።"</string>
     <string name="whats_new_dialog_title" msgid="4230806739326698666">"ምን አዲስ ነገር አለ"</string>
     <string name="notification_action_reply" msgid="6015299134424685297">"ምላሽ ይስጡ"</string>
     <string name="notification_action_reply_all" msgid="20020468410400912">"ለሁሉም ምላሽ ይስጡ"</string>
@@ -411,4 +415,5 @@
     <string name="drawer_open" msgid="6074646853178471940">"የአሰሳ መሣቢያውን ክፈት"</string>
     <string name="drawer_close" msgid="2764774620737876943">"የአሰሳ መሣቢያውን ዝጋ"</string>
     <string name="conversation_photo_welcome_text" msgid="4274875219447670662">"ያንን ውይይት ለመምረጥ የላኪ ምስል ይንኩ።"</string>
+    <string name="folder_icon_desc" msgid="1500547397347480618">"የአቃፊ አዶ"</string>
 </resources>
diff --git a/res/values-ar/strings.xml b/res/values-ar/strings.xml
index 628a64b..843d245 100644
--- a/res/values-ar/strings.xml
+++ b/res/values-ar/strings.xml
@@ -361,6 +361,10 @@
     <item msgid="732746454445519134">"عرض المحادثة الأقدم بعد الحذف"</item>
     <item msgid="2189929276292165301">"عرض قائمة المحادثات بعد الحذف"</item>
   </string-array>
+    <string name="clear_display_images_whitelist_title" msgid="7120575487854245735">"محو الموافقات على الصور"</string>
+    <string name="clear_display_images_whitelist_dialog_title" msgid="3190704164490442683">"هل تريد محو الموافقات على الصور؟"</string>
+    <string name="clear_display_images_whitelist_dialog_message" msgid="1169152185612117654">"إيقاف عرض الصور المضمنة من المرسلين الذين سبق أن سمحت لهم."</string>
+    <string name="sender_whitelist_cleared" msgid="917434007919176024">"لن يتم عرض الصور تلقائيًا."</string>
     <string name="whats_new_dialog_title" msgid="4230806739326698666">"الميزات الجديدة"</string>
     <string name="notification_action_reply" msgid="6015299134424685297">"رد"</string>
     <string name="notification_action_reply_all" msgid="20020468410400912">"الرد على الكل"</string>
@@ -411,4 +415,5 @@
     <string name="drawer_open" msgid="6074646853178471940">"فتح ساحب التنقل"</string>
     <string name="drawer_close" msgid="2764774620737876943">"إغلاق ساحب التنقل"</string>
     <string name="conversation_photo_welcome_text" msgid="4274875219447670662">"المس صورة أحد المرسلين لتحديد تلك المحادثة."</string>
+    <string name="folder_icon_desc" msgid="1500547397347480618">"رمز المجلد"</string>
 </resources>
diff --git a/res/values-be/strings.xml b/res/values-be/strings.xml
index 308d848..499231e 100644
--- a/res/values-be/strings.xml
+++ b/res/values-be/strings.xml
@@ -361,6 +361,10 @@
     <item msgid="732746454445519134">"Паказаць больш раннюю гутарку пасля выдалення"</item>
     <item msgid="2189929276292165301">"Паказаць спіс гутарак пасля выдалення"</item>
   </string-array>
+    <string name="clear_display_images_whitelist_title" msgid="7120575487854245735">"Выдаліць дазволы па выявах"</string>
+    <string name="clear_display_images_whitelist_dialog_title" msgid="3190704164490442683">"Выдаліць дазволы па выявах?"</string>
+    <string name="clear_display_images_whitelist_dialog_message" msgid="1169152185612117654">"Спыненне адлюстравання убудаваных выяў ад адпраўнікоў, якое раней было дазволена."</string>
+    <string name="sender_whitelist_cleared" msgid="917434007919176024">"Выявы не будуць адлюстроўвацца аўтаматычна"</string>
     <string name="whats_new_dialog_title" msgid="4230806739326698666">"Што новага"</string>
     <string name="notification_action_reply" msgid="6015299134424685297">"Адказаць"</string>
     <string name="notification_action_reply_all" msgid="20020468410400912">"Адказаць усім"</string>
@@ -411,4 +415,5 @@
     <string name="drawer_open" msgid="6074646853178471940">"Адкрыць скрыню навігацыі"</string>
     <string name="drawer_close" msgid="2764774620737876943">"Закрыць скрыню навігацыі"</string>
     <string name="conversation_photo_welcome_text" msgid="4274875219447670662">"Націсніце на малюнак адпраўніка малюнак, каб выбраць размову."</string>
+    <string name="folder_icon_desc" msgid="1500547397347480618">"Значок тэчкі"</string>
 </resources>
diff --git a/res/values-bg/strings.xml b/res/values-bg/strings.xml
index aec72c4..549994e 100644
--- a/res/values-bg/strings.xml
+++ b/res/values-bg/strings.xml
@@ -361,6 +361,10 @@
     <item msgid="732746454445519134">"Показване на по-старата кореспонденция след изтриване"</item>
     <item msgid="2189929276292165301">"Показване на списъка с кореспонденции след изтриване"</item>
   </string-array>
+    <string name="clear_display_images_whitelist_title" msgid="7120575487854245735">"Изчистване на одобренията за снимки"</string>
+    <string name="clear_display_images_whitelist_dialog_title" msgid="3190704164490442683">"Да се изчистят ли одобренията за снимки?"</string>
+    <string name="clear_display_images_whitelist_dialog_message" msgid="1169152185612117654">"Спиране на показването на вградени изображения от по-рано разрешени от вас податели."</string>
+    <string name="sender_whitelist_cleared" msgid="917434007919176024">"Снимките няма да бъдат показвани автоматично."</string>
     <string name="whats_new_dialog_title" msgid="4230806739326698666">"Новите неща"</string>
     <string name="notification_action_reply" msgid="6015299134424685297">"Отговор"</string>
     <string name="notification_action_reply_all" msgid="20020468410400912">"Отговор до всички"</string>
@@ -411,4 +415,5 @@
     <string name="drawer_open" msgid="6074646853178471940">"Отваряне на слоя за навигация"</string>
     <string name="drawer_close" msgid="2764774620737876943">"Затваряне на чекмеджето за навигация"</string>
     <string name="conversation_photo_welcome_text" msgid="4274875219447670662">"Докоснете изображението на подателя, за да изберете съответната кореспонденция."</string>
+    <string name="folder_icon_desc" msgid="1500547397347480618">"Икона на папката"</string>
 </resources>
diff --git a/res/values-ca/strings.xml b/res/values-ca/strings.xml
index 68e7b41..1e16eea 100644
--- a/res/values-ca/strings.xml
+++ b/res/values-ca/strings.xml
@@ -361,6 +361,10 @@
     <item msgid="732746454445519134">"Mostra una conversa més antiga després de suprimir"</item>
     <item msgid="2189929276292165301">"Mostra la llista de converses després de suprimir"</item>
   </string-array>
+    <string name="clear_display_images_whitelist_title" msgid="7120575487854245735">"Esborra les aprovacions d\'imatges"</string>
+    <string name="clear_display_images_whitelist_dialog_title" msgid="3190704164490442683">"Vols esborrar les aprovacions d\'imatges?"</string>
+    <string name="clear_display_images_whitelist_dialog_message" msgid="1169152185612117654">"Deixa de mostrar les imatges en línia de remitents que havies permès anteriorment."</string>
+    <string name="sender_whitelist_cleared" msgid="917434007919176024">"Les imatges no es mostraran de manera automàtica."</string>
     <string name="whats_new_dialog_title" msgid="4230806739326698666">"Novetats"</string>
     <string name="notification_action_reply" msgid="6015299134424685297">"Respon"</string>
     <string name="notification_action_reply_all" msgid="20020468410400912">"Respon a tots"</string>
@@ -411,4 +415,5 @@
     <string name="drawer_open" msgid="6074646853178471940">"Obre el tauler de navegació"</string>
     <string name="drawer_close" msgid="2764774620737876943">"Tanca el tauler de navegació"</string>
     <string name="conversation_photo_welcome_text" msgid="4274875219447670662">"Toca la imatge d\'un remitent per seleccionar-ne la conversa."</string>
+    <string name="folder_icon_desc" msgid="1500547397347480618">"Icona de la carpeta"</string>
 </resources>
diff --git a/res/values-cs/strings.xml b/res/values-cs/strings.xml
index 9952a1c..6095064 100644
--- a/res/values-cs/strings.xml
+++ b/res/values-cs/strings.xml
@@ -361,6 +361,10 @@
     <item msgid="732746454445519134">"Po smazání zobrazit starší konverzaci"</item>
     <item msgid="2189929276292165301">"Po smazání zobrazit seznam konverzací"</item>
   </string-array>
+    <string name="clear_display_images_whitelist_title" msgid="7120575487854245735">"Vymazat schválení obrázků"</string>
+    <string name="clear_display_images_whitelist_dialog_title" msgid="3190704164490442683">"Vymazat schválení obrázků?"</string>
+    <string name="clear_display_images_whitelist_dialog_message" msgid="1169152185612117654">"Přestat zobrazovat vložené obrázky od odesílatelů, u kterých jste to dříve povolili."</string>
+    <string name="sender_whitelist_cleared" msgid="917434007919176024">"Obrázky se nebudou zobrazovat automaticky."</string>
     <string name="whats_new_dialog_title" msgid="4230806739326698666">"Novinky"</string>
     <string name="notification_action_reply" msgid="6015299134424685297">"Odpovědět"</string>
     <string name="notification_action_reply_all" msgid="20020468410400912">"Odpovědět všem"</string>
@@ -411,4 +415,5 @@
     <string name="drawer_open" msgid="6074646853178471940">"Otevřít navigační složku"</string>
     <string name="drawer_close" msgid="2764774620737876943">"Zavřít navigační složku"</string>
     <string name="conversation_photo_welcome_text" msgid="4274875219447670662">"Klepnutím na obrázek odesílatele vyberete příslušnou konverzaci."</string>
+    <string name="folder_icon_desc" msgid="1500547397347480618">"Ikona složky"</string>
 </resources>
diff --git a/res/values-da/strings.xml b/res/values-da/strings.xml
index 4f23b4c..c6d826b 100644
--- a/res/values-da/strings.xml
+++ b/res/values-da/strings.xml
@@ -361,6 +361,10 @@
     <item msgid="732746454445519134">"Vis ældre samtale efter sletning"</item>
     <item msgid="2189929276292165301">"Vis samtalelisten efter sletning"</item>
   </string-array>
+    <string name="clear_display_images_whitelist_title" msgid="7120575487854245735">"Ryd billedgodkendelserne"</string>
+    <string name="clear_display_images_whitelist_dialog_title" msgid="3190704164490442683">"Vil du rydde billedgodkendelserne?"</string>
+    <string name="clear_display_images_whitelist_dialog_message" msgid="1169152185612117654">"Stop visning af indlejrede billeder i tekst fra afsendere, du tidligere har tilladt."</string>
+    <string name="sender_whitelist_cleared" msgid="917434007919176024">"Billeder vil ikke blive vist automatisk."</string>
     <string name="whats_new_dialog_title" msgid="4230806739326698666">"Nyheder"</string>
     <string name="notification_action_reply" msgid="6015299134424685297">"Svar"</string>
     <string name="notification_action_reply_all" msgid="20020468410400912">"Svar alle"</string>
@@ -411,4 +415,5 @@
     <string name="drawer_open" msgid="6074646853178471940">"Åbn navigationsskuffen"</string>
     <string name="drawer_close" msgid="2764774620737876943">"Luk navigationsskuffen"</string>
     <string name="conversation_photo_welcome_text" msgid="4274875219447670662">"Tryk på et afsenderbillede for at vælge den samtale."</string>
+    <string name="folder_icon_desc" msgid="1500547397347480618">"Mappeikon"</string>
 </resources>
diff --git a/res/values-de/strings.xml b/res/values-de/strings.xml
index f7d2bdb..4860541 100644
--- a/res/values-de/strings.xml
+++ b/res/values-de/strings.xml
@@ -361,6 +361,10 @@
     <item msgid="732746454445519134">"Nach dem Löschen ältere Konversation anzeigen"</item>
     <item msgid="2189929276292165301">"Nach dem Löschen Konversationsliste anzeigen"</item>
   </string-array>
+    <string name="clear_display_images_whitelist_title" msgid="7120575487854245735">"Bildgenehmigungen löschen"</string>
+    <string name="clear_display_images_whitelist_dialog_title" msgid="3190704164490442683">"Bildgenehmigungen löschen?"</string>
+    <string name="clear_display_images_whitelist_dialog_message" msgid="1169152185612117654">"Keine Inline-Bilder mehr von zuvor zugelassenen Absendern anzeigen"</string>
+    <string name="sender_whitelist_cleared" msgid="917434007919176024">"Bilder werden nicht automatisch angezeigt."</string>
     <string name="whats_new_dialog_title" msgid="4230806739326698666">"Neue Funktionen"</string>
     <string name="notification_action_reply" msgid="6015299134424685297">"Antworten"</string>
     <string name="notification_action_reply_all" msgid="20020468410400912">"Allen antworten"</string>
@@ -411,4 +415,5 @@
     <string name="drawer_open" msgid="6074646853178471940">"Navigationsleiste öffnen"</string>
     <string name="drawer_close" msgid="2764774620737876943">"Navigationsleiste schließen"</string>
     <string name="conversation_photo_welcome_text" msgid="4274875219447670662">"Auf Absenderbild tippen, um die entsprechende Konversation auszuwählen"</string>
+    <string name="folder_icon_desc" msgid="1500547397347480618">"Ordnersymbol"</string>
 </resources>
diff --git a/res/values-el/strings.xml b/res/values-el/strings.xml
index 73dd1cb..e960002 100644
--- a/res/values-el/strings.xml
+++ b/res/values-el/strings.xml
@@ -361,6 +361,10 @@
     <item msgid="732746454445519134">"Εμφάνιση παλαιότερης συνομιλίας μετά τη διαγραφή"</item>
     <item msgid="2189929276292165301">"Εμφάνιση λίστας συνομιλιών μετά τη διαγραφή"</item>
   </string-array>
+    <string name="clear_display_images_whitelist_title" msgid="7120575487854245735">"Διαγραφή εγκρίσεων εικόνας"</string>
+    <string name="clear_display_images_whitelist_dialog_title" msgid="3190704164490442683">"Να διαγραφούν οι εγκρίσεις εικόνας;"</string>
+    <string name="clear_display_images_whitelist_dialog_message" msgid="1169152185612117654">"Διακόψτε την εμφάνιση ενσωματωμένων εικόνων από αποστολείς που έχετε προηγουμένως επιτρέψει."</string>
+    <string name="sender_whitelist_cleared" msgid="917434007919176024">"Οι εικόνες δεν θα εμφανίζονται αυτόματα."</string>
     <string name="whats_new_dialog_title" msgid="4230806739326698666">"Τι νέο υπάρχει"</string>
     <string name="notification_action_reply" msgid="6015299134424685297">"Απάντηση"</string>
     <string name="notification_action_reply_all" msgid="20020468410400912">"Απάντηση σε όλους"</string>
@@ -411,4 +415,5 @@
     <string name="drawer_open" msgid="6074646853178471940">"Άνοιγμα συρταριού πλοήγησης"</string>
     <string name="drawer_close" msgid="2764774620737876943">"Κλείσιμο συρταριού πλοήγησης"</string>
     <string name="conversation_photo_welcome_text" msgid="4274875219447670662">"Αγγίξτε την εικόνα ενός αποστολέα για να επιλέξετε αυτήν τη συνομιλία."</string>
+    <string name="folder_icon_desc" msgid="1500547397347480618">"Εικονίδιο φακέλου"</string>
 </resources>
diff --git a/res/values-en-rGB/strings.xml b/res/values-en-rGB/strings.xml
index 8b24006..910f42f 100644
--- a/res/values-en-rGB/strings.xml
+++ b/res/values-en-rGB/strings.xml
@@ -361,6 +361,10 @@
     <item msgid="732746454445519134">"Show older conversation after you delete"</item>
     <item msgid="2189929276292165301">"Show conversation list after you delete"</item>
   </string-array>
+    <string name="clear_display_images_whitelist_title" msgid="7120575487854245735">"Clear picture approvals"</string>
+    <string name="clear_display_images_whitelist_dialog_title" msgid="3190704164490442683">"Clear picture approvals?"</string>
+    <string name="clear_display_images_whitelist_dialog_message" msgid="1169152185612117654">"Stop displaying inline images from senders that you previously allowed."</string>
+    <string name="sender_whitelist_cleared" msgid="917434007919176024">"Pictures won\'t be shown automatically."</string>
     <string name="whats_new_dialog_title" msgid="4230806739326698666">"What\'s new"</string>
     <string name="notification_action_reply" msgid="6015299134424685297">"Reply"</string>
     <string name="notification_action_reply_all" msgid="20020468410400912">"Reply all"</string>
@@ -411,4 +415,5 @@
     <string name="drawer_open" msgid="6074646853178471940">"Open navigation drawer"</string>
     <string name="drawer_close" msgid="2764774620737876943">"Close navigation drawer"</string>
     <string name="conversation_photo_welcome_text" msgid="4274875219447670662">"Touch a sender image to select that conversation."</string>
+    <string name="folder_icon_desc" msgid="1500547397347480618">"Folder icon"</string>
 </resources>
diff --git a/res/values-es-rUS/strings.xml b/res/values-es-rUS/strings.xml
index 90c6d49..61c5393 100644
--- a/res/values-es-rUS/strings.xml
+++ b/res/values-es-rUS/strings.xml
@@ -361,6 +361,10 @@
     <item msgid="732746454445519134">"Mostrar la conversación más antigua luego de la eliminación"</item>
     <item msgid="2189929276292165301">"Mostrar la lista de conversaciones luego de la eliminación"</item>
   </string-array>
+    <string name="clear_display_images_whitelist_title" msgid="7120575487854245735">"Borrar aprobaciones de imágenes"</string>
+    <string name="clear_display_images_whitelist_dialog_title" msgid="3190704164490442683">"¿Borrar aprobaciones de imágenes?"</string>
+    <string name="clear_display_images_whitelist_dialog_message" msgid="1169152185612117654">"Dejar de mostrar imágenes intercaladas de remitentes que permitiste anteriormente"</string>
+    <string name="sender_whitelist_cleared" msgid="917434007919176024">"Las imágenes no se mostrarán automáticamente."</string>
     <string name="whats_new_dialog_title" msgid="4230806739326698666">"Novedades"</string>
     <string name="notification_action_reply" msgid="6015299134424685297">"Responder"</string>
     <string name="notification_action_reply_all" msgid="20020468410400912">"Responder a todos"</string>
@@ -411,4 +415,5 @@
     <string name="drawer_open" msgid="6074646853178471940">"Abrir panel de navegación"</string>
     <string name="drawer_close" msgid="2764774620737876943">"Cerrar panel de navegación"</string>
     <string name="conversation_photo_welcome_text" msgid="4274875219447670662">"Toca la imagen de un remitente para seleccionar esa conversación."</string>
+    <string name="folder_icon_desc" msgid="1500547397347480618">"Ícono de carpeta"</string>
 </resources>
diff --git a/res/values-es/strings.xml b/res/values-es/strings.xml
index 7cb2cfd..1afd9e9 100644
--- a/res/values-es/strings.xml
+++ b/res/values-es/strings.xml
@@ -361,6 +361,10 @@
     <item msgid="732746454445519134">"Mostrar conversación más antigua después de eliminar"</item>
     <item msgid="2189929276292165301">"Mostrar lista de conversaciones después de eliminar"</item>
   </string-array>
+    <string name="clear_display_images_whitelist_title" msgid="7120575487854245735">"Borrar aprobaciones de imágenes"</string>
+    <string name="clear_display_images_whitelist_dialog_title" msgid="3190704164490442683">"¿Borrar aprobaciones de imágenes?"</string>
+    <string name="clear_display_images_whitelist_dialog_message" msgid="1169152185612117654">"Dejar de mostrar imágenes entre líneas de remitentes que se hayan permitido anteriormente."</string>
+    <string name="sender_whitelist_cleared" msgid="917434007919176024">"Las imágenes de los mensajes no se mostrarán automáticamente."</string>
     <string name="whats_new_dialog_title" msgid="4230806739326698666">"Novedades"</string>
     <string name="notification_action_reply" msgid="6015299134424685297">"Responder"</string>
     <string name="notification_action_reply_all" msgid="20020468410400912">"Responder a todos"</string>
@@ -411,4 +415,5 @@
     <string name="drawer_open" msgid="6074646853178471940">"Abrir control de navegación"</string>
     <string name="drawer_close" msgid="2764774620737876943">"Cerrar control de navegación"</string>
     <string name="conversation_photo_welcome_text" msgid="4274875219447670662">"Toca la imagen de un remitente para seleccionar esa conversación."</string>
+    <string name="folder_icon_desc" msgid="1500547397347480618">"Icono de carpeta"</string>
 </resources>
diff --git a/res/values-et/strings.xml b/res/values-et/strings.xml
index 2d77c16..ae4ca29 100644
--- a/res/values-et/strings.xml
+++ b/res/values-et/strings.xml
@@ -361,6 +361,10 @@
     <item msgid="732746454445519134">"Vanema vestluse näitamine pärast kustutamist"</item>
     <item msgid="2189929276292165301">"Vestlusloendi näitamine pärast kustutamist"</item>
   </string-array>
+    <string name="clear_display_images_whitelist_title" msgid="7120575487854245735">"Pildilubade kustutamine"</string>
+    <string name="clear_display_images_whitelist_dialog_title" msgid="3190704164490442683">"Kas kustutada pildiload?"</string>
+    <string name="clear_display_images_whitelist_dialog_message" msgid="1169152185612117654">"Varasemalt lubatud saatjate tekstisiseste piltide kuvamise peatamine."</string>
+    <string name="sender_whitelist_cleared" msgid="917434007919176024">"Pilte ei kuvata automaatselt."</string>
     <string name="whats_new_dialog_title" msgid="4230806739326698666">"Mis on uut?"</string>
     <string name="notification_action_reply" msgid="6015299134424685297">"Vasta"</string>
     <string name="notification_action_reply_all" msgid="20020468410400912">"Vasta kõigile"</string>
@@ -411,4 +415,5 @@
     <string name="drawer_open" msgid="6074646853178471940">"Ava navigeerimissahtel"</string>
     <string name="drawer_close" msgid="2764774620737876943">"Sule navigeerimissahtel"</string>
     <string name="conversation_photo_welcome_text" msgid="4274875219447670662">"Vestluse valimiseks puudutage saatja kujutist."</string>
+    <string name="folder_icon_desc" msgid="1500547397347480618">"Kausta ikoon"</string>
 </resources>
diff --git a/res/values-fa/strings.xml b/res/values-fa/strings.xml
index ddfd0c4..c306541 100644
--- a/res/values-fa/strings.xml
+++ b/res/values-fa/strings.xml
@@ -361,6 +361,10 @@
     <item msgid="732746454445519134">"بعد از حذف مکالمه قدیمی‌تر را نشان می‌دهد"</item>
     <item msgid="2189929276292165301">"بعد از حذف فهرست مکالمات را نشان می‌دهد"</item>
   </string-array>
+    <string name="clear_display_images_whitelist_title" msgid="7120575487854245735">"پاک کردن تأییدیه‌های مربوط به عکس"</string>
+    <string name="clear_display_images_whitelist_dialog_title" msgid="3190704164490442683">"تأییدیه‌های مربوط به عکس پاک شود؟"</string>
+    <string name="clear_display_images_whitelist_dialog_message" msgid="1169152185612117654">"توقف نمایش تصاویر درون برنامه‌ای از فرستندگانی که قبلاً اجازه داده‌ بودید."</string>
+    <string name="sender_whitelist_cleared" msgid="917434007919176024">"عکس‌ها به صورت خودکار نشان داده نخواهند شد."</string>
     <string name="whats_new_dialog_title" msgid="4230806739326698666">"ویژگی جدید چیست"</string>
     <string name="notification_action_reply" msgid="6015299134424685297">"پاسخ"</string>
     <string name="notification_action_reply_all" msgid="20020468410400912">"پاسخ به همه"</string>
@@ -411,4 +415,5 @@
     <string name="drawer_open" msgid="6074646853178471940">"باز کردن کشوی پیمایش"</string>
     <string name="drawer_close" msgid="2764774620737876943">"بستن کشوی پیمایش"</string>
     <string name="conversation_photo_welcome_text" msgid="4274875219447670662">"برای انتخاب مکالمه، تصویر فرستنده را لمس کنید."</string>
+    <string name="folder_icon_desc" msgid="1500547397347480618">"نماد پوشه"</string>
 </resources>
diff --git a/res/values-fi/strings.xml b/res/values-fi/strings.xml
index 3312583..ad9bc7f 100644
--- a/res/values-fi/strings.xml
+++ b/res/values-fi/strings.xml
@@ -361,6 +361,10 @@
     <item msgid="732746454445519134">"Näytä vanhempi keskustelu poistamisen jälkeen"</item>
     <item msgid="2189929276292165301">"Näytä keskusteluluettelo poistamisen jälkeen"</item>
   </string-array>
+    <string name="clear_display_images_whitelist_title" msgid="7120575487854245735">"Tyhjennä kuvien näyttöluvat"</string>
+    <string name="clear_display_images_whitelist_dialog_title" msgid="3190704164490442683">"Tyhjennetäänkö kuvien näyttöluvat?"</string>
+    <string name="clear_display_images_whitelist_dialog_message" msgid="1169152185612117654">"Lopeta upotettujen kuvien näyttäminen aiemmin sallituilta lähettäjiltä."</string>
+    <string name="sender_whitelist_cleared" msgid="917434007919176024">"Kuvia ei näytetä automaattisesti."</string>
     <string name="whats_new_dialog_title" msgid="4230806739326698666">"Uutuudet"</string>
     <string name="notification_action_reply" msgid="6015299134424685297">"Vastaa"</string>
     <string name="notification_action_reply_all" msgid="20020468410400912">"Vastaa kaikille"</string>
@@ -411,4 +415,5 @@
     <string name="drawer_open" msgid="6074646853178471940">"Avaa navigointipalkki"</string>
     <string name="drawer_close" msgid="2764774620737876943">"Sulje navigointipalkki"</string>
     <string name="conversation_photo_welcome_text" msgid="4274875219447670662">"Valitse keskustelu koskettamalla lähettäjän kuvaa."</string>
+    <string name="folder_icon_desc" msgid="1500547397347480618">"Kansiokuvake"</string>
 </resources>
diff --git a/res/values-fr/strings.xml b/res/values-fr/strings.xml
index f3e7db2..f7fcc5e 100644
--- a/res/values-fr/strings.xml
+++ b/res/values-fr/strings.xml
@@ -361,6 +361,10 @@
     <item msgid="732746454445519134">"Afficher les conversations plus anciennes après suppression"</item>
     <item msgid="2189929276292165301">"Afficher la liste des conversations après suppression"</item>
   </string-array>
+    <string name="clear_display_images_whitelist_title" msgid="7120575487854245735">"Supprimer les autorisations liées aux images"</string>
+    <string name="clear_display_images_whitelist_dialog_title" msgid="3190704164490442683">"Supprimer les autorisations liées aux images ?"</string>
+    <string name="clear_display_images_whitelist_dialog_message" msgid="1169152185612117654">"Les images intégrées ne seront plus affichées pour les expéditeurs que vous aviez autorisés."</string>
+    <string name="sender_whitelist_cleared" msgid="917434007919176024">"Les images ne s\'afficheront pas automatiquement."</string>
     <string name="whats_new_dialog_title" msgid="4230806739326698666">"Nouveautés"</string>
     <string name="notification_action_reply" msgid="6015299134424685297">"Répondre"</string>
     <string name="notification_action_reply_all" msgid="20020468410400912">"Répondre à tous"</string>
@@ -411,4 +415,5 @@
     <string name="drawer_open" msgid="6074646853178471940">"Ouvrir le panneau de navigation"</string>
     <string name="drawer_close" msgid="2764774620737876943">"Fermer le panneau de navigation"</string>
     <string name="conversation_photo_welcome_text" msgid="4274875219447670662">"Appuyez sur l\'image d\'un expéditeur pour sélectionner cette conversation."</string>
+    <string name="folder_icon_desc" msgid="1500547397347480618">"Icône Dossier"</string>
 </resources>
diff --git a/res/values-hi/strings.xml b/res/values-hi/strings.xml
index 240eb18..20b5e21 100644
--- a/res/values-hi/strings.xml
+++ b/res/values-hi/strings.xml
@@ -361,6 +361,10 @@
     <item msgid="732746454445519134">"आपके द्वारा हटाए जाने के बाद पुराना वार्तालाप दिखाएं"</item>
     <item msgid="2189929276292165301">"आपके द्वारा हटाए जाने के बाद वार्तालाप सूची दिखाएं"</item>
   </string-array>
+    <string name="clear_display_images_whitelist_title" msgid="7120575487854245735">"चित्र स्वीकृतियां साफ़ करें"</string>
+    <string name="clear_display_images_whitelist_dialog_title" msgid="3190704164490442683">"चित्र स्वीकृतियां साफ़ करें?"</string>
+    <string name="clear_display_images_whitelist_dialog_message" msgid="1169152185612117654">"आपने जिन प्रेषकों को पहले अनुमति दे रखी है, उनके इनलाइन चित्रों का प्रदर्शन रोकें."</string>
+    <string name="sender_whitelist_cleared" msgid="917434007919176024">"चित्र अपने आप नहीं दिखाए जाएंगे."</string>
     <string name="whats_new_dialog_title" msgid="4230806739326698666">"नया क्या है"</string>
     <string name="notification_action_reply" msgid="6015299134424685297">"जवाब दें"</string>
     <string name="notification_action_reply_all" msgid="20020468410400912">"सभी को जवाब दें"</string>
@@ -411,4 +415,5 @@
     <string name="drawer_open" msgid="6074646853178471940">"नेविगेशन ड्रॉवर खोलें"</string>
     <string name="drawer_close" msgid="2764774620737876943">"नेविगेशन ड्रॉवर बंद करें"</string>
     <string name="conversation_photo_welcome_text" msgid="4274875219447670662">"उस बातचीत को चुनने के लिए प्रेषक के चित्र को स्पर्श करें."</string>
+    <string name="folder_icon_desc" msgid="1500547397347480618">"फ़ोल्डर आइकन"</string>
 </resources>
diff --git a/res/values-hr/strings.xml b/res/values-hr/strings.xml
index 45be6b4..df50ceb 100644
--- a/res/values-hr/strings.xml
+++ b/res/values-hr/strings.xml
@@ -361,6 +361,10 @@
     <item msgid="732746454445519134">"Prikaz starijeg razgovora nakon brisanja"</item>
     <item msgid="2189929276292165301">"Prikaz popisa razgovora nakon brisanja"</item>
   </string-array>
+    <string name="clear_display_images_whitelist_title" msgid="7120575487854245735">"Brisanje odobrenja za slike"</string>
+    <string name="clear_display_images_whitelist_dialog_title" msgid="3190704164490442683">"Želite li izbrisati odobrenja za slike?"</string>
+    <string name="clear_display_images_whitelist_dialog_message" msgid="1169152185612117654">"Prestani prikazivati slike u tekstu od pošiljatelja koji su prethodno bili dopušteni."</string>
+    <string name="sender_whitelist_cleared" msgid="917434007919176024">"Slike se neće prikazivati automatski."</string>
     <string name="whats_new_dialog_title" msgid="4230806739326698666">"Što je novo"</string>
     <string name="notification_action_reply" msgid="6015299134424685297">"Odgovori"</string>
     <string name="notification_action_reply_all" msgid="20020468410400912">"Odgovori svima"</string>
@@ -411,4 +415,5 @@
     <string name="drawer_open" msgid="6074646853178471940">"Otvaranje pretinca za navigaciju"</string>
     <string name="drawer_close" msgid="2764774620737876943">"Zatvaranje pretinca za navigaciju"</string>
     <string name="conversation_photo_welcome_text" msgid="4274875219447670662">"Dodirnite sliku pošiljatelja da biste odabrali taj razgovor."</string>
+    <string name="folder_icon_desc" msgid="1500547397347480618">"Ikona mape"</string>
 </resources>
diff --git a/res/values-hu/strings.xml b/res/values-hu/strings.xml
index bc65da1..5e0bcff 100644
--- a/res/values-hu/strings.xml
+++ b/res/values-hu/strings.xml
@@ -361,6 +361,10 @@
     <item msgid="732746454445519134">"Törlés után a korábbi beszélgetés megjelenítése"</item>
     <item msgid="2189929276292165301">"Törlés után a beszélgetéslista megjelenítése"</item>
   </string-array>
+    <string name="clear_display_images_whitelist_title" msgid="7120575487854245735">"Képengedélyek törlése"</string>
+    <string name="clear_display_images_whitelist_dialog_title" msgid="3190704164490442683">"Törli a képengedélyeket?"</string>
+    <string name="clear_display_images_whitelist_dialog_message" msgid="1169152185612117654">"Ne jelenítsen meg beágyazott képeket olyan feladóktól, akiktől ezt korábban engedélyezte."</string>
+    <string name="sender_whitelist_cleared" msgid="917434007919176024">"A képek nem jelennek meg automatikusan."</string>
     <string name="whats_new_dialog_title" msgid="4230806739326698666">"Újdonságok"</string>
     <string name="notification_action_reply" msgid="6015299134424685297">"Válasz"</string>
     <string name="notification_action_reply_all" msgid="20020468410400912">"Válasz mindenkinek"</string>
@@ -411,4 +415,5 @@
     <string name="drawer_open" msgid="6074646853178471940">"Navigációs fiók kinyitása"</string>
     <string name="drawer_close" msgid="2764774620737876943">"Navigációs fiók bezárása"</string>
     <string name="conversation_photo_welcome_text" msgid="4274875219447670662">"A beszélgetés kiválasztásához érintse meg a küldő képét."</string>
+    <string name="folder_icon_desc" msgid="1500547397347480618">"Mappa ikon"</string>
 </resources>
diff --git a/res/values-in/strings.xml b/res/values-in/strings.xml
index da5705d..6caf6db 100644
--- a/res/values-in/strings.xml
+++ b/res/values-in/strings.xml
@@ -361,6 +361,10 @@
     <item msgid="732746454445519134">"Tampilkan percakapan lama setelah menghapus"</item>
     <item msgid="2189929276292165301">"Tampilkan daftar percakapan setelah menghapus"</item>
   </string-array>
+    <string name="clear_display_images_whitelist_title" msgid="7120575487854245735">"Hapus persetujuan gambar"</string>
+    <string name="clear_display_images_whitelist_dialog_title" msgid="3190704164490442683">"Hapus persetujuan gambar?"</string>
+    <string name="clear_display_images_whitelist_dialog_message" msgid="1169152185612117654">"Berhenti menampilkan gambar sebaris dari pengirim yang sebelumnya Anda izinkan."</string>
+    <string name="sender_whitelist_cleared" msgid="917434007919176024">"Gambar tidak akan ditampilkan secara otomatis."</string>
     <string name="whats_new_dialog_title" msgid="4230806739326698666">"Yang baru"</string>
     <string name="notification_action_reply" msgid="6015299134424685297">"Balas"</string>
     <string name="notification_action_reply_all" msgid="20020468410400912">"Balas ke semua"</string>
@@ -411,4 +415,5 @@
     <string name="drawer_open" msgid="6074646853178471940">"Buka laci navigasi"</string>
     <string name="drawer_close" msgid="2764774620737876943">"Tutup laci navigasi"</string>
     <string name="conversation_photo_welcome_text" msgid="4274875219447670662">"Sentuh gambar pengirim untuk memilih percakapan tersebut."</string>
+    <string name="folder_icon_desc" msgid="1500547397347480618">"Ikon folder"</string>
 </resources>
diff --git a/res/values-it/strings.xml b/res/values-it/strings.xml
index 2e8f7ec..0584bbe 100644
--- a/res/values-it/strings.xml
+++ b/res/values-it/strings.xml
@@ -361,6 +361,10 @@
     <item msgid="732746454445519134">"Mostra conversazione meno recente dopo eliminazione"</item>
     <item msgid="2189929276292165301">"Mostra elenco conversazioni dopo eliminazione"</item>
   </string-array>
+    <string name="clear_display_images_whitelist_title" msgid="7120575487854245735">"Cancella approvazioni immagini"</string>
+    <string name="clear_display_images_whitelist_dialog_title" msgid="3190704164490442683">"Cancellare le approvazioni per le immagini?"</string>
+    <string name="clear_display_images_whitelist_dialog_message" msgid="1169152185612117654">"Smetti di visualizzare immagini incorporate da mittenti in precedenza consentiti."</string>
+    <string name="sender_whitelist_cleared" msgid="917434007919176024">"Le immagini non saranno mostrate automaticamente."</string>
     <string name="whats_new_dialog_title" msgid="4230806739326698666">"Novità"</string>
     <string name="notification_action_reply" msgid="6015299134424685297">"Rispondi"</string>
     <string name="notification_action_reply_all" msgid="20020468410400912">"Rispondi a tutti"</string>
@@ -411,4 +415,5 @@
     <string name="drawer_open" msgid="6074646853178471940">"Apri riquadro di navigazione"</string>
     <string name="drawer_close" msgid="2764774620737876943">"Chiudi riquadro di navigazione"</string>
     <string name="conversation_photo_welcome_text" msgid="4274875219447670662">"Tocca l\'immagine di un mittente per selezionare tale conversazione."</string>
+    <string name="folder_icon_desc" msgid="1500547397347480618">"Icona cartella"</string>
 </resources>
diff --git a/res/values-iw/strings.xml b/res/values-iw/strings.xml
index 5f90ef9..b99362e 100644
--- a/res/values-iw/strings.xml
+++ b/res/values-iw/strings.xml
@@ -361,6 +361,10 @@
     <item msgid="732746454445519134">"הצג שיחה ישנה יותר לאחר ביצוע מחיקה"</item>
     <item msgid="2189929276292165301">"הצג רשימת שיחות לאחר ביצוע מחיקה"</item>
   </string-array>
+    <string name="clear_display_images_whitelist_title" msgid="7120575487854245735">"ניקוי של אישורי תמונות"</string>
+    <string name="clear_display_images_whitelist_dialog_title" msgid="3190704164490442683">"האם לנקות אישורי תמונות?"</string>
+    <string name="clear_display_images_whitelist_dialog_message" msgid="1169152185612117654">"הפסק להציג תמונות מוטבעות משולחים שהתרת זאת עבורם בעבר."</string>
+    <string name="sender_whitelist_cleared" msgid="917434007919176024">"תמונות לא יוצגו באופן אוטומטי."</string>
     <string name="whats_new_dialog_title" msgid="4230806739326698666">"מה חדש"</string>
     <string name="notification_action_reply" msgid="6015299134424685297">"השב"</string>
     <string name="notification_action_reply_all" msgid="20020468410400912">"השב לכולם"</string>
@@ -411,4 +415,5 @@
     <string name="drawer_open" msgid="6074646853178471940">"פתח את חלונית הניווט"</string>
     <string name="drawer_close" msgid="2764774620737876943">"סגור את חלונית הניווט"</string>
     <string name="conversation_photo_welcome_text" msgid="4274875219447670662">"גע בתמונת שולח כדי לבחור בשיחה."</string>
+    <string name="folder_icon_desc" msgid="1500547397347480618">"סמל תיקיה"</string>
 </resources>
diff --git a/res/values-ja/strings.xml b/res/values-ja/strings.xml
index 4b76dda..3338a6f 100644
--- a/res/values-ja/strings.xml
+++ b/res/values-ja/strings.xml
@@ -361,6 +361,10 @@
     <item msgid="732746454445519134">"削除後に古いスレッドを表示する"</item>
     <item msgid="2189929276292165301">"削除後にスレッドリストを表示する"</item>
   </string-array>
+    <string name="clear_display_images_whitelist_title" msgid="7120575487854245735">"写真の許可の取り消し"</string>
+    <string name="clear_display_images_whitelist_dialog_title" msgid="3190704164490442683">"写真の許可を取り消しますか?"</string>
+    <string name="clear_display_images_whitelist_dialog_message" msgid="1169152185612117654">"以前許可した送信者からのインライン画像の表示を停止します。"</string>
+    <string name="sender_whitelist_cleared" msgid="917434007919176024">"画像が自動的に表示されることはありません。"</string>
     <string name="whats_new_dialog_title" msgid="4230806739326698666">"新機能"</string>
     <string name="notification_action_reply" msgid="6015299134424685297">"返信"</string>
     <string name="notification_action_reply_all" msgid="20020468410400912">"全員に返信"</string>
@@ -411,4 +415,5 @@
     <string name="drawer_open" msgid="6074646853178471940">"ナビゲーションドロワーを開く"</string>
     <string name="drawer_close" msgid="2764774620737876943">"ナビゲーションドロワーを閉じる"</string>
     <string name="conversation_photo_welcome_text" msgid="4274875219447670662">"スレッドを選択するには送信者画像をタップしてください。"</string>
+    <string name="folder_icon_desc" msgid="1500547397347480618">"フォルダアイコン"</string>
 </resources>
diff --git a/res/values-ko/strings.xml b/res/values-ko/strings.xml
index d3b0d84..42792b4 100644
--- a/res/values-ko/strings.xml
+++ b/res/values-ko/strings.xml
@@ -361,6 +361,10 @@
     <item msgid="732746454445519134">"삭제 후 이전 대화 표시"</item>
     <item msgid="2189929276292165301">"삭제 후 대화 목록 표시"</item>
   </string-array>
+    <string name="clear_display_images_whitelist_title" msgid="7120575487854245735">"이미지 승인 취소"</string>
+    <string name="clear_display_images_whitelist_dialog_title" msgid="3190704164490442683">"이미지 승인을 취소하시겠습니까?"</string>
+    <string name="clear_display_images_whitelist_dialog_message" msgid="1169152185612117654">"이전에 이미지 표시를 허용했던 발신자가 보낸 이메일의 이미지를 표시하지 않습니다."</string>
+    <string name="sender_whitelist_cleared" msgid="917434007919176024">"사진이 자동으로 표시되지 않습니다."</string>
     <string name="whats_new_dialog_title" msgid="4230806739326698666">"새로운 기능"</string>
     <string name="notification_action_reply" msgid="6015299134424685297">"답장"</string>
     <string name="notification_action_reply_all" msgid="20020468410400912">"전체답장"</string>
@@ -411,4 +415,5 @@
     <string name="drawer_open" msgid="6074646853178471940">"내비게이션 폴더 열기"</string>
     <string name="drawer_close" msgid="2764774620737876943">"내비게이션 폴더 닫기"</string>
     <string name="conversation_photo_welcome_text" msgid="4274875219447670662">"대화를 선택하려면 발신자 이미지를 터치합니다."</string>
+    <string name="folder_icon_desc" msgid="1500547397347480618">"폴더 아이콘"</string>
 </resources>
diff --git a/res/values-lt/strings.xml b/res/values-lt/strings.xml
index 2acdff0..c38c4a3 100644
--- a/res/values-lt/strings.xml
+++ b/res/values-lt/strings.xml
@@ -361,6 +361,10 @@
     <item msgid="732746454445519134">"Ištrynus rodyti senesnį pokalbį"</item>
     <item msgid="2189929276292165301">"Ištrynus rodyti pokalbių sąrašą"</item>
   </string-array>
+    <string name="clear_display_images_whitelist_title" msgid="7120575487854245735">"Išvalyti nuotraukų patvirtinimus"</string>
+    <string name="clear_display_images_whitelist_dialog_title" msgid="3190704164490442683">"Išvalyti nuotraukų patvirtinimus?"</string>
+    <string name="clear_display_images_whitelist_dialog_message" msgid="1169152185612117654">"Nebepateikti įterptųjų vaizdų iš anksčiau leistų siuntėjų."</string>
+    <string name="sender_whitelist_cleared" msgid="917434007919176024">"Nuotraukos nebus rodomos automatiškai."</string>
     <string name="whats_new_dialog_title" msgid="4230806739326698666">"Kas naujo?"</string>
     <string name="notification_action_reply" msgid="6015299134424685297">"Atsakyti"</string>
     <string name="notification_action_reply_all" msgid="20020468410400912">"Atsakyti visiems"</string>
@@ -411,4 +415,5 @@
     <string name="drawer_open" msgid="6074646853178471940">"Atidaryti naršymo juostą"</string>
     <string name="drawer_close" msgid="2764774620737876943">"Uždaryti naršymo juostą"</string>
     <string name="conversation_photo_welcome_text" msgid="4274875219447670662">"Palieskite siuntėjo vaizdą, kad pasirinktumėte tą pokalbį."</string>
+    <string name="folder_icon_desc" msgid="1500547397347480618">"Aplanko piktograma"</string>
 </resources>
diff --git a/res/values-lv/strings.xml b/res/values-lv/strings.xml
index 6828394..70cdeb6 100644
--- a/res/values-lv/strings.xml
+++ b/res/values-lv/strings.xml
@@ -361,6 +361,10 @@
     <item msgid="732746454445519134">"Rādīt vecāku sarunu pēc dzēšanas"</item>
     <item msgid="2189929276292165301">"Rādīt sarunu sarakstu pēc dzēšanas"</item>
   </string-array>
+    <string name="clear_display_images_whitelist_title" msgid="7120575487854245735">"Attēlu rādīšanas apstiprinājumu atcelšana"</string>
+    <string name="clear_display_images_whitelist_dialog_title" msgid="3190704164490442683">"Vai atcelt attēlu rādīšanas apstiprinājumus?"</string>
+    <string name="clear_display_images_whitelist_dialog_message" msgid="1169152185612117654">"Vairs nerādīt iekļautos attēlus no sūtītājiem, kuriem iepriekš tas bija atļauts"</string>
+    <string name="sender_whitelist_cleared" msgid="917434007919176024">"Attēli netiks automātiski rādīti."</string>
     <string name="whats_new_dialog_title" msgid="4230806739326698666">"Jaunumi"</string>
     <string name="notification_action_reply" msgid="6015299134424685297">"Atbildēt"</string>
     <string name="notification_action_reply_all" msgid="20020468410400912">"Atbildēt visiem"</string>
@@ -411,4 +415,5 @@
     <string name="drawer_open" msgid="6074646853178471940">"Atvērt navigācijas paneli"</string>
     <string name="drawer_close" msgid="2764774620737876943">"Aizvērt navigācijas paneli"</string>
     <string name="conversation_photo_welcome_text" msgid="4274875219447670662">"Pieskarieties sūtītāja attēlam, lai atlasītu šo sarunu."</string>
+    <string name="folder_icon_desc" msgid="1500547397347480618">"Mapes ikona"</string>
 </resources>
diff --git a/res/values-ms/strings.xml b/res/values-ms/strings.xml
index 45ec3b1..8e74e37 100644
--- a/res/values-ms/strings.xml
+++ b/res/values-ms/strings.xml
@@ -361,6 +361,10 @@
     <item msgid="732746454445519134">"Tunjukkan perbualan yang lebih lama selepas memadamkan"</item>
     <item msgid="2189929276292165301">"Tunjukkan senarai perbualan selepas memadamkan"</item>
   </string-array>
+    <string name="clear_display_images_whitelist_title" msgid="7120575487854245735">"Padam bersih kebenaran gambar"</string>
+    <string name="clear_display_images_whitelist_dialog_title" msgid="3190704164490442683">"Padam bersih kebenaran gambar?"</string>
+    <string name="clear_display_images_whitelist_dialog_message" msgid="1169152185612117654">"Berhenti memaparkan imej sebaris daripada penghantar yang anda benarkan sebelum ini."</string>
+    <string name="sender_whitelist_cleared" msgid="917434007919176024">"Gambar tidak akan ditunjukkan secara automatik."</string>
     <string name="whats_new_dialog_title" msgid="4230806739326698666">"Apa yang baharu"</string>
     <string name="notification_action_reply" msgid="6015299134424685297">"Balas"</string>
     <string name="notification_action_reply_all" msgid="20020468410400912">"Balas kepada semua"</string>
@@ -411,4 +415,5 @@
     <string name="drawer_open" msgid="6074646853178471940">"Buka laci navigasi"</string>
     <string name="drawer_close" msgid="2764774620737876943">"Tutup laci navigasi"</string>
     <string name="conversation_photo_welcome_text" msgid="4274875219447670662">"Sentuh imej penghantar untuk memilih perbualan itu."</string>
+    <string name="folder_icon_desc" msgid="1500547397347480618">"Ikon folder"</string>
 </resources>
diff --git a/res/values-nb/strings.xml b/res/values-nb/strings.xml
index 2360133..2466908 100644
--- a/res/values-nb/strings.xml
+++ b/res/values-nb/strings.xml
@@ -361,6 +361,10 @@
     <item msgid="732746454445519134">"Vis eldre samtaler etter du har slettet"</item>
     <item msgid="2189929276292165301">"Vis samtaleliste etter du har slettet"</item>
   </string-array>
+    <string name="clear_display_images_whitelist_title" msgid="7120575487854245735">"Fjern bildegodkjenninger"</string>
+    <string name="clear_display_images_whitelist_dialog_title" msgid="3190704164490442683">"Vil du fjerne bildegodkjenninger?"</string>
+    <string name="clear_display_images_whitelist_dialog_message" msgid="1169152185612117654">"Slutt å vise innebygde bilder fra avsendere du tidligere har godkjent."</string>
+    <string name="sender_whitelist_cleared" msgid="917434007919176024">"Bilder vises ikke automatisk."</string>
     <string name="whats_new_dialog_title" msgid="4230806739326698666">"Dette er nytt"</string>
     <string name="notification_action_reply" msgid="6015299134424685297">"Svar"</string>
     <string name="notification_action_reply_all" msgid="20020468410400912">"Svar alle"</string>
@@ -411,4 +415,5 @@
     <string name="drawer_open" msgid="6074646853178471940">"Åpne navigasjonsskuffen"</string>
     <string name="drawer_close" msgid="2764774620737876943">"Lukk navigasjonsskuffen"</string>
     <string name="conversation_photo_welcome_text" msgid="4274875219447670662">"Trykk på et avsenderbilde for å velge den samtalen."</string>
+    <string name="folder_icon_desc" msgid="1500547397347480618">"Mappeikon"</string>
 </resources>
diff --git a/res/values-nl/strings.xml b/res/values-nl/strings.xml
index b57268c..6b4abe4 100644
--- a/res/values-nl/strings.xml
+++ b/res/values-nl/strings.xml
@@ -361,6 +361,10 @@
     <item msgid="732746454445519134">"Oudere conversatie weergeven na het verwijderen"</item>
     <item msgid="2189929276292165301">"Conversatielijst weergeven na het verwijderen"</item>
   </string-array>
+    <string name="clear_display_images_whitelist_title" msgid="7120575487854245735">"Goedkeuringen voor foto\'s wissen"</string>
+    <string name="clear_display_images_whitelist_dialog_title" msgid="3190704164490442683">"Goedkeuringen voor foto\'s wissen?"</string>
+    <string name="clear_display_images_whitelist_dialog_message" msgid="1169152185612117654">"Niet langer inline-afbeeldingen weergeven van afzenders die u eerder heeft toegestaan."</string>
+    <string name="sender_whitelist_cleared" msgid="917434007919176024">"Afbeeldingen worden niet automatisch weergegeven."</string>
     <string name="whats_new_dialog_title" msgid="4230806739326698666">"Wat is er nieuw"</string>
     <string name="notification_action_reply" msgid="6015299134424685297">"Beantwoorden"</string>
     <string name="notification_action_reply_all" msgid="20020468410400912">"Allen beantwoorden"</string>
@@ -411,4 +415,5 @@
     <string name="drawer_open" msgid="6074646853178471940">"Navigatielade openen"</string>
     <string name="drawer_close" msgid="2764774620737876943">"Navigatielade sluiten"</string>
     <string name="conversation_photo_welcome_text" msgid="4274875219447670662">"Raak de afbeelding van een afzender aan om dat gesprek te selecteren."</string>
+    <string name="folder_icon_desc" msgid="1500547397347480618">"Mappictogram"</string>
 </resources>
diff --git a/res/values-pl/strings.xml b/res/values-pl/strings.xml
index 0b76f1b..d18d57c 100644
--- a/res/values-pl/strings.xml
+++ b/res/values-pl/strings.xml
@@ -359,6 +359,10 @@
     <item msgid="732746454445519134">"Pokazuj starszą konwersację po usunięciu"</item>
     <item msgid="2189929276292165301">"Pokazuj listę konwersacji po usunięciu"</item>
   </string-array>
+    <string name="clear_display_images_whitelist_title" msgid="7120575487854245735">"Wyłącz pokazywanie obrazów"</string>
+    <string name="clear_display_images_whitelist_dialog_title" msgid="3190704164490442683">"Wyłączyć pokazywanie obrazów?"</string>
+    <string name="clear_display_images_whitelist_dialog_message" msgid="1169152185612117654">"Przestań pokazywać w wiadomościach obrazy od zatwierdzonych wcześniej nadawców."</string>
+    <string name="sender_whitelist_cleared" msgid="917434007919176024">"Obrazy nie będą pokazywane automatycznie."</string>
     <string name="whats_new_dialog_title" msgid="4230806739326698666">"Co nowego"</string>
     <string name="notification_action_reply" msgid="6015299134424685297">"Odpowiedz"</string>
     <string name="notification_action_reply_all" msgid="20020468410400912">"Odpowiedz wszystkim"</string>
@@ -409,4 +413,5 @@
     <string name="drawer_open" msgid="6074646853178471940">"Otwórz szufladę nawigacji"</string>
     <string name="drawer_close" msgid="2764774620737876943">"Zamknij szufladę nawigacji"</string>
     <string name="conversation_photo_welcome_text" msgid="4274875219447670662">"Kliknij zdjęcie nadawcy, by wybrać tę rozmowę."</string>
+    <string name="folder_icon_desc" msgid="1500547397347480618">"Ikona folderu"</string>
 </resources>
diff --git a/res/values-pt-rPT/strings.xml b/res/values-pt-rPT/strings.xml
index a8ccd31..236e09d 100644
--- a/res/values-pt-rPT/strings.xml
+++ b/res/values-pt-rPT/strings.xml
@@ -361,6 +361,10 @@
     <item msgid="732746454445519134">"Mostrar conversa mais antiga após eliminar"</item>
     <item msgid="2189929276292165301">"Mostrar lista de conversas após eliminar"</item>
   </string-array>
+    <string name="clear_display_images_whitelist_title" msgid="7120575487854245735">"Limpar aprovações de fotografias"</string>
+    <string name="clear_display_images_whitelist_dialog_title" msgid="3190704164490442683">"Limpar aprovações de fotografias?"</string>
+    <string name="clear_display_images_whitelist_dialog_message" msgid="1169152185612117654">"Parar apresentação de imagens inline de remetentes anteriormente permitidos."</string>
+    <string name="sender_whitelist_cleared" msgid="917434007919176024">"As imagens não serão mostradas automaticamente."</string>
     <string name="whats_new_dialog_title" msgid="4230806739326698666">"Novidades"</string>
     <string name="notification_action_reply" msgid="6015299134424685297">"Responder"</string>
     <string name="notification_action_reply_all" msgid="20020468410400912">"Responder a todos"</string>
@@ -411,4 +415,5 @@
     <string name="drawer_open" msgid="6074646853178471940">"Abrir separador de navegação"</string>
     <string name="drawer_close" msgid="2764774620737876943">"Fechar separador de navegação"</string>
     <string name="conversation_photo_welcome_text" msgid="4274875219447670662">"Toque numa imagem de remetente para selecionar essa conversa."</string>
+    <string name="folder_icon_desc" msgid="1500547397347480618">"Ícone de pasta"</string>
 </resources>
diff --git a/res/values-pt/strings.xml b/res/values-pt/strings.xml
index 8d8e0fb..3a2fbde 100644
--- a/res/values-pt/strings.xml
+++ b/res/values-pt/strings.xml
@@ -361,6 +361,10 @@
     <item msgid="732746454445519134">"Exibir conversa mais antiga depois de excluir"</item>
     <item msgid="2189929276292165301">"Exibir a lista de conversas depois de excluir"</item>
   </string-array>
+    <string name="clear_display_images_whitelist_title" msgid="7120575487854245735">"Limpar aprovações de fotos"</string>
+    <string name="clear_display_images_whitelist_dialog_title" msgid="3190704164490442683">"Limpar aprovações de fotos?"</string>
+    <string name="clear_display_images_whitelist_dialog_message" msgid="1169152185612117654">"Não mostrar mais imagens inline de remetentes que você autorizou anteriormente."</string>
+    <string name="sender_whitelist_cleared" msgid="917434007919176024">"As imagens não serão exibidas automaticamente."</string>
     <string name="whats_new_dialog_title" msgid="4230806739326698666">"O que há de novo"</string>
     <string name="notification_action_reply" msgid="6015299134424685297">"Responder"</string>
     <string name="notification_action_reply_all" msgid="20020468410400912">"Responder a todos"</string>
@@ -411,4 +415,5 @@
     <string name="drawer_open" msgid="6074646853178471940">"Abrir gaveta de navegação"</string>
     <string name="drawer_close" msgid="2764774620737876943">"Fechar gaveta de navegação"</string>
     <string name="conversation_photo_welcome_text" msgid="4274875219447670662">"Toque na imagem do remetente para selecionar a conversa."</string>
+    <string name="folder_icon_desc" msgid="1500547397347480618">"Ícone de pasta"</string>
 </resources>
diff --git a/res/values-rm/strings.xml b/res/values-rm/strings.xml
index b55956e..4f20042 100644
--- a/res/values-rm/strings.xml
+++ b/res/values-rm/strings.xml
@@ -560,6 +560,14 @@
     <!-- no translation found for prefSummaries_autoAdvance:0 (6389534341359835440) -->
     <!-- no translation found for prefSummaries_autoAdvance:1 (732746454445519134) -->
     <!-- no translation found for prefSummaries_autoAdvance:2 (2189929276292165301) -->
+    <!-- no translation found for clear_display_images_whitelist_title (7120575487854245735) -->
+    <skip />
+    <!-- no translation found for clear_display_images_whitelist_dialog_title (3190704164490442683) -->
+    <skip />
+    <!-- no translation found for clear_display_images_whitelist_dialog_message (1169152185612117654) -->
+    <skip />
+    <!-- no translation found for sender_whitelist_cleared (917434007919176024) -->
+    <skip />
     <!-- no translation found for whats_new_dialog_title (4230806739326698666) -->
     <skip />
     <!-- no translation found for notification_action_reply (6015299134424685297) -->
@@ -636,4 +644,6 @@
     <skip />
     <!-- no translation found for conversation_photo_welcome_text (4274875219447670662) -->
     <skip />
+    <!-- no translation found for folder_icon_desc (1500547397347480618) -->
+    <skip />
 </resources>
diff --git a/res/values-ro/strings.xml b/res/values-ro/strings.xml
index 181a32f..537c29a 100644
--- a/res/values-ro/strings.xml
+++ b/res/values-ro/strings.xml
@@ -361,6 +361,10 @@
     <item msgid="732746454445519134">"Afişaţi conversaţia mai veche după ce ştergeţi"</item>
     <item msgid="2189929276292165301">"Afişaţi lista de conversaţii după ce ştergeţi"</item>
   </string-array>
+    <string name="clear_display_images_whitelist_title" msgid="7120575487854245735">"Ștergeți aprobările pentru imagini"</string>
+    <string name="clear_display_images_whitelist_dialog_title" msgid="3190704164490442683">"Ștergeți aprobările pentru imagini?"</string>
+    <string name="clear_display_images_whitelist_dialog_message" msgid="1169152185612117654">"Nu mai afișați imaginile inline de la expeditori acceptați anterior."</string>
+    <string name="sender_whitelist_cleared" msgid="917434007919176024">"Imaginile nu vor fi afișate în mod automat."</string>
     <string name="whats_new_dialog_title" msgid="4230806739326698666">"Ce este nou"</string>
     <string name="notification_action_reply" msgid="6015299134424685297">"Răspundeți"</string>
     <string name="notification_action_reply_all" msgid="20020468410400912">"Răspundeți tuturor"</string>
@@ -411,4 +415,5 @@
     <string name="drawer_open" msgid="6074646853178471940">"Deschideți panoul de navigare"</string>
     <string name="drawer_close" msgid="2764774620737876943">"Închideți panoul de navigare"</string>
     <string name="conversation_photo_welcome_text" msgid="4274875219447670662">"Atingeți imaginea unui expeditor pentru a selecta respectiva conversație."</string>
+    <string name="folder_icon_desc" msgid="1500547397347480618">"Pictogramă dosar"</string>
 </resources>
diff --git a/res/values-ru/strings.xml b/res/values-ru/strings.xml
index c3097ac..3ee15b9 100644
--- a/res/values-ru/strings.xml
+++ b/res/values-ru/strings.xml
@@ -361,6 +361,10 @@
     <item msgid="732746454445519134">"Показывать более раннюю цепочку после удаления текущей"</item>
     <item msgid="2189929276292165301">"Показывать список цепочек после удаления текущей цепочки"</item>
   </string-array>
+    <string name="clear_display_images_whitelist_title" msgid="7120575487854245735">"Запретить показ изображений"</string>
+    <string name="clear_display_images_whitelist_dialog_title" msgid="3190704164490442683">"Запретить показ изображений?"</string>
+    <string name="clear_display_images_whitelist_dialog_message" msgid="1169152185612117654">"Изображения от ранее одобренных отправителей показываться не будут."</string>
+    <string name="sender_whitelist_cleared" msgid="917434007919176024">"Автоматический показ изображений отключен."</string>
     <string name="whats_new_dialog_title" msgid="4230806739326698666">"Что нового"</string>
     <string name="notification_action_reply" msgid="6015299134424685297">"Ответить"</string>
     <string name="notification_action_reply_all" msgid="20020468410400912">"Ответить всем"</string>
@@ -411,4 +415,5 @@
     <string name="drawer_open" msgid="6074646853178471940">"Открыть панель навигации"</string>
     <string name="drawer_close" msgid="2764774620737876943">"Закрыть панель навигации"</string>
     <string name="conversation_photo_welcome_text" msgid="4274875219447670662">"Чтобы открыть цепочку писем, нажмите на фото отправителя."</string>
+    <string name="folder_icon_desc" msgid="1500547397347480618">"Значок папки"</string>
 </resources>
diff --git a/res/values-sk/strings.xml b/res/values-sk/strings.xml
index 9e4999d..2217abe 100644
--- a/res/values-sk/strings.xml
+++ b/res/values-sk/strings.xml
@@ -361,6 +361,10 @@
     <item msgid="732746454445519134">"Po odstránení zobraziť staršiu konverzáciu"</item>
     <item msgid="2189929276292165301">"Po odstránení zobraziť zoznam konverzácií"</item>
   </string-array>
+    <string name="clear_display_images_whitelist_title" msgid="7120575487854245735">"Vymazať schválenia obrázkov"</string>
+    <string name="clear_display_images_whitelist_dialog_title" msgid="3190704164490442683">"Chcete vymazať schválenia obrázkov?"</string>
+    <string name="clear_display_images_whitelist_dialog_message" msgid="1169152185612117654">"Zastaví sa zobrazovanie vložených obrázkov od odosielateľov, pre ktorých ste to predtým povolili."</string>
+    <string name="sender_whitelist_cleared" msgid="917434007919176024">"Obrázky sa nebudú zobrazovať automaticky."</string>
     <string name="whats_new_dialog_title" msgid="4230806739326698666">"Novinky"</string>
     <string name="notification_action_reply" msgid="6015299134424685297">"Odpovedať"</string>
     <string name="notification_action_reply_all" msgid="20020468410400912">"Odpovedať všetkým"</string>
@@ -411,4 +415,5 @@
     <string name="drawer_open" msgid="6074646853178471940">"Otvoriť priečinok s navigáciou"</string>
     <string name="drawer_close" msgid="2764774620737876943">"Zavrieť priečinok s navigáciou"</string>
     <string name="conversation_photo_welcome_text" msgid="4274875219447670662">"Dotknutím sa obrázka odosielateľa vyberiete danú konverzáciu."</string>
+    <string name="folder_icon_desc" msgid="1500547397347480618">"Ikona priečinka"</string>
 </resources>
diff --git a/res/values-sl/strings.xml b/res/values-sl/strings.xml
index 721d21b..9ae4c13 100644
--- a/res/values-sl/strings.xml
+++ b/res/values-sl/strings.xml
@@ -361,6 +361,10 @@
     <item msgid="732746454445519134">"Po izbrisu pokaži starejši pogovor"</item>
     <item msgid="2189929276292165301">"Po izbrisu pokaži seznam pogovorov"</item>
   </string-array>
+    <string name="clear_display_images_whitelist_title" msgid="7120575487854245735">"Izbriši odobritve slik"</string>
+    <string name="clear_display_images_whitelist_dialog_title" msgid="3190704164490442683">"Želite izbrisati odobritve slik?"</string>
+    <string name="clear_display_images_whitelist_dialog_message" msgid="1169152185612117654">"Prenehanje prikazovanja slik v sporočilih pošiljateljev, za katere ste to v preteklosti odobrili."</string>
+    <string name="sender_whitelist_cleared" msgid="917434007919176024">"Slike ne bodo samodejno prikazane."</string>
     <string name="whats_new_dialog_title" msgid="4230806739326698666">"Kaj je novega?"</string>
     <string name="notification_action_reply" msgid="6015299134424685297">"Odgovori"</string>
     <string name="notification_action_reply_all" msgid="20020468410400912">"Odgovori vsem"</string>
@@ -411,4 +415,5 @@
     <string name="drawer_open" msgid="6074646853178471940">"Odpri predal za navigacijo"</string>
     <string name="drawer_close" msgid="2764774620737876943">"Zapri predal za navigacijo"</string>
     <string name="conversation_photo_welcome_text" msgid="4274875219447670662">"Če želite izbrati ta pogovor, se dotaknite slike pošiljatelja."</string>
+    <string name="folder_icon_desc" msgid="1500547397347480618">"Ikona mape"</string>
 </resources>
diff --git a/res/values-sr/strings.xml b/res/values-sr/strings.xml
index 879d19c..27e7ea9 100644
--- a/res/values-sr/strings.xml
+++ b/res/values-sr/strings.xml
@@ -361,6 +361,10 @@
     <item msgid="732746454445519134">"Приказ старије преписке након брисања"</item>
     <item msgid="2189929276292165301">"Приказ листе преписки након брисања"</item>
   </string-array>
+    <string name="clear_display_images_whitelist_title" msgid="7120575487854245735">"Обриши одобрења за слике"</string>
+    <string name="clear_display_images_whitelist_dialog_title" msgid="3190704164490442683">"Желите ли да обришете одобрења за слике?"</string>
+    <string name="clear_display_images_whitelist_dialog_message" msgid="1169152185612117654">"Престаните да приказујете уметнуте слике пошиљалаца које сте раније одобрили."</string>
+    <string name="sender_whitelist_cleared" msgid="917434007919176024">"Слике неће бити аутоматски приказиване."</string>
     <string name="whats_new_dialog_title" msgid="4230806739326698666">"Шта је ново"</string>
     <string name="notification_action_reply" msgid="6015299134424685297">"Одговори"</string>
     <string name="notification_action_reply_all" msgid="20020468410400912">"Одговори свима"</string>
@@ -411,4 +415,5 @@
     <string name="drawer_open" msgid="6074646853178471940">"Отвори фиоку за навигацију"</string>
     <string name="drawer_close" msgid="2764774620737876943">"Затвори фиоку за навигацију"</string>
     <string name="conversation_photo_welcome_text" msgid="4274875219447670662">"Додирните слику пошиљаоца да бисте изабрали ту преписку."</string>
+    <string name="folder_icon_desc" msgid="1500547397347480618">"Икона директоријума"</string>
 </resources>
diff --git a/res/values-sv/strings.xml b/res/values-sv/strings.xml
index d8a1b94..51f318b 100644
--- a/res/values-sv/strings.xml
+++ b/res/values-sv/strings.xml
@@ -361,6 +361,10 @@
     <item msgid="732746454445519134">"Visa en äldre konversation efter borttagning"</item>
     <item msgid="2189929276292165301">"Visa konversationslistan efter borttagning"</item>
   </string-array>
+    <string name="clear_display_images_whitelist_title" msgid="7120575487854245735">"Ta bort godkännanden av bilder"</string>
+    <string name="clear_display_images_whitelist_dialog_title" msgid="3190704164490442683">"Vill du ta bort godkännanden av bilder?"</string>
+    <string name="clear_display_images_whitelist_dialog_message" msgid="1169152185612117654">"Sluta visa infogade bilder från avsändare vars bilder du tidigare har godkänt."</string>
+    <string name="sender_whitelist_cleared" msgid="917434007919176024">"Bilder kommer inte att visas automatiskt."</string>
     <string name="whats_new_dialog_title" msgid="4230806739326698666">"Nyheter"</string>
     <string name="notification_action_reply" msgid="6015299134424685297">"Svara"</string>
     <string name="notification_action_reply_all" msgid="20020468410400912">"Svara alla"</string>
@@ -411,4 +415,5 @@
     <string name="drawer_open" msgid="6074646853178471940">"Öppna navigeringsmenyn"</string>
     <string name="drawer_close" msgid="2764774620737876943">"Stäng navigeringsmenyn"</string>
     <string name="conversation_photo_welcome_text" msgid="4274875219447670662">"Tryck på avsändarens bild om du vill välja den konversationen."</string>
+    <string name="folder_icon_desc" msgid="1500547397347480618">"Mappikon"</string>
 </resources>
diff --git a/res/values-sw/strings.xml b/res/values-sw/strings.xml
index ac36d72..aff2c26 100644
--- a/res/values-sw/strings.xml
+++ b/res/values-sw/strings.xml
@@ -361,6 +361,10 @@
     <item msgid="732746454445519134">"Onyesha mazungumzo ya zamani zaidi baada ya kufuta"</item>
     <item msgid="2189929276292165301">"Onyesha orodha ya mazungumzo baada ya kufuta"</item>
   </string-array>
+    <string name="clear_display_images_whitelist_title" msgid="7120575487854245735">"Futa uidhinishaji wa picha"</string>
+    <string name="clear_display_images_whitelist_dialog_title" msgid="3190704164490442683">"Ungependa kufuta uidhinishaji wa picha?"</string>
+    <string name="clear_display_images_whitelist_dialog_message" msgid="1169152185612117654">"Acha kuonyesha picha zinazolingana na maandishi kutoka kwa watumaji uliowaruhusu hapo awali."</string>
+    <string name="sender_whitelist_cleared" msgid="917434007919176024">"Picha hazitaonyeshwa kiotomatiki."</string>
     <string name="whats_new_dialog_title" msgid="4230806739326698666">"Nini kipya"</string>
     <string name="notification_action_reply" msgid="6015299134424685297">"Jibu"</string>
     <string name="notification_action_reply_all" msgid="20020468410400912">"Jibu wote"</string>
@@ -411,4 +415,5 @@
     <string name="drawer_open" msgid="6074646853178471940">"Fungua menyu"</string>
     <string name="drawer_close" msgid="2764774620737876943">"Funga menyu"</string>
     <string name="conversation_photo_welcome_text" msgid="4274875219447670662">"Gusa picha ya mtumaji ili uchague mazungumzo hayo."</string>
+    <string name="folder_icon_desc" msgid="1500547397347480618">"Ikoni ya folda"</string>
 </resources>
diff --git a/res/values-th/strings.xml b/res/values-th/strings.xml
index 77bd6aa..9907515 100644
--- a/res/values-th/strings.xml
+++ b/res/values-th/strings.xml
@@ -361,6 +361,10 @@
     <item msgid="732746454445519134">"แสดงการสนทนาที่เก่ากว่าหลังจากลบ"</item>
     <item msgid="2189929276292165301">"แสดงรายการสนทนาหลังจากลบ"</item>
   </string-array>
+    <string name="clear_display_images_whitelist_title" msgid="7120575487854245735">"ล้างการอนุมัติภาพ"</string>
+    <string name="clear_display_images_whitelist_dialog_title" msgid="3190704164490442683">"ล้างการอนุมัติภาพไหม"</string>
+    <string name="clear_display_images_whitelist_dialog_message" msgid="1169152185612117654">"หยุดการแสดงภาพในบรรทัดจากผู้ส่งที่คุณอนุญาตไว้ก่อนหน้านี้"</string>
+    <string name="sender_whitelist_cleared" msgid="917434007919176024">"รูปภาพจะไม่แสดงโดยอัตโนมัติ"</string>
     <string name="whats_new_dialog_title" msgid="4230806739326698666">"มีอะไรใหม่"</string>
     <string name="notification_action_reply" msgid="6015299134424685297">"ตอบ"</string>
     <string name="notification_action_reply_all" msgid="20020468410400912">"ตอบทั้งหมด"</string>
@@ -411,4 +415,5 @@
     <string name="drawer_open" msgid="6074646853178471940">"เปิดช่องการนำทาง"</string>
     <string name="drawer_close" msgid="2764774620737876943">"ปิดช่องการนำทาง"</string>
     <string name="conversation_photo_welcome_text" msgid="4274875219447670662">"แตะภาพผู้ส่งเพื่อเลือกการสนทนานั้น"</string>
+    <string name="folder_icon_desc" msgid="1500547397347480618">"ไอคอนโฟลเดอร์"</string>
 </resources>
diff --git a/res/values-tl/strings.xml b/res/values-tl/strings.xml
index d4c10ae..cc86cbc 100644
--- a/res/values-tl/strings.xml
+++ b/res/values-tl/strings.xml
@@ -361,6 +361,10 @@
     <item msgid="732746454445519134">"Magpakita ng mas lumang pag-uusap pagkatapos mong magtanggal"</item>
     <item msgid="2189929276292165301">"Ipakita ang listahan ng pag-uusap pagkatapos mong magtanggal"</item>
   </string-array>
+    <string name="clear_display_images_whitelist_title" msgid="7120575487854245735">"I-clear ang mga pag-apruba sa larawan"</string>
+    <string name="clear_display_images_whitelist_dialog_title" msgid="3190704164490442683">"I-clear ang mga pag-apruba sa larawan?"</string>
+    <string name="clear_display_images_whitelist_dialog_message" msgid="1169152185612117654">"Huminto sa pagpapakita ng mga inline na larawan mula sa mga nagpadalang dati mong pinayagan."</string>
+    <string name="sender_whitelist_cleared" msgid="917434007919176024">"Hindi awtomatikong ipapakita ang mga larawan."</string>
     <string name="whats_new_dialog_title" msgid="4230806739326698666">"Ano\'ng bago"</string>
     <string name="notification_action_reply" msgid="6015299134424685297">"Tumugon"</string>
     <string name="notification_action_reply_all" msgid="20020468410400912">"Tumugon sa lahat"</string>
@@ -411,4 +415,5 @@
     <string name="drawer_open" msgid="6074646853178471940">"Buksan ang drawer ng nabigasyon"</string>
     <string name="drawer_close" msgid="2764774620737876943">"Isara ang drawer ng nabigasyon"</string>
     <string name="conversation_photo_welcome_text" msgid="4274875219447670662">"Pumidot ng larawan ng nagpadala upang piliin ang pag-uusap na iyon."</string>
+    <string name="folder_icon_desc" msgid="1500547397347480618">"Icon ng folder"</string>
 </resources>
diff --git a/res/values-tr/strings.xml b/res/values-tr/strings.xml
index e83a23e..189ae2a 100644
--- a/res/values-tr/strings.xml
+++ b/res/values-tr/strings.xml
@@ -361,6 +361,10 @@
     <item msgid="732746454445519134">"Sildikten sonra daha eski ileti dizisini göster"</item>
     <item msgid="2189929276292165301">"Sildikten sonra ileti dizisi listesini göster"</item>
   </string-array>
+    <string name="clear_display_images_whitelist_title" msgid="7120575487854245735">"Resim onaylarını temizle"</string>
+    <string name="clear_display_images_whitelist_dialog_title" msgid="3190704164490442683">"Resim onayları temizlensin mi?"</string>
+    <string name="clear_display_images_whitelist_dialog_message" msgid="1169152185612117654">"Önceden izin verdiğiniz gönderenlerden gelen satır içi resimleri görüntülemeyi durdur"</string>
+    <string name="sender_whitelist_cleared" msgid="917434007919176024">"Resimler otomatik olarak gösterilmeyecek."</string>
     <string name="whats_new_dialog_title" msgid="4230806739326698666">"Yenilikler"</string>
     <string name="notification_action_reply" msgid="6015299134424685297">"Yanıtla"</string>
     <string name="notification_action_reply_all" msgid="20020468410400912">"Tümünü yanıtla"</string>
@@ -411,4 +415,5 @@
     <string name="drawer_open" msgid="6074646853178471940">"Gezinme menüsünü aç"</string>
     <string name="drawer_close" msgid="2764774620737876943">"Gezinme menüsünü kapat"</string>
     <string name="conversation_photo_welcome_text" msgid="4274875219447670662">"İlgili ileti dizisini seçmek için gönderenin resmine dokunun."</string>
+    <string name="folder_icon_desc" msgid="1500547397347480618">"Klasör simgesi"</string>
 </resources>
diff --git a/res/values-uk/strings.xml b/res/values-uk/strings.xml
index 238066a..633eefd 100644
--- a/res/values-uk/strings.xml
+++ b/res/values-uk/strings.xml
@@ -361,6 +361,10 @@
     <item msgid="732746454445519134">"Показувати старішу бесіду після видалення"</item>
     <item msgid="2189929276292165301">"Показувати список бесід після видалення"</item>
   </string-array>
+    <string name="clear_display_images_whitelist_title" msgid="7120575487854245735">"Очистити дозволи для зображень"</string>
+    <string name="clear_display_images_whitelist_dialog_title" msgid="3190704164490442683">"Очистити дозволи для зображень?"</string>
+    <string name="clear_display_images_whitelist_dialog_message" msgid="1169152185612117654">"Не показувати вставлені зображення від відправників, дозволені раніше."</string>
+    <string name="sender_whitelist_cleared" msgid="917434007919176024">"Зображення не показуватимуться автоматично."</string>
     <string name="whats_new_dialog_title" msgid="4230806739326698666">"Новинки"</string>
     <string name="notification_action_reply" msgid="6015299134424685297">"Відповісти"</string>
     <string name="notification_action_reply_all" msgid="20020468410400912">"Відповісти всім"</string>
@@ -411,4 +415,5 @@
     <string name="drawer_open" msgid="6074646853178471940">"Висунути навігаційну панель"</string>
     <string name="drawer_close" msgid="2764774620737876943">"Сховати навігаційну панель"</string>
     <string name="conversation_photo_welcome_text" msgid="4274875219447670662">"Торкніться зображення відправника, щоб вибрати бесіду."</string>
+    <string name="folder_icon_desc" msgid="1500547397347480618">"Значок папки"</string>
 </resources>
diff --git a/res/values-vi/strings.xml b/res/values-vi/strings.xml
index 62cc02e..ab38ab4 100644
--- a/res/values-vi/strings.xml
+++ b/res/values-vi/strings.xml
@@ -361,6 +361,10 @@
     <item msgid="732746454445519134">"Hiển thị cuộc hội thoại cũ hơn sau khi bạn xóa"</item>
     <item msgid="2189929276292165301">"Hiển thị danh sách cuộc hội thoại sau khi bạn xóa"</item>
   </string-array>
+    <string name="clear_display_images_whitelist_title" msgid="7120575487854245735">"Xóa phê duyệt ảnh"</string>
+    <string name="clear_display_images_whitelist_dialog_title" msgid="3190704164490442683">"Xóa phê duyệt ảnh?"</string>
+    <string name="clear_display_images_whitelist_dialog_message" msgid="1169152185612117654">"Dừng hiển thị các hình ảnh nội tuyến từ người gửi bạn đã cho phép trước đó."</string>
+    <string name="sender_whitelist_cleared" msgid="917434007919176024">"Hình ảnh sẽ không được tự động hiển thị."</string>
     <string name="whats_new_dialog_title" msgid="4230806739326698666">"Tính năng mới"</string>
     <string name="notification_action_reply" msgid="6015299134424685297">"Trả lời"</string>
     <string name="notification_action_reply_all" msgid="20020468410400912">"Trả lời tất cả"</string>
@@ -411,4 +415,5 @@
     <string name="drawer_open" msgid="6074646853178471940">"Mở ngăn kéo điều hướng"</string>
     <string name="drawer_close" msgid="2764774620737876943">"Đóng ngăn kéo điều hướng"</string>
     <string name="conversation_photo_welcome_text" msgid="4274875219447670662">"Chạm vào hình ảnh của người gửi để chọn cuộc hội thoại đó."</string>
+    <string name="folder_icon_desc" msgid="1500547397347480618">"Biểu tượng thư mục"</string>
 </resources>
diff --git a/res/values-zh-rCN/strings.xml b/res/values-zh-rCN/strings.xml
index aaa1018..4a861c1 100644
--- a/res/values-zh-rCN/strings.xml
+++ b/res/values-zh-rCN/strings.xml
@@ -361,6 +361,10 @@
     <item msgid="732746454445519134">"在您执行删除操作后显示较旧的前一个会话"</item>
     <item msgid="2189929276292165301">"在您执行删除操作后显示会话列表"</item>
   </string-array>
+    <string name="clear_display_images_whitelist_title" msgid="7120575487854245735">"清除图片显示许可"</string>
+    <string name="clear_display_images_whitelist_dialog_title" msgid="3190704164490442683">"清除图片显示许可?"</string>
+    <string name="clear_display_images_whitelist_dialog_message" msgid="1169152185612117654">"对于您之前允许显示其邮件内嵌图片的发件人,停止显示其邮件内的图片。"</string>
+    <string name="sender_whitelist_cleared" msgid="917434007919176024">"图片不会自动显示。"</string>
     <string name="whats_new_dialog_title" msgid="4230806739326698666">"新功能"</string>
     <string name="notification_action_reply" msgid="6015299134424685297">"回复"</string>
     <string name="notification_action_reply_all" msgid="20020468410400912">"全部回复"</string>
@@ -411,4 +415,5 @@
     <string name="drawer_open" msgid="6074646853178471940">"打开抽屉式导航栏"</string>
     <string name="drawer_close" msgid="2764774620737876943">"关闭抽屉式导航栏"</string>
     <string name="conversation_photo_welcome_text" msgid="4274875219447670662">"触摸发件人头像即可选择该会话。"</string>
+    <string name="folder_icon_desc" msgid="1500547397347480618">"文件夹图标"</string>
 </resources>
diff --git a/res/values-zh-rTW/strings.xml b/res/values-zh-rTW/strings.xml
index 828e041..8d49bb6 100644
--- a/res/values-zh-rTW/strings.xml
+++ b/res/values-zh-rTW/strings.xml
@@ -361,6 +361,10 @@
     <item msgid="732746454445519134">"在刪除動作完成後顯示較舊的會話群組"</item>
     <item msgid="2189929276292165301">"在刪除動作完成後顯示會話群組清單"</item>
   </string-array>
+    <string name="clear_display_images_whitelist_title" msgid="7120575487854245735">"取消圖片許可"</string>
+    <string name="clear_display_images_whitelist_dialog_title" msgid="3190704164490442683">"要取消圖片許可嗎?"</string>
+    <string name="clear_display_images_whitelist_dialog_message" msgid="1169152185612117654">"不再顯示先前許可的寄件者寄來的郵件內置圖片。"</string>
+    <string name="sender_whitelist_cleared" msgid="917434007919176024">"系統不會自動顯示圖片。"</string>
     <string name="whats_new_dialog_title" msgid="4230806739326698666">"新功能"</string>
     <string name="notification_action_reply" msgid="6015299134424685297">"回覆"</string>
     <string name="notification_action_reply_all" msgid="20020468410400912">"回覆所有人"</string>
@@ -411,4 +415,5 @@
     <string name="drawer_open" msgid="6074646853178471940">"開啟導覽匣"</string>
     <string name="drawer_close" msgid="2764774620737876943">"關閉導覽匣"</string>
     <string name="conversation_photo_welcome_text" msgid="4274875219447670662">"輕觸寄件者圖片即可選取該會話群組。"</string>
+    <string name="folder_icon_desc" msgid="1500547397347480618">"資料夾圖示"</string>
 </resources>
diff --git a/res/values-zu/strings.xml b/res/values-zu/strings.xml
index 69d8e61..b9240fe 100644
--- a/res/values-zu/strings.xml
+++ b/res/values-zu/strings.xml
@@ -361,6 +361,10 @@
     <item msgid="732746454445519134">"Bonisa ingxoxo endala ngemuva kokususa"</item>
     <item msgid="2189929276292165301">"Bonisa uhlu lwengxoxo ngemuva kokususa"</item>
   </string-array>
+    <string name="clear_display_images_whitelist_title" msgid="7120575487854245735">"Sula ukuvunywa kwesithombe"</string>
+    <string name="clear_display_images_whitelist_dialog_title" msgid="3190704164490442683">"Sula ukuvunywa kwesithombe?"</string>
+    <string name="clear_display_images_whitelist_dialog_message" msgid="1169152185612117654">"Misa ukubonisa izithombe ezisemugqeni ezisuka kubathumeli obavumele ngaphambilini."</string>
+    <string name="sender_whitelist_cleared" msgid="917434007919176024">"Izithombe azizukukhonjiswa ngokuzenzakalela."</string>
     <string name="whats_new_dialog_title" msgid="4230806739326698666">"Yini entsha"</string>
     <string name="notification_action_reply" msgid="6015299134424685297">"Phendula"</string>
     <string name="notification_action_reply_all" msgid="20020468410400912">"Phendula konke"</string>
@@ -411,4 +415,5 @@
     <string name="drawer_open" msgid="6074646853178471940">"Vula ukuzulazula kwekhabethe"</string>
     <string name="drawer_close" msgid="2764774620737876943">"Vala ukuzulazula kwekhabethe"</string>
     <string name="conversation_photo_welcome_text" msgid="4274875219447670662">"Thinta isithombe somthumeli ukuze ukhethe ingxoxo."</string>
+    <string name="folder_icon_desc" msgid="1500547397347480618">"Isithonjana sefolda"</string>
 </resources>
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 4c6ed01..f6b13d3 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -151,6 +151,8 @@
     <string name="menu_change_folders">Change folders</string>
     <!-- Menu item: moves to folders for selected conversation(s). [CHAR LIMIT = 30] -->
     <string name="menu_move_to">Move to</string>
+    <!-- Menu item: moves current or selected conversation(s) to Inbox. [CHAR LIMIT = 30] -->
+    <string name="menu_move_to_inbox">Move to Inbox</string>
     <!-- Menu item: manages the folders for this account. [CHAR LIMIT = 30] -->
     <string name="menu_manage_folders">Folder settings</string>
     <!-- Menu item: report an email was not readable or poorly rendered -->
@@ -795,6 +797,20 @@
         <item>list</item>
     </string-array>
 
+    <!-- Settings screen, title of "Restore default for "Show pictures"" [CHAR LIMIT=1000]-->
+    <string name="clear_display_images_whitelist_title">Clear picture approvals</string>
+
+    <!-- Settings screen, title of dialog shown to confirm action when user taps
+    "Clear picture approvals" in preferences [CHAR LIMIT=200]-->
+    <string name="clear_display_images_whitelist_dialog_title">Clear picture approvals?</string>
+    <!-- Settings screen, message of dialog shown to confirm action when tapping
+    "Clear picture approvals" [CHAR LIMIT=1000]-->
+    <string name="clear_display_images_whitelist_dialog_message">Stop displaying inline images from senders you previously allowed.</string>
+
+    <!-- Message shown in toast when the user taps "Restore default for "Show pictures"" in Gmail general preferences. [CHAR LIMIT=50] -->
+    <string name="sender_whitelist_cleared">Pictures won\'t be shown automatically.</string>
+
+
     <!-- Dialog title for the What's New dialog. [CHAR LIMIT=50] -->
     <string name="whats_new_dialog_title">What\'s new</string>
 
@@ -911,4 +927,10 @@
     <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>
+
+    <!--  Button, "Add account" in the preference screen [CHAR LIMIT=30] -->
+    <string name="add_account">Add account</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..a5fda5f 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;
@@ -280,19 +279,24 @@
 
             @Override
             public Void doInBackground(Void... param) {
-                for (int i = mStartPos; i < getCount(); i++) {
-                    if (isCancelled()) {
-                        break;
-                    }
+                try {
+                    Utils.traceBeginSection("backgroundCaching");
+                    for (int i = mStartPos; i < getCount(); i++) {
+                        if (isCancelled()) {
+                            break;
+                        }
 
-                    final UnderlyingRowData rowData = mRowCache.get(i);
-                    if (rowData.conversation == null) {
-                        // We are running in a background thread.  Set the position to the row
-                        // we are interested in.
-                        if (moveToPosition(i)) {
-                            cacheConversation(new Conversation(UnderlyingCursorWrapper.this));
+                        final UnderlyingRowData rowData = mRowCache.get(i);
+                        if (rowData.conversation == null) {
+                            // We are running in a background thread.  Set the position to the row
+                            // we are interested in.
+                            if (moveToPosition(i)) {
+                                cacheConversation(new Conversation(UnderlyingCursorWrapper.this));
+                            }
                         }
                     }
+                } finally {
+                    Utils.traceEndSection();
                 }
                 return null;
             }
@@ -343,6 +347,7 @@
             final UnderlyingRowData[] cache;
             final int count;
             int numCached = 0;
+            Utils.traceBeginSection("blockingCaching");
             if (result != null && super.moveToFirst()) {
                 count = super.getCount();
                 cache = new UnderlyingRowData[count];
@@ -421,6 +426,8 @@
                     " %sms n=%s cached=%s CONV_PRECACHING=%s",
                     (end-start), count, numCached, ENABLE_CONVERSATION_PRECACHING);
 
+            Utils.traceEndSection();
+
             // If we haven't cached all of the conversations, start a task to do that
             if (ENABLE_CONVERSATION_PRECACHING && numCached < count) {
                 mCacheLoaderTask = new CacheLoaderTask(numCached);
@@ -561,7 +568,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..8e09513 100644
--- a/src/com/android/mail/browse/ConversationItemView.java
+++ b/src/com/android/mail/browse/ConversationItemView.java
@@ -579,6 +579,7 @@
     @Override
     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
         startTimer(PERF_TAG_LAYOUT);
+        Utils.traceBeginSection("CIVC.layout");
 
         super.onLayout(changed, left, top, right, bottom);
 
@@ -599,6 +600,7 @@
             sTimer = new Timer();
             sLayoutCount = 0;
         }
+        Utils.traceEndSection();
     }
 
     private void setContentDescription() {
@@ -633,7 +635,7 @@
         updateBackground(isUnread);
 
         mHeader.sendersDisplayText = new SpannableStringBuilder();
-        mHeader.styledSendersString = new SpannableStringBuilder();
+        mHeader.styledSendersString = null;
 
         // Parse senders fragments.
         if (mHeader.conversation.conversationInfo != null) {
@@ -782,7 +784,7 @@
     private void createSubject(final boolean isUnread) {
         final String subject = filterTag(mHeader.conversation.subject);
         final String snippet = mHeader.conversation.getSnippet();
-        final SpannableStringBuilder displayedStringBuilder = new SpannableStringBuilder(
+        final Spannable displayedStringBuilder = new SpannableString(
                 Conversation.getSubjectAndSnippetForDisplay(mContext, subject, snippet));
 
         // since spans affect text metrics, add spans to the string before measure/layout or fancy
@@ -1088,6 +1090,7 @@
 
     @Override
     protected void onDraw(Canvas canvas) {
+        Utils.traceBeginSection("CIVC.draw");
         // Contact photo
         if (mGadgetMode == ConversationItemViewCoordinates.GADGET_CONTACT_PHOTO) {
             canvas.save();
@@ -1190,6 +1193,7 @@
             final int y = (getHeight() - VISIBLE_CONVERSATION_CARET.getHeight()) / 2;
             canvas.drawBitmap(VISIBLE_CONVERSATION_CARET, x, y, null);
         }
+        Utils.traceEndSection();
     }
 
     private void drawContactImages(Canvas canvas) {
@@ -1402,10 +1406,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..e66494f
--- /dev/null
+++ b/src/com/android/mail/browse/ConversationMessage.java
@@ -0,0 +1,113 @@
+/*
+ * 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);
+    }
+
+    public void setController(ConversationController controller) {
+        mController = controller;
+    }
+
+    public Conversation getConversation() {
+        return mController != null ? mController.getConversation() : null;
+    }
+
+    /**
+     * 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..89ccb09 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;
@@ -120,7 +119,10 @@
 
     }
 
-    public class MessageHeaderItem extends ConversationOverlayItem {
+    public static class MessageHeaderItem extends ConversationOverlayItem {
+
+        private final ConversationViewAdapter mAdapter;
+
         private ConversationMessage mMessage;
 
         // view state variables
@@ -133,7 +135,9 @@
         public CharSequence timestampLong;
         public CharSequence recipientSummaryText;
 
-        MessageHeaderItem(ConversationMessage message, boolean expanded, boolean showImages) {
+        MessageHeaderItem(ConversationViewAdapter adapter, ConversationMessage message,
+                boolean expanded, boolean showImages) {
+            mAdapter = adapter;
             mMessage = message;
             mExpanded = expanded;
             mShowImages = showImages;
@@ -154,10 +158,11 @@
         public View createView(Context context, LayoutInflater inflater, ViewGroup parent) {
             final MessageHeaderView v = (MessageHeaderView) inflater.inflate(
                     R.layout.conversation_message_header, parent, false);
-            v.initialize(mDateBuilder, mAccountController, mAddressCache);
-            v.setCallbacks(mMessageCallbacks);
-            v.setContactInfoSource(mContactInfoSource);
-            v.setVeiledMatcher(mMatcher);
+            v.initialize(mAdapter.mDateBuilder, mAdapter.mAccountController,
+                    mAdapter.mAddressCache);
+            v.setCallbacks(mAdapter.mMessageCallbacks);
+            v.setContactInfoSource(mAdapter.mContactInfoSource);
+            v.setVeiledMatcher(mAdapter.mMatcher);
             return v;
         }
 
@@ -403,16 +408,16 @@
     }
 
     public int addMessageHeader(ConversationMessage msg, boolean expanded, boolean showImages) {
-        return addItem(new MessageHeaderItem(msg, expanded, showImages));
+        return addItem(new MessageHeaderItem(this, msg, expanded, showImages));
     }
 
     public int addMessageFooter(MessageHeaderItem headerItem) {
         return addItem(new MessageFooterItem(headerItem));
     }
 
-    public MessageHeaderItem newMessageHeaderItem(ConversationMessage message, boolean expanded,
-            boolean showImages) {
-        return new MessageHeaderItem(message, expanded, showImages);
+    public static MessageHeaderItem newMessageHeaderItem(ConversationViewAdapter adapter,
+            ConversationMessage message, boolean expanded, boolean showImages) {
+        return new MessageHeaderItem(adapter, message, expanded, showImages);
     }
 
     public MessageFooterItem newMessageFooterItem(MessageHeaderItem headerItem) {
diff --git a/src/com/android/mail/browse/EmlMessageLoader.java b/src/com/android/mail/browse/EmlMessageLoader.java
new file mode 100644
index 0000000..09e5b6a
--- /dev/null
+++ b/src/com/android/mail/browse/EmlMessageLoader.java
@@ -0,0 +1,180 @@
+/*
+ * 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
+ *and
+ * 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.content.AsyncTaskLoader;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.net.Uri;
+
+import com.android.emailcommon.TempDirectory;
+import com.android.emailcommon.internet.MimeMessage;
+import com.android.emailcommon.mail.MessagingException;
+import com.android.mail.utils.LogTag;
+import com.android.mail.utils.LogUtils;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Loader that builds a ConversationMessage from an EML file Uri.
+ */
+public class EmlMessageLoader extends AsyncTaskLoader<ConversationMessage> {
+    private static final String LOG_TAG = LogTag.getLogTag();
+
+    private Uri mEmlFileUri;
+    private ConversationMessage mMessage;
+
+    public EmlMessageLoader(Context context, Uri emlFileUri) {
+        super(context);
+        mEmlFileUri = emlFileUri;
+    }
+
+    @Override
+    public ConversationMessage loadInBackground() {
+        final Context context = getContext();
+        TempDirectory.setTempDirectory(context);
+        final ContentResolver resolver = context.getContentResolver();
+        final InputStream stream;
+        try {
+            stream = resolver.openInputStream(mEmlFileUri);
+        } catch (FileNotFoundException e) {
+            LogUtils.e(LOG_TAG, e, "Could not find eml file at uri: %s", mEmlFileUri);
+            return null;
+        }
+
+        final MimeMessage mimeMessage;
+        final ConversationMessage convMessage;
+        try {
+            mimeMessage = new MimeMessage(stream);
+            convMessage = new ConversationMessage(mimeMessage);
+        } catch (IOException e) {
+            LogUtils.e(LOG_TAG, e, "Could not read eml file");
+            return null;
+        } catch (MessagingException e) {
+            LogUtils.e(LOG_TAG, e, "Error in parsing eml file");
+            return null;
+        } finally {
+            try {
+                stream.close();
+            } catch (IOException e) {
+                return null;
+            }
+        }
+
+        return convMessage;
+    }
+
+    /**
+     * Called when there is new data to deliver to the client.  The
+     * super class will take care of delivering it; the implementation
+     * here just adds a little more logic.
+     */
+    @Override
+    public void deliverResult(ConversationMessage result) {
+        if (isReset()) {
+            // An async query came in while the loader is stopped.  We
+            // don't need the result.
+            if (result != null) {
+                onReleaseResources(result);
+            }
+        }
+        ConversationMessage oldMessage = mMessage;
+        mMessage = result;
+
+        if (isStarted()) {
+            // If the Loader is currently started, we can immediately
+            // deliver its results.
+            super.deliverResult(result);
+        }
+
+        // At this point we can release the resources associated with
+        // 'oldMessage' if needed; now that the new result is delivered we
+        // know that it is no longer in use.
+        if (oldMessage != null && oldMessage != mMessage) {
+            onReleaseResources(oldMessage);
+        }
+    }
+
+    /**
+     * Handles a request to start the Loader.
+     */
+    @Override
+    protected void onStartLoading() {
+        if (mMessage != null) {
+            // If we currently have a result available, deliver it immediately.
+            deliverResult(mMessage);
+        }
+
+        if (takeContentChanged() || mMessage == null) {
+            // If the data has changed since the last time it was loaded
+            // or is not currently available, start a load.
+            forceLoad();
+        }
+    }
+
+    /**
+     * Handles a request to stop the Loader.
+     */
+    @Override protected void onStopLoading() {
+        // Attempt to cancel the current load task if possible.
+        cancelLoad();
+    }
+
+    /**
+     * Handles a request to cancel a load.
+     */
+    @Override
+    public void onCanceled(ConversationMessage result) {
+        super.onCanceled(result);
+
+        // At this point we can release the resources associated with
+        // the message, if needed.
+        if (result != null) {
+            onReleaseResources(result);
+        }
+    }
+
+    /**
+     * Handles a request to completely reset the Loader.
+     */
+    @Override
+    protected void onReset() {
+        super.onReset();
+
+        // Ensure the loader is stopped
+        onStopLoading();
+
+        // At this point we can release the resources associated with
+        // the message, if needed.
+        if (mMessage != null) {
+            onReleaseResources(mMessage);
+            mMessage = null;
+        }
+    }
+
+
+    /**
+     * Helper function to take care of releasing resources associated
+     * with an actively loaded data set.
+     */
+    protected void onReleaseResources(ConversationMessage message) {
+        // DO NOTHING
+    }
+}
diff --git a/src/com/android/mail/browse/EmlMessageViewFragment.java b/src/com/android/mail/browse/EmlMessageViewFragment.java
new file mode 100644
index 0000000..13a3e54
--- /dev/null
+++ b/src/com/android/mail/browse/EmlMessageViewFragment.java
@@ -0,0 +1,234 @@
+/*
+ * 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.Fragment;
+import android.app.LoaderManager;
+import android.content.Loader;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.webkit.WebView;
+
+import com.android.mail.providers.Account;
+import com.android.mail.providers.Address;
+import com.android.mail.ui.AbstractConversationWebViewClient;
+import com.android.mail.ui.ContactLoaderCallbacks;
+import com.android.mail.ui.SecureConversationViewController;
+import com.android.mail.ui.SecureConversationViewControllerCallbacks;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Sets;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Fragment that is used to view EML files. It is mostly stubs
+ * that calls {@link SecureConversationViewController} to do most
+ * of the rendering work.
+ */
+public class EmlMessageViewFragment extends Fragment
+        implements SecureConversationViewControllerCallbacks,
+        LoaderManager.LoaderCallbacks<ConversationMessage> {
+    private static final String ARG_EML_FILE_URI = "eml_file_uri";
+    private static final String BASE_URI = "x-thread://message/rfc822/";
+    private static final int MESSAGE_LOADER = 0;
+    private static final int CONTACT_LOADER = 1;
+
+    private final Handler mHandler = new Handler();
+
+    private EmlWebViewClient mWebViewClient;
+    private SecureConversationViewController mViewController;
+    private ContactLoaderCallbacks mContactLoaderCallbacks;
+
+    private Uri mEmlFileUri;
+
+    /**
+     * Cache of email address strings to parsed Address objects.
+     * <p>
+     * Remember to synchronize on the map when reading or writing to this cache, because some
+     * instances use it off the UI thread (e.g. from WebView).
+     */
+    protected final Map<String, Address> mAddressCache = Collections.synchronizedMap(
+            new HashMap<String, Address>());
+
+    private class EmlWebViewClient extends AbstractConversationWebViewClient {
+        public EmlWebViewClient(Account account) {
+            super(account);
+        }
+
+        @Override
+        public void onPageFinished(WebView view, String url) {
+            mViewController.dismissLoadingStatus();
+
+            final Set<String> emailAddresses = Sets.newHashSet();
+            final List<Address> cacheCopy;
+            synchronized (mAddressCache) {
+                cacheCopy = ImmutableList.copyOf(mAddressCache.values());
+            }
+            for (Address addr : cacheCopy) {
+                emailAddresses.add(addr.getAddress());
+            }
+            final ContactLoaderCallbacks callbacks = getContactInfoSource();
+            callbacks.setSenders(emailAddresses);
+            getLoaderManager().restartLoader(CONTACT_LOADER, Bundle.EMPTY, callbacks);
+        }
+    };
+
+    /**
+     * Creates a new instance of {@link EmlMessageViewFragment},
+     * initialized to display an eml file from the specified {@link Uri}.
+     */
+    public static EmlMessageViewFragment newInstance(Uri emlFileUri) {
+        EmlMessageViewFragment f = new EmlMessageViewFragment();
+        Bundle args = new Bundle();
+        args.putParcelable(ARG_EML_FILE_URI, emlFileUri);
+        f.setArguments(args);
+        return f;
+    }
+
+    /**
+     * Constructor needs to be public to handle orientation changes and activity
+     * lifecycle events.
+     */
+    public EmlMessageViewFragment() {
+        super();
+    }
+
+    @Override
+    public void onCreate(Bundle savedState) {
+        super.onCreate(savedState);
+
+        Bundle args = getArguments();
+        mEmlFileUri = args.getParcelable(ARG_EML_FILE_URI);
+
+        mWebViewClient = new EmlWebViewClient(null);
+        mViewController = new SecureConversationViewController(this);
+    }
+
+    @Override
+    public View onCreateView(LayoutInflater inflater, ViewGroup container,
+            Bundle savedInstanceState) {
+        return mViewController.onCreateView(inflater, container, savedInstanceState);
+    }
+
+    @Override
+    public void onActivityCreated(Bundle savedInstanceState) {
+        super.onActivityCreated(savedInstanceState);
+        mWebViewClient.setActivity(getActivity());
+        mViewController.onActivityCreated(savedInstanceState);
+    }
+
+    // Start SecureConversationViewControllerCallbacks
+
+    @Override
+    public Handler getHandler() {
+        return mHandler;
+    }
+
+    @Override
+    public AbstractConversationWebViewClient getWebViewClient() {
+        return mWebViewClient;
+    }
+
+    @Override
+    public Fragment getFragment() {
+        return this;
+    }
+
+    @Override
+    public void setupConversationHeaderView(ConversationViewHeader headerView) {
+        // DO NOTHING
+    }
+
+    @Override
+    public boolean isViewVisibleToUser() {
+        return true;
+    }
+
+    @Override
+    public ContactLoaderCallbacks getContactInfoSource() {
+        if (mContactLoaderCallbacks == null) {
+            mContactLoaderCallbacks = new ContactLoaderCallbacks(getActivity());
+        }
+        return mContactLoaderCallbacks;
+    }
+
+    @Override
+    public ConversationAccountController getConversationAccountController() {
+        return null;
+    }
+
+    @Override
+    public Map<String, Address> getAddressCache() {
+        return mAddressCache;
+    }
+
+    @Override
+    public void setupMessageHeaderVeiledMatcher(MessageHeaderView messageHeaderView) {
+        // DO NOTHING
+    }
+
+    @Override
+    public void startMessageLoader() {
+        getLoaderManager().initLoader(MESSAGE_LOADER, null, this);
+    }
+
+    @Override
+    public String getBaseUri() {
+        return BASE_URI;
+    }
+
+    @Override
+    public boolean isViewOnlyMode() {
+        return true;
+    }
+
+    // End SecureConversationViewControllerCallbacks
+
+    // Start LoaderCallbacks
+
+    @Override
+    public Loader<ConversationMessage> onCreateLoader(int id, Bundle args) {
+        switch (id) {
+            case MESSAGE_LOADER:
+                return new EmlMessageLoader(getActivity(), mEmlFileUri);
+            default:
+                return null;
+        }
+    }
+
+    @Override
+    public void onLoadFinished(Loader<ConversationMessage> loader, ConversationMessage data) {
+        mViewController.setSubject(data.subject);
+        mViewController.renderMessage(data);
+    }
+
+    @Override
+    public void onLoaderReset(Loader<ConversationMessage> loader) {
+        // Do nothing
+    }
+
+    // End LoaderCallbacks
+}
diff --git a/src/com/android/mail/browse/EmlViewerActivity.java b/src/com/android/mail/browse/EmlViewerActivity.java
new file mode 100644
index 0000000..2524b0f
--- /dev/null
+++ b/src/com/android/mail/browse/EmlViewerActivity.java
@@ -0,0 +1,79 @@
+/*
+ * 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.ActionBar;
+import android.app.Activity;
+import android.app.FragmentManager;
+import android.app.FragmentTransaction;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.MenuItem;
+
+import com.android.mail.R;
+import com.android.mail.utils.LogTag;
+import com.android.mail.utils.LogUtils;
+import com.android.mail.utils.MimeType;
+
+public class EmlViewerActivity extends Activity {
+    private static final String LOG_TAG = LogTag.getLogTag();
+
+    private static final String FRAGMENT_TAG = "eml_message_fragment";
+
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.eml_viewer_activity);
+
+        final ActionBar actionBar = getActionBar();
+        actionBar.setDisplayHomeAsUpEnabled(true);
+
+        final Intent intent = getIntent();
+        final String action = intent.getAction();
+        final String type = intent.getType();
+
+        if (Intent.ACTION_VIEW.equals(action) &&
+                MimeType.isEmlMimeType(type)) {
+            final FragmentManager manager = getFragmentManager();
+
+            if (manager.findFragmentByTag(FRAGMENT_TAG) == null) {
+                final FragmentTransaction transaction = manager.beginTransaction();
+                transaction.add(R.id.eml_root,
+                        EmlMessageViewFragment.newInstance(intent.getData()), FRAGMENT_TAG);
+                transaction.commit();
+            }
+        } 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.
+        }
+    }
+
+
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        switch (item.getItemId()) {
+            case android.R.id.home:
+                finish();
+                return true;
+        }
+
+        return super.onOptionsItemSelected(item);
+    }
+}
diff --git a/src/com/android/mail/browse/MessageAttachmentBar.java b/src/com/android/mail/browse/MessageAttachmentBar.java
index 3c6f4b4..e61fdcf 100644
--- a/src/com/android/mail/browse/MessageAttachmentBar.java
+++ b/src/com/android/mail/browse/MessageAttachmentBar.java
@@ -283,8 +283,17 @@
         Intent intent = new Intent(Intent.ACTION_VIEW);
         intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
                 | Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
+
+        final String contentType = mAttachment.getContentType();
         Utils.setIntentDataAndTypeAndNormalize(
-                intent, mAttachment.contentUri, mAttachment.getContentType());
+                intent, mAttachment.contentUri, contentType);
+
+        // For EML files, we want to open our dedicated
+        // viewer rather than let any activity open it.
+        if (MimeType.isEmlMimeType(contentType)) {
+            intent.setClass(getContext(), EmlViewerActivity.class);
+        }
+
         try {
             getContext().startActivity(intent);
         } catch (ActivityNotFoundException e) {
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..5036818 100644
--- a/src/com/android/mail/browse/MessageHeaderView.java
+++ b/src/com/android/mail/browse/MessageHeaderView.java
@@ -50,12 +50,12 @@
 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;
 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;
 import com.android.mail.providers.UIProvider;
@@ -218,6 +218,8 @@
 
     private VeiledAddressMatcher mVeiledMatcher;
 
+    private boolean mIsViewOnlyMode = false;
+
     public interface MessageHeaderViewCallbacks {
         void setMessageSpacerHeight(MessageHeaderItem item, int newSpacerHeight);
 
@@ -231,6 +233,7 @@
         void showExternalResources(String senderRawAddress);
 
         boolean supportsMessageTransforms();
+
         String getMessageTransforms(Message msg);
     }
 
@@ -332,7 +335,7 @@
             mLeftSpacer.setVisibility(View.VISIBLE);
             mRightSpacer.setVisibility(View.VISIBLE);
         } else {
-            setBackgroundColor(android.R.color.white);
+            setBackgroundColor(getResources().getColor(android.R.color.white));
             // scrolling layer does have padding so we don't need spacers
             mLeftSpacer.setVisibility(View.GONE);
             mRightSpacer.setVisibility(View.GONE);
@@ -369,7 +372,7 @@
     }
 
     private Account getAccount() {
-        return mAccountController.getAccount();
+        return mAccountController != null ? mAccountController.getAccount() : null;
     }
 
     public void bind(MessageHeaderItem headerItem, boolean measureOnly) {
@@ -438,10 +441,14 @@
         mStarView.setContentDescription(getResources().getString(
                 mStarView.isSelected() ? R.string.remove_star : R.string.add_star));
         mStarShown = true;
-        for (Folder folder : mMessage.getConversation().getRawFolders()) {
-            if (folder.isTrash()) {
-                mStarShown = false;
-                break;
+
+        final Conversation conversation = mMessage.getConversation();
+        if (conversation != null) {
+            for (Folder folder : conversation.getRawFolders()) {
+                if (folder.isTrash()) {
+                    mStarShown = false;
+                    break;
+                }
             }
         }
 
@@ -601,7 +608,18 @@
     private void updateChildVisibility() {
         // Too bad this can't be done with an XML state list...
 
-        if (isExpanded()) {
+        if (mIsViewOnlyMode) {
+            setMessageDetailsVisibility(VISIBLE);
+
+
+            setChildVisibility(GONE, mReplyButton, mReplyAllButton, mForwardButton,
+                    mOverflowButton, mDraftIcon, mEditDraftButton, mStarView,
+                    mAttachmentIcon, mUpperDateView);
+            setChildVisibility(VISIBLE, mPhotoView, mPhotoSpacerView,
+                    mSenderEmailView);
+
+            setChildMarginRight(mTitleContainerView, 0);
+        } else if (isExpanded()) {
             int normalVis, draftVis;
 
             setMessageDetailsVisibility((mIsSnappy) ? GONE : VISIBLE);
@@ -670,8 +688,9 @@
             return;
         }
 
-        final boolean defaultReplyAll = getAccount().settings.replyBehavior
-                == UIProvider.DefaultReplyBehavior.REPLY_ALL;
+        final Account account = getAccount();
+        final boolean defaultReplyAll = (account != null) ? account.settings.replyBehavior
+                == UIProvider.DefaultReplyBehavior.REPLY_ALL : false;
         setChildVisibility(defaultReplyAll ? GONE : VISIBLE, mReplyButton);
         setChildVisibility(defaultReplyAll ? VISIBLE : GONE, mReplyAllButton);
     }
@@ -703,7 +722,8 @@
             final String address = email.getAddress();
             // Check if the address here is a veiled address.  If it is, we need to display an
             // alternate layout
-            final boolean isVeiledAddress = mVeiledMatcher.isVeiledAddress(address);
+            final boolean isVeiledAddress = mVeiledMatcher != null &&
+                    mVeiledMatcher.isVeiledAddress(address);
             final String addressShown;
             if (isVeiledAddress) {
                 // Add the warning at the end of the name, and remove the address.  The alternate
@@ -798,7 +818,7 @@
                 final Address email = getAddress(mAddressCache, rawAddrs[i]);
                 final String emailAddress = email.getAddress();
                 final String name;
-                if (mMatcher.isVeiledAddress(emailAddress)) {
+                if (mMatcher != null && mMatcher.isVeiledAddress(emailAddress)) {
                     if (TextUtils.isEmpty(email.getName())) {
                         // Let's write something more readable.
                         name = mContext.getString(VeiledAddressMatcher.VEILED_SUMMARY_UNKNOWN);
@@ -984,6 +1004,16 @@
         return handled;
     }
 
+    /**
+     * Set to true if the user should not be able to perfrom message actions
+     * on the message such as reply/reply all/forward/star/etc.
+     *
+     * Default is false.
+     */
+    public void setViewOnlyMode(boolean isViewOnlyMode) {
+        mIsViewOnlyMode = isViewOnlyMode;
+    }
+
     public void setExpandable(boolean expandable) {
         mExpandable = expandable;
     }
@@ -1195,7 +1225,11 @@
                 if (mMessageHeaderItem != null) {
                     mMessageHeaderItem.setShowImages(true);
                 }
-                showImagePromptAlways(false);
+                if (mIsViewOnlyMode) {
+                    hideShowImagePrompt();
+                } else {
+                    showImagePromptAlways(false);
+                }
                 break;
             case SHOW_IMAGE_PROMPT_ALWAYS:
                 mMessage.markAlwaysShowImages(getQueryHandler(), 0 /* token */, null /* cookie */);
@@ -1234,8 +1268,10 @@
         }
         if (!mCollapsedDetailsValid) {
             if (mMessageHeaderItem.recipientSummaryText == null) {
+                final Account account = getAccount();
+                final String name = (account != null) ? account.name : "";
                 mMessageHeaderItem.recipientSummaryText = getRecipientSummaryText(getContext(),
-                        getAccount().name, mMyName, mTo, mCc, mBcc, mAddressCache, mVeiledMatcher);
+                        name, mMyName, mTo, mCc, mBcc, mAddressCache, mVeiledMatcher);
             }
             ((TextView) findViewById(R.id.recipients_summary))
                     .setText(mMessageHeaderItem.recipientSummaryText);
diff --git a/src/com/android/mail/browse/SelectedConversationsActionMenu.java b/src/com/android/mail/browse/SelectedConversationsActionMenu.java
index de3c968..604be37 100644
--- a/src/com/android/mail/browse/SelectedConversationsActionMenu.java
+++ b/src/com/android/mail/browse/SelectedConversationsActionMenu.java
@@ -19,6 +19,7 @@
 
 import android.content.Context;
 import android.net.Uri;
+import android.os.AsyncTask;
 import android.view.ActionMode;
 import android.view.Menu;
 import android.view.MenuInflater;
@@ -43,14 +44,17 @@
 import com.android.mail.ui.ConversationSetObserver;
 import com.android.mail.ui.ConversationUpdater;
 import com.android.mail.ui.DestructiveAction;
+import com.android.mail.ui.FolderOperation;
 import com.android.mail.ui.FolderSelectionDialog;
 import com.android.mail.ui.MailActionBarView;
 import com.android.mail.utils.LogTag;
 import com.android.mail.utils.LogUtils;
 import com.android.mail.utils.Utils;
 import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.Lists;
 
 import java.util.Collection;
+import java.util.List;
 
 /**
  * A component that displays a custom view for an {@code ActionBar}'s {@code
@@ -198,6 +202,25 @@
                     }
                 }
                 break;
+            case R.id.move_to_inbox:
+                new AsyncTask<Void, Void, Folder>() {
+                    @Override
+                    protected Folder doInBackground(final Void... params) {
+                        // Get the "move to" inbox
+                        return Utils.getFolder(mContext, mAccount.settings.moveToInbox,
+                                true /* allowHidden */);
+                    }
+
+                    @Override
+                    protected void onPostExecute(final Folder moveToInbox) {
+                        final List<FolderOperation> ops = Lists.newArrayListWithCapacity(1);
+                        // Add inbox
+                        ops.add(new FolderOperation(moveToInbox, true));
+                        mUpdater.assignFolder(ops, mSelectionSet.values(), true,
+                                true /* showUndo */, false /* isMoveTo */);
+                    }
+                }.execute((Void[]) null);
+                break;
             case R.id.mark_important:
                 markConversationsImportant(true);
                 break;
@@ -382,13 +405,18 @@
         // archive icon if the setting for that is true.
         final MenuItem removeFolder = menu.findItem(R.id.remove_folder);
         final MenuItem moveTo = menu.findItem(R.id.move_to);
+        final MenuItem moveToInbox = menu.findItem(R.id.move_to_inbox);
         final boolean showRemoveFolder = mFolder != null && mFolder.isType(FolderType.DEFAULT)
                 && mFolder.supportsCapability(FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES)
                 && !mFolder.isProviderFolder();
         final boolean showMoveTo = mFolder != null
                 && mFolder.supportsCapability(FolderCapabilities.ALLOWS_REMOVE_CONVERSATION);
+        final boolean showMoveToInbox = mFolder != null
+                && mFolder.supportsCapability(FolderCapabilities.ALLOWS_MOVE_TO_INBOX);
         removeFolder.setVisible(showRemoveFolder);
         moveTo.setVisible(showMoveTo);
+        moveToInbox.setVisible(showMoveToInbox);
+
         if (mFolder != null && showRemoveFolder) {
             removeFolder.setTitle(mActivity.getActivityContext().getString(R.string.remove_folder,
                     mFolder.name));
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 d8ec3cf..ea588bc 100644
--- a/src/com/android/mail/preferences/MailPrefs.java
+++ b/src/com/android/mail/preferences/MailPrefs.java
@@ -20,13 +20,15 @@
 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;
 import com.google.common.collect.ImmutableSet;
 
+import java.util.Collections;
+import java.util.List;
 import java.util.Set;
+import java.util.regex.Pattern;
 
 /**
  * A high-level API to store and retrieve unified mail preferences.
@@ -35,7 +37,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";
 
@@ -71,12 +73,17 @@
         private static final String
                 CONVERSATION_PHOTO_TEASER_SHOWN = "conversation-photo-teaser-shown-two";
 
+        public static final String DISPLAY_IMAGES = "display_images";
+        public static final String DISPLAY_IMAGES_PATTERNS = "display_sender_images_patterns_set";
+
         public static final ImmutableSet<String> BACKUP_KEYS =
                 new ImmutableSet.Builder<String>()
                 .add(DEFAULT_REPLY_ALL)
                 .add(CONVERSATION_LIST_SWIPE)
                 .add(REMOVAL_ACTION)
                 .add(REMOVAL_ACTION_DIALOG_SHOWN)
+                .add(DISPLAY_IMAGES)
+                .add(DISPLAY_IMAGES_PATTERNS)
                 .build();
 
     }
@@ -164,7 +171,7 @@
 
     public void setDefaultReplyAll(final boolean replyAll) {
         getEditor().putBoolean(PreferenceKeys.DEFAULT_REPLY_ALL, replyAll).apply();
-        MailIntentService.broadcastBackupDataChanged(getContext());
+        notifyBackupPreferenceChanged();
     }
 
     /**
@@ -185,7 +192,7 @@
      */
     public void setRemovalAction(final String removalAction) {
         getEditor().putString(PreferenceKeys.REMOVAL_ACTION, removalAction).apply();
-        MailIntentService.broadcastBackupDataChanged(getContext());
+        notifyBackupPreferenceChanged();
     }
 
     /**
@@ -198,7 +205,7 @@
 
     public void setConversationListSwipeEnabled(final boolean enabled) {
         getEditor().putBoolean(PreferenceKeys.CONVERSATION_LIST_SWIPE, enabled).apply();
-        MailIntentService.broadcastBackupDataChanged(getContext());
+        notifyBackupPreferenceChanged();
     }
 
     /**
@@ -267,6 +274,93 @@
 
     public void setRemovalActionDialogShown() {
         getEditor().putBoolean(PreferenceKeys.REMOVAL_ACTION_DIALOG_SHOWN, true).apply();
-        MailIntentService.broadcastBackupDataChanged(getContext());
+        notifyBackupPreferenceChanged();
+    }
+
+    void setSenderWhitelist(Set<String> addresses) {
+        getEditor().putStringSet(PreferenceKeys.DISPLAY_IMAGES, addresses).apply();
+        notifyBackupPreferenceChanged();
+    }
+    void setSenderWhitelistPatterns(Set<String> patterns) {
+        getEditor().putStringSet(PreferenceKeys.DISPLAY_IMAGES_PATTERNS, patterns).apply();
+        notifyBackupPreferenceChanged();
+    }
+
+    /**
+     * Returns whether or not an email address is in the whitelist of senders to show images for.
+     * This method reads the entire whitelist, so if you have multiple emails to check, you should
+     * probably call getSenderWhitelist() and check membership yourself.
+     *
+     * @param sender raw email address ("foo@bar.com")
+     * @return whether we should show pictures for this sender
+     */
+    public boolean getDisplayImagesFromSender(String sender) {
+        boolean displayImages = getSenderWhitelist().contains(sender);
+        if (!displayImages) {
+            final SharedPreferences sharedPreferences = getSharedPreferences();
+            // Check the saved email address patterns to determine if this pattern matches
+            final Set<String> defaultPatternSet = Collections.emptySet();
+            final Set<String> currentPatterns = sharedPreferences.getStringSet(
+                        PreferenceKeys.DISPLAY_IMAGES_PATTERNS, defaultPatternSet);
+            for (String pattern : currentPatterns) {
+                displayImages = Pattern.compile(pattern).matcher(sender).matches();
+                if (displayImages) {
+                    break;
+                }
+            }
+        }
+
+        return displayImages;
+    }
+
+
+    public void setDisplayImagesFromSender(String sender, List<Pattern> allowedPatterns) {
+        if (allowedPatterns != null) {
+            // Look at the list of patterns where we want to allow a particular class of
+            // email address
+            for (Pattern pattern : allowedPatterns) {
+                if (pattern.matcher(sender).matches()) {
+                    // The specified email address matches one of the social network patterns.
+                    // Save the pattern itself
+                    final Set<String> currentPatterns = getSenderWhitelistPatterns();
+                    final String patternRegex = pattern.pattern();
+                    if (!currentPatterns.contains(patternRegex)) {
+                        currentPatterns.add(patternRegex);
+                        setSenderWhitelistPatterns(currentPatterns);
+                    }
+                    return;
+                }
+            }
+        }
+        final Set<String> whitelist = getSenderWhitelist();
+        if (!whitelist.contains(sender)) {
+            // Storing a JSONObject is slightly more nice in that maps are guaranteed to not have
+            // duplicate entries, but using a Set as intermediate representation guarantees this
+            // for us anyway. Also, using maps to represent sets forces you to pick values for
+            // them, and that's weird.
+            whitelist.add(sender);
+            setSenderWhitelist(whitelist);
+        }
+    }
+
+    private Set<String> getSenderWhitelist() {
+        final SharedPreferences sharedPreferences = getSharedPreferences();
+        final Set<String> defaultAddressSet = Collections.emptySet();
+        return sharedPreferences.getStringSet(PreferenceKeys.DISPLAY_IMAGES, defaultAddressSet);
+    }
+
+
+    private Set<String> getSenderWhitelistPatterns() {
+        final SharedPreferences sharedPreferences = getSharedPreferences();
+        final Set<String> defaultPatternSet = Collections.emptySet();
+        return sharedPreferences.getStringSet(PreferenceKeys.DISPLAY_IMAGES_PATTERNS,
+                defaultPatternSet);
+    }
+
+    public void clearSenderWhiteList() {
+        final SharedPreferences.Editor editor = getEditor();
+        editor.putStringSet(PreferenceKeys.DISPLAY_IMAGES, Collections.EMPTY_SET);
+        editor.putStringSet(PreferenceKeys.DISPLAY_IMAGES_PATTERNS, Collections.EMPTY_SET);
+        editor.apply();
     }
 }
diff --git a/src/com/android/mail/preferences/VersionedPrefs.java b/src/com/android/mail/preferences/VersionedPrefs.java
index b5dd9c9..bc50d98 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;
 
@@ -49,7 +51,7 @@
      * The current version number for {@link SharedPreferences}. This is a constant for all
      * applications based on UnifiedEmail.
      */
-    protected static final int CURRENT_VERSION_NUMBER = 1;
+    protected static final int CURRENT_VERSION_NUMBER = 2;
 
     protected static final String LOG_TAG = LogTag.getLogTag();
 
@@ -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/Account.java b/src/com/android/mail/providers/Account.java
index 391ffe0..256b30a 100644
--- a/src/com/android/mail/providers/Account.java
+++ b/src/com/android/mail/providers/Account.java
@@ -756,6 +756,7 @@
                 settings.conversationViewMode);
         map.put(AccountColumns.SettingsColumns.VEILED_ADDRESS_PATTERN,
                 settings.veiledAddressPattern);
+        map.put(AccountColumns.SettingsColumns.MOVE_TO_INBOX, settings.moveToInbox);
 
         return map;
     }
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/Settings.java b/src/com/android/mail/providers/Settings.java
index 9a8fddd..d07a60e 100644
--- a/src/com/android/mail/providers/Settings.java
+++ b/src/com/android/mail/providers/Settings.java
@@ -89,6 +89,12 @@
     public final Uri setupIntentUri;
     public final String veiledAddressPattern;
 
+    /**
+     * The {@link Uri} to use when moving a conversation to the inbox. May
+     * differ from {@link #defaultInbox}.
+     */
+    public final Uri moveToInbox;
+
     /** Cached value of hashCode */
     private int mHashCode;
 
@@ -114,6 +120,7 @@
         setupIntentUri = Uri.EMPTY;
         conversationViewMode = UIProvider.ConversationViewMode.UNDEFINED;
         veiledAddressPattern = null;
+        moveToInbox = Uri.EMPTY;
     }
 
     public Settings(Parcel inParcel) {
@@ -135,6 +142,7 @@
         setupIntentUri = Utils.getValidUri(inParcel.readString());
         conversationViewMode = inParcel.readInt();
         veiledAddressPattern = inParcel.readString();
+        moveToInbox = Utils.getValidUri(inParcel.readString());
     }
 
     public Settings(Cursor cursor) {
@@ -164,6 +172,8 @@
                 cursor.getInt(cursor.getColumnIndex(SettingsColumns.CONVERSATION_VIEW_MODE));
         veiledAddressPattern =
                 cursor.getString(cursor.getColumnIndex(SettingsColumns.VEILED_ADDRESS_PATTERN));
+        moveToInbox = Utils.getValidUri(
+                cursor.getString(cursor.getColumnIndex(SettingsColumns.MOVE_TO_INBOX)));
     }
 
     private Settings(JSONObject json) {
@@ -190,6 +200,7 @@
         conversationViewMode = json.optInt(SettingsColumns.CONVERSATION_VIEW_MODE,
                 UIProvider.ConversationViewMode.UNDEFINED);
         veiledAddressPattern = json.optString(SettingsColumns.VEILED_ADDRESS_PATTERN, null);
+        moveToInbox = Utils.getValidUri(json.optString(SettingsColumns.MOVE_TO_INBOX));
     }
 
     /**
@@ -232,6 +243,8 @@
             json.put(SettingsColumns.SETUP_INTENT_URI, setupIntentUri);
             json.put(SettingsColumns.CONVERSATION_VIEW_MODE, conversationViewMode);
             json.put(SettingsColumns.VEILED_ADDRESS_PATTERN, veiledAddressPattern);
+            json.put(SettingsColumns.MOVE_TO_INBOX,
+                    getNonNull(moveToInbox, sDefault.moveToInbox));
         } catch (JSONException e) {
             LogUtils.wtf(LOG_TAG, e, "Could not serialize settings");
         }
@@ -298,6 +311,7 @@
         dest.writeString(((Uri) getNonNull(setupIntentUri, sDefault.setupIntentUri)).toString());
         dest.writeInt(conversationViewMode);
         dest.writeString(veiledAddressPattern);
+        dest.writeString(((Uri) getNonNull(moveToInbox, sDefault.moveToInbox)).toString());
     }
 
     /**
@@ -403,7 +417,8 @@
                 && priorityArrowsEnabled == that.priorityArrowsEnabled
                 && setupIntentUri == that.setupIntentUri
                 && conversationViewMode == that.conversationViewMode
-                && TextUtils.equals(veiledAddressPattern, that.veiledAddressPattern));
+                && TextUtils.equals(veiledAddressPattern, that.veiledAddressPattern))
+                && Objects.equal(moveToInbox, that.moveToInbox);
     }
 
     @Override
@@ -423,6 +438,6 @@
                         snapHeaders, replyBehavior, convListIcon, confirmDelete, confirmArchive,
                         confirmSend, defaultInbox, forceReplyFromDefault, maxAttachmentSize, swipe,
                         priorityArrowsEnabled, setupIntentUri, conversationViewMode,
-                        veiledAddressPattern);
+                        veiledAddressPattern, moveToInbox);
     }
 }
diff --git a/src/com/android/mail/providers/UIProvider.java b/src/com/android/mail/providers/UIProvider.java
index a41f8a0..7fd379d 100644
--- a/src/com/android/mail/providers/UIProvider.java
+++ b/src/com/android/mail/providers/UIProvider.java
@@ -170,6 +170,7 @@
             .put(AccountColumns.SettingsColumns.VEILED_ADDRESS_PATTERN, String.class)
             .put(AccountColumns.UPDATE_SETTINGS_URI, String.class)
             .put(AccountColumns.ENABLE_MESSAGE_TRANSFORMS, Integer.class)
+            .put(AccountColumns.SettingsColumns.MOVE_TO_INBOX, String.class)
             .build();
 
     public static final Map<String, Class<?>> ACCOUNTS_COLUMNS =
@@ -557,6 +558,11 @@
              * constants from  {@link ConversationViewMode}
              */
             public static final String CONVERSATION_VIEW_MODE = "conversation_view_mode";
+            /**
+             * String containing the URI for the inbox conversations should be moved to for this
+             * account.
+             */
+            public static final String MOVE_TO_INBOX = "move_to_inbox";
         }
     }
 
@@ -577,11 +583,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() {}
     }
 
@@ -681,9 +682,9 @@
         public static final int STARRED = 1 << 7;
         /** Any other system label that we do not have a specific name for. */
         public static final int OTHER_PROVIDER_FOLDER = 1 << 8;
-        /** All mail folder **/
+        /** All mail folder */
         public static final int ALL_MAIL = 1 << 9;
-        /** Gmail's inbox sections **/
+        /** Gmail's inbox sections */
         public static final int INBOX_SECTION = 1 << 10;
     }
 
@@ -758,6 +759,12 @@
          * {@link com.android.mail.ui.MultiFoldersSelectionDialog}).
          */
         public static final int MULTI_MOVE = 0x8000;
+
+        /**
+         * This flag indicates that a conversation may be moved from this folder into the account's
+         * inbox.
+         */
+        public static final int ALLOWS_MOVE_TO_INBOX = 0x10000;
     }
 
     public static final class FolderColumns {
@@ -901,6 +908,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;
@@ -1943,6 +1957,11 @@
      */
     public static final String FORCE_UI_NOTIFICATIONS_QUERY_PARAMETER = "forceUiNotifications";
 
+    /**
+     * Parameter used to allow returning hidden folders.
+     */
+    public static final String ALLOW_HIDDEN_FOLDERS_QUERY_PARAM = "allowHiddenFolders";
+
     public static final String AUTO_ADVANCE_MODE_OLDER = "older";
     public static final String AUTO_ADVANCE_MODE_NEWER = "newer";
     public static final String AUTO_ADVANCE_MODE_LIST = "list";
diff --git a/src/com/android/mail/ui/AbstractActivityController.java b/src/com/android/mail/ui/AbstractActivityController.java
index 7bc6797..d91a450 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;
@@ -268,8 +268,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();
 
@@ -286,17 +287,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
@@ -315,7 +400,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.*/
@@ -553,12 +640,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);
     }
 
@@ -694,8 +780,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
@@ -717,6 +808,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
@@ -905,6 +1001,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) {
@@ -1017,6 +1124,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();
@@ -1079,7 +1224,7 @@
         // Sync the toggle state after onRestoreInstanceState has occurred.
         mDrawerToggle.syncState();
 
-        mHideMenuItems = isDrawerEnabled() ? mDrawerContainer.isDrawerOpen(mDrawerPullout) : false;
+        mHideMenuItems = isDrawerEnabled() && mDrawerContainer.isDrawerOpen(mDrawerPullout);
     }
 
     @Override
@@ -1249,6 +1394,25 @@
                     dialog.show();
                 }
                 break;
+            case R.id.move_to_inbox:
+                new AsyncTask<Void, Void, Folder>() {
+                    @Override
+                    protected Folder doInBackground(final Void... params) {
+                        // Get the "move to" inbox
+                        return Utils.getFolder(mContext, mAccount.settings.moveToInbox,
+                                true /* allowHidden */);
+                    }
+
+                    @Override
+                    protected void onPostExecute(final Folder moveToInbox) {
+                        final List<FolderOperation> ops = Lists.newArrayListWithCapacity(1);
+                        // Add inbox
+                        ops.add(new FolderOperation(moveToInbox, true));
+                        assignFolder(ops, Conversation.listOf(mCurrentConversation), true,
+                                true /* showUndo */, false /* isMoveTo */);
+                    }
+                }.execute((Void[]) null);
+                break;
             case R.id.empty_trash:
                 showEmptyDialog();
                 break;
@@ -2061,6 +2225,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) {
@@ -3098,6 +3266,9 @@
         return in != null && in.isVisible() && mActivity.hasWindowFocus();
     }
 
+    /**
+     * This class handles callbacks that create a {@link ConversationCursor}.
+     */
     private class ConversationListLoaderCallbacks implements
         LoaderManager.LoaderCallbacks<ConversationCursor> {
 
@@ -3355,6 +3526,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;
@@ -3389,13 +3561,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
@@ -3422,6 +3591,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..29cedef 100644
--- a/src/com/android/mail/ui/AbstractConversationViewFragment.java
+++ b/src/com/android/mail/ui/AbstractConversationViewFragment.java
@@ -17,46 +17,25 @@
 
 package com.android.mail.ui;
 
-import android.animation.Animator;
-import android.animation.AnimatorInflater;
-import android.animation.AnimatorListenerAdapter;
 import android.app.Activity;
 import android.app.Fragment;
 import android.app.LoaderManager;
-import android.content.ActivityNotFoundException;
 import android.content.Context;
-import android.content.Intent;
 import android.content.Loader;
-import android.content.pm.ActivityInfo;
-import android.content.pm.PackageManager;
-import android.content.pm.ResolveInfo;
-import android.content.res.Resources;
 import android.database.Cursor;
-import android.database.DataSetObservable;
-import android.database.DataSetObserver;
 import android.net.Uri;
 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.WebView;
-import android.webkit.WebViewClient;
 
-import com.android.mail.ContactInfo;
-import com.android.mail.ContactInfoSource;
-import com.android.mail.FormattedDateBuilder;
 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;
 import com.android.mail.providers.Account;
@@ -70,17 +49,15 @@
 import com.android.mail.utils.LogTag;
 import com.android.mail.utils.LogUtils;
 import com.android.mail.utils.Utils;
-import com.google.common.collect.ImmutableMap;
 
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashMap;
-import java.util.List;
 import java.util.Map;
-import java.util.Set;
+
 
 public abstract class AbstractConversationViewFragment extends Fragment implements
-        ConversationController, ConversationAccountController, MessageHeaderViewCallbacks,
+        ConversationController, ConversationAccountController,
         ConversationViewHeaderCallbacks {
 
     private static final String ARG_ACCOUNT = "account";
@@ -89,17 +66,20 @@
     private static final String LOG_TAG = LogTag.getLogTag();
     protected static final int MESSAGE_LOADER = 0;
     protected static final int CONTACT_LOADER = 1;
-    private static int sMinDelay = -1;
-    private static int sMinShowTime = -1;
     protected ControllableActivity mActivity;
     private final MessageLoaderCallbacks mMessageLoaderCallbacks = new MessageLoaderCallbacks();
-    protected FormattedDateBuilder mDateBuilder;
-    private final ContactLoaderCallbacks mContactLoaderCallbacks = new ContactLoaderCallbacks();
+    private ContactLoaderCallbacks mContactLoaderCallbacks;
     private MenuItem mChangeFoldersMenuItem;
     protected Conversation mConversation;
     protected Folder mFolder;
     protected String mBaseUri;
     protected Account mAccount;
+
+    /**
+     * Must be instantiated in a derived class's onCreate.
+     */
+    protected AbstractConversationWebViewClient mWebViewClient;
+
     /**
      * Cache of email address strings to parsed Address objects.
      * <p>
@@ -115,8 +95,7 @@
      * this flag is saved and restored.
      */
     private boolean mUserVisible;
-    private View mProgressView;
-    private View mBackgroundView;
+
     private final Handler mHandler = new Handler();
     /** True if we want to avoid marking the conversation as viewed and read. */
     private boolean mSuppressMarkingViewed;
@@ -126,26 +105,17 @@
      */
     protected ConversationViewState mViewState;
 
-    private long mLoadingShownTime = -1;
-
     private boolean mIsDetached;
 
     private boolean mHasConversationBeenTransformed;
     private boolean mHasConversationTransformBeenReverted;
 
-    private final Runnable mDelayedShow = new FragmentRunnable("mDelayedShow") {
-        @Override
-        public void go() {
-            mLoadingShownTime = System.currentTimeMillis();
-            mProgressView.setVisibility(View.VISIBLE);
-        }
-    };
-
     private final AccountObserver mAccountObserver = new AccountObserver() {
         @Override
         public void onChanged(Account newAccount) {
             final Account oldAccount = mAccount;
             mAccount = newAccount;
+            mWebViewClient.setAccount(mAccount);
             onAccountChanged(newAccount, oldAccount);
         }
     };
@@ -255,81 +225,6 @@
         return "(" + s + " conv=" + mConversation + ")";
     }
 
-    protected abstract WebView getWebView();
-
-    public void instantiateProgressIndicators(View rootView) {
-        mBackgroundView = rootView.findViewById(R.id.background_view);
-        mProgressView = rootView.findViewById(R.id.loading_progress);
-    }
-
-    protected void dismissLoadingStatus() {
-        dismissLoadingStatus(null);
-    }
-
-    /**
-     * Begin the fade-out animation to hide the Progress overlay, either immediately or after some
-     * timeout (to ensure that the progress minimum time elapses).
-     *
-     * @param doAfter an optional Runnable action to execute after the animation completes
-     */
-    protected void dismissLoadingStatus(final Runnable doAfter) {
-        if (mLoadingShownTime == -1) {
-            // The runnable hasn't run yet, so just remove it.
-            mHandler.removeCallbacks(mDelayedShow);
-            dismiss(doAfter);
-            return;
-        }
-        final long diff = Math.abs(System.currentTimeMillis() - mLoadingShownTime);
-        if (diff > sMinShowTime) {
-            dismiss(doAfter);
-        } else {
-            mHandler.postDelayed(new FragmentRunnable("dismissLoadingStatus") {
-                @Override
-                public void go() {
-                    dismiss(doAfter);
-                }
-            }, Math.abs(sMinShowTime - diff));
-        }
-    }
-
-    private void dismiss(final Runnable doAfter) {
-        // Reset loading shown time.
-        mLoadingShownTime = -1;
-        mProgressView.setVisibility(View.GONE);
-        if (mBackgroundView.getVisibility() == View.VISIBLE) {
-            animateDismiss(doAfter);
-        } else {
-            if (doAfter != null) {
-                doAfter.run();
-            }
-        }
-    }
-
-    private void animateDismiss(final Runnable doAfter) {
-        // the animation can only work (and is only worth doing) if this fragment is added
-        // reasons it may not be added: fragment is being destroyed, or in the process of being
-        // restored
-        if (!isAdded()) {
-            mBackgroundView.setVisibility(View.GONE);
-            return;
-        }
-
-        Utils.enableHardwareLayer(mBackgroundView);
-        final Animator animator = AnimatorInflater.loadAnimator(getContext(), R.anim.fade_out);
-        animator.setTarget(mBackgroundView);
-        animator.addListener(new AnimatorListenerAdapter() {
-            @Override
-            public void onAnimationEnd(Animator animation) {
-                mBackgroundView.setVisibility(View.GONE);
-                mBackgroundView.setLayerType(View.LAYER_TYPE_NONE, null);
-                if (doAfter != null) {
-                    doAfter.run();
-                }
-            }
-        });
-        animator.start();
-    }
-
     @Override
     public void onActivityCreated(Bundle savedInstanceState) {
         super.onActivityCreated(savedInstanceState);
@@ -344,8 +239,9 @@
         }
         mActivity = (ControllableActivity) activity;
         mContext = activity.getApplicationContext();
-        mDateBuilder = new FormattedDateBuilder((Context) mActivity);
+        mWebViewClient.setActivity(activity);
         mAccount = mAccountObserver.initialize(mActivity.getAccountController());
+        mWebViewClient.setAccount(mAccount);
     }
 
     @Override
@@ -354,23 +250,6 @@
         return activity != null ? activity.getConversationUpdater() : null;
     }
 
-
-    protected void showLoadingStatus() {
-        if (!mUserVisible) {
-            return;
-        }
-        if (sMinDelay == -1) {
-            Resources res = getContext().getResources();
-            sMinDelay = res.getInteger(R.integer.conversationview_show_loading_delay);
-            sMinShowTime = res.getInteger(R.integer.conversationview_min_show_loading);
-        }
-        // If the loading view isn't already showing, show it and remove any
-        // pending calls to show the loading screen.
-        mBackgroundView.setVisibility(View.VISIBLE);
-        mHandler.removeCallbacks(mDelayedShow);
-        mHandler.postDelayed(mDelayedShow, sMinDelay);
-    }
-
     public Context getContext() {
         return mContext;
     }
@@ -394,6 +273,9 @@
     }
 
     public ContactLoaderCallbacks getContactInfoSource() {
+        if (mContactLoaderCallbacks == null) {
+            mContactLoaderCallbacks = new ContactLoaderCallbacks(mActivity.getActivityContext());
+        }
         return mContactLoaderCallbacks;
     }
 
@@ -421,7 +303,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;
         }
@@ -447,6 +329,8 @@
                 mHasConversationBeenTransformed && !mHasConversationTransformBeenReverted);
     }
 
+    abstract boolean supportsMessageTransforms();
+
     // BEGIN conversation header callbacks
     @Override
     public void onFoldersClicked() {
@@ -591,7 +475,7 @@
     }
 
     private void popOut() {
-        mHandler.post(new FragmentRunnable("popOut") {
+        mHandler.post(new FragmentRunnable("popOut", this) {
             @Override
             public void go() {
                 if (mActivity != null) {
@@ -687,154 +571,8 @@
         }
     }
 
-    /**
-     * Inner class to to asynchronously load contact data for all senders in the conversation,
-     * and notify observers when the data is ready.
-     *
-     */
-    protected class ContactLoaderCallbacks implements ContactInfoSource,
-            LoaderManager.LoaderCallbacks<ImmutableMap<String, ContactInfo>> {
-
-        private Set<String> mSenders;
-        private ImmutableMap<String, ContactInfo> mContactInfoMap;
-        private DataSetObservable mObservable = new DataSetObservable();
-
-        public void setSenders(Set<String> emailAddresses) {
-            mSenders = emailAddresses;
-        }
-
-        @Override
-        public Loader<ImmutableMap<String, ContactInfo>> onCreateLoader(int id, Bundle args) {
-            return new SenderInfoLoader(mActivity.getActivityContext(), mSenders);
-        }
-
-        @Override
-        public void onLoadFinished(Loader<ImmutableMap<String, ContactInfo>> loader,
-                ImmutableMap<String, ContactInfo> data) {
-            mContactInfoMap = data;
-            mObservable.notifyChanged();
-        }
-
-        @Override
-        public void onLoaderReset(Loader<ImmutableMap<String, ContactInfo>> loader) {
-        }
-
-        @Override
-        public ContactInfo getContactInfo(String email) {
-            if (mContactInfoMap == null) {
-                return null;
-            }
-            return mContactInfoMap.get(email);
-        }
-
-        @Override
-        public void registerObserver(DataSetObserver observer) {
-            mObservable.registerObserver(observer);
-        }
-
-        @Override
-        public void unregisterObserver(DataSetObserver observer) {
-            mObservable.unregisterObserver(observer);
-        }
-    }
-
-    protected class AbstractConversationWebViewClient extends WebViewClient {
-        @Override
-        public boolean shouldOverrideUrlLoading(WebView view, String url) {
-            final Activity activity = getActivity();
-            if (activity == null) {
-                return false;
-            }
-
-            boolean result = false;
-            final Intent intent;
-            Uri uri = Uri.parse(url);
-            if (!Utils.isEmpty(mAccount.viewIntentProxyUri)) {
-                intent = generateProxyIntent(uri);
-            } else {
-                intent = new Intent(Intent.ACTION_VIEW, uri);
-                intent.putExtra(Browser.EXTRA_APPLICATION_ID, activity.getPackageName());
-            }
-
-            try {
-                intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
-                activity.startActivity(intent);
-                result = true;
-            } catch (ActivityNotFoundException ex) {
-                // If no application can handle the URL, assume that the
-                // caller can handle it.
-            }
-
-            return result;
-        }
-
-        private Intent generateProxyIntent(Uri uri) {
-            final Intent intent = new Intent(Intent.ACTION_VIEW, mAccount.viewIntentProxyUri);
-            intent.putExtra(UIProvider.ViewProxyExtras.EXTRA_ORIGINAL_URI, uri);
-            intent.putExtra(UIProvider.ViewProxyExtras.EXTRA_ACCOUNT, mAccount);
-
-            final Context context = getContext();
-            PackageManager manager = null;
-            // We need to catch the exception to make CanvasConversationHeaderView
-            // test pass.  Bug: http://b/issue?id=3470653.
-            try {
-                manager = context.getPackageManager();
-            } catch (UnsupportedOperationException e) {
-                LogUtils.e(LOG_TAG, e, "Error getting package manager");
-            }
-
-            if (manager != null) {
-                // Try and resolve the intent, to find an activity from this package
-                final List<ResolveInfo> resolvedActivities = manager.queryIntentActivities(
-                        intent, PackageManager.MATCH_DEFAULT_ONLY);
-
-                final String packageName = context.getPackageName();
-
-                // Now try and find one that came from this package, if one is not found, the UI
-                // provider must have specified an intent that is to be handled by a different apk.
-                // In that case, the class name will not be set on the intent, so the default
-                // intent resolution will be used.
-                for (ResolveInfo resolveInfo: resolvedActivities) {
-                    final ActivityInfo activityInfo = resolveInfo.activityInfo;
-                    if (packageName.equals(activityInfo.packageName)) {
-                        intent.setClassName(activityInfo.packageName, activityInfo.name);
-                        break;
-                    }
-                }
-            }
-
-            return intent;
-        }
-    }
-
     public abstract void onConversationUpdated(Conversation conversation);
 
-    /**
-     * Small Runnable-like wrapper that first checks that the Fragment is in a good state before
-     * doing any work. Ideal for use with a {@link Handler}.
-     */
-    protected abstract class FragmentRunnable implements Runnable {
-
-        private final String mOpName;
-
-        public FragmentRunnable(String opName) {
-            mOpName = opName;
-        }
-
-        public abstract void go();
-
-        @Override
-        public void run() {
-            if (!isAdded()) {
-                LogUtils.i(LOG_TAG, "Unable to run op='%s' b/c fragment is not attached: %s",
-                        mOpName, AbstractConversationViewFragment.this);
-                return;
-            }
-            go();
-        }
-
-    }
-
     public void onDetachedModeEntered() {
         // If we have no messages, then we have nothing to display, so leave this view.
         // Otherwise, just set the detached flag.
@@ -854,7 +592,7 @@
      */
     public void onConversationTransformed() {
         mHasConversationBeenTransformed = true;
-        mHandler.post(new FragmentRunnable("invalidateOptionsMenu") {
+        mHandler.post(new FragmentRunnable("invalidateOptionsMenu", this) {
             @Override
             public void go() {
                 mActivity.invalidateOptionsMenu();
diff --git a/src/com/android/mail/ui/AbstractConversationWebViewClient.java b/src/com/android/mail/ui/AbstractConversationWebViewClient.java
new file mode 100644
index 0000000..0684b0c
--- /dev/null
+++ b/src/com/android/mail/ui/AbstractConversationWebViewClient.java
@@ -0,0 +1,126 @@
+/*
+ * 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.ui;
+
+import android.app.Activity;
+import android.content.ActivityNotFoundException;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.net.Uri;
+import android.provider.Browser;
+import android.webkit.WebView;
+import android.webkit.WebViewClient;
+
+import com.android.mail.providers.Account;
+import com.android.mail.providers.UIProvider;
+import com.android.mail.utils.LogTag;
+import com.android.mail.utils.LogUtils;
+import com.android.mail.utils.Utils;
+
+import java.util.List;
+
+/**
+ * Base implementation of a web view client for the conversation views.
+ * Handles proxying the view intent so that additional information can
+ * be sent with the intent when links are clicked.
+ */
+public class AbstractConversationWebViewClient extends WebViewClient {
+    private static final String LOG_TAG = LogTag.getLogTag();
+
+    private Account mAccount;
+    private Activity mActivity;
+
+    public AbstractConversationWebViewClient(Account account) {
+        mAccount = account;
+    }
+
+    public void setAccount(Account account) {
+        mAccount = account;
+    }
+
+    public void setActivity(Activity activity) {
+        mActivity = activity;
+    }
+
+    @Override
+    public boolean shouldOverrideUrlLoading(WebView view, String url) {
+        if (mActivity == null) {
+            return false;
+        }
+
+        boolean result = false;
+        final Intent intent;
+        Uri uri = Uri.parse(url);
+        if (mAccount != null && !Utils.isEmpty(mAccount.viewIntentProxyUri)) {
+            intent = generateProxyIntent(uri);
+        } else {
+            intent = new Intent(Intent.ACTION_VIEW, uri);
+            intent.putExtra(Browser.EXTRA_APPLICATION_ID, mActivity.getPackageName());
+        }
+
+        try {
+            intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
+            mActivity.startActivity(intent);
+            result = true;
+        } catch (ActivityNotFoundException ex) {
+            // If no application can handle the URL, assume that the
+            // caller can handle it.
+        }
+
+        return result;
+    }
+
+    private Intent generateProxyIntent(Uri uri) {
+        final Intent intent = new Intent(Intent.ACTION_VIEW, mAccount.viewIntentProxyUri);
+        intent.putExtra(UIProvider.ViewProxyExtras.EXTRA_ORIGINAL_URI, uri);
+        intent.putExtra(UIProvider.ViewProxyExtras.EXTRA_ACCOUNT, mAccount);
+
+        PackageManager manager = null;
+        // We need to catch the exception to make CanvasConversationHeaderView
+        // test pass.  Bug: http://b/issue?id=3470653.
+        try {
+            manager = mActivity.getPackageManager();
+        } catch (UnsupportedOperationException e) {
+            LogUtils.e(LOG_TAG, e, "Error getting package manager");
+        }
+
+        if (manager != null) {
+            // Try and resolve the intent, to find an activity from this package
+            final List<ResolveInfo> resolvedActivities = manager.queryIntentActivities(
+                    intent, PackageManager.MATCH_DEFAULT_ONLY);
+
+            final String packageName = mActivity.getPackageName();
+
+            // Now try and find one that came from this package, if one is not found, the UI
+            // provider must have specified an intent that is to be handled by a different apk.
+            // In that case, the class name will not be set on the intent, so the default
+            // intent resolution will be used.
+            for (ResolveInfo resolveInfo: resolvedActivities) {
+                final ActivityInfo activityInfo = resolveInfo.activityInfo;
+                if (packageName.equals(activityInfo.packageName)) {
+                    intent.setClassName(activityInfo.packageName, activityInfo.name);
+                    break;
+                }
+            }
+        }
+
+        return intent;
+    }
+}
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 6c6fb14..a6c02ac 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,6 +48,8 @@
 import com.android.mail.ui.SwipeableListView.ListItemsRemovedListener;
 import com.android.mail.utils.LogTag;
 import com.android.mail.utils.LogUtils;
+import com.android.mail.utils.Utils;
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Maps;
 
 import java.util.ArrayList;
@@ -134,10 +138,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 +174,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 +213,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 +289,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 +383,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 +456,14 @@
         }
 
         // 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);
+        Utils.traceBeginSection("AA.getView");
+
+        final ConversationCursor cursor = (ConversationCursor) getItem(position);
         final Conversation conv = cursor.getConversation();
 
         // Notify the provider of this change in the position of Conversation cursor
@@ -468,8 +514,10 @@
         } else if (convertView != null) {
             ((SwipeableConversationItemView) convertView).reset();
         }
-        return createConversationItemView((SwipeableConversationItemView) convertView, mContext,
-                conv);
+        final View v = createConversationItemView((SwipeableConversationItemView) convertView,
+                mContext, conv);
+        Utils.traceEndSection();
+        return v;
     }
 
     private boolean hasLeaveBehinds() {
@@ -616,7 +664,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 +716,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 +746,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 +785,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 +809,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 +875,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 +949,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 +1023,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,20 +1034,21 @@
     }
 
     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();
         }
     }
 
     public void onCabModeEntered() {
-        for (final ConversationSpecialItemView specialView : mSpecialViews) {
+        for (final ConversationSpecialItemView specialView : mFleetingViews) {
             specialView.onCabModeEntered();
         }
     }
diff --git a/src/com/android/mail/ui/ContactLoaderCallbacks.java b/src/com/android/mail/ui/ContactLoaderCallbacks.java
new file mode 100644
index 0000000..cd09398
--- /dev/null
+++ b/src/com/android/mail/ui/ContactLoaderCallbacks.java
@@ -0,0 +1,88 @@
+/*
+ * 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.ui;
+
+import android.app.LoaderManager;
+import android.content.Context;
+import android.content.Loader;
+import android.database.DataSetObservable;
+import android.database.DataSetObserver;
+import android.os.Bundle;
+
+import com.android.mail.ContactInfo;
+import com.android.mail.ContactInfoSource;
+import com.android.mail.SenderInfoLoader;
+import com.google.common.collect.ImmutableMap;
+
+import java.util.Set;
+
+/**
+ * Asynchronously loads contact data for all senders in the conversation,
+ * and notifies observers when the data is ready.
+ */
+public class ContactLoaderCallbacks implements ContactInfoSource,
+        LoaderManager.LoaderCallbacks<ImmutableMap<String, ContactInfo>> {
+
+    private Set<String> mSenders;
+    private ImmutableMap<String, ContactInfo> mContactInfoMap;
+    private DataSetObservable mObservable = new DataSetObservable();
+
+    private Context mContext;
+
+    public ContactLoaderCallbacks(Context context) {
+        mContext = context;
+    }
+
+    public void setSenders(Set<String> emailAddresses) {
+        mSenders = emailAddresses;
+    }
+
+    @Override
+    public Loader<ImmutableMap<String, ContactInfo>> onCreateLoader(int id, Bundle args) {
+        return new SenderInfoLoader(mContext, mSenders);
+    }
+
+    @Override
+    public void onLoadFinished(Loader<ImmutableMap<String, ContactInfo>> loader,
+            ImmutableMap<String, ContactInfo> data) {
+        mContactInfoMap = data;
+        mObservable.notifyChanged();
+    }
+
+    @Override
+    public void onLoaderReset(Loader<ImmutableMap<String, ContactInfo>> loader) {
+    }
+
+    @Override
+    public ContactInfo getContactInfo(String email) {
+        if (mContactInfoMap == null) {
+            return null;
+        }
+        return mContactInfoMap.get(email);
+    }
+
+    @Override
+    public void registerObserver(DataSetObserver observer) {
+        mObservable.registerObserver(observer);
+    }
+
+    @Override
+    public void unregisterObserver(DataSetObserver observer) {
+        mObservable.unregisterObserver(observer);
+    }
+}
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 88f148e..74e7667 100644
--- a/src/com/android/mail/ui/ConversationPhotoTeaserView.java
+++ b/src/com/android/mail/ui/ConversationPhotoTeaserView.java
@@ -141,6 +141,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 8a25ea2..26aaa44 100644
--- a/src/com/android/mail/ui/ConversationSpecialItemView.java
+++ b/src/com/android/mail/ui/ConversationSpecialItemView.java
@@ -33,8 +33,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);
@@ -55,4 +64,7 @@
      * Called whenever Cab Mode has been entered via long press or selecting a sender image.
      */
     void onCabModeEntered();
+
+    /** 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 a63ffa8..c3b71af 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..6de375e 100644
--- a/src/com/android/mail/ui/ConversationViewFragment.java
+++ b/src/com/android/mail/ui/ConversationViewFragment.java
@@ -17,7 +17,6 @@
 
 package com.android.mail.ui;
 
-
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.Loader;
@@ -42,13 +41,13 @@
 import android.webkit.WebChromeClient;
 import android.webkit.WebSettings;
 import android.webkit.WebView;
-import android.webkit.WebViewClient;
 import android.widget.TextView;
 
 import com.android.mail.FormattedDateBuilder;
 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 +57,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;
@@ -71,6 +69,7 @@
 import com.android.mail.providers.Message;
 import com.android.mail.providers.UIProvider;
 import com.android.mail.ui.ConversationViewState.ExpansionState;
+import com.android.mail.utils.ConversationViewUtils;
 import com.android.mail.utils.LogTag;
 import com.android.mail.utils.LogUtils;
 import com.android.mail.utils.Utils;
@@ -84,13 +83,12 @@
 import java.util.Map;
 import java.util.Set;
 
-
 /**
  * The conversation view UI component.
  */
 public final class ConversationViewFragment extends AbstractConversationViewFragment implements
-        SuperCollapsedBlock.OnClickListener,
-        OnLayoutChangeListener {
+        SuperCollapsedBlock.OnClickListener, OnLayoutChangeListener,
+        MessageHeaderView.MessageHeaderViewCallbacks {
 
     private static final String LOG_TAG = LogTag.getLogTag();
     public static final String LAYOUT_TAG = "ConvLayout";
@@ -127,14 +125,14 @@
 
     private ScrollIndicatorsView mScrollIndicators;
 
+    private ConversationViewProgressController mProgressController;
+
     private View mNewMessageBar;
 
     private HtmlConversationTemplates mTemplates;
 
     private final MailJsBridge mJsBridge = new MailJsBridge();
 
-    private final WebViewClient mWebViewClient = new ConversationWebViewClient();
-
     private ConversationViewAdapter mAdapter;
 
     private boolean mViewsCreated;
@@ -179,7 +177,8 @@
     private final DataSetObserver mLoadedObserver = new DataSetObserver() {
         @Override
         public void onChanged() {
-            getHandler().post(new FragmentRunnable("delayedConversationLoad") {
+            getHandler().post(new FragmentRunnable("delayedConversationLoad",
+                    ConversationViewFragment.this) {
                 @Override
                 public void go() {
                     LogUtils.d(LOG_TAG, "CVF load observer fired, this=%s",
@@ -190,7 +189,7 @@
         }
     };
 
-    private final Runnable mOnProgressDismiss = new FragmentRunnable("onProgressDismiss") {
+    private final Runnable mOnProgressDismiss = new FragmentRunnable("onProgressDismiss", this) {
         @Override
         public void go() {
             LogUtils.d(LOG_TAG, "onProgressDismiss go() - isUserVisible() = %b", isUserVisible());
@@ -295,7 +294,7 @@
         // the initial primary item
         // Then CPC immediately sets the primary item to #10, which tears down #0/#1 and sets up
         // #9/#10/#11.
-        getHandler().post(new FragmentRunnable("showConversation") {
+        getHandler().post(new FragmentRunnable("showConversation", this) {
             @Override
             public void go() {
                 showConversation();
@@ -321,6 +320,8 @@
     public void onCreate(Bundle savedState) {
         super.onCreate(savedState);
 
+        mWebViewClient = new ConversationWebViewClient(mAccount);
+
         if (savedState != null) {
             mWebViewYPercent = savedState.getFloat(BUNDLE_KEY_WEBVIEW_Y_PERCENT);
         }
@@ -343,7 +344,8 @@
             }
         });
 
-        instantiateProgressIndicators(rootView);
+        mProgressController = new ConversationViewProgressController(this, getHandler());
+        mProgressController.instantiateProgressIndicators(rootView);
 
         mWebView = (ConversationWebView) mConversationContainer.findViewById(R.id.webview);
 
@@ -380,18 +382,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);
+        ConversationViewUtils.setTextZoom(getResources(), settings);
 
         mViewsCreated = true;
         mWebViewLoadedData = false;
@@ -409,11 +400,6 @@
     }
 
     @Override
-    protected WebView getWebView() {
-        return mWebView;
-    }
-
-    @Override
     public void onSaveInstanceState(Bundle outState) {
         super.onSaveInstanceState(outState);
 
@@ -476,7 +462,7 @@
                 userVisible);
 
         if (!userVisible) {
-            dismissLoadingStatus();
+            mProgressController.dismissLoadingStatus();
         } else if (mViewsCreated) {
             if (getMessageCursor() != null) {
                 LogUtils.d(LOG_TAG, "Fragment is now user-visible, onConversationSeen: %s", this);
@@ -543,12 +529,12 @@
         // TODO(mindyp): don't show loading status for a previously rendered
         // conversation. Ielieve this is better done by making sure don't show loading status
         // until XX ms have passed without loading completed.
-        showLoadingStatus();
+        mProgressController.showLoadingStatus(isUserVisible());
     }
 
     private void revealConversation() {
         timerMark("revealing conversation");
-        dismissLoadingStatus(mOnProgressDismiss);
+        mProgressController.dismissLoadingStatus(mOnProgressDismiss);
     }
 
     private boolean isLoadWaiting() {
@@ -677,6 +663,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;
             }
 
@@ -745,8 +735,8 @@
         for (int i = blockToReplace.getStart(), end = blockToReplace.getEnd(); i <= end; i++) {
             cursor.moveToPosition(i);
             final ConversationMessage msg = cursor.getMessage();
-            final MessageHeaderItem header = mAdapter.newMessageHeaderItem(msg,
-                    false /* expanded */, mViewState.getShouldShowImages(msg));
+            final MessageHeaderItem header = ConversationViewAdapter.newMessageHeaderItem(
+                    mAdapter, msg, false /* expanded */, mViewState.getShouldShowImages(msg));
             final MessageFooterItem footer = mAdapter.newMessageFooterItem(header);
 
             final int headerPx = measureOverlayHeight(header);
@@ -986,6 +976,10 @@
     }
 
     private class ConversationWebViewClient extends AbstractConversationWebViewClient {
+        public ConversationWebViewClient(Account account) {
+            super(account);
+        }
+
         @Override
         public void onPageFinished(WebView view, String url) {
             // Ignore unsafe calls made after a fragment is detached from an activity.
@@ -1015,8 +1009,8 @@
             for (Address addr : cacheCopy) {
                 emailAddresses.add(addr.getAddress());
             }
-            ContactLoaderCallbacks callbacks = getContactInfoSource();
-            getContactInfoSource().setSenders(emailAddresses);
+            final ContactLoaderCallbacks callbacks = getContactInfoSource();
+            callbacks.setSenders(emailAddresses);
             getLoaderManager().restartLoader(CONTACT_LOADER, Bundle.EMPTY, callbacks);
         }
 
@@ -1037,7 +1031,8 @@
         @JavascriptInterface
         public void onWebContentGeometryChange(final String[] overlayTopStrs,
                 final String[] overlayBottomStrs) {
-            getHandler().post(new FragmentRunnable("onWebContentGeometryChange") {
+            getHandler().post(new FragmentRunnable("onWebContentGeometryChange",
+                    ConversationViewFragment.this) {
 
                 @Override
                 public void go() {
@@ -1134,7 +1129,8 @@
         @SuppressWarnings("unused")
         @JavascriptInterface
         public void onContentReady() {
-            getHandler().post(new FragmentRunnable("onContentReady") {
+            getHandler().post(new FragmentRunnable("onContentReady",
+                    ConversationViewFragment.this) {
                 @Override
                 public void go() {
                     try {
diff --git a/src/com/android/mail/ui/ConversationViewProgressController.java b/src/com/android/mail/ui/ConversationViewProgressController.java
new file mode 100644
index 0000000..9bce381
--- /dev/null
+++ b/src/com/android/mail/ui/ConversationViewProgressController.java
@@ -0,0 +1,153 @@
+/*
+ * 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.ui;
+
+import android.animation.Animator;
+import android.animation.AnimatorInflater;
+import android.animation.AnimatorListenerAdapter;
+import android.app.Fragment;
+import android.content.res.Resources;
+import android.os.Handler;
+import android.view.View;
+
+import com.android.mail.R;
+import com.android.mail.utils.Utils;
+
+/**
+ * <p>Controller to handle showing and hiding progress in the conversation views.</p>
+ * <br>
+ * <b>NOTE:</b> This class makes several assumptions about the view hierarchy
+ * of the conversation view. Use with care.
+ */
+public class ConversationViewProgressController {
+    private static int sMinDelay = -1;
+    private static int sMinShowTime = -1;
+
+    private final Handler mHandler;
+
+    private final Fragment mFragment;
+
+    private long mLoadingShownTime = -1;
+    private View mProgressView;
+    private View mBackgroundView;
+
+    private final Runnable mDelayedShow;
+
+    public ConversationViewProgressController(Fragment fragment, Handler handler) {
+        mFragment = fragment;
+        mHandler = handler;
+
+        mDelayedShow = new FragmentRunnable("mDelayedShow", mFragment) {
+            @Override
+            public void go() {
+                mLoadingShownTime = System.currentTimeMillis();
+                mProgressView.setVisibility(View.VISIBLE);
+            }
+        };
+    }
+
+    public void instantiateProgressIndicators(View rootView) {
+        mBackgroundView = rootView.findViewById(R.id.background_view);
+        mProgressView = rootView.findViewById(R.id.loading_progress);
+    }
+
+    public void showLoadingStatus(boolean isFragmentUserVisible) {
+        if (!isFragmentUserVisible) {
+            return;
+        }
+        if (sMinDelay == -1) {
+            Resources res = mFragment.getResources();
+            sMinDelay = res.getInteger(R.integer.conversationview_show_loading_delay);
+            sMinShowTime = res.getInteger(R.integer.conversationview_min_show_loading);
+        }
+        // If the loading view isn't already showing, show it and remove any
+        // pending calls to show the loading screen.
+        mBackgroundView.setVisibility(View.VISIBLE);
+        mHandler.removeCallbacks(mDelayedShow);
+        mHandler.postDelayed(mDelayedShow, sMinDelay);
+    }
+
+    protected void dismissLoadingStatus() {
+        dismissLoadingStatus(null);
+    }
+
+    /**
+     * Begin the fade-out animation to hide the Progress overlay, either immediately or after some
+     * timeout (to ensure that the progress minimum time elapses).
+     *
+     * @param doAfter an optional Runnable action to execute after the animation completes
+     */
+    protected void dismissLoadingStatus(final Runnable doAfter) {
+        if (mLoadingShownTime == -1) {
+            // The runnable hasn't run yet, so just remove it.
+            mHandler.removeCallbacks(mDelayedShow);
+            dismiss(doAfter);
+            return;
+        }
+        final long diff = Math.abs(System.currentTimeMillis() - mLoadingShownTime);
+        if (diff > sMinShowTime) {
+            dismiss(doAfter);
+        } else {
+            mHandler.postDelayed(new FragmentRunnable("dismissLoadingStatus", mFragment) {
+                @Override
+                public void go() {
+                    dismiss(doAfter);
+                }
+            }, Math.abs(sMinShowTime - diff));
+        }
+    }
+
+    private void dismiss(final Runnable doAfter) {
+        // Reset loading shown time.
+        mLoadingShownTime = -1;
+        mProgressView.setVisibility(View.GONE);
+        if (mBackgroundView.getVisibility() == View.VISIBLE) {
+            animateDismiss(doAfter);
+        } else {
+            if (doAfter != null) {
+                doAfter.run();
+            }
+        }
+    }
+
+    private void animateDismiss(final Runnable doAfter) {
+        // the animation can only work (and is only worth doing) if this fragment is added
+        // reasons it may not be added: fragment is being destroyed, or in the process of being
+        // restored
+        if (!mFragment.isAdded()) {
+            mBackgroundView.setVisibility(View.GONE);
+            return;
+        }
+
+        Utils.enableHardwareLayer(mBackgroundView);
+        final Animator animator = AnimatorInflater.loadAnimator(
+                mFragment.getActivity().getApplicationContext(), R.anim.fade_out);
+        animator.setTarget(mBackgroundView);
+        animator.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                mBackgroundView.setVisibility(View.GONE);
+                mBackgroundView.setLayerType(View.LAYER_TYPE_NONE, null);
+                if (doAfter != null) {
+                    doAfter.run();
+                }
+            }
+        });
+        animator.start();
+    }
+}
diff --git a/src/com/android/mail/ui/DrawerFragment.java b/src/com/android/mail/ui/DrawerFragment.java
index 85436e4..d2c3227 100644
--- a/src/com/android/mail/ui/DrawerFragment.java
+++ b/src/com/android/mail/ui/DrawerFragment.java
@@ -17,14 +17,40 @@
 
 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 {
+    /**
+     * The only way a drawer is constructed is through XML layouts, and so it needs no constructor
+     * like {@link FolderListFragment#ofTopLevelTree(android.net.Uri, java.util.ArrayList}
+     */
     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;
+        // The drawer also switches accounts, so don't hide accounts.
+        mHideAccounts = false;
+    }
+
+    @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..dc4c5ec 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,22 @@
     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.
+     * This is true only for the drawer: for all others it is false.
+     */
+    protected boolean mIsDivided = false;
     /** True if the folder list belongs to a folder selection activity (one account only) */
-    private boolean mHideAccounts;
+    protected boolean mHideAccounts = true;
     /** 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,26 +118,15 @@
     /** 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 #mFolderListUri}. */
     private static final String ARG_FOLDER_LIST_URI = "arg-folder-list-uri";
     /** Key to store {@link #mExcludedFolderTypes} */
     private static final String ARG_EXCLUDED_FOLDER_TYPES = "arg-excluded-folder-types";
-    /** Key to store {@link #mType} */
-    private static final String ARG_TYPE = "arg-flf-type";
-    /** Key to store {@link #mHideAccounts} */
-    private static final String ARG_HIDE_ACCOUNTS = "arg-hide-accounts";
-
-    /** Either {@link #TYPE_DRAWER} for drawers or {@link #TYPE_TREE} for hierarchy trees */
-    private int mType;
-    /** This fragment is a drawer */
-    private static final int TYPE_DRAWER = 0;
-    /** This fragment is a folder tree */
-    private static final int TYPE_TREE = 1;
 
     private static final String BUNDLE_LIST_STATE = "flf-list-state";
     private static final String BUNDLE_SELECTED_FOLDER = "flf-selected-folder";
@@ -156,75 +176,52 @@
     }
 
     /**
-     * Creates a new instance of {@link FolderListFragment}. Gets the current account and current
-     * folder through observers.
-     */
-    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));
-        return fragment;
-    }
-
-    /**
      * Creates a new instance of {@link FolderListFragment}, initialized
      * to display the folder and its immediate children.
      * @param folder parent folder whose children are shown
-     * @param hideAccounts True if accounts should be hidden, false otherwise
+     *
      */
-    public static FolderListFragment ofTree(Folder folder, final boolean hideAccounts) {
+    public static FolderListFragment ofTree(Folder folder) {
         final FolderListFragment fragment = new FolderListFragment();
-        // Trees are never sectioned.
-        final boolean isSectioned = false;
-        fragment.setArguments(getBundleFromArgs(TYPE_TREE, folder, folder.childFoldersListUri,
-                isSectioned, null, hideAccounts));
+        fragment.setArguments(getBundleFromArgs(folder, folder.childFoldersListUri, null));
         return fragment;
     }
 
     /**
      * Creates a new instance of {@link FolderListFragment}, initialized
-     * to display the folder and its immediate children.
+     * to display the top level: where we have no parent folder, but we have a list of folders
+     * from the account.
      * @param folderListUri the URI which contains all the list of folders
      * @param excludedFolderTypes A list of {@link FolderType}s to exclude from displaying
-     * @param hideAccounts True if accounts should be hidden, false otherwise
      */
     public static FolderListFragment ofTopLevelTree(Uri folderListUri,
-            final ArrayList<Integer> excludedFolderTypes, final boolean hideAccounts) {
+            final ArrayList<Integer> excludedFolderTypes) {
         final FolderListFragment fragment = new FolderListFragment();
-        // Trees are never sectioned.
-        final boolean isSectioned = false;
-        fragment.setArguments(getBundleFromArgs(TYPE_TREE, null, folderListUri,
-                isSectioned, excludedFolderTypes, hideAccounts));
+        fragment.setArguments(getBundleFromArgs(null, folderListUri, excludedFolderTypes));
         return fragment;
     }
 
     /**
      * 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 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,
-            final boolean hideAccounts) {
+    private static Bundle getBundleFromArgs(Folder parentFolder, Uri folderListUri,
+            final ArrayList<Integer> excludedFolderTypes) {
         final Bundle args = new Bundle();
-        args.putInt(ARG_TYPE, type);
         if (parentFolder != null) {
             args.putParcelable(ARG_PARENT_FOLDER, parentFolder);
         }
         if (folderListUri != null) {
             args.putString(ARG_FOLDER_LIST_URI, folderListUri.toString());
         }
-        args.putBoolean(ARG_IS_SECTIONED, isSectioned);
         if (excludedFolderTypes != null) {
             args.putIntegerArrayList(ARG_EXCLUDED_FOLDER_TYPES, excludedFolderTypes);
         }
-        args.putBoolean(ARG_HIDE_ACCOUNTS, hideAccounts);
         return args;
     }
 
@@ -237,10 +234,10 @@
         // activity is creating ConversationListFragments. This activity must be of type
         // ControllableActivity.
         final Activity activity = getActivity();
-        Folder currentFolder = null;
         if (! (activity instanceof ControllableActivity)){
             LogUtils.wtf(LOG_TAG, "FolderListFragment expects only a ControllableActivity to" +
                     "create it. Cannot proceed.");
+            return;
         }
         mActivity = (ControllableActivity) activity;
         final FolderController controller = mActivity.getFolderController();
@@ -251,10 +248,13 @@
                 setSelectedFolder(newFolder);
             }
         };
+        final Folder currentFolder;
         if (controller != null) {
             // Only register for selected folder updates if we have a controller.
             currentFolder = mFolderObserver.initialize(controller);
             mCurrentFolderForUnreadCheck = currentFolder;
+        } else {
+            currentFolder = null;
         }
 
         // Initialize adapter for folder/heirarchical list.  Note this relies on
@@ -264,7 +264,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 +280,7 @@
                 setSelectedAccount(newAccount);
             }
         };
-        mFolderChanger = mActivity.getFolderListSelectionListener();
+        mFolderChanger = mActivity.getFolderSelector();
         if (accountController != null) {
             // Current account and its observer.
             setSelectedAccount(mAccountObserver.initialize(accountController));
@@ -292,7 +292,7 @@
                 }
             };
             mAllAccountsObserver.initialize(accountController);
-            mAccountChanger = accountController;
+            mAccountController = accountController;
 
             // Observer for when the drawer is closed
             mDrawerObserver = new DrawerClosedObserver() {
@@ -301,12 +301,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,40 +318,34 @@
             return;
         }
 
+        mListView.setChoiceMode(getListViewChoiceMode());
+
         setListAdapter(mCursorAdapter);
     }
 
     /**
      * Set the instance variables from the arguments provided here.
-     * @param args
+     * @param args bundle of arguments with keys named ARG_*
      */
     private void setInstanceFromBundle(Bundle args) {
         if (args == null) {
             return;
         }
-        mParentFolder = (Folder) args.getParcelable(ARG_PARENT_FOLDER);
+        mParentFolder = args.getParcelable(ARG_PARENT_FOLDER);
         final String folderUri = args.getString(ARG_FOLDER_LIST_URI);
-        if (folderUri == null) {
-            mFolderListUri = Uri.EMPTY;
-        } else {
+        if (folderUri != null) {
             mFolderListUri = Uri.parse(folderUri);
         }
-        mIsSectioned = args.getBoolean(ARG_IS_SECTIONED);
         mExcludedFolderTypes = args.getIntegerArrayList(ARG_EXCLUDED_FOLDER_TYPES);
-        mType = args.getInt(ARG_TYPE);
-        mHideAccounts = args.getBoolean(ARG_HIDE_ACCOUNTS, false);
     }
 
     @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 +431,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 +458,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);
@@ -484,8 +477,6 @@
             }
         } else if (item instanceof Folder) {
             folder = (Folder) item;
-        } else if (item instanceof ObjectCursor){
-            folder = ((ObjectCursor<Folder>) item).getModel();
         } else {
             // Don't know how we got here.
             LogUtils.wtf(LOG_TAG, "viewFolderOrChangeAccount(): invalid item");
@@ -502,10 +493,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,17 +505,16 @@
     public Loader<ObjectCursor<Folder>> onCreateLoader(int id, Bundle args) {
         mListView.setEmptyView(null);
         final Uri folderListUri;
-        if (mType == TYPE_TREE) {
-            // Folder trees, they specify a URI at construction time.
-            folderListUri = mFolderListUri;
-        } else if (mType == TYPE_DRAWER) {
-            // Drawers should have a valid account
-            if (mCurrentAccount != null) {
-                folderListUri = mCurrentAccount.folderListUri;
+        if (id == FOLDER_LIST_LOADER_ID) {
+            if (mFolderListUri != null) {
+                // Folder trees, they specify a URI at construction time.
+                folderListUri = mFolderListUri;
             } else {
-                LogUtils.wtf(LOG_TAG, "FLF.onCreateLoader() for Drawer with null account");
-                return null;
+                // Drawers get the folder list from the current account.
+                folderListUri = mCurrentAccount.folderListUri;
             }
+        } 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 +526,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,14 +563,14 @@
     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}
          * @return item the type of the item.
          */
         int getItemType(DrawerItem item);
-        /** Get the folder associated with this item. **/
-        Folder getFullFolder(DrawerItem item);
         /** Notify that the all accounts changed. */
         void notifyAllAccountsChanged();
         /** Remove all observers and destroy the object. */
@@ -591,7 +589,7 @@
         private final RecentFolderObserver mRecentFolderObserver = new RecentFolderObserver() {
             @Override
             public void onChanged() {
-                if (!isCursorInvalid(mCursor)) {
+                if (!isCursorInvalid()) {
                     recalculateList();
                 }
             }
@@ -600,12 +598,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 +614,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 +632,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());
@@ -680,12 +682,7 @@
         @Override
         public boolean isEnabled(int position) {
             final DrawerItem drawerItem = ((DrawerItem) getItem(position));
-            if (drawerItem == null) {
-                // If there is no item, return false as there's nothing there to be enabled
-                return false;
-            } else {
-                return drawerItem.isItemEnabled();
-            }
+            return drawerItem != null && drawerItem.isItemEnabled();
         }
 
         private Uri getCurrentAccountUri() {
@@ -764,14 +761,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 +781,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 +794,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 +889,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 +903,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()) {
@@ -921,33 +940,16 @@
         public int getItemType(DrawerItem item) {
             return item.mType;
         }
-
-        // TODO(viki): This is strange. We have the full folder and yet we create on from scratch.
-        @Override
-        public Folder getFullFolder(DrawerItem folderItem) {
-            if (folderItem.mFolderType == DrawerItem.FOLDER_RECENT) {
-                return folderItem.mFolder;
-            } else {
-                final int pos = folderItem.mPosition;
-                if (pos > -1 && mCursor != null && !mCursor.isClosed()
-                        && mCursor.moveToPosition(folderItem.mPosition)) {
-                    return mCursor.getModel();
-                } else {
-                    return null;
-                }
-            }
-        }
     }
 
     private class HierarchicalFolderListAdapter extends ArrayAdapter<Folder>
-            implements FolderListFragmentCursorAdapter{
+            implements FolderListFragmentCursorAdapter {
 
         private static final int PARENT = 0;
         private static final int CHILD = 1;
         private final Uri mParentUri;
         private final Folder mParent;
         private final FolderItemView.DropHandler mDropHandler;
-        private ObjectCursor<Folder> mCursor;
 
         public HierarchicalFolderListAdapter(ObjectCursor<Folder> c, Folder parentFolder) {
             super(mActivity.getActivityContext(), R.layout.folder_item);
@@ -999,7 +1001,6 @@
 
         @Override
         public void setCursor(ObjectCursor<Folder> cursor) {
-            mCursor = cursor;
             clear();
             if (mParent != null) {
                 add(mParent);
@@ -1015,6 +1016,11 @@
         }
 
         @Override
+        public void setFullFolderListCursor(final ObjectCursor<Folder> cursor) {
+            // Not necessary in HierarchicalFolderListAdapter
+        }
+
+        @Override
         public void destroy() {
             // Do nothing.
         }
@@ -1031,20 +1037,6 @@
         }
 
         @Override
-        public Folder getFullFolder(DrawerItem folderItem) {
-            final int pos = folderItem.mPosition;
-            if (mCursor == null || mCursor.isClosed()) {
-                return null;
-            }
-            if (pos > -1 && mCursor != null && !mCursor.isClosed()
-                    && mCursor.moveToPosition(folderItem.mPosition)) {
-                return mCursor.getModel();
-            } else {
-                return null;
-            }
-        }
-
-        @Override
         public void notifyAllAccountsChanged() {
             // Do nothing. We don't care about changes to all accounts.
         }
@@ -1056,7 +1048,7 @@
 
     /**
      * Sets the currently selected folder safely.
-     * @param folder
+     * @param folder the folder to change to. It is an error to pass null here.
      */
     private void setSelectedFolder(Folder folder) {
         if (folder == null) {
@@ -1065,12 +1057,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 +1096,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 +1110,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 +1139,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..f490bc7 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
@@ -191,7 +196,7 @@
         }
         firstButton.setOnClickListener(this);
         createFolderListFragment(FolderListFragment.ofTopLevelTree(mAccount.folderListUri,
-                getExcludedFolderTypes(), true));
+                getExcludedFolderTypes()));
     }
 
     /**
@@ -351,14 +356,14 @@
             // 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.
-            createFolderListFragment(FolderListFragment.ofTree(folder, true));
+            createFolderListFragment(FolderListFragment.ofTree(folder));
             return;
         }
         onFolderChanged(folder);
     }
 
     @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/FragmentRunnable.java b/src/com/android/mail/ui/FragmentRunnable.java
new file mode 100644
index 0000000..c422838
--- /dev/null
+++ b/src/com/android/mail/ui/FragmentRunnable.java
@@ -0,0 +1,51 @@
+/*
+ * 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.ui;
+
+import android.app.Fragment;
+
+import com.android.mail.utils.LogTag;
+import com.android.mail.utils.LogUtils;
+
+/**
+ * Small Runnable-like wrapper that first checks that the Fragment is in a good state before
+ * doing any work. Ideal for use with a {@link android.os.Handler}.
+ */
+public abstract class FragmentRunnable implements Runnable {
+    private static final String LOG_TAG = LogTag.getLogTag();
+
+    private final String mOpName;
+    private final Fragment mFragment;
+
+    public FragmentRunnable(String opName, Fragment fragment) {
+        mOpName = opName;
+        mFragment = fragment;
+    }
+
+    public abstract void go();
+
+    @Override
+    public void run() {
+        if (!mFragment.isAdded()) {
+            LogUtils.i(LOG_TAG, "Unable to run op='%s' b/c fragment is not attached: %s",
+                    mOpName, mFragment);
+            return;
+        }
+        go();
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/mail/ui/MailActionBarView.java b/src/com/android/mail/ui/MailActionBarView.java
index d7bf719..c967530 100644
--- a/src/com/android/mail/ui/MailActionBarView.java
+++ b/src/com/android/mail/ui/MailActionBarView.java
@@ -894,6 +894,9 @@
                 && !mFolder.isProviderFolder());
         Utils.setMenuItemVisibility(menu, R.id.move_to, mFolder != null
                 && mFolder.supportsCapability(FolderCapabilities.ALLOWS_REMOVE_CONVERSATION));
+        Utils.setMenuItemVisibility(menu, R.id.move_to_inbox, mFolder != null
+                && mFolder.supportsCapability(FolderCapabilities.ALLOWS_MOVE_TO_INBOX));
+
         final MenuItem removeFolder = menu.findItem(R.id.remove_folder);
         if (mFolder != null && removeFolder != null) {
             removeFolder.setTitle(mActivity.getApplicationContext().getString(
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..578a3ba
--- /dev/null
+++ b/src/com/android/mail/ui/NestedFolderView.java
@@ -0,0 +1,162 @@
+/*
+ * 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 android.app.LoaderManager;
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+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;
+
+/**
+ * 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 void onCabModeEntered() {
+        // Do nothing. We don't care if cab mode was entered.
+    }
+
+    @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/SecureConversationViewController.java b/src/com/android/mail/ui/SecureConversationViewController.java
new file mode 100644
index 0000000..a3e6ec8
--- /dev/null
+++ b/src/com/android/mail/ui/SecureConversationViewController.java
@@ -0,0 +1,210 @@
+/*
+ * 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.ui;
+
+import android.app.Fragment;
+import android.content.res.Resources;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.webkit.WebSettings;
+
+import com.android.mail.FormattedDateBuilder;
+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.MessageFooterView;
+import com.android.mail.browse.MessageHeaderView;
+import com.android.mail.browse.MessageScrollView;
+import com.android.mail.browse.MessageWebView;
+import com.android.mail.providers.Message;
+import com.android.mail.utils.ConversationViewUtils;
+
+/**
+ * Controller to do most of the heavy lifting for {@link SecureConversationViewFragment}
+ * and {@link com.android.mail.browse.EmlMessageViewFragment}. Currently that work is
+ * pretty much the rendering logic.
+ */
+public class SecureConversationViewController implements
+        MessageHeaderView.MessageHeaderViewCallbacks {
+    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 final SecureConversationViewControllerCallbacks mCallbacks;
+
+    private MessageWebView mWebView;
+    private ConversationViewHeader mConversationHeaderView;
+    private MessageHeaderView mMessageHeaderView;
+    private MessageFooterView mMessageFooterView;
+    private ConversationMessage mMessage;
+    private MessageScrollView mScrollView;
+
+    private ConversationViewProgressController mProgressController;
+    private FormattedDateBuilder mDateBuilder;
+
+    private int mSideMarginInWebPx;
+
+    public SecureConversationViewController(SecureConversationViewControllerCallbacks callbacks) {
+        mCallbacks = callbacks;
+    }
+
+    public View onCreateView(LayoutInflater inflater, ViewGroup container,
+            Bundle savedInstanceState) {
+        View rootView = inflater.inflate(R.layout.secure_conversation_view, container, false);
+        mScrollView = (MessageScrollView) rootView.findViewById(R.id.scroll_view);
+        mConversationHeaderView = (ConversationViewHeader) rootView.findViewById(R.id.conv_header);
+        mMessageHeaderView = (MessageHeaderView) rootView.findViewById(R.id.message_header);
+        mMessageFooterView = (MessageFooterView) rootView.findViewById(R.id.message_footer);
+
+        mProgressController = new ConversationViewProgressController(
+                mCallbacks.getFragment(), mCallbacks.getHandler());
+        mProgressController.instantiateProgressIndicators(rootView);
+        mWebView = (MessageWebView) rootView.findViewById(R.id.webview);
+        mWebView.setWebViewClient(mCallbacks.getWebViewClient());
+        mWebView.setFocusable(false);
+        final WebSettings settings = mWebView.getSettings();
+
+        settings.setJavaScriptEnabled(false);
+        settings.setLayoutAlgorithm(WebSettings.LayoutAlgorithm.NORMAL);
+
+        ConversationViewUtils.setTextZoom(mCallbacks.getFragment().getResources(), settings);
+
+        settings.setSupportZoom(true);
+        settings.setBuiltInZoomControls(true);
+        settings.setDisplayZoomControls(false);
+
+        mScrollView.setInnerScrollableView(mWebView);
+
+        return rootView;
+    }
+
+    public void onActivityCreated(Bundle savedInstanceState) {
+        mCallbacks.setupConversationHeaderView(mConversationHeaderView);
+
+        final Fragment fragment = mCallbacks.getFragment();
+
+        mDateBuilder = new FormattedDateBuilder(fragment.getActivity());
+        mMessageHeaderView.initialize(mDateBuilder,
+                mCallbacks.getConversationAccountController(), mCallbacks.getAddressCache());
+        mMessageHeaderView.setExpandMode(MessageHeaderView.POPUP_MODE);
+        mMessageHeaderView.setContactInfoSource(mCallbacks.getContactInfoSource());
+        mMessageHeaderView.setCallbacks(this);
+        mMessageHeaderView.setExpandable(false);
+        mMessageHeaderView.setViewOnlyMode(mCallbacks.isViewOnlyMode());
+
+        mCallbacks.setupMessageHeaderVeiledMatcher(mMessageHeaderView);
+
+        mMessageFooterView.initialize(fragment.getLoaderManager(), fragment.getFragmentManager());
+
+        mCallbacks.startMessageLoader();
+
+        mProgressController.showLoadingStatus(mCallbacks.isViewVisibleToUser());
+
+        final Resources r = mCallbacks.getFragment().getResources();
+        mSideMarginInWebPx = (int) ((r.getDimensionPixelOffset(
+                R.dimen.conversation_view_margin_side) + r.getDimensionPixelOffset(
+                R.dimen.conversation_message_content_margin_side)) / r.getDisplayMetrics().density);
+    }
+
+    /**
+     * Populate the adapter with overlay views (message headers, super-collapsed
+     * blocks, a conversation header), and return an HTML document with spacer
+     * divs inserted for all overlays.
+     */
+    public void renderMessage(ConversationMessage message) {
+        mMessage = message;
+
+        mWebView.getSettings().setBlockNetworkImage(!mMessage.alwaysShowImages);
+
+        // 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(mCallbacks.getBaseUri(), dataBuilder.toString(),
+                "text/html", "utf-8", null);
+        final MessageHeaderItem item = ConversationViewAdapter.newMessageHeaderItem(
+                null, mMessage, true, mMessage.alwaysShowImages);
+        mMessageHeaderView.bind(item, false);
+        if (mMessage.hasAttachments) {
+            mMessageFooterView.setVisibility(View.VISIBLE);
+            mMessageFooterView.bind(item, false);
+        }
+    }
+
+    public ConversationMessage getMessage() {
+        return mMessage;
+    }
+
+    public ConversationViewHeader getConversationHeaderView() {
+        return mConversationHeaderView;
+    }
+
+    public void dismissLoadingStatus() {
+        mProgressController.dismissLoadingStatus();
+    }
+
+    public void setSubject(String subject) {
+        mConversationHeaderView.setSubject(subject);
+    }
+
+    // Start MessageHeaderViewCallbacks implementations
+
+    @Override
+    public void setMessageSpacerHeight(MessageHeaderItem item, int newSpacerHeight) {
+        // Do nothing.
+    }
+
+    @Override
+    public void setMessageExpanded(MessageHeaderItem item, int newSpacerHeight) {
+        // Do nothing.
+    }
+
+    @Override
+    public void setMessageDetailsExpanded(MessageHeaderItem i, boolean expanded, int heightBefore) {
+        // Do nothing.
+    }
+
+    @Override
+    public void showExternalResources(final Message msg) {
+        mWebView.getSettings().setBlockNetworkImage(false);
+    }
+
+    @Override
+    public void showExternalResources(final String rawSenderAddress) {
+        mWebView.getSettings().setBlockNetworkImage(false);
+    }
+
+    @Override
+    public boolean supportsMessageTransforms() {
+        return false;
+    }
+
+    @Override
+    public String getMessageTransforms(final Message msg) {
+        return null;
+    }
+
+    // End MessageHeaderViewCallbacks implementations
+}
diff --git a/src/com/android/mail/ui/SecureConversationViewControllerCallbacks.java b/src/com/android/mail/ui/SecureConversationViewControllerCallbacks.java
new file mode 100644
index 0000000..06857ed
--- /dev/null
+++ b/src/com/android/mail/ui/SecureConversationViewControllerCallbacks.java
@@ -0,0 +1,47 @@
+/*
+ * 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.ui;
+
+import android.app.Fragment;
+import android.os.Handler;
+
+import com.android.mail.ContactInfoSource;
+import com.android.mail.browse.ConversationAccountController;
+import com.android.mail.browse.ConversationViewHeader;
+import com.android.mail.browse.MessageHeaderView;
+import com.android.mail.providers.Address;
+
+import java.util.Map;
+
+/**
+ * Callbacks for fragments that use the {@link SecureConversationViewController}.
+ */
+public interface SecureConversationViewControllerCallbacks {
+    public Handler getHandler();
+    public AbstractConversationWebViewClient getWebViewClient();
+    public Fragment getFragment();
+    public void setupConversationHeaderView(ConversationViewHeader headerView);
+    public boolean isViewVisibleToUser();
+    public ContactInfoSource getContactInfoSource();
+    public ConversationAccountController getConversationAccountController();
+    public Map<String, Address> getAddressCache();
+    public void setupMessageHeaderVeiledMatcher(MessageHeaderView messageHeaderView);
+    public void startMessageLoader();
+    public String getBaseUri();
+    public boolean isViewOnlyMode();
+}
diff --git a/src/com/android/mail/ui/SecureConversationViewFragment.java b/src/com/android/mail/ui/SecureConversationViewFragment.java
index 3a74d35..c3efd59 100644
--- a/src/com/android/mail/ui/SecureConversationViewFragment.java
+++ b/src/com/android/mail/ui/SecureConversationViewFragment.java
@@ -17,56 +17,64 @@
 
 package com.android.mail.ui;
 
+import android.app.Fragment;
 import android.content.Loader;
-import android.database.Cursor;
 import android.net.Uri;
 import android.os.Bundle;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
-import android.webkit.WebSettings;
-import android.webkit.WebSettings.LayoutAlgorithm;
 import android.webkit.WebView;
-import android.webkit.WebViewClient;
 
-import com.android.mail.R;
-import com.android.mail.browse.ConversationViewAdapter;
-import com.android.mail.browse.ConversationViewAdapter.MessageHeaderItem;
+import com.android.mail.browse.ConversationAccountController;
+import com.android.mail.browse.ConversationMessage;
 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;
-import com.android.mail.browse.MessageScrollView;
-import com.android.mail.browse.MessageWebView;
 import com.android.mail.content.ObjectCursor;
 import com.android.mail.providers.Account;
+import com.android.mail.providers.Address;
 import com.android.mail.providers.Conversation;
-import com.android.mail.providers.Message;
 import com.android.mail.utils.LogTag;
 import com.android.mail.utils.LogUtils;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Sets;
 
 import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
 
-public class SecureConversationViewFragment extends AbstractConversationViewFragment implements
-        MessageHeaderViewCallbacks {
+public class SecureConversationViewFragment extends AbstractConversationViewFragment
+        implements SecureConversationViewControllerCallbacks {
     private static final String LOG_TAG = LogTag.getLogTag();
-    private MessageWebView mWebView;
-    private ConversationViewHeader mConversationHeaderView;
-    private MessageHeaderView mMessageHeaderView;
-    private MessageFooterView mMessageFooterView;
-    private ConversationMessage mMessage;
-    private MessageScrollView mScrollView;
 
-    private final WebViewClient mWebViewClient = new AbstractConversationWebViewClient() {
+    private SecureConversationViewController mViewController;
+
+    private class SecureConversationWebViewClient extends AbstractConversationWebViewClient {
+        public SecureConversationWebViewClient(Account account) {
+            super(account);
+        }
+
         @Override
         public void onPageFinished(WebView view, String url) {
             if (isUserVisible()) {
                 onConversationSeen();
             }
 
-            dismissLoadingStatus();
+            mViewController.dismissLoadingStatus();
+
+            final Set<String> emailAddresses = Sets.newHashSet();
+            final List<Address> cacheCopy;
+            synchronized (mAddressCache) {
+                cacheCopy = ImmutableList.copyOf(mAddressCache.values());
+            }
+            for (Address addr : cacheCopy) {
+                emailAddresses.add(addr.getAddress());
+            }
+            final ContactLoaderCallbacks callbacks = getContactInfoSource();
+            callbacks.setSenders(emailAddresses);
+            getLoaderManager().restartLoader(CONTACT_LOADER, Bundle.EMPTY, callbacks);
         }
     };
 
@@ -93,67 +101,96 @@
     }
 
     @Override
-    public void onActivityCreated(Bundle savedInstanceState) {
-        super.onActivityCreated(savedInstanceState);
-        mConversationHeaderView.setCallbacks(this, this);
-        mConversationHeaderView.setFoldersVisible(false);
-        mConversationHeaderView.setSubject(mConversation.subject);
-        mMessageHeaderView.initialize(mDateBuilder, this, mAddressCache);
-        mMessageHeaderView.setExpandMode(MessageHeaderView.POPUP_MODE);
-        mMessageHeaderView.setContactInfoSource(getContactInfoSource());
-        mMessageHeaderView.setCallbacks(this);
-        mMessageHeaderView.setExpandable(false);
-        mMessageHeaderView.setVeiledMatcher(
-                ((ControllableActivity) getActivity()).getAccountController()
-                        .getVeiledAddressMatcher());
-        mMessageFooterView.initialize(getLoaderManager(), getFragmentManager());
-        getLoaderManager().initLoader(MESSAGE_LOADER, null, getMessageLoaderCallbacks());
-        showLoadingStatus();
+    public void onCreate(Bundle savedState) {
+        super.onCreate(savedState);
+
+        mWebViewClient = new SecureConversationWebViewClient(mAccount);
+        mViewController = new SecureConversationViewController(this);
     }
 
     @Override
     public View onCreateView(LayoutInflater inflater, ViewGroup container,
             Bundle savedInstanceState) {
-        View rootView = inflater.inflate(R.layout.secure_conversation_view, container, false);
-        mScrollView = (MessageScrollView) rootView.findViewById(R.id.scroll_view);
-        mConversationHeaderView = (ConversationViewHeader) rootView.findViewById(R.id.conv_header);
-        mMessageHeaderView = (MessageHeaderView) rootView.findViewById(R.id.message_header);
-        mMessageFooterView = (MessageFooterView) rootView.findViewById(R.id.message_footer);
-        instantiateProgressIndicators(rootView);
-        mWebView = (MessageWebView) rootView.findViewById(R.id.webview);
-        mWebView.setWebViewClient(mWebViewClient);
-        mWebView.setFocusable(false);
-        final WebSettings settings = mWebView.getSettings();
-
-        settings.setJavaScriptEnabled(false);
-        settings.setLayoutAlgorithm(LayoutAlgorithm.NORMAL);
-
-        settings.setSupportZoom(true);
-        settings.setBuiltInZoomControls(true);
-        settings.setDisplayZoomControls(false);
-
-        mScrollView.setInnerScrollableView(mWebView);
-
-        return rootView;
+        return mViewController.onCreateView(inflater, container, savedInstanceState);
     }
 
     @Override
-    protected WebView getWebView() {
-        return mWebView;
+    public void onActivityCreated(Bundle savedInstanceState) {
+        super.onActivityCreated(savedInstanceState);
+        mViewController.onActivityCreated(savedInstanceState);
     }
 
+    // Start implementations of SecureConversationViewControllerCallbacks
+
+    @Override
+    public Fragment getFragment() {
+        return this;
+    }
+
+    @Override
+    public AbstractConversationWebViewClient getWebViewClient() {
+        return mWebViewClient;
+    }
+
+    @Override
+    public void setupConversationHeaderView(ConversationViewHeader headerView) {
+        headerView.setCallbacks(this, this);
+        headerView.setFolders(mConversation);
+        headerView.setSubject(mConversation.subject);
+    }
+
+    @Override
+    public boolean isViewVisibleToUser() {
+        return isUserVisible();
+    }
+
+    @Override
+    public ConversationAccountController getConversationAccountController() {
+        return this;
+    }
+
+    @Override
+    public Map<String, Address> getAddressCache() {
+        return mAddressCache;
+    }
+
+    @Override
+    public void setupMessageHeaderVeiledMatcher(MessageHeaderView messageHeaderView) {
+        messageHeaderView.setVeiledMatcher(
+                ((ControllableActivity) getActivity()).getAccountController()
+                        .getVeiledAddressMatcher());
+    }
+
+    @Override
+    public void startMessageLoader() {
+        getLoaderManager().initLoader(MESSAGE_LOADER, null, getMessageLoaderCallbacks());
+    }
+
+    @Override
+    public String getBaseUri() {
+        return mBaseUri;
+    }
+
+    @Override
+    public boolean isViewOnlyMode() {
+        return false;
+    }
+
+    // End implementations of SecureConversationViewControllerCallbacks
+
     @Override
     protected void markUnread() {
         super.markUnread();
         // Ignore unsafe calls made after a fragment is detached from an activity
         final ControllableActivity activity = (ControllableActivity) getActivity();
-        if (activity == null || mConversation == null || mMessage == null) {
+        final ConversationMessage message = mViewController.getMessage();
+        if (activity == null || mConversation == null || message == null) {
             LogUtils.w(LOG_TAG, "ignoring markUnread for conv=%s",
                     mConversation != null ? mConversation.id : 0);
             return;
         }
         final HashSet<Uri> uris = new HashSet<Uri>();
-        uris.add(mMessage.uri);
+        uris.add(message.uri);
         activity.getConversationUpdater().markConversationMessagesUnread(mConversation, uris,
                 mViewState.getConversationInfo());
     }
@@ -179,41 +216,6 @@
     }
 
     @Override
-    public void setMessageSpacerHeight(MessageHeaderItem item, int newSpacerHeight) {
-        // Do nothing.
-    }
-
-    @Override
-    public void setMessageExpanded(MessageHeaderItem item, int newSpacerHeight) {
-        // Do nothing.
-    }
-
-    @Override
-    public void setMessageDetailsExpanded(MessageHeaderItem i, boolean expanded, int heightbefore) {
-        // Do nothing.
-    }
-
-    @Override
-    public void showExternalResources(final Message msg) {
-        mWebView.getSettings().setBlockNetworkImage(false);
-    }
-
-    @Override
-    public void showExternalResources(final String rawSenderAddress) {
-        mWebView.getSettings().setBlockNetworkImage(false);
-    }
-
-    @Override
-    public boolean supportsMessageTransforms() {
-        return false;
-    }
-
-    @Override
-    public String getMessageTransforms(final Message msg) {
-        return null;
-    }
-
-    @Override
     protected void onMessageCursorLoadFinished(Loader<ObjectCursor<ConversationMessage>> loader,
             MessageCursor newCursor, MessageCursor oldCursor) {
         // ignore cursors that are still loading results
@@ -225,41 +227,25 @@
             // Activity is finishing, just bail.
             return;
         }
-        renderMessageBodies(newCursor);
-    }
-
-    /**
-     * Populate the adapter with overlay views (message headers, super-collapsed
-     * blocks, a conversation header), and return an HTML document with spacer
-     * divs inserted for all overlays.
-     */
-    private void renderMessageBodies(MessageCursor messageCursor) {
-        if (!messageCursor.moveToFirst()) {
+        if (!newCursor.moveToFirst()) {
             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);
-        final ConversationViewAdapter adapter = new ConversationViewAdapter(mActivity, null, null,
-                null, null, null, null, null, null);
-        final MessageHeaderItem item = adapter.newMessageHeaderItem(mMessage, true,
-                mMessage.alwaysShowImages);
-        mMessageHeaderView.bind(item, false);
-        if (mMessage.hasAttachments) {
-            mMessageFooterView.setVisibility(View.VISIBLE);
-            mMessageFooterView.bind(item, false);
-        }
+
+        mViewController.renderMessage(newCursor.getMessage());
     }
 
     @Override
     public void onConversationUpdated(Conversation conv) {
-        final ConversationViewHeader headerView = mConversationHeaderView;
+        final ConversationViewHeader headerView = mViewController.getConversationHeaderView();
         if (headerView != null) {
             headerView.onConversationUpdated(conv);
             headerView.setSubject(conv.subject);
         }
     }
 
+    // Need this stub here
+    public boolean supportsMessageTransforms() {
+        return false;
+    }
 }
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..ede87fd 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();
@@ -90,30 +92,6 @@
         }
     }
 
-    /**
-     * Create a {@link FolderListFragment} for trees with the specified parent
-     * @param parent the parent folder whose children need to be displayed in this list
-     */
-    private void createFolderTree(Folder parent) {
-        setHierarchyFolder(parent);
-        createFolderListFragment(FolderListFragment.ofTree(parent, false));
-    }
-
-    private void createFolderListFragment(Fragment folderList) {
-        // Create a sectioned FolderListFragment.
-        FragmentTransaction fragmentTransaction = mActivity.getFragmentManager().beginTransaction();
-        if (Utils.useFolderListFragmentTransition(mActivity.getActivityContext())) {
-            fragmentTransaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN);
-        }
-        fragmentTransaction.replace(R.id.content_pane, folderList, TAG_FOLDER_LIST);
-        fragmentTransaction.commitAllowingStateLoss();
-        // We only set the action bar if the viewmode has been set previously. Otherwise, we leave
-        // the action bar in the state it is currently in.
-        if (mViewMode.getMode() != ViewMode.UNKNOWN) {
-            resetActionBarIcon();
-        }
-    }
-
     @Override
     protected boolean isConversationListVisible() {
         return !mLayout.isConversationListCollapsed();
@@ -132,8 +110,9 @@
         mDrawerPullout = mDrawerContainer.findViewById(R.id.content_pane);
         mLayout = (TwoPaneLayout) mActivity.findViewById(R.id.two_pane_activity);
         if (mLayout == null) {
-            // We need the layout for everything. Crash early if it is null.
+            // We need the layout for everything. Crash/Return early if it is null.
             LogUtils.wtf(LOG_TAG, "mLayout is null!");
+            return false;
         }
         mLayout.setController(this, Intent.ACTION_SEARCH.equals(mActivity.getIntent().getAction()));
         mLayout.setDrawerLayout(mDrawerContainer);
@@ -166,26 +145,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);
         }
     }
@@ -428,7 +399,6 @@
                     // Show inbox; we are at the top of the hierarchy we were
                     // showing, and it doesn't have a parent, so we must want to
                     // the basic account folder list.
-                    createFolderListFragment(FolderListFragment.ofDrawer());
                     loadAccountInbox();
                 }
             // Otherwise, if we are in the conversation list but not in the default
@@ -573,4 +543,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/ui/settings/ClearPictureApprovalsDialogFragment.java b/src/com/android/mail/ui/settings/ClearPictureApprovalsDialogFragment.java
new file mode 100644
index 0000000..f04eb9f
--- /dev/null
+++ b/src/com/android/mail/ui/settings/ClearPictureApprovalsDialogFragment.java
@@ -0,0 +1,64 @@
+/*
+ * 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.ui.settings;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.os.Bundle;
+import android.widget.Toast;
+
+import com.android.mail.R;
+import com.android.mail.preferences.MailPrefs;
+
+public class ClearPictureApprovalsDialogFragment extends DialogFragment implements OnClickListener {
+
+    public static final String FRAGMENT_TAG = "ClearPictureApprovalsDialogFragment";
+
+    /**
+     * Creates a new instance of {@link ClearPictureApprovalsDialogFragment}.
+     * @return The newly created {@link ClearPictureApprovalsDialogFragment}.
+     */
+    public static ClearPictureApprovalsDialogFragment newInstance() {
+        final ClearPictureApprovalsDialogFragment fragment =
+                new ClearPictureApprovalsDialogFragment();
+        return fragment;
+    }
+
+    @Override
+    public Dialog onCreateDialog(final Bundle savedInstanceState) {
+        return new AlertDialog.Builder(getActivity())
+                .setTitle(R.string.clear_display_images_whitelist_dialog_title)
+                .setMessage(R.string.clear_display_images_whitelist_dialog_message)
+                .setIconAttribute(android.R.attr.alertDialogIcon)
+                .setPositiveButton(R.string.clear, this)
+                .setNegativeButton(R.string.cancel, this)
+                .create();
+    }
+
+    @Override
+    public void onClick(DialogInterface dialog, int which) {
+        if (which == DialogInterface.BUTTON_POSITIVE) {
+            final MailPrefs mailPrefs = MailPrefs.get(getActivity());
+            mailPrefs.clearSenderWhiteList();
+            Toast.makeText(getActivity(), R.string.sender_whitelist_cleared, Toast.LENGTH_SHORT)
+                    .show();
+        }
+    }
+}
diff --git a/src/com/android/mail/utils/ConversationViewUtils.java b/src/com/android/mail/utils/ConversationViewUtils.java
new file mode 100644
index 0000000..482b746
--- /dev/null
+++ b/src/com/android/mail/utils/ConversationViewUtils.java
@@ -0,0 +1,40 @@
+/*
+ * 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.utils;
+
+import android.content.res.Resources;
+import android.webkit.WebSettings;
+
+import com.android.mail.R;
+
+public class ConversationViewUtils {
+    public static void setTextZoom(Resources resources, WebSettings settings) {
+        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/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..ab2c6e4 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,14 @@
     static final String GENERIC_MIMETYPE = "application/octet-stream";
 
     @VisibleForTesting
-    static final String EML_ATTACHMENT_CONTENT_TYPE = "application/eml";
+    private static final Set<String> EML_ATTACHMENT_CONTENT_TYPES = ImmutableSet.of(
+            "message/rfc822", "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 +112,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 +133,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 +156,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.
@@ -182,4 +164,14 @@
             }
         }
     }
+
+    /**
+     * Checks the supplied mime type to determine if it is a valid eml file.
+     * Valid mime types are "message/rfc822" and "application/eml".
+     * @param mimeType the mime type to check
+     * @return {@code true} if the mime type is one of the valid mime types.
+     */
+    public static boolean isEmlMimeType(String mimeType) {
+        return EML_ATTACHMENT_CONTENT_TYPES.contains(mimeType);
+    }
 }
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/android/mail/utils/Utils.java b/src/com/android/mail/utils/Utils.java
index 38e6c1d..e5b9e10 100644
--- a/src/com/android/mail/utils/Utils.java
+++ b/src/com/android/mail/utils/Utils.java
@@ -899,8 +899,14 @@
      * Show the feedback screen for the supplied account.
      */
     public static void sendFeedback(FeedbackEnabledActivity activity, Account account,
+                                    boolean reportingProblem) {
+        if (activity != null && account != null) {
+            sendFeedback(activity, account.sendFeedbackIntentUri, reportingProblem);
+        }
+    }
+    public static void sendFeedback(FeedbackEnabledActivity activity, Uri feedbackIntentUri,
             boolean reportingProblem) {
-        if (activity != null && account != null && !isEmpty(account.sendFeedbackIntentUri)) {
+        if (activity != null &&  !isEmpty(feedbackIntentUri)) {
             final Bundle optionalExtras = new Bundle(2);
             optionalExtras.putBoolean(
                     UIProvider.SendFeedbackExtras.EXTRA_REPORTING_PROBLEM, reportingProblem);
@@ -909,10 +915,11 @@
                 optionalExtras.putParcelable(
                         UIProvider.SendFeedbackExtras.EXTRA_SCREEN_SHOT, screenBitmap);
             }
-            openUrl(activity.getActivityContext(), account.sendFeedbackIntentUri, optionalExtras);
+            openUrl(activity.getActivityContext(), feedbackIntentUri, optionalExtras);
         }
     }
 
+
     public static Bitmap getReducedSizeBitmap(FeedbackEnabledActivity activity) {
         final Window activityWindow = activity.getWindow();
         final View currentView = activityWindow != null ? activityWindow.getDecorView() : null;
@@ -1280,4 +1287,64 @@
         return uri.buildUpon().appendQueryParameter(APP_VERSION_QUERY_PARAMETER,
                 Integer.toString(appVersion)).build();
     }
+
+    /**
+     * Gets the specified {@link Folder} object.
+     *
+     * @param folderUri The {@link Uri} for the folder
+     * @param allowHidden <code>true</code> to allow a hidden folder to be returned,
+     *        <code>false</code> to return <code>null</code> instead
+     * @return the specified {@link Folder} object, or <code>null</code>
+     */
+    public static Folder getFolder(final Context context, final Uri folderUri,
+            final boolean allowHidden) {
+        final Uri uri = folderUri
+                .buildUpon()
+                .appendQueryParameter(UIProvider.ALLOW_HIDDEN_FOLDERS_QUERY_PARAM,
+                        Boolean.toString(allowHidden))
+                .build();
+
+        final Cursor cursor = context.getContentResolver().query(uri,
+                UIProvider.FOLDERS_PROJECTION, null, null, null);
+
+        if (cursor == null) {
+            return null;
+        }
+
+        try {
+            if (cursor.moveToFirst()) {
+                return new Folder(cursor);
+            } else {
+                return null;
+            }
+        } finally {
+            cursor.close();
+        }
+    }
+
+    /**
+     * Begins systrace tracing for a given tag. No-op on unsupported platform versions.
+     *
+     * @param tag systrace tag to use
+     *
+     * @see android.os.Trace#beginSection(String)
+     */
+    public static void traceBeginSection(String tag) {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
+            android.os.Trace.beginSection(tag);
+        }
+    }
+
+    /**
+     * Ends systrace tracing for the most recently begun section. No-op on unsupported platform
+     * versions.
+     *
+     * @see android.os.Trace#endSection()
+     */
+    public static void traceEndSection() {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
+            android.os.Trace.endSection();
+        }
+    }
+
 }
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"));
+    }
+}