Copy emailcommon to email2

* Also, fix Exchange logging
* Also, get notifications working
* Refactor former Email class into MailActivityEmail

Change-Id: Id726f4178134485f4a3ec1ee317861d984d659a0
diff --git a/email2/emailcommon/Android.mk b/email2/emailcommon/Android.mk
new file mode 100644
index 0000000..cd70309
--- /dev/null
+++ b/email2/emailcommon/Android.mk
@@ -0,0 +1,44 @@
+# Copyright 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.
+
+LOCAL_PATH := $(call my-dir)
+
+# Build the com.android.emailcommon static library. At the moment, this includes
+# the emailcommon files themselves plus everything under src/org (apache code).  All of our
+# AIDL files are also compiled into the static library
+
+include $(CLEAR_VARS)
+
+unified_email_src_dir := ../../../UnifiedEmail/src
+apache_src_dir := ../../../UnifiedEmail/src/org
+
+imported_unified_email_files := \
+        $(unified_email_src_dir)/com/android/mail/utils/LogUtils.java \
+        $(unified_email_src_dir)/com/android/mail/utils/LoggingInputStream.java \
+        $(unified_email_src_dir)/com/android/mail/providers/UIProvider.java
+
+LOCAL_MODULE := com.android.emailcommon
+LOCAL_STATIC_JAVA_LIBRARIES := guava android-common
+LOCAL_SRC_FILES := $(call all-java-files-under, src/com/android/emailcommon)
+LOCAL_SRC_FILES += \
+    src/com/android/emailcommon/service/IEmailService.aidl \
+    src/com/android/emailcommon/service/IEmailServiceCallback.aidl \
+    src/com/android/emailcommon/service/IPolicyService.aidl \
+    src/com/android/emailcommon/service/IAccountService.aidl
+LOCAL_SRC_FILES += $(call all-java-files-under, $(apache_src_dir))
+LOCAL_SRC_FILES += $(imported_unified_email_files)
+
+LOCAL_SDK_VERSION := current
+
+include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/email2/emailcommon/src/com/android/emailcommon/AccountManagerTypes.java b/email2/emailcommon/src/com/android/emailcommon/AccountManagerTypes.java
new file mode 100644
index 0000000..4ccd480
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/AccountManagerTypes.java
@@ -0,0 +1,23 @@
+/*
+ /*
+ * 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;
+
+public class AccountManagerTypes {
+    public static final String TYPE_EXCHANGE = "com.android.exchange";
+    public static final String TYPE_POP_IMAP = "com.android.email";
+}
diff --git a/email2/emailcommon/src/com/android/emailcommon/Api.java b/email2/emailcommon/src/com/android/emailcommon/Api.java
new file mode 100644
index 0000000..2b1e89b
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/Api.java
@@ -0,0 +1,28 @@
+/*
+ * 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;
+
+/**
+ * This class will be used for API-related definitions; for now, just the api "level"
+ *
+ * Level 1: As shipped in HC/MR1
+ * Level 2: Adds searchMessages to EmailService
+ *
+ */
+public class Api {
+    public static final int LEVEL = 2;
+}
diff --git a/email2/emailcommon/src/com/android/emailcommon/CalendarProviderStub.java b/email2/emailcommon/src/com/android/emailcommon/CalendarProviderStub.java
new file mode 100644
index 0000000..eac371a
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/CalendarProviderStub.java
@@ -0,0 +1,25 @@
+/*
+ * 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;
+
+/**
+ * This is the only non-SDK reference in the com.android.email project, referencing the once and
+ * future CalendarProvider authority name.
+ */
+public class CalendarProviderStub {
+    public static final String AUTHORITY = "com.android.calendar";
+}
diff --git a/email2/emailcommon/src/com/android/emailcommon/Configuration.java b/email2/emailcommon/src/com/android/emailcommon/Configuration.java
new file mode 100644
index 0000000..d9469d9
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/Configuration.java
@@ -0,0 +1,23 @@
+/*
+ * 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;
+
+public class Configuration {
+    // Bundle key for Exchange configuration (boolean value)
+    public static final String EXCHANGE_CONFIGURATION_USE_ALTERNATE_STRINGS =
+        "com.android.email.EXCHANGE_CONFIGURATION_USE_ALTERNATE_STRINGS";
+}
diff --git a/email2/emailcommon/src/com/android/emailcommon/Device.java b/email2/emailcommon/src/com/android/emailcommon/Device.java
new file mode 100644
index 0000000..ba93062
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/Device.java
@@ -0,0 +1,112 @@
+/*
+ /*
+ * 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 android.telephony.TelephonyManager;
+import android.util.Log;
+
+import com.android.emailcommon.utility.Utility;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+
+public class Device {
+    private static String sDeviceId = null;
+
+    /**
+     * EAS requires a unique device id, so that sync is possible from a variety of different
+     * devices (e.g. the syncKey is specific to a device)  If we're on an emulator or some other
+     * device that doesn't provide one, we can create it as android<n> where <n> is system time.
+     * This would work on a real device as well, but it would be better to use the "real" id if
+     * it's available
+     */
+    static public synchronized String getDeviceId(Context context) throws IOException {
+        if (sDeviceId == null) {
+            sDeviceId = getDeviceIdInternal(context);
+        }
+        return sDeviceId;
+    }
+
+    static private String getDeviceIdInternal(Context context) throws IOException {
+        if (context == null) {
+            throw new IllegalStateException("getDeviceId requires a Context");
+        }
+        File f = context.getFileStreamPath("deviceName");
+        BufferedReader rdr = null;
+        String id;
+        if (f.exists()) {
+            if (f.canRead()) {
+                rdr = new BufferedReader(new FileReader(f), 128);
+                id = rdr.readLine();
+                rdr.close();
+                if (id == null) {
+                    // It's very bad if we read a null device id; let's delete that file
+                    if (!f.delete()) {
+                        Log.e(Logging.LOG_TAG, "Can't delete null deviceName file; try overwrite.");
+                    }
+                } else {
+                    return id;
+                }
+            } else {
+                Log.w(Logging.LOG_TAG, f.getAbsolutePath() + ": File exists, but can't read?" +
+                    "  Trying to remove.");
+                if (!f.delete()) {
+                    Log.w(Logging.LOG_TAG, "Remove failed. Tring to overwrite.");
+                }
+            }
+        }
+        BufferedWriter w = new BufferedWriter(new FileWriter(f), 128);
+        final String consistentDeviceId = getConsistentDeviceId(context);
+        if (consistentDeviceId != null) {
+            // Use different prefix from random IDs.
+            id = "androidc" + consistentDeviceId;
+        } else {
+            id = "android" + System.currentTimeMillis();
+        }
+        w.write(id);
+        w.close();
+        return id;
+    }
+
+    /**
+     * @return Device's unique ID if available.  null if the device has no unique ID.
+     */
+    public static String getConsistentDeviceId(Context context) {
+        final String deviceId;
+        try {
+            TelephonyManager tm =
+                (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
+            if (tm == null) {
+                return null;
+            }
+            deviceId = tm.getDeviceId();
+            if (deviceId == null) {
+                return null;
+            }
+        } catch (Exception e) {
+            Log.d(Logging.LOG_TAG, "Error in TelephonyManager.getDeviceId(): " + e.getMessage());
+            return null;
+        }
+        return Utility.getSmallHash(deviceId);
+    }
+}
diff --git a/email2/emailcommon/src/com/android/emailcommon/Logging.java b/email2/emailcommon/src/com/android/emailcommon/Logging.java
new file mode 100644
index 0000000..1fae76f
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/Logging.java
@@ -0,0 +1,47 @@
+/*
+ * 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;
+
+public class Logging {
+    public static final String LOG_TAG = "Email";
+
+    /**
+     * Set this to 'true' to enable as much Email logging as possible.
+     */
+    public static final boolean LOGD;
+
+    /**
+     * If this is enabled then logging that normally hides sensitive information
+     * like passwords will show that information.
+     */
+    public static final boolean DEBUG_SENSITIVE;
+
+    /**
+     * If true, logging regarding UI (such as activity/fragment lifecycle) will be enabled.
+     *
+     * TODO rename it to DEBUG_UI.
+     */
+    public static final boolean DEBUG_LIFECYCLE;
+
+    static {
+        // Declare values here to avoid dead code warnings; it means we have some extra
+        // "if" statements in the byte code that always evaluate to "if (false)"
+        LOGD = false; // DO NOT CHECK IN WITH TRUE
+        DEBUG_SENSITIVE = false; // DO NOT CHECK IN WITH TRUE
+        DEBUG_LIFECYCLE = false; // DO NOT CHECK IN WITH TRUE
+    }
+}
diff --git a/email2/emailcommon/src/com/android/emailcommon/TempDirectory.java b/email2/emailcommon/src/com/android/emailcommon/TempDirectory.java
new file mode 100644
index 0000000..252488c
--- /dev/null
+++ b/email2/emailcommon/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/email2/emailcommon/src/com/android/emailcommon/TrafficFlags.java b/email2/emailcommon/src/com/android/emailcommon/TrafficFlags.java
new file mode 100644
index 0000000..c8c4e03
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/TrafficFlags.java
@@ -0,0 +1,113 @@
+/*
+ * 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 com.android.emailcommon.provider.Account;
+import com.android.emailcommon.provider.HostAuth;
+import com.android.emailcommon.utility.Utility;
+
+/**
+ * Constants for tagging threads for traffic stats, and associated utilities
+ *
+ * Example usage:
+ *  TrafficStats.setThreadStatsTag(accountId | PROTOCOL_IMAP | DATA_EMAIL | REASON_SYNC);
+ */
+public class TrafficFlags {
+    // Bits 0->15, account id
+    private static final int ACCOUNT_MASK = 0x0000FFFF;
+
+    // Bits 16&17, protocol (0 = POP3)
+    private static final int PROTOCOL_SHIFT = 16;
+    private static final int PROTOCOL_MASK = 3 << PROTOCOL_SHIFT;
+    public static final int PROTOCOL_POP3 = 0 << PROTOCOL_SHIFT;
+    public static final int PROTOCOL_IMAP = 1 << PROTOCOL_SHIFT;
+    public static final int PROTOCOL_EAS = 2 << PROTOCOL_SHIFT;
+    public static final int PROTOCOL_SMTP = 3 << PROTOCOL_SHIFT;
+    private static final String[] PROTOCOLS = new String[] {HostAuth.SCHEME_POP3,
+        HostAuth.SCHEME_IMAP, HostAuth.SCHEME_EAS, HostAuth.SCHEME_SMTP};
+
+    // Bits 18&19, type (0 = EMAIL)
+    private static final int DATA_SHIFT = 18;
+    private static final int DATA_MASK = 3 << DATA_SHIFT;
+    public static final int DATA_EMAIL = 0 << DATA_SHIFT;
+    public static final int DATA_CONTACTS = 1 << DATA_SHIFT;
+    public static final int DATA_CALENDAR = 2 << DATA_SHIFT;
+
+    // Bits 20&21, reason (if protocol != SMTP)
+    private static final int REASON_SHIFT = 20;
+    private static final int REASON_MASK = 3 << REASON_SHIFT;
+    public static final int REASON_SYNC = 0 << REASON_SHIFT;
+    public static final int REASON_ATTACHMENT_USER = 1 << REASON_SHIFT;
+    // Note: We don't yet use the PRECACHE reason (it's complicated...)
+    public static final int REASON_ATTACHMENT_PRECACHE = 2 << REASON_SHIFT;
+    private static final String[] REASONS = new String[] {"sync", "attachment", "precache"};
+
+    /**
+     * Get flags indicating sync of the passed-in account; note that, by default, these flags
+     * indicate an email sync; to change the type of sync, simply "or" in DATA_CONTACTS or
+     * DATA_CALENDAR (since DATA_EMAIL = 0)
+     *
+     * @param context the caller's context
+     * @param account the account being used
+     * @return flags for syncing this account
+     */
+    public static int getSyncFlags(Context context, Account account) {
+        int protocolIndex = Utility.arrayIndex(PROTOCOLS, account.getProtocol(context));
+        return (int)account.mId | REASON_SYNC | (protocolIndex << PROTOCOL_SHIFT);
+    }
+
+    /**
+     * Get flags indicating attachment loading from the passed-in account
+     *
+     * @param context the caller's context
+     * @param account the account being used
+     * @return flags for loading an attachment in this account
+     */
+    public static int getAttachmentFlags(Context context, Account account) {
+        int protocolIndex = Utility.arrayIndex(PROTOCOLS, account.getProtocol(context));
+        return (int)account.mId | REASON_ATTACHMENT_USER | (protocolIndex << PROTOCOL_SHIFT);
+    }
+
+    /**
+     * Get flags indicating sending SMTP email from the passed-in account
+     *
+     * @param context the caller's context
+     * @param account the account being used
+     * @return flags for sending SMTP email from this account
+     */
+    public static int getSmtpFlags(Context context, Account account) {
+        return (int)account.mId | REASON_SYNC | PROTOCOL_SMTP;
+    }
+
+    public static String toString(int flags) {
+        StringBuilder sb = new StringBuilder();
+        sb.append("account ");
+        sb.append(flags & ACCOUNT_MASK);
+        sb.append(',');
+        sb.append(REASONS[(flags & REASON_MASK) >> REASON_SHIFT]);
+        sb.append(',');
+        sb.append(PROTOCOLS[(flags & PROTOCOL_MASK) >> PROTOCOL_SHIFT]);
+        int maskedData = flags & DATA_MASK;
+        if (maskedData != 0) {
+            sb.append(',');
+            sb.append(maskedData == DATA_CALENDAR ? "calendar" : "contacts");
+        }
+        return sb.toString();
+    }
+}
diff --git a/email2/emailcommon/src/com/android/emailcommon/internet/BinaryTempFileBody.java b/email2/emailcommon/src/com/android/emailcommon/internet/BinaryTempFileBody.java
new file mode 100644
index 0000000..f0821ed
--- /dev/null
+++ b/email2/emailcommon/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/email2/emailcommon/src/com/android/emailcommon/internet/MimeBodyPart.java b/email2/emailcommon/src/com/android/emailcommon/internet/MimeBodyPart.java
new file mode 100644
index 0000000..01efd55
--- /dev/null
+++ b/email2/emailcommon/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/email2/emailcommon/src/com/android/emailcommon/internet/MimeHeader.java b/email2/emailcommon/src/com/android/emailcommon/internet/MimeHeader.java
new file mode 100644
index 0000000..b0ad777
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/internet/MimeHeader.java
@@ -0,0 +1,153 @@
+/*
+ * 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 com.android.emailcommon.utility.Utility;
+
+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 (!Utility.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 (!Utility.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();
+    }
+}
diff --git a/email2/emailcommon/src/com/android/emailcommon/internet/MimeMessage.java b/email2/emailcommon/src/com/android/emailcommon/internet/MimeMessage.java
new file mode 100644
index 0000000..412092d
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/internet/MimeMessage.java
@@ -0,0 +1,626 @@
+/*
+ * 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;
+
+    // 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);
+    }
+
+    protected void parse(InputStream in) throws IOException, MessagingException {
+        // 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());
+        parser.parse(new EOLConvertingInputStream(in));
+    }
+
+    /**
+     * 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 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/email2/emailcommon/src/com/android/emailcommon/internet/MimeMultipart.java b/email2/emailcommon/src/com/android/emailcommon/internet/MimeMultipart.java
new file mode 100644
index 0000000..e6977ee
--- /dev/null
+++ b/email2/emailcommon/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/email2/emailcommon/src/com/android/emailcommon/internet/MimeUtility.java b/email2/emailcommon/src/com/android/emailcommon/internet/MimeUtility.java
new file mode 100644
index 0000000..3e1488d
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/internet/MimeUtility.java
@@ -0,0 +1,461 @@
+/*
+ * 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.Logging;
+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 android.util.Base64;
+import android.util.Base64DataException;
+import android.util.Base64InputStream;
+import android.util.Log;
+
+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 {
+
+    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(Logging.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(Logging.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 = null;
+        String dispositionFilename = null;
+        if (disposition != null) {
+            dispositionType = MimeUtility.getHeaderParameter(disposition, null);
+            dispositionFilename = MimeUtility.getHeaderParameter(disposition, "filename");
+        }
+        // An attachment filename can be defined in either the Content-Disposition header
+        // or the Content-Type header. Content-Disposition is preferred, so we only try
+        // the Content-Type header as a last resort.
+        if (dispositionFilename == null) {
+            String contentType = part.getContentType();
+            dispositionFilename = MimeUtility.getHeaderParameter(contentType, "name");
+        }
+        boolean attachmentDisposition = "attachment".equalsIgnoreCase(dispositionType);
+        // If a disposition is not specified, default to "inline"
+        boolean inlineDisposition = dispositionType == null
+                || "inline".equalsIgnoreCase(dispositionType);
+
+        // A guess that this part is intended to be an attachment
+        boolean attachment = attachmentDisposition
+                || (dispositionFilename != null && !inlineDisposition);
+
+        // A guess that this part is intended to be an inline.
+        boolean inline = inlineDisposition && (dispositionFilename != null);
+
+        // One or the other
+        boolean attachmentOrInline = attachment || inline;
+
+        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.
+            Multipart mp = (Multipart)part.getBody();
+            for (int i = 0; i < mp.getCount(); i++) {
+                collectParts(mp.getBodyPart(i), 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 ((!attachmentOrInline) && ("text/html".equalsIgnoreCase(part.getMimeType()))) {
+            // If the part is HTML and we got this far, it's a viewable part of a mixed
+            viewables.add(part);
+        } else if ((!attachmentOrInline) && ("text/plain".equalsIgnoreCase(part.getMimeType()))) {
+            // If the part is text and we got this far, it's a viewable part of a mixed
+            viewables.add(part);
+        } else if (attachmentOrInline) {
+            // Finally, if it's an attachment or an inline we will include it as an attachment.
+            attachments.add(part);
+        }
+    }
+}
diff --git a/email2/emailcommon/src/com/android/emailcommon/internet/Rfc822Output.java b/email2/emailcommon/src/com/android/emailcommon/internet/Rfc822Output.java
new file mode 100644
index 0000000..e03a926
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/internet/Rfc822Output.java
@@ -0,0 +1,473 @@
+/*
+ * Copyright (C) 2009 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.MessagingException;
+import com.android.emailcommon.provider.EmailContent.Attachment;
+import com.android.emailcommon.provider.EmailContent.Body;
+import com.android.emailcommon.provider.EmailContent.Message;
+
+import org.apache.commons.io.IOUtils;
+
+import android.content.ContentUris;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.text.Html;
+import android.text.TextUtils;
+import android.util.Base64;
+import android.util.Base64OutputStream;
+
+import java.io.BufferedOutputStream;
+import java.io.ByteArrayInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Utility class to output RFC 822 messages from provider email messages
+ */
+public class Rfc822Output {
+
+    private static final Pattern PATTERN_START_OF_LINE = Pattern.compile("(?m)^");
+    private static final Pattern PATTERN_ENDLINE_CRLF = Pattern.compile("\r\n");
+
+    // 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).
+    private static final SimpleDateFormat DATE_FORMAT =
+        new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z", Locale.US);
+
+    private static final String WHERE_NOT_SMART_FORWARD = "(" + Attachment.FLAGS + "&" +
+        Attachment.FLAG_SMART_FORWARD + ")=0";
+
+    /** A less-than-perfect pattern to pull out <body> content */
+    private static final Pattern BODY_PATTERN = Pattern.compile(
+                "(?:<\\s*body[^>]*>)(.*)(?:<\\s*/\\s*body\\s*>)",
+                Pattern.CASE_INSENSITIVE | Pattern.DOTALL);
+    /** Match group in {@code BODDY_PATTERN} for the body HTML */ 
+    private static final int BODY_PATTERN_GROUP = 1;
+    /** Pattern to find both dos and unix newlines */
+    private static final Pattern NEWLINE_PATTERN =
+        Pattern.compile("\\r?\\n");
+    /** HTML string to use when replacing text newlines */
+    private static final String NEWLINE_HTML = "<br>";
+    /** Index of the plain text version of the message body */
+    private final static int INDEX_BODY_TEXT = 0;
+    /** Index of the HTML version of the message body */
+    private final static int INDEX_BODY_HTML = 1;
+    /** Single digit [0-9] to ensure uniqueness of the MIME boundary */
+    /*package*/ static byte sBoundaryDigit;
+
+    /**
+     * Returns just the content between the <body></body> tags. This is not perfect and breaks
+     * with malformed HTML or if there happens to be special characters in the attributes of
+     * the <body> tag (e.g. a '>' in a java script block).
+     */
+    /*package*/ static String getHtmlBody(String html) {
+        Matcher match = BODY_PATTERN.matcher(html);
+        if (match.find()) {
+            return match.group(BODY_PATTERN_GROUP);    // Found body; return
+        } else {
+            return html;              // Body not found; return the full HTML and hope for the best
+        }
+    }
+
+    /**
+     * Returns an HTML encoded message alternate
+     */
+    /*package*/ static String getHtmlAlternate(Body body, boolean useSmartReply) {
+        if (body.mHtmlReply == null) {
+            return null;
+        }
+        StringBuffer altMessage = new StringBuffer();
+        String htmlContent = TextUtils.htmlEncode(body.mTextContent); // Escape HTML reserved chars
+        htmlContent = NEWLINE_PATTERN.matcher(htmlContent).replaceAll(NEWLINE_HTML);
+        altMessage.append(htmlContent);
+        if (body.mIntroText != null) {
+            String htmlIntro = TextUtils.htmlEncode(body.mIntroText);
+            htmlIntro = NEWLINE_PATTERN.matcher(htmlIntro).replaceAll(NEWLINE_HTML);
+            altMessage.append(htmlIntro);
+        }
+        if (!useSmartReply) {
+            String htmlBody = getHtmlBody(body.mHtmlReply);
+            altMessage.append(htmlBody);
+        }
+        return altMessage.toString();
+    }
+
+    /**
+     * Gets both the plain text and HTML versions of the message body.
+     */
+    /*package*/ static String[] buildBodyText(Body body, int flags, boolean useSmartReply) {
+        String[] messageBody = new String[] { null, null };
+        if (body == null) {
+            return messageBody;
+        }
+        String text = body.mTextContent;
+        boolean isReply = (flags & Message.FLAG_TYPE_REPLY) != 0;
+        boolean isForward = (flags & Message.FLAG_TYPE_FORWARD) != 0;
+        // For all forwards/replies, we add the intro text
+        if (isReply || isForward) {
+            String intro = body.mIntroText == null ? "" : body.mIntroText;
+            text += intro;
+        }
+        if (useSmartReply) {
+            // useSmartReply is set to true for use by SmartReply/SmartForward in EAS.
+            // SmartForward doesn't put a break between the original and new text, so we add an LF
+            if (isForward) {
+                text += "\n";
+            }
+        } else {
+            String quotedText = body.mTextReply;
+            // If there is no plain-text body, use de-tagified HTML as the text body
+            if (quotedText == null && body.mHtmlReply != null) {
+                quotedText = Html.fromHtml(body.mHtmlReply).toString();
+            }
+            if (quotedText != null) {
+                // fix CR-LF line endings to LF-only needed by EditText.
+                Matcher matcher = PATTERN_ENDLINE_CRLF.matcher(quotedText);
+                quotedText = matcher.replaceAll("\n");
+            }
+            if (isReply) {
+                if (quotedText != null) {
+                    Matcher matcher = PATTERN_START_OF_LINE.matcher(quotedText);
+                    text += matcher.replaceAll(">");
+                }
+            } else if (isForward) {
+                if (quotedText != null) {
+                    text += quotedText;
+                }
+            }
+        }
+        messageBody[INDEX_BODY_TEXT] = text;
+        // Exchange 2003 doesn't seem to support multipart w/SmartReply and SmartForward, so
+        // we'll skip this.  Really, it would only matter if we could compose HTML replies
+        if (!useSmartReply) {
+            messageBody[INDEX_BODY_HTML] = getHtmlAlternate(body, useSmartReply);
+        }
+        return messageBody;
+    }
+
+    /**
+     * Write the entire message to an output stream.  This method provides buffering, so it is
+     * not necessary to pass in a buffered output stream here.
+     *
+     * @param context system context for accessing the provider
+     * @param messageId the message to write out
+     * @param out the output stream to write the message to
+     * @param useSmartReply whether or not quoted text is appended to a reply/forward
+     */
+    public static void writeTo(Context context, long messageId, OutputStream out,
+            boolean useSmartReply, boolean sendBcc) throws IOException, MessagingException {
+        Message message = Message.restoreMessageWithId(context, messageId);
+        if (message == null) {
+            // throw something?
+            return;
+        }
+
+        OutputStream stream = new BufferedOutputStream(out, 1024);
+        Writer writer = new OutputStreamWriter(stream);
+
+        // Write the fixed headers.  Ordering is arbitrary (the legacy code iterated through a
+        // hashmap here).
+
+        String date = DATE_FORMAT.format(new Date(message.mTimeStamp));
+        writeHeader(writer, "Date", date);
+
+        writeEncodedHeader(writer, "Subject", message.mSubject);
+
+        writeHeader(writer, "Message-ID", message.mMessageId);
+
+        writeAddressHeader(writer, "From", message.mFrom);
+        writeAddressHeader(writer, "To", message.mTo);
+        writeAddressHeader(writer, "Cc", message.mCc);
+        // Address fields.  Note that we skip bcc unless the sendBcc argument is true
+        // SMTP should NOT send bcc headers, but EAS must send it!
+        if (sendBcc) {
+            writeAddressHeader(writer, "Bcc", message.mBcc);
+        }
+        writeAddressHeader(writer, "Reply-To", message.mReplyTo);
+        writeHeader(writer, "MIME-Version", "1.0");
+
+        // Analyze message and determine if we have multiparts
+        Body body = Body.restoreBodyWithMessageId(context, message.mId);
+        String[] bodyText = buildBodyText(body, message.mFlags, useSmartReply);
+
+        Uri uri = ContentUris.withAppendedId(Attachment.MESSAGE_ID_URI, messageId);
+        Cursor attachmentsCursor = context.getContentResolver().query(uri,
+                Attachment.CONTENT_PROJECTION, WHERE_NOT_SMART_FORWARD, null, null);
+
+        try {
+            int attachmentCount = attachmentsCursor.getCount();
+            boolean multipart = attachmentCount > 0;
+            String multipartBoundary = null;
+            String multipartType = "mixed";
+
+            // Simplified case for no multipart - just emit text and be done.
+            if (!multipart) {
+                writeTextWithHeaders(writer, stream, bodyText);
+            } else {
+                // continue with multipart headers, then into multipart body
+                multipartBoundary = getNextBoundary();
+
+                // Move to the first attachment; this must succeed because multipart is true
+                attachmentsCursor.moveToFirst();
+                if (attachmentCount == 1) {
+                    // If we've got one attachment and it's an ics "attachment", we want to send
+                    // this as multipart/alternative instead of multipart/mixed
+                    int flags = attachmentsCursor.getInt(Attachment.CONTENT_FLAGS_COLUMN);
+                    if ((flags & Attachment.FLAG_ICS_ALTERNATIVE_PART) != 0) {
+                        multipartType = "alternative";
+                    }
+                }
+
+                writeHeader(writer, "Content-Type",
+                        "multipart/" + multipartType + "; boundary=\"" + multipartBoundary + "\"");
+                // Finish headers and prepare for body section(s)
+                writer.write("\r\n");
+
+                // first multipart element is the body
+                if (bodyText[INDEX_BODY_TEXT] != null) {
+                    writeBoundary(writer, multipartBoundary, false);
+                    writeTextWithHeaders(writer, stream, bodyText);
+                }
+
+                // Write out the attachments until we run out
+                do {
+                    writeBoundary(writer, multipartBoundary, false);
+                    Attachment attachment =
+                        Attachment.getContent(attachmentsCursor, Attachment.class);
+                    writeOneAttachment(context, writer, stream, attachment);
+                    writer.write("\r\n");
+                } while (attachmentsCursor.moveToNext());
+
+                // end of multipart section
+                writeBoundary(writer, multipartBoundary, true);
+            }
+        } finally {
+            attachmentsCursor.close();
+        }
+
+        writer.flush();
+        out.flush();
+    }
+
+    /**
+     * Write a single attachment and its payload
+     */
+    private static void writeOneAttachment(Context context, Writer writer, OutputStream out,
+            Attachment attachment) throws IOException, MessagingException {
+        writeHeader(writer, "Content-Type",
+                attachment.mMimeType + ";\n name=\"" + attachment.mFileName + "\"");
+        writeHeader(writer, "Content-Transfer-Encoding", "base64");
+        // Most attachments (real files) will send Content-Disposition.  The suppression option
+        // is used when sending calendar invites.
+        if ((attachment.mFlags & Attachment.FLAG_ICS_ALTERNATIVE_PART) == 0) {
+            writeHeader(writer, "Content-Disposition",
+                    "attachment;"
+                    + "\n filename=\"" + attachment.mFileName + "\";"
+                    + "\n size=" + Long.toString(attachment.mSize));
+        }
+        if (attachment.mContentId != null) {
+            writeHeader(writer, "Content-ID", attachment.mContentId);
+        }
+        writer.append("\r\n");
+
+        // Set up input stream and write it out via base64
+        InputStream inStream = null;
+        try {
+            // Use content, if provided; otherwise, use the contentUri
+            if (attachment.mContentBytes != null) {
+                inStream = new ByteArrayInputStream(attachment.mContentBytes);
+            } else {
+                // try to open the file
+                Uri fileUri = Uri.parse(attachment.mContentUri);
+                inStream = context.getContentResolver().openInputStream(fileUri);
+            }
+            // switch to output stream for base64 text output
+            writer.flush();
+            Base64OutputStream base64Out = new Base64OutputStream(
+                out, Base64.CRLF | Base64.NO_CLOSE);
+            // copy base64 data and close up
+            IOUtils.copy(inStream, base64Out);
+            base64Out.close();
+
+            // The old Base64OutputStream wrote an extra CRLF after
+            // the output.  It's not required by the base-64 spec; not
+            // sure if it's required by RFC 822 or not.
+            out.write('\r');
+            out.write('\n');
+            out.flush();
+        }
+        catch (FileNotFoundException fnfe) {
+            // Ignore this - empty file is OK
+        }
+        catch (IOException ioe) {
+            throw new MessagingException("Invalid attachment.", ioe);
+        }
+    }
+
+    /**
+     * Write a single header with no wrapping or encoding
+     *
+     * @param writer the output writer
+     * @param name the header name
+     * @param value the header value
+     */
+    private static void writeHeader(Writer writer, String name, String value) throws IOException {
+        if (value != null && value.length() > 0) {
+            writer.append(name);
+            writer.append(": ");
+            writer.append(value);
+            writer.append("\r\n");
+        }
+    }
+
+    /**
+     * Write a single header using appropriate folding & encoding
+     *
+     * @param writer the output writer
+     * @param name the header name
+     * @param value the header value
+     */
+    private static void writeEncodedHeader(Writer writer, String name, String value)
+            throws IOException {
+        if (value != null && value.length() > 0) {
+            writer.append(name);
+            writer.append(": ");
+            writer.append(MimeUtility.foldAndEncode2(value, name.length() + 2));
+            writer.append("\r\n");
+        }
+    }
+
+    /**
+     * Unpack, encode, and fold address(es) into a header
+     *
+     * @param writer the output writer
+     * @param name the header name
+     * @param value the header value (a packed list of addresses)
+     */
+    private static void writeAddressHeader(Writer writer, String name, String value)
+            throws IOException {
+        if (value != null && value.length() > 0) {
+            writer.append(name);
+            writer.append(": ");
+            writer.append(MimeUtility.fold(Address.packedToHeader(value), name.length() + 2));
+            writer.append("\r\n");
+        }
+    }
+
+    /**
+     * Write a multipart boundary
+     *
+     * @param writer the output writer
+     * @param boundary the boundary string
+     * @param end false if inner boundary, true if final boundary
+     */
+    private static void writeBoundary(Writer writer, String boundary, boolean end)
+            throws IOException {
+        writer.append("--");
+        writer.append(boundary);
+        if (end) {
+            writer.append("--");
+        }
+        writer.append("\r\n");
+    }
+
+    /**
+     * Write the body text. If only one version of the body is specified (either plain text
+     * or HTML), the text is written directly. Otherwise, the plain text and HTML bodies
+     * are both written with the appropriate headers.
+     *
+     * Note this always uses base64, even when not required.  Slightly less efficient for
+     * US-ASCII text, but handles all formats even when non-ascii chars are involved.  A small
+     * optimization might be to prescan the string for safety and send raw if possible.
+     *
+     * @param writer the output writer
+     * @param out the output stream inside the writer (used for byte[] access)
+     * @param bodyText Plain text and HTML versions of the original text of the message
+     */
+    private static void writeTextWithHeaders(Writer writer, OutputStream out, String[] bodyText)
+            throws IOException {
+        String text = bodyText[INDEX_BODY_TEXT];
+        String html = bodyText[INDEX_BODY_HTML];
+
+        if (text == null) {
+            writer.write("\r\n");       // a truly empty message
+        } else {
+            String multipartBoundary = null;
+            boolean multipart = html != null;
+
+            // Simplified case for no multipart - just emit text and be done.
+            if (multipart) {
+                // continue with multipart headers, then into multipart body
+                multipartBoundary = getNextBoundary();
+
+                writeHeader(writer, "Content-Type",
+                        "multipart/alternative; boundary=\"" + multipartBoundary + "\"");
+                // Finish headers and prepare for body section(s)
+                writer.write("\r\n");
+                writeBoundary(writer, multipartBoundary, false);
+            }
+
+            // first multipart element is the body
+            writeHeader(writer, "Content-Type", "text/plain; charset=utf-8");
+            writeHeader(writer, "Content-Transfer-Encoding", "base64");
+            writer.write("\r\n");
+            byte[] textBytes = text.getBytes("UTF-8");
+            writer.flush();
+            out.write(Base64.encode(textBytes, Base64.CRLF));
+
+            if (multipart) {
+                // next multipart section
+                writeBoundary(writer, multipartBoundary, false);
+
+                writeHeader(writer, "Content-Type", "text/html; charset=utf-8");
+                writeHeader(writer, "Content-Transfer-Encoding", "base64");
+                writer.write("\r\n");
+                byte[] htmlBytes = html.getBytes("UTF-8");
+                writer.flush();
+                out.write(Base64.encode(htmlBytes, Base64.CRLF));
+
+                // end of multipart section
+                writeBoundary(writer, multipartBoundary, true);
+            }
+        }
+    }
+
+    /**
+     * Returns a unique boundary string.
+     */
+    /*package*/ static String getNextBoundary() {
+        StringBuilder boundary = new StringBuilder();
+        boundary.append("--_com.android.email_").append(System.nanoTime());
+        synchronized (Rfc822Output.class) {
+            boundary = boundary.append(sBoundaryDigit);
+            sBoundaryDigit = (byte)((sBoundaryDigit + 1) % 10);
+        }
+        return boundary.toString();
+    }
+}
diff --git a/email2/emailcommon/src/com/android/emailcommon/internet/TextBody.java b/email2/emailcommon/src/com/android/emailcommon/internet/TextBody.java
new file mode 100644
index 0000000..09c265c
--- /dev/null
+++ b/email2/emailcommon/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/email2/emailcommon/src/com/android/emailcommon/mail/Address.java b/email2/emailcommon/src/com/android/emailcommon/mail/Address.java
new file mode 100644
index 0000000..5245c56
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/mail/Address.java
@@ -0,0 +1,428 @@
+/*
+ * 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.android.emailcommon.utility.Utility;
+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 Utility.quoteString(mPersonal) + " <" + mAddress + ">";
+            } else {
+                return mPersonal + " <" + mAddress + ">";
+            }
+        } else {
+            return mAddress;
+        }
+    }
+
+    /**
+     * 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/email2/emailcommon/src/com/android/emailcommon/mail/AuthenticationFailedException.java b/email2/emailcommon/src/com/android/emailcommon/mail/AuthenticationFailedException.java
new file mode 100644
index 0000000..af8d96c
--- /dev/null
+++ b/email2/emailcommon/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/email2/emailcommon/src/com/android/emailcommon/mail/Body.java b/email2/emailcommon/src/com/android/emailcommon/mail/Body.java
new file mode 100644
index 0000000..841ab42
--- /dev/null
+++ b/email2/emailcommon/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/email2/emailcommon/src/com/android/emailcommon/mail/BodyPart.java b/email2/emailcommon/src/com/android/emailcommon/mail/BodyPart.java
new file mode 100644
index 0000000..f698a13
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/mail/BodyPart.java
@@ -0,0 +1,25 @@
+/*

+ * 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/email2/emailcommon/src/com/android/emailcommon/mail/CertificateValidationException.java b/email2/emailcommon/src/com/android/emailcommon/mail/CertificateValidationException.java
new file mode 100644
index 0000000..83c6224
--- /dev/null
+++ b/email2/emailcommon/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/email2/emailcommon/src/com/android/emailcommon/mail/FetchProfile.java b/email2/emailcommon/src/com/android/emailcommon/mail/FetchProfile.java
new file mode 100644
index 0000000..bfa48d3
--- /dev/null
+++ b/email2/emailcommon/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/email2/emailcommon/src/com/android/emailcommon/mail/Fetchable.java b/email2/emailcommon/src/com/android/emailcommon/mail/Fetchable.java
new file mode 100644
index 0000000..4314f93
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/mail/Fetchable.java
@@ -0,0 +1,24 @@
+/*
+ * 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;
+
+/**
+ * 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/email2/emailcommon/src/com/android/emailcommon/mail/Flag.java b/email2/emailcommon/src/com/android/emailcommon/mail/Flag.java
new file mode 100644
index 0000000..bcdcb8b
--- /dev/null
+++ b/email2/emailcommon/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/email2/emailcommon/src/com/android/emailcommon/mail/Folder.java b/email2/emailcommon/src/com/android/emailcommon/mail/Folder.java
new file mode 100644
index 0000000..c58988d
--- /dev/null
+++ b/email2/emailcommon/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/email2/emailcommon/src/com/android/emailcommon/mail/MeetingInfo.java b/email2/emailcommon/src/com/android/emailcommon/mail/MeetingInfo.java
new file mode 100644
index 0000000..1489a55
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/mail/MeetingInfo.java
@@ -0,0 +1,29 @@
+/*
+ * 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";
+}
diff --git a/email2/emailcommon/src/com/android/emailcommon/mail/Message.java b/email2/emailcommon/src/com/android/emailcommon/mail/Message.java
new file mode 100644
index 0000000..09aef87
--- /dev/null
+++ b/email2/emailcommon/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/email2/emailcommon/src/com/android/emailcommon/mail/MessageDateComparator.java b/email2/emailcommon/src/com/android/emailcommon/mail/MessageDateComparator.java
new file mode 100644
index 0000000..0b1a551
--- /dev/null
+++ b/email2/emailcommon/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/email2/emailcommon/src/com/android/emailcommon/mail/MessagingException.java b/email2/emailcommon/src/com/android/emailcommon/mail/MessagingException.java
new file mode 100644
index 0000000..4a8ceba
--- /dev/null
+++ b/email2/emailcommon/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/email2/emailcommon/src/com/android/emailcommon/mail/Multipart.java b/email2/emailcommon/src/com/android/emailcommon/mail/Multipart.java
new file mode 100644
index 0000000..4a1a067
--- /dev/null
+++ b/email2/emailcommon/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/email2/emailcommon/src/com/android/emailcommon/mail/PackedString.java b/email2/emailcommon/src/com/android/emailcommon/mail/PackedString.java
new file mode 100644
index 0000000..de5fe46
--- /dev/null
+++ b/email2/emailcommon/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/email2/emailcommon/src/com/android/emailcommon/mail/Part.java b/email2/emailcommon/src/com/android/emailcommon/mail/Part.java
new file mode 100644
index 0000000..eeb233c
--- /dev/null
+++ b/email2/emailcommon/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/email2/emailcommon/src/com/android/emailcommon/provider/Account.java b/email2/emailcommon/src/com/android/emailcommon/provider/Account.java
new file mode 100755
index 0000000..3914176
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/provider/Account.java
@@ -0,0 +1,968 @@
+/*
+ * 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.provider;
+
+import android.content.ContentProviderOperation;
+import android.content.ContentProviderResult;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.OperationApplicationException;
+import android.database.Cursor;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.RemoteException;
+
+import com.android.emailcommon.provider.EmailContent.AccountColumns;
+import com.android.emailcommon.utility.Utility;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+public final class Account extends EmailContent implements AccountColumns, Parcelable {
+    public static final String TABLE_NAME = "Account";
+    @SuppressWarnings("hiding")
+    public static final Uri CONTENT_URI = Uri.parse(EmailContent.CONTENT_URI + "/account");
+    public static final Uri ADD_TO_FIELD_URI =
+        Uri.parse(EmailContent.CONTENT_URI + "/accountIdAddToField");
+    public static final Uri RESET_NEW_MESSAGE_COUNT_URI =
+        Uri.parse(EmailContent.CONTENT_URI + "/resetNewMessageCount");
+    public static final Uri NOTIFIER_URI =
+        Uri.parse(EmailContent.CONTENT_NOTIFIER_URI + "/account");
+    public static final Uri DEFAULT_ACCOUNT_ID_URI =
+        Uri.parse(EmailContent.CONTENT_URI + "/account/default");
+
+    // Define all pseudo account IDs here to avoid conflict with one another.
+    /**
+     * Pseudo account ID to represent a "combined account" that includes messages and mailboxes
+     * from all defined accounts.
+     *
+     * <em>IMPORTANT</em>: This must never be stored to the database.
+     */
+    public static final long ACCOUNT_ID_COMBINED_VIEW = 0x1000000000000000L;
+    /**
+     * Pseudo account ID to represent "no account". This may be used any time the account ID
+     * may not be known or when we want to specifically select "no" account.
+     *
+     * <em>IMPORTANT</em>: This must never be stored to the database.
+     */
+    public static final long NO_ACCOUNT = -1L;
+
+    // Whether or not the user has asked for notifications of new mail in this account
+    public final static int FLAGS_NOTIFY_NEW_MAIL = 1<<0;
+    // Whether or not the user has asked for vibration notifications with all new mail
+    public final static int FLAGS_VIBRATE_ALWAYS = 1<<1;
+    // Bit mask for the account's deletion policy (see DELETE_POLICY_x below)
+    public static final int FLAGS_DELETE_POLICY_MASK = 1<<2 | 1<<3;
+    public static final int FLAGS_DELETE_POLICY_SHIFT = 2;
+    // Whether the account is in the process of being created; any account reconciliation code
+    // MUST ignore accounts with this bit set; in addition, ContentObservers for this data
+    // SHOULD consider the state of this flag during operation
+    public static final int FLAGS_INCOMPLETE = 1<<4;
+    // Security hold is used when the device is not in compliance with security policies
+    // required by the server; in this state, the user MUST be alerted to the need to update
+    // security settings.  Sync adapters SHOULD NOT attempt to sync when this flag is set.
+    public static final int FLAGS_SECURITY_HOLD = 1<<5;
+    // Whether or not the user has asked for vibration notifications when the ringer is silent
+    public static final int FLAGS_VIBRATE_WHEN_SILENT = 1<<6;
+    // Whether the account supports "smart forward" (i.e. the server appends the original
+    // message along with any attachments to the outgoing message)
+    public static final int FLAGS_SUPPORTS_SMART_FORWARD = 1<<7;
+    // Whether the account should try to cache attachments in the background
+    public static final int FLAGS_BACKGROUND_ATTACHMENTS = 1<<8;
+    // Available to sync adapter
+    public static final int FLAGS_SYNC_ADAPTER = 1<<9;
+    // Sync disabled is a status commanded by the server; the sync adapter SHOULD NOT try to
+    // sync mailboxes in this account automatically.  A manual sync request to sync a mailbox
+    // with sync disabled SHOULD try to sync and report any failure result via the UI.
+    public static final int FLAGS_SYNC_DISABLED = 1<<10;
+    // Whether or not server-side search is supported by this account
+    public static final int FLAGS_SUPPORTS_SEARCH = 1<<11;
+    // Whether or not server-side search supports global search (i.e. all mailboxes); only valid
+    // if FLAGS_SUPPORTS_SEARCH is true
+    public static final int FLAGS_SUPPORTS_GLOBAL_SEARCH = 1<<12;
+
+    // Deletion policy (see FLAGS_DELETE_POLICY_MASK, above)
+    public static final int DELETE_POLICY_NEVER = 0;
+    public static final int DELETE_POLICY_7DAYS = 1<<0;        // not supported
+    public static final int DELETE_POLICY_ON_DELETE = 1<<1;
+
+    // Sentinel values for the mSyncInterval field of both Account records
+    public static final int CHECK_INTERVAL_NEVER = -1;
+    public static final int CHECK_INTERVAL_PUSH = -2;
+
+    public String mDisplayName;
+    public String mEmailAddress;
+    public String mSyncKey;
+    public int mSyncLookback;
+    public int mSyncInterval;
+    public long mHostAuthKeyRecv;
+    public long mHostAuthKeySend;
+    public int mFlags;
+    public boolean mIsDefault;          // note: callers should use getDefaultAccountId()
+    public String mCompatibilityUuid;
+    public String mSenderName;
+    public String mRingtoneUri;
+    public String mProtocolVersion;
+    public int mNewMessageCount;
+    public String mSecuritySyncKey;
+    public String mSignature;
+    public long mPolicyKey;
+
+    // Convenience for creating/working with an account
+    public transient HostAuth mHostAuthRecv;
+    public transient HostAuth mHostAuthSend;
+    public transient Policy mPolicy;
+    // Might hold the corresponding AccountManager account structure
+    public transient android.accounts.Account mAmAccount;
+
+    public static final int CONTENT_ID_COLUMN = 0;
+    public static final int CONTENT_DISPLAY_NAME_COLUMN = 1;
+    public static final int CONTENT_EMAIL_ADDRESS_COLUMN = 2;
+    public static final int CONTENT_SYNC_KEY_COLUMN = 3;
+    public static final int CONTENT_SYNC_LOOKBACK_COLUMN = 4;
+    public static final int CONTENT_SYNC_INTERVAL_COLUMN = 5;
+    public static final int CONTENT_HOST_AUTH_KEY_RECV_COLUMN = 6;
+    public static final int CONTENT_HOST_AUTH_KEY_SEND_COLUMN = 7;
+    public static final int CONTENT_FLAGS_COLUMN = 8;
+    public static final int CONTENT_IS_DEFAULT_COLUMN = 9;
+    public static final int CONTENT_COMPATIBILITY_UUID_COLUMN = 10;
+    public static final int CONTENT_SENDER_NAME_COLUMN = 11;
+    public static final int CONTENT_RINGTONE_URI_COLUMN = 12;
+    public static final int CONTENT_PROTOCOL_VERSION_COLUMN = 13;
+    public static final int CONTENT_NEW_MESSAGE_COUNT_COLUMN = 14;
+    public static final int CONTENT_SECURITY_SYNC_KEY_COLUMN = 15;
+    public static final int CONTENT_SIGNATURE_COLUMN = 16;
+    public static final int CONTENT_POLICY_KEY = 17;
+
+    public static final String[] CONTENT_PROJECTION = new String[] {
+        RECORD_ID, AccountColumns.DISPLAY_NAME,
+        AccountColumns.EMAIL_ADDRESS, AccountColumns.SYNC_KEY, AccountColumns.SYNC_LOOKBACK,
+        AccountColumns.SYNC_INTERVAL, AccountColumns.HOST_AUTH_KEY_RECV,
+        AccountColumns.HOST_AUTH_KEY_SEND, AccountColumns.FLAGS, AccountColumns.IS_DEFAULT,
+        AccountColumns.COMPATIBILITY_UUID, AccountColumns.SENDER_NAME,
+        AccountColumns.RINGTONE_URI, AccountColumns.PROTOCOL_VERSION,
+        AccountColumns.NEW_MESSAGE_COUNT, AccountColumns.SECURITY_SYNC_KEY,
+        AccountColumns.SIGNATURE, AccountColumns.POLICY_KEY
+    };
+
+    public static final int CONTENT_MAILBOX_TYPE_COLUMN = 1;
+
+    /**
+     * This projection is for listing account id's only
+     */
+    public static final String[] ID_TYPE_PROJECTION = new String[] {
+        RECORD_ID, MailboxColumns.TYPE
+    };
+
+    public static final int ACCOUNT_FLAGS_COLUMN_ID = 0;
+    public static final int ACCOUNT_FLAGS_COLUMN_FLAGS = 1;
+    public static final String[] ACCOUNT_FLAGS_PROJECTION = new String[] {
+            AccountColumns.ID, AccountColumns.FLAGS};
+
+    public static final String MAILBOX_SELECTION =
+        MessageColumns.MAILBOX_KEY + " =?";
+
+    public static final String UNREAD_COUNT_SELECTION =
+        MessageColumns.MAILBOX_KEY + " =? and " + MessageColumns.FLAG_READ + "= 0";
+
+    private static final String UUID_SELECTION = AccountColumns.COMPATIBILITY_UUID + " =?";
+
+    public static final String SECURITY_NONZERO_SELECTION =
+        Account.POLICY_KEY + " IS NOT NULL AND " + Account.POLICY_KEY + "!=0";
+
+    private static final String FIND_INBOX_SELECTION =
+            MailboxColumns.TYPE + " = " + Mailbox.TYPE_INBOX +
+            " AND " + MailboxColumns.ACCOUNT_KEY + " =?";
+
+    /**
+     * This projection is for searching for the default account
+     */
+    private static final String[] DEFAULT_ID_PROJECTION = new String[] {
+        RECORD_ID, IS_DEFAULT
+    };
+
+    /**
+     * no public constructor since this is a utility class
+     */
+    public Account() {
+        mBaseUri = CONTENT_URI;
+
+        // other defaults (policy)
+        mRingtoneUri = "content://settings/system/notification_sound";
+        mSyncInterval = -1;
+        mSyncLookback = -1;
+        mFlags = FLAGS_NOTIFY_NEW_MAIL;
+        mCompatibilityUuid = UUID.randomUUID().toString();
+    }
+
+    public static Account restoreAccountWithId(Context context, long id) {
+        return EmailContent.restoreContentWithId(context, Account.class,
+                Account.CONTENT_URI, Account.CONTENT_PROJECTION, id);
+    }
+
+    /**
+     * Returns {@code true} if the given account ID is a "normal" account. Normal accounts
+     * always have an ID greater than {@code 0} and not equal to any pseudo account IDs
+     * (such as {@link #ACCOUNT_ID_COMBINED_VIEW})
+     */
+    public static boolean isNormalAccount(long accountId) {
+        return (accountId > 0L) && (accountId != ACCOUNT_ID_COMBINED_VIEW);
+    }
+
+    /**
+     * Refresh an account that has already been loaded.  This is slightly less expensive
+     * that generating a brand-new account object.
+     */
+    public void refresh(Context context) {
+        Cursor c = context.getContentResolver().query(getUri(), Account.CONTENT_PROJECTION,
+                null, null, null);
+        try {
+            c.moveToFirst();
+            restore(c);
+        } finally {
+            if (c != null) {
+                c.close();
+            }
+        }
+    }
+
+    @Override
+    public void restore(Cursor cursor) {
+        mId = cursor.getLong(CONTENT_ID_COLUMN);
+        mBaseUri = CONTENT_URI;
+        mDisplayName = cursor.getString(CONTENT_DISPLAY_NAME_COLUMN);
+        mEmailAddress = cursor.getString(CONTENT_EMAIL_ADDRESS_COLUMN);
+        mSyncKey = cursor.getString(CONTENT_SYNC_KEY_COLUMN);
+        mSyncLookback = cursor.getInt(CONTENT_SYNC_LOOKBACK_COLUMN);
+        mSyncInterval = cursor.getInt(CONTENT_SYNC_INTERVAL_COLUMN);
+        mHostAuthKeyRecv = cursor.getLong(CONTENT_HOST_AUTH_KEY_RECV_COLUMN);
+        mHostAuthKeySend = cursor.getLong(CONTENT_HOST_AUTH_KEY_SEND_COLUMN);
+        mFlags = cursor.getInt(CONTENT_FLAGS_COLUMN);
+        mIsDefault = cursor.getInt(CONTENT_IS_DEFAULT_COLUMN) == 1;
+        mCompatibilityUuid = cursor.getString(CONTENT_COMPATIBILITY_UUID_COLUMN);
+        mSenderName = cursor.getString(CONTENT_SENDER_NAME_COLUMN);
+        mRingtoneUri = cursor.getString(CONTENT_RINGTONE_URI_COLUMN);
+        mProtocolVersion = cursor.getString(CONTENT_PROTOCOL_VERSION_COLUMN);
+        mNewMessageCount = cursor.getInt(CONTENT_NEW_MESSAGE_COUNT_COLUMN);
+        mSecuritySyncKey = cursor.getString(CONTENT_SECURITY_SYNC_KEY_COLUMN);
+        mSignature = cursor.getString(CONTENT_SIGNATURE_COLUMN);
+        mPolicyKey = cursor.getLong(CONTENT_POLICY_KEY);
+    }
+
+    private long getId(Uri u) {
+        return Long.parseLong(u.getPathSegments().get(1));
+    }
+
+    /**
+     * @return the user-visible name for the account
+     */
+    public String getDisplayName() {
+        return mDisplayName;
+    }
+
+    /**
+     * Set the description.  Be sure to call save() to commit to database.
+     * @param description the new description
+     */
+    public void setDisplayName(String description) {
+        mDisplayName = description;
+    }
+
+    /**
+     * @return the email address for this account
+     */
+    public String getEmailAddress() {
+        return mEmailAddress;
+    }
+
+    /**
+     * Set the Email address for this account.  Be sure to call save() to commit to database.
+     * @param emailAddress the new email address for this account
+     */
+    public void setEmailAddress(String emailAddress) {
+        mEmailAddress = emailAddress;
+    }
+
+    /**
+     * @return the sender's name for this account
+     */
+    public String getSenderName() {
+        return mSenderName;
+    }
+
+    /**
+     * Set the sender's name.  Be sure to call save() to commit to database.
+     * @param name the new sender name
+     */
+    public void setSenderName(String name) {
+        mSenderName = name;
+    }
+
+    public String getSignature() {
+        return mSignature;
+    }
+
+    public void setSignature(String signature) {
+        mSignature = signature;
+    }
+
+    /**
+     * @return the minutes per check (for polling)
+     * TODO define sentinel values for "never", "push", etc.  See Account.java
+     */
+    public int getSyncInterval() {
+        return mSyncInterval;
+    }
+
+    /**
+     * Set the minutes per check (for polling).  Be sure to call save() to commit to database.
+     * TODO define sentinel values for "never", "push", etc.  See Account.java
+     * @param minutes the number of minutes between polling checks
+     */
+    public void setSyncInterval(int minutes) {
+        mSyncInterval = minutes;
+    }
+
+    /**
+     * @return One of the {@code Account.SYNC_WINDOW_*} constants that represents the sync
+     *     lookback window.
+     * TODO define sentinel values for "all", "1 month", etc.  See Account.java
+     */
+    public int getSyncLookback() {
+        return mSyncLookback;
+    }
+
+    /**
+     * Set the sync lookback window.  Be sure to call save() to commit to database.
+     * TODO define sentinel values for "all", "1 month", etc.  See Account.java
+     * @param value One of the {@link com.android.emailcommon.service.SyncWindow} constants
+     */
+    public void setSyncLookback(int value) {
+        mSyncLookback = value;
+    }
+
+    /**
+     * @return the flags for this account
+     * @see #FLAGS_NOTIFY_NEW_MAIL
+     * @see #FLAGS_VIBRATE_ALWAYS
+     * @see #FLAGS_VIBRATE_WHEN_SILENT
+     */
+    public int getFlags() {
+        return mFlags;
+    }
+
+    /**
+     * Set the flags for this account
+     * @see #FLAGS_NOTIFY_NEW_MAIL
+     * @see #FLAGS_VIBRATE_ALWAYS
+     * @see #FLAGS_VIBRATE_WHEN_SILENT
+     * @param newFlags the new value for the flags
+     */
+    public void setFlags(int newFlags) {
+        mFlags = newFlags;
+    }
+
+    /**
+     * @return the ringtone Uri for this account
+     */
+    public String getRingtone() {
+        return mRingtoneUri;
+    }
+
+    /**
+     * Set the ringtone Uri for this account
+     * @param newUri the new URI string for the ringtone for this account
+     */
+    public void setRingtone(String newUri) {
+        mRingtoneUri = newUri;
+    }
+
+    /**
+     * Set the "delete policy" as a simple 0,1,2 value set.
+     * @param newPolicy the new delete policy
+     */
+    public void setDeletePolicy(int newPolicy) {
+        mFlags &= ~FLAGS_DELETE_POLICY_MASK;
+        mFlags |= (newPolicy << FLAGS_DELETE_POLICY_SHIFT) & FLAGS_DELETE_POLICY_MASK;
+    }
+
+    /**
+     * Return the "delete policy" as a simple 0,1,2 value set.
+     * @return the current delete policy
+     */
+    public int getDeletePolicy() {
+        return (mFlags & FLAGS_DELETE_POLICY_MASK) >> FLAGS_DELETE_POLICY_SHIFT;
+    }
+
+    /**
+     * Return the Uuid associated with this account.  This is primarily for compatibility
+     * with accounts set up by previous versions, because there are externals references
+     * to the Uuid (e.g. desktop shortcuts).
+     */
+    public String getUuid() {
+        return mCompatibilityUuid;
+    }
+
+    public HostAuth getOrCreateHostAuthSend(Context context) {
+        if (mHostAuthSend == null) {
+            if (mHostAuthKeySend != 0) {
+                mHostAuthSend = HostAuth.restoreHostAuthWithId(context, mHostAuthKeySend);
+            } else {
+                mHostAuthSend = new HostAuth();
+            }
+        }
+        return mHostAuthSend;
+    }
+
+    public HostAuth getOrCreateHostAuthRecv(Context context) {
+        if (mHostAuthRecv == null) {
+            if (mHostAuthKeyRecv != 0) {
+                mHostAuthRecv = HostAuth.restoreHostAuthWithId(context, mHostAuthKeyRecv);
+            } else {
+                mHostAuthRecv = new HostAuth();
+            }
+        }
+        return mHostAuthRecv;
+    }
+
+    /**
+     * For compatibility while converting to provider model, generate a "local store URI"
+     *
+     * @return a string in the form of a Uri, as used by the other parts of the email app
+     */
+    public String getLocalStoreUri(Context context) {
+        return "local://localhost/" + context.getDatabasePath(getUuid() + ".db");
+    }
+
+    /**
+     * @return true if the instance is of an EAS account.
+     *
+     * NOTE This method accesses the DB if {@link #mHostAuthRecv} hasn't been restored yet.
+     * Use caution when you use this on the main thread.
+     */
+    public boolean isEasAccount(Context context) {
+        return "eas".equals(getProtocol(context));
+    }
+
+    public boolean supportsMoveMessages(Context context) {
+        String protocol = getProtocol(context);
+        return "eas".equals(protocol) || "imap".equals(protocol);
+    }
+
+    /**
+     * @return true if the account supports "search".
+     */
+    public static boolean supportsServerSearch(Context context, long accountId) {
+        Account account = Account.restoreAccountWithId(context, accountId);
+        if (account == null) return false;
+        return (account.mFlags & Account.FLAGS_SUPPORTS_SEARCH) != 0;
+    }
+
+    /**
+     * Set the account to be the default account.  If this is set to "true", when the account
+     * is saved, all other accounts will have the same value set to "false".
+     * @param newDefaultState the new default state - if true, others will be cleared.
+     */
+    public void setDefaultAccount(boolean newDefaultState) {
+        mIsDefault = newDefaultState;
+    }
+
+    /**
+     * @return {@link Uri} to this {@link Account} in the
+     * {@code content://com.android.email.provider/account/UUID} format, which is safe to use
+     * for desktop shortcuts.
+     *
+     * <p>We don't want to store _id in shortcuts, because
+     * {@link com.android.email.provider.AccountBackupRestore} won't preserve it.
+     */
+    public Uri getShortcutSafeUri() {
+        return getShortcutSafeUriFromUuid(mCompatibilityUuid);
+    }
+
+    /**
+     * @return {@link Uri} to an {@link Account} with a {@code uuid}.
+     */
+    public static Uri getShortcutSafeUriFromUuid(String uuid) {
+        return CONTENT_URI.buildUpon().appendEncodedPath(uuid).build();
+    }
+
+    /**
+     * Parse {@link Uri} in the {@code content://com.android.email.provider/account/ID} format
+     * where ID = account id (used on Eclair, Android 2.0-2.1) or UUID, and return _id of
+     * the {@link Account} associated with it.
+     *
+     * @param context context to access DB
+     * @param uri URI of interest
+     * @return _id of the {@link Account} associated with ID, or -1 if none found.
+     */
+    public static long getAccountIdFromShortcutSafeUri(Context context, Uri uri) {
+        // Make sure the URI is in the correct format.
+        if (!"content".equals(uri.getScheme())
+                || !AUTHORITY.equals(uri.getAuthority())) {
+            return -1;
+        }
+
+        final List<String> ps = uri.getPathSegments();
+        if (ps.size() != 2 || !"account".equals(ps.get(0))) {
+            return -1;
+        }
+
+        // Now get the ID part.
+        final String id = ps.get(1);
+
+        // First, see if ID can be parsed as long.  (Eclair-style)
+        // (UUIDs have '-' in them, so they are always non-parsable.)
+        try {
+            return Long.parseLong(id);
+        } catch (NumberFormatException ok) {
+            // OK, it's not a long.  Continue...
+        }
+
+        // Now id is a UUId.
+        return getAccountIdFromUuid(context, id);
+    }
+
+    /**
+     * @return ID of the account with the given UUID.
+     */
+    public static long getAccountIdFromUuid(Context context, String uuid) {
+        return Utility.getFirstRowLong(context,
+                CONTENT_URI, ID_PROJECTION,
+                UUID_SELECTION, new String[] {uuid}, null, 0, -1L);
+    }
+
+    /**
+     * Return the id of the default account.  If one hasn't been explicitly specified, return
+     * the first one in the database (the logic is provided within EmailProvider)
+     * @param context the caller's context
+     * @return the id of the default account, or Account.NO_ACCOUNT if there are no accounts
+     */
+    static public long getDefaultAccountId(Context context) {
+        Cursor c = context.getContentResolver().query(
+                Account.DEFAULT_ACCOUNT_ID_URI, Account.ID_PROJECTION, null, null, null);
+        try {
+            if (c != null && c.moveToFirst()) {
+                return c.getLong(Account.ID_PROJECTION_COLUMN);
+            }
+        } finally {
+            c.close();
+        }
+        return Account.NO_ACCOUNT;
+    }
+
+    /**
+     * Given an account id, return the account's protocol
+     * @param context the caller's context
+     * @param accountId the id of the account to be examined
+     * @return the account's protocol (or null if the Account or HostAuth do not exist)
+     */
+    public static String getProtocol(Context context, long accountId) {
+        Account account = Account.restoreAccountWithId(context, accountId);
+        if (account != null) {
+            return account.getProtocol(context);
+         }
+        return null;
+    }
+
+    /**
+     * Return the account's protocol
+     * @param context the caller's context
+     * @return the account's protocol (or null if the HostAuth doesn't not exist)
+     */
+    public String getProtocol(Context context) {
+        HostAuth hostAuth = HostAuth.restoreHostAuthWithId(context, mHostAuthKeyRecv);
+        if (hostAuth != null) {
+            return hostAuth.mProtocol;
+        }
+        return null;
+    }
+
+    /**
+     * Return the account ID for a message with a given id
+     *
+     * @param context the caller's context
+     * @param messageId the id of the message
+     * @return the account ID, or -1 if the account doesn't exist
+     */
+    public static long getAccountIdForMessageId(Context context, long messageId) {
+        return Message.getKeyColumnLong(context, messageId, MessageColumns.ACCOUNT_KEY);
+    }
+
+    /**
+     * Return the account for a message with a given id
+     * @param context the caller's context
+     * @param messageId the id of the message
+     * @return the account, or null if the account doesn't exist
+     */
+    public static Account getAccountForMessageId(Context context, long messageId) {
+        long accountId = getAccountIdForMessageId(context, messageId);
+        if (accountId != -1) {
+            return Account.restoreAccountWithId(context, accountId);
+        }
+        return null;
+    }
+
+    /**
+     * @return true if an {@code accountId} is assigned to any existing account.
+     */
+    public static boolean isValidId(Context context, long accountId) {
+        return null != Utility.getFirstRowLong(context, CONTENT_URI, ID_PROJECTION,
+                ID_SELECTION, new String[] {Long.toString(accountId)}, null,
+                ID_PROJECTION_COLUMN);
+    }
+
+    /**
+     * Check a single account for security hold status.
+     */
+    public static boolean isSecurityHold(Context context, long accountId) {
+        return (Utility.getFirstRowLong(context,
+                ContentUris.withAppendedId(Account.CONTENT_URI, accountId),
+                ACCOUNT_FLAGS_PROJECTION, null, null, null, ACCOUNT_FLAGS_COLUMN_FLAGS, 0L)
+                & Account.FLAGS_SECURITY_HOLD) != 0;
+    }
+
+    /**
+     * @return id of the "inbox" mailbox, or -1 if not found.
+     */
+    public static long getInboxId(Context context, long accountId) {
+        return Utility.getFirstRowLong(context, Mailbox.CONTENT_URI, ID_PROJECTION,
+                FIND_INBOX_SELECTION, new String[] {Long.toString(accountId)}, null,
+                ID_PROJECTION_COLUMN, -1L);
+    }
+
+    /**
+     * Clear all account hold flags that are set.
+     *
+     * (This will trigger watchers, and in particular will cause EAS to try and resync the
+     * account(s).)
+     */
+    public static void clearSecurityHoldOnAllAccounts(Context context) {
+        ContentResolver resolver = context.getContentResolver();
+        Cursor c = resolver.query(Account.CONTENT_URI, ACCOUNT_FLAGS_PROJECTION,
+                SECURITY_NONZERO_SELECTION, null, null);
+        try {
+            while (c.moveToNext()) {
+                int flags = c.getInt(ACCOUNT_FLAGS_COLUMN_FLAGS);
+
+                if (0 != (flags & FLAGS_SECURITY_HOLD)) {
+                    ContentValues cv = new ContentValues();
+                    cv.put(AccountColumns.FLAGS, flags & ~FLAGS_SECURITY_HOLD);
+                    long accountId = c.getLong(ACCOUNT_FLAGS_COLUMN_ID);
+                    Uri uri = ContentUris.withAppendedId(Account.CONTENT_URI, accountId);
+                    resolver.update(uri, cv, null, null);
+                }
+            }
+        } finally {
+            c.close();
+        }
+    }
+
+    /**
+     * Given an account id, determine whether the account is currently prohibited from automatic
+     * sync, due to roaming while the account's policy disables this
+     * @param context the caller's context
+     * @param accountId the account id
+     * @return true if the account can't automatically sync due to roaming; false otherwise
+     */
+    public static boolean isAutomaticSyncDisabledByRoaming(Context context, long accountId) {
+        Account account = Account.restoreAccountWithId(context, accountId);
+        // Account being deleted; just return
+        if (account == null) return false;
+        long policyKey = account.mPolicyKey;
+        // If no security policy, we're good
+        if (policyKey <= 0) return false;
+
+        ConnectivityManager cm =
+            (ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE);
+        NetworkInfo info = cm.getActiveNetworkInfo();
+        // If we're not on mobile, we're good
+        if (info == null || (info.getType() != ConnectivityManager.TYPE_MOBILE)) return false;
+        // If we're not roaming, we're good
+        if (!info.isRoaming()) return false;
+        Policy policy = Policy.restorePolicyWithId(context, policyKey);
+        // Account being deleted; just return
+        if (policy == null) return false;
+        return policy.mRequireManualSyncWhenRoaming;
+    }
+
+    /**
+     * Override update to enforce a single default account, and do it atomically
+     */
+    @Override
+    public int update(Context context, ContentValues cv) {
+        if (cv.containsKey(AccountColumns.IS_DEFAULT) &&
+                cv.getAsBoolean(AccountColumns.IS_DEFAULT)) {
+            ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();
+            ContentValues cv1 = new ContentValues();
+            cv1.put(AccountColumns.IS_DEFAULT, false);
+            // Clear the default flag in all accounts
+            ops.add(ContentProviderOperation.newUpdate(CONTENT_URI).withValues(cv1).build());
+            // Update this account
+            ops.add(ContentProviderOperation
+                    .newUpdate(ContentUris.withAppendedId(CONTENT_URI, mId))
+                    .withValues(cv).build());
+            try {
+                context.getContentResolver().applyBatch(AUTHORITY, ops);
+                return 1;
+            } catch (RemoteException e) {
+                // There is nothing to be done here; fail by returning 0
+            } catch (OperationApplicationException e) {
+                // There is nothing to be done here; fail by returning 0
+            }
+            return 0;
+        }
+        return super.update(context, cv);
+    }
+
+    /*
+     * Override this so that we can store the HostAuth's first and link them to the Account
+     * (non-Javadoc)
+     * @see com.android.email.provider.EmailContent#save(android.content.Context)
+     */
+    @Override
+    public Uri save(Context context) {
+        if (isSaved()) {
+            throw new UnsupportedOperationException();
+        }
+        // This logic is in place so I can (a) short circuit the expensive stuff when
+        // possible, and (b) override (and throw) if anyone tries to call save() or update()
+        // directly for Account, which are unsupported.
+        if (mHostAuthRecv == null && mHostAuthSend == null && mIsDefault == false &&
+                mPolicy != null) {
+            return super.save(context);
+        }
+
+        int index = 0;
+        int recvIndex = -1;
+        int sendIndex = -1;
+
+        // Create operations for saving the send and recv hostAuths
+        // Also, remember which operation in the array they represent
+        ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();
+        if (mHostAuthRecv != null) {
+            recvIndex = index++;
+            ops.add(ContentProviderOperation.newInsert(mHostAuthRecv.mBaseUri)
+                    .withValues(mHostAuthRecv.toContentValues())
+                    .build());
+        }
+        if (mHostAuthSend != null) {
+            sendIndex = index++;
+            ops.add(ContentProviderOperation.newInsert(mHostAuthSend.mBaseUri)
+                    .withValues(mHostAuthSend.toContentValues())
+                    .build());
+        }
+
+        // Create operations for making this the only default account
+        // Note, these are always updates because they change existing accounts
+        if (mIsDefault) {
+            index++;
+            ContentValues cv1 = new ContentValues();
+            cv1.put(AccountColumns.IS_DEFAULT, 0);
+            ops.add(ContentProviderOperation.newUpdate(CONTENT_URI).withValues(cv1).build());
+        }
+
+        // Now do the Account
+        ContentValues cv = null;
+        if (recvIndex >= 0 || sendIndex >= 0) {
+            cv = new ContentValues();
+            if (recvIndex >= 0) {
+                cv.put(Account.HOST_AUTH_KEY_RECV, recvIndex);
+            }
+            if (sendIndex >= 0) {
+                cv.put(Account.HOST_AUTH_KEY_SEND, sendIndex);
+            }
+        }
+
+        ContentProviderOperation.Builder b = ContentProviderOperation.newInsert(mBaseUri);
+        b.withValues(toContentValues());
+        if (cv != null) {
+            b.withValueBackReferences(cv);
+        }
+        ops.add(b.build());
+
+        try {
+            ContentProviderResult[] results =
+                context.getContentResolver().applyBatch(AUTHORITY, ops);
+            // If saving, set the mId's of the various saved objects
+            if (recvIndex >= 0) {
+                long newId = getId(results[recvIndex].uri);
+                mHostAuthKeyRecv = newId;
+                mHostAuthRecv.mId = newId;
+            }
+            if (sendIndex >= 0) {
+                long newId = getId(results[sendIndex].uri);
+                mHostAuthKeySend = newId;
+                mHostAuthSend.mId = newId;
+            }
+            Uri u = results[index].uri;
+            mId = getId(u);
+            return u;
+        } catch (RemoteException e) {
+            // There is nothing to be done here; fail by returning null
+        } catch (OperationApplicationException e) {
+            // There is nothing to be done here; fail by returning null
+        }
+        return null;
+    }
+
+    @Override
+    public ContentValues toContentValues() {
+        ContentValues values = new ContentValues();
+        values.put(AccountColumns.DISPLAY_NAME, mDisplayName);
+        values.put(AccountColumns.EMAIL_ADDRESS, mEmailAddress);
+        values.put(AccountColumns.SYNC_KEY, mSyncKey);
+        values.put(AccountColumns.SYNC_LOOKBACK, mSyncLookback);
+        values.put(AccountColumns.SYNC_INTERVAL, mSyncInterval);
+        values.put(AccountColumns.HOST_AUTH_KEY_RECV, mHostAuthKeyRecv);
+        values.put(AccountColumns.HOST_AUTH_KEY_SEND, mHostAuthKeySend);
+        values.put(AccountColumns.FLAGS, mFlags);
+        values.put(AccountColumns.IS_DEFAULT, mIsDefault);
+        values.put(AccountColumns.COMPATIBILITY_UUID, mCompatibilityUuid);
+        values.put(AccountColumns.SENDER_NAME, mSenderName);
+        values.put(AccountColumns.RINGTONE_URI, mRingtoneUri);
+        values.put(AccountColumns.PROTOCOL_VERSION, mProtocolVersion);
+        values.put(AccountColumns.NEW_MESSAGE_COUNT, mNewMessageCount);
+        values.put(AccountColumns.SECURITY_SYNC_KEY, mSecuritySyncKey);
+        values.put(AccountColumns.SIGNATURE, mSignature);
+        values.put(AccountColumns.POLICY_KEY, mPolicyKey);
+        return values;
+    }
+
+    /**
+     * Supports Parcelable
+     */
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    /**
+     * Supports Parcelable
+     */
+    public static final Parcelable.Creator<Account> CREATOR
+            = new Parcelable.Creator<Account>() {
+        @Override
+        public Account createFromParcel(Parcel in) {
+            return new Account(in);
+        }
+
+        @Override
+        public Account[] newArray(int size) {
+            return new Account[size];
+        }
+    };
+
+    /**
+     * Supports Parcelable
+     */
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        // mBaseUri is not parceled
+        dest.writeLong(mId);
+        dest.writeString(mDisplayName);
+        dest.writeString(mEmailAddress);
+        dest.writeString(mSyncKey);
+        dest.writeInt(mSyncLookback);
+        dest.writeInt(mSyncInterval);
+        dest.writeLong(mHostAuthKeyRecv);
+        dest.writeLong(mHostAuthKeySend);
+        dest.writeInt(mFlags);
+        dest.writeByte(mIsDefault ? (byte)1 : (byte)0);
+        dest.writeString(mCompatibilityUuid);
+        dest.writeString(mSenderName);
+        dest.writeString(mRingtoneUri);
+        dest.writeString(mProtocolVersion);
+        dest.writeInt(mNewMessageCount);
+        dest.writeString(mSecuritySyncKey);
+        dest.writeString(mSignature);
+        dest.writeLong(mPolicyKey);
+
+        if (mHostAuthRecv != null) {
+            dest.writeByte((byte)1);
+            mHostAuthRecv.writeToParcel(dest, flags);
+        } else {
+            dest.writeByte((byte)0);
+        }
+
+        if (mHostAuthSend != null) {
+            dest.writeByte((byte)1);
+            mHostAuthSend.writeToParcel(dest, flags);
+        } else {
+            dest.writeByte((byte)0);
+        }
+    }
+
+    /**
+     * Supports Parcelable
+     */
+    public Account(Parcel in) {
+        mBaseUri = Account.CONTENT_URI;
+        mId = in.readLong();
+        mDisplayName = in.readString();
+        mEmailAddress = in.readString();
+        mSyncKey = in.readString();
+        mSyncLookback = in.readInt();
+        mSyncInterval = in.readInt();
+        mHostAuthKeyRecv = in.readLong();
+        mHostAuthKeySend = in.readLong();
+        mFlags = in.readInt();
+        mIsDefault = in.readByte() == 1;
+        mCompatibilityUuid = in.readString();
+        mSenderName = in.readString();
+        mRingtoneUri = in.readString();
+        mProtocolVersion = in.readString();
+        mNewMessageCount = in.readInt();
+        mSecuritySyncKey = in.readString();
+        mSignature = in.readString();
+        mPolicyKey = in.readLong();
+
+        mHostAuthRecv = null;
+        if (in.readByte() == 1) {
+            mHostAuthRecv = new HostAuth(in);
+        }
+
+        mHostAuthSend = null;
+        if (in.readByte() == 1) {
+            mHostAuthSend = new HostAuth(in);
+        }
+    }
+
+    /**
+     * For debugger support only - DO NOT use for code.
+     */
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder('[');
+        if (mHostAuthRecv != null && mHostAuthRecv.mProtocol != null) {
+            sb.append(mHostAuthRecv.mProtocol);
+            sb.append(':');
+        }
+        if (mDisplayName != null)   sb.append(mDisplayName);
+        sb.append(':');
+        if (mEmailAddress != null)  sb.append(mEmailAddress);
+        sb.append(':');
+        if (mSenderName != null)    sb.append(mSenderName);
+        sb.append(']');
+        return sb.toString();
+    }
+}
\ No newline at end of file
diff --git a/email2/emailcommon/src/com/android/emailcommon/provider/EmailContent.java b/email2/emailcommon/src/com/android/emailcommon/provider/EmailContent.java
new file mode 100755
index 0000000..eb229be
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/provider/EmailContent.java
@@ -0,0 +1,1525 @@
+/*
+ * Copyright (C) 2009 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.provider;
+
+import android.content.ContentProviderOperation;
+import android.content.ContentProviderResult;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.OperationApplicationException;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Environment;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.RemoteException;
+
+import com.android.emailcommon.utility.TextUtilities;
+import com.android.emailcommon.utility.Utility;
+import com.android.mail.providers.UIProvider;
+import com.google.common.annotations.VisibleForTesting;
+
+import java.io.File;
+import java.util.ArrayList;
+
+
+/**
+ * EmailContent is the superclass of the various classes of content stored by EmailProvider.
+ *
+ * It is intended to include 1) column definitions for use with the Provider, and 2) convenience
+ * methods for saving and retrieving content from the Provider.
+ *
+ * This class will be used by 1) the Email process (which includes the application and
+ * EmaiLProvider) as well as 2) the Exchange process (which runs independently).  It will
+ * necessarily be cloned for use in these two cases.
+ *
+ * Conventions used in naming columns:
+ *   RECORD_ID is the primary key for all Email records
+ *   The SyncColumns interface is used by all classes that are synced to the server directly
+ *   (Mailbox and Email)
+ *
+ *   <name>_KEY always refers to a foreign key
+ *   <name>_ID always refers to a unique identifier (whether on client, server, etc.)
+ *
+ */
+public abstract class EmailContent {
+
+    public static final String AUTHORITY = "com.android.email.provider";
+    // The notifier authority is used to send notifications regarding changes to messages (insert,
+    // delete, or update) and is intended as an optimization for use by clients of message list
+    // cursors (initially, the email AppWidget).
+    public static final String NOTIFIER_AUTHORITY = "com.android.email.notifier";
+
+    public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY);
+    public static final String PARAMETER_LIMIT = "limit";
+
+    public static final Uri CONTENT_NOTIFIER_URI = Uri.parse("content://" + NOTIFIER_AUTHORITY);
+
+    public static final Uri MAILBOX_NOTIFICATION_URI =
+            Uri.parse("content://" + EmailContent.AUTHORITY + "/mailboxNotification");
+    public static final String[] NOTIFICATION_PROJECTION =
+        new String[] {MailboxColumns.ID, MailboxColumns.UNREAD_COUNT, MailboxColumns.MESSAGE_COUNT};
+    public static final int NOTIFICATION_MAILBOX_ID_COLUMN = 0;
+    public static final int NOTIFICATION_MAILBOX_UNREAD_COUNT_COLUMN = 1;
+    public static final int NOTIFICATION_MAILBOX_MESSAGE_COUNT_COLUMN = 2;
+
+    public static final Uri MAILBOX_MOST_RECENT_MESSAGE_URI =
+            Uri.parse("content://" + EmailContent.AUTHORITY + "/mailboxMostRecentMessage");
+
+    public static final String PROVIDER_PERMISSION = "com.android.email.permission.ACCESS_PROVIDER";
+
+    // All classes share this
+    public static final String RECORD_ID = "_id";
+
+    public static final String[] COUNT_COLUMNS = new String[]{"count(*)"};
+
+    /**
+     * This projection can be used with any of the EmailContent classes, when all you need
+     * is a list of id's.  Use ID_PROJECTION_COLUMN to access the row data.
+     */
+    public static final String[] ID_PROJECTION = new String[] {
+        RECORD_ID
+    };
+    public static final int ID_PROJECTION_COLUMN = 0;
+
+    public static final String ID_SELECTION = RECORD_ID + " =?";
+
+    public static final String FIELD_COLUMN_NAME = "field";
+    public static final String ADD_COLUMN_NAME = "add";
+    public static final String SET_COLUMN_NAME = "set";
+
+    public static final int SYNC_STATUS_NONE = UIProvider.SyncStatus.NO_SYNC;
+    public static final int SYNC_STATUS_USER = UIProvider.SyncStatus.USER_REFRESH;
+    public static final int SYNC_STATUS_BACKGROUND = UIProvider.SyncStatus.BACKGROUND_SYNC;
+
+    public static final int LAST_SYNC_RESULT_SUCCESS = UIProvider.LastSyncResult.SUCCESS;
+    public static final int LAST_SYNC_RESULT_AUTH_ERROR = UIProvider.LastSyncResult.AUTH_ERROR;
+    public static final int LAST_SYNC_RESULT_SECURITY_ERROR =
+            UIProvider.LastSyncResult.SECURITY_ERROR;
+    public static final int LAST_SYNC_RESULT_CONNECTION_ERROR =
+            UIProvider.LastSyncResult.CONNECTION_ERROR;
+    public static final int LAST_SYNC_RESULT_INTERNAL_ERROR =
+            UIProvider.LastSyncResult.INTERNAL_ERROR;
+
+    // Newly created objects get this id
+    public static final int NOT_SAVED = -1;
+    // The base Uri that this piece of content came from
+    public Uri mBaseUri;
+    // Lazily initialized uri for this Content
+    private Uri mUri = null;
+    // The id of the Content
+    public long mId = NOT_SAVED;
+
+    // Write the Content into a ContentValues container
+    public abstract ContentValues toContentValues();
+    // Read the Content from a ContentCursor
+    public abstract void restore (Cursor cursor);
+
+    // The Uri is lazily initialized
+    public Uri getUri() {
+        if (mUri == null) {
+            mUri = ContentUris.withAppendedId(mBaseUri, mId);
+        }
+        return mUri;
+    }
+
+    public boolean isSaved() {
+        return mId != NOT_SAVED;
+    }
+
+
+    /**
+     * Restore a subclass of EmailContent from the database
+     * @param context the caller's context
+     * @param klass the class to restore
+     * @param contentUri the content uri of the EmailContent subclass
+     * @param contentProjection the content projection for the EmailContent subclass
+     * @param id the unique id of the object
+     * @return the instantiated object
+     */
+    public static <T extends EmailContent> T restoreContentWithId(Context context,
+            Class<T> klass, Uri contentUri, String[] contentProjection, long id) {
+        Uri u = ContentUris.withAppendedId(contentUri, id);
+        Cursor c = context.getContentResolver().query(u, contentProjection, null, null, null);
+        if (c == null) throw new ProviderUnavailableException();
+        try {
+            if (c.moveToFirst()) {
+                return getContent(c, klass);
+            } else {
+                return null;
+            }
+        } finally {
+            c.close();
+        }
+    }
+
+
+    // The Content sub class must have a no-arg constructor
+    static public <T extends EmailContent> T getContent(Cursor cursor, Class<T> klass) {
+        try {
+            T content = klass.newInstance();
+            content.mId = cursor.getLong(0);
+            content.restore(cursor);
+            return content;
+        } catch (IllegalAccessException e) {
+            e.printStackTrace();
+        } catch (InstantiationException e) {
+            e.printStackTrace();
+        }
+        return null;
+    }
+
+    public Uri save(Context context) {
+        if (isSaved()) {
+            throw new UnsupportedOperationException();
+        }
+        Uri res = context.getContentResolver().insert(mBaseUri, toContentValues());
+        mId = Long.parseLong(res.getPathSegments().get(1));
+        return res;
+    }
+
+    public int update(Context context, ContentValues contentValues) {
+        if (!isSaved()) {
+            throw new UnsupportedOperationException();
+        }
+        return context.getContentResolver().update(getUri(), contentValues, null, null);
+    }
+
+    static public int update(Context context, Uri baseUri, long id, ContentValues contentValues) {
+        return context.getContentResolver()
+            .update(ContentUris.withAppendedId(baseUri, id), contentValues, null, null);
+    }
+
+    static public int delete(Context context, Uri baseUri, long id) {
+        return context.getContentResolver()
+            .delete(ContentUris.withAppendedId(baseUri, id), null, null);
+    }
+
+    /**
+     * Generic count method that can be used for any ContentProvider
+     *
+     * @param context the calling Context
+     * @param uri the Uri for the provider query
+     * @param selection as with a query call
+     * @param selectionArgs as with a query call
+     * @return the number of items matching the query (or zero)
+     */
+    static public int count(Context context, Uri uri, String selection, String[] selectionArgs) {
+        return Utility.getFirstRowLong(context,
+                uri, COUNT_COLUMNS, selection, selectionArgs, null, 0, Long.valueOf(0)).intValue();
+    }
+
+    /**
+     * Same as {@link #count(Context, Uri, String, String[])} without selection.
+     */
+    static public int count(Context context, Uri uri) {
+        return count(context, uri, null, null);
+    }
+
+    static public Uri uriWithLimit(Uri uri, int limit) {
+        return uri.buildUpon().appendQueryParameter(EmailContent.PARAMETER_LIMIT,
+                Integer.toString(limit)).build();
+    }
+
+    /**
+     * no public constructor since this is a utility class
+     */
+    protected EmailContent() {
+    }
+
+    public interface SyncColumns {
+        public static final String ID = "_id";
+        // source id (string) : the source's name of this item
+        public static final String SERVER_ID = "syncServerId";
+        // source's timestamp (long) for this item
+        public static final String SERVER_TIMESTAMP = "syncServerTimeStamp";
+    }
+
+    public interface BodyColumns {
+        public static final String ID = "_id";
+        // Foreign key to the message corresponding to this body
+        public static final String MESSAGE_KEY = "messageKey";
+        // The html content itself
+        public static final String HTML_CONTENT = "htmlContent";
+        // The plain text content itself
+        public static final String TEXT_CONTENT = "textContent";
+        // Replied-to or forwarded body (in html form)
+        public static final String HTML_REPLY = "htmlReply";
+        // Replied-to or forwarded body (in text form)
+        public static final String TEXT_REPLY = "textReply";
+        // A reference to a message's unique id used in reply/forward.
+        // Protocol code can be expected to use this column in determining whether a message can be
+        // deleted safely (i.e. isn't referenced by other messages)
+        public static final String SOURCE_MESSAGE_KEY = "sourceMessageKey";
+        // The text to be placed between a reply/forward response and the original message
+        public static final String INTRO_TEXT = "introText";
+    }
+
+    public static final class Body extends EmailContent implements BodyColumns {
+        public static final String TABLE_NAME = "Body";
+
+        @SuppressWarnings("hiding")
+        public static final Uri CONTENT_URI = Uri.parse(EmailContent.CONTENT_URI + "/body");
+
+        public static final int CONTENT_ID_COLUMN = 0;
+        public static final int CONTENT_MESSAGE_KEY_COLUMN = 1;
+        public static final int CONTENT_HTML_CONTENT_COLUMN = 2;
+        public static final int CONTENT_TEXT_CONTENT_COLUMN = 3;
+        public static final int CONTENT_HTML_REPLY_COLUMN = 4;
+        public static final int CONTENT_TEXT_REPLY_COLUMN = 5;
+        public static final int CONTENT_SOURCE_KEY_COLUMN = 6;
+        public static final int CONTENT_INTRO_TEXT_COLUMN = 7;
+        public static final String[] CONTENT_PROJECTION = new String[] {
+            RECORD_ID, BodyColumns.MESSAGE_KEY, BodyColumns.HTML_CONTENT, BodyColumns.TEXT_CONTENT,
+            BodyColumns.HTML_REPLY, BodyColumns.TEXT_REPLY, BodyColumns.SOURCE_MESSAGE_KEY,
+            BodyColumns.INTRO_TEXT
+        };
+
+        public static final String[] COMMON_PROJECTION_TEXT = new String[] {
+            RECORD_ID, BodyColumns.TEXT_CONTENT
+        };
+        public static final String[] COMMON_PROJECTION_HTML = new String[] {
+            RECORD_ID, BodyColumns.HTML_CONTENT
+        };
+        public static final String[] COMMON_PROJECTION_REPLY_TEXT = new String[] {
+            RECORD_ID, BodyColumns.TEXT_REPLY
+        };
+        public static final String[] COMMON_PROJECTION_REPLY_HTML = new String[] {
+            RECORD_ID, BodyColumns.HTML_REPLY
+        };
+        public static final String[] COMMON_PROJECTION_INTRO = new String[] {
+            RECORD_ID, BodyColumns.INTRO_TEXT
+        };
+        public static final String[] COMMON_PROJECTION_SOURCE = new String[] {
+            RECORD_ID, BodyColumns.SOURCE_MESSAGE_KEY
+        };
+         public static final int COMMON_PROJECTION_COLUMN_TEXT = 1;
+
+        private static final String[] PROJECTION_SOURCE_KEY =
+            new String[] { BodyColumns.SOURCE_MESSAGE_KEY };
+
+        public long mMessageKey;
+        public String mHtmlContent;
+        public String mTextContent;
+        public String mHtmlReply;
+        public String mTextReply;
+
+        /**
+         * Points to the ID of the message being replied to or forwarded. Will always be set,
+         * even if {@link #mHtmlReply} and {@link #mTextReply} are null (indicating the user doesn't
+         * want to include quoted text.
+         */
+        public long mSourceKey;
+        public String mIntroText;
+
+        public Body() {
+            mBaseUri = CONTENT_URI;
+        }
+
+        @Override
+        public ContentValues toContentValues() {
+            ContentValues values = new ContentValues();
+
+            // Assign values for each row.
+            values.put(BodyColumns.MESSAGE_KEY, mMessageKey);
+            values.put(BodyColumns.HTML_CONTENT, mHtmlContent);
+            values.put(BodyColumns.TEXT_CONTENT, mTextContent);
+            values.put(BodyColumns.HTML_REPLY, mHtmlReply);
+            values.put(BodyColumns.TEXT_REPLY, mTextReply);
+            values.put(BodyColumns.SOURCE_MESSAGE_KEY, mSourceKey);
+            values.put(BodyColumns.INTRO_TEXT, mIntroText);
+            return values;
+        }
+
+        /**
+         * Given a cursor, restore a Body from it
+         * @param cursor a cursor which must NOT be null
+         * @return the Body as restored from the cursor
+         */
+        private static Body restoreBodyWithCursor(Cursor cursor) {
+            try {
+                if (cursor.moveToFirst()) {
+                    return getContent(cursor, Body.class);
+                } else {
+                    return null;
+                }
+            } finally {
+                cursor.close();
+            }
+        }
+
+        public static Body restoreBodyWithId(Context context, long id) {
+            Uri u = ContentUris.withAppendedId(Body.CONTENT_URI, id);
+            Cursor c = context.getContentResolver().query(u, Body.CONTENT_PROJECTION,
+                    null, null, null);
+            if (c == null) throw new ProviderUnavailableException();
+            return restoreBodyWithCursor(c);
+        }
+
+        public static Body restoreBodyWithMessageId(Context context, long messageId) {
+            Cursor c = context.getContentResolver().query(Body.CONTENT_URI,
+                    Body.CONTENT_PROJECTION, Body.MESSAGE_KEY + "=?",
+                    new String[] {Long.toString(messageId)}, null);
+            if (c == null) throw new ProviderUnavailableException();
+            return restoreBodyWithCursor(c);
+        }
+
+        /**
+         * Returns the bodyId for the given messageId, or -1 if no body is found.
+         */
+        public static long lookupBodyIdWithMessageId(Context context, long messageId) {
+            return Utility.getFirstRowLong(context, Body.CONTENT_URI,
+                    ID_PROJECTION, Body.MESSAGE_KEY + "=?",
+                    new String[] {Long.toString(messageId)}, null, ID_PROJECTION_COLUMN,
+                            Long.valueOf(-1));
+        }
+
+        /**
+         * Updates the Body for a messageId with the given ContentValues.
+         * If the message has no body, a new body is inserted for the message.
+         * Warning: the argument "values" is modified by this method, setting MESSAGE_KEY.
+         */
+        public static void updateBodyWithMessageId(Context context, long messageId,
+                ContentValues values) {
+            ContentResolver resolver = context.getContentResolver();
+            long bodyId = lookupBodyIdWithMessageId(context, messageId);
+            values.put(BodyColumns.MESSAGE_KEY, messageId);
+            if (bodyId == -1) {
+                resolver.insert(CONTENT_URI, values);
+            } else {
+                final Uri uri = ContentUris.withAppendedId(CONTENT_URI, bodyId);
+                resolver.update(uri, values, null, null);
+            }
+        }
+
+        @VisibleForTesting
+        public static long restoreBodySourceKey(Context context, long messageId) {
+            return Utility.getFirstRowLong(context, Body.CONTENT_URI,
+                    Body.PROJECTION_SOURCE_KEY,
+                    Body.MESSAGE_KEY + "=?", new String[] {Long.toString(messageId)}, null, 0,
+                            Long.valueOf(0));
+        }
+
+        private static String restoreTextWithMessageId(Context context, long messageId,
+                String[] projection) {
+            Cursor c = context.getContentResolver().query(Body.CONTENT_URI, projection,
+                    Body.MESSAGE_KEY + "=?", new String[] {Long.toString(messageId)}, null);
+            if (c == null) throw new ProviderUnavailableException();
+            try {
+                if (c.moveToFirst()) {
+                    return c.getString(COMMON_PROJECTION_COLUMN_TEXT);
+                } else {
+                    return null;
+                }
+            } finally {
+                c.close();
+            }
+        }
+
+        public static String restoreBodyTextWithMessageId(Context context, long messageId) {
+            return restoreTextWithMessageId(context, messageId, Body.COMMON_PROJECTION_TEXT);
+        }
+
+        public static String restoreBodyHtmlWithMessageId(Context context, long messageId) {
+            return restoreTextWithMessageId(context, messageId, Body.COMMON_PROJECTION_HTML);
+        }
+
+        public static String restoreReplyTextWithMessageId(Context context, long messageId) {
+            return restoreTextWithMessageId(context, messageId, Body.COMMON_PROJECTION_REPLY_TEXT);
+        }
+
+        public static String restoreReplyHtmlWithMessageId(Context context, long messageId) {
+            return restoreTextWithMessageId(context, messageId, Body.COMMON_PROJECTION_REPLY_HTML);
+        }
+
+        public static String restoreIntroTextWithMessageId(Context context, long messageId) {
+            return restoreTextWithMessageId(context, messageId, Body.COMMON_PROJECTION_INTRO);
+        }
+
+        @Override
+        public void restore(Cursor cursor) {
+            mBaseUri = EmailContent.Body.CONTENT_URI;
+            mMessageKey = cursor.getLong(CONTENT_MESSAGE_KEY_COLUMN);
+            mHtmlContent = cursor.getString(CONTENT_HTML_CONTENT_COLUMN);
+            mTextContent = cursor.getString(CONTENT_TEXT_CONTENT_COLUMN);
+            mHtmlReply = cursor.getString(CONTENT_HTML_REPLY_COLUMN);
+            mTextReply = cursor.getString(CONTENT_TEXT_REPLY_COLUMN);
+            mSourceKey = cursor.getLong(CONTENT_SOURCE_KEY_COLUMN);
+            mIntroText = cursor.getString(CONTENT_INTRO_TEXT_COLUMN);
+        }
+
+        public boolean update() {
+            // TODO Auto-generated method stub
+            return false;
+        }
+    }
+
+    public interface MessageColumns {
+        public static final String ID = "_id";
+        // Basic columns used in message list presentation
+        // The name as shown to the user in a message list
+        public static final String DISPLAY_NAME = "displayName";
+        // The time (millis) as shown to the user in a message list [INDEX]
+        public static final String TIMESTAMP = "timeStamp";
+        // Message subject
+        public static final String SUBJECT = "subject";
+        // Boolean, unread = 0, read = 1 [INDEX]
+        public static final String FLAG_READ = "flagRead";
+        // Load state, see constants below (unloaded, partial, complete, deleted)
+        public static final String FLAG_LOADED = "flagLoaded";
+        // Boolean, unflagged = 0, flagged (favorite) = 1
+        public static final String FLAG_FAVORITE = "flagFavorite";
+        // Boolean, no attachment = 0, attachment = 1
+        public static final String FLAG_ATTACHMENT = "flagAttachment";
+        // Bit field for flags which we'll not be selecting on
+        public static final String FLAGS = "flags";
+
+        // Sync related identifiers
+        // Any client-required identifier
+        public static final String CLIENT_ID = "clientId";
+        // The message-id in the message's header
+        public static final String MESSAGE_ID = "messageId";
+
+        // References to other Email objects in the database
+        // Foreign key to the Mailbox holding this message [INDEX]
+        public static final String MAILBOX_KEY = "mailboxKey";
+        // Foreign key to the Account holding this message
+        public static final String ACCOUNT_KEY = "accountKey";
+
+        // Address lists, packed with Address.pack()
+        public static final String FROM_LIST = "fromList";
+        public static final String TO_LIST = "toList";
+        public static final String CC_LIST = "ccList";
+        public static final String BCC_LIST = "bccList";
+        public static final String REPLY_TO_LIST = "replyToList";
+        // Meeting invitation related information (for now, start time in ms)
+        public static final String MEETING_INFO = "meetingInfo";
+        // A text "snippet" derived from the body of the message
+        public static final String SNIPPET = "snippet";
+        // A column that can be used by sync adapters to store search-related information about
+        // a retrieved message (the messageKey for search results will be a TYPE_SEARCH mailbox
+        // and the sync adapter might, for example, need more information about the original source
+        // of the message)
+        public static final String PROTOCOL_SEARCH_INFO = "protocolSearchInfo";
+    }
+
+    public static final class Message extends EmailContent implements SyncColumns, MessageColumns {
+        public static final String TABLE_NAME = "Message";
+        public static final String UPDATED_TABLE_NAME = "Message_Updates";
+        public static final String DELETED_TABLE_NAME = "Message_Deletes";
+
+        // To refer to a specific message, use ContentUris.withAppendedId(CONTENT_URI, id)
+        @SuppressWarnings("hiding")
+        public static final Uri CONTENT_URI = Uri.parse(EmailContent.CONTENT_URI + "/message");
+        public static final Uri CONTENT_URI_LIMIT_1 = uriWithLimit(CONTENT_URI, 1);
+        public static final Uri SYNCED_CONTENT_URI =
+            Uri.parse(EmailContent.CONTENT_URI + "/syncedMessage");
+        public static final Uri DELETED_CONTENT_URI =
+            Uri.parse(EmailContent.CONTENT_URI + "/deletedMessage");
+        public static final Uri UPDATED_CONTENT_URI =
+            Uri.parse(EmailContent.CONTENT_URI + "/updatedMessage");
+        public static final Uri NOTIFIER_URI =
+            Uri.parse(EmailContent.CONTENT_NOTIFIER_URI + "/message");
+
+        public static final String KEY_TIMESTAMP_DESC = MessageColumns.TIMESTAMP + " desc";
+
+        public static final int CONTENT_ID_COLUMN = 0;
+        public static final int CONTENT_DISPLAY_NAME_COLUMN = 1;
+        public static final int CONTENT_TIMESTAMP_COLUMN = 2;
+        public static final int CONTENT_SUBJECT_COLUMN = 3;
+        public static final int CONTENT_FLAG_READ_COLUMN = 4;
+        public static final int CONTENT_FLAG_LOADED_COLUMN = 5;
+        public static final int CONTENT_FLAG_FAVORITE_COLUMN = 6;
+        public static final int CONTENT_FLAG_ATTACHMENT_COLUMN = 7;
+        public static final int CONTENT_FLAGS_COLUMN = 8;
+        public static final int CONTENT_SERVER_ID_COLUMN = 9;
+        public static final int CONTENT_CLIENT_ID_COLUMN = 10;
+        public static final int CONTENT_MESSAGE_ID_COLUMN = 11;
+        public static final int CONTENT_MAILBOX_KEY_COLUMN = 12;
+        public static final int CONTENT_ACCOUNT_KEY_COLUMN = 13;
+        public static final int CONTENT_FROM_LIST_COLUMN = 14;
+        public static final int CONTENT_TO_LIST_COLUMN = 15;
+        public static final int CONTENT_CC_LIST_COLUMN = 16;
+        public static final int CONTENT_BCC_LIST_COLUMN = 17;
+        public static final int CONTENT_REPLY_TO_COLUMN = 18;
+        public static final int CONTENT_SERVER_TIMESTAMP_COLUMN = 19;
+        public static final int CONTENT_MEETING_INFO_COLUMN = 20;
+        public static final int CONTENT_SNIPPET_COLUMN = 21;
+        public static final int CONTENT_PROTOCOL_SEARCH_INFO_COLUMN = 22;
+
+        public static final String[] CONTENT_PROJECTION = new String[] {
+            RECORD_ID,
+            MessageColumns.DISPLAY_NAME, MessageColumns.TIMESTAMP,
+            MessageColumns.SUBJECT, MessageColumns.FLAG_READ,
+            MessageColumns.FLAG_LOADED, MessageColumns.FLAG_FAVORITE,
+            MessageColumns.FLAG_ATTACHMENT, MessageColumns.FLAGS,
+            SyncColumns.SERVER_ID, MessageColumns.CLIENT_ID,
+            MessageColumns.MESSAGE_ID, MessageColumns.MAILBOX_KEY,
+            MessageColumns.ACCOUNT_KEY, MessageColumns.FROM_LIST,
+            MessageColumns.TO_LIST, MessageColumns.CC_LIST,
+            MessageColumns.BCC_LIST, MessageColumns.REPLY_TO_LIST,
+            SyncColumns.SERVER_TIMESTAMP, MessageColumns.MEETING_INFO,
+            MessageColumns.SNIPPET, MessageColumns.PROTOCOL_SEARCH_INFO
+        };
+
+        public static final int LIST_ID_COLUMN = 0;
+        public static final int LIST_DISPLAY_NAME_COLUMN = 1;
+        public static final int LIST_TIMESTAMP_COLUMN = 2;
+        public static final int LIST_SUBJECT_COLUMN = 3;
+        public static final int LIST_READ_COLUMN = 4;
+        public static final int LIST_LOADED_COLUMN = 5;
+        public static final int LIST_FAVORITE_COLUMN = 6;
+        public static final int LIST_ATTACHMENT_COLUMN = 7;
+        public static final int LIST_FLAGS_COLUMN = 8;
+        public static final int LIST_MAILBOX_KEY_COLUMN = 9;
+        public static final int LIST_ACCOUNT_KEY_COLUMN = 10;
+        public static final int LIST_SERVER_ID_COLUMN = 11;
+        public static final int LIST_SNIPPET_COLUMN = 12;
+
+        // Public projection for common list columns
+        public static final String[] LIST_PROJECTION = new String[] {
+            RECORD_ID,
+            MessageColumns.DISPLAY_NAME, MessageColumns.TIMESTAMP,
+            MessageColumns.SUBJECT, MessageColumns.FLAG_READ,
+            MessageColumns.FLAG_LOADED, MessageColumns.FLAG_FAVORITE,
+            MessageColumns.FLAG_ATTACHMENT, MessageColumns.FLAGS,
+            MessageColumns.MAILBOX_KEY, MessageColumns.ACCOUNT_KEY,
+            SyncColumns.SERVER_ID, MessageColumns.SNIPPET
+        };
+
+        public static final int ID_COLUMNS_ID_COLUMN = 0;
+        public static final int ID_COLUMNS_SYNC_SERVER_ID = 1;
+        public static final String[] ID_COLUMNS_PROJECTION = new String[] {
+            RECORD_ID, SyncColumns.SERVER_ID
+        };
+
+        public static final int ID_MAILBOX_COLUMN_ID = 0;
+        public static final int ID_MAILBOX_COLUMN_MAILBOX_KEY = 1;
+        public static final String[] ID_MAILBOX_PROJECTION = new String[] {
+            RECORD_ID, MessageColumns.MAILBOX_KEY
+        };
+
+        public static final String[] ID_COLUMN_PROJECTION = new String[] { RECORD_ID };
+
+        private static final String ACCOUNT_KEY_SELECTION =
+            MessageColumns.ACCOUNT_KEY + "=?";
+
+        /**
+         * Selection for messages that are loaded
+         *
+         * POP messages at the initial stage have very little information. (Server UID only)
+         * Use this to make sure they're not visible on any UI.
+         * This means unread counts on the mailbox list can be different from the
+         * number of messages in the message list, but it should be transient...
+         */
+        public static final String FLAG_LOADED_SELECTION =
+            MessageColumns.FLAG_LOADED + " IN ("
+            +     Message.FLAG_LOADED_PARTIAL + "," + Message.FLAG_LOADED_COMPLETE
+            +     ")";
+
+        public static final String ALL_FAVORITE_SELECTION =
+            MessageColumns.FLAG_FAVORITE + "=1 AND "
+            + MessageColumns.MAILBOX_KEY + " NOT IN ("
+            +     "SELECT " + MailboxColumns.ID + " FROM " + Mailbox.TABLE_NAME + ""
+            +     " WHERE " + MailboxColumns.TYPE + " = " + Mailbox.TYPE_TRASH
+            +     ")"
+            + " AND " + FLAG_LOADED_SELECTION;
+
+        /** Selection to retrieve all messages in "inbox" for any account */
+        public static final String ALL_INBOX_SELECTION =
+            MessageColumns.MAILBOX_KEY + " IN ("
+            +     "SELECT " + MailboxColumns.ID + " FROM " + Mailbox.TABLE_NAME
+            +     " WHERE " + MailboxColumns.TYPE + " = " + Mailbox.TYPE_INBOX
+            +     ")"
+            + " AND " + FLAG_LOADED_SELECTION;
+
+        /** Selection to retrieve all messages in "drafts" for any account */
+        public static final String ALL_DRAFT_SELECTION =
+            MessageColumns.MAILBOX_KEY + " IN ("
+            +     "SELECT " + MailboxColumns.ID + " FROM " + Mailbox.TABLE_NAME
+            +     " WHERE " + MailboxColumns.TYPE + " = " + Mailbox.TYPE_DRAFTS
+            +     ")"
+            + " AND " + FLAG_LOADED_SELECTION;
+
+        /** Selection to retrieve all messages in "outbox" for any account */
+        public static final String ALL_OUTBOX_SELECTION =
+            MessageColumns.MAILBOX_KEY + " IN ("
+            +     "SELECT " + MailboxColumns.ID + " FROM " + Mailbox.TABLE_NAME
+            +     " WHERE " + MailboxColumns.TYPE + " = " + Mailbox.TYPE_OUTBOX
+            +     ")"; // NOTE No flag_loaded test for outboxes.
+
+        /** Selection to retrieve unread messages in "inbox" for any account */
+        public static final String ALL_UNREAD_SELECTION =
+            MessageColumns.FLAG_READ + "=0 AND " + ALL_INBOX_SELECTION;
+
+        /** Selection to retrieve unread messages in "inbox" for one account */
+        public static final String PER_ACCOUNT_UNREAD_SELECTION =
+            ACCOUNT_KEY_SELECTION + " AND " + ALL_UNREAD_SELECTION;
+
+        /** Selection to retrieve all messages in "inbox" for one account */
+        public static final String PER_ACCOUNT_INBOX_SELECTION =
+            ACCOUNT_KEY_SELECTION + " AND " + ALL_INBOX_SELECTION;
+
+        public static final String PER_ACCOUNT_FAVORITE_SELECTION =
+            ACCOUNT_KEY_SELECTION + " AND " + ALL_FAVORITE_SELECTION;
+
+        // _id field is in AbstractContent
+        public String mDisplayName;
+        public long mTimeStamp;
+        public String mSubject;
+        public boolean mFlagRead = false;
+        public int mFlagLoaded = FLAG_LOADED_UNLOADED;
+        public boolean mFlagFavorite = false;
+        public boolean mFlagAttachment = false;
+        public int mFlags = 0;
+
+        public String mServerId;
+        public long mServerTimeStamp;
+        public String mClientId;
+        public String mMessageId;
+
+        public long mMailboxKey;
+        public long mAccountKey;
+
+        public String mFrom;
+        public String mTo;
+        public String mCc;
+        public String mBcc;
+        public String mReplyTo;
+
+        // For now, just the start time of a meeting invite, in ms
+        public String mMeetingInfo;
+
+        public String mSnippet;
+
+        public String mProtocolSearchInfo;
+
+        /**
+         * Base64-encoded representation of the byte array provided by servers for identifying
+         * messages belonging to the same conversation thread. Currently unsupported and not
+         * persisted in the database.
+         */
+        public String mServerConversationId;
+
+        // The following transient members may be used while building and manipulating messages,
+        // but they are NOT persisted directly by EmailProvider. See Body for related fields.
+        transient public String mText;
+        transient public String mHtml;
+        transient public String mTextReply;
+        transient public String mHtmlReply;
+        transient public long mSourceKey;
+        transient public ArrayList<Attachment> mAttachments = null;
+        transient public String mIntroText;
+
+
+        // Values used in mFlagRead
+        public static final int UNREAD = 0;
+        public static final int READ = 1;
+
+        // Values used in mFlagLoaded
+        public static final int FLAG_LOADED_UNLOADED = 0;
+        public static final int FLAG_LOADED_COMPLETE = 1;
+        public static final int FLAG_LOADED_PARTIAL = 2;
+        public static final int FLAG_LOADED_DELETED = 3;
+
+        // Bits used in mFlags
+        // The following three states are mutually exclusive, and indicate whether the message is an
+        // original, a reply, or a forward
+        public static final int FLAG_TYPE_ORIGINAL = 0;
+        public static final int FLAG_TYPE_REPLY = 1<<0;
+        public static final int FLAG_TYPE_FORWARD = 1<<1;
+        public static final int FLAG_TYPE_MASK = FLAG_TYPE_REPLY | FLAG_TYPE_FORWARD;
+        // The following flags indicate messages that are determined to be incoming meeting related
+        // (e.g. invites from others)
+        public static final int FLAG_INCOMING_MEETING_INVITE = 1<<2;
+        public static final int FLAG_INCOMING_MEETING_CANCEL = 1<<3;
+        public static final int FLAG_INCOMING_MEETING_MASK =
+            FLAG_INCOMING_MEETING_INVITE | FLAG_INCOMING_MEETING_CANCEL;
+        // The following flags indicate messages that are outgoing and meeting related
+        // (e.g. invites TO others)
+        public static final int FLAG_OUTGOING_MEETING_INVITE = 1<<4;
+        public static final int FLAG_OUTGOING_MEETING_CANCEL = 1<<5;
+        public static final int FLAG_OUTGOING_MEETING_ACCEPT = 1<<6;
+        public static final int FLAG_OUTGOING_MEETING_DECLINE = 1<<7;
+        public static final int FLAG_OUTGOING_MEETING_TENTATIVE = 1<<8;
+        public static final int FLAG_OUTGOING_MEETING_MASK =
+            FLAG_OUTGOING_MEETING_INVITE | FLAG_OUTGOING_MEETING_CANCEL |
+            FLAG_OUTGOING_MEETING_ACCEPT | FLAG_OUTGOING_MEETING_DECLINE |
+            FLAG_OUTGOING_MEETING_TENTATIVE;
+        public static final int FLAG_OUTGOING_MEETING_REQUEST_MASK =
+            FLAG_OUTGOING_MEETING_INVITE | FLAG_OUTGOING_MEETING_CANCEL;
+        // 8 general purpose flags (bits) that may be used at the discretion of the sync adapter
+        public static final int FLAG_SYNC_ADAPTER_SHIFT = 9;
+        public static final int FLAG_SYNC_ADAPTER_MASK = 255 << FLAG_SYNC_ADAPTER_SHIFT;
+        /** If set, the outgoing message should *not* include the quoted original message. */
+        public static final int FLAG_NOT_INCLUDE_QUOTED_TEXT = 1 << 17;
+        public static final int FLAG_REPLIED_TO = 1 << 18;
+        public static final int FLAG_FORWARDED = 1 << 19;
+
+        /** a pseudo ID for "no message". */
+        public static final long NO_MESSAGE = -1L;
+
+        public Message() {
+            mBaseUri = CONTENT_URI;
+        }
+
+        @Override
+        public ContentValues toContentValues() {
+            ContentValues values = new ContentValues();
+
+            // Assign values for each row.
+            values.put(MessageColumns.DISPLAY_NAME, mDisplayName);
+            values.put(MessageColumns.TIMESTAMP, mTimeStamp);
+            values.put(MessageColumns.SUBJECT, mSubject);
+            values.put(MessageColumns.FLAG_READ, mFlagRead);
+            values.put(MessageColumns.FLAG_LOADED, mFlagLoaded);
+            values.put(MessageColumns.FLAG_FAVORITE, mFlagFavorite);
+            values.put(MessageColumns.FLAG_ATTACHMENT, mFlagAttachment);
+            values.put(MessageColumns.FLAGS, mFlags);
+
+            values.put(SyncColumns.SERVER_ID, mServerId);
+            values.put(SyncColumns.SERVER_TIMESTAMP, mServerTimeStamp);
+            values.put(MessageColumns.CLIENT_ID, mClientId);
+            values.put(MessageColumns.MESSAGE_ID, mMessageId);
+
+            values.put(MessageColumns.MAILBOX_KEY, mMailboxKey);
+            values.put(MessageColumns.ACCOUNT_KEY, mAccountKey);
+
+            values.put(MessageColumns.FROM_LIST, mFrom);
+            values.put(MessageColumns.TO_LIST, mTo);
+            values.put(MessageColumns.CC_LIST, mCc);
+            values.put(MessageColumns.BCC_LIST, mBcc);
+            values.put(MessageColumns.REPLY_TO_LIST, mReplyTo);
+
+            values.put(MessageColumns.MEETING_INFO, mMeetingInfo);
+
+            values.put(MessageColumns.SNIPPET, mSnippet);
+
+            values.put(MessageColumns.PROTOCOL_SEARCH_INFO, mProtocolSearchInfo);
+            return values;
+        }
+
+        public static Message restoreMessageWithId(Context context, long id) {
+            return EmailContent.restoreContentWithId(context, Message.class,
+                    Message.CONTENT_URI, Message.CONTENT_PROJECTION, id);
+        }
+
+        @Override
+        public void restore(Cursor cursor) {
+            mBaseUri = CONTENT_URI;
+            mId = cursor.getLong(CONTENT_ID_COLUMN);
+            mDisplayName = cursor.getString(CONTENT_DISPLAY_NAME_COLUMN);
+            mTimeStamp = cursor.getLong(CONTENT_TIMESTAMP_COLUMN);
+            mSubject = cursor.getString(CONTENT_SUBJECT_COLUMN);
+            mFlagRead = cursor.getInt(CONTENT_FLAG_READ_COLUMN) == 1;
+            mFlagLoaded = cursor.getInt(CONTENT_FLAG_LOADED_COLUMN);
+            mFlagFavorite = cursor.getInt(CONTENT_FLAG_FAVORITE_COLUMN) == 1;
+            mFlagAttachment = cursor.getInt(CONTENT_FLAG_ATTACHMENT_COLUMN) == 1;
+            mFlags = cursor.getInt(CONTENT_FLAGS_COLUMN);
+            mServerId = cursor.getString(CONTENT_SERVER_ID_COLUMN);
+            mServerTimeStamp = cursor.getLong(CONTENT_SERVER_TIMESTAMP_COLUMN);
+            mClientId = cursor.getString(CONTENT_CLIENT_ID_COLUMN);
+            mMessageId = cursor.getString(CONTENT_MESSAGE_ID_COLUMN);
+            mMailboxKey = cursor.getLong(CONTENT_MAILBOX_KEY_COLUMN);
+            mAccountKey = cursor.getLong(CONTENT_ACCOUNT_KEY_COLUMN);
+            mFrom = cursor.getString(CONTENT_FROM_LIST_COLUMN);
+            mTo = cursor.getString(CONTENT_TO_LIST_COLUMN);
+            mCc = cursor.getString(CONTENT_CC_LIST_COLUMN);
+            mBcc = cursor.getString(CONTENT_BCC_LIST_COLUMN);
+            mReplyTo = cursor.getString(CONTENT_REPLY_TO_COLUMN);
+            mMeetingInfo = cursor.getString(CONTENT_MEETING_INFO_COLUMN);
+            mSnippet = cursor.getString(CONTENT_SNIPPET_COLUMN);
+            mProtocolSearchInfo = cursor.getString(CONTENT_PROTOCOL_SEARCH_INFO_COLUMN);
+        }
+
+        public boolean update() {
+            // TODO Auto-generated method stub
+            return false;
+        }
+
+        /*
+         * Override this so that we can store the Body first and link it to the Message
+         * Also, attachments when we get there...
+         * (non-Javadoc)
+         * @see com.android.email.provider.EmailContent#save(android.content.Context)
+         */
+        @Override
+        public Uri save(Context context) {
+
+            boolean doSave = !isSaved();
+
+            // This logic is in place so I can (a) short circuit the expensive stuff when
+            // possible, and (b) override (and throw) if anyone tries to call save() or update()
+            // directly for Message, which are unsupported.
+            if (mText == null && mHtml == null && mTextReply == null && mHtmlReply == null &&
+                    (mAttachments == null || mAttachments.isEmpty())) {
+                if (doSave) {
+                    return super.save(context);
+                } else {
+                    // Call update, rather than super.update in case we ever override it
+                    if (update(context, toContentValues()) == 1) {
+                        return getUri();
+                    }
+                    return null;
+                }
+            }
+
+            ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();
+            addSaveOps(ops);
+            try {
+                ContentProviderResult[] results =
+                    context.getContentResolver().applyBatch(AUTHORITY, ops);
+                // If saving, set the mId's of the various saved objects
+                if (doSave) {
+                    Uri u = results[0].uri;
+                    mId = Long.parseLong(u.getPathSegments().get(1));
+                    if (mAttachments != null) {
+                        int resultOffset = 2;
+                        for (Attachment a : mAttachments) {
+                            // Save the id of the attachment record
+                            u = results[resultOffset++].uri;
+                            if (u != null) {
+                                a.mId = Long.parseLong(u.getPathSegments().get(1));
+                            }
+                            a.mMessageKey = mId;
+                        }
+                    }
+                    return u;
+                } else {
+                    return null;
+                }
+            } catch (RemoteException e) {
+                // There is nothing to be done here; fail by returning null
+            } catch (OperationApplicationException e) {
+                // There is nothing to be done here; fail by returning null
+            }
+            return null;
+        }
+
+        /**
+         * Save or update a message
+         * @param ops an array of CPOs that we'll add to
+         */
+        public void addSaveOps(ArrayList<ContentProviderOperation> ops) {
+            boolean isNew = !isSaved();
+            ContentProviderOperation.Builder b;
+            // First, save/update the message
+            if (isNew) {
+                b = ContentProviderOperation.newInsert(mBaseUri);
+            } else {
+                b = ContentProviderOperation.newUpdate(mBaseUri)
+                        .withSelection(Message.RECORD_ID + "=?", new String[] {Long.toString(mId)});
+            }
+            // Generate the snippet here, before we create the CPO for Message
+            if (mText != null) {
+                mSnippet = TextUtilities.makeSnippetFromPlainText(mText);
+            } else if (mHtml != null) {
+                mSnippet = TextUtilities.makeSnippetFromHtmlText(mHtml);
+            }
+            ops.add(b.withValues(toContentValues()).build());
+
+            // Create and save the body
+            ContentValues cv = new ContentValues();
+            if (mText != null) {
+                cv.put(Body.TEXT_CONTENT, mText);
+            }
+            if (mHtml != null) {
+                cv.put(Body.HTML_CONTENT, mHtml);
+            }
+            if (mTextReply != null) {
+                cv.put(Body.TEXT_REPLY, mTextReply);
+            }
+            if (mHtmlReply != null) {
+                cv.put(Body.HTML_REPLY, mHtmlReply);
+            }
+            if (mSourceKey != 0) {
+                cv.put(Body.SOURCE_MESSAGE_KEY, mSourceKey);
+            }
+            if (mIntroText != null) {
+                cv.put(Body.INTRO_TEXT, mIntroText);
+            }
+            b = ContentProviderOperation.newInsert(Body.CONTENT_URI);
+            // Put our message id in the Body
+            if (!isNew) {
+                cv.put(Body.MESSAGE_KEY, mId);
+            }
+            b.withValues(cv);
+            // We'll need this if we're new
+            int messageBackValue = ops.size() - 1;
+            // If we're new, create a back value entry
+            if (isNew) {
+                ContentValues backValues = new ContentValues();
+                backValues.put(Body.MESSAGE_KEY, messageBackValue);
+                b.withValueBackReferences(backValues);
+            }
+            // And add the Body operation
+            ops.add(b.build());
+
+            // Create the attaachments, if any
+            if (mAttachments != null) {
+                for (Attachment att: mAttachments) {
+                    if (!isNew) {
+                        att.mMessageKey = mId;
+                    }
+                    b = ContentProviderOperation.newInsert(Attachment.CONTENT_URI)
+                            .withValues(att.toContentValues());
+                    if (isNew) {
+                        b.withValueBackReference(Attachment.MESSAGE_KEY, messageBackValue);
+                    }
+                    ops.add(b.build());
+                }
+            }
+        }
+
+        /**
+         * @return number of favorite (starred) messages throughout all accounts.
+         */
+        public static int getFavoriteMessageCount(Context context) {
+            return count(context, Message.CONTENT_URI, ALL_FAVORITE_SELECTION, null);
+        }
+
+        /**
+         * @return number of favorite (starred) messages for an account
+         */
+        public static int getFavoriteMessageCount(Context context, long accountId) {
+            return count(context, Message.CONTENT_URI, PER_ACCOUNT_FAVORITE_SELECTION,
+                    new String[]{Long.toString(accountId)});
+        }
+
+        public static long getKeyColumnLong(Context context, long messageId, String column) {
+            String[] columns =
+                Utility.getRowColumns(context, Message.CONTENT_URI, messageId, column);
+            if (columns != null && columns[0] != null) {
+                return Long.parseLong(columns[0]);
+            }
+            return -1;
+        }
+
+        /**
+         * Returns the where clause for a message list selection.
+         *
+         * Accesses the detabase to determine the mailbox type.  DO NOT CALL FROM UI THREAD.
+         */
+        public static String buildMessageListSelection(
+                Context context, long accountId, long mailboxId) {
+
+            if (mailboxId == Mailbox.QUERY_ALL_INBOXES) {
+                return Message.ALL_INBOX_SELECTION;
+            }
+            if (mailboxId == Mailbox.QUERY_ALL_DRAFTS) {
+                return Message.ALL_DRAFT_SELECTION;
+            }
+            if (mailboxId == Mailbox.QUERY_ALL_OUTBOX) {
+                return Message.ALL_OUTBOX_SELECTION;
+            }
+            if (mailboxId == Mailbox.QUERY_ALL_UNREAD) {
+                return Message.ALL_UNREAD_SELECTION;
+            }
+            // TODO: we only support per-account starred mailbox right now, but presumably, we
+            // can surface the same thing for unread.
+            if (mailboxId == Mailbox.QUERY_ALL_FAVORITES) {
+                if (accountId == Account.ACCOUNT_ID_COMBINED_VIEW) {
+                    return Message.ALL_FAVORITE_SELECTION;
+                }
+
+                final StringBuilder selection = new StringBuilder();
+                selection.append(MessageColumns.ACCOUNT_KEY).append('=').append(accountId)
+                        .append(" AND ")
+                        .append(Message.ALL_FAVORITE_SELECTION);
+                return selection.toString();
+            }
+
+            // Now it's a regular mailbox.
+            final StringBuilder selection = new StringBuilder();
+
+            selection.append(MessageColumns.MAILBOX_KEY).append('=').append(mailboxId);
+
+            if (Mailbox.getMailboxType(context, mailboxId) != Mailbox.TYPE_OUTBOX) {
+                selection.append(" AND ").append(Message.FLAG_LOADED_SELECTION);
+            }
+            return selection.toString();
+        }
+    }
+
+    public interface AttachmentColumns {
+        public static final String ID = "_id";
+        // The display name of the attachment
+        public static final String FILENAME = "fileName";
+        // The mime type of the attachment
+        public static final String MIME_TYPE = "mimeType";
+        // The size of the attachment in bytes
+        public static final String SIZE = "size";
+        // The (internal) contentId of the attachment (inline attachments will have these)
+        public static final String CONTENT_ID = "contentId";
+        // The location of the loaded attachment (probably a file)
+        @SuppressWarnings("hiding")
+        public static final String CONTENT_URI = "contentUri";
+        // A foreign key into the Message table (the message owning this attachment)
+        public static final String MESSAGE_KEY = "messageKey";
+        // The location of the attachment on the server side
+        // For IMAP, this is a part number (e.g. 2.1); for EAS, it's the internal file name
+        public static final String LOCATION = "location";
+        // The transfer encoding of the attachment
+        public static final String ENCODING = "encoding";
+        // Not currently used
+        public static final String CONTENT = "content";
+        // Flags
+        public static final String FLAGS = "flags";
+        // Content that is actually contained in the Attachment row
+        public static final String CONTENT_BYTES = "content_bytes";
+        // A foreign key into the Account table (for the message owning this attachment)
+        public static final String ACCOUNT_KEY = "accountKey";
+        // The UIProvider state of the attachment
+        public static final String UI_STATE = "uiState";
+        // The UIProvider destination of the attachment
+        public static final String UI_DESTINATION = "uiDestination";
+        // The UIProvider downloaded size of the attachment
+        public static final String UI_DOWNLOADED_SIZE = "uiDownloadedSize";
+    }
+
+    public static final class Attachment extends EmailContent
+            implements AttachmentColumns, Parcelable {
+        public static final String TABLE_NAME = "Attachment";
+        @SuppressWarnings("hiding")
+        public static final Uri CONTENT_URI = Uri.parse(EmailContent.CONTENT_URI + "/attachment");
+        // This must be used with an appended id: ContentUris.withAppendedId(MESSAGE_ID_URI, id)
+        public static final Uri MESSAGE_ID_URI = Uri.parse(
+                EmailContent.CONTENT_URI + "/attachment/message");
+
+        public String mFileName;
+        public String mMimeType;
+        public long mSize;
+        public String mContentId;
+        public String mContentUri;
+        public long mMessageKey;
+        public String mLocation;
+        public String mEncoding;
+        public String mContent; // Not currently used
+        public int mFlags;
+        public byte[] mContentBytes;
+        public long mAccountKey;
+        public int mUiState;
+        public int mUiDestination;
+        public int mUiDownloadedSize;
+
+        public static final int CONTENT_ID_COLUMN = 0;
+        public static final int CONTENT_FILENAME_COLUMN = 1;
+        public static final int CONTENT_MIME_TYPE_COLUMN = 2;
+        public static final int CONTENT_SIZE_COLUMN = 3;
+        public static final int CONTENT_CONTENT_ID_COLUMN = 4;
+        public static final int CONTENT_CONTENT_URI_COLUMN = 5;
+        public static final int CONTENT_MESSAGE_ID_COLUMN = 6;
+        public static final int CONTENT_LOCATION_COLUMN = 7;
+        public static final int CONTENT_ENCODING_COLUMN = 8;
+        public static final int CONTENT_CONTENT_COLUMN = 9; // Not currently used
+        public static final int CONTENT_FLAGS_COLUMN = 10;
+        public static final int CONTENT_CONTENT_BYTES_COLUMN = 11;
+        public static final int CONTENT_ACCOUNT_KEY_COLUMN = 12;
+        public static final int CONTENT_UI_STATE_COLUMN = 13;
+        public static final int CONTENT_UI_DESTINATION_COLUMN = 14;
+        public static final int CONTENT_UI_DOWNLOADED_SIZE_COLUMN = 15;
+        public static final String[] CONTENT_PROJECTION = new String[] {
+            RECORD_ID, AttachmentColumns.FILENAME, AttachmentColumns.MIME_TYPE,
+            AttachmentColumns.SIZE, AttachmentColumns.CONTENT_ID, AttachmentColumns.CONTENT_URI,
+            AttachmentColumns.MESSAGE_KEY, AttachmentColumns.LOCATION, AttachmentColumns.ENCODING,
+            AttachmentColumns.CONTENT, AttachmentColumns.FLAGS, AttachmentColumns.CONTENT_BYTES,
+            AttachmentColumns.ACCOUNT_KEY, AttachmentColumns.UI_STATE,
+            AttachmentColumns.UI_DESTINATION, AttachmentColumns.UI_DOWNLOADED_SIZE
+        };
+
+        // All attachments with an empty URI, regardless of mailbox
+        public static final String PRECACHE_SELECTION =
+            AttachmentColumns.CONTENT_URI + " isnull AND " + Attachment.FLAGS + "=0";
+        // Attachments with an empty URI that are in an inbox
+        public static final String PRECACHE_INBOX_SELECTION =
+            PRECACHE_SELECTION + " AND " + AttachmentColumns.MESSAGE_KEY + " IN ("
+            +     "SELECT " + MessageColumns.ID + " FROM " + Message.TABLE_NAME
+            +     " WHERE " + Message.ALL_INBOX_SELECTION
+            +     ")";
+
+        // Bits used in mFlags
+        // WARNING: AttachmentDownloadService relies on the fact that ALL of the flags below
+        // disqualify attachments for precaching.  If you add a flag that does NOT disqualify an
+        // attachment for precaching, you MUST change the PRECACHE_SELECTION definition above
+
+        // Instruct Rfc822Output to 1) not use Content-Disposition and 2) use multipart/alternative
+        // with this attachment.  This is only valid if there is one and only one attachment and
+        // that attachment has this flag set
+        public static final int FLAG_ICS_ALTERNATIVE_PART = 1<<0;
+        // Indicate that this attachment has been requested for downloading by the user; this is
+        // the highest priority for attachment downloading
+        public static final int FLAG_DOWNLOAD_USER_REQUEST = 1<<1;
+        // Indicate that this attachment needs to be downloaded as part of an outgoing forwarded
+        // message
+        public static final int FLAG_DOWNLOAD_FORWARD = 1<<2;
+        // Indicates that the attachment download failed in a non-recoverable manner
+        public static final int FLAG_DOWNLOAD_FAILED = 1<<3;
+        // Allow "room" for some additional download-related flags here
+        // Indicates that the attachment will be smart-forwarded
+        public static final int FLAG_SMART_FORWARD = 1<<8;
+        // Indicates that the attachment cannot be forwarded due to a policy restriction
+        public static final int FLAG_POLICY_DISALLOWS_DOWNLOAD = 1<<9;
+        /**
+         * no public constructor since this is a utility class
+         */
+        public Attachment() {
+            mBaseUri = CONTENT_URI;
+        }
+
+         /**
+         * Restore an Attachment from the database, given its unique id
+         * @param context
+         * @param id
+         * @return the instantiated Attachment
+         */
+        public static Attachment restoreAttachmentWithId(Context context, long id) {
+            return EmailContent.restoreContentWithId(context, Attachment.class,
+                    Attachment.CONTENT_URI, Attachment.CONTENT_PROJECTION, id);
+        }
+
+        /**
+         * Restore all the Attachments of a message given its messageId
+         */
+        public static Attachment[] restoreAttachmentsWithMessageId(Context context,
+                long messageId) {
+            Uri uri = ContentUris.withAppendedId(MESSAGE_ID_URI, messageId);
+            Cursor c = context.getContentResolver().query(uri, CONTENT_PROJECTION,
+                    null, null, null);
+            try {
+                int count = c.getCount();
+                Attachment[] attachments = new Attachment[count];
+                for (int i = 0; i < count; ++i) {
+                    c.moveToNext();
+                    Attachment attach = new Attachment();
+                    attach.restore(c);
+                    attachments[i] = attach;
+                }
+                return attachments;
+            } finally {
+                c.close();
+            }
+        }
+
+        /**
+         * Creates a unique file in the external store by appending a hyphen
+         * and a number to the given filename.
+         * @param filename
+         * @return a new File object, or null if one could not be created
+         */
+        public static File createUniqueFile(String filename) {
+            // TODO Handle internal storage, as required
+            if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
+                File directory = Environment.getExternalStorageDirectory();
+                File file = new File(directory, filename);
+                if (!file.exists()) {
+                    return file;
+                }
+                // Get the extension of the file, if any.
+                int index = filename.lastIndexOf('.');
+                String name = filename;
+                String extension = "";
+                if (index != -1) {
+                    name = filename.substring(0, index);
+                    extension = filename.substring(index);
+                }
+                for (int i = 2; i < Integer.MAX_VALUE; i++) {
+                    file = new File(directory, name + '-' + i + extension);
+                    if (!file.exists()) {
+                        return file;
+                    }
+                }
+                return null;
+            }
+            return null;
+        }
+
+        @Override
+        public void restore(Cursor cursor) {
+            mBaseUri = CONTENT_URI;
+            mId = cursor.getLong(CONTENT_ID_COLUMN);
+            mFileName= cursor.getString(CONTENT_FILENAME_COLUMN);
+            mMimeType = cursor.getString(CONTENT_MIME_TYPE_COLUMN);
+            mSize = cursor.getLong(CONTENT_SIZE_COLUMN);
+            mContentId = cursor.getString(CONTENT_CONTENT_ID_COLUMN);
+            mContentUri = cursor.getString(CONTENT_CONTENT_URI_COLUMN);
+            mMessageKey = cursor.getLong(CONTENT_MESSAGE_ID_COLUMN);
+            mLocation = cursor.getString(CONTENT_LOCATION_COLUMN);
+            mEncoding = cursor.getString(CONTENT_ENCODING_COLUMN);
+            mContent = cursor.getString(CONTENT_CONTENT_COLUMN);
+            mFlags = cursor.getInt(CONTENT_FLAGS_COLUMN);
+            mContentBytes = cursor.getBlob(CONTENT_CONTENT_BYTES_COLUMN);
+            mAccountKey = cursor.getLong(CONTENT_ACCOUNT_KEY_COLUMN);
+            mUiState = cursor.getInt(CONTENT_UI_STATE_COLUMN);
+            mUiDestination = cursor.getInt(CONTENT_UI_DESTINATION_COLUMN);
+            mUiDownloadedSize = cursor.getInt(CONTENT_UI_DOWNLOADED_SIZE_COLUMN);
+        }
+
+        @Override
+        public ContentValues toContentValues() {
+            ContentValues values = new ContentValues();
+            values.put(AttachmentColumns.FILENAME, mFileName);
+            values.put(AttachmentColumns.MIME_TYPE, mMimeType);
+            values.put(AttachmentColumns.SIZE, mSize);
+            values.put(AttachmentColumns.CONTENT_ID, mContentId);
+            values.put(AttachmentColumns.CONTENT_URI, mContentUri);
+            values.put(AttachmentColumns.MESSAGE_KEY, mMessageKey);
+            values.put(AttachmentColumns.LOCATION, mLocation);
+            values.put(AttachmentColumns.ENCODING, mEncoding);
+            values.put(AttachmentColumns.CONTENT, mContent);
+            values.put(AttachmentColumns.FLAGS, mFlags);
+            values.put(AttachmentColumns.CONTENT_BYTES, mContentBytes);
+            values.put(AttachmentColumns.ACCOUNT_KEY, mAccountKey);
+            values.put(AttachmentColumns.UI_STATE, mUiState);
+            values.put(AttachmentColumns.UI_DESTINATION, mUiDestination);
+            values.put(AttachmentColumns.UI_DOWNLOADED_SIZE, mUiDownloadedSize);
+            return values;
+        }
+
+        @Override
+        public int describeContents() {
+             return 0;
+        }
+
+        @Override
+        public void writeToParcel(Parcel dest, int flags) {
+            // mBaseUri is not parceled
+            dest.writeLong(mId);
+            dest.writeString(mFileName);
+            dest.writeString(mMimeType);
+            dest.writeLong(mSize);
+            dest.writeString(mContentId);
+            dest.writeString(mContentUri);
+            dest.writeLong(mMessageKey);
+            dest.writeString(mLocation);
+            dest.writeString(mEncoding);
+            dest.writeString(mContent);
+            dest.writeInt(mFlags);
+            dest.writeLong(mAccountKey);
+            if (mContentBytes == null) {
+                dest.writeInt(-1);
+            } else {
+                dest.writeInt(mContentBytes.length);
+                dest.writeByteArray(mContentBytes);
+            }
+            dest.writeInt(mUiState);
+            dest.writeInt(mUiDestination);
+            dest.writeInt(mUiDownloadedSize);
+        }
+
+        public Attachment(Parcel in) {
+            mBaseUri = EmailContent.Attachment.CONTENT_URI;
+            mId = in.readLong();
+            mFileName = in.readString();
+            mMimeType = in.readString();
+            mSize = in.readLong();
+            mContentId = in.readString();
+            mContentUri = in.readString();
+            mMessageKey = in.readLong();
+            mLocation = in.readString();
+            mEncoding = in.readString();
+            mContent = in.readString();
+            mFlags = in.readInt();
+            mAccountKey = in.readLong();
+            final int contentBytesLen = in.readInt();
+            if (contentBytesLen == -1) {
+                mContentBytes = null;
+            } else {
+                mContentBytes = new byte[contentBytesLen];
+                in.readByteArray(mContentBytes);
+            }
+            mUiState = in.readInt();
+            mUiDestination = in.readInt();
+            mUiDownloadedSize = in.readInt();
+         }
+
+        public static final Parcelable.Creator<EmailContent.Attachment> CREATOR
+                = new Parcelable.Creator<EmailContent.Attachment>() {
+            @Override
+            public EmailContent.Attachment createFromParcel(Parcel in) {
+                return new EmailContent.Attachment(in);
+            }
+
+            @Override
+            public EmailContent.Attachment[] newArray(int size) {
+                return new EmailContent.Attachment[size];
+            }
+        };
+
+        @Override
+        public String toString() {
+            return "[" + mFileName + ", " + mMimeType + ", " + mSize + ", " + mContentId + ", "
+                    + mContentUri + ", " + mMessageKey + ", " + mLocation + ", " + mEncoding  + ", "
+                    + mFlags + ", " + mContentBytes + ", " + mAccountKey +  "," + mUiState + ","
+                    + mUiDestination + "," + mUiDownloadedSize + "]";
+        }
+    }
+
+    public interface AccountColumns {
+        public static final String ID = "_id";
+        // The display name of the account (user-settable)
+        public static final String DISPLAY_NAME = "displayName";
+        // The email address corresponding to this account
+        public static final String EMAIL_ADDRESS = "emailAddress";
+        // A server-based sync key on an account-wide basis (EAS needs this)
+        public static final String SYNC_KEY = "syncKey";
+        // The default sync lookback period for this account
+        public static final String SYNC_LOOKBACK = "syncLookback";
+        // The default sync frequency for this account, in minutes
+        public static final String SYNC_INTERVAL = "syncInterval";
+        // A foreign key into the account manager, having host, login, password, port, and ssl flags
+        public static final String HOST_AUTH_KEY_RECV = "hostAuthKeyRecv";
+        // (optional) A foreign key into the account manager, having host, login, password, port,
+        // and ssl flags
+        public static final String HOST_AUTH_KEY_SEND = "hostAuthKeySend";
+        // Flags
+        public static final String FLAGS = "flags";
+        // Default account
+        public static final String IS_DEFAULT = "isDefault";
+        // Old-Style UUID for compatibility with previous versions
+        public static final String COMPATIBILITY_UUID = "compatibilityUuid";
+        // User name (for outgoing messages)
+        public static final String SENDER_NAME = "senderName";
+        // Ringtone
+        public static final String RINGTONE_URI = "ringtoneUri";
+        // Protocol version (arbitrary string, used by EAS currently)
+        public static final String PROTOCOL_VERSION = "protocolVersion";
+        // The number of new messages (reported by the sync/download engines
+        public static final String NEW_MESSAGE_COUNT = "newMessageCount";
+        // Legacy flags defining security (provisioning) requirements of this account; this
+        // information is now found in the Policy table; POLICY_KEY (below) is the foreign key
+        @Deprecated
+        public static final String SECURITY_FLAGS = "securityFlags";
+        // Server-based sync key for the security policies currently enforced
+        public static final String SECURITY_SYNC_KEY = "securitySyncKey";
+        // Signature to use with this account
+        public static final String SIGNATURE = "signature";
+        // A foreign key into the Policy table
+        public static final String POLICY_KEY = "policyKey";
+    }
+
+    public interface QuickResponseColumns {
+        // The QuickResponse text
+        static final String TEXT = "quickResponse";
+        // A foreign key into the Account table owning the QuickResponse
+        static final String ACCOUNT_KEY = "accountKey";
+    }
+
+    public interface MailboxColumns {
+        public static final String ID = "_id";
+        // The display name of this mailbox [INDEX]
+        static final String DISPLAY_NAME = "displayName";
+        // The server's identifier for this mailbox
+        public static final String SERVER_ID = "serverId";
+        // The server's identifier for the parent of this mailbox (null = top-level)
+        public static final String PARENT_SERVER_ID = "parentServerId";
+        // A foreign key for the parent of this mailbox (-1 = top-level, 0=uninitialized)
+        public static final String PARENT_KEY = "parentKey";
+        // A foreign key to the Account that owns this mailbox
+        public static final String ACCOUNT_KEY = "accountKey";
+        // The type (role) of this mailbox
+        public static final String TYPE = "type";
+        // The hierarchy separator character
+        public static final String DELIMITER = "delimiter";
+        // Server-based sync key or validity marker (e.g. "SyncKey" for EAS, "uidvalidity" for IMAP)
+        public static final String SYNC_KEY = "syncKey";
+        // The sync lookback period for this mailbox (or null if using the account default)
+        public static final String SYNC_LOOKBACK = "syncLookback";
+        // The sync frequency for this mailbox (or null if using the account default)
+        public static final String SYNC_INTERVAL = "syncInterval";
+        // The time of last successful sync completion (millis)
+        public static final String SYNC_TIME = "syncTime";
+        // Cached unread count
+        public static final String UNREAD_COUNT = "unreadCount";
+        // Visibility of this folder in a list of folders [INDEX]
+        public static final String FLAG_VISIBLE = "flagVisible";
+        // Other states, as a bit field, e.g. CHILDREN_VISIBLE, HAS_CHILDREN
+        public static final String FLAGS = "flags";
+        // Backward compatible
+        public static final String VISIBLE_LIMIT = "visibleLimit";
+        // Sync status (can be used as desired by sync services)
+        public static final String SYNC_STATUS = "syncStatus";
+        // Number of messages in the mailbox.
+        public static final String MESSAGE_COUNT = "messageCount";
+        // The last time a message in this mailbox has been read (in millis)
+        public static final String LAST_TOUCHED_TIME = "lastTouchedTime";
+        // The UIProvider sync status
+        public static final String UI_SYNC_STATUS = "uiSyncStatus";
+        // The UIProvider last sync result
+        public static final String UI_LAST_SYNC_RESULT = "uiLastSyncResult";
+        // The UIProvider sync status
+        public static final String LAST_NOTIFIED_MESSAGE_KEY = "lastNotifiedMessageKey";
+        // The UIProvider last sync result
+        public static final String LAST_NOTIFIED_MESSAGE_COUNT = "lastNotifiedMessageCount";
+        // The total number of messages in the remote mailbox
+        public static final String TOTAL_COUNT = "totalCount";
+    }
+
+    public interface HostAuthColumns {
+        public static final String ID = "_id";
+        // The protocol (e.g. "imap", "pop3", "eas", "smtp"
+        static final String PROTOCOL = "protocol";
+        // The host address
+        static final String ADDRESS = "address";
+        // The port to use for the connection
+        static final String PORT = "port";
+        // General purpose flags
+        static final String FLAGS = "flags";
+        // The login (user name)
+        static final String LOGIN = "login";
+        // Password
+        static final String PASSWORD = "password";
+        // A domain or path, if required (used in IMAP and EAS)
+        static final String DOMAIN = "domain";
+        // An alias to a local client certificate for SSL
+        static final String CLIENT_CERT_ALIAS = "certAlias";
+        // DEPRECATED - Will not be set or stored
+        static final String ACCOUNT_KEY = "accountKey";
+    }
+
+    public interface PolicyColumns {
+        public static final String ID = "_id";
+        public static final String PASSWORD_MODE = "passwordMode";
+        public static final String PASSWORD_MIN_LENGTH = "passwordMinLength";
+        public static final String PASSWORD_EXPIRATION_DAYS = "passwordExpirationDays";
+        public static final String PASSWORD_HISTORY = "passwordHistory";
+        public static final String PASSWORD_COMPLEX_CHARS = "passwordComplexChars";
+        public static final String PASSWORD_MAX_FAILS = "passwordMaxFails";
+        public static final String MAX_SCREEN_LOCK_TIME = "maxScreenLockTime";
+        public static final String REQUIRE_REMOTE_WIPE = "requireRemoteWipe";
+        public static final String REQUIRE_ENCRYPTION = "requireEncryption";
+        public static final String REQUIRE_ENCRYPTION_EXTERNAL = "requireEncryptionExternal";
+        // ICS additions
+        // Note: the appearance of these columns does not imply that we support these features; only
+        // that we store them in the Policy structure
+        public static final String REQUIRE_MANUAL_SYNC_WHEN_ROAMING = "requireManualSyncRoaming";
+        public static final String DONT_ALLOW_CAMERA = "dontAllowCamera";
+        public static final String DONT_ALLOW_ATTACHMENTS = "dontAllowAttachments";
+        public static final String DONT_ALLOW_HTML = "dontAllowHtml";
+        public static final String MAX_ATTACHMENT_SIZE = "maxAttachmentSize";
+        public static final String MAX_TEXT_TRUNCATION_SIZE = "maxTextTruncationSize";
+        public static final String MAX_HTML_TRUNCATION_SIZE = "maxHTMLTruncationSize";
+        public static final String MAX_EMAIL_LOOKBACK = "maxEmailLookback";
+        public static final String MAX_CALENDAR_LOOKBACK = "maxCalendarLookback";
+        // Indicates that the server allows password recovery, not that we support it
+        public static final String PASSWORD_RECOVERY_ENABLED = "passwordRecoveryEnabled";
+        // Tokenized strings indicating protocol specific policies enforced/unsupported
+        public static final String PROTOCOL_POLICIES_ENFORCED = "protocolPoliciesEnforced";
+        public static final String PROTOCOL_POLICIES_UNSUPPORTED = "protocolPoliciesUnsupported";
+    }
+}
diff --git a/email2/emailcommon/src/com/android/emailcommon/provider/HostAuth.aidl b/email2/emailcommon/src/com/android/emailcommon/provider/HostAuth.aidl
new file mode 100644
index 0000000..88eb88f
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/provider/HostAuth.aidl
@@ -0,0 +1,18 @@
+/* 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.provider;
+
+parcelable HostAuth;
\ No newline at end of file
diff --git a/email2/emailcommon/src/com/android/emailcommon/provider/HostAuth.java b/email2/emailcommon/src/com/android/emailcommon/provider/HostAuth.java
new file mode 100644
index 0000000..8729418
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/provider/HostAuth.java
@@ -0,0 +1,441 @@
+/*
+ * 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.provider;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+
+import com.android.emailcommon.provider.EmailContent.HostAuthColumns;
+import com.android.emailcommon.utility.SSLUtils;
+import com.android.emailcommon.utility.Utility;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+
+public final class HostAuth extends EmailContent implements HostAuthColumns, Parcelable {
+    public static final String TABLE_NAME = "HostAuth";
+    @SuppressWarnings("hiding")
+    public static final Uri CONTENT_URI = Uri.parse(EmailContent.CONTENT_URI + "/hostauth");
+    // TODO the three following constants duplicate constants in Store.java; remove those and
+    //      just reference these.
+    public static final String SCHEME_IMAP = "imap";
+    public static final String SCHEME_POP3 = "pop3";
+    public static final String SCHEME_EAS = "eas";
+    public static final String SCHEME_SMTP = "smtp";
+    public static final String SCHEME_TRUST_ALL_CERTS = "trustallcerts";
+
+    public static final int PORT_UNKNOWN = -1;
+
+    public static final int FLAG_NONE         = 0x00;    // No flags
+    public static final int FLAG_SSL          = 0x01;    // Use SSL
+    public static final int FLAG_TLS          = 0x02;    // Use TLS
+    public static final int FLAG_AUTHENTICATE = 0x04;    // Use name/password for authentication
+    public static final int FLAG_TRUST_ALL    = 0x08;    // Trust all certificates
+    // Mask of settings directly configurable by the user
+    public static final int USER_CONFIG_MASK  = 0x0b;
+
+    public String mProtocol;
+    public String mAddress;
+    public int mPort;
+    public int mFlags;
+    public String mLogin;
+    public String mPassword;
+    public String mDomain;
+    public String mClientCertAlias = null;
+
+    public static final int CONTENT_ID_COLUMN = 0;
+    public static final int CONTENT_PROTOCOL_COLUMN = 1;
+    public static final int CONTENT_ADDRESS_COLUMN = 2;
+    public static final int CONTENT_PORT_COLUMN = 3;
+    public static final int CONTENT_FLAGS_COLUMN = 4;
+    public static final int CONTENT_LOGIN_COLUMN = 5;
+    public static final int CONTENT_PASSWORD_COLUMN = 6;
+    public static final int CONTENT_DOMAIN_COLUMN = 7;
+    public static final int CONTENT_CLIENT_CERT_ALIAS_COLUMN = 8;
+
+    public static final String[] CONTENT_PROJECTION = new String[] {
+        RECORD_ID, HostAuthColumns.PROTOCOL, HostAuthColumns.ADDRESS, HostAuthColumns.PORT,
+        HostAuthColumns.FLAGS, HostAuthColumns.LOGIN,
+        HostAuthColumns.PASSWORD, HostAuthColumns.DOMAIN, HostAuthColumns.CLIENT_CERT_ALIAS
+    };
+
+    /**
+     * no public constructor since this is a utility class
+     */
+    public HostAuth() {
+        mBaseUri = CONTENT_URI;
+
+        // other defaults policy)
+        mPort = PORT_UNKNOWN;
+    }
+
+     /**
+     * Restore a HostAuth from the database, given its unique id
+     * @param context
+     * @param id
+     * @return the instantiated HostAuth
+     */
+    public static HostAuth restoreHostAuthWithId(Context context, long id) {
+        return EmailContent.restoreContentWithId(context, HostAuth.class,
+                HostAuth.CONTENT_URI, HostAuth.CONTENT_PROJECTION, id);
+    }
+
+
+    /**
+     * Returns the scheme for the specified flags.
+     */
+    public static String getSchemeString(String protocol, int flags) {
+        return getSchemeString(protocol, flags, null);
+    }
+
+    /**
+     * Builds a URI scheme name given the parameters for a {@code HostAuth}.
+     * If a {@code clientAlias} is provided, this indicates that a secure connection must be used.
+     */
+    public static String getSchemeString(String protocol, int flags, String clientAlias) {
+        String security = "";
+        switch (flags & USER_CONFIG_MASK) {
+            case FLAG_SSL:
+                security = "+ssl+";
+                break;
+            case FLAG_SSL | FLAG_TRUST_ALL:
+                security = "+ssl+trustallcerts";
+                break;
+            case FLAG_TLS:
+                security = "+tls+";
+                break;
+            case FLAG_TLS | FLAG_TRUST_ALL:
+                security = "+tls+trustallcerts";
+                break;
+        }
+
+        if (!TextUtils.isEmpty(clientAlias)) {
+            if (TextUtils.isEmpty(security)) {
+                throw new IllegalArgumentException(
+                        "Can't specify a certificate alias for a non-secure connection");
+            }
+            if (!security.endsWith("+")) {
+                security += "+";
+            }
+            security += SSLUtils.escapeForSchemeName(clientAlias);
+        }
+
+        return protocol + security;
+    }
+
+    /**
+     * Returns the flags for the specified scheme.
+     */
+    public static int getSchemeFlags(String scheme) {
+        String[] schemeParts = scheme.split("\\+");
+        int flags = HostAuth.FLAG_NONE;
+        if (schemeParts.length >= 2) {
+            String part1 = schemeParts[1];
+            if ("ssl".equals(part1)) {
+                flags |= HostAuth.FLAG_SSL;
+            } else if ("tls".equals(part1)) {
+                flags |= HostAuth.FLAG_TLS;
+            }
+            if (schemeParts.length >= 3) {
+                String part2 = schemeParts[2];
+                if (SCHEME_TRUST_ALL_CERTS.equals(part2)) {
+                    flags |= HostAuth.FLAG_TRUST_ALL;
+                }
+            }
+        }
+        return flags;
+    }
+
+    @Override
+    public void restore(Cursor cursor) {
+        mBaseUri = CONTENT_URI;
+        mId = cursor.getLong(CONTENT_ID_COLUMN);
+        mProtocol = cursor.getString(CONTENT_PROTOCOL_COLUMN);
+        mAddress = cursor.getString(CONTENT_ADDRESS_COLUMN);
+        mPort = cursor.getInt(CONTENT_PORT_COLUMN);
+        mFlags = cursor.getInt(CONTENT_FLAGS_COLUMN);
+        mLogin = cursor.getString(CONTENT_LOGIN_COLUMN);
+        mPassword = cursor.getString(CONTENT_PASSWORD_COLUMN);
+        mDomain = cursor.getString(CONTENT_DOMAIN_COLUMN);
+        mClientCertAlias = cursor.getString(CONTENT_CLIENT_CERT_ALIAS_COLUMN);
+    }
+
+    @Override
+    public ContentValues toContentValues() {
+        ContentValues values = new ContentValues();
+        values.put(HostAuthColumns.PROTOCOL, mProtocol);
+        values.put(HostAuthColumns.ADDRESS, mAddress);
+        values.put(HostAuthColumns.PORT, mPort);
+        values.put(HostAuthColumns.FLAGS, mFlags);
+        values.put(HostAuthColumns.LOGIN, mLogin);
+        values.put(HostAuthColumns.PASSWORD, mPassword);
+        values.put(HostAuthColumns.DOMAIN, mDomain);
+        values.put(HostAuthColumns.CLIENT_CERT_ALIAS, mClientCertAlias);
+        values.put(HostAuthColumns.ACCOUNT_KEY, 0); // Need something to satisfy the DB
+        return values;
+    }
+
+    /**
+     * Sets the user name and password from URI user info string
+     */
+    public void setLogin(String userInfo) {
+        String userName = null;
+        String userPassword = null;
+        if (!TextUtils.isEmpty(userInfo)) {
+            String[] userInfoParts = userInfo.split(":", 2);
+            userName = userInfoParts[0];
+            if (userInfoParts.length > 1) {
+                userPassword = userInfoParts[1];
+            }
+        }
+        setLogin(userName, userPassword);
+    }
+
+    /**
+     * Sets the user name and password
+     */
+    public void setLogin(String userName, String userPassword) {
+        mLogin = userName;
+        mPassword = userPassword;
+
+        if (mLogin == null) {
+            mFlags &= ~FLAG_AUTHENTICATE;
+        } else {
+            mFlags |= FLAG_AUTHENTICATE;
+        }
+    }
+
+    /**
+     * Returns the login information. [0] is the username and [1] is the password. If
+     * {@link #FLAG_AUTHENTICATE} is not set, {@code null} is returned.
+     */
+    public String[] getLogin() {
+        if ((mFlags & FLAG_AUTHENTICATE) != 0) {
+            String trimUser = (mLogin != null) ? mLogin.trim() : "";
+            String password = (mPassword != null) ? mPassword : "";
+            return new String[] { trimUser, password };
+        }
+        return null;
+    }
+
+    public void setConnection(String protocol, String address, int port, int flags) {
+        setConnection(protocol, address, port, flags, null);
+    }
+
+    /**
+     * Sets the internal connection parameters based on the specified parameter values.
+     * @param protocol the mail protocol to use (e.g. "eas", "imap").
+     * @param address the address of the server
+     * @param port the port for the connection
+     * @param flags flags indicating the security and type of the connection
+     * @param clientCertAlias an optional alias to use if a client user certificate is to be
+     *     presented during connection establishment. If this is non-empty, it must be the case
+     *     that flags indicates use of a secure connection
+     */
+    public void setConnection(String protocol, String address,
+            int port, int flags, String clientCertAlias) {
+        // Set protocol, security, and additional flags based on uri scheme
+        mProtocol = protocol;
+
+        mFlags &= ~(FLAG_SSL | FLAG_TLS | FLAG_TRUST_ALL);
+        mFlags |= (flags & USER_CONFIG_MASK);
+
+        boolean useSecureConnection = (flags & (FLAG_SSL | FLAG_TLS)) != 0;
+        if (!useSecureConnection && !TextUtils.isEmpty(clientCertAlias)) {
+            throw new IllegalArgumentException("Can't use client alias on non-secure connections");
+        }
+
+        mAddress = address;
+        mPort = port;
+        if (mPort == PORT_UNKNOWN) {
+            boolean useSSL = ((mFlags & FLAG_SSL) != 0);
+            // infer port# from protocol + security
+            // SSL implies a different port - TLS runs in the "regular" port
+            // NOTE: Although the port should be setup in the various setup screens, this
+            // block cannot easily be moved because we get process URIs from other sources
+            // (e.g. for tests, provider templates and account restore) that may or may not
+            // have a port specified.
+            if (SCHEME_POP3.equals(mProtocol)) {
+                mPort = useSSL ? 995 : 110;
+            } else if (SCHEME_IMAP.equals(mProtocol)) {
+                mPort = useSSL ? 993 : 143;
+            } else if (SCHEME_EAS.equals(mProtocol)) {
+                mPort = useSSL ? 443 : 80;
+            } else if (SCHEME_SMTP.equals(mProtocol)) {
+                mPort = useSSL ? 465 : 587;
+            }
+        }
+
+        mClientCertAlias = clientCertAlias;
+    }
+
+    /** Returns {@code true} if this is an EAS connection; otherwise, {@code false}. */
+    public boolean isEasConnection() {
+        return SCHEME_EAS.equals(mProtocol);
+    }
+
+    /** Convenience method to determine if SSL is used. */
+    public boolean shouldUseSsl() {
+        return (mFlags & FLAG_SSL) != 0;
+    }
+
+    /** Convenience method to determine if all server certs should be used. */
+    public boolean shouldTrustAllServerCerts() {
+        return (mFlags & FLAG_TRUST_ALL) != 0;
+    }
+
+    /**
+     * Supports Parcelable
+     */
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    /**
+     * Supports Parcelable
+     */
+    public static final Parcelable.Creator<HostAuth> CREATOR
+            = new Parcelable.Creator<HostAuth>() {
+        @Override
+        public HostAuth createFromParcel(Parcel in) {
+            return new HostAuth(in);
+        }
+
+        @Override
+        public HostAuth[] newArray(int size) {
+            return new HostAuth[size];
+        }
+    };
+
+    /**
+     * Supports Parcelable
+     */
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        // mBaseUri is not parceled
+        dest.writeLong(mId);
+        dest.writeString(mProtocol);
+        dest.writeString(mAddress);
+        dest.writeInt(mPort);
+        dest.writeInt(mFlags);
+        dest.writeString(mLogin);
+        dest.writeString(mPassword);
+        dest.writeString(mDomain);
+        dest.writeString(mClientCertAlias);
+    }
+
+    /**
+     * Supports Parcelable
+     */
+    public HostAuth(Parcel in) {
+        mBaseUri = CONTENT_URI;
+        mId = in.readLong();
+        mProtocol = in.readString();
+        mAddress = in.readString();
+        mPort = in.readInt();
+        mFlags = in.readInt();
+        mLogin = in.readString();
+        mPassword = in.readString();
+        mDomain = in.readString();
+        mClientCertAlias = in.readString();
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (!(o instanceof HostAuth)) {
+            return false;
+        }
+        HostAuth that = (HostAuth)o;
+        return mPort == that.mPort
+                && mFlags == that.mFlags
+                && Utility.areStringsEqual(mProtocol, that.mProtocol)
+                && Utility.areStringsEqual(mAddress, that.mAddress)
+                && Utility.areStringsEqual(mLogin, that.mLogin)
+                && Utility.areStringsEqual(mPassword, that.mPassword)
+                && Utility.areStringsEqual(mDomain, that.mDomain)
+                && Utility.areStringsEqual(mClientCertAlias, that.mClientCertAlias);
+    }
+
+    /**
+     * The flag, password, and client cert alias are the only items likely to change after a
+     * HostAuth is created
+     */
+    @Override
+    public int hashCode() {
+        int hashCode = 29;
+        if (mPassword != null) {
+            hashCode += mPassword.hashCode();
+        }
+        if (mClientCertAlias != null) {
+            hashCode += (mClientCertAlias.hashCode() << 8);
+        }
+        return (hashCode << 8) + mFlags;
+    }
+
+    /**
+     * Legacy URI parser. Used in parsing template from provider.xml
+     * Example string:
+     *   "eas+ssl+trustallcerts://user:password@server/domain:123"
+     *
+     * Note that the use of client certificate is specified in the URI, a secure connection type
+     * must be used.
+     */
+    public static void setHostAuthFromString(HostAuth auth, String uriString)
+            throws URISyntaxException {
+        URI uri = new URI(uriString);
+        String path = uri.getPath();
+        String domain = null;
+        if (!TextUtils.isEmpty(path)) {
+            // Strip off the leading slash that begins the path.
+            domain = path.substring(1);
+        }
+        auth.mDomain = domain;
+        auth.setLogin(uri.getUserInfo());
+
+        String scheme = uri.getScheme();
+        auth.setConnection(scheme, uri.getHost(), uri.getPort());
+    }
+
+    /**
+     * Legacy code for setting connection values from a "scheme" (see above)
+     */
+    public void setConnection(String scheme, String host, int port) {
+        String[] schemeParts = scheme.split("\\+");
+        String protocol = schemeParts[0];
+        String clientCertAlias = null;
+        int flags = getSchemeFlags(scheme);
+
+        // Example scheme: "eas+ssl+trustallcerts" or "eas+tls+trustallcerts+client-cert-alias"
+        if (schemeParts.length > 3) {
+            clientCertAlias = schemeParts[3];
+        } else if (schemeParts.length > 2) {
+            if (!SCHEME_TRUST_ALL_CERTS.equals(schemeParts[2])) {
+                mClientCertAlias = schemeParts[2];
+            }
+        }
+
+        setConnection(protocol, host, port, flags, clientCertAlias);
+    }
+
+}
diff --git a/email2/emailcommon/src/com/android/emailcommon/provider/Mailbox.java b/email2/emailcommon/src/com/android/emailcommon/provider/Mailbox.java
new file mode 100644
index 0000000..4e44ad2
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/provider/Mailbox.java
@@ -0,0 +1,653 @@
+/*
+ * Copyright (C) 2009 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.provider;
+
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.Log;
+
+import com.android.emailcommon.Logging;
+import com.android.emailcommon.provider.EmailContent.MailboxColumns;
+import com.android.emailcommon.provider.EmailContent.SyncColumns;
+import com.android.emailcommon.utility.Utility;
+
+public class Mailbox extends EmailContent implements SyncColumns, MailboxColumns, Parcelable {
+    public static final String TABLE_NAME = "Mailbox";
+    @SuppressWarnings("hiding")
+    public static final Uri CONTENT_URI = Uri.parse(EmailContent.CONTENT_URI + "/mailbox");
+    public static final Uri ADD_TO_FIELD_URI =
+        Uri.parse(EmailContent.CONTENT_URI + "/mailboxIdAddToField");
+    public static final Uri FROM_ACCOUNT_AND_TYPE_URI =
+        Uri.parse(EmailContent.CONTENT_URI + "/mailboxIdFromAccountAndType");
+
+    public String mDisplayName;
+    public String mServerId;
+    public String mParentServerId;
+    public long mParentKey;
+    public long mAccountKey;
+    public int mType;
+    public int mDelimiter;
+    public String mSyncKey;
+    public int mSyncLookback;
+    public int mSyncInterval;
+    public long mSyncTime;
+    public boolean mFlagVisible = true;
+    public int mFlags;
+    public int mVisibleLimit;
+    public String mSyncStatus;
+    public long mLastTouchedTime;
+    public int mUiSyncStatus;
+    public int mUiLastSyncResult;
+    public long mLastNotifiedMessageKey;
+    public int mLastNotifiedMessageCount;
+    public int mTotalCount;
+
+    public static final int CONTENT_ID_COLUMN = 0;
+    public static final int CONTENT_DISPLAY_NAME_COLUMN = 1;
+    public static final int CONTENT_SERVER_ID_COLUMN = 2;
+    public static final int CONTENT_PARENT_SERVER_ID_COLUMN = 3;
+    public static final int CONTENT_ACCOUNT_KEY_COLUMN = 4;
+    public static final int CONTENT_TYPE_COLUMN = 5;
+    public static final int CONTENT_DELIMITER_COLUMN = 6;
+    public static final int CONTENT_SYNC_KEY_COLUMN = 7;
+    public static final int CONTENT_SYNC_LOOKBACK_COLUMN = 8;
+    public static final int CONTENT_SYNC_INTERVAL_COLUMN = 9;
+    public static final int CONTENT_SYNC_TIME_COLUMN = 10;
+    public static final int CONTENT_FLAG_VISIBLE_COLUMN = 11;
+    public static final int CONTENT_FLAGS_COLUMN = 12;
+    public static final int CONTENT_VISIBLE_LIMIT_COLUMN = 13;
+    public static final int CONTENT_SYNC_STATUS_COLUMN = 14;
+    public static final int CONTENT_PARENT_KEY_COLUMN = 15;
+    public static final int CONTENT_LAST_TOUCHED_TIME_COLUMN = 16;
+    public static final int CONTENT_UI_SYNC_STATUS_COLUMN = 17;
+    public static final int CONTENT_UI_LAST_SYNC_RESULT_COLUMN = 18;
+    public static final int CONTENT_LAST_NOTIFIED_MESSAGE_KEY_COLUMN = 19;
+    public static final int CONTENT_LAST_NOTIFIED_MESSAGE_COUNT_COLUMN = 20;
+    public static final int CONTENT_TOTAL_COUNT_COLUMN = 21;
+
+    /**
+     * <em>NOTE</em>: If fields are added or removed, the method {@link #getHashes()}
+     * MUST be updated.
+     */
+    public static final String[] CONTENT_PROJECTION = new String[] {
+        RECORD_ID, MailboxColumns.DISPLAY_NAME, MailboxColumns.SERVER_ID,
+        MailboxColumns.PARENT_SERVER_ID, MailboxColumns.ACCOUNT_KEY, MailboxColumns.TYPE,
+        MailboxColumns.DELIMITER, MailboxColumns.SYNC_KEY, MailboxColumns.SYNC_LOOKBACK,
+        MailboxColumns.SYNC_INTERVAL, MailboxColumns.SYNC_TIME,
+        MailboxColumns.FLAG_VISIBLE, MailboxColumns.FLAGS, MailboxColumns.VISIBLE_LIMIT,
+        MailboxColumns.SYNC_STATUS, MailboxColumns.PARENT_KEY, MailboxColumns.LAST_TOUCHED_TIME,
+        MailboxColumns.UI_SYNC_STATUS, MailboxColumns.UI_LAST_SYNC_RESULT,
+        MailboxColumns.LAST_NOTIFIED_MESSAGE_KEY, MailboxColumns.LAST_NOTIFIED_MESSAGE_COUNT,
+        MailboxColumns.TOTAL_COUNT
+    };
+
+    private static final String ACCOUNT_AND_MAILBOX_TYPE_SELECTION =
+            MailboxColumns.ACCOUNT_KEY + " =? AND " +
+            MailboxColumns.TYPE + " =?";
+    private static final String MAILBOX_TYPE_SELECTION =
+            MailboxColumns.TYPE + " =?";
+    /** Selection by server pathname for a given account */
+    public static final String PATH_AND_ACCOUNT_SELECTION =
+        MailboxColumns.SERVER_ID + "=? and " + MailboxColumns.ACCOUNT_KEY + "=?";
+
+    private static final String[] MAILBOX_SUM_OF_UNREAD_COUNT_PROJECTION = new String [] {
+            "sum(" + MailboxColumns.UNREAD_COUNT + ")"
+            };
+    private static final int UNREAD_COUNT_COUNT_COLUMN = 0;
+    private static final String[] MAILBOX_SUM_OF_MESSAGE_COUNT_PROJECTION = new String [] {
+            "sum(" + MailboxColumns.MESSAGE_COUNT + ")"
+            };
+    private static final int MESSAGE_COUNT_COUNT_COLUMN = 0;
+
+    private static final String[] MAILBOX_TYPE_PROJECTION = new String [] {
+            MailboxColumns.TYPE
+            };
+    private static final int MAILBOX_TYPE_TYPE_COLUMN = 0;
+
+    private static final String[] MAILBOX_DISPLAY_NAME_PROJECTION = new String [] {
+            MailboxColumns.DISPLAY_NAME
+            };
+    private static final int MAILBOX_DISPLAY_NAME_COLUMN = 0;
+
+    public static final long NO_MAILBOX = -1;
+
+    // Sentinel values for the mSyncInterval field of both Mailbox records
+    public static final int CHECK_INTERVAL_NEVER = -1;
+    public static final int CHECK_INTERVAL_PUSH = -2;
+    // The following two sentinel values are used by EAS
+    // Ping indicates that the EAS mailbox is synced based on a "ping" from the server
+    public static final int CHECK_INTERVAL_PING = -3;
+    // Push-Hold indicates an EAS push or ping Mailbox shouldn't sync just yet
+    public static final int CHECK_INTERVAL_PUSH_HOLD = -4;
+
+    // Sentinel for PARENT_KEY.  Use NO_MAILBOX for toplevel mailboxes (i.e. no parents).
+    public static final long PARENT_KEY_UNINITIALIZED = 0L;
+
+    private static final String WHERE_TYPE_AND_ACCOUNT_KEY =
+        MailboxColumns.TYPE + "=? and " + MailboxColumns.ACCOUNT_KEY + "=?";
+
+    public static final Integer[] INVALID_DROP_TARGETS = new Integer[] {Mailbox.TYPE_DRAFTS,
+        Mailbox.TYPE_OUTBOX, Mailbox.TYPE_SENT};
+
+    public static final String USER_VISIBLE_MAILBOX_SELECTION =
+        MailboxColumns.TYPE + "<" + Mailbox.TYPE_NOT_EMAIL +
+        " AND " + MailboxColumns.FLAG_VISIBLE + "=1";
+
+    // Types of mailboxes.  The list is ordered to match a typical UI presentation, e.g.
+    // placing the inbox at the top.
+    // Arrays of "special_mailbox_display_names" and "special_mailbox_icons" are depends on
+    // types Id of mailboxes.
+    /** No type specified */
+    public static final int TYPE_NONE = -1;
+    /** The "main" mailbox for the account, almost always referred to as "Inbox" */
+    public static final int TYPE_INBOX = 0;
+    // Types of mailboxes
+    /** Generic mailbox that holds mail */
+    public static final int TYPE_MAIL = 1;
+    /** Parent-only mailbox; does not hold any mail */
+    public static final int TYPE_PARENT = 2;
+    /** Drafts mailbox */
+    public static final int TYPE_DRAFTS = 3;
+    /** Local mailbox associated with the account's outgoing mail */
+    public static final int TYPE_OUTBOX = 4;
+    /** Sent mail; mail that was sent from the account */
+    public static final int TYPE_SENT = 5;
+    /** Deleted mail */
+    public static final int TYPE_TRASH = 6;
+    /** Junk mail */
+    public static final int TYPE_JUNK = 7;
+    /** Search results */
+    public static final int TYPE_SEARCH = 8;
+
+    // Types after this are used for non-mail mailboxes (as in EAS)
+    public static final int TYPE_NOT_EMAIL = 0x40;
+    public static final int TYPE_CALENDAR = 0x41;
+    public static final int TYPE_CONTACTS = 0x42;
+    public static final int TYPE_TASKS = 0x43;
+    public static final int TYPE_EAS_ACCOUNT_MAILBOX = 0x44;
+    public static final int TYPE_UNKNOWN = 0x45;
+
+    public static final int TYPE_NOT_SYNCABLE = 0x100;
+    // A mailbox that holds Messages that are attachments
+    public static final int TYPE_ATTACHMENT = 0x101;
+
+    // Default "touch" time for system mailboxes
+    public static final int DRAFTS_DEFAULT_TOUCH_TIME = 2;
+    public static final int SENT_DEFAULT_TOUCH_TIME = 1;
+
+    // Bit field flags; each is defined below
+    // Warning: Do not read these flags until POP/IMAP/EAS all populate them
+    /** No flags set */
+    public static final int FLAG_NONE = 0;
+    /** Has children in the mailbox hierarchy */
+    public static final int FLAG_HAS_CHILDREN = 1<<0;
+    /** Children are visible in the UI */
+    public static final int FLAG_CHILDREN_VISIBLE = 1<<1;
+    /** cannot receive "pushed" mail */
+    public static final int FLAG_CANT_PUSH = 1<<2;
+    /** can hold emails (i.e. some parent mailboxes cannot themselves contain mail) */
+    public static final int FLAG_HOLDS_MAIL = 1<<3;
+    /** can be used as a target for moving messages within the account */
+    public static final int FLAG_ACCEPTS_MOVED_MAIL = 1<<4;
+    /** can be used as a target for appending messages */
+    public static final int FLAG_ACCEPTS_APPENDED_MAIL = 1<<5;
+
+    // Magic mailbox ID's
+    // NOTE:  This is a quick solution for merged mailboxes.  I would rather implement this
+    // with a more generic way of packaging and sharing queries between activities
+    public static final long QUERY_ALL_INBOXES = -2;
+    public static final long QUERY_ALL_UNREAD = -3;
+    public static final long QUERY_ALL_FAVORITES = -4;
+    public static final long QUERY_ALL_DRAFTS = -5;
+    public static final long QUERY_ALL_OUTBOX = -6;
+
+    public Mailbox() {
+        mBaseUri = CONTENT_URI;
+    }
+
+     /**
+     * Restore a Mailbox from the database, given its unique id
+     * @param context
+     * @param id
+     * @return the instantiated Mailbox
+     */
+    public static Mailbox restoreMailboxWithId(Context context, long id) {
+        return EmailContent.restoreContentWithId(context, Mailbox.class,
+                Mailbox.CONTENT_URI, Mailbox.CONTENT_PROJECTION, id);
+    }
+
+    /**
+     * Builds a new mailbox with "typical" settings for a system mailbox, such as a local "Drafts"
+     * mailbox. This is useful for protocols like POP3 or IMAP who don't have certain local
+     * system mailboxes synced with the server.
+     * Note: the mailbox is not persisted - clients must call {@link #save} themselves.
+     */
+    public static Mailbox newSystemMailbox(long accountId, int mailboxType, String name) {
+        if (mailboxType == Mailbox.TYPE_MAIL) {
+            throw new IllegalArgumentException("Cannot specify TYPE_MAIL for a system mailbox");
+        }
+        Mailbox box = new Mailbox();
+        box.mAccountKey = accountId;
+        box.mType = mailboxType;
+        box.mSyncInterval = Account.CHECK_INTERVAL_NEVER;
+        box.mFlagVisible = true;
+        box.mServerId = box.mDisplayName = name;
+        box.mParentKey = Mailbox.NO_MAILBOX;
+        box.mFlags = Mailbox.FLAG_HOLDS_MAIL;
+        return box;
+    }
+
+    /**
+     * Returns a Mailbox from the database, given its pathname and account id. All mailbox
+     * paths for a particular account must be unique. Paths are stored in the column
+     * {@link MailboxColumns#SERVER_ID} for want of yet another column in the table.
+     * @param context
+     * @param accountId the ID of the account
+     * @param path the fully qualified, remote pathname
+     */
+    public static Mailbox restoreMailboxForPath(Context context, long accountId, String path) {
+        Cursor c = context.getContentResolver().query(
+                Mailbox.CONTENT_URI,
+                Mailbox.CONTENT_PROJECTION,
+                Mailbox.PATH_AND_ACCOUNT_SELECTION,
+                new String[] { path, Long.toString(accountId) },
+                null);
+        if (c == null) throw new ProviderUnavailableException();
+        try {
+            Mailbox mailbox = null;
+            if (c.moveToFirst()) {
+                mailbox = getContent(c, Mailbox.class);
+                if (c.moveToNext()) {
+                    Log.w(Logging.LOG_TAG, "Multiple mailboxes named \"" + path + "\"");
+                }
+            } else {
+                Log.i(Logging.LOG_TAG, "Could not find mailbox at \"" + path + "\"");
+            }
+            return mailbox;
+        } finally {
+            c.close();
+        }
+    }
+
+    /**
+     * Returns a {@link Mailbox} for the given path. If the path is not in the database, a new
+     * mailbox will be created.
+     */
+    public static Mailbox getMailboxForPath(Context context, long accountId, String path) {
+        Mailbox mailbox = restoreMailboxForPath(context, accountId, path);
+        if (mailbox == null) {
+            mailbox = new Mailbox();
+        }
+        return mailbox;
+    }
+
+    @Override
+    public void restore(Cursor cursor) {
+        mBaseUri = CONTENT_URI;
+        mId = cursor.getLong(CONTENT_ID_COLUMN);
+        mDisplayName = cursor.getString(CONTENT_DISPLAY_NAME_COLUMN);
+        mServerId = cursor.getString(CONTENT_SERVER_ID_COLUMN);
+        mParentServerId = cursor.getString(CONTENT_PARENT_SERVER_ID_COLUMN);
+        mParentKey = cursor.getLong(CONTENT_PARENT_KEY_COLUMN);
+        mAccountKey = cursor.getLong(CONTENT_ACCOUNT_KEY_COLUMN);
+        mType = cursor.getInt(CONTENT_TYPE_COLUMN);
+        mDelimiter = cursor.getInt(CONTENT_DELIMITER_COLUMN);
+        mSyncKey = cursor.getString(CONTENT_SYNC_KEY_COLUMN);
+        mSyncLookback = cursor.getInt(CONTENT_SYNC_LOOKBACK_COLUMN);
+        mSyncInterval = cursor.getInt(CONTENT_SYNC_INTERVAL_COLUMN);
+        mSyncTime = cursor.getLong(CONTENT_SYNC_TIME_COLUMN);
+        mFlagVisible = cursor.getInt(CONTENT_FLAG_VISIBLE_COLUMN) == 1;
+        mFlags = cursor.getInt(CONTENT_FLAGS_COLUMN);
+        mVisibleLimit = cursor.getInt(CONTENT_VISIBLE_LIMIT_COLUMN);
+        mSyncStatus = cursor.getString(CONTENT_SYNC_STATUS_COLUMN);
+        mLastTouchedTime = cursor.getLong(CONTENT_LAST_TOUCHED_TIME_COLUMN);
+        mUiSyncStatus = cursor.getInt(CONTENT_UI_SYNC_STATUS_COLUMN);
+        mUiLastSyncResult = cursor.getInt(CONTENT_UI_LAST_SYNC_RESULT_COLUMN);
+        mLastNotifiedMessageKey = cursor.getLong(CONTENT_LAST_NOTIFIED_MESSAGE_KEY_COLUMN);
+        mLastNotifiedMessageCount = cursor.getInt(CONTENT_LAST_NOTIFIED_MESSAGE_COUNT_COLUMN);
+        mTotalCount = cursor.getInt(CONTENT_TOTAL_COUNT_COLUMN);
+    }
+
+    @Override
+    public ContentValues toContentValues() {
+        ContentValues values = new ContentValues();
+        values.put(MailboxColumns.DISPLAY_NAME, mDisplayName);
+        values.put(MailboxColumns.SERVER_ID, mServerId);
+        values.put(MailboxColumns.PARENT_SERVER_ID, mParentServerId);
+        values.put(MailboxColumns.PARENT_KEY, mParentKey);
+        values.put(MailboxColumns.ACCOUNT_KEY, mAccountKey);
+        values.put(MailboxColumns.TYPE, mType);
+        values.put(MailboxColumns.DELIMITER, mDelimiter);
+        values.put(MailboxColumns.SYNC_KEY, mSyncKey);
+        values.put(MailboxColumns.SYNC_LOOKBACK, mSyncLookback);
+        values.put(MailboxColumns.SYNC_INTERVAL, mSyncInterval);
+        values.put(MailboxColumns.SYNC_TIME, mSyncTime);
+        values.put(MailboxColumns.FLAG_VISIBLE, mFlagVisible);
+        values.put(MailboxColumns.FLAGS, mFlags);
+        values.put(MailboxColumns.VISIBLE_LIMIT, mVisibleLimit);
+        values.put(MailboxColumns.SYNC_STATUS, mSyncStatus);
+        values.put(MailboxColumns.LAST_TOUCHED_TIME, mLastTouchedTime);
+        values.put(MailboxColumns.UI_SYNC_STATUS, mUiSyncStatus);
+        values.put(MailboxColumns.UI_LAST_SYNC_RESULT, mUiLastSyncResult);
+        values.put(MailboxColumns.LAST_NOTIFIED_MESSAGE_KEY, mLastNotifiedMessageKey);
+        values.put(MailboxColumns.LAST_NOTIFIED_MESSAGE_COUNT, mLastNotifiedMessageCount);
+        values.put(MailboxColumns.TOTAL_COUNT, mTotalCount);
+        return values;
+    }
+
+    /**
+     * Convenience method to return the id of a given type of Mailbox for a given Account; the
+     * common Mailbox types (Inbox, Outbox, Sent, Drafts, Trash, and Search) are all cached by
+     * EmailProvider; therefore, we warn if the mailbox is not found in the cache
+     *
+     * @param context the caller's context, used to get a ContentResolver
+     * @param accountId the id of the account to be queried
+     * @param type the mailbox type, as defined above
+     * @return the id of the mailbox, or -1 if not found
+     */
+    public static long findMailboxOfType(Context context, long accountId, int type) {
+        // First use special URI
+        Uri uri = FROM_ACCOUNT_AND_TYPE_URI.buildUpon().appendPath(Long.toString(accountId))
+            .appendPath(Integer.toString(type)).build();
+        Cursor c = context.getContentResolver().query(uri, ID_PROJECTION, null, null, null);
+        if (c != null) {
+            try {
+                c.moveToFirst();
+                Long mailboxId = c.getLong(ID_PROJECTION_COLUMN);
+                if (mailboxId != null
+                        && mailboxId != 0L
+                        && mailboxId != NO_MAILBOX) {
+                    return mailboxId;
+                }
+            } finally {
+                c.close();
+            }
+        }
+        // Fallback to querying the database directly.
+        String[] bindArguments = new String[] {Long.toString(type), Long.toString(accountId)};
+        return Utility.getFirstRowLong(context, Mailbox.CONTENT_URI,
+                ID_PROJECTION, WHERE_TYPE_AND_ACCOUNT_KEY, bindArguments, null,
+                ID_PROJECTION_COLUMN, NO_MAILBOX);
+    }
+
+    /**
+     * Convenience method that returns the mailbox found using the method above
+     */
+    public static Mailbox restoreMailboxOfType(Context context, long accountId, int type) {
+        long mailboxId = findMailboxOfType(context, accountId, type);
+        if (mailboxId != Mailbox.NO_MAILBOX) {
+            return Mailbox.restoreMailboxWithId(context, mailboxId);
+        }
+        return null;
+    }
+
+    public static int getUnreadCountByAccountAndMailboxType(Context context, long accountId,
+            int type) {
+        return Utility.getFirstRowInt(context, Mailbox.CONTENT_URI,
+                MAILBOX_SUM_OF_UNREAD_COUNT_PROJECTION,
+                ACCOUNT_AND_MAILBOX_TYPE_SELECTION,
+                new String[] { String.valueOf(accountId), String.valueOf(type) },
+                null, UNREAD_COUNT_COUNT_COLUMN, 0);
+    }
+
+    public static int getUnreadCountByMailboxType(Context context, int type) {
+        return Utility.getFirstRowInt(context, Mailbox.CONTENT_URI,
+                MAILBOX_SUM_OF_UNREAD_COUNT_PROJECTION,
+                MAILBOX_TYPE_SELECTION,
+                new String[] { String.valueOf(type) }, null, UNREAD_COUNT_COUNT_COLUMN, 0);
+    }
+
+    public static int getMessageCountByMailboxType(Context context, int type) {
+        return Utility.getFirstRowInt(context, Mailbox.CONTENT_URI,
+                MAILBOX_SUM_OF_MESSAGE_COUNT_PROJECTION,
+                MAILBOX_TYPE_SELECTION,
+                new String[] { String.valueOf(type) }, null, MESSAGE_COUNT_COUNT_COLUMN, 0);
+    }
+
+    /**
+     * Return the mailbox for a message with a given id
+     * @param context the caller's context
+     * @param messageId the id of the message
+     * @return the mailbox, or null if the mailbox doesn't exist
+     */
+    public static Mailbox getMailboxForMessageId(Context context, long messageId) {
+        long mailboxId = Message.getKeyColumnLong(context, messageId,
+                MessageColumns.MAILBOX_KEY);
+        if (mailboxId != -1) {
+            return Mailbox.restoreMailboxWithId(context, mailboxId);
+        }
+        return null;
+    }
+
+    /**
+     * @return mailbox type, or -1 if mailbox not found.
+     */
+    public static int getMailboxType(Context context, long mailboxId) {
+        Uri url = ContentUris.withAppendedId(Mailbox.CONTENT_URI, mailboxId);
+        return Utility.getFirstRowInt(context, url, MAILBOX_TYPE_PROJECTION,
+                null, null, null, MAILBOX_TYPE_TYPE_COLUMN, -1);
+    }
+
+    /**
+     * @return mailbox display name, or null if mailbox not found.
+     */
+    public static String getDisplayName(Context context, long mailboxId) {
+        Uri url = ContentUris.withAppendedId(Mailbox.CONTENT_URI, mailboxId);
+        return Utility.getFirstRowString(context, url, MAILBOX_DISPLAY_NAME_PROJECTION,
+                null, null, null, MAILBOX_DISPLAY_NAME_COLUMN);
+    }
+
+    /**
+     * @param mailboxId ID of a mailbox.  This method accepts magic mailbox IDs, such as
+     * {@link #QUERY_ALL_INBOXES}. (They're all non-refreshable.)
+     * @return true if a mailbox is refreshable.
+     */
+    public static boolean isRefreshable(Context context, long mailboxId) {
+        if (mailboxId < 0) {
+            return false; // magic mailboxes
+        }
+        switch (getMailboxType(context, mailboxId)) {
+            case -1: // not found
+            case TYPE_DRAFTS:
+            case TYPE_OUTBOX:
+                return false;
+        }
+        return true;
+    }
+
+    /**
+     * @return whether or not this mailbox supports moving messages out of it
+     */
+    public boolean canHaveMessagesMoved() {
+        switch (mType) {
+            case TYPE_INBOX:
+            case TYPE_MAIL:
+            case TYPE_TRASH:
+            case TYPE_JUNK:
+                return true;
+        }
+        return false; // TYPE_DRAFTS, TYPE_OUTBOX, TYPE_SENT, etc
+    }
+
+    /**
+     * @return whether or not this mailbox retrieves its data from the server (as opposed to just
+     *     a local mailbox that is never synced).
+     */
+    public boolean loadsFromServer(String protocol) {
+        if (HostAuth.SCHEME_EAS.equals(protocol)) {
+            return mType != Mailbox.TYPE_DRAFTS
+                    && mType != Mailbox.TYPE_OUTBOX
+                    && mType != Mailbox.TYPE_SEARCH
+                    && mType < Mailbox.TYPE_NOT_SYNCABLE;
+
+        } else if (HostAuth.SCHEME_IMAP.equals(protocol)) {
+            // TODO: actually use a sync flag when creating the mailboxes. Right now we use an
+            // approximation for IMAP.
+            return mType != Mailbox.TYPE_DRAFTS
+                    && mType != Mailbox.TYPE_OUTBOX
+                    && mType != Mailbox.TYPE_SEARCH;
+
+        } else if (HostAuth.SCHEME_POP3.equals(protocol)) {
+            return TYPE_INBOX == mType;
+        }
+
+        return false;
+    }
+
+    /**
+     * @return true if messages in a mailbox of a type can be replied/forwarded.
+     */
+    public static boolean isMailboxTypeReplyAndForwardable(int type) {
+        return (type != TYPE_TRASH) && (type != TYPE_DRAFTS);
+    }
+
+    /**
+     * Returns a set of hashes that can identify this mailbox. These can be used to
+     * determine if any of the fields have been modified.
+     */
+    public Object[] getHashes() {
+        Object[] hash = new Object[CONTENT_PROJECTION.length];
+
+        hash[CONTENT_ID_COLUMN]
+             = mId;
+        hash[CONTENT_DISPLAY_NAME_COLUMN]
+                = mDisplayName;
+        hash[CONTENT_SERVER_ID_COLUMN]
+                = mServerId;
+        hash[CONTENT_PARENT_SERVER_ID_COLUMN]
+                = mParentServerId;
+        hash[CONTENT_ACCOUNT_KEY_COLUMN]
+                = mAccountKey;
+        hash[CONTENT_TYPE_COLUMN]
+                = mType;
+        hash[CONTENT_DELIMITER_COLUMN]
+                = mDelimiter;
+        hash[CONTENT_SYNC_KEY_COLUMN]
+                = mSyncKey;
+        hash[CONTENT_SYNC_LOOKBACK_COLUMN]
+                = mSyncLookback;
+        hash[CONTENT_SYNC_INTERVAL_COLUMN]
+                = mSyncInterval;
+        hash[CONTENT_SYNC_TIME_COLUMN]
+                = mSyncTime;
+        hash[CONTENT_FLAG_VISIBLE_COLUMN]
+                = mFlagVisible;
+        hash[CONTENT_FLAGS_COLUMN]
+                = mFlags;
+        hash[CONTENT_VISIBLE_LIMIT_COLUMN]
+                = mVisibleLimit;
+        hash[CONTENT_SYNC_STATUS_COLUMN]
+                = mSyncStatus;
+        hash[CONTENT_PARENT_KEY_COLUMN]
+                = mParentKey;
+        hash[CONTENT_LAST_TOUCHED_TIME_COLUMN]
+                = mLastTouchedTime;
+        hash[CONTENT_UI_SYNC_STATUS_COLUMN]
+                = mUiSyncStatus;
+        hash[CONTENT_UI_LAST_SYNC_RESULT_COLUMN]
+                = mUiLastSyncResult;
+        hash[CONTENT_LAST_NOTIFIED_MESSAGE_KEY_COLUMN]
+                = mLastNotifiedMessageKey;
+        hash[CONTENT_LAST_NOTIFIED_MESSAGE_COUNT_COLUMN]
+                = mLastNotifiedMessageCount;
+        hash[CONTENT_TOTAL_COUNT_COLUMN]
+                = mTotalCount;
+        return hash;
+    }
+
+    // Parcelable
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    // Parcelable
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeParcelable(mBaseUri, flags);
+        dest.writeLong(mId);
+        dest.writeString(mDisplayName);
+        dest.writeString(mServerId);
+        dest.writeString(mParentServerId);
+        dest.writeLong(mParentKey);
+        dest.writeLong(mAccountKey);
+        dest.writeInt(mType);
+        dest.writeInt(mDelimiter);
+        dest.writeString(mSyncKey);
+        dest.writeInt(mSyncLookback);
+        dest.writeInt(mSyncInterval);
+        dest.writeLong(mSyncTime);
+        dest.writeInt(mFlagVisible ? 1 : 0);
+        dest.writeInt(mFlags);
+        dest.writeInt(mVisibleLimit);
+        dest.writeString(mSyncStatus);
+        dest.writeLong(mLastTouchedTime);
+        dest.writeInt(mUiSyncStatus);
+        dest.writeInt(mUiLastSyncResult);
+        dest.writeLong(mLastNotifiedMessageKey);
+        dest.writeInt(mLastNotifiedMessageCount);
+        dest.writeInt(mTotalCount);
+    }
+
+    public Mailbox(Parcel in) {
+        mBaseUri = in.readParcelable(null);
+        mId = in.readLong();
+        mDisplayName = in.readString();
+        mServerId = in.readString();
+        mParentServerId = in.readString();
+        mParentKey = in.readLong();
+        mAccountKey = in.readLong();
+        mType = in.readInt();
+        mDelimiter = in.readInt();
+        mSyncKey = in.readString();
+        mSyncLookback = in.readInt();
+        mSyncInterval = in.readInt();
+        mSyncTime = in.readLong();
+        mFlagVisible = in.readInt() == 1;
+        mFlags = in.readInt();
+        mVisibleLimit = in.readInt();
+        mSyncStatus = in.readString();
+        mLastTouchedTime = in.readLong();
+        mUiSyncStatus = in.readInt();
+        mUiLastSyncResult = in.readInt();
+        mLastNotifiedMessageKey = in.readLong();
+        mLastNotifiedMessageCount = in.readInt();
+        mTotalCount = in.readInt();
+    }
+
+    public static final Parcelable.Creator<Mailbox> CREATOR = new Parcelable.Creator<Mailbox>() {
+        @Override
+        public Mailbox createFromParcel(Parcel source) {
+            return new Mailbox(source);
+        }
+
+        @Override
+        public Mailbox[] newArray(int size) {
+            return new Mailbox[size];
+        }
+    };
+
+    public String toString() {
+        return "[Mailbox " + mId + ": " + mDisplayName + "]";
+    }
+}
diff --git a/email2/emailcommon/src/com/android/emailcommon/provider/Policy.aidl b/email2/emailcommon/src/com/android/emailcommon/provider/Policy.aidl
new file mode 100644
index 0000000..02be51b
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/provider/Policy.aidl
@@ -0,0 +1,19 @@
+/* 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.provider;
+
+parcelable Policy;
+
diff --git a/email2/emailcommon/src/com/android/emailcommon/provider/Policy.java b/email2/emailcommon/src/com/android/emailcommon/provider/Policy.java
new file mode 100755
index 0000000..d43290f
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/provider/Policy.java
@@ -0,0 +1,513 @@
+/*
+ * 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.provider;
+import android.app.admin.DevicePolicyManager;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.emailcommon.utility.TextUtilities;
+import com.android.emailcommon.utility.Utility;
+
+import java.util.ArrayList;
+
+/**
+ * The Policy class represents a set of security requirements that are associated with an Account.
+ * The requirements may be either device-specific (e.g. password) or application-specific (e.g.
+ * a limit on the sync window for the Account)
+ */
+public final class Policy extends EmailContent implements EmailContent.PolicyColumns, Parcelable {
+    public static final boolean DEBUG_POLICY = false;  // DO NOT SUBMIT WITH THIS SET TO TRUE
+    public static final String TAG = "Email/Policy";
+
+    public static final String TABLE_NAME = "Policy";
+    @SuppressWarnings("hiding")
+    public static final Uri CONTENT_URI = Uri.parse(EmailContent.CONTENT_URI + "/policy");
+
+    /* Convert days to mSec (used for password expiration) */
+    private static final long DAYS_TO_MSEC = 24 * 60 * 60 * 1000;
+    /* Small offset (2 minutes) added to policy expiration to make user testing easier. */
+    private static final long EXPIRATION_OFFSET_MSEC = 2 * 60 * 1000;
+
+    public static final int PASSWORD_MODE_NONE = 0;
+    public static final int PASSWORD_MODE_SIMPLE = 1;
+    public static final int PASSWORD_MODE_STRONG = 2;
+
+    public static final char POLICY_STRING_DELIMITER = '\1';
+
+    public int mPasswordMode;
+    public int mPasswordMinLength;
+    public int mPasswordMaxFails;
+    public int mPasswordExpirationDays;
+    public int mPasswordHistory;
+    public int mPasswordComplexChars;
+    public int mMaxScreenLockTime;
+    public boolean mRequireRemoteWipe;
+    public boolean mRequireEncryption;
+    public boolean mRequireEncryptionExternal;
+    public boolean mRequireManualSyncWhenRoaming;
+    public boolean mDontAllowCamera;
+    public boolean mDontAllowAttachments;
+    public boolean mDontAllowHtml;
+    public int mMaxAttachmentSize;
+    public int mMaxTextTruncationSize;
+    public int mMaxHtmlTruncationSize;
+    public int mMaxEmailLookback;
+    public int mMaxCalendarLookback;
+    public boolean mPasswordRecoveryEnabled;
+    public String mProtocolPoliciesEnforced;
+    public String mProtocolPoliciesUnsupported;
+
+    public static final int CONTENT_ID_COLUMN = 0;
+    public static final int CONTENT_PASSWORD_MODE_COLUMN = 1;
+    public static final int CONTENT_PASSWORD_MIN_LENGTH_COLUMN = 2;
+    public static final int CONTENT_PASSWORD_EXPIRATION_DAYS_COLUMN = 3;
+    public static final int CONTENT_PASSWORD_HISTORY_COLUMN = 4;
+    public static final int CONTENT_PASSWORD_COMPLEX_CHARS_COLUMN = 5;
+    public static final int CONTENT_PASSWORD_MAX_FAILS_COLUMN = 6;
+    public static final int CONTENT_MAX_SCREEN_LOCK_TIME_COLUMN = 7;
+    public static final int CONTENT_REQUIRE_REMOTE_WIPE_COLUMN = 8;
+    public static final int CONTENT_REQUIRE_ENCRYPTION_COLUMN = 9;
+    public static final int CONTENT_REQUIRE_ENCRYPTION_EXTERNAL_COLUMN = 10;
+    public static final int CONTENT_REQUIRE_MANUAL_SYNC_WHEN_ROAMING = 11;
+    public static final int CONTENT_DONT_ALLOW_CAMERA_COLUMN = 12;
+    public static final int CONTENT_DONT_ALLOW_ATTACHMENTS_COLUMN = 13;
+    public static final int CONTENT_DONT_ALLOW_HTML_COLUMN = 14;
+    public static final int CONTENT_MAX_ATTACHMENT_SIZE_COLUMN = 15;
+    public static final int CONTENT_MAX_TEXT_TRUNCATION_SIZE_COLUMN = 16;
+    public static final int CONTENT_MAX_HTML_TRUNCATION_SIZE_COLUMN = 17;
+    public static final int CONTENT_MAX_EMAIL_LOOKBACK_COLUMN = 18;
+    public static final int CONTENT_MAX_CALENDAR_LOOKBACK_COLUMN = 19;
+    public static final int CONTENT_PASSWORD_RECOVERY_ENABLED_COLUMN = 20;
+    public static final int CONTENT_PROTOCOL_POLICIES_ENFORCED_COLUMN = 21;
+    public static final int CONTENT_PROTOCOL_POLICIES_UNSUPPORTED_COLUMN = 22;
+
+    public static final String[] CONTENT_PROJECTION = new String[] {RECORD_ID,
+        PolicyColumns.PASSWORD_MODE, PolicyColumns.PASSWORD_MIN_LENGTH,
+        PolicyColumns.PASSWORD_EXPIRATION_DAYS, PolicyColumns.PASSWORD_HISTORY,
+        PolicyColumns.PASSWORD_COMPLEX_CHARS, PolicyColumns.PASSWORD_MAX_FAILS,
+        PolicyColumns.MAX_SCREEN_LOCK_TIME, PolicyColumns.REQUIRE_REMOTE_WIPE,
+        PolicyColumns.REQUIRE_ENCRYPTION, PolicyColumns.REQUIRE_ENCRYPTION_EXTERNAL,
+        PolicyColumns.REQUIRE_MANUAL_SYNC_WHEN_ROAMING, PolicyColumns.DONT_ALLOW_CAMERA,
+        PolicyColumns.DONT_ALLOW_ATTACHMENTS, PolicyColumns.DONT_ALLOW_HTML,
+        PolicyColumns.MAX_ATTACHMENT_SIZE, PolicyColumns.MAX_TEXT_TRUNCATION_SIZE,
+        PolicyColumns.MAX_HTML_TRUNCATION_SIZE, PolicyColumns.MAX_EMAIL_LOOKBACK,
+        PolicyColumns.MAX_CALENDAR_LOOKBACK, PolicyColumns.PASSWORD_RECOVERY_ENABLED,
+        PolicyColumns.PROTOCOL_POLICIES_ENFORCED, PolicyColumns.PROTOCOL_POLICIES_UNSUPPORTED
+    };
+
+    public static final Policy NO_POLICY = new Policy();
+
+    private static final String[] ATTACHMENT_RESET_PROJECTION =
+        new String[] {EmailContent.RECORD_ID, AttachmentColumns.SIZE, AttachmentColumns.FLAGS};
+    private static final int ATTACHMENT_RESET_PROJECTION_ID = 0;
+    private static final int ATTACHMENT_RESET_PROJECTION_SIZE = 1;
+    private static final int ATTACHMENT_RESET_PROJECTION_FLAGS = 2;
+
+    public Policy() {
+        mBaseUri = CONTENT_URI;
+        // By default, the password mode is "none"
+        mPasswordMode = PASSWORD_MODE_NONE;
+        // All server policies require the ability to wipe the device
+        mRequireRemoteWipe = true;
+    }
+
+    public static Policy restorePolicyWithId(Context context, long id) {
+        return EmailContent.restoreContentWithId(context, Policy.class, Policy.CONTENT_URI,
+                Policy.CONTENT_PROJECTION, id);
+    }
+
+    public static long getAccountIdWithPolicyKey(Context context, long id) {
+        return Utility.getFirstRowLong(context, Account.CONTENT_URI, Account.ID_PROJECTION,
+                AccountColumns.POLICY_KEY + "=?", new String[] {Long.toString(id)}, null,
+                Account.ID_PROJECTION_COLUMN, Account.NO_ACCOUNT);
+    }
+
+    public static ArrayList<String> addPolicyStringToList(String policyString,
+            ArrayList<String> policyList) {
+        if (policyString != null) {
+            int start = 0;
+            int len = policyString.length();
+            while(start < len) {
+                int end = policyString.indexOf(POLICY_STRING_DELIMITER, start);
+                if (end > start) {
+                    policyList.add(policyString.substring(start, end));
+                    start = end + 1;
+                } else {
+                    break;
+                }
+            }
+        }
+        return policyList;
+    }
+
+    // We override this method to insure that we never write invalid policy data to the provider
+    @Override
+    public Uri save(Context context) {
+        normalize();
+        return super.save(context);
+    }
+
+    /**
+     * Review all attachment records for this account, and reset the "don't allow download" flag
+     * as required by the account's new security policies
+     * @param context the caller's context
+     * @param account the account whose attachments need to be reviewed
+     * @param policy the new policy for this account
+     */
+    public static void setAttachmentFlagsForNewPolicy(Context context, Account account,
+            Policy policy) {
+        // A nasty bit of work; start with all attachments for a given account
+        ContentResolver resolver = context.getContentResolver();
+        Cursor c = resolver.query(Attachment.CONTENT_URI, ATTACHMENT_RESET_PROJECTION,
+                AttachmentColumns.ACCOUNT_KEY + "=?", new String[] {Long.toString(account.mId)},
+                null);
+        ContentValues cv = new ContentValues();
+        try {
+            // Get maximum allowed size (0 if we don't allow attachments at all)
+            int policyMax = policy.mDontAllowAttachments ? 0 : (policy.mMaxAttachmentSize > 0) ?
+                    policy.mMaxAttachmentSize : Integer.MAX_VALUE;
+            while (c.moveToNext()) {
+                int flags = c.getInt(ATTACHMENT_RESET_PROJECTION_FLAGS);
+                int size = c.getInt(ATTACHMENT_RESET_PROJECTION_SIZE);
+                boolean wasRestricted = (flags & Attachment.FLAG_POLICY_DISALLOWS_DOWNLOAD) != 0;
+                boolean isRestricted = size > policyMax;
+                if (isRestricted != wasRestricted) {
+                    if (isRestricted) {
+                        flags |= Attachment.FLAG_POLICY_DISALLOWS_DOWNLOAD;
+                    } else {
+                        flags &= ~Attachment.FLAG_POLICY_DISALLOWS_DOWNLOAD;
+                    }
+                    long id = c.getLong(ATTACHMENT_RESET_PROJECTION_ID);
+                    cv.put(AttachmentColumns.FLAGS, flags);
+                    resolver.update(ContentUris.withAppendedId(Attachment.CONTENT_URI, id),
+                            cv, null, null);
+                }
+            }
+        } finally {
+            c.close();
+        }
+    }
+
+    /**
+     * Normalize the Policy.  If the password mode is "none", zero out all password-related fields;
+     * zero out complex characters for simple passwords.
+     */
+    public void normalize() {
+        if (mPasswordMode == PASSWORD_MODE_NONE) {
+            mPasswordMaxFails = 0;
+            mMaxScreenLockTime = 0;
+            mPasswordMinLength = 0;
+            mPasswordComplexChars = 0;
+            mPasswordHistory = 0;
+            mPasswordExpirationDays = 0;
+        } else {
+            if ((mPasswordMode != PASSWORD_MODE_SIMPLE) &&
+                    (mPasswordMode != PASSWORD_MODE_STRONG)) {
+                throw new IllegalArgumentException("password mode");
+            }
+            // If we're only requiring a simple password, set complex chars to zero; note
+            // that EAS can erroneously send non-zero values in this case
+            if (mPasswordMode == PASSWORD_MODE_SIMPLE) {
+                mPasswordComplexChars = 0;
+            }
+        }
+    }
+
+    @Override
+    public boolean equals(Object other) {
+        if (!(other instanceof Policy)) return false;
+        Policy otherPolicy = (Policy)other;
+        // Policies here are enforced by the DPM
+        if (mRequireEncryption != otherPolicy.mRequireEncryption) return false;
+        if (mRequireEncryptionExternal != otherPolicy.mRequireEncryptionExternal) return false;
+        if (mRequireRemoteWipe != otherPolicy.mRequireRemoteWipe) return false;
+        if (mMaxScreenLockTime != otherPolicy.mMaxScreenLockTime) return false;
+        if (mPasswordComplexChars != otherPolicy.mPasswordComplexChars) return false;
+        if (mPasswordExpirationDays != otherPolicy.mPasswordExpirationDays) return false;
+        if (mPasswordHistory != otherPolicy.mPasswordHistory) return false;
+        if (mPasswordMaxFails != otherPolicy.mPasswordMaxFails) return false;
+        if (mPasswordMinLength != otherPolicy.mPasswordMinLength) return false;
+        if (mPasswordMode != otherPolicy.mPasswordMode) return false;
+        if (mDontAllowCamera != otherPolicy.mDontAllowCamera) return false;
+
+        // Policies here are enforced by the Exchange sync manager
+        // They should eventually be removed from Policy and replaced with some opaque data
+        if (mRequireManualSyncWhenRoaming != otherPolicy.mRequireManualSyncWhenRoaming) {
+            return false;
+        }
+        if (mDontAllowAttachments != otherPolicy.mDontAllowAttachments) return false;
+        if (mDontAllowHtml != otherPolicy.mDontAllowHtml) return false;
+        if (mMaxAttachmentSize != otherPolicy.mMaxAttachmentSize) return false;
+        if (mMaxTextTruncationSize != otherPolicy.mMaxTextTruncationSize) return false;
+        if (mMaxHtmlTruncationSize != otherPolicy.mMaxHtmlTruncationSize) return false;
+        if (mMaxEmailLookback != otherPolicy.mMaxEmailLookback) return false;
+        if (mMaxCalendarLookback != otherPolicy.mMaxCalendarLookback) return false;
+        if (mPasswordRecoveryEnabled != otherPolicy.mPasswordRecoveryEnabled) return false;
+
+        if (!TextUtilities.stringOrNullEquals(mProtocolPoliciesEnforced,
+                otherPolicy.mProtocolPoliciesEnforced)) {
+            return false;
+        }
+        if (!TextUtilities.stringOrNullEquals(mProtocolPoliciesUnsupported,
+                otherPolicy.mProtocolPoliciesUnsupported)) {
+            return false;
+        }
+        return true;
+    }
+
+    @Override
+    public int hashCode() {
+        int code = mRequireEncryption ? 1 : 0;
+        code += (mRequireEncryptionExternal ? 1 : 0) << 1;
+        code += (mRequireRemoteWipe ? 1 : 0) << 2;
+        code += (mMaxScreenLockTime << 3);
+        code += (mPasswordComplexChars << 6);
+        code += (mPasswordExpirationDays << 12);
+        code += (mPasswordHistory << 15);
+        code += (mPasswordMaxFails << 18);
+        code += (mPasswordMinLength << 22);
+        code += (mPasswordMode << 26);
+        // Don't need to include the other fields
+        return code;
+    }
+
+    @Override
+    public void restore(Cursor cursor) {
+        mBaseUri = CONTENT_URI;
+        mId = cursor.getLong(CONTENT_ID_COLUMN);
+        mPasswordMode = cursor.getInt(CONTENT_PASSWORD_MODE_COLUMN);
+        mPasswordMinLength = cursor.getInt(CONTENT_PASSWORD_MIN_LENGTH_COLUMN);
+        mPasswordMaxFails = cursor.getInt(CONTENT_PASSWORD_MAX_FAILS_COLUMN);
+        mPasswordHistory = cursor.getInt(CONTENT_PASSWORD_HISTORY_COLUMN);
+        mPasswordExpirationDays = cursor.getInt(CONTENT_PASSWORD_EXPIRATION_DAYS_COLUMN);
+        mPasswordComplexChars = cursor.getInt(CONTENT_PASSWORD_COMPLEX_CHARS_COLUMN);
+        mMaxScreenLockTime = cursor.getInt(CONTENT_MAX_SCREEN_LOCK_TIME_COLUMN);
+        mRequireRemoteWipe = cursor.getInt(CONTENT_REQUIRE_REMOTE_WIPE_COLUMN) == 1;
+        mRequireEncryption = cursor.getInt(CONTENT_REQUIRE_ENCRYPTION_COLUMN) == 1;
+        mRequireEncryptionExternal =
+            cursor.getInt(CONTENT_REQUIRE_ENCRYPTION_EXTERNAL_COLUMN) == 1;
+        mRequireManualSyncWhenRoaming =
+            cursor.getInt(CONTENT_REQUIRE_MANUAL_SYNC_WHEN_ROAMING) == 1;
+        mDontAllowCamera = cursor.getInt(CONTENT_DONT_ALLOW_CAMERA_COLUMN) == 1;
+        mDontAllowAttachments = cursor.getInt(CONTENT_DONT_ALLOW_ATTACHMENTS_COLUMN) == 1;
+        mDontAllowHtml = cursor.getInt(CONTENT_DONT_ALLOW_HTML_COLUMN) == 1;
+        mMaxAttachmentSize = cursor.getInt(CONTENT_MAX_ATTACHMENT_SIZE_COLUMN);
+        mMaxTextTruncationSize = cursor.getInt(CONTENT_MAX_TEXT_TRUNCATION_SIZE_COLUMN);
+        mMaxHtmlTruncationSize = cursor.getInt(CONTENT_MAX_HTML_TRUNCATION_SIZE_COLUMN);
+        mMaxEmailLookback = cursor.getInt(CONTENT_MAX_EMAIL_LOOKBACK_COLUMN);
+        mMaxCalendarLookback = cursor.getInt(CONTENT_MAX_CALENDAR_LOOKBACK_COLUMN);
+        mPasswordRecoveryEnabled = cursor.getInt(CONTENT_PASSWORD_RECOVERY_ENABLED_COLUMN) == 1;
+        mProtocolPoliciesEnforced = cursor.getString(CONTENT_PROTOCOL_POLICIES_ENFORCED_COLUMN);
+        mProtocolPoliciesUnsupported =
+            cursor.getString(CONTENT_PROTOCOL_POLICIES_UNSUPPORTED_COLUMN);
+    }
+
+    @Override
+    public ContentValues toContentValues() {
+        ContentValues values = new ContentValues();
+        values.put(PolicyColumns.PASSWORD_MODE, mPasswordMode);
+        values.put(PolicyColumns.PASSWORD_MIN_LENGTH, mPasswordMinLength);
+        values.put(PolicyColumns.PASSWORD_MAX_FAILS, mPasswordMaxFails);
+        values.put(PolicyColumns.PASSWORD_HISTORY, mPasswordHistory);
+        values.put(PolicyColumns.PASSWORD_EXPIRATION_DAYS, mPasswordExpirationDays);
+        values.put(PolicyColumns.PASSWORD_COMPLEX_CHARS, mPasswordComplexChars);
+        values.put(PolicyColumns.MAX_SCREEN_LOCK_TIME, mMaxScreenLockTime);
+        values.put(PolicyColumns.REQUIRE_REMOTE_WIPE, mRequireRemoteWipe);
+        values.put(PolicyColumns.REQUIRE_ENCRYPTION, mRequireEncryption);
+        values.put(PolicyColumns.REQUIRE_ENCRYPTION_EXTERNAL, mRequireEncryptionExternal);
+        values.put(PolicyColumns.REQUIRE_MANUAL_SYNC_WHEN_ROAMING, mRequireManualSyncWhenRoaming);
+        values.put(PolicyColumns.DONT_ALLOW_CAMERA, mDontAllowCamera);
+        values.put(PolicyColumns.DONT_ALLOW_ATTACHMENTS, mDontAllowAttachments);
+        values.put(PolicyColumns.DONT_ALLOW_HTML, mDontAllowHtml);
+        values.put(PolicyColumns.MAX_ATTACHMENT_SIZE, mMaxAttachmentSize);
+        values.put(PolicyColumns.MAX_TEXT_TRUNCATION_SIZE, mMaxTextTruncationSize);
+        values.put(PolicyColumns.MAX_HTML_TRUNCATION_SIZE, mMaxHtmlTruncationSize);
+        values.put(PolicyColumns.MAX_EMAIL_LOOKBACK, mMaxEmailLookback);
+        values.put(PolicyColumns.MAX_CALENDAR_LOOKBACK, mMaxCalendarLookback);
+        values.put(PolicyColumns.PASSWORD_RECOVERY_ENABLED, mPasswordRecoveryEnabled);
+        values.put(PolicyColumns.PROTOCOL_POLICIES_ENFORCED, mProtocolPoliciesEnforced);
+        values.put(PolicyColumns.PROTOCOL_POLICIES_UNSUPPORTED, mProtocolPoliciesUnsupported);
+        return values;
+    }
+
+    /**
+     * Helper to map our internal encoding to DevicePolicyManager password modes.
+     */
+    public int getDPManagerPasswordQuality() {
+        switch (mPasswordMode) {
+            case PASSWORD_MODE_SIMPLE:
+                return DevicePolicyManager.PASSWORD_QUALITY_NUMERIC;
+            case PASSWORD_MODE_STRONG:
+                if (mPasswordComplexChars == 0) {
+                    return DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC;
+                } else {
+                    return DevicePolicyManager.PASSWORD_QUALITY_COMPLEX;
+                }
+            default:
+                return DevicePolicyManager .PASSWORD_QUALITY_UNSPECIFIED;
+        }
+    }
+
+    /**
+     * Helper to map expiration times to the millisecond values used by DevicePolicyManager.
+     */
+    public long getDPManagerPasswordExpirationTimeout() {
+        long result = mPasswordExpirationDays * DAYS_TO_MSEC;
+        // Add a small offset to the password expiration.  This makes it easier to test
+        // by changing (for example) 1 day to 1 day + 5 minutes.  If you set an expiration
+        // that is within the warning period, you should get a warning fairly quickly.
+        if (result > 0) {
+            result += EXPIRATION_OFFSET_MSEC;
+        }
+        return result;
+    }
+
+    private void appendPolicy(StringBuilder sb, String code, int value) {
+        sb.append(code);
+        sb.append(":");
+        sb.append(value);
+        sb.append(" ");
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder("[");
+        if (equals(NO_POLICY)) {
+            sb.append("No policies]");
+        } else {
+            if (mPasswordMode == PASSWORD_MODE_NONE) {
+                sb.append("Pwd none ");
+            } else {
+                appendPolicy(sb, "Pwd strong", mPasswordMode == PASSWORD_MODE_STRONG ? 1 : 0);
+                appendPolicy(sb, "len", mPasswordMinLength);
+                appendPolicy(sb, "cmpx", mPasswordComplexChars);
+                appendPolicy(sb, "expy", mPasswordExpirationDays);
+                appendPolicy(sb, "hist", mPasswordHistory);
+                appendPolicy(sb, "fail", mPasswordMaxFails);
+                appendPolicy(sb, "idle", mMaxScreenLockTime);
+            }
+            if (mRequireEncryption) {
+                sb.append("encrypt ");
+            }
+            if (mRequireEncryptionExternal) {
+                sb.append("encryptsd ");
+            }
+            if (mDontAllowCamera) {
+                sb.append("nocamera ");
+            }
+            if (mDontAllowAttachments) {
+                sb.append("noatts ");
+            }
+            if (mRequireManualSyncWhenRoaming) {
+                sb.append("nopushroam ");
+            }
+            if (mMaxAttachmentSize > 0) {
+                appendPolicy(sb, "attmax", mMaxAttachmentSize);
+            }
+            sb.append("]");
+        }
+        return sb.toString();
+    }
+
+    /**
+     * Supports Parcelable
+     */
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    /**
+     * Supports Parcelable
+     */
+    public static final Parcelable.Creator<Policy> CREATOR = new Parcelable.Creator<Policy>() {
+        public Policy createFromParcel(Parcel in) {
+            return new Policy(in);
+        }
+
+        public Policy[] newArray(int size) {
+            return new Policy[size];
+        }
+    };
+
+    /**
+     * Supports Parcelable
+     */
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        // mBaseUri is not parceled
+        dest.writeLong(mId);
+        dest.writeInt(mPasswordMode);
+        dest.writeInt(mPasswordMinLength);
+        dest.writeInt(mPasswordMaxFails);
+        dest.writeInt(mPasswordHistory);
+        dest.writeInt(mPasswordExpirationDays);
+        dest.writeInt(mPasswordComplexChars);
+        dest.writeInt(mMaxScreenLockTime);
+        dest.writeInt(mRequireRemoteWipe ? 1 : 0);
+        dest.writeInt(mRequireEncryption ? 1 : 0);
+        dest.writeInt(mRequireEncryptionExternal ? 1 : 0);
+        dest.writeInt(mRequireManualSyncWhenRoaming ? 1 : 0);
+        dest.writeInt(mDontAllowCamera ? 1 : 0);
+        dest.writeInt(mDontAllowAttachments ? 1 : 0);
+        dest.writeInt(mDontAllowHtml ? 1 : 0);
+        dest.writeInt(mMaxAttachmentSize);
+        dest.writeInt(mMaxTextTruncationSize);
+        dest.writeInt(mMaxHtmlTruncationSize);
+        dest.writeInt(mMaxEmailLookback);
+        dest.writeInt(mMaxCalendarLookback);
+        dest.writeInt(mPasswordRecoveryEnabled ? 1 : 0);
+        dest.writeString(mProtocolPoliciesEnforced);
+        dest.writeString(mProtocolPoliciesUnsupported);
+    }
+
+    /**
+     * Supports Parcelable
+     */
+    public Policy(Parcel in) {
+        mBaseUri = CONTENT_URI;
+        mId = in.readLong();
+        mPasswordMode = in.readInt();
+        mPasswordMinLength = in.readInt();
+        mPasswordMaxFails = in.readInt();
+        mPasswordHistory = in.readInt();
+        mPasswordExpirationDays = in.readInt();
+        mPasswordComplexChars = in.readInt();
+        mMaxScreenLockTime = in.readInt();
+        mRequireRemoteWipe = in.readInt() == 1;
+        mRequireEncryption = in.readInt() == 1;
+        mRequireEncryptionExternal = in.readInt() == 1;
+        mRequireManualSyncWhenRoaming = in.readInt() == 1;
+        mDontAllowCamera = in.readInt() == 1;
+        mDontAllowAttachments = in.readInt() == 1;
+        mDontAllowHtml = in.readInt() == 1;
+        mMaxAttachmentSize = in.readInt();
+        mMaxTextTruncationSize = in.readInt();
+        mMaxHtmlTruncationSize = in.readInt();
+        mMaxEmailLookback = in.readInt();
+        mMaxCalendarLookback = in.readInt();
+        mPasswordRecoveryEnabled = in.readInt() == 1;
+        mProtocolPoliciesEnforced = in.readString();
+        mProtocolPoliciesUnsupported = in.readString();
+    }
+}
\ No newline at end of file
diff --git a/email2/emailcommon/src/com/android/emailcommon/provider/ProviderUnavailableException.java b/email2/emailcommon/src/com/android/emailcommon/provider/ProviderUnavailableException.java
new file mode 100644
index 0000000..8f33d34
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/provider/ProviderUnavailableException.java
@@ -0,0 +1,21 @@
+/*
+ * 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.provider;
+
+public class ProviderUnavailableException extends RuntimeException {
+    private static final long serialVersionUID = 1L;
+}
\ No newline at end of file
diff --git a/email2/emailcommon/src/com/android/emailcommon/provider/QuickResponse.java b/email2/emailcommon/src/com/android/emailcommon/provider/QuickResponse.java
new file mode 100644
index 0000000..e88e01d
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/provider/QuickResponse.java
@@ -0,0 +1,217 @@
+/*
+ * 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.provider;
+
+import com.android.emailcommon.provider.EmailContent;
+import com.android.emailcommon.provider.EmailContent.QuickResponseColumns;
+import com.google.common.base.Objects;
+
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * A user-modifiable message that may be quickly inserted into the body while user is composing
+ * a message. Tied to a specific account.
+ */
+public final class QuickResponse extends EmailContent
+        implements QuickResponseColumns, Parcelable {
+    public static final String TABLE_NAME = "QuickResponse";
+    @SuppressWarnings("hiding")
+    public static final Uri CONTENT_URI = Uri.parse(EmailContent.CONTENT_URI
+            + "/quickresponse");
+    public static final Uri ACCOUNT_ID_URI = Uri.parse(
+            EmailContent.CONTENT_URI + "/quickresponse/account");
+
+    private String mText;
+    private long mAccountKey;
+
+    private static final int CONTENT_ID_COLUMN = 0;
+    private static final int CONTENT_QUICK_RESPONSE_COLUMN = 1;
+    private static final int CONTENT_ACCOUNT_KEY_COLUMN = 2;
+    public static final String[] CONTENT_PROJECTION = new String[] {
+            RECORD_ID,
+            QuickResponseColumns.TEXT,
+            QuickResponseColumns.ACCOUNT_KEY
+    };
+
+    /**
+     * Creates an empty QuickResponse. Restore should be called after.
+     */
+    private QuickResponse() {
+        // empty
+    }
+
+    /**
+     * Constructor used by CREATOR for parceling.
+     */
+    private QuickResponse(Parcel in) {
+        mBaseUri = CONTENT_URI;
+        mId = in.readLong();
+        mText = in.readString();
+        mAccountKey = in.readLong();
+    }
+
+    /**
+     * Creates QuickResponse associated with a particular account using the given string.
+     */
+    public QuickResponse(long accountKey, String quickResponse) {
+        mBaseUri = CONTENT_URI;
+        mAccountKey = accountKey;
+        mText = quickResponse;
+    }
+
+    /**
+     * @see com.android.emailcommon.provider.EmailContent#restore(android.database.Cursor)
+     */
+    @Override
+    public void restore(Cursor cursor) {
+        mBaseUri = CONTENT_URI;
+        mId = cursor.getLong(CONTENT_ID_COLUMN);
+        mText = cursor.getString(CONTENT_QUICK_RESPONSE_COLUMN);
+        mAccountKey = cursor.getLong(CONTENT_ACCOUNT_KEY_COLUMN);
+    }
+
+    /**
+     * @see com.android.emailcommon.provider.EmailContent#toContentValues()
+     */
+    @Override
+    public ContentValues toContentValues() {
+        ContentValues values = new ContentValues();
+
+        values.put(QuickResponseColumns.TEXT, mText);
+        values.put(QuickResponseColumns.ACCOUNT_KEY, mAccountKey);
+
+        return values;
+    }
+
+    @Override
+    public String toString() {
+        return mText;
+    }
+
+    /**
+     * Given an array of QuickResponses, returns the an array of the String values
+     * corresponding to each QuickResponse.
+     */
+    public static String[] getQuickResponseStrings(QuickResponse[] quickResponses) {
+        int count = quickResponses.length;
+        String[] quickResponseStrings = new String[count];
+        for (int i = 0; i < count; i++) {
+            quickResponseStrings[i] = quickResponses[i].toString();
+        }
+
+        return quickResponseStrings;
+    }
+
+    /**
+     * @param context
+     * @param accountId
+     * @return array of QuickResponses for the account with id accountId
+     */
+    public static QuickResponse[] restoreQuickResponsesWithAccountId(Context context,
+            long accountId) {
+        Uri uri = ContentUris.withAppendedId(ACCOUNT_ID_URI, accountId);
+        Cursor c = context.getContentResolver().query(uri, CONTENT_PROJECTION,
+                null, null, null);
+
+        try {
+            int count = c.getCount();
+            QuickResponse[] quickResponses = new QuickResponse[count];
+            for (int i = 0; i < count; ++i) {
+                c.moveToNext();
+                QuickResponse quickResponse = new QuickResponse();
+                quickResponse.restore(c);
+                quickResponses[i] = quickResponse;
+            }
+            return quickResponses;
+        } finally {
+            c.close();
+        }
+    }
+
+    /**
+     * Returns the base URI for this QuickResponse
+     */
+    public Uri getBaseUri() {
+        return mBaseUri;
+    }
+
+    /**
+     * Returns the unique id for this QuickResponse
+     */
+    public long getId() {
+        return mId;
+    }
+
+    @Override
+    public boolean equals(Object objectThat) {
+        if (this == objectThat) return true;
+        if (!(objectThat instanceof QuickResponse)) return false;
+
+        QuickResponse that = (QuickResponse) objectThat;
+        return
+            mText.equals(that.mText) &&
+            mId == that.mId &&
+            mAccountKey == that.mAccountKey;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hashCode(mId, mText, mAccountKey);
+    }
+
+    /**
+     * Implements Parcelable. Not used.
+     */
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    /**
+     * Implements Parcelable.
+     */
+    public void writeToParcel(Parcel dest, int flags) {
+        // mBaseUri is not parceled
+        dest.writeLong(mId);
+        dest.writeString(mText);
+        dest.writeLong(mAccountKey);
+    }
+
+    /**
+     * Implements Parcelable
+     */
+    public static final Parcelable.Creator<QuickResponse> CREATOR
+            = new Parcelable.Creator<QuickResponse>() {
+        @Override
+        public QuickResponse createFromParcel(Parcel in) {
+            return new QuickResponse(in);
+        }
+
+        @Override
+        public QuickResponse[] newArray(int size) {
+            return new QuickResponse[size];
+        }
+    };
+
+}
\ No newline at end of file
diff --git a/email2/emailcommon/src/com/android/emailcommon/service/AccountServiceProxy.java b/email2/emailcommon/src/com/android/emailcommon/service/AccountServiceProxy.java
new file mode 100644
index 0000000..f4eb930
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/service/AccountServiceProxy.java
@@ -0,0 +1,130 @@
+/*
+ * 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.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.RemoteException;
+
+public class AccountServiceProxy extends ServiceProxy implements IAccountService {
+
+    public static final String ACCOUNT_INTENT = "com.android.email.ACCOUNT_INTENT";
+    public static final int DEFAULT_ACCOUNT_COLOR = 0xFF0000FF;
+
+    private IAccountService mService = null;
+    private Object mReturn;
+
+    public AccountServiceProxy(Context _context) {
+        super(_context, new Intent(ACCOUNT_INTENT));
+    }
+
+    @Override
+    public void onConnected(IBinder binder) {
+        mService = IAccountService.Stub.asInterface(binder);
+    }
+
+    @Override
+    public IBinder asBinder() {
+        return null;
+    }
+
+    @Override
+    public void notifyLoginFailed(final long accountId) {
+        setTask(new ProxyTask() {
+            @Override
+            public void run() throws RemoteException {
+                mService.notifyLoginFailed(accountId);
+            }
+        }, "notifyLoginFailed");
+    }
+
+    @Override
+    public void notifyLoginSucceeded(final long accountId) {
+        setTask(new ProxyTask() {
+            @Override
+            public void run() throws RemoteException {
+                mService.notifyLoginSucceeded(accountId);
+            }
+        }, "notifyLoginSucceeded");
+    }
+
+    // The following call is synchronous, and should not be made from the UI thread
+    @Override
+    public void reconcileAccounts(final String protocol, final String accountManagerType) {
+        setTask(new ProxyTask() {
+            @Override
+            public void run() throws RemoteException {
+                mService.reconcileAccounts(protocol, accountManagerType);
+            }
+        }, "reconcileAccounts");
+        waitForCompletion();
+    }
+
+    // The following call is synchronous, and should not be made from the UI thread
+    @Override
+    public int getAccountColor(final long accountId) {
+        setTask(new ProxyTask() {
+            @Override
+            public void run() throws RemoteException{
+                mReturn = mService.getAccountColor(accountId);
+            }
+        }, "getAccountColor");
+        waitForCompletion();
+        if (mReturn == null) {
+            return DEFAULT_ACCOUNT_COLOR;
+        } else {
+            return (Integer)mReturn;
+        }
+    }
+
+    // The following call is synchronous, and should not be made from the UI thread
+    @Override
+    public Bundle getConfigurationData(final String accountType) {
+        setTask(new ProxyTask() {
+            @Override
+            public void run() throws RemoteException{
+                mReturn = mService.getConfigurationData(accountType);
+            }
+        }, "getConfigurationData");
+        waitForCompletion();
+        if (mReturn == null) {
+            return null;
+        } else {
+            return (Bundle)mReturn;
+        }
+    }
+
+    // The following call is synchronous, and should not be made from the UI thread
+    @Override
+    public String getDeviceId() {
+        setTask(new ProxyTask() {
+            @Override
+            public void run() throws RemoteException{
+                mReturn = mService.getDeviceId();
+            }
+        }, "getDeviceId");
+        waitForCompletion();
+        if (mReturn == null) {
+            return null;
+        } else {
+            return (String)mReturn;
+        }
+    }
+}
+
diff --git a/email2/emailcommon/src/com/android/emailcommon/service/EmailServiceConstants.java b/email2/emailcommon/src/com/android/emailcommon/service/EmailServiceConstants.java
new file mode 100644
index 0000000..8e8d552
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/service/EmailServiceConstants.java
@@ -0,0 +1,25 @@
+/*
+ * 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.service;
+
+public class EmailServiceConstants {
+    /** "Not responded yet", used ONLY for UI. */
+    public static final int MEETING_REQUEST_NOT_RESPONDED = 0;
+    public static final int MEETING_REQUEST_ACCEPTED = 1;
+    public static final int MEETING_REQUEST_TENTATIVE = 2;
+    public static final int MEETING_REQUEST_DECLINED = 3;
+}
diff --git a/email2/emailcommon/src/com/android/emailcommon/service/EmailServiceProxy.java b/email2/emailcommon/src/com/android/emailcommon/service/EmailServiceProxy.java
new file mode 100644
index 0000000..b2b50b7
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/service/EmailServiceProxy.java
@@ -0,0 +1,468 @@
+/*
+ * Copyright (C) 2009 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.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.Log;
+
+import com.android.emailcommon.Api;
+import com.android.emailcommon.Device;
+import com.android.emailcommon.mail.MessagingException;
+import com.android.emailcommon.provider.HostAuth;
+import com.android.emailcommon.provider.Policy;
+
+import java.io.IOException;
+
+/**
+ * The EmailServiceProxy class provides a simple interface for the UI to call into the various
+ * EmailService classes (e.g. ExchangeService for EAS).  It wraps the service connect/disconnect
+ * process so that the caller need not be concerned with it.
+ *
+ * Use the class like this:
+ *   new EmailServiceProxy(context, class).loadAttachment(attachmentId, callback)
+ *
+ * Methods without a return value return immediately (i.e. are asynchronous); methods with a
+ * return value wait for a result from the Service (i.e. they should not be called from the UI
+ * thread) with a default timeout of 30 seconds (settable)
+ *
+ * An EmailServiceProxy object cannot be reused (trying to do so generates a RemoteException)
+ */
+
+public class EmailServiceProxy extends ServiceProxy implements IEmailService {
+    private static final String TAG = "EmailServiceProxy";
+
+    // Private intent that will be used to connect to an independent Exchange service
+    public static final String EXCHANGE_INTENT = "com.android.email.EXCHANGE_INTENT";
+    public static final String IMAP_INTENT = "com.android.email.IMAP_INTENT";
+
+    public static final String AUTO_DISCOVER_BUNDLE_ERROR_CODE = "autodiscover_error_code";
+    public static final String AUTO_DISCOVER_BUNDLE_HOST_AUTH = "autodiscover_host_auth";
+
+    public static final String VALIDATE_BUNDLE_RESULT_CODE = "validate_result_code";
+    public static final String VALIDATE_BUNDLE_POLICY_SET = "validate_policy_set";
+    public static final String VALIDATE_BUNDLE_ERROR_MESSAGE = "validate_error_message";
+    public static final String VALIDATE_BUNDLE_UNSUPPORTED_POLICIES =
+        "validate_unsupported_policies";
+
+    private final IEmailServiceCallback mCallback;
+    private Object mReturn = null;
+    private IEmailService mService;
+    private final boolean isRemote;
+
+    // Standard debugging
+    public static final int DEBUG_BIT = 1;
+    // Verbose (parser) logging
+    public static final int DEBUG_VERBOSE_BIT = 2;
+    // File (SD card) logging
+    public static final int DEBUG_FILE_BIT = 4;
+    // Enable strict mode
+    public static final int DEBUG_ENABLE_STRICT_MODE = 8;
+
+    // The first two constructors are used with local services that can be referenced by class
+    public EmailServiceProxy(Context _context, Class<?> _class) {
+        this(_context, _class, null);
+    }
+
+    public EmailServiceProxy(Context _context, Class<?> _class, IEmailServiceCallback _callback) {
+        super(_context, new Intent(_context, _class));
+        mCallback = _callback;
+        isRemote = false;
+    }
+
+    // The following two constructors are used with remote services that must be referenced by
+    // a known action or by a prebuilt intent
+    public EmailServiceProxy(Context _context, Intent _intent, IEmailServiceCallback _callback) {
+        super(_context, _intent);
+        try {
+            Device.getDeviceId(_context);
+        } catch (IOException e) {
+        }
+        mCallback = _callback;
+        isRemote = true;
+    }
+
+    public EmailServiceProxy(Context _context, String _action, IEmailServiceCallback _callback) {
+        super(_context, new Intent(_action));
+        try {
+            Device.getDeviceId(_context);
+        } catch (IOException e) {
+        }
+        mCallback = _callback;
+        isRemote = true;
+    }
+
+    @Override
+    public void onConnected(IBinder binder) {
+        mService = IEmailService.Stub.asInterface(binder);
+    }
+
+    public boolean isRemote() {
+        return isRemote;
+    }
+
+    @Override
+    public int getApiLevel() {
+        return Api.LEVEL;
+    }
+
+    /**
+     * Request an attachment to be loaded; the service MUST give higher priority to
+     * non-background loading.  The service MUST use the loadAttachmentStatus callback when
+     * loading has started and stopped and SHOULD send callbacks with progress information if
+     * possible.
+     *
+     * @param attachmentId the id of the attachment record
+     * @param background whether or not this request corresponds to a background action (i.e.
+     * prefetch) vs a foreground action (user request)
+     */
+    @Override
+    public void loadAttachment(final long attachmentId, final boolean background)
+            throws RemoteException {
+        setTask(new ProxyTask() {
+            @Override
+            public void run() throws RemoteException {
+                try {
+                    if (mCallback != null) mService.setCallback(mCallback);
+                    mService.loadAttachment(attachmentId, background);
+                } catch (RemoteException e) {
+                    try {
+                        // Try to send a callback (if set)
+                        if (mCallback != null) {
+                            mCallback.loadAttachmentStatus(-1, attachmentId,
+                                    EmailServiceStatus.REMOTE_EXCEPTION, 0);
+                        }
+                    } catch (RemoteException e1) {
+                    }
+                }
+            }
+        }, "loadAttachment");
+    }
+
+    /**
+     * Request the sync of a mailbox; the service MUST send the syncMailboxStatus callback
+     * indicating "starting" and "finished" (or error), regardless of whether the mailbox is
+     * actually syncable.
+     *
+     * @param mailboxId the id of the mailbox record
+     * @param userRequest whether or not the user specifically asked for the sync
+     */
+    @Override
+    public void startSync(final long mailboxId, final boolean userRequest) throws RemoteException {
+        setTask(new ProxyTask() {
+            @Override
+            public void run() throws RemoteException {
+                if (mCallback != null) mService.setCallback(mCallback);
+                mService.startSync(mailboxId, userRequest);
+            }
+        }, "startSync");
+    }
+
+    /**
+     * Request the immediate termination of a mailbox sync. Although the service is not required to
+     * acknowledge this request, it MUST send a "finished" (or error) syncMailboxStatus callback if
+     * the sync was started via the startSync service call.
+     *
+     * @param mailboxId the id of the mailbox record
+     * @param userRequest whether or not the user specifically asked for the sync
+     */
+    @Override
+    public void stopSync(final long mailboxId) throws RemoteException {
+        setTask(new ProxyTask() {
+            @Override
+            public void run() throws RemoteException {
+                if (mCallback != null) mService.setCallback(mCallback);
+                mService.stopSync(mailboxId);
+            }
+        }, "stopSync");
+    }
+
+    /**
+     * Validate a user account, given a protocol, host address, port, ssl status, and credentials.
+     * The result of this call is returned in a Bundle which MUST include a result code and MAY
+     * include a PolicySet that is required by the account. A successful validation implies a host
+     * address that serves the specified protocol and credentials sufficient to be authorized
+     * by the server to do so.
+     *
+     * @param hostAuth the hostauth object to validate
+     * @return a Bundle as described above
+     */
+    @Override
+    public Bundle validate(final HostAuth hostAuth) throws RemoteException {
+        setTask(new ProxyTask() {
+            @Override
+            public void run() throws RemoteException{
+                if (mCallback != null) mService.setCallback(mCallback);
+                mReturn = mService.validate(hostAuth);
+            }
+        }, "validate");
+        waitForCompletion();
+        if (mReturn == null) {
+            Bundle bundle = new Bundle();
+            bundle.putInt(VALIDATE_BUNDLE_RESULT_CODE, MessagingException.UNSPECIFIED_EXCEPTION);
+            return bundle;
+        } else {
+            Bundle bundle = (Bundle) mReturn;
+            bundle.setClassLoader(Policy.class.getClassLoader());
+            Log.v(TAG, "validate returns " + bundle.getInt(VALIDATE_BUNDLE_RESULT_CODE));
+            return bundle;
+        }
+    }
+
+    /**
+     * Attempt to determine a user's host address and credentials from an email address and
+     * password. The result is returned in a Bundle which MUST include an error code and MAY (on
+     * success) include a HostAuth record sufficient to enable the service to validate the user's
+     * account.
+     *
+     * @param userName the user's email address
+     * @param password the user's password
+     * @return a Bundle as described above
+     */
+    @Override
+    public Bundle autoDiscover(final String userName, final String password)
+            throws RemoteException {
+        setTask(new ProxyTask() {
+            @Override
+            public void run() throws RemoteException{
+                if (mCallback != null) mService.setCallback(mCallback);
+                mReturn = mService.autoDiscover(userName, password);
+            }
+        }, "autoDiscover");
+        waitForCompletion();
+        if (mReturn == null) {
+            return null;
+        } else {
+            Bundle bundle = (Bundle) mReturn;
+            bundle.setClassLoader(HostAuth.class.getClassLoader());
+            Log.v(TAG, "autoDiscover returns " + bundle.getInt(AUTO_DISCOVER_BUNDLE_ERROR_CODE));
+            return bundle;
+        }
+    }
+
+    /**
+     * Request that the service reload the folder list for the specified account. The service
+     * MUST use the syncMailboxListStatus callback to indicate "starting" and "finished"
+     *
+     * @param accoundId the id of the account whose folder list is to be updated
+     */
+    @Override
+    public void updateFolderList(final long accountId) throws RemoteException {
+        setTask(new ProxyTask() {
+            @Override
+            public void run() throws RemoteException {
+                if (mCallback != null) mService.setCallback(mCallback);
+                mService.updateFolderList(accountId);
+            }
+        }, "updateFolderList");
+    }
+
+    /**
+     * Specify the debug flags selected by the user.  The service SHOULD log debug information as
+     * requested.
+     *
+     * @param flags an integer whose bits represent logging flags as defined in DEBUG_* flags above
+     */
+    @Override
+    public void setLogging(final int flags) throws RemoteException {
+        setTask(new ProxyTask() {
+            @Override
+            public void run() throws RemoteException {
+                if (mCallback != null) mService.setCallback(mCallback);
+                mService.setLogging(flags);
+            }
+        }, "setLogging");
+    }
+
+    /**
+     * Set the global callback object to be used by the service; the service MUST always use the
+     * most recently set callback object
+     *
+     * @param cb a callback object through which all service callbacks are executed
+     */
+    @Override
+    public void setCallback(final IEmailServiceCallback cb) throws RemoteException {
+        setTask(new ProxyTask() {
+            @Override
+            public void run() throws RemoteException {
+                mService.setCallback(cb);
+            }
+        }, "setCallback");
+    }
+
+    /**
+     * Alert the sync adapter that the account's host information has (or may have) changed; the
+     * service MUST stop all in-process or pending syncs, clear error states related to the
+     * account and its mailboxes, and restart necessary sync adapters (e.g. pushed mailboxes)
+     *
+     * @param accountId the id of the account whose host information has changed
+     */
+    @Override
+    public void hostChanged(final long accountId) throws RemoteException {
+        setTask(new ProxyTask() {
+            @Override
+            public void run() throws RemoteException {
+                mService.hostChanged(accountId);
+            }
+        }, "hostChanged");
+    }
+
+    /**
+     * Send a meeting response for the specified message
+     *
+     * @param messageId the id of the message containing the meeting request
+     * @param response the response code, as defined in EmailServiceConstants
+     */
+    @Override
+    public void sendMeetingResponse(final long messageId, final int response)
+            throws RemoteException {
+        setTask(new ProxyTask() {
+            @Override
+            public void run() throws RemoteException {
+                if (mCallback != null) mService.setCallback(mCallback);
+                mService.sendMeetingResponse(messageId, response);
+            }
+        }, "sendMeetingResponse");
+    }
+
+    /**
+     * Request the sync adapter to load a complete message
+     *
+     * @param messageId the id of the message to be loaded
+     */
+    @Override
+    public void loadMore(final long messageId) throws RemoteException {
+        setTask(new ProxyTask() {
+            @Override
+            public void run() throws RemoteException {
+                if (mCallback != null) mService.setCallback(mCallback);
+                mService.loadMore(messageId);
+            }
+        }, "startSync");
+    }
+
+    /**
+     * Not yet used
+     *
+     * @param accountId the account in which the folder is to be created
+     * @param name the name of the folder to be created
+    */
+    @Override
+    public boolean createFolder(long accountId, String name) throws RemoteException {
+        return false;
+    }
+
+    /**
+     * Not yet used
+     *
+     * @param accountId the account in which the folder resides
+     * @param name the name of the folder to be deleted
+     */
+    @Override
+    public boolean deleteFolder(long accountId, String name) throws RemoteException {
+        return false;
+    }
+
+    /**
+     * Not yet used
+     *
+     * @param accountId the account in which the folder resides
+     * @param oldName the name of the existing folder
+     * @param newName the new name for the folder
+     */
+    @Override
+    public boolean renameFolder(long accountId, String oldName, String newName)
+            throws RemoteException {
+        return false;
+    }
+
+    /**
+     * Request the service to delete the account's PIM (personal information management) data. This
+     * data includes any data that is 1) associated with the account and 2) created/stored by the
+     * service or its sync adapters and 3) not stored in the EmailProvider database (e.g. contact
+     * and calendar information).
+     *
+     * @param accountId the account whose data is to be deleted
+     */
+    @Override
+    public void deleteAccountPIMData(final long accountId) throws RemoteException {
+        setTask(new ProxyTask() {
+            @Override
+            public void run() throws RemoteException {
+                mService.deleteAccountPIMData(accountId);
+            }
+        }, "deleteAccountPIMData");
+    }
+
+
+    /**
+     * PRELIMINARY
+     * Search for messages given a query string.  The string is interpreted as the logical AND of
+     * terms separated by white space.  The search is performed on the specified mailbox in the
+     * specified account (including subfolders, as specified by the includeSubfolders parameter).
+     * At most numResults messages matching the query term(s) will be added to the mailbox specified
+     * as destMailboxId. If mailboxId is -1, the entire account will be searched. If firstResult is
+     * specified and non-zero, results will be added starting with the firstResult'th match (i.e.
+     * for the continuation of a previous search)
+     *
+     * @param accountId the id of the account to be searched
+     * @param searchParams the search specification
+     * @param destMailboxId the id of the mailbox into which search results are appended
+     * @return the total number of matches for this search (regardless of how many were requested)
+     */
+    @Override
+    public int searchMessages(final long accountId, final SearchParams searchParams,
+            final long destMailboxId) throws RemoteException {
+        setTask(new ProxyTask() {
+            @Override
+            public void run() throws RemoteException{
+                if (mCallback != null) mService.setCallback(mCallback);
+                mReturn = mService.searchMessages(accountId, searchParams, destMailboxId);
+            }
+        }, "searchMessages");
+        waitForCompletion();
+        if (mReturn == null) {
+            return 0;
+        } else {
+            return (Integer)mReturn;
+        }
+    }
+
+    /**
+     * Request the service to send mail in the specified account's Outbox
+     *
+     * @param accountId the account whose outgoing mail should be sent
+     */
+    @Override
+    public void sendMail(final long accountId) throws RemoteException {
+        setTask(new ProxyTask() {
+            @Override
+            public void run() throws RemoteException{
+                if (mCallback != null) mService.setCallback(mCallback);
+                mService.sendMail(accountId);
+            }
+        }, "sendMail");
+    }
+
+    @Override
+    public IBinder asBinder() {
+        return null;
+    }
+}
diff --git a/email2/emailcommon/src/com/android/emailcommon/service/EmailServiceStatus.java b/email2/emailcommon/src/com/android/emailcommon/service/EmailServiceStatus.java
new file mode 100644
index 0000000..8cd577c
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/service/EmailServiceStatus.java
@@ -0,0 +1,43 @@
+/*
+ *  Copyright (C) 2008-2009 Marc Blank
+ * 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.emailcommon.service;
+
+/**
+ * Definitions of service status codes returned to IEmailServiceCallback's status method
+ */
+public interface EmailServiceStatus {
+    public static final int SUCCESS = 0;
+    public static final int IN_PROGRESS = 1;
+
+    public static final int MESSAGE_NOT_FOUND = 0x10;
+    public static final int ATTACHMENT_NOT_FOUND = 0x11;
+    public static final int FOLDER_NOT_DELETED = 0x12;
+    public static final int FOLDER_NOT_RENAMED = 0x13;
+    public static final int FOLDER_NOT_CREATED = 0x14;
+    public static final int REMOTE_EXCEPTION = 0x15;
+    public static final int LOGIN_FAILED = 0x16;
+    public static final int SECURITY_FAILURE = 0x17;
+    public static final int ACCOUNT_UNINITIALIZED = 0x18;
+    public static final int ACCESS_DENIED = 0x19;
+
+    // Maybe we should automatically retry these?
+    public static final int CONNECTION_ERROR = 0x20;
+
+    // Client certificates used to authenticate cannot be retrieved from the system.
+    public static final int CLIENT_CERTIFICATE_ERROR = 0x21;
+}
diff --git a/email2/emailcommon/src/com/android/emailcommon/service/IAccountService.aidl b/email2/emailcommon/src/com/android/emailcommon/service/IAccountService.aidl
new file mode 100644
index 0000000..a29baf5
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/service/IAccountService.aidl
@@ -0,0 +1,32 @@
+/*
+ * 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.Bundle;
+
+interface IAccountService {
+    oneway void notifyLoginFailed(long accountId);
+    oneway void notifyLoginSucceeded(long accountId);
+
+    void reconcileAccounts(String protocol, String accountManagerType);
+
+    int getAccountColor(long accountId);
+
+    Bundle getConfigurationData(String accountType);
+
+    String getDeviceId();
+}
\ No newline at end of file
diff --git a/email2/emailcommon/src/com/android/emailcommon/service/IEmailService.aidl b/email2/emailcommon/src/com/android/emailcommon/service/IEmailService.aidl
new file mode 100644
index 0000000..cd5cd07
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/service/IEmailService.aidl
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2008-2010 Marc Blank
+ * 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.emailcommon.service;
+
+import com.android.emailcommon.provider.HostAuth;
+import com.android.emailcommon.service.IEmailServiceCallback;
+import com.android.emailcommon.service.SearchParams;
+import android.os.Bundle;
+
+interface IEmailService {
+    Bundle validate(in HostAuth hostauth);
+
+    oneway void startSync(long mailboxId, boolean userRequest);
+    oneway void stopSync(long mailboxId);
+
+    oneway void loadMore(long messageId);
+    oneway void loadAttachment(long attachmentId, boolean background);
+
+    oneway void updateFolderList(long accountId);
+
+    boolean createFolder(long accountId, String name);
+    boolean deleteFolder(long accountId, String name);
+    boolean renameFolder(long accountId, String oldName, String newName);
+
+    // Must not be oneway; unless an exception is thrown, the caller is guaranteed that the callback
+    // has been registered
+    void setCallback(IEmailServiceCallback cb);
+
+    oneway void setLogging(int on);
+
+    oneway void hostChanged(long accountId);
+
+    Bundle autoDiscover(String userName, String password);
+
+    oneway void sendMeetingResponse(long messageId, int response);
+
+    // Must not be oneway; unless an exception is thrown, the caller is guaranteed that the action
+    // has been completed
+    void deleteAccountPIMData(long accountId);
+
+    int getApiLevel();
+
+    // API level 2
+    int searchMessages(long accountId, in SearchParams params, long destMailboxId);
+
+    void sendMail(long accountId);
+}
diff --git a/email2/emailcommon/src/com/android/emailcommon/service/IEmailServiceCallback.aidl b/email2/emailcommon/src/com/android/emailcommon/service/IEmailServiceCallback.aidl
new file mode 100644
index 0000000..c713f52
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/service/IEmailServiceCallback.aidl
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2008-2009 Marc Blank
+ * 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.emailcommon.service;
+
+oneway interface IEmailServiceCallback {
+    /*
+     * Ordinary results:
+     *   statuscode = 1, progress = 0:      "starting"
+     *   statuscode = 0, progress = n/a:    "finished"
+     *
+     * If there is an error, it must be reported as follows:
+     *   statuscode = err, progress = n/a:  "stopping due to error"
+     *
+     * *Optionally* a callback can also include intermediate values from 1..99 e.g.
+     *   statuscode = 1, progress = 0:      "starting"
+     *   statuscode = 1, progress = 30:     "working"
+     *   statuscode = 1, progress = 60:     "working"
+     *   statuscode = 0, progress = n/a:    "finished"
+     */
+
+    /**
+     * Callback to indicate that an account is being synced (updating folder list)
+     * accountId = the account being synced
+     * statusCode = 0 for OK, 1 for progress, other codes for error
+     * progress = 0 for "start", 1..100 for optional progress reports
+     */
+    void syncMailboxListStatus(long accountId, int statusCode, int progress);
+
+    /**
+     * Callback to indicate that a mailbox is being synced
+     * mailboxId = the mailbox being synced
+     * statusCode = 0 for OK, 1 for progress, other codes for error
+     * progress = 0 for "start", 1..100 for optional progress reports
+     */
+    void syncMailboxStatus(long mailboxId, int statusCode, int progress);
+
+    /**
+     * Callback to indicate that a particular attachment is being synced
+     * messageId = the message that owns the attachment
+     * attachmentId = the attachment being synced
+     * statusCode = 0 for OK, 1 for progress, other codes for error
+     * progress = 0 for "start", 1..100 for optional progress reports
+     */
+    void loadAttachmentStatus(long messageId, long attachmentId, int statusCode, int progress);
+
+    /**
+     * Callback to indicate that a particular message is being sent
+     * messageId = the message being sent
+     * statusCode = 0 for OK, 1 for progress, other codes for error
+     * progress = 0 for "start", 1..100 for optional progress reports
+     */
+    void sendMessageStatus(long messageId, String subject, int statusCode, int progress);
+
+    /**
+     * Callback to indicate that a particular message is being loaded
+     * messageId = the message being sent
+     * statusCode = 0 for OK, 1 for progress, other codes for error
+     * progress = 0 for "start", 1..100 for optional progress reports
+     */
+    void loadMessageStatus(long messageId, int statusCode, int progress);
+}
diff --git a/email2/emailcommon/src/com/android/emailcommon/service/IPolicyService.aidl b/email2/emailcommon/src/com/android/emailcommon/service/IPolicyService.aidl
new file mode 100755
index 0000000..9d4be36
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/service/IPolicyService.aidl
@@ -0,0 +1,25 @@
+/*
+ * 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 com.android.emailcommon.provider.Policy;
+
+interface IPolicyService {
+    boolean isActive(in Policy policies);
+    void setAccountHoldFlag(long accountId, boolean newState);
+    void setAccountPolicy(long accountId, in Policy policy, String securityKey);
+    oneway void remoteWipe();
+}
\ No newline at end of file
diff --git a/email2/emailcommon/src/com/android/emailcommon/service/LegacyPolicySet.java b/email2/emailcommon/src/com/android/emailcommon/service/LegacyPolicySet.java
new file mode 100644
index 0000000..e5d4436
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/service/LegacyPolicySet.java
@@ -0,0 +1,87 @@
+/* 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 com.android.emailcommon.provider.Policy;
+
+/**
+ * Legacy class for policy storage as a bit field of flags
+ */
+public class LegacyPolicySet {
+
+    // Security (provisioning) flags
+        // bits 0..4: password length (0=no password required)
+    public static final int PASSWORD_LENGTH_MASK = 31;
+    public static final int PASSWORD_LENGTH_SHIFT = 0;
+    public static final int PASSWORD_LENGTH_MAX = 30;
+        // bits 5..8: password mode
+    public static final int PASSWORD_MODE_SHIFT = 5;
+    public static final int PASSWORD_MODE_MASK = 15 << PASSWORD_MODE_SHIFT;
+    public static final int PASSWORD_MODE_NONE = 0 << PASSWORD_MODE_SHIFT;
+    public static final int PASSWORD_MODE_SIMPLE = 1 << PASSWORD_MODE_SHIFT;
+    public static final int PASSWORD_MODE_STRONG = 2 << PASSWORD_MODE_SHIFT;
+        // bits 9..13: password failures -> wipe device (0=disabled)
+    public static final int PASSWORD_MAX_FAILS_SHIFT = 9;
+    public static final int PASSWORD_MAX_FAILS_MASK = 31 << PASSWORD_MAX_FAILS_SHIFT;
+    public static final int PASSWORD_MAX_FAILS_MAX = 31;
+        // bits 14..24: seconds to screen lock (0=not required)
+    public static final int SCREEN_LOCK_TIME_SHIFT = 14;
+    public static final int SCREEN_LOCK_TIME_MASK = 2047 << SCREEN_LOCK_TIME_SHIFT;
+    public static final int SCREEN_LOCK_TIME_MAX = 2047;
+        // bit 25: remote wipe capability required
+    public static final int REQUIRE_REMOTE_WIPE = 1 << 25;
+        // bit 26..35: password expiration (days; 0=not required)
+    public static final int PASSWORD_EXPIRATION_SHIFT = 26;
+    public static final long PASSWORD_EXPIRATION_MASK = 1023L << PASSWORD_EXPIRATION_SHIFT;
+    public static final int PASSWORD_EXPIRATION_MAX = 1023;
+        // bit 36..43: password history (length; 0=not required)
+    public static final int PASSWORD_HISTORY_SHIFT = 36;
+    public static final long PASSWORD_HISTORY_MASK = 255L << PASSWORD_HISTORY_SHIFT;
+    public static final int PASSWORD_HISTORY_MAX = 255;
+        // bit 44..48: min complex characters (0=not required)
+    public static final int PASSWORD_COMPLEX_CHARS_SHIFT = 44;
+    public static final long PASSWORD_COMPLEX_CHARS_MASK = 31L << PASSWORD_COMPLEX_CHARS_SHIFT;
+    public static final int PASSWORD_COMPLEX_CHARS_MAX = 31;
+        // bit 49: requires device encryption (internal)
+    public static final long REQUIRE_ENCRYPTION = 1L << 49;
+        // bit 50: requires external storage encryption
+    public static final long REQUIRE_ENCRYPTION_EXTERNAL = 1L << 50;
+
+    /**
+     * Convert legacy policy flags to a Policy
+     * @param flags legacy policy flags
+     * @return a Policy representing the legacy policy flag
+     */
+    public static Policy flagsToPolicy(long flags) {
+        Policy policy = new Policy();
+        policy.mPasswordMode = ((int) (flags & PASSWORD_MODE_MASK)) >> PASSWORD_MODE_SHIFT;
+        policy.mPasswordMinLength = (int) ((flags & PASSWORD_LENGTH_MASK) >> PASSWORD_LENGTH_SHIFT);
+        policy.mPasswordMaxFails =
+            (int) ((flags & PASSWORD_MAX_FAILS_MASK) >> PASSWORD_MAX_FAILS_SHIFT);
+        policy.mPasswordComplexChars =
+            (int) ((flags & PASSWORD_COMPLEX_CHARS_MASK) >> PASSWORD_COMPLEX_CHARS_SHIFT);
+        policy.mPasswordHistory = (int) ((flags & PASSWORD_HISTORY_MASK) >> PASSWORD_HISTORY_SHIFT);
+        policy.mPasswordExpirationDays =
+            (int) ((flags & PASSWORD_EXPIRATION_MASK) >> PASSWORD_EXPIRATION_SHIFT);
+        policy.mMaxScreenLockTime =
+            (int) ((flags & SCREEN_LOCK_TIME_MASK) >> SCREEN_LOCK_TIME_SHIFT);
+        policy.mRequireRemoteWipe = 0 != (flags & REQUIRE_REMOTE_WIPE);
+        policy.mRequireEncryption = 0 != (flags & REQUIRE_ENCRYPTION);
+        policy.mRequireEncryptionExternal = 0 != (flags & REQUIRE_ENCRYPTION_EXTERNAL);
+        return policy;
+    }
+}
+
diff --git a/email2/emailcommon/src/com/android/emailcommon/service/PolicyServiceProxy.java b/email2/emailcommon/src/com/android/emailcommon/service/PolicyServiceProxy.java
new file mode 100755
index 0000000..26e820d
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/service/PolicyServiceProxy.java
@@ -0,0 +1,133 @@
+/*
+ * 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.content.Context;
+import android.content.Intent;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.Log;
+
+import com.android.emailcommon.provider.Account;
+import com.android.emailcommon.provider.Policy;
+
+public class PolicyServiceProxy extends ServiceProxy implements IPolicyService {
+    private static final boolean DEBUG_PROXY = false; // DO NOT CHECK THIS IN SET TO TRUE
+    private static final String TAG = "PolicyServiceProxy";
+
+    // The intent used by sync adapter services to connect to the PolicyService
+    public static final String POLICY_INTENT = "com.android.email.POLICY_INTENT";
+
+    private IPolicyService mService = null;
+    private Object mReturn = null;
+
+    public PolicyServiceProxy(Context _context) {
+        super(_context, new Intent(POLICY_INTENT));
+    }
+
+    @Override
+    public void onConnected(IBinder binder) {
+        mService = IPolicyService.Stub.asInterface(binder);
+    }
+
+    public IBinder asBinder() {
+        return null;
+    }
+
+    @Override
+    public boolean isActive(final Policy arg0) throws RemoteException {
+        setTask(new ProxyTask() {
+            public void run() throws RemoteException {
+                mReturn = mService.isActive(arg0);
+            }
+        }, "isActive");
+        waitForCompletion();
+        if (DEBUG_PROXY) {
+            Log.v(TAG, "isActive: " + ((mReturn == null) ? "null" : mReturn));
+        }
+        if (mReturn == null) {
+            throw new ServiceUnavailableException("isActive");
+        } else {
+            return (Boolean)mReturn;
+        }
+    }
+
+    @Override
+    public void setAccountPolicy(final long accountId, final Policy policy,
+            final String securityKey) throws RemoteException {
+        setTask(new ProxyTask() {
+            public void run() throws RemoteException {
+                mService.setAccountPolicy(accountId, policy, securityKey);
+            }
+        }, "setAccountPolicy");
+        waitForCompletion();
+    }
+
+    @Override
+    public void remoteWipe() throws RemoteException {
+        setTask(new ProxyTask() {
+            public void run() throws RemoteException {
+                mService.remoteWipe();
+            }
+        }, "remoteWipe");
+    }
+
+    @Override
+    public void setAccountHoldFlag(final long arg0, final boolean arg1) throws RemoteException {
+        setTask(new ProxyTask() {
+            public void run() throws RemoteException {
+                mService.setAccountHoldFlag(arg0, arg1);
+            }
+        }, "setAccountHoldFlag");
+    }
+
+    // Static methods that encapsulate the proxy calls above
+    public static boolean isActive(Context context, Policy policies) {
+        try {
+            return new PolicyServiceProxy(context).isActive(policies);
+        } catch (RemoteException e) {
+        }
+        return false;
+    }
+
+    public static void setAccountHoldFlag(Context context, Account account, boolean newState) {
+        try {
+            new PolicyServiceProxy(context).setAccountHoldFlag(account.mId, newState);
+        } catch (RemoteException e) {
+            throw new IllegalStateException("PolicyService transaction failed");
+        }
+    }
+
+    public static void remoteWipe(Context context) {
+        try {
+            new PolicyServiceProxy(context).remoteWipe();
+        } catch (RemoteException e) {
+            throw new IllegalStateException("PolicyService transaction failed");
+        }
+    }
+
+    public static void setAccountPolicy(Context context, long accountId, Policy policy,
+            String securityKey) {
+        try {
+            new PolicyServiceProxy(context).setAccountPolicy(accountId, policy, securityKey);
+            return;
+        } catch (RemoteException e) {
+        }
+        throw new IllegalStateException("PolicyService transaction failed");
+    }
+}
+
diff --git a/email2/emailcommon/src/com/android/emailcommon/service/SearchParams.aidl b/email2/emailcommon/src/com/android/emailcommon/service/SearchParams.aidl
new file mode 100644
index 0000000..77dedae
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/service/SearchParams.aidl
@@ -0,0 +1,18 @@
+/*
+ * 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;
+
+parcelable SearchParams;
\ No newline at end of file
diff --git a/email2/emailcommon/src/com/android/emailcommon/service/SearchParams.java b/email2/emailcommon/src/com/android/emailcommon/service/SearchParams.java
new file mode 100644
index 0000000..eacf01d
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/service/SearchParams.java
@@ -0,0 +1,137 @@
+/*
+ * 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.android.emailcommon.provider.Mailbox;
+import com.google.common.base.Objects;
+
+public class SearchParams implements Parcelable {
+    public static final long ALL_MAILBOXES = Mailbox.NO_MAILBOX;
+
+    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/email2/emailcommon/src/com/android/emailcommon/service/ServiceProxy.java b/email2/emailcommon/src/com/android/emailcommon/service/ServiceProxy.java
new file mode 100644
index 0000000..8e3bcff
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/service/ServiceProxy.java
@@ -0,0 +1,204 @@
+/*
+ /*
+ * 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.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.Debug;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.Log;
+
+/**
+ * The EmailServiceProxy class provides a simple interface for the UI to call into the various
+ * EmailService classes (e.g. ExchangeService for EAS).  It wraps the service connect/disconnect
+ * process so that the caller need not be concerned with it.
+ *
+ * Use the class like this:
+ *   new EmailServiceClass(context, class).loadAttachment(attachmentId, callback)
+ *
+ * Methods without a return value return immediately (i.e. are asynchronous); methods with a
+ * return value wait for a result from the Service (i.e. they should not be called from the UI
+ * thread) with a default timeout of 30 seconds (settable)
+ *
+ * An EmailServiceProxy object cannot be reused (trying to do so generates a RemoteException)
+ */
+
+public abstract class ServiceProxy {
+    private static final boolean DEBUG_PROXY = false; // DO NOT CHECK THIS IN SET TO TRUE
+    private final String mTag;
+
+    private final Context mContext;
+    protected final Intent mIntent;
+    private Runnable mRunnable = new ProxyRunnable();
+    private ProxyTask mTask;
+    private String mName = " unnamed";
+    private final ServiceConnection mConnection = new ProxyConnection();
+    // Service call timeout (in seconds)
+    private int mTimeout = 45;
+    private long mStartTime;
+    private boolean mDead = false;
+
+    public abstract void onConnected(IBinder binder);
+
+    public ServiceProxy(Context _context, Intent _intent) {
+        mContext = _context;
+        mIntent = _intent;
+        mTag = getClass().getSimpleName();
+        if (Debug.isDebuggerConnected()) {
+            mTimeout <<= 2;
+        }
+    }
+
+    private class ProxyConnection implements ServiceConnection {
+        public void onServiceConnected(ComponentName name, IBinder binder) {
+            onConnected(binder);
+            if (DEBUG_PROXY) {
+                Log.v(mTag, "Connected: " + name.getShortClassName());
+            }
+            // Run our task on a new thread
+            new Thread(new Runnable() {
+                public void run() {
+                    try {
+                        runTask();
+                    } finally {
+                        endTask();
+                    }
+                }}).start();
+        }
+
+        public void onServiceDisconnected(ComponentName name) {
+            if (DEBUG_PROXY) {
+                Log.v(mTag, "Disconnected: " + name.getShortClassName());
+            }
+        }
+    }
+
+    public interface ProxyTask {
+        public void run() throws RemoteException;
+    }
+
+    private class ProxyRunnable implements Runnable {
+        @Override
+        public void run() {
+            try {
+                mTask.run();
+            } catch (RemoteException e) {
+            }
+        }
+    }
+
+    public ServiceProxy setTimeout(int secs) {
+        mTimeout = secs;
+        return this;
+    }
+
+    public int getTimeout() {
+        return mTimeout;
+    }
+
+    public void endTask() {
+        try {
+            mContext.unbindService(mConnection);
+        } catch (IllegalArgumentException e) {
+            // This can happen if the user ended the activity that was using the service
+            // This is harmless, but we've got to catch it
+        }
+
+        mDead = true;
+        synchronized(mConnection) {
+            if (DEBUG_PROXY) {
+                Log.v(mTag, "Task " + mName + " completed; disconnecting");
+            }
+            mConnection.notify();
+        }
+    }
+
+    private void runTask() {
+        Thread thread = new Thread(mRunnable);
+        thread.start();
+        try {
+            thread.join();
+        } catch (InterruptedException e) {
+        }
+    }
+
+    public boolean setTask(ProxyTask task, String name) {
+        mName = name;
+        return setTask(task);
+    }
+
+    public boolean setTask(ProxyTask task) throws IllegalStateException {
+        if (mDead) {
+            throw new IllegalStateException();
+        }
+        mTask = task;
+        mStartTime = System.currentTimeMillis();
+        if (DEBUG_PROXY) {
+            Log.v(mTag, "Bind requested for task " + mName);
+        }
+        return mContext.bindService(mIntent, mConnection, Context.BIND_AUTO_CREATE);
+    }
+
+    public void waitForCompletion() {
+        synchronized (mConnection) {
+            long time = System.currentTimeMillis();
+            try {
+                if (DEBUG_PROXY) {
+                    Log.v(mTag, "Waiting for task " + mName + " to complete...");
+                }
+                mConnection.wait(mTimeout * 1000L);
+            } catch (InterruptedException e) {
+                // Can be ignored safely
+            }
+            if (DEBUG_PROXY) {
+                Log.v(mTag, "Wait for " + mName + " finished in " +
+                        (System.currentTimeMillis() - time) + "ms");
+            }
+        }
+    }
+
+    public void close() throws RemoteException {
+        if (mDead) {
+            throw new RemoteException();
+        }
+        endTask();
+    }
+
+    /**
+     * Connection test; return indicates whether the remote service can be connected to
+     * @return the result of trying to connect to the remote service
+     */
+    public boolean test() {
+        try {
+            return setTask(new ProxyTask() {
+                public void run() throws RemoteException {
+                    if (DEBUG_PROXY) {
+                        Log.v(mTag, "Connection test succeeded in " +
+                                (System.currentTimeMillis() - mStartTime) + "ms");
+                    }
+                }
+            }, "test");
+        } catch (Exception e) {
+            // For any failure, return false.
+            return false;
+        }
+    }
+}
diff --git a/email2/emailcommon/src/com/android/emailcommon/service/ServiceUnavailableException.java b/email2/emailcommon/src/com/android/emailcommon/service/ServiceUnavailableException.java
new file mode 100644
index 0000000..1ba22a7
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/service/ServiceUnavailableException.java
@@ -0,0 +1,29 @@
+/*
+ * 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;
+
+/**
+ * An Exception thrown when a service proxy requires a result and there's a remote exception; this
+ * prevents the caller from receiving an invalid result.
+ */
+public class ServiceUnavailableException extends RuntimeException {
+    private static final long serialVersionUID = 1L;
+
+    public ServiceUnavailableException(String string) {
+        super(string);
+    }
+}
\ No newline at end of file
diff --git a/email2/emailcommon/src/com/android/emailcommon/service/SyncWindow.java b/email2/emailcommon/src/com/android/emailcommon/service/SyncWindow.java
new file mode 100644
index 0000000..52839b2
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/service/SyncWindow.java
@@ -0,0 +1,29 @@
+/*
+ * 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;
+
+public class SyncWindow {
+    public static final int SYNC_WINDOW_AUTO = -2;
+    public static final int SYNC_WINDOW_USER = -1;
+    public static final int SYNC_WINDOW_UNKNOWN = 0;
+    public static final int SYNC_WINDOW_1_DAY = 1;
+    public static final int SYNC_WINDOW_3_DAYS = 2;
+    public static final int SYNC_WINDOW_1_WEEK = 3;
+    public static final int SYNC_WINDOW_2_WEEKS = 4;
+    public static final int SYNC_WINDOW_1_MONTH = 5;
+    public static final int SYNC_WINDOW_ALL = 6;
+}
diff --git a/email2/emailcommon/src/com/android/emailcommon/utility/AttachmentUtilities.java b/email2/emailcommon/src/com/android/emailcommon/utility/AttachmentUtilities.java
new file mode 100644
index 0000000..e4d119b
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/utility/AttachmentUtilities.java
@@ -0,0 +1,352 @@
+/*
+ * 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.Logging;
+import com.android.emailcommon.provider.EmailContent.Attachment;
+import com.android.emailcommon.provider.EmailContent.Message;
+import com.android.emailcommon.provider.EmailContent.MessageColumns;
+
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.text.TextUtils;
+import android.util.Log;
+import android.webkit.MimeTypeMap;
+
+import java.io.File;
+
+public class AttachmentUtilities {
+    public static final String AUTHORITY = "com.android.email.attachmentprovider";
+    public static final Uri CONTENT_URI = Uri.parse( "content://" + AUTHORITY);
+
+    public static final String FORMAT_RAW = "RAW";
+    public static final String FORMAT_THUMBNAIL = "THUMBNAIL";
+
+    public static class Columns {
+        public static final String _ID = "_id";
+        public static final String DATA = "_data";
+        public static final String DISPLAY_NAME = "_display_name";
+        public static final String SIZE = "_size";
+    }
+
+    /**
+     * The MIME type(s) of attachments we're willing to send via attachments.
+     *
+     * Any attachments may be added via Intents with Intent.ACTION_SEND or ACTION_SEND_MULTIPLE.
+     */
+    public static final String[] ACCEPTABLE_ATTACHMENT_SEND_INTENT_TYPES = new String[] {
+        "*/*",
+    };
+    /**
+     * The MIME type(s) of attachments we're willing to send from the internal UI.
+     *
+     * NOTE:  At the moment it is not possible to open a chooser with a list of filter types, so
+     * the chooser is only opened with the first item in the list.
+     */
+    public static final String[] ACCEPTABLE_ATTACHMENT_SEND_UI_TYPES = new String[] {
+        "image/*",
+        "video/*",
+    };
+    /**
+     * The MIME type(s) of attachments we're willing to view.
+     */
+    public static final String[] ACCEPTABLE_ATTACHMENT_VIEW_TYPES = new String[] {
+        "*/*",
+    };
+    /**
+     * The MIME type(s) of attachments we're not willing to view.
+     */
+    public static final String[] UNACCEPTABLE_ATTACHMENT_VIEW_TYPES = new String[] {
+    };
+    /**
+     * The MIME type(s) of attachments we're willing to download to SD.
+     */
+    public static final String[] ACCEPTABLE_ATTACHMENT_DOWNLOAD_TYPES = new String[] {
+        "*/*",
+    };
+    /**
+     * The MIME type(s) of attachments we're not willing to download to SD.
+     */
+    public static final String[] UNACCEPTABLE_ATTACHMENT_DOWNLOAD_TYPES = new String[] {
+    };
+    /**
+     * Filename extensions of attachments we're never willing to download (potential malware).
+     * Entries in this list are compared to the end of the lower-cased filename, so they must
+     * be lower case, and should not include a "."
+     */
+    public static final String[] UNACCEPTABLE_ATTACHMENT_EXTENSIONS = new String[] {
+        // File types that contain malware
+        "ade", "adp", "bat", "chm", "cmd", "com", "cpl", "dll", "exe",
+        "hta", "ins", "isp", "jse", "lib", "mde", "msc", "msp",
+        "mst", "pif", "scr", "sct", "shb", "sys", "vb", "vbe",
+        "vbs", "vxd", "wsc", "wsf", "wsh",
+        // File types of common compression/container formats (again, to avoid malware)
+        "zip", "gz", "z", "tar", "tgz", "bz2",
+    };
+    /**
+     * Filename extensions of attachments that can be installed.
+     * Entries in this list are compared to the end of the lower-cased filename, so they must
+     * be lower case, and should not include a "."
+     */
+    public static final String[] INSTALLABLE_ATTACHMENT_EXTENSIONS = new String[] {
+        "apk",
+    };
+    /**
+     * The maximum size of an attachment we're willing to download (either View or Save)
+     * Attachments that are base64 encoded (most) will be about 1.375x their actual size
+     * so we should probably factor that in. A 5MB attachment will generally be around
+     * 6.8MB downloaded but only 5MB saved.
+     */
+    public static final int MAX_ATTACHMENT_DOWNLOAD_SIZE = (5 * 1024 * 1024);
+    /**
+     * The maximum size of an attachment we're willing to upload (measured as stored on disk).
+     * Attachments that are base64 encoded (most) will be about 1.375x their actual size
+     * so we should probably factor that in. A 5MB attachment will generally be around
+     * 6.8MB uploaded.
+     */
+    public static final int MAX_ATTACHMENT_UPLOAD_SIZE = (5 * 1024 * 1024);
+
+    public static Uri getAttachmentUri(long accountId, long id) {
+        return CONTENT_URI.buildUpon()
+        .appendPath(Long.toString(accountId))
+        .appendPath(Long.toString(id))
+        .appendPath(FORMAT_RAW)
+        .build();
+    }
+
+    public static Uri getAttachmentThumbnailUri(long accountId, long id,
+            int width, int height) {
+        return CONTENT_URI.buildUpon()
+        .appendPath(Long.toString(accountId))
+        .appendPath(Long.toString(id))
+        .appendPath(FORMAT_THUMBNAIL)
+        .appendPath(Integer.toString(width))
+        .appendPath(Integer.toString(height))
+        .build();
+    }
+
+    /**
+     * Return the filename for a given attachment.  This should be used by any code that is
+     * going to *write* attachments.
+     *
+     * This does not create or write the file, or even the directories.  It simply builds
+     * the filename that should be used.
+     */
+    public static File getAttachmentFilename(Context context, long accountId, long attachmentId) {
+        return new File(getAttachmentDirectory(context, accountId), Long.toString(attachmentId));
+    }
+
+    /**
+     * Return the directory for a given attachment.  This should be used by any code that is
+     * going to *write* attachments.
+     *
+     * This does not create or write the directory.  It simply builds the pathname that should be
+     * used.
+     */
+    public static File getAttachmentDirectory(Context context, long accountId) {
+        return context.getDatabasePath(accountId + ".db_att");
+    }
+
+    /**
+     * Helper to convert unknown or unmapped attachments to something useful based on filename
+     * extensions. The mime type is inferred based upon the table below. It's not perfect, but
+     * it helps.
+     *
+     * <pre>
+     *                   |---------------------------------------------------------|
+     *                   |                  E X T E N S I O N                      |
+     *                   |---------------------------------------------------------|
+     *                   | .eml        | known(.png) | unknown(.abc) | none        |
+     * | M |-----------------------------------------------------------------------|
+     * | I | none        | msg/rfc822  | image/png   | app/abc       | app/oct-str |
+     * | M |-------------| (always     |             |               |             |
+     * | E | app/oct-str |  overrides  |             |               |             |
+     * | T |-------------|             |             |-----------------------------|
+     * | Y | text/plain  |             |             | text/plain                  |
+     * | P |-------------|             |-------------------------------------------|
+     * | E | any/type    |             | any/type                                  |
+     * |---|-----------------------------------------------------------------------|
+     * </pre>
+     *
+     * NOTE: Since mime types on Android are case-*sensitive*, return values are always in
+     * lower case.
+     *
+     * @param fileName The given filename
+     * @param mimeType The given mime type
+     * @return A likely mime type for the attachment
+     */
+    public static String inferMimeType(final String fileName, final String mimeType) {
+        String resultType = null;
+        String fileExtension = getFilenameExtension(fileName);
+        boolean isTextPlain = "text/plain".equalsIgnoreCase(mimeType);
+
+        if ("eml".equals(fileExtension)) {
+            resultType = "message/rfc822";
+        } else {
+            boolean isGenericType =
+                    isTextPlain || "application/octet-stream".equalsIgnoreCase(mimeType);
+            // If the given mime type is non-empty and non-generic, return it
+            if (isGenericType || TextUtils.isEmpty(mimeType)) {
+                if (!TextUtils.isEmpty(fileExtension)) {
+                    // Otherwise, try to find a mime type based upon the file extension
+                    resultType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(fileExtension);
+                    if (TextUtils.isEmpty(resultType)) {
+                        // Finally, if original mimetype is text/plain, use it; otherwise synthesize
+                        resultType = isTextPlain ? mimeType : "application/" + fileExtension;
+                    }
+                }
+            } else {
+                resultType = mimeType;
+            }
+        }
+
+        // No good guess could be made; use an appropriate generic type
+        if (TextUtils.isEmpty(resultType)) {
+            resultType = isTextPlain ? "text/plain" : "application/octet-stream";
+        }
+        return resultType.toLowerCase();
+    }
+
+    /**
+     * @return mime-type for a {@link Uri}.
+     *    - Use {@link ContentResolver#getType} for a content: URI.
+     *    - Use {@link #inferMimeType} for a file: URI.
+     *    - Otherwise throw {@link IllegalArgumentException}.
+     */
+    public static String inferMimeTypeForUri(Context context, Uri uri) {
+        final String scheme = uri.getScheme();
+        if ("content".equals(scheme)) {
+            return context.getContentResolver().getType(uri);
+        } else if ("file".equals(scheme)) {
+            return inferMimeType(uri.getLastPathSegment(), "");
+        } else {
+            throw new IllegalArgumentException();
+        }
+    }
+
+    /**
+     * Extract and return filename's extension, converted to lower case, and not including the "."
+     *
+     * @return extension, or null if not found (or null/empty filename)
+     */
+    public static String getFilenameExtension(String fileName) {
+        String extension = null;
+        if (!TextUtils.isEmpty(fileName)) {
+            int lastDot = fileName.lastIndexOf('.');
+            if ((lastDot > 0) && (lastDot < fileName.length() - 1)) {
+                extension = fileName.substring(lastDot + 1).toLowerCase();
+            }
+        }
+        return extension;
+    }
+
+    /**
+     * Resolve attachment id to content URI.  Returns the resolved content URI (from the attachment
+     * DB) or, if not found, simply returns the incoming value.
+     *
+     * @param attachmentUri
+     * @return resolved content URI
+     *
+     * TODO:  Throws an SQLite exception on a missing DB file (e.g. unknown URI) instead of just
+     * returning the incoming uri, as it should.
+     */
+    public static Uri resolveAttachmentIdToContentUri(ContentResolver resolver, Uri attachmentUri) {
+        Cursor c = resolver.query(attachmentUri,
+                new String[] { Columns.DATA },
+                null, null, null);
+        if (c != null) {
+            try {
+                if (c.moveToFirst()) {
+                    final String strUri = c.getString(0);
+                    if (strUri != null) {
+                        return Uri.parse(strUri);
+                    }
+                }
+            } finally {
+                c.close();
+            }
+        }
+        return attachmentUri;
+    }
+
+    /**
+     * In support of deleting a message, find all attachments and delete associated attachment
+     * files.
+     * @param context
+     * @param accountId the account for the message
+     * @param messageId the message
+     */
+    public static void deleteAllAttachmentFiles(Context context, long accountId, long messageId) {
+        Uri uri = ContentUris.withAppendedId(Attachment.MESSAGE_ID_URI, messageId);
+        Cursor c = context.getContentResolver().query(uri, Attachment.ID_PROJECTION,
+                null, null, null);
+        try {
+            while (c.moveToNext()) {
+                long attachmentId = c.getLong(Attachment.ID_PROJECTION_COLUMN);
+                File attachmentFile = getAttachmentFilename(context, accountId, attachmentId);
+                // Note, delete() throws no exceptions for basic FS errors (e.g. file not found)
+                // it just returns false, which we ignore, and proceed to the next file.
+                // This entire loop is best-effort only.
+                attachmentFile.delete();
+            }
+        } finally {
+            c.close();
+        }
+    }
+
+    /**
+     * In support of deleting a mailbox, find all messages and delete their attachments.
+     *
+     * @param context
+     * @param accountId the account for the mailbox
+     * @param mailboxId the mailbox for the messages
+     */
+    public static void deleteAllMailboxAttachmentFiles(Context context, long accountId,
+            long mailboxId) {
+        Cursor c = context.getContentResolver().query(Message.CONTENT_URI,
+                Message.ID_COLUMN_PROJECTION, MessageColumns.MAILBOX_KEY + "=?",
+                new String[] { Long.toString(mailboxId) }, null);
+        try {
+            while (c.moveToNext()) {
+                long messageId = c.getLong(Message.ID_PROJECTION_COLUMN);
+                deleteAllAttachmentFiles(context, accountId, messageId);
+            }
+        } finally {
+            c.close();
+        }
+    }
+
+    /**
+     * In support of deleting or wiping an account, delete all related attachments.
+     *
+     * @param context
+     * @param accountId the account to scrub
+     */
+    public static void deleteAllAccountAttachmentFiles(Context context, long accountId) {
+        File[] files = getAttachmentDirectory(context, accountId).listFiles();
+        if (files == null) return;
+        for (File file : files) {
+            boolean result = file.delete();
+            if (!result) {
+                Log.e(Logging.LOG_TAG, "Failed to delete attachment file " + file.getName());
+            }
+        }
+    }
+}
diff --git a/email2/emailcommon/src/com/android/emailcommon/utility/CertificateRequestor.java b/email2/emailcommon/src/com/android/emailcommon/utility/CertificateRequestor.java
new file mode 100644
index 0000000..b78895b
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/utility/CertificateRequestor.java
@@ -0,0 +1,68 @@
+/*
+ * 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 android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.security.KeyChain;
+import android.security.KeyChainAliasCallback;
+
+/**
+ * A headless Activity which simply calls into the framework {@link KeyChain} service to select
+ * a certificate to use for establishing secure connections in the Email app.
+ */
+public class CertificateRequestor extends Activity implements KeyChainAliasCallback {
+
+    public static final String ACTION_REQUEST_CERT = "com.android.emailcommon.REQUEST_CERT";
+
+    public static final String EXTRA_HOST = "CertificateRequestor.host";
+    public static final String EXTRA_PORT = "CertificateRequestor.port";
+
+    public static final String RESULT_ALIAS = "CertificateRequestor.alias";
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        Intent i = getIntent();
+        String host = i.getStringExtra(EXTRA_HOST);
+        int port = i.getIntExtra(EXTRA_PORT, -1);
+
+        KeyChain.choosePrivateKeyAlias(
+                this, this,
+                null /* keytypes */, null /* issuers */,
+                host, port,
+                null /* alias */);
+    }
+
+    /**
+     * Callback for the certificate request. Does not happen on the UI thread.
+     */
+    @Override
+    public void alias(String alias) {
+        if (alias == null) {
+            setResult(RESULT_CANCELED);
+        } else {
+            Intent data = new Intent();
+            data.putExtra(RESULT_ALIAS, alias);
+            setResult(RESULT_OK, data);
+        }
+        finish();
+    }
+}
diff --git a/email2/emailcommon/src/com/android/emailcommon/utility/ConversionUtilities.java b/email2/emailcommon/src/com/android/emailcommon/utility/ConversionUtilities.java
new file mode 100644
index 0000000..41ba12d
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/utility/ConversionUtilities.java
@@ -0,0 +1,138 @@
+/*
+ * 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 com.android.emailcommon.provider.EmailContent;
+
+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;
+    }
+
+    /**
+     * Copy body text (plain and/or HTML) from MimeMessage to provider Message
+     */
+    public static boolean updateBodyFields(EmailContent.Body body,
+            EmailContent.Message localMessage, ArrayList<Part> viewables)
+    throws MessagingException {
+
+        body.mMessageKey = localMessage.mId;
+
+        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) {
+                boolean isQuotedReply = BODY_QUOTED_PART_REPLY.equalsIgnoreCase(replyTag);
+                boolean isQuotedForward = BODY_QUOTED_PART_FORWARD.equalsIgnoreCase(replyTag);
+                boolean isQuotedIntro = BODY_QUOTED_PART_INTRO.equalsIgnoreCase(replyTag);
+
+                if (isQuotedReply || isQuotedForward) {
+                    if (isHtml) {
+                        sbHtmlReply = appendTextPart(sbHtmlReply, text);
+                    } else {
+                        sbTextReply = appendTextPart(sbTextReply, text);
+                    }
+                    // Set message flags as well
+                    localMessage.mFlags &= ~EmailContent.Message.FLAG_TYPE_MASK;
+                    localMessage.mFlags |= isQuotedReply
+                        ? EmailContent.Message.FLAG_TYPE_REPLY
+                            : EmailContent.Message.FLAG_TYPE_FORWARD;
+                    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();
+            body.mTextContent = text;
+            localMessage.mSnippet = TextUtilities.makeSnippetFromPlainText(text);
+        }
+        if (!TextUtils.isEmpty(sbHtml)) {
+            String text = sbHtml.toString();
+            body.mHtmlContent = text;
+            if (localMessage.mSnippet == null) {
+                localMessage.mSnippet = TextUtilities.makeSnippetFromHtmlText(text);
+            }
+        }
+        if (sbHtmlReply != null && sbHtmlReply.length() != 0) {
+            body.mHtmlReply = sbHtmlReply.toString();
+        }
+        if (sbTextReply != null && sbTextReply.length() != 0) {
+            body.mTextReply = sbTextReply.toString();
+        }
+        if (sbIntroText != null && sbIntroText.length() != 0) {
+            body.mIntroText = sbIntroText.toString();
+        }
+        return true;
+    }
+}
diff --git a/email2/emailcommon/src/com/android/emailcommon/utility/DelayedOperations.java b/email2/emailcommon/src/com/android/emailcommon/utility/DelayedOperations.java
new file mode 100644
index 0000000..29324a3
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/utility/DelayedOperations.java
@@ -0,0 +1,106 @@
+/*
+ * 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.google.common.annotations.VisibleForTesting;
+
+import android.os.Handler;
+
+import java.util.ArrayList;
+import java.util.LinkedList;
+
+/**
+ * Class that helps post {@link Runnable}s to a {@link Handler}, and cancel pending ones
+ * at once.
+ */
+public class DelayedOperations {
+    private final Handler mHandler;
+
+    @VisibleForTesting
+    final LinkedList<QueuedOperation> mPendingOperations = new LinkedList<QueuedOperation>();
+
+    private class QueuedOperation implements Runnable {
+        private final Runnable mActualRannable;
+
+        public QueuedOperation(Runnable actualRannable) {
+            mActualRannable = actualRannable;
+        }
+
+        @Override
+        public void run() {
+            mPendingOperations.remove(this);
+            mActualRannable.run();
+        }
+
+        public void cancel() {
+            mPendingOperations.remove(this);
+            cancelRunnable(this);
+        }
+    }
+
+    public DelayedOperations(Handler handler) {
+        mHandler = handler;
+    }
+
+    /**
+     * Post a {@link Runnable} to the handler.  Equivalent to {@link Handler#post(Runnable)}.
+     */
+    public void post(Runnable r) {
+        final QueuedOperation qo = new QueuedOperation(r);
+        mPendingOperations.add(qo);
+        postRunnable(qo);
+    }
+
+    /**
+     * Cancel a runnable that's been posted with {@link #post(Runnable)}.
+     *
+     * Equivalent to {@link Handler#removeCallbacks(Runnable)}.
+     */
+    public void removeCallbacks(Runnable r) {
+        QueuedOperation found = null;
+        for (QueuedOperation qo : mPendingOperations) {
+            if (qo.mActualRannable == r) {
+                found = qo;
+                break;
+            }
+        }
+        if (found != null) {
+            found.cancel();
+        }
+    }
+
+    /**
+     * Cancel all pending {@link Runnable}s.
+     */
+    public void removeCallbacks() {
+        // To avoid ConcurrentModificationException
+        final ArrayList<QueuedOperation> temp = new ArrayList<QueuedOperation>(mPendingOperations);
+        for (QueuedOperation qo : temp) {
+            qo.cancel();
+        }
+    }
+
+    /** Overridden by test, as Handler is not mockable. */
+    void postRunnable(Runnable r) {
+        mHandler.post(r);
+    }
+
+    /** Overridden by test, as Handler is not mockable. */
+    void cancelRunnable(Runnable r) {
+        mHandler.removeCallbacks(r);
+    }
+}
diff --git a/email2/emailcommon/src/com/android/emailcommon/utility/EmailAsyncTask.java b/email2/emailcommon/src/com/android/emailcommon/utility/EmailAsyncTask.java
new file mode 100644
index 0000000..3d2e69c
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/utility/EmailAsyncTask.java
@@ -0,0 +1,274 @@
+/*
+ * 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 android.os.AsyncTask;
+
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executor;
+
+/**
+ * {@link AsyncTask} substitution for the email app.
+ *
+ * Modeled after {@link AsyncTask}; the basic usage is the same, with extra features:
+ * - Bulk cancellation of multiple tasks.  This is mainly used by UI to cancel pending tasks
+ *   in onDestroy() or similar places.
+ * - Instead of {@link AsyncTask#onPostExecute}, it has {@link #onSuccess(Object)}, as the
+ *   regular {@link AsyncTask#onPostExecute} is a bit hard to predict when it'll be called and
+ *   whel it won't.
+ *
+ * Note this class is missing some of the {@link AsyncTask} features, e.g. it lacks
+ * {@link AsyncTask#onProgressUpdate}.  Add these when necessary.
+ */
+public abstract class EmailAsyncTask<Params, Progress, Result> {
+    private static final Executor SERIAL_EXECUTOR = AsyncTask.SERIAL_EXECUTOR;
+    private static final Executor PARALLEL_EXECUTOR = AsyncTask.THREAD_POOL_EXECUTOR;
+
+    /**
+     * Tracks {@link EmailAsyncTask}.
+     *
+     * Call {@link #cancellAllInterrupt()} to cancel all tasks registered.
+     */
+    public static class Tracker {
+        private final LinkedList<EmailAsyncTask<?, ?, ?>> mTasks =
+                new LinkedList<EmailAsyncTask<?, ?, ?>>();
+
+        private void add(EmailAsyncTask<?, ?, ?> task) {
+            synchronized (mTasks) {
+                mTasks.add(task);
+            }
+        }
+
+        private void remove(EmailAsyncTask<?, ?, ?> task) {
+            synchronized (mTasks) {
+                mTasks.remove(task);
+            }
+        }
+
+        /**
+         * Cancel all registered tasks.
+         */
+        public void cancellAllInterrupt() {
+            synchronized (mTasks) {
+                for (EmailAsyncTask<?, ?, ?> task : mTasks) {
+                    task.cancel(true);
+                }
+                mTasks.clear();
+            }
+        }
+
+        /**
+         * Cancel all instances of the same class as {@code current} other than
+         * {@code current} itself.
+         */
+        /* package */ void cancelOthers(EmailAsyncTask<?, ?, ?> current) {
+            final Class<?> clazz = current.getClass();
+            synchronized (mTasks) {
+                final ArrayList<EmailAsyncTask<?, ?, ?>> toRemove =
+                        new ArrayList<EmailAsyncTask<?, ?, ?>>();
+                for (EmailAsyncTask<?, ?, ?> task : mTasks) {
+                    if ((task != current) && task.getClass().equals(clazz)) {
+                        task.cancel(true);
+                        toRemove.add(task);
+                    }
+                }
+                for (EmailAsyncTask<?, ?, ?> task : toRemove) {
+                    mTasks.remove(task);
+                }
+            }
+        }
+
+        /* package */ int getTaskCountForTest() {
+            return mTasks.size();
+        }
+
+        /* package */ boolean containsTaskForTest(EmailAsyncTask<?, ?, ?> task) {
+            return mTasks.contains(task);
+        }
+    }
+
+    private final Tracker mTracker;
+
+    private static class InnerTask<Params2, Progress2, Result2>
+            extends AsyncTask<Params2, Progress2, Result2> {
+        private final EmailAsyncTask<Params2, Progress2, Result2> mOwner;
+
+        public InnerTask(EmailAsyncTask<Params2, Progress2, Result2> owner) {
+            mOwner = owner;
+        }
+
+        @Override
+        protected Result2 doInBackground(Params2... params) {
+            return mOwner.doInBackground(params);
+        }
+
+        @Override
+        public void onCancelled(Result2 result) {
+            mOwner.unregisterSelf();
+            mOwner.onCancelled(result);
+        }
+
+        @Override
+        public void onPostExecute(Result2 result) {
+            mOwner.unregisterSelf();
+            if (mOwner.mCancelled) {
+                mOwner.onCancelled(result);
+            } else {
+                mOwner.onSuccess(result);
+            }
+        }
+    }
+
+    private final InnerTask<Params, Progress, Result> mInnerTask;
+    private volatile boolean mCancelled;
+
+    public EmailAsyncTask(Tracker tracker) {
+        mTracker = tracker;
+        if (mTracker != null) {
+            mTracker.add(this);
+        }
+        mInnerTask = new InnerTask<Params, Progress, Result>(this);
+    }
+
+    /* package */ final void unregisterSelf() {
+        if (mTracker != null) {
+            mTracker.remove(this);
+        }
+    }
+
+    /** @see AsyncTask#doInBackground */
+    protected abstract Result doInBackground(Params... params);
+
+
+    /** @see AsyncTask#cancel(boolean) */
+    public final void cancel(boolean mayInterruptIfRunning) {
+        mCancelled = true;
+        mInnerTask.cancel(mayInterruptIfRunning);
+    }
+
+    /** @see AsyncTask#onCancelled */
+    protected void onCancelled(Result result) {
+    }
+
+    /**
+     * Similar to {@link AsyncTask#onPostExecute}, but this will never be executed if
+     * {@link #cancel(boolean)} has been called before its execution, even if
+     * {@link #doInBackground(Object...)} has completed when cancelled.
+     *
+     * @see AsyncTask#onPostExecute
+     */
+    protected void onSuccess(Result result) {
+    }
+
+    /**
+     * execute on {@link #PARALLEL_EXECUTOR}
+     *
+     * @see AsyncTask#execute
+     */
+    public final EmailAsyncTask<Params, Progress, Result> executeParallel(Params... params) {
+        return executeInternal(PARALLEL_EXECUTOR, false, params);
+    }
+
+    /**
+     * execute on {@link #SERIAL_EXECUTOR}
+     *
+     * @see AsyncTask#execute
+     */
+    public final EmailAsyncTask<Params, Progress, Result> executeSerial(Params... params) {
+        return executeInternal(SERIAL_EXECUTOR, false, params);
+    }
+
+    /**
+     * Cancel all previously created instances of the same class tracked by the same
+     * {@link Tracker}, and then {@link #executeParallel}.
+     */
+    public final EmailAsyncTask<Params, Progress, Result> cancelPreviousAndExecuteParallel(
+            Params... params) {
+        return executeInternal(PARALLEL_EXECUTOR, true, params);
+    }
+
+    /**
+     * Cancel all previously created instances of the same class tracked by the same
+     * {@link Tracker}, and then {@link #executeSerial}.
+     */
+    public final EmailAsyncTask<Params, Progress, Result> cancelPreviousAndExecuteSerial(
+            Params... params) {
+        return executeInternal(SERIAL_EXECUTOR, true, params);
+    }
+
+    private final EmailAsyncTask<Params, Progress, Result> executeInternal(Executor executor,
+            boolean cancelPrevious, Params... params) {
+        if (cancelPrevious) {
+            if (mTracker == null) {
+                throw new IllegalStateException();
+            } else {
+                mTracker.cancelOthers(this);
+            }
+        }
+        mInnerTask.executeOnExecutor(executor, params);
+        return this;
+    }
+
+    /**
+     * Runs a {@link Runnable} in a bg thread, using {@link #PARALLEL_EXECUTOR}.
+     */
+    public static EmailAsyncTask<Void, Void, Void> runAsyncParallel(Runnable runnable) {
+        return runAsyncInternal(PARALLEL_EXECUTOR, runnable);
+    }
+
+    /**
+     * Runs a {@link Runnable} in a bg thread, using {@link #SERIAL_EXECUTOR}.
+     */
+    public static EmailAsyncTask<Void, Void, Void> runAsyncSerial(Runnable runnable) {
+        return runAsyncInternal(SERIAL_EXECUTOR, runnable);
+    }
+
+    private static EmailAsyncTask<Void, Void, Void> runAsyncInternal(Executor executor,
+            final Runnable runnable) {
+        EmailAsyncTask<Void, Void, Void> task = new EmailAsyncTask<Void, Void, Void>(null) {
+            @Override
+            protected Void doInBackground(Void... params) {
+                runnable.run();
+                return null;
+            }
+        };
+        return task.executeInternal(executor, false, (Void[]) null);
+    }
+
+    /**
+     * Wait until {@link #doInBackground} finishes and returns the results of the computation.
+     *
+     * @see AsyncTask#get
+     */
+    public final Result get() throws InterruptedException, ExecutionException {
+        return mInnerTask.get();
+    }
+
+    /* package */ final Result callDoInBackgroundForTest(Params... params) {
+        return mInnerTask.doInBackground(params);
+    }
+
+    /* package */ final void callOnCancelledForTest(Result result) {
+        mInnerTask.onCancelled(result);
+    }
+
+    /* package */ final void callOnPostExecuteForTest(Result result) {
+        mInnerTask.onPostExecute(result);
+    }
+}
diff --git a/email2/emailcommon/src/com/android/emailcommon/utility/EmailClientConnectionManager.java b/email2/emailcommon/src/com/android/emailcommon/utility/EmailClientConnectionManager.java
new file mode 100644
index 0000000..36ece42
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/utility/EmailClientConnectionManager.java
@@ -0,0 +1,148 @@
+/*
+ * 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 android.content.Context;
+import android.net.SSLCertificateSocketFactory;
+import android.util.Log;
+
+import com.android.emailcommon.Logging;
+import com.android.emailcommon.utility.SSLUtils.KeyChainKeyManager;
+import com.android.emailcommon.utility.SSLUtils.TrackingKeyManager;
+
+import org.apache.http.conn.scheme.PlainSocketFactory;
+import org.apache.http.conn.scheme.Scheme;
+import org.apache.http.conn.scheme.SchemeRegistry;
+import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
+import org.apache.http.params.HttpParams;
+
+import java.security.cert.CertificateException;
+
+import javax.net.ssl.KeyManager;
+
+/**
+ * A thread-safe client connection manager that manages the use of client certificates from the
+ * {@link android.security.KeyChain} for SSL connections.
+ */
+public class EmailClientConnectionManager extends ThreadSafeClientConnManager {
+
+    private static final boolean LOG_ENABLED = false;
+
+    /**
+     * A {@link KeyManager} to track client certificate requests from servers.
+     */
+    private final TrackingKeyManager mTrackingKeyManager;
+
+    /**
+     * Not publicly instantiable except via {@link #newInstance(HttpParams)}
+     */
+    private EmailClientConnectionManager(
+            HttpParams params, SchemeRegistry registry, TrackingKeyManager keyManager) {
+        super(params, registry);
+        mTrackingKeyManager = keyManager;
+    }
+
+    public static EmailClientConnectionManager newInstance(HttpParams params) {
+        TrackingKeyManager keyManager = new TrackingKeyManager();
+
+        // Create a registry for our three schemes; http and https will use built-in factories
+        SchemeRegistry registry = new SchemeRegistry();
+        registry.register(new Scheme("http",
+                PlainSocketFactory.getSocketFactory(), 80));
+        registry.register(new Scheme("https",
+                SSLUtils.getHttpSocketFactory(false, keyManager), 443));
+
+        // Register the httpts scheme with our insecure factory
+        registry.register(new Scheme("httpts",
+                SSLUtils.getHttpSocketFactory(true /*insecure*/, keyManager), 443));
+
+        return new EmailClientConnectionManager(params, registry, keyManager);
+    }
+
+    /**
+     * Ensures that a client SSL certificate is known to be used for the specified connection
+     * manager.
+     * A {@link SchemeRegistry} is used to denote which client certificates to use for a given
+     * connection, so clients of this connection manager should use
+     * {@link #makeSchemeForClientCert(String, boolean)}.
+     */
+    public synchronized void registerClientCert(
+            Context context, String clientCertAlias, boolean trustAllServerCerts)
+            throws CertificateException {
+        SchemeRegistry registry = getSchemeRegistry();
+        String schemeName = makeSchemeForClientCert(clientCertAlias, trustAllServerCerts);
+        Scheme existing = registry.get(schemeName);
+        if (existing == null) {
+            if (LOG_ENABLED) {
+                Log.i(Logging.LOG_TAG, "Registering socket factory for certificate alias ["
+                        + clientCertAlias + "]");
+            }
+            KeyManager keyManager = KeyChainKeyManager.fromAlias(context, clientCertAlias);
+            SSLCertificateSocketFactory underlying = SSLUtils.getSSLSocketFactory(
+                    trustAllServerCerts);
+            underlying.setKeyManagers(new KeyManager[] { keyManager });
+            registry.register(new Scheme(schemeName, new SSLSocketFactory(underlying), 443));
+        }
+    }
+
+    /**
+     * Unregisters a custom connection type that uses a client certificate on the connection
+     * manager.
+     * @see #registerClientCert(Context, String, boolean)
+     */
+    public synchronized void unregisterClientCert(
+            String clientCertAlias, boolean trustAllServerCerts) {
+        SchemeRegistry registry = getSchemeRegistry();
+        String schemeName = makeSchemeForClientCert(clientCertAlias, trustAllServerCerts);
+        Scheme existing = registry.get(schemeName);
+        if (existing != null) {
+            registry.unregister(schemeName);
+        }
+    }
+
+    /**
+     * Builds a custom scheme name to be used in a connection manager according to the connection
+     * parameters.
+     */
+    public static String makeScheme(
+            boolean useSsl, boolean trustAllServerCerts, String clientCertAlias) {
+        if (clientCertAlias != null) {
+            return makeSchemeForClientCert(clientCertAlias, trustAllServerCerts);
+        } else {
+            return useSsl ? (trustAllServerCerts ? "httpts" : "https") : "http";
+        }
+    }
+
+    /**
+     * Builds a unique scheme name for an SSL connection that uses a client user certificate.
+     */
+    private static String makeSchemeForClientCert(
+            String clientCertAlias, boolean trustAllServerCerts) {
+        String safeAlias = SSLUtils.escapeForSchemeName(clientCertAlias);
+        return (trustAllServerCerts ? "httpts" : "https") + "+clientCert+" + safeAlias;
+    }
+
+    /**
+     * @param since A timestamp in millis from epoch from which to check
+     * @return whether or not this connection manager has detected any unsatisfied requests for
+     *     a client SSL certificate by any servers
+     */
+    public synchronized boolean hasDetectedUnsatisfiedCertReq(long since) {
+        return mTrackingKeyManager.getLastCertReqTime() >= since;
+    }
+}
diff --git a/email2/emailcommon/src/com/android/emailcommon/utility/IntentUtilities.java b/email2/emailcommon/src/com/android/emailcommon/utility/IntentUtilities.java
new file mode 100644
index 0000000..d38caad
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/utility/IntentUtilities.java
@@ -0,0 +1,166 @@
+/*
+ * 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 android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.text.TextUtils;
+
+public final class IntentUtilities {
+    // Format for activity URIs: content://ui.email.android.com/...
+    private static final String ACTIVITY_INTENT_SCHEME = "content";
+    private static final String ACTIVITY_INTENT_HOST = "ui.email.android.com";
+
+    private static final String ACCOUNT_ID_PARAM = "ACCOUNT_ID";
+    private static final String MAILBOX_ID_PARAM = "MAILBOX_ID";
+    private static final String MESSAGE_ID_PARAM = "MESSAGE_ID";
+    private static final String ACCOUNT_UUID_PARAM = "ACCOUNT_UUID";
+
+    private IntentUtilities() {
+    }
+
+    /**
+     * @return a URI builder for "content://ui.email.android.com/..."
+     */
+    public static Uri.Builder createActivityIntentUrlBuilder(String path) {
+        final Uri.Builder b = new Uri.Builder();
+        b.scheme(ACTIVITY_INTENT_SCHEME);
+        b.authority(ACTIVITY_INTENT_HOST);
+        b.path(path);
+        return b;
+    }
+
+    /**
+     * Add the account ID parameter.
+     */
+    public static void setAccountId(Uri.Builder b, long accountId) {
+        if (accountId != -1) {
+            b.appendQueryParameter(ACCOUNT_ID_PARAM, Long.toString(accountId));
+        }
+    }
+
+    /**
+     * Add the mailbox ID parameter.
+     */
+    public static void setMailboxId(Uri.Builder b, long mailboxId) {
+        if (mailboxId != -1) {
+            b.appendQueryParameter(MAILBOX_ID_PARAM, Long.toString(mailboxId));
+        }
+    }
+
+    /**
+     * Add the message ID parameter.
+     */
+    public static void setMessageId(Uri.Builder b, long messageId) {
+        if (messageId != -1) {
+            b.appendQueryParameter(MESSAGE_ID_PARAM, Long.toString(messageId));
+        }
+    }
+
+    /**
+     * Add the account UUID parameter.
+     */
+    public static void setAccountUuid(Uri.Builder b, String mUuid) {
+        if (TextUtils.isEmpty(mUuid)) {
+            throw new IllegalArgumentException();
+        }
+        b.appendQueryParameter(ACCOUNT_UUID_PARAM, mUuid);
+    }
+
+    /**
+     * Retrieve the account ID.
+     */
+    public static long getAccountIdFromIntent(Intent intent) {
+        return getLongFromIntent(intent, ACCOUNT_ID_PARAM);
+    }
+
+    /**
+     * Retrieve the mailbox ID.
+     */
+    public static long getMailboxIdFromIntent(Intent intent) {
+        return getLongFromIntent(intent, MAILBOX_ID_PARAM);
+    }
+
+    /**
+     * Retrieve the message ID.
+     */
+    public static long getMessageIdFromIntent(Intent intent) {
+        return getLongFromIntent(intent, MESSAGE_ID_PARAM);
+    }
+
+    /**
+     * Retrieve the account UUID, or null if the UUID param is not found.
+     */
+    public static String getAccountUuidFromIntent(Intent intent) {
+        final Uri uri = intent.getData();
+        if (uri == null) {
+            return null;
+        }
+        String uuid = uri.getQueryParameter(ACCOUNT_UUID_PARAM);
+        return TextUtils.isEmpty(uuid) ? null : uuid;
+    }
+
+    private static long getLongFromIntent(Intent intent, String paramName) {
+        long value = -1;
+        if (intent.getData() != null) {
+            value = getLongParamFromUri(intent.getData(), paramName, -1);
+        }
+        return value;
+    }
+
+    private static long getLongParamFromUri(Uri uri, String paramName, long defaultValue) {
+        final String value = uri.getQueryParameter(paramName);
+        if (!TextUtils.isEmpty(value)) {
+            try {
+                return Long.parseLong(value);
+            } catch (NumberFormatException e) {
+                // return default
+            }
+        }
+        return defaultValue;
+    }
+
+    /**
+     * Create an {@link Intent} to launch an activity as the main entry point.  Existing activities
+     * will all be closed.
+     */
+    public static Intent createRestartAppIntent(Context context, Class<? extends Activity> clazz) {
+        Intent i = new Intent(context, clazz);
+        prepareRestartAppIntent(i);
+        return i;
+    }
+
+    /**
+     * Create an {@link Intent} to launch an activity as the main entry point.  Existing activities
+     * will all be closed.
+     */
+    public static Intent createRestartAppIntent(Uri data) {
+        Intent i = new Intent(Intent.ACTION_MAIN, data);
+        prepareRestartAppIntent(i);
+        return i;
+    }
+
+    private static void prepareRestartAppIntent(Intent i) {
+        i.setAction(Intent.ACTION_MAIN);
+        i.addCategory(Intent.CATEGORY_LAUNCHER);
+        i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+        i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+    }
+}
diff --git a/email2/emailcommon/src/com/android/emailcommon/utility/LoggingInputStream.java b/email2/emailcommon/src/com/android/emailcommon/utility/LoggingInputStream.java
new file mode 100644
index 0000000..212efa1
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/utility/LoggingInputStream.java
@@ -0,0 +1,109 @@
+/*
+ * 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.utility;
+
+import com.android.emailcommon.Logging;
+
+import android.util.Log;
+
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Simple class used for debugging only that affords us a view of the raw IMAP or POP3 stream,
+ * in addition to the tokenized version.
+ * 
+ * Use of this class *MUST* be restricted to logging-enabled situations only.
+ */
+public class LoggingInputStream extends FilterInputStream {
+    private StringBuilder mSb;
+    private boolean mDumpEmptyLines;
+    private final String mTag;
+
+    public LoggingInputStream(InputStream in) {
+        this(in, "RAW", false);
+    }
+
+    public LoggingInputStream(InputStream in, String tag, boolean dumpEmptyLines) {
+        super(in);
+        mTag = tag + " ";
+        mDumpEmptyLines = dumpEmptyLines;
+        initBuffer();
+        Log.d(Logging.LOG_TAG, mTag + "dump start");
+    }
+
+    private void initBuffer() {
+        mSb = new StringBuilder(mTag);
+    }
+
+    /**
+     * Collect chars as read, and log them when EOL reached.
+     */
+    @Override
+    public int read() throws IOException {
+        int oneByte = super.read();
+        logRaw(oneByte);
+        return oneByte;
+    }
+
+    /**
+     * Collect chars as read, and log them when EOL reached.
+     */
+    @Override
+    public int read(byte[] b, int offset, int length) throws IOException {
+        int bytesRead = super.read(b, offset, length);
+        int copyBytes = bytesRead;
+        while (copyBytes > 0) {
+            logRaw(b[offset] & 0xFF);
+            copyBytes--;
+            offset++;
+        }
+
+        return bytesRead;
+    }
+
+    /**
+     * Write and clear the buffer
+     */
+    private void logRaw(int oneByte) {
+        if (oneByte == '\r') {
+            // Don't log.
+        } else if (oneByte == '\n') {
+            flushLog();
+        } else if (0x20 <= oneByte && oneByte <= 0x7e) { // Printable ASCII.
+            mSb.append((char)oneByte);
+        } else {
+            // email protocols are supposed to be all 7bits, but there are wrong implementations
+            // that do send 8 bit characters...
+            mSb.append("\\x" + Utility.byteToHex(oneByte));
+        }
+    }
+
+    private void flushLog() {
+        if (mDumpEmptyLines || (mSb.length() > mTag.length())) {
+            Log.d(Logging.LOG_TAG, mSb.toString());
+            initBuffer();
+        }
+    }
+
+    @Override
+    public void close() throws IOException {
+        super.close();
+        flushLog();
+    }
+}
diff --git a/email2/emailcommon/src/com/android/emailcommon/utility/SSLSocketFactory.java b/email2/emailcommon/src/com/android/emailcommon/utility/SSLSocketFactory.java
new file mode 100644
index 0000000..6247455
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/utility/SSLSocketFactory.java
@@ -0,0 +1,401 @@
+/*
+ * $HeadURL: http://svn.apache.org/repos/asf/httpcomponents/httpclient/trunk/module-client/src/main/java/org/apache/http/conn/ssl/SSLSocketFactory.java $
+ * $Revision: 659194 $
+ * $Date: 2008-05-22 11:33:47 -0700 (Thu, 22 May 2008) $
+ *
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation.  For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ * This class was copied from org.apache.http.conn.ssl, because it didn't have a suitable
+ * constructor.
+ */
+
+package com.android.emailcommon.utility;
+
+import org.apache.http.conn.scheme.HostNameResolver;
+import org.apache.http.conn.scheme.LayeredSocketFactory;
+import org.apache.http.conn.ssl.AllowAllHostnameVerifier;
+import org.apache.http.conn.ssl.BrowserCompatHostnameVerifier;
+import org.apache.http.conn.ssl.StrictHostnameVerifier;
+import org.apache.http.conn.ssl.X509HostnameVerifier;
+import org.apache.http.params.HttpConnectionParams;
+import org.apache.http.params.HttpParams;
+
+import javax.net.ssl.HttpsURLConnection;
+import javax.net.ssl.KeyManager;
+import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSocket;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.TrustManagerFactory;
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.net.UnknownHostException;
+import java.security.KeyManagementException;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.security.UnrecoverableKeyException;
+
+/**
+ * Layered socket factory for TLS/SSL connections, based on JSSE.
+ *.
+ * <p>
+ * SSLSocketFactory can be used to validate the identity of the HTTPS
+ * server against a list of trusted certificates and to authenticate to
+ * the HTTPS server using a private key.
+ * </p>
+ *
+ * <p>
+ * SSLSocketFactory will enable server authentication when supplied with
+ * a {@link KeyStore truststore} file containg one or several trusted
+ * certificates. The client secure socket will reject the connection during
+ * the SSL session handshake if the target HTTPS server attempts to
+ * authenticate itself with a non-trusted certificate.
+ * </p>
+ *
+ * <p>
+ * Use JDK keytool utility to import a trusted certificate and generate a truststore file:
+ *    <pre>
+ *     keytool -import -alias "my server cert" -file server.crt -keystore my.truststore
+ *    </pre>
+ * </p>
+ *
+ * <p>
+ * SSLSocketFactory will enable client authentication when supplied with
+ * a {@link KeyStore keystore} file containg a private key/public certificate
+ * pair. The client secure socket will use the private key to authenticate
+ * itself to the target HTTPS server during the SSL session handshake if
+ * requested to do so by the server.
+ * The target HTTPS server will in its turn verify the certificate presented
+ * by the client in order to establish client's authenticity
+ * </p>
+ *
+ * <p>
+ * Use the following sequence of actions to generate a keystore file
+ * </p>
+ *   <ul>
+ *     <li>
+ *      <p>
+ *      Use JDK keytool utility to generate a new key
+ *      <pre>keytool -genkey -v -alias "my client key" -validity 365 -keystore my.keystore</pre>
+ *      For simplicity use the same password for the key as that of the keystore
+ *      </p>
+ *     </li>
+ *     <li>
+ *      <p>
+ *      Issue a certificate signing request (CSR)
+ *      <pre>keytool -certreq -alias "my client key" -file mycertreq.csr -keystore my.keystore</pre>
+ *     </p>
+ *     </li>
+ *     <li>
+ *      <p>
+ *      Send the certificate request to the trusted Certificate Authority for signature.
+ *      One may choose to act as her own CA and sign the certificate request using a PKI
+ *      tool, such as OpenSSL.
+ *      </p>
+ *     </li>
+ *     <li>
+ *      <p>
+ *       Import the trusted CA root certificate
+ *       <pre>keytool -import -alias "my trusted ca" -file caroot.crt -keystore my.keystore</pre>
+ *      </p>
+ *     </li>
+ *     <li>
+ *      <p>
+ *       Import the PKCS#7 file containg the complete certificate chain
+ *       <pre>keytool -import -alias "my client key" -file mycert.p7 -keystore my.keystore</pre>
+ *      </p>
+ *     </li>
+ *     <li>
+ *      <p>
+ *       Verify the content the resultant keystore file
+ *       <pre>keytool -list -v -keystore my.keystore</pre>
+ *      </p>
+ *     </li>
+ *   </ul>
+ * @author <a href="mailto:oleg at ural.ru">Oleg Kalnichevski</a>
+ * @author Julius Davies
+ */
+
+public class SSLSocketFactory implements LayeredSocketFactory {
+
+    public static final String TLS   = "TLS";
+    public static final String SSL   = "SSL";
+    public static final String SSLV2 = "SSLv2";
+
+    public static final X509HostnameVerifier ALLOW_ALL_HOSTNAME_VERIFIER
+        = new AllowAllHostnameVerifier();
+
+    public static final X509HostnameVerifier BROWSER_COMPATIBLE_HOSTNAME_VERIFIER
+        = new BrowserCompatHostnameVerifier();
+
+    public static final X509HostnameVerifier STRICT_HOSTNAME_VERIFIER
+        = new StrictHostnameVerifier();
+    /**
+     * The factory using the default JVM settings for secure connections.
+     */
+    private static final SSLSocketFactory DEFAULT_FACTORY = new SSLSocketFactory();
+
+    /**
+     * Gets an singleton instance of the SSLProtocolSocketFactory.
+     * @return a SSLProtocolSocketFactory
+     */
+    public static SSLSocketFactory getSocketFactory() {
+        return DEFAULT_FACTORY;
+    }
+
+    private final SSLContext sslcontext;
+    private final javax.net.ssl.SSLSocketFactory socketfactory;
+    private final HostNameResolver nameResolver;
+    private X509HostnameVerifier hostnameVerifier = BROWSER_COMPATIBLE_HOSTNAME_VERIFIER;
+
+    public SSLSocketFactory(
+        String algorithm,
+        final KeyStore keystore,
+        final String keystorePassword,
+        final KeyStore truststore,
+        final SecureRandom random,
+        final HostNameResolver nameResolver)
+        throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException
+    {
+        super();
+        if (algorithm == null) {
+            algorithm = TLS;
+        }
+        KeyManager[] keymanagers = null;
+        if (keystore != null) {
+            keymanagers = createKeyManagers(keystore, keystorePassword);
+        }
+        TrustManager[] trustmanagers = null;
+        if (truststore != null) {
+            trustmanagers = createTrustManagers(truststore);
+        }
+        sslcontext = SSLContext.getInstance(algorithm);
+        sslcontext.init(keymanagers, trustmanagers, random);
+        socketfactory = sslcontext.getSocketFactory();
+        this.nameResolver = nameResolver;
+    }
+
+    public SSLSocketFactory(
+            final KeyStore keystore,
+            final String keystorePassword,
+            final KeyStore truststore)
+            throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException
+    {
+        this(TLS, keystore, keystorePassword, truststore, null, null);
+    }
+
+    public SSLSocketFactory(final KeyStore keystore, final String keystorePassword)
+            throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException
+    {
+        this(TLS, keystore, keystorePassword, null, null, null);
+    }
+
+    public SSLSocketFactory(final KeyStore truststore)
+            throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException
+    {
+        this(TLS, null, null, truststore, null, null);
+    }
+
+    /**
+     * Constructs an HttpClient SSLSocketFactory backed by the given JSSE
+     * SSLSocketFactory.
+     */
+    public SSLSocketFactory(javax.net.ssl.SSLSocketFactory socketfactory) {
+        super();
+        sslcontext = null;
+        this.socketfactory = socketfactory;
+        nameResolver = null;
+    }
+
+    /**
+     * Creates the default SSL socket factory.
+     * This constructor is used exclusively to instantiate the factory for
+     * {@link #getSocketFactory getSocketFactory}.
+     */
+    private SSLSocketFactory() {
+        super();
+        sslcontext = null;
+        socketfactory = HttpsURLConnection.getDefaultSSLSocketFactory();
+        nameResolver = null;
+    }
+
+    private static KeyManager[] createKeyManagers(final KeyStore keystore, final String password)
+        throws KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException {
+        if (keystore == null) {
+            throw new IllegalArgumentException("Keystore may not be null");
+        }
+        KeyManagerFactory kmfactory = KeyManagerFactory.getInstance(
+            KeyManagerFactory.getDefaultAlgorithm());
+        kmfactory.init(keystore, password != null ? password.toCharArray(): null);
+        return kmfactory.getKeyManagers();
+    }
+
+    private static TrustManager[] createTrustManagers(final KeyStore keystore)
+        throws KeyStoreException, NoSuchAlgorithmException {
+        if (keystore == null) {
+            throw new IllegalArgumentException("Keystore may not be null");
+        }
+        TrustManagerFactory tmfactory = TrustManagerFactory.getInstance(
+            TrustManagerFactory.getDefaultAlgorithm());
+        tmfactory.init(keystore);
+        return tmfactory.getTrustManagers();
+    }
+
+
+    // non-javadoc, see interface org.apache.http.conn.SocketFactory
+    public Socket createSocket()
+        throws IOException {
+
+        // the cast makes sure that the factory is working as expected
+        return socketfactory.createSocket();
+    }
+
+
+    // non-javadoc, see interface org.apache.http.conn.SocketFactory
+    public Socket connectSocket(
+        final Socket sock,
+        final String host,
+        final int port,
+        final InetAddress localAddress,
+        int localPort,
+        final HttpParams params
+    ) throws IOException {
+
+        if (host == null) {
+            throw new IllegalArgumentException("Target host may not be null.");
+        }
+        if (params == null) {
+            throw new IllegalArgumentException("Parameters may not be null.");
+        }
+
+        SSLSocket sslsock = (SSLSocket)
+            ((sock != null) ? sock : createSocket());
+
+        if ((localAddress != null) || (localPort > 0)) {
+
+            // we need to bind explicitly
+            if (localPort < 0)
+                localPort = 0; // indicates "any"
+
+            InetSocketAddress isa =
+                new InetSocketAddress(localAddress, localPort);
+            sslsock.bind(isa);
+        }
+
+        int connTimeout = HttpConnectionParams.getConnectionTimeout(params);
+        int soTimeout = HttpConnectionParams.getSoTimeout(params);
+
+        InetSocketAddress remoteAddress;
+        if (nameResolver != null) {
+            remoteAddress = new InetSocketAddress(nameResolver.resolve(host), port);
+        } else {
+            remoteAddress = new InetSocketAddress(host, port);
+        }
+
+        sslsock.connect(remoteAddress, connTimeout);
+
+        sslsock.setSoTimeout(soTimeout);
+        try {
+            hostnameVerifier.verify(host, sslsock);
+            // verifyHostName() didn't blowup - good!
+        } catch (IOException iox) {
+            // close the socket before re-throwing the exception
+            try { sslsock.close(); } catch (Exception x) { /*ignore*/ }
+            throw iox;
+        }
+
+        return sslsock;
+    }
+
+
+    /**
+     * Checks whether a socket connection is secure.
+     * This factory creates TLS/SSL socket connections
+     * which, by default, are considered secure.
+     * <br/>
+     * Derived classes may override this method to perform
+     * runtime checks, for example based on the cypher suite.
+     *
+     * @param sock      the connected socket
+     *
+     * @return  <code>true</code>
+     *
+     * @throws IllegalArgumentException if the argument is invalid
+     */
+    public boolean isSecure(Socket sock)
+        throws IllegalArgumentException {
+
+        if (sock == null) {
+            throw new IllegalArgumentException("Socket may not be null.");
+        }
+        // This instanceof check is in line with createSocket() above.
+        if (!(sock instanceof SSLSocket)) {
+            throw new IllegalArgumentException
+                ("Socket not created by this factory.");
+        }
+        // This check is performed last since it calls the argument object.
+        if (sock.isClosed()) {
+            throw new IllegalArgumentException("Socket is closed.");
+        }
+
+        return true;
+
+    } // isSecure
+
+
+    // non-javadoc, see interface LayeredSocketFactory
+    public Socket createSocket(
+        final Socket socket,
+        final String host,
+        final int port,
+        final boolean autoClose
+    ) throws IOException, UnknownHostException {
+        SSLSocket sslSocket = (SSLSocket) socketfactory.createSocket(
+              socket,
+              host,
+              port,
+              autoClose
+        );
+        hostnameVerifier.verify(host, sslSocket);
+        // verifyHostName() didn't blowup - good!
+        return sslSocket;
+    }
+
+    public void setHostnameVerifier(X509HostnameVerifier hostnameVerifier) {
+        if ( hostnameVerifier == null ) {
+            throw new IllegalArgumentException("Hostname verifier may not be null");
+        }
+        this.hostnameVerifier = hostnameVerifier;
+    }
+
+    public X509HostnameVerifier getHostnameVerifier() {
+        return hostnameVerifier;
+    }
+
+}
diff --git a/email2/emailcommon/src/com/android/emailcommon/utility/SSLUtils.java b/email2/emailcommon/src/com/android/emailcommon/utility/SSLUtils.java
new file mode 100644
index 0000000..b21c68f
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/utility/SSLUtils.java
@@ -0,0 +1,272 @@
+/*
+ * 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 android.content.Context;
+import android.net.SSLCertificateSocketFactory;
+import android.security.KeyChain;
+import android.security.KeyChainException;
+import android.util.Log;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import java.net.InetAddress;
+import java.net.Socket;
+import java.security.Principal;
+import java.security.PrivateKey;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import java.util.Arrays;
+
+import javax.net.ssl.KeyManager;
+import javax.net.ssl.X509ExtendedKeyManager;
+
+public class SSLUtils {
+    private static SSLCertificateSocketFactory sInsecureFactory;
+    private static SSLCertificateSocketFactory sSecureFactory;
+
+    private static final boolean LOG_ENABLED = false;
+    private static final String TAG = "Email.Ssl";
+
+    /**
+     * Returns a {@link javax.net.ssl.SSLSocketFactory}.
+     * Optionally bypass all SSL certificate checks.
+     *
+     * @param insecure if true, bypass all SSL certificate checks
+     */
+    public synchronized static SSLCertificateSocketFactory getSSLSocketFactory(
+            boolean insecure) {
+        if (insecure) {
+            if (sInsecureFactory == null) {
+                sInsecureFactory = (SSLCertificateSocketFactory)
+                        SSLCertificateSocketFactory.getInsecure(0, null);
+            }
+            return sInsecureFactory;
+        } else {
+            if (sSecureFactory == null) {
+                sSecureFactory = (SSLCertificateSocketFactory)
+                        SSLCertificateSocketFactory.getDefault(0, null);
+            }
+            return sSecureFactory;
+        }
+    }
+
+    /**
+     * Returns a {@link org.apache.http.conn.ssl.SSLSocketFactory SSLSocketFactory} for use with the
+     * Apache HTTP stack.
+     */
+    public static SSLSocketFactory getHttpSocketFactory(boolean insecure, KeyManager keyManager) {
+        SSLCertificateSocketFactory underlying = getSSLSocketFactory(insecure);
+        if (keyManager != null) {
+            underlying.setKeyManagers(new KeyManager[] { keyManager });
+        }
+        SSLSocketFactory wrapped = new SSLSocketFactory(underlying);
+        if (insecure) {
+            wrapped.setHostnameVerifier(SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
+        }
+        return wrapped;
+    }
+
+    /**
+     * Escapes the contents a string to be used as a safe scheme name in the URI according to
+     * http://tools.ietf.org/html/rfc3986#section-3.1
+     *
+     * This does not ensure that the first character is a letter (which is required by the RFC).
+     */
+    @VisibleForTesting
+    public static String escapeForSchemeName(String s) {
+        // According to the RFC, scheme names are case-insensitive.
+        s = s.toLowerCase();
+
+        StringBuilder sb = new StringBuilder();
+        for (int i = 0; i < s.length(); i++) {
+            char c = s.charAt(i);
+            if (Character.isLetter(c) || Character.isDigit(c)
+                    || ('-' == c) || ('.' == c)) {
+                // Safe - use as is.
+                sb.append(c);
+            } else if ('+' == c) {
+                // + is used as our escape character, so double it up.
+                sb.append("++");
+            } else {
+                // Unsafe - escape.
+                sb.append('+').append((int) c);
+            }
+        }
+        return sb.toString();
+    }
+
+    private static abstract class StubKeyManager extends X509ExtendedKeyManager {
+        @Override public abstract String chooseClientAlias(
+                String[] keyTypes, Principal[] issuers, Socket socket);
+
+        @Override public abstract X509Certificate[] getCertificateChain(String alias);
+
+        @Override public abstract PrivateKey getPrivateKey(String alias);
+
+
+        // The following methods are unused.
+
+        @Override
+        public final String chooseServerAlias(
+                String keyType, Principal[] issuers, Socket socket) {
+            // not a client SSLSocket callback
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public final String[] getClientAliases(String keyType, Principal[] issuers) {
+            // not a client SSLSocket callback
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public final String[] getServerAliases(String keyType, Principal[] issuers) {
+            // not a client SSLSocket callback
+            throw new UnsupportedOperationException();
+        }
+    }
+
+    /**
+     * A dummy {@link KeyManager} which keeps track of the last time a server has requested
+     * a client certificate.
+     */
+    public static class TrackingKeyManager extends StubKeyManager {
+        private volatile long mLastTimeCertRequested = 0L;
+
+        @Override
+        public String chooseClientAlias(String[] keyTypes, Principal[] issuers, Socket socket) {
+            if (LOG_ENABLED) {
+                InetAddress address = socket.getInetAddress();
+                Log.i(TAG, "TrackingKeyManager: requesting a client cert alias for "
+                        + address.getCanonicalHostName());
+            }
+            mLastTimeCertRequested = System.currentTimeMillis();
+            return null;
+        }
+
+        @Override
+        public X509Certificate[] getCertificateChain(String alias) {
+            if (LOG_ENABLED) {
+                Log.i(TAG, "TrackingKeyManager: returning a null cert chain");
+            }
+            return null;
+        }
+
+        @Override
+        public PrivateKey getPrivateKey(String alias) {
+            if (LOG_ENABLED) {
+                Log.i(TAG, "TrackingKeyManager: returning a null private key");
+            }
+            return null;
+        }
+
+        /**
+         * @return the last time that this {@link KeyManager} detected a request by a server
+         *     for a client certificate (in millis since epoch).
+         */
+        public long getLastCertReqTime() {
+            return mLastTimeCertRequested;
+        }
+    }
+
+    /**
+     * A {@link KeyManager} that reads uses credentials stored in the system {@link KeyChain}.
+     */
+    public static class KeyChainKeyManager extends StubKeyManager {
+        private final String mClientAlias;
+        private final X509Certificate[] mCertificateChain;
+        private final PrivateKey mPrivateKey;
+
+        /**
+         * Builds an instance of a KeyChainKeyManager using the given certificate alias.
+         * If for any reason retrieval of the credentials from the system {@link KeyChain} fails,
+         * a {@code null} value will be returned.
+         */
+        public static KeyChainKeyManager fromAlias(Context context, String alias)
+                throws CertificateException {
+            X509Certificate[] certificateChain;
+            try {
+                certificateChain = KeyChain.getCertificateChain(context, alias);
+            } catch (KeyChainException e) {
+                logError(alias, "certificate chain", e);
+                throw new CertificateException(e);
+            } catch (InterruptedException e) {
+                logError(alias, "certificate chain", e);
+                throw new CertificateException(e);
+            }
+
+            PrivateKey privateKey;
+            try {
+                privateKey = KeyChain.getPrivateKey(context, alias);
+            } catch (KeyChainException e) {
+                logError(alias, "private key", e);
+                throw new CertificateException(e);
+            } catch (InterruptedException e) {
+                logError(alias, "private key", e);
+                throw new CertificateException(e);
+            }
+
+            if (certificateChain == null || privateKey == null) {
+                throw new CertificateException("Can't access certificate from keystore");
+            }
+
+            return new KeyChainKeyManager(alias, certificateChain, privateKey);
+        }
+
+        private static void logError(String alias, String type, Exception ex) {
+            // Avoid logging PII when explicit logging is not on.
+            if (LOG_ENABLED) {
+                Log.e(TAG, "Unable to retrieve " + type + " for [" + alias + "] due to " + ex);
+            } else {
+                Log.e(TAG, "Unable to retrieve " + type + " due to " + ex);
+            }
+        }
+
+        private KeyChainKeyManager(
+                String clientAlias, X509Certificate[] certificateChain, PrivateKey privateKey) {
+            mClientAlias = clientAlias;
+            mCertificateChain = certificateChain;
+            mPrivateKey = privateKey;
+        }
+
+
+        @Override
+        public String chooseClientAlias(String[] keyTypes, Principal[] issuers, Socket socket) {
+            if (LOG_ENABLED) {
+                Log.i(TAG, "Requesting a client cert alias for " + Arrays.toString(keyTypes));
+            }
+            return mClientAlias;
+        }
+
+        @Override
+        public X509Certificate[] getCertificateChain(String alias) {
+            if (LOG_ENABLED) {
+                Log.i(TAG, "Requesting a client certificate chain for alias [" + alias + "]");
+            }
+            return mCertificateChain;
+        }
+
+        @Override
+        public PrivateKey getPrivateKey(String alias) {
+            if (LOG_ENABLED) {
+                Log.i(TAG, "Requesting a client private key for alias [" + alias + "]");
+            }
+            return mPrivateKey;
+        }
+    }
+}
diff --git a/email2/emailcommon/src/com/android/emailcommon/utility/TextUtilities.java b/email2/emailcommon/src/com/android/emailcommon/utility/TextUtilities.java
new file mode 100755
index 0000000..0aa9190
--- /dev/null
+++ b/email2/emailcommon/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/email2/emailcommon/src/com/android/emailcommon/utility/Utility.java b/email2/emailcommon/src/com/android/emailcommon/utility/Utility.java
new file mode 100644
index 0000000..b74d4b3
--- /dev/null
+++ b/email2/emailcommon/src/com/android/emailcommon/utility/Utility.java
@@ -0,0 +1,1153 @@
+/*
+ * 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.utility;
+
+import android.app.Activity;
+import android.app.Fragment;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.CursorWrapper;
+import android.graphics.Typeface;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Environment;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.StrictMode;
+import android.provider.OpenableColumns;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.SpannableStringBuilder;
+import android.text.TextUtils;
+import android.text.style.StyleSpan;
+import android.util.Base64;
+import android.util.Log;
+import android.widget.ListView;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.android.emailcommon.Logging;
+import com.android.emailcommon.provider.Account;
+import com.android.emailcommon.provider.EmailContent;
+import com.android.emailcommon.provider.EmailContent.AccountColumns;
+import com.android.emailcommon.provider.EmailContent.Attachment;
+import com.android.emailcommon.provider.EmailContent.AttachmentColumns;
+import com.android.emailcommon.provider.EmailContent.HostAuthColumns;
+import com.android.emailcommon.provider.EmailContent.MailboxColumns;
+import com.android.emailcommon.provider.EmailContent.Message;
+import com.android.emailcommon.provider.HostAuth;
+import com.android.emailcommon.provider.Mailbox;
+import com.android.emailcommon.provider.ProviderUnavailableException;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileDescriptor;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.io.UnsupportedEncodingException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+import java.nio.charset.Charset;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.GregorianCalendar;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.TimeZone;
+import java.util.regex.Pattern;
+
+public class Utility {
+    public static final Charset UTF_8 = Charset.forName("UTF-8");
+    public static final Charset ASCII = Charset.forName("US-ASCII");
+
+    public static final String[] EMPTY_STRINGS = new String[0];
+    public static final Long[] EMPTY_LONGS = new Long[0];
+
+    // "GMT" + "+" or "-" + 4 digits
+    private static final Pattern DATE_CLEANUP_PATTERN_WRONG_TIMEZONE =
+            Pattern.compile("GMT([-+]\\d{4})$");
+
+    private static Handler sMainThreadHandler;
+
+    /**
+     * @return a {@link Handler} tied to the main thread.
+     */
+    public static Handler getMainThreadHandler() {
+        if (sMainThreadHandler == null) {
+            // No need to synchronize -- it's okay to create an extra Handler, which will be used
+            // only once and then thrown away.
+            sMainThreadHandler = new Handler(Looper.getMainLooper());
+        }
+        return sMainThreadHandler;
+    }
+
+    public final static String readInputStream(InputStream in, String encoding) throws IOException {
+        InputStreamReader reader = new InputStreamReader(in, encoding);
+        StringBuffer sb = new StringBuffer();
+        int count;
+        char[] buf = new char[512];
+        while ((count = reader.read(buf)) != -1) {
+            sb.append(buf, 0, count);
+        }
+        return sb.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;
+    }
+
+    /**
+     * Returns a concatenated string containing the output of every Object's
+     * toString() method, each separated by the given separator character.
+     */
+    public static String combine(Object[] parts, char separator) {
+        if (parts == null) {
+            return null;
+        }
+        StringBuffer sb = new StringBuffer();
+        for (int i = 0; i < parts.length; i++) {
+            sb.append(parts[i].toString());
+            if (i < parts.length - 1) {
+                sb.append(separator);
+            }
+        }
+        return sb.toString();
+    }
+    public static String base64Decode(String encoded) {
+        if (encoded == null) {
+            return null;
+        }
+        byte[] decoded = Base64.decode(encoded, Base64.DEFAULT);
+        return new String(decoded);
+    }
+
+    public static String base64Encode(String s) {
+        if (s == null) {
+            return s;
+        }
+        return Base64.encodeToString(s.getBytes(), Base64.NO_WRAP);
+    }
+
+    public static boolean isTextViewNotEmpty(TextView view) {
+        return !TextUtils.isEmpty(view.getText());
+    }
+
+    public static boolean isPortFieldValid(TextView view) {
+        CharSequence chars = view.getText();
+        if (TextUtils.isEmpty(chars)) return false;
+        Integer port;
+        // In theory, we can't get an illegal value here, since the field is monitored for valid
+        // numeric input. But this might be used elsewhere without such a check.
+        try {
+            port = Integer.parseInt(chars.toString());
+        } catch (NumberFormatException e) {
+            return false;
+        }
+        return port > 0 && port < 65536;
+    }
+
+    /**
+     * Validate a hostname name field.
+     *
+     * Because we just use the {@link URI} class for validation, it'll accept some invalid
+     * host names, but it works well enough...
+     */
+    public static boolean isServerNameValid(TextView view) {
+        return isServerNameValid(view.getText().toString());
+    }
+
+    public static boolean isServerNameValid(String serverName) {
+        serverName = serverName.trim();
+        if (TextUtils.isEmpty(serverName)) {
+            return false;
+        }
+        try {
+            URI uri = new URI(
+                    "http",
+                    null,
+                    serverName,
+                    -1,
+                    null, // path
+                    null, // query
+                    null);
+            return true;
+        } catch (URISyntaxException e) {
+            return false;
+        }
+    }
+
+    /**
+     * 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;
+        }
+    }
+
+    /**
+     * A fast version of  URLDecoder.decode() that works only with UTF-8 and does only two
+     * allocations. This version is around 3x as fast as the standard one and I'm using it
+     * hundreds of times in places that slow down the UI, so it helps.
+     */
+    public static String fastUrlDecode(String s) {
+        try {
+            byte[] bytes = s.getBytes("UTF-8");
+            byte ch;
+            int length = 0;
+            for (int i = 0, count = bytes.length; i < count; i++) {
+                ch = bytes[i];
+                if (ch == '%') {
+                    int h = (bytes[i + 1] - '0');
+                    int l = (bytes[i + 2] - '0');
+                    if (h > 9) {
+                        h -= 7;
+                    }
+                    if (l > 9) {
+                        l -= 7;
+                    }
+                    bytes[length] = (byte) ((h << 4) | l);
+                    i += 2;
+                }
+                else if (ch == '+') {
+                    bytes[length] = ' ';
+                }
+                else {
+                    bytes[length] = bytes[i];
+                }
+                length++;
+            }
+            return new String(bytes, 0, length, "UTF-8");
+        }
+        catch (UnsupportedEncodingException uee) {
+            return null;
+        }
+    }
+    private final static String HOSTAUTH_WHERE_CREDENTIALS = HostAuthColumns.ADDRESS + " like ?"
+            + " and " + HostAuthColumns.LOGIN + " like ?  ESCAPE '\\'"
+            + " and " + HostAuthColumns.PROTOCOL + " not like \"smtp\"";
+    private final static String ACCOUNT_WHERE_HOSTAUTH = AccountColumns.HOST_AUTH_KEY_RECV + "=?";
+
+    /**
+     * Look for an existing account with the same username & server
+     *
+     * @param context a system context
+     * @param allowAccountId this account Id will not trigger (when editing an existing account)
+     * @param hostName the server's address
+     * @param userLogin the user's login string
+     * @result null = no matching account found.  Account = matching account
+     */
+    public static Account findExistingAccount(Context context, long allowAccountId,
+            String hostName, String userLogin) {
+        ContentResolver resolver = context.getContentResolver();
+        String userName = userLogin.replace("_", "\\_");
+        Cursor c = resolver.query(HostAuth.CONTENT_URI, HostAuth.ID_PROJECTION,
+                HOSTAUTH_WHERE_CREDENTIALS, new String[] { hostName, userName }, null);
+        if (c == null) throw new ProviderUnavailableException();
+        try {
+            while (c.moveToNext()) {
+                long hostAuthId = c.getLong(HostAuth.ID_PROJECTION_COLUMN);
+                // Find account with matching hostauthrecv key, and return it
+                Cursor c2 = resolver.query(Account.CONTENT_URI, Account.ID_PROJECTION,
+                        ACCOUNT_WHERE_HOSTAUTH, new String[] { Long.toString(hostAuthId) }, null);
+                try {
+                    while (c2.moveToNext()) {
+                        long accountId = c2.getLong(Account.ID_PROJECTION_COLUMN);
+                        if (accountId != allowAccountId) {
+                            Account account = Account.restoreAccountWithId(context, accountId);
+                            if (account != null) {
+                                return account;
+                            }
+                        }
+                    }
+                } finally {
+                    c2.close();
+                }
+            }
+        } finally {
+            c.close();
+        }
+
+        return null;
+    }
+
+    /**
+     * Generate a random message-id header for locally-generated messages.
+     */
+    public static String generateMessageId() {
+        StringBuffer sb = new StringBuffer();
+        sb.append("<");
+        for (int i = 0; i < 24; i++) {
+            sb.append(Integer.toString((int)(Math.random() * 35), 36));
+        }
+        sb.append(".");
+        sb.append(Long.toString(System.currentTimeMillis()));
+        sb.append("@email.android.com>");
+        return sb.toString();
+    }
+
+    /**
+     * Generate a time in milliseconds from a date string that represents a date/time in GMT
+     * @param date string in format 20090211T180303Z (rfc2445, iCalendar).
+     * @return the time in milliseconds (since Jan 1, 1970)
+     */
+    public static long parseDateTimeToMillis(String date) {
+        GregorianCalendar cal = parseDateTimeToCalendar(date);
+        return cal.getTimeInMillis();
+    }
+
+    /**
+     * Generate a GregorianCalendar from a date string that represents a date/time in GMT
+     * @param date string in format 20090211T180303Z (rfc2445, iCalendar).
+     * @return the GregorianCalendar
+     */
+    public static GregorianCalendar parseDateTimeToCalendar(String date) {
+        GregorianCalendar cal = new GregorianCalendar(Integer.parseInt(date.substring(0, 4)),
+                Integer.parseInt(date.substring(4, 6)) - 1, Integer.parseInt(date.substring(6, 8)),
+                Integer.parseInt(date.substring(9, 11)), Integer.parseInt(date.substring(11, 13)),
+                Integer.parseInt(date.substring(13, 15)));
+        cal.setTimeZone(TimeZone.getTimeZone("GMT"));
+        return cal;
+    }
+
+    /**
+     * Generate a time in milliseconds from an email date string that represents a date/time in GMT
+     * @param date string in format 2010-02-23T16:00:00.000Z (ISO 8601, rfc3339)
+     * @return the time in milliseconds (since Jan 1, 1970)
+     */
+    public static long parseEmailDateTimeToMillis(String date) {
+        GregorianCalendar cal = new GregorianCalendar(Integer.parseInt(date.substring(0, 4)),
+                Integer.parseInt(date.substring(5, 7)) - 1, Integer.parseInt(date.substring(8, 10)),
+                Integer.parseInt(date.substring(11, 13)), Integer.parseInt(date.substring(14, 16)),
+                Integer.parseInt(date.substring(17, 19)));
+        cal.setTimeZone(TimeZone.getTimeZone("GMT"));
+        return cal.getTimeInMillis();
+    }
+
+    private static byte[] encode(Charset charset, String s) {
+        if (s == null) {
+            return null;
+        }
+        final ByteBuffer buffer = charset.encode(CharBuffer.wrap(s));
+        final byte[] bytes = new byte[buffer.limit()];
+        buffer.get(bytes);
+        return bytes;
+    }
+
+    private static String decode(Charset charset, byte[] b) {
+        if (b == null) {
+            return null;
+        }
+        final CharBuffer cb = charset.decode(ByteBuffer.wrap(b));
+        return new String(cb.array(), 0, cb.length());
+    }
+
+    /** Converts a String to UTF-8 */
+    public static byte[] toUtf8(String s) {
+        return encode(UTF_8, s);
+    }
+
+    /** Builds a String from UTF-8 bytes */
+    public static String fromUtf8(byte[] b) {
+        return decode(UTF_8, b);
+    }
+
+    /** Converts a String to ASCII bytes */
+    public static byte[] toAscii(String s) {
+        return encode(ASCII, s);
+    }
+
+    /** Builds a String from ASCII bytes */
+    public static String fromAscii(byte[] b) {
+        return decode(ASCII, b);
+    }
+
+    /**
+     * @return true if the input is the first (or only) byte in a UTF-8 character
+     */
+    public static boolean isFirstUtf8Byte(byte b) {
+        // If the top 2 bits is '10', it's not a first byte.
+        return (b & 0xc0) != 0x80;
+    }
+
+    public static String byteToHex(int b) {
+        return byteToHex(new StringBuilder(), b).toString();
+    }
+
+    public static StringBuilder byteToHex(StringBuilder sb, int b) {
+        b &= 0xFF;
+        sb.append("0123456789ABCDEF".charAt(b >> 4));
+        sb.append("0123456789ABCDEF".charAt(b & 0xF));
+        return sb;
+    }
+
+    public static String replaceBareLfWithCrlf(String str) {
+        return str.replace("\r", "").replace("\n", "\r\n");
+    }
+
+    /**
+     * Cancel an {@link AsyncTask}.  If it's already running, it'll be interrupted.
+     */
+    public static void cancelTaskInterrupt(AsyncTask<?, ?, ?> task) {
+        cancelTask(task, true);
+    }
+
+    /**
+     * Cancel an {@link EmailAsyncTask}.  If it's already running, it'll be interrupted.
+     */
+    public static void cancelTaskInterrupt(EmailAsyncTask<?, ?, ?> task) {
+        if (task != null) {
+            task.cancel(true);
+        }
+    }
+
+    /**
+     * Cancel an {@link AsyncTask}.
+     *
+     * @param mayInterruptIfRunning <tt>true</tt> if the thread executing this
+     *        task should be interrupted; otherwise, in-progress tasks are allowed
+     *        to complete.
+     */
+    public static void cancelTask(AsyncTask<?, ?, ?> task, boolean mayInterruptIfRunning) {
+        if (task != null && task.getStatus() != AsyncTask.Status.FINISHED) {
+            task.cancel(mayInterruptIfRunning);
+        }
+    }
+
+    public static String getSmallHash(final String value) {
+        final MessageDigest sha;
+        try {
+            sha = MessageDigest.getInstance("SHA-1");
+        } catch (NoSuchAlgorithmException impossible) {
+            return null;
+        }
+        sha.update(Utility.toUtf8(value));
+        final int hash = getSmallHashFromSha1(sha.digest());
+        return Integer.toString(hash);
+    }
+
+    /**
+     * @return a non-negative integer generated from 20 byte SHA-1 hash.
+     */
+    /* package for testing */ static int getSmallHashFromSha1(byte[] sha1) {
+        final int offset = sha1[19] & 0xf; // SHA1 is 20 bytes.
+        return ((sha1[offset]  & 0x7f) << 24)
+                | ((sha1[offset + 1] & 0xff) << 16)
+                | ((sha1[offset + 2] & 0xff) << 8)
+                | ((sha1[offset + 3] & 0xff));
+    }
+
+    /**
+     * Try to make a date MIME(RFC 2822/5322)-compliant.
+     *
+     * It fixes:
+     * - "Thu, 10 Dec 09 15:08:08 GMT-0700" to "Thu, 10 Dec 09 15:08:08 -0700"
+     *   (4 digit zone value can't be preceded by "GMT")
+     *   We got a report saying eBay sends a date in this format
+     */
+    public static String cleanUpMimeDate(String date) {
+        if (TextUtils.isEmpty(date)) {
+            return date;
+        }
+        date = DATE_CLEANUP_PATTERN_WRONG_TIMEZONE.matcher(date).replaceFirst("$1");
+        return date;
+    }
+
+    public static ByteArrayInputStream streamFromAsciiString(String ascii) {
+        return new ByteArrayInputStream(toAscii(ascii));
+    }
+
+    /**
+     * A thread safe way to show a Toast.  Can be called from any thread.
+     *
+     * @param context context
+     * @param resId Resource ID of the message string.
+     */
+    public static void showToast(Context context, int resId) {
+        showToast(context, context.getResources().getString(resId));
+    }
+
+    /**
+     * A thread safe way to show a Toast.  Can be called from any thread.
+     *
+     * @param context context
+     * @param message Message to show.
+     */
+    public static void showToast(final Context context, final String message) {
+        getMainThreadHandler().post(new Runnable() {
+            @Override
+            public void run() {
+                Toast.makeText(context, message, Toast.LENGTH_LONG).show();
+            }
+        });
+    }
+
+    /**
+     * Run {@code r} on a worker thread, returning the AsyncTask
+     * @return the AsyncTask; this is primarily for use by unit tests, which require the
+     * result of the task
+     *
+     * @deprecated use {@link EmailAsyncTask#runAsyncParallel} or
+     *     {@link EmailAsyncTask#runAsyncSerial}
+     */
+    @Deprecated
+    public static AsyncTask<Void, Void, Void> runAsync(final Runnable r) {
+        return new AsyncTask<Void, Void, Void>() {
+            @Override protected Void doInBackground(Void... params) {
+                r.run();
+                return null;
+            }
+        }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+    }
+
+    /**
+     * Interface used in {@link #createUniqueFile} instead of {@link File#createNewFile()} to make
+     * it testable.
+     */
+    /* package */ interface NewFileCreator {
+        public static final NewFileCreator DEFAULT = new NewFileCreator() {
+                    @Override public boolean createNewFile(File f) throws IOException {
+                        return f.createNewFile();
+                    }
+        };
+        public boolean createNewFile(File f) throws IOException ;
+    }
+
+    /**
+     * Creates a new empty file with a unique name in the given directory by appending a hyphen and
+     * a number to the given filename.
+     *
+     * @return a new File object, or null if one could not be created
+     */
+    public static File createUniqueFile(File directory, String filename) throws IOException {
+        return createUniqueFileInternal(NewFileCreator.DEFAULT, directory, filename);
+    }
+
+    /* package */ static File createUniqueFileInternal(NewFileCreator nfc,
+            File directory, String filename) throws IOException {
+        File file = new File(directory, filename);
+        if (nfc.createNewFile(file)) {
+            return file;
+        }
+        // Get the extension of the file, if any.
+        int index = filename.lastIndexOf('.');
+        String format;
+        if (index != -1) {
+            String name = filename.substring(0, index);
+            String extension = filename.substring(index);
+            format = name + "-%d" + extension;
+        } else {
+            format = filename + "-%d";
+        }
+
+        for (int i = 2; i < Integer.MAX_VALUE; i++) {
+            file = new File(directory, String.format(format, i));
+            if (nfc.createNewFile(file)) {
+                return file;
+            }
+        }
+        return null;
+    }
+
+    public interface CursorGetter<T> {
+        T get(Cursor cursor, int column);
+    }
+
+    private static final CursorGetter<Long> LONG_GETTER = new CursorGetter<Long>() {
+        @Override
+        public Long get(Cursor cursor, int column) {
+            return cursor.getLong(column);
+        }
+    };
+
+    private static final CursorGetter<Integer> INT_GETTER = new CursorGetter<Integer>() {
+        @Override
+        public Integer get(Cursor cursor, int column) {
+            return cursor.getInt(column);
+        }
+    };
+
+    private static final CursorGetter<String> STRING_GETTER = new CursorGetter<String>() {
+        @Override
+        public String get(Cursor cursor, int column) {
+            return cursor.getString(column);
+        }
+    };
+
+    private static final CursorGetter<byte[]> BLOB_GETTER = new CursorGetter<byte[]>() {
+        @Override
+        public byte[] get(Cursor cursor, int column) {
+            return cursor.getBlob(column);
+        }
+    };
+
+    /**
+     * @return if {@code original} is to the EmailProvider, add "?limit=1".  Otherwise just returns
+     * {@code original}.
+     *
+     * Other providers don't support the limit param.  Also, changing URI passed from other apps
+     * can cause permission errors.
+     */
+    /* package */ static Uri buildLimitOneUri(Uri original) {
+        if ("content".equals(original.getScheme()) &&
+                EmailContent.AUTHORITY.equals(original.getAuthority())) {
+            return EmailContent.uriWithLimit(original, 1);
+        }
+        return original;
+    }
+
+    /**
+     * @return a generic in column {@code column} of the first result row, if the query returns at
+     * least 1 row.  Otherwise returns {@code defaultValue}.
+     */
+    public static <T extends Object> T getFirstRowColumn(Context context, Uri uri,
+            String[] projection, String selection, String[] selectionArgs, String sortOrder,
+            int column, T defaultValue, CursorGetter<T> getter) {
+        // Use PARAMETER_LIMIT to restrict the query to the single row we need
+        uri = buildLimitOneUri(uri);
+        Cursor c = context.getContentResolver().query(uri, projection, selection, selectionArgs,
+                sortOrder);
+        if (c != null) {
+            try {
+                if (c.moveToFirst()) {
+                    return getter.get(c, column);
+                }
+            } finally {
+                c.close();
+            }
+        }
+        return defaultValue;
+    }
+
+    /**
+     * {@link #getFirstRowColumn} for a Long with null as a default value.
+     */
+    public static Long getFirstRowLong(Context context, Uri uri, String[] projection,
+            String selection, String[] selectionArgs, String sortOrder, int column) {
+        return getFirstRowColumn(context, uri, projection, selection, selectionArgs,
+                sortOrder, column, null, LONG_GETTER);
+    }
+
+    /**
+     * {@link #getFirstRowColumn} for a Long with a provided default value.
+     */
+    public static Long getFirstRowLong(Context context, Uri uri, String[] projection,
+            String selection, String[] selectionArgs, String sortOrder, int column,
+            Long defaultValue) {
+        return getFirstRowColumn(context, uri, projection, selection, selectionArgs,
+                sortOrder, column, defaultValue, LONG_GETTER);
+    }
+
+    /**
+     * {@link #getFirstRowColumn} for an Integer with null as a default value.
+     */
+    public static Integer getFirstRowInt(Context context, Uri uri, String[] projection,
+            String selection, String[] selectionArgs, String sortOrder, int column) {
+        return getFirstRowColumn(context, uri, projection, selection, selectionArgs,
+                sortOrder, column, null, INT_GETTER);
+    }
+
+    /**
+     * {@link #getFirstRowColumn} for an Integer with a provided default value.
+     */
+    public static Integer getFirstRowInt(Context context, Uri uri, String[] projection,
+            String selection, String[] selectionArgs, String sortOrder, int column,
+            Integer defaultValue) {
+        return getFirstRowColumn(context, uri, projection, selection, selectionArgs,
+                sortOrder, column, defaultValue, INT_GETTER);
+    }
+
+    /**
+     * {@link #getFirstRowColumn} for a String with null as a default value.
+     */
+    public static String getFirstRowString(Context context, Uri uri, String[] projection,
+            String selection, String[] selectionArgs, String sortOrder, int column) {
+        return getFirstRowString(context, uri, projection, selection, selectionArgs, sortOrder,
+                column, null);
+    }
+
+    /**
+     * {@link #getFirstRowColumn} for a String with a provided default value.
+     */
+    public static String getFirstRowString(Context context, Uri uri, String[] projection,
+            String selection, String[] selectionArgs, String sortOrder, int column,
+            String defaultValue) {
+        return getFirstRowColumn(context, uri, projection, selection, selectionArgs,
+                sortOrder, column, defaultValue, STRING_GETTER);
+    }
+
+    /**
+     * {@link #getFirstRowColumn} for a byte array with a provided default value.
+     */
+    public static byte[] getFirstRowBlob(Context context, Uri uri, String[] projection,
+            String selection, String[] selectionArgs, String sortOrder, int column,
+            byte[] defaultValue) {
+        return getFirstRowColumn(context, uri, projection, selection, selectionArgs, sortOrder,
+                column, defaultValue, BLOB_GETTER);
+    }
+
+    public static boolean attachmentExists(Context context, Attachment attachment) {
+        if (attachment == null) {
+            return false;
+        } else if (attachment.mContentBytes != null) {
+            return true;
+        } else if (TextUtils.isEmpty(attachment.mContentUri)) {
+            return false;
+        }
+        try {
+            Uri fileUri = Uri.parse(attachment.mContentUri);
+            try {
+                InputStream inStream = context.getContentResolver().openInputStream(fileUri);
+                try {
+                    inStream.close();
+                } catch (IOException e) {
+                    // Nothing to be done if can't close the stream
+                }
+                return true;
+            } catch (FileNotFoundException e) {
+                return false;
+            }
+        } catch (RuntimeException re) {
+            Log.w(Logging.LOG_TAG, "attachmentExists RuntimeException=" + re);
+            return false;
+        }
+    }
+
+    /**
+     * Check whether the message with a given id has unloaded attachments.  If the message is
+     * a forwarded message, we look instead at the messages's source for the attachments.  If the
+     * message or forward source can't be found, we return false
+     * @param context the caller's context
+     * @param messageId the id of the message
+     * @return whether or not the message has unloaded attachments
+     */
+    public static boolean hasUnloadedAttachments(Context context, long messageId) {
+        Message msg = Message.restoreMessageWithId(context, messageId);
+        if (msg == null) return false;
+        Attachment[] atts = Attachment.restoreAttachmentsWithMessageId(context, messageId);
+        for (Attachment att: atts) {
+            if (!attachmentExists(context, att)) {
+                // If the attachment doesn't exist and isn't marked for download, we're in trouble
+                // since the outbound message will be stuck indefinitely in the Outbox.  Instead,
+                // we'll just delete the attachment and continue; this is far better than the
+                // alternative.  In theory, this situation shouldn't be possible.
+                if ((att.mFlags & (Attachment.FLAG_DOWNLOAD_FORWARD |
+                        Attachment.FLAG_DOWNLOAD_USER_REQUEST)) == 0) {
+                    Log.d(Logging.LOG_TAG, "Unloaded attachment isn't marked for download: " +
+                            att.mFileName + ", #" + att.mId);
+                    Attachment.delete(context, Attachment.CONTENT_URI, att.mId);
+                } else if (att.mContentUri != null) {
+                    // In this case, the attachment file is gone from the cache; let's clear the
+                    // contentUri; this should be a very unusual case
+                    ContentValues cv = new ContentValues();
+                    cv.putNull(AttachmentColumns.CONTENT_URI);
+                    Attachment.update(context, Attachment.CONTENT_URI, att.mId, cv);
+                }
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Convenience method wrapping calls to retrieve columns from a single row, via EmailProvider.
+     * The arguments are exactly the same as to contentResolver.query().  Results are returned in
+     * an array of Strings corresponding to the columns in the projection.  If the cursor has no
+     * rows, null is returned.
+     */
+    public static String[] getRowColumns(Context context, Uri contentUri, String[] projection,
+            String selection, String[] selectionArgs) {
+        String[] values = new String[projection.length];
+        ContentResolver cr = context.getContentResolver();
+        Cursor c = cr.query(contentUri, projection, selection, selectionArgs, null);
+        try {
+            if (c.moveToFirst()) {
+                for (int i = 0; i < projection.length; i++) {
+                    values[i] = c.getString(i);
+                }
+            } else {
+                return null;
+            }
+        } finally {
+            c.close();
+        }
+        return values;
+    }
+
+    /**
+     * Convenience method for retrieving columns from a particular row in EmailProvider.
+     * Passed in here are a base uri (e.g. Message.CONTENT_URI), the unique id of a row, and
+     * a projection.  This method calls the previous one with the appropriate URI.
+     */
+    public static String[] getRowColumns(Context context, Uri baseUri, long id,
+            String ... projection) {
+        return getRowColumns(context, ContentUris.withAppendedId(baseUri, id), projection, null,
+                null);
+    }
+
+    public static boolean isExternalStorageMounted() {
+        return Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED);
+    }
+
+    /**
+     * Class that supports running any operation for each account.
+     */
+    public abstract static class ForEachAccount extends AsyncTask<Void, Void, Long[]> {
+        private final Context mContext;
+
+        public ForEachAccount(Context context) {
+            mContext = context;
+        }
+
+        @Override
+        protected final Long[] doInBackground(Void... params) {
+            ArrayList<Long> ids = new ArrayList<Long>();
+            Cursor c = mContext.getContentResolver().query(Account.CONTENT_URI,
+                    Account.ID_PROJECTION, null, null, null);
+            try {
+                while (c.moveToNext()) {
+                    ids.add(c.getLong(Account.ID_PROJECTION_COLUMN));
+                }
+            } finally {
+                c.close();
+            }
+            return ids.toArray(EMPTY_LONGS);
+        }
+
+        @Override
+        protected final void onPostExecute(Long[] ids) {
+            if (ids != null && !isCancelled()) {
+                for (long id : ids) {
+                    performAction(id);
+                }
+            }
+            onFinished();
+        }
+
+        /**
+         * This method will be called for each account.
+         */
+        protected abstract void performAction(long accountId);
+
+        /**
+         * Called when the iteration is finished.
+         */
+        protected void onFinished() {
+        }
+    }
+
+    /**
+     * Updates the last seen message key in the mailbox data base for the INBOX of the currently
+     * shown account. If the account is {@link Account#ACCOUNT_ID_COMBINED_VIEW}, the INBOX for
+     * all accounts are updated.
+     * @return an {@link EmailAsyncTask} for test only.
+     */
+    public static EmailAsyncTask<Void, Void, Void> updateLastNotifiedMessageKey(
+            final Context context, final long mailboxId) {
+        return EmailAsyncTask.runAsyncParallel(new Runnable() {
+            private void updateLastSeenMessageKeyForMailbox(long mailboxId) {
+                ContentResolver resolver = context.getContentResolver();
+                if (mailboxId == Mailbox.QUERY_ALL_INBOXES) {
+                    Cursor c = resolver.query(
+                            Mailbox.CONTENT_URI, EmailContent.ID_PROJECTION, Mailbox.TYPE + "=?",
+                            new String[] { Integer.toString(Mailbox.TYPE_INBOX) }, null);
+                    if (c == null) throw new ProviderUnavailableException();
+                    try {
+                        while (c.moveToNext()) {
+                            final long id = c.getLong(EmailContent.ID_PROJECTION_COLUMN);
+                            updateLastSeenMessageKeyForMailbox(id);
+                        }
+                    } finally {
+                        c.close();
+                    }
+                } else if (mailboxId > 0L) {
+                    Mailbox mailbox = Mailbox.restoreMailboxWithId(context, mailboxId);
+                   // mailbox has been removed
+                    if (mailbox == null) {
+                        return;
+                    }
+                    // We use the highest _id for the account the mailbox table as the "last seen
+                    // message key". We don't care if the message has been read or not. We only
+                    // need a point at which we can compare against in the future. By setting this
+                    // value, we are claiming that every message before this has potentially been
+                    // seen by the user.
+                    long mostRecentMessageId = Utility.getFirstRowLong(context,
+                            ContentUris.withAppendedId(
+                                    EmailContent.MAILBOX_MOST_RECENT_MESSAGE_URI, mailboxId),
+                            Message.ID_COLUMN_PROJECTION, null, null, null,
+                            Message.ID_MAILBOX_COLUMN_ID, -1L);
+                    long lastNotifiedMessageId = mailbox.mLastNotifiedMessageKey;
+                    // Only update the db if the value has changed
+                    if (mostRecentMessageId != lastNotifiedMessageId) {
+                        Log.d(Logging.LOG_TAG, "Most recent = " + mostRecentMessageId +
+                                ", last notified: " + lastNotifiedMessageId +
+                                "; updating last notified");
+                        ContentValues values = mailbox.toContentValues();
+                        values.put(MailboxColumns.LAST_NOTIFIED_MESSAGE_KEY, mostRecentMessageId);
+                        resolver.update(
+                                Mailbox.CONTENT_URI,
+                                values,
+                                EmailContent.ID_SELECTION,
+                                new String[] { Long.toString(mailbox.mId) });
+                    } else {
+                        Log.d(Logging.LOG_TAG, "Most recent = last notified; no change");
+                    }
+                }
+            }
+
+            @Override
+            public void run() {
+                updateLastSeenMessageKeyForMailbox(mailboxId);
+            }
+        });
+    }
+
+    public static long[] toPrimitiveLongArray(Collection<Long> collection) {
+        // Need to do this manually because we're converting to a primitive long array, not
+        // a Long array.
+        final int size = collection.size();
+        final long[] ret = new long[size];
+        // Collection doesn't have get(i).  (Iterable doesn't have size())
+        int i = 0;
+        for (Long value : collection) {
+            ret[i++] = value;
+        }
+        return ret;
+    }
+
+    public static Set<Long> toLongSet(long[] array) {
+        // Need to do this manually because we're converting from a primitive long array, not
+        // a Long array.
+        final int size = array.length;
+        HashSet<Long> ret = new HashSet<Long>(size);
+        for (int i = 0; i < size; i++) {
+            ret.add(array[i]);
+        }
+        return ret;
+    }
+
+    /**
+     * Workaround for the {@link ListView#smoothScrollToPosition} randomly scroll the view bug
+     * if it's called right after {@link ListView#setAdapter}.
+     */
+    public static void listViewSmoothScrollToPosition(final Activity activity,
+            final ListView listView, final int position) {
+        // Workarond: delay-call smoothScrollToPosition()
+        new Handler().post(new Runnable() {
+            @Override
+            public void run() {
+                if (activity.isFinishing()) {
+                    return; // Activity being destroyed
+                }
+                listView.smoothScrollToPosition(position);
+            }
+        });
+    }
+
+    private static final String[] ATTACHMENT_META_NAME_PROJECTION = {
+        OpenableColumns.DISPLAY_NAME
+    };
+    private static final int ATTACHMENT_META_NAME_COLUMN_DISPLAY_NAME = 0;
+
+    /**
+     * @return Filename of a content of {@code contentUri}.  If the provider doesn't provide the
+     * filename, returns the last path segment of the URI.
+     */
+    public static String getContentFileName(Context context, Uri contentUri) {
+        String name = getFirstRowString(context, contentUri, ATTACHMENT_META_NAME_PROJECTION, null,
+                null, null, ATTACHMENT_META_NAME_COLUMN_DISPLAY_NAME);
+        if (name == null) {
+            name = contentUri.getLastPathSegment();
+        }
+        return name;
+    }
+
+    /**
+     * Append a bold span to a {@link SpannableStringBuilder}.
+     */
+    public static SpannableStringBuilder appendBold(SpannableStringBuilder ssb, String text) {
+        if (!TextUtils.isEmpty(text)) {
+            SpannableString ss = new SpannableString(text);
+            ss.setSpan(new StyleSpan(Typeface.BOLD), 0, ss.length(),
+                    Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+            ssb.append(ss);
+        }
+
+        return ssb;
+    }
+
+    /**
+     * Stringify a cursor for logging purpose.
+     */
+    public static String dumpCursor(Cursor c) {
+        StringBuilder sb = new StringBuilder();
+        sb.append("[");
+        while (c != null) {
+            sb.append(c.getClass()); // Class name may not be available if toString() is overridden
+            sb.append("/");
+            sb.append(c.toString());
+            if (c.isClosed()) {
+                sb.append(" (closed)");
+            }
+            if (c instanceof CursorWrapper) {
+                c = ((CursorWrapper) c).getWrappedCursor();
+                sb.append(", ");
+            } else {
+                break;
+            }
+        }
+        sb.append("]");
+        return sb.toString();
+    }
+
+    /**
+     * Cursor wrapper that remembers where it was closed.
+     *
+     * Use {@link #get} to create a wrapped cursor.
+     * USe {@link #getTraceIfAvailable} to get the stack trace.
+     * Use {@link #log} to log if/where it was closed.
+     */
+    public static class CloseTraceCursorWrapper extends CursorWrapper {
+        private static final boolean TRACE_ENABLED = false;
+
+        private Exception mTrace;
+
+        private CloseTraceCursorWrapper(Cursor cursor) {
+            super(cursor);
+        }
+
+        @Override
+        public void close() {
+            mTrace = new Exception("STACK TRACE");
+            super.close();
+        }
+
+        public static Exception getTraceIfAvailable(Cursor c) {
+            if (c instanceof CloseTraceCursorWrapper) {
+                return ((CloseTraceCursorWrapper) c).mTrace;
+            } else {
+                return null;
+            }
+        }
+
+        public static void log(Cursor c) {
+            if (c == null) {
+                return;
+            }
+            if (c.isClosed()) {
+                Log.w(Logging.LOG_TAG, "Cursor was closed here: Cursor=" + c,
+                        getTraceIfAvailable(c));
+            } else {
+                Log.w(Logging.LOG_TAG, "Cursor not closed.  Cursor=" + c);
+            }
+        }
+
+        public static Cursor get(Cursor original) {
+            return TRACE_ENABLED ? new CloseTraceCursorWrapper(original) : original;
+        }
+
+        /* package */ static CloseTraceCursorWrapper alwaysCreateForTest(Cursor original) {
+            return new CloseTraceCursorWrapper(original);
+        }
+    }
+
+    /**
+     * Test that the given strings are equal in a null-pointer safe fashion.
+     */
+    public static boolean areStringsEqual(String s1, String s2) {
+        return (s1 != null && s1.equals(s2)) || (s1 == null && s2 == null);
+    }
+
+    public static void enableStrictMode(boolean enabled) {
+        StrictMode.setThreadPolicy(enabled
+                ? new StrictMode.ThreadPolicy.Builder().detectAll().build()
+                : StrictMode.ThreadPolicy.LAX);
+        StrictMode.setVmPolicy(enabled
+                ? new StrictMode.VmPolicy.Builder().detectAll().build()
+                : StrictMode.VmPolicy.LAX);
+    }
+
+    public static String dumpFragment(Fragment f) {
+        StringWriter sw = new StringWriter();
+        PrintWriter w = new PrintWriter(sw);
+        f.dump("", new FileDescriptor(), w, new String[0]);
+        return sw.toString();
+    }
+
+    /**
+     * Builds an "in" expression for SQLite.
+     *
+     * e.g. "ID" + 1,2,3 -> "ID in (1,2,3)".  If {@code values} is empty or null, it returns an
+     * empty string.
+     */
+    public static String buildInSelection(String columnName, Collection<? extends Number> values) {
+        if ((values == null) || (values.size() == 0)) {
+            return "";
+        }
+        StringBuilder sb = new StringBuilder();
+        sb.append(columnName);
+        sb.append(" in (");
+        String sep = "";
+        for (Number n : values) {
+            sb.append(sep);
+            sb.append(n.toString());
+            sep = ",";
+        }
+        sb.append(')');
+        return sb.toString();
+    }
+}
diff --git a/email2/src/com/android/email/Email.java b/email2/src/com/android/email/Email.java
deleted file mode 100644
index d651649..0000000
--- a/email2/src/com/android/email/Email.java
+++ /dev/null
@@ -1,236 +0,0 @@
-/*
- * 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.email;
-
-import android.app.Application;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.content.pm.PackageManager;
-import android.database.Cursor;
-import android.util.Log;
-
-import com.android.email.service.AttachmentDownloadService;
-import com.android.email.service.MailService;
-import com.android.emailcommon.Logging;
-import com.android.emailcommon.TempDirectory;
-import com.android.emailcommon.provider.Account;
-import com.android.emailcommon.service.EmailServiceProxy;
-import com.android.emailcommon.utility.EmailAsyncTask;
-import com.android.emailcommon.utility.Utility;
-
-public class Email extends Application {
-    /**
-     * If this is enabled there will be additional logging information sent to
-     * Log.d, including protocol dumps.
-     *
-     * This should only be used for logs that are useful for debbuging user problems,
-     * not for internal/development logs.
-     *
-     * This can be enabled by typing "debug" in the AccountFolderList activity.
-     * Changing the value to 'true' here will likely have no effect at all!
-     *
-     * TODO: rename this to sUserDebug, and rename LOGD below to DEBUG.
-     */
-    public static boolean DEBUG;
-
-    // Exchange debugging flags (passed to Exchange, when available, via EmailServiceProxy)
-    public static boolean DEBUG_EXCHANGE;
-    public static boolean DEBUG_EXCHANGE_VERBOSE;
-    public static boolean DEBUG_EXCHANGE_FILE;
-
-    /**
-     * If true, inhibit hardware graphics acceleration in UI (for a/b testing)
-     */
-    public static boolean sDebugInhibitGraphicsAcceleration = false;
-
-    /**
-     * Specifies how many messages will be shown in a folder by default. This number is set
-     * on each new folder and can be incremented with "Load more messages..." by the
-     * VISIBLE_LIMIT_INCREMENT
-     */
-    public static final int VISIBLE_LIMIT_DEFAULT = 25;
-
-    /**
-     * Number of additional messages to load when a user selects "Load more messages..."
-     */
-    public static final int VISIBLE_LIMIT_INCREMENT = 25;
-
-    /**
-     * This is used to force stacked UI to return to the "welcome" screen any time we change
-     * the accounts list (e.g. deleting accounts in the Account Manager preferences.)
-     */
-    private static boolean sAccountsChangedNotification = false;
-
-    private static String sMessageDecodeErrorString;
-
-    private static Thread sUiThread;
-
-    /**
-     * Asynchronous version of {@link #setServicesEnabledSync(Context)}.  Use when calling from
-     * UI thread (or lifecycle entry points.)
-     *
-     * @param context
-     */
-    public static void setServicesEnabledAsync(final Context context) {
-        EmailAsyncTask.runAsyncParallel(new Runnable() {
-            @Override
-            public void run() {
-                setServicesEnabledSync(context);
-            }
-        });
-    }
-
-    /**
-     * Called throughout the application when the number of accounts has changed. This method
-     * enables or disables the Compose activity, the boot receiver and the service based on
-     * whether any accounts are configured.
-     *
-     * Blocking call - do not call from UI/lifecycle threads.
-     *
-     * @param context
-     * @return true if there are any accounts configured.
-     */
-    public static boolean setServicesEnabledSync(Context context) {
-        Cursor c = null;
-        try {
-            c = context.getContentResolver().query(
-                    Account.CONTENT_URI,
-                    Account.ID_PROJECTION,
-                    null, null, null);
-            boolean enable = c.getCount() > 0;
-            setServicesEnabled(context, enable);
-            return enable;
-        } finally {
-            if (c != null) {
-                c.close();
-            }
-        }
-    }
-
-    private static void setServicesEnabled(Context context, boolean enabled) {
-        PackageManager pm = context.getPackageManager();
-        pm.setComponentEnabledSetting(
-                new ComponentName(context, MailService.class),
-                enabled ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED :
-                    PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
-                PackageManager.DONT_KILL_APP);
-        pm.setComponentEnabledSetting(
-                new ComponentName(context, AttachmentDownloadService.class),
-                enabled ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED :
-                    PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
-                PackageManager.DONT_KILL_APP);
-
-        // Start/stop the various services depending on whether there are any accounts
-        startOrStopService(enabled, context, new Intent(context, AttachmentDownloadService.class));
-        NotificationController.getInstance(context).watchForMessages(enabled);
-    }
-
-    /**
-     * Starts or stops the service as necessary.
-     * @param enabled If {@code true}, the service will be started. Otherwise, it will be stopped.
-     * @param context The context to manage the service with.
-     * @param intent The intent of the service to be managed.
-     */
-    private static void startOrStopService(boolean enabled, Context context, Intent intent) {
-        if (enabled) {
-            context.startService(intent);
-        } else {
-            context.stopService(intent);
-        }
-    }
-
-    @Override
-    public void onCreate() {
-        super.onCreate();
-        sUiThread = Thread.currentThread();
-        Preferences prefs = Preferences.getPreferences(this);
-        DEBUG = true; //prefs.getEnableDebugLogging();
-        sDebugInhibitGraphicsAcceleration = prefs.getInhibitGraphicsAcceleration();
-        enableStrictMode(prefs.getEnableStrictMode());
-        TempDirectory.setTempDirectory(this);
-
-        // Enable logging in the EAS service, so it starts up as early as possible.
-        updateLoggingFlags(this);
-
-        // Get a helper string used deep inside message decoders (which don't have context)
-        sMessageDecodeErrorString = getString(R.string.message_decode_error);
-
-        // Make sure all required services are running when the app is started (can prevent
-        // issues after an adb sync/install)
-        setServicesEnabledAsync(this);
-    }
-
-    /**
-     * Load enabled debug flags from the preferences and update the EAS debug flag.
-     */
-    public static void updateLoggingFlags(Context context) {
-        Preferences prefs = Preferences.getPreferences(context);
-        int debugLogging = prefs.getEnableDebugLogging() ? EmailServiceProxy.DEBUG_BIT : 0;
-        int verboseLogging =
-            prefs.getEnableExchangeLogging() ? EmailServiceProxy.DEBUG_VERBOSE_BIT : 0;
-        int fileLogging =
-            prefs.getEnableExchangeFileLogging() ? EmailServiceProxy.DEBUG_FILE_BIT : 0;
-        int enableStrictMode =
-            prefs.getEnableStrictMode() ? EmailServiceProxy.DEBUG_ENABLE_STRICT_MODE : 0;
-        int debugBits = debugLogging | verboseLogging | fileLogging | enableStrictMode;
-        //Controller.getInstance(context).serviceLogging(debugBits);
-    }
-
-    /**
-     * Internal, utility method for logging.
-     * The calls to log() must be guarded with "if (Email.LOGD)" for performance reasons.
-     */
-    public static void log(String message) {
-        Log.d(Logging.LOG_TAG, message);
-    }
-
-    /**
-     * Called by the accounts reconciler to notify that accounts have changed, or by  "Welcome"
-     * to clear the flag.
-     * @param setFlag true to set the notification flag, false to clear it
-     */
-    public static synchronized void setNotifyUiAccountsChanged(boolean setFlag) {
-        sAccountsChangedNotification = setFlag;
-    }
-
-    /**
-     * Called from activity onResume() functions to check for an accounts-changed condition, at
-     * which point they should finish() and jump to the Welcome activity.
-     */
-    public static synchronized boolean getNotifyUiAccountsChanged() {
-        return sAccountsChangedNotification;
-    }
-
-    public static void warnIfUiThread() {
-        if (Thread.currentThread().equals(sUiThread)) {
-            Log.w(Logging.LOG_TAG, "Method called on the UI thread", new Exception("STACK TRACE"));
-        }
-    }
-
-    /**
-     * Retrieve a simple string that can be used when message decoders encounter bad data.
-     * This is provided here because the protocol decoders typically don't have mContext.
-     */
-    public static String getMessageDecodeErrorString() {
-        return sMessageDecodeErrorString != null ? sMessageDecodeErrorString : "";
-    }
-
-    public static void enableStrictMode(boolean enabled) {
-        Utility.enableStrictMode(enabled);
-    }
-}
diff --git a/email2/src/com/android/email/EmailConnectivityManager.java b/email2/src/com/android/email/EmailConnectivityManager.java
index c618c38..6746152 100644
--- a/email2/src/com/android/email/EmailConnectivityManager.java
+++ b/email2/src/com/android/email/EmailConnectivityManager.java
@@ -29,6 +29,8 @@
 import android.os.PowerManager.WakeLock;
 import android.util.Log;
 
+import com.android.email2.ui.MailActivityEmail;
+
 /**
  * Encapsulates functionality of ConnectivityManager for use in the Email application.  In
  * particular, this class provides callbacks for connectivity lost, connectivity restored, and
@@ -179,14 +181,14 @@
                 if (info != null) {
                     // We're done if there's an active network
                     if (waiting) {
-                        if (Email.DEBUG) {
+                        if (MailActivityEmail.DEBUG) {
                             Log.d(TAG, mName + ": Connectivity wait ended");
                         }
                     }
                     return;
                 } else {
                     if (!waiting) {
-                        if (Email.DEBUG) {
+                        if (MailActivityEmail.DEBUG) {
                             Log.d(TAG, mName + ": Connectivity waiting...");
                         }
                         waiting = true;
diff --git a/email2/src/com/android/email/NotificationController.java b/email2/src/com/android/email/NotificationController.java
index 54f03bb..b64f04a 100644
--- a/email2/src/com/android/email/NotificationController.java
+++ b/email2/src/com/android/email/NotificationController.java
@@ -31,6 +31,7 @@
 import android.graphics.BitmapFactory;
 import android.media.AudioManager;
 import android.net.Uri;
+import android.os.Debug;
 import android.os.Handler;
 import android.os.Looper;
 import android.os.Process;
@@ -41,6 +42,7 @@
 import com.android.email.activity.setup.AccountSecurity;
 import com.android.email.activity.setup.AccountSettings;
 import com.android.email.provider.EmailProvider;
+import com.android.email2.ui.MailActivityEmail;
 import com.android.emailcommon.Logging;
 import com.android.emailcommon.mail.Address;
 import com.android.emailcommon.provider.Account;
@@ -230,7 +232,7 @@
      *              notifications enabled. Otherwise, all observers are unregistered.
      */
     public void watchForMessages(final boolean watch) {
-        if (Email.DEBUG) {
+        if (MailActivityEmail.DEBUG) {
             Log.i(Logging.LOG_TAG, "Notifications being toggled: " + watch);
         }
         // Don't create the thread if we're only going to stop watching
@@ -259,7 +261,7 @@
                 registerMessageNotification(Account.ACCOUNT_ID_COMBINED_VIEW);
                 // If we're already observing account changes, don't do anything else
                 if (mAccountObserver == null) {
-                    if (Email.DEBUG) {
+                    if (MailActivityEmail.DEBUG) {
                         Log.i(Logging.LOG_TAG, "Observing account changes for notifications");
                     }
                     mAccountObserver = new AccountContentObserver(sNotificationHandler, mContext);
@@ -341,7 +343,7 @@
         } else {
             ContentObserver obs = mNotificationMap.get(accountId);
             if (obs != null) return;  // we're already observing; nothing to do
-            if (Email.DEBUG) {
+            if (MailActivityEmail.DEBUG) {
                 Log.i(Logging.LOG_TAG, "Registering for notifications for account " + accountId);
             }
             ContentObserver observer = new MessageContentObserver(
@@ -364,7 +366,7 @@
     private void unregisterMessageNotification(long accountId) {
         ContentResolver resolver = mContext.getContentResolver();
         if (accountId == Account.ACCOUNT_ID_COMBINED_VIEW) {
-            if (Email.DEBUG) {
+            if (MailActivityEmail.DEBUG) {
                 Log.i(Logging.LOG_TAG, "Unregistering notifications for all accounts");
             }
             // cancel all existing message observers
@@ -373,7 +375,7 @@
             }
             mNotificationMap.clear();
         } else {
-            if (Email.DEBUG) {
+            if (MailActivityEmail.DEBUG) {
                 Log.i(Logging.LOG_TAG, "Unregistering notifications for account " + accountId);
             }
             ContentObserver observer = mNotificationMap.remove(accountId);
@@ -407,16 +409,16 @@
     public static final String EXTRA_CONVERSATION = "conversationUri";
     public static final String EXTRA_FOLDER = "folder";
 
-//    private Intent createViewConversationIntent(Conversation conversation, Folder folder,
-//            com.android.mail.providers.Account account) {
-//        final Intent intent = new Intent(Intent.ACTION_VIEW);
-//        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
-//        intent.setDataAndType(conversation.uri, account.mimeType);
-//        intent.putExtra(EXTRA_ACCOUNT, account);
-//        intent.putExtra(EXTRA_FOLDER, folder);
-//        intent.putExtra(EXTRA_CONVERSATION, conversation);
-//        return intent;
-//    }
+    private Intent createViewConversationIntent(Conversation conversation, Folder folder,
+            com.android.mail.providers.Account account) {
+        final Intent intent = new Intent(Intent.ACTION_VIEW);
+        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+        intent.setDataAndType(conversation.uri, account.mimeType);
+        intent.putExtra(EXTRA_ACCOUNT, account);
+        intent.putExtra(EXTRA_FOLDER, folder);
+        intent.putExtra(EXTRA_CONVERSATION, conversation);
+        return intent;
+    }
     
     private Cursor getUiCursor(Uri uri, String[] projection) {
         Cursor c = mContext.getContentResolver().query(uri, projection, null, null, null);
@@ -440,12 +442,12 @@
         if (c == null) return null;
         Folder folder = new Folder(c);
         c.close();
-        c = getUiCursor(EmailProvider.uiUri("uimessage", message.mId),
-                UIProvider.MESSAGE_PROJECTION);
+        c = getUiCursor(EmailProvider.uiUri("uiconversation", message.mId),
+                UIProvider.CONVERSATION_PROJECTION);
         if (c == null) return null;
         Conversation conv = new Conversation(c);
         c.close();
-        return null; //return createViewConversationIntent(conv, folder, acct);
+        return createViewConversationIntent(conv, folder, acct);
     }
     
     /**
@@ -498,17 +500,16 @@
 //        if (unseenMessageCount > 1) {
 //            intent = createViewConversationIntent(message);
 //        } else {
-//            intent = createViewConversationIntent(message);
+            intent = createViewConversationIntent(message);
 //        }
-//        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
         long now = mClock.getTime();
         boolean enableAudio = (now - mLastMessageNotifyTime) > MIN_SOUND_INTERVAL_MS;
-//        Notification notification = createMailboxNotification(
-//                mailbox, title.toString(), title, text,
-//                intent, largeIcon, number, enableAudio, false);
+        Notification notification = createMailboxNotification(
+                mailbox, title.toString(), title, text,
+                intent, largeIcon, number, enableAudio, false);
         mLastMessageNotifyTime = now;
-        return null;
-//        return notification;
+        return notification;
     }
 
     /**
diff --git a/email2/src/com/android/email/SecurityPolicy.java b/email2/src/com/android/email/SecurityPolicy.java
index c2a2871..9b6cfb8 100644
--- a/email2/src/com/android/email/SecurityPolicy.java
+++ b/email2/src/com/android/email/SecurityPolicy.java
@@ -34,6 +34,7 @@
 
 import com.android.email.provider.EmailProvider;
 import com.android.email.service.EmailBroadcastProcessorService;
+import com.android.email2.ui.MailActivityEmail;
 import com.android.emailcommon.Logging;
 import com.android.emailcommon.provider.Account;
 import com.android.emailcommon.provider.EmailContent;
@@ -136,7 +137,7 @@
         try {
             while (c.moveToNext()) {
                 policy.restore(c);
-                if (Email.DEBUG) {
+                if (MailActivityEmail.DEBUG) {
                     Log.d(TAG, "Aggregate from: " + policy);
                 }
                 aggregate.mPasswordMinLength =
@@ -181,12 +182,12 @@
                 aggregate.mPasswordExpirationDays = 0;
             if (aggregate.mPasswordComplexChars == Integer.MIN_VALUE)
                 aggregate.mPasswordComplexChars = 0;
-            if (Email.DEBUG) {
+            if (MailActivityEmail.DEBUG) {
                 Log.d(TAG, "Calculated Aggregate: " + aggregate);
             }
             return aggregate;
         }
-        if (Email.DEBUG) {
+        if (MailActivityEmail.DEBUG) {
             Log.d(TAG, "Calculated Aggregate: no policy");
         }
         return Policy.NO_POLICY;
@@ -228,7 +229,7 @@
      * rollbacks.
      */
     public void reducePolicies() {
-        if (Email.DEBUG) {
+        if (MailActivityEmail.DEBUG) {
             Log.d(TAG, "reducePolicies");
         }
         policiesUpdated();
@@ -243,7 +244,7 @@
      */
     public boolean isActive(Policy policy) {
         int reasons = getInactiveReasons(policy);
-        if (Email.DEBUG && (reasons != 0)) {
+        if (MailActivityEmail.DEBUG && (reasons != 0)) {
             StringBuilder sb = new StringBuilder("isActive for " + policy + ": ");
             if (reasons == 0) {
                 sb.append("true");
@@ -407,12 +408,12 @@
         Policy aggregatePolicy = getAggregatePolicy();
         // if empty set, detach from policy manager
         if (aggregatePolicy == Policy.NO_POLICY) {
-            if (Email.DEBUG) {
+            if (MailActivityEmail.DEBUG) {
                 Log.d(TAG, "setActivePolicies: none, remove admin");
             }
             dpm.removeActiveAdmin(mAdminName);
         } else if (isActiveAdmin()) {
-            if (Email.DEBUG) {
+            if (MailActivityEmail.DEBUG) {
                 Log.d(TAG, "setActivePolicies: " + aggregatePolicy);
             }
             // set each policy in the policy manager
@@ -488,7 +489,7 @@
         if (account.mPolicyKey == 0) return;
         Policy policy = Policy.restorePolicyWithId(mContext, account.mPolicyKey);
         if (policy == null) return;
-        if (Email.DEBUG) {
+        if (MailActivityEmail.DEBUG) {
             Log.d(TAG, "policiesRequired for " + account.mDisplayName + ": " + policy);
         }
 
diff --git a/email2/src/com/android/email/activity/ActivityHelper.java b/email2/src/com/android/email/activity/ActivityHelper.java
index 1ada78c..62d21f6 100644
--- a/email2/src/com/android/email/activity/ActivityHelper.java
+++ b/email2/src/com/android/email/activity/ActivityHelper.java
@@ -23,8 +23,8 @@
 import android.provider.Browser;
 import android.view.WindowManager;
 
-import com.android.email.Email;
 import com.android.email.activity.setup.AccountSecurity;
+import com.android.email2.ui.MailActivityEmail;
 import com.android.emailcommon.provider.Account;
 
 /**
@@ -94,7 +94,7 @@
      * NOTE: Currently, this only works if HW accel is *not* enabled via the manifest.
      */
     public static void debugSetWindowFlags(Activity activity) {
-        if (Email.sDebugInhibitGraphicsAcceleration) {
+        if (MailActivityEmail.sDebugInhibitGraphicsAcceleration) {
             // Clear the flag in the activity's window
             activity.getWindow().setFlags(0,
                     WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED);
diff --git a/email2/src/com/android/email/activity/setup/AccountSecurity.java b/email2/src/com/android/email/activity/setup/AccountSecurity.java
index ec336d5..5e6dd9f 100644
--- a/email2/src/com/android/email/activity/setup/AccountSecurity.java
+++ b/email2/src/com/android/email/activity/setup/AccountSecurity.java
@@ -29,10 +29,10 @@
 import android.os.Bundle;
 import android.util.Log;
 
-import com.android.email.Email;
 import com.android.email.R;
 import com.android.email.SecurityPolicy;
 import com.android.email.activity.ActivityHelper;
+import com.android.email2.ui.MailActivityEmail;
 import com.android.emailcommon.provider.Account;
 import com.android.emailcommon.provider.HostAuth;
 import com.android.emailcommon.utility.Utility;
@@ -178,7 +178,7 @@
         // Step 1.  Check if we are an active device administrator, and stop here to activate
         if (!security.isActiveAdmin()) {
             if (mTriedAddAdministrator) {
-                if (Email.DEBUG) {
+                if (MailActivityEmail.DEBUG) {
                     Log.d(TAG, "Not active admin: repost notification");
                 }
                 repostNotification(account, security);
@@ -188,13 +188,13 @@
                 // retrieve name of server for the format string
                 HostAuth hostAuth = HostAuth.restoreHostAuthWithId(this, account.mHostAuthKeyRecv);
                 if (hostAuth == null) {
-                    if (Email.DEBUG) {
+                    if (MailActivityEmail.DEBUG) {
                         Log.d(TAG, "No HostAuth: repost notification");
                     }
                     repostNotification(account, security);
                     finish();
                 } else {
-                    if (Email.DEBUG) {
+                    if (MailActivityEmail.DEBUG) {
                         Log.d(TAG, "Not active admin: post initial notification");
                     }
                     // try to become active - must happen here in activity, to get result
@@ -213,7 +213,7 @@
         // Step 2.  Check if the current aggregate security policy is being satisfied by the
         // DevicePolicyManager (the current system security level).
         if (security.isActive(null)) {
-            if (Email.DEBUG) {
+            if (MailActivityEmail.DEBUG) {
                 Log.d(TAG, "Security active; clear holds");
             }
             Account.clearSecurityHoldOnAllAccounts(this);
@@ -232,13 +232,13 @@
         // Step 5.  If password is needed, try to have the user set it
         if ((inactiveReasons & SecurityPolicy.INACTIVE_NEED_PASSWORD) != 0) {
             if (mTriedSetPassword) {
-                if (Email.DEBUG) {
+                if (MailActivityEmail.DEBUG) {
                     Log.d(TAG, "Password needed; repost notification");
                 }
                 repostNotification(account, security);
                 finish();
             } else {
-                if (Email.DEBUG) {
+                if (MailActivityEmail.DEBUG) {
                     Log.d(TAG, "Password needed; request it via DPM");
                 }
                 mTriedSetPassword = true;
@@ -252,13 +252,13 @@
         // Step 6.  If encryption is needed, try to have the user set it
         if ((inactiveReasons & SecurityPolicy.INACTIVE_NEED_ENCRYPTION) != 0) {
             if (mTriedSetEncryption) {
-                if (Email.DEBUG) {
+                if (MailActivityEmail.DEBUG) {
                     Log.d(TAG, "Encryption needed; repost notification");
                 }
                 repostNotification(account, security);
                 finish();
             } else {
-                if (Email.DEBUG) {
+                if (MailActivityEmail.DEBUG) {
                     Log.d(TAG, "Encryption needed; request it via DPM");
                 }
                 mTriedSetEncryption = true;
@@ -270,7 +270,7 @@
         }
 
         // Step 7.  No problems were found, so clear holds and exit
-        if (Email.DEBUG) {
+        if (MailActivityEmail.DEBUG) {
             Log.d(TAG, "Policies enforced; clear holds");
         }
         Account.clearSecurityHoldOnAllAccounts(this);
@@ -324,7 +324,7 @@
             b.setMessage(res.getString(R.string.account_security_dialog_content_fmt, accountName));
             b.setPositiveButton(R.string.okay_action, this);
             b.setNegativeButton(R.string.cancel_action, this);
-            if (Email.DEBUG) {
+            if (MailActivityEmail.DEBUG) {
                 Log.d(TAG, "Posting security needed dialog");
             }
             return b.create();
@@ -341,13 +341,13 @@
             }
             switch (which) {
                 case DialogInterface.BUTTON_POSITIVE:
-                    if (Email.DEBUG) {
+                    if (MailActivityEmail.DEBUG) {
                         Log.d(TAG, "User accepts; advance to next step");
                     }
                     activity.tryAdvanceSecurity(activity.mAccount);
                     break;
                 case DialogInterface.BUTTON_NEGATIVE:
-                    if (Email.DEBUG) {
+                    if (MailActivityEmail.DEBUG) {
                         Log.d(TAG, "User declines; repost notification");
                     }
                     activity.repostNotification(
diff --git a/email2/src/com/android/email/activity/setup/AccountSettingsEditQuickResponsesFragment.java b/email2/src/com/android/email/activity/setup/AccountSettingsEditQuickResponsesFragment.java
index 776291e..9e70aeb 100644
--- a/email2/src/com/android/email/activity/setup/AccountSettingsEditQuickResponsesFragment.java
+++ b/email2/src/com/android/email/activity/setup/AccountSettingsEditQuickResponsesFragment.java
@@ -16,9 +16,9 @@
 
 package com.android.email.activity.setup;
 
-import com.android.email.Email;
 import com.android.email.R;
 import com.android.email.activity.UiUtilities;
+import com.android.email2.ui.MailActivityEmail;
 import com.android.emailcommon.Logging;
 import com.android.emailcommon.provider.EmailContent;
 import com.android.emailcommon.provider.Account;
@@ -225,7 +225,7 @@
 
     @Override
     public void onCreate(Bundle savedInstanceState) {
-        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
+        if (Logging.DEBUG_LIFECYCLE && MailActivityEmail.DEBUG) {
             Log.d(Logging.LOG_TAG, "AccountSettingsEditQuickResponsesFragment onCreate");
         }
         super.onCreate(savedInstanceState);
@@ -238,7 +238,7 @@
     @Override
     public View onCreateView(LayoutInflater inflater, ViewGroup container,
             Bundle savedInstanceState) {
-        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
+        if (Logging.DEBUG_LIFECYCLE && MailActivityEmail.DEBUG) {
             Log.d(Logging.LOG_TAG, "AccountSettingsEditQuickResponsesFragment onCreateView");
         }
         int layoutId = R.layout.account_settings_edit_quick_responses_fragment;
diff --git a/email2/src/com/android/email/activity/setup/AccountSettingsFragment.java b/email2/src/com/android/email/activity/setup/AccountSettingsFragment.java
index e955dd4..ccd5f9c 100644
--- a/email2/src/com/android/email/activity/setup/AccountSettingsFragment.java
+++ b/email2/src/com/android/email/activity/setup/AccountSettingsFragment.java
@@ -42,10 +42,10 @@
 import android.text.TextUtils;
 import android.util.Log;
 
-import com.android.email.Email;
 import com.android.email.R;
 import com.android.email.SecurityPolicy;
 import com.android.email.mail.Sender;
+import com.android.email2.ui.MailActivityEmail;
 import com.android.emailcommon.AccountManagerTypes;
 import com.android.emailcommon.CalendarProviderStub;
 import com.android.emailcommon.Logging;
@@ -180,7 +180,7 @@
      */
     @Override
     public void onCreate(Bundle savedInstanceState) {
-        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
+        if (Logging.DEBUG_LIFECYCLE && MailActivityEmail.DEBUG) {
             Log.d(Logging.LOG_TAG, "AccountSettingsFragment onCreate");
         }
         super.onCreate(savedInstanceState);
@@ -204,7 +204,7 @@
 
     @Override
     public void onActivityCreated(Bundle savedInstanceState) {
-        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
+        if (Logging.DEBUG_LIFECYCLE && MailActivityEmail.DEBUG) {
             Log.d(Logging.LOG_TAG, "AccountSettingsFragment onActivityCreated");
         }
         super.onActivityCreated(savedInstanceState);
@@ -215,7 +215,7 @@
      */
     @Override
     public void onStart() {
-        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
+        if (Logging.DEBUG_LIFECYCLE && MailActivityEmail.DEBUG) {
             Log.d(Logging.LOG_TAG, "AccountSettingsFragment onStart");
         }
         super.onStart();
@@ -234,7 +234,7 @@
      */
     @Override
     public void onResume() {
-        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
+        if (Logging.DEBUG_LIFECYCLE && MailActivityEmail.DEBUG) {
             Log.d(Logging.LOG_TAG, "AccountSettingsFragment onResume");
         }
         super.onResume();
@@ -263,7 +263,7 @@
 
     @Override
     public void onPause() {
-        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
+        if (Logging.DEBUG_LIFECYCLE && MailActivityEmail.DEBUG) {
             Log.d(Logging.LOG_TAG, "AccountSettingsFragment onPause");
         }
         super.onPause();
@@ -277,7 +277,7 @@
      */
     @Override
     public void onStop() {
-        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
+        if (Logging.DEBUG_LIFECYCLE && MailActivityEmail.DEBUG) {
             Log.d(Logging.LOG_TAG, "AccountSettingsFragment onStop");
         }
         super.onStop();
@@ -348,7 +348,7 @@
      */
     @Override
     public void onDestroy() {
-        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
+        if (Logging.DEBUG_LIFECYCLE && MailActivityEmail.DEBUG) {
             Log.d(Logging.LOG_TAG, "AccountSettingsFragment onDestroy");
         }
         super.onDestroy();
@@ -359,7 +359,7 @@
 
     @Override
     public void onSaveInstanceState(Bundle outState) {
-        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
+        if (Logging.DEBUG_LIFECYCLE && MailActivityEmail.DEBUG) {
             Log.d(Logging.LOG_TAG, "AccountSettingsFragment onSaveInstanceState");
         }
         super.onSaveInstanceState(outState);
@@ -773,7 +773,7 @@
         mAccount.update(mContext, cv);
 
         // Run the remaining changes off-thread
-        Email.setServicesEnabledAsync(mContext);
+        MailActivityEmail.setServicesEnabledAsync(mContext);
     }
 
     /**
diff --git a/email2/src/com/android/email/activity/setup/AccountSetupExchangeFragment.java b/email2/src/com/android/email/activity/setup/AccountSetupExchangeFragment.java
index 1ff5bee..299c1f0 100644
--- a/email2/src/com/android/email/activity/setup/AccountSetupExchangeFragment.java
+++ b/email2/src/com/android/email/activity/setup/AccountSetupExchangeFragment.java
@@ -34,13 +34,13 @@
 import android.widget.EditText;
 import android.widget.TextView;
 
-import com.android.email.Email;
 import com.android.email.R;
 import com.android.email.activity.UiUtilities;
 import com.android.email.provider.AccountBackupRestore;
 import com.android.email.service.EmailServiceUtils;
 import com.android.email.view.CertificateSelector;
 import com.android.email.view.CertificateSelector.HostCallback;
+import com.android.email2.ui.MailActivityEmail;
 import com.android.emailcommon.Device;
 import com.android.emailcommon.Logging;
 import com.android.emailcommon.provider.Account;
@@ -81,7 +81,7 @@
      */
     @Override
     public void onCreate(Bundle savedInstanceState) {
-        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
+        if (Logging.DEBUG_LIFECYCLE && MailActivityEmail.DEBUG) {
             Log.d(Logging.LOG_TAG, "AccountSetupExchangeFragment onCreate");
         }
         super.onCreate(savedInstanceState);
@@ -96,7 +96,7 @@
     @Override
     public View onCreateView(LayoutInflater inflater, ViewGroup container,
             Bundle savedInstanceState) {
-        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
+        if (Logging.DEBUG_LIFECYCLE && MailActivityEmail.DEBUG) {
             Log.d(Logging.LOG_TAG, "AccountSetupExchangeFragment onCreateView");
         }
         int layoutId = mSettingsMode
@@ -152,7 +152,7 @@
 
     @Override
     public void onActivityCreated(Bundle savedInstanceState) {
-        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
+        if (Logging.DEBUG_LIFECYCLE && MailActivityEmail.DEBUG) {
             Log.d(Logging.LOG_TAG, "AccountSetupExchangeFragment onActivityCreated");
         }
         super.onActivityCreated(savedInstanceState);
@@ -164,7 +164,7 @@
      */
     @Override
     public void onStart() {
-        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
+        if (Logging.DEBUG_LIFECYCLE && MailActivityEmail.DEBUG) {
             Log.d(Logging.LOG_TAG, "AccountSetupExchangeFragment onStart");
         }
         super.onStart();
@@ -177,7 +177,7 @@
      */
     @Override
     public void onResume() {
-        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
+        if (Logging.DEBUG_LIFECYCLE && MailActivityEmail.DEBUG) {
             Log.d(Logging.LOG_TAG, "AccountSetupExchangeFragment onResume");
         }
         super.onResume();
@@ -186,7 +186,7 @@
 
     @Override
     public void onPause() {
-        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
+        if (Logging.DEBUG_LIFECYCLE && MailActivityEmail.DEBUG) {
             Log.d(Logging.LOG_TAG, "AccountSetupExchangeFragment onPause");
         }
         super.onPause();
@@ -197,7 +197,7 @@
      */
     @Override
     public void onStop() {
-        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
+        if (Logging.DEBUG_LIFECYCLE && MailActivityEmail.DEBUG) {
             Log.d(Logging.LOG_TAG, "AccountSetupExchangeFragment onStop");
         }
         super.onStop();
@@ -209,7 +209,7 @@
      */
     @Override
     public void onDestroy() {
-        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
+        if (Logging.DEBUG_LIFECYCLE && MailActivityEmail.DEBUG) {
             Log.d(Logging.LOG_TAG, "AccountSetupExchangeFragment onDestroy");
         }
         super.onDestroy();
@@ -217,7 +217,7 @@
 
     @Override
     public void onSaveInstanceState(Bundle outState) {
-        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
+        if (Logging.DEBUG_LIFECYCLE && MailActivityEmail.DEBUG) {
             Log.d(Logging.LOG_TAG, "AccountSetupExchangeFragment onSaveInstanceState");
         }
         super.onSaveInstanceState(outState);
diff --git a/email2/src/com/android/email/activity/setup/AccountSetupIncomingFragment.java b/email2/src/com/android/email/activity/setup/AccountSetupIncomingFragment.java
index efd63e3..d55e7df 100644
--- a/email2/src/com/android/email/activity/setup/AccountSetupIncomingFragment.java
+++ b/email2/src/com/android/email/activity/setup/AccountSetupIncomingFragment.java
@@ -34,10 +34,10 @@
 import android.widget.Spinner;
 import android.widget.TextView;
 
-import com.android.email.Email;
 import com.android.email.R;
 import com.android.email.activity.UiUtilities;
 import com.android.email.provider.AccountBackupRestore;
+import com.android.email2.ui.MailActivityEmail;
 import com.android.emailcommon.Logging;
 import com.android.emailcommon.provider.Account;
 import com.android.emailcommon.provider.HostAuth;
@@ -85,7 +85,7 @@
      */
     @Override
     public void onCreate(Bundle savedInstanceState) {
-        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
+        if (Logging.DEBUG_LIFECYCLE && MailActivityEmail.DEBUG) {
             Log.d(Logging.LOG_TAG, "AccountSetupIncomingFragment onCreate");
         }
         super.onCreate(savedInstanceState);
@@ -99,7 +99,7 @@
     @Override
     public View onCreateView(LayoutInflater inflater, ViewGroup container,
             Bundle savedInstanceState) {
-        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
+        if (Logging.DEBUG_LIFECYCLE && MailActivityEmail.DEBUG) {
             Log.d(Logging.LOG_TAG, "AccountSetupIncomingFragment onCreateView");
         }
         int layoutId = mSettingsMode
@@ -193,7 +193,7 @@
 
     @Override
     public void onActivityCreated(Bundle savedInstanceState) {
-        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
+        if (Logging.DEBUG_LIFECYCLE && MailActivityEmail.DEBUG) {
             Log.d(Logging.LOG_TAG, "AccountSetupIncomingFragment onActivityCreated");
         }
         super.onActivityCreated(savedInstanceState);
@@ -204,7 +204,7 @@
      */
     @Override
     public void onStart() {
-        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
+        if (Logging.DEBUG_LIFECYCLE && MailActivityEmail.DEBUG) {
             Log.d(Logging.LOG_TAG, "AccountSetupIncomingFragment onStart");
         }
         super.onStart();
@@ -218,7 +218,7 @@
      */
     @Override
     public void onResume() {
-        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
+        if (Logging.DEBUG_LIFECYCLE && MailActivityEmail.DEBUG) {
             Log.d(Logging.LOG_TAG, "AccountSetupIncomingFragment onResume");
         }
         super.onResume();
@@ -227,7 +227,7 @@
 
     @Override
     public void onPause() {
-        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
+        if (Logging.DEBUG_LIFECYCLE && MailActivityEmail.DEBUG) {
             Log.d(Logging.LOG_TAG, "AccountSetupIncomingFragment onPause");
         }
         super.onPause();
@@ -238,7 +238,7 @@
      */
     @Override
     public void onStop() {
-        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
+        if (Logging.DEBUG_LIFECYCLE && MailActivityEmail.DEBUG) {
             Log.d(Logging.LOG_TAG, "AccountSetupIncomingFragment onStop");
         }
         super.onStop();
@@ -250,7 +250,7 @@
      */
     @Override
     public void onDestroy() {
-        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
+        if (Logging.DEBUG_LIFECYCLE && MailActivityEmail.DEBUG) {
             Log.d(Logging.LOG_TAG, "AccountSetupIncomingFragment onDestroy");
         }
         super.onDestroy();
@@ -258,7 +258,7 @@
 
     @Override
     public void onSaveInstanceState(Bundle outState) {
-        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
+        if (Logging.DEBUG_LIFECYCLE && MailActivityEmail.DEBUG) {
             Log.d(Logging.LOG_TAG, "AccountSetupIncomingFragment onSaveInstanceState");
         }
         super.onSaveInstanceState(outState);
diff --git a/email2/src/com/android/email/activity/setup/AccountSetupOptions.java b/email2/src/com/android/email/activity/setup/AccountSetupOptions.java
index 1a246c9..a541751 100644
--- a/email2/src/com/android/email/activity/setup/AccountSetupOptions.java
+++ b/email2/src/com/android/email/activity/setup/AccountSetupOptions.java
@@ -35,12 +35,12 @@
 import android.widget.CheckBox;
 import android.widget.Spinner;
 
-import com.android.email.Email;
 import com.android.email.R;
 import com.android.email.activity.ActivityHelper;
 import com.android.email.activity.UiUtilities;
 import com.android.email.service.EmailServiceUtils;
 import com.android.email.service.MailService;
+import com.android.email2.ui.MailActivityEmail;
 import com.android.emailcommon.Logging;
 import com.android.emailcommon.provider.Account;
 import com.android.emailcommon.provider.HostAuth;
@@ -368,7 +368,7 @@
                 account.mFlags &= ~Account.FLAGS_SECURITY_HOLD;
                 AccountSettingsUtils.commitSettings(context, account);
                 // Start up services based on new account(s)
-                Email.setServicesEnabledSync(context);
+                MailActivityEmail.setServicesEnabledSync(context);
                 EmailServiceUtils.startExchangeService(context);
                 // Move to final setup screen
                 AccountSetupNames.actionSetNames(context);
diff --git a/email2/src/com/android/email/activity/setup/AccountSetupOutgoingFragment.java b/email2/src/com/android/email/activity/setup/AccountSetupOutgoingFragment.java
index b903932..59191fb 100644
--- a/email2/src/com/android/email/activity/setup/AccountSetupOutgoingFragment.java
+++ b/email2/src/com/android/email/activity/setup/AccountSetupOutgoingFragment.java
@@ -34,10 +34,10 @@
 import android.widget.EditText;
 import android.widget.Spinner;
 
-import com.android.email.Email;
 import com.android.email.R;
 import com.android.email.activity.UiUtilities;
 import com.android.email.provider.AccountBackupRestore;
+import com.android.email2.ui.MailActivityEmail;
 import com.android.emailcommon.Logging;
 import com.android.emailcommon.provider.Account;
 import com.android.emailcommon.provider.HostAuth;
@@ -74,7 +74,7 @@
      */
     @Override
     public void onCreate(Bundle savedInstanceState) {
-        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
+        if (Logging.DEBUG_LIFECYCLE && MailActivityEmail.DEBUG) {
             Log.d(Logging.LOG_TAG, "AccountSetupOutgoingFragment onCreate");
         }
         super.onCreate(savedInstanceState);
@@ -88,7 +88,7 @@
     @Override
     public View onCreateView(LayoutInflater inflater, ViewGroup container,
             Bundle savedInstanceState) {
-        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
+        if (Logging.DEBUG_LIFECYCLE && MailActivityEmail.DEBUG) {
             Log.d(Logging.LOG_TAG, "AccountSetupOutgoingFragment onCreateView");
         }
         int layoutId = mSettingsMode
@@ -160,7 +160,7 @@
 
     @Override
     public void onActivityCreated(Bundle savedInstanceState) {
-        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
+        if (Logging.DEBUG_LIFECYCLE && MailActivityEmail.DEBUG) {
             Log.d(Logging.LOG_TAG, "AccountSetupOutgoingFragment onActivityCreated");
         }
         super.onActivityCreated(savedInstanceState);
@@ -171,7 +171,7 @@
      */
     @Override
     public void onStart() {
-        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
+        if (Logging.DEBUG_LIFECYCLE && MailActivityEmail.DEBUG) {
             Log.d(Logging.LOG_TAG, "AccountSetupOutgoingFragment onStart");
         }
         super.onStart();
@@ -184,7 +184,7 @@
      */
     @Override
     public void onResume() {
-        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
+        if (Logging.DEBUG_LIFECYCLE && MailActivityEmail.DEBUG) {
             Log.d(Logging.LOG_TAG, "AccountSetupOutgoingFragment onResume");
         }
         super.onResume();
@@ -193,7 +193,7 @@
 
     @Override
     public void onPause() {
-        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
+        if (Logging.DEBUG_LIFECYCLE && MailActivityEmail.DEBUG) {
             Log.d(Logging.LOG_TAG, "AccountSetupOutgoingFragment onPause");
         }
         super.onPause();
@@ -204,7 +204,7 @@
      */
     @Override
     public void onStop() {
-        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
+        if (Logging.DEBUG_LIFECYCLE && MailActivityEmail.DEBUG) {
             Log.d(Logging.LOG_TAG, "AccountSetupOutgoingFragment onStop");
         }
         super.onStop();
@@ -216,7 +216,7 @@
      */
     @Override
     public void onDestroy() {
-        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
+        if (Logging.DEBUG_LIFECYCLE && MailActivityEmail.DEBUG) {
             Log.d(Logging.LOG_TAG, "AccountSetupOutgoingFragment onDestroy");
         }
         super.onDestroy();
@@ -224,7 +224,7 @@
 
     @Override
     public void onSaveInstanceState(Bundle outState) {
-        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
+        if (Logging.DEBUG_LIFECYCLE && MailActivityEmail.DEBUG) {
             Log.d(Logging.LOG_TAG, "AccountSetupOutgoingFragment onSaveInstanceState");
         }
         super.onSaveInstanceState(outState);
diff --git a/email2/src/com/android/email/activity/setup/DebugFragment.java b/email2/src/com/android/email/activity/setup/DebugFragment.java
index d2b8409..dab0056 100644
--- a/email2/src/com/android/email/activity/setup/DebugFragment.java
+++ b/email2/src/com/android/email/activity/setup/DebugFragment.java
@@ -16,11 +16,11 @@
 
 package com.android.email.activity.setup;
 
-import com.android.email.Email;
 import com.android.email.Preferences;
 import com.android.email.R;
 import com.android.email.activity.UiUtilities;
 import com.android.email.service.EmailServiceUtils;
+import com.android.email2.ui.MailActivityEmail;
 import com.android.emailcommon.Logging;
 
 import android.app.Fragment;
@@ -50,7 +50,7 @@
     @Override
     public View onCreateView(LayoutInflater inflater, ViewGroup container,
             Bundle savedInstanceState) {
-        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
+        if (Logging.DEBUG_LIFECYCLE && MailActivityEmail.DEBUG) {
             Log.d(Logging.LOG_TAG, "AccountSetupBasicsFragment onCreateView");
         }
         View view = inflater.inflate(R.layout.debug, container, false);
@@ -63,7 +63,7 @@
                 context.getString(R.string.build_number)));
 
         mEnableDebugLoggingView = (CheckBox) UiUtilities.getView(view, R.id.debug_logging);
-        mEnableDebugLoggingView.setChecked(Email.DEBUG);
+        mEnableDebugLoggingView.setChecked(MailActivityEmail.DEBUG);
 
         mEnableExchangeLoggingView = (CheckBox) UiUtilities.getView(view, R.id.exchange_logging);
         mEnableExchangeFileLoggingView =
@@ -74,8 +74,8 @@
 
         boolean exchangeAvailable = EmailServiceUtils.isExchangeAvailable(context);
         if (exchangeAvailable) {
-            mEnableExchangeLoggingView.setChecked(Email.DEBUG_EXCHANGE_VERBOSE);
-            mEnableExchangeFileLoggingView.setChecked(Email.DEBUG_EXCHANGE_FILE);
+            mEnableExchangeLoggingView.setChecked(MailActivityEmail.DEBUG_EXCHANGE_VERBOSE);
+            mEnableExchangeFileLoggingView.setChecked(MailActivityEmail.DEBUG_EXCHANGE_FILE);
             mEnableExchangeLoggingView.setOnCheckedChangeListener(this);
             mEnableExchangeFileLoggingView.setOnCheckedChangeListener(this);
         } else {
@@ -87,7 +87,7 @@
 
         mInhibitGraphicsAccelerationView = (CheckBox)
                 UiUtilities.getView(view, R.id.debug_disable_graphics_acceleration);
-        mInhibitGraphicsAccelerationView.setChecked(Email.sDebugInhibitGraphicsAcceleration);
+        mInhibitGraphicsAccelerationView.setChecked(MailActivityEmail.sDebugInhibitGraphicsAcceleration);
         mInhibitGraphicsAccelerationView.setOnCheckedChangeListener(this);
 
         mEnableStrictModeView = (CheckBox)
@@ -103,28 +103,28 @@
         switch (buttonView.getId()) {
             case R.id.debug_logging:
                 mPreferences.setEnableDebugLogging(isChecked);
-                Email.DEBUG = isChecked;
-                Email.DEBUG_EXCHANGE = isChecked;
+                MailActivityEmail.DEBUG = isChecked;
+                MailActivityEmail.DEBUG_EXCHANGE = isChecked;
                 break;
              case R.id.exchange_logging:
                 mPreferences.setEnableExchangeLogging(isChecked);
-                Email.DEBUG_EXCHANGE_VERBOSE = isChecked;
+                MailActivityEmail.DEBUG_EXCHANGE_VERBOSE = isChecked;
                 break;
             case R.id.exchange_file_logging:
                 mPreferences.setEnableExchangeFileLogging(isChecked);
-                Email.DEBUG_EXCHANGE_FILE = isChecked;
+                MailActivityEmail.DEBUG_EXCHANGE_FILE = isChecked;
                 break;
            case R.id.debug_disable_graphics_acceleration:
-                Email.sDebugInhibitGraphicsAcceleration = isChecked;
+                MailActivityEmail.sDebugInhibitGraphicsAcceleration = isChecked;
                 mPreferences.setInhibitGraphicsAcceleration(isChecked);
                 break;
             case R.id.debug_enable_strict_mode:
                 mPreferences.setEnableStrictMode(isChecked);
-                Email.enableStrictMode(isChecked);
+                MailActivityEmail.enableStrictMode(isChecked);
                 break;
         }
 
-        Email.updateLoggingFlags(getActivity());
+        MailActivityEmail.updateLoggingFlags(getActivity());
     }
 
     @Override
diff --git a/email2/src/com/android/email/activity/setup/MailboxSettings.java b/email2/src/com/android/email/activity/setup/MailboxSettings.java
index 99d69fd..a4eb141 100644
--- a/email2/src/com/android/email/activity/setup/MailboxSettings.java
+++ b/email2/src/com/android/email/activity/setup/MailboxSettings.java
@@ -32,9 +32,9 @@
 import android.util.Log;
 import android.view.MenuItem;
 
-import com.android.email.Email;
 import com.android.email.FolderProperties;
 import com.android.email.R;
+import com.android.email2.ui.MailActivityEmail;
 import com.android.emailcommon.Logging;
 import com.android.emailcommon.provider.Account;
 import com.android.emailcommon.provider.Policy;
@@ -292,7 +292,7 @@
                 return false;
             }
             mNeedsSave = true;
-            if (Email.DEBUG) {
+            if (MailActivityEmail.DEBUG) {
                 Log.i(Logging.LOG_TAG, "Setting changed");
             }
             // In order to set the current entry to the summary, we need to udpate the value
@@ -310,7 +310,7 @@
     private void updateObjects() {
         final int syncInterval = Integer.valueOf(mSyncIntervalPref.getValue());
         final int syncLookback = Integer.valueOf(mSyncLookbackPref.getValue());
-        if (Email.DEBUG) {
+        if (MailActivityEmail.DEBUG) {
             Log.i(Logging.LOG_TAG, "Updating object: " + syncInterval + "," + syncLookback);
         }
         if (mMailbox.mType == Mailbox.TYPE_INBOX) {
diff --git a/email2/src/com/android/email/mail/Store.java b/email2/src/com/android/email/mail/Store.java
index bdf9039..63e6f5a 100644
--- a/email2/src/com/android/email/mail/Store.java
+++ b/email2/src/com/android/email/mail/Store.java
@@ -20,10 +20,10 @@
 import android.os.Bundle;
 import android.util.Log;
 
-import com.android.email.Email;
 import com.android.email.mail.store.ExchangeStore;
 import com.android.email.mail.store.ImapStore;
 import com.android.email.mail.store.Pop3Store;
+import com.android.email2.ui.MailActivityEmail;
 import com.android.emailcommon.Logging;
 import com.android.emailcommon.mail.Folder;
 import com.android.emailcommon.mail.MessagingException;
@@ -204,6 +204,6 @@
         //mailbox.mSyncTime;
         mailbox.mType = type;
         //box.mUnreadCount;
-        mailbox.mVisibleLimit = Email.VISIBLE_LIMIT_DEFAULT;
+        mailbox.mVisibleLimit = MailActivityEmail.VISIBLE_LIMIT_DEFAULT;
     }
 }
diff --git a/email2/src/com/android/email/mail/store/ImapConnection.java b/email2/src/com/android/email/mail/store/ImapConnection.java
index 0fbf603..e83b9fc 100644
--- a/email2/src/com/android/email/mail/store/ImapConnection.java
+++ b/email2/src/com/android/email/mail/store/ImapConnection.java
@@ -19,7 +19,6 @@
 import android.text.TextUtils;
 import android.util.Log;
 
-import com.android.email.Email;
 import com.android.email.mail.Transport;
 import com.android.email.mail.store.ImapStore.ImapException;
 import com.android.email.mail.store.imap.ImapConstants;
@@ -29,6 +28,7 @@
 import com.android.email.mail.store.imap.ImapUtility;
 import com.android.email.mail.transport.DiscourseLogger;
 import com.android.email.mail.transport.MailTransport;
+import com.android.email2.ui.MailActivityEmail;
 import com.android.emailcommon.Logging;
 import com.android.emailcommon.mail.AuthenticationFailedException;
 import com.android.emailcommon.mail.CertificateValidationException;
@@ -146,7 +146,7 @@
 
             mImapStore.ensurePrefixIsValid();
         } catch (SSLException e) {
-            if (Email.DEBUG) {
+            if (MailActivityEmail.DEBUG) {
                 Log.d(Logging.LOG_TAG, e.toString());
             }
             throw new CertificateValidationException(e.getMessage(), e);
@@ -154,7 +154,7 @@
             // NOTE:  Unlike similar code in POP3, I'm going to rethrow as-is.  There is a lot
             // of other code here that catches IOException and I don't want to break it.
             // This catch is only here to enhance logging of connection-time issues.
-            if (Email.DEBUG) {
+            if (MailActivityEmail.DEBUG) {
                 Log.d(Logging.LOG_TAG, ioe.toString());
             }
             throw ioe;
@@ -391,7 +391,7 @@
                 executeSimpleCommand(mIdPhrase);
             } catch (ImapException ie) {
                 // Log for debugging, but this is not a fatal problem.
-                if (Email.DEBUG) {
+                if (MailActivityEmail.DEBUG) {
                     Log.d(Logging.LOG_TAG, ie.toString());
                 }
             } catch (IOException ioe) {
@@ -416,7 +416,7 @@
                 responseList = executeSimpleCommand(ImapConstants.NAMESPACE);
             } catch (ImapException ie) {
                 // Log for debugging, but this is not a fatal problem.
-                if (Email.DEBUG) {
+                if (MailActivityEmail.DEBUG) {
                     Log.d(Logging.LOG_TAG, ie.toString());
                 }
             } catch (IOException ioe) {
@@ -447,7 +447,7 @@
             // options such as SASL
             executeSimpleCommand(mLoginPhrase, true);
         } catch (ImapException ie) {
-            if (Email.DEBUG) {
+            if (MailActivityEmail.DEBUG) {
                 Log.d(Logging.LOG_TAG, ie.toString());
             }
             throw new AuthenticationFailedException(ie.getAlertText(), ie);
@@ -471,7 +471,7 @@
                 responseList = executeSimpleCommand(ImapConstants.LIST + " \"\" \"\"");
             } catch (ImapException ie) {
                 // Log for debugging, but this is not a fatal problem.
-                if (Email.DEBUG) {
+                if (MailActivityEmail.DEBUG) {
                     Log.d(Logging.LOG_TAG, ie.toString());
                 }
             } catch (IOException ioe) {
@@ -504,7 +504,7 @@
                 // Per RFC requirement (3501-6.2.1) gather new capabilities
                 return(queryCapabilities());
             } else {
-                if (Email.DEBUG) {
+                if (MailActivityEmail.DEBUG) {
                     Log.d(Logging.LOG_TAG, "TLS not supported but required");
                 }
                 throw new MessagingException(MessagingException.TLS_REQUIRED);
diff --git a/email2/src/com/android/email/mail/store/ImapFolder.java b/email2/src/com/android/email/mail/store/ImapFolder.java
index 98d25c9..0609bb8 100644
--- a/email2/src/com/android/email/mail/store/ImapFolder.java
+++ b/email2/src/com/android/email/mail/store/ImapFolder.java
@@ -21,7 +21,6 @@
 import android.util.Base64DataException;
 import android.util.Log;
 
-import com.android.email.Email;
 import com.android.email.mail.store.ImapStore.ImapException;
 import com.android.email.mail.store.ImapStore.ImapMessage;
 import com.android.email.mail.store.imap.ImapConstants;
@@ -32,6 +31,7 @@
 import com.android.email.mail.store.imap.ImapUtility;
 import com.android.email.mail.transport.CountingOutputStream;
 import com.android.email.mail.transport.EOLConvertingOutputStream;
+import com.android.email2.ui.MailActivityEmail;
 import com.android.emailcommon.Logging;
 import com.android.emailcommon.internet.BinaryTempFileBody;
 import com.android.emailcommon.internet.MimeBodyPart;
@@ -696,7 +696,7 @@
                 }
             }
         } catch (Base64DataException bde) {
-            String warning = "\n\n" + Email.getMessageDecodeErrorString();
+            String warning = "\n\n" + MailActivityEmail.getMessageDecodeErrorString();
             out.write(warning.getBytes());
         } finally {
             out.close();
@@ -1110,7 +1110,7 @@
     }
 
     private MessagingException ioExceptionHandler(ImapConnection connection, IOException ioe) {
-        if (Email.DEBUG) {
+        if (MailActivityEmail.DEBUG) {
             Log.d(Logging.LOG_TAG, "IO Exception detected: ", ioe);
         }
         connection.close();
diff --git a/email2/src/com/android/email/mail/store/Pop3Store.java b/email2/src/com/android/email/mail/store/Pop3Store.java
index 1e01ba8..6eaa6e5 100644
--- a/email2/src/com/android/email/mail/store/Pop3Store.java
+++ b/email2/src/com/android/email/mail/store/Pop3Store.java
@@ -20,11 +20,11 @@
 import android.os.Bundle;
 import android.util.Log;
 
-import com.android.email.Email;
 import com.android.email.R;
 import com.android.email.mail.Store;
 import com.android.email.mail.Transport;
 import com.android.email.mail.transport.MailTransport;
+import com.android.email2.ui.MailActivityEmail;
 import com.android.emailcommon.Logging;
 import com.android.emailcommon.internet.MimeMessage;
 import com.android.emailcommon.mail.AuthenticationFailedException;
@@ -311,7 +311,7 @@
                         executeSimpleCommand("STLS");
                         mTransport.reopenTls();
                     } else {
-                        if (Email.DEBUG) {
+                        if (MailActivityEmail.DEBUG) {
                             Log.d(Logging.LOG_TAG, "TLS not supported but required");
                         }
                         throw new MessagingException(MessagingException.TLS_REQUIRED);
@@ -322,14 +322,14 @@
                     executeSensitiveCommand("USER " + mUsername, "USER /redacted/");
                     executeSensitiveCommand("PASS " + mPassword, "PASS /redacted/");
                 } catch (MessagingException me) {
-                    if (Email.DEBUG) {
+                    if (MailActivityEmail.DEBUG) {
                         Log.d(Logging.LOG_TAG, me.toString());
                     }
                     throw new AuthenticationFailedException(null, me);
                 }
             } catch (IOException ioe) {
                 mTransport.close();
-                if (Email.DEBUG) {
+                if (MailActivityEmail.DEBUG) {
                     Log.d(Logging.LOG_TAG, ioe.toString());
                 }
                 throw new MessagingException(MessagingException.IOERROR, ioe.toString());
@@ -351,7 +351,7 @@
             }
             if (statException != null) {
                 mTransport.close();
-                if (Email.DEBUG) {
+                if (MailActivityEmail.DEBUG) {
                     Log.d(Logging.LOG_TAG, statException.toString());
                 }
                 throw new MessagingException("POP3 STAT", statException);
@@ -422,7 +422,7 @@
                     indexMsgNums(1, mMessageCount);
                 } catch (IOException ioe) {
                     mTransport.close();
-                    if (Email.DEBUG) {
+                    if (MailActivityEmail.DEBUG) {
                         Log.d(Logging.LOG_TAG, "Unable to index during getMessage " + ioe);
                     }
                     throw new MessagingException("getMessages", ioe);
@@ -443,7 +443,7 @@
                 indexMsgNums(start, end);
             } catch (IOException ioe) {
                 mTransport.close();
-                if (Email.DEBUG) {
+                if (MailActivityEmail.DEBUG) {
                     Log.d(Logging.LOG_TAG, ioe.toString());
                 }
                 throw new MessagingException("getMessages", ioe);
@@ -687,7 +687,7 @@
                 }
             } catch (IOException ioe) {
                 mTransport.close();
-                if (Email.DEBUG) {
+                if (MailActivityEmail.DEBUG) {
                     Log.d(Logging.LOG_TAG, ioe.toString());
                 }
                 throw new MessagingException("fetch", ioe);
@@ -722,7 +722,7 @@
                     }
                 } catch (IOException ioe) {
                     mTransport.close();
-                    if (Email.DEBUG) {
+                    if (MailActivityEmail.DEBUG) {
                         Log.d(Logging.LOG_TAG, ioe.toString());
                     }
                     throw new MessagingException("Unable to fetch message", ioe);
@@ -829,7 +829,7 @@
             if (response != null)  {
                 try {
                     InputStream in = mTransport.getInputStream();
-                    if (DEBUG_LOG_RAW_STREAM && Email.DEBUG) {
+                    if (DEBUG_LOG_RAW_STREAM && MailActivityEmail.DEBUG) {
                         in = new LoggingInputStream(in);
                     }
                     message.parse(new Pop3ResponseInputStream(in));
@@ -883,7 +883,7 @@
             }
             catch (IOException ioe) {
                 mTransport.close();
-                if (Email.DEBUG) {
+                if (MailActivityEmail.DEBUG) {
                     Log.d(Logging.LOG_TAG, ioe.toString());
                 }
                 throw new MessagingException("setFlags()", ioe);
diff --git a/email2/src/com/android/email/mail/store/imap/ImapResponseParser.java b/email2/src/com/android/email/mail/store/imap/ImapResponseParser.java
index 6b68c00..39bbe26 100644
--- a/email2/src/com/android/email/mail/store/imap/ImapResponseParser.java
+++ b/email2/src/com/android/email/mail/store/imap/ImapResponseParser.java
@@ -16,10 +16,10 @@
 
 package com.android.email.mail.store.imap;
 
-import com.android.email.Email;
 import com.android.email.FixedLengthInputStream;
 import com.android.email.PeekableInputStream;
 import com.android.email.mail.transport.DiscourseLogger;
+import com.android.email2.ui.MailActivityEmail;
 import com.android.emailcommon.Logging;
 import com.android.emailcommon.mail.MessagingException;
 import com.android.emailcommon.utility.LoggingInputStream;
@@ -89,7 +89,7 @@
      */
     /* package for test */ ImapResponseParser(InputStream in, DiscourseLogger discourseLogger,
             int literalKeepInMemoryThreshold) {
-        if (DEBUG_LOG_RAW_STREAM && Email.DEBUG) {
+        if (DEBUG_LOG_RAW_STREAM && MailActivityEmail.DEBUG) {
             in = new LoggingInputStream(in);
         }
         mIn = new PeekableInputStream(in);
@@ -99,7 +99,7 @@
 
     private static IOException newEOSException() {
         final String message = "End of stream reached";
-        if (Email.DEBUG) {
+        if (MailActivityEmail.DEBUG) {
             Log.d(Logging.LOG_TAG, message);
         }
         return new IOException(message);
@@ -161,7 +161,7 @@
         ImapResponse response = null;
         try {
             response = parseResponse();
-            if (Email.DEBUG) {
+            if (MailActivityEmail.DEBUG) {
                 Log.d(Logging.LOG_TAG, "<<< " + response.toString());
             }
 
diff --git a/email2/src/com/android/email/mail/transport/MailTransport.java b/email2/src/com/android/email/mail/transport/MailTransport.java
index 751be50..7f59f46 100644
--- a/email2/src/com/android/email/mail/transport/MailTransport.java
+++ b/email2/src/com/android/email/mail/transport/MailTransport.java
@@ -16,8 +16,8 @@
 
 package com.android.email.mail.transport;
 
-import com.android.email.Email;
 import com.android.email.mail.Transport;
+import com.android.email2.ui.MailActivityEmail;
 import com.android.emailcommon.Logging;
 import com.android.emailcommon.mail.CertificateValidationException;
 import com.android.emailcommon.mail.MessagingException;
@@ -159,7 +159,7 @@
      */
     @Override
     public void open() throws MessagingException, CertificateValidationException {
-        if (Email.DEBUG) {
+        if (MailActivityEmail.DEBUG) {
             Log.d(Logging.LOG_TAG, "*** " + mDebugLabel + " open " +
                     getHost() + ":" + String.valueOf(getPort()));
         }
@@ -180,12 +180,12 @@
             mOut = new BufferedOutputStream(mSocket.getOutputStream(), 512);
 
         } catch (SSLException e) {
-            if (Email.DEBUG) {
+            if (MailActivityEmail.DEBUG) {
                 Log.d(Logging.LOG_TAG, e.toString());
             }
             throw new CertificateValidationException(e.getMessage(), e);
         } catch (IOException ioe) {
-            if (Email.DEBUG) {
+            if (MailActivityEmail.DEBUG) {
                 Log.d(Logging.LOG_TAG, ioe.toString());
             }
             throw new MessagingException(MessagingException.IOERROR, ioe.toString());
@@ -210,12 +210,12 @@
             mOut = new BufferedOutputStream(mSocket.getOutputStream(), 512);
 
         } catch (SSLException e) {
-            if (Email.DEBUG) {
+            if (MailActivityEmail.DEBUG) {
                 Log.d(Logging.LOG_TAG, e.toString());
             }
             throw new CertificateValidationException(e.getMessage(), e);
         } catch (IOException ioe) {
-            if (Email.DEBUG) {
+            if (MailActivityEmail.DEBUG) {
                 Log.d(Logging.LOG_TAG, ioe.toString());
             }
             throw new MessagingException(MessagingException.IOERROR, ioe.toString());
@@ -316,7 +316,7 @@
      */
     @Override
     public void writeLine(String s, String sensitiveReplacement) throws IOException {
-        if (Email.DEBUG) {
+        if (MailActivityEmail.DEBUG) {
             if (sensitiveReplacement != null && !Logging.DEBUG_SENSITIVE) {
                 Log.d(Logging.LOG_TAG, ">>> " + sensitiveReplacement);
             } else {
@@ -349,11 +349,11 @@
                 sb.append((char)d);
             }
         }
-        if (d == -1 && Email.DEBUG) {
+        if (d == -1 && MailActivityEmail.DEBUG) {
             Log.d(Logging.LOG_TAG, "End of stream reached while trying to read line.");
         }
         String ret = sb.toString();
-        if (Email.DEBUG) {
+        if (MailActivityEmail.DEBUG) {
             Log.d(Logging.LOG_TAG, "<<< " + ret);
         }
         return ret;
diff --git a/email2/src/com/android/email/mail/transport/SmtpSender.java b/email2/src/com/android/email/mail/transport/SmtpSender.java
index a9b13a6..3ceb330 100644
--- a/email2/src/com/android/email/mail/transport/SmtpSender.java
+++ b/email2/src/com/android/email/mail/transport/SmtpSender.java
@@ -20,9 +20,9 @@
 import android.util.Base64;
 import android.util.Log;
 
-import com.android.email.Email;
 import com.android.email.mail.Sender;
 import com.android.email.mail.Transport;
+import com.android.email2.ui.MailActivityEmail;
 import com.android.emailcommon.Logging;
 import com.android.emailcommon.internet.Rfc822Output;
 import com.android.emailcommon.mail.Address;
@@ -145,7 +145,7 @@
                      */
                     result = executeSimpleCommand("EHLO " + localHost);
                 } else {
-                    if (Email.DEBUG) {
+                    if (MailActivityEmail.DEBUG) {
                         Log.d(Logging.LOG_TAG, "TLS not supported but required");
                     }
                     throw new MessagingException(MessagingException.TLS_REQUIRED);
@@ -167,19 +167,19 @@
                     saslAuthLogin(mUsername, mPassword);
                 }
                 else {
-                    if (Email.DEBUG) {
+                    if (MailActivityEmail.DEBUG) {
                         Log.d(Logging.LOG_TAG, "No valid authentication mechanism found.");
                     }
                     throw new MessagingException(MessagingException.AUTH_REQUIRED);
                 }
             }
         } catch (SSLException e) {
-            if (Email.DEBUG) {
+            if (MailActivityEmail.DEBUG) {
                 Log.d(Logging.LOG_TAG, e.toString());
             }
             throw new CertificateValidationException(e.getMessage(), e);
         } catch (IOException ioe) {
-            if (Email.DEBUG) {
+            if (MailActivityEmail.DEBUG) {
                 Log.d(Logging.LOG_TAG, ioe.toString());
             }
             throw new MessagingException(MessagingException.IOERROR, ioe.toString());
diff --git a/email2/src/com/android/email/provider/ContentCache.java b/email2/src/com/android/email/provider/ContentCache.java
index 041f296..0968fd2 100644
--- a/email2/src/com/android/email/provider/ContentCache.java
+++ b/email2/src/com/android/email/provider/ContentCache.java
@@ -26,7 +26,7 @@
 import android.util.Log;
 import android.util.LruCache;
 
-import com.android.email.Email;
+import com.android.email2.ui.MailActivityEmail;
 import com.google.common.annotations.VisibleForTesting;
 
 import java.util.ArrayList;
@@ -184,7 +184,7 @@
         }
 
         /*package*/ int invalidateTokens(String id) {
-            if (Email.DEBUG && DEBUG_TOKENS) {
+            if (MailActivityEmail.DEBUG && DEBUG_TOKENS) {
                 Log.d(mLogTag, "============ Invalidate tokens for: " + id);
             }
             ArrayList<CacheToken> removeList = new ArrayList<CacheToken>();
@@ -203,7 +203,7 @@
         }
 
         /*package*/ void invalidate() {
-            if (Email.DEBUG && DEBUG_TOKENS) {
+            if (MailActivityEmail.DEBUG && DEBUG_TOKENS) {
                 Log.d(mLogTag, "============ List invalidated");
             }
             for (CacheToken token: this) {
@@ -214,7 +214,7 @@
 
         /*package*/ boolean remove(CacheToken token) {
             boolean result = super.remove(token);
-            if (Email.DEBUG && DEBUG_TOKENS) {
+            if (MailActivityEmail.DEBUG && DEBUG_TOKENS) {
                 if (result) {
                     Log.d(mLogTag, "============ Removing token for: " + token.mId);
                 } else {
@@ -227,7 +227,7 @@
         public CacheToken add(String id) {
             CacheToken token = new CacheToken(id);
             super.add(token);
-            if (Email.DEBUG && DEBUG_TOKENS) {
+            if (MailActivityEmail.DEBUG && DEBUG_TOKENS) {
                 Log.d(mLogTag, "============ Taking token for: " + token.mId);
             }
             return token;
@@ -482,14 +482,14 @@
             CacheToken token) {
         try {
             if (!token.isValid()) {
-                if (Email.DEBUG && DEBUG_CACHE) {
+                if (MailActivityEmail.DEBUG && DEBUG_CACHE) {
                     Log.d(mLogTag, "============ Stale token for " + id);
                 }
                 mStats.mStaleCount++;
                 return c;
             }
             if (c != null && Arrays.equals(projection, mBaseProjection) && !sLockCache) {
-                if (Email.DEBUG && DEBUG_CACHE) {
+                if (MailActivityEmail.DEBUG && DEBUG_CACHE) {
                     Log.d(mLogTag, "============ Caching cursor for: " + id);
                 }
                 // If we've already cached this cursor, invalidate the older one
@@ -513,7 +513,7 @@
      * @return a cursor based on cached values, or null if the row is not cached
      */
     public synchronized Cursor getCachedCursor(String id, String[] projection) {
-        if (Email.DEBUG && DEBUG_STATISTICS) {
+        if (MailActivityEmail.DEBUG && DEBUG_STATISTICS) {
             // Every 200 calls to getCursor, report cache statistics
             dumpOnCount(200);
         }
@@ -594,7 +594,7 @@
         mLockMap.add(id);
         // Invalidate current tokens
         int count = mTokenList.invalidateTokens(id);
-        if (Email.DEBUG && DEBUG_TOKENS) {
+        if (MailActivityEmail.DEBUG && DEBUG_TOKENS) {
             Log.d(mTokenList.mLogTag, "============ Lock invalidated " + count +
                     " tokens for: " + id);
         }
@@ -631,13 +631,13 @@
     private void unlockImpl(String id, ContentValues values, boolean wasLocked) {
         Cursor c = get(id);
         if (c != null) {
-            if (Email.DEBUG && DEBUG_CACHE) {
+            if (MailActivityEmail.DEBUG && DEBUG_CACHE) {
                 Log.d(mLogTag, "=========== Unlocking cache for: " + id);
             }
             if (values != null && !sLockCache) {
                 MatrixCursor cursor = getMatrixCursor(id, mBaseProjection, values);
                 if (cursor != null) {
-                    if (Email.DEBUG && DEBUG_CACHE) {
+                    if (MailActivityEmail.DEBUG && DEBUG_CACHE) {
                         Log.d(mLogTag, "=========== Recaching with new values: " + id);
                     }
                     cursor.moveToFirst();
diff --git a/email2/src/com/android/email/provider/DBHelper.java b/email2/src/com/android/email/provider/DBHelper.java
index 791adc2..68c4dd9 100644
--- a/email2/src/com/android/email/provider/DBHelper.java
+++ b/email2/src/com/android/email/provider/DBHelper.java
@@ -27,7 +27,7 @@
 import android.provider.ContactsContract;
 import android.util.Log;
 
-import com.android.email.Email;
+import com.android.email2.ui.MailActivityEmail;
 import com.android.emailcommon.AccountManagerTypes;
 import com.android.emailcommon.CalendarProviderStub;
 import com.android.emailcommon.mail.Address;
@@ -977,7 +977,7 @@
                             // If this is a pop3 or imap account, create the account manager account
                             if (HostAuth.SCHEME_IMAP.equals(protocol) ||
                                     HostAuth.SCHEME_POP3.equals(protocol)) {
-                                if (Email.DEBUG) {
+                                if (MailActivityEmail.DEBUG) {
                                     Log.d(TAG, "Create AccountManager account for " + protocol +
                                             "account: " +
                                             accountCursor.getString(V21_ACCOUNT_EMAIL));
diff --git a/email2/src/com/android/email/provider/EmailProvider.java b/email2/src/com/android/email/provider/EmailProvider.java
index 437475c..bc0e9d9 100644
--- a/email2/src/com/android/email/provider/EmailProvider.java
+++ b/email2/src/com/android/email/provider/EmailProvider.java
@@ -38,13 +38,13 @@
 import android.util.Log;
 
 import com.android.common.content.ProjectionMap;
-import com.android.email.Email;
 import com.android.email.Preferences;
 import com.android.email.R;
 import com.android.email.SecurityPolicy;
 import com.android.email.provider.ContentCache.CacheToken;
 import com.android.email.service.AttachmentDownloadService;
 import com.android.email.service.EmailServiceUtils;
+import com.android.email2.ui.MailActivityEmail;
 import com.android.emailcommon.Logging;
 import com.android.emailcommon.provider.Account;
 import com.android.emailcommon.provider.EmailContent;
@@ -216,6 +216,7 @@
     private static final int UI_SEARCH = UI_BASE + 16;
     private static final int UI_ACCOUNT_DATA = UI_BASE + 17;
     private static final int UI_FOLDER_LOAD_MORE = UI_BASE + 18;
+    private static final int UI_CONVERSATION = UI_BASE + 19;
 
     // MUST ALWAYS EQUAL THE LAST OF THE PREVIOUS BASE CONSTANTS
     private static final int LAST_EMAIL_PROVIDER_DB_BASE = UI_BASE;
@@ -432,6 +433,7 @@
         matcher.addURI(EmailContent.AUTHORITY, "uisearch/#", UI_SEARCH);
         matcher.addURI(EmailContent.AUTHORITY, "uiaccountdata/#", UI_ACCOUNT_DATA);
         matcher.addURI(EmailContent.AUTHORITY, "uiloadmore/#", UI_FOLDER_LOAD_MORE);
+        matcher.addURI(EmailContent.AUTHORITY, "uiconversation/#", UI_CONVERSATION);
     }
 
     /**
@@ -503,7 +505,7 @@
         // Restore accounts if the database is corrupted...
         restoreIfNeeded(context, mDatabase);
 
-        if (Email.DEBUG) {
+        if (MailActivityEmail.DEBUG) {
             Log.d(TAG, "Deleting orphans...");
         }
         // Check for any orphaned Messages in the updated/deleted tables
@@ -517,11 +519,11 @@
         deleteUnlinked(mDatabase, Policy.TABLE_NAME, PolicyColumns.ID, AccountColumns.POLICY_KEY,
                 Account.TABLE_NAME);
 
-        if (Email.DEBUG) {
+        if (MailActivityEmail.DEBUG) {
             Log.d(TAG, "EmailProvider pre-caching...");
         }
         preCacheData();
-        if (Email.DEBUG) {
+        if (MailActivityEmail.DEBUG) {
             Log.d(TAG, "EmailProvider ready.");
         }
         return mDatabase;
@@ -622,7 +624,7 @@
      * Restore user Account and HostAuth data from our backup database
      */
     public static void restoreIfNeeded(Context context, SQLiteDatabase mainDatabase) {
-        if (Email.DEBUG) {
+        if (MailActivityEmail.DEBUG) {
             Log.w(TAG, "restoreIfNeeded...");
         }
         // Check for legacy backup
@@ -642,7 +644,7 @@
                 null, null, null);
         try {
             if (c.moveToFirst()) {
-                if (Email.DEBUG) {
+                if (MailActivityEmail.DEBUG) {
                     Log.w(TAG, "restoreIfNeeded: Account exists.");
                 }
                 return; // At least one account exists.
@@ -1120,7 +1122,7 @@
     public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
             String sortOrder) {
         long time = 0L;
-        if (Email.DEBUG) {
+        if (MailActivityEmail.DEBUG) {
             time = System.nanoTime();
         }
         Cursor c = null;
@@ -1186,6 +1188,7 @@
                 case UI_SETTINGS:
                 case UI_ATTACHMENT:
                 case UI_ATTACHMENTS:
+                case UI_CONVERSATION:
                     // For now, we don't allow selection criteria within these queries
                     if (selection != null || selectionArgs != null) {
                         throw new IllegalArgumentException("UI queries can't have selection/args");
@@ -1320,7 +1323,7 @@
             e.printStackTrace();
             throw e;
         } finally {
-            if (cache != null && c != null && Email.DEBUG) {
+            if (cache != null && c != null && MailActivityEmail.DEBUG) {
                 cache.recordQueryTime(c, System.nanoTime() - time);
             }
             if (c == null) {
@@ -1471,7 +1474,7 @@
      * Backup account data, returning the number of accounts backed up
      */
     private static int backupAccounts(Context context, SQLiteDatabase mainDatabase) {
-        if (Email.DEBUG) {
+        if (MailActivityEmail.DEBUG) {
             Log.d(TAG, "backupAccounts...");
         }
         SQLiteDatabase backupDatabase = getBackupDatabase(context);
@@ -1479,7 +1482,7 @@
             int numBackedUp = copyAccountTables(mainDatabase, backupDatabase);
             if (numBackedUp < 0) {
                 Log.e(TAG, "Account backup failed!");
-            } else if (Email.DEBUG) {
+            } else if (MailActivityEmail.DEBUG) {
                 Log.d(TAG, "Backed up " + numBackedUp + " accounts...");
             }
             return numBackedUp;
@@ -1494,7 +1497,7 @@
      * Restore account data, returning the number of accounts restored
      */
     private static int restoreAccounts(Context context, SQLiteDatabase mainDatabase) {
-        if (Email.DEBUG) {
+        if (MailActivityEmail.DEBUG) {
             Log.d(TAG, "restoreAccounts...");
         }
         SQLiteDatabase backupDatabase = getBackupDatabase(context);
@@ -1504,7 +1507,7 @@
                 Log.e(TAG, "Recovered " + numRecovered + " accounts!");
             } else if (numRecovered < 0) {
                 Log.e(TAG, "Account recovery failed?");
-            } else if (Email.DEBUG) {
+            } else if (MailActivityEmail.DEBUG) {
                 Log.d(TAG, "No accounts to restore...");
             }
             return numRecovered;
@@ -2174,6 +2177,19 @@
     }
 
     /**
+     * Generate the "message list" SQLite query, given a projection from UnifiedEmail
+     *
+     * @param uiProjection as passed from UnifiedEmail
+     * @return the SQLite query to be executed on the EmailProvider database
+     */
+    private String genQueryConversation(String[] uiProjection) {
+        StringBuilder sb = genSelect(sMessageListMap, uiProjection);
+        // Make constant
+        sb.append(" FROM " + Message.TABLE_NAME + " WHERE " + Message.RECORD_ID + "=?");
+        return sb.toString();
+    }
+
+    /**
      * Generate the "top level folder list" SQLite query, given a projection from UnifiedEmail
      *
      * @param uiProjection as passed from UnifiedEmail
@@ -2431,6 +2447,9 @@
                 c = db.rawQuery(genQuerySettings(uiProjection, id), new String[] {id});
                 notifyUri = UIPROVIDER_SETTINGS_NOTIFIER.buildUpon().appendPath(id).build();
                 break;
+            case UI_CONVERSATION:
+                c = db.rawQuery(genQueryConversation(uiProjection), new String[] {id});
+                break;
         }
         if (notifyUri != null) {
             c.setNotificationUri(resolver, notifyUri);
@@ -3055,7 +3074,7 @@
             // Clean up
             AccountBackupRestore.backup(context);
             SecurityPolicy.getInstance(context).reducePolicies();
-            Email.setServicesEnabledSync(context);
+            MailActivityEmail.setServicesEnabledSync(context);
             return 1;
         } catch (Exception e) {
             Log.w(Logging.LOG_TAG, "Exception while deleting account", e);
diff --git a/email2/src/com/android/email/service/AccountService.java b/email2/src/com/android/email/service/AccountService.java
index 88bd646..a12108a 100644
--- a/email2/src/com/android/email/service/AccountService.java
+++ b/email2/src/com/android/email/service/AccountService.java
@@ -24,11 +24,11 @@
 import android.os.Bundle;
 import android.os.IBinder;
 
-import com.android.email.Email;
 import com.android.email.NotificationController;
 import com.android.email.ResourceHelper;
 import com.android.email.VendorPolicyLoader;
 import com.android.email.provider.AccountReconciler;
+import com.android.email2.ui.MailActivityEmail;
 import com.android.emailcommon.Configuration;
 import com.android.emailcommon.Device;
 import com.android.emailcommon.provider.Account;
@@ -106,7 +106,7 @@
                         // Make sure the service is properly running (re: lifecycle)
                         EmailServiceUtils.startExchangeService(mContext);
                         // Send current logging flags
-                        Email.updateLoggingFlags(mContext);
+                        MailActivityEmail.updateLoggingFlags(mContext);
                     }});
                 return Device.getDeviceId(mContext);
             } catch (IOException e) {
diff --git a/email2/src/com/android/email/service/AttachmentDownloadService.java b/email2/src/com/android/email/service/AttachmentDownloadService.java
index 7a72b6d..2a78749 100644
--- a/email2/src/com/android/email/service/AttachmentDownloadService.java
+++ b/email2/src/com/android/email/service/AttachmentDownloadService.java
@@ -33,9 +33,9 @@
 import android.util.Log;
 
 import com.android.email.AttachmentInfo;
-import com.android.email.Email;
 import com.android.email.EmailConnectivityManager;
 import com.android.email.NotificationController;
+import com.android.email2.ui.MailActivityEmail;
 import com.android.emailcommon.provider.Account;
 import com.android.emailcommon.provider.EmailContent;
 import com.android.emailcommon.provider.EmailContent.Attachment;
@@ -257,14 +257,14 @@
             DownloadRequest req = findDownloadRequest(att.mId);
             long priority = getPriority(att);
             if (priority == PRIORITY_NONE) {
-                if (Email.DEBUG) {
+                if (MailActivityEmail.DEBUG) {
                     Log.d(TAG, "== Attachment changed: " + att.mId);
                 }
                 // In this case, there is no download priority for this attachment
                 if (req != null) {
                     // If it exists in the map, remove it
                     // NOTE: We don't yet support deleting downloads in progress
-                    if (Email.DEBUG) {
+                    if (MailActivityEmail.DEBUG) {
                         Log.d(TAG, "== Attachment " + att.mId + " was in queue, removing");
                     }
                     remove(req);
@@ -279,7 +279,7 @@
                 }
                 // If the request already existed, we'll update the priority (so that the time is
                 // up-to-date); otherwise, we create a new request
-                if (Email.DEBUG) {
+                if (MailActivityEmail.DEBUG) {
                     Log.d(TAG, "== Download queued for attachment " + att.mId + ", class " +
                             req.priority + ", priority time " + req.time);
                 }
@@ -314,7 +314,7 @@
          * the limit on maximum downloads
          */
         /*package*/ synchronized void processQueue() {
-            if (Email.DEBUG) {
+            if (MailActivityEmail.DEBUG) {
                 Log.d(TAG, "== Checking attachment queue, " + mDownloadSet.size() + " entries");
             }
 
@@ -325,7 +325,7 @@
                 DownloadRequest req = iterator.next();
                  // Enforce per-account limit here
                 if (downloadsForAccount(req.accountId) >= MAX_SIMULTANEOUS_DOWNLOADS_PER_ACCOUNT) {
-                    if (Email.DEBUG) {
+                    if (MailActivityEmail.DEBUG) {
                         Log.d(TAG, "== Skip #" + req.attachmentId + "; maxed for acct #" +
                                 req.accountId);
                     }
@@ -427,7 +427,7 @@
                 // Check how long it's been since receiving a callback
                 long timeSinceCallback = now - req.lastCallbackTime;
                 if (timeSinceCallback > CALLBACK_TIMEOUT) {
-                    if (Email.DEBUG) {
+                    if (MailActivityEmail.DEBUG) {
                         Log.d(TAG, "== Download of " + req.attachmentId + " timed out");
                     }
                    cancelDownload(req);
@@ -458,7 +458,7 @@
             if (alreadyInProgress) return false;
 
             try {
-                if (Email.DEBUG) {
+                if (MailActivityEmail.DEBUG) {
                     Log.d(TAG, ">> Starting download for attachment #" + req.attachmentId);
                 }
                 startDownload(service, req);
@@ -537,7 +537,7 @@
             DownloadRequest req = mDownloadSet.findDownloadRequest(attachmentId);
             if (statusCode == EmailServiceStatus.CONNECTION_ERROR) {
                 // If this needs to be retried, just process the queue again
-                if (Email.DEBUG) {
+                if (MailActivityEmail.DEBUG) {
                     Log.d(TAG, "== The download for attachment #" + attachmentId +
                             " will be retried");
                 }
@@ -552,7 +552,7 @@
             if (req != null) {
                 remove(req);
             }
-            if (Email.DEBUG) {
+            if (MailActivityEmail.DEBUG) {
                 long secs = 0;
                 if (req != null) {
                     secs = (System.currentTimeMillis() - req.time) / 1000;
@@ -588,7 +588,7 @@
                     // try to send pending mail now (as mediated by MailService)
                     if ((req != null) &&
                             !Utility.hasUnloadedAttachments(mContext, attachment.mMessageKey)) {
-                        if (Email.DEBUG) {
+                        if (MailActivityEmail.DEBUG) {
                             Log.d(TAG, "== Downloads finished for outgoing msg #" + req.messageId);
                         }
                         MailService.actionSendPendingMail(mContext, req.accountId);
@@ -657,7 +657,7 @@
             // Record status and progress
             DownloadRequest req = mDownloadSet.getDownloadInProgress(attachmentId);
             if (req != null) {
-                if (Email.DEBUG) {
+                if (MailActivityEmail.DEBUG) {
                     String code;
                     switch(statusCode) {
                         case EmailServiceStatus.SUCCESS: code = "Success"; break;
@@ -732,7 +732,7 @@
     /*package*/ boolean dequeue(long attachmentId) {
         DownloadRequest req = mDownloadSet.findDownloadRequest(attachmentId);
         if (req != null) {
-            if (Email.DEBUG) {
+            if (MailActivityEmail.DEBUG) {
                 Log.d(TAG, "Dequeued attachmentId:  " + attachmentId);
             }
             mDownloadSet.remove(req);
@@ -853,7 +853,7 @@
         if (accountStorage < perAccountMaxStorage) {
             return true;
         } else {
-            if (Email.DEBUG) {
+            if (MailActivityEmail.DEBUG) {
                 Log.d(TAG, ">> Prefetch not allowed for account " + account.mId + "; used " +
                         accountStorage + ", limit " + perAccountMaxStorage);
             }
diff --git a/email2/src/com/android/email/service/EmailServiceStub.java b/email2/src/com/android/email/service/EmailServiceStub.java
index ae0f33d..833c4f6 100644
--- a/email2/src/com/android/email/service/EmailServiceStub.java
+++ b/email2/src/com/android/email/service/EmailServiceStub.java
@@ -27,12 +27,12 @@
 import android.text.TextUtils;
 import android.util.Log;
 
-import com.android.email.Email;
 import com.android.email.LegacyConversions;
 import com.android.email.NotificationController;
 import com.android.email.mail.Sender;
 import com.android.email.mail.Store;
 import com.android.email.provider.Utilities;
+import com.android.email2.ui.MailActivityEmail;
 import com.android.emailcommon.AccountManagerTypes;
 import com.android.emailcommon.Api;
 import com.android.emailcommon.Logging;
@@ -486,7 +486,7 @@
                     messageId = c.getLong(0);
                     // Don't send messages with unloaded attachments
                     if (Utility.hasUnloadedAttachments(context, messageId)) {
-                        if (Email.DEBUG) {
+                        if (MailActivityEmail.DEBUG) {
                             Log.d(Logging.LOG_TAG, "Can't send #" + messageId +
                                     "; unloaded attachments");
                         }
diff --git a/email2/src/com/android/email/service/ImapService.java b/email2/src/com/android/email/service/ImapService.java
index 6dcfbeb..34a1f2b 100644
--- a/email2/src/com/android/email/service/ImapService.java
+++ b/email2/src/com/android/email/service/ImapService.java
@@ -31,11 +31,11 @@
 import android.text.TextUtils;
 import android.util.Log;
 
-import com.android.email.Email;
 import com.android.email.LegacyConversions;
 import com.android.email.NotificationController;
 import com.android.email.mail.Store;
 import com.android.email.provider.Utilities;
+import com.android.email2.ui.MailActivityEmail;
 import com.android.emailcommon.Logging;
 import com.android.emailcommon.TrafficFlags;
 import com.android.emailcommon.internet.MimeUtility;
@@ -559,7 +559,7 @@
         // 6. Determine the limit # of messages to download
         int visibleLimit = mailbox.mVisibleLimit;
         if (visibleLimit <= 0) {
-            visibleLimit = Email.VISIBLE_LIMIT_DEFAULT;
+            visibleLimit = MailActivityEmail.VISIBLE_LIMIT_DEFAULT;
         }
 
         // 7.  Create a list of messages to download
@@ -821,7 +821,7 @@
         } catch (MessagingException me) {
             // Presumably an error here is an account connection failure, so there is
             // no point in continuing through the rest of the pending updates.
-            if (Email.DEBUG) {
+            if (MailActivityEmail.DEBUG) {
                 Log.d(Logging.LOG_TAG, "Unable to process pending delete for id="
                             + lastMessageId + ": " + me);
             }
@@ -927,7 +927,7 @@
         } catch (MessagingException me) {
             // Presumably an error here is an account connection failure, so there is
             // no point in continuing through the rest of the pending updates.
-            if (Email.DEBUG) {
+            if (MailActivityEmail.DEBUG) {
                 Log.d(Logging.LOG_TAG, "Unable to process pending upsync for id="
                         + lastMessageId + ": " + me);
             }
@@ -1016,7 +1016,7 @@
         } catch (MessagingException me) {
             // Presumably an error here is an account connection failure, so there is
             // no point in continuing through the rest of the pending updates.
-            if (Email.DEBUG) {
+            if (MailActivityEmail.DEBUG) {
                 Log.d(Logging.LOG_TAG, "Unable to process pending update for id="
                             + lastMessageId + ": " + me);
             }
@@ -1124,7 +1124,7 @@
         if (remoteMessage == null) {
             return;
         }
-        if (Email.DEBUG) {
+        if (MailActivityEmail.DEBUG) {
             Log.d(Logging.LOG_TAG,
                     "Update for msg id=" + newMessage.mId
                     + " read=" + newMessage.mFlagRead
diff --git a/email2/src/com/android/email/service/Pop3Service.java b/email2/src/com/android/email/service/Pop3Service.java
index d69641a..0d535d8 100644
--- a/email2/src/com/android/email/service/Pop3Service.java
+++ b/email2/src/com/android/email/service/Pop3Service.java
@@ -31,11 +31,11 @@
 import android.os.RemoteException;
 import android.util.Log;
 
-import com.android.email.Email;
 import com.android.email.LegacyConversions;
 import com.android.email.NotificationController;
 import com.android.email.mail.Store;
 import com.android.email.provider.Utilities;
+import com.android.email2.ui.MailActivityEmail;
 import com.android.emailcommon.AccountManagerTypes;
 import com.android.emailcommon.Logging;
 import com.android.emailcommon.TrafficFlags;
@@ -544,7 +544,7 @@
         // 6. Determine the limit # of messages to download
         int visibleLimit = mailbox.mVisibleLimit;
         if (visibleLimit <= 0) {
-            visibleLimit = Email.VISIBLE_LIMIT_DEFAULT;
+            visibleLimit = MailActivityEmail.VISIBLE_LIMIT_DEFAULT;
         }
 
         // 7.  Create a list of messages to download
diff --git a/email2/src/com/android/email2/ui/MailActivityEmail.java b/email2/src/com/android/email2/ui/MailActivityEmail.java
index 479f944..a7d36e2 100644
--- a/email2/src/com/android/email2/ui/MailActivityEmail.java
+++ b/email2/src/com/android/email2/ui/MailActivityEmail.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2012 The Android Open Source Project
+ * 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.
@@ -13,8 +13,236 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
 package com.android.email2.ui;
 
-public class MailActivityEmail extends com.android.mail.ui.MailActivity {
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.database.Cursor;
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.util.Log;
 
-}
\ No newline at end of file
+import com.android.email.NotificationController;
+import com.android.email.Preferences;
+import com.android.email.R;
+import com.android.email.R.string;
+import com.android.email.service.AttachmentDownloadService;
+import com.android.email.service.EmailServiceUtils;
+import com.android.email.service.MailService;
+import com.android.emailcommon.Logging;
+import com.android.emailcommon.TempDirectory;
+import com.android.emailcommon.provider.Account;
+import com.android.emailcommon.service.EmailServiceProxy;
+import com.android.emailcommon.utility.EmailAsyncTask;
+import com.android.emailcommon.utility.Utility;
+
+public class MailActivityEmail extends com.android.mail.ui.MailActivity {
+    /**
+     * If this is enabled there will be additional logging information sent to
+     * Log.d, including protocol dumps.
+     *
+     * This should only be used for logs that are useful for debbuging user problems,
+     * not for internal/development logs.
+     *
+     * This can be enabled by typing "debug" in the AccountFolderList activity.
+     * Changing the value to 'true' here will likely have no effect at all!
+     *
+     * TODO: rename this to sUserDebug, and rename LOGD below to DEBUG.
+     */
+    public static boolean DEBUG;
+
+    // Exchange debugging flags (passed to Exchange, when available, via EmailServiceProxy)
+    public static boolean DEBUG_EXCHANGE;
+    public static boolean DEBUG_EXCHANGE_VERBOSE;
+    public static boolean DEBUG_EXCHANGE_FILE;
+
+    /**
+     * If true, inhibit hardware graphics acceleration in UI (for a/b testing)
+     */
+    public static boolean sDebugInhibitGraphicsAcceleration = false;
+
+    /**
+     * Specifies how many messages will be shown in a folder by default. This number is set
+     * on each new folder and can be incremented with "Load more messages..." by the
+     * VISIBLE_LIMIT_INCREMENT
+     */
+    public static final int VISIBLE_LIMIT_DEFAULT = 25;
+
+    /**
+     * Number of additional messages to load when a user selects "Load more messages..."
+     */
+    public static final int VISIBLE_LIMIT_INCREMENT = 25;
+
+    /**
+     * This is used to force stacked UI to return to the "welcome" screen any time we change
+     * the accounts list (e.g. deleting accounts in the Account Manager preferences.)
+     */
+    private static boolean sAccountsChangedNotification = false;
+
+    private static String sMessageDecodeErrorString;
+
+    private static Thread sUiThread;
+
+    /**
+     * Asynchronous version of {@link #setServicesEnabledSync(Context)}.  Use when calling from
+     * UI thread (or lifecycle entry points.)
+     *
+     * @param context
+     */
+    public static void setServicesEnabledAsync(final Context context) {
+        EmailAsyncTask.runAsyncParallel(new Runnable() {
+            @Override
+            public void run() {
+                setServicesEnabledSync(context);
+            }
+        });
+    }
+
+    /**
+     * Called throughout the application when the number of accounts has changed. This method
+     * enables or disables the Compose activity, the boot receiver and the service based on
+     * whether any accounts are configured.
+     *
+     * Blocking call - do not call from UI/lifecycle threads.
+     *
+     * @param context
+     * @return true if there are any accounts configured.
+     */
+    public static boolean setServicesEnabledSync(Context context) {
+        Cursor c = null;
+        try {
+            c = context.getContentResolver().query(
+                    Account.CONTENT_URI,
+                    Account.ID_PROJECTION,
+                    null, null, null);
+            boolean enable = c.getCount() > 0;
+            setServicesEnabled(context, enable);
+            return enable;
+        } finally {
+            if (c != null) {
+                c.close();
+            }
+        }
+    }
+
+    private static void setServicesEnabled(Context context, boolean enabled) {
+        PackageManager pm = context.getPackageManager();
+        pm.setComponentEnabledSetting(
+                new ComponentName(context, MailService.class),
+                enabled ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED :
+                    PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
+                PackageManager.DONT_KILL_APP);
+        pm.setComponentEnabledSetting(
+                new ComponentName(context, AttachmentDownloadService.class),
+                enabled ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED :
+                    PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
+                PackageManager.DONT_KILL_APP);
+
+        // Start/stop the various services depending on whether there are any accounts
+        startOrStopService(enabled, context, new Intent(context, AttachmentDownloadService.class));
+        NotificationController.getInstance(context).watchForMessages(enabled);
+    }
+
+    /**
+     * Starts or stops the service as necessary.
+     * @param enabled If {@code true}, the service will be started. Otherwise, it will be stopped.
+     * @param context The context to manage the service with.
+     * @param intent The intent of the service to be managed.
+     */
+    private static void startOrStopService(boolean enabled, Context context, Intent intent) {
+        if (enabled) {
+            context.startService(intent);
+        } else {
+            context.stopService(intent);
+        }
+    }
+
+    @Override
+    public void onCreate(Bundle bundle) {
+        super.onCreate(bundle);
+        sUiThread = Thread.currentThread();
+        Preferences prefs = Preferences.getPreferences(this);
+        DEBUG = prefs.getEnableDebugLogging();
+        sDebugInhibitGraphicsAcceleration = prefs.getInhibitGraphicsAcceleration();
+        enableStrictMode(prefs.getEnableStrictMode());
+        TempDirectory.setTempDirectory(this);
+
+        // Enable logging in the EAS service, so it starts up as early as possible.
+        updateLoggingFlags(this);
+
+        // Get a helper string used deep inside message decoders (which don't have context)
+        sMessageDecodeErrorString = getString(R.string.message_decode_error);
+
+        // Make sure all required services are running when the app is started (can prevent
+        // issues after an adb sync/install)
+        setServicesEnabledAsync(this);
+    }
+
+    /**
+     * Load enabled debug flags from the preferences and update the EAS debug flag.
+     */
+    public static void updateLoggingFlags(Context context) {
+        Preferences prefs = Preferences.getPreferences(context);
+        int debugLogging = prefs.getEnableDebugLogging() ? EmailServiceProxy.DEBUG_BIT : 0;
+        int verboseLogging =
+            prefs.getEnableExchangeLogging() ? EmailServiceProxy.DEBUG_VERBOSE_BIT : 0;
+        int fileLogging =
+            prefs.getEnableExchangeFileLogging() ? EmailServiceProxy.DEBUG_FILE_BIT : 0;
+        int enableStrictMode =
+            prefs.getEnableStrictMode() ? EmailServiceProxy.DEBUG_ENABLE_STRICT_MODE : 0;
+        int debugBits = debugLogging | verboseLogging | fileLogging | enableStrictMode;
+        EmailServiceProxy service = EmailServiceUtils.getExchangeService(context, null);
+        if (service != null) {
+            try {
+                service.setLogging(debugBits);
+            } catch (RemoteException e) {
+            }
+        }
+    }
+
+    /**
+     * Internal, utility method for logging.
+     * The calls to log() must be guarded with "if (Email.LOGD)" for performance reasons.
+     */
+    public static void log(String message) {
+        Log.d(Logging.LOG_TAG, message);
+    }
+
+    /**
+     * Called by the accounts reconciler to notify that accounts have changed, or by  "Welcome"
+     * to clear the flag.
+     * @param setFlag true to set the notification flag, false to clear it
+     */
+    public static synchronized void setNotifyUiAccountsChanged(boolean setFlag) {
+        sAccountsChangedNotification = setFlag;
+    }
+
+    /**
+     * Called from activity onResume() functions to check for an accounts-changed condition, at
+     * which point they should finish() and jump to the Welcome activity.
+     */
+    public static synchronized boolean getNotifyUiAccountsChanged() {
+        return sAccountsChangedNotification;
+    }
+
+    public static void warnIfUiThread() {
+        if (Thread.currentThread().equals(sUiThread)) {
+            Log.w(Logging.LOG_TAG, "Method called on the UI thread", new Exception("STACK TRACE"));
+        }
+    }
+
+    /**
+     * Retrieve a simple string that can be used when message decoders encounter bad data.
+     * This is provided here because the protocol decoders typically don't have mContext.
+     */
+    public static String getMessageDecodeErrorString() {
+        return sMessageDecodeErrorString != null ? sMessageDecodeErrorString : "";
+    }
+
+    public static void enableStrictMode(boolean enabled) {
+        Utility.enableStrictMode(enabled);
+    }
+}