() {
- @Override
- public Void doDbWork(final SQLiteDatabase db) throws WrappedException,
- UnavailableStorageException {
-
- selectionCallback.doDbWork(db, selection.toString(),
- selectionArgs.toArray(EMPTY_STRING_ARRAY));
-
- return null;
- }
- });
-
- selectionCallback.postDbWork();
-
- } catch (WrappedException e) {
- throw(MessagingException) e.getCause();
- }
-
- selectionArgs.clear();
- start += count;
- }
- }
-
- /**
- * Defines the behavior of {@link LocalStore#doBatchSetSelection(BatchSetSelection, int)}.
- */
- public interface BatchSetSelection {
- /**
- * @return The size of the argument list.
- */
- int getListSize();
-
- /**
- * Get a specific item of the argument list.
- *
- * @param index
- * The index of the item.
- *
- * @return Item at position {@code i} of the argument list.
- */
- String getListItem(int index);
-
- /**
- * Execute the SQL statement.
- *
- * @param db
- * Use this {@link SQLiteDatabase} instance for your SQL statement.
- * @param selectionSet
- * A partial selection string containing place holders for the argument list, e.g.
- * {@code " IN (?,?,?)"} (starts with a space).
- * @param selectionArgs
- * The current subset of the argument list.
- * @throws UnavailableStorageException
- */
- void doDbWork(SQLiteDatabase db, String selectionSet, String[] selectionArgs)
- throws UnavailableStorageException;
-
- /**
- * This will be executed after each invocation of
- * {@link #doDbWork(SQLiteDatabase, String, String[])} (after the transaction has been
- * committed).
- */
- void postDbWork();
- }
-
- /**
- * Change the state of a flag for a list of messages.
- *
- *
- * The goal of this method is to be fast. Currently this means using as few SQL UPDATE
- * statements as possible.
- *
- * @param messageIds
- * A list of primary keys in the "messages" table.
- * @param flag
- * The flag to change. This must be a flag with a separate column in the database.
- * @param newState
- * {@code true}, if the flag should be set. {@code false}, otherwise.
- *
- * @throws MessagingException
- */
- public void setFlag(final List messageIds, final Flag flag, final boolean newState)
- throws MessagingException {
-
- final ContentValues cv = new ContentValues();
- cv.put(getColumnNameForFlag(flag), newState);
-
- doBatchSetSelection(new BatchSetSelection() {
-
- @Override
- public int getListSize() {
- return messageIds.size();
- }
-
- @Override
- public String getListItem(int index) {
- return Long.toString(messageIds.get(index));
- }
-
- @Override
- public void doDbWork(SQLiteDatabase db, String selectionSet, String[] selectionArgs)
- throws UnavailableStorageException {
-
- db.update("messages", cv, "(empty IS NULL OR empty != 1) AND id" + selectionSet,
- selectionArgs);
- }
-
- @Override
- public void postDbWork() {
- notifyChange();
- }
- }, FLAG_UPDATE_BATCH_SIZE);
- }
-
- /**
- * Change the state of a flag for a list of threads.
- *
- *
- * The goal of this method is to be fast. Currently this means using as few SQL UPDATE
- * statements as possible.
- *
- * @param threadRootIds
- * A list of root thread IDs.
- * @param flag
- * The flag to change. This must be a flag with a separate column in the database.
- * @param newState
- * {@code true}, if the flag should be set. {@code false}, otherwise.
- *
- * @throws MessagingException
- */
- public void setFlagForThreads(final List threadRootIds, Flag flag, final boolean newState)
- throws MessagingException {
-
- final String flagColumn = getColumnNameForFlag(flag);
-
- doBatchSetSelection(new BatchSetSelection() {
-
- @Override
- public int getListSize() {
- return threadRootIds.size();
- }
-
- @Override
- public String getListItem(int index) {
- return Long.toString(threadRootIds.get(index));
- }
-
- @Override
- public void doDbWork(SQLiteDatabase db, String selectionSet, String[] selectionArgs)
- throws UnavailableStorageException {
-
- db.execSQL("UPDATE messages SET " + flagColumn + " = " + ((newState) ? "1" : "0") +
- " WHERE id IN (" +
- "SELECT m.id FROM threads t " +
- "LEFT JOIN messages m ON (t.message_id = m.id) " +
- "WHERE (m.empty IS NULL OR m.empty != 1) AND m.deleted = 0 " +
- "AND t.root" + selectionSet + ")",
- selectionArgs);
- }
-
- @Override
- public void postDbWork() {
- notifyChange();
- }
- }, THREAD_FLAG_UPDATE_BATCH_SIZE);
- }
-
- /**
- * Get folder name and UID for the supplied messages.
- *
- * @param messageIds
- * A list of primary keys in the "messages" table.
- * @param threadedList
- * If this is {@code true}, {@code messageIds} contains the thread IDs of the messages
- * at the root of a thread. In that case return UIDs for all messages in these threads.
- * If this is {@code false} only the UIDs for messages in {@code messageIds} are
- * returned.
- *
- * @return The list of UIDs for the messages grouped by folder name.
- *
- * @throws MessagingException
- */
- public Map> getFoldersAndUids(final List messageIds,
- final boolean threadedList) throws MessagingException {
-
- final Map> folderMap = new HashMap>();
-
- doBatchSetSelection(new BatchSetSelection() {
-
- @Override
- public int getListSize() {
- return messageIds.size();
- }
-
- @Override
- public String getListItem(int index) {
- return Long.toString(messageIds.get(index));
- }
-
- @Override
- public void doDbWork(SQLiteDatabase db, String selectionSet, String[] selectionArgs)
- throws UnavailableStorageException {
-
- if (threadedList) {
- String sql = "SELECT m.uid, f.name " +
- "FROM threads t " +
- "LEFT JOIN messages m ON (t.message_id = m.id) " +
- "LEFT JOIN folders f ON (m.folder_id = f.id) " +
- "WHERE (m.empty IS NULL OR m.empty != 1) AND m.deleted = 0 " +
- "AND t.root" + selectionSet;
-
- getDataFromCursor(db.rawQuery(sql, selectionArgs));
-
- } else {
- String sql =
- "SELECT m.uid, f.name " +
- "FROM messages m " +
- "LEFT JOIN folders f ON (m.folder_id = f.id) " +
- "WHERE (m.empty IS NULL OR m.empty != 1) AND m.id" + selectionSet;
-
- getDataFromCursor(db.rawQuery(sql, selectionArgs));
- }
- }
-
- private void getDataFromCursor(Cursor cursor) {
- try {
- while (cursor.moveToNext()) {
- String uid = cursor.getString(0);
- String folderName = cursor.getString(1);
-
- List uidList = folderMap.get(folderName);
- if (uidList == null) {
- uidList = new ArrayList();
- folderMap.put(folderName, uidList);
- }
-
- uidList.add(uid);
- }
- } finally {
- cursor.close();
- }
- }
-
- @Override
- public void postDbWork() {
- notifyChange();
-
- }
- }, UID_CHECK_BATCH_SIZE);
-
- return folderMap;
- }
-}
diff --git a/src/com/fsck/k9/mail/store/LockableDatabase.java b/src/com/fsck/k9/mail/store/LockableDatabase.java
index 81138d6e8..00b3f0add 100644
--- a/src/com/fsck/k9/mail/store/LockableDatabase.java
+++ b/src/com/fsck/k9/mail/store/LockableDatabase.java
@@ -376,24 +376,12 @@ public class LockableDatabase {
try {
final File databaseFile = prepareStorage(mStorageProviderId);
try {
- if (StorageManager.InternalStorageProvider.ID.equals(mStorageProviderId)) {
- // internal storage
- mDb = application.openOrCreateDatabase(databaseFile.getName(), Context.MODE_PRIVATE, null);
- } else {
- // external storage
- mDb = SQLiteDatabase.openOrCreateDatabase(databaseFile, null);
- }
+ doOpenOrCreateDb(application, databaseFile);
} catch (SQLiteException e) {
// try to gracefully handle DB corruption - see issue 2537
Log.w(K9.LOG_TAG, "Unable to open DB " + databaseFile + " - removing file and retrying", e);
databaseFile.delete();
- if (StorageManager.InternalStorageProvider.ID.equals(mStorageProviderId)) {
- // internal storage
- mDb = application.openOrCreateDatabase(databaseFile.getName(), Context.MODE_PRIVATE, null);
- } else {
- // external storage
- mDb = SQLiteDatabase.openOrCreateDatabase(databaseFile, null);
- }
+ doOpenOrCreateDb(application, databaseFile);
}
if (mDb.getVersion() != mSchemaDefinition.getVersion()) {
mSchemaDefinition.doDbUpgrade(mDb);
@@ -403,6 +391,17 @@ public class LockableDatabase {
}
}
+ private void doOpenOrCreateDb(final Application application, final File databaseFile) {
+ if (StorageManager.InternalStorageProvider.ID.equals(mStorageProviderId)) {
+ // internal storage
+ mDb = application.openOrCreateDatabase(databaseFile.getName(), Context.MODE_PRIVATE,
+ null);
+ } else {
+ // external storage
+ mDb = SQLiteDatabase.openOrCreateDatabase(databaseFile, null);
+ }
+ }
+
/**
* @param providerId
* Never null
.
@@ -412,10 +411,8 @@ public class LockableDatabase {
protected File prepareStorage(final String providerId) throws UnavailableStorageException {
final StorageManager storageManager = getStorageManager();
- final File databaseFile;
- final File databaseParentDir;
- databaseFile = storageManager.getDatabase(uUid, providerId);
- databaseParentDir = databaseFile.getParentFile();
+ final File databaseFile = storageManager.getDatabase(uUid, providerId);
+ final File databaseParentDir = databaseFile.getParentFile();
if (databaseParentDir.isFile()) {
// should be safe to unconditionally delete clashing file: user is not supposed to mess with our directory
databaseParentDir.delete();
@@ -428,11 +425,8 @@ public class LockableDatabase {
Utility.touchFile(databaseParentDir, ".nomedia");
}
- final File attachmentDir;
- final File attachmentParentDir;
- attachmentDir = storageManager
- .getAttachmentDirectory(uUid, providerId);
- attachmentParentDir = attachmentDir.getParentFile();
+ final File attachmentDir = storageManager.getAttachmentDirectory(uUid, providerId);
+ final File attachmentParentDir = attachmentDir.getParentFile();
if (!attachmentParentDir.exists()) {
attachmentParentDir.mkdirs();
Utility.touchFile(attachmentParentDir, ".nomedia");
@@ -467,7 +461,8 @@ public class LockableDatabase {
try {
mDb.close();
} catch (Exception e) {
-
+ if (K9.DEBUG)
+ Log.d(K9.LOG_TAG, "Exception caught in DB close: " + e.getMessage());
}
final StorageManager storageManager = getStorageManager();
try {
@@ -482,6 +477,8 @@ public class LockableDatabase {
attachmentDirectory.delete();
}
} catch (Exception e) {
+ if (K9.DEBUG)
+ Log.d(K9.LOG_TAG, "Exception caught in clearing attachments: " + e.getMessage());
}
try {
deleteDatabase(storageManager.getDatabase(uUid, mStorageProviderId));
diff --git a/src/com/fsck/k9/mail/store/local/AttachmentMessageBodyUtil.java b/src/com/fsck/k9/mail/store/local/AttachmentMessageBodyUtil.java
new file mode 100644
index 000000000..61cb8456a
--- /dev/null
+++ b/src/com/fsck/k9/mail/store/local/AttachmentMessageBodyUtil.java
@@ -0,0 +1,34 @@
+package com.fsck.k9.mail.store.local;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.james.mime4j.util.MimeUtil;
+
+import com.fsck.k9.mail.MessagingException;
+import com.fsck.k9.mail.internet.MimeMessage;
+
+public class AttachmentMessageBodyUtil {
+ public static void writeTo(BinaryAttachmentBody body, OutputStream out) throws IOException,
+ MessagingException {
+ InputStream in = body.getInputStream();
+ try {
+ if (MimeUtil.ENC_7BIT.equalsIgnoreCase(body.getEncoding())) {
+ /*
+ * If we knew the message was already 7bit clean, then it
+ * could be sent along without processing. But since we
+ * don't know, we recursively parse it.
+ */
+ MimeMessage message = new MimeMessage(in, true);
+ message.setUsing7bitTransport();
+ message.writeTo(out);
+ } else {
+ IOUtils.copy(in, out);
+ }
+ } finally {
+ in.close();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/com/fsck/k9/mail/store/local/BinaryAttachmentBody.java b/src/com/fsck/k9/mail/store/local/BinaryAttachmentBody.java
new file mode 100644
index 000000000..c03fceb0d
--- /dev/null
+++ b/src/com/fsck/k9/mail/store/local/BinaryAttachmentBody.java
@@ -0,0 +1,54 @@
+package com.fsck.k9.mail.store.local;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.james.mime4j.codec.QuotedPrintableOutputStream;
+import org.apache.james.mime4j.util.MimeUtil;
+
+import com.fsck.k9.mail.Body;
+import com.fsck.k9.mail.MessagingException;
+import com.fsck.k9.mail.filter.Base64OutputStream;
+
+public abstract class BinaryAttachmentBody implements Body {
+ protected String mEncoding;
+
+ @Override
+ public abstract InputStream getInputStream() throws MessagingException;
+
+ @Override
+ public void writeTo(OutputStream out) throws IOException, MessagingException {
+ InputStream in = getInputStream();
+ try {
+ boolean closeStream = false;
+ if (MimeUtil.isBase64Encoding(mEncoding)) {
+ out = new Base64OutputStream(out);
+ closeStream = true;
+ } else if (MimeUtil.isQuotedPrintableEncoded(mEncoding)){
+ out = new QuotedPrintableOutputStream(out, false);
+ closeStream = true;
+ }
+
+ try {
+ IOUtils.copy(in, out);
+ } finally {
+ if (closeStream) {
+ out.close();
+ }
+ }
+ } finally {
+ in.close();
+ }
+ }
+
+ @Override
+ public void setEncoding(String encoding) throws MessagingException {
+ mEncoding = encoding;
+ }
+
+ public String getEncoding() {
+ return mEncoding;
+ }
+}
\ No newline at end of file
diff --git a/src/com/fsck/k9/mail/store/local/LocalAttachmentBody.java b/src/com/fsck/k9/mail/store/local/LocalAttachmentBody.java
new file mode 100644
index 000000000..34772be95
--- /dev/null
+++ b/src/com/fsck/k9/mail/store/local/LocalAttachmentBody.java
@@ -0,0 +1,37 @@
+package com.fsck.k9.mail.store.local;
+
+import java.io.ByteArrayInputStream;
+import java.io.FileNotFoundException;
+import java.io.InputStream;
+
+import android.app.Application;
+import android.net.Uri;
+
+import com.fsck.k9.mail.MessagingException;
+
+public class LocalAttachmentBody extends BinaryAttachmentBody {
+ private Application mApplication;
+ private Uri mUri;
+
+ public LocalAttachmentBody(Uri uri, Application application) {
+ mApplication = application;
+ mUri = uri;
+ }
+
+ @Override
+ public InputStream getInputStream() throws MessagingException {
+ try {
+ return mApplication.getContentResolver().openInputStream(mUri);
+ } catch (FileNotFoundException fnfe) {
+ /*
+ * Since it's completely normal for us to try to serve up attachments that
+ * have been blown away, we just return an empty stream.
+ */
+ return new ByteArrayInputStream(LocalStore.EMPTY_BYTE_ARRAY);
+ }
+ }
+
+ public Uri getContentUri() {
+ return mUri;
+ }
+}
\ No newline at end of file
diff --git a/src/com/fsck/k9/mail/store/local/LocalAttachmentBodyPart.java b/src/com/fsck/k9/mail/store/local/LocalAttachmentBodyPart.java
new file mode 100644
index 000000000..9fcdf71b3
--- /dev/null
+++ b/src/com/fsck/k9/mail/store/local/LocalAttachmentBodyPart.java
@@ -0,0 +1,31 @@
+package com.fsck.k9.mail.store.local;
+
+import com.fsck.k9.mail.Body;
+import com.fsck.k9.mail.MessagingException;
+import com.fsck.k9.mail.internet.MimeBodyPart;
+
+public class LocalAttachmentBodyPart extends MimeBodyPart {
+ private long mAttachmentId = -1;
+
+ public LocalAttachmentBodyPart(Body body, long attachmentId) throws MessagingException {
+ super(body);
+ mAttachmentId = attachmentId;
+ }
+
+ /**
+ * Returns the local attachment id of this body, or -1 if it is not stored.
+ * @return
+ */
+ public long getAttachmentId() {
+ return mAttachmentId;
+ }
+
+ public void setAttachmentId(long attachmentId) {
+ mAttachmentId = attachmentId;
+ }
+
+ @Override
+ public String toString() {
+ return "" + mAttachmentId;
+ }
+}
\ No newline at end of file
diff --git a/src/com/fsck/k9/mail/store/local/LocalAttachmentMessageBody.java b/src/com/fsck/k9/mail/store/local/LocalAttachmentMessageBody.java
new file mode 100644
index 000000000..5c451cf15
--- /dev/null
+++ b/src/com/fsck/k9/mail/store/local/LocalAttachmentMessageBody.java
@@ -0,0 +1,49 @@
+package com.fsck.k9.mail.store.local;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+import org.apache.james.mime4j.util.MimeUtil;
+
+import android.app.Application;
+import android.net.Uri;
+
+import com.fsck.k9.mail.CompositeBody;
+import com.fsck.k9.mail.MessagingException;
+
+/**
+ * A {@link LocalAttachmentBody} extension containing a message/rfc822 type body
+ *
+ */
+public class LocalAttachmentMessageBody extends LocalAttachmentBody implements CompositeBody {
+
+ public LocalAttachmentMessageBody(Uri uri, Application application) {
+ super(uri, application);
+ }
+
+ @Override
+ public void writeTo(OutputStream out) throws IOException, MessagingException {
+ AttachmentMessageBodyUtil.writeTo(this, out);
+ }
+
+ @Override
+ public void setUsing7bitTransport() throws MessagingException {
+ /*
+ * There's nothing to recurse into here, so there's nothing to do.
+ * The enclosing BodyPart already called setEncoding(MimeUtil.ENC_7BIT). Once
+ * writeTo() is called, the file with the rfc822 body will be opened
+ * for reading and will then be recursed.
+ */
+
+ }
+
+ @Override
+ public void setEncoding(String encoding) throws MessagingException {
+ if (!MimeUtil.ENC_7BIT.equalsIgnoreCase(encoding)
+ && !MimeUtil.ENC_8BIT.equalsIgnoreCase(encoding)) {
+ throw new MessagingException(
+ "Incompatible content-transfer-encoding applied to a CompositeBody");
+ }
+ mEncoding = encoding;
+ }
+}
\ No newline at end of file
diff --git a/src/com/fsck/k9/mail/store/local/LocalFolder.java b/src/com/fsck/k9/mail/store/local/LocalFolder.java
new file mode 100644
index 000000000..009d630ae
--- /dev/null
+++ b/src/com/fsck/k9/mail/store/local/LocalFolder.java
@@ -0,0 +1,2197 @@
+package com.fsck.k9.mail.store.local;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+import java.util.regex.Pattern;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.james.mime4j.util.MimeUtil;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.net.Uri;
+import android.util.Log;
+
+import com.fsck.k9.K9;
+import com.fsck.k9.Account.MessageFormat;
+import com.fsck.k9.activity.Search;
+import com.fsck.k9.controller.MessageRemovalListener;
+import com.fsck.k9.controller.MessageRetrievalListener;
+import com.fsck.k9.helper.HtmlConverter;
+import com.fsck.k9.helper.Utility;
+import com.fsck.k9.mail.Address;
+import com.fsck.k9.mail.Body;
+import com.fsck.k9.mail.BodyPart;
+import com.fsck.k9.mail.FetchProfile;
+import com.fsck.k9.mail.Flag;
+import com.fsck.k9.mail.Folder;
+import com.fsck.k9.mail.Message;
+import com.fsck.k9.mail.MessagingException;
+import com.fsck.k9.mail.Part;
+import com.fsck.k9.mail.Message.RecipientType;
+import com.fsck.k9.mail.internet.MimeBodyPart;
+import com.fsck.k9.mail.internet.MimeHeader;
+import com.fsck.k9.mail.internet.MimeMessage;
+import com.fsck.k9.mail.internet.MimeMultipart;
+import com.fsck.k9.mail.internet.MimeUtility;
+import com.fsck.k9.mail.internet.TextBody;
+import com.fsck.k9.mail.internet.MimeUtility.ViewableContainer;
+import com.fsck.k9.mail.store.StorageManager;
+import com.fsck.k9.mail.store.UnavailableStorageException;
+import com.fsck.k9.mail.store.LockableDatabase.DbCallback;
+import com.fsck.k9.mail.store.LockableDatabase.WrappedException;
+import com.fsck.k9.provider.AttachmentProvider;
+
+public class LocalFolder extends Folder implements Serializable {
+
+ private static final long serialVersionUID = -1973296520918624767L;
+
+ private final LocalStore localStore;
+
+ private String mName = null;
+ private long mFolderId = -1;
+ private int mVisibleLimit = -1;
+ private String prefId = null;
+ private FolderClass mDisplayClass = FolderClass.NO_CLASS;
+ private FolderClass mSyncClass = FolderClass.INHERITED;
+ private FolderClass mPushClass = FolderClass.SECOND_CLASS;
+ private FolderClass mNotifyClass = FolderClass.INHERITED;
+ private boolean mInTopGroup = false;
+ private String mPushState = null;
+ private boolean mIntegrate = false;
+ // mLastUid is used during syncs. It holds the highest UID within the local folder so we
+ // know whether or not an unread message added to the local folder is actually "new" or not.
+ private Integer mLastUid = null;
+
+ public LocalFolder(LocalStore localStore, String name) {
+ super(localStore.getAccount());
+ this.localStore = localStore;
+ this.mName = name;
+
+ if (this.localStore.getAccount().getInboxFolderName().equals(getName())) {
+
+ mSyncClass = FolderClass.FIRST_CLASS;
+ mPushClass = FolderClass.FIRST_CLASS;
+ mInTopGroup = true;
+ }
+
+
+ }
+
+ public LocalFolder(LocalStore localStore, long id) {
+ super(localStore.getAccount());
+ this.localStore = localStore;
+ this.mFolderId = id;
+ }
+
+ public long getId() {
+ return mFolderId;
+ }
+
+ @Override
+ public void open(final int mode) throws MessagingException {
+
+ if (isOpen() && (getMode() == mode || mode == OPEN_MODE_RO)) {
+ return;
+ } else if (isOpen()) {
+ //previously opened in READ_ONLY and now requesting READ_WRITE
+ //so close connection and reopen
+ close();
+ }
+
+ try {
+ this.localStore.database.execute(false, new DbCallback() {
+ @Override
+ public Void doDbWork(final SQLiteDatabase db) throws WrappedException {
+ Cursor cursor = null;
+ try {
+ String baseQuery = "SELECT " + LocalStore.GET_FOLDER_COLS + " FROM folders ";
+
+ if (mName != null) {
+ cursor = db.rawQuery(baseQuery + "where folders.name = ?", new String[] { mName });
+ } else {
+ cursor = db.rawQuery(baseQuery + "where folders.id = ?", new String[] { Long.toString(mFolderId) });
+ }
+
+ if (cursor.moveToFirst() && !cursor.isNull(LocalStore.FOLDER_ID_INDEX)) {
+ int folderId = cursor.getInt(LocalStore.FOLDER_ID_INDEX);
+ if (folderId > 0) {
+ open(cursor);
+ }
+ } else {
+ Log.w(K9.LOG_TAG, "Creating folder " + getName() + " with existing id " + getId());
+ create(FolderType.HOLDS_MESSAGES);
+ open(mode);
+ }
+ } catch (MessagingException e) {
+ throw new WrappedException(e);
+ } finally {
+ Utility.closeQuietly(cursor);
+ }
+ return null;
+ }
+ });
+ } catch (WrappedException e) {
+ throw(MessagingException) e.getCause();
+ }
+ }
+
+ void open(Cursor cursor) throws MessagingException {
+ mFolderId = cursor.getInt(LocalStore.FOLDER_ID_INDEX);
+ mName = cursor.getString(LocalStore.FOLDER_NAME_INDEX);
+ mVisibleLimit = cursor.getInt(LocalStore.FOLDER_VISIBLE_LIMIT_INDEX);
+ mPushState = cursor.getString(LocalStore.FOLDER_PUSH_STATE_INDEX);
+ super.setStatus(cursor.getString(LocalStore.FOLDER_STATUS_INDEX));
+ // Only want to set the local variable stored in the super class. This class
+ // does a DB update on setLastChecked
+ super.setLastChecked(cursor.getLong(LocalStore.FOLDER_LAST_CHECKED_INDEX));
+ super.setLastPush(cursor.getLong(LocalStore.FOLDER_LAST_PUSHED_INDEX));
+ mInTopGroup = (cursor.getInt(LocalStore.FOLDER_TOP_GROUP_INDEX)) == 1 ? true : false;
+ mIntegrate = (cursor.getInt(LocalStore.FOLDER_INTEGRATE_INDEX) == 1) ? true : false;
+ String noClass = FolderClass.NO_CLASS.toString();
+ String displayClass = cursor.getString(LocalStore.FOLDER_DISPLAY_CLASS_INDEX);
+ mDisplayClass = Folder.FolderClass.valueOf((displayClass == null) ? noClass : displayClass);
+ String notifyClass = cursor.getString(LocalStore.FOLDER_NOTIFY_CLASS_INDEX);
+ mNotifyClass = Folder.FolderClass.valueOf((notifyClass == null) ? noClass : notifyClass);
+ String pushClass = cursor.getString(LocalStore.FOLDER_PUSH_CLASS_INDEX);
+ mPushClass = Folder.FolderClass.valueOf((pushClass == null) ? noClass : pushClass);
+ String syncClass = cursor.getString(LocalStore.FOLDER_SYNC_CLASS_INDEX);
+ mSyncClass = Folder.FolderClass.valueOf((syncClass == null) ? noClass : syncClass);
+ }
+
+ @Override
+ public boolean isOpen() {
+ return (mFolderId != -1 && mName != null);
+ }
+
+ @Override
+ public int getMode() {
+ return OPEN_MODE_RW;
+ }
+
+ @Override
+ public String getName() {
+ return mName;
+ }
+
+ @Override
+ public boolean exists() throws MessagingException {
+ return this.localStore.database.execute(false, new DbCallback() {
+ @Override
+ public Boolean doDbWork(final SQLiteDatabase db) throws WrappedException {
+ Cursor cursor = null;
+ try {
+ cursor = db.rawQuery("SELECT id FROM folders "
+ + "where folders.name = ?", new String[] { LocalFolder.
+ this.getName()
+ });
+ if (cursor.moveToFirst()) {
+ int folderId = cursor.getInt(0);
+ return (folderId > 0);
+ }
+
+ return false;
+ } finally {
+ Utility.closeQuietly(cursor);
+ }
+ }
+ });
+ }
+
+ @Override
+ public boolean create(FolderType type) throws MessagingException {
+ return create(type, mAccount.getDisplayCount());
+ }
+
+ @Override
+ public boolean create(FolderType type, final int visibleLimit) throws MessagingException {
+ if (exists()) {
+ throw new MessagingException("Folder " + mName + " already exists.");
+ }
+ List foldersToCreate = new ArrayList(1);
+ foldersToCreate.add(this);
+ this.localStore.createFolders(foldersToCreate, visibleLimit);
+
+ return true;
+ }
+
+ class PreferencesHolder {
+ FolderClass displayClass = mDisplayClass;
+ FolderClass syncClass = mSyncClass;
+ FolderClass notifyClass = mNotifyClass;
+ FolderClass pushClass = mPushClass;
+ boolean inTopGroup = mInTopGroup;
+ boolean integrate = mIntegrate;
+ }
+
+ @Override
+ public void close() {
+ mFolderId = -1;
+ }
+
+ @Override
+ public int getMessageCount() throws MessagingException {
+ try {
+ return this.localStore.database.execute(false, new DbCallback() {
+ @Override
+ public Integer doDbWork(final SQLiteDatabase db) throws WrappedException {
+ try {
+ open(OPEN_MODE_RW);
+ } catch (MessagingException e) {
+ throw new WrappedException(e);
+ }
+ Cursor cursor = null;
+ try {
+ cursor = db.rawQuery("SELECT COUNT(id) FROM messages WHERE (empty IS NULL OR empty != 1) AND deleted = 0 and folder_id = ?",
+ new String[] {
+ Long.toString(mFolderId)
+ });
+ cursor.moveToFirst();
+ return cursor.getInt(0); //messagecount
+ } finally {
+ Utility.closeQuietly(cursor);
+ }
+ }
+ });
+ } catch (WrappedException e) {
+ throw(MessagingException) e.getCause();
+ }
+ }
+
+ @Override
+ public int getUnreadMessageCount() throws MessagingException {
+ if (mFolderId == -1) {
+ open(OPEN_MODE_RW);
+ }
+
+ try {
+ return this.localStore.database.execute(false, new DbCallback() {
+ @Override
+ public Integer doDbWork(final SQLiteDatabase db) throws WrappedException {
+ int unreadMessageCount = 0;
+ Cursor cursor = db.query("messages", new String[] { "COUNT(id)" },
+ "folder_id = ? AND (empty IS NULL OR empty != 1) AND deleted = 0 AND read=0",
+ new String[] { Long.toString(mFolderId) }, null, null, null);
+
+ try {
+ if (cursor.moveToFirst()) {
+ unreadMessageCount = cursor.getInt(0);
+ }
+ } finally {
+ cursor.close();
+ }
+
+ return unreadMessageCount;
+ }
+ });
+ } catch (WrappedException e) {
+ throw(MessagingException) e.getCause();
+ }
+ }
+
+ @Override
+ public int getFlaggedMessageCount() throws MessagingException {
+ if (mFolderId == -1) {
+ open(OPEN_MODE_RW);
+ }
+
+ try {
+ return this.localStore.database.execute(false, new DbCallback() {
+ @Override
+ public Integer doDbWork(final SQLiteDatabase db) throws WrappedException {
+ int flaggedMessageCount = 0;
+ Cursor cursor = db.query("messages", new String[] { "COUNT(id)" },
+ "folder_id = ? AND (empty IS NULL OR empty != 1) AND deleted = 0 AND flagged = 1",
+ new String[] { Long.toString(mFolderId) }, null, null, null);
+
+ try {
+ if (cursor.moveToFirst()) {
+ flaggedMessageCount = cursor.getInt(0);
+ }
+ } finally {
+ cursor.close();
+ }
+
+ return flaggedMessageCount;
+ }
+ });
+ } catch (WrappedException e) {
+ throw(MessagingException) e.getCause();
+ }
+ }
+
+ @Override
+ public void setLastChecked(final long lastChecked) throws MessagingException {
+ try {
+ open(OPEN_MODE_RW);
+ LocalFolder.super.setLastChecked(lastChecked);
+ } catch (MessagingException e) {
+ throw new WrappedException(e);
+ }
+ updateFolderColumn("last_updated", lastChecked);
+ }
+
+ @Override
+ public void setLastPush(final long lastChecked) throws MessagingException {
+ try {
+ open(OPEN_MODE_RW);
+ LocalFolder.super.setLastPush(lastChecked);
+ } catch (MessagingException e) {
+ throw new WrappedException(e);
+ }
+ updateFolderColumn("last_pushed", lastChecked);
+ }
+
+ public int getVisibleLimit() throws MessagingException {
+ open(OPEN_MODE_RW);
+ return mVisibleLimit;
+ }
+
+ public void purgeToVisibleLimit(MessageRemovalListener listener) throws MessagingException {
+ //don't purge messages while a Search is active since it might throw away search results
+ if (!Search.isActive()) {
+ if (mVisibleLimit == 0) {
+ return ;
+ }
+ open(OPEN_MODE_RW);
+ Message[] messages = getMessages(null, false);
+ for (int i = mVisibleLimit; i < messages.length; i++) {
+ if (listener != null) {
+ listener.messageRemoved(messages[i]);
+ }
+ messages[i].destroy();
+ }
+ }
+ }
+
+
+ public void setVisibleLimit(final int visibleLimit) throws MessagingException {
+ mVisibleLimit = visibleLimit;
+ updateFolderColumn("visible_limit", mVisibleLimit);
+ }
+
+ @Override
+ public void setStatus(final String status) throws MessagingException {
+ updateFolderColumn("status", status);
+ }
+ public void setPushState(final String pushState) throws MessagingException {
+ mPushState = pushState;
+ updateFolderColumn("push_state", pushState);
+ }
+
+ private void updateFolderColumn(final String column, final Object value) throws MessagingException {
+ try {
+ this.localStore.database.execute(false, new DbCallback() {
+ @Override
+ public Void doDbWork(final SQLiteDatabase db) throws WrappedException {
+ try {
+ open(OPEN_MODE_RW);
+ } catch (MessagingException e) {
+ throw new WrappedException(e);
+ }
+ db.execSQL("UPDATE folders SET " + column + " = ? WHERE id = ?", new Object[] { value, mFolderId });
+ return null;
+ }
+ });
+ } catch (WrappedException e) {
+ throw(MessagingException) e.getCause();
+ }
+ }
+
+ public String getPushState() {
+ return mPushState;
+ }
+
+ @Override
+ public FolderClass getDisplayClass() {
+ return mDisplayClass;
+ }
+
+ @Override
+ public FolderClass getSyncClass() {
+ return (FolderClass.INHERITED == mSyncClass) ? getDisplayClass() : mSyncClass;
+ }
+
+ public FolderClass getRawSyncClass() {
+ return mSyncClass;
+ }
+
+ public FolderClass getNotifyClass() {
+ return (FolderClass.INHERITED == mNotifyClass) ? getPushClass() : mNotifyClass;
+ }
+
+ public FolderClass getRawNotifyClass() {
+ return mNotifyClass;
+ }
+
+ @Override
+ public FolderClass getPushClass() {
+ return (FolderClass.INHERITED == mPushClass) ? getSyncClass() : mPushClass;
+ }
+
+ public FolderClass getRawPushClass() {
+ return mPushClass;
+ }
+
+ public void setDisplayClass(FolderClass displayClass) throws MessagingException {
+ mDisplayClass = displayClass;
+ updateFolderColumn("display_class", mDisplayClass.name());
+
+ }
+
+ public void setSyncClass(FolderClass syncClass) throws MessagingException {
+ mSyncClass = syncClass;
+ updateFolderColumn("poll_class", mSyncClass.name());
+ }
+
+ public void setPushClass(FolderClass pushClass) throws MessagingException {
+ mPushClass = pushClass;
+ updateFolderColumn("push_class", mPushClass.name());
+ }
+
+ public void setNotifyClass(FolderClass notifyClass) throws MessagingException {
+ mNotifyClass = notifyClass;
+ updateFolderColumn("notify_class", mNotifyClass.name());
+ }
+
+ public boolean isIntegrate() {
+ return mIntegrate;
+ }
+
+ public void setIntegrate(boolean integrate) throws MessagingException {
+ mIntegrate = integrate;
+ updateFolderColumn("integrate", mIntegrate ? 1 : 0);
+ }
+
+ private String getPrefId(String name) {
+ if (prefId == null) {
+ prefId = this.localStore.uUid + "." + name;
+ }
+
+ return prefId;
+ }
+
+ private String getPrefId() throws MessagingException {
+ open(OPEN_MODE_RW);
+ return getPrefId(mName);
+
+ }
+
+ public void delete() throws MessagingException {
+ String id = getPrefId();
+
+ SharedPreferences.Editor editor = this.localStore.getPreferences().edit();
+
+ editor.remove(id + ".displayMode");
+ editor.remove(id + ".syncMode");
+ editor.remove(id + ".pushMode");
+ editor.remove(id + ".inTopGroup");
+ editor.remove(id + ".integrate");
+
+ editor.commit();
+ }
+
+ public void save() throws MessagingException {
+ SharedPreferences.Editor editor = this.localStore.getPreferences().edit();
+ save(editor);
+ editor.commit();
+ }
+
+ public void save(SharedPreferences.Editor editor) throws MessagingException {
+ String id = getPrefId();
+
+ // there can be a lot of folders. For the defaults, let's not save prefs, saving space, except for INBOX
+ if (mDisplayClass == FolderClass.NO_CLASS && !mAccount.getInboxFolderName().equals(getName())) {
+ editor.remove(id + ".displayMode");
+ } else {
+ editor.putString(id + ".displayMode", mDisplayClass.name());
+ }
+
+ if (mSyncClass == FolderClass.INHERITED && !mAccount.getInboxFolderName().equals(getName())) {
+ editor.remove(id + ".syncMode");
+ } else {
+ editor.putString(id + ".syncMode", mSyncClass.name());
+ }
+
+ if (mNotifyClass == FolderClass.INHERITED && !mAccount.getInboxFolderName().equals(getName())) {
+ editor.remove(id + ".notifyMode");
+ } else {
+ editor.putString(id + ".notifyMode", mNotifyClass.name());
+ }
+
+ if (mPushClass == FolderClass.SECOND_CLASS && !mAccount.getInboxFolderName().equals(getName())) {
+ editor.remove(id + ".pushMode");
+ } else {
+ editor.putString(id + ".pushMode", mPushClass.name());
+ }
+ editor.putBoolean(id + ".inTopGroup", mInTopGroup);
+
+ editor.putBoolean(id + ".integrate", mIntegrate);
+
+ }
+
+ public void refresh(String name, PreferencesHolder prefHolder) {
+ String id = getPrefId(name);
+
+ SharedPreferences preferences = this.localStore.getPreferences();
+
+ try {
+ prefHolder.displayClass = FolderClass.valueOf(preferences.getString(id + ".displayMode",
+ prefHolder.displayClass.name()));
+ } catch (Exception e) {
+ Log.e(K9.LOG_TAG, "Unable to load displayMode for " + getName(), e);
+ }
+ if (prefHolder.displayClass == FolderClass.NONE) {
+ prefHolder.displayClass = FolderClass.NO_CLASS;
+ }
+
+ try {
+ prefHolder.syncClass = FolderClass.valueOf(preferences.getString(id + ".syncMode",
+ prefHolder.syncClass.name()));
+ } catch (Exception e) {
+ Log.e(K9.LOG_TAG, "Unable to load syncMode for " + getName(), e);
+
+ }
+ if (prefHolder.syncClass == FolderClass.NONE) {
+ prefHolder.syncClass = FolderClass.INHERITED;
+ }
+
+ try {
+ prefHolder.notifyClass = FolderClass.valueOf(preferences.getString(id + ".notifyMode",
+ prefHolder.notifyClass.name()));
+ } catch (Exception e) {
+ Log.e(K9.LOG_TAG, "Unable to load notifyMode for " + getName(), e);
+ }
+ if (prefHolder.notifyClass == FolderClass.NONE) {
+ prefHolder.notifyClass = FolderClass.INHERITED;
+ }
+
+ try {
+ prefHolder.pushClass = FolderClass.valueOf(preferences.getString(id + ".pushMode",
+ prefHolder.pushClass.name()));
+ } catch (Exception e) {
+ Log.e(K9.LOG_TAG, "Unable to load pushMode for " + getName(), e);
+ }
+ if (prefHolder.pushClass == FolderClass.NONE) {
+ prefHolder.pushClass = FolderClass.INHERITED;
+ }
+ prefHolder.inTopGroup = preferences.getBoolean(id + ".inTopGroup", prefHolder.inTopGroup);
+ prefHolder.integrate = preferences.getBoolean(id + ".integrate", prefHolder.integrate);
+
+ }
+
+ @Override
+ public void fetch(final Message[] messages, final FetchProfile fp, final MessageRetrievalListener listener)
+ throws MessagingException {
+ try {
+ this.localStore.database.execute(false, new DbCallback() {
+ @Override
+ public Void doDbWork(final SQLiteDatabase db) throws WrappedException {
+ try {
+ open(OPEN_MODE_RW);
+ if (fp.contains(FetchProfile.Item.BODY)) {
+ for (Message message : messages) {
+ LocalMessage localMessage = (LocalMessage)message;
+ Cursor cursor = null;
+ MimeMultipart mp = new MimeMultipart();
+ mp.setSubType("mixed");
+ try {
+ cursor = db.rawQuery("SELECT html_content, text_content, mime_type FROM messages "
+ + "WHERE id = ?",
+ new String[] { Long.toString(localMessage.mId) });
+ cursor.moveToNext();
+ String htmlContent = cursor.getString(0);
+ String textContent = cursor.getString(1);
+ String mimeType = cursor.getString(2);
+ if (mimeType != null && mimeType.toLowerCase(Locale.US).startsWith("multipart/")) {
+ // If this is a multipart message, preserve both text
+ // and html parts, as well as the subtype.
+ mp.setSubType(mimeType.toLowerCase(Locale.US).replaceFirst("^multipart/", ""));
+ if (textContent != null) {
+ LocalTextBody body = new LocalTextBody(textContent, htmlContent);
+ MimeBodyPart bp = new MimeBodyPart(body, "text/plain");
+ mp.addBodyPart(bp);
+ }
+
+ if (mAccount.getMessageFormat() != MessageFormat.TEXT) {
+ if (htmlContent != null) {
+ TextBody body = new TextBody(htmlContent);
+ MimeBodyPart bp = new MimeBodyPart(body, "text/html");
+ mp.addBodyPart(bp);
+ }
+
+ // If we have both text and html content and our MIME type
+ // isn't multipart/alternative, then corral them into a new
+ // multipart/alternative part and put that into the parent.
+ // If it turns out that this is the only part in the parent
+ // MimeMultipart, it'll get fixed below before we attach to
+ // the message.
+ if (textContent != null && htmlContent != null && !mimeType.equalsIgnoreCase("multipart/alternative")) {
+ MimeMultipart alternativeParts = mp;
+ alternativeParts.setSubType("alternative");
+ mp = new MimeMultipart();
+ mp.addBodyPart(new MimeBodyPart(alternativeParts));
+ }
+ }
+ } else if (mimeType != null && mimeType.equalsIgnoreCase("text/plain")) {
+ // If it's text, add only the plain part. The MIME
+ // container will drop away below.
+ if (textContent != null) {
+ LocalTextBody body = new LocalTextBody(textContent, htmlContent);
+ MimeBodyPart bp = new MimeBodyPart(body, "text/plain");
+ mp.addBodyPart(bp);
+ }
+ } else if (mimeType != null && mimeType.equalsIgnoreCase("text/html")) {
+ // If it's html, add only the html part. The MIME
+ // container will drop away below.
+ if (htmlContent != null) {
+ TextBody body = new TextBody(htmlContent);
+ MimeBodyPart bp = new MimeBodyPart(body, "text/html");
+ mp.addBodyPart(bp);
+ }
+ } else {
+ // MIME type not set. Grab whatever part we can get,
+ // with Text taking precedence. This preserves pre-HTML
+ // composition behaviour.
+ if (textContent != null) {
+ LocalTextBody body = new LocalTextBody(textContent, htmlContent);
+ MimeBodyPart bp = new MimeBodyPart(body, "text/plain");
+ mp.addBodyPart(bp);
+ } else if (htmlContent != null) {
+ TextBody body = new TextBody(htmlContent);
+ MimeBodyPart bp = new MimeBodyPart(body, "text/html");
+ mp.addBodyPart(bp);
+ }
+ }
+
+ } catch (Exception e) {
+ Log.e(K9.LOG_TAG, "Exception fetching message:", e);
+ } finally {
+ Utility.closeQuietly(cursor);
+ }
+
+ try {
+ cursor = db.query(
+ "attachments",
+ new String[] {
+ "id",
+ "size",
+ "name",
+ "mime_type",
+ "store_data",
+ "content_uri",
+ "content_id",
+ "content_disposition"
+ },
+ "message_id = ?",
+ new String[] { Long.toString(localMessage.mId) },
+ null,
+ null,
+ null);
+
+ while (cursor.moveToNext()) {
+ long id = cursor.getLong(0);
+ int size = cursor.getInt(1);
+ String name = cursor.getString(2);
+ String type = cursor.getString(3);
+ String storeData = cursor.getString(4);
+ String contentUri = cursor.getString(5);
+ String contentId = cursor.getString(6);
+ String contentDisposition = cursor.getString(7);
+ String encoding = MimeUtility.getEncodingforType(type);
+ Body body = null;
+
+ if (contentDisposition == null) {
+ contentDisposition = "attachment";
+ }
+
+ if (contentUri != null) {
+ if (MimeUtil.isMessage(type)) {
+ body = new LocalAttachmentMessageBody(
+ Uri.parse(contentUri),
+ LocalFolder.this.localStore.mApplication);
+ } else {
+ body = new LocalAttachmentBody(
+ Uri.parse(contentUri),
+ LocalFolder.this.localStore.mApplication);
+ }
+ }
+
+ MimeBodyPart bp = new LocalAttachmentBodyPart(body, id);
+ bp.setEncoding(encoding);
+ if (name != null) {
+ bp.setHeader(MimeHeader.HEADER_CONTENT_TYPE,
+ String.format("%s;\r\n name=\"%s\"",
+ type,
+ name));
+ bp.setHeader(MimeHeader.HEADER_CONTENT_DISPOSITION,
+ String.format(Locale.US, "%s;\r\n filename=\"%s\";\r\n size=%d",
+ contentDisposition,
+ name, // TODO: Should use encoded word defined in RFC 2231.
+ size));
+ } else {
+ bp.setHeader(MimeHeader.HEADER_CONTENT_TYPE, type);
+ bp.setHeader(MimeHeader.HEADER_CONTENT_DISPOSITION,
+ String.format(Locale.US, "%s;\r\n size=%d",
+ contentDisposition,
+ size));
+ }
+
+ bp.setHeader(MimeHeader.HEADER_CONTENT_ID, contentId);
+ /*
+ * HEADER_ANDROID_ATTACHMENT_STORE_DATA is a custom header we add to that
+ * we can later pull the attachment from the remote store if necessary.
+ */
+ bp.setHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA, storeData);
+
+ mp.addBodyPart(bp);
+ }
+ } finally {
+ Utility.closeQuietly(cursor);
+ }
+
+ if (mp.getCount() == 0) {
+ // If we have no body, remove the container and create a
+ // dummy plain text body. This check helps prevents us from
+ // triggering T_MIME_NO_TEXT and T_TVD_MIME_NO_HEADERS
+ // SpamAssassin rules.
+ localMessage.setHeader(MimeHeader.HEADER_CONTENT_TYPE, "text/plain");
+ localMessage.setBody(new TextBody(""));
+ } else if (mp.getCount() == 1 && (mp.getBodyPart(0) instanceof LocalAttachmentBodyPart) == false)
+
+ {
+ // If we have only one part, drop the MimeMultipart container.
+ BodyPart part = mp.getBodyPart(0);
+ localMessage.setHeader(MimeHeader.HEADER_CONTENT_TYPE, part.getContentType());
+ localMessage.setBody(part.getBody());
+ } else {
+ // Otherwise, attach the MimeMultipart to the message.
+ localMessage.setBody(mp);
+ }
+ }
+ }
+ } catch (MessagingException e) {
+ throw new WrappedException(e);
+ }
+ return null;
+ }
+ });
+ } catch (WrappedException e) {
+ throw(MessagingException) e.getCause();
+ }
+ }
+
+ @Override
+ public Message[] getMessages(int start, int end, Date earliestDate, MessageRetrievalListener listener)
+ throws MessagingException {
+ open(OPEN_MODE_RW);
+ throw new MessagingException(
+ "LocalStore.getMessages(int, int, MessageRetrievalListener) not yet implemented");
+ }
+
+ /**
+ * Populate the header fields of the given list of messages by reading
+ * the saved header data from the database.
+ *
+ * @param messages
+ * The messages whose headers should be loaded.
+ * @throws UnavailableStorageException
+ */
+ void populateHeaders(final List messages) throws UnavailableStorageException {
+ this.localStore.database.execute(false, new DbCallback() {
+ @Override
+ public Void doDbWork(final SQLiteDatabase db) throws WrappedException, UnavailableStorageException {
+ Cursor cursor = null;
+ if (messages.isEmpty()) {
+ return null;
+ }
+ try {
+ Map popMessages = new HashMap();
+ List ids = new ArrayList();
+ StringBuilder questions = new StringBuilder();
+
+ for (int i = 0; i < messages.size(); i++) {
+ if (i != 0) {
+ questions.append(", ");
+ }
+ questions.append("?");
+ LocalMessage message = messages.get(i);
+ Long id = message.getId();
+ ids.add(Long.toString(id));
+ popMessages.put(id, message);
+
+ }
+
+ cursor = db.rawQuery(
+ "SELECT message_id, name, value FROM headers " + "WHERE message_id in ( " + questions + ") ORDER BY id ASC",
+ ids.toArray(LocalStore.EMPTY_STRING_ARRAY));
+
+
+ while (cursor.moveToNext()) {
+ Long id = cursor.getLong(0);
+ String name = cursor.getString(1);
+ String value = cursor.getString(2);
+ //Log.i(K9.LOG_TAG, "Retrieved header name= " + name + ", value = " + value + " for message " + id);
+ popMessages.get(id).addHeader(name, value);
+ }
+ } finally {
+ Utility.closeQuietly(cursor);
+ }
+ return null;
+ }
+ });
+ }
+
+ public String getMessageUidById(final long id) throws MessagingException {
+ try {
+ return this.localStore.database.execute(false, new DbCallback() {
+ @Override
+ public String doDbWork(final SQLiteDatabase db) throws WrappedException, UnavailableStorageException {
+ try {
+ open(OPEN_MODE_RW);
+ Cursor cursor = null;
+
+ try {
+ cursor = db.rawQuery(
+ "SELECT uid FROM messages " +
+ "WHERE id = ? AND folder_id = ?",
+ new String[] {
+ Long.toString(id), Long.toString(mFolderId)
+ });
+ if (!cursor.moveToNext()) {
+ return null;
+ }
+ return cursor.getString(0);
+ } finally {
+ Utility.closeQuietly(cursor);
+ }
+ } catch (MessagingException e) {
+ throw new WrappedException(e);
+ }
+ }
+ });
+ } catch (WrappedException e) {
+ throw(MessagingException) e.getCause();
+ }
+ }
+
+ @Override
+ public LocalMessage getMessage(final String uid) throws MessagingException {
+ try {
+ return this.localStore.database.execute(false, new DbCallback() {
+ @Override
+ public LocalMessage doDbWork(final SQLiteDatabase db) throws WrappedException, UnavailableStorageException {
+ try {
+ open(OPEN_MODE_RW);
+ LocalMessage message = new LocalMessage(LocalFolder.this.localStore, uid, LocalFolder.this);
+ Cursor cursor = null;
+
+ try {
+ cursor = db.rawQuery(
+ "SELECT " +
+ LocalStore.GET_MESSAGES_COLS +
+ "FROM messages " +
+ "LEFT JOIN threads ON (threads.message_id = messages.id) " +
+ "WHERE uid = ? AND folder_id = ?",
+ new String[] {
+ message.getUid(), Long.toString(mFolderId)
+ });
+ if (!cursor.moveToNext()) {
+ return null;
+ }
+ message.populateFromGetMessageCursor(cursor);
+ } finally {
+ Utility.closeQuietly(cursor);
+ }
+ return message;
+ } catch (MessagingException e) {
+ throw new WrappedException(e);
+ }
+ }
+ });
+ } catch (WrappedException e) {
+ throw(MessagingException) e.getCause();
+ }
+ }
+
+ @Override
+ public Message[] getMessages(MessageRetrievalListener listener) throws MessagingException {
+ return getMessages(listener, true);
+ }
+
+ @Override
+ public Message[] getMessages(final MessageRetrievalListener listener, final boolean includeDeleted) throws MessagingException {
+ try {
+ return this.localStore.database.execute(false, new DbCallback() {
+ @Override
+ public Message[] doDbWork(final SQLiteDatabase db) throws WrappedException, UnavailableStorageException {
+ try {
+ open(OPEN_MODE_RW);
+ return LocalFolder.this.localStore.getMessages(
+ listener,
+ LocalFolder.this,
+ "SELECT " + LocalStore.GET_MESSAGES_COLS +
+ "FROM messages " +
+ "LEFT JOIN threads ON (threads.message_id = messages.id) " +
+ "WHERE (empty IS NULL OR empty != 1) AND " +
+ (includeDeleted ? "" : "deleted = 0 AND ") +
+ "folder_id = ? ORDER BY date DESC",
+ new String[] { Long.toString(mFolderId) }
+ );
+ } catch (MessagingException e) {
+ throw new WrappedException(e);
+ }
+ }
+ });
+ } catch (WrappedException e) {
+ throw(MessagingException) e.getCause();
+ }
+ }
+
+ @Override
+ public Message[] getMessages(String[] uids, MessageRetrievalListener listener)
+ throws MessagingException {
+ open(OPEN_MODE_RW);
+ if (uids == null) {
+ return getMessages(listener);
+ }
+ ArrayList messages = new ArrayList();
+ for (String uid : uids) {
+ Message message = getMessage(uid);
+ if (message != null) {
+ messages.add(message);
+ }
+ }
+ return messages.toArray(LocalStore.EMPTY_MESSAGE_ARRAY);
+ }
+
+ @Override
+ public Map copyMessages(Message[] msgs, Folder folder) throws MessagingException {
+ if (!(folder instanceof LocalFolder)) {
+ throw new MessagingException("copyMessages called with incorrect Folder");
+ }
+ return ((LocalFolder) folder).appendMessages(msgs, true);
+ }
+
+ @Override
+ public Map moveMessages(final Message[] msgs, final Folder destFolder) throws MessagingException {
+ if (!(destFolder instanceof LocalFolder)) {
+ throw new MessagingException("moveMessages called with non-LocalFolder");
+ }
+
+ final LocalFolder lDestFolder = (LocalFolder)destFolder;
+
+ final Map uidMap = new HashMap();
+
+ try {
+ this.localStore.database.execute(false, new DbCallback() {
+ @Override
+ public Void doDbWork(final SQLiteDatabase db) throws WrappedException, UnavailableStorageException {
+ try {
+ lDestFolder.open(OPEN_MODE_RW);
+ for (Message message : msgs) {
+ LocalMessage lMessage = (LocalMessage)message;
+
+ String oldUID = message.getUid();
+
+ if (K9.DEBUG) {
+ Log.d(K9.LOG_TAG, "Updating folder_id to " + lDestFolder.getId() + " for message with UID "
+ + message.getUid() + ", id " + lMessage.getId() + " currently in folder " + getName());
+ }
+
+ String newUid = K9.LOCAL_UID_PREFIX + UUID.randomUUID().toString();
+ message.setUid(newUid);
+
+ uidMap.put(oldUID, newUid);
+
+ // Message threading in the target folder
+ ThreadInfo threadInfo = lDestFolder.doMessageThreading(db, message);
+
+ /*
+ * "Move" the message into the new folder
+ */
+ long msgId = lMessage.getId();
+ String[] idArg = new String[] { Long.toString(msgId) };
+
+ ContentValues cv = new ContentValues();
+ cv.put("folder_id", lDestFolder.getId());
+ cv.put("uid", newUid);
+
+ db.update("messages", cv, "id = ?", idArg);
+
+ // Create/update entry in 'threads' table for the message in the
+ // target folder
+ cv.clear();
+ cv.put("message_id", msgId);
+ if (threadInfo.threadId == -1) {
+ if (threadInfo.rootId != -1) {
+ cv.put("root", threadInfo.rootId);
+ }
+
+ if (threadInfo.parentId != -1) {
+ cv.put("parent", threadInfo.parentId);
+ }
+
+ db.insert("threads", null, cv);
+ } else {
+ db.update("threads", cv, "id = ?",
+ new String[] { Long.toString(threadInfo.threadId) });
+ }
+
+ /*
+ * Add a placeholder message so we won't download the original
+ * message again if we synchronize before the remote move is
+ * complete.
+ */
+
+ // We need to open this folder to get the folder id
+ open(OPEN_MODE_RW);
+
+ cv.clear();
+ cv.put("uid", oldUID);
+ cv.putNull("flags");
+ cv.put("read", 1);
+ cv.put("deleted", 1);
+ cv.put("folder_id", mFolderId);
+ cv.put("empty", 0);
+
+ String messageId = message.getMessageId();
+ if (messageId != null) {
+ cv.put("message_id", messageId);
+ }
+
+ final long newId;
+ if (threadInfo.msgId != -1) {
+ // There already existed an empty message in the target folder.
+ // Let's use it as placeholder.
+
+ newId = threadInfo.msgId;
+
+ db.update("messages", cv, "id = ?",
+ new String[] { Long.toString(newId) });
+ } else {
+ newId = db.insert("messages", null, cv);
+ }
+
+ /*
+ * Update old entry in 'threads' table to point to the newly
+ * created placeholder.
+ */
+
+ cv.clear();
+ cv.put("message_id", newId);
+ db.update("threads", cv, "id = ?",
+ new String[] { Long.toString(lMessage.getThreadId()) });
+ }
+ } catch (MessagingException e) {
+ throw new WrappedException(e);
+ }
+ return null;
+ }
+ });
+
+ this.localStore.notifyChange();
+
+ return uidMap;
+ } catch (WrappedException e) {
+ throw(MessagingException) e.getCause();
+ }
+
+ }
+
+ /**
+ * Convenience transaction wrapper for storing a message and set it as fully downloaded. Implemented mainly to speed up DB transaction commit.
+ *
+ * @param message Message to store. Never null
.
+ * @param runnable What to do before setting {@link Flag#X_DOWNLOADED_FULL}. Never null
.
+ * @return The local version of the message. Never null
.
+ * @throws MessagingException
+ */
+ public Message storeSmallMessage(final Message message, final Runnable runnable) throws MessagingException {
+ return this.localStore.database.execute(true, new DbCallback() {
+ @Override
+ public Message doDbWork(final SQLiteDatabase db) throws WrappedException, UnavailableStorageException {
+ try {
+ appendMessages(new Message[] { message });
+ final String uid = message.getUid();
+ final Message result = getMessage(uid);
+ runnable.run();
+ // Set a flag indicating this message has now be fully downloaded
+ result.setFlag(Flag.X_DOWNLOADED_FULL, true);
+ return result;
+ } catch (MessagingException e) {
+ throw new WrappedException(e);
+ }
+ }
+ });
+ }
+
+ /**
+ * The method differs slightly from the contract; If an incoming message already has a uid
+ * assigned and it matches the uid of an existing message then this message will replace the
+ * old message. It is implemented as a delete/insert. This functionality is used in saving
+ * of drafts and re-synchronization of updated server messages.
+ *
+ * NOTE that although this method is located in the LocalStore class, it is not guaranteed
+ * that the messages supplied as parameters are actually {@link LocalMessage} instances (in
+ * fact, in most cases, they are not). Therefore, if you want to make local changes only to a
+ * message, retrieve the appropriate local message instance first (if it already exists).
+ */
+ @Override
+ public Map appendMessages(Message[] messages) throws MessagingException {
+ return appendMessages(messages, false);
+ }
+
+ public void destroyMessages(final Message[] messages) {
+ try {
+ this.localStore.database.execute(true, new DbCallback() {
+ @Override
+ public Void doDbWork(final SQLiteDatabase db) throws WrappedException, UnavailableStorageException {
+ for (Message message : messages) {
+ try {
+ message.destroy();
+ } catch (MessagingException e) {
+ throw new WrappedException(e);
+ }
+ }
+ return null;
+ }
+ });
+ } catch (MessagingException e) {
+ throw new WrappedException(e);
+ }
+ }
+
+ private ThreadInfo getThreadInfo(SQLiteDatabase db, String messageId, boolean onlyEmpty) {
+ String sql = "SELECT t.id, t.message_id, t.root, t.parent " +
+ "FROM messages m " +
+ "LEFT JOIN threads t ON (t.message_id = m.id) " +
+ "WHERE m.folder_id = ? AND m.message_id = ? " +
+ ((onlyEmpty) ? "AND m.empty = 1 " : "") +
+ "ORDER BY m.id LIMIT 1";
+ String[] selectionArgs = { Long.toString(mFolderId), messageId };
+ Cursor cursor = db.rawQuery(sql, selectionArgs);
+
+ if (cursor != null) {
+ try {
+ if (cursor.getCount() > 0) {
+ cursor.moveToFirst();
+ long threadId = cursor.getLong(0);
+ long msgId = cursor.getLong(1);
+ long rootId = (cursor.isNull(2)) ? -1 : cursor.getLong(2);
+ long parentId = (cursor.isNull(3)) ? -1 : cursor.getLong(3);
+
+ return new ThreadInfo(threadId, msgId, messageId, rootId, parentId);
+ }
+ } finally {
+ cursor.close();
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * The method differs slightly from the contract; If an incoming message already has a uid
+ * assigned and it matches the uid of an existing message then this message will replace
+ * the old message. This functionality is used in saving of drafts and re-synchronization
+ * of updated server messages.
+ *
+ * NOTE that although this method is located in the LocalStore class, it is not guaranteed
+ * that the messages supplied as parameters are actually {@link LocalMessage} instances (in
+ * fact, in most cases, they are not). Therefore, if you want to make local changes only to a
+ * message, retrieve the appropriate local message instance first (if it already exists).
+ * @param messages
+ * @param copy
+ * @return Map uidMap of srcUids -> destUids
+ */
+ private Map appendMessages(final Message[] messages, final boolean copy) throws MessagingException {
+ open(OPEN_MODE_RW);
+ try {
+ final Map uidMap = new HashMap();
+ this.localStore.database.execute(true, new DbCallback() {
+ @Override
+ public Void doDbWork(final SQLiteDatabase db) throws WrappedException, UnavailableStorageException {
+ try {
+ for (Message message : messages) {
+ if (!(message instanceof MimeMessage)) {
+ throw new Error("LocalStore can only store Messages that extend MimeMessage");
+ }
+
+ long oldMessageId = -1;
+ String uid = message.getUid();
+ if (uid == null || copy) {
+ /*
+ * Create a new message in the database
+ */
+ String randomLocalUid = K9.LOCAL_UID_PREFIX +
+ UUID.randomUUID().toString();
+
+ if (copy) {
+ // Save mapping: source UID -> target UID
+ uidMap.put(uid, randomLocalUid);
+ } else {
+ // Modify the Message instance to reference the new UID
+ message.setUid(randomLocalUid);
+ }
+
+ // The message will be saved with the newly generated UID
+ uid = randomLocalUid;
+ } else {
+ /*
+ * Replace an existing message in the database
+ */
+ LocalMessage oldMessage = getMessage(uid);
+
+ if (oldMessage != null) {
+ oldMessageId = oldMessage.getId();
+ }
+
+ deleteAttachments(message.getUid());
+ }
+
+ long rootId = -1;
+ long parentId = -1;
+
+ if (oldMessageId == -1) {
+ // This is a new message. Do the message threading.
+ ThreadInfo threadInfo = doMessageThreading(db, message);
+ oldMessageId = threadInfo.msgId;
+ rootId = threadInfo.rootId;
+ parentId = threadInfo.parentId;
+ }
+
+ boolean isDraft = (message.getHeader(K9.IDENTITY_HEADER) != null);
+
+ List attachments;
+ String text;
+ String html;
+ if (isDraft) {
+ // Don't modify the text/plain or text/html part of our own
+ // draft messages because this will cause the values stored in
+ // the identity header to be wrong.
+ ViewableContainer container =
+ MimeUtility.extractPartsFromDraft(message);
+
+ text = container.text;
+ html = container.html;
+ attachments = container.attachments;
+ } else {
+ ViewableContainer container =
+ MimeUtility.extractTextAndAttachments(LocalFolder.this.localStore.mApplication, message);
+
+ attachments = container.attachments;
+ text = container.text;
+ html = HtmlConverter.convertEmoji2Img(container.html);
+ }
+
+ String preview = Message.calculateContentPreview(text);
+
+ try {
+ ContentValues cv = new ContentValues();
+ cv.put("uid", uid);
+ cv.put("subject", message.getSubject());
+ cv.put("sender_list", Address.pack(message.getFrom()));
+ cv.put("date", message.getSentDate() == null
+ ? System.currentTimeMillis() : message.getSentDate().getTime());
+ cv.put("flags", LocalFolder.this.localStore.serializeFlags(message.getFlags()));
+ cv.put("deleted", message.isSet(Flag.DELETED) ? 1 : 0);
+ cv.put("read", message.isSet(Flag.SEEN) ? 1 : 0);
+ cv.put("flagged", message.isSet(Flag.FLAGGED) ? 1 : 0);
+ cv.put("answered", message.isSet(Flag.ANSWERED) ? 1 : 0);
+ cv.put("forwarded", message.isSet(Flag.FORWARDED) ? 1 : 0);
+ cv.put("folder_id", mFolderId);
+ cv.put("to_list", Address.pack(message.getRecipients(RecipientType.TO)));
+ cv.put("cc_list", Address.pack(message.getRecipients(RecipientType.CC)));
+ cv.put("bcc_list", Address.pack(message.getRecipients(RecipientType.BCC)));
+ cv.put("html_content", html.length() > 0 ? html : null);
+ cv.put("text_content", text.length() > 0 ? text : null);
+ cv.put("preview", preview.length() > 0 ? preview : null);
+ cv.put("reply_to_list", Address.pack(message.getReplyTo()));
+ cv.put("attachment_count", attachments.size());
+ cv.put("internal_date", message.getInternalDate() == null
+ ? System.currentTimeMillis() : message.getInternalDate().getTime());
+ cv.put("mime_type", message.getMimeType());
+ cv.put("empty", 0);
+
+ String messageId = message.getMessageId();
+ if (messageId != null) {
+ cv.put("message_id", messageId);
+ }
+
+ long msgId;
+
+ if (oldMessageId == -1) {
+ msgId = db.insert("messages", "uid", cv);
+
+ // Create entry in 'threads' table
+ cv.clear();
+ cv.put("message_id", msgId);
+
+ if (rootId != -1) {
+ cv.put("root", rootId);
+ }
+ if (parentId != -1) {
+ cv.put("parent", parentId);
+ }
+
+ db.insert("threads", null, cv);
+ } else {
+ db.update("messages", cv, "id = ?", new String[] { Long.toString(oldMessageId) });
+ msgId = oldMessageId;
+ }
+
+ for (Part attachment : attachments) {
+ saveAttachment(msgId, attachment, copy);
+ }
+ saveHeaders(msgId, (MimeMessage)message);
+ } catch (Exception e) {
+ throw new MessagingException("Error appending message", e);
+ }
+ }
+ } catch (MessagingException e) {
+ throw new WrappedException(e);
+ }
+ return null;
+ }
+ });
+
+ this.localStore.notifyChange();
+
+ return uidMap;
+ } catch (WrappedException e) {
+ throw(MessagingException) e.getCause();
+ }
+ }
+
+ /**
+ * Update the given message in the LocalStore without first deleting the existing
+ * message (contrast with appendMessages). This method is used to store changes
+ * to the given message while updating attachments and not removing existing
+ * attachment data.
+ * TODO In the future this method should be combined with appendMessages since the Message
+ * contains enough data to decide what to do.
+ * @param message
+ * @throws MessagingException
+ */
+ public void updateMessage(final LocalMessage message) throws MessagingException {
+ open(OPEN_MODE_RW);
+ try {
+ this.localStore.database.execute(false, new DbCallback() {
+ @Override
+ public Void doDbWork(final SQLiteDatabase db) throws WrappedException, UnavailableStorageException {
+ try {
+ message.buildMimeRepresentation();
+
+ ViewableContainer container =
+ MimeUtility.extractTextAndAttachments(LocalFolder.this.localStore.mApplication, message);
+
+ List attachments = container.attachments;
+ String text = container.text;
+ String html = HtmlConverter.convertEmoji2Img(container.html);
+
+ String preview = Message.calculateContentPreview(text);
+
+ try {
+ db.execSQL("UPDATE messages SET "
+ + "uid = ?, subject = ?, sender_list = ?, date = ?, flags = ?, "
+ + "folder_id = ?, to_list = ?, cc_list = ?, bcc_list = ?, "
+ + "html_content = ?, text_content = ?, preview = ?, reply_to_list = ?, "
+ + "attachment_count = ?, read = ?, flagged = ?, answered = ?, forwarded = ? "
+ + "WHERE id = ?",
+ new Object[] {
+ message.getUid(),
+ message.getSubject(),
+ Address.pack(message.getFrom()),
+ message.getSentDate() == null ? System
+ .currentTimeMillis() : message.getSentDate()
+ .getTime(),
+ LocalFolder.this.localStore.serializeFlags(message.getFlags()),
+ mFolderId,
+ Address.pack(message
+ .getRecipients(RecipientType.TO)),
+ Address.pack(message
+ .getRecipients(RecipientType.CC)),
+ Address.pack(message
+ .getRecipients(RecipientType.BCC)),
+ html.length() > 0 ? html : null,
+ text.length() > 0 ? text : null,
+ preview.length() > 0 ? preview : null,
+ Address.pack(message.getReplyTo()),
+ attachments.size(),
+ message.isSet(Flag.SEEN) ? 1 : 0,
+ message.isSet(Flag.FLAGGED) ? 1 : 0,
+ message.isSet(Flag.ANSWERED) ? 1 : 0,
+ message.isSet(Flag.FORWARDED) ? 1 : 0,
+ message.mId
+ });
+
+ for (int i = 0, count = attachments.size(); i < count; i++) {
+ Part attachment = attachments.get(i);
+ saveAttachment(message.mId, attachment, false);
+ }
+ saveHeaders(message.getId(), message);
+ } catch (Exception e) {
+ throw new MessagingException("Error appending message", e);
+ }
+ } catch (MessagingException e) {
+ throw new WrappedException(e);
+ }
+ return null;
+ }
+ });
+ } catch (WrappedException e) {
+ throw(MessagingException) e.getCause();
+ }
+
+ this.localStore.notifyChange();
+ }
+
+ /**
+ * Save the headers of the given message. Note that the message is not
+ * necessarily a {@link LocalMessage} instance.
+ * @param id
+ * @param message
+ * @throws com.fsck.k9.mail.MessagingException
+ */
+ private void saveHeaders(final long id, final MimeMessage message) throws MessagingException {
+ this.localStore.database.execute(true, new DbCallback() {
+ @Override
+ public Void doDbWork(final SQLiteDatabase db) throws WrappedException, UnavailableStorageException {
+
+ deleteHeaders(id);
+ for (String name : message.getHeaderNames()) {
+ String[] values = message.getHeader(name);
+ for (String value : values) {
+ ContentValues cv = new ContentValues();
+ cv.put("message_id", id);
+ cv.put("name", name);
+ cv.put("value", value);
+ db.insert("headers", "name", cv);
+ }
+ }
+
+ // Remember that all headers for this message have been saved, so it is
+ // not necessary to download them again in case the user wants to see all headers.
+ List appendedFlags = new ArrayList();
+ appendedFlags.addAll(Arrays.asList(message.getFlags()));
+ appendedFlags.add(Flag.X_GOT_ALL_HEADERS);
+
+ db.execSQL("UPDATE messages " + "SET flags = ? " + " WHERE id = ?",
+ new Object[]
+ { LocalFolder.this.localStore.serializeFlags(appendedFlags.toArray(LocalStore.EMPTY_FLAG_ARRAY)), id });
+
+ return null;
+ }
+ });
+ }
+
+ void deleteHeaders(final long id) throws UnavailableStorageException {
+ this.localStore.database.execute(false, new DbCallback() {
+ @Override
+ public Void doDbWork(final SQLiteDatabase db) throws WrappedException, UnavailableStorageException {
+ db.execSQL("DELETE FROM headers WHERE message_id = ?", new Object[]
+ { id });
+ return null;
+ }
+ });
+ }
+
+ /**
+ * @param messageId
+ * @param attachment
+ * @param saveAsNew
+ * @throws IOException
+ * @throws MessagingException
+ */
+ private void saveAttachment(final long messageId, final Part attachment, final boolean saveAsNew)
+ throws IOException, MessagingException {
+ try {
+ this.localStore.database.execute(true, new DbCallback() {
+ @Override
+ public Void doDbWork(final SQLiteDatabase db) throws WrappedException, UnavailableStorageException {
+ try {
+ long attachmentId = -1;
+ Uri contentUri = null;
+ int size = -1;
+ File tempAttachmentFile = null;
+
+ if ((!saveAsNew) && (attachment instanceof LocalAttachmentBodyPart)) {
+ attachmentId = ((LocalAttachmentBodyPart) attachment).getAttachmentId();
+ }
+
+ final File attachmentDirectory = StorageManager.getInstance(LocalFolder.this.localStore.mApplication).getAttachmentDirectory(LocalFolder.this.localStore.uUid, LocalFolder.this.localStore.database.getStorageProviderId());
+ if (attachment.getBody() != null) {
+ Body body = attachment.getBody();
+ if (body instanceof LocalAttachmentBody) {
+ contentUri = ((LocalAttachmentBody) body).getContentUri();
+ } else if (body instanceof Message) {
+ // It's a message, so use Message.writeTo() to output the
+ // message including all children.
+ Message message = (Message) body;
+ tempAttachmentFile = File.createTempFile("att", null, attachmentDirectory);
+ FileOutputStream out = new FileOutputStream(tempAttachmentFile);
+ try {
+ message.writeTo(out);
+ } finally {
+ out.close();
+ }
+ size = (int) (tempAttachmentFile.length() & 0x7FFFFFFFL);
+ } else {
+ /*
+ * If the attachment has a body we're expected to save it into the local store
+ * so we copy the data into a cached attachment file.
+ */
+ InputStream in = attachment.getBody().getInputStream();
+ try {
+ tempAttachmentFile = File.createTempFile("att", null, attachmentDirectory);
+ FileOutputStream out = new FileOutputStream(tempAttachmentFile);
+ try {
+ size = IOUtils.copy(in, out);
+ } finally {
+ out.close();
+ }
+ } finally {
+ try { in.close(); } catch (Throwable ignore) {}
+ }
+ }
+ }
+
+ if (size == -1) {
+ /*
+ * If the attachment is not yet downloaded see if we can pull a size
+ * off the Content-Disposition.
+ */
+ String disposition = attachment.getDisposition();
+ if (disposition != null) {
+ String sizeParam = MimeUtility.getHeaderParameter(disposition, "size");
+ if (sizeParam != null) {
+ try {
+ size = Integer.parseInt(sizeParam);
+ } catch (NumberFormatException e) { /* Ignore */ }
+ }
+ }
+ }
+ if (size == -1) {
+ size = 0;
+ }
+
+ String storeData =
+ Utility.combine(attachment.getHeader(
+ MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA), ',');
+
+ String name = MimeUtility.getHeaderParameter(attachment.getContentType(), "name");
+ String contentId = MimeUtility.getHeaderParameter(attachment.getContentId(), null);
+
+ String contentDisposition = MimeUtility.unfoldAndDecode(attachment.getDisposition());
+ String dispositionType = contentDisposition;
+
+ if (dispositionType != null) {
+ int pos = dispositionType.indexOf(';');
+ if (pos != -1) {
+ // extract the disposition-type, "attachment", "inline" or extension-token (see the RFC 2183)
+ dispositionType = dispositionType.substring(0, pos);
+ }
+ }
+
+ if (name == null && contentDisposition != null) {
+ name = MimeUtility.getHeaderParameter(contentDisposition, "filename");
+ }
+ if (attachmentId == -1) {
+ ContentValues cv = new ContentValues();
+ cv.put("message_id", messageId);
+ cv.put("content_uri", contentUri != null ? contentUri.toString() : null);
+ cv.put("store_data", storeData);
+ cv.put("size", size);
+ cv.put("name", name);
+ cv.put("mime_type", attachment.getMimeType());
+ cv.put("content_id", contentId);
+ cv.put("content_disposition", dispositionType);
+
+ attachmentId = db.insert("attachments", "message_id", cv);
+ } else {
+ ContentValues cv = new ContentValues();
+ cv.put("content_uri", contentUri != null ? contentUri.toString() : null);
+ cv.put("size", size);
+ db.update("attachments", cv, "id = ?", new String[]
+ { Long.toString(attachmentId) });
+ }
+
+ if (attachmentId != -1 && tempAttachmentFile != null) {
+ File attachmentFile = new File(attachmentDirectory, Long.toString(attachmentId));
+ tempAttachmentFile.renameTo(attachmentFile);
+ contentUri = AttachmentProvider.getAttachmentUri(
+ mAccount,
+ attachmentId);
+ if (MimeUtil.isMessage(attachment.getMimeType())) {
+ attachment.setBody(new LocalAttachmentMessageBody(
+ contentUri, LocalFolder.this.localStore.mApplication));
+ } else {
+ attachment.setBody(new LocalAttachmentBody(
+ contentUri, LocalFolder.this.localStore.mApplication));
+ }
+ ContentValues cv = new ContentValues();
+ cv.put("content_uri", contentUri != null ? contentUri.toString() : null);
+ db.update("attachments", cv, "id = ?", new String[]
+ { Long.toString(attachmentId) });
+ }
+
+ /* The message has attachment with Content-ID */
+ if (contentId != null && contentUri != null) {
+ Cursor cursor = db.query("messages", new String[]
+ { "html_content" }, "id = ?", new String[]
+ { Long.toString(messageId) }, null, null, null);
+ try {
+ if (cursor.moveToNext()) {
+ String htmlContent = cursor.getString(0);
+
+ if (htmlContent != null) {
+ String newHtmlContent = htmlContent.replaceAll(
+ Pattern.quote("cid:" + contentId),
+ contentUri.toString());
+
+ ContentValues cv = new ContentValues();
+ cv.put("html_content", newHtmlContent);
+ db.update("messages", cv, "id = ?", new String[]
+ { Long.toString(messageId) });
+ }
+ }
+ } finally {
+ Utility.closeQuietly(cursor);
+ }
+ }
+
+ if (attachmentId != -1 && attachment instanceof LocalAttachmentBodyPart) {
+ ((LocalAttachmentBodyPart) attachment).setAttachmentId(attachmentId);
+ }
+ return null;
+ } catch (MessagingException e) {
+ throw new WrappedException(e);
+ } catch (IOException e) {
+ throw new WrappedException(e);
+ }
+ }
+ });
+ } catch (WrappedException e) {
+ final Throwable cause = e.getCause();
+ if (cause instanceof IOException) {
+ throw (IOException) cause;
+ }
+
+ throw (MessagingException) cause;
+ }
+ }
+
+ /**
+ * Changes the stored uid of the given message (using it's internal id as a key) to
+ * the uid in the message.
+ * @param message
+ * @throws com.fsck.k9.mail.MessagingException
+ */
+ public void changeUid(final LocalMessage message) throws MessagingException {
+ open(OPEN_MODE_RW);
+ final ContentValues cv = new ContentValues();
+ cv.put("uid", message.getUid());
+ this.localStore.database.execute(false, new DbCallback() {
+ @Override
+ public Void doDbWork(final SQLiteDatabase db) throws WrappedException, UnavailableStorageException {
+ db.update("messages", cv, "id = ?", new String[]
+ { Long.toString(message.mId) });
+ return null;
+ }
+ });
+
+ //TODO: remove this once the UI code exclusively uses the database id
+ this.localStore.notifyChange();
+ }
+
+ @Override
+ public void setFlags(final Message[] messages, final Flag[] flags, final boolean value)
+ throws MessagingException {
+ open(OPEN_MODE_RW);
+
+ // Use one transaction to set all flags
+ try {
+ this.localStore.database.execute(true, new DbCallback() {
+ @Override
+ public Void doDbWork(final SQLiteDatabase db) throws WrappedException,
+ UnavailableStorageException {
+
+ for (Message message : messages) {
+ try {
+ message.setFlags(flags, value);
+ } catch (MessagingException e) {
+ Log.e(K9.LOG_TAG, "Something went wrong while setting flag", e);
+ }
+ }
+
+ return null;
+ }
+ });
+ } catch (WrappedException e) {
+ throw(MessagingException) e.getCause();
+ }
+ }
+
+ @Override
+ public void setFlags(Flag[] flags, boolean value)
+ throws MessagingException {
+ open(OPEN_MODE_RW);
+ for (Message message : getMessages(null)) {
+ message.setFlags(flags, value);
+ }
+ }
+
+ @Override
+ public String getUidFromMessageId(Message message) throws MessagingException {
+ throw new MessagingException("Cannot call getUidFromMessageId on LocalFolder");
+ }
+
+ public void clearMessagesOlderThan(long cutoff) throws MessagingException {
+ open(OPEN_MODE_RO);
+
+ Message[] messages = this.localStore.getMessages(
+ null,
+ this,
+ "SELECT " + LocalStore.GET_MESSAGES_COLS +
+ "FROM messages " +
+ "LEFT JOIN threads ON (threads.message_id = messages.id) " +
+ "WHERE (empty IS NULL OR empty != 1) AND " +
+ "(folder_id = ? and date < ?)",
+ new String[] {
+ Long.toString(mFolderId), Long.toString(cutoff)
+ });
+
+ for (Message message : messages) {
+ message.destroy();
+ }
+
+ this.localStore.notifyChange();
+ }
+
+ public void clearAllMessages() throws MessagingException {
+ final String[] folderIdArg = new String[] { Long.toString(mFolderId) };
+
+ open(OPEN_MODE_RO);
+
+ try {
+ this.localStore.database.execute(false, new DbCallback() {
+ @Override
+ public Void doDbWork(final SQLiteDatabase db) throws WrappedException {
+ try {
+ // Get UIDs for all messages to delete
+ Cursor cursor = db.query("messages", new String[] { "uid" },
+ "folder_id = ? AND (empty IS NULL OR empty != 1)",
+ folderIdArg, null, null, null);
+
+ try {
+ // Delete attachments of these messages
+ while (cursor.moveToNext()) {
+ deleteAttachments(cursor.getString(0));
+ }
+ } finally {
+ cursor.close();
+ }
+
+ // Delete entries in 'threads' and 'messages'
+ db.execSQL("DELETE FROM threads WHERE message_id IN " +
+ "(SELECT id FROM messages WHERE folder_id = ?)", folderIdArg);
+ db.execSQL("DELETE FROM messages WHERE folder_id = ?", folderIdArg);
+
+ return null;
+ } catch (MessagingException e) {
+ throw new WrappedException(e);
+ }
+ }
+ });
+ } catch (WrappedException e) {
+ throw(MessagingException) e.getCause();
+ }
+
+ this.localStore.notifyChange();
+
+ setPushState(null);
+ setLastPush(0);
+ setLastChecked(0);
+ setVisibleLimit(mAccount.getDisplayCount());
+ }
+
+ @Override
+ public void delete(final boolean recurse) throws MessagingException {
+ try {
+ this.localStore.database.execute(false, new DbCallback() {
+ @Override
+ public Void doDbWork(final SQLiteDatabase db) throws WrappedException, UnavailableStorageException {
+ try {
+ // We need to open the folder first to make sure we've got it's id
+ open(OPEN_MODE_RO);
+ Message[] messages = getMessages(null);
+ for (Message message : messages) {
+ deleteAttachments(message.getUid());
+ }
+ } catch (MessagingException e) {
+ throw new WrappedException(e);
+ }
+ db.execSQL("DELETE FROM folders WHERE id = ?", new Object[]
+ { Long.toString(mFolderId), });
+ return null;
+ }
+ });
+ } catch (WrappedException e) {
+ throw(MessagingException) e.getCause();
+ }
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o instanceof LocalFolder) {
+ return ((LocalFolder)o).mName.equals(mName);
+ }
+ return super.equals(o);
+ }
+
+ @Override
+ public int hashCode() {
+ return mName.hashCode();
+ }
+
+ void deleteAttachments(final long messageId) throws MessagingException {
+ open(OPEN_MODE_RW);
+ this.localStore.database.execute(false, new DbCallback() {
+ @Override
+ public Void doDbWork(final SQLiteDatabase db) throws WrappedException, UnavailableStorageException {
+ Cursor attachmentsCursor = null;
+ try {
+ String accountUuid = mAccount.getUuid();
+ Context context = LocalFolder.this.localStore.mApplication;
+
+ // Get attachment IDs
+ String[] whereArgs = new String[] { Long.toString(messageId) };
+ attachmentsCursor = db.query("attachments", new String[] { "id" },
+ "message_id = ?", whereArgs, null, null, null);
+
+ final File attachmentDirectory = StorageManager.getInstance(LocalFolder.this.localStore.mApplication)
+ .getAttachmentDirectory(LocalFolder.this.localStore.uUid, LocalFolder.this.localStore.database.getStorageProviderId());
+
+ while (attachmentsCursor.moveToNext()) {
+ String attachmentId = Long.toString(attachmentsCursor.getLong(0));
+ try {
+ // Delete stored attachment
+ File file = new File(attachmentDirectory, attachmentId);
+ if (file.exists()) {
+ file.delete();
+ }
+
+ // Delete thumbnail file
+ AttachmentProvider.deleteThumbnail(context, accountUuid,
+ attachmentId);
+ } catch (Exception e) { /* ignore */ }
+ }
+
+ // Delete attachment metadata from the database
+ db.delete("attachments", "message_id = ?", whereArgs);
+ } finally {
+ Utility.closeQuietly(attachmentsCursor);
+ }
+ return null;
+ }
+ });
+ }
+
+ private void deleteAttachments(final String uid) throws MessagingException {
+ open(OPEN_MODE_RW);
+ try {
+ this.localStore.database.execute(false, new DbCallback() {
+ @Override
+ public Void doDbWork(final SQLiteDatabase db) throws WrappedException, UnavailableStorageException {
+ Cursor messagesCursor = null;
+ try {
+ messagesCursor = db.query("messages", new String[]
+ { "id" }, "folder_id = ? AND uid = ?", new String[]
+ { Long.toString(mFolderId), uid }, null, null, null);
+ while (messagesCursor.moveToNext()) {
+ long messageId = messagesCursor.getLong(0);
+ deleteAttachments(messageId);
+
+ }
+ } catch (MessagingException e) {
+ throw new WrappedException(e);
+ } finally {
+ Utility.closeQuietly(messagesCursor);
+ }
+ return null;
+ }
+ });
+ } catch (WrappedException e) {
+ throw(MessagingException) e.getCause();
+ }
+ }
+
+ @Override
+ public boolean isInTopGroup() {
+ return mInTopGroup;
+ }
+
+ public void setInTopGroup(boolean inTopGroup) throws MessagingException {
+ mInTopGroup = inTopGroup;
+ updateFolderColumn("top_group", mInTopGroup ? 1 : 0);
+ }
+
+ public Integer getLastUid() {
+ return mLastUid;
+ }
+
+ /**
+ * Fetches the most recent numeric UID value in this folder. This is used by
+ * {@link com.fsck.k9.controller.MessagingController#shouldNotifyForMessage} to see if messages being
+ * fetched are new and unread. Messages are "new" if they have a UID higher than the most recent UID prior
+ * to synchronization.
+ *
+ * This only works for protocols with numeric UIDs (like IMAP). For protocols with
+ * alphanumeric UIDs (like POP), this method quietly fails and shouldNotifyForMessage() will
+ * always notify for unread messages.
+ *
+ * Once Issue 1072 has been fixed, this method and shouldNotifyForMessage() should be
+ * updated to use internal dates rather than UIDs to determine new-ness. While this doesn't
+ * solve things for POP (which doesn't have internal dates), we can likely use this as a
+ * framework to examine send date in lieu of internal date.
+ * @throws MessagingException
+ */
+ public void updateLastUid() throws MessagingException {
+ Integer lastUid = this.localStore.database.execute(false, new DbCallback() {
+ @Override
+ public Integer doDbWork(final SQLiteDatabase db) {
+ Cursor cursor = null;
+ try {
+ open(OPEN_MODE_RO);
+ cursor = db.rawQuery("SELECT MAX(uid) FROM messages WHERE folder_id=?", new String[] { Long.toString(mFolderId) });
+ if (cursor.getCount() > 0) {
+ cursor.moveToFirst();
+ return cursor.getInt(0);
+ }
+ } catch (Exception e) {
+ Log.e(K9.LOG_TAG, "Unable to updateLastUid: ", e);
+ } finally {
+ Utility.closeQuietly(cursor);
+ }
+ return null;
+ }
+ });
+ if (K9.DEBUG)
+ Log.d(K9.LOG_TAG, "Updated last UID for folder " + mName + " to " + lastUid);
+ mLastUid = lastUid;
+ }
+
+ public Long getOldestMessageDate() throws MessagingException {
+ return this.localStore.database.execute(false, new DbCallback() {
+ @Override
+ public Long doDbWork(final SQLiteDatabase db) {
+ Cursor cursor = null;
+ try {
+ open(OPEN_MODE_RO);
+ cursor = db.rawQuery("SELECT MIN(date) FROM messages WHERE folder_id=?", new String[] { Long.toString(mFolderId) });
+ if (cursor.getCount() > 0) {
+ cursor.moveToFirst();
+ return cursor.getLong(0);
+ }
+ } catch (Exception e) {
+ Log.e(K9.LOG_TAG, "Unable to fetch oldest message date: ", e);
+ } finally {
+ Utility.closeQuietly(cursor);
+ }
+ return null;
+ }
+ });
+ }
+
+ private ThreadInfo doMessageThreading(SQLiteDatabase db, Message message)
+ throws MessagingException {
+ long rootId = -1;
+ long parentId = -1;
+
+ String messageId = message.getMessageId();
+
+ // If there's already an empty message in the database, update that
+ ThreadInfo msgThreadInfo = getThreadInfo(db, messageId, true);
+
+ // Get the message IDs from the "References" header line
+ String[] referencesArray = message.getHeader("References");
+ List messageIds = null;
+ if (referencesArray != null && referencesArray.length > 0) {
+ messageIds = Utility.extractMessageIds(referencesArray[0]);
+ }
+
+ // Append the first message ID from the "In-Reply-To" header line
+ String[] inReplyToArray = message.getHeader("In-Reply-To");
+ String inReplyTo = null;
+ if (inReplyToArray != null && inReplyToArray.length > 0) {
+ inReplyTo = Utility.extractMessageId(inReplyToArray[0]);
+ if (inReplyTo != null) {
+ if (messageIds == null) {
+ messageIds = new ArrayList(1);
+ messageIds.add(inReplyTo);
+ } else if (!messageIds.contains(inReplyTo)) {
+ messageIds.add(inReplyTo);
+ }
+ }
+ }
+
+ if (messageIds == null) {
+ // This is not a reply, nothing to do for us.
+ return (msgThreadInfo != null) ?
+ msgThreadInfo : new ThreadInfo(-1, -1, messageId, -1, -1);
+ }
+
+ for (String reference : messageIds) {
+ ThreadInfo threadInfo = getThreadInfo(db, reference, false);
+
+ if (threadInfo == null) {
+ // Create placeholder message in 'messages' table
+ ContentValues cv = new ContentValues();
+ cv.put("message_id", reference);
+ cv.put("folder_id", mFolderId);
+ cv.put("empty", 1);
+
+ long newMsgId = db.insert("messages", null, cv);
+
+ // Create entry in 'threads' table
+ cv.clear();
+ cv.put("message_id", newMsgId);
+ if (rootId != -1) {
+ cv.put("root", rootId);
+ }
+ if (parentId != -1) {
+ cv.put("parent", parentId);
+ }
+
+ parentId = db.insert("threads", null, cv);
+ if (rootId == -1) {
+ rootId = parentId;
+ }
+ } else {
+ if (rootId != -1 && threadInfo.rootId == -1 && rootId != threadInfo.threadId) {
+ // We found an existing root container that is not
+ // the root of our current path (References).
+ // Connect it to the current parent.
+
+ // Let all children know who's the new root
+ ContentValues cv = new ContentValues();
+ cv.put("root", rootId);
+ db.update("threads", cv, "root = ?",
+ new String[] { Long.toString(threadInfo.threadId) });
+
+ // Connect the message to the current parent
+ cv.put("parent", parentId);
+ db.update("threads", cv, "id = ?",
+ new String[] { Long.toString(threadInfo.threadId) });
+ } else {
+ rootId = (threadInfo.rootId == -1) ?
+ threadInfo.threadId : threadInfo.rootId;
+ }
+ parentId = threadInfo.threadId;
+ }
+ }
+
+ //TODO: set in-reply-to "link" even if one already exists
+
+ long threadId;
+ long msgId;
+ if (msgThreadInfo != null) {
+ threadId = msgThreadInfo.threadId;
+ msgId = msgThreadInfo.msgId;
+ } else {
+ threadId = -1;
+ msgId = -1;
+ }
+
+ return new ThreadInfo(threadId, msgId, messageId, rootId, parentId);
+ }
+
+ public List extractNewMessages(final List messages)
+ throws MessagingException {
+
+ try {
+ return this.localStore.database.execute(false, new DbCallback>() {
+ @Override
+ public List doDbWork(final SQLiteDatabase db) throws WrappedException {
+ try {
+ open(OPEN_MODE_RW);
+ } catch (MessagingException e) {
+ throw new WrappedException(e);
+ }
+
+ List result = new ArrayList();
+
+ List selectionArgs = new ArrayList();
+ Set existingMessages = new HashSet();
+ int start = 0;
+
+ while (start < messages.size()) {
+ StringBuilder selection = new StringBuilder();
+
+ selection.append("folder_id = ? AND UID IN (");
+ selectionArgs.add(Long.toString(mFolderId));
+
+ int count = Math.min(messages.size() - start, LocalStore.UID_CHECK_BATCH_SIZE);
+
+ for (int i = start, end = start + count; i < end; i++) {
+ if (i > start) {
+ selection.append(",?");
+ } else {
+ selection.append("?");
+ }
+
+ selectionArgs.add(messages.get(i).getUid());
+ }
+
+ selection.append(")");
+
+ Cursor cursor = db.query("messages", LocalStore.UID_CHECK_PROJECTION,
+ selection.toString(), selectionArgs.toArray(LocalStore.EMPTY_STRING_ARRAY),
+ null, null, null);
+
+ try {
+ while (cursor.moveToNext()) {
+ String uid = cursor.getString(0);
+ existingMessages.add(uid);
+ }
+ } finally {
+ Utility.closeQuietly(cursor);
+ }
+
+ for (int i = start, end = start + count; i < end; i++) {
+ Message message = messages.get(i);
+ if (!existingMessages.contains(message.getUid())) {
+ result.add(message);
+ }
+ }
+
+ existingMessages.clear();
+ selectionArgs.clear();
+ start += count;
+ }
+
+ return result;
+ }
+ });
+ } catch (WrappedException e) {
+ throw(MessagingException) e.getCause();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/com/fsck/k9/mail/store/local/LocalMessage.java b/src/com/fsck/k9/mail/store/local/LocalMessage.java
new file mode 100644
index 000000000..8f9dda8d3
--- /dev/null
+++ b/src/com/fsck/k9/mail/store/local/LocalMessage.java
@@ -0,0 +1,559 @@
+package com.fsck.k9.mail.store.local;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.Set;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.util.Log;
+
+import com.fsck.k9.K9;
+import com.fsck.k9.mail.Address;
+import com.fsck.k9.mail.Flag;
+import com.fsck.k9.mail.Folder;
+import com.fsck.k9.mail.MessagingException;
+import com.fsck.k9.mail.Part;
+import com.fsck.k9.mail.internet.MimeMessage;
+import com.fsck.k9.mail.internet.MimeUtility;
+import com.fsck.k9.mail.store.UnavailableStorageException;
+import com.fsck.k9.mail.store.LockableDatabase.DbCallback;
+import com.fsck.k9.mail.store.LockableDatabase.WrappedException;
+
+public class LocalMessage extends MimeMessage {
+
+ private final LocalStore localStore;
+
+ long mId;
+ private int mAttachmentCount;
+ private String mSubject;
+
+ private String mPreview = "";
+
+ private boolean mHeadersLoaded = false;
+ private boolean mMessageDirty = false;
+
+ private long mThreadId;
+ private long mRootId;
+
+ public LocalMessage(LocalStore localStore) {
+ this.localStore = localStore;
+ }
+
+ LocalMessage(LocalStore localStore, String uid, Folder folder) {
+ this.localStore = localStore;
+ this.mUid = uid;
+ this.mFolder = folder;
+ }
+
+ void populateFromGetMessageCursor(Cursor cursor)
+ throws MessagingException {
+ final String subject = cursor.getString(0);
+ this.setSubject(subject == null ? "" : subject);
+
+ Address[] from = Address.unpack(cursor.getString(1));
+ if (from.length > 0) {
+ this.setFrom(from[0]);
+ }
+ this.setInternalSentDate(new Date(cursor.getLong(2)));
+ this.setUid(cursor.getString(3));
+ String flagList = cursor.getString(4);
+ if (flagList != null && flagList.length() > 0) {
+ String[] flags = flagList.split(",");
+
+ for (String flag : flags) {
+ try {
+ this.setFlagInternal(Flag.valueOf(flag), true);
+ }
+
+ catch (Exception e) {
+ if (!"X_BAD_FLAG".equals(flag)) {
+ Log.w(K9.LOG_TAG, "Unable to parse flag " + flag);
+ }
+ }
+ }
+ }
+ this.mId = cursor.getLong(5);
+ this.setRecipients(RecipientType.TO, Address.unpack(cursor.getString(6)));
+ this.setRecipients(RecipientType.CC, Address.unpack(cursor.getString(7)));
+ this.setRecipients(RecipientType.BCC, Address.unpack(cursor.getString(8)));
+ this.setReplyTo(Address.unpack(cursor.getString(9)));
+
+ this.mAttachmentCount = cursor.getInt(10);
+ this.setInternalDate(new Date(cursor.getLong(11)));
+ this.setMessageId(cursor.getString(12));
+
+ final String preview = cursor.getString(14);
+ mPreview = (preview == null ? "" : preview);
+
+ if (this.mFolder == null) {
+ LocalFolder f = new LocalFolder(this.localStore, cursor.getInt(13));
+ f.open(LocalFolder.OPEN_MODE_RW);
+ this.mFolder = f;
+ }
+
+ mThreadId = (cursor.isNull(15)) ? -1 : cursor.getLong(15);
+ mRootId = (cursor.isNull(16)) ? -1 : cursor.getLong(16);
+
+ boolean deleted = (cursor.getInt(17) == 1);
+ boolean read = (cursor.getInt(18) == 1);
+ boolean flagged = (cursor.getInt(19) == 1);
+ boolean answered = (cursor.getInt(20) == 1);
+ boolean forwarded = (cursor.getInt(21) == 1);
+
+ setFlagInternal(Flag.DELETED, deleted);
+ setFlagInternal(Flag.SEEN, read);
+ setFlagInternal(Flag.FLAGGED, flagged);
+ setFlagInternal(Flag.ANSWERED, answered);
+ setFlagInternal(Flag.FORWARDED, forwarded);
+ }
+
+ /**
+ * Fetch the message text for display. This always returns an HTML-ified version of the
+ * message, even if it was originally a text-only message.
+ * @return HTML version of message for display purposes or null.
+ * @throws MessagingException
+ */
+ public String getTextForDisplay() throws MessagingException {
+ String text = null; // First try and fetch an HTML part.
+ Part part = MimeUtility.findFirstPartByMimeType(this, "text/html");
+ if (part == null) {
+ // If that fails, try and get a text part.
+ part = MimeUtility.findFirstPartByMimeType(this, "text/plain");
+ if (part != null && part.getBody() instanceof LocalTextBody) {
+ text = ((LocalTextBody) part.getBody()).getBodyForDisplay();
+ }
+ } else {
+ // We successfully found an HTML part; do the necessary character set decoding.
+ text = MimeUtility.getTextFromPart(part);
+ }
+ return text;
+ }
+
+
+ /* Custom version of writeTo that updates the MIME message based on localMessage
+ * changes.
+ */
+
+ @Override
+ public void writeTo(OutputStream out) throws IOException, MessagingException {
+ if (mMessageDirty) buildMimeRepresentation();
+ super.writeTo(out);
+ }
+
+ void buildMimeRepresentation() throws MessagingException {
+ if (!mMessageDirty) {
+ return;
+ }
+
+ super.setSubject(mSubject);
+ if (this.mFrom != null && this.mFrom.length > 0) {
+ super.setFrom(this.mFrom[0]);
+ }
+
+ super.setReplyTo(mReplyTo);
+ super.setSentDate(this.getSentDate());
+ super.setRecipients(RecipientType.TO, mTo);
+ super.setRecipients(RecipientType.CC, mCc);
+ super.setRecipients(RecipientType.BCC, mBcc);
+ if (mMessageId != null) super.setMessageId(mMessageId);
+
+ mMessageDirty = false;
+ }
+
+ @Override
+ public String getPreview() {
+ return mPreview;
+ }
+
+ @Override
+ public String getSubject() {
+ return mSubject;
+ }
+
+
+ @Override
+ public void setSubject(String subject) throws MessagingException {
+ mSubject = subject;
+ mMessageDirty = true;
+ }
+
+
+ @Override
+ public void setMessageId(String messageId) {
+ mMessageId = messageId;
+ mMessageDirty = true;
+ }
+
+ @Override
+ public boolean hasAttachments() {
+ return (mAttachmentCount > 0);
+ }
+
+ public int getAttachmentCount() {
+ return mAttachmentCount;
+ }
+
+ @Override
+ public void setFrom(Address from) throws MessagingException {
+ this.mFrom = new Address[] { from };
+ mMessageDirty = true;
+ }
+
+
+ @Override
+ public void setReplyTo(Address[] replyTo) throws MessagingException {
+ if (replyTo == null || replyTo.length == 0) {
+ mReplyTo = null;
+ } else {
+ mReplyTo = replyTo;
+ }
+ mMessageDirty = true;
+ }
+
+
+ /*
+ * For performance reasons, we add headers instead of setting them (see super implementation)
+ * which removes (expensive) them before adding them
+ */
+ @Override
+ public void setRecipients(RecipientType type, Address[] addresses) throws MessagingException {
+ if (type == RecipientType.TO) {
+ if (addresses == null || addresses.length == 0) {
+ this.mTo = null;
+ } else {
+ this.mTo = addresses;
+ }
+ } else if (type == RecipientType.CC) {
+ if (addresses == null || addresses.length == 0) {
+ this.mCc = null;
+ } else {
+ this.mCc = addresses;
+ }
+ } else if (type == RecipientType.BCC) {
+ if (addresses == null || addresses.length == 0) {
+ this.mBcc = null;
+ } else {
+ this.mBcc = addresses;
+ }
+ } else {
+ throw new MessagingException("Unrecognized recipient type.");
+ }
+ mMessageDirty = true;
+ }
+
+ public void setFlagInternal(Flag flag, boolean set) throws MessagingException {
+ super.setFlag(flag, set);
+ }
+
+ @Override
+ public long getId() {
+ return mId;
+ }
+
+ @Override
+ public void setFlag(final Flag flag, final boolean set) throws MessagingException {
+
+ try {
+ this.localStore.database.execute(true, new DbCallback() {
+ @Override
+ public Void doDbWork(final SQLiteDatabase db) throws WrappedException, UnavailableStorageException {
+ try {
+ if (flag == Flag.DELETED && set) {
+ delete();
+ }
+
+ LocalMessage.super.setFlag(flag, set);
+ } catch (MessagingException e) {
+ throw new WrappedException(e);
+ }
+ /*
+ * Set the flags on the message.
+ */
+ ContentValues cv = new ContentValues();
+ cv.put("flags", LocalMessage.this.localStore.serializeFlags(getFlags()));
+ cv.put("read", isSet(Flag.SEEN) ? 1 : 0);
+ cv.put("flagged", isSet(Flag.FLAGGED) ? 1 : 0);
+ cv.put("answered", isSet(Flag.ANSWERED) ? 1 : 0);
+ cv.put("forwarded", isSet(Flag.FORWARDED) ? 1 : 0);
+
+ db.update("messages", cv, "id = ?", new String[] { Long.toString(mId) });
+
+ return null;
+ }
+ });
+ } catch (WrappedException e) {
+ throw(MessagingException) e.getCause();
+ }
+
+ this.localStore.notifyChange();
+ }
+
+ /*
+ * If a message is being marked as deleted we want to clear out it's content
+ * and attachments as well. Delete will not actually remove the row since we need
+ * to retain the uid for synchronization purposes.
+ */
+ private void delete() throws MessagingException
+
+ {
+ /*
+ * Delete all of the message's content to save space.
+ */
+ try {
+ this.localStore.database.execute(true, new DbCallback() {
+ @Override
+ public Void doDbWork(final SQLiteDatabase db) throws WrappedException,
+ UnavailableStorageException {
+ String[] idArg = new String[] { Long.toString(mId) };
+
+ ContentValues cv = new ContentValues();
+ cv.put("deleted", 1);
+ cv.put("empty", 1);
+ cv.putNull("subject");
+ cv.putNull("sender_list");
+ cv.putNull("date");
+ cv.putNull("to_list");
+ cv.putNull("cc_list");
+ cv.putNull("bcc_list");
+ cv.putNull("preview");
+ cv.putNull("html_content");
+ cv.putNull("text_content");
+ cv.putNull("reply_to_list");
+
+ db.update("messages", cv, "id = ?", idArg);
+
+ /*
+ * Delete all of the message's attachments to save space.
+ * We do this explicit deletion here because we're not deleting the record
+ * in messages, which means our ON DELETE trigger for messages won't cascade
+ */
+ try {
+ ((LocalFolder) mFolder).deleteAttachments(mId);
+ } catch (MessagingException e) {
+ throw new WrappedException(e);
+ }
+
+ db.delete("attachments", "message_id = ?", idArg);
+ return null;
+ }
+ });
+ } catch (WrappedException e) {
+ throw(MessagingException) e.getCause();
+ }
+ ((LocalFolder)mFolder).deleteHeaders(mId);
+
+ this.localStore.notifyChange();
+ }
+
+ /*
+ * Completely remove a message from the local database
+ *
+ * TODO: document how this updates the thread structure
+ */
+ @Override
+ public void destroy() throws MessagingException {
+ try {
+ this.localStore.database.execute(true, new DbCallback() {
+ @Override
+ public Void doDbWork(final SQLiteDatabase db) throws WrappedException,
+ UnavailableStorageException {
+ try {
+ LocalFolder localFolder = (LocalFolder) mFolder;
+
+ localFolder.deleteAttachments(mId);
+
+ if (hasThreadChildren(db, mId)) {
+ // This message has children in the thread structure so we need to
+ // make it an empty message.
+ ContentValues cv = new ContentValues();
+ cv.put("id", mId);
+ cv.put("folder_id", localFolder.getId());
+ cv.put("deleted", 0);
+ cv.put("message_id", getMessageId());
+ cv.put("empty", 1);
+
+ db.replace("messages", null, cv);
+
+ // Nothing else to do
+ return null;
+ }
+
+ // Get the message ID of the parent message if it's empty
+ long currentId = getEmptyThreadParent(db, mId);
+
+ // Delete the placeholder message
+ deleteMessageRow(db, mId);
+
+ /*
+ * Walk the thread tree to delete all empty parents without children
+ */
+
+ while (currentId != -1) {
+ if (hasThreadChildren(db, currentId)) {
+ // We made sure there are no empty leaf nodes and can stop now.
+ break;
+ }
+
+ // Get ID of the (empty) parent for the next iteration
+ long newId = getEmptyThreadParent(db, currentId);
+
+ // Delete the empty message
+ deleteMessageRow(db, currentId);
+
+ currentId = newId;
+ }
+
+ } catch (MessagingException e) {
+ throw new WrappedException(e);
+ }
+ return null;
+ }
+ });
+ } catch (WrappedException e) {
+ throw(MessagingException) e.getCause();
+ }
+
+ this.localStore.notifyChange();
+ }
+
+ /**
+ * Get ID of the the given message's parent if the parent is an empty message.
+ *
+ * @param db
+ * {@link SQLiteDatabase} instance to access the database.
+ * @param messageId
+ * The database ID of the message to get the parent for.
+ *
+ * @return Message ID of the parent message if there exists a parent and it is empty.
+ * Otherwise {@code -1}.
+ */
+ private long getEmptyThreadParent(SQLiteDatabase db, long messageId) {
+ Cursor cursor = db.rawQuery(
+ "SELECT m.id " +
+ "FROM threads t1 " +
+ "JOIN threads t2 ON (t1.parent = t2.id) " +
+ "LEFT JOIN messages m ON (t2.message_id = m.id) " +
+ "WHERE t1.message_id = ? AND m.empty = 1",
+ new String[] { Long.toString(messageId) });
+
+ try {
+ return (cursor.moveToFirst() && !cursor.isNull(0)) ? cursor.getLong(0) : -1;
+ } finally {
+ cursor.close();
+ }
+ }
+
+ /**
+ * Check whether or not a message has child messages in the thread structure.
+ *
+ * @param db
+ * {@link SQLiteDatabase} instance to access the database.
+ * @param messageId
+ * The database ID of the message to get the children for.
+ *
+ * @return {@code true} if the message has children. {@code false} otherwise.
+ */
+ private boolean hasThreadChildren(SQLiteDatabase db, long messageId) {
+ Cursor cursor = db.rawQuery(
+ "SELECT COUNT(t2.id) " +
+ "FROM threads t1 " +
+ "JOIN threads t2 ON (t2.parent = t1.id) " +
+ "WHERE t1.message_id = ?",
+ new String[] { Long.toString(messageId) });
+
+ try {
+ return (cursor.moveToFirst() && !cursor.isNull(0) && cursor.getLong(0) > 0L);
+ } finally {
+ cursor.close();
+ }
+ }
+
+ /**
+ * Delete a message from the 'messages' and 'threads' tables.
+ *
+ * @param db
+ * {@link SQLiteDatabase} instance to access the database.
+ * @param messageId
+ * The database ID of the message to delete.
+ */
+ private void deleteMessageRow(SQLiteDatabase db, long messageId) {
+ String[] idArg = { Long.toString(messageId) };
+
+ // Delete the message
+ db.delete("messages", "id = ?", idArg);
+
+ // Delete row in 'threads' table
+ // TODO: create trigger for 'messages' table to get rid of the row in 'threads' table
+ db.delete("threads", "message_id = ?", idArg);
+ }
+
+ private void loadHeaders() throws UnavailableStorageException {
+ ArrayList messages = new ArrayList();
+ messages.add(this);
+ mHeadersLoaded = true; // set true before calling populate headers to stop recursion
+ ((LocalFolder) mFolder).populateHeaders(messages);
+
+ }
+
+ @Override
+ public void addHeader(String name, String value) throws UnavailableStorageException {
+ if (!mHeadersLoaded)
+ loadHeaders();
+ super.addHeader(name, value);
+ }
+
+ @Override
+ public void setHeader(String name, String value) throws UnavailableStorageException {
+ if (!mHeadersLoaded)
+ loadHeaders();
+ super.setHeader(name, value);
+ }
+
+ @Override
+ public String[] getHeader(String name) throws UnavailableStorageException {
+ if (!mHeadersLoaded)
+ loadHeaders();
+ return super.getHeader(name);
+ }
+
+ @Override
+ public void removeHeader(String name) throws UnavailableStorageException {
+ if (!mHeadersLoaded)
+ loadHeaders();
+ super.removeHeader(name);
+ }
+
+ @Override
+ public Set getHeaderNames() throws UnavailableStorageException {
+ if (!mHeadersLoaded)
+ loadHeaders();
+ return super.getHeaderNames();
+ }
+
+ @Override
+ public LocalMessage clone() {
+ LocalMessage message = new LocalMessage(this.localStore);
+ super.copy(message);
+
+ message.mId = mId;
+ message.mAttachmentCount = mAttachmentCount;
+ message.mSubject = mSubject;
+ message.mPreview = mPreview;
+ message.mHeadersLoaded = mHeadersLoaded;
+ message.mMessageDirty = mMessageDirty;
+
+ return message;
+ }
+
+ public long getThreadId() {
+ return mThreadId;
+ }
+
+ public long getRootId() {
+ return mRootId;
+ }
+}
\ No newline at end of file
diff --git a/src/com/fsck/k9/mail/store/local/LocalStore.java b/src/com/fsck/k9/mail/store/local/LocalStore.java
new file mode 100644
index 000000000..3442aa988
--- /dev/null
+++ b/src/com/fsck/k9/mail/store/local/LocalStore.java
@@ -0,0 +1,1046 @@
+
+package com.fsck.k9.mail.store.local;
+
+import java.io.File;
+import java.io.Serializable;
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+
+import android.app.Application;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.net.Uri;
+import android.util.Log;
+
+import com.fsck.k9.Account;
+import com.fsck.k9.K9;
+import com.fsck.k9.Preferences;
+import com.fsck.k9.controller.MessageRetrievalListener;
+import com.fsck.k9.helper.StringUtils;
+import com.fsck.k9.helper.Utility;
+import com.fsck.k9.mail.Flag;
+import com.fsck.k9.mail.Folder;
+import com.fsck.k9.mail.Message;
+import com.fsck.k9.mail.MessagingException;
+import com.fsck.k9.mail.Store;
+import com.fsck.k9.mail.store.LockableDatabase;
+import com.fsck.k9.mail.store.StorageManager;
+import com.fsck.k9.mail.store.UnavailableStorageException;
+import com.fsck.k9.mail.store.LockableDatabase.DbCallback;
+import com.fsck.k9.mail.store.LockableDatabase.SchemaDefinition;
+import com.fsck.k9.mail.store.LockableDatabase.WrappedException;
+import com.fsck.k9.mail.store.StorageManager.StorageProvider;
+import com.fsck.k9.provider.EmailProvider;
+import com.fsck.k9.provider.EmailProvider.MessageColumns;
+import com.fsck.k9.search.LocalSearch;
+import com.fsck.k9.search.SearchSpecification.Attribute;
+import com.fsck.k9.search.SearchSpecification.Searchfield;
+import com.fsck.k9.search.SqlQueryBuilder;
+
+/**
+ *
+ * Implements a SQLite database backed local store for Messages.
+ *
+ */
+public class LocalStore extends Store implements Serializable {
+
+ private static final long serialVersionUID = -5142141896809423072L;
+
+ static final Message[] EMPTY_MESSAGE_ARRAY = new Message[0];
+ static final String[] EMPTY_STRING_ARRAY = new String[0];
+ static final Flag[] EMPTY_FLAG_ARRAY = new Flag[0];
+ static final byte[] EMPTY_BYTE_ARRAY = new byte[0];
+
+ /*
+ * a String containing the columns getMessages expects to work with
+ * in the correct order.
+ */
+ static String GET_MESSAGES_COLS =
+ "subject, sender_list, date, uid, flags, messages.id, to_list, cc_list, " +
+ "bcc_list, reply_to_list, attachment_count, internal_date, messages.message_id, " +
+ "folder_id, preview, threads.id, threads.root, deleted, read, flagged, answered, " +
+ "forwarded ";
+
+ static final String GET_FOLDER_COLS =
+ "folders.id, name, visible_limit, last_updated, status, push_state, last_pushed, " +
+ "integrate, top_group, poll_class, push_class, display_class, notify_class";
+
+ static final int FOLDER_ID_INDEX = 0;
+ static final int FOLDER_NAME_INDEX = 1;
+ static final int FOLDER_VISIBLE_LIMIT_INDEX = 2;
+ static final int FOLDER_LAST_CHECKED_INDEX = 3;
+ static final int FOLDER_STATUS_INDEX = 4;
+ static final int FOLDER_PUSH_STATE_INDEX = 5;
+ static final int FOLDER_LAST_PUSHED_INDEX = 6;
+ static final int FOLDER_INTEGRATE_INDEX = 7;
+ static final int FOLDER_TOP_GROUP_INDEX = 8;
+ static final int FOLDER_SYNC_CLASS_INDEX = 9;
+ static final int FOLDER_PUSH_CLASS_INDEX = 10;
+ static final int FOLDER_DISPLAY_CLASS_INDEX = 11;
+ static final int FOLDER_NOTIFY_CLASS_INDEX = 12;
+
+ static final String[] UID_CHECK_PROJECTION = { "uid" };
+
+ /**
+ * Maximum number of UIDs to check for existence at once.
+ *
+ * @see LocalFolder#extractNewMessages(List)
+ */
+ static final int UID_CHECK_BATCH_SIZE = 500;
+
+ /**
+ * Maximum number of messages to perform flag updates on at once.
+ *
+ * @see #setFlag(List, Flag, boolean, boolean)
+ */
+ private static final int FLAG_UPDATE_BATCH_SIZE = 500;
+
+ /**
+ * Maximum number of threads to perform flag updates on at once.
+ *
+ * @see #setFlagForThreads(List, Flag, boolean)
+ */
+ private static final int THREAD_FLAG_UPDATE_BATCH_SIZE = 500;
+
+ public static final int DB_VERSION = 50;
+
+
+ public static String getColumnNameForFlag(Flag flag) {
+ switch (flag) {
+ case SEEN: {
+ return MessageColumns.READ;
+ }
+ case FLAGGED: {
+ return MessageColumns.FLAGGED;
+ }
+ case ANSWERED: {
+ return MessageColumns.ANSWERED;
+ }
+ case FORWARDED: {
+ return MessageColumns.FORWARDED;
+ }
+ default: {
+ throw new IllegalArgumentException("Flag must be a special column flag");
+ }
+ }
+ }
+
+
+ protected String uUid = null;
+
+ final Application mApplication;
+
+ LockableDatabase database;
+
+ private ContentResolver mContentResolver;
+
+ /**
+ * local://localhost/path/to/database/uuid.db
+ * This constructor is only used by {@link Store#getLocalInstance(Account, Application)}
+ * @param account
+ * @param application
+ * @throws UnavailableStorageException if not {@link StorageProvider#isReady(Context)}
+ */
+ public LocalStore(final Account account, final Application application) throws MessagingException {
+ super(account);
+ database = new LockableDatabase(application, account.getUuid(), new StoreSchemaDefinition(this));
+
+ mApplication = application;
+ mContentResolver = application.getContentResolver();
+ database.setStorageProviderId(account.getLocalStorageProviderId());
+ uUid = account.getUuid();
+
+ database.open();
+ }
+
+ public void switchLocalStorage(final String newStorageProviderId) throws MessagingException {
+ database.switchProvider(newStorageProviderId);
+ }
+
+ protected SharedPreferences getPreferences() {
+ return Preferences.getPreferences(mApplication).getPreferences();
+ }
+
+ public long getSize() throws UnavailableStorageException {
+
+ final StorageManager storageManager = StorageManager.getInstance(mApplication);
+
+ final File attachmentDirectory = storageManager.getAttachmentDirectory(uUid,
+ database.getStorageProviderId());
+
+ return database.execute(false, new DbCallback() {
+ @Override
+ public Long doDbWork(final SQLiteDatabase db) {
+ final File[] files = attachmentDirectory.listFiles();
+ long attachmentLength = 0;
+ if (files != null) {
+ for (File file : files) {
+ if (file.exists()) {
+ attachmentLength += file.length();
+ }
+ }
+ }
+
+ final File dbFile = storageManager.getDatabase(uUid, database.getStorageProviderId());
+ return dbFile.length() + attachmentLength;
+ }
+ });
+ }
+
+ public void compact() throws MessagingException {
+ if (K9.DEBUG)
+ Log.i(K9.LOG_TAG, "Before compaction size = " + getSize());
+
+ database.execute(false, new DbCallback() {
+ @Override
+ public Void doDbWork(final SQLiteDatabase db) throws WrappedException {
+ db.execSQL("VACUUM");
+ return null;
+ }
+ });
+ if (K9.DEBUG)
+ Log.i(K9.LOG_TAG, "After compaction size = " + getSize());
+ }
+
+
+ public void clear() throws MessagingException {
+ if (K9.DEBUG)
+ Log.i(K9.LOG_TAG, "Before prune size = " + getSize());
+
+ pruneCachedAttachments(true);
+ if (K9.DEBUG) {
+ Log.i(K9.LOG_TAG, "After prune / before compaction size = " + getSize());
+
+ Log.i(K9.LOG_TAG, "Before clear folder count = " + getFolderCount());
+ Log.i(K9.LOG_TAG, "Before clear message count = " + getMessageCount());
+
+ Log.i(K9.LOG_TAG, "After prune / before clear size = " + getSize());
+ }
+ // don't delete messages that are Local, since there is no copy on the server.
+ // Don't delete deleted messages. They are essentially placeholders for UIDs of messages that have
+ // been deleted locally. They take up insignificant space
+ database.execute(false, new DbCallback() {
+ @Override
+ public Void doDbWork(final SQLiteDatabase db) {
+ // Delete entries from 'threads' table
+ db.execSQL("DELETE FROM threads WHERE message_id IN " +
+ "(SELECT id FROM messages WHERE deleted = 0 AND uid NOT LIKE 'Local%')");
+
+ // Set 'root' and 'parent' of remaining entries in 'thread' table to 'NULL' to make
+ // sure the thread structure is in a valid state (this may destroy existing valid
+ // thread trees, but is much faster than adjusting the tree by removing messages
+ // one by one).
+ db.execSQL("UPDATE threads SET root=id, parent=NULL");
+
+ // Delete entries from 'messages' table
+ db.execSQL("DELETE FROM messages WHERE deleted = 0 AND uid NOT LIKE 'Local%'");
+ return null;
+ }
+ });
+
+ compact();
+
+ if (K9.DEBUG) {
+ Log.i(K9.LOG_TAG, "After clear message count = " + getMessageCount());
+
+ Log.i(K9.LOG_TAG, "After clear size = " + getSize());
+ }
+ }
+
+ public int getMessageCount() throws MessagingException {
+ return database.execute(false, new DbCallback() {
+ @Override
+ public Integer doDbWork(final SQLiteDatabase db) {
+ Cursor cursor = null;
+ try {
+ cursor = db.rawQuery("SELECT COUNT(*) FROM messages", null);
+ cursor.moveToFirst();
+ return cursor.getInt(0); // message count
+ } finally {
+ Utility.closeQuietly(cursor);
+ }
+ }
+ });
+ }
+
+ public int getFolderCount() throws MessagingException {
+ return database.execute(false, new DbCallback() {
+ @Override
+ public Integer doDbWork(final SQLiteDatabase db) {
+ Cursor cursor = null;
+ try {
+ cursor = db.rawQuery("SELECT COUNT(*) FROM folders", null);
+ cursor.moveToFirst();
+ return cursor.getInt(0); // folder count
+ } finally {
+ Utility.closeQuietly(cursor);
+ }
+ }
+ });
+ }
+
+ @Override
+ public LocalFolder getFolder(String name) {
+ return new LocalFolder(this, name);
+ }
+
+ public LocalFolder getFolderById(long folderId) {
+ return new LocalFolder(this, folderId);
+ }
+
+ // TODO this takes about 260-300ms, seems slow.
+ @Override
+ public List extends Folder > getPersonalNamespaces(boolean forceListAll) throws MessagingException {
+ final List folders = new LinkedList();
+ try {
+ database.execute(false, new DbCallback < List extends Folder >> () {
+ @Override
+ public List extends Folder > doDbWork(final SQLiteDatabase db) throws WrappedException {
+ Cursor cursor = null;
+
+ try {
+ cursor = db.rawQuery("SELECT " + GET_FOLDER_COLS + " FROM folders " +
+ "ORDER BY name ASC", null);
+ while (cursor.moveToNext()) {
+ if (cursor.isNull(FOLDER_ID_INDEX)) {
+ continue;
+ }
+ String folderName = cursor.getString(FOLDER_NAME_INDEX);
+ LocalFolder folder = new LocalFolder(LocalStore.this, folderName);
+ folder.open(cursor);
+
+ folders.add(folder);
+ }
+ return folders;
+ } catch (MessagingException e) {
+ throw new WrappedException(e);
+ } finally {
+ Utility.closeQuietly(cursor);
+ }
+ }
+ });
+ } catch (WrappedException e) {
+ throw(MessagingException) e.getCause();
+ }
+ return folders;
+ }
+
+ @Override
+ public void checkSettings() throws MessagingException {
+ }
+
+ public void delete() throws UnavailableStorageException {
+ database.delete();
+ }
+
+ public void recreate() throws UnavailableStorageException {
+ database.recreate();
+ }
+
+ public void pruneCachedAttachments() throws MessagingException {
+ pruneCachedAttachments(false);
+ }
+
+ /**
+ * Deletes all cached attachments for the entire store.
+ * @param force
+ * @throws com.fsck.k9.mail.MessagingException
+ */
+ private void pruneCachedAttachments(final boolean force) throws MessagingException {
+ database.execute(false, new DbCallback() {
+ @Override
+ public Void doDbWork(final SQLiteDatabase db) throws WrappedException {
+ if (force) {
+ ContentValues cv = new ContentValues();
+ cv.putNull("content_uri");
+ db.update("attachments", cv, null, null);
+ }
+ final StorageManager storageManager = StorageManager.getInstance(mApplication);
+ File[] files = storageManager.getAttachmentDirectory(uUid, database.getStorageProviderId()).listFiles();
+ for (File file : files) {
+ if (file.exists()) {
+ if (!force) {
+ Cursor cursor = null;
+ try {
+ cursor = db.query(
+ "attachments",
+ new String[] { "store_data" },
+ "id = ?",
+ new String[] { file.getName() },
+ null,
+ null,
+ null);
+ if (cursor.moveToNext()) {
+ if (cursor.getString(0) == null) {
+ if (K9.DEBUG)
+ Log.d(K9.LOG_TAG, "Attachment " + file.getAbsolutePath() + " has no store data, not deleting");
+ /*
+ * If the attachment has no store data it is not recoverable, so
+ * we won't delete it.
+ */
+ continue;
+ }
+ }
+ } finally {
+ Utility.closeQuietly(cursor);
+ }
+ }
+ if (!force) {
+ try {
+ ContentValues cv = new ContentValues();
+ cv.putNull("content_uri");
+ db.update("attachments", cv, "id = ?", new String[] { file.getName() });
+ } catch (Exception e) {
+ /*
+ * If the row has gone away before we got to mark it not-downloaded that's
+ * okay.
+ */
+ }
+ }
+ if (!file.delete()) {
+ file.deleteOnExit();
+ }
+ }
+ }
+ return null;
+ }
+ });
+ }
+
+ public void resetVisibleLimits() throws UnavailableStorageException {
+ resetVisibleLimits(mAccount.getDisplayCount());
+ }
+
+ public void resetVisibleLimits(int visibleLimit) throws UnavailableStorageException {
+ final ContentValues cv = new ContentValues();
+ cv.put("visible_limit", Integer.toString(visibleLimit));
+ database.execute(false, new DbCallback() {
+ @Override
+ public Void doDbWork(final SQLiteDatabase db) throws WrappedException {
+ db.update("folders", cv, null, null);
+ return null;
+ }
+ });
+ }
+
+ public ArrayList getPendingCommands() throws UnavailableStorageException {
+ return database.execute(false, new DbCallback>() {
+ @Override
+ public ArrayList doDbWork(final SQLiteDatabase db) throws WrappedException {
+ Cursor cursor = null;
+ try {
+ cursor = db.query("pending_commands",
+ new String[] { "id", "command", "arguments" },
+ null,
+ null,
+ null,
+ null,
+ "id ASC");
+ ArrayList commands = new ArrayList();
+ while (cursor.moveToNext()) {
+ PendingCommand command = new PendingCommand();
+ command.mId = cursor.getLong(0);
+ command.command = cursor.getString(1);
+ String arguments = cursor.getString(2);
+ command.arguments = arguments.split(",");
+ for (int i = 0; i < command.arguments.length; i++) {
+ command.arguments[i] = Utility.fastUrlDecode(command.arguments[i]);
+ }
+ commands.add(command);
+ }
+ return commands;
+ } finally {
+ Utility.closeQuietly(cursor);
+ }
+ }
+ });
+ }
+
+ public void addPendingCommand(PendingCommand command) throws UnavailableStorageException {
+ try {
+ for (int i = 0; i < command.arguments.length; i++) {
+ command.arguments[i] = URLEncoder.encode(command.arguments[i], "UTF-8");
+ }
+ final ContentValues cv = new ContentValues();
+ cv.put("command", command.command);
+ cv.put("arguments", Utility.combine(command.arguments, ','));
+ database.execute(false, new DbCallback() {
+ @Override
+ public Void doDbWork(final SQLiteDatabase db) throws WrappedException {
+ db.insert("pending_commands", "command", cv);
+ return null;
+ }
+ });
+ } catch (UnsupportedEncodingException uee) {
+ throw new Error("Aparently UTF-8 has been lost to the annals of history.");
+ }
+ }
+
+ public void removePendingCommand(final PendingCommand command) throws UnavailableStorageException {
+ database.execute(false, new DbCallback() {
+ @Override
+ public Void doDbWork(final SQLiteDatabase db) throws WrappedException {
+ db.delete("pending_commands", "id = ?", new String[] { Long.toString(command.mId) });
+ return null;
+ }
+ });
+ }
+
+ public void removePendingCommands() throws UnavailableStorageException {
+ database.execute(false, new DbCallback() {
+ @Override
+ public Void doDbWork(final SQLiteDatabase db) throws WrappedException {
+ db.delete("pending_commands", null, null);
+ return null;
+ }
+ });
+ }
+
+ public static class PendingCommand {
+ private long mId;
+ public String command;
+ public String[] arguments;
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append(command);
+ sb.append(": ");
+ for (String argument : arguments) {
+ sb.append(", ");
+ sb.append(argument);
+ //sb.append("\n");
+ }
+ return sb.toString();
+ }
+ }
+
+ @Override
+ public boolean isMoveCapable() {
+ return true;
+ }
+
+ @Override
+ public boolean isCopyCapable() {
+ return true;
+ }
+
+ public Message[] searchForMessages(MessageRetrievalListener retrievalListener,
+ LocalSearch search) throws MessagingException {
+
+ StringBuilder query = new StringBuilder();
+ List queryArgs = new ArrayList();
+ SqlQueryBuilder.buildWhereClause(mAccount, search.getConditions(), query, queryArgs);
+
+ // Avoid "ambiguous column name" error by prefixing "id" with the message table name
+ String where = SqlQueryBuilder.addPrefixToSelection(new String[] { "id" },
+ "messages.", query.toString());
+
+ String[] selectionArgs = queryArgs.toArray(EMPTY_STRING_ARRAY);
+
+ String sqlQuery = "SELECT " + GET_MESSAGES_COLS + "FROM messages " +
+ "LEFT JOIN threads ON (threads.message_id = messages.id) " +
+ "LEFT JOIN folders ON (folders.id = messages.folder_id) WHERE " +
+ "((empty IS NULL OR empty != 1) AND deleted = 0)" +
+ ((!StringUtils.isNullOrEmpty(where)) ? " AND (" + where + ")" : "") +
+ " ORDER BY date DESC";
+
+ if (K9.DEBUG) {
+ Log.d(K9.LOG_TAG, "Query = " + sqlQuery);
+ }
+
+ return getMessages(retrievalListener, null, sqlQuery, selectionArgs);
+ }
+
+ /*
+ * Given a query string, actually do the query for the messages and
+ * call the MessageRetrievalListener for each one
+ */
+ Message[] getMessages(
+ final MessageRetrievalListener listener,
+ final LocalFolder folder,
+ final String queryString, final String[] placeHolders
+ ) throws MessagingException {
+ final ArrayList messages = new ArrayList();
+ final int j = database.execute(false, new DbCallback() {
+ @Override
+ public Integer doDbWork(final SQLiteDatabase db) throws WrappedException {
+ Cursor cursor = null;
+ int i = 0;
+ try {
+ cursor = db.rawQuery(queryString + " LIMIT 10", placeHolders);
+
+ while (cursor.moveToNext()) {
+ LocalMessage message = new LocalMessage(LocalStore.this, null, folder);
+ message.populateFromGetMessageCursor(cursor);
+
+ messages.add(message);
+ if (listener != null) {
+ listener.messageFinished(message, i, -1);
+ }
+ i++;
+ }
+ cursor.close();
+ cursor = db.rawQuery(queryString + " LIMIT -1 OFFSET 10", placeHolders);
+
+ while (cursor.moveToNext()) {
+ LocalMessage message = new LocalMessage(LocalStore.this, null, folder);
+ message.populateFromGetMessageCursor(cursor);
+
+ messages.add(message);
+ if (listener != null) {
+ listener.messageFinished(message, i, -1);
+ }
+ i++;
+ }
+ } catch (Exception e) {
+ Log.d(K9.LOG_TAG, "Got an exception", e);
+ } finally {
+ Utility.closeQuietly(cursor);
+ }
+ return i;
+ }
+ });
+ if (listener != null) {
+ listener.messagesFinished(j);
+ }
+
+ return messages.toArray(EMPTY_MESSAGE_ARRAY);
+
+ }
+
+ public Message[] getMessagesInThread(final long rootId) throws MessagingException {
+ String rootIdString = Long.toString(rootId);
+
+ LocalSearch search = new LocalSearch();
+ search.and(Searchfield.THREAD_ID, rootIdString, Attribute.EQUALS);
+
+ return searchForMessages(null, search);
+ }
+
+ public AttachmentInfo getAttachmentInfo(final String attachmentId) throws UnavailableStorageException {
+ return database.execute(false, new DbCallback() {
+ @Override
+ public AttachmentInfo doDbWork(final SQLiteDatabase db) throws WrappedException {
+ String name;
+ String type;
+ int size;
+ Cursor cursor = null;
+ try {
+ cursor = db.query(
+ "attachments",
+ new String[] { "name", "size", "mime_type" },
+ "id = ?",
+ new String[] { attachmentId },
+ null,
+ null,
+ null);
+ if (!cursor.moveToFirst()) {
+ return null;
+ }
+ name = cursor.getString(0);
+ size = cursor.getInt(1);
+ type = cursor.getString(2);
+ final AttachmentInfo attachmentInfo = new AttachmentInfo();
+ attachmentInfo.name = name;
+ attachmentInfo.size = size;
+ attachmentInfo.type = type;
+ return attachmentInfo;
+ } finally {
+ Utility.closeQuietly(cursor);
+ }
+ }
+ });
+ }
+
+ public static class AttachmentInfo {
+ public String name;
+ public int size;
+ public String type;
+ }
+
+ public void createFolders(final List foldersToCreate, final int visibleLimit) throws UnavailableStorageException {
+ database.execute(true, new DbCallback() {
+ @Override
+ public Void doDbWork(final SQLiteDatabase db) throws WrappedException {
+ for (LocalFolder folder : foldersToCreate) {
+ String name = folder.getName();
+ final LocalFolder.PreferencesHolder prefHolder = folder.new PreferencesHolder();
+
+ // When created, special folders should always be displayed
+ // inbox should be integrated
+ // and the inbox and drafts folders should be syncced by default
+ if (mAccount.isSpecialFolder(name)) {
+ prefHolder.inTopGroup = true;
+ prefHolder.displayClass = LocalFolder.FolderClass.FIRST_CLASS;
+ if (name.equalsIgnoreCase(mAccount.getInboxFolderName())) {
+ prefHolder.integrate = true;
+ prefHolder.notifyClass = LocalFolder.FolderClass.FIRST_CLASS;
+ prefHolder.pushClass = LocalFolder.FolderClass.FIRST_CLASS;
+ } else {
+ prefHolder.pushClass = LocalFolder.FolderClass.INHERITED;
+
+ }
+ if (name.equalsIgnoreCase(mAccount.getInboxFolderName()) ||
+ name.equalsIgnoreCase(mAccount.getDraftsFolderName())) {
+ prefHolder.syncClass = LocalFolder.FolderClass.FIRST_CLASS;
+ } else {
+ prefHolder.syncClass = LocalFolder.FolderClass.NO_CLASS;
+ }
+ }
+ folder.refresh(name, prefHolder); // Recover settings from Preferences
+
+ db.execSQL("INSERT INTO folders (name, visible_limit, top_group, display_class, poll_class, notify_class, push_class, integrate) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", new Object[] {
+ name,
+ visibleLimit,
+ prefHolder.inTopGroup ? 1 : 0,
+ prefHolder.displayClass.name(),
+ prefHolder.syncClass.name(),
+ prefHolder.notifyClass.name(),
+ prefHolder.pushClass.name(),
+ prefHolder.integrate ? 1 : 0,
+ });
+
+ }
+ return null;
+ }
+ });
+ }
+
+
+ String serializeFlags(Flag[] flags) {
+ List extraFlags = new ArrayList();
+
+ for (Flag flag : flags) {
+ switch (flag) {
+ case DELETED:
+ case SEEN:
+ case FLAGGED:
+ case ANSWERED:
+ case FORWARDED: {
+ break;
+ }
+ default: {
+ extraFlags.add(flag);
+ }
+ }
+ }
+
+ return Utility.combine(extraFlags.toArray(EMPTY_FLAG_ARRAY), ',').toUpperCase(Locale.US);
+ }
+
+ public LockableDatabase getDatabase() {
+ return database;
+ }
+
+ void notifyChange() {
+ Uri uri = Uri.withAppendedPath(EmailProvider.CONTENT_URI, "account/" + uUid + "/messages");
+ mContentResolver.notifyChange(uri, null);
+ }
+
+ /**
+ * Split database operations with a large set of arguments into multiple SQL statements.
+ *
+ *
+ * At the time of this writing (2012-12-06) SQLite only supports around 1000 arguments. That's
+ * why we have to split SQL statements with a large set of arguments into multiple SQL
+ * statements each working on a subset of the arguments.
+ *
+ *
+ * @param selectionCallback
+ * Supplies the argument set and the code to query/update the database.
+ * @param batchSize
+ * The maximum size of the selection set in each SQL statement.
+ *
+ * @throws MessagingException
+ */
+ public void doBatchSetSelection(final BatchSetSelection selectionCallback, final int batchSize)
+ throws MessagingException {
+
+ final List selectionArgs = new ArrayList();
+ int start = 0;
+
+ while (start < selectionCallback.getListSize()) {
+ final StringBuilder selection = new StringBuilder();
+
+ selection.append(" IN (");
+
+ int count = Math.min(selectionCallback.getListSize() - start, batchSize);
+
+ for (int i = start, end = start + count; i < end; i++) {
+ if (i > start) {
+ selection.append(",?");
+ } else {
+ selection.append("?");
+ }
+
+ selectionArgs.add(selectionCallback.getListItem(i));
+ }
+
+ selection.append(")");
+
+ try {
+ database.execute(true, new DbCallback() {
+ @Override
+ public Void doDbWork(final SQLiteDatabase db) throws WrappedException,
+ UnavailableStorageException {
+
+ selectionCallback.doDbWork(db, selection.toString(),
+ selectionArgs.toArray(EMPTY_STRING_ARRAY));
+
+ return null;
+ }
+ });
+
+ selectionCallback.postDbWork();
+
+ } catch (WrappedException e) {
+ throw(MessagingException) e.getCause();
+ }
+
+ selectionArgs.clear();
+ start += count;
+ }
+ }
+
+ /**
+ * Defines the behavior of {@link LocalStore#doBatchSetSelection(BatchSetSelection, int)}.
+ */
+ public interface BatchSetSelection {
+ /**
+ * @return The size of the argument list.
+ */
+ int getListSize();
+
+ /**
+ * Get a specific item of the argument list.
+ *
+ * @param index
+ * The index of the item.
+ *
+ * @return Item at position {@code i} of the argument list.
+ */
+ String getListItem(int index);
+
+ /**
+ * Execute the SQL statement.
+ *
+ * @param db
+ * Use this {@link SQLiteDatabase} instance for your SQL statement.
+ * @param selectionSet
+ * A partial selection string containing place holders for the argument list, e.g.
+ * {@code " IN (?,?,?)"} (starts with a space).
+ * @param selectionArgs
+ * The current subset of the argument list.
+ * @throws UnavailableStorageException
+ */
+ void doDbWork(SQLiteDatabase db, String selectionSet, String[] selectionArgs)
+ throws UnavailableStorageException;
+
+ /**
+ * This will be executed after each invocation of
+ * {@link #doDbWork(SQLiteDatabase, String, String[])} (after the transaction has been
+ * committed).
+ */
+ void postDbWork();
+ }
+
+ /**
+ * Change the state of a flag for a list of messages.
+ *
+ *
+ * The goal of this method is to be fast. Currently this means using as few SQL UPDATE
+ * statements as possible.
+ *
+ * @param messageIds
+ * A list of primary keys in the "messages" table.
+ * @param flag
+ * The flag to change. This must be a flag with a separate column in the database.
+ * @param newState
+ * {@code true}, if the flag should be set. {@code false}, otherwise.
+ *
+ * @throws MessagingException
+ */
+ public void setFlag(final List messageIds, final Flag flag, final boolean newState)
+ throws MessagingException {
+
+ final ContentValues cv = new ContentValues();
+ cv.put(getColumnNameForFlag(flag), newState);
+
+ doBatchSetSelection(new BatchSetSelection() {
+
+ @Override
+ public int getListSize() {
+ return messageIds.size();
+ }
+
+ @Override
+ public String getListItem(int index) {
+ return Long.toString(messageIds.get(index));
+ }
+
+ @Override
+ public void doDbWork(SQLiteDatabase db, String selectionSet, String[] selectionArgs)
+ throws UnavailableStorageException {
+
+ db.update("messages", cv, "(empty IS NULL OR empty != 1) AND id" + selectionSet,
+ selectionArgs);
+ }
+
+ @Override
+ public void postDbWork() {
+ notifyChange();
+ }
+ }, FLAG_UPDATE_BATCH_SIZE);
+ }
+
+ /**
+ * Change the state of a flag for a list of threads.
+ *
+ *
+ * The goal of this method is to be fast. Currently this means using as few SQL UPDATE
+ * statements as possible.
+ *
+ * @param threadRootIds
+ * A list of root thread IDs.
+ * @param flag
+ * The flag to change. This must be a flag with a separate column in the database.
+ * @param newState
+ * {@code true}, if the flag should be set. {@code false}, otherwise.
+ *
+ * @throws MessagingException
+ */
+ public void setFlagForThreads(final List threadRootIds, Flag flag, final boolean newState)
+ throws MessagingException {
+
+ final String flagColumn = getColumnNameForFlag(flag);
+
+ doBatchSetSelection(new BatchSetSelection() {
+
+ @Override
+ public int getListSize() {
+ return threadRootIds.size();
+ }
+
+ @Override
+ public String getListItem(int index) {
+ return Long.toString(threadRootIds.get(index));
+ }
+
+ @Override
+ public void doDbWork(SQLiteDatabase db, String selectionSet, String[] selectionArgs)
+ throws UnavailableStorageException {
+
+ db.execSQL("UPDATE messages SET " + flagColumn + " = " + ((newState) ? "1" : "0") +
+ " WHERE id IN (" +
+ "SELECT m.id FROM threads t " +
+ "LEFT JOIN messages m ON (t.message_id = m.id) " +
+ "WHERE (m.empty IS NULL OR m.empty != 1) AND m.deleted = 0 " +
+ "AND t.root" + selectionSet + ")",
+ selectionArgs);
+ }
+
+ @Override
+ public void postDbWork() {
+ notifyChange();
+ }
+ }, THREAD_FLAG_UPDATE_BATCH_SIZE);
+ }
+
+ /**
+ * Get folder name and UID for the supplied messages.
+ *
+ * @param messageIds
+ * A list of primary keys in the "messages" table.
+ * @param threadedList
+ * If this is {@code true}, {@code messageIds} contains the thread IDs of the messages
+ * at the root of a thread. In that case return UIDs for all messages in these threads.
+ * If this is {@code false} only the UIDs for messages in {@code messageIds} are
+ * returned.
+ *
+ * @return The list of UIDs for the messages grouped by folder name.
+ *
+ * @throws MessagingException
+ */
+ public Map> getFoldersAndUids(final List messageIds,
+ final boolean threadedList) throws MessagingException {
+
+ final Map> folderMap = new HashMap>();
+
+ doBatchSetSelection(new BatchSetSelection() {
+
+ @Override
+ public int getListSize() {
+ return messageIds.size();
+ }
+
+ @Override
+ public String getListItem(int index) {
+ return Long.toString(messageIds.get(index));
+ }
+
+ @Override
+ public void doDbWork(SQLiteDatabase db, String selectionSet, String[] selectionArgs)
+ throws UnavailableStorageException {
+
+ if (threadedList) {
+ String sql = "SELECT m.uid, f.name " +
+ "FROM threads t " +
+ "LEFT JOIN messages m ON (t.message_id = m.id) " +
+ "LEFT JOIN folders f ON (m.folder_id = f.id) " +
+ "WHERE (m.empty IS NULL OR m.empty != 1) AND m.deleted = 0 " +
+ "AND t.root" + selectionSet;
+
+ getDataFromCursor(db.rawQuery(sql, selectionArgs));
+
+ } else {
+ String sql =
+ "SELECT m.uid, f.name " +
+ "FROM messages m " +
+ "LEFT JOIN folders f ON (m.folder_id = f.id) " +
+ "WHERE (m.empty IS NULL OR m.empty != 1) AND m.id" + selectionSet;
+
+ getDataFromCursor(db.rawQuery(sql, selectionArgs));
+ }
+ }
+
+ private void getDataFromCursor(Cursor cursor) {
+ try {
+ while (cursor.moveToNext()) {
+ String uid = cursor.getString(0);
+ String folderName = cursor.getString(1);
+
+ List uidList = folderMap.get(folderName);
+ if (uidList == null) {
+ uidList = new ArrayList();
+ folderMap.put(folderName, uidList);
+ }
+
+ uidList.add(uid);
+ }
+ } finally {
+ cursor.close();
+ }
+ }
+
+ @Override
+ public void postDbWork() {
+ notifyChange();
+
+ }
+ }, UID_CHECK_BATCH_SIZE);
+
+ return folderMap;
+ }
+}
diff --git a/src/com/fsck/k9/mail/store/local/LocalTextBody.java b/src/com/fsck/k9/mail/store/local/LocalTextBody.java
new file mode 100644
index 000000000..c28bccfee
--- /dev/null
+++ b/src/com/fsck/k9/mail/store/local/LocalTextBody.java
@@ -0,0 +1,28 @@
+package com.fsck.k9.mail.store.local;
+
+import com.fsck.k9.mail.internet.TextBody;
+
+public class LocalTextBody extends TextBody {
+ /**
+ * This is an HTML-ified version of the message for display purposes.
+ */
+ private String mBodyForDisplay;
+
+ public LocalTextBody(String body) {
+ super(body);
+ }
+
+ public LocalTextBody(String body, String bodyForDisplay) {
+ super(body);
+ this.mBodyForDisplay = bodyForDisplay;
+ }
+
+ public String getBodyForDisplay() {
+ return mBodyForDisplay;
+ }
+
+ public void setBodyForDisplay(String mBodyForDisplay) {
+ this.mBodyForDisplay = mBodyForDisplay;
+ }
+
+}//LocalTextBody
\ No newline at end of file
diff --git a/src/com/fsck/k9/mail/store/local/StoreSchemaDefinition.java b/src/com/fsck/k9/mail/store/local/StoreSchemaDefinition.java
new file mode 100644
index 000000000..277ee719e
--- /dev/null
+++ b/src/com/fsck/k9/mail/store/local/StoreSchemaDefinition.java
@@ -0,0 +1,599 @@
+package com.fsck.k9.mail.store.local;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+
+import android.content.ContentValues;
+import android.content.SharedPreferences;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteException;
+import android.util.Log;
+
+import com.fsck.k9.Account;
+import com.fsck.k9.K9;
+import com.fsck.k9.R;
+import com.fsck.k9.helper.Utility;
+import com.fsck.k9.mail.Flag;
+import com.fsck.k9.mail.Folder;
+import com.fsck.k9.mail.Message;
+import com.fsck.k9.mail.store.LockableDatabase;
+import com.fsck.k9.provider.AttachmentProvider;
+
+class StoreSchemaDefinition implements LockableDatabase.SchemaDefinition {
+ /**
+ *
+ */
+ private final LocalStore localStore;
+
+ /**
+ * @param localStore
+ */
+ StoreSchemaDefinition(LocalStore localStore) {
+ this.localStore = localStore;
+ }
+
+ @Override
+ public int getVersion() {
+ return LocalStore.DB_VERSION;
+ }
+
+ @Override
+ public void doDbUpgrade(final SQLiteDatabase db) {
+ try {
+ upgradeDatabase(db);
+ } catch (Exception e) {
+ Log.e(K9.LOG_TAG, "Exception while upgrading database. Resetting the DB to v0", e);
+ db.setVersion(0);
+ upgradeDatabase(db);
+ }
+ }
+
+ private void upgradeDatabase(final SQLiteDatabase db) {
+ Log.i(K9.LOG_TAG, String.format(Locale.US, "Upgrading database from version %d to version %d",
+ db.getVersion(), LocalStore.DB_VERSION));
+
+ AttachmentProvider.clear(this.localStore.mApplication);
+
+ db.beginTransaction();
+ try {
+ // schema version 29 was when we moved to incremental updates
+ // in the case of a new db or a < v29 db, we blow away and start from scratch
+ if (db.getVersion() < 29) {
+
+ db.execSQL("DROP TABLE IF EXISTS folders");
+ db.execSQL("CREATE TABLE folders (id INTEGER PRIMARY KEY, name TEXT, "
+ + "last_updated INTEGER, unread_count INTEGER, visible_limit INTEGER, status TEXT, "
+ + "push_state TEXT, last_pushed INTEGER, flagged_count INTEGER default 0, "
+ + "integrate INTEGER, top_group INTEGER, poll_class TEXT, push_class TEXT, display_class TEXT, notify_class TEXT"
+ + ")");
+
+ db.execSQL("CREATE INDEX IF NOT EXISTS folder_name ON folders (name)");
+ db.execSQL("DROP TABLE IF EXISTS messages");
+ db.execSQL("CREATE TABLE messages (" +
+ "id INTEGER PRIMARY KEY, " +
+ "deleted INTEGER default 0, " +
+ "folder_id INTEGER, " +
+ "uid TEXT, " +
+ "subject TEXT, " +
+ "date INTEGER, " +
+ "flags TEXT, " +
+ "sender_list TEXT, " +
+ "to_list TEXT, " +
+ "cc_list TEXT, " +
+ "bcc_list TEXT, " +
+ "reply_to_list TEXT, " +
+ "html_content TEXT, " +
+ "text_content TEXT, " +
+ "attachment_count INTEGER, " +
+ "internal_date INTEGER, " +
+ "message_id TEXT, " +
+ "preview TEXT, " +
+ "mime_type TEXT, "+
+ "normalized_subject_hash INTEGER, " +
+ "empty INTEGER, " +
+ "read INTEGER default 0, " +
+ "flagged INTEGER default 0, " +
+ "answered INTEGER default 0, " +
+ "forwarded INTEGER default 0" +
+ ")");
+
+ db.execSQL("DROP TABLE IF EXISTS headers");
+ db.execSQL("CREATE TABLE headers (id INTEGER PRIMARY KEY, message_id INTEGER, name TEXT, value TEXT)");
+ db.execSQL("CREATE INDEX IF NOT EXISTS header_folder ON headers (message_id)");
+
+ db.execSQL("CREATE INDEX IF NOT EXISTS msg_uid ON messages (uid, folder_id)");
+ db.execSQL("DROP INDEX IF EXISTS msg_folder_id");
+ db.execSQL("DROP INDEX IF EXISTS msg_folder_id_date");
+ db.execSQL("CREATE INDEX IF NOT EXISTS msg_folder_id_deleted_date ON messages (folder_id,deleted,internal_date)");
+
+ db.execSQL("DROP INDEX IF EXISTS msg_empty");
+ db.execSQL("CREATE INDEX IF NOT EXISTS msg_empty ON messages (empty)");
+
+ db.execSQL("DROP INDEX IF EXISTS msg_read");
+ db.execSQL("CREATE INDEX IF NOT EXISTS msg_read ON messages (read)");
+
+ db.execSQL("DROP INDEX IF EXISTS msg_flagged");
+ db.execSQL("CREATE INDEX IF NOT EXISTS msg_flagged ON messages (flagged)");
+
+ db.execSQL("DROP INDEX IF EXISTS msg_composite");
+ db.execSQL("CREATE INDEX IF NOT EXISTS msg_composite ON messages (deleted, empty,folder_id,flagged,read)");
+
+
+
+ db.execSQL("DROP TABLE IF EXISTS threads");
+ db.execSQL("CREATE TABLE threads (" +
+ "id INTEGER PRIMARY KEY, " +
+ "message_id INTEGER, " +
+ "root INTEGER, " +
+ "parent INTEGER" +
+ ")");
+
+ db.execSQL("DROP INDEX IF EXISTS threads_message_id");
+ db.execSQL("CREATE INDEX IF NOT EXISTS threads_message_id ON threads (message_id)");
+
+ db.execSQL("DROP INDEX IF EXISTS threads_root");
+ db.execSQL("CREATE INDEX IF NOT EXISTS threads_root ON threads (root)");
+
+ db.execSQL("DROP INDEX IF EXISTS threads_parent");
+ db.execSQL("CREATE INDEX IF NOT EXISTS threads_parent ON threads (parent)");
+
+ db.execSQL("DROP TRIGGER IF EXISTS set_thread_root");
+ db.execSQL("CREATE TRIGGER set_thread_root " +
+ "AFTER INSERT ON threads " +
+ "BEGIN " +
+ "UPDATE threads SET root=id WHERE root IS NULL AND ROWID = NEW.ROWID; " +
+ "END");
+
+ db.execSQL("DROP TABLE IF EXISTS attachments");
+ db.execSQL("CREATE TABLE attachments (id INTEGER PRIMARY KEY, message_id INTEGER,"
+ + "store_data TEXT, content_uri TEXT, size INTEGER, name TEXT,"
+ + "mime_type TEXT, content_id TEXT, content_disposition TEXT)");
+
+ db.execSQL("DROP TABLE IF EXISTS pending_commands");
+ db.execSQL("CREATE TABLE pending_commands " +
+ "(id INTEGER PRIMARY KEY, command TEXT, arguments TEXT)");
+
+ db.execSQL("DROP TRIGGER IF EXISTS delete_folder");
+ db.execSQL("CREATE TRIGGER delete_folder BEFORE DELETE ON folders BEGIN DELETE FROM messages WHERE old.id = folder_id; END;");
+
+ db.execSQL("DROP TRIGGER IF EXISTS delete_message");
+ db.execSQL("CREATE TRIGGER delete_message BEFORE DELETE ON messages BEGIN DELETE FROM attachments WHERE old.id = message_id; "
+ + "DELETE FROM headers where old.id = message_id; END;");
+ } else {
+ // in the case that we're starting out at 29 or newer, run all the needed updates
+
+ if (db.getVersion() < 30) {
+ try {
+ db.execSQL("ALTER TABLE messages ADD deleted INTEGER default 0");
+ } catch (SQLiteException e) {
+ if (! e.toString().startsWith("duplicate column name: deleted")) {
+ throw e;
+ }
+ }
+ }
+ if (db.getVersion() < 31) {
+ db.execSQL("DROP INDEX IF EXISTS msg_folder_id_date");
+ db.execSQL("CREATE INDEX IF NOT EXISTS msg_folder_id_deleted_date ON messages (folder_id,deleted,internal_date)");
+ }
+ if (db.getVersion() < 32) {
+ db.execSQL("UPDATE messages SET deleted = 1 WHERE flags LIKE '%DELETED%'");
+ }
+ if (db.getVersion() < 33) {
+
+ try {
+ db.execSQL("ALTER TABLE messages ADD preview TEXT");
+ } catch (SQLiteException e) {
+ if (! e.toString().startsWith("duplicate column name: preview")) {
+ throw e;
+ }
+ }
+
+ }
+ if (db.getVersion() < 34) {
+ try {
+ db.execSQL("ALTER TABLE folders ADD flagged_count INTEGER default 0");
+ } catch (SQLiteException e) {
+ if (! e.getMessage().startsWith("duplicate column name: flagged_count")) {
+ throw e;
+ }
+ }
+ }
+ if (db.getVersion() < 35) {
+ try {
+ db.execSQL("update messages set flags = replace(flags, 'X_NO_SEEN_INFO', 'X_BAD_FLAG')");
+ } catch (SQLiteException e) {
+ Log.e(K9.LOG_TAG, "Unable to get rid of obsolete flag X_NO_SEEN_INFO", e);
+ }
+ }
+ if (db.getVersion() < 36) {
+ try {
+ db.execSQL("ALTER TABLE attachments ADD content_id TEXT");
+ } catch (SQLiteException e) {
+ Log.e(K9.LOG_TAG, "Unable to add content_id column to attachments");
+ }
+ }
+ if (db.getVersion() < 37) {
+ try {
+ db.execSQL("ALTER TABLE attachments ADD content_disposition TEXT");
+ } catch (SQLiteException e) {
+ Log.e(K9.LOG_TAG, "Unable to add content_disposition column to attachments");
+ }
+ }
+
+ // Database version 38 is solely to prune cached attachments now that we clear them better
+ if (db.getVersion() < 39) {
+ try {
+ db.execSQL("DELETE FROM headers WHERE id in (SELECT headers.id FROM headers LEFT JOIN messages ON headers.message_id = messages.id WHERE messages.id IS NULL)");
+ } catch (SQLiteException e) {
+ Log.e(K9.LOG_TAG, "Unable to remove extra header data from the database");
+ }
+ }
+
+ // V40: Store the MIME type for a message.
+ if (db.getVersion() < 40) {
+ try {
+ db.execSQL("ALTER TABLE messages ADD mime_type TEXT");
+ } catch (SQLiteException e) {
+ Log.e(K9.LOG_TAG, "Unable to add mime_type column to messages");
+ }
+ }
+
+ if (db.getVersion() < 41) {
+ try {
+ db.execSQL("ALTER TABLE folders ADD integrate INTEGER");
+ db.execSQL("ALTER TABLE folders ADD top_group INTEGER");
+ db.execSQL("ALTER TABLE folders ADD poll_class TEXT");
+ db.execSQL("ALTER TABLE folders ADD push_class TEXT");
+ db.execSQL("ALTER TABLE folders ADD display_class TEXT");
+ } catch (SQLiteException e) {
+ if (! e.getMessage().startsWith("duplicate column name:")) {
+ throw e;
+ }
+ }
+
+ Cursor cursor = null;
+ try {
+ SharedPreferences prefs = this.localStore.getPreferences();
+ cursor = db.rawQuery("SELECT id, name FROM folders", null);
+ while (cursor.moveToNext()) {
+ try {
+ int id = cursor.getInt(0);
+ String name = cursor.getString(1);
+ update41Metadata(db, prefs, id, name);
+ } catch (Exception e) {
+ Log.e(K9.LOG_TAG, " error trying to ugpgrade a folder class", e);
+ }
+ }
+ } catch (SQLiteException e) {
+ Log.e(K9.LOG_TAG, "Exception while upgrading database to v41. folder classes may have vanished", e);
+ } finally {
+ Utility.closeQuietly(cursor);
+ }
+ }
+ if (db.getVersion() == 41) {
+ try {
+ long startTime = System.currentTimeMillis();
+ SharedPreferences.Editor editor = this.localStore.getPreferences().edit();
+
+ List extends Folder > folders = this.localStore.getPersonalNamespaces(true);
+ for (Folder folder : folders) {
+ if (folder instanceof LocalFolder) {
+ LocalFolder lFolder = (LocalFolder)folder;
+ lFolder.save(editor);
+ }
+ }
+
+ editor.commit();
+ long endTime = System.currentTimeMillis();
+ Log.i(K9.LOG_TAG, "Putting folder preferences for " + folders.size() + " folders back into Preferences took " + (endTime - startTime) + " ms");
+ } catch (Exception e) {
+ Log.e(K9.LOG_TAG, "Could not replace Preferences in upgrade from DB_VERSION 41", e);
+ }
+ }
+ if (db.getVersion() < 43) {
+ try {
+ // If folder "OUTBOX" (old, v3.800 - v3.802) exists, rename it to
+ // "K9MAIL_INTERNAL_OUTBOX" (new)
+ LocalFolder oldOutbox = new LocalFolder(this.localStore, "OUTBOX");
+ if (oldOutbox.exists()) {
+ ContentValues cv = new ContentValues();
+ cv.put("name", Account.OUTBOX);
+ db.update("folders", cv, "name = ?", new String[] { "OUTBOX" });
+ Log.i(K9.LOG_TAG, "Renamed folder OUTBOX to " + Account.OUTBOX);
+ }
+
+ // Check if old (pre v3.800) localized outbox folder exists
+ String localizedOutbox = K9.app.getString(R.string.special_mailbox_name_outbox);
+ LocalFolder obsoleteOutbox = new LocalFolder(this.localStore, localizedOutbox);
+ if (obsoleteOutbox.exists()) {
+ // Get all messages from the localized outbox ...
+ Message[] messages = obsoleteOutbox.getMessages(null, false);
+
+ if (messages.length > 0) {
+ // ... and move them to the drafts folder (we don't want to
+ // surprise the user by sending potentially very old messages)
+ LocalFolder drafts = new LocalFolder(this.localStore, this.localStore.getAccount().getDraftsFolderName());
+ obsoleteOutbox.moveMessages(messages, drafts);
+ }
+
+ // Now get rid of the localized outbox
+ obsoleteOutbox.delete();
+ obsoleteOutbox.delete(true);
+ }
+ } catch (Exception e) {
+ Log.e(K9.LOG_TAG, "Error trying to fix the outbox folders", e);
+ }
+ }
+ if (db.getVersion() < 44) {
+ try {
+ db.execSQL("ALTER TABLE messages ADD thread_root INTEGER");
+ db.execSQL("ALTER TABLE messages ADD thread_parent INTEGER");
+ db.execSQL("ALTER TABLE messages ADD normalized_subject_hash INTEGER");
+ db.execSQL("ALTER TABLE messages ADD empty INTEGER");
+ } catch (SQLiteException e) {
+ if (! e.getMessage().startsWith("duplicate column name:")) {
+ throw e;
+ }
+ }
+ }
+ if (db.getVersion() < 45) {
+ try {
+ db.execSQL("DROP INDEX IF EXISTS msg_empty");
+ db.execSQL("CREATE INDEX IF NOT EXISTS msg_empty ON messages (empty)");
+
+ db.execSQL("DROP INDEX IF EXISTS msg_thread_root");
+ db.execSQL("CREATE INDEX IF NOT EXISTS msg_thread_root ON messages (thread_root)");
+
+ db.execSQL("DROP INDEX IF EXISTS msg_thread_parent");
+ db.execSQL("CREATE INDEX IF NOT EXISTS msg_thread_parent ON messages (thread_parent)");
+ } catch (SQLiteException e) {
+ if (! e.getMessage().startsWith("duplicate column name:")) {
+ throw e;
+ }
+ }
+ }
+ if (db.getVersion() < 46) {
+ db.execSQL("ALTER TABLE messages ADD read INTEGER default 0");
+ db.execSQL("ALTER TABLE messages ADD flagged INTEGER default 0");
+ db.execSQL("ALTER TABLE messages ADD answered INTEGER default 0");
+ db.execSQL("ALTER TABLE messages ADD forwarded INTEGER default 0");
+
+ String[] projection = { "id", "flags" };
+
+ ContentValues cv = new ContentValues();
+ List extraFlags = new ArrayList();
+
+ Cursor cursor = db.query("messages", projection, null, null, null, null, null);
+ try {
+ while (cursor.moveToNext()) {
+ long id = cursor.getLong(0);
+ String flagList = cursor.getString(1);
+
+ boolean read = false;
+ boolean flagged = false;
+ boolean answered = false;
+ boolean forwarded = false;
+
+ if (flagList != null && flagList.length() > 0) {
+ String[] flags = flagList.split(",");
+
+ for (String flagStr : flags) {
+ try {
+ Flag flag = Flag.valueOf(flagStr);
+
+ switch (flag) {
+ case ANSWERED: {
+ answered = true;
+ break;
+ }
+ case DELETED: {
+ // Don't store this in column 'flags'
+ break;
+ }
+ case FLAGGED: {
+ flagged = true;
+ break;
+ }
+ case FORWARDED: {
+ forwarded = true;
+ break;
+ }
+ case SEEN: {
+ read = true;
+ break;
+ }
+ case DRAFT:
+ case RECENT:
+ case X_DESTROYED:
+ case X_DOWNLOADED_FULL:
+ case X_DOWNLOADED_PARTIAL:
+ case X_GOT_ALL_HEADERS:
+ case X_REMOTE_COPY_STARTED:
+ case X_SEND_FAILED:
+ case X_SEND_IN_PROGRESS: {
+ extraFlags.add(flag);
+ break;
+ }
+ }
+ } catch (Exception e) {
+ // Ignore bad flags
+ }
+ }
+ }
+
+
+ cv.put("flags", this.localStore.serializeFlags(extraFlags.toArray(LocalStore.EMPTY_FLAG_ARRAY)));
+ cv.put("read", read);
+ cv.put("flagged", flagged);
+ cv.put("answered", answered);
+ cv.put("forwarded", forwarded);
+
+ db.update("messages", cv, "id = ?", new String[] { Long.toString(id) });
+
+ cv.clear();
+ extraFlags.clear();
+ }
+ } finally {
+ cursor.close();
+ }
+
+ db.execSQL("CREATE INDEX IF NOT EXISTS msg_read ON messages (read)");
+ db.execSQL("CREATE INDEX IF NOT EXISTS msg_flagged ON messages (flagged)");
+ }
+
+ if (db.getVersion() < 47) {
+ // Create new 'threads' table
+ db.execSQL("DROP TABLE IF EXISTS threads");
+ db.execSQL("CREATE TABLE threads (" +
+ "id INTEGER PRIMARY KEY, " +
+ "message_id INTEGER, " +
+ "root INTEGER, " +
+ "parent INTEGER" +
+ ")");
+
+ // Create indices for new table
+ db.execSQL("DROP INDEX IF EXISTS threads_message_id");
+ db.execSQL("CREATE INDEX IF NOT EXISTS threads_message_id ON threads (message_id)");
+
+ db.execSQL("DROP INDEX IF EXISTS threads_root");
+ db.execSQL("CREATE INDEX IF NOT EXISTS threads_root ON threads (root)");
+
+ db.execSQL("DROP INDEX IF EXISTS threads_parent");
+ db.execSQL("CREATE INDEX IF NOT EXISTS threads_parent ON threads (parent)");
+
+ // Create entries for all messages in 'threads' table
+ db.execSQL("INSERT INTO threads (message_id) SELECT id FROM messages");
+
+ // Copy thread structure from 'messages' table to 'threads'
+ Cursor cursor = db.query("messages",
+ new String[] { "id", "thread_root", "thread_parent" },
+ null, null, null, null, null);
+ try {
+ ContentValues cv = new ContentValues();
+ while (cursor.moveToNext()) {
+ cv.clear();
+ long messageId = cursor.getLong(0);
+
+ if (!cursor.isNull(1)) {
+ long threadRootMessageId = cursor.getLong(1);
+ db.execSQL("UPDATE threads SET root = (SELECT t.id FROM " +
+ "threads t WHERE t.message_id = ?) " +
+ "WHERE message_id = ?",
+ new String[] {
+ Long.toString(threadRootMessageId),
+ Long.toString(messageId)
+ });
+ }
+
+ if (!cursor.isNull(2)) {
+ long threadParentMessageId = cursor.getLong(2);
+ db.execSQL("UPDATE threads SET parent = (SELECT t.id FROM " +
+ "threads t WHERE t.message_id = ?) " +
+ "WHERE message_id = ?",
+ new String[] {
+ Long.toString(threadParentMessageId),
+ Long.toString(messageId)
+ });
+ }
+ }
+ } finally {
+ cursor.close();
+ }
+
+ // Remove indices for old thread-related columns in 'messages' table
+ db.execSQL("DROP INDEX IF EXISTS msg_thread_root");
+ db.execSQL("DROP INDEX IF EXISTS msg_thread_parent");
+
+ // Clear out old thread-related columns in 'messages'
+ ContentValues cv = new ContentValues();
+ cv.putNull("thread_root");
+ cv.putNull("thread_parent");
+ db.update("messages", cv, null, null);
+ }
+
+ if (db.getVersion() < 48) {
+ db.execSQL("UPDATE threads SET root=id WHERE root IS NULL");
+
+ db.execSQL("CREATE TRIGGER set_thread_root " +
+ "AFTER INSERT ON threads " +
+ "BEGIN " +
+ "UPDATE threads SET root=id WHERE root IS NULL AND ROWID = NEW.ROWID; " +
+ "END");
+ }
+ if (db.getVersion() < 49) {
+ db.execSQL("CREATE INDEX IF NOT EXISTS msg_composite ON messages (deleted, empty,folder_id,flagged,read)");
+
+ }
+ if (db.getVersion() < 50) {
+ try {
+ db.execSQL("ALTER TABLE folders ADD notify_class TEXT default '" +
+ Folder.FolderClass.INHERITED.name() + "'");
+ } catch (SQLiteException e) {
+ if (! e.getMessage().startsWith("duplicate column name:")) {
+ throw e;
+ }
+ }
+
+ ContentValues cv = new ContentValues();
+ cv.put("notify_class", Folder.FolderClass.FIRST_CLASS.name());
+
+ db.update("folders", cv, "name = ?",
+ new String[] { this.localStore.getAccount().getInboxFolderName() });
+ }
+ }
+
+ db.setVersion(LocalStore.DB_VERSION);
+
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
+ }
+
+ if (db.getVersion() != LocalStore.DB_VERSION) {
+ throw new RuntimeException("Database upgrade failed!");
+ }
+ }
+
+ private void update41Metadata(final SQLiteDatabase db, SharedPreferences prefs, int id, String name) {
+
+
+ Folder.FolderClass displayClass = Folder.FolderClass.NO_CLASS;
+ Folder.FolderClass syncClass = Folder.FolderClass.INHERITED;
+ Folder.FolderClass pushClass = Folder.FolderClass.SECOND_CLASS;
+ boolean inTopGroup = false;
+ boolean integrate = false;
+ if (this.localStore.getAccount().getInboxFolderName().equals(name)) {
+ displayClass = Folder.FolderClass.FIRST_CLASS;
+ syncClass = Folder.FolderClass.FIRST_CLASS;
+ pushClass = Folder.FolderClass.FIRST_CLASS;
+ inTopGroup = true;
+ integrate = true;
+ }
+
+ try {
+ displayClass = Folder.FolderClass.valueOf(prefs.getString(this.localStore.uUid + "." + name + ".displayMode", displayClass.name()));
+ syncClass = Folder.FolderClass.valueOf(prefs.getString(this.localStore.uUid + "." + name + ".syncMode", syncClass.name()));
+ pushClass = Folder.FolderClass.valueOf(prefs.getString(this.localStore.uUid + "." + name + ".pushMode", pushClass.name()));
+ inTopGroup = prefs.getBoolean(this.localStore.uUid + "." + name + ".inTopGroup", inTopGroup);
+ integrate = prefs.getBoolean(this.localStore.uUid + "." + name + ".integrate", integrate);
+ } catch (Exception e) {
+ Log.e(K9.LOG_TAG, " Throwing away an error while trying to upgrade folder metadata", e);
+ }
+
+ if (displayClass == Folder.FolderClass.NONE) {
+ displayClass = Folder.FolderClass.NO_CLASS;
+ }
+ if (syncClass == Folder.FolderClass.NONE) {
+ syncClass = Folder.FolderClass.INHERITED;
+ }
+ if (pushClass == Folder.FolderClass.NONE) {
+ pushClass = Folder.FolderClass.INHERITED;
+ }
+
+ db.execSQL("UPDATE folders SET integrate = ?, top_group = ?, poll_class=?, push_class =?, display_class = ? WHERE id = ?",
+ new Object[] { integrate, inTopGroup, syncClass, pushClass, displayClass, id });
+
+ }
+}
\ No newline at end of file
diff --git a/src/com/fsck/k9/mail/store/local/TempFileBody.java b/src/com/fsck/k9/mail/store/local/TempFileBody.java
new file mode 100644
index 000000000..f093998d8
--- /dev/null
+++ b/src/com/fsck/k9/mail/store/local/TempFileBody.java
@@ -0,0 +1,26 @@
+package com.fsck.k9.mail.store.local;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.InputStream;
+
+import com.fsck.k9.mail.MessagingException;
+
+public class TempFileBody extends BinaryAttachmentBody {
+ private final File mFile;
+
+ public TempFileBody(String filename) {
+ mFile = new File(filename);
+ }
+
+ @Override
+ public InputStream getInputStream() throws MessagingException {
+ try {
+ return new FileInputStream(mFile);
+ } catch (FileNotFoundException e) {
+ return new ByteArrayInputStream(LocalStore.EMPTY_BYTE_ARRAY);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/com/fsck/k9/mail/store/local/TempFileMessageBody.java b/src/com/fsck/k9/mail/store/local/TempFileMessageBody.java
new file mode 100644
index 000000000..82188f485
--- /dev/null
+++ b/src/com/fsck/k9/mail/store/local/TempFileMessageBody.java
@@ -0,0 +1,36 @@
+package com.fsck.k9.mail.store.local;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+import org.apache.james.mime4j.util.MimeUtil;
+
+import com.fsck.k9.mail.CompositeBody;
+import com.fsck.k9.mail.MessagingException;
+
+public class TempFileMessageBody extends TempFileBody implements CompositeBody {
+
+ public TempFileMessageBody(String filename) {
+ super(filename);
+ }
+
+ @Override
+ public void writeTo(OutputStream out) throws IOException, MessagingException {
+ AttachmentMessageBodyUtil.writeTo(this, out);
+ }
+
+ @Override
+ public void setUsing7bitTransport() throws MessagingException {
+ // see LocalAttachmentMessageBody.setUsing7bitTransport()
+ }
+
+ @Override
+ public void setEncoding(String encoding) throws MessagingException {
+ if (!MimeUtil.ENC_7BIT.equalsIgnoreCase(encoding)
+ && !MimeUtil.ENC_8BIT.equalsIgnoreCase(encoding)) {
+ throw new MessagingException(
+ "Incompatible content-transfer-encoding applied to a CompositeBody");
+ }
+ mEncoding = encoding;
+ }
+}
\ No newline at end of file
diff --git a/src/com/fsck/k9/mail/store/local/ThreadInfo.java b/src/com/fsck/k9/mail/store/local/ThreadInfo.java
new file mode 100644
index 000000000..4115c16d6
--- /dev/null
+++ b/src/com/fsck/k9/mail/store/local/ThreadInfo.java
@@ -0,0 +1,17 @@
+package com.fsck.k9.mail.store.local;
+
+class ThreadInfo {
+ public final long threadId;
+ public final long msgId;
+ public final String messageId;
+ public final long rootId;
+ public final long parentId;
+
+ public ThreadInfo(long threadId, long msgId, String messageId, long rootId, long parentId) {
+ this.threadId = threadId;
+ this.msgId = msgId;
+ this.messageId = messageId;
+ this.rootId = rootId;
+ this.parentId = parentId;
+ }
+}
\ No newline at end of file
diff --git a/src/com/fsck/k9/mail/transport/SmtpTransport.java b/src/com/fsck/k9/mail/transport/SmtpTransport.java
index cedfaeaf5..155661e97 100644
--- a/src/com/fsck/k9/mail/transport/SmtpTransport.java
+++ b/src/com/fsck/k9/mail/transport/SmtpTransport.java
@@ -14,7 +14,7 @@ import com.fsck.k9.mail.filter.LineWrapOutputStream;
import com.fsck.k9.mail.filter.PeekableInputStream;
import com.fsck.k9.mail.filter.SmtpDataStuffing;
import com.fsck.k9.mail.internet.MimeUtility;
-import com.fsck.k9.mail.store.LocalStore.LocalMessage;
+import com.fsck.k9.mail.store.local.LocalMessage;
import com.fsck.k9.net.ssl.SslHelper;
import javax.net.ssl.SSLException;
diff --git a/src/com/fsck/k9/provider/AttachmentProvider.java b/src/com/fsck/k9/provider/AttachmentProvider.java
index f3ba78590..c0bd8b0f7 100644
--- a/src/com/fsck/k9/provider/AttachmentProvider.java
+++ b/src/com/fsck/k9/provider/AttachmentProvider.java
@@ -15,8 +15,8 @@ import com.fsck.k9.K9;
import com.fsck.k9.Preferences;
import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mail.internet.MimeUtility;
-import com.fsck.k9.mail.store.LocalStore;
-import com.fsck.k9.mail.store.LocalStore.AttachmentInfo;
+import com.fsck.k9.mail.store.local.LocalStore;
+import com.fsck.k9.mail.store.local.LocalStore.AttachmentInfo;
import com.fsck.k9.mail.store.StorageManager;
import java.io.*;
diff --git a/src/com/fsck/k9/provider/EmailProvider.java b/src/com/fsck/k9/provider/EmailProvider.java
index 9537ed64a..c99bdb1f4 100644
--- a/src/com/fsck/k9/provider/EmailProvider.java
+++ b/src/com/fsck/k9/provider/EmailProvider.java
@@ -11,10 +11,10 @@ import com.fsck.k9.cache.EmailProviderCacheCursor;
import com.fsck.k9.helper.StringUtils;
import com.fsck.k9.helper.Utility;
import com.fsck.k9.mail.MessagingException;
-import com.fsck.k9.mail.store.LocalStore;
import com.fsck.k9.mail.store.LockableDatabase;
import com.fsck.k9.mail.store.LockableDatabase.DbCallback;
import com.fsck.k9.mail.store.LockableDatabase.WrappedException;
+import com.fsck.k9.mail.store.local.LocalStore;
import com.fsck.k9.mail.store.UnavailableStorageException;
import com.fsck.k9.search.SqlQueryBuilder;
diff --git a/src/com/fsck/k9/provider/MessageProvider.java b/src/com/fsck/k9/provider/MessageProvider.java
index 86a858337..6ce34baf3 100644
--- a/src/com/fsck/k9/provider/MessageProvider.java
+++ b/src/com/fsck/k9/provider/MessageProvider.java
@@ -32,7 +32,8 @@ import com.fsck.k9.mail.Flag;
import com.fsck.k9.mail.Folder;
import com.fsck.k9.mail.Message;
import com.fsck.k9.mail.MessagingException;
-import com.fsck.k9.mail.store.LocalStore;
+import com.fsck.k9.mail.store.local.LocalMessage;
+import com.fsck.k9.mail.store.local.LocalStore;
import com.fsck.k9.search.SearchAccount;
import java.lang.ref.WeakReference;
@@ -152,9 +153,9 @@ public class MessageProvider extends ContentProvider {
}
/**
- * Extracts the {@link LocalStore.LocalMessage#getId() ID} from the given
+ * Extracts the {@link LocalMessage#getId() ID} from the given
* {@link MessageInfoHolder}. The underlying {@link Message} is expected to
- * be a {@link LocalStore.LocalMessage}.
+ * be a {@link LocalMessage}.
*/
public static class IdExtractor implements FieldExtractor {
@Override
diff --git a/src/com/fsck/k9/search/SqlQueryBuilder.java b/src/com/fsck/k9/search/SqlQueryBuilder.java
index da6e0850f..1b6ecf6a2 100644
--- a/src/com/fsck/k9/search/SqlQueryBuilder.java
+++ b/src/com/fsck/k9/search/SqlQueryBuilder.java
@@ -5,8 +5,8 @@ import java.util.List;
import com.fsck.k9.Account;
import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mail.Folder;
-import com.fsck.k9.mail.store.LocalStore;
-import com.fsck.k9.mail.store.LocalStore.LocalFolder;
+import com.fsck.k9.mail.store.local.LocalFolder;
+import com.fsck.k9.mail.store.local.LocalStore;
import com.fsck.k9.search.SearchSpecification.Attribute;
import com.fsck.k9.search.SearchSpecification.SearchCondition;
import com.fsck.k9.search.SearchSpecification.Searchfield;
diff --git a/src/com/fsck/k9/view/AttachmentView.java b/src/com/fsck/k9/view/AttachmentView.java
index 9c5de50a0..cffb37c2a 100644
--- a/src/com/fsck/k9/view/AttachmentView.java
+++ b/src/com/fsck/k9/view/AttachmentView.java
@@ -40,7 +40,7 @@ import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mail.Part;
import com.fsck.k9.mail.internet.MimeHeader;
import com.fsck.k9.mail.internet.MimeUtility;
-import com.fsck.k9.mail.store.LocalStore.LocalAttachmentBodyPart;
+import com.fsck.k9.mail.store.local.LocalAttachmentBodyPart;
import com.fsck.k9.provider.AttachmentProvider;
public class AttachmentView extends FrameLayout implements OnClickListener, OnLongClickListener {
diff --git a/src/com/fsck/k9/view/SingleMessageView.java b/src/com/fsck/k9/view/SingleMessageView.java
index 58a5821cf..f9e1cdda1 100644
--- a/src/com/fsck/k9/view/SingleMessageView.java
+++ b/src/com/fsck/k9/view/SingleMessageView.java
@@ -55,8 +55,8 @@ import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mail.Multipart;
import com.fsck.k9.mail.Part;
import com.fsck.k9.mail.internet.MimeUtility;
-import com.fsck.k9.mail.store.LocalStore;
-import com.fsck.k9.mail.store.LocalStore.LocalMessage;
+import com.fsck.k9.mail.store.local.LocalAttachmentBodyPart;
+import com.fsck.k9.mail.store.local.LocalMessage;
import com.fsck.k9.provider.AttachmentProvider.AttachmentProviderColumns;
import org.apache.commons.io.IOUtils;
@@ -626,7 +626,7 @@ public class SingleMessageView extends LinearLayout implements OnClickListener,
for (int i = 0; i < mp.getCount(); i++) {
renderAttachments(mp.getBodyPart(i), depth + 1, message, account, controller, listener);
}
- } else if (part instanceof LocalStore.LocalAttachmentBodyPart) {
+ } else if (part instanceof LocalAttachmentBodyPart) {
AttachmentView view = (AttachmentView)mInflater.inflate(R.layout.message_view_attachment, null);
view.setCallback(attachmentCallback);