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