From 038fceabf044a21a60e104444445d1a5070889d5 Mon Sep 17 00:00:00 2001 From: Christian Frommeyer Date: Sun, 7 Sep 2014 14:39:23 +0200 Subject: [PATCH 1/6] Move LocalStore to new subpackage to prepare decomposition of nested classes. --- src/com/fsck/k9/Account.java | 2 +- src/com/fsck/k9/K9.java | 2 +- src/com/fsck/k9/activity/FolderList.java | 2 +- src/com/fsck/k9/activity/MessageCompose.java | 6 +++--- src/com/fsck/k9/activity/UpgradeDatabases.java | 2 +- src/com/fsck/k9/activity/setup/AccountSettings.java | 2 +- src/com/fsck/k9/activity/setup/FolderSettings.java | 4 ++-- src/com/fsck/k9/cache/EmailProviderCache.java | 4 ++-- src/com/fsck/k9/controller/MessagingController.java | 8 ++++---- .../k9/controller/MessagingControllerPushReceiver.java | 4 ++-- src/com/fsck/k9/fragment/MessageListFragment.java | 4 ++-- src/com/fsck/k9/fragment/MessageViewFragment.java | 2 +- src/com/fsck/k9/mail/Store.java | 2 +- src/com/fsck/k9/mail/store/{ => local}/LocalStore.java | 6 +++++- src/com/fsck/k9/mail/transport/SmtpTransport.java | 2 +- src/com/fsck/k9/provider/AttachmentProvider.java | 4 ++-- src/com/fsck/k9/provider/EmailProvider.java | 2 +- src/com/fsck/k9/provider/MessageProvider.java | 2 +- src/com/fsck/k9/search/SqlQueryBuilder.java | 4 ++-- src/com/fsck/k9/view/AttachmentView.java | 2 +- src/com/fsck/k9/view/SingleMessageView.java | 4 ++-- 21 files changed, 37 insertions(+), 33 deletions(-) rename src/com/fsck/k9/mail/store/{ => local}/LocalStore.java (99%) diff --git a/src/com/fsck/k9/Account.java b/src/com/fsck/k9/Account.java index a497e1124..00f58fa64 100644 --- a/src/com/fsck/k9/Account.java +++ b/src/com/fsck/k9/Account.java @@ -31,9 +31,9 @@ import com.fsck.k9.mail.Address; import com.fsck.k9.mail.MessagingException; import com.fsck.k9.mail.Store; import com.fsck.k9.mail.Folder.FolderClass; -import com.fsck.k9.mail.store.LocalStore; import com.fsck.k9.mail.store.StorageManager; import com.fsck.k9.mail.store.StorageManager.StorageProvider; +import com.fsck.k9.mail.store.local.LocalStore; import com.fsck.k9.provider.EmailProvider; import com.fsck.k9.provider.EmailProvider.StatsColumns; import com.fsck.k9.search.ConditionsTreeNode; diff --git a/src/com/fsck/k9/K9.java b/src/com/fsck/k9/K9.java index 624fdfb58..d997fd950 100644 --- a/src/com/fsck/k9/K9.java +++ b/src/com/fsck/k9/K9.java @@ -37,7 +37,7 @@ import com.fsck.k9.mail.Address; import com.fsck.k9.mail.Message; import com.fsck.k9.mail.MessagingException; import com.fsck.k9.mail.internet.BinaryTempFileBody; -import com.fsck.k9.mail.store.LocalStore; +import com.fsck.k9.mail.store.local.LocalStore; import com.fsck.k9.provider.UnreadWidgetProvider; import com.fsck.k9.security.LocalKeyStore; import com.fsck.k9.service.BootReceiver; diff --git a/src/com/fsck/k9/activity/FolderList.java b/src/com/fsck/k9/activity/FolderList.java index 1270c724a..3903ec0dd 100644 --- a/src/com/fsck/k9/activity/FolderList.java +++ b/src/com/fsck/k9/activity/FolderList.java @@ -56,7 +56,7 @@ import com.fsck.k9.helper.power.TracingPowerManager.TracingWakeLock; 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.LocalFolder; +import com.fsck.k9.mail.store.local.LocalStore.LocalFolder; import com.fsck.k9.search.LocalSearch; import com.fsck.k9.search.SearchSpecification.Attribute; import com.fsck.k9.search.SearchSpecification.Searchfield; diff --git a/src/com/fsck/k9/activity/MessageCompose.java b/src/com/fsck/k9/activity/MessageCompose.java index f27e334fc..35507d3ab 100644 --- a/src/com/fsck/k9/activity/MessageCompose.java +++ b/src/com/fsck/k9/activity/MessageCompose.java @@ -107,9 +107,9 @@ 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.TextBodyBuilder; -import com.fsck.k9.mail.store.LocalStore.LocalAttachmentBody; -import com.fsck.k9.mail.store.LocalStore.TempFileBody; -import com.fsck.k9.mail.store.LocalStore.TempFileMessageBody; +import com.fsck.k9.mail.store.local.LocalStore.LocalAttachmentBody; +import com.fsck.k9.mail.store.local.LocalStore.TempFileBody; +import com.fsck.k9.mail.store.local.LocalStore.TempFileMessageBody; import com.fsck.k9.view.MessageWebView; import org.apache.james.mime4j.codec.EncoderUtil; diff --git a/src/com/fsck/k9/activity/UpgradeDatabases.java b/src/com/fsck/k9/activity/UpgradeDatabases.java index f109c382c..b0cf3587a 100644 --- a/src/com/fsck/k9/activity/UpgradeDatabases.java +++ b/src/com/fsck/k9/activity/UpgradeDatabases.java @@ -30,7 +30,7 @@ import android.widget.TextView; *
  • {@link #actionUpgradeDatabases(Context, Intent)} will call {@link K9#areDatabasesUpToDate()} * to check if we already know whether the databases have been upgraded.
  • *
  • {@link K9#areDatabasesUpToDate()} will compare the last known database version stored in a - * {@link SharedPreferences} file to {@link com.fsck.k9.mail.store.LocalStore#DB_VERSION}. This + * {@link SharedPreferences} file to {@link com.fsck.k9.mail.store.local.LocalStore#DB_VERSION}. This * is done as an optimization because it's faster than opening all of the accounts' databases * one by one.
  • *
  • If there was an error reading the cached database version or if it shows the databases need diff --git a/src/com/fsck/k9/activity/setup/AccountSettings.java b/src/com/fsck/k9/activity/setup/AccountSettings.java index 9c27ae4de..3e2e49351 100644 --- a/src/com/fsck/k9/activity/setup/AccountSettings.java +++ b/src/com/fsck/k9/activity/setup/AccountSettings.java @@ -41,7 +41,7 @@ import com.fsck.k9.activity.ManageIdentities; import com.fsck.k9.crypto.Apg; import com.fsck.k9.mail.Folder; import com.fsck.k9.mail.Store; -import com.fsck.k9.mail.store.LocalStore.LocalFolder; +import com.fsck.k9.mail.store.local.LocalStore.LocalFolder; import com.fsck.k9.mail.store.StorageManager; import com.fsck.k9.service.MailService; diff --git a/src/com/fsck/k9/activity/setup/FolderSettings.java b/src/com/fsck/k9/activity/setup/FolderSettings.java index 5cab0a26e..ad912f305 100644 --- a/src/com/fsck/k9/activity/setup/FolderSettings.java +++ b/src/com/fsck/k9/activity/setup/FolderSettings.java @@ -16,8 +16,8 @@ import com.fsck.k9.mail.Folder.FolderClass; import com.fsck.k9.mail.MessagingException; import com.fsck.k9.mail.Store; -import com.fsck.k9.mail.store.LocalStore; -import com.fsck.k9.mail.store.LocalStore.LocalFolder; +import com.fsck.k9.mail.store.local.LocalStore; +import com.fsck.k9.mail.store.local.LocalStore.LocalFolder; import com.fsck.k9.service.MailService; public class FolderSettings extends K9PreferenceActivity { diff --git a/src/com/fsck/k9/cache/EmailProviderCache.java b/src/com/fsck/k9/cache/EmailProviderCache.java index 6c923f112..179d41a28 100644 --- a/src/com/fsck/k9/cache/EmailProviderCache.java +++ b/src/com/fsck/k9/cache/EmailProviderCache.java @@ -11,8 +11,8 @@ import android.support.v4.content.LocalBroadcastManager; import com.fsck.k9.fragment.MessageListFragment; import com.fsck.k9.mail.Message; -import com.fsck.k9.mail.store.LocalStore.LocalFolder; -import com.fsck.k9.mail.store.LocalStore.LocalMessage; +import com.fsck.k9.mail.store.local.LocalStore.LocalFolder; +import com.fsck.k9.mail.store.local.LocalStore.LocalMessage; import com.fsck.k9.provider.EmailProvider; /** diff --git a/src/com/fsck/k9/controller/MessagingController.java b/src/com/fsck/k9/controller/MessagingController.java index 8c1091227..3e775c161 100644 --- a/src/com/fsck/k9/controller/MessagingController.java +++ b/src/com/fsck/k9/controller/MessagingController.java @@ -79,10 +79,10 @@ import com.fsck.k9.mail.Transport; import com.fsck.k9.mail.internet.MimeMessage; import com.fsck.k9.mail.internet.MimeUtility; import com.fsck.k9.mail.internet.TextBody; -import com.fsck.k9.mail.store.LocalStore; -import com.fsck.k9.mail.store.LocalStore.LocalFolder; -import com.fsck.k9.mail.store.LocalStore.LocalMessage; -import com.fsck.k9.mail.store.LocalStore.PendingCommand; +import com.fsck.k9.mail.store.local.LocalStore; +import com.fsck.k9.mail.store.local.LocalStore.LocalFolder; +import com.fsck.k9.mail.store.local.LocalStore.LocalMessage; +import com.fsck.k9.mail.store.local.LocalStore.PendingCommand; import com.fsck.k9.mail.store.Pop3Store; import com.fsck.k9.mail.store.UnavailableAccountException; import com.fsck.k9.mail.store.UnavailableStorageException; diff --git a/src/com/fsck/k9/controller/MessagingControllerPushReceiver.java b/src/com/fsck/k9/controller/MessagingControllerPushReceiver.java index 0a2a1711b..c9df4a4cd 100644 --- a/src/com/fsck/k9/controller/MessagingControllerPushReceiver.java +++ b/src/com/fsck/k9/controller/MessagingControllerPushReceiver.java @@ -11,8 +11,8 @@ import com.fsck.k9.mail.Folder; import com.fsck.k9.mail.Message; import com.fsck.k9.mail.PushReceiver; -import com.fsck.k9.mail.store.LocalStore; -import com.fsck.k9.mail.store.LocalStore.LocalFolder; +import com.fsck.k9.mail.store.local.LocalStore; +import com.fsck.k9.mail.store.local.LocalStore.LocalFolder; import com.fsck.k9.service.SleepService; import java.util.List; diff --git a/src/com/fsck/k9/fragment/MessageListFragment.java b/src/com/fsck/k9/fragment/MessageListFragment.java index 3648e8fe3..4ed0f6f25 100644 --- a/src/com/fsck/k9/fragment/MessageListFragment.java +++ b/src/com/fsck/k9/fragment/MessageListFragment.java @@ -88,8 +88,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.LocalStore.LocalFolder; +import com.fsck.k9.mail.store.local.LocalStore; +import com.fsck.k9.mail.store.local.LocalStore.LocalFolder; import com.fsck.k9.provider.EmailProvider; import com.fsck.k9.provider.EmailProvider.MessageColumns; import com.fsck.k9.provider.EmailProvider.SpecialColumns; diff --git a/src/com/fsck/k9/fragment/MessageViewFragment.java b/src/com/fsck/k9/fragment/MessageViewFragment.java index 5f267bb64..73fb6fbfd 100644 --- a/src/com/fsck/k9/fragment/MessageViewFragment.java +++ b/src/com/fsck/k9/fragment/MessageViewFragment.java @@ -39,7 +39,7 @@ import com.fsck.k9.mail.Flag; import com.fsck.k9.mail.Message; import com.fsck.k9.mail.MessagingException; import com.fsck.k9.mail.Part; -import com.fsck.k9.mail.store.LocalStore.LocalMessage; +import com.fsck.k9.mail.store.local.LocalStore.LocalMessage; import com.fsck.k9.view.AttachmentView; import com.fsck.k9.view.AttachmentView.AttachmentFileDownloadCallback; import com.fsck.k9.view.MessageHeader; diff --git a/src/com/fsck/k9/mail/Store.java b/src/com/fsck/k9/mail/Store.java index 45efb2a34..659b81161 100644 --- a/src/com/fsck/k9/mail/Store.java +++ b/src/com/fsck/k9/mail/Store.java @@ -12,9 +12,9 @@ import android.util.Log; import com.fsck.k9.Account; import com.fsck.k9.K9; import com.fsck.k9.mail.store.ImapStore; -import com.fsck.k9.mail.store.LocalStore; import com.fsck.k9.mail.store.Pop3Store; import com.fsck.k9.mail.store.StorageManager.StorageProvider; +import com.fsck.k9.mail.store.local.LocalStore; import com.fsck.k9.mail.store.UnavailableStorageException; import com.fsck.k9.mail.store.WebDavStore; diff --git a/src/com/fsck/k9/mail/store/LocalStore.java b/src/com/fsck/k9/mail/store/local/LocalStore.java similarity index 99% rename from src/com/fsck/k9/mail/store/LocalStore.java rename to src/com/fsck/k9/mail/store/local/LocalStore.java index 8e1aca3eb..9749531b0 100644 --- a/src/com/fsck/k9/mail/store/LocalStore.java +++ b/src/com/fsck/k9/mail/store/local/LocalStore.java @@ -1,5 +1,5 @@ -package com.fsck.k9.mail.store; +package com.fsck.k9.mail.store.local; import java.io.ByteArrayInputStream; import java.io.File; @@ -71,7 +71,11 @@ import com.fsck.k9.mail.internet.MimeMultipart; import com.fsck.k9.mail.internet.MimeUtility; import com.fsck.k9.mail.internet.MimeUtility.ViewableContainer; import com.fsck.k9.mail.internet.TextBody; +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.AttachmentProvider; diff --git a/src/com/fsck/k9/mail/transport/SmtpTransport.java b/src/com/fsck/k9/mail/transport/SmtpTransport.java index cedfaeaf5..bb80239a2 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.LocalStore.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..343f9fbe6 100644 --- a/src/com/fsck/k9/provider/MessageProvider.java +++ b/src/com/fsck/k9/provider/MessageProvider.java @@ -32,7 +32,7 @@ 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.LocalStore; import com.fsck.k9.search.SearchAccount; import java.lang.ref.WeakReference; diff --git a/src/com/fsck/k9/search/SqlQueryBuilder.java b/src/com/fsck/k9/search/SqlQueryBuilder.java index da6e0850f..224c0b901 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.LocalStore; +import com.fsck.k9.mail.store.local.LocalStore.LocalFolder; 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..405ecbd99 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.LocalStore.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..db842707f 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.LocalStore; +import com.fsck.k9.mail.store.local.LocalStore.LocalMessage; import com.fsck.k9.provider.AttachmentProvider.AttachmentProviderColumns; import org.apache.commons.io.IOUtils; From f92da3af5918cc428dd121ac0fe6c3092a230205 Mon Sep 17 00:00:00 2001 From: Christian Frommeyer Date: Sun, 7 Sep 2014 14:54:02 +0200 Subject: [PATCH 2/6] Extracting local attachment classes from LocalStore to reduce file size. --- src/com/fsck/k9/activity/MessageCompose.java | 6 +- .../local/AttachmentMessageBodyUtil.java | 34 ++++ .../store/local/BinaryAttachmentBody.java | 54 ++++++ .../mail/store/local/LocalAttachmentBody.java | 37 ++++ .../local/LocalAttachmentMessageBody.java | 49 +++++ .../fsck/k9/mail/store/local/LocalStore.java | 180 +----------------- .../k9/mail/store/local/TempFileBody.java | 26 +++ .../mail/store/local/TempFileMessageBody.java | 36 ++++ 8 files changed, 240 insertions(+), 182 deletions(-) create mode 100644 src/com/fsck/k9/mail/store/local/AttachmentMessageBodyUtil.java create mode 100644 src/com/fsck/k9/mail/store/local/BinaryAttachmentBody.java create mode 100644 src/com/fsck/k9/mail/store/local/LocalAttachmentBody.java create mode 100644 src/com/fsck/k9/mail/store/local/LocalAttachmentMessageBody.java create mode 100644 src/com/fsck/k9/mail/store/local/TempFileBody.java create mode 100644 src/com/fsck/k9/mail/store/local/TempFileMessageBody.java diff --git a/src/com/fsck/k9/activity/MessageCompose.java b/src/com/fsck/k9/activity/MessageCompose.java index 35507d3ab..727f524ca 100644 --- a/src/com/fsck/k9/activity/MessageCompose.java +++ b/src/com/fsck/k9/activity/MessageCompose.java @@ -107,9 +107,9 @@ 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.TextBodyBuilder; -import com.fsck.k9.mail.store.local.LocalStore.LocalAttachmentBody; -import com.fsck.k9.mail.store.local.LocalStore.TempFileBody; -import com.fsck.k9.mail.store.local.LocalStore.TempFileMessageBody; +import com.fsck.k9.mail.store.local.LocalAttachmentBody; +import com.fsck.k9.mail.store.local.TempFileBody; +import com.fsck.k9.mail.store.local.TempFileMessageBody; import com.fsck.k9.view.MessageWebView; import org.apache.james.mime4j.codec.EncoderUtil; 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/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/LocalStore.java b/src/com/fsck/k9/mail/store/local/LocalStore.java index 9749531b0..dde0f0f0c 100644 --- a/src/com/fsck/k9/mail/store/local/LocalStore.java +++ b/src/com/fsck/k9/mail/store/local/LocalStore.java @@ -1,10 +1,7 @@ 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.FileOutputStream; import java.io.IOException; import java.io.InputStream; @@ -26,7 +23,6 @@ import java.util.UUID; import java.util.regex.Pattern; import org.apache.commons.io.IOUtils; -import org.apache.james.mime4j.codec.QuotedPrintableOutputStream; import org.apache.james.mime4j.util.MimeUtil; import android.app.Application; @@ -54,7 +50,6 @@ 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.CompositeBody; import com.fsck.k9.mail.FetchProfile; import com.fsck.k9.mail.Flag; import com.fsck.k9.mail.Folder; @@ -63,7 +58,6 @@ import com.fsck.k9.mail.Message.RecipientType; import com.fsck.k9.mail.MessagingException; import com.fsck.k9.mail.Part; import com.fsck.k9.mail.Store; -import com.fsck.k9.mail.filter.Base64OutputStream; import com.fsck.k9.mail.internet.MimeBodyPart; import com.fsck.k9.mail.internet.MimeHeader; import com.fsck.k9.mail.internet.MimeMessage; @@ -98,7 +92,7 @@ public class LocalStore extends Store implements Serializable { private static final Message[] EMPTY_MESSAGE_ARRAY = new Message[0]; private static final String[] EMPTY_STRING_ARRAY = new String[0]; private static final Flag[] EMPTY_FLAG_ARRAY = new Flag[0]; - private static final byte[] EMPTY_BYTE_ARRAY = new byte[0]; + static final byte[] EMPTY_BYTE_ARRAY = new byte[0]; /* * a String containing the columns getMessages expects to work with @@ -4060,178 +4054,6 @@ public class LocalStore extends Store implements Serializable { } } - public abstract static 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; - } - } - - public static 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(EMPTY_BYTE_ARRAY); - } - } - } - - public static 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(EMPTY_BYTE_ARRAY); - } - } - - public Uri getContentUri() { - return mUri; - } - } - - /** - * A {@link LocalAttachmentBody} extension containing a message/rfc822 type body - * - */ - public static 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; - } - } - - public static 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; - } - } - - public static 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(); - } - } - } - static class ThreadInfo { public final long threadId; public final long msgId; 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 From 89ba2c510bd9d78c0a6a244109acb30fa8fcf2fb Mon Sep 17 00:00:00 2001 From: Christian Frommeyer Date: Sun, 7 Sep 2014 15:01:48 +0200 Subject: [PATCH 3/6] More nested classes extracted from LocalStore. --- .../store/local/LocalAttachmentBodyPart.java | 31 +++++++++++ .../fsck/k9/mail/store/local/LocalStore.java | 55 +------------------ .../k9/mail/store/local/LocalTextBody.java | 28 ++++++++++ src/com/fsck/k9/view/AttachmentView.java | 2 +- src/com/fsck/k9/view/SingleMessageView.java | 4 +- 5 files changed, 64 insertions(+), 56 deletions(-) create mode 100644 src/com/fsck/k9/mail/store/local/LocalAttachmentBodyPart.java create mode 100644 src/com/fsck/k9/mail/store/local/LocalTextBody.java 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/LocalStore.java b/src/com/fsck/k9/mail/store/local/LocalStore.java index dde0f0f0c..327cf5388 100644 --- a/src/com/fsck/k9/mail/store/local/LocalStore.java +++ b/src/com/fsck/k9/mail/store/local/LocalStore.java @@ -3473,31 +3473,6 @@ public class LocalStore extends Store implements Serializable { } } - public static 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 - public class LocalMessage extends MimeMessage { private long mId; private int mAttachmentCount; @@ -3593,8 +3568,8 @@ public class LocalStore extends Store implements Serializable { if (part == null) { // If that fails, try and get a text part. part = MimeUtility.findFirstPartByMimeType(this, "text/plain"); - if (part != null && part.getBody() instanceof LocalStore.LocalTextBody) { - text = ((LocalStore.LocalTextBody) part.getBody()).getBodyForDisplay(); + 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. @@ -4028,32 +4003,6 @@ public class LocalStore extends Store implements Serializable { } } - public static 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; - } - } - static class ThreadInfo { public final long threadId; public final long msgId; 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/view/AttachmentView.java b/src/com/fsck/k9/view/AttachmentView.java index 405ecbd99..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.local.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 db842707f..f791f030c 100644 --- a/src/com/fsck/k9/view/SingleMessageView.java +++ b/src/com/fsck/k9/view/SingleMessageView.java @@ -55,7 +55,7 @@ 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.local.LocalStore; +import com.fsck.k9.mail.store.local.LocalAttachmentBodyPart; import com.fsck.k9.mail.store.local.LocalStore.LocalMessage; import com.fsck.k9.provider.AttachmentProvider.AttachmentProviderColumns; @@ -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); From 91ef5fa816b1e2265af49cace75793dd22732bec Mon Sep 17 00:00:00 2001 From: Christian Frommeyer Date: Sun, 7 Sep 2014 16:01:33 +0200 Subject: [PATCH 4/6] Extracted LocalFolder and LocalMessage definition from LocalStore --- src/com/fsck/k9/activity/FolderList.java | 2 +- .../k9/activity/setup/AccountSettings.java | 2 +- .../k9/activity/setup/FolderSettings.java | 2 +- src/com/fsck/k9/cache/EmailProviderCache.java | 4 +- .../k9/controller/MessagingController.java | 4 +- .../MessagingControllerPushReceiver.java | 2 +- .../fsck/k9/fragment/MessageListFragment.java | 2 +- .../fsck/k9/fragment/MessageViewFragment.java | 2 +- .../fsck/k9/mail/store/local/LocalFolder.java | 2197 +++++++++++++ .../k9/mail/store/local/LocalMessage.java | 559 ++++ .../fsck/k9/mail/store/local/LocalStore.java | 2776 +---------------- .../fsck/k9/mail/store/local/ThreadInfo.java | 17 + .../fsck/k9/mail/transport/SmtpTransport.java | 2 +- src/com/fsck/k9/provider/MessageProvider.java | 5 +- src/com/fsck/k9/search/SqlQueryBuilder.java | 2 +- src/com/fsck/k9/view/SingleMessageView.java | 2 +- 16 files changed, 2822 insertions(+), 2758 deletions(-) create mode 100644 src/com/fsck/k9/mail/store/local/LocalFolder.java create mode 100644 src/com/fsck/k9/mail/store/local/LocalMessage.java create mode 100644 src/com/fsck/k9/mail/store/local/ThreadInfo.java diff --git a/src/com/fsck/k9/activity/FolderList.java b/src/com/fsck/k9/activity/FolderList.java index 3903ec0dd..bd5be538b 100644 --- a/src/com/fsck/k9/activity/FolderList.java +++ b/src/com/fsck/k9/activity/FolderList.java @@ -56,7 +56,7 @@ import com.fsck.k9.helper.power.TracingPowerManager.TracingWakeLock; import com.fsck.k9.mail.Folder; import com.fsck.k9.mail.Message; import com.fsck.k9.mail.MessagingException; -import com.fsck.k9.mail.store.local.LocalStore.LocalFolder; +import com.fsck.k9.mail.store.local.LocalFolder; import com.fsck.k9.search.LocalSearch; import com.fsck.k9.search.SearchSpecification.Attribute; import com.fsck.k9.search.SearchSpecification.Searchfield; diff --git a/src/com/fsck/k9/activity/setup/AccountSettings.java b/src/com/fsck/k9/activity/setup/AccountSettings.java index 3e2e49351..5d2f34fce 100644 --- a/src/com/fsck/k9/activity/setup/AccountSettings.java +++ b/src/com/fsck/k9/activity/setup/AccountSettings.java @@ -41,7 +41,7 @@ import com.fsck.k9.activity.ManageIdentities; import com.fsck.k9.crypto.Apg; import com.fsck.k9.mail.Folder; import com.fsck.k9.mail.Store; -import com.fsck.k9.mail.store.local.LocalStore.LocalFolder; +import com.fsck.k9.mail.store.local.LocalFolder; import com.fsck.k9.mail.store.StorageManager; import com.fsck.k9.service.MailService; diff --git a/src/com/fsck/k9/activity/setup/FolderSettings.java b/src/com/fsck/k9/activity/setup/FolderSettings.java index ad912f305..31bb8e95c 100644 --- a/src/com/fsck/k9/activity/setup/FolderSettings.java +++ b/src/com/fsck/k9/activity/setup/FolderSettings.java @@ -16,8 +16,8 @@ import com.fsck.k9.mail.Folder.FolderClass; import com.fsck.k9.mail.MessagingException; import com.fsck.k9.mail.Store; +import com.fsck.k9.mail.store.local.LocalFolder; import com.fsck.k9.mail.store.local.LocalStore; -import com.fsck.k9.mail.store.local.LocalStore.LocalFolder; import com.fsck.k9.service.MailService; public class FolderSettings extends K9PreferenceActivity { diff --git a/src/com/fsck/k9/cache/EmailProviderCache.java b/src/com/fsck/k9/cache/EmailProviderCache.java index 179d41a28..ba3741f49 100644 --- a/src/com/fsck/k9/cache/EmailProviderCache.java +++ b/src/com/fsck/k9/cache/EmailProviderCache.java @@ -11,8 +11,8 @@ import android.support.v4.content.LocalBroadcastManager; import com.fsck.k9.fragment.MessageListFragment; import com.fsck.k9.mail.Message; -import com.fsck.k9.mail.store.local.LocalStore.LocalFolder; -import com.fsck.k9.mail.store.local.LocalStore.LocalMessage; +import com.fsck.k9.mail.store.local.LocalFolder; +import com.fsck.k9.mail.store.local.LocalMessage; import com.fsck.k9.provider.EmailProvider; /** diff --git a/src/com/fsck/k9/controller/MessagingController.java b/src/com/fsck/k9/controller/MessagingController.java index 3e775c161..7b979371b 100644 --- a/src/com/fsck/k9/controller/MessagingController.java +++ b/src/com/fsck/k9/controller/MessagingController.java @@ -79,9 +79,9 @@ import com.fsck.k9.mail.Transport; import com.fsck.k9.mail.internet.MimeMessage; import com.fsck.k9.mail.internet.MimeUtility; import com.fsck.k9.mail.internet.TextBody; +import com.fsck.k9.mail.store.local.LocalFolder; +import com.fsck.k9.mail.store.local.LocalMessage; import com.fsck.k9.mail.store.local.LocalStore; -import com.fsck.k9.mail.store.local.LocalStore.LocalFolder; -import com.fsck.k9.mail.store.local.LocalStore.LocalMessage; import com.fsck.k9.mail.store.local.LocalStore.PendingCommand; import com.fsck.k9.mail.store.Pop3Store; import com.fsck.k9.mail.store.UnavailableAccountException; diff --git a/src/com/fsck/k9/controller/MessagingControllerPushReceiver.java b/src/com/fsck/k9/controller/MessagingControllerPushReceiver.java index c9df4a4cd..ccd13a59f 100644 --- a/src/com/fsck/k9/controller/MessagingControllerPushReceiver.java +++ b/src/com/fsck/k9/controller/MessagingControllerPushReceiver.java @@ -11,8 +11,8 @@ import com.fsck.k9.mail.Folder; import com.fsck.k9.mail.Message; import com.fsck.k9.mail.PushReceiver; +import com.fsck.k9.mail.store.local.LocalFolder; import com.fsck.k9.mail.store.local.LocalStore; -import com.fsck.k9.mail.store.local.LocalStore.LocalFolder; import com.fsck.k9.service.SleepService; import java.util.List; diff --git a/src/com/fsck/k9/fragment/MessageListFragment.java b/src/com/fsck/k9/fragment/MessageListFragment.java index 4ed0f6f25..e72da130d 100644 --- a/src/com/fsck/k9/fragment/MessageListFragment.java +++ b/src/com/fsck/k9/fragment/MessageListFragment.java @@ -88,8 +88,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.local.LocalFolder; import com.fsck.k9.mail.store.local.LocalStore; -import com.fsck.k9.mail.store.local.LocalStore.LocalFolder; import com.fsck.k9.provider.EmailProvider; import com.fsck.k9.provider.EmailProvider.MessageColumns; import com.fsck.k9.provider.EmailProvider.SpecialColumns; diff --git a/src/com/fsck/k9/fragment/MessageViewFragment.java b/src/com/fsck/k9/fragment/MessageViewFragment.java index 73fb6fbfd..ec1219780 100644 --- a/src/com/fsck/k9/fragment/MessageViewFragment.java +++ b/src/com/fsck/k9/fragment/MessageViewFragment.java @@ -39,7 +39,7 @@ import com.fsck.k9.mail.Flag; import com.fsck.k9.mail.Message; import com.fsck.k9.mail.MessagingException; import com.fsck.k9.mail.Part; -import com.fsck.k9.mail.store.local.LocalStore.LocalMessage; +import com.fsck.k9.mail.store.local.LocalMessage; import com.fsck.k9.view.AttachmentView; import com.fsck.k9.view.AttachmentView.AttachmentFileDownloadCallback; import com.fsck.k9.view.MessageHeader; 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 index 327cf5388..7ede93e36 100644 --- a/src/com/fsck/k9/mail/store/local/LocalStore.java +++ b/src/com/fsck/k9/mail/store/local/LocalStore.java @@ -2,28 +2,16 @@ 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.OutputStream; import java.io.Serializable; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.util.ArrayList; -import java.util.Arrays; -import java.util.Date; import java.util.HashMap; -import java.util.HashSet; import java.util.LinkedList; 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.app.Application; import android.content.ContentResolver; @@ -40,31 +28,14 @@ import com.fsck.k9.Account; import com.fsck.k9.K9; import com.fsck.k9.Preferences; import com.fsck.k9.R; -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.StringUtils; 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.Message.RecipientType; import com.fsck.k9.mail.MessagingException; -import com.fsck.k9.mail.Part; import com.fsck.k9.mail.Store; -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.MimeUtility.ViewableContainer; -import com.fsck.k9.mail.internet.TextBody; import com.fsck.k9.mail.store.LockableDatabase; import com.fsck.k9.mail.store.StorageManager; import com.fsck.k9.mail.store.UnavailableStorageException; @@ -89,47 +60,47 @@ public class LocalStore extends Store implements Serializable { private static final long serialVersionUID = -5142141896809423072L; - private static final Message[] EMPTY_MESSAGE_ARRAY = new Message[0]; - private static final String[] EMPTY_STRING_ARRAY = new String[0]; - private static final Flag[] EMPTY_FLAG_ARRAY = new Flag[0]; + 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 private String GET_MESSAGES_COLS = + 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 "; - private static final String GET_FOLDER_COLS = + 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"; - private static final int FOLDER_ID_INDEX = 0; - private static final int FOLDER_NAME_INDEX = 1; - private static final int FOLDER_VISIBLE_LIMIT_INDEX = 2; - private static final int FOLDER_LAST_CHECKED_INDEX = 3; - private static final int FOLDER_STATUS_INDEX = 4; - private static final int FOLDER_PUSH_STATE_INDEX = 5; - private static final int FOLDER_LAST_PUSHED_INDEX = 6; - private static final int FOLDER_INTEGRATE_INDEX = 7; - private static final int FOLDER_TOP_GROUP_INDEX = 8; - private static final int FOLDER_SYNC_CLASS_INDEX = 9; - private static final int FOLDER_PUSH_CLASS_INDEX = 10; - private static final int FOLDER_DISPLAY_CLASS_INDEX = 11; - private static final int FOLDER_NOTIFY_CLASS_INDEX = 12; + 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; - private static final String[] UID_CHECK_PROJECTION = { "uid" }; + static final String[] UID_CHECK_PROJECTION = { "uid" }; /** * Maximum number of UIDs to check for existence at once. * * @see LocalFolder#extractNewMessages(List) */ - private static final int UID_CHECK_BATCH_SIZE = 500; + static final int UID_CHECK_BATCH_SIZE = 500; /** * Maximum number of messages to perform flag updates on at once. @@ -171,9 +142,9 @@ public class LocalStore extends Store implements Serializable { protected String uUid = null; - private final Application mApplication; + final Application mApplication; - private LockableDatabase database; + LockableDatabase database; private ContentResolver mContentResolver; @@ -467,7 +438,7 @@ public class LocalStore extends Store implements Serializable { try { // If folder "OUTBOX" (old, v3.800 - v3.802) exists, rename it to // "K9MAIL_INTERNAL_OUTBOX" (new) - LocalFolder oldOutbox = new LocalFolder("OUTBOX"); + LocalFolder oldOutbox = new LocalFolder(LocalStore.this, "OUTBOX"); if (oldOutbox.exists()) { ContentValues cv = new ContentValues(); cv.put("name", Account.OUTBOX); @@ -477,7 +448,7 @@ public class LocalStore extends Store implements Serializable { // 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(localizedOutbox); + LocalFolder obsoleteOutbox = new LocalFolder(LocalStore.this, localizedOutbox); if (obsoleteOutbox.exists()) { // Get all messages from the localized outbox ... Message[] messages = obsoleteOutbox.getMessages(null, false); @@ -485,7 +456,7 @@ public class LocalStore extends Store implements Serializable { 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(mAccount.getDraftsFolderName()); + LocalFolder drafts = new LocalFolder(LocalStore.this, mAccount.getDraftsFolderName()); obsoleteOutbox.moveMessages(messages, drafts); } @@ -890,11 +861,11 @@ public class LocalStore extends Store implements Serializable { @Override public LocalFolder getFolder(String name) { - return new LocalFolder(name); + return new LocalFolder(this, name); } public LocalFolder getFolderById(long folderId) { - return new LocalFolder(folderId); + return new LocalFolder(this, folderId); } // TODO this takes about 260-300ms, seems slow. @@ -915,7 +886,7 @@ public class LocalStore extends Store implements Serializable { continue; } String folderName = cursor.getString(FOLDER_NAME_INDEX); - LocalFolder folder = new LocalFolder(folderName); + LocalFolder folder = new LocalFolder(LocalStore.this, folderName); folder.open(cursor); folders.add(folder); @@ -1165,7 +1136,7 @@ public class LocalStore extends Store implements Serializable { * Given a query string, actually do the query for the messages and * call the MessageRetrievalListener for each one */ - private Message[] getMessages( + Message[] getMessages( final MessageRetrievalListener listener, final LocalFolder folder, final String queryString, final String[] placeHolders @@ -1180,7 +1151,7 @@ public class LocalStore extends Store implements Serializable { cursor = db.rawQuery(queryString + " LIMIT 10", placeHolders); while (cursor.moveToNext()) { - LocalMessage message = new LocalMessage(null, folder); + LocalMessage message = new LocalMessage(LocalStore.this, null, folder); message.populateFromGetMessageCursor(cursor); messages.add(message); @@ -1193,7 +1164,7 @@ public class LocalStore extends Store implements Serializable { cursor = db.rawQuery(queryString + " LIMIT -1 OFFSET 10", placeHolders); while (cursor.moveToNext()) { - LocalMessage message = new LocalMessage(null, folder); + LocalMessage message = new LocalMessage(LocalStore.this, null, folder); message.populateFromGetMessageCursor(cursor); messages.add(message); @@ -1317,7 +1288,7 @@ public class LocalStore extends Store implements Serializable { } - private String serializeFlags(Flag[] flags) { + String serializeFlags(Flag[] flags) { List extraFlags = new ArrayList(); for (Flag flag : flags) { @@ -1338,2692 +1309,11 @@ public class LocalStore extends Store implements Serializable { return Utility.combine(extraFlags.toArray(EMPTY_FLAG_ARRAY), ',').toUpperCase(Locale.US); } - public class LocalFolder extends Folder implements Serializable { - /** - * - */ - private static final long serialVersionUID = -1973296520918624767L; - 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(String name) { - super(LocalStore.this.mAccount); - this.mName = name; - - if (LocalStore.this.mAccount.getInboxFolderName().equals(getName())) { - - mSyncClass = FolderClass.FIRST_CLASS; - mPushClass = FolderClass.FIRST_CLASS; - mInTopGroup = true; - } - - - } - - public LocalFolder(long id) { - super(LocalStore.this.mAccount); - 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 { - database.execute(false, new DbCallback() { - @Override - public Void doDbWork(final SQLiteDatabase db) throws WrappedException { - Cursor cursor = null; - try { - String baseQuery = "SELECT " + 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(FOLDER_ID_INDEX)) { - int folderId = cursor.getInt(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(); - } - } - - private void open(Cursor cursor) throws MessagingException { - mFolderId = cursor.getInt(FOLDER_ID_INDEX); - mName = cursor.getString(FOLDER_NAME_INDEX); - mVisibleLimit = cursor.getInt(FOLDER_VISIBLE_LIMIT_INDEX); - mPushState = cursor.getString(FOLDER_PUSH_STATE_INDEX); - super.setStatus(cursor.getString(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(FOLDER_LAST_CHECKED_INDEX)); - super.setLastPush(cursor.getLong(FOLDER_LAST_PUSHED_INDEX)); - mInTopGroup = (cursor.getInt(FOLDER_TOP_GROUP_INDEX)) == 1 ? true : false; - mIntegrate = (cursor.getInt(FOLDER_INTEGRATE_INDEX) == 1) ? true : false; - String noClass = FolderClass.NO_CLASS.toString(); - String displayClass = cursor.getString(FOLDER_DISPLAY_CLASS_INDEX); - mDisplayClass = Folder.FolderClass.valueOf((displayClass == null) ? noClass : displayClass); - String notifyClass = cursor.getString(FOLDER_NOTIFY_CLASS_INDEX); - mNotifyClass = Folder.FolderClass.valueOf((notifyClass == null) ? noClass : notifyClass); - String pushClass = cursor.getString(FOLDER_PUSH_CLASS_INDEX); - mPushClass = Folder.FolderClass.valueOf((pushClass == null) ? noClass : pushClass); - String syncClass = cursor.getString(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 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); - LocalStore.this.createFolders(foldersToCreate, visibleLimit); - - return true; - } - - private 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 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 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 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 { - 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 = 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 = LocalStore.this.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 = LocalStore.this.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 = LocalStore.this.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 { - 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), - mApplication); - } else { - body = new LocalAttachmentBody( - Uri.parse(contentUri), - 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 - */ - private void populateHeaders(final List messages) throws UnavailableStorageException { - 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(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 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 database.execute(false, new DbCallback() { - @Override - public LocalMessage doDbWork(final SQLiteDatabase db) throws WrappedException, UnavailableStorageException { - try { - open(OPEN_MODE_RW); - LocalMessage message = new LocalMessage(uid, LocalFolder.this); - Cursor cursor = null; - - try { - cursor = db.rawQuery( - "SELECT " + - 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 database.execute(false, new DbCallback() { - @Override - public Message[] doDbWork(final SQLiteDatabase db) throws WrappedException, UnavailableStorageException { - try { - open(OPEN_MODE_RW); - return LocalStore.this.getMessages( - listener, - LocalFolder.this, - "SELECT " + 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(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 { - 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; - } - }); - - 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 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 { - 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(); - 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(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", 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; - } - }); - - 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 { - database.execute(false, new DbCallback() { - @Override - public Void doDbWork(final SQLiteDatabase db) throws WrappedException, UnavailableStorageException { - try { - message.buildMimeRepresentation(); - - ViewableContainer container = - MimeUtility.extractTextAndAttachments(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(), - 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(); - } - - 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 { - 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[] - { serializeFlags(appendedFlags.toArray(EMPTY_FLAG_ARRAY)), id }); - - return null; - } - }); - } - - private void deleteHeaders(final long id) throws UnavailableStorageException { - 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 { - 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(mApplication).getAttachmentDirectory(uUid, 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, mApplication)); - } else { - attachment.setBody(new LocalAttachmentBody( - contentUri, 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()); - 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 - 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 { - 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 = LocalStore.this.getMessages( - null, - this, - "SELECT " + 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(); - } - - notifyChange(); - } - - public void clearAllMessages() throws MessagingException { - final String[] folderIdArg = new String[] { Long.toString(mFolderId) }; - - open(OPEN_MODE_RO); - - try { - 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(); - } - - notifyChange(); - - setPushState(null); - setLastPush(0); - setLastChecked(0); - setVisibleLimit(mAccount.getDisplayCount()); - } - - @Override - public void delete(final boolean recurse) throws MessagingException { - try { - 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(); - } - - private void deleteAttachments(final long messageId) throws MessagingException { - open(OPEN_MODE_RW); - 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 = 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(mApplication) - .getAttachmentDirectory(uUid, 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 { - 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 = 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 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 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, 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", UID_CHECK_PROJECTION, - selection.toString(), selectionArgs.toArray(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(); - } - } - } - - public class LocalMessage extends MimeMessage { - private 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() { - } - - LocalMessage(String uid, Folder folder) { - this.mUid = uid; - this.mFolder = folder; - } - - private 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(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); - } - - private 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 { - 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", 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(); - } - - 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 { - 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); - - notifyChange(); - } - - /* - * Completely remove a message from the local database - * - * TODO: document how this updates the thread structure - */ - @Override - public void destroy() throws MessagingException { - try { - 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(); - } - - 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(); - 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; - } - } - - static 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; - } - } - public LockableDatabase getDatabase() { return database; } - private void notifyChange() { + void notifyChange() { Uri uri = Uri.withAppendedPath(EmailProvider.CONTENT_URI, "account/" + uUid + "/messages"); mContentResolver.notifyChange(uri, null); } 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 bb80239a2..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.local.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/MessageProvider.java b/src/com/fsck/k9/provider/MessageProvider.java index 343f9fbe6..6ce34baf3 100644 --- a/src/com/fsck/k9/provider/MessageProvider.java +++ b/src/com/fsck/k9/provider/MessageProvider.java @@ -32,6 +32,7 @@ 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.local.LocalMessage; import com.fsck.k9.mail.store.local.LocalStore; import com.fsck.k9.search.SearchAccount; @@ -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 224c0b901..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.local.LocalFolder; import com.fsck.k9.mail.store.local.LocalStore; -import com.fsck.k9.mail.store.local.LocalStore.LocalFolder; 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/SingleMessageView.java b/src/com/fsck/k9/view/SingleMessageView.java index f791f030c..f9e1cdda1 100644 --- a/src/com/fsck/k9/view/SingleMessageView.java +++ b/src/com/fsck/k9/view/SingleMessageView.java @@ -56,7 +56,7 @@ 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.local.LocalAttachmentBodyPart; -import com.fsck.k9.mail.store.local.LocalStore.LocalMessage; +import com.fsck.k9.mail.store.local.LocalMessage; import com.fsck.k9.provider.AttachmentProvider.AttachmentProviderColumns; import org.apache.commons.io.IOUtils; From eced036d69926dab1f1baf7f1dc77f1d9367e71e Mon Sep 17 00:00:00 2001 From: Christian Frommeyer Date: Sun, 7 Sep 2014 16:18:06 +0200 Subject: [PATCH 5/6] Extracting Database Setup Schema definition form LocalStore. --- .../fsck/k9/mail/store/local/LocalStore.java | 571 +---------------- .../store/local/StoreSchemaDefinition.java | 599 ++++++++++++++++++ 2 files changed, 600 insertions(+), 570 deletions(-) create mode 100644 src/com/fsck/k9/mail/store/local/StoreSchemaDefinition.java diff --git a/src/com/fsck/k9/mail/store/local/LocalStore.java b/src/com/fsck/k9/mail/store/local/LocalStore.java index 7ede93e36..3442aa988 100644 --- a/src/com/fsck/k9/mail/store/local/LocalStore.java +++ b/src/com/fsck/k9/mail/store/local/LocalStore.java @@ -20,14 +20,12 @@ import android.content.Context; import android.content.SharedPreferences; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; -import android.database.sqlite.SQLiteException; 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.R; import com.fsck.k9.controller.MessageRetrievalListener; import com.fsck.k9.helper.StringUtils; import com.fsck.k9.helper.Utility; @@ -43,7 +41,6 @@ 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.AttachmentProvider; import com.fsck.k9.provider.EmailProvider; import com.fsck.k9.provider.EmailProvider.MessageColumns; import com.fsck.k9.search.LocalSearch; @@ -157,7 +154,7 @@ public class LocalStore extends Store implements Serializable { */ public LocalStore(final Account account, final Application application) throws MessagingException { super(account); - database = new LockableDatabase(application, account.getUuid(), new StoreSchemaDefinition()); + database = new LockableDatabase(application, account.getUuid(), new StoreSchemaDefinition(this)); mApplication = application; mContentResolver = application.getContentResolver(); @@ -175,572 +172,6 @@ public class LocalStore extends Store implements Serializable { return Preferences.getPreferences(mApplication).getPreferences(); } - private class StoreSchemaDefinition implements LockableDatabase.SchemaDefinition { - @Override - public int getVersion() { - return 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(), DB_VERSION)); - - AttachmentProvider.clear(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 = 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 = getPreferences().edit(); - - List folders = 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(LocalStore.this, "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(LocalStore.this, 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(LocalStore.this, mAccount.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", serializeFlags(extraFlags.toArray(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[] { getAccount().getInboxFolderName() }); - } - } - - db.setVersion(DB_VERSION); - - db.setTransactionSuccessful(); - } finally { - db.endTransaction(); - } - - if (db.getVersion() != 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 (mAccount.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(uUid + "." + name + ".displayMode", displayClass.name())); - syncClass = Folder.FolderClass.valueOf(prefs.getString(uUid + "." + name + ".syncMode", syncClass.name())); - pushClass = Folder.FolderClass.valueOf(prefs.getString(uUid + "." + name + ".pushMode", pushClass.name())); - inTopGroup = prefs.getBoolean(uUid + "." + name + ".inTopGroup", inTopGroup); - integrate = prefs.getBoolean(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 }); - - } - } - - public long getSize() throws UnavailableStorageException { final StorageManager storageManager = StorageManager.getInstance(mApplication); 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 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 From 9dba60c997cbeffb4605b79ce3e6bdf762a7c39d Mon Sep 17 00:00:00 2001 From: Christian Frommeyer Date: Sun, 7 Sep 2014 16:58:46 +0200 Subject: [PATCH 6/6] Some minor code cleanings and logging for LockableDatabase --- .../fsck/k9/mail/store/LockableDatabase.java | 45 +++++++++---------- 1 file changed, 21 insertions(+), 24 deletions(-) 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));