Home | History | Annotate | Download | only in store
      1 /*
      2  * Copyright (C) 2008 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.email.mail.store;
     18 
     19 import com.android.email.Email;
     20 import com.android.email.Utility;
     21 import com.android.email.mail.Address;
     22 import com.android.email.mail.Body;
     23 import com.android.email.mail.FetchProfile;
     24 import com.android.email.mail.Flag;
     25 import com.android.email.mail.Folder;
     26 import com.android.email.mail.Message;
     27 import com.android.email.mail.Message.RecipientType;
     28 import com.android.email.mail.MessageRetrievalListener;
     29 import com.android.email.mail.MessagingException;
     30 import com.android.email.mail.Part;
     31 import com.android.email.mail.Store;
     32 import com.android.email.mail.Store.PersistentDataCallbacks;
     33 import com.android.email.mail.internet.MimeBodyPart;
     34 import com.android.email.mail.internet.MimeHeader;
     35 import com.android.email.mail.internet.MimeMessage;
     36 import com.android.email.mail.internet.MimeMultipart;
     37 import com.android.email.mail.internet.MimeUtility;
     38 import com.android.email.mail.internet.TextBody;
     39 
     40 import org.apache.commons.io.IOUtils;
     41 
     42 import android.content.ContentValues;
     43 import android.content.Context;
     44 import android.database.Cursor;
     45 import android.database.sqlite.SQLiteDatabase;
     46 import android.net.Uri;
     47 import android.util.Log;
     48 import android.util.Base64;
     49 import android.util.Base64OutputStream;
     50 
     51 import java.io.ByteArrayInputStream;
     52 import java.io.File;
     53 import java.io.FileNotFoundException;
     54 import java.io.FileOutputStream;
     55 import java.io.IOException;
     56 import java.io.InputStream;
     57 import java.io.OutputStream;
     58 import java.io.UnsupportedEncodingException;
     59 import java.net.URI;
     60 import java.net.URLEncoder;
     61 import java.util.ArrayList;
     62 import java.util.Date;
     63 import java.util.UUID;
     64 
     65 /**
     66  * <pre>
     67  * Implements a SQLite database backed local store for Messages.
     68  * </pre>
     69  */
     70 public class LocalStore extends Store implements PersistentDataCallbacks {
     71     /**
     72      * History of database revisions.
     73      *
     74      * db version   Shipped in  Notes
     75      * ----------   ----------  -----
     76      *      18      pre-1.0     Development versions.  No upgrade path.
     77      *      18      1.0, 1.1    1.0 Release version.
     78      *      19      -           Added message_id column to messages table.
     79      *      20      1.5         Added content_id column to attachments table.
     80      *      21      -           Added remote_store_data table
     81      *      22      -           Added store_flag_1 and store_flag_2 columns to messages table.
     82      *      23      -           Added flag_downloaded_full, flag_downloaded_partial, flag_deleted
     83      *                          columns to message table.
     84      *      24      -           Added x_headers to messages table.
     85      */
     86 
     87     private static final int DB_VERSION = 24;
     88 
     89     private static final Flag[] PERMANENT_FLAGS = { Flag.DELETED, Flag.X_DESTROYED, Flag.SEEN };
     90 
     91     private String mPath;
     92     private SQLiteDatabase mDb;
     93     private File mAttachmentsDir;
     94     private Context mContext;
     95     private int mVisibleLimitDefault = -1;
     96 
     97     /**
     98      * Static named constructor.
     99      */
    100     public static LocalStore newInstance(String uri, Context context,
    101             PersistentDataCallbacks callbacks) throws MessagingException {
    102         return new LocalStore(uri, context);
    103     }
    104 
    105     /**
    106      * @param uri local://localhost/path/to/database/uuid.db
    107      */
    108     private LocalStore(String _uri, Context context) throws MessagingException {
    109         mContext = context;
    110         URI uri = null;
    111         try {
    112             uri = new URI(_uri);
    113         } catch (Exception e) {
    114             throw new MessagingException("Invalid uri for LocalStore");
    115         }
    116         if (!uri.getScheme().equals("local")) {
    117             throw new MessagingException("Invalid scheme");
    118         }
    119         mPath = uri.getPath();
    120 
    121         File parentDir = new File(mPath).getParentFile();
    122         if (!parentDir.exists()) {
    123             parentDir.mkdirs();
    124         }
    125         mDb = SQLiteDatabase.openOrCreateDatabase(mPath, null);
    126         int oldVersion = mDb.getVersion();
    127 
    128         /*
    129          *  TODO we should have more sophisticated way to upgrade database.
    130          */
    131         if (oldVersion != DB_VERSION) {
    132             if (Email.LOGD) {
    133                 Log.v(Email.LOG_TAG, String.format("Upgrading database from %d to %d",
    134                         oldVersion, DB_VERSION));
    135             }
    136             if (oldVersion < 18) {
    137                 /**
    138                  * Missing or old:  Create up-to-date tables
    139                  */
    140                 mDb.execSQL("DROP TABLE IF EXISTS folders");
    141                 mDb.execSQL("CREATE TABLE folders (id INTEGER PRIMARY KEY, name TEXT, "
    142                         + "last_updated INTEGER, unread_count INTEGER, visible_limit INTEGER)");
    143 
    144                 mDb.execSQL("DROP TABLE IF EXISTS messages");
    145                 mDb.execSQL("CREATE TABLE messages (id INTEGER PRIMARY KEY, folder_id INTEGER, " +
    146                         "uid TEXT, subject TEXT, date INTEGER, flags TEXT, sender_list TEXT, " +
    147                         "to_list TEXT, cc_list TEXT, bcc_list TEXT, reply_to_list TEXT, " +
    148                         "html_content TEXT, text_content TEXT, attachment_count INTEGER, " +
    149                         "internal_date INTEGER, message_id TEXT, store_flag_1 INTEGER, " +
    150                         "store_flag_2 INTEGER, flag_downloaded_full INTEGER," +
    151                         "flag_downloaded_partial INTEGER, flag_deleted INTEGER, x_headers TEXT)");
    152 
    153                 mDb.execSQL("DROP TABLE IF EXISTS attachments");
    154                 mDb.execSQL("CREATE TABLE attachments (id INTEGER PRIMARY KEY, message_id INTEGER,"
    155                         + "store_data TEXT, content_uri TEXT, size INTEGER, name TEXT,"
    156                         + "mime_type TEXT, content_id TEXT)");
    157 
    158                 mDb.execSQL("DROP TABLE IF EXISTS pending_commands");
    159                 mDb.execSQL("CREATE TABLE pending_commands " +
    160                         "(id INTEGER PRIMARY KEY, command TEXT, arguments TEXT)");
    161 
    162                 addRemoteStoreDataTable();
    163 
    164                 addFolderDeleteTrigger();
    165 
    166                 mDb.execSQL("DROP TRIGGER IF EXISTS delete_message");
    167                 mDb.execSQL("CREATE TRIGGER delete_message BEFORE DELETE ON messages BEGIN DELETE FROM attachments WHERE old.id = message_id; END;");
    168                 mDb.setVersion(DB_VERSION);
    169             }
    170             else {
    171                 if (oldVersion < 19) {
    172                     /**
    173                      * Upgrade 18 to 19:  add message_id to messages table
    174                      */
    175                     mDb.execSQL("ALTER TABLE messages ADD COLUMN message_id TEXT;");
    176                     mDb.setVersion(19);
    177                 }
    178                 if (oldVersion < 20) {
    179                     /**
    180                      * Upgrade 19 to 20:  add content_id to attachments table
    181                      */
    182                     mDb.execSQL("ALTER TABLE attachments ADD COLUMN content_id TEXT;");
    183                     mDb.setVersion(20);
    184                 }
    185                 if (oldVersion < 21) {
    186                     /**
    187                      * Upgrade 20 to 21:  add remote_store_data and update triggers to match
    188                      */
    189                     addRemoteStoreDataTable();
    190                     addFolderDeleteTrigger();
    191                     mDb.setVersion(21);
    192                 }
    193                 if (oldVersion < 22) {
    194                     /**
    195                      * Upgrade 21 to 22:  add store_flag_1 and store_flag_2 to messages table
    196                      */
    197                     mDb.execSQL("ALTER TABLE messages ADD COLUMN store_flag_1 INTEGER;");
    198                     mDb.execSQL("ALTER TABLE messages ADD COLUMN store_flag_2 INTEGER;");
    199                     mDb.setVersion(22);
    200                 }
    201                 if (oldVersion < 23) {
    202                     /**
    203                      * Upgrade 22 to 23:  add flag_downloaded_full & flag_downloaded_partial
    204                      * and flag_deleted columns to message table *and upgrade existing messages*.
    205                      */
    206                     mDb.beginTransaction();
    207                     try {
    208                         mDb.execSQL(
    209                                 "ALTER TABLE messages ADD COLUMN flag_downloaded_full INTEGER;");
    210                         mDb.execSQL(
    211                                 "ALTER TABLE messages ADD COLUMN flag_downloaded_partial INTEGER;");
    212                         mDb.execSQL(
    213                                 "ALTER TABLE messages ADD COLUMN flag_deleted INTEGER;");
    214                         migrateMessageFlags();
    215                         mDb.setVersion(23);
    216                         mDb.setTransactionSuccessful();
    217                     } finally {
    218                         mDb.endTransaction();
    219                     }
    220                 }
    221                 if (oldVersion < 24) {
    222                     /**
    223                      * Upgrade 23 to 24:  add x_headers to messages table
    224                      */
    225                     mDb.execSQL("ALTER TABLE messages ADD COLUMN x_headers TEXT;");
    226                     mDb.setVersion(24);
    227                 }
    228             }
    229 
    230             if (mDb.getVersion() != DB_VERSION) {
    231                 throw new Error("Database upgrade failed!");
    232             }
    233         }
    234         mAttachmentsDir = new File(mPath + "_att");
    235         if (!mAttachmentsDir.exists()) {
    236             mAttachmentsDir.mkdirs();
    237         }
    238     }
    239 
    240     /**
    241      * Common code to add the remote_store_data table
    242      */
    243     private void addRemoteStoreDataTable() {
    244         mDb.execSQL("DROP TABLE IF EXISTS remote_store_data");
    245         mDb.execSQL("CREATE TABLE remote_store_data (" +
    246         		"id INTEGER PRIMARY KEY, folder_id INTEGER, data_key TEXT, data TEXT, " +
    247                 "UNIQUE (folder_id, data_key) ON CONFLICT REPLACE" +
    248                 ")");
    249     }
    250 
    251     /**
    252      * Common code to add folder delete trigger
    253      */
    254     private void addFolderDeleteTrigger() {
    255         mDb.execSQL("DROP TRIGGER IF EXISTS delete_folder");
    256         mDb.execSQL("CREATE TRIGGER delete_folder "
    257                 + "BEFORE DELETE ON folders "
    258                 + "BEGIN "
    259                     + "DELETE FROM messages WHERE old.id = folder_id; "
    260                     + "DELETE FROM remote_store_data WHERE old.id = folder_id; "
    261                 + "END;");
    262     }
    263 
    264     /**
    265      * When upgrading from 22 to 23, we have to move any flags "X_DOWNLOADED_FULL" or
    266      * "X_DOWNLOADED_PARTIAL" or "DELETED" from the old string-based storage to their own columns.
    267      *
    268      * Note:  Caller should open a db transaction around this
    269      */
    270     private void migrateMessageFlags() {
    271         Cursor cursor = mDb.query("messages",
    272                 new String[] { "id", "flags" },
    273                 null, null, null, null, null);
    274         try {
    275             int columnId = cursor.getColumnIndexOrThrow("id");
    276             int columnFlags = cursor.getColumnIndexOrThrow("flags");
    277 
    278             while (cursor.moveToNext()) {
    279                 String oldFlags = cursor.getString(columnFlags);
    280                 ContentValues values = new ContentValues();
    281                 int newFlagDlFull = 0;
    282                 int newFlagDlPartial = 0;
    283                 int newFlagDeleted = 0;
    284                 if (oldFlags != null) {
    285                     if (oldFlags.contains(Flag.X_DOWNLOADED_FULL.toString())) {
    286                         newFlagDlFull = 1;
    287                     }
    288                     if (oldFlags.contains(Flag.X_DOWNLOADED_PARTIAL.toString())) {
    289                         newFlagDlPartial = 1;
    290                     }
    291                     if (oldFlags.contains(Flag.DELETED.toString())) {
    292                         newFlagDeleted = 1;
    293                     }
    294                 }
    295                 // Always commit the new flags.
    296                 // Note:  We don't have to pay the cost of rewriting the old string,
    297                 // because the old flag will be ignored, and will eventually be overwritten
    298                 // anyway.
    299                 values.put("flag_downloaded_full", newFlagDlFull);
    300                 values.put("flag_downloaded_partial", newFlagDlPartial);
    301                 values.put("flag_deleted", newFlagDeleted);
    302                 int rowId = cursor.getInt(columnId);
    303                 mDb.update("messages", values, "id=" + rowId, null);
    304             }
    305         } finally {
    306             cursor.close();
    307         }
    308     }
    309 
    310     @Override
    311     public Folder getFolder(String name) throws MessagingException {
    312         return new LocalFolder(name);
    313     }
    314 
    315     // TODO this takes about 260-300ms, seems slow.
    316     @Override
    317     public Folder[] getPersonalNamespaces() throws MessagingException {
    318         ArrayList<Folder> folders = new ArrayList<Folder>();
    319         Cursor cursor = null;
    320         try {
    321             cursor = mDb.rawQuery("SELECT name FROM folders", null);
    322             while (cursor.moveToNext()) {
    323                 folders.add(new LocalFolder(cursor.getString(0)));
    324             }
    325         }
    326         finally {
    327             if (cursor != null) {
    328                 cursor.close();
    329             }
    330         }
    331         return folders.toArray(new Folder[] {});
    332     }
    333 
    334     @Override
    335     public void checkSettings() throws MessagingException {
    336     }
    337 
    338     /**
    339      * Local store only:  Allow it to be closed.  This is necessary for the account upgrade process
    340      * because we open and close each database a few times as we proceed.
    341      */
    342     public void close() {
    343         try {
    344             mDb.close();
    345             mDb = null;
    346         } catch (Exception e) {
    347             // Log and discard.  This is best-effort, and database finalizers will try again.
    348             Log.d(Email.LOG_TAG, "Caught exception while closing localstore db: " + e);
    349         }
    350     }
    351 
    352     /**
    353      * Delete the entire Store and it's backing database.
    354      */
    355     @Override
    356     public void delete() {
    357         try {
    358             mDb.close();
    359         } catch (Exception e) {
    360 
    361         }
    362         try{
    363             File[] attachments = mAttachmentsDir.listFiles();
    364             for (File attachment : attachments) {
    365                 if (attachment.exists()) {
    366                     attachment.delete();
    367                 }
    368             }
    369             if (mAttachmentsDir.exists()) {
    370                 mAttachmentsDir.delete();
    371             }
    372         }
    373         catch (Exception e) {
    374         }
    375         try {
    376             new File(mPath).delete();
    377         }
    378         catch (Exception e) {
    379 
    380         }
    381     }
    382 
    383     /**
    384      * Report # of attachments (for migration estimates only - catches all exceptions and
    385      * just returns zero)
    386      */
    387     public int getStoredAttachmentCount() {
    388         try{
    389             File[] attachments = mAttachmentsDir.listFiles();
    390             return attachments.length;
    391         }
    392         catch (Exception e) {
    393             return 0;
    394         }
    395     }
    396 
    397     /**
    398      * Deletes all cached attachments for the entire store.
    399      */
    400     public int pruneCachedAttachments() throws MessagingException {
    401         int prunedCount = 0;
    402         File[] files = mAttachmentsDir.listFiles();
    403         for (File file : files) {
    404             if (file.exists()) {
    405                 try {
    406                     Cursor cursor = null;
    407                     try {
    408                         cursor = mDb.query(
    409                             "attachments",
    410                             new String[] { "store_data" },
    411                             "id = ?",
    412                             new String[] { file.getName() },
    413                             null,
    414                             null,
    415                             null);
    416                         if (cursor.moveToNext()) {
    417                             if (cursor.getString(0) == null) {
    418                                 /*
    419                                  * If the attachment has no store data it is not recoverable, so
    420                                  * we won't delete it.
    421                                  */
    422                                 continue;
    423                             }
    424                         }
    425                     }
    426                     finally {
    427                         if (cursor != null) {
    428                             cursor.close();
    429                         }
    430                     }
    431                     ContentValues cv = new ContentValues();
    432                     cv.putNull("content_uri");
    433                     mDb.update("attachments", cv, "id = ?", new String[] { file.getName() });
    434                 }
    435                 catch (Exception e) {
    436                     /*
    437                      * If the row has gone away before we got to mark it not-downloaded that's
    438                      * okay.
    439                      */
    440                 }
    441                 if (!file.delete()) {
    442                     file.deleteOnExit();
    443                 }
    444                 prunedCount++;
    445             }
    446         }
    447         return prunedCount;
    448     }
    449 
    450     /**
    451      * Set the visible limit for all folders in a given store.
    452      *
    453      * @param visibleLimit the value to write to all folders.  -1 may also be used as a marker.
    454      */
    455     public void resetVisibleLimits(int visibleLimit) {
    456         mVisibleLimitDefault = visibleLimit;            // used for future Folder.create ops
    457         ContentValues cv = new ContentValues();
    458         cv.put("visible_limit", Integer.toString(visibleLimit));
    459         mDb.update("folders", cv, null, null);
    460     }
    461 
    462     public ArrayList<PendingCommand> getPendingCommands() {
    463         Cursor cursor = null;
    464         try {
    465             cursor = mDb.query("pending_commands",
    466                     new String[] { "id", "command", "arguments" },
    467                     null,
    468                     null,
    469                     null,
    470                     null,
    471                     "id ASC");
    472             ArrayList<PendingCommand> commands = new ArrayList<PendingCommand>();
    473             while (cursor.moveToNext()) {
    474                 PendingCommand command = new PendingCommand();
    475                 command.mId = cursor.getLong(0);
    476                 command.command = cursor.getString(1);
    477                 String arguments = cursor.getString(2);
    478                 command.arguments = arguments.split(",");
    479                 for (int i = 0; i < command.arguments.length; i++) {
    480                     command.arguments[i] = Utility.fastUrlDecode(command.arguments[i]);
    481                 }
    482                 commands.add(command);
    483             }
    484             return commands;
    485         }
    486         finally {
    487             if (cursor != null) {
    488                 cursor.close();
    489             }
    490         }
    491     }
    492 
    493     public void addPendingCommand(PendingCommand command) {
    494         try {
    495             for (int i = 0; i < command.arguments.length; i++) {
    496                 command.arguments[i] = URLEncoder.encode(command.arguments[i], "UTF-8");
    497             }
    498             ContentValues cv = new ContentValues();
    499             cv.put("command", command.command);
    500             cv.put("arguments", Utility.combine(command.arguments, ','));
    501             mDb.insert("pending_commands", "command", cv);
    502         }
    503         catch (UnsupportedEncodingException usee) {
    504             throw new Error("Aparently UTF-8 has been lost to the annals of history.");
    505         }
    506     }
    507 
    508     public void removePendingCommand(PendingCommand command) {
    509         mDb.delete("pending_commands", "id = ?", new String[] { Long.toString(command.mId) });
    510     }
    511 
    512     public static class PendingCommand {
    513         private long mId;
    514         public String command;
    515         public String[] arguments;
    516 
    517         @Override
    518         public String toString() {
    519             StringBuffer sb = new StringBuffer();
    520             sb.append(command);
    521             sb.append("\n");
    522             for (String argument : arguments) {
    523                 sb.append("  ");
    524                 sb.append(argument);
    525                 sb.append("\n");
    526             }
    527             return sb.toString();
    528         }
    529     }
    530 
    531     /**
    532      * LocalStore-only function to get the callbacks API
    533      */
    534     public PersistentDataCallbacks getPersistentCallbacks() throws MessagingException {
    535         return this;
    536     }
    537 
    538     public String getPersistentString(String key, String defaultValue) {
    539         return getPersistentString(-1, key, defaultValue);
    540     }
    541 
    542     public void setPersistentString(String key, String value) {
    543         setPersistentString(-1, key, value);
    544     }
    545 
    546     /**
    547      * Common implementation of getPersistentString
    548      * @param folderId The id of the associated folder, or -1 for "store" values
    549      * @param key The key
    550      * @param defaultValue The value to return if the row is not found
    551      * @return The row data or the default
    552      */
    553     private String getPersistentString(long folderId, String key, String defaultValue) {
    554         String result = defaultValue;
    555         Cursor cursor = null;
    556         try {
    557             cursor = mDb.query("remote_store_data",
    558                     new String[] { "data" },
    559                     "folder_id = ? AND data_key = ?",
    560                     new String[] { Long.toString(folderId), key },
    561                     null,
    562                     null,
    563                     null);
    564             if (cursor != null && cursor.moveToNext()) {
    565                 result = cursor.getString(0);
    566             }
    567         }
    568         finally {
    569             if (cursor != null) {
    570                 cursor.close();
    571             }
    572         }
    573         return result;
    574     }
    575 
    576     /**
    577      * Common implementation of setPersistentString
    578      * @param folderId The id of the associated folder, or -1 for "store" values
    579      * @param key The key
    580      * @param value The value to store
    581      */
    582     private void setPersistentString(long folderId, String key, String value) {
    583         ContentValues cv = new ContentValues();
    584         cv.put("folder_id", Long.toString(folderId));
    585         cv.put("data_key", key);
    586         cv.put("data", value);
    587         // Note:  Table has on-conflict-replace rule
    588         mDb.insert("remote_store_data", null, cv);
    589     }
    590 
    591     public class LocalFolder extends Folder implements Folder.PersistentDataCallbacks {
    592         private String mName;
    593         private long mFolderId = -1;
    594         private int mUnreadMessageCount = -1;
    595         private int mVisibleLimit = -1;
    596 
    597         public LocalFolder(String name) {
    598             this.mName = name;
    599         }
    600 
    601         public long getId() {
    602             return mFolderId;
    603         }
    604 
    605         /**
    606          * This is just used by the internal callers
    607          */
    608         private void open(OpenMode mode) throws MessagingException {
    609             open(mode, null);
    610         }
    611 
    612         @Override
    613         public void open(OpenMode mode, PersistentDataCallbacks callbacks)
    614                 throws MessagingException {
    615             if (isOpen()) {
    616                 return;
    617             }
    618             if (!exists()) {
    619                 create(FolderType.HOLDS_MESSAGES);
    620             }
    621             Cursor cursor = null;
    622             try {
    623                 cursor = mDb.rawQuery("SELECT id, unread_count, visible_limit FROM folders "
    624                         + "where folders.name = ?",
    625                     new String[] {
    626                         mName
    627                     });
    628                 if (!cursor.moveToFirst()) {
    629                     throw new MessagingException("Nonexistent folder");
    630                 }
    631                 mFolderId = cursor.getInt(0);
    632                 mUnreadMessageCount = cursor.getInt(1);
    633                 mVisibleLimit = cursor.getInt(2);
    634             }
    635             finally {
    636                 if (cursor != null) {
    637                     cursor.close();
    638                 }
    639             }
    640         }
    641 
    642         @Override
    643         public boolean isOpen() {
    644             return mFolderId != -1;
    645         }
    646 
    647         @Override
    648         public OpenMode getMode() throws MessagingException {
    649             return OpenMode.READ_WRITE;
    650         }
    651 
    652         @Override
    653         public String getName() {
    654             return mName;
    655         }
    656 
    657         @Override
    658         public boolean exists() throws MessagingException {
    659             return Utility.arrayContains(getPersonalNamespaces(), this);
    660         }
    661 
    662         // LocalStore supports folder creation
    663         @Override
    664         public boolean canCreate(FolderType type) {
    665             return true;
    666         }
    667 
    668         @Override
    669         public boolean create(FolderType type) throws MessagingException {
    670             if (exists()) {
    671                 throw new MessagingException("Folder " + mName + " already exists.");
    672             }
    673             mDb.execSQL("INSERT INTO folders (name, visible_limit) VALUES (?, ?)", new Object[] {
    674                 mName,
    675                 mVisibleLimitDefault
    676             });
    677             return true;
    678         }
    679 
    680         @Override
    681         public void close(boolean expunge) throws MessagingException {
    682             if (expunge) {
    683                 expunge();
    684             }
    685             mFolderId = -1;
    686         }
    687 
    688         @Override
    689         public int getMessageCount() throws MessagingException {
    690             return getMessageCount(null, null);
    691         }
    692 
    693         /**
    694          * Return number of messages based on the state of the flags.
    695          *
    696          * @param setFlags The flags that should be set for a message to be selected (null ok)
    697          * @param clearFlags The flags that should be clear for a message to be selected (null ok)
    698          * @return The number of messages matching the desired flag states.
    699          * @throws MessagingException
    700          */
    701         public int getMessageCount(Flag[] setFlags, Flag[] clearFlags) throws MessagingException {
    702             // Generate WHERE clause based on flags observed
    703             StringBuilder sql = new StringBuilder("SELECT COUNT(*) FROM messages WHERE ");
    704             buildFlagPredicates(sql, setFlags, clearFlags);
    705             sql.append("messages.folder_id = ?");
    706 
    707             open(OpenMode.READ_WRITE);
    708             Cursor cursor = null;
    709             try {
    710                 cursor = mDb.rawQuery(
    711                         sql.toString(),
    712                         new String[] {
    713                             Long.toString(mFolderId)
    714                         });
    715                 cursor.moveToFirst();
    716                 int messageCount = cursor.getInt(0);
    717                 return messageCount;
    718             }
    719             finally {
    720                 if (cursor != null) {
    721                     cursor.close();
    722                 }
    723             }
    724         }
    725 
    726         @Override
    727         public int getUnreadMessageCount() throws MessagingException {
    728             if (!isOpen()) {
    729                 // opening it will read all columns including mUnreadMessageCount
    730                 open(OpenMode.READ_WRITE);
    731             } else {
    732                 // already open.  refresh from db in case another instance wrote to it
    733                 Cursor cursor = null;
    734                 try {
    735                     cursor = mDb.rawQuery("SELECT unread_count FROM folders WHERE folders.name = ?",
    736                             new String[] { mName });
    737                     if (!cursor.moveToFirst()) {
    738                         throw new MessagingException("Nonexistent folder");
    739                     }
    740                     mUnreadMessageCount = cursor.getInt(0);
    741                 } finally {
    742                     if (cursor != null) {
    743                         cursor.close();
    744                     }
    745                 }
    746             }
    747             return mUnreadMessageCount;
    748         }
    749 
    750         public void setUnreadMessageCount(int unreadMessageCount) throws MessagingException {
    751             open(OpenMode.READ_WRITE);
    752             mUnreadMessageCount = Math.max(0, unreadMessageCount);
    753             mDb.execSQL("UPDATE folders SET unread_count = ? WHERE id = ?",
    754                     new Object[] { mUnreadMessageCount, mFolderId });
    755         }
    756 
    757         public int getVisibleLimit() throws MessagingException {
    758             if (!isOpen()) {
    759                 // opening it will read all columns including mVisibleLimit
    760                 open(OpenMode.READ_WRITE);
    761             } else {
    762                 // already open.  refresh from db in case another instance wrote to it
    763                 Cursor cursor = null;
    764                 try {
    765                     cursor = mDb.rawQuery(
    766                             "SELECT visible_limit FROM folders WHERE folders.name = ?",
    767                             new String[] { mName });
    768                     if (!cursor.moveToFirst()) {
    769                         throw new MessagingException("Nonexistent folder");
    770                     }
    771                     mVisibleLimit = cursor.getInt(0);
    772                 } finally {
    773                     if (cursor != null) {
    774                         cursor.close();
    775                     }
    776                 }
    777             }
    778             return mVisibleLimit;
    779         }
    780 
    781         public void setVisibleLimit(int visibleLimit) throws MessagingException {
    782             open(OpenMode.READ_WRITE);
    783             mVisibleLimit = visibleLimit;
    784             mDb.execSQL("UPDATE folders SET visible_limit = ? WHERE id = ?",
    785                     new Object[] { mVisibleLimit, mFolderId });
    786         }
    787 
    788         /**
    789          * Supports FetchProfile.Item.BODY and FetchProfile.Item.STRUCTURE
    790          */
    791         @Override
    792         public void fetch(Message[] messages, FetchProfile fp, MessageRetrievalListener listener)
    793                 throws MessagingException {
    794             open(OpenMode.READ_WRITE);
    795             if (fp.contains(FetchProfile.Item.BODY) || fp.contains(FetchProfile.Item.STRUCTURE)) {
    796                 for (Message message : messages) {
    797                     LocalMessage localMessage = (LocalMessage)message;
    798                     Cursor cursor = null;
    799                     localMessage.setHeader(MimeHeader.HEADER_CONTENT_TYPE, "multipart/mixed");
    800                     MimeMultipart mp = new MimeMultipart();
    801                     mp.setSubType("mixed");
    802                     localMessage.setBody(mp);
    803 
    804                     // If fetching the body, retrieve html & plaintext from DB.
    805                     // If fetching structure, simply build placeholders for them.
    806                     if (fp.contains(FetchProfile.Item.BODY)) {
    807                         try {
    808                             cursor = mDb.rawQuery("SELECT html_content, text_content FROM messages "
    809                                     + "WHERE id = ?",
    810                                     new String[] { Long.toString(localMessage.mId) });
    811                             cursor.moveToNext();
    812                             String htmlContent = cursor.getString(0);
    813                             String textContent = cursor.getString(1);
    814 
    815                             if (htmlContent != null) {
    816                                 TextBody body = new TextBody(htmlContent);
    817                                 MimeBodyPart bp = new MimeBodyPart(body, "text/html");
    818                                 mp.addBodyPart(bp);
    819                             }
    820 
    821                             if (textContent != null) {
    822                                 TextBody body = new TextBody(textContent);
    823                                 MimeBodyPart bp = new MimeBodyPart(body, "text/plain");
    824                                 mp.addBodyPart(bp);
    825                             }
    826                         }
    827                         finally {
    828                             if (cursor != null) {
    829                                 cursor.close();
    830                             }
    831                         }
    832                     } else {
    833                         MimeBodyPart bp = new MimeBodyPart();
    834                         bp.setHeader(MimeHeader.HEADER_CONTENT_TYPE,
    835                                 "text/html;\n charset=\"UTF-8\"");
    836                         mp.addBodyPart(bp);
    837 
    838                         bp = new MimeBodyPart();
    839                         bp.setHeader(MimeHeader.HEADER_CONTENT_TYPE,
    840                                 "text/plain;\n charset=\"UTF-8\"");
    841                         mp.addBodyPart(bp);
    842                     }
    843 
    844                     try {
    845                         cursor = mDb.query(
    846                                 "attachments",
    847                                 new String[] {
    848                                         "id",
    849                                         "size",
    850                                         "name",
    851                                         "mime_type",
    852                                         "store_data",
    853                                         "content_uri",
    854                                         "content_id" },
    855                                 "message_id = ?",
    856                                 new String[] { Long.toString(localMessage.mId) },
    857                                 null,
    858                                 null,
    859                                 null);
    860 
    861                         while (cursor.moveToNext()) {
    862                             long id = cursor.getLong(0);
    863                             int size = cursor.getInt(1);
    864                             String name = cursor.getString(2);
    865                             String type = cursor.getString(3);
    866                             String storeData = cursor.getString(4);
    867                             String contentUri = cursor.getString(5);
    868                             String contentId = cursor.getString(6);
    869                             Body body = null;
    870                             if (contentUri != null) {
    871                                 body = new LocalAttachmentBody(Uri.parse(contentUri), mContext);
    872                             }
    873                             MimeBodyPart bp = new LocalAttachmentBodyPart(body, id);
    874                             bp.setHeader(MimeHeader.HEADER_CONTENT_TYPE,
    875                                     String.format("%s;\n name=\"%s\"",
    876                                     type,
    877                                     name));
    878                             bp.setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, "base64");
    879                             bp.setHeader(MimeHeader.HEADER_CONTENT_DISPOSITION,
    880                                     String.format("attachment;\n filename=\"%s\";\n size=%d",
    881                                     name,
    882                                     size));
    883                             bp.setHeader(MimeHeader.HEADER_CONTENT_ID, contentId);
    884 
    885                             /*
    886                              * HEADER_ANDROID_ATTACHMENT_STORE_DATA is a custom header we add to that
    887                              * we can later pull the attachment from the remote store if neccesary.
    888                              */
    889                             bp.setHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA, storeData);
    890 
    891                             mp.addBodyPart(bp);
    892                         }
    893                     }
    894                     finally {
    895                         if (cursor != null) {
    896                             cursor.close();
    897                         }
    898                     }
    899                 }
    900             }
    901         }
    902 
    903         /**
    904          * The columns to select when calling populateMessageFromGetMessageCursor()
    905          */
    906         private final String POPULATE_MESSAGE_SELECT_COLUMNS =
    907             "subject, sender_list, date, uid, flags, id, to_list, cc_list, " +
    908             "bcc_list, reply_to_list, attachment_count, internal_date, message_id, " +
    909             "store_flag_1, store_flag_2, flag_downloaded_full, flag_downloaded_partial, " +
    910             "flag_deleted, x_headers";
    911 
    912         /**
    913          * Populate a message from a cursor with the following columns:
    914          *
    915          * 0    subject
    916          * 1    from address
    917          * 2    date (long)
    918          * 3    uid
    919          * 4    flag list (older flags - comma-separated string)
    920          * 5    local id (long)
    921          * 6    to addresses
    922          * 7    cc addresses
    923          * 8    bcc addresses
    924          * 9    reply-to address
    925          * 10   attachment count (int)
    926          * 11   internal date (long)
    927          * 12   message id (from Mime headers)
    928          * 13   store flag 1
    929          * 14   store flag 2
    930          * 15   flag "downloaded full"
    931          * 16   flag "downloaded partial"
    932          * 17   flag "deleted"
    933          * 18   extended headers ("\r\n"-separated string)
    934          */
    935         private void populateMessageFromGetMessageCursor(LocalMessage message, Cursor cursor)
    936                 throws MessagingException{
    937             message.setSubject(cursor.getString(0) == null ? "" : cursor.getString(0));
    938             Address[] from = Address.legacyUnpack(cursor.getString(1));
    939             if (from.length > 0) {
    940                 message.setFrom(from[0]);
    941             }
    942             message.setSentDate(new Date(cursor.getLong(2)));
    943             message.setUid(cursor.getString(3));
    944             String flagList = cursor.getString(4);
    945             if (flagList != null && flagList.length() > 0) {
    946                 String[] flags = flagList.split(",");
    947                 try {
    948                     for (String flag : flags) {
    949                         message.setFlagInternal(Flag.valueOf(flag.toUpperCase()), true);
    950                     }
    951                 } catch (Exception e) {
    952                 }
    953             }
    954             message.mId = cursor.getLong(5);
    955             message.setRecipients(RecipientType.TO, Address.legacyUnpack(cursor.getString(6)));
    956             message.setRecipients(RecipientType.CC, Address.legacyUnpack(cursor.getString(7)));
    957             message.setRecipients(RecipientType.BCC, Address.legacyUnpack(cursor.getString(8)));
    958             message.setReplyTo(Address.legacyUnpack(cursor.getString(9)));
    959             message.mAttachmentCount = cursor.getInt(10);
    960             message.setInternalDate(new Date(cursor.getLong(11)));
    961             message.setMessageId(cursor.getString(12));
    962             message.setFlagInternal(Flag.X_STORE_1, (0 != cursor.getInt(13)));
    963             message.setFlagInternal(Flag.X_STORE_2, (0 != cursor.getInt(14)));
    964             message.setFlagInternal(Flag.X_DOWNLOADED_FULL, (0 != cursor.getInt(15)));
    965             message.setFlagInternal(Flag.X_DOWNLOADED_PARTIAL, (0 != cursor.getInt(16)));
    966             message.setFlagInternal(Flag.DELETED, (0 != cursor.getInt(17)));
    967             message.setExtendedHeaders(cursor.getString(18));
    968         }
    969 
    970         @Override
    971         public Message[] getMessages(int start, int end, MessageRetrievalListener listener)
    972                 throws MessagingException {
    973             open(OpenMode.READ_WRITE);
    974             throw new MessagingException(
    975                     "LocalStore.getMessages(int, int, MessageRetrievalListener) not yet implemented");
    976         }
    977 
    978         @Override
    979         public Message getMessage(String uid) throws MessagingException {
    980             open(OpenMode.READ_WRITE);
    981             LocalMessage message = new LocalMessage(uid, this);
    982             Cursor cursor = null;
    983             try {
    984                 cursor = mDb.rawQuery(
    985                         "SELECT " + POPULATE_MESSAGE_SELECT_COLUMNS +
    986                         " FROM messages" +
    987                         " WHERE uid = ? AND folder_id = ?",
    988                         new String[] {
    989                                 message.getUid(), Long.toString(mFolderId)
    990                         });
    991                 if (!cursor.moveToNext()) {
    992                     return null;
    993                 }
    994                 populateMessageFromGetMessageCursor(message, cursor);
    995             }
    996             finally {
    997                 if (cursor != null) {
    998                     cursor.close();
    999                 }
   1000             }
   1001             return message;
   1002         }
   1003 
   1004         @Override
   1005         public Message[] getMessages(MessageRetrievalListener listener) throws MessagingException {
   1006             open(OpenMode.READ_WRITE);
   1007             ArrayList<Message> messages = new ArrayList<Message>();
   1008             Cursor cursor = null;
   1009             try {
   1010                 cursor = mDb.rawQuery(
   1011                         "SELECT " + POPULATE_MESSAGE_SELECT_COLUMNS +
   1012                         " FROM messages" +
   1013                         " WHERE folder_id = ?",
   1014                         new String[] {
   1015                                 Long.toString(mFolderId)
   1016                         });
   1017 
   1018                 while (cursor.moveToNext()) {
   1019                     LocalMessage message = new LocalMessage(null, this);
   1020                     populateMessageFromGetMessageCursor(message, cursor);
   1021                     messages.add(message);
   1022                 }
   1023             }
   1024             finally {
   1025                 if (cursor != null) {
   1026                     cursor.close();
   1027                 }
   1028             }
   1029 
   1030             return messages.toArray(new Message[] {});
   1031         }
   1032 
   1033         @Override
   1034         public Message[] getMessages(String[] uids, MessageRetrievalListener listener)
   1035                 throws MessagingException {
   1036             open(OpenMode.READ_WRITE);
   1037             if (uids == null) {
   1038                 return getMessages(listener);
   1039             }
   1040             ArrayList<Message> messages = new ArrayList<Message>();
   1041             for (String uid : uids) {
   1042                 messages.add(getMessage(uid));
   1043             }
   1044             return messages.toArray(new Message[] {});
   1045         }
   1046 
   1047         /**
   1048          * Return a set of messages based on the state of the flags.
   1049          *
   1050          * @param setFlags The flags that should be set for a message to be selected (null ok)
   1051          * @param clearFlags The flags that should be clear for a message to be selected (null ok)
   1052          * @param listener
   1053          * @return A list of messages matching the desired flag states.
   1054          * @throws MessagingException
   1055          */
   1056         @Override
   1057         public Message[] getMessages(Flag[] setFlags, Flag[] clearFlags,
   1058                 MessageRetrievalListener listener) throws MessagingException {
   1059             // Generate WHERE clause based on flags observed
   1060             StringBuilder sql = new StringBuilder(
   1061                     "SELECT " + POPULATE_MESSAGE_SELECT_COLUMNS +
   1062                     " FROM messages" +
   1063                     " WHERE ");
   1064             buildFlagPredicates(sql, setFlags, clearFlags);
   1065             sql.append("folder_id = ?");
   1066 
   1067             open(OpenMode.READ_WRITE);
   1068             ArrayList<Message> messages = new ArrayList<Message>();
   1069 
   1070             Cursor cursor = null;
   1071             try {
   1072                 cursor = mDb.rawQuery(
   1073                         sql.toString(),
   1074                         new String[] {
   1075                                 Long.toString(mFolderId)
   1076                         });
   1077 
   1078                 while (cursor.moveToNext()) {
   1079                     LocalMessage message = new LocalMessage(null, this);
   1080                     populateMessageFromGetMessageCursor(message, cursor);
   1081                     messages.add(message);
   1082                 }
   1083             } finally {
   1084                 if (cursor != null) {
   1085                     cursor.close();
   1086                 }
   1087             }
   1088 
   1089             return messages.toArray(new Message[] {});
   1090         }
   1091 
   1092         /*
   1093          * Build SQL where predicates expression from set and clear flag arrays.
   1094          */
   1095         private void buildFlagPredicates(StringBuilder sql, Flag[] setFlags, Flag[] clearFlags)
   1096                 throws MessagingException {
   1097             if (setFlags != null) {
   1098                 for (Flag flag : setFlags) {
   1099                     if (flag == Flag.X_STORE_1) {
   1100                         sql.append("store_flag_1 = 1 AND ");
   1101                     } else if (flag == Flag.X_STORE_2) {
   1102                         sql.append("store_flag_2 = 1 AND ");
   1103                     } else if (flag == Flag.X_DOWNLOADED_FULL) {
   1104                         sql.append("flag_downloaded_full = 1 AND ");
   1105                     } else if (flag == Flag.X_DOWNLOADED_PARTIAL) {
   1106                         sql.append("flag_downloaded_partial = 1 AND ");
   1107                     } else if (flag == Flag.DELETED) {
   1108                         sql.append("flag_deleted = 1 AND ");
   1109                     } else {
   1110                         throw new MessagingException("Unsupported flag " + flag);
   1111                     }
   1112                 }
   1113             }
   1114             if (clearFlags != null) {
   1115                 for (Flag flag : clearFlags) {
   1116                     if (flag == Flag.X_STORE_1) {
   1117                         sql.append("store_flag_1 = 0 AND ");
   1118                     } else if (flag == Flag.X_STORE_2) {
   1119                         sql.append("store_flag_2 = 0 AND ");
   1120                     } else if (flag == Flag.X_DOWNLOADED_FULL) {
   1121                         sql.append("flag_downloaded_full = 0 AND ");
   1122                     } else if (flag == Flag.X_DOWNLOADED_PARTIAL) {
   1123                         sql.append("flag_downloaded_partial = 0 AND ");
   1124                     } else if (flag == Flag.DELETED) {
   1125                         sql.append("flag_deleted = 0 AND ");
   1126                     } else {
   1127                         throw new MessagingException("Unsupported flag " + flag);
   1128                     }
   1129                 }
   1130             }
   1131         }
   1132 
   1133         @Override
   1134         public void copyMessages(Message[] msgs, Folder folder, MessageUpdateCallbacks callbacks)
   1135                 throws MessagingException {
   1136             if (!(folder instanceof LocalFolder)) {
   1137                 throw new MessagingException("copyMessages called with incorrect Folder");
   1138             }
   1139             ((LocalFolder) folder).appendMessages(msgs, true);
   1140         }
   1141 
   1142         /**
   1143          * The method differs slightly from the contract; If an incoming message already has a uid
   1144          * assigned and it matches the uid of an existing message then this message will replace the
   1145          * old message. It is implemented as a delete/insert. This functionality is used in saving
   1146          * of drafts and re-synchronization of updated server messages.
   1147          */
   1148         @Override
   1149         public void appendMessages(Message[] messages) throws MessagingException {
   1150             appendMessages(messages, false);
   1151         }
   1152 
   1153         /**
   1154          * The method differs slightly from the contract; If an incoming message already has a uid
   1155          * assigned and it matches the uid of an existing message then this message will replace the
   1156          * old message. It is implemented as a delete/insert. This functionality is used in saving
   1157          * of drafts and re-synchronization of updated server messages.
   1158          */
   1159         public void appendMessages(Message[] messages, boolean copy) throws MessagingException {
   1160             open(OpenMode.READ_WRITE);
   1161             for (Message message : messages) {
   1162                 if (!(message instanceof MimeMessage)) {
   1163                     throw new Error("LocalStore can only store Messages that extend MimeMessage");
   1164                 }
   1165 
   1166                 String uid = message.getUid();
   1167                 if (uid == null) {
   1168                     message.setUid("Local" + UUID.randomUUID().toString());
   1169                 }
   1170                 else {
   1171                     /*
   1172                      * The message may already exist in this Folder, so delete it first.
   1173                      */
   1174                     deleteAttachments(message.getUid());
   1175                     mDb.execSQL("DELETE FROM messages WHERE folder_id = ? AND uid = ?",
   1176                             new Object[] { mFolderId, message.getUid() });
   1177                 }
   1178 
   1179                 ArrayList<Part> viewables = new ArrayList<Part>();
   1180                 ArrayList<Part> attachments = new ArrayList<Part>();
   1181                 MimeUtility.collectParts(message, viewables, attachments);
   1182 
   1183                 StringBuffer sbHtml = new StringBuffer();
   1184                 StringBuffer sbText = new StringBuffer();
   1185                 for (Part viewable : viewables) {
   1186                     try {
   1187                         String text = MimeUtility.getTextFromPart(viewable);
   1188                         /*
   1189                          * Anything with MIME type text/html will be stored as such. Anything
   1190                          * else will be stored as text/plain.
   1191                          */
   1192                         if (viewable.getMimeType().equalsIgnoreCase("text/html")) {
   1193                             sbHtml.append(text);
   1194                         }
   1195                         else {
   1196                             sbText.append(text);
   1197                         }
   1198                     } catch (Exception e) {
   1199                         throw new MessagingException("Unable to get text for message part", e);
   1200                     }
   1201                 }
   1202 
   1203                 try {
   1204                     ContentValues cv = new ContentValues();
   1205                     cv.put("uid", message.getUid());
   1206                     cv.put("subject", message.getSubject());
   1207                     cv.put("sender_list", Address.legacyPack(message.getFrom()));
   1208                     cv.put("date", message.getSentDate() == null
   1209                             ? System.currentTimeMillis() : message.getSentDate().getTime());
   1210                     cv.put("flags", makeFlagsString(message));
   1211                     cv.put("folder_id", mFolderId);
   1212                     cv.put("to_list", Address.legacyPack(message.getRecipients(RecipientType.TO)));
   1213                     cv.put("cc_list", Address.legacyPack(message.getRecipients(RecipientType.CC)));
   1214                     cv.put("bcc_list", Address.legacyPack(
   1215                             message.getRecipients(RecipientType.BCC)));
   1216                     cv.put("html_content", sbHtml.length() > 0 ? sbHtml.toString() : null);
   1217                     cv.put("text_content", sbText.length() > 0 ? sbText.toString() : null);
   1218                     cv.put("reply_to_list", Address.legacyPack(message.getReplyTo()));
   1219                     cv.put("attachment_count", attachments.size());
   1220                     cv.put("internal_date",  message.getInternalDate() == null
   1221                             ? System.currentTimeMillis() : message.getInternalDate().getTime());
   1222                     cv.put("message_id", ((MimeMessage)message).getMessageId());
   1223                     cv.put("store_flag_1", makeFlagNumeric(message, Flag.X_STORE_1));
   1224                     cv.put("store_flag_2", makeFlagNumeric(message, Flag.X_STORE_2));
   1225                     cv.put("flag_downloaded_full",
   1226                             makeFlagNumeric(message, Flag.X_DOWNLOADED_FULL));
   1227                     cv.put("flag_downloaded_partial",
   1228                             makeFlagNumeric(message, Flag.X_DOWNLOADED_PARTIAL));
   1229                     cv.put("flag_deleted", makeFlagNumeric(message, Flag.DELETED));
   1230                     cv.put("x_headers", ((MimeMessage) message).getExtendedHeaders());
   1231                     long messageId = mDb.insert("messages", "uid", cv);
   1232                     for (Part attachment : attachments) {
   1233                         saveAttachment(messageId, attachment, copy);
   1234                     }
   1235                 } catch (Exception e) {
   1236                     throw new MessagingException("Error appending message", e);
   1237                 }
   1238             }
   1239         }
   1240 
   1241         /**
   1242          * Update the given message in the LocalStore without first deleting the existing
   1243          * message (contrast with appendMessages). This method is used to store changes
   1244          * to the given message while updating attachments and not removing existing
   1245          * attachment data.
   1246          * TODO In the future this method should be combined with appendMessages since the Message
   1247          * contains enough data to decide what to do.
   1248          * @param message
   1249          * @throws MessagingException
   1250          */
   1251         public void updateMessage(LocalMessage message) throws MessagingException {
   1252             open(OpenMode.READ_WRITE);
   1253             ArrayList<Part> viewables = new ArrayList<Part>();
   1254             ArrayList<Part> attachments = new ArrayList<Part>();
   1255             MimeUtility.collectParts(message, viewables, attachments);
   1256 
   1257             StringBuffer sbHtml = new StringBuffer();
   1258             StringBuffer sbText = new StringBuffer();
   1259             for (int i = 0, count = viewables.size(); i < count; i++)  {
   1260                 Part viewable = viewables.get(i);
   1261                 try {
   1262                     String text = MimeUtility.getTextFromPart(viewable);
   1263                     /*
   1264                      * Anything with MIME type text/html will be stored as such. Anything
   1265                      * else will be stored as text/plain.
   1266                      */
   1267                     if (viewable.getMimeType().equalsIgnoreCase("text/html")) {
   1268                         sbHtml.append(text);
   1269                     }
   1270                     else {
   1271                         sbText.append(text);
   1272                     }
   1273                 } catch (Exception e) {
   1274                     throw new MessagingException("Unable to get text for message part", e);
   1275                 }
   1276             }
   1277 
   1278             try {
   1279                 mDb.execSQL("UPDATE messages SET "
   1280                         + "uid = ?, subject = ?, sender_list = ?, date = ?, flags = ?, "
   1281                         + "folder_id = ?, to_list = ?, cc_list = ?, bcc_list = ?, "
   1282                         + "html_content = ?, text_content = ?, reply_to_list = ?, "
   1283                         + "attachment_count = ?, message_id = ?, store_flag_1 = ?, "
   1284                         + "store_flag_2 = ?, flag_downloaded_full = ?, "
   1285                         + "flag_downloaded_partial = ?, flag_deleted = ?, x_headers = ? "
   1286                         + "WHERE id = ?",
   1287                         new Object[] {
   1288                                 message.getUid(),
   1289                                 message.getSubject(),
   1290                                 Address.legacyPack(message.getFrom()),
   1291                                 message.getSentDate() == null ? System
   1292                                         .currentTimeMillis() : message.getSentDate()
   1293                                         .getTime(),
   1294                                 makeFlagsString(message),
   1295                                 mFolderId,
   1296                                 Address.legacyPack(message
   1297                                         .getRecipients(RecipientType.TO)),
   1298                                 Address.legacyPack(message
   1299                                         .getRecipients(RecipientType.CC)),
   1300                                 Address.legacyPack(message
   1301                                         .getRecipients(RecipientType.BCC)),
   1302                                 sbHtml.length() > 0 ? sbHtml.toString() : null,
   1303                                 sbText.length() > 0 ? sbText.toString() : null,
   1304                                 Address.legacyPack(message.getReplyTo()),
   1305                                 attachments.size(),
   1306                                 message.getMessageId(),
   1307                                 makeFlagNumeric(message, Flag.X_STORE_1),
   1308                                 makeFlagNumeric(message, Flag.X_STORE_2),
   1309                                 makeFlagNumeric(message, Flag.X_DOWNLOADED_FULL),
   1310                                 makeFlagNumeric(message, Flag.X_DOWNLOADED_PARTIAL),
   1311                                 makeFlagNumeric(message, Flag.DELETED),
   1312                                 message.getExtendedHeaders(),
   1313 
   1314                                 message.mId
   1315                                 });
   1316 
   1317                 for (int i = 0, count = attachments.size(); i < count; i++) {
   1318                     Part attachment = attachments.get(i);
   1319                     saveAttachment(message.mId, attachment, false);
   1320                 }
   1321             } catch (Exception e) {
   1322                 throw new MessagingException("Error appending message", e);
   1323             }
   1324         }
   1325 
   1326         /**
   1327          * @param messageId
   1328          * @param attachment
   1329          * @param attachmentId -1 to create a new attachment or >= 0 to update an existing
   1330          * @throws IOException
   1331          * @throws MessagingException
   1332          */
   1333         private void saveAttachment(long messageId, Part attachment, boolean saveAsNew)
   1334                 throws IOException, MessagingException {
   1335             long attachmentId = -1;
   1336             Uri contentUri = null;
   1337             int size = -1;
   1338             File tempAttachmentFile = null;
   1339 
   1340             if ((!saveAsNew) && (attachment instanceof LocalAttachmentBodyPart)) {
   1341                 attachmentId = ((LocalAttachmentBodyPart) attachment).getAttachmentId();
   1342             }
   1343 
   1344             if (attachment.getBody() != null) {
   1345                 Body body = attachment.getBody();
   1346                 if (body instanceof LocalAttachmentBody) {
   1347                     contentUri = ((LocalAttachmentBody) body).getContentUri();
   1348                 }
   1349                 else {
   1350                     /*
   1351                      * If the attachment has a body we're expected to save it into the local store
   1352                      * so we copy the data into a cached attachment file.
   1353                      */
   1354                     InputStream in = attachment.getBody().getInputStream();
   1355                     tempAttachmentFile = File.createTempFile("att", null, mAttachmentsDir);
   1356                     FileOutputStream out = new FileOutputStream(tempAttachmentFile);
   1357                     size = IOUtils.copy(in, out);
   1358                     in.close();
   1359                     out.close();
   1360                 }
   1361             }
   1362 
   1363             if (size == -1) {
   1364                 /*
   1365                  * If the attachment is not yet downloaded see if we can pull a size
   1366                  * off the Content-Disposition.
   1367                  */
   1368                 String disposition = attachment.getDisposition();
   1369                 if (disposition != null) {
   1370                     String s = MimeUtility.getHeaderParameter(disposition, "size");
   1371                     if (s != null) {
   1372                         size = Integer.parseInt(s);
   1373                     }
   1374                 }
   1375             }
   1376             if (size == -1) {
   1377                 size = 0;
   1378             }
   1379 
   1380             String storeData =
   1381                 Utility.combine(attachment.getHeader(
   1382                         MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA), ',');
   1383 
   1384             String name = MimeUtility.getHeaderParameter(attachment.getContentType(), "name");
   1385             String contentId = attachment.getContentId();
   1386 
   1387             if (attachmentId == -1) {
   1388                 ContentValues cv = new ContentValues();
   1389                 cv.put("message_id", messageId);
   1390                 cv.put("content_uri", contentUri != null ? contentUri.toString() : null);
   1391                 cv.put("store_data", storeData);
   1392                 cv.put("size", size);
   1393                 cv.put("name", name);
   1394                 cv.put("mime_type", attachment.getMimeType());
   1395                 cv.put("content_id", contentId);
   1396 
   1397                 attachmentId = mDb.insert("attachments", "message_id", cv);
   1398             }
   1399             else {
   1400                 ContentValues cv = new ContentValues();
   1401                 cv.put("content_uri", contentUri != null ? contentUri.toString() : null);
   1402                 cv.put("size", size);
   1403                 cv.put("content_id", contentId);
   1404                 cv.put("message_id", messageId);
   1405                 mDb.update(
   1406                         "attachments",
   1407                         cv,
   1408                         "id = ?",
   1409                         new String[] { Long.toString(attachmentId) });
   1410             }
   1411 
   1412             if (tempAttachmentFile != null) {
   1413                 File attachmentFile = new File(mAttachmentsDir, Long.toString(attachmentId));
   1414                 tempAttachmentFile.renameTo(attachmentFile);
   1415                 // Doing this requires knowing the account id
   1416 //                contentUri = AttachmentProvider.getAttachmentUri(
   1417 //                        new File(mPath).getName(),
   1418 //                        attachmentId);
   1419                 attachment.setBody(new LocalAttachmentBody(contentUri, mContext));
   1420                 ContentValues cv = new ContentValues();
   1421                 cv.put("content_uri", contentUri != null ? contentUri.toString() : null);
   1422                 mDb.update(
   1423                         "attachments",
   1424                         cv,
   1425                         "id = ?",
   1426                         new String[] { Long.toString(attachmentId) });
   1427             }
   1428 
   1429             if (attachment instanceof LocalAttachmentBodyPart) {
   1430                 ((LocalAttachmentBodyPart) attachment).setAttachmentId(attachmentId);
   1431             }
   1432         }
   1433 
   1434         /**
   1435          * Changes the stored uid of the given message (using it's internal id as a key) to
   1436          * the uid in the message.
   1437          * @param message
   1438          */
   1439         public void changeUid(LocalMessage message) throws MessagingException {
   1440             open(OpenMode.READ_WRITE);
   1441             ContentValues cv = new ContentValues();
   1442             cv.put("uid", message.getUid());
   1443             mDb.update("messages", cv, "id = ?", new String[] { Long.toString(message.mId) });
   1444         }
   1445 
   1446         @Override
   1447         public void setFlags(Message[] messages, Flag[] flags, boolean value)
   1448                 throws MessagingException {
   1449             open(OpenMode.READ_WRITE);
   1450             for (Message message : messages) {
   1451                 message.setFlags(flags, value);
   1452             }
   1453         }
   1454 
   1455         @Override
   1456         public Message[] expunge() throws MessagingException {
   1457             open(OpenMode.READ_WRITE);
   1458             ArrayList<Message> expungedMessages = new ArrayList<Message>();
   1459             /*
   1460              * epunge() doesn't do anything because deleted messages are saved for their uids
   1461              * and really, really deleted messages are "Destroyed" and removed immediately.
   1462              */
   1463             return expungedMessages.toArray(new Message[] {});
   1464         }
   1465 
   1466         @Override
   1467         public void delete(boolean recurse) throws MessagingException {
   1468             // We need to open the folder first to make sure we've got it's id
   1469             open(OpenMode.READ_ONLY);
   1470             Message[] messages = getMessages(null);
   1471             for (Message message : messages) {
   1472                 deleteAttachments(message.getUid());
   1473             }
   1474             mDb.execSQL("DELETE FROM folders WHERE id = ?", new Object[] {
   1475                 Long.toString(mFolderId),
   1476             });
   1477         }
   1478 
   1479         @Override
   1480         public boolean equals(Object o) {
   1481             if (o instanceof LocalFolder) {
   1482                 return ((LocalFolder)o).mName.equals(mName);
   1483             }
   1484             return super.equals(o);
   1485         }
   1486 
   1487         @Override
   1488         public Flag[] getPermanentFlags() throws MessagingException {
   1489             return PERMANENT_FLAGS;
   1490         }
   1491 
   1492         private void deleteAttachments(String uid) throws MessagingException {
   1493             open(OpenMode.READ_WRITE);
   1494             Cursor messagesCursor = null;
   1495             try {
   1496                 messagesCursor = mDb.query(
   1497                         "messages",
   1498                         new String[] { "id" },
   1499                         "folder_id = ? AND uid = ?",
   1500                         new String[] { Long.toString(mFolderId), uid },
   1501                         null,
   1502                         null,
   1503                         null);
   1504                 while (messagesCursor.moveToNext()) {
   1505                     long messageId = messagesCursor.getLong(0);
   1506                     Cursor attachmentsCursor = null;
   1507                     try {
   1508                         attachmentsCursor = mDb.query(
   1509                                 "attachments",
   1510                                 new String[] { "id" },
   1511                                 "message_id = ?",
   1512                                 new String[] { Long.toString(messageId) },
   1513                                 null,
   1514                                 null,
   1515                                 null);
   1516                         while (attachmentsCursor.moveToNext()) {
   1517                             long attachmentId = attachmentsCursor.getLong(0);
   1518                             try{
   1519                                 File file = new File(mAttachmentsDir, Long.toString(attachmentId));
   1520                                 if (file.exists()) {
   1521                                     file.delete();
   1522                                 }
   1523                             }
   1524                             catch (Exception e) {
   1525 
   1526                             }
   1527                         }
   1528                     }
   1529                     finally {
   1530                         if (attachmentsCursor != null) {
   1531                             attachmentsCursor.close();
   1532                         }
   1533                     }
   1534                 }
   1535             }
   1536             finally {
   1537                 if (messagesCursor != null) {
   1538                     messagesCursor.close();
   1539                 }
   1540             }
   1541         }
   1542 
   1543         /**
   1544          * Support for local persistence for our remote stores.
   1545          * Will open the folder if necessary.
   1546          */
   1547         public Folder.PersistentDataCallbacks getPersistentCallbacks() throws MessagingException {
   1548             open(OpenMode.READ_WRITE);
   1549             return this;
   1550         }
   1551 
   1552         public String getPersistentString(String key, String defaultValue) {
   1553             return LocalStore.this.getPersistentString(mFolderId, key, defaultValue);
   1554         }
   1555 
   1556         public void setPersistentString(String key, String value) {
   1557             LocalStore.this.setPersistentString(mFolderId, key, value);
   1558         }
   1559 
   1560         /**
   1561          * Transactionally combine a key/value and a complete message flags flip.  Used
   1562          * for setting sync bits in messages.
   1563          *
   1564          * Note:  Not all flags are supported here and can only be changed with Message.setFlag().
   1565          * For example, Flag.DELETED has side effects (removes attachments).
   1566          *
   1567          * @param key
   1568          * @param value
   1569          * @param setFlags
   1570          * @param clearFlags
   1571          */
   1572         public void setPersistentStringAndMessageFlags(String key, String value,
   1573                 Flag[] setFlags, Flag[] clearFlags) throws MessagingException {
   1574             mDb.beginTransaction();
   1575             try {
   1576                 // take care of folder persistence
   1577                 if (key != null) {
   1578                     setPersistentString(key, value);
   1579                 }
   1580 
   1581                 // take care of flags
   1582                 ContentValues cv = new ContentValues();
   1583                 if (setFlags != null) {
   1584                     for (Flag flag : setFlags) {
   1585                         if (flag == Flag.X_STORE_1) {
   1586                             cv.put("store_flag_1", 1);
   1587                         } else if (flag == Flag.X_STORE_2) {
   1588                             cv.put("store_flag_2", 1);
   1589                         } else if (flag == Flag.X_DOWNLOADED_FULL) {
   1590                             cv.put("flag_downloaded_full", 1);
   1591                         } else if (flag == Flag.X_DOWNLOADED_PARTIAL) {
   1592                             cv.put("flag_downloaded_partial", 1);
   1593                         } else {
   1594                             throw new MessagingException("Unsupported flag " + flag);
   1595                         }
   1596                     }
   1597                 }
   1598                 if (clearFlags != null) {
   1599                     for (Flag flag : clearFlags) {
   1600                         if (flag == Flag.X_STORE_1) {
   1601                             cv.put("store_flag_1", 0);
   1602                         } else if (flag == Flag.X_STORE_2) {
   1603                             cv.put("store_flag_2", 0);
   1604                         } else if (flag == Flag.X_DOWNLOADED_FULL) {
   1605                             cv.put("flag_downloaded_full", 0);
   1606                         } else if (flag == Flag.X_DOWNLOADED_PARTIAL) {
   1607                             cv.put("flag_downloaded_partial", 0);
   1608                         } else {
   1609                             throw new MessagingException("Unsupported flag " + flag);
   1610                         }
   1611                     }
   1612                 }
   1613                 mDb.update("messages", cv,
   1614                         "folder_id = ?", new String[] { Long.toString(mFolderId) });
   1615 
   1616                 mDb.setTransactionSuccessful();
   1617             } finally {
   1618                 mDb.endTransaction();
   1619             }
   1620 
   1621         }
   1622 
   1623         @Override
   1624         public Message createMessage(String uid) throws MessagingException {
   1625             return new LocalMessage(uid, this);
   1626         }
   1627     }
   1628 
   1629     public class LocalMessage extends MimeMessage {
   1630         private long mId;
   1631         private int mAttachmentCount;
   1632 
   1633         LocalMessage(String uid, Folder folder) throws MessagingException {
   1634             this.mUid = uid;
   1635             this.mFolder = folder;
   1636         }
   1637 
   1638         public int getAttachmentCount() {
   1639             return mAttachmentCount;
   1640         }
   1641 
   1642         public void parse(InputStream in) throws IOException, MessagingException {
   1643             super.parse(in);
   1644         }
   1645 
   1646         public void setFlagInternal(Flag flag, boolean set) throws MessagingException {
   1647             super.setFlag(flag, set);
   1648         }
   1649 
   1650         public long getId() {
   1651             return mId;
   1652         }
   1653 
   1654         @Override
   1655         public void setFlag(Flag flag, boolean set) throws MessagingException {
   1656             if (flag == Flag.DELETED && set) {
   1657                 /*
   1658                  * If a message is being marked as deleted we want to clear out it's content
   1659                  * and attachments as well. Delete will not actually remove the row since we need
   1660                  * to retain the uid for synchronization purposes.
   1661                  */
   1662 
   1663                 /*
   1664                  * Delete all of the messages' content to save space.
   1665                  */
   1666                 mDb.execSQL(
   1667                         "UPDATE messages SET " +
   1668                         "subject = NULL, " +
   1669                         "sender_list = NULL, " +
   1670                         "date = NULL, " +
   1671                         "to_list = NULL, " +
   1672                         "cc_list = NULL, " +
   1673                         "bcc_list = NULL, " +
   1674                         "html_content = NULL, " +
   1675                         "text_content = NULL, " +
   1676                         "reply_to_list = NULL " +
   1677                         "WHERE id = ?",
   1678                         new Object[] {
   1679                                 mId
   1680                         });
   1681 
   1682                 ((LocalFolder) mFolder).deleteAttachments(getUid());
   1683 
   1684                 /*
   1685                  * Delete all of the messages' attachments to save space.
   1686                  */
   1687                 mDb.execSQL("DELETE FROM attachments WHERE id = ?",
   1688                         new Object[] {
   1689                                 mId
   1690                         });
   1691             }
   1692             else if (flag == Flag.X_DESTROYED && set) {
   1693                 ((LocalFolder) mFolder).deleteAttachments(getUid());
   1694                 mDb.execSQL("DELETE FROM messages WHERE id = ?",
   1695                         new Object[] { mId });
   1696             }
   1697 
   1698             /*
   1699              * Update the unread count on the folder.
   1700              */
   1701             try {
   1702                 if (flag == Flag.DELETED || flag == Flag.X_DESTROYED || flag == Flag.SEEN) {
   1703                     LocalFolder folder = (LocalFolder)mFolder;
   1704                     if (set && !isSet(Flag.SEEN)) {
   1705                         folder.setUnreadMessageCount(folder.getUnreadMessageCount() - 1);
   1706                     }
   1707                     else if (!set && isSet(Flag.SEEN)) {
   1708                         folder.setUnreadMessageCount(folder.getUnreadMessageCount() + 1);
   1709                     }
   1710                 }
   1711             }
   1712             catch (MessagingException me) {
   1713                 Log.e(Email.LOG_TAG, "Unable to update LocalStore unread message count",
   1714                         me);
   1715                 throw new RuntimeException(me);
   1716             }
   1717 
   1718             super.setFlag(flag, set);
   1719             /*
   1720              * Set the flags on the message.
   1721              */
   1722             mDb.execSQL("UPDATE messages "
   1723                     + "SET flags = ?, store_flag_1 = ?, store_flag_2 = ?, "
   1724                     + "flag_downloaded_full = ?, flag_downloaded_partial = ?, flag_deleted = ? "
   1725                     + "WHERE id = ?",
   1726                     new Object[] {
   1727                             makeFlagsString(this),
   1728                             makeFlagNumeric(this, Flag.X_STORE_1),
   1729                             makeFlagNumeric(this, Flag.X_STORE_2),
   1730                             makeFlagNumeric(this, Flag.X_DOWNLOADED_FULL),
   1731                             makeFlagNumeric(this, Flag.X_DOWNLOADED_PARTIAL),
   1732                             makeFlagNumeric(this, Flag.DELETED),
   1733                             mId
   1734             });
   1735         }
   1736     }
   1737 
   1738     /**
   1739      * Convert *old* flags to flags string.  Some flags are kept in their own columns
   1740      * (for selecting) and are not included here.
   1741      * @param message The message containing the flag(s)
   1742      * @return a comma-separated list of flags, to write into the "flags" column
   1743      */
   1744     /* package */ String makeFlagsString(Message message) {
   1745         StringBuilder sb = null;
   1746         boolean nonEmpty = false;
   1747         for (Flag flag : Flag.values()) {
   1748             if (flag != Flag.X_STORE_1 && flag != Flag.X_STORE_2 &&
   1749                     flag != Flag.X_DOWNLOADED_FULL && flag != Flag.X_DOWNLOADED_PARTIAL &&
   1750                     flag != Flag.DELETED &&
   1751                     message.isSet(flag)) {
   1752                 if (sb == null) {
   1753                     sb = new StringBuilder();
   1754                 }
   1755                 if (nonEmpty) {
   1756                     sb.append(',');
   1757                 }
   1758                 sb.append(flag.toString());
   1759                 nonEmpty = true;
   1760             }
   1761         }
   1762         return (sb == null) ? null : sb.toString();
   1763     }
   1764 
   1765     /**
   1766      * Convert flags to numeric form (0 or 1) for database storage.
   1767      * @param message The message containing the flag of interest
   1768      * @param flag The flag of interest
   1769      *
   1770      */
   1771     /* package */ int makeFlagNumeric(Message message, Flag flag) {
   1772         return message.isSet(flag) ? 1 : 0;
   1773     }
   1774 
   1775 
   1776     public class LocalAttachmentBodyPart extends MimeBodyPart {
   1777         private long mAttachmentId = -1;
   1778 
   1779         public LocalAttachmentBodyPart(Body body, long attachmentId) throws MessagingException {
   1780             super(body);
   1781             mAttachmentId = attachmentId;
   1782         }
   1783 
   1784         /**
   1785          * Returns the local attachment id of this body, or -1 if it is not stored.
   1786          * @return
   1787          */
   1788         public long getAttachmentId() {
   1789             return mAttachmentId;
   1790         }
   1791 
   1792         public void setAttachmentId(long attachmentId) {
   1793             mAttachmentId = attachmentId;
   1794         }
   1795 
   1796         public String toString() {
   1797             return "" + mAttachmentId;
   1798         }
   1799     }
   1800 
   1801     public static class LocalAttachmentBody implements Body {
   1802         private Context mContext;
   1803         private Uri mUri;
   1804 
   1805         public LocalAttachmentBody(Uri uri, Context context) {
   1806             mContext = context;
   1807             mUri = uri;
   1808         }
   1809 
   1810         public InputStream getInputStream() throws MessagingException {
   1811             try {
   1812                 return mContext.getContentResolver().openInputStream(mUri);
   1813             }
   1814             catch (FileNotFoundException fnfe) {
   1815                 /*
   1816                  * Since it's completely normal for us to try to serve up attachments that
   1817                  * have been blown away, we just return an empty stream.
   1818                  */
   1819                 return new ByteArrayInputStream(new byte[0]);
   1820             }
   1821             catch (IOException ioe) {
   1822                 throw new MessagingException("Invalid attachment.", ioe);
   1823             }
   1824         }
   1825 
   1826         public void writeTo(OutputStream out) throws IOException, MessagingException {
   1827             InputStream in = getInputStream();
   1828             Base64OutputStream base64Out = new Base64OutputStream(
   1829                 out, Base64.CRLF | Base64.NO_CLOSE);
   1830             IOUtils.copy(in, base64Out);
   1831             base64Out.close();
   1832         }
   1833 
   1834         public Uri getContentUri() {
   1835             return mUri;
   1836         }
   1837     }
   1838 
   1839     /**
   1840      * LocalStore does not have SettingActivity.
   1841      */
   1842     @Override
   1843     public Class<? extends android.app.Activity> getSettingActivityClass() {
   1844         return null;
   1845     }
   1846 }
   1847