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
17package com.android.email.mail.store;
18
19import com.android.email.Email;
20import com.android.email.Utility;
21import com.android.email.mail.Address;
22import com.android.email.mail.Body;
23import com.android.email.mail.FetchProfile;
24import com.android.email.mail.Flag;
25import com.android.email.mail.Folder;
26import com.android.email.mail.Message;
27import com.android.email.mail.Message.RecipientType;
28import com.android.email.mail.MessagingException;
29import com.android.email.mail.Part;
30import com.android.email.mail.Store;
31import com.android.email.mail.Store.PersistentDataCallbacks;
32import com.android.email.mail.internet.MimeBodyPart;
33import com.android.email.mail.internet.MimeHeader;
34import com.android.email.mail.internet.MimeMessage;
35import com.android.email.mail.internet.MimeMultipart;
36import com.android.email.mail.internet.MimeUtility;
37import com.android.email.mail.internet.TextBody;
38
39import org.apache.commons.io.IOUtils;
40
41import android.content.ContentValues;
42import android.content.Context;
43import android.database.Cursor;
44import android.database.sqlite.SQLiteDatabase;
45import android.net.Uri;
46import android.util.Log;
47import android.util.Base64;
48import android.util.Base64OutputStream;
49
50import java.io.ByteArrayInputStream;
51import java.io.File;
52import java.io.FileNotFoundException;
53import java.io.FileOutputStream;
54import java.io.IOException;
55import java.io.InputStream;
56import java.io.OutputStream;
57import java.io.UnsupportedEncodingException;
58import java.net.URI;
59import java.net.URLEncoder;
60import java.util.ArrayList;
61import java.util.Date;
62import java.util.UUID;
63
64/**
65 * <pre>
66 * Implements a SQLite database backed local store for Messages.
67 * </pre>
68 */
69public 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