diff --git a/src/com/fsck/k9/Account.java b/src/com/fsck/k9/Account.java index 54297b580..ce565138b 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; @@ -181,7 +181,7 @@ public class Account implements BaseAccount { private boolean mPushPollOnConnect; private boolean mNotifySync; private SortType mSortType; - private HashMap mSortAscending = new HashMap(); + private Map mSortAscending = new HashMap(); private ShowPictures mShowPictures; private boolean mIsSignatureBeforeQuotedText; private String mExpungePolicy = EXPUNGE_IMMEDIATELY; @@ -338,7 +338,7 @@ public class Account implements BaseAccount { * Pick a nice Android guidelines color if we haven't used them all yet. */ private int pickColor(Context context) { - Account[] accounts = Preferences.getPreferences(context).getAccounts(); + List accounts = Preferences.getPreferences(context).getAccounts(); List availableColors = new ArrayList(PREDEFINED_COLORS.length); Collections.addAll(availableColors, PREDEFINED_COLORS); @@ -620,8 +620,8 @@ public class Account implements BaseAccount { } public static List getExistingAccountNumbers(Preferences preferences) { - Account[] accounts = preferences.getAccounts(); - List accountNumbers = new ArrayList(accounts.length); + List accounts = preferences.getAccounts(); + List accountNumbers = new ArrayList(accounts.size()); for (Account a : accounts) { accountNumbers.add(a.getAccountNumber()); } @@ -679,10 +679,10 @@ public class Account implements BaseAccount { * * I bet there is a much smarter way to do this. Anyone like to suggest it? */ - Account[] accounts = preferences.getAccounts(); - int[] accountNumbers = new int[accounts.length]; - for (int i = 0; i < accounts.length; i++) { - accountNumbers[i] = accounts[i].getAccountNumber(); + List accounts = preferences.getAccounts(); + int[] accountNumbers = new int[accounts.size()]; + for (int i = 0; i < accounts.size(); i++) { + accountNumbers[i] = accounts.get(i).getAccountNumber(); } Arrays.sort(accountNumbers); for (int accountNumber : accountNumbers) { diff --git a/src/com/fsck/k9/K9.java b/src/com/fsck/k9/K9.java index 593bc07a9..6b5d83d56 100644 --- a/src/com/fsck/k9/K9.java +++ b/src/com/fsck/k9/K9.java @@ -5,6 +5,7 @@ import java.io.File; import java.util.ArrayList; import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.concurrent.BlockingQueue; import java.util.concurrent.SynchronousQueue; @@ -35,7 +36,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; @@ -88,7 +89,7 @@ public class K9 extends Application { * * @see ApplicationAware */ - private static List observers = new ArrayList(); + private static final List observers = new ArrayList(); /** * This will be {@code true} once the initialization is complete and {@link #notifyObservers()} @@ -263,7 +264,7 @@ public class K9 extends Application { private static boolean mHideTimeZone = false; private static SortType mSortType; - private static HashMap mSortAscending = new HashMap(); + private static Map mSortAscending = new HashMap(); private static boolean sUseBackgroundAsUnreadIndicator = true; private static boolean sThreadedViewEnabled = true; diff --git a/src/com/fsck/k9/PRNGFixes.java b/src/com/fsck/k9/PRNGFixes.java index e1b20d23b..33d4ff6c9 100644 --- a/src/com/fsck/k9/PRNGFixes.java +++ b/src/com/fsck/k9/PRNGFixes.java @@ -19,7 +19,7 @@ import java.io.DataOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; -import java.io.UnsupportedEncodingException; +import java.nio.charset.Charset; import java.security.NoSuchAlgorithmException; import java.security.Provider; import java.security.SecureRandom; @@ -284,10 +284,7 @@ public final class PRNGFixes { if (serial != null) { result.append(serial); } - try { - return result.toString().getBytes("UTF-8"); - } catch (UnsupportedEncodingException e) { - throw new RuntimeException("UTF-8 encoding not supported"); - } + + return result.toString().getBytes(Charset.forName("UTF-8")); } } diff --git a/src/com/fsck/k9/Preferences.java b/src/com/fsck/k9/Preferences.java index 51b83d873..5fd4929a3 100644 --- a/src/com/fsck/k9/Preferences.java +++ b/src/com/fsck/k9/Preferences.java @@ -3,6 +3,7 @@ package com.fsck.k9; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.LinkedList; import java.util.List; @@ -18,11 +19,6 @@ import com.fsck.k9.preferences.Storage; public class Preferences { - /** - * Immutable empty {@link Account} array - */ - private static final Account[] EMPTY_ACCOUNT_ARRAY = new Account[0]; - private static Preferences preferences; public static synchronized Preferences getPreferences(Context context) { @@ -43,7 +39,7 @@ public class Preferences { private Preferences(Context context) { mStorage = Storage.getStorage(context); mContext = context; - if (mStorage.size() == 0) { + if (mStorage.isEmpty()) { Log.i(K9.LOG_TAG, "Preferences storage is zero-size, importing from Android-style preferences"); Editor editor = mStorage.edit(); editor.copy(context.getSharedPreferences("AndroidMail.Main", Context.MODE_PRIVATE)); @@ -75,12 +71,12 @@ public class Preferences { * registered the method returns an empty array. * @return all accounts */ - public synchronized Account[] getAccounts() { + public synchronized List getAccounts() { if (accounts == null) { loadAccounts(); } - return accountsInOrder.toArray(EMPTY_ACCOUNT_ARRAY); + return Collections.unmodifiableList(accountsInOrder); } /** @@ -89,7 +85,7 @@ public class Preferences { * @return all accounts with {@link Account#isAvailable(Context)} */ public synchronized Collection getAvailableAccounts() { - Account[] allAccounts = getAccounts(); + List allAccounts = getAccounts(); Collection retval = new ArrayList(accounts.size()); for (Account account : allAccounts) { if (account.isEnabled() && account.isAvailable(mContext)) { diff --git a/src/com/fsck/k9/activity/AccountList.java b/src/com/fsck/k9/activity/AccountList.java index d5f29b32e..571dfc907 100644 --- a/src/com/fsck/k9/activity/AccountList.java +++ b/src/com/fsck/k9/activity/AccountList.java @@ -68,7 +68,7 @@ public abstract class AccountList extends K9ListActivity implements OnItemClickL * @param realAccounts * An array of accounts to display. */ - public void populateListView(Account[] realAccounts) { + public void populateListView(List realAccounts) { List accounts = new ArrayList(); if (displaySpecialAccounts() && !K9.isHideSpecialAccounts()) { @@ -79,7 +79,7 @@ public abstract class AccountList extends K9ListActivity implements OnItemClickL accounts.add(allMessagesAccount); } - accounts.addAll(Arrays.asList(realAccounts)); + accounts.addAll(realAccounts); AccountsAdapter adapter = new AccountsAdapter(accounts); ListView listView = getListView(); listView.setAdapter(adapter); @@ -169,15 +169,15 @@ public abstract class AccountList extends K9ListActivity implements OnItemClickL /** * Load accounts in a background thread */ - class LoadAccounts extends AsyncTask { + class LoadAccounts extends AsyncTask> { @Override - protected Account[] doInBackground(Void... params) { - Account[] accounts = Preferences.getPreferences(getApplicationContext()).getAccounts(); + protected List doInBackground(Void... params) { + List accounts = Preferences.getPreferences(getApplicationContext()).getAccounts(); return accounts; } @Override - protected void onPostExecute(Account[] accounts) { + protected void onPostExecute(List accounts) { populateListView(accounts); } } diff --git a/src/com/fsck/k9/activity/Accounts.java b/src/com/fsck/k9/activity/Accounts.java index 76cfe3abb..738ee32a3 100644 --- a/src/com/fsck/k9/activity/Accounts.java +++ b/src/com/fsck/k9/activity/Accounts.java @@ -13,6 +13,7 @@ import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; import android.app.ActionBar; import android.app.Activity; @@ -75,6 +76,7 @@ import com.fsck.k9.activity.setup.Prefs; import com.fsck.k9.activity.setup.WelcomeMessage; import com.fsck.k9.controller.MessagingController; import com.fsck.k9.helper.SizeFormatter; +import com.fsck.k9.helper.Utility; import com.fsck.k9.mail.AuthType; import com.fsck.k9.mail.ServerSettings; import com.fsck.k9.mail.Store; @@ -100,11 +102,6 @@ import de.cketti.library.changelog.ChangeLog; public class Accounts extends K9ListActivity implements OnItemClickListener { - /** - * Immutable empty {@link BaseAccount} array - */ - private static final BaseAccount[] EMPTY_BASE_ACCOUNT_ARRAY = new BaseAccount[0]; - /** * URL used to open Android Market application */ @@ -120,9 +117,12 @@ public class Accounts extends K9ListActivity implements OnItemClickListener { private static final int DIALOG_RECREATE_ACCOUNT = 3; private static final int DIALOG_NO_FILE_MANAGER = 4; + /* + * Must be serializable hence implementation class used for declaration. + */ private ConcurrentHashMap accountStats = new ConcurrentHashMap(); - private ConcurrentHashMap pendingWork = new ConcurrentHashMap(); + private ConcurrentMap pendingWork = new ConcurrentHashMap(); private BaseAccount mSelectedContextAccount; private int mUnreadMessageCount = 0; @@ -398,14 +398,14 @@ public class Accounts extends K9ListActivity implements OnItemClickListener { createSpecialAccounts(); } - Account[] accounts = Preferences.getPreferences(this).getAccounts(); + List accounts = Preferences.getPreferences(this).getAccounts(); Intent intent = getIntent(); //onNewIntent(intent); // see if we should show the welcome message if (ACTION_IMPORT_SETTINGS.equals(intent.getAction())) { onImport(); - } else if (accounts.length < 1) { + } else if (accounts.size() < 1) { WelcomeMessage.showWelcomeMessage(this); finish(); return; @@ -421,7 +421,7 @@ public class Accounts extends K9ListActivity implements OnItemClickListener { onOpenAccount(mUnifiedInboxAccount); finish(); return; - } else if (startup && accounts.length == 1 && onOpenAccount(accounts[0])) { + } else if (startup && accounts.size() == 1 && onOpenAccount(accounts.get(0))) { finish(); return; } @@ -541,18 +541,18 @@ public class Accounts extends K9ListActivity implements OnItemClickListener { return retain; } - private BaseAccount[] accounts = new BaseAccount[0]; + private List accounts = new ArrayList(); private enum ACCOUNT_LOCATION { TOP, MIDDLE, BOTTOM; } private EnumSet accountLocation(BaseAccount account) { EnumSet accountLocation = EnumSet.of(ACCOUNT_LOCATION.MIDDLE); - if (accounts.length > 0) { - if (accounts[0].equals(account)) { + if (accounts.size() > 0) { + if (accounts.get(0).equals(account)) { accountLocation.remove(ACCOUNT_LOCATION.MIDDLE); accountLocation.add(ACCOUNT_LOCATION.TOP); } - if (accounts[accounts.length - 1].equals(account)) { + if (accounts.get(accounts.size() - 1).equals(account)) { accountLocation.remove(ACCOUNT_LOCATION.MIDDLE); accountLocation.add(ACCOUNT_LOCATION.BOTTOM); } @@ -562,7 +562,8 @@ public class Accounts extends K9ListActivity implements OnItemClickListener { private void refresh() { - accounts = Preferences.getPreferences(this).getAccounts(); + accounts.clear(); + accounts.addAll(Preferences.getPreferences(this).getAccounts()); // see if we should show the welcome message // if (accounts.length < 1) { @@ -571,22 +572,22 @@ public class Accounts extends K9ListActivity implements OnItemClickListener { // } List newAccounts; - if (!K9.isHideSpecialAccounts() && accounts.length > 0) { + if (!K9.isHideSpecialAccounts() && accounts.size() > 0) { if (mUnifiedInboxAccount == null || mAllMessagesAccount == null) { createSpecialAccounts(); } - newAccounts = new ArrayList(accounts.length + + newAccounts = new ArrayList(accounts.size() + SPECIAL_ACCOUNTS_COUNT); newAccounts.add(mUnifiedInboxAccount); newAccounts.add(mAllMessagesAccount); } else { - newAccounts = new ArrayList(accounts.length); + newAccounts = new ArrayList(accounts.size()); } - newAccounts.addAll(Arrays.asList(accounts)); + newAccounts.addAll(accounts); - mAdapter = new AccountsAdapter(newAccounts.toArray(EMPTY_BASE_ACCOUNT_ARRAY)); + mAdapter = new AccountsAdapter(newAccounts); getListView().setAdapter(mAdapter); if (!newAccounts.isEmpty()) { mHandler.progress(Window.PROGRESS_START); @@ -1735,7 +1736,7 @@ public class Accounts extends K9ListActivity implements OnItemClickListener { } class AccountsAdapter extends ArrayAdapter { - public AccountsAdapter(BaseAccount[] accounts) { + public AccountsAdapter(List accounts) { super(Accounts.this, 0, accounts); } diff --git a/src/com/fsck/k9/activity/ChooseFolder.java b/src/com/fsck/k9/activity/ChooseFolder.java index 3061b2223..83e8070b8 100644 --- a/src/com/fsck/k9/activity/ChooseFolder.java +++ b/src/com/fsck/k9/activity/ChooseFolder.java @@ -261,7 +261,7 @@ public class ChooseFolder extends K9ListActivity { mHandler.progress(false); } @Override - public void listFolders(Account account, Folder[] folders) { + public void listFolders(Account account, List folders) { if (!account.equals(mAccount)) { return; } diff --git a/src/com/fsck/k9/activity/FolderList.java b/src/com/fsck/k9/activity/FolderList.java index 1270c724a..670419cf1 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.LocalFolder; import com.fsck.k9.search.LocalSearch; import com.fsck.k9.search.SearchSpecification.Attribute; import com.fsck.k9.search.SearchSpecification.Searchfield; @@ -660,7 +660,7 @@ public class FolderList extends K9ListActivity { } class FolderListAdapter extends BaseAdapter implements Filterable { - private ArrayList mFolders = new ArrayList(); + private List mFolders = new ArrayList(); private List mFilteredFolders = Collections.unmodifiableList(mFolders); private Filter mFilter = new FolderListFilter(); @@ -740,7 +740,7 @@ public class FolderList extends K9ListActivity { } @Override - public void listFolders(Account account, Folder[] folders) { + public void listFolders(Account account, List folders) { if (account.equals(mAccount)) { List newFolders = new LinkedList(); @@ -1176,7 +1176,7 @@ public class FolderList extends K9ListActivity { Locale locale = Locale.getDefault(); if ((searchTerm == null) || (searchTerm.length() == 0)) { - ArrayList list = new ArrayList(mFolders); + List list = new ArrayList(mFolders); results.values = list; results.count = list.size(); } else { @@ -1184,7 +1184,7 @@ public class FolderList extends K9ListActivity { final String[] words = searchTermString.split(" "); final int wordCount = words.length; - final ArrayList newValues = new ArrayList(); + final List newValues = new ArrayList(); for (final FolderInfoHolder value : mFolders) { if (value.displayName == null) { diff --git a/src/com/fsck/k9/activity/FolderListFilter.java b/src/com/fsck/k9/activity/FolderListFilter.java index 30a012600..ec01589c3 100644 --- a/src/com/fsck/k9/activity/FolderListFilter.java +++ b/src/com/fsck/k9/activity/FolderListFilter.java @@ -29,7 +29,7 @@ public class FolderListFilter extends Filter { /** * All folders. */ - private ArrayList mOriginalValues = null; + private List mOriginalValues = null; /** * Create a filter for a list of folders. @@ -62,7 +62,7 @@ public class FolderListFilter extends Filter { Locale locale = Locale.getDefault(); if ((searchTerm == null) || (searchTerm.length() == 0)) { - ArrayList list = new ArrayList(mOriginalValues); + List list = new ArrayList(mOriginalValues); results.values = list; results.count = list.size(); } else { @@ -70,9 +70,9 @@ public class FolderListFilter extends Filter { final String[] words = searchTermString.split(" "); final int wordCount = words.length; - final ArrayList values = mOriginalValues; + final List values = mOriginalValues; - final ArrayList newValues = new ArrayList(); + final List newValues = new ArrayList(); for (final T value : values) { final String valueText = value.toString().toLowerCase(locale); diff --git a/src/com/fsck/k9/activity/MessageCompose.java b/src/com/fsck/k9/activity/MessageCompose.java index 9c473629a..5b975247c 100644 --- a/src/com/fsck/k9/activity/MessageCompose.java +++ b/src/com/fsck/k9/activity/MessageCompose.java @@ -5,9 +5,11 @@ import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.InputStream; import java.io.UnsupportedEncodingException; +import java.nio.charset.Charset; import java.text.DateFormat; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Date; import java.util.HashMap; import java.util.List; @@ -107,9 +109,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.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; @@ -190,8 +192,6 @@ public class MessageCompose extends K9Activity implements OnClickListener, private static final int CONTACT_PICKER_CC2 = 8; private static final int CONTACT_PICKER_BCC2 = 9; - private static final Account[] EMPTY_ACCOUNT_ARRAY = new Account[0]; - private static final int REQUEST_CODE_SIGN_ENCRYPT = 12; /** @@ -1012,7 +1012,7 @@ public class MessageCompose extends K9Activity implements OnClickListener, addAttachment(stream, type); } } else { - ArrayList list = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM); + List list = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM); if (list != null) { for (Parcelable parcelable : list) { Uri stream = (Uri) parcelable; @@ -1056,7 +1056,7 @@ public class MessageCompose extends K9Activity implements OnClickListener, } private boolean addRecipients(TextView view, List recipients) { - if (recipients == null || recipients.size() == 0) { + if (recipients == null || recipients.isEmpty()) { return false; } @@ -1198,7 +1198,7 @@ public class MessageCompose extends K9Activity implements OnClickListener, "\" from saved instance state", e); } - ArrayList attachments = savedInstanceState.getParcelableArrayList(STATE_KEY_ATTACHMENTS); + List attachments = savedInstanceState.getParcelableArrayList(STATE_KEY_ATTACHMENTS); for (Attachment attachment : attachments) { addAttachmentView(attachment); if (attachment.loaderId > mMaxLoaderId) { @@ -1807,7 +1807,7 @@ public class MessageCompose extends K9Activity implements OnClickListener, String[] emailsArray = null; if (mEncryptCheckbox.isChecked()) { // get emails as array - ArrayList emails = new ArrayList(); + List emails = new ArrayList(); for (Address address : getRecipientAddresses()) { emails.add(address.getAddress()); @@ -1890,13 +1890,7 @@ public class MessageCompose extends K9Activity implements OnClickListener, private InputStream getOpenPgpInputStream() { String text = buildText(false).getText(); - InputStream is = null; - try { - is = new ByteArrayInputStream(text.getBytes("UTF-8")); - } catch (UnsupportedEncodingException e) { - Log.e(K9.LOG_TAG, "UnsupportedEncodingException.", e); - } - return is; + return new ByteArrayInputStream(text.getBytes(Charset.forName("UTF-8"))); } private void executeOpenPgpMethod(Intent intent) { @@ -3921,7 +3915,7 @@ public class MessageCompose extends K9Activity implements OnClickListener, List items = new ArrayList(); Preferences prefs = Preferences.getPreferences(context.getApplicationContext()); - Account[] accounts = prefs.getAvailableAccounts().toArray(EMPTY_ACCOUNT_ARRAY); + Collection accounts = prefs.getAvailableAccounts(); for (Account account : accounts) { items.add(account); List identities = account.getIdentities(); diff --git a/src/com/fsck/k9/activity/MessageList.java b/src/com/fsck/k9/activity/MessageList.java index 3da206094..25306140f 100644 --- a/src/com/fsck/k9/activity/MessageList.java +++ b/src/com/fsck/k9/activity/MessageList.java @@ -451,10 +451,10 @@ public class MessageList extends K9FragmentActivity implements MessageListFragme String[] accountUuids = mSearch.getAccountUuids(); if (mSearch.searchAllAccounts()) { - Account[] accounts = prefs.getAccounts(); - mSingleAccountMode = (accounts.length == 1); + List accounts = prefs.getAccounts(); + mSingleAccountMode = (accounts.size() == 1); if (mSingleAccountMode) { - mAccount = accounts[0]; + mAccount = accounts.get(0); } } else { mSingleAccountMode = (accountUuids.length == 1); diff --git a/src/com/fsck/k9/activity/NotificationDeleteConfirmation.java b/src/com/fsck/k9/activity/NotificationDeleteConfirmation.java index a740768c6..fd6f87db5 100644 --- a/src/com/fsck/k9/activity/NotificationDeleteConfirmation.java +++ b/src/com/fsck/k9/activity/NotificationDeleteConfirmation.java @@ -1,6 +1,8 @@ package com.fsck.k9.activity; +import java.io.Serializable; import java.util.ArrayList; +import java.util.List; import android.app.Activity; import android.app.AlertDialog; @@ -14,6 +16,7 @@ import com.fsck.k9.Account; import com.fsck.k9.K9; import com.fsck.k9.Preferences; import com.fsck.k9.R; +import com.fsck.k9.helper.Utility; import com.fsck.k9.service.NotificationActionService; public class NotificationDeleteConfirmation extends Activity { @@ -25,7 +28,7 @@ public class NotificationDeleteConfirmation extends Activity { private Account mAccount; private ArrayList mMessageRefs; - public static PendingIntent getIntent(Context context, final Account account, final ArrayList refs) { + public static PendingIntent getIntent(Context context, final Account account, final Serializable refs) { Intent i = new Intent(context, NotificationDeleteConfirmation.class); i.putExtra(EXTRA_ACCOUNT, account.getUuid()); i.putExtra(EXTRA_MESSAGE_LIST, refs); 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..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.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/AccountSetupBasics.java b/src/com/fsck/k9/activity/setup/AccountSetupBasics.java index 861edea53..56d409743 100644 --- a/src/com/fsck/k9/activity/setup/AccountSetupBasics.java +++ b/src/com/fsck/k9/activity/setup/AccountSetupBasics.java @@ -3,10 +3,8 @@ package com.fsck.k9.activity.setup; import java.io.Serializable; -import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URISyntaxException; -import java.net.URLEncoder; import java.util.Locale; import android.app.AlertDialog; @@ -35,6 +33,7 @@ import com.fsck.k9.Preferences; import com.fsck.k9.R; import com.fsck.k9.activity.K9Activity; import com.fsck.k9.activity.setup.AccountSetupCheckSettings.CheckDirection; +import com.fsck.k9.helper.UrlEncodingHelper; import com.fsck.k9.helper.Utility; import com.fsck.k9.mail.AuthType; import com.fsck.k9.mail.ConnectionSecurity; @@ -281,8 +280,8 @@ public class AccountSetupBasics extends K9Activity URI incomingUri = null; URI outgoingUri = null; try { - String userEnc = URLEncoder.encode(user, "UTF-8"); - String passwordEnc = URLEncoder.encode(password, "UTF-8"); + String userEnc = UrlEncodingHelper.encodeUtf8(user); + String passwordEnc = UrlEncodingHelper.encodeUtf8(password); String incomingUsername = mProvider.incomingUsernameTemplate; incomingUsername = incomingUsername.replaceAll("\\$email", email); @@ -338,9 +337,6 @@ public class AccountSetupBasics extends K9Activity } // Check incoming here. Then check outgoing in onActivityResult() AccountSetupCheckSettings.actionCheckSettings(this, mAccount, CheckDirection.INCOMING); - } catch (UnsupportedEncodingException enc) { - // This really shouldn't happen since the encoding is hardcoded to UTF-8 - Log.e(K9.LOG_TAG, "Couldn't urlencode username or password.", enc); } catch (URISyntaxException use) { /* * If there is some problem with the URI we give up and go on to diff --git a/src/com/fsck/k9/activity/setup/FolderSettings.java b/src/com/fsck/k9/activity/setup/FolderSettings.java index 5cab0a26e..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.LocalStore; -import com.fsck.k9.mail.store.LocalStore.LocalFolder; +import com.fsck.k9.mail.store.local.LocalFolder; +import com.fsck.k9.mail.store.local.LocalStore; import com.fsck.k9.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..9b3e7d5e0 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.LocalFolder; +import com.fsck.k9.mail.store.local.LocalMessage; import com.fsck.k9.provider.EmailProvider; /** @@ -42,9 +42,9 @@ public class EmailProviderCache { private String mAccountUuid; - private Map> mMessageCache = new HashMap>(); - private Map> mThreadCache = new HashMap>(); - private Map mHiddenMessageCache = new HashMap(); + private final Map> mMessageCache = new HashMap>(); + private final Map> mThreadCache = new HashMap>(); + private final Map mHiddenMessageCache = new HashMap(); private EmailProviderCache(String accountUuid) { @@ -101,7 +101,7 @@ public class EmailProviderCache { Map map = mMessageCache.get(messageId); if (map != null) { map.remove(columnName); - if (map.size() == 0) { + if (map.isEmpty()) { mMessageCache.remove(messageId); } } @@ -115,7 +115,7 @@ public class EmailProviderCache { Map map = mThreadCache.get(threadRootId); if (map != null) { map.remove(columnName); - if (map.size() == 0) { + if (map.isEmpty()) { mThreadCache.remove(threadRootId); } } @@ -143,7 +143,7 @@ public class EmailProviderCache { } } - public void unhideMessages(Message[] messages) { + public void unhideMessages(List messages) { synchronized (mHiddenMessageCache) { for (Message message : messages) { LocalMessage localMessage = (LocalMessage) message; diff --git a/src/com/fsck/k9/cache/EmailProviderCacheCursor.java b/src/com/fsck/k9/cache/EmailProviderCacheCursor.java index aeb4bdf13..fdf75993a 100644 --- a/src/com/fsck/k9/cache/EmailProviderCacheCursor.java +++ b/src/com/fsck/k9/cache/EmailProviderCacheCursor.java @@ -108,7 +108,7 @@ public class EmailProviderCacheCursor extends CursorWrapper { @Override public boolean moveToPosition(int position) { - if (mHiddenRows.size() == 0) { + if (mHiddenRows.isEmpty()) { return super.moveToPosition(position); } @@ -126,7 +126,7 @@ public class EmailProviderCacheCursor extends CursorWrapper { @Override public int getPosition() { - if (mHiddenRows.size() == 0) { + if (mHiddenRows.isEmpty()) { return super.getPosition(); } diff --git a/src/com/fsck/k9/controller/MessagingController.java b/src/com/fsck/k9/controller/MessagingController.java index 8c1091227..07f2f5e05 100644 --- a/src/com/fsck/k9/controller/MessagingController.java +++ b/src/com/fsck/k9/controller/MessagingController.java @@ -2,12 +2,14 @@ package com.fsck.k9.controller; import java.io.CharArrayWriter; import java.io.PrintWriter; +import java.io.Serializable; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.Date; +import java.util.EnumSet; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; @@ -79,10 +81,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.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.PendingCommand; import com.fsck.k9.mail.store.Pop3Store; import com.fsck.k9.mail.store.UnavailableAccountException; import com.fsck.k9.mail.store.UnavailableStorageException; @@ -116,16 +118,6 @@ public class MessagingController implements Runnable { */ private static final String[] EMPTY_STRING_ARRAY = new String[0]; - /** - * Immutable empty {@link Message} array - */ - private static final Message[] EMPTY_MESSAGE_ARRAY = new Message[0]; - - /** - * Immutable empty {@link Folder} array - */ - private static final Folder[] EMPTY_FOLDER_ARRAY = new Folder[0]; - /** * The maximum message size that we'll consider to be "small". A small message is downloaded * in full immediately instead of in pieces. Anything over this size will be downloaded in @@ -295,17 +287,14 @@ public class MessagingController implements Runnable { } /** - * Gets a list of references for all pending messages for the notification. - * - * @return Message reference list + * Adds a list of references for all pending messages for the notification to the supplied + * List. */ - public ArrayList getAllMessageRefs() { - ArrayList refs = new ArrayList(); + public void supplyAllMessageRefs(List refs) { for (Message m : messages) { refs.add(m.makeMessageReference()); } refs.addAll(droppedMessages); - return refs; } /** @@ -319,10 +308,9 @@ public class MessagingController implements Runnable { }; // Key is accountNumber - private ConcurrentHashMap notificationData = new ConcurrentHashMap(); - - private static final Flag[] SYNC_FLAGS = new Flag[] { Flag.SEEN, Flag.FLAGGED, Flag.ANSWERED, Flag.FORWARDED }; + private final ConcurrentMap notificationData = new ConcurrentHashMap(); + private static final Set SYNC_FLAGS = EnumSet.of(Flag.SEEN, Flag.FLAGGED, Flag.ANSWERED, Flag.FORWARDED); private void suppressMessages(Account account, List messages) { EmailProviderCache cache = EmailProviderCache.getCache(account.getUuid(), @@ -330,7 +318,7 @@ public class MessagingController implements Runnable { cache.hideMessages(messages); } - private void unsuppressMessages(Account account, Message[] messages) { + private void unsuppressMessages(Account account, List messages) { EmailProviderCache cache = EmailProviderCache.getCache(account.getUuid(), mApplication.getApplicationContext()); cache.unhideMessages(messages); @@ -574,15 +562,13 @@ public class MessagingController implements Runnable { Store localStore = account.getLocalStore(); localFolders = localStore.getPersonalNamespaces(false); - Folder[] folderArray = localFolders.toArray(EMPTY_FOLDER_ARRAY); - if (refreshRemote || localFolders.isEmpty()) { doRefreshRemote(account, listener); return; } for (MessagingListener l : getListeners(listener)) { - l.listFolders(account, folderArray); + l.listFolders(account, localFolders); } } catch (Exception e) { for (MessagingListener l : getListeners(listener)) { @@ -654,10 +640,9 @@ public class MessagingController implements Runnable { } localFolders = localStore.getPersonalNamespaces(false); - Folder[] folderArray = localFolders.toArray(EMPTY_FOLDER_ARRAY); for (MessagingListener l : getListeners(listener)) { - l.listFolders(account, folderArray); + l.listFolders(account, localFolders); } for (MessagingListener l : getListeners(listener)) { l.listFoldersFinished(account); @@ -694,7 +679,7 @@ public class MessagingController implements Runnable { public void searchLocalMessagesSynchronous(final LocalSearch search, final MessagingListener listener) { final AccountStats stats = new AccountStats(); final Set uuidSet = new HashSet(Arrays.asList(search.getAccountUuids())); - Account[] accounts = Preferences.getPreferences(mApplication.getApplicationContext()).getAccounts(); + List accounts = Preferences.getPreferences(mApplication.getApplicationContext()).getAccounts(); boolean allAccounts = uuidSet.contains(SearchSpecification.ALL_ACCOUNTS); // for every account we want to search do the query in the localstore @@ -755,7 +740,7 @@ public class MessagingController implements Runnable { public Future searchRemoteMessages(final String acctUuid, final String folderName, final String query, - final Flag[] requiredFlags, final Flag[] forbiddenFlags, final MessagingListener listener) { + final Set requiredFlags, final Set forbiddenFlags, final MessagingListener listener) { if (K9.DEBUG) { String msg = "searchRemoteMessages (" + "acct=" + acctUuid @@ -773,7 +758,7 @@ public class MessagingController implements Runnable { }); } public void searchRemoteMessagesSynchronous(final String acctUuid, final String folderName, final String query, - final Flag[] requiredFlags, final Flag[] forbiddenFlags, final MessagingListener listener) { + final Set requiredFlags, final Set forbiddenFlags, final MessagingListener listener) { final Account acct = Preferences.getPreferences(mApplication.getApplicationContext()).getAccount(acctUuid); if (listener != null) { @@ -885,10 +870,10 @@ public class MessagingController implements Runnable { LocalMessage localMsg = localFolder.getMessage(message.getUid()); if (localMsg == null) { - remoteFolder.fetch(new Message [] {message}, header, null); + remoteFolder.fetch(Collections.singletonList(message), header, null); //fun fact: ImapFolder.fetch can't handle getting STRUCTURE at same time as headers - remoteFolder.fetch(new Message [] {message}, structure, null); - localFolder.appendMessages(new Message [] {message}); + remoteFolder.fetch(Collections.singletonList(message), structure, null); + localFolder.appendMessages(Collections.singletonList(message)); localMsg = localFolder.getMessage(message.getUid()); } @@ -992,8 +977,8 @@ public class MessagingController implements Runnable { final LocalFolder localFolder = tLocalFolder; localFolder.open(Folder.OPEN_MODE_RW); localFolder.updateLastUid(); - Message[] localMessages = localFolder.getMessages(null); - HashMap localUidMap = new HashMap(); + List localMessages = localFolder.getMessages(null); + Map localUidMap = new HashMap(); for (Message message : localMessages) { localUidMap.put(message.getUid(), message); } @@ -1058,9 +1043,8 @@ public class MessagingController implements Runnable { visibleLimit = K9.DEFAULT_VISIBLE_LIMIT; } - Message[] remoteMessageArray = EMPTY_MESSAGE_ARRAY; - final ArrayList remoteMessages = new ArrayList(); - HashMap remoteUidMap = new HashMap(); + final List remoteMessages = new ArrayList(); + Map remoteUidMap = new HashMap(); if (K9.DEBUG) Log.v(K9.LOG_TAG, "SYNC: Remote message count for folder " + folder + " is " + remoteMessageCount); @@ -1086,9 +1070,9 @@ public class MessagingController implements Runnable { } - remoteMessageArray = remoteFolder.getMessages(remoteStart, remoteEnd, earliestDate, null); + List remoteMessageArray = remoteFolder.getMessages(remoteStart, remoteEnd, earliestDate, null); - int messageCount = remoteMessageArray.length; + int messageCount = remoteMessageArray.size(); for (Message thisMess : remoteMessageArray) { headerProgress.incrementAndGet(); @@ -1104,7 +1088,6 @@ public class MessagingController implements Runnable { if (K9.DEBUG) Log.v(K9.LOG_TAG, "SYNC: Got " + remoteUidMap.size() + " messages for folder " + folder); - remoteMessageArray = null; for (MessagingListener l : getListeners(listener)) { l.synchronizeMailboxHeadersFinished(account, folder, headerProgress.get(), remoteUidMap.size()); } @@ -1117,7 +1100,7 @@ public class MessagingController implements Runnable { * Remove any messages that are in the local store but no longer on the remote store or are too old */ if (account.syncRemoteDeletions()) { - ArrayList destroyMessages = new ArrayList(); + List destroyMessages = new ArrayList(); for (Message localMessage : localMessages) { if (remoteUidMap.get(localMessage.getUid()) == null) { destroyMessages.add(localMessage); @@ -1125,7 +1108,7 @@ public class MessagingController implements Runnable { } - localFolder.destroyMessages(destroyMessages.toArray(EMPTY_MESSAGE_ARRAY)); + localFolder.destroyMessages(destroyMessages); for (Message destroyMessage : destroyMessages) { for (MessagingListener l : getListeners(listener)) { @@ -1280,7 +1263,7 @@ public class MessagingController implements Runnable { Log.e(K9.LOG_TAG, "Unable to getUnreadMessageCount for account: " + account, e); } - ArrayList syncFlagMessages = new ArrayList(); + List syncFlagMessages = new ArrayList(); List unsyncedMessages = new ArrayList(); final AtomicInteger newMessages = new AtomicInteger(0); @@ -1300,8 +1283,8 @@ public class MessagingController implements Runnable { Log.d(K9.LOG_TAG, "SYNC: Have " + unsyncedMessages.size() + " unsynced messages"); messages.clear(); - final ArrayList largeMessages = new ArrayList(); - final ArrayList smallMessages = new ArrayList(); + final List largeMessages = new ArrayList(); + final List smallMessages = new ArrayList(); if (!unsyncedMessages.isEmpty()) { /* @@ -1416,7 +1399,7 @@ public class MessagingController implements Runnable { final Folder remoteFolder, final Account account, final List unsyncedMessages, - final ArrayList syncFlagMessages, + final List syncFlagMessages, boolean flagSyncOnly) throws MessagingException { if (message.isSet(Flag.DELETED)) { syncFlagMessages.add(message); @@ -1437,7 +1420,7 @@ public class MessagingController implements Runnable { Log.v(K9.LOG_TAG, "Message with uid " + message.getUid() + " is partially or fully downloaded"); // Store the updated message locally - localFolder.appendMessages(new Message[] { message }); + localFolder.appendMessages(Collections.singletonList(message)); localMessage = localFolder.getMessage(message.getUid()); @@ -1475,8 +1458,8 @@ public class MessagingController implements Runnable { private void fetchUnsyncedMessages(final Account account, final Folder remoteFolder, final LocalFolder localFolder, List unsyncedMessages, - final ArrayList smallMessages, - final ArrayList largeMessages, + final List smallMessages, + final List largeMessages, final AtomicInteger progress, final int todo, FetchProfile fp) throws MessagingException { @@ -1489,7 +1472,7 @@ public class MessagingController implements Runnable { */ final List chunk = new ArrayList(UNSYNC_CHUNK_SIZE); - remoteFolder.fetch(unsyncedMessages.toArray(EMPTY_MESSAGE_ARRAY), fp, + remoteFolder.fetch(unsyncedMessages, fp, new MessageRetrievalListener() { @Override public void messageFinished(Message message, int number, int ofTotal) { @@ -1578,7 +1561,7 @@ public class MessagingController implements Runnable { } try { // Store the new message locally - localFolder.appendMessages(messages.toArray(new Message[messages.size()])); + localFolder.appendMessages(messages); for (final Message message : messages) { final Message localMessage = localFolder.getMessage(message.getUid()); @@ -1611,7 +1594,7 @@ public class MessagingController implements Runnable { private void downloadSmallMessages(final Account account, final Folder remoteFolder, final LocalFolder localFolder, - ArrayList smallMessages, + List smallMessages, final AtomicInteger progress, final int unreadBeforeStart, final AtomicInteger newMessages, @@ -1624,7 +1607,7 @@ public class MessagingController implements Runnable { if (K9.DEBUG) Log.d(K9.LOG_TAG, "SYNC: Fetching small messages for folder " + folder); - remoteFolder.fetch(smallMessages.toArray(new Message[smallMessages.size()]), + remoteFolder.fetch(smallMessages, fp, new MessageRetrievalListener() { @Override public void messageFinished(final Message message, int number, int ofTotal) { @@ -1690,7 +1673,7 @@ public class MessagingController implements Runnable { private void downloadLargeMessages(final Account account, final Folder remoteFolder, final LocalFolder localFolder, - ArrayList largeMessages, + List largeMessages, final AtomicInteger progress, final int unreadBeforeStart, final AtomicInteger newMessages, @@ -1703,7 +1686,7 @@ public class MessagingController implements Runnable { if (K9.DEBUG) Log.d(K9.LOG_TAG, "SYNC: Fetching large messages for folder " + folder); - remoteFolder.fetch(largeMessages.toArray(new Message[largeMessages.size()]), fp, null); + remoteFolder.fetch(largeMessages, fp, null); for (Message message : largeMessages) { if (!shouldImportMessage(account, folder, message, progress, earliestDate)) { @@ -1726,10 +1709,10 @@ public class MessagingController implements Runnable { * they equal we can mark this SYNCHRONIZED instead of PARTIALLY_SYNCHRONIZED */ - remoteFolder.fetch(new Message[] { message }, fp, null); + remoteFolder.fetch(Collections.singletonList(message), fp, null); // Store the updated message locally - localFolder.appendMessages(new Message[] { message }); + localFolder.appendMessages(Collections.singletonList(message)); Message localMessage = localFolder.getMessage(message.getUid()); @@ -1770,7 +1753,7 @@ public class MessagingController implements Runnable { remoteFolder.fetchPart(message, part, null); } // Store the updated message locally - localFolder.appendMessages(new Message[] { message }); + localFolder.appendMessages(Collections.singletonList(message)); Message localMessage = localFolder.getMessage(message.getUid()); @@ -1815,7 +1798,7 @@ public class MessagingController implements Runnable { private void refreshLocalMessageFlags(final Account account, final Folder remoteFolder, final LocalFolder localFolder, - ArrayList syncFlagMessages, + List syncFlagMessages, final AtomicInteger progress, final int todo ) throws MessagingException { @@ -1836,7 +1819,7 @@ public class MessagingController implements Runnable { } } - remoteFolder.fetch(undeletedMessages.toArray(EMPTY_MESSAGE_ARRAY), fp, null); + remoteFolder.fetch(undeletedMessages, fp, null); for (Message remoteMessage : syncFlagMessages) { Message localMessage = localFolder.getMessage(remoteMessage.getUid()); boolean messageChanged = syncFlags(localMessage, remoteMessage); @@ -1952,7 +1935,7 @@ public class MessagingController implements Runnable { private void processPendingCommandsSynchronous(Account account) throws MessagingException { LocalStore localStore = account.getLocalStore(); - ArrayList commands = localStore.getPendingCommands(); + List commands = localStore.getPendingCommands(); int progress = 0; int todo = commands.size(); @@ -2111,10 +2094,10 @@ public class MessagingController implements Runnable { */ FetchProfile fp = new FetchProfile(); fp.add(FetchProfile.Item.BODY); - localFolder.fetch(new Message[] { localMessage } , fp, null); + localFolder.fetch(Collections.singletonList(localMessage) , fp, null); String oldUid = localMessage.getUid(); localMessage.setFlag(Flag.X_REMOTE_COPY_STARTED, true); - remoteFolder.appendMessages(new Message[] { localMessage }); + remoteFolder.appendMessages(Collections.singletonList(localMessage)); localFolder.changeUid(localMessage); for (MessagingListener l : getListeners()) { @@ -2129,7 +2112,7 @@ public class MessagingController implements Runnable { */ FetchProfile fp = new FetchProfile(); fp.add(FetchProfile.Item.ENVELOPE); - remoteFolder.fetch(new Message[] { remoteMessage }, fp, null); + remoteFolder.fetch(Collections.singletonList(remoteMessage), fp, null); Date localDate = localMessage.getInternalDate(); Date remoteDate = remoteMessage.getInternalDate(); if (remoteDate != null && remoteDate.compareTo(localDate) > 0) { @@ -2146,12 +2129,12 @@ public class MessagingController implements Runnable { fp.clear(); fp = new FetchProfile(); fp.add(FetchProfile.Item.BODY); - localFolder.fetch(new Message[] { localMessage }, fp, null); + localFolder.fetch(Collections.singletonList(localMessage), fp, null); String oldUid = localMessage.getUid(); localMessage.setFlag(Flag.X_REMOTE_COPY_STARTED, true); - remoteFolder.appendMessages(new Message[] { localMessage }); + remoteFolder.appendMessages(Collections.singletonList(localMessage)); localFolder.changeUid(localMessage); for (MessagingListener l : getListeners()) { l.messageUidChanged(account, folder, oldUid, localMessage.getUid()); @@ -2325,14 +2308,14 @@ public class MessagingController implements Runnable { if (K9.FOLDER_NONE.equals(destFolderName)) { destFolderName = null; } - remoteSrcFolder.delete(messages.toArray(EMPTY_MESSAGE_ARRAY), destFolderName); + remoteSrcFolder.delete(messages, destFolderName); } else { remoteDestFolder = remoteStore.getFolder(destFolder); if (isCopy) { - remoteUidMap = remoteSrcFolder.copyMessages(messages.toArray(EMPTY_MESSAGE_ARRAY), remoteDestFolder); + remoteUidMap = remoteSrcFolder.copyMessages(messages, remoteDestFolder); } else { - remoteUidMap = remoteSrcFolder.moveMessages(messages.toArray(EMPTY_MESSAGE_ARRAY), remoteDestFolder); + remoteUidMap = remoteSrcFolder.moveMessages(messages, remoteDestFolder); } } if (!isCopy && Account.EXPUNGE_IMMEDIATELY.equals(account.getExpungePolicy())) { @@ -2425,7 +2408,7 @@ public class MessagingController implements Runnable { if (messages.isEmpty()) { return; } - remoteFolder.setFlags(messages.toArray(EMPTY_MESSAGE_ARRAY), new Flag[] { flag }, newState); + remoteFolder.setFlags(messages, Collections.singleton(flag), newState); } finally { closeFolder(remoteFolder); } @@ -2571,9 +2554,9 @@ public class MessagingController implements Runnable { } if (isCopy) { - remoteSrcFolder.copyMessages(new Message[] { remoteMessage }, remoteDestFolder); + remoteSrcFolder.copyMessages(Collections.singletonList(remoteMessage), remoteDestFolder); } else { - remoteSrcFolder.moveMessages(new Message[] { remoteMessage }, remoteDestFolder); + remoteSrcFolder.moveMessages(Collections.singletonList(remoteMessage), remoteDestFolder); } remoteSrcFolder.close(); remoteDestFolder.close(); @@ -2587,7 +2570,7 @@ public class MessagingController implements Runnable { Store localStore = account.getLocalStore(); localFolder = (LocalFolder) localStore.getFolder(folder); localFolder.open(Folder.OPEN_MODE_RW); - Message[] messages = localFolder.getMessages(null, false); + List messages = localFolder.getMessages(null, false); for (Message message : messages) { if (!message.isSet(Flag.SEEN)) { message.setFlag(Flag.SEEN, true); @@ -2617,7 +2600,7 @@ public class MessagingController implements Runnable { return; } - remoteFolder.setFlags(new Flag[] {Flag.SEEN}, true); + remoteFolder.setFlags(Collections.singleton(Flag.SEEN), true); remoteFolder.close(); } catch (UnsupportedOperationException uoe) { Log.w(K9.LOG_TAG, "Could not mark all server-side as read because store doesn't support operation", uoe); @@ -2727,10 +2710,8 @@ public class MessagingController implements Runnable { Store localStore = account.getLocalStore(); LocalFolder localFolder = (LocalFolder)localStore.getFolder(account.getErrorFolderName()); - Message[] messages = new Message[1]; MimeMessage message = new MimeMessage(); - message.setBody(new TextBody(body)); message.setFlag(Flag.X_DOWNLOADED_FULL, true); message.setSubject(subject); @@ -2740,9 +2721,8 @@ public class MessagingController implements Runnable { message.setInternalDate(nowDate); message.addSentDate(nowDate); message.setFrom(new Address(account.getEmail(), "K9mail internal")); - messages[0] = message; - localFolder.appendMessages(messages); + localFolder.appendMessages(Collections.singletonList(message)); localFolder.clearMessagesOlderThan(nowTime - (15 * 60 * 1000)); @@ -2874,7 +2854,7 @@ public class MessagingController implements Runnable { * @param newState * {@code true}, if the flag should be set. {@code false} if it should be removed. */ - public void setFlag(Account account, String folderName, Message[] messages, Flag flag, + public void setFlag(Account account, String folderName, List messages, Flag flag, boolean newState) { // TODO: Put this into the background, but right now some callers depend on the message // objects being modified right after this method returns. @@ -2896,7 +2876,7 @@ public class MessagingController implements Runnable { } // Update the messages in the local store - localFolder.setFlags(messages, new Flag[] {flag}, newState); + localFolder.setFlags(messages, Collections.singleton(flag), newState); int unreadMessageCount = localFolder.getUnreadMessageCount(); for (MessagingListener l : getListeners()) { @@ -2914,9 +2894,9 @@ public class MessagingController implements Runnable { return; } - String[] uids = new String[messages.length]; + String[] uids = new String[messages.size()]; for (int i = 0, end = uids.length; i < end; i++) { - uids[i] = messages[i].getUid(); + uids[i] = messages.get(i).getUid(); } queueSetFlag(account, folderName, Boolean.toString(newState), flag.toString(), uids); @@ -2953,7 +2933,7 @@ public class MessagingController implements Runnable { Message message = localFolder.getMessage(uid); if (message != null) { - setFlag(account, folderName, new Message[] { message }, flag, newState); + setFlag(account, folderName, Collections.singletonList(message), flag, newState); } } catch (MessagingException me) { addErrorMessage(account, null, me); @@ -3026,7 +3006,7 @@ public class MessagingController implements Runnable { FetchProfile fp = new FetchProfile(); fp.add(FetchProfile.Item.ENVELOPE); fp.add(FetchProfile.Item.BODY); - localFolder.fetch(new Message[] { message }, fp, null); + localFolder.fetch(Collections.singletonList(message), fp, null); } else { /* * At this point the message is not available, so we need to download it @@ -3042,16 +3022,16 @@ public class MessagingController implements Runnable { FetchProfile fp = new FetchProfile(); fp.add(FetchProfile.Item.BODY); - remoteFolder.fetch(new Message[] { remoteMessage }, fp, null); + remoteFolder.fetch(Collections.singletonList(remoteMessage), fp, null); // Store the message locally and load the stored message into memory - localFolder.appendMessages(new Message[] { remoteMessage }); + localFolder.appendMessages(Collections.singletonList(remoteMessage)); if (loadPartialFromSearch) { fp.add(FetchProfile.Item.BODY); } fp.add(FetchProfile.Item.ENVELOPE); message = localFolder.getMessage(uid); - localFolder.fetch(new Message[] { message }, fp, null); + localFolder.fetch(Collections.singletonList(message), fp, null); // Mark that this message is now fully synched if (account.isMarkMessageAsReadOnView()) { @@ -3124,9 +3104,7 @@ public class MessagingController implements Runnable { FetchProfile fp = new FetchProfile(); fp.add(FetchProfile.Item.ENVELOPE); fp.add(FetchProfile.Item.BODY); - localFolder.fetch(new Message[] { - message - }, fp, null); + localFolder.fetch(Collections.singletonList(message), fp, null); localFolder.close(); for (MessagingListener l : getListeners(listener)) { @@ -3266,7 +3244,7 @@ public class MessagingController implements Runnable { LocalStore localStore = account.getLocalStore(); LocalFolder localFolder = localStore.getFolder(account.getOutboxFolderName()); localFolder.open(Folder.OPEN_MODE_RW); - localFolder.appendMessages(new Message[] { message }); + localFolder.appendMessages(Collections.singletonList(message)); Message localMessage = localFolder.getMessage(message.getUid()); localMessage.setFlag(Flag.X_DOWNLOADED_FULL, true); localFolder.close(); @@ -3504,9 +3482,9 @@ public class MessagingController implements Runnable { } localFolder.open(Folder.OPEN_MODE_RW); - Message[] localMessages = localFolder.getMessages(null); + List localMessages = localFolder.getMessages(null); int progress = 0; - int todo = localMessages.length; + int todo = localMessages.size(); for (MessagingListener l : getListeners()) { l.synchronizeMailboxProgress(account, account.getSentFolderName(), progress, todo); } @@ -3545,7 +3523,7 @@ public class MessagingController implements Runnable { - localFolder.fetch(new Message[] { message }, fp, null); + localFolder.fetch(Collections.singletonList(message), fp, null); try { @@ -3576,7 +3554,7 @@ public class MessagingController implements Runnable { if (K9.DEBUG) Log.i(K9.LOG_TAG, "Moving sent message to folder '" + account.getSentFolderName() + "' (" + localSentFolder.getId() + ") "); - localFolder.moveMessages(new Message[] { message }, localSentFolder); + localFolder.moveMessages(Collections.singletonList(message), localSentFolder); if (K9.DEBUG) Log.i(K9.LOG_TAG, "Moved sent message to folder '" + account.getSentFolderName() + "' (" + localSentFolder.getId() + ") "); @@ -3594,7 +3572,7 @@ public class MessagingController implements Runnable { // This is a complete hack, but is worlds better than the previous // "don't even bother" functionality if (getRootCauseMessage(e).startsWith("5")) { - localFolder.moveMessages(new Message[] { message }, (LocalFolder) localStore.getFolder(account.getDraftsFolderName())); + localFolder.moveMessages(Collections.singletonList(message), (LocalFolder) localStore.getFolder(account.getDraftsFolderName())); } notifyUserIfCertificateProblem(mApplication, e, account, false); @@ -3679,14 +3657,14 @@ public class MessagingController implements Runnable { // Collect accounts that belong to the search String[] accountUuids = search.getAccountUuids(); - Account[] accounts; + List accounts; if (search.searchAllAccounts()) { accounts = preferences.getAccounts(); } else { - accounts = new Account[accountUuids.length]; + accounts = new ArrayList(accountUuids.length); for (int i = 0, len = accountUuids.length; i < len; i++) { String accountUuid = accountUuids[i]; - accounts[i] = preferences.getAccount(accountUuid); + accounts.set(i, preferences.getAccount(accountUuid)); } } @@ -3895,8 +3873,8 @@ public class MessagingController implements Runnable { } } - Message[] messages = localSrcFolder.getMessages(uids.toArray(EMPTY_STRING_ARRAY), null); - if (messages.length > 0) { + List messages = localSrcFolder.getMessages(uids.toArray(EMPTY_STRING_ARRAY), null); + if (messages.size() > 0) { Map origUidMap = new HashMap(); for (Message message : messages) { @@ -3905,7 +3883,7 @@ public class MessagingController implements Runnable { if (K9.DEBUG) Log.i(K9.LOG_TAG, "moveOrCopyMessageSynchronous: source folder = " + srcFolder - + ", " + messages.length + " messages, " + ", destination folder = " + destFolder + ", isCopy = " + isCopy); + + ", " + messages.size() + " messages, " + ", destination folder = " + destFolder + ", isCopy = " + isCopy); if (isCopy) { FetchProfile fp = new FetchProfile(); @@ -4014,7 +3992,7 @@ public class MessagingController implements Runnable { List messagesToDelete = collectMessagesInThreads(account, messages); deleteMessagesSynchronous(account, folderName, - messagesToDelete.toArray(EMPTY_MESSAGE_ARRAY), null); + messagesToDelete, null); } catch (MessagingException e) { Log.e(K9.LOG_TAG, "Something went wrong while deleting threads", e); } @@ -4031,8 +4009,9 @@ public class MessagingController implements Runnable { long rootId = localMessage.getRootId(); long threadId = (rootId == -1) ? localMessage.getThreadId() : rootId; - Message[] messagesInThread = localStore.getMessagesInThread(threadId); - Collections.addAll(messagesInThreads, messagesInThread); + List messagesInThread = localStore.getMessagesInThread(threadId); + + messagesInThreads.addAll(messagesInThread); } return messagesInThreads; @@ -4050,7 +4029,7 @@ public class MessagingController implements Runnable { @Override public void run() { deleteMessagesSynchronous(account, folder.getName(), - accountMessages.toArray(EMPTY_MESSAGE_ARRAY), listener); + accountMessages, listener); } }); } @@ -4059,7 +4038,7 @@ public class MessagingController implements Runnable { } - private void deleteMessagesSynchronous(final Account account, final String folder, final Message[] messages, + private void deleteMessagesSynchronous(final Account account, final String folder, final List messages, MessagingListener listener) { Folder localFolder = null; Folder localTrashFolder = null; @@ -4079,7 +4058,7 @@ public class MessagingController implements Runnable { if (K9.DEBUG) Log.d(K9.LOG_TAG, "Deleting messages in trash folder or trash set to -None-, not copying"); - localFolder.setFlags(messages, new Flag[] { Flag.DELETED }, true); + localFolder.setFlags(messages, Collections.singleton(Flag.DELETED), true); } else { localTrashFolder = localStore.getFolder(account.getTrashFolderName()); if (!localTrashFolder.exists()) { @@ -4147,10 +4126,10 @@ public class MessagingController implements Runnable { } } - private String[] getUidsFromMessages(Message[] messages) { - String[] uids = new String[messages.length]; - for (int i = 0; i < messages.length; i++) { - uids[i] = messages[i].getUid(); + private String[] getUidsFromMessages(List messages) { + String[] uids = new String[messages.size()]; + for (int i = 0; i < messages.size(); i++) { + uids[i] = messages.get(i).getUid(); } return uids; } @@ -4162,7 +4141,7 @@ public class MessagingController implements Runnable { try { if (remoteFolder.exists()) { remoteFolder.open(Folder.OPEN_MODE_RW); - remoteFolder.setFlags(new Flag [] { Flag.DELETED }, true); + remoteFolder.setFlags(Collections.singleton(Flag.DELETED), true); if (Account.EXPUNGE_IMMEDIATELY.equals(account.getExpungePolicy())) { remoteFolder.expunge(); } @@ -4193,7 +4172,7 @@ public class MessagingController implements Runnable { if (isTrashLocalOnly) { localFolder.clearAllMessages(); } else { - localFolder.setFlags(new Flag[] { Flag.DELETED }, true); + localFolder.setFlags(Collections.singleton(Flag.DELETED), true); } for (MessagingListener l : getListeners()) { @@ -4863,7 +4842,8 @@ public class MessagingController implements Runnable { String accountDescr = (account.getDescription() != null) ? account.getDescription() : account.getEmail(); - final ArrayList allRefs = data.getAllMessageRefs(); + final ArrayList allRefs = new ArrayList(); + data.supplyAllMessageRefs(allRefs); if (platformSupportsExtendedNotifications() && !privacyModeEnabled) { if (newMessages > 1) { @@ -5030,7 +5010,7 @@ public class MessagingController implements Runnable { } private boolean skipAccountsInBackStack(Context context) { - return Preferences.getPreferences(context).getAccounts().length == 1; + return Preferences.getPreferences(context).getAccounts().size() == 1; } /** @@ -5112,9 +5092,7 @@ public class MessagingController implements Runnable { } // Save the message to the store. - localFolder.appendMessages(new Message[] { - message - }); + localFolder.appendMessages(Collections.singletonList(message)); // Fetch the message back from the store. This is the Message that's returned to the caller. localMessage = localFolder.getMessage(message.getUid()); localMessage.setFlag(Flag.X_DOWNLOADED_FULL, true); @@ -5424,7 +5402,7 @@ public class MessagingController implements Runnable { return taccount.getDescription() + ":" + tfolderName; } static class MemorizingListener extends MessagingListener { - HashMap memories = new HashMap(31); + Map memories = new HashMap(31); Memory getMemory(Account account, String folderName) { Memory memory = memories.get(getMemoryKey(account, folderName)); diff --git a/src/com/fsck/k9/controller/MessagingControllerPushReceiver.java b/src/com/fsck/k9/controller/MessagingControllerPushReceiver.java index 0a2a1711b..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.LocalStore; -import com.fsck.k9.mail.store.LocalStore.LocalFolder; +import com.fsck.k9.mail.store.local.LocalFolder; +import com.fsck.k9.mail.store.local.LocalStore; import com.fsck.k9.service.SleepService; import java.util.List; diff --git a/src/com/fsck/k9/controller/MessagingListener.java b/src/com/fsck/k9/controller/MessagingListener.java index 6fce2c6e2..5c5a66f50 100644 --- a/src/com/fsck/k9/controller/MessagingListener.java +++ b/src/com/fsck/k9/controller/MessagingListener.java @@ -33,7 +33,7 @@ public class MessagingListener { public void listFoldersStarted(Account account) {} - public void listFolders(Account account, Folder[] folders) {} + public void listFolders(Account account, List folders) {} public void listFoldersFinished(Account account) {} diff --git a/src/com/fsck/k9/fragment/MessageListFragment.java b/src/com/fsck/k9/fragment/MessageListFragment.java index 3648e8fe3..f6bd09f74 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.LocalFolder; +import com.fsck.k9.mail.store.local.LocalStore; import com.fsck.k9.provider.EmailProvider; import com.fsck.k9.provider.EmailProvider.MessageColumns; import com.fsck.k9.provider.EmailProvider.SpecialColumns; @@ -960,16 +960,16 @@ public class MessageListFragment extends Fragment implements OnItemClickListener accountUuids[0].equals(SearchSpecification.ALL_ACCOUNTS)) { mAllAccounts = true; - Account[] accounts = mPreferences.getAccounts(); + List accounts = mPreferences.getAccounts(); - mAccountUuids = new String[accounts.length]; - for (int i = 0, len = accounts.length; i < len; i++) { - mAccountUuids[i] = accounts[i].getUuid(); + mAccountUuids = new String[accounts.size()]; + for (int i = 0, len = accounts.size(); i < len; i++) { + mAccountUuids[i] = accounts.get(i).getUuid(); } if (mAccountUuids.length == 1) { mSingleAccountMode = true; - mAccount = accounts[0]; + mAccount = accounts.get(0); } } else { mAccountUuids = accountUuids; @@ -1084,11 +1084,11 @@ public class MessageListFragment extends Fragment implements OnItemClickListener mController.addListener(mListener); //Cancel pending new mail notifications when we open an account - Account[] accountsWithNotification; + List accountsWithNotification; Account account = mAccount; if (account != null) { - accountsWithNotification = new Account[] { account }; + accountsWithNotification = Collections.singletonList(account); } else { accountsWithNotification = mPreferences.getAccounts(); } @@ -1814,7 +1814,7 @@ public class MessageListFragment extends Fragment implements OnItemClickListener } List folderNames = mSearch.getFolderNames(); - return (folderNames.size() == 0 || folderNames.contains(folder)); + return (folderNames.isEmpty() || folderNames.contains(folder)); } } @@ -2362,7 +2362,7 @@ public class MessageListFragment extends Fragment implements OnItemClickListener } private void setFlagForSelected(final Flag flag, final boolean newState) { - if (mSelected.size() == 0) { + if (mSelected.isEmpty()) { return; } @@ -2586,7 +2586,7 @@ public class MessageListFragment extends Fragment implements OnItemClickListener private boolean checkCopyOrMovePossible(final List messages, final FolderOperation operation) { - if (messages.size() == 0) { + if (messages.isEmpty()) { return false; } @@ -2995,8 +2995,8 @@ public class MessageListFragment extends Fragment implements OnItemClickListener super.onStop(); } - public ArrayList getMessageReferences() { - ArrayList messageRefs = new ArrayList(); + public List getMessageReferences() { + List messageRefs = new ArrayList(); for (int i = 0, len = mAdapter.getCount(); i < len; i++) { Cursor cursor = (Cursor) mAdapter.getItem(i); @@ -3515,7 +3515,7 @@ public class MessageListFragment extends Fragment implements OnItemClickListener } private void cleanupSelected(Cursor cursor) { - if (mSelected.size() == 0) { + if (mSelected.isEmpty()) { return; } @@ -3534,7 +3534,7 @@ public class MessageListFragment extends Fragment implements OnItemClickListener * Starts or finishes the action mode when necessary. */ private void resetActionMode() { - if (mSelected.size() == 0) { + if (mSelected.isEmpty()) { if (mActionMode != null) { mActionMode.finish(); } diff --git a/src/com/fsck/k9/fragment/MessageViewFragment.java b/src/com/fsck/k9/fragment/MessageViewFragment.java index 5f267bb64..5af307260 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.LocalMessage; import com.fsck.k9.view.AttachmentView; import com.fsck.k9.view.AttachmentView.AttachmentFileDownloadCallback; import com.fsck.k9.view.MessageHeader; @@ -369,7 +369,7 @@ public class MessageViewFragment extends Fragment implements OnClickListener, if (mMessage != null) { boolean newState = !mMessage.isSet(Flag.FLAGGED); mController.setFlag(mAccount, mMessage.getFolder().getName(), - new Message[] { mMessage }, Flag.FLAGGED, newState); + Collections.singletonList(mMessage), Flag.FLAGGED, newState); mMessageView.setHeaders(mMessage, mAccount); } } @@ -485,7 +485,7 @@ public class MessageViewFragment extends Fragment implements OnClickListener, public void onToggleRead() { if (mMessage != null) { mController.setFlag(mAccount, mMessage.getFolder().getName(), - new Message[] { mMessage }, Flag.SEEN, !mMessage.isSet(Flag.SEEN)); + Collections.singletonList(mMessage), Flag.SEEN, !mMessage.isSet(Flag.SEEN)); mMessageView.setHeaders(mMessage, mAccount); String subject = mMessage.getSubject(); displayMessageSubject(subject); diff --git a/src/com/fsck/k9/helper/Contacts.java b/src/com/fsck/k9/helper/Contacts.java index 56866c21c..022139f89 100644 --- a/src/com/fsck/k9/helper/Contacts.java +++ b/src/com/fsck/k9/helper/Contacts.java @@ -13,6 +13,7 @@ import com.fsck.k9.K9; import com.fsck.k9.mail.Address; import java.util.ArrayList; +import java.util.List; /** * Helper class to access the contacts stored on the device. @@ -276,7 +277,7 @@ public class Contacts { */ public ContactItem extractInfoFromContactPickerIntent(final Intent intent) { Cursor cursor = null; - ArrayList email = new ArrayList(); + List email = new ArrayList(); try { Uri result = intent.getData(); @@ -301,7 +302,7 @@ public class Contacts { } // Return 'null' if no email addresses have been found - if (email.size() == 0) { + if (email.isEmpty()) { return null; } diff --git a/src/com/fsck/k9/helper/UrlEncodingHelper.java b/src/com/fsck/k9/helper/UrlEncodingHelper.java new file mode 100644 index 000000000..2e6f1d591 --- /dev/null +++ b/src/com/fsck/k9/helper/UrlEncodingHelper.java @@ -0,0 +1,33 @@ +package com.fsck.k9.helper; + +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.net.URLEncoder; + +/** + * Wraps the java.net.URLDecoder to avoid unhelpful checked exceptions. + */ +public class UrlEncodingHelper { + + public static String decodeUtf8(String s) { + try { + return URLDecoder.decode(s, "UTF-8"); + } catch (UnsupportedEncodingException e) { + /* + * This is impossible, UTF-8 is always supported + */ + throw new RuntimeException("UTF-8 not found"); + } + } + + public static String encodeUtf8(String s) { + try { + return URLEncoder.encode(s, "UTF-8"); + } catch (UnsupportedEncodingException e) { + /* + * This is impossible, UTF-8 is always supported + */ + throw new RuntimeException("UTF-8 not found"); + } + } +} diff --git a/src/com/fsck/k9/helper/Utility.java b/src/com/fsck/k9/helper/Utility.java index a5bea3448..370920d59 100644 --- a/src/com/fsck/k9/helper/Utility.java +++ b/src/com/fsck/k9/helper/Utility.java @@ -20,10 +20,13 @@ import com.fsck.k9.mail.filter.Base64; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; -import java.io.UnsupportedEncodingException; +import java.io.Serializable; +import java.nio.charset.Charset; import java.util.ArrayList; import java.util.List; import java.util.Locale; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -102,6 +105,22 @@ public class Utility { return TextUtils.join(String.valueOf(separator), parts); } + /** + * Combines the given Objects into a single String using + * each Object's toString() method and the separator character + * between each part. + * + * @param parts + * @param separator + * @return new String + */ + public static String combine(Iterable parts, char separator) { + if (parts == null) { + return null; + } + return TextUtils.join(String.valueOf(separator), parts); + } + public static String base64Decode(String encoded) { if (encoded == null) { return null; @@ -189,8 +208,8 @@ public class Utility { * hundreds of times in places that slow down the UI, so it helps. */ public static String fastUrlDecode(String s) { - try { - byte[] bytes = s.getBytes("UTF-8"); + + byte[] bytes = s.getBytes(Charset.forName("UTF-8")); byte ch; int length = 0; for (int i = 0, count = bytes.length; i < count; i++) { @@ -213,10 +232,8 @@ public class Utility { } length++; } - return new String(bytes, 0, length, "UTF-8"); - } catch (UnsupportedEncodingException uee) { - return null; - } + return new String(bytes, 0, length, Charset.forName("UTF-8")); + } /* @@ -706,4 +723,5 @@ public class Utility { } return sMainThreadHandler; } + } diff --git a/src/com/fsck/k9/mail/Address.java b/src/com/fsck/k9/mail/Address.java index a8ef1ee0c..ef79b3790 100644 --- a/src/com/fsck/k9/mail/Address.java +++ b/src/com/fsck/k9/mail/Address.java @@ -44,9 +44,9 @@ public class Address { */ private static final Address[] EMPTY_ADDRESS_ARRAY = new Address[0]; - String mAddress; + private String mAddress; - String mPersonal; + private String mPersonal; public Address(Address address) { @@ -315,7 +315,7 @@ public class Address { if (addressList == null) { return new Address[] { }; } - ArrayList
    addresses = new ArrayList
    (); + List
    addresses = new ArrayList
    (); int length = addressList.length(); int pairStartIndex = 0; int pairEndIndex = 0; diff --git a/src/com/fsck/k9/mail/Body.java b/src/com/fsck/k9/mail/Body.java index 7b18a75a7..93960892c 100644 --- a/src/com/fsck/k9/mail/Body.java +++ b/src/com/fsck/k9/mail/Body.java @@ -8,7 +8,21 @@ import java.io.OutputStream; import com.fsck.k9.mail.store.UnavailableStorageException; public interface Body { + /** + * Returns the raw data of the body, without transfer encoding etc applied. + * TODO perhaps it would be better to have an intermediate "simple part" class where this method could reside + * because it makes no sense for multiparts + */ public InputStream getInputStream() throws MessagingException; + + /** + * Sets the content transfer encoding (7bit, 8bit, quoted-printable or base64). + */ public void setEncoding(String encoding) throws UnavailableStorageException, MessagingException; + + /** + * Writes the body's data to the given {@link OutputStream}. + * The written data is transfer encoded (e.g. transformed to Base64 when needed). + */ public void writeTo(OutputStream out) throws IOException, MessagingException; } diff --git a/src/com/fsck/k9/mail/BodyPart.java b/src/com/fsck/k9/mail/BodyPart.java index 163fe7ba6..1118304ba 100644 --- a/src/com/fsck/k9/mail/BodyPart.java +++ b/src/com/fsck/k9/mail/BodyPart.java @@ -14,5 +14,6 @@ public abstract class BodyPart implements Part { public abstract void setEncoding(String encoding) throws MessagingException; + @Override public abstract void setUsing7bitTransport() throws MessagingException; } diff --git a/src/com/fsck/k9/mail/CertificateChainException.java b/src/com/fsck/k9/mail/CertificateChainException.java index f7ce58f4f..c76145607 100644 --- a/src/com/fsck/k9/mail/CertificateChainException.java +++ b/src/com/fsck/k9/mail/CertificateChainException.java @@ -13,14 +13,8 @@ public class CertificateChainException extends CertificateException { private static final long serialVersionUID = 1103894512106650107L; private X509Certificate[] mCertChain; - public CertificateChainException(String msg, X509Certificate[] chain) { - super(msg); - setCertChain(chain); - } - - public CertificateChainException(CertificateException ce, - X509Certificate[] chain) { - super.initCause(ce); + public CertificateChainException(String msg, X509Certificate[] chain, Throwable cause) { + super(msg, cause); setCertChain(chain); } diff --git a/src/com/fsck/k9/mail/CompositeBody.java b/src/com/fsck/k9/mail/CompositeBody.java index 88a1996e1..affc792b3 100644 --- a/src/com/fsck/k9/mail/CompositeBody.java +++ b/src/com/fsck/k9/mail/CompositeBody.java @@ -23,7 +23,6 @@ public interface CompositeBody extends Body { * @throws MessagingException * */ - public abstract void setUsing7bitTransport() throws MessagingException; } diff --git a/src/com/fsck/k9/mail/Folder.java b/src/com/fsck/k9/mail/Folder.java index d4bebf7da..88af1a31a 100644 --- a/src/com/fsck/k9/mail/Folder.java +++ b/src/com/fsck/k9/mail/Folder.java @@ -1,8 +1,10 @@ package com.fsck.k9.mail; +import java.util.Collection; import java.util.Date; import java.util.List; import java.util.Map; +import java.util.Set; import android.util.Log; import com.fsck.k9.Account; @@ -92,7 +94,7 @@ public abstract class Folder { * @return List of messages * @throws MessagingException */ - public abstract Message[] getMessages(int start, int end, Date earliestDate, MessageRetrievalListener listener) throws MessagingException; + public abstract List getMessages(int start, int end, Date earliestDate, MessageRetrievalListener listener) throws MessagingException; /** * Fetches the given list of messages. The specified listener is notified as @@ -102,36 +104,36 @@ public abstract class Folder { * @param listener Listener to notify as we download messages. * @return List of messages */ - public abstract Message[] getMessages(MessageRetrievalListener listener) throws MessagingException; + public abstract List getMessages(MessageRetrievalListener listener) throws MessagingException; - public Message[] getMessages(MessageRetrievalListener listener, boolean includeDeleted) throws MessagingException { + public List getMessages(MessageRetrievalListener listener, boolean includeDeleted) throws MessagingException { return getMessages(listener); } - public abstract Message[] getMessages(String[] uids, MessageRetrievalListener listener) + public abstract List getMessages(String[] uids, MessageRetrievalListener listener) throws MessagingException; - public abstract Map appendMessages(Message[] messages) throws MessagingException; + public abstract Map appendMessages(List messages) throws MessagingException; - public Map copyMessages(Message[] msgs, Folder folder) throws MessagingException { + public Map copyMessages(List msgs, Folder folder) throws MessagingException { return null; } - public Map moveMessages(Message[] msgs, Folder folder) throws MessagingException { + public Map moveMessages(List msgs, Folder folder) throws MessagingException { return null; } - public void delete(Message[] msgs, String trashFolderName) throws MessagingException { + public void delete(List msgs, String trashFolderName) throws MessagingException { for (Message message : msgs) { Message myMessage = getMessage(message.getUid()); myMessage.delete(trashFolderName); } } - public abstract void setFlags(Message[] messages, Flag[] flags, boolean value) + public abstract void setFlags(List messages, Set flags, boolean value) throws MessagingException; - public abstract void setFlags(Flag[] flags, boolean value) throws MessagingException; + public abstract void setFlags(Set flags, boolean value) throws MessagingException; public abstract String getUidFromMessageId(Message message) throws MessagingException; @@ -146,7 +148,7 @@ public abstract class Folder { * @param listener Listener to notify as we fetch messages. * @throws MessagingException */ - public abstract void fetch(Message[] messages, FetchProfile fp, + public abstract void fetch(List messages, FetchProfile fp, MessageRetrievalListener listener) throws MessagingException; public void fetchPart(Message message, Part part, @@ -243,7 +245,7 @@ public abstract class Folder { return mAccount; } - public List search(String queryString, final Flag[] requiredFlags, final Flag[] forbiddenFlags) + public List search(String queryString, final Set requiredFlags, final Set forbiddenFlags) throws MessagingException { throw new MessagingException("K-9 does not support searches on this folder type"); } diff --git a/src/com/fsck/k9/mail/Message.java b/src/com/fsck/k9/mail/Message.java index 69d65e763..5a4d7a34a 100644 --- a/src/com/fsck/k9/mail/Message.java +++ b/src/com/fsck/k9/mail/Message.java @@ -2,7 +2,10 @@ package com.fsck.k9.mail; import java.io.IOException; +import java.util.Collection; +import java.util.Collections; import java.util.Date; +import java.util.EnumSet; import java.util.HashSet; import java.util.Set; @@ -16,7 +19,6 @@ import com.fsck.k9.mail.store.UnavailableStorageException; public abstract class Message implements Part, CompositeBody { - private static final Flag[] EMPTY_FLAG_ARRAY = new Flag[0]; private MessageReference mReference = null; @@ -26,9 +28,9 @@ public abstract class Message implements Part, CompositeBody { protected String mUid; - protected Set mFlags = new HashSet(); + private Set mFlags = EnumSet.noneOf(Flag.class); - protected Date mInternalDate; + private Date mInternalDate; protected Folder mFolder; @@ -45,6 +47,7 @@ public abstract class Message implements Part, CompositeBody { } return false; } + @Override public boolean equals(Object o) { if (o == null || !(o instanceof Message)) { @@ -123,20 +126,27 @@ public abstract class Message implements Part, CompositeBody { public abstract void setReferences(String references) throws MessagingException; + @Override public abstract Body getBody(); + @Override public abstract String getContentType() throws MessagingException; + @Override public abstract void addHeader(String name, String value) throws MessagingException; + @Override public abstract void setHeader(String name, String value) throws MessagingException; + @Override public abstract String[] getHeader(String name) throws MessagingException; public abstract Set getHeaderNames() throws UnavailableStorageException; + @Override public abstract void removeHeader(String name) throws MessagingException; + @Override public abstract void setBody(Body body) throws MessagingException; public abstract long getId(); @@ -144,6 +154,8 @@ public abstract class Message implements Part, CompositeBody { public abstract String getPreview(); public abstract boolean hasAttachments(); + public abstract int getSize(); + /* * calculateContentPreview * Takes a plain text message body as a string. @@ -198,8 +210,8 @@ public abstract class Message implements Part, CompositeBody { /* * TODO Refactor Flags at some point to be able to store user defined flags. */ - public Flag[] getFlags() { - return mFlags.toArray(EMPTY_FLAG_ARRAY); + public Set getFlags() { + return Collections.unmodifiableSet(mFlags); } /** @@ -223,7 +235,7 @@ public abstract class Message implements Part, CompositeBody { * @param flags * @param set */ - public void setFlags(Flag[] flags, boolean set) throws MessagingException { + public void setFlags(final Set flags, boolean set) throws MessagingException { for (Flag flag : flags) { setFlag(flag, set); } @@ -236,6 +248,7 @@ public abstract class Message implements Part, CompositeBody { public void destroy() throws MessagingException {} + @Override public abstract void setEncoding(String encoding) throws UnavailableStorageException, MessagingException; public abstract void setCharset(String charset) throws MessagingException; @@ -279,7 +292,7 @@ public abstract class Message implements Part, CompositeBody { destination.mReference = mReference; // mFlags contents can change during the object lifetime, so copy the Set - destination.mFlags = new HashSet(mFlags); + destination.mFlags = EnumSet.copyOf(mFlags); } /** @@ -293,6 +306,8 @@ public abstract class Message implements Part, CompositeBody { * for more information. *

    */ + @Override public abstract Message clone(); + @Override public abstract void setUsing7bitTransport() throws MessagingException; } diff --git a/src/com/fsck/k9/mail/MessagingException.java b/src/com/fsck/k9/mail/MessagingException.java index b5f563a1b..bdb24ab42 100644 --- a/src/com/fsck/k9/mail/MessagingException.java +++ b/src/com/fsck/k9/mail/MessagingException.java @@ -4,7 +4,7 @@ package com.fsck.k9.mail; public class MessagingException extends Exception { public static final long serialVersionUID = -1; - boolean permanentFailure = false; + private boolean permanentFailure = false; public MessagingException(String message) { super(message); @@ -28,9 +28,9 @@ public class MessagingException extends Exception { return permanentFailure; } + //TODO setters in Exception are bad style, remove (it's nearly unused anyway) public void setPermanentFailure(boolean permanentFailure) { this.permanentFailure = permanentFailure; } - } diff --git a/src/com/fsck/k9/mail/Multipart.java b/src/com/fsck/k9/mail/Multipart.java index cab1fa280..567b0902d 100644 --- a/src/com/fsck/k9/mail/Multipart.java +++ b/src/com/fsck/k9/mail/Multipart.java @@ -2,6 +2,8 @@ package com.fsck.k9.mail; import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import org.apache.james.mime4j.util.MimeUtil; @@ -9,26 +11,25 @@ import com.fsck.k9.mail.internet.MimeUtility; import com.fsck.k9.mail.internet.TextBody; public abstract class Multipart implements CompositeBody { - protected Part mParent; + private Part mParent; - protected ArrayList mParts = new ArrayList(); + private final List mParts = new ArrayList(); - protected String mContentType; + private String mContentType; public void addBodyPart(BodyPart part) { mParts.add(part); part.setParent(this); } - public void addBodyPart(BodyPart part, int index) { - mParts.add(index, part); - part.setParent(this); - } - public BodyPart getBodyPart(int index) { return mParts.get(index); } + public List getBodyParts() { + return Collections.unmodifiableList(mParts); + } + public String getContentType() { return mContentType; } @@ -37,16 +38,6 @@ public abstract class Multipart implements CompositeBody { return mParts.size(); } - public boolean removeBodyPart(BodyPart part) { - part.setParent(null); - return mParts.remove(part); - } - - public void removeBodyPart(int index) { - mParts.get(index).setParent(null); - mParts.remove(index); - } - public Part getParent() { return mParent; } @@ -55,6 +46,7 @@ public abstract class Multipart implements CompositeBody { this.mParent = parent; } + @Override public void setEncoding(String encoding) throws MessagingException { if (!MimeUtil.ENC_7BIT.equalsIgnoreCase(encoding) && !MimeUtil.ENC_8BIT.equalsIgnoreCase(encoding)) { diff --git a/src/com/fsck/k9/mail/Part.java b/src/com/fsck/k9/mail/Part.java index 731861b76..38c1ad9da 100644 --- a/src/com/fsck/k9/mail/Part.java +++ b/src/com/fsck/k9/mail/Part.java @@ -21,8 +21,6 @@ public interface Part { public String[] getHeader(String name) throws MessagingException; - public int getSize(); - public boolean isMimeType(String mimeType) throws MessagingException; public String getMimeType() throws MessagingException; @@ -42,5 +40,6 @@ public interface Part { * @throws MessagingException * */ + //TODO perhaps it would be clearer to use a flag "force7bit" in writeTo public abstract void setUsing7bitTransport() throws MessagingException; } diff --git a/src/com/fsck/k9/mail/Store.java b/src/com/fsck/k9/mail/Store.java index 45efb2a34..7dbfade7e 100644 --- a/src/com/fsck/k9/mail/Store.java +++ b/src/com/fsck/k9/mail/Store.java @@ -3,7 +3,9 @@ package com.fsck.k9.mail; import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; import android.app.Application; import android.content.Context; @@ -12,9 +14,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; @@ -33,19 +35,19 @@ public abstract class Store { /** * Remote stores indexed by Uri. */ - private static HashMap sStores = new HashMap(); + private static Map sStores = new HashMap(); /** * Local stores indexed by UUID because the Uri may change due to migration to/from SD-card. */ - private static ConcurrentHashMap sLocalStores = new ConcurrentHashMap(); + private static ConcurrentMap sLocalStores = new ConcurrentHashMap(); /** * Lock objects indexed by account UUID. * * @see #getLocalInstance(Account, Application) */ - private static ConcurrentHashMap sAccountLocks = new ConcurrentHashMap(); + private static ConcurrentMap sAccountLocks = new ConcurrentHashMap(); /** * Get an instance of a remote mail store. @@ -242,7 +244,7 @@ public abstract class Store { return true; } - public void sendMessages(Message[] messages) throws MessagingException { + public void sendMessages(List messages) throws MessagingException { } public Pusher getPusher(PushReceiver receiver) { diff --git a/src/com/fsck/k9/mail/filter/Base64.java b/src/com/fsck/k9/mail/filter/Base64.java index ffc7abf94..934c3b222 100644 --- a/src/com/fsck/k9/mail/filter/Base64.java +++ b/src/com/fsck/k9/mail/filter/Base64.java @@ -17,8 +17,8 @@ package com.fsck.k9.mail.filter; -import java.io.UnsupportedEncodingException; import java.math.BigInteger; +import java.nio.charset.Charset; /** * Provides Base64 encoding and decoding as defined by RFC 2045. @@ -225,12 +225,8 @@ public class Base64 { } this.decodeSize = encodeSize - 1; if (containsBase64Byte(lineSeparator)) { - String sep; - try { - sep = new String(lineSeparator, "UTF-8"); - } catch (UnsupportedEncodingException uee) { - sep = new String(lineSeparator); - } + String sep = new String(lineSeparator, Charset.forName("UTF-8")); + throw new IllegalArgumentException("lineSeperator must not contain base64 characters: [" + sep + "]"); } } diff --git a/src/com/fsck/k9/mail/internet/DecoderUtil.java b/src/com/fsck/k9/mail/internet/DecoderUtil.java index 5c7d5034f..80d5b573e 100644 --- a/src/com/fsck/k9/mail/internet/DecoderUtil.java +++ b/src/com/fsck/k9/mail/internet/DecoderUtil.java @@ -7,7 +7,8 @@ import com.fsck.k9.mail.Message; import com.fsck.k9.mail.MessagingException; import java.io.ByteArrayInputStream; import java.io.IOException; -import java.io.UnsupportedEncodingException; +import java.nio.charset.Charset; + import org.apache.james.mime4j.codec.Base64InputStream; import org.apache.james.mime4j.codec.QuotedPrintableInputStream; import org.apache.james.mime4j.util.CharsetUtil; @@ -30,12 +31,7 @@ public class DecoderUtil { * @return the decoded string. */ private static String decodeB(String encodedWord, String charset) { - byte[] bytes; - try { - bytes = encodedWord.getBytes("US-ASCII"); - } catch (UnsupportedEncodingException e) { - return null; - } + byte[] bytes = encodedWord.getBytes(Charset.forName("US-ASCII")); Base64InputStream is = new Base64InputStream(new ByteArrayInputStream(bytes)); try { @@ -68,12 +64,7 @@ public class DecoderUtil { } } - byte[] bytes; - try { - bytes = sb.toString().getBytes("US-ASCII"); - } catch (UnsupportedEncodingException e) { - return null; - } + byte[] bytes = sb.toString().getBytes(Charset.forName("US-ASCII")); QuotedPrintableInputStream is = new QuotedPrintableInputStream(new ByteArrayInputStream(bytes)); try { diff --git a/src/com/fsck/k9/mail/internet/MimeBodyPart.java b/src/com/fsck/k9/mail/internet/MimeBodyPart.java index 60e3b4cca..c4bca428a 100644 --- a/src/com/fsck/k9/mail/internet/MimeBodyPart.java +++ b/src/com/fsck/k9/mail/internet/MimeBodyPart.java @@ -20,9 +20,8 @@ import org.apache.james.mime4j.util.MimeUtil; * Message. */ public class MimeBodyPart extends BodyPart { - protected MimeHeader mHeader = new MimeHeader(); - protected Body mBody; - protected int mSize; + private final MimeHeader mHeader = new MimeHeader(); + private Body mBody; public MimeBodyPart() throws MessagingException { this(null); @@ -39,30 +38,36 @@ public class MimeBodyPart extends BodyPart { setBody(body); } - protected String getFirstHeader(String name) { + private String getFirstHeader(String name) { return mHeader.getFirstHeader(name); } + @Override public void addHeader(String name, String value) throws MessagingException { mHeader.addHeader(name, value); } + @Override public void setHeader(String name, String value) { mHeader.setHeader(name, value); } + @Override public String[] getHeader(String name) throws MessagingException { return mHeader.getHeader(name); } + @Override public void removeHeader(String name) throws MessagingException { mHeader.removeHeader(name); } + @Override public Body getBody() { return mBody; } + @Override public void setBody(Body body) throws MessagingException { this.mBody = body; if (body instanceof Multipart) { @@ -94,15 +99,18 @@ public class MimeBodyPart extends BodyPart { setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, encoding); } + @Override public String getContentType() throws MessagingException { String contentType = getFirstHeader(MimeHeader.HEADER_CONTENT_TYPE); return (contentType == null) ? "text/plain" : contentType; } + @Override public String getDisposition() throws MessagingException { return getFirstHeader(MimeHeader.HEADER_CONTENT_DISPOSITION); } + @Override public String getContentId() throws MessagingException { String contentId = getFirstHeader(MimeHeader.HEADER_CONTENT_ID); if (contentId == null) { @@ -117,21 +125,20 @@ public class MimeBodyPart extends BodyPart { contentId; } + @Override public String getMimeType() throws MessagingException { return MimeUtility.getHeaderParameter(getContentType(), null); } + @Override public boolean isMimeType(String mimeType) throws MessagingException { return getMimeType().equalsIgnoreCase(mimeType); } - public int getSize() { - return mSize; - } - /** * Write the MimeMessage out in MIME format. */ + @Override public void writeTo(OutputStream out) throws IOException, MessagingException { BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out), 1024); mHeader.writeTo(out); diff --git a/src/com/fsck/k9/mail/internet/MimeHeader.java b/src/com/fsck/k9/mail/internet/MimeHeader.java index 5175f4ad2..12bd5fd31 100644 --- a/src/com/fsck/k9/mail/internet/MimeHeader.java +++ b/src/com/fsck/k9/mail/internet/MimeHeader.java @@ -36,7 +36,7 @@ public class MimeHeader { HEADER_ANDROID_ATTACHMENT_STORE_DATA }; - protected ArrayList mFields = new ArrayList(); + private List mFields = new ArrayList(); private String mCharset = null; public void clear() { @@ -72,7 +72,7 @@ public class MimeHeader { } public String[] getHeader(String name) { - ArrayList values = new ArrayList(); + List values = new ArrayList(); for (Field field : mFields) { if (field.name.equalsIgnoreCase(name)) { values.add(field.value); @@ -85,7 +85,7 @@ public class MimeHeader { } public void removeHeader(String name) { - ArrayList removeFields = new ArrayList(); + List removeFields = new ArrayList(); for (Field field : mFields) { if (field.name.equalsIgnoreCase(name)) { removeFields.add(field); @@ -119,7 +119,7 @@ public class MimeHeader { } // encode non printable characters except LF/CR/TAB codes. - public boolean hasToBeEncoded(String text) { + private boolean hasToBeEncoded(String text) { for (int i = 0; i < text.length(); i++) { char c = text.charAt(i); if ((c < 0x20 || 0x7e < c) && // non printable @@ -131,10 +131,10 @@ public class MimeHeader { return false; } - static class Field { - final String name; + private static class Field { + private final String name; - final String value; + private final String value; public Field(String name, String value) { this.name = name; @@ -153,6 +153,7 @@ public class MimeHeader { mCharset = charset; } + @Override public MimeHeader clone() { MimeHeader header = new MimeHeader(); header.mCharset = mCharset; diff --git a/src/com/fsck/k9/mail/internet/MimeMessage.java b/src/com/fsck/k9/mail/internet/MimeMessage.java index 2f69cba39..75fa217d8 100644 --- a/src/com/fsck/k9/mail/internet/MimeMessage.java +++ b/src/com/fsck/k9/mail/internet/MimeMessage.java @@ -41,7 +41,7 @@ import com.fsck.k9.K9; * RFC 2045 style headers. */ public class MimeMessage extends Message { - protected MimeHeader mHeader = new MimeHeader(); + private MimeHeader mHeader = new MimeHeader(); protected Address[] mFrom; protected Address[] mTo; protected Address[] mCc; @@ -49,33 +49,19 @@ public class MimeMessage extends Message { protected Address[] mReplyTo; protected String mMessageId; - protected String[] mReferences; - protected String[] mInReplyTo; + private String[] mReferences; + private String[] mInReplyTo; - protected Date mSentDate; - protected SimpleDateFormat mDateFormat; + private Date mSentDate; + private SimpleDateFormat mDateFormat; - protected Body mBody; + private Body mBody; protected int mSize; public MimeMessage() { } - /** - * Parse the given InputStream using Apache Mime4J to build a MimeMessage. - * Nested messages will not be recursively parsed. - * - * @param in - * @throws IOException - * @throws MessagingException - * - * @see #MimeMessage(InputStream in, boolean recurse) - */ - public MimeMessage(InputStream in) throws IOException, MessagingException { - parse(in); - } - /** * Parse the given InputStream using Apache Mime4J to build a MimeMessage. * @@ -88,11 +74,15 @@ public class MimeMessage extends Message { parse(in, recurse); } - protected void parse(InputStream in) throws IOException, MessagingException { + /** + * Parse the given InputStream using Apache Mime4J to build a MimeMessage. + * Does not recurse through nested bodyparts. + */ + public final void parse(InputStream in) throws IOException, MessagingException { parse(in, false); } - protected void parse(InputStream in, boolean recurse) throws IOException, MessagingException { + private void parse(InputStream in, boolean recurse) throws IOException, MessagingException { mHeader.clear(); mFrom = null; mTo = null; @@ -121,8 +111,8 @@ public class MimeMessage extends Message { try { parser.parse(new EOLConvertingInputStream(in)); } catch (MimeException me) { + //TODO wouldn't a MessagingException be better? throw new Error(me); - } } @@ -177,20 +167,25 @@ public class MimeMessage extends Message { return (contentType == null) ? "text/plain" : contentType; } + @Override public String getDisposition() throws MessagingException { return getFirstHeader(MimeHeader.HEADER_CONTENT_DISPOSITION); } + @Override public String getContentId() throws MessagingException { return null; } + @Override public String getMimeType() throws MessagingException { return MimeUtility.getHeaderParameter(getContentType(), null); } + @Override public boolean isMimeType(String mimeType) throws MessagingException { return getMimeType().equalsIgnoreCase(mimeType); } + @Override public int getSize() { return mSize; } @@ -419,7 +414,7 @@ public class MimeMessage extends Message { } } - protected String getFirstHeader(String name) { + private String getFirstHeader(String name) { return mHeader.getFirstHeader(name); } @@ -448,6 +443,7 @@ public class MimeMessage extends Message { return mHeader.getHeaderNames(); } + @Override public void writeTo(OutputStream out) throws IOException, MessagingException { BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out), 1024); @@ -459,6 +455,7 @@ public class MimeMessage extends Message { } } + @Override public InputStream getInputStream() throws MessagingException { return null; } @@ -482,7 +479,7 @@ public class MimeMessage extends Message { } } - class MimeMessageBuilder implements ContentHandler { + private class MimeMessageBuilder implements ContentHandler { private final LinkedList stack = new LinkedList(); public MimeMessageBuilder() { @@ -495,6 +492,7 @@ public class MimeMessage extends Message { } } + @Override public void startMessage() { if (stack.isEmpty()) { stack.addFirst(MimeMessage.this); @@ -510,22 +508,23 @@ public class MimeMessage extends Message { } } + @Override public void endMessage() { expect(MimeMessage.class); stack.removeFirst(); } + @Override public void startHeader() { expect(Part.class); } - - - + @Override public void endHeader() { expect(Part.class); } + @Override public void startMultipart(BodyDescriptor bd) { expect(Part.class); @@ -539,6 +538,7 @@ public class MimeMessage extends Message { } } + @Override public void body(BodyDescriptor bd, InputStream in) throws IOException { expect(Part.class); try { @@ -550,10 +550,12 @@ public class MimeMessage extends Message { } } + @Override public void endMultipart() { stack.removeFirst(); } + @Override public void startBodyPart() { expect(MimeMultipart.class); @@ -566,21 +568,13 @@ public class MimeMessage extends Message { } } + @Override public void endBodyPart() { expect(BodyPart.class); stack.removeFirst(); } - public void epilogue(InputStream is) throws IOException { - expect(MimeMultipart.class); - StringBuilder sb = new StringBuilder(); - int b; - while ((b = is.read()) != -1) { - sb.append((char)b); - } - // ((Multipart) stack.peek()).setEpilogue(sb.toString()); - } - + @Override public void preamble(InputStream is) throws IOException { expect(MimeMultipart.class); StringBuilder sb = new StringBuilder(); @@ -589,9 +583,13 @@ public class MimeMessage extends Message { sb.append((char)b); } ((MimeMultipart)stack.peek()).setPreamble(sb.toString()); - } + @Override + public void epilogue(InputStream is) throws IOException { + } + + @Override public void raw(InputStream is) throws IOException { throw new UnsupportedOperationException("Not supported"); } @@ -641,14 +639,17 @@ public class MimeMessage extends Message { return message; } + @Override public long getId() { return Long.parseLong(mUid); //or maybe .mMessageId? } + @Override public String getPreview() { return ""; } + @Override public boolean hasAttachments() { return false; } diff --git a/src/com/fsck/k9/mail/internet/MimeMultipart.java b/src/com/fsck/k9/mail/internet/MimeMultipart.java index e00d4da3e..124ca5e4c 100644 --- a/src/com/fsck/k9/mail/internet/MimeMultipart.java +++ b/src/com/fsck/k9/mail/internet/MimeMultipart.java @@ -10,13 +10,11 @@ import java.util.Locale; import java.util.Random; public class MimeMultipart extends Multipart { - protected String mPreamble; + private String mPreamble; - protected String mContentType; + private String mContentType; - protected String mBoundary; - - protected String mSubType; + private final String mBoundary; public MimeMultipart() throws MessagingException { mBoundary = generateBoundary(); @@ -26,7 +24,6 @@ public class MimeMultipart extends Multipart { public MimeMultipart(String contentType) throws MessagingException { this.mContentType = contentType; try { - mSubType = MimeUtility.getHeaderParameter(contentType, null).split("/")[1]; mBoundary = MimeUtility.getHeaderParameter(contentType, "boundary"); if (mBoundary == null) { throw new MessagingException("MultiPart does not contain boundary: " + contentType); @@ -62,10 +59,10 @@ public class MimeMultipart extends Multipart { } public void setSubType(String subType) { - this.mSubType = subType; mContentType = String.format("multipart/%s; boundary=\"%s\"", subType, mBoundary); } + @Override public void writeTo(OutputStream out) throws IOException, MessagingException { BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out), 1024); @@ -74,20 +71,19 @@ public class MimeMultipart extends Multipart { writer.write("\r\n"); } - if (mParts.isEmpty()) { + if (getBodyParts().isEmpty()) { writer.write("--"); writer.write(mBoundary); writer.write("\r\n"); - } - - for (int i = 0, count = mParts.size(); i < count; i++) { - BodyPart bodyPart = mParts.get(i); - writer.write("--"); - writer.write(mBoundary); - writer.write("\r\n"); - writer.flush(); - bodyPart.writeTo(out); - writer.write("\r\n"); + } else { + for (BodyPart bodyPart : getBodyParts()) { + writer.write("--"); + writer.write(mBoundary); + writer.write("\r\n"); + writer.flush(); + bodyPart.writeTo(out); + writer.write("\r\n"); + } } writer.write("--"); @@ -96,13 +92,14 @@ public class MimeMultipart extends Multipart { writer.flush(); } + @Override public InputStream getInputStream() throws MessagingException { return null; } @Override public void setUsing7bitTransport() throws MessagingException { - for (BodyPart part : mParts) { + for (BodyPart part : getBodyParts()) { part.setUsing7bitTransport(); } } diff --git a/src/com/fsck/k9/mail/internet/MimeUtility.java b/src/com/fsck/k9/mail/internet/MimeUtility.java index c97e25df6..21dad28c7 100644 --- a/src/com/fsck/k9/mail/internet/MimeUtility.java +++ b/src/com/fsck/k9/mail/internet/MimeUtility.java @@ -927,11 +927,7 @@ public class MimeUtility { return s.replaceAll("\r|\n", ""); } - public static String decode(String s) { - return decode(s, null); - } - - public static String decode(String s, Message message) { + private static String decode(String s, Message message) { if (s == null) { return null; } @@ -993,8 +989,7 @@ public class MimeUtility { throws MessagingException { if (part.getBody() instanceof Multipart) { Multipart multipart = (Multipart)part.getBody(); - for (int i = 0, count = multipart.getCount(); i < count; i++) { - BodyPart bodyPart = multipart.getBodyPart(i); + for (BodyPart bodyPart : multipart.getBodyParts()) { Part ret = findFirstPartByMimeType(bodyPart, mimeType); if (ret != null) { return ret; @@ -1006,28 +1001,6 @@ public class MimeUtility { return null; } - public static Part findPartByContentId(Part part, String contentId) throws Exception { - if (part.getBody() instanceof Multipart) { - Multipart multipart = (Multipart)part.getBody(); - for (int i = 0, count = multipart.getCount(); i < count; i++) { - BodyPart bodyPart = multipart.getBodyPart(i); - Part ret = findPartByContentId(bodyPart, contentId); - if (ret != null) { - return ret; - } - } - } - String[] header = part.getHeader(MimeHeader.HEADER_CONTENT_ID); - if (header != null) { - for (String s : header) { - if (s.equals(contentId)) { - return part; - } - } - } - return null; - } - /** * Reads the Part's body and returns a String based on any charset conversion that needed * to be done. Note, this does not return a text representation of HTML. @@ -1468,7 +1441,7 @@ public class MimeUtility { * @throws MessagingException * In case of an error. */ - public static List getViewables(Part part, List attachments) throws MessagingException { + private static List getViewables(Part part, List attachments) throws MessagingException { List viewables = new ArrayList(); Body body = part.getBody(); @@ -1490,9 +1463,7 @@ public class MimeUtility { } } else { // For all other multipart parts we recurse to grab all viewable children. - int childCount = multipart.getCount(); - for (int i = 0; i < childCount; i++) { - Part bodyPart = multipart.getBodyPart(i); + for (Part bodyPart : multipart.getBodyParts()) { viewables.addAll(getViewables(bodyPart, attachments)); } } @@ -1547,9 +1518,7 @@ public class MimeUtility { throws MessagingException { List viewables = new ArrayList(); - int childCount = multipart.getCount(); - for (int i = 0; i < childCount; i++) { - Part part = multipart.getBodyPart(i); + for (Part part : multipart.getBodyParts()) { Body body = part.getBody(); if (body instanceof Multipart) { Multipart innerMultipart = (Multipart) body; @@ -1612,9 +1581,7 @@ public class MimeUtility { List viewables = new ArrayList(); boolean partFound = false; - int childCount = multipart.getCount(); - for (int i = 0; i < childCount; i++) { - Part part = multipart.getBodyPart(i); + for (Part part : multipart.getBodyParts()) { Body body = part.getBody(); if (body instanceof Multipart) { Multipart innerMultipart = (Multipart) body; @@ -1698,9 +1665,7 @@ public class MimeUtility { */ private static void findAttachments(Multipart multipart, Set knownTextParts, List attachments) { - int childCount = multipart.getCount(); - for (int i = 0; i < childCount; i++) { - Part part = multipart.getBodyPart(i); + for (Part part : multipart.getBodyParts()) { Body body = part.getBody(); if (body instanceof Multipart) { Multipart innerMultipart = (Multipart) body; @@ -2049,7 +2014,7 @@ public class MimeUtility { return null; } - public static Boolean isPartTextualBody(Part part) throws MessagingException { + private static Boolean isPartTextualBody(Part part) throws MessagingException { String disposition = part.getDisposition(); String dispositionType = null; String dispositionFilename = null; @@ -2137,7 +2102,7 @@ public class MimeUtility { * * @see #MIME_TYPE_REPLACEMENT_MAP */ - public static String canonicalizeMimeType(String mimeType) { + private static String canonicalizeMimeType(String mimeType) { String lowerCaseMimeType = mimeType.toLowerCase(Locale.US); for (String[] mimeTypeMapEntry : MIME_TYPE_REPLACEMENT_MAP) { if (mimeTypeMapEntry[0].equals(lowerCaseMimeType)) { @@ -3422,8 +3387,7 @@ public class MimeUtility { } else if (part.isMimeType("multipart/alternative") && firstBody instanceof MimeMultipart) { MimeMultipart multipart = (MimeMultipart) firstBody; - for (int i = 0, count = multipart.getCount(); i < count; i++) { - BodyPart bodyPart = multipart.getBodyPart(i); + for (BodyPart bodyPart : multipart.getBodyParts()) { String bodyText = getTextFromPart(bodyPart); if (bodyText != null) { if (text.isEmpty() && bodyPart.isMimeType("text/plain")) { diff --git a/src/com/fsck/k9/mail/internet/TextBody.java b/src/com/fsck/k9/mail/internet/TextBody.java index 2d15b9ab8..ad6e474ac 100644 --- a/src/com/fsck/k9/mail/internet/TextBody.java +++ b/src/com/fsck/k9/mail/internet/TextBody.java @@ -16,7 +16,7 @@ public class TextBody implements Body { */ private static final byte[] EMPTY_BYTE_ARRAY = new byte[0]; - private String mBody; + private final String mBody; private String mEncoding; private String mCharset = "UTF-8"; // Length of the message composed (as opposed to quoted). I don't like the name of this variable and am open to @@ -29,6 +29,7 @@ public class TextBody implements Body { this.mBody = body; } + @Override public void writeTo(OutputStream out) throws IOException, MessagingException { if (mBody != null) { byte[] bytes = mBody.getBytes(mCharset); @@ -54,6 +55,7 @@ public class TextBody implements Body { /** * Returns an InputStream that reads this body's text. */ + @Override public InputStream getInputStream() throws MessagingException { try { byte[] b; @@ -68,6 +70,7 @@ public class TextBody implements Body { } } + @Override public void setEncoding(String encoding) { mEncoding = encoding; } diff --git a/src/com/fsck/k9/mail/internet/TextBodyBuilder.java b/src/com/fsck/k9/mail/internet/TextBodyBuilder.java index 96e19e9e9..5f6cfd38c 100644 --- a/src/com/fsck/k9/mail/internet/TextBodyBuilder.java +++ b/src/com/fsck/k9/mail/internet/TextBodyBuilder.java @@ -204,7 +204,10 @@ public class TextBodyBuilder { return mQuotedTextHtml; } - public String textToHtmlFragment(String text) { + /** + * protected for unit-test purposes + */ + protected String textToHtmlFragment(String text) { return HtmlConverter.textToHtmlFragment(text); } diff --git a/src/com/fsck/k9/mail/store/ImapStore.java b/src/com/fsck/k9/mail/store/ImapStore.java index f2c002e05..9042f9d9b 100644 --- a/src/com/fsck/k9/mail/store/ImapStore.java +++ b/src/com/fsck/k9/mail/store/ImapStore.java @@ -7,7 +7,6 @@ import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.io.UnsupportedEncodingException; import java.net.ConnectException; import java.net.InetAddress; import java.net.InetSocketAddress; @@ -16,8 +15,6 @@ import java.net.SocketAddress; import java.net.SocketException; import java.net.URI; import java.net.URISyntaxException; -import java.net.URLDecoder; -import java.net.URLEncoder; import java.nio.ByteBuffer; import java.nio.CharBuffer; import java.nio.charset.CharacterCodingException; @@ -29,8 +26,11 @@ import java.security.Security; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.Date; +import java.util.Deque; +import java.util.EnumSet; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; @@ -64,6 +64,7 @@ import com.fsck.k9.K9; import com.fsck.k9.R; import com.fsck.k9.controller.MessageRetrievalListener; import com.fsck.k9.helper.StringUtils; +import com.fsck.k9.helper.UrlEncodingHelper; import com.fsck.k9.helper.Utility; import com.fsck.k9.helper.power.TracingPowerManager; import com.fsck.k9.helper.power.TracingPowerManager.TracingWakeLock; @@ -116,7 +117,7 @@ public class ImapStore extends Store { private static int FETCH_WINDOW_SIZE = 100; - private Set mPermanentFlagsIndex = new HashSet(); + private Set mPermanentFlagsIndex = EnumSet.noneOf(Flag.class); private static final String CAPABILITY_IDLE = "IDLE"; private static final String CAPABILITY_AUTH_CRAM_MD5 = "AUTH=CRAM-MD5"; @@ -133,8 +134,6 @@ public class ImapStore extends Store { private static final String CAPABILITY_COMPRESS_DEFLATE = "COMPRESS=DEFLATE"; private static final String COMMAND_COMPRESS_DEFLATE = "COMPRESS DEFLATE"; - private static final Message[] EMPTY_MESSAGE_ARRAY = new Message[0]; - private static final String[] EMPTY_STRING_ARRAY = new String[0]; /** @@ -198,31 +197,26 @@ public class ImapStore extends Store { } if (imapUri.getUserInfo() != null) { - try { - String userinfo = imapUri.getUserInfo(); - String[] userInfoParts = userinfo.split(":"); + String userinfo = imapUri.getUserInfo(); + String[] userInfoParts = userinfo.split(":"); - if (userinfo.endsWith(":")) { - // Password is empty. This can only happen after an account was imported. - authenticationType = AuthType.valueOf(userInfoParts[0]); - username = URLDecoder.decode(userInfoParts[1], "UTF-8"); - } else if (userInfoParts.length == 2) { - authenticationType = AuthType.PLAIN; - username = URLDecoder.decode(userInfoParts[0], "UTF-8"); - password = URLDecoder.decode(userInfoParts[1], "UTF-8"); - } else if (userInfoParts.length == 3) { - authenticationType = AuthType.valueOf(userInfoParts[0]); - username = URLDecoder.decode(userInfoParts[1], "UTF-8"); + if (userinfo.endsWith(":")) { + // Password is empty. This can only happen after an account was imported. + authenticationType = AuthType.valueOf(userInfoParts[0]); + username = UrlEncodingHelper.decodeUtf8(userInfoParts[1]); + } else if (userInfoParts.length == 2) { + authenticationType = AuthType.PLAIN; + username = UrlEncodingHelper.decodeUtf8(userInfoParts[0]); + password = UrlEncodingHelper.decodeUtf8(userInfoParts[1]); + } else if (userInfoParts.length == 3) { + authenticationType = AuthType.valueOf(userInfoParts[0]); + username = UrlEncodingHelper.decodeUtf8(userInfoParts[1]); - if (AuthType.EXTERNAL == authenticationType) { - clientCertificateAlias = URLDecoder.decode(userInfoParts[2], "UTF-8"); - } else { - password = URLDecoder.decode(userInfoParts[2], "UTF-8"); - } + if (AuthType.EXTERNAL == authenticationType) { + clientCertificateAlias = UrlEncodingHelper.decodeUtf8(userInfoParts[2]); + } else { + password = UrlEncodingHelper.decodeUtf8(userInfoParts[2]); } - } catch (UnsupportedEncodingException enc) { - // This shouldn't happen since the encoding is hardcoded to UTF-8 - throw new IllegalArgumentException("Couldn't urldecode username or password.", enc); } } @@ -260,19 +254,11 @@ public class ImapStore extends Store { * @see ImapStore#decodeUri(String) */ public static String createUri(ServerSettings server) { - String userEnc; - String passwordEnc; - String clientCertificateAliasEnc; - try { - userEnc = URLEncoder.encode(server.username, "UTF-8"); - passwordEnc = (server.password != null) ? - URLEncoder.encode(server.password, "UTF-8") : ""; - clientCertificateAliasEnc = (server.clientCertificateAlias != null) ? - URLEncoder.encode(server.clientCertificateAlias, "UTF-8") : ""; - } - catch (UnsupportedEncodingException e) { - throw new IllegalArgumentException("Could not encode username or password", e); - } + String userEnc = UrlEncodingHelper.encodeUtf8(server.username); + String passwordEnc = (server.password != null) ? + UrlEncodingHelper.encodeUtf8(server.password) : ""; + String clientCertificateAliasEnc = (server.clientCertificateAlias != null) ? + UrlEncodingHelper.encodeUtf8(server.clientCertificateAlias) : ""; String scheme; switch (server.connectionSecurity) { @@ -439,7 +425,7 @@ public class ImapStore extends Store { private static final SimpleDateFormat RFC3501_DATE = new SimpleDateFormat("dd-MMM-yyyy", Locale.US); - private LinkedList mConnections = + private final Deque mConnections = new LinkedList(); /** @@ -453,7 +439,7 @@ public class ImapStore extends Store { * requests. This cache lets us make sure we always reuse, if possible, for a given * folder name. */ - private HashMap mFolderCache = new HashMap(); + private final Map mFolderCache = new HashMap(); public ImapStore(Account account) throws MessagingException { super(account); @@ -756,18 +742,10 @@ public class ImapStore extends Store { } private String encodeFolderName(String name) { - try { - ByteBuffer bb = mModifiedUtf7Charset.encode(name); - byte[] b = new byte[bb.limit()]; - bb.get(b); - return new String(b, "US-ASCII"); - } catch (UnsupportedEncodingException uee) { - /* - * The only thing that can throw this is getBytes("US-ASCII") and if US-ASCII doesn't - * exist we're totally screwed. - */ - throw new RuntimeException("Unable to encode folder name: " + name, uee); - } + ByteBuffer bb = mModifiedUtf7Charset.encode(name); + byte[] b = new byte[bb.limit()]; + bb.get(b); + return new String(b, Charset.forName("US-ASCII")); } private String decodeFolderName(String name) throws CharacterCodingException { @@ -775,18 +753,11 @@ public class ImapStore extends Store { * Convert the encoded name to US-ASCII, then pass it through the modified UTF-7 * decoder and return the Unicode String. */ - try { - // Make sure the decoder throws an exception if it encounters an invalid encoding. - CharsetDecoder decoder = mModifiedUtf7Charset.newDecoder().onMalformedInput(CodingErrorAction.REPORT); - CharBuffer cb = decoder.decode(ByteBuffer.wrap(name.getBytes("US-ASCII"))); - return cb.toString(); - } catch (UnsupportedEncodingException uee) { - /* - * The only thing that can throw this is getBytes("US-ASCII") and if US-ASCII doesn't - * exist we're totally screwed. - */ - throw new RuntimeException("Unable to decode folder name: " + name, uee); - } + // Make sure the decoder throws an exception if it encounters an invalid encoding. + CharsetDecoder decoder = mModifiedUtf7Charset.newDecoder().onMalformedInput(CodingErrorAction.REPORT); + CharBuffer cb = decoder.decode(ByteBuffer.wrap(name.getBytes(Charset.forName("US-ASCII")))); + return cb.toString(); + } @Override @@ -1121,22 +1092,22 @@ public class ImapStore extends Store { * @return The mapping of original message UIDs to the new server UIDs. */ @Override - public Map copyMessages(Message[] messages, Folder folder) + public Map copyMessages(List messages, Folder folder) throws MessagingException { if (!(folder instanceof ImapFolder)) { throw new MessagingException("ImapFolder.copyMessages passed non-ImapFolder"); } - if (messages.length == 0) { + if (messages.isEmpty()) { return null; } ImapFolder iFolder = (ImapFolder)folder; checkOpen(); //only need READ access - String[] uids = new String[messages.length]; - for (int i = 0, count = messages.length; i < count; i++) { - uids[i] = messages[i].getUid(); + String[] uids = new String[messages.size()]; + for (int i = 0, count = messages.size(); i < count; i++) { + uids[i] = messages.get(i).getUid(); } try { @@ -1222,21 +1193,21 @@ public class ImapStore extends Store { } @Override - public Map moveMessages(Message[] messages, Folder folder) throws MessagingException { - if (messages.length == 0) + public Map moveMessages(List messages, Folder folder) throws MessagingException { + if (messages.isEmpty()) return null; Map uidMap = copyMessages(messages, folder); - setFlags(messages, new Flag[] { Flag.DELETED }, true); + setFlags(messages, Collections.singleton(Flag.DELETED), true); return uidMap; } @Override - public void delete(Message[] messages, String trashFolderName) throws MessagingException { - if (messages.length == 0) + public void delete(List messages, String trashFolderName) throws MessagingException { + if (messages.isEmpty()) return; if (trashFolderName == null || getName().equalsIgnoreCase(trashFolderName)) { - setFlags(messages, new Flag[] { Flag.DELETED }, true); + setFlags(messages, Collections.singleton(Flag.DELETED), true); } else { ImapFolder remoteTrashFolder = (ImapFolder)getStore().getFolder(trashFolderName); String remoteTrashName = encodeString(encodeFolderName(remoteTrashFolder.getPrefixedName())); @@ -1252,7 +1223,7 @@ public class ImapStore extends Store { if (exists(remoteTrashName)) { if (K9.DEBUG) - Log.d(K9.LOG_TAG, "IMAPMessage.delete: copying remote " + messages.length + " messages to '" + trashFolderName + "' for " + getLogId()); + Log.d(K9.LOG_TAG, "IMAPMessage.delete: copying remote " + messages.size() + " messages to '" + trashFolderName + "' for " + getLogId()); moveMessages(messages, remoteTrashFolder); } else { @@ -1302,13 +1273,14 @@ public class ImapStore extends Store { protected long getHighestUid() { try { ImapSearcher searcher = new ImapSearcher() { + @Override public List search() throws IOException, MessagingException { return executeSimpleCommand("UID SEARCH *:*"); } }; - Message[] messages = search(searcher, null).toArray(EMPTY_MESSAGE_ARRAY); - if (messages.length > 0) { - return Long.parseLong(messages[0].getUid()); + List messages = search(searcher, null); + if (messages.size() > 0) { + return Long.parseLong(messages.get(0).getUid()); } } catch (Exception e) { Log.e(K9.LOG_TAG, "Unable to find highest UID in folder " + getName(), e); @@ -1329,12 +1301,12 @@ public class ImapStore extends Store { @Override - public Message[] getMessages(int start, int end, Date earliestDate, MessageRetrievalListener listener) + public List getMessages(int start, int end, Date earliestDate, MessageRetrievalListener listener) throws MessagingException { return getMessages(start, end, earliestDate, false, listener); } - protected Message[] getMessages(final int start, final int end, Date earliestDate, final boolean includeDeleted, final MessageRetrievalListener listener) + protected List getMessages(final int start, final int end, Date earliestDate, final boolean includeDeleted, final MessageRetrievalListener listener) throws MessagingException { if (start < 1 || end < 1 || end < start) { throw new MessagingException( @@ -1351,39 +1323,42 @@ public class ImapStore extends Store { ImapSearcher searcher = new ImapSearcher() { + @Override public List search() throws IOException, MessagingException { return executeSimpleCommand(String.format(Locale.US, "UID SEARCH %d:%d%s%s", start, end, dateSearchString, includeDeleted ? "" : " NOT DELETED")); } }; - return search(searcher, listener).toArray(EMPTY_MESSAGE_ARRAY); + return search(searcher, listener); } - protected Message[] getMessages(final List mesgSeqs, final boolean includeDeleted, final MessageRetrievalListener listener) + protected List getMessages(final List mesgSeqs, final boolean includeDeleted, final MessageRetrievalListener listener) throws MessagingException { ImapSearcher searcher = new ImapSearcher() { + @Override public List search() throws IOException, MessagingException { return executeSimpleCommand(String.format("UID SEARCH %s%s", Utility.combine(mesgSeqs.toArray(), ','), includeDeleted ? "" : " NOT DELETED")); } }; - return search(searcher, listener).toArray(EMPTY_MESSAGE_ARRAY); + return search(searcher, listener); } - protected Message[] getMessagesFromUids(final List mesgUids, final boolean includeDeleted, final MessageRetrievalListener listener) + protected List getMessagesFromUids(final List mesgUids, final boolean includeDeleted, final MessageRetrievalListener listener) throws MessagingException { ImapSearcher searcher = new ImapSearcher() { + @Override public List search() throws IOException, MessagingException { return executeSimpleCommand(String.format("UID SEARCH UID %s%s", Utility.combine(mesgUids.toArray(), ','), includeDeleted ? "" : " NOT DELETED")); } }; - return search(searcher, listener).toArray(EMPTY_MESSAGE_ARRAY); + return search(searcher, listener); } private List search(ImapSearcher searcher, MessageRetrievalListener listener) throws MessagingException { checkOpen(); //only need READ access - ArrayList messages = new ArrayList(); + List messages = new ArrayList(); try { - ArrayList uids = new ArrayList(); + List uids = new ArrayList(); List responses = searcher.search(); // for (ImapResponse response : responses) { if (response.mTag == null) { @@ -1420,19 +1395,19 @@ public class ImapStore extends Store { @Override - public Message[] getMessages(MessageRetrievalListener listener) throws MessagingException { + public List getMessages(MessageRetrievalListener listener) throws MessagingException { return getMessages(null, listener); } @Override - public Message[] getMessages(String[] uids, MessageRetrievalListener listener) + public List getMessages(String[] uids, MessageRetrievalListener listener) throws MessagingException { checkOpen(); //only need READ access - ArrayList messages = new ArrayList(); + List messages = new ArrayList(); try { if (uids == null) { List responses = executeSimpleCommand("UID SEARCH 1:* NOT DELETED"); - ArrayList tempUids = new ArrayList(); + List tempUids = new ArrayList(); for (ImapResponse response : responses) { if (ImapResponseParser.equalsIgnoreCase(response.get(0), "SEARCH")) { for (int i = 1, count = response.size(); i < count; i++) { @@ -1455,17 +1430,17 @@ public class ImapStore extends Store { } catch (IOException ioe) { throw ioExceptionHandler(mConnection, ioe); } - return messages.toArray(EMPTY_MESSAGE_ARRAY); + return messages; } @Override - public void fetch(Message[] messages, FetchProfile fp, MessageRetrievalListener listener) + public void fetch(List messages, FetchProfile fp, MessageRetrievalListener listener) throws MessagingException { - if (messages == null || messages.length == 0) { + if (messages == null || messages.isEmpty()) { return; } checkOpen(); //only need READ access - List uids = new ArrayList(messages.length); + List uids = new ArrayList(messages.size()); HashMap messageMap = new HashMap(); for (Message msg : messages) { String uid = msg.getUid(); @@ -1507,8 +1482,8 @@ public class ImapStore extends Store { - for (int windowStart = 0; windowStart < messages.length; windowStart += (FETCH_WINDOW_SIZE)) { - List uidWindow = uids.subList(windowStart, Math.min((windowStart + FETCH_WINDOW_SIZE), messages.length)); + for (int windowStart = 0; windowStart < messages.size(); windowStart += (FETCH_WINDOW_SIZE)) { + List uidWindow = uids.subList(windowStart, Math.min((windowStart + FETCH_WINDOW_SIZE), messages.size())); try { mConnection.sendCommand(String.format("UID FETCH %s (%s)", @@ -1834,7 +1809,7 @@ public class ImapStore extends Store { * For each part in the message we're going to add a new BodyPart and parse * into it. */ - ImapBodyPart bp = new ImapBodyPart(); + MimeBodyPart bp = new MimeBodyPart(); if (id.equalsIgnoreCase("TEXT")) { parseBodyStructure(bs.getList(i), bp, Integer.toString(i + 1)); } else { @@ -1970,10 +1945,6 @@ public class ImapStore extends Store { if (part instanceof ImapMessage) { ((ImapMessage) part).setSize(size); - } else if (part instanceof ImapBodyPart) { - ((ImapBodyPart) part).setSize(size); - } else { - throw new MessagingException("Unknown part type " + part.toString()); } part.setHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA, id); } @@ -1994,7 +1965,7 @@ public class ImapStore extends Store { * @return The mapping of original message UIDs to the new server UIDs. */ @Override - public Map appendMessages(Message[] messages) throws MessagingException { + public Map appendMessages(List messages) throws MessagingException { open(OPEN_MODE_RW); checkOpen(); try { @@ -2066,7 +2037,7 @@ public class ImapStore extends Store { * with the behavior of other similar methods (copyMessages, moveMessages) which * return null. */ - return (uidMap.size() == 0) ? null : uidMap; + return (uidMap.isEmpty()) ? null : uidMap; } catch (IOException ioe) { throw ioExceptionHandler(mConnection, ioe); } @@ -2117,8 +2088,8 @@ public class ImapStore extends Store { } } - private String combineFlags(Flag[] flags) { - ArrayList flagNames = new ArrayList(); + private String combineFlags(Iterable flags) { + List flagNames = new ArrayList(); for (Flag flag : flags) { if (flag == Flag.SEEN) { flagNames.add("\\Seen"); @@ -2139,7 +2110,7 @@ public class ImapStore extends Store { @Override - public void setFlags(Flag[] flags, boolean value) + public void setFlags(Set flags, boolean value) throws MessagingException { open(OPEN_MODE_RW); checkOpen(); @@ -2174,13 +2145,13 @@ public class ImapStore extends Store { @Override - public void setFlags(Message[] messages, Flag[] flags, boolean value) + public void setFlags(List messages, final Set flags, boolean value) throws MessagingException { open(OPEN_MODE_RW); checkOpen(); - String[] uids = new String[messages.length]; - for (int i = 0, count = messages.length; i < count; i++) { - uids[i] = messages[i].getUid(); + String[] uids = new String[messages.size()]; + for (int i = 0, count = messages.size(); i < count; i++) { + uids[i] = messages.get(i).getUid(); } try { executeSimpleCommand(String.format("UID STORE %s %sFLAGS.SILENT (%s)", @@ -2241,7 +2212,7 @@ public class ImapStore extends Store { * @throws MessagingException On any error. */ @Override - public List search(final String queryString, final Flag[] requiredFlags, final Flag[] forbiddenFlags) + public List search(final String queryString, final Set requiredFlags, final Set forbiddenFlags) throws MessagingException { if (!mAccount.allowRemoteSearch()) { @@ -2250,6 +2221,7 @@ public class ImapStore extends Store { // Setup the searcher final ImapSearcher searcher = new ImapSearcher() { + @Override public List search() throws IOException, MessagingException { String imapQuery = "UID SEARCH "; if (requiredFlags != null) { @@ -2345,12 +2317,12 @@ public class ImapStore extends Store { * A cacheable class that stores the details for a single IMAP connection. */ public static class ImapConnection { - protected Socket mSocket; - protected PeekableInputStream mIn; - protected OutputStream mOut; - protected ImapResponseParser mParser; - protected int mNextCommandTag; - protected Set capabilities = new HashSet(); + private Socket mSocket; + private PeekableInputStream mIn; + private OutputStream mOut; + private ImapResponseParser mParser; + private int mNextCommandTag; + private Set capabilities = new HashSet(); private ImapSettings mSettings; @@ -2761,10 +2733,10 @@ public class ImapStore extends Store { return response; } - protected ArrayList readStatusResponse(String tag, + protected List readStatusResponse(String tag, String commandToLog, UntaggedHandler untaggedHandler) throws IOException, MessagingException { - ArrayList responses = new ArrayList(); + List responses = new ArrayList(); ImapResponse response; do { response = mParser.readResponse(); @@ -2931,11 +2903,6 @@ public class ImapStore extends Store { this.mSize = size; } - @Override - public void parse(InputStream in) throws IOException, MessagingException { - super.parse(in); - } - public void setFlagInternal(Flag flag, boolean set) throws MessagingException { super.setFlag(flag, set); } @@ -2944,28 +2911,18 @@ public class ImapStore extends Store { @Override public void setFlag(Flag flag, boolean set) throws MessagingException { super.setFlag(flag, set); - mFolder.setFlags(new Message[] { this }, new Flag[] { flag }, set); + mFolder.setFlags(Collections.singletonList(this), Collections.singleton(flag), set); } @Override public void delete(String trashFolderName) throws MessagingException { - getFolder().delete(new Message[] { this }, trashFolderName); - } - } - - static class ImapBodyPart extends MimeBodyPart { - public ImapBodyPart() throws MessagingException { - super(); - } - - public void setSize(int size) { - this.mSize = size; + getFolder().delete(Collections.singletonList(this), trashFolderName); } } static class ImapException extends MessagingException { private static final long serialVersionUID = 3725007182205882394L; - String mAlertText; + private final String mAlertText; public ImapException(String message, String alertText) { super(message, true); @@ -2976,22 +2933,19 @@ public class ImapStore extends Store { return mAlertText; } - public void setAlertText(String alertText) { - mAlertText = alertText; - } } public class ImapFolderPusher extends ImapFolder implements UntaggedHandler { - final PushReceiver receiver; - Thread listeningThread = null; - final AtomicBoolean stop = new AtomicBoolean(false); - final AtomicBoolean idling = new AtomicBoolean(false); - final AtomicBoolean doneSent = new AtomicBoolean(false); - final AtomicInteger delayTime = new AtomicInteger(NORMAL_DELAY_TIME); - final AtomicInteger idleFailureCount = new AtomicInteger(0); - final AtomicBoolean needsPoll = new AtomicBoolean(false); - List storedUntaggedResponses = new ArrayList(); - TracingWakeLock wakeLock = null; + private final PushReceiver receiver; + private Thread listeningThread = null; + private final AtomicBoolean stop = new AtomicBoolean(false); + private final AtomicBoolean idling = new AtomicBoolean(false); + private final AtomicBoolean doneSent = new AtomicBoolean(false); + private final AtomicInteger delayTime = new AtomicInteger(NORMAL_DELAY_TIME); + private final AtomicInteger idleFailureCount = new AtomicInteger(0); + private final AtomicBoolean needsPoll = new AtomicBoolean(false); + private List storedUntaggedResponses = new ArrayList(); + private TracingWakeLock wakeLock = null; public ImapFolderPusher(ImapStore store, String name, PushReceiver nReceiver) { super(store, name); @@ -3029,6 +2983,7 @@ public class ImapStore extends Store { public void start() { Runnable runner = new Runnable() { + @Override public void run() { wakeLock.acquire(K9.PUSH_WAKE_LOCK_TIMEOUT); if (K9.DEBUG) @@ -3254,9 +3209,9 @@ public class ImapStore extends Store { Log.e(K9.LOG_TAG, "Unable to get oldUidNext for " + getLogId(), e); } - Message[] messageArray = getMessages(end, end, null, true, null); - if (messageArray != null && messageArray.length > 0) { - long newUid = Long.parseLong(messageArray[0].getUid()); + List messageList = getMessages(end, end, null, true, null); + if (messageList != null && messageList.size() > 0) { + long newUid = Long.parseLong(messageList.get(0).getUid()); if (K9.DEBUG) Log.i(K9.LOG_TAG, "Got newUid " + newUid + " for message " + end + " on " + getLogId()); long startUid = oldUidNext; @@ -3284,12 +3239,10 @@ public class ImapStore extends Store { private void syncMessages(List flagSyncMsgSeqs) { try { - Message[] messageArray = null; - - messageArray = getMessages(flagSyncMsgSeqs, true, null); + List messageList = getMessages(flagSyncMsgSeqs, true, null); List messages = new ArrayList(); - messages.addAll(Arrays.asList(messageArray)); + messages.addAll(messageList); pushMessages(messages, false); } catch (Exception e) { @@ -3301,7 +3254,7 @@ public class ImapStore extends Store { List messages = new ArrayList(removeUids.size()); try { - Message[] existingMessages = getMessagesFromUids(removeUids, true, null); + List existingMessages = getMessagesFromUids(removeUids, true, null); for (Message existingMessage : existingMessages) { needsPoll.set(true); msgSeqUidMap.clear(); @@ -3428,6 +3381,7 @@ public class ImapStore extends Store { } } + @Override public void handleAsyncUntaggedResponse(ImapResponse response) { if (K9.DEBUG) Log.v(K9.LOG_TAG, "Got async response: " + response); @@ -3478,17 +3432,18 @@ public class ImapStore extends Store { } public class ImapPusher implements Pusher { - final ImapStore mStore; + private final ImapStore mStore; final PushReceiver mReceiver; private long lastRefresh = -1; - HashMap folderPushers = new HashMap(); + final Map folderPushers = new HashMap(); public ImapPusher(ImapStore store, PushReceiver receiver) { mStore = store; mReceiver = receiver; } + @Override public void start(List folderNames) { stop(); synchronized (folderPushers) { @@ -3504,6 +3459,7 @@ public class ImapStore extends Store { } } + @Override public void refresh() { synchronized (folderPushers) { for (ImapFolderPusher folderPusher : folderPushers.values()) { @@ -3516,6 +3472,7 @@ public class ImapStore extends Store { } } + @Override public void stop() { if (K9.DEBUG) Log.i(K9.LOG_TAG, "Requested stop of IMAP pusher"); @@ -3534,14 +3491,17 @@ public class ImapStore extends Store { } } + @Override public int getRefreshInterval() { return (getAccount().getIdleRefreshMinutes() * 60 * 1000); } + @Override public long getLastRefresh() { return lastRefresh; } + @Override public void setLastRefresh(long lastRefresh) { this.lastRefresh = lastRefresh; } @@ -3590,9 +3550,9 @@ public class ImapStore extends Store { } private static class FetchBodyCallback implements ImapResponseParser.IImapResponseCallback { - private HashMap mMessageMap; + private Map mMessageMap; - FetchBodyCallback(HashMap mesageMap) { + FetchBodyCallback(Map mesageMap) { mMessageMap = mesageMap; } diff --git a/src/com/fsck/k9/mail/store/LocalStore.java b/src/com/fsck/k9/mail/store/LocalStore.java deleted file mode 100644 index 8e1aca3eb..000000000 --- a/src/com/fsck/k9/mail/store/LocalStore.java +++ /dev/null @@ -1,4550 +0,0 @@ - -package com.fsck.k9.mail.store; - -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; -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.codec.QuotedPrintableOutputStream; -import org.apache.james.mime4j.util.MimeUtil; - -import android.app.Application; -import android.content.ContentResolver; -import android.content.ContentValues; -import android.content.Context; -import android.content.SharedPreferences; -import android.database.Cursor; -import android.database.sqlite.SQLiteDatabase; -import android.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.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.CompositeBody; -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.filter.Base64OutputStream; -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.DbCallback; -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; -import com.fsck.k9.search.SearchSpecification.Attribute; -import com.fsck.k9.search.SearchSpecification.Searchfield; -import com.fsck.k9.search.SqlQueryBuilder; - -/** - *
    - * Implements a SQLite database backed local store for Messages.
    - * 
    - */ -public class LocalStore extends Store implements Serializable { - - private static final long serialVersionUID = -5142141896809423072L; - - 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]; - - /* - * a String containing the columns getMessages expects to work with - * in the correct order. - */ - static private 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 = - "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; - - private 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; - - /** - * Maximum number of messages to perform flag updates on at once. - * - * @see #setFlag(List, Flag, boolean, boolean) - */ - private static final int FLAG_UPDATE_BATCH_SIZE = 500; - - /** - * Maximum number of threads to perform flag updates on at once. - * - * @see #setFlagForThreads(List, Flag, boolean) - */ - private static final int THREAD_FLAG_UPDATE_BATCH_SIZE = 500; - - public static final int DB_VERSION = 50; - - - public static String getColumnNameForFlag(Flag flag) { - switch (flag) { - case SEEN: { - return MessageColumns.READ; - } - case FLAGGED: { - return MessageColumns.FLAGGED; - } - case ANSWERED: { - return MessageColumns.ANSWERED; - } - case FORWARDED: { - return MessageColumns.FORWARDED; - } - default: { - throw new IllegalArgumentException("Flag must be a special column flag"); - } - } - } - - - protected String uUid = null; - - private final Application mApplication; - - private LockableDatabase database; - - private ContentResolver mContentResolver; - - /** - * local://localhost/path/to/database/uuid.db - * This constructor is only used by {@link Store#getLocalInstance(Account, Application)} - * @param account - * @param application - * @throws UnavailableStorageException if not {@link StorageProvider#isReady(Context)} - */ - public LocalStore(final Account account, final Application application) throws MessagingException { - super(account); - database = new LockableDatabase(application, account.getUuid(), new StoreSchemaDefinition()); - - mApplication = application; - mContentResolver = application.getContentResolver(); - database.setStorageProviderId(account.getLocalStorageProviderId()); - uUid = account.getUuid(); - - database.open(); - } - - public void switchLocalStorage(final String newStorageProviderId) throws MessagingException { - database.switchProvider(newStorageProviderId); - } - - protected SharedPreferences getPreferences() { - return Preferences.getPreferences(mApplication).getPreferences(); - } - - 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("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(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(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); - - final File attachmentDirectory = storageManager.getAttachmentDirectory(uUid, - database.getStorageProviderId()); - - return database.execute(false, new DbCallback() { - @Override - public Long doDbWork(final SQLiteDatabase db) { - final File[] files = attachmentDirectory.listFiles(); - long attachmentLength = 0; - if (files != null) { - for (File file : files) { - if (file.exists()) { - attachmentLength += file.length(); - } - } - } - - final File dbFile = storageManager.getDatabase(uUid, database.getStorageProviderId()); - return dbFile.length() + attachmentLength; - } - }); - } - - public void compact() throws MessagingException { - if (K9.DEBUG) - Log.i(K9.LOG_TAG, "Before compaction size = " + getSize()); - - database.execute(false, new DbCallback() { - @Override - public Void doDbWork(final SQLiteDatabase db) throws WrappedException { - db.execSQL("VACUUM"); - return null; - } - }); - if (K9.DEBUG) - Log.i(K9.LOG_TAG, "After compaction size = " + getSize()); - } - - - public void clear() throws MessagingException { - if (K9.DEBUG) - Log.i(K9.LOG_TAG, "Before prune size = " + getSize()); - - pruneCachedAttachments(true); - if (K9.DEBUG) { - Log.i(K9.LOG_TAG, "After prune / before compaction size = " + getSize()); - - Log.i(K9.LOG_TAG, "Before clear folder count = " + getFolderCount()); - Log.i(K9.LOG_TAG, "Before clear message count = " + getMessageCount()); - - Log.i(K9.LOG_TAG, "After prune / before clear size = " + getSize()); - } - // don't delete messages that are Local, since there is no copy on the server. - // Don't delete deleted messages. They are essentially placeholders for UIDs of messages that have - // been deleted locally. They take up insignificant space - database.execute(false, new DbCallback() { - @Override - public Void doDbWork(final SQLiteDatabase db) { - // Delete entries from 'threads' table - db.execSQL("DELETE FROM threads WHERE message_id IN " + - "(SELECT id FROM messages WHERE deleted = 0 AND uid NOT LIKE 'Local%')"); - - // Set 'root' and 'parent' of remaining entries in 'thread' table to 'NULL' to make - // sure the thread structure is in a valid state (this may destroy existing valid - // thread trees, but is much faster than adjusting the tree by removing messages - // one by one). - db.execSQL("UPDATE threads SET root=id, parent=NULL"); - - // Delete entries from 'messages' table - db.execSQL("DELETE FROM messages WHERE deleted = 0 AND uid NOT LIKE 'Local%'"); - return null; - } - }); - - compact(); - - if (K9.DEBUG) { - Log.i(K9.LOG_TAG, "After clear message count = " + getMessageCount()); - - Log.i(K9.LOG_TAG, "After clear size = " + getSize()); - } - } - - public int getMessageCount() throws MessagingException { - return database.execute(false, new DbCallback() { - @Override - public Integer doDbWork(final SQLiteDatabase db) { - Cursor cursor = null; - try { - cursor = db.rawQuery("SELECT COUNT(*) FROM messages", null); - cursor.moveToFirst(); - return cursor.getInt(0); // message count - } finally { - Utility.closeQuietly(cursor); - } - } - }); - } - - public int getFolderCount() throws MessagingException { - return database.execute(false, new DbCallback() { - @Override - public Integer doDbWork(final SQLiteDatabase db) { - Cursor cursor = null; - try { - cursor = db.rawQuery("SELECT COUNT(*) FROM folders", null); - cursor.moveToFirst(); - return cursor.getInt(0); // folder count - } finally { - Utility.closeQuietly(cursor); - } - } - }); - } - - @Override - public LocalFolder getFolder(String name) { - return new LocalFolder(name); - } - - public LocalFolder getFolderById(long folderId) { - return new LocalFolder(folderId); - } - - // TODO this takes about 260-300ms, seems slow. - @Override - public List getPersonalNamespaces(boolean forceListAll) throws MessagingException { - final List folders = new LinkedList(); - try { - database.execute(false, new DbCallback < List > () { - @Override - public List doDbWork(final SQLiteDatabase db) throws WrappedException { - Cursor cursor = null; - - try { - cursor = db.rawQuery("SELECT " + GET_FOLDER_COLS + " FROM folders " + - "ORDER BY name ASC", null); - while (cursor.moveToNext()) { - if (cursor.isNull(FOLDER_ID_INDEX)) { - continue; - } - String folderName = cursor.getString(FOLDER_NAME_INDEX); - LocalFolder folder = new LocalFolder(folderName); - folder.open(cursor); - - folders.add(folder); - } - return folders; - } catch (MessagingException e) { - throw new WrappedException(e); - } finally { - Utility.closeQuietly(cursor); - } - } - }); - } catch (WrappedException e) { - throw(MessagingException) e.getCause(); - } - return folders; - } - - @Override - public void checkSettings() throws MessagingException { - } - - public void delete() throws UnavailableStorageException { - database.delete(); - } - - public void recreate() throws UnavailableStorageException { - database.recreate(); - } - - public void pruneCachedAttachments() throws MessagingException { - pruneCachedAttachments(false); - } - - /** - * Deletes all cached attachments for the entire store. - * @param force - * @throws com.fsck.k9.mail.MessagingException - */ - private void pruneCachedAttachments(final boolean force) throws MessagingException { - database.execute(false, new DbCallback() { - @Override - public Void doDbWork(final SQLiteDatabase db) throws WrappedException { - if (force) { - ContentValues cv = new ContentValues(); - cv.putNull("content_uri"); - db.update("attachments", cv, null, null); - } - final StorageManager storageManager = StorageManager.getInstance(mApplication); - File[] files = storageManager.getAttachmentDirectory(uUid, database.getStorageProviderId()).listFiles(); - for (File file : files) { - if (file.exists()) { - if (!force) { - Cursor cursor = null; - try { - cursor = db.query( - "attachments", - new String[] { "store_data" }, - "id = ?", - new String[] { file.getName() }, - null, - null, - null); - if (cursor.moveToNext()) { - if (cursor.getString(0) == null) { - if (K9.DEBUG) - Log.d(K9.LOG_TAG, "Attachment " + file.getAbsolutePath() + " has no store data, not deleting"); - /* - * If the attachment has no store data it is not recoverable, so - * we won't delete it. - */ - continue; - } - } - } finally { - Utility.closeQuietly(cursor); - } - } - if (!force) { - try { - ContentValues cv = new ContentValues(); - cv.putNull("content_uri"); - db.update("attachments", cv, "id = ?", new String[] { file.getName() }); - } catch (Exception e) { - /* - * If the row has gone away before we got to mark it not-downloaded that's - * okay. - */ - } - } - if (!file.delete()) { - file.deleteOnExit(); - } - } - } - return null; - } - }); - } - - public void resetVisibleLimits() throws UnavailableStorageException { - resetVisibleLimits(mAccount.getDisplayCount()); - } - - public void resetVisibleLimits(int visibleLimit) throws UnavailableStorageException { - final ContentValues cv = new ContentValues(); - cv.put("visible_limit", Integer.toString(visibleLimit)); - database.execute(false, new DbCallback() { - @Override - public Void doDbWork(final SQLiteDatabase db) throws WrappedException { - db.update("folders", cv, null, null); - return null; - } - }); - } - - public ArrayList getPendingCommands() throws UnavailableStorageException { - return database.execute(false, new DbCallback>() { - @Override - public ArrayList doDbWork(final SQLiteDatabase db) throws WrappedException { - Cursor cursor = null; - try { - cursor = db.query("pending_commands", - new String[] { "id", "command", "arguments" }, - null, - null, - null, - null, - "id ASC"); - ArrayList commands = new ArrayList(); - while (cursor.moveToNext()) { - PendingCommand command = new PendingCommand(); - command.mId = cursor.getLong(0); - command.command = cursor.getString(1); - String arguments = cursor.getString(2); - command.arguments = arguments.split(","); - for (int i = 0; i < command.arguments.length; i++) { - command.arguments[i] = Utility.fastUrlDecode(command.arguments[i]); - } - commands.add(command); - } - return commands; - } finally { - Utility.closeQuietly(cursor); - } - } - }); - } - - public void addPendingCommand(PendingCommand command) throws UnavailableStorageException { - try { - for (int i = 0; i < command.arguments.length; i++) { - command.arguments[i] = URLEncoder.encode(command.arguments[i], "UTF-8"); - } - final ContentValues cv = new ContentValues(); - cv.put("command", command.command); - cv.put("arguments", Utility.combine(command.arguments, ',')); - database.execute(false, new DbCallback() { - @Override - public Void doDbWork(final SQLiteDatabase db) throws WrappedException { - db.insert("pending_commands", "command", cv); - return null; - } - }); - } catch (UnsupportedEncodingException uee) { - throw new Error("Aparently UTF-8 has been lost to the annals of history."); - } - } - - public void removePendingCommand(final PendingCommand command) throws UnavailableStorageException { - database.execute(false, new DbCallback() { - @Override - public Void doDbWork(final SQLiteDatabase db) throws WrappedException { - db.delete("pending_commands", "id = ?", new String[] { Long.toString(command.mId) }); - return null; - } - }); - } - - public void removePendingCommands() throws UnavailableStorageException { - database.execute(false, new DbCallback() { - @Override - public Void doDbWork(final SQLiteDatabase db) throws WrappedException { - db.delete("pending_commands", null, null); - return null; - } - }); - } - - public static class PendingCommand { - private long mId; - public String command; - public String[] arguments; - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append(command); - sb.append(": "); - for (String argument : arguments) { - sb.append(", "); - sb.append(argument); - //sb.append("\n"); - } - return sb.toString(); - } - } - - @Override - public boolean isMoveCapable() { - return true; - } - - @Override - public boolean isCopyCapable() { - return true; - } - - public Message[] searchForMessages(MessageRetrievalListener retrievalListener, - LocalSearch search) throws MessagingException { - - StringBuilder query = new StringBuilder(); - List queryArgs = new ArrayList(); - SqlQueryBuilder.buildWhereClause(mAccount, search.getConditions(), query, queryArgs); - - // Avoid "ambiguous column name" error by prefixing "id" with the message table name - String where = SqlQueryBuilder.addPrefixToSelection(new String[] { "id" }, - "messages.", query.toString()); - - String[] selectionArgs = queryArgs.toArray(EMPTY_STRING_ARRAY); - - String sqlQuery = "SELECT " + GET_MESSAGES_COLS + "FROM messages " + - "LEFT JOIN threads ON (threads.message_id = messages.id) " + - "LEFT JOIN folders ON (folders.id = messages.folder_id) WHERE " + - "((empty IS NULL OR empty != 1) AND deleted = 0)" + - ((!StringUtils.isNullOrEmpty(where)) ? " AND (" + where + ")" : "") + - " ORDER BY date DESC"; - - if (K9.DEBUG) { - Log.d(K9.LOG_TAG, "Query = " + sqlQuery); - } - - return getMessages(retrievalListener, null, sqlQuery, selectionArgs); - } - - /* - * Given a query string, actually do the query for the messages and - * call the MessageRetrievalListener for each one - */ - private Message[] getMessages( - final MessageRetrievalListener listener, - final LocalFolder folder, - final String queryString, final String[] placeHolders - ) throws MessagingException { - final ArrayList messages = new ArrayList(); - final int j = database.execute(false, new DbCallback() { - @Override - public Integer doDbWork(final SQLiteDatabase db) throws WrappedException { - Cursor cursor = null; - int i = 0; - try { - cursor = db.rawQuery(queryString + " LIMIT 10", placeHolders); - - while (cursor.moveToNext()) { - LocalMessage message = new LocalMessage(null, folder); - message.populateFromGetMessageCursor(cursor); - - messages.add(message); - if (listener != null) { - listener.messageFinished(message, i, -1); - } - i++; - } - cursor.close(); - cursor = db.rawQuery(queryString + " LIMIT -1 OFFSET 10", placeHolders); - - while (cursor.moveToNext()) { - LocalMessage message = new LocalMessage(null, folder); - message.populateFromGetMessageCursor(cursor); - - messages.add(message); - if (listener != null) { - listener.messageFinished(message, i, -1); - } - i++; - } - } catch (Exception e) { - Log.d(K9.LOG_TAG, "Got an exception", e); - } finally { - Utility.closeQuietly(cursor); - } - return i; - } - }); - if (listener != null) { - listener.messagesFinished(j); - } - - return messages.toArray(EMPTY_MESSAGE_ARRAY); - - } - - public Message[] getMessagesInThread(final long rootId) throws MessagingException { - String rootIdString = Long.toString(rootId); - - LocalSearch search = new LocalSearch(); - search.and(Searchfield.THREAD_ID, rootIdString, Attribute.EQUALS); - - return searchForMessages(null, search); - } - - public AttachmentInfo getAttachmentInfo(final String attachmentId) throws UnavailableStorageException { - return database.execute(false, new DbCallback() { - @Override - public AttachmentInfo doDbWork(final SQLiteDatabase db) throws WrappedException { - String name; - String type; - int size; - Cursor cursor = null; - try { - cursor = db.query( - "attachments", - new String[] { "name", "size", "mime_type" }, - "id = ?", - new String[] { attachmentId }, - null, - null, - null); - if (!cursor.moveToFirst()) { - return null; - } - name = cursor.getString(0); - size = cursor.getInt(1); - type = cursor.getString(2); - final AttachmentInfo attachmentInfo = new AttachmentInfo(); - attachmentInfo.name = name; - attachmentInfo.size = size; - attachmentInfo.type = type; - return attachmentInfo; - } finally { - Utility.closeQuietly(cursor); - } - } - }); - } - - public static class AttachmentInfo { - public String name; - public int size; - public String type; - } - - public void createFolders(final List foldersToCreate, final int visibleLimit) throws UnavailableStorageException { - database.execute(true, new DbCallback() { - @Override - public Void doDbWork(final SQLiteDatabase db) throws WrappedException { - for (LocalFolder folder : foldersToCreate) { - String name = folder.getName(); - final LocalFolder.PreferencesHolder prefHolder = folder.new PreferencesHolder(); - - // When created, special folders should always be displayed - // inbox should be integrated - // and the inbox and drafts folders should be syncced by default - if (mAccount.isSpecialFolder(name)) { - prefHolder.inTopGroup = true; - prefHolder.displayClass = LocalFolder.FolderClass.FIRST_CLASS; - if (name.equalsIgnoreCase(mAccount.getInboxFolderName())) { - prefHolder.integrate = true; - prefHolder.notifyClass = LocalFolder.FolderClass.FIRST_CLASS; - prefHolder.pushClass = LocalFolder.FolderClass.FIRST_CLASS; - } else { - prefHolder.pushClass = LocalFolder.FolderClass.INHERITED; - - } - if (name.equalsIgnoreCase(mAccount.getInboxFolderName()) || - name.equalsIgnoreCase(mAccount.getDraftsFolderName())) { - prefHolder.syncClass = LocalFolder.FolderClass.FIRST_CLASS; - } else { - prefHolder.syncClass = LocalFolder.FolderClass.NO_CLASS; - } - } - folder.refresh(name, prefHolder); // Recover settings from Preferences - - db.execSQL("INSERT INTO folders (name, visible_limit, top_group, display_class, poll_class, notify_class, push_class, integrate) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", new Object[] { - name, - visibleLimit, - prefHolder.inTopGroup ? 1 : 0, - prefHolder.displayClass.name(), - prefHolder.syncClass.name(), - prefHolder.notifyClass.name(), - prefHolder.pushClass.name(), - prefHolder.integrate ? 1 : 0, - }); - - } - return null; - } - }); - } - - - private String serializeFlags(Flag[] flags) { - List extraFlags = new ArrayList(); - - for (Flag flag : flags) { - switch (flag) { - case DELETED: - case SEEN: - case FLAGGED: - case ANSWERED: - case FORWARDED: { - break; - } - default: { - extraFlags.add(flag); - } - } - } - - return Utility.combine(extraFlags.toArray(EMPTY_FLAG_ARRAY), ',').toUpperCase(Locale.US); - } - - public 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 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; - 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 LocalStore.LocalTextBody) { - text = ((LocalStore.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; - } - } - - 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; - } - } - - 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; - 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() { - Uri uri = Uri.withAppendedPath(EmailProvider.CONTENT_URI, "account/" + uUid + "/messages"); - mContentResolver.notifyChange(uri, null); - } - - /** - * Split database operations with a large set of arguments into multiple SQL statements. - * - *

    - * At the time of this writing (2012-12-06) SQLite only supports around 1000 arguments. That's - * why we have to split SQL statements with a large set of arguments into multiple SQL - * statements each working on a subset of the arguments. - *

    - * - * @param selectionCallback - * Supplies the argument set and the code to query/update the database. - * @param batchSize - * The maximum size of the selection set in each SQL statement. - * - * @throws MessagingException - */ - public void doBatchSetSelection(final BatchSetSelection selectionCallback, final int batchSize) - throws MessagingException { - - final List selectionArgs = new ArrayList(); - int start = 0; - - while (start < selectionCallback.getListSize()) { - final StringBuilder selection = new StringBuilder(); - - selection.append(" IN ("); - - int count = Math.min(selectionCallback.getListSize() - start, batchSize); - - for (int i = start, end = start + count; i < end; i++) { - if (i > start) { - selection.append(",?"); - } else { - selection.append("?"); - } - - selectionArgs.add(selectionCallback.getListItem(i)); - } - - selection.append(")"); - - try { - database.execute(true, new DbCallback() { - @Override - public Void doDbWork(final SQLiteDatabase db) throws WrappedException, - UnavailableStorageException { - - selectionCallback.doDbWork(db, selection.toString(), - selectionArgs.toArray(EMPTY_STRING_ARRAY)); - - return null; - } - }); - - selectionCallback.postDbWork(); - - } catch (WrappedException e) { - throw(MessagingException) e.getCause(); - } - - selectionArgs.clear(); - start += count; - } - } - - /** - * Defines the behavior of {@link LocalStore#doBatchSetSelection(BatchSetSelection, int)}. - */ - public interface BatchSetSelection { - /** - * @return The size of the argument list. - */ - int getListSize(); - - /** - * Get a specific item of the argument list. - * - * @param index - * The index of the item. - * - * @return Item at position {@code i} of the argument list. - */ - String getListItem(int index); - - /** - * Execute the SQL statement. - * - * @param db - * Use this {@link SQLiteDatabase} instance for your SQL statement. - * @param selectionSet - * A partial selection string containing place holders for the argument list, e.g. - * {@code " IN (?,?,?)"} (starts with a space). - * @param selectionArgs - * The current subset of the argument list. - * @throws UnavailableStorageException - */ - void doDbWork(SQLiteDatabase db, String selectionSet, String[] selectionArgs) - throws UnavailableStorageException; - - /** - * This will be executed after each invocation of - * {@link #doDbWork(SQLiteDatabase, String, String[])} (after the transaction has been - * committed). - */ - void postDbWork(); - } - - /** - * Change the state of a flag for a list of messages. - * - *

    - * The goal of this method is to be fast. Currently this means using as few SQL UPDATE - * statements as possible. - * - * @param messageIds - * A list of primary keys in the "messages" table. - * @param flag - * The flag to change. This must be a flag with a separate column in the database. - * @param newState - * {@code true}, if the flag should be set. {@code false}, otherwise. - * - * @throws MessagingException - */ - public void setFlag(final List messageIds, final Flag flag, final boolean newState) - throws MessagingException { - - final ContentValues cv = new ContentValues(); - cv.put(getColumnNameForFlag(flag), newState); - - doBatchSetSelection(new BatchSetSelection() { - - @Override - public int getListSize() { - return messageIds.size(); - } - - @Override - public String getListItem(int index) { - return Long.toString(messageIds.get(index)); - } - - @Override - public void doDbWork(SQLiteDatabase db, String selectionSet, String[] selectionArgs) - throws UnavailableStorageException { - - db.update("messages", cv, "(empty IS NULL OR empty != 1) AND id" + selectionSet, - selectionArgs); - } - - @Override - public void postDbWork() { - notifyChange(); - } - }, FLAG_UPDATE_BATCH_SIZE); - } - - /** - * Change the state of a flag for a list of threads. - * - *

    - * The goal of this method is to be fast. Currently this means using as few SQL UPDATE - * statements as possible. - * - * @param threadRootIds - * A list of root thread IDs. - * @param flag - * The flag to change. This must be a flag with a separate column in the database. - * @param newState - * {@code true}, if the flag should be set. {@code false}, otherwise. - * - * @throws MessagingException - */ - public void setFlagForThreads(final List threadRootIds, Flag flag, final boolean newState) - throws MessagingException { - - final String flagColumn = getColumnNameForFlag(flag); - - doBatchSetSelection(new BatchSetSelection() { - - @Override - public int getListSize() { - return threadRootIds.size(); - } - - @Override - public String getListItem(int index) { - return Long.toString(threadRootIds.get(index)); - } - - @Override - public void doDbWork(SQLiteDatabase db, String selectionSet, String[] selectionArgs) - throws UnavailableStorageException { - - db.execSQL("UPDATE messages SET " + flagColumn + " = " + ((newState) ? "1" : "0") + - " WHERE id IN (" + - "SELECT m.id FROM threads t " + - "LEFT JOIN messages m ON (t.message_id = m.id) " + - "WHERE (m.empty IS NULL OR m.empty != 1) AND m.deleted = 0 " + - "AND t.root" + selectionSet + ")", - selectionArgs); - } - - @Override - public void postDbWork() { - notifyChange(); - } - }, THREAD_FLAG_UPDATE_BATCH_SIZE); - } - - /** - * Get folder name and UID for the supplied messages. - * - * @param messageIds - * A list of primary keys in the "messages" table. - * @param threadedList - * If this is {@code true}, {@code messageIds} contains the thread IDs of the messages - * at the root of a thread. In that case return UIDs for all messages in these threads. - * If this is {@code false} only the UIDs for messages in {@code messageIds} are - * returned. - * - * @return The list of UIDs for the messages grouped by folder name. - * - * @throws MessagingException - */ - public Map> getFoldersAndUids(final List messageIds, - final boolean threadedList) throws MessagingException { - - final Map> folderMap = new HashMap>(); - - doBatchSetSelection(new BatchSetSelection() { - - @Override - public int getListSize() { - return messageIds.size(); - } - - @Override - public String getListItem(int index) { - return Long.toString(messageIds.get(index)); - } - - @Override - public void doDbWork(SQLiteDatabase db, String selectionSet, String[] selectionArgs) - throws UnavailableStorageException { - - if (threadedList) { - String sql = "SELECT m.uid, f.name " + - "FROM threads t " + - "LEFT JOIN messages m ON (t.message_id = m.id) " + - "LEFT JOIN folders f ON (m.folder_id = f.id) " + - "WHERE (m.empty IS NULL OR m.empty != 1) AND m.deleted = 0 " + - "AND t.root" + selectionSet; - - getDataFromCursor(db.rawQuery(sql, selectionArgs)); - - } else { - String sql = - "SELECT m.uid, f.name " + - "FROM messages m " + - "LEFT JOIN folders f ON (m.folder_id = f.id) " + - "WHERE (m.empty IS NULL OR m.empty != 1) AND m.id" + selectionSet; - - getDataFromCursor(db.rawQuery(sql, selectionArgs)); - } - } - - private void getDataFromCursor(Cursor cursor) { - try { - while (cursor.moveToNext()) { - String uid = cursor.getString(0); - String folderName = cursor.getString(1); - - List uidList = folderMap.get(folderName); - if (uidList == null) { - uidList = new ArrayList(); - folderMap.put(folderName, uidList); - } - - uidList.add(uid); - } - } finally { - cursor.close(); - } - } - - @Override - public void postDbWork() { - notifyChange(); - - } - }, UID_CHECK_BATCH_SIZE); - - return folderMap; - } -} diff --git a/src/com/fsck/k9/mail/store/LockableDatabase.java b/src/com/fsck/k9/mail/store/LockableDatabase.java index 81138d6e8..f08076087 100644 --- a/src/com/fsck/k9/mail/store/LockableDatabase.java +++ b/src/com/fsck/k9/mail/store/LockableDatabase.java @@ -99,7 +99,7 @@ public class LockableDatabase { } try { - openOrCreateDataspace(mApplication); + openOrCreateDataspace(); } catch (UnavailableStorageException e) { Log.e(K9.LOG_TAG, "Unable to open DB on mount", e); } @@ -346,7 +346,7 @@ public class LockableDatabase { mStorageProviderId = newProviderId; // re-initialize this class with the new Uri - openOrCreateDataspace(mApplication); + openOrCreateDataspace(); } finally { unlockWrite(newProviderId); } @@ -358,7 +358,7 @@ public class LockableDatabase { public void open() throws UnavailableStorageException { lockWrite(); try { - openOrCreateDataspace(mApplication); + openOrCreateDataspace(); } finally { unlockWrite(); } @@ -367,33 +367,20 @@ public class LockableDatabase { /** * - * @param application * @throws UnavailableStorageException */ - protected void openOrCreateDataspace(final Application application) throws UnavailableStorageException { + private void openOrCreateDataspace() throws UnavailableStorageException { lockWrite(); 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(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(databaseFile); } if (mDb.getVersion() != mSchemaDefinition.getVersion()) { mSchemaDefinition.doDbUpgrade(mDb); @@ -403,6 +390,17 @@ public class LockableDatabase { } } + private void doOpenOrCreateDb(final File databaseFile) { + if (StorageManager.InternalStorageProvider.ID.equals(mStorageProviderId)) { + // internal storage + mDb = mApplication.openOrCreateDatabase(databaseFile.getName(), Context.MODE_PRIVATE, + null); + } else { + // external storage + mDb = SQLiteDatabase.openOrCreateDatabase(databaseFile, null); + } + } + /** * @param providerId * Never null. @@ -412,10 +410,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 +424,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 +460,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 +476,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)); @@ -490,7 +486,7 @@ public class LockableDatabase { } if (recreate) { - openOrCreateDataspace(mApplication); + openOrCreateDataspace(); } else { // stop waiting for mount/unmount events getStorageManager().removeListener(mStorageListener); diff --git a/src/com/fsck/k9/mail/store/Pop3Store.java b/src/com/fsck/k9/mail/store/Pop3Store.java index 37c9b8efe..5e2ee632a 100644 --- a/src/com/fsck/k9/mail/store/Pop3Store.java +++ b/src/com/fsck/k9/mail/store/Pop3Store.java @@ -7,6 +7,7 @@ import com.fsck.k9.Account; import com.fsck.k9.K9; import com.fsck.k9.R; import com.fsck.k9.controller.MessageRetrievalListener; +import com.fsck.k9.helper.UrlEncodingHelper; import com.fsck.k9.helper.Utility; import com.fsck.k9.mail.*; import com.fsck.k9.mail.filter.Base64; @@ -23,6 +24,8 @@ import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; import java.util.Date; import java.util.LinkedList; import java.util.HashMap; @@ -115,28 +118,23 @@ public class Pop3Store extends Store { AuthType authType = AuthType.PLAIN; if (pop3Uri.getUserInfo() != null) { - try { - int userIndex = 0, passwordIndex = 1; - String userinfo = pop3Uri.getUserInfo(); - String[] userInfoParts = userinfo.split(":"); - if (userInfoParts.length > 2 || userinfo.endsWith(":") ) { - // If 'userinfo' ends with ":" the password is empty. This can only happen - // after an account was imported (so authType and username are present). - userIndex++; - passwordIndex++; - authType = AuthType.valueOf(userInfoParts[0]); + int userIndex = 0, passwordIndex = 1; + String userinfo = pop3Uri.getUserInfo(); + String[] userInfoParts = userinfo.split(":"); + if (userInfoParts.length > 2 || userinfo.endsWith(":") ) { + // If 'userinfo' ends with ":" the password is empty. This can only happen + // after an account was imported (so authType and username are present). + userIndex++; + passwordIndex++; + authType = AuthType.valueOf(userInfoParts[0]); + } + username = UrlEncodingHelper.decodeUtf8(userInfoParts[userIndex]); + if (userInfoParts.length > passwordIndex) { + if (authType == AuthType.EXTERNAL) { + clientCertificateAlias = UrlEncodingHelper.decodeUtf8(userInfoParts[passwordIndex]); + } else { + password = UrlEncodingHelper.decodeUtf8(userInfoParts[passwordIndex]); } - username = URLDecoder.decode(userInfoParts[userIndex], "UTF-8"); - if (userInfoParts.length > passwordIndex) { - if (authType == AuthType.EXTERNAL) { - clientCertificateAlias = URLDecoder.decode(userInfoParts[passwordIndex], "UTF-8"); - } else { - password = URLDecoder.decode(userInfoParts[passwordIndex], "UTF-8"); - } - } - } catch (UnsupportedEncodingException enc) { - // This shouldn't happen since the encoding is hardcoded to UTF-8 - throw new IllegalArgumentException("Couldn't urldecode username or password.", enc); } } @@ -156,19 +154,11 @@ public class Pop3Store extends Store { * @see Pop3Store#decodeUri(String) */ public static String createUri(ServerSettings server) { - String userEnc; - String passwordEnc; - String clientCertificateAliasEnc; - try { - userEnc = URLEncoder.encode(server.username, "UTF-8"); - passwordEnc = (server.password != null) ? - URLEncoder.encode(server.password, "UTF-8") : ""; - clientCertificateAliasEnc = (server.clientCertificateAlias != null) ? - URLEncoder.encode(server.clientCertificateAlias, "UTF-8") : ""; - } - catch (UnsupportedEncodingException e) { - throw new IllegalArgumentException("Could not encode username or password", e); - } + String userEnc = UrlEncodingHelper.encodeUtf8(server.username); + String passwordEnc = (server.password != null) ? + UrlEncodingHelper.encodeUtf8(server.password) : ""; + String clientCertificateAliasEnc = (server.clientCertificateAlias != null) ? + UrlEncodingHelper.encodeUtf8(server.clientCertificateAlias) : ""; String scheme; switch (server.connectionSecurity) { @@ -208,7 +198,7 @@ public class Pop3Store extends Store { private String mClientCertificateAlias; private AuthType mAuthType; private ConnectionSecurity mConnectionSecurity; - private HashMap mFolders = new HashMap(); + private Map mFolders = new HashMap(); private Pop3Capabilities mCapabilities; /** @@ -286,9 +276,9 @@ public class Pop3Store extends Store { private Socket mSocket; private InputStream mIn; private OutputStream mOut; - private HashMap mUidToMsgMap = new HashMap(); - private HashMap mMsgNumToMsgMap = new HashMap(); - private HashMap mUidToMsgNumMap = new HashMap(); + private Map mUidToMsgMap = new HashMap(); + private Map mMsgNumToMsgMap = new HashMap(); + private Map mUidToMsgNumMap = new HashMap(); private String mName; private int mMessageCount; @@ -578,7 +568,7 @@ public class Pop3Store extends Store { } @Override - public Message[] getMessages(int start, int end, Date earliestDate, MessageRetrievalListener listener) + public List getMessages(int start, int end, Date earliestDate, MessageRetrievalListener listener) throws MessagingException { if (start < 1 || end < 1 || end < start) { throw new MessagingException(String.format(Locale.US, "Invalid message set %d %d", @@ -589,7 +579,7 @@ public class Pop3Store extends Store { } catch (IOException ioe) { throw new MessagingException("getMessages", ioe); } - ArrayList messages = new ArrayList(); + List messages = new ArrayList(); int i = 0; for (int msgNum = start; msgNum <= end; msgNum++) { Pop3Message message = mMsgNumToMsgMap.get(msgNum); @@ -611,7 +601,7 @@ public class Pop3Store extends Store { listener.messageFinished(message, i++, (end - start) + 1); } } - return messages.toArray(new Message[messages.size()]); + return messages; } /** @@ -698,7 +688,7 @@ public class Pop3Store extends Store { } } - private void indexUids(ArrayList uids) + private void indexUids(List uids) throws MessagingException, IOException { Set unindexedUids = new HashSet(); for (String uid : uids) { @@ -753,12 +743,12 @@ public class Pop3Store extends Store { } @Override - public Message[] getMessages(MessageRetrievalListener listener) throws MessagingException { + public List getMessages(MessageRetrievalListener listener) throws MessagingException { throw new UnsupportedOperationException("Pop3: No getMessages"); } @Override - public Message[] getMessages(String[] uids, MessageRetrievalListener listener) + public List getMessages(String[] uids, MessageRetrievalListener listener) throws MessagingException { throw new UnsupportedOperationException("Pop3: No getMessages by uids"); } @@ -771,12 +761,12 @@ public class Pop3Store extends Store { * @throws MessagingException */ @Override - public void fetch(Message[] messages, FetchProfile fp, MessageRetrievalListener listener) + public void fetch(List messages, FetchProfile fp, MessageRetrievalListener listener) throws MessagingException { - if (messages == null || messages.length == 0) { + if (messages == null || messages.isEmpty()) { return; } - ArrayList uids = new ArrayList(); + List uids = new ArrayList(); for (Message message : messages) { uids.add(message.getUid()); } @@ -798,8 +788,8 @@ public class Pop3Store extends Store { } catch (IOException ioe) { throw new MessagingException("fetch", ioe); } - for (int i = 0, count = messages.length; i < count; i++) { - Message message = messages[i]; + for (int i = 0, count = messages.size(); i < count; i++) { + Message message = messages.get(i); if (!(message instanceof Pop3Message)) { throw new MessagingException("Pop3Store.fetch called with non-Pop3 Message"); } @@ -837,7 +827,7 @@ public class Pop3Store extends Store { } } - private void fetchEnvelope(Message[] messages, + private void fetchEnvelope(List messages, MessageRetrievalListener listener) throws IOException, MessagingException { int unsizedMessages = 0; for (Message message : messages) { @@ -853,8 +843,8 @@ public class Pop3Store extends Store { * In extreme cases we'll do a command per message instead of a bulk request * to hopefully save some time and bandwidth. */ - for (int i = 0, count = messages.length; i < count; i++) { - Message message = messages[i]; + for (int i = 0, count = messages.size(); i < count; i++) { + Message message = messages.get(i); if (!(message instanceof Pop3Message)) { throw new MessagingException("Pop3Store.fetch called with non-Pop3 Message"); } @@ -877,7 +867,7 @@ public class Pop3Store extends Store { for (Message message : messages) { msgUidIndex.add(message.getUid()); } - int i = 0, count = messages.length; + int i = 0, count = messages.size(); String response = executeSimpleCommand(LIST_COMMAND); while ((response = readLine()) != null) { if (response.equals(".")) { @@ -968,7 +958,7 @@ public class Pop3Store extends Store { } @Override - public Map appendMessages(Message[] messages) throws MessagingException { + public Map appendMessages(List messages) throws MessagingException { return null; } @@ -977,8 +967,8 @@ public class Pop3Store extends Store { } @Override - public void delete(Message[] msgs, String trashFolderName) throws MessagingException { - setFlags(msgs, new Flag[] { Flag.DELETED }, true); + public void delete(List msgs, String trashFolderName) throws MessagingException { + setFlags(msgs, Collections.singleton(Flag.DELETED), true); } @Override @@ -987,20 +977,20 @@ public class Pop3Store extends Store { } @Override - public void setFlags(Flag[] flags, boolean value) throws MessagingException { - throw new UnsupportedOperationException("POP3: No setFlags(Flag[],boolean)"); + public void setFlags(final Set flags, boolean value) throws MessagingException { + throw new UnsupportedOperationException("POP3: No setFlags(Set,boolean)"); } @Override - public void setFlags(Message[] messages, Flag[] flags, boolean value) + public void setFlags(List messages, final Set flags, boolean value) throws MessagingException { - if (!value || !Utility.arrayContains(flags, Flag.DELETED)) { + if (!value || !flags.contains(Flag.DELETED)) { /* * The only flagging we support is setting the Deleted flag. */ return; } - ArrayList uids = new ArrayList(); + List uids = new ArrayList(); try { for (Message message : messages) { uids.add(message.getUid()); @@ -1194,15 +1184,10 @@ public class Pop3Store extends Store { mSize = size; } - @Override - protected void parse(InputStream in) throws IOException, MessagingException { - super.parse(in); - } - @Override public void setFlag(Flag flag, boolean set) throws MessagingException { super.setFlag(flag, set); - mFolder.setFlags(new Message[] { this }, new Flag[] { flag }, set); + mFolder.setFlags(Collections.singletonList(this), Collections.singleton(flag), set); } @Override @@ -1240,9 +1225,9 @@ public class Pop3Store extends Store { } static class Pop3ResponseInputStream extends InputStream { - InputStream mIn; - boolean mStartOfLine = true; - boolean mFinished; + private InputStream mIn; + private boolean mStartOfLine = true; + private boolean mFinished; public Pop3ResponseInputStream(InputStream in) { mIn = in; diff --git a/src/com/fsck/k9/mail/store/StorageManager.java b/src/com/fsck/k9/mail/store/StorageManager.java index 27dd266d1..68cc216df 100644 --- a/src/com/fsck/k9/mail/store/StorageManager.java +++ b/src/com/fsck/k9/mail/store/StorageManager.java @@ -176,12 +176,12 @@ public class StorageManager { /** * The root of the denoted storage. Used for mount points checking. */ - protected File mRoot; + private File mRoot; /** * Choosen base directory */ - protected File mApplicationDir; + private File mApplicationDir; @Override public void init(final Context context) { @@ -258,7 +258,7 @@ public class StorageManager { public static final String ID = "InternalStorage"; - protected File mRoot; + private File mRoot; @Override public String getId() { @@ -328,13 +328,14 @@ public class StorageManager { /** * Root of the denoted storage. */ - protected File mRoot; + private File mRoot; /** * Choosen base directory. */ - protected File mApplicationDirectory; + private File mApplicationDirectory; + @Override public String getId() { return ID; } @@ -392,6 +393,7 @@ public class StorageManager { public static final String ID = "HtcIncredibleStorage"; + @Override public String getId() { return ID; } @@ -428,6 +430,7 @@ public class StorageManager { public static final String ID = "SamsungGalaxySStorage"; + @Override public String getId() { return ID; } diff --git a/src/com/fsck/k9/mail/store/WebDavStore.java b/src/com/fsck/k9/mail/store/WebDavStore.java index 44493588d..27d0c7ccb 100644 --- a/src/com/fsck/k9/mail/store/WebDavStore.java +++ b/src/com/fsck/k9/mail/store/WebDavStore.java @@ -5,6 +5,8 @@ import android.util.Log; import com.fsck.k9.Account; import com.fsck.k9.K9; import com.fsck.k9.controller.MessageRetrievalListener; + +import com.fsck.k9.helper.UrlEncodingHelper; import com.fsck.k9.helper.Utility; import com.fsck.k9.mail.*; import com.fsck.k9.mail.filter.EOLConvertingOutputStream; @@ -40,8 +42,6 @@ import javax.xml.parsers.SAXParserFactory; import java.io.*; import java.net.URI; import java.net.URISyntaxException; -import java.net.URLDecoder; -import java.net.URLEncoder; import java.security.KeyManagementException; import java.security.NoSuchAlgorithmException; import java.text.DateFormat; @@ -65,8 +65,6 @@ public class WebDavStore extends Store { private static final String[] EMPTY_STRING_ARRAY = new String[0]; - private static final Message[] EMPTY_MESSAGE_ARRAY = new Message[0]; - // These are the ids used from Exchange server to identify the special folders // http://social.technet.microsoft.com/Forums/en/exchangesvrdevelopment/thread/1cd2e98c-8a12-44bd-a3e3-9c5ee9e4e14d private static final String DAV_MAIL_INBOX_FOLDER = "inbox"; @@ -139,22 +137,17 @@ public class WebDavStore extends Store { String userInfo = webDavUri.getUserInfo(); if (userInfo != null) { - try { - String[] userInfoParts = userInfo.split(":"); - username = URLDecoder.decode(userInfoParts[0], "UTF-8"); - String userParts[] = username.split("\\\\", 2); + String[] userInfoParts = userInfo.split(":"); + username = UrlEncodingHelper.decodeUtf8(userInfoParts[0]); + String userParts[] = username.split("\\\\", 2); - if (userParts.length > 1) { - alias = userParts[1]; - } else { - alias = username; - } - if (userInfoParts.length > 1) { - password = URLDecoder.decode(userInfoParts[1], "UTF-8"); - } - } catch (UnsupportedEncodingException enc) { - // This shouldn't happen since the encoding is hardcoded to UTF-8 - throw new IllegalArgumentException("Couldn't urldecode username or password.", enc); + if (userParts.length > 1) { + alias = userParts[1]; + } else { + alias = username; + } + if (userInfoParts.length > 1) { + password = UrlEncodingHelper.decodeUtf8(userInfoParts[1]); } } @@ -194,16 +187,9 @@ public class WebDavStore extends Store { * @see WebDavStore#decodeUri(String) */ public static String createUri(ServerSettings server) { - String userEnc; - String passwordEnc; - try { - userEnc = URLEncoder.encode(server.username, "UTF-8"); - passwordEnc = (server.password != null) ? - URLEncoder.encode(server.password, "UTF-8") : ""; - } - catch (UnsupportedEncodingException e) { - throw new IllegalArgumentException("Could not encode username or password", e); - } + String userEnc = UrlEncodingHelper.encodeUtf8(server.username); + String passwordEnc = (server.password != null) ? + UrlEncodingHelper.encodeUtf8(server.password) : ""; String scheme; switch (server.connectionSecurity) { @@ -305,7 +291,7 @@ public class WebDavStore extends Store { private String mCachedLoginUrl; private Folder mSendFolder = null; - private HashMap mFolderList = new HashMap(); + private Map mFolderList = new HashMap(); public WebDavStore(Account account) throws MessagingException { @@ -375,7 +361,7 @@ public class WebDavStore extends Store { @Override public List getPersonalNamespaces(boolean forceListAll) throws MessagingException { - LinkedList folderList = new LinkedList(); + List folderList = new LinkedList(); /** * We have to check authentication here so we have the proper URL stored */ @@ -385,13 +371,13 @@ public class WebDavStore extends Store { * Firstly we get the "special" folders list (inbox, outbox, etc) * and setup the account accordingly */ - HashMap headers = new HashMap(); + Map headers = new HashMap(); DataSet dataset = new DataSet(); headers.put("Depth", "0"); headers.put("Brief", "t"); dataset = processRequest(this.mUrl, "PROPFIND", getSpecialFoldersList(), headers); - HashMap specialFoldersMap = dataset.getSpecialFolderToUrl(); + Map specialFoldersMap = dataset.getSpecialFolderToUrl(); String folderName = getFolderName(specialFoldersMap.get(DAV_MAIL_INBOX_FOLDER)); if (folderName != null) { mAccount.setAutoExpandFolderName(folderName); @@ -479,7 +465,6 @@ public class WebDavStore extends Store { } if (folderSlash > 0) { - String folderName; String fullPathName; // Removes the final slash if present @@ -489,17 +474,8 @@ public class WebDavStore extends Store { fullPathName = folderUrl.substring(folderSlash + 1); // Decodes the url-encoded folder name (i.e. "My%20folder" => "My Folder" - try { - folderName = java.net.URLDecoder.decode(fullPathName, "UTF-8"); - } catch (UnsupportedEncodingException uee) { - /** - * If we don't support UTF-8 there's a problem, don't decode - * it then - */ - folderName = fullPathName; - } - return folderName; + return UrlEncodingHelper.decodeUtf8(fullPathName); } return null; @@ -847,7 +823,7 @@ public class WebDavStore extends Store { request.setMethod("POST"); // Build the POST data. - ArrayList pairs = new ArrayList(); + List pairs = new ArrayList(); pairs.add(new BasicNameValuePair("destination", mUrl)); pairs.add(new BasicNameValuePair("username", mUsername)); pairs.add(new BasicNameValuePair("password", mPassword)); @@ -1059,7 +1035,7 @@ public class WebDavStore extends Store { } private InputStream sendRequest(String url, String method, StringEntity messageBody, - HashMap headers, boolean tryAuth) + Map headers, boolean tryAuth) throws MessagingException { InputStream istream = null; @@ -1137,12 +1113,12 @@ public class WebDavStore extends Store { * not all requests will need them. There are two signatures to support calls that don't require parsing of the * response. */ - private DataSet processRequest(String url, String method, String messageBody, HashMap headers) + private DataSet processRequest(String url, String method, String messageBody, Map headers) throws MessagingException { return processRequest(url, method, messageBody, headers, true); } - private DataSet processRequest(String url, String method, String messageBody, HashMap headers, + private DataSet processRequest(String url, String method, String messageBody, Map headers, boolean needsParsing) throws MessagingException { DataSet dataset = new DataSet(); @@ -1219,11 +1195,11 @@ public class WebDavStore extends Store { } @Override - public void sendMessages(Message[] messages) throws MessagingException { + public void sendMessages(List messages) throws MessagingException { WebDavFolder tmpFolder = (WebDavStore.WebDavFolder) getFolder(mAccount.getDraftsFolderName()); try { tmpFolder.open(Folder.OPEN_MODE_RW); - Message[] retMessages = tmpFolder.appendWebDavMessages(messages); + List retMessages = tmpFolder.appendWebDavMessages(messages); tmpFolder.moveMessages(retMessages, getSendSpoolFolder()); } finally { @@ -1258,21 +1234,16 @@ public class WebDavStore extends Store { this.mName = name; String encodedName = ""; - try { - String[] urlParts = name.split("/"); - String url = ""; - for (int i = 0, count = urlParts.length; i < count; i++) { - if (i != 0) { - url = url + "/" + java.net.URLEncoder.encode(urlParts[i], "UTF-8"); - } else { - url = java.net.URLEncoder.encode(urlParts[i], "UTF-8"); - } + String[] urlParts = name.split("/"); + String url = ""; + for (int i = 0, count = urlParts.length; i < count; i++) { + if (i != 0) { + url = url + "/" + UrlEncodingHelper.encodeUtf8(urlParts[i]); + } else { + url = UrlEncodingHelper.encodeUtf8(urlParts[i]); } - encodedName = url; - } catch (UnsupportedEncodingException uee) { - Log.e(K9.LOG_TAG, "UnsupportedEncodingException URLEncoding folder name, skipping encoded"); - encodedName = name; } + encodedName = url; encodedName = encodedName.replaceAll("\\+", "%20"); @@ -1297,38 +1268,38 @@ public class WebDavStore extends Store { } @Override - public Map copyMessages(Message[] messages, Folder folder) throws MessagingException { + public Map copyMessages(List messages, Folder folder) throws MessagingException { moveOrCopyMessages(messages, folder.getName(), false); return null; } @Override - public Map moveMessages(Message[] messages, Folder folder) throws MessagingException { + public Map moveMessages(List messages, Folder folder) throws MessagingException { moveOrCopyMessages(messages, folder.getName(), true); return null; } @Override - public void delete(Message[] msgs, String trashFolderName) throws MessagingException { + public void delete(List msgs, String trashFolderName) throws MessagingException { moveOrCopyMessages(msgs, trashFolderName, true); } - private void moveOrCopyMessages(Message[] messages, String folderName, boolean isMove) + private void moveOrCopyMessages(List messages, String folderName, boolean isMove) throws MessagingException { - String[] uids = new String[messages.length]; + String[] uids = new String[messages.size()]; - for (int i = 0, count = messages.length; i < count; i++) { - uids[i] = messages[i].getUid(); + for (int i = 0, count = messages.size(); i < count; i++) { + uids[i] = messages.get(i).getUid(); } String messageBody = ""; - HashMap headers = new HashMap(); - HashMap uidToUrl = getMessageUrls(uids); + Map headers = new HashMap(); + Map uidToUrl = getMessageUrls(uids); String[] urls = new String[uids.length]; for (int i = 0, count = uids.length; i < count; i++) { urls[i] = uidToUrl.get(uids[i]); - if (urls[i] == null && messages[i] instanceof WebDavMessage) { - WebDavMessage wdMessage = (WebDavMessage) messages[i]; + if (urls[i] == null && messages.get(i) instanceof WebDavMessage) { + WebDavMessage wdMessage = (WebDavMessage) messages.get(i); urls[i] = wdMessage.getUrl(); } } @@ -1339,7 +1310,7 @@ public class WebDavStore extends Store { headers.put("Brief", "t"); headers.put("If-Match", "*"); String action = (isMove ? "BMOVE" : "BCOPY"); - Log.i(K9.LOG_TAG, "Moving " + messages.length + " messages to " + destFolder.mFolderUrl); + Log.i(K9.LOG_TAG, "Moving " + messages.size() + " messages to " + destFolder.mFolderUrl); processRequest(mFolderUrl, action, messageBody, headers, false); } @@ -1347,7 +1318,7 @@ public class WebDavStore extends Store { private int getMessageCount(boolean read) throws MessagingException { String isRead; int messageCount = 0; - HashMap headers = new HashMap(); + Map headers = new HashMap(); String messageBody; if (read) { @@ -1431,11 +1402,11 @@ public class WebDavStore extends Store { } @Override - public Message[] getMessages(int start, int end, Date earliestDate, MessageRetrievalListener listener) + public List getMessages(int start, int end, Date earliestDate, MessageRetrievalListener listener) throws MessagingException { - ArrayList messages = new ArrayList(); + List messages = new ArrayList(); String[] uids; - HashMap headers = new HashMap(); + Map headers = new HashMap(); int uidsLength = -1; String messageBody; @@ -1461,7 +1432,7 @@ public class WebDavStore extends Store { DataSet dataset = processRequest(this.mFolderUrl, "SEARCH", messageBody, headers); uids = dataset.getUids(); - HashMap uidToUrl = dataset.getUidToUrl(); + Map uidToUrl = dataset.getUidToUrl(); uidsLength = uids.length; for (int i = 0; i < uidsLength; i++) { @@ -1477,22 +1448,22 @@ public class WebDavStore extends Store { } } - return messages.toArray(EMPTY_MESSAGE_ARRAY); + return messages; } @Override - public Message[] getMessages(MessageRetrievalListener listener) throws MessagingException { + public List getMessages(MessageRetrievalListener listener) throws MessagingException { return getMessages(null, listener); } @Override - public Message[] getMessages(String[] uids, MessageRetrievalListener listener) throws MessagingException { - ArrayList messageList = new ArrayList(); - Message[] messages; + public List getMessages(String[] uids, MessageRetrievalListener listener) throws MessagingException { + List messageList = new ArrayList(); + List messages; if (uids == null || uids.length == 0) { - return messageList.toArray(EMPTY_MESSAGE_ARRAY); + return messageList; } for (int i = 0, count = uids.length; i < count; i++) { @@ -1507,13 +1478,13 @@ public class WebDavStore extends Store { listener.messageFinished(message, i, count); } } - messages = messageList.toArray(EMPTY_MESSAGE_ARRAY); + messages = messageList; return messages; } - private HashMap getMessageUrls(String[] uids) throws MessagingException { - HashMap headers = new HashMap(); + private Map getMessageUrls(String[] uids) throws MessagingException { + Map headers = new HashMap(); String messageBody; /** Retrieve and parse the XML entity for our messages */ @@ -1521,16 +1492,16 @@ public class WebDavStore extends Store { headers.put("Brief", "t"); DataSet dataset = processRequest(this.mFolderUrl, "SEARCH", messageBody, headers); - HashMap uidToUrl = dataset.getUidToUrl(); + Map uidToUrl = dataset.getUidToUrl(); return uidToUrl; } @Override - public void fetch(Message[] messages, FetchProfile fp, MessageRetrievalListener listener) + public void fetch(List messages, FetchProfile fp, MessageRetrievalListener listener) throws MessagingException { if (messages == null || - messages.length == 0) { + messages.isEmpty()) { return; } @@ -1562,7 +1533,7 @@ public class WebDavStore extends Store { /** * Fetches the full messages or up to lines lines and passes them to the message parser. */ - private void fetchMessages(Message[] messages, MessageRetrievalListener listener, int lines) + private void fetchMessages(List messages, MessageRetrievalListener listener, int lines) throws MessagingException { WebDavHttpClient httpclient; httpclient = getHttpClient(); @@ -1570,15 +1541,15 @@ public class WebDavStore extends Store { /** * We can't hand off to processRequest() since we need the stream to parse. */ - for (int i = 0, count = messages.length; i < count; i++) { + for (int i = 0, count = messages.size(); i < count; i++) { WebDavMessage wdMessage; int statusCode = 0; - if (!(messages[i] instanceof WebDavMessage)) { + if (!(messages.get(i) instanceof WebDavMessage)) { throw new MessagingException("WebDavStore fetch called with non-WebDavMessage"); } - wdMessage = (WebDavMessage) messages[i]; + wdMessage = (WebDavMessage) messages.get(i); if (listener != null) { listener.messageStarted(wdMessage.getUid(), i, count); @@ -1676,36 +1647,36 @@ public class WebDavStore extends Store { * Fetches and sets the message flags for the supplied messages. The idea is to have this be recursive so that * we do a series of medium calls instead of one large massive call or a large number of smaller calls. */ - private void fetchFlags(Message[] startMessages, MessageRetrievalListener listener) throws MessagingException { + private void fetchFlags(List startMessages, MessageRetrievalListener listener) throws MessagingException { HashMap headers = new HashMap(); String messageBody = ""; - Message[] messages = new Message[20]; + List messages = new ArrayList(20); String[] uids; if (startMessages == null || - startMessages.length == 0) { + startMessages.isEmpty()) { return; } - if (startMessages.length > 20) { - Message[] newMessages = new Message[startMessages.length - 20]; - for (int i = 0, count = startMessages.length; i < count; i++) { + if (startMessages.size() > 20) { + List newMessages = new ArrayList(startMessages.size() - 20); + for (int i = 0, count = startMessages.size(); i < count; i++) { if (i < 20) { - messages[i] = startMessages[i]; + messages.set(i, startMessages.get(i)); } else { - newMessages[i - 20] = startMessages[i]; + newMessages.set(i - 20, startMessages.get(i)); } } fetchFlags(newMessages, listener); } else { - messages = startMessages; + messages.addAll(startMessages); } - uids = new String[messages.length]; + uids = new String[messages.size()]; - for (int i = 0, count = messages.length; i < count; i++) { - uids[i] = messages[i].getUid(); + for (int i = 0, count = messages.size(); i < count; i++) { + uids[i] = messages.get(i).getUid(); } messageBody = getMessageFlagsXml(uids); @@ -1716,13 +1687,13 @@ public class WebDavStore extends Store { throw new MessagingException("Data Set from request was null"); } - HashMap uidToReadStatus = dataset.getUidToRead(); + Map uidToReadStatus = dataset.getUidToRead(); - for (int i = 0, count = messages.length; i < count; i++) { - if (!(messages[i] instanceof WebDavMessage)) { + for (int i = 0, count = messages.size(); i < count; i++) { + if (!(messages.get(i) instanceof WebDavMessage)) { throw new MessagingException("WebDavStore fetch called with non-WebDavMessage"); } - WebDavMessage wdMessage = (WebDavMessage) messages[i]; + WebDavMessage wdMessage = (WebDavMessage) messages.get(i); if (listener != null) { listener.messageStarted(wdMessage.getUid(), i, count); @@ -1745,37 +1716,37 @@ public class WebDavStore extends Store { * that we do a series of medium calls instead of one large massive call or a large number of smaller calls. * Call it a happy balance */ - private void fetchEnvelope(Message[] startMessages, MessageRetrievalListener listener) + private void fetchEnvelope(List startMessages, MessageRetrievalListener listener) throws MessagingException { - HashMap headers = new HashMap(); + Map headers = new HashMap(); String messageBody = ""; String[] uids; - Message[] messages = new Message[10]; + List messages = new ArrayList(10); if (startMessages == null || - startMessages.length == 0) { + startMessages.isEmpty()) { return; } - if (startMessages.length > 10) { - Message[] newMessages = new Message[startMessages.length - 10]; - for (int i = 0, count = startMessages.length; i < count; i++) { + if (startMessages.size() > 10) { + List newMessages = new ArrayList(startMessages.size() - 10); + for (int i = 0, count = startMessages.size(); i < count; i++) { if (i < 10) { - messages[i] = startMessages[i]; + messages.set(i, startMessages.get(i)); } else { - newMessages[i - 10] = startMessages[i]; + newMessages.set(i - 10,startMessages.get(i)); } } fetchEnvelope(newMessages, listener); } else { - messages = startMessages; + messages.addAll(startMessages); } - uids = new String[messages.length]; + uids = new String[messages.size()]; - for (int i = 0, count = messages.length; i < count; i++) { - uids[i] = messages[i].getUid(); + for (int i = 0, count = messages.size(); i < count; i++) { + uids[i] = messages.get(i).getUid(); } messageBody = getMessageEnvelopeXml(uids); @@ -1784,15 +1755,15 @@ public class WebDavStore extends Store { Map envelopes = dataset.getMessageEnvelopes(); - int count = messages.length; - for (int i = messages.length - 1; i >= 0; i--) { - if (!(messages[i] instanceof WebDavMessage)) { + int count = messages.size(); + for (int i = messages.size() - 1; i >= 0; i--) { + if (!(messages.get(i) instanceof WebDavMessage)) { throw new MessagingException("WebDavStore fetch called with non-WebDavMessage"); } - WebDavMessage wdMessage = (WebDavMessage) messages[i]; + WebDavMessage wdMessage = (WebDavMessage) messages.get(i); if (listener != null) { - listener.messageStarted(messages[i].getUid(), i, count); + listener.messageStarted(messages.get(i).getUid(), i, count); } ParsedMessageEnvelope envelope = envelopes.get(wdMessage.getUid()); @@ -1804,18 +1775,18 @@ public class WebDavStore extends Store { } if (listener != null) { - listener.messageFinished(messages[i], i, count); + listener.messageFinished(messages.get(i), i, count); } } } @Override - public void setFlags(Message[] messages, Flag[] flags, boolean value) + public void setFlags(List messages, final Set flags, boolean value) throws MessagingException { - String[] uids = new String[messages.length]; + String[] uids = new String[messages.size()]; - for (int i = 0, count = messages.length; i < count; i++) { - uids[i] = messages[i].getUid(); + for (int i = 0, count = messages.size(); i < count; i++) { + uids[i] = messages.get(i).getUid(); } for (Flag flag : flags) { @@ -1829,8 +1800,8 @@ public class WebDavStore extends Store { private void markServerMessagesRead(String[] uids, boolean read) throws MessagingException { String messageBody = ""; - HashMap headers = new HashMap(); - HashMap uidToUrl = getMessageUrls(uids); + Map headers = new HashMap(); + Map uidToUrl = getMessageUrls(uids); String[] urls = new String[uids.length]; for (int i = 0, count = uids.length; i < count; i++) { @@ -1845,10 +1816,10 @@ public class WebDavStore extends Store { } private void deleteServerMessages(String[] uids) throws MessagingException { - HashMap uidToUrl = getMessageUrls(uids); + Map uidToUrl = getMessageUrls(uids); for (String uid : uids) { - HashMap headers = new HashMap(); + Map headers = new HashMap(); String url = uidToUrl.get(uid); String destinationUrl = generateDeleteUrl(url); @@ -1875,13 +1846,13 @@ public class WebDavStore extends Store { } @Override - public Map appendMessages(Message[] messages) throws MessagingException { + public Map appendMessages(List messages) throws MessagingException { appendWebDavMessages(messages); return null; } - public Message[] appendWebDavMessages(Message[] messages) throws MessagingException { - Message[] retMessages = new Message[messages.length]; + public List appendWebDavMessages(List messages) throws MessagingException { + List retMessages = new ArrayList(messages.size()); int ind = 0; WebDavHttpClient httpclient = getHttpClient(); @@ -1910,7 +1881,7 @@ public class WebDavStore extends Store { if (!messageURL.endsWith("/")) { messageURL += "/"; } - messageURL += URLEncoder.encode(message.getUid() + ":" + System.currentTimeMillis() + ".eml", "UTF-8"); + messageURL += UrlEncodingHelper.encodeUtf8(message.getUid() + ":" + System.currentTimeMillis() + ".eml"); Log.i(K9.LOG_TAG, "Uploading message as " + messageURL); @@ -1936,7 +1907,7 @@ public class WebDavStore extends Store { WebDavMessage retMessage = new WebDavMessage(message.getUid(), this); retMessage.setUrl(messageURL); - retMessages[ind++] = retMessage; + retMessages.set(ind++, retMessage); } catch (Exception e) { throw new MessagingException("Unable to append", e); } @@ -1958,9 +1929,9 @@ public class WebDavStore extends Store { } @Override - public void setFlags(Flag[] flags, boolean value) throws MessagingException { + public void setFlags(final Set flags, boolean value) throws MessagingException { Log.e(K9.LOG_TAG, - "Unimplemented method setFlags(Flag[], boolean) breaks markAllMessagesAsRead and EmptyTrash"); + "Unimplemented method setFlags(Set, boolean) breaks markAllMessagesAsRead and EmptyTrash"); // Try to make this efficient by not retrieving all of the messages } } @@ -1997,12 +1968,9 @@ public class WebDavStore extends Store { * We have to decode, then encode the URL because Exchange likes to not properly encode all characters */ try { - end = java.net.URLDecoder.decode(end, "UTF-8"); - end = java.net.URLEncoder.encode(end, "UTF-8"); + end = UrlEncodingHelper.decodeUtf8(end); + end = UrlEncodingHelper.encodeUtf8(end); end = end.replaceAll("\\+", "%20"); - } catch (UnsupportedEncodingException uee) { - Log.e(K9.LOG_TAG, "UnsupportedEncodingException caught in setUrl: " + uee + "\nTrace: " - + processException(uee)); } catch (IllegalArgumentException iae) { Log.e(K9.LOG_TAG, "IllegalArgumentException caught in setUrl: " + iae + "\nTrace: " + processException(iae)); @@ -2029,18 +1997,13 @@ public class WebDavStore extends Store { this.mSize = size; } - @Override - public void parse(InputStream in) throws IOException, MessagingException { - super.parse(in); - } - public void setFlagInternal(Flag flag, boolean set) throws MessagingException { super.setFlag(flag, set); } public void setNewHeaders(ParsedMessageEnvelope envelope) throws MessagingException { String[] headers = envelope.getHeaderList(); - HashMap messageHeaders = envelope.getMessageHeaders(); + Map messageHeaders = envelope.getMessageHeaders(); for (String header : headers) { String headerValue = messageHeaders.get(header); @@ -2060,13 +2023,13 @@ public class WebDavStore extends Store { public void delete(String trashFolderName) throws MessagingException { WebDavFolder wdFolder = (WebDavFolder) getFolder(); Log.i(K9.LOG_TAG, "Deleting message by moving to " + trashFolderName); - wdFolder.moveMessages(new Message[] { this }, wdFolder.getStore().getFolder(trashFolderName)); + wdFolder.moveMessages(Collections.singletonList(this), wdFolder.getStore().getFolder(trashFolderName)); } @Override public void setFlag(Flag flag, boolean set) throws MessagingException { super.setFlag(flag, set); - mFolder.setFlags(new Message[] { this }, new Flag[] { flag }, set); + mFolder.setFlags(Collections.singletonList(this), Collections.singleton(flag), set); } } @@ -2142,8 +2105,8 @@ public class WebDavStore extends Store { private boolean mReadStatus = false; private String mUid = ""; - private HashMap mMessageHeaders = new HashMap(); - private ArrayList mHeaders = new ArrayList(); + private Map mMessageHeaders = new HashMap(); + private List mHeaders = new ArrayList(); public void addHeader(String field, String value) { String headerName = HEADER_MAPPINGS.get(field); @@ -2154,7 +2117,7 @@ public class WebDavStore extends Store { } } - public HashMap getMessageHeaders() { + public Map getMessageHeaders() { return this.mMessageHeaders; } @@ -2186,9 +2149,9 @@ public class WebDavStore extends Store { * depending on the accessor calls made. */ public class DataSet { - private HashMap> mData = new HashMap>(); + private Map> mData = new HashMap>(); private StringBuilder mUid = new StringBuilder(); - private HashMap mTempData = new HashMap(); + private Map mTempData = new HashMap(); public void addValue(String value, String tagName) { if (tagName.equals("uid")) { @@ -2220,9 +2183,9 @@ public class WebDavStore extends Store { /** * Returns a hashmap of special folder name => special folder url */ - public HashMap getSpecialFolderToUrl() { + public Map getSpecialFolderToUrl() { // We return the first (and only) map - for (HashMap folderMap : mData.values()) { + for (Map folderMap : mData.values()) { return folderMap; } return new HashMap(); @@ -2231,11 +2194,11 @@ public class WebDavStore extends Store { /** * Returns a hashmap of Message UID => Message Url */ - public HashMap getUidToUrl() { - HashMap uidToUrl = new HashMap(); + public Map getUidToUrl() { + Map uidToUrl = new HashMap(); for (String uid : mData.keySet()) { - HashMap data = mData.get(uid); + Map data = mData.get(uid); String value = data.get("href"); if (value != null && !value.equals("")) { @@ -2249,11 +2212,11 @@ public class WebDavStore extends Store { /** * Returns a hashmap of Message UID => Read Status */ - public HashMap getUidToRead() { - HashMap uidToRead = new HashMap(); + public Map getUidToRead() { + Map uidToRead = new HashMap(); for (String uid : mData.keySet()) { - HashMap data = mData.get(uid); + Map data = mData.get(uid); String readStatus = data.get("read"); if (readStatus != null && !readStatus.equals("")) { Boolean value = !readStatus.equals("0"); @@ -2273,10 +2236,10 @@ public class WebDavStore extends Store { * Returns an array of all hrefs (urls) that were received */ public String[] getHrefs() { - ArrayList hrefs = new ArrayList(); + List hrefs = new ArrayList(); for (String uid : mData.keySet()) { - HashMap data = mData.get(uid); + Map data = mData.get(uid); String href = data.get("href"); hrefs.add(href); } @@ -2288,7 +2251,7 @@ public class WebDavStore extends Store { * Return an array of all Message UIDs that were received */ public String[] getUids() { - ArrayList uids = new ArrayList(); + List uids = new ArrayList(); for (String uid : mData.keySet()) { uids.add(uid); @@ -2309,7 +2272,7 @@ public class WebDavStore extends Store { int messageCount = 0; for (String uid : mData.keySet()) { - HashMap data = mData.get(uid); + Map data = mData.get(uid); String count = data.get("visiblecount"); if (count != null && @@ -2323,14 +2286,14 @@ public class WebDavStore extends Store { } /** - * Returns a HashMap of message UID => ParsedMessageEnvelope + * Returns a Map of message UID => ParsedMessageEnvelope */ - public HashMap getMessageEnvelopes() { - HashMap envelopes = new HashMap(); + public Map getMessageEnvelopes() { + Map envelopes = new HashMap(); for (String uid : mData.keySet()) { ParsedMessageEnvelope envelope = new ParsedMessageEnvelope(); - HashMap data = mData.get(uid); + Map data = mData.get(uid); if (data != null) { for (Map.Entry entry : data.entrySet()) { @@ -2410,13 +2373,10 @@ public class WebDavStore extends Store { */ try { if (length > 3) { - end = java.net.URLDecoder.decode(end, "UTF-8"); - end = java.net.URLEncoder.encode(end, "UTF-8"); + end = UrlEncodingHelper.decodeUtf8(end); + end = UrlEncodingHelper.encodeUtf8(end); end = end.replaceAll("\\+", "%20"); } - } catch (UnsupportedEncodingException uee) { - Log.e(K9.LOG_TAG, "UnsupportedEncodingException caught in HttpGeneric(String uri): " + uee - + "\nTrace: " + processException(uee)); } catch (IllegalArgumentException iae) { Log.e(K9.LOG_TAG, "IllegalArgumentException caught in HttpGeneric(String uri): " + iae + "\nTrace: " + processException(iae)); diff --git a/src/com/fsck/k9/mail/store/imap/ImapUtility.java b/src/com/fsck/k9/mail/store/imap/ImapUtility.java index b7312eae7..51d1aa070 100644 --- a/src/com/fsck/k9/mail/store/imap/ImapUtility.java +++ b/src/com/fsck/k9/mail/store/imap/ImapUtility.java @@ -48,7 +48,7 @@ public class ImapUtility { * list is returned. */ public static List getImapSequenceValues(String set) { - ArrayList list = new ArrayList(); + List list = new ArrayList(); if (set != null) { String[] setItems = set.split(","); for (String item : setItems) { @@ -83,7 +83,7 @@ public class ImapUtility { * is returned. */ public static List getImapRangeValues(String range) { - ArrayList list = new ArrayList(); + List list = new ArrayList(); try { if (range != null) { int colonPos = range.indexOf(':'); 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..27eeb243e --- /dev/null +++ b/src/com/fsck/k9/mail/store/local/BinaryAttachmentBody.java @@ -0,0 +1,58 @@ +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; + +/** + * Superclass for attachments that contain binary data. + * The source for the data differs for the subclasses. + */ +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..faea07a6c --- /dev/null +++ b/src/com/fsck/k9/mail/store/local/LocalAttachmentBody.java @@ -0,0 +1,40 @@ +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; + +/** + * An attachment whose contents are loaded from an URI. + */ +public class LocalAttachmentBody extends BinaryAttachmentBody { + private Application mApplication; + private Uri mUri; + + public LocalAttachmentBody(Uri uri, Application application) { + mApplication = application; + mUri = uri; + } + + @Override + public InputStream getInputStream() throws MessagingException { + try { + return mApplication.getContentResolver().openInputStream(mUri); + } catch (FileNotFoundException fnfe) { + /* + * Since it's completely normal for us to try to serve up attachments that + * have been blown away, we just return an empty stream. + */ + return new ByteArrayInputStream(LocalStore.EMPTY_BYTE_ARRAY); + } + } + + public Uri getContentUri() { + return mUri; + } +} \ No newline at end of file diff --git a/src/com/fsck/k9/mail/store/local/LocalAttachmentBodyPart.java b/src/com/fsck/k9/mail/store/local/LocalAttachmentBodyPart.java new file mode 100644 index 000000000..9fcdf71b3 --- /dev/null +++ b/src/com/fsck/k9/mail/store/local/LocalAttachmentBodyPart.java @@ -0,0 +1,31 @@ +package com.fsck.k9.mail.store.local; + +import com.fsck.k9.mail.Body; +import com.fsck.k9.mail.MessagingException; +import com.fsck.k9.mail.internet.MimeBodyPart; + +public class LocalAttachmentBodyPart extends MimeBodyPart { + private long mAttachmentId = -1; + + public LocalAttachmentBodyPart(Body body, long attachmentId) throws MessagingException { + super(body); + mAttachmentId = attachmentId; + } + + /** + * Returns the local attachment id of this body, or -1 if it is not stored. + * @return + */ + public long getAttachmentId() { + return mAttachmentId; + } + + public void setAttachmentId(long attachmentId) { + mAttachmentId = attachmentId; + } + + @Override + public String toString() { + return "" + mAttachmentId; + } +} \ No newline at end of file diff --git a/src/com/fsck/k9/mail/store/local/LocalAttachmentMessageBody.java b/src/com/fsck/k9/mail/store/local/LocalAttachmentMessageBody.java new file mode 100644 index 000000000..5c451cf15 --- /dev/null +++ b/src/com/fsck/k9/mail/store/local/LocalAttachmentMessageBody.java @@ -0,0 +1,49 @@ +package com.fsck.k9.mail.store.local; + +import java.io.IOException; +import java.io.OutputStream; + +import org.apache.james.mime4j.util.MimeUtil; + +import android.app.Application; +import android.net.Uri; + +import com.fsck.k9.mail.CompositeBody; +import com.fsck.k9.mail.MessagingException; + +/** + * A {@link LocalAttachmentBody} extension containing a message/rfc822 type body + * + */ +public class LocalAttachmentMessageBody extends LocalAttachmentBody implements CompositeBody { + + public LocalAttachmentMessageBody(Uri uri, Application application) { + super(uri, application); + } + + @Override + public void writeTo(OutputStream out) throws IOException, MessagingException { + AttachmentMessageBodyUtil.writeTo(this, out); + } + + @Override + public void setUsing7bitTransport() throws MessagingException { + /* + * There's nothing to recurse into here, so there's nothing to do. + * The enclosing BodyPart already called setEncoding(MimeUtil.ENC_7BIT). Once + * writeTo() is called, the file with the rfc822 body will be opened + * for reading and will then be recursed. + */ + + } + + @Override + public void setEncoding(String encoding) throws MessagingException { + if (!MimeUtil.ENC_7BIT.equalsIgnoreCase(encoding) + && !MimeUtil.ENC_8BIT.equalsIgnoreCase(encoding)) { + throw new MessagingException( + "Incompatible content-transfer-encoding applied to a CompositeBody"); + } + mEncoding = encoding; + } +} \ No newline at end of file diff --git a/src/com/fsck/k9/mail/store/local/LocalFolder.java b/src/com/fsck/k9/mail/store/local/LocalFolder.java new file mode 100644 index 000000000..bc5895f04 --- /dev/null +++ b/src/com/fsck/k9/mail/store/local/LocalFolder.java @@ -0,0 +1,2199 @@ +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.Collection; +import java.util.Collections; +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); + List messages = getMessages(null, false); + for (int i = mVisibleLimit; i < messages.size(); i++) { + if (listener != null) { + listener.messageRemoved(messages.get(i)); + } + messages.get(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 List 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 List 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 List getMessages(MessageRetrievalListener listener) throws MessagingException { + return getMessages(listener, true); + } + + @Override + public List getMessages(final MessageRetrievalListener listener, final boolean includeDeleted) throws MessagingException { + try { + return this.localStore.database.execute(false, new DbCallback>() { + @Override + public List 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 List getMessages(String[] uids, MessageRetrievalListener listener) + throws MessagingException { + open(OPEN_MODE_RW); + if (uids == null) { + return getMessages(listener); + } + List messages = new ArrayList(); + for (String uid : uids) { + Message message = getMessage(uid); + if (message != null) { + messages.add(message); + } + } + return messages; + } + + @Override + public Map copyMessages(List 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 List 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(Collections.singletonList(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(List messages) throws MessagingException { + return appendMessages(messages, false); + } + + public void destroyMessages(final List 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 List 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(message.getFlags()); + appendedFlags.add(Flag.X_GOT_ALL_HEADERS); + + db.execSQL("UPDATE messages " + "SET flags = ? " + " WHERE id = ?", + new Object[] + { LocalFolder.this.localStore.serializeFlags(appendedFlags), 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 List messages, final Set 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(final Set 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); + + List 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); + List 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..474a4c849 --- /dev/null +++ b/src/com/fsck/k9/mail/store/local/LocalMessage.java @@ -0,0 +1,560 @@ +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.List; +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; + + private 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 { + List messages = new ArrayList(); + messages.add(this); + mHeadersLoaded = true; // set true before calling populate headers to stop recursion + ((LocalFolder) mFolder).populateHeaders(messages); + + } + + @Override + public void addHeader(String name, String value) throws UnavailableStorageException { + if (!mHeadersLoaded) + loadHeaders(); + super.addHeader(name, value); + } + + @Override + public void setHeader(String name, String value) throws UnavailableStorageException { + if (!mHeadersLoaded) + loadHeaders(); + super.setHeader(name, value); + } + + @Override + public String[] getHeader(String name) throws UnavailableStorageException { + if (!mHeadersLoaded) + loadHeaders(); + return super.getHeader(name); + } + + @Override + public void removeHeader(String name) throws UnavailableStorageException { + if (!mHeadersLoaded) + loadHeaders(); + super.removeHeader(name); + } + + @Override + public Set getHeaderNames() throws UnavailableStorageException { + if (!mHeadersLoaded) + loadHeaders(); + return super.getHeaderNames(); + } + + @Override + public LocalMessage clone() { + LocalMessage message = new LocalMessage(this.localStore); + super.copy(message); + + message.mId = mId; + message.mAttachmentCount = mAttachmentCount; + message.mSubject = mSubject; + message.mPreview = mPreview; + message.mHeadersLoaded = mHeadersLoaded; + message.mMessageDirty = mMessageDirty; + + return message; + } + + public long getThreadId() { + return mThreadId; + } + + public long getRootId() { + return mRootId; + } +} \ No newline at end of file diff --git a/src/com/fsck/k9/mail/store/local/LocalStore.java b/src/com/fsck/k9/mail/store/local/LocalStore.java new file mode 100644 index 000000000..e36f453b6 --- /dev/null +++ b/src/com/fsck/k9/mail/store/local/LocalStore.java @@ -0,0 +1,1032 @@ + +package com.fsck.k9.mail.store.local; + +import java.io.File; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Locale; +import java.util.Map; + + +import android.app.Application; +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.content.SharedPreferences; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.net.Uri; +import android.util.Log; + +import com.fsck.k9.Account; +import com.fsck.k9.K9; +import com.fsck.k9.Preferences; +import com.fsck.k9.controller.MessageRetrievalListener; +import com.fsck.k9.helper.StringUtils; +import com.fsck.k9.helper.UrlEncodingHelper; +import com.fsck.k9.helper.Utility; +import com.fsck.k9.mail.Flag; +import com.fsck.k9.mail.Folder; +import com.fsck.k9.mail.Message; +import com.fsck.k9.mail.MessagingException; +import com.fsck.k9.mail.Store; +import com.fsck.k9.mail.store.LockableDatabase; +import com.fsck.k9.mail.store.StorageManager; +import com.fsck.k9.mail.store.UnavailableStorageException; +import com.fsck.k9.mail.store.LockableDatabase.DbCallback; +import com.fsck.k9.mail.store.LockableDatabase.WrappedException; +import com.fsck.k9.mail.store.StorageManager.StorageProvider; +import com.fsck.k9.provider.EmailProvider; +import com.fsck.k9.provider.EmailProvider.MessageColumns; +import com.fsck.k9.search.LocalSearch; +import com.fsck.k9.search.SearchSpecification.Attribute; +import com.fsck.k9.search.SearchSpecification.Searchfield; +import com.fsck.k9.search.SqlQueryBuilder; + +/** + *
    + * Implements a SQLite database backed local store for Messages.
    + * 
    + */ +public class LocalStore extends Store implements Serializable { + + private static final long serialVersionUID = -5142141896809423072L; + + static final String[] EMPTY_STRING_ARRAY = new String[0]; + static final byte[] EMPTY_BYTE_ARRAY = new byte[0]; + + /* + * a String containing the columns getMessages expects to work with + * in the correct order. + */ + static String GET_MESSAGES_COLS = + "subject, sender_list, date, uid, flags, messages.id, to_list, cc_list, " + + "bcc_list, reply_to_list, attachment_count, internal_date, messages.message_id, " + + "folder_id, preview, threads.id, threads.root, deleted, read, flagged, answered, " + + "forwarded "; + + static final String GET_FOLDER_COLS = + "folders.id, name, visible_limit, last_updated, status, push_state, last_pushed, " + + "integrate, top_group, poll_class, push_class, display_class, notify_class"; + + static final int FOLDER_ID_INDEX = 0; + static final int FOLDER_NAME_INDEX = 1; + static final int FOLDER_VISIBLE_LIMIT_INDEX = 2; + static final int FOLDER_LAST_CHECKED_INDEX = 3; + static final int FOLDER_STATUS_INDEX = 4; + static final int FOLDER_PUSH_STATE_INDEX = 5; + static final int FOLDER_LAST_PUSHED_INDEX = 6; + static final int FOLDER_INTEGRATE_INDEX = 7; + static final int FOLDER_TOP_GROUP_INDEX = 8; + static final int FOLDER_SYNC_CLASS_INDEX = 9; + static final int FOLDER_PUSH_CLASS_INDEX = 10; + static final int FOLDER_DISPLAY_CLASS_INDEX = 11; + static final int FOLDER_NOTIFY_CLASS_INDEX = 12; + + static final String[] UID_CHECK_PROJECTION = { "uid" }; + + /** + * Maximum number of UIDs to check for existence at once. + * + * @see LocalFolder#extractNewMessages(List) + */ + static final int UID_CHECK_BATCH_SIZE = 500; + + /** + * Maximum number of messages to perform flag updates on at once. + * + * @see #setFlag(List, Flag, boolean) + */ + private static final int FLAG_UPDATE_BATCH_SIZE = 500; + + /** + * Maximum number of threads to perform flag updates on at once. + * + * @see #setFlagForThreads(List, Flag, boolean) + */ + private static final int THREAD_FLAG_UPDATE_BATCH_SIZE = 500; + + public static final int DB_VERSION = 50; + + + public static String getColumnNameForFlag(Flag flag) { + switch (flag) { + case SEEN: { + return MessageColumns.READ; + } + case FLAGGED: { + return MessageColumns.FLAGGED; + } + case ANSWERED: { + return MessageColumns.ANSWERED; + } + case FORWARDED: { + return MessageColumns.FORWARDED; + } + default: { + throw new IllegalArgumentException("Flag must be a special column flag"); + } + } + } + + + protected String uUid = null; + + final Application mApplication; + + LockableDatabase database; + + private ContentResolver mContentResolver; + + /** + * local://localhost/path/to/database/uuid.db + * This constructor is only used by {@link Store#getLocalInstance(Account, Application)} + * @param account + * @param application + * @throws UnavailableStorageException if not {@link StorageProvider#isReady(Context)} + */ + public LocalStore(final Account account, final Application application) throws MessagingException { + super(account); + database = new LockableDatabase(application, account.getUuid(), new StoreSchemaDefinition(this)); + + mApplication = application; + mContentResolver = application.getContentResolver(); + database.setStorageProviderId(account.getLocalStorageProviderId()); + uUid = account.getUuid(); + + database.open(); + } + + public void switchLocalStorage(final String newStorageProviderId) throws MessagingException { + database.switchProvider(newStorageProviderId); + } + + protected SharedPreferences getPreferences() { + return Preferences.getPreferences(mApplication).getPreferences(); + } + + public long getSize() throws UnavailableStorageException { + + final StorageManager storageManager = StorageManager.getInstance(mApplication); + + final File attachmentDirectory = storageManager.getAttachmentDirectory(uUid, + database.getStorageProviderId()); + + return database.execute(false, new DbCallback() { + @Override + public Long doDbWork(final SQLiteDatabase db) { + final File[] files = attachmentDirectory.listFiles(); + long attachmentLength = 0; + if (files != null) { + for (File file : files) { + if (file.exists()) { + attachmentLength += file.length(); + } + } + } + + final File dbFile = storageManager.getDatabase(uUid, database.getStorageProviderId()); + return dbFile.length() + attachmentLength; + } + }); + } + + public void compact() throws MessagingException { + if (K9.DEBUG) + Log.i(K9.LOG_TAG, "Before compaction size = " + getSize()); + + database.execute(false, new DbCallback() { + @Override + public Void doDbWork(final SQLiteDatabase db) throws WrappedException { + db.execSQL("VACUUM"); + return null; + } + }); + if (K9.DEBUG) + Log.i(K9.LOG_TAG, "After compaction size = " + getSize()); + } + + + public void clear() throws MessagingException { + if (K9.DEBUG) + Log.i(K9.LOG_TAG, "Before prune size = " + getSize()); + + pruneCachedAttachments(true); + if (K9.DEBUG) { + Log.i(K9.LOG_TAG, "After prune / before compaction size = " + getSize()); + + Log.i(K9.LOG_TAG, "Before clear folder count = " + getFolderCount()); + Log.i(K9.LOG_TAG, "Before clear message count = " + getMessageCount()); + + Log.i(K9.LOG_TAG, "After prune / before clear size = " + getSize()); + } + // don't delete messages that are Local, since there is no copy on the server. + // Don't delete deleted messages. They are essentially placeholders for UIDs of messages that have + // been deleted locally. They take up insignificant space + database.execute(false, new DbCallback() { + @Override + public Void doDbWork(final SQLiteDatabase db) { + // Delete entries from 'threads' table + db.execSQL("DELETE FROM threads WHERE message_id IN " + + "(SELECT id FROM messages WHERE deleted = 0 AND uid NOT LIKE 'Local%')"); + + // Set 'root' and 'parent' of remaining entries in 'thread' table to 'NULL' to make + // sure the thread structure is in a valid state (this may destroy existing valid + // thread trees, but is much faster than adjusting the tree by removing messages + // one by one). + db.execSQL("UPDATE threads SET root=id, parent=NULL"); + + // Delete entries from 'messages' table + db.execSQL("DELETE FROM messages WHERE deleted = 0 AND uid NOT LIKE 'Local%'"); + return null; + } + }); + + compact(); + + if (K9.DEBUG) { + Log.i(K9.LOG_TAG, "After clear message count = " + getMessageCount()); + + Log.i(K9.LOG_TAG, "After clear size = " + getSize()); + } + } + + public int getMessageCount() throws MessagingException { + return database.execute(false, new DbCallback() { + @Override + public Integer doDbWork(final SQLiteDatabase db) { + Cursor cursor = null; + try { + cursor = db.rawQuery("SELECT COUNT(*) FROM messages", null); + cursor.moveToFirst(); + return cursor.getInt(0); // message count + } finally { + Utility.closeQuietly(cursor); + } + } + }); + } + + public int getFolderCount() throws MessagingException { + return database.execute(false, new DbCallback() { + @Override + public Integer doDbWork(final SQLiteDatabase db) { + Cursor cursor = null; + try { + cursor = db.rawQuery("SELECT COUNT(*) FROM folders", null); + cursor.moveToFirst(); + return cursor.getInt(0); // folder count + } finally { + Utility.closeQuietly(cursor); + } + } + }); + } + + @Override + public LocalFolder getFolder(String name) { + return new LocalFolder(this, name); + } + + public LocalFolder getFolderById(long folderId) { + return new LocalFolder(this, folderId); + } + + // TODO this takes about 260-300ms, seems slow. + @Override + public List getPersonalNamespaces(boolean forceListAll) throws MessagingException { + final List folders = new LinkedList(); + try { + database.execute(false, new DbCallback < List > () { + @Override + public List doDbWork(final SQLiteDatabase db) throws WrappedException { + Cursor cursor = null; + + try { + cursor = db.rawQuery("SELECT " + GET_FOLDER_COLS + " FROM folders " + + "ORDER BY name ASC", null); + while (cursor.moveToNext()) { + if (cursor.isNull(FOLDER_ID_INDEX)) { + continue; + } + String folderName = cursor.getString(FOLDER_NAME_INDEX); + LocalFolder folder = new LocalFolder(LocalStore.this, folderName); + folder.open(cursor); + + folders.add(folder); + } + return folders; + } catch (MessagingException e) { + throw new WrappedException(e); + } finally { + Utility.closeQuietly(cursor); + } + } + }); + } catch (WrappedException e) { + throw(MessagingException) e.getCause(); + } + return folders; + } + + @Override + public void checkSettings() throws MessagingException { + } + + public void delete() throws UnavailableStorageException { + database.delete(); + } + + public void recreate() throws UnavailableStorageException { + database.recreate(); + } + + /** + * Deletes all cached attachments for the entire store. + * @param force + * @throws com.fsck.k9.mail.MessagingException + */ + //TODO this method seems to be only called with force=true, simplify accordingly + private void pruneCachedAttachments(final boolean force) throws MessagingException { + database.execute(false, new DbCallback() { + @Override + public Void doDbWork(final SQLiteDatabase db) throws WrappedException { + if (force) { + ContentValues cv = new ContentValues(); + cv.putNull("content_uri"); + db.update("attachments", cv, null, null); + } + final StorageManager storageManager = StorageManager.getInstance(mApplication); + File[] files = storageManager.getAttachmentDirectory(uUid, database.getStorageProviderId()).listFiles(); + for (File file : files) { + if (file.exists()) { + if (!force) { + Cursor cursor = null; + try { + cursor = db.query( + "attachments", + new String[] { "store_data" }, + "id = ?", + new String[] { file.getName() }, + null, + null, + null); + if (cursor.moveToNext()) { + if (cursor.getString(0) == null) { + if (K9.DEBUG) + Log.d(K9.LOG_TAG, "Attachment " + file.getAbsolutePath() + " has no store data, not deleting"); + /* + * If the attachment has no store data it is not recoverable, so + * we won't delete it. + */ + continue; + } + } + } finally { + Utility.closeQuietly(cursor); + } + } + if (!force) { + try { + ContentValues cv = new ContentValues(); + cv.putNull("content_uri"); + db.update("attachments", cv, "id = ?", new String[] { file.getName() }); + } catch (Exception e) { + /* + * If the row has gone away before we got to mark it not-downloaded that's + * okay. + */ + } + } + if (!file.delete()) { + file.deleteOnExit(); + } + } + } + return null; + } + }); + } + + public void resetVisibleLimits(int visibleLimit) throws UnavailableStorageException { + final ContentValues cv = new ContentValues(); + cv.put("visible_limit", Integer.toString(visibleLimit)); + database.execute(false, new DbCallback() { + @Override + public Void doDbWork(final SQLiteDatabase db) throws WrappedException { + db.update("folders", cv, null, null); + return null; + } + }); + } + + public List getPendingCommands() throws UnavailableStorageException { + return database.execute(false, new DbCallback>() { + @Override + public List doDbWork(final SQLiteDatabase db) throws WrappedException { + Cursor cursor = null; + try { + cursor = db.query("pending_commands", + new String[] { "id", "command", "arguments" }, + null, + null, + null, + null, + "id ASC"); + List commands = new ArrayList(); + while (cursor.moveToNext()) { + PendingCommand command = new PendingCommand(); + command.mId = cursor.getLong(0); + command.command = cursor.getString(1); + String arguments = cursor.getString(2); + command.arguments = arguments.split(","); + for (int i = 0; i < command.arguments.length; i++) { + command.arguments[i] = Utility.fastUrlDecode(command.arguments[i]); + } + commands.add(command); + } + return commands; + } finally { + Utility.closeQuietly(cursor); + } + } + }); + } + + public void addPendingCommand(PendingCommand command) throws UnavailableStorageException { + for (int i = 0; i < command.arguments.length; i++) { + command.arguments[i] = UrlEncodingHelper.encodeUtf8(command.arguments[i]); + } + final ContentValues cv = new ContentValues(); + cv.put("command", command.command); + cv.put("arguments", Utility.combine(command.arguments, ',')); + database.execute(false, new DbCallback() { + @Override + public Void doDbWork(final SQLiteDatabase db) throws WrappedException { + db.insert("pending_commands", "command", cv); + return null; + } + }); + } + + public void removePendingCommand(final PendingCommand command) throws UnavailableStorageException { + database.execute(false, new DbCallback() { + @Override + public Void doDbWork(final SQLiteDatabase db) throws WrappedException { + db.delete("pending_commands", "id = ?", new String[] { Long.toString(command.mId) }); + return null; + } + }); + } + + public void removePendingCommands() throws UnavailableStorageException { + database.execute(false, new DbCallback() { + @Override + public Void doDbWork(final SQLiteDatabase db) throws WrappedException { + db.delete("pending_commands", null, null); + return null; + } + }); + } + + public static class PendingCommand { + private long mId; + public String command; + public String[] arguments; + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(command); + sb.append(": "); + for (String argument : arguments) { + sb.append(", "); + sb.append(argument); + //sb.append("\n"); + } + return sb.toString(); + } + } + + @Override + public boolean isMoveCapable() { + return true; + } + + @Override + public boolean isCopyCapable() { + return true; + } + + public List searchForMessages(MessageRetrievalListener retrievalListener, + LocalSearch search) throws MessagingException { + + StringBuilder query = new StringBuilder(); + List queryArgs = new ArrayList(); + SqlQueryBuilder.buildWhereClause(mAccount, search.getConditions(), query, queryArgs); + + // Avoid "ambiguous column name" error by prefixing "id" with the message table name + String where = SqlQueryBuilder.addPrefixToSelection(new String[] { "id" }, + "messages.", query.toString()); + + String[] selectionArgs = queryArgs.toArray(EMPTY_STRING_ARRAY); + + String sqlQuery = "SELECT " + GET_MESSAGES_COLS + "FROM messages " + + "LEFT JOIN threads ON (threads.message_id = messages.id) " + + "LEFT JOIN folders ON (folders.id = messages.folder_id) WHERE " + + "((empty IS NULL OR empty != 1) AND deleted = 0)" + + ((!StringUtils.isNullOrEmpty(where)) ? " AND (" + where + ")" : "") + + " ORDER BY date DESC"; + + if (K9.DEBUG) { + Log.d(K9.LOG_TAG, "Query = " + sqlQuery); + } + + return getMessages(retrievalListener, null, sqlQuery, selectionArgs); + } + + /* + * Given a query string, actually do the query for the messages and + * call the MessageRetrievalListener for each one + */ + List getMessages( + final MessageRetrievalListener listener, + final LocalFolder folder, + final String queryString, final String[] placeHolders + ) throws MessagingException { + final List messages = new ArrayList(); + final int j = database.execute(false, new DbCallback() { + @Override + public Integer doDbWork(final SQLiteDatabase db) throws WrappedException { + Cursor cursor = null; + int i = 0; + try { + cursor = db.rawQuery(queryString + " LIMIT 10", placeHolders); + + while (cursor.moveToNext()) { + LocalMessage message = new LocalMessage(LocalStore.this, null, folder); + message.populateFromGetMessageCursor(cursor); + + messages.add(message); + if (listener != null) { + listener.messageFinished(message, i, -1); + } + i++; + } + cursor.close(); + cursor = db.rawQuery(queryString + " LIMIT -1 OFFSET 10", placeHolders); + + while (cursor.moveToNext()) { + LocalMessage message = new LocalMessage(LocalStore.this, null, folder); + message.populateFromGetMessageCursor(cursor); + + messages.add(message); + if (listener != null) { + listener.messageFinished(message, i, -1); + } + i++; + } + } catch (Exception e) { + Log.d(K9.LOG_TAG, "Got an exception", e); + } finally { + Utility.closeQuietly(cursor); + } + return i; + } + }); + if (listener != null) { + listener.messagesFinished(j); + } + + return Collections.unmodifiableList(messages); + + } + + public List getMessagesInThread(final long rootId) throws MessagingException { + String rootIdString = Long.toString(rootId); + + LocalSearch search = new LocalSearch(); + search.and(Searchfield.THREAD_ID, rootIdString, Attribute.EQUALS); + + return searchForMessages(null, search); + } + + public AttachmentInfo getAttachmentInfo(final String attachmentId) throws UnavailableStorageException { + return database.execute(false, new DbCallback() { + @Override + public AttachmentInfo doDbWork(final SQLiteDatabase db) throws WrappedException { + String name; + String type; + int size; + Cursor cursor = null; + try { + cursor = db.query( + "attachments", + new String[] { "name", "size", "mime_type" }, + "id = ?", + new String[] { attachmentId }, + null, + null, + null); + if (!cursor.moveToFirst()) { + return null; + } + name = cursor.getString(0); + size = cursor.getInt(1); + type = cursor.getString(2); + final AttachmentInfo attachmentInfo = new AttachmentInfo(); + attachmentInfo.name = name; + attachmentInfo.size = size; + attachmentInfo.type = type; + return attachmentInfo; + } finally { + Utility.closeQuietly(cursor); + } + } + }); + } + + public static class AttachmentInfo { + public String name; + public int size; + public String type; + } + + public void createFolders(final List foldersToCreate, final int visibleLimit) throws UnavailableStorageException { + database.execute(true, new DbCallback() { + @Override + public Void doDbWork(final SQLiteDatabase db) throws WrappedException { + for (LocalFolder folder : foldersToCreate) { + String name = folder.getName(); + final LocalFolder.PreferencesHolder prefHolder = folder.new PreferencesHolder(); + + // When created, special folders should always be displayed + // inbox should be integrated + // and the inbox and drafts folders should be syncced by default + if (mAccount.isSpecialFolder(name)) { + prefHolder.inTopGroup = true; + prefHolder.displayClass = LocalFolder.FolderClass.FIRST_CLASS; + if (name.equalsIgnoreCase(mAccount.getInboxFolderName())) { + prefHolder.integrate = true; + prefHolder.notifyClass = LocalFolder.FolderClass.FIRST_CLASS; + prefHolder.pushClass = LocalFolder.FolderClass.FIRST_CLASS; + } else { + prefHolder.pushClass = LocalFolder.FolderClass.INHERITED; + + } + if (name.equalsIgnoreCase(mAccount.getInboxFolderName()) || + name.equalsIgnoreCase(mAccount.getDraftsFolderName())) { + prefHolder.syncClass = LocalFolder.FolderClass.FIRST_CLASS; + } else { + prefHolder.syncClass = LocalFolder.FolderClass.NO_CLASS; + } + } + folder.refresh(name, prefHolder); // Recover settings from Preferences + + db.execSQL("INSERT INTO folders (name, visible_limit, top_group, display_class, poll_class, notify_class, push_class, integrate) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", new Object[] { + name, + visibleLimit, + prefHolder.inTopGroup ? 1 : 0, + prefHolder.displayClass.name(), + prefHolder.syncClass.name(), + prefHolder.notifyClass.name(), + prefHolder.pushClass.name(), + prefHolder.integrate ? 1 : 0, + }); + + } + return null; + } + }); + } + + + String serializeFlags(Iterable flags) { + List extraFlags = new ArrayList(); + + for (Flag flag : flags) { + switch (flag) { + case DELETED: + case SEEN: + case FLAGGED: + case ANSWERED: + case FORWARDED: { + break; + } + default: { + extraFlags.add(flag); + } + } + } + + return Utility.combine(extraFlags, ',').toUpperCase(Locale.US); + } + + public LockableDatabase getDatabase() { + return database; + } + + void notifyChange() { + Uri uri = Uri.withAppendedPath(EmailProvider.CONTENT_URI, "account/" + uUid + "/messages"); + mContentResolver.notifyChange(uri, null); + } + + /** + * Split database operations with a large set of arguments into multiple SQL statements. + * + *

    + * At the time of this writing (2012-12-06) SQLite only supports around 1000 arguments. That's + * why we have to split SQL statements with a large set of arguments into multiple SQL + * statements each working on a subset of the arguments. + *

    + * + * @param selectionCallback + * Supplies the argument set and the code to query/update the database. + * @param batchSize + * The maximum size of the selection set in each SQL statement. + * + * @throws MessagingException + */ + public void doBatchSetSelection(final BatchSetSelection selectionCallback, final int batchSize) + throws MessagingException { + + final List selectionArgs = new ArrayList(); + int start = 0; + + while (start < selectionCallback.getListSize()) { + final StringBuilder selection = new StringBuilder(); + + selection.append(" IN ("); + + int count = Math.min(selectionCallback.getListSize() - start, batchSize); + + for (int i = start, end = start + count; i < end; i++) { + if (i > start) { + selection.append(",?"); + } else { + selection.append("?"); + } + + selectionArgs.add(selectionCallback.getListItem(i)); + } + + selection.append(")"); + + try { + database.execute(true, new DbCallback() { + @Override + public Void doDbWork(final SQLiteDatabase db) throws WrappedException, + UnavailableStorageException { + + selectionCallback.doDbWork(db, selection.toString(), + selectionArgs.toArray(EMPTY_STRING_ARRAY)); + + return null; + } + }); + + selectionCallback.postDbWork(); + + } catch (WrappedException e) { + throw(MessagingException) e.getCause(); + } + + selectionArgs.clear(); + start += count; + } + } + + /** + * Defines the behavior of {@link LocalStore#doBatchSetSelection(BatchSetSelection, int)}. + */ + public interface BatchSetSelection { + /** + * @return The size of the argument list. + */ + int getListSize(); + + /** + * Get a specific item of the argument list. + * + * @param index + * The index of the item. + * + * @return Item at position {@code i} of the argument list. + */ + String getListItem(int index); + + /** + * Execute the SQL statement. + * + * @param db + * Use this {@link SQLiteDatabase} instance for your SQL statement. + * @param selectionSet + * A partial selection string containing place holders for the argument list, e.g. + * {@code " IN (?,?,?)"} (starts with a space). + * @param selectionArgs + * The current subset of the argument list. + * @throws UnavailableStorageException + */ + void doDbWork(SQLiteDatabase db, String selectionSet, String[] selectionArgs) + throws UnavailableStorageException; + + /** + * This will be executed after each invocation of + * {@link #doDbWork(SQLiteDatabase, String, String[])} (after the transaction has been + * committed). + */ + void postDbWork(); + } + + /** + * Change the state of a flag for a list of messages. + * + *

    + * The goal of this method is to be fast. Currently this means using as few SQL UPDATE + * statements as possible. + * + * @param messageIds + * A list of primary keys in the "messages" table. + * @param flag + * The flag to change. This must be a flag with a separate column in the database. + * @param newState + * {@code true}, if the flag should be set. {@code false}, otherwise. + * + * @throws MessagingException + */ + public void setFlag(final List messageIds, final Flag flag, final boolean newState) + throws MessagingException { + + final ContentValues cv = new ContentValues(); + cv.put(getColumnNameForFlag(flag), newState); + + doBatchSetSelection(new BatchSetSelection() { + + @Override + public int getListSize() { + return messageIds.size(); + } + + @Override + public String getListItem(int index) { + return Long.toString(messageIds.get(index)); + } + + @Override + public void doDbWork(SQLiteDatabase db, String selectionSet, String[] selectionArgs) + throws UnavailableStorageException { + + db.update("messages", cv, "(empty IS NULL OR empty != 1) AND id" + selectionSet, + selectionArgs); + } + + @Override + public void postDbWork() { + notifyChange(); + } + }, FLAG_UPDATE_BATCH_SIZE); + } + + /** + * Change the state of a flag for a list of threads. + * + *

    + * The goal of this method is to be fast. Currently this means using as few SQL UPDATE + * statements as possible. + * + * @param threadRootIds + * A list of root thread IDs. + * @param flag + * The flag to change. This must be a flag with a separate column in the database. + * @param newState + * {@code true}, if the flag should be set. {@code false}, otherwise. + * + * @throws MessagingException + */ + public void setFlagForThreads(final List threadRootIds, Flag flag, final boolean newState) + throws MessagingException { + + final String flagColumn = getColumnNameForFlag(flag); + + doBatchSetSelection(new BatchSetSelection() { + + @Override + public int getListSize() { + return threadRootIds.size(); + } + + @Override + public String getListItem(int index) { + return Long.toString(threadRootIds.get(index)); + } + + @Override + public void doDbWork(SQLiteDatabase db, String selectionSet, String[] selectionArgs) + throws UnavailableStorageException { + + db.execSQL("UPDATE messages SET " + flagColumn + " = " + ((newState) ? "1" : "0") + + " WHERE id IN (" + + "SELECT m.id FROM threads t " + + "LEFT JOIN messages m ON (t.message_id = m.id) " + + "WHERE (m.empty IS NULL OR m.empty != 1) AND m.deleted = 0 " + + "AND t.root" + selectionSet + ")", + selectionArgs); + } + + @Override + public void postDbWork() { + notifyChange(); + } + }, THREAD_FLAG_UPDATE_BATCH_SIZE); + } + + /** + * Get folder name and UID for the supplied messages. + * + * @param messageIds + * A list of primary keys in the "messages" table. + * @param threadedList + * If this is {@code true}, {@code messageIds} contains the thread IDs of the messages + * at the root of a thread. In that case return UIDs for all messages in these threads. + * If this is {@code false} only the UIDs for messages in {@code messageIds} are + * returned. + * + * @return The list of UIDs for the messages grouped by folder name. + * + * @throws MessagingException + */ + public Map> getFoldersAndUids(final List messageIds, + final boolean threadedList) throws MessagingException { + + final Map> folderMap = new HashMap>(); + + doBatchSetSelection(new BatchSetSelection() { + + @Override + public int getListSize() { + return messageIds.size(); + } + + @Override + public String getListItem(int index) { + return Long.toString(messageIds.get(index)); + } + + @Override + public void doDbWork(SQLiteDatabase db, String selectionSet, String[] selectionArgs) + throws UnavailableStorageException { + + if (threadedList) { + String sql = "SELECT m.uid, f.name " + + "FROM threads t " + + "LEFT JOIN messages m ON (t.message_id = m.id) " + + "LEFT JOIN folders f ON (m.folder_id = f.id) " + + "WHERE (m.empty IS NULL OR m.empty != 1) AND m.deleted = 0 " + + "AND t.root" + selectionSet; + + getDataFromCursor(db.rawQuery(sql, selectionArgs)); + + } else { + String sql = + "SELECT m.uid, f.name " + + "FROM messages m " + + "LEFT JOIN folders f ON (m.folder_id = f.id) " + + "WHERE (m.empty IS NULL OR m.empty != 1) AND m.id" + selectionSet; + + getDataFromCursor(db.rawQuery(sql, selectionArgs)); + } + } + + private void getDataFromCursor(Cursor cursor) { + try { + while (cursor.moveToNext()) { + String uid = cursor.getString(0); + String folderName = cursor.getString(1); + + List uidList = folderMap.get(folderName); + if (uidList == null) { + uidList = new ArrayList(); + folderMap.put(folderName, uidList); + } + + uidList.add(uid); + } + } finally { + cursor.close(); + } + } + + @Override + public void postDbWork() { + notifyChange(); + + } + }, UID_CHECK_BATCH_SIZE); + + return folderMap; + } +} diff --git a/src/com/fsck/k9/mail/store/local/LocalTextBody.java b/src/com/fsck/k9/mail/store/local/LocalTextBody.java new file mode 100644 index 000000000..c555cd43c --- /dev/null +++ b/src/com/fsck/k9/mail/store/local/LocalTextBody.java @@ -0,0 +1,20 @@ +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 final String mBodyForDisplay; + + public LocalTextBody(String body, String bodyForDisplay) { + super(body); + this.mBodyForDisplay = bodyForDisplay; + } + + public String getBodyForDisplay() { + return mBodyForDisplay; + } + +}//LocalTextBody \ No newline at end of file diff --git a/src/com/fsck/k9/mail/store/local/StoreSchemaDefinition.java b/src/com/fsck/k9/mail/store/local/StoreSchemaDefinition.java new file mode 100644 index 000000000..494a62af5 --- /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 ... + List messages = obsoleteOutbox.getMessages(null, false); + + if (messages.size() > 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)); + cv.put("read", read); + cv.put("flagged", flagged); + cv.put("answered", answered); + cv.put("forwarded", forwarded); + + db.update("messages", cv, "id = ?", new String[] { Long.toString(id) }); + + cv.clear(); + extraFlags.clear(); + } + } finally { + cursor.close(); + } + + db.execSQL("CREATE INDEX IF NOT EXISTS msg_read ON messages (read)"); + db.execSQL("CREATE INDEX IF NOT EXISTS msg_flagged ON messages (flagged)"); + } + + if (db.getVersion() < 47) { + // Create new 'threads' table + db.execSQL("DROP TABLE IF EXISTS threads"); + db.execSQL("CREATE TABLE threads (" + + "id INTEGER PRIMARY KEY, " + + "message_id INTEGER, " + + "root INTEGER, " + + "parent INTEGER" + + ")"); + + // Create indices for new table + db.execSQL("DROP INDEX IF EXISTS threads_message_id"); + db.execSQL("CREATE INDEX IF NOT EXISTS threads_message_id ON threads (message_id)"); + + db.execSQL("DROP INDEX IF EXISTS threads_root"); + db.execSQL("CREATE INDEX IF NOT EXISTS threads_root ON threads (root)"); + + db.execSQL("DROP INDEX IF EXISTS threads_parent"); + db.execSQL("CREATE INDEX IF NOT EXISTS threads_parent ON threads (parent)"); + + // Create entries for all messages in 'threads' table + db.execSQL("INSERT INTO threads (message_id) SELECT id FROM messages"); + + // Copy thread structure from 'messages' table to 'threads' + Cursor cursor = db.query("messages", + new String[] { "id", "thread_root", "thread_parent" }, + null, null, null, null, null); + try { + ContentValues cv = new ContentValues(); + while (cursor.moveToNext()) { + cv.clear(); + long messageId = cursor.getLong(0); + + if (!cursor.isNull(1)) { + long threadRootMessageId = cursor.getLong(1); + db.execSQL("UPDATE threads SET root = (SELECT t.id FROM " + + "threads t WHERE t.message_id = ?) " + + "WHERE message_id = ?", + new String[] { + Long.toString(threadRootMessageId), + Long.toString(messageId) + }); + } + + if (!cursor.isNull(2)) { + long threadParentMessageId = cursor.getLong(2); + db.execSQL("UPDATE threads SET parent = (SELECT t.id FROM " + + "threads t WHERE t.message_id = ?) " + + "WHERE message_id = ?", + new String[] { + Long.toString(threadParentMessageId), + Long.toString(messageId) + }); + } + } + } finally { + cursor.close(); + } + + // Remove indices for old thread-related columns in 'messages' table + db.execSQL("DROP INDEX IF EXISTS msg_thread_root"); + db.execSQL("DROP INDEX IF EXISTS msg_thread_parent"); + + // Clear out old thread-related columns in 'messages' + ContentValues cv = new ContentValues(); + cv.putNull("thread_root"); + cv.putNull("thread_parent"); + db.update("messages", cv, null, null); + } + + if (db.getVersion() < 48) { + db.execSQL("UPDATE threads SET root=id WHERE root IS NULL"); + + db.execSQL("CREATE TRIGGER set_thread_root " + + "AFTER INSERT ON threads " + + "BEGIN " + + "UPDATE threads SET root=id WHERE root IS NULL AND ROWID = NEW.ROWID; " + + "END"); + } + if (db.getVersion() < 49) { + db.execSQL("CREATE INDEX IF NOT EXISTS msg_composite ON messages (deleted, empty,folder_id,flagged,read)"); + + } + if (db.getVersion() < 50) { + try { + db.execSQL("ALTER TABLE folders ADD notify_class TEXT default '" + + Folder.FolderClass.INHERITED.name() + "'"); + } catch (SQLiteException e) { + if (! e.getMessage().startsWith("duplicate column name:")) { + throw e; + } + } + + ContentValues cv = new ContentValues(); + cv.put("notify_class", Folder.FolderClass.FIRST_CLASS.name()); + + db.update("folders", cv, "name = ?", + new String[] { this.localStore.getAccount().getInboxFolderName() }); + } + } + + db.setVersion(LocalStore.DB_VERSION); + + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + + if (db.getVersion() != LocalStore.DB_VERSION) { + throw new RuntimeException("Database upgrade failed!"); + } + } + + private void update41Metadata(final SQLiteDatabase db, SharedPreferences prefs, int id, String name) { + + + Folder.FolderClass displayClass = Folder.FolderClass.NO_CLASS; + Folder.FolderClass syncClass = Folder.FolderClass.INHERITED; + Folder.FolderClass pushClass = Folder.FolderClass.SECOND_CLASS; + boolean inTopGroup = false; + boolean integrate = false; + if (this.localStore.getAccount().getInboxFolderName().equals(name)) { + displayClass = Folder.FolderClass.FIRST_CLASS; + syncClass = Folder.FolderClass.FIRST_CLASS; + pushClass = Folder.FolderClass.FIRST_CLASS; + inTopGroup = true; + integrate = true; + } + + try { + displayClass = Folder.FolderClass.valueOf(prefs.getString(this.localStore.uUid + "." + name + ".displayMode", displayClass.name())); + syncClass = Folder.FolderClass.valueOf(prefs.getString(this.localStore.uUid + "." + name + ".syncMode", syncClass.name())); + pushClass = Folder.FolderClass.valueOf(prefs.getString(this.localStore.uUid + "." + name + ".pushMode", pushClass.name())); + inTopGroup = prefs.getBoolean(this.localStore.uUid + "." + name + ".inTopGroup", inTopGroup); + integrate = prefs.getBoolean(this.localStore.uUid + "." + name + ".integrate", integrate); + } catch (Exception e) { + Log.e(K9.LOG_TAG, " Throwing away an error while trying to upgrade folder metadata", e); + } + + if (displayClass == Folder.FolderClass.NONE) { + displayClass = Folder.FolderClass.NO_CLASS; + } + if (syncClass == Folder.FolderClass.NONE) { + syncClass = Folder.FolderClass.INHERITED; + } + if (pushClass == Folder.FolderClass.NONE) { + pushClass = Folder.FolderClass.INHERITED; + } + + db.execSQL("UPDATE folders SET integrate = ?, top_group = ?, poll_class=?, push_class =?, display_class = ? WHERE id = ?", + new Object[] { integrate, inTopGroup, syncClass, pushClass, displayClass, id }); + + } +} \ No newline at end of file diff --git a/src/com/fsck/k9/mail/store/local/TempFileBody.java b/src/com/fsck/k9/mail/store/local/TempFileBody.java new file mode 100644 index 000000000..82720521e --- /dev/null +++ b/src/com/fsck/k9/mail/store/local/TempFileBody.java @@ -0,0 +1,29 @@ +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; + +/** + * An attachment whose contents are contained in a file. + */ +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..1fdb03e4b --- /dev/null +++ b/src/com/fsck/k9/mail/store/local/TempFileMessageBody.java @@ -0,0 +1,40 @@ +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; + +/** + * An attachment containing a body of type message/rfc822 + * whose contents are contained in a file. + */ +public class TempFileMessageBody extends TempFileBody implements CompositeBody { + + public TempFileMessageBody(String filename) { + super(filename); + } + + @Override + public void writeTo(OutputStream out) throws IOException, MessagingException { + AttachmentMessageBodyUtil.writeTo(this, out); + } + + @Override + public void setUsing7bitTransport() throws MessagingException { + // see LocalAttachmentMessageBody.setUsing7bitTransport() + } + + @Override + public void setEncoding(String encoding) throws MessagingException { + if (!MimeUtil.ENC_7BIT.equalsIgnoreCase(encoding) + && !MimeUtil.ENC_8BIT.equalsIgnoreCase(encoding)) { + throw new MessagingException( + "Incompatible content-transfer-encoding applied to a CompositeBody"); + } + mEncoding = encoding; + } +} \ No newline at end of file diff --git a/src/com/fsck/k9/mail/store/local/ThreadInfo.java b/src/com/fsck/k9/mail/store/local/ThreadInfo.java new file mode 100644 index 000000000..4115c16d6 --- /dev/null +++ b/src/com/fsck/k9/mail/store/local/ThreadInfo.java @@ -0,0 +1,17 @@ +package com.fsck.k9.mail.store.local; + +class ThreadInfo { + public final long threadId; + public final long msgId; + public final String messageId; + public final long rootId; + public final long parentId; + + public ThreadInfo(long threadId, long msgId, String messageId, long rootId, long parentId) { + this.threadId = threadId; + this.msgId = msgId; + this.messageId = messageId; + this.rootId = rootId; + this.parentId = parentId; + } +} \ No newline at end of file diff --git a/src/com/fsck/k9/mail/transport/SmtpTransport.java b/src/com/fsck/k9/mail/transport/SmtpTransport.java index cd8423c07..428c0dce2 100644 --- a/src/com/fsck/k9/mail/transport/SmtpTransport.java +++ b/src/com/fsck/k9/mail/transport/SmtpTransport.java @@ -6,6 +6,7 @@ import android.util.Log; import com.fsck.k9.Account; import com.fsck.k9.K9; import com.fsck.k9.R; +import com.fsck.k9.helper.UrlEncodingHelper; import com.fsck.k9.helper.Utility; import com.fsck.k9.mail.*; import com.fsck.k9.mail.Message.RecipientType; @@ -14,7 +15,7 @@ import com.fsck.k9.mail.filter.LineWrapOutputStream; import com.fsck.k9.mail.filter.PeekableInputStream; import com.fsck.k9.mail.filter.SmtpDataStuffing; import com.fsck.k9.mail.internet.MimeUtility; -import com.fsck.k9.mail.store.LocalStore.LocalMessage; +import com.fsck.k9.mail.store.local.LocalMessage; import com.fsck.k9.net.ssl.TrustedSocketFactory; import javax.net.ssl.SSLException; @@ -23,7 +24,6 @@ import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.IOException; import java.io.OutputStream; -import java.io.UnsupportedEncodingException; import java.net.*; import java.security.GeneralSecurityException; import java.util.*; @@ -33,7 +33,7 @@ public class SmtpTransport extends Transport { /** * Decodes a SmtpTransport URI. - * + * * NOTE: In contrast to ImapStore and Pop3Store, the authType is appended at the end! * *

    Possible forms:

    @@ -92,28 +92,23 @@ public class SmtpTransport extends Transport { } if (smtpUri.getUserInfo() != null) { - try { - String[] userInfoParts = smtpUri.getUserInfo().split(":"); - if (userInfoParts.length == 1) { - authType = AuthType.PLAIN; - username = URLDecoder.decode(userInfoParts[0], "UTF-8"); - } else if (userInfoParts.length == 2) { - authType = AuthType.PLAIN; - username = URLDecoder.decode(userInfoParts[0], "UTF-8"); - password = URLDecoder.decode(userInfoParts[1], "UTF-8"); - } else if (userInfoParts.length == 3) { - // NOTE: In SmptTransport URIs, the authType comes last! - authType = AuthType.valueOf(userInfoParts[2]); - username = URLDecoder.decode(userInfoParts[0], "UTF-8"); - if (authType == AuthType.EXTERNAL) { - clientCertificateAlias = URLDecoder.decode(userInfoParts[1], "UTF-8"); - } else { - password = URLDecoder.decode(userInfoParts[1], "UTF-8"); - } + String[] userInfoParts = smtpUri.getUserInfo().split(":"); + if (userInfoParts.length == 1) { + authType = AuthType.PLAIN; + username = UrlEncodingHelper.decodeUtf8(userInfoParts[0]); + } else if (userInfoParts.length == 2) { + authType = AuthType.PLAIN; + username = UrlEncodingHelper.decodeUtf8(userInfoParts[0]); + password = UrlEncodingHelper.decodeUtf8(userInfoParts[1]); + } else if (userInfoParts.length == 3) { + // NOTE: In SmptTransport URIs, the authType comes last! + authType = AuthType.valueOf(userInfoParts[2]); + username = UrlEncodingHelper.decodeUtf8(userInfoParts[0]); + if (authType == AuthType.EXTERNAL) { + clientCertificateAlias = UrlEncodingHelper.decodeUtf8(userInfoParts[1]); + } else { + password = UrlEncodingHelper.decodeUtf8(userInfoParts[1]); } - } catch (UnsupportedEncodingException enc) { - // This shouldn't happen since the encoding is hardcoded to UTF-8 - throw new IllegalArgumentException("Couldn't urldecode username or password.", enc); } } @@ -133,20 +128,12 @@ public class SmtpTransport extends Transport { * @see SmtpTransport#decodeUri(String) */ public static String createUri(ServerSettings server) { - String userEnc; - String passwordEnc; - String clientCertificateAliasEnc; - try { - userEnc = (server.username != null) ? - URLEncoder.encode(server.username, "UTF-8") : ""; - passwordEnc = (server.password != null) ? - URLEncoder.encode(server.password, "UTF-8") : ""; - clientCertificateAliasEnc = (server.clientCertificateAlias != null) ? - URLEncoder.encode(server.clientCertificateAlias, "UTF-8") : ""; - } - catch (UnsupportedEncodingException e) { - throw new IllegalArgumentException("Could not encode username or password", e); - } + String userEnc = (server.username != null) ? + UrlEncodingHelper.encodeUtf8(server.username) : ""; + String passwordEnc = (server.password != null) ? + UrlEncodingHelper.encodeUtf8(server.password) : ""; + String clientCertificateAliasEnc = (server.clientCertificateAlias != null) ? + UrlEncodingHelper.encodeUtf8(server.clientCertificateAlias) : ""; String scheme; switch (server.connectionSecurity) { @@ -183,16 +170,16 @@ public class SmtpTransport extends Transport { } - String mHost; - int mPort; - String mUsername; - String mPassword; - String mClientCertificateAlias; - AuthType mAuthType; - ConnectionSecurity mConnectionSecurity; - Socket mSocket; - PeekableInputStream mIn; - OutputStream mOut; + private String mHost; + private int mPort; + private String mUsername; + private String mPassword; + private String mClientCertificateAlias; + private AuthType mAuthType; + private ConnectionSecurity mConnectionSecurity; + private Socket mSocket; + private PeekableInputStream mIn; + private OutputStream mOut; private boolean m8bitEncodingAllowed; private int mLargestAcceptableMessage; @@ -269,7 +256,7 @@ public class SmtpTransport extends Transport { } } - HashMap extensions = sendHello(localHost); + Map extensions = sendHello(localHost); m8bitEncodingAllowed = extensions.containsKey("8BITMIME"); @@ -432,7 +419,7 @@ public class SmtpTransport extends Transport { * @param host * The EHLO/HELO parameter as defined by the RFC. * - * @return A (possibly empty) {@code HashMap} of extensions (upper case) and + * @return A (possibly empty) {@code Map} of extensions (upper case) and * their parameters (possibly 0 length) as returned by the EHLO command * * @throws IOException @@ -440,8 +427,8 @@ public class SmtpTransport extends Transport { * @throws MessagingException * In case of a malformed response. */ - private HashMap sendHello(String host) throws IOException, MessagingException { - HashMap extensions = new HashMap(); + private Map sendHello(String host) throws IOException, MessagingException { + Map extensions = new HashMap(); try { List results = executeSimpleCommand("EHLO " + host); // Remove the EHLO greeting response @@ -466,7 +453,7 @@ public class SmtpTransport extends Transport { @Override public void sendMessage(Message message) throws MessagingException { - ArrayList
    addresses = new ArrayList
    (); + List
    addresses = new ArrayList
    (); { addresses.addAll(Arrays.asList(message.getRecipients(RecipientType.TO))); addresses.addAll(Arrays.asList(message.getRecipients(RecipientType.CC))); @@ -474,12 +461,12 @@ public class SmtpTransport extends Transport { } message.setRecipients(RecipientType.BCC, null); - HashMap> charsetAddressesMap = - new HashMap>(); + Map> charsetAddressesMap = + new HashMap>(); for (Address address : addresses) { String addressString = address.getAddress(); String charset = MimeUtility.getCharsetFromAddress(addressString); - ArrayList addressesOfCharset = charsetAddressesMap.get(charset); + List addressesOfCharset = charsetAddressesMap.get(charset); if (addressesOfCharset == null) { addressesOfCharset = new ArrayList(); charsetAddressesMap.put(charset, addressesOfCharset); @@ -487,16 +474,16 @@ public class SmtpTransport extends Transport { addressesOfCharset.add(addressString); } - for (Map.Entry> charsetAddressesMapEntry : + for (Map.Entry> charsetAddressesMapEntry : charsetAddressesMap.entrySet()) { String charset = charsetAddressesMapEntry.getKey(); - ArrayList addressesOfCharset = charsetAddressesMapEntry.getValue(); + List addressesOfCharset = charsetAddressesMapEntry.getValue(); message.setCharset(charset); sendMessageTo(addressesOfCharset, message); } } - private void sendMessageTo(ArrayList addresses, Message message) + private void sendMessageTo(List addresses, Message message) throws MessagingException { boolean possibleSend = false; @@ -511,6 +498,7 @@ public class SmtpTransport extends Transport { if (mLargestAcceptableMessage > 0 && ((LocalMessage)message).hasAttachments()) { if (message.calculateSize() > mLargestAcceptableMessage) { MessagingException me = new MessagingException("Message too large for server"); + //TODO this looks rather suspicious... shouldn't it be true? me.setPermanentFailure(possibleSend); throw me; } @@ -545,14 +533,13 @@ public class SmtpTransport extends Transport { possibleSend = false; } + //TODO this looks rather suspicious... why is possibleSend used, and why are 5xx NOT permanent (in contrast to the log text) me.setPermanentFailure(possibleSend); throw me; } finally { close(); } - - } @Override diff --git a/src/com/fsck/k9/mail/transport/WebDavTransport.java b/src/com/fsck/k9/mail/transport/WebDavTransport.java index 6b5b61d60..1c94d01d5 100644 --- a/src/com/fsck/k9/mail/transport/WebDavTransport.java +++ b/src/com/fsck/k9/mail/transport/WebDavTransport.java @@ -11,6 +11,8 @@ import com.fsck.k9.mail.ServerSettings; import com.fsck.k9.mail.Transport; import com.fsck.k9.mail.store.WebDavStore; +import java.util.Collections; + public class WebDavTransport extends Transport { public static final String TRANSPORT_TYPE = WebDavStore.STORE_TYPE; @@ -66,6 +68,6 @@ public class WebDavTransport extends Transport { @Override public void sendMessage(Message message) throws MessagingException { - store.sendMessages(new Message[] { message }); + store.sendMessages(Collections.singletonList(message)); } } diff --git a/src/com/fsck/k9/net/ssl/TrustManagerFactory.java b/src/com/fsck/k9/net/ssl/TrustManagerFactory.java index 4a2c7206a..4e20f5c67 100644 --- a/src/com/fsck/k9/net/ssl/TrustManagerFactory.java +++ b/src/com/fsck/k9/net/ssl/TrustManagerFactory.java @@ -63,6 +63,8 @@ public final class TrustManagerFactory { String message = null; X509Certificate certificate = chain[0]; + Throwable cause = null; + try { defaultTrustManager.checkServerTrusted(chain, authType); new StrictHostnameVerifier().verify(mHost, certificate); @@ -70,15 +72,17 @@ public final class TrustManagerFactory { } catch (CertificateException e) { // cert. chain can't be validated message = e.getMessage(); + cause = e; } catch (SSLException e) { // host name doesn't match certificate message = e.getMessage(); + cause = e; } // Check the local key store if we couldn't verify the certificate using the global // key store or if the host name doesn't match the certificate name if (!keyStore.isValidCertificate(certificate, mHost, mPort)) { - throw new CertificateChainException(message, chain); + throw new CertificateChainException(message, chain, cause); } } diff --git a/src/com/fsck/k9/preferences/Editor.java b/src/com/fsck/k9/preferences/Editor.java index 88479d0e2..fdbc5d3eb 100644 --- a/src/com/fsck/k9/preferences/Editor.java +++ b/src/com/fsck/k9/preferences/Editor.java @@ -5,14 +5,15 @@ import com.fsck.k9.K9; import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; public class Editor implements android.content.SharedPreferences.Editor { private Storage storage; - private HashMap changes = new HashMap(); - private ArrayList removals = new ArrayList(); + private Map changes = new HashMap(); + private List removals = new ArrayList(); private boolean removeAll = false; Map snapshot = new HashMap(); diff --git a/src/com/fsck/k9/preferences/Settings.java b/src/com/fsck/k9/preferences/Settings.java index 1d06b7fd2..63e41225d 100644 --- a/src/com/fsck/k9/preferences/Settings.java +++ b/src/com/fsck/k9/preferences/Settings.java @@ -51,7 +51,7 @@ public class Settings { SortedMap headMap = versions.headMap(version + 1); // Skip this setting if it was introduced after 'version' - if (headMap.size() == 0) { + if (headMap.isEmpty()) { continue; } diff --git a/src/com/fsck/k9/preferences/SettingsExporter.java b/src/com/fsck/k9/preferences/SettingsExporter.java index e753e31f3..d7d77f6dc 100644 --- a/src/com/fsck/k9/preferences/SettingsExporter.java +++ b/src/com/fsck/k9/preferences/SettingsExporter.java @@ -132,7 +132,7 @@ public class SettingsExporter { Set exportAccounts; if (accountUuids == null) { - Account[] accounts = preferences.getAccounts(); + List accounts = preferences.getAccounts(); exportAccounts = new HashSet(); for (Account account : accounts) { exportAccounts.add(account.getUuid()); diff --git a/src/com/fsck/k9/preferences/SettingsImporter.java b/src/com/fsck/k9/preferences/SettingsImporter.java index c42995607..b3143f724 100644 --- a/src/com/fsck/k9/preferences/SettingsImporter.java +++ b/src/com/fsck/k9/preferences/SettingsImporter.java @@ -340,7 +340,7 @@ public class SettingsImporter { AccountDescription original = new AccountDescription(account.name, account.uuid); Preferences prefs = Preferences.getPreferences(context); - Account[] accounts = prefs.getAccounts(); + List accounts = prefs.getAccounts(); String uuid = account.uuid; Account existingAccount = prefs.getAccount(uuid); @@ -357,7 +357,7 @@ public class SettingsImporter { if (isAccountNameUsed(accountName, accounts)) { // Account name is already in use. So generate a new one by appending " (x)", where x // is the first number >= 1 that results in an unused account name. - for (int i = 1; i <= accounts.length; i++) { + for (int i = 1; i <= accounts.size(); i++) { accountName = account.name + " (" + i + ")"; if (!isAccountNameUsed(accountName, accounts)) { break; @@ -605,7 +605,7 @@ public class SettingsImporter { } } - private static boolean isAccountNameUsed(String name, Account[] accounts) { + private static boolean isAccountNameUsed(String name, List accounts) { for (Account account : accounts) { if (account == null) { continue; diff --git a/src/com/fsck/k9/preferences/Storage.java b/src/com/fsck/k9/preferences/Storage.java index e4a624847..88260f7ff 100644 --- a/src/com/fsck/k9/preferences/Storage.java +++ b/src/com/fsck/k9/preferences/Storage.java @@ -9,21 +9,24 @@ import android.database.sqlite.SQLiteStatement; import android.util.Log; import com.fsck.k9.K9; +import com.fsck.k9.helper.UrlEncodingHelper; import com.fsck.k9.helper.Utility; import java.net.URI; import java.net.URLEncoder; import java.util.ArrayList; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; import java.util.concurrent.CopyOnWriteArrayList; public class Storage implements SharedPreferences { - private static ConcurrentHashMap storages = + private static ConcurrentMap storages = new ConcurrentHashMap(); - private volatile ConcurrentHashMap storage = new ConcurrentHashMap(); + private volatile ConcurrentMap storage = new ConcurrentHashMap(); private CopyOnWriteArrayList listeners = new CopyOnWriteArrayList(); @@ -31,11 +34,11 @@ public class Storage implements SharedPreferences { private int DB_VERSION = 2; private String DB_NAME = "preferences_storage"; - private ThreadLocal> workingStorage - = new ThreadLocal>(); + private ThreadLocal> workingStorage + = new ThreadLocal>(); private ThreadLocal workingDB = new ThreadLocal(); - private ThreadLocal> workingChangedKeys = new ThreadLocal>(); + private ThreadLocal> workingChangedKeys = new ThreadLocal>(); private Context context = null; @@ -59,11 +62,11 @@ public class Storage implements SharedPreferences { if (transportUriStr != null) { String[] userInfoParts = uri.getUserInfo().split(":"); - String usernameEnc = URLEncoder.encode(userInfoParts[0], "UTF-8"); + String usernameEnc = UrlEncodingHelper.encodeUtf8(userInfoParts[0]); String passwordEnc = ""; String authType = ""; if (userInfoParts.length > 1) { - passwordEnc = ":" + URLEncoder.encode(userInfoParts[1], "UTF-8"); + passwordEnc = ":" + UrlEncodingHelper.encodeUtf8(userInfoParts[1]); } if (userInfoParts.length > 2) { authType = ":" + userInfoParts[2]; @@ -83,34 +86,34 @@ public class Storage implements SharedPreferences { if (storeUriStr.startsWith("imap")) { String[] userInfoParts = uri.getUserInfo().split(":"); if (userInfoParts.length == 2) { - String usernameEnc = URLEncoder.encode(userInfoParts[0], "UTF-8"); - String passwordEnc = URLEncoder.encode(userInfoParts[1], "UTF-8"); + String usernameEnc = UrlEncodingHelper.encodeUtf8(userInfoParts[0]); + String passwordEnc = UrlEncodingHelper.encodeUtf8(userInfoParts[1]); newUserInfo = usernameEnc + ":" + passwordEnc; } else { String authType = userInfoParts[0]; - String usernameEnc = URLEncoder.encode(userInfoParts[1], "UTF-8"); - String passwordEnc = URLEncoder.encode(userInfoParts[2], "UTF-8"); + String usernameEnc = UrlEncodingHelper.encodeUtf8(userInfoParts[1]); + String passwordEnc = UrlEncodingHelper.encodeUtf8(userInfoParts[2]); newUserInfo = authType + ":" + usernameEnc + ":" + passwordEnc; } } else if (storeUriStr.startsWith("pop3")) { String[] userInfoParts = uri.getUserInfo().split(":", 2); - String usernameEnc = URLEncoder.encode(userInfoParts[0], "UTF-8"); + String usernameEnc = UrlEncodingHelper.encodeUtf8(userInfoParts[0]); String passwordEnc = ""; if (userInfoParts.length > 1) { - passwordEnc = ":" + URLEncoder.encode(userInfoParts[1], "UTF-8"); + passwordEnc = ":" + UrlEncodingHelper.encodeUtf8(userInfoParts[1]); } newUserInfo = usernameEnc + passwordEnc; } else if (storeUriStr.startsWith("webdav")) { String[] userInfoParts = uri.getUserInfo().split(":", 2); - String usernameEnc = URLEncoder.encode(userInfoParts[0], "UTF-8"); + String usernameEnc = UrlEncodingHelper.encodeUtf8(userInfoParts[0]); String passwordEnc = ""; if (userInfoParts.length > 1) { - passwordEnc = ":" + URLEncoder.encode(userInfoParts[1], "UTF-8"); + passwordEnc = ":" + UrlEncodingHelper.encodeUtf8(userInfoParts[1]); } newUserInfo = usernameEnc + passwordEnc; @@ -201,7 +204,7 @@ public class Storage implements SharedPreferences { } private void keyChange(String key) { - ArrayList changedKeys = workingChangedKeys.get(); + List changedKeys = workingChangedKeys.get(); if (!changedKeys.contains(key)) { changedKeys.add(key); } @@ -258,14 +261,14 @@ public class Storage implements SharedPreferences { } protected void doInTransaction(Runnable dbWork) { - ConcurrentHashMap newStorage = new ConcurrentHashMap(); + ConcurrentMap newStorage = new ConcurrentHashMap(); newStorage.putAll(storage); workingStorage.set(newStorage); SQLiteDatabase mDb = openDB(); workingDB.set(mDb); - ArrayList changedKeys = new ArrayList(); + List changedKeys = new ArrayList(); workingChangedKeys.set(changedKeys); mDb.beginTransaction(); @@ -287,13 +290,17 @@ public class Storage implements SharedPreferences { } } - public long size() { - return storage.size(); + public boolean isEmpty() { + return storage.isEmpty(); } //@Override public boolean contains(String key) { - return storage.contains(key); + // TODO this used to be ConcurrentHashMap#contains which is + // actually containsValue. But looking at the usage of this method, + // it's clear that containsKey is what's intended. Investigate if this + // was a bug previously. Looks like it was only used once, when upgrading + return storage.containsKey(key); } //@Override diff --git a/src/com/fsck/k9/provider/AttachmentProvider.java b/src/com/fsck/k9/provider/AttachmentProvider.java index f3ba78590..c0bd8b0f7 100644 --- a/src/com/fsck/k9/provider/AttachmentProvider.java +++ b/src/com/fsck/k9/provider/AttachmentProvider.java @@ -15,8 +15,8 @@ import com.fsck.k9.K9; import com.fsck.k9.Preferences; import com.fsck.k9.mail.MessagingException; import com.fsck.k9.mail.internet.MimeUtility; -import com.fsck.k9.mail.store.LocalStore; -import com.fsck.k9.mail.store.LocalStore.AttachmentInfo; +import com.fsck.k9.mail.store.local.LocalStore; +import com.fsck.k9.mail.store.local.LocalStore.AttachmentInfo; import com.fsck.k9.mail.store.StorageManager; import java.io.*; diff --git a/src/com/fsck/k9/provider/EmailProvider.java b/src/com/fsck/k9/provider/EmailProvider.java index 9537ed64a..c99bdb1f4 100644 --- a/src/com/fsck/k9/provider/EmailProvider.java +++ b/src/com/fsck/k9/provider/EmailProvider.java @@ -11,10 +11,10 @@ import com.fsck.k9.cache.EmailProviderCacheCursor; import com.fsck.k9.helper.StringUtils; import com.fsck.k9.helper.Utility; import com.fsck.k9.mail.MessagingException; -import com.fsck.k9.mail.store.LocalStore; import com.fsck.k9.mail.store.LockableDatabase; import com.fsck.k9.mail.store.LockableDatabase.DbCallback; import com.fsck.k9.mail.store.LockableDatabase.WrappedException; +import com.fsck.k9.mail.store.local.LocalStore; import com.fsck.k9.mail.store.UnavailableStorageException; import com.fsck.k9.search.SqlQueryBuilder; diff --git a/src/com/fsck/k9/provider/MessageProvider.java b/src/com/fsck/k9/provider/MessageProvider.java index 86a858337..6ce34baf3 100644 --- a/src/com/fsck/k9/provider/MessageProvider.java +++ b/src/com/fsck/k9/provider/MessageProvider.java @@ -32,7 +32,8 @@ import com.fsck.k9.mail.Flag; import com.fsck.k9.mail.Folder; import com.fsck.k9.mail.Message; import com.fsck.k9.mail.MessagingException; -import com.fsck.k9.mail.store.LocalStore; +import com.fsck.k9.mail.store.local.LocalMessage; +import com.fsck.k9.mail.store.local.LocalStore; import com.fsck.k9.search.SearchAccount; import java.lang.ref.WeakReference; @@ -152,9 +153,9 @@ public class MessageProvider extends ContentProvider { } /** - * Extracts the {@link LocalStore.LocalMessage#getId() ID} from the given + * Extracts the {@link LocalMessage#getId() ID} from the given * {@link MessageInfoHolder}. The underlying {@link Message} is expected to - * be a {@link LocalStore.LocalMessage}. + * be a {@link LocalMessage}. */ public static class IdExtractor implements FieldExtractor { @Override diff --git a/src/com/fsck/k9/search/ConditionsTreeNode.java b/src/com/fsck/k9/search/ConditionsTreeNode.java index 7730e0725..3299c744e 100644 --- a/src/com/fsck/k9/search/ConditionsTreeNode.java +++ b/src/com/fsck/k9/search/ConditionsTreeNode.java @@ -54,7 +54,7 @@ public class ConditionsTreeNode implements Parcelable { * should point to rows representing the nodes of the tree. * * @param cursor Cursor pointing to the first of a bunch or rows. Each rows - * should contains 1 tree node. + * should contains 1 tree node. * @return A condition tree. */ public static ConditionsTreeNode buildTreeFromDB(Cursor cursor) { @@ -256,7 +256,7 @@ public class ConditionsTreeNode implements Parcelable { * @return List of all nodes in subtree in preorder. */ public List preorder() { - ArrayList result = new ArrayList(); + List result = new ArrayList(); Stack stack = new Stack(); stack.push(this); diff --git a/src/com/fsck/k9/search/LocalSearch.java b/src/com/fsck/k9/search/LocalSearch.java index e109e50f4..abff83163 100644 --- a/src/com/fsck/k9/search/LocalSearch.java +++ b/src/com/fsck/k9/search/LocalSearch.java @@ -240,8 +240,8 @@ public class LocalSearch implements SearchSpecification { public void addAllowedFolder(String name) { /* * TODO find folder sub-tree - * - do and on root of it & rest of search - * - do or between folder nodes + * - do and on root of it & rest of search + * - do or between folder nodes */ mConditions = and(new SearchCondition(Searchfield.FOLDER, Attribute.EQUALS, name)); } @@ -252,7 +252,7 @@ public class LocalSearch implements SearchSpecification { * real searches because of possible extra conditions to a folder requirement. */ public List getFolderNames() { - ArrayList results = new ArrayList(); + List results = new ArrayList(); for (ConditionsTreeNode node : mLeafSet) { if (node.mCondition.field == Searchfield.FOLDER && node.mCondition.attribute == Attribute.EQUALS) { @@ -328,7 +328,7 @@ public class LocalSearch implements SearchSpecification { */ @Override public String[] getAccountUuids() { - if (mAccountUuids.size() == 0) { + if (mAccountUuids.isEmpty()) { return new String[] { SearchSpecification.ALL_ACCOUNTS }; } @@ -343,7 +343,7 @@ public class LocalSearch implements SearchSpecification { * @return {@code true} if all accounts should be searched. */ public boolean searchAllAccounts() { - return (mAccountUuids.size() == 0); + return (mAccountUuids.isEmpty()); } /** diff --git a/src/com/fsck/k9/search/SearchSpecification.java b/src/com/fsck/k9/search/SearchSpecification.java index 99edef8a7..7c70b832a 100644 --- a/src/com/fsck/k9/search/SearchSpecification.java +++ b/src/com/fsck/k9/search/SearchSpecification.java @@ -58,8 +58,8 @@ public interface SearchSpecification extends Parcelable { * By result, only the fields in here are searchable. * * Fields not in here at this moment ( and by effect not searchable ): - * id, html_content, internal_date, message_id, - * preview, mime_type + * id, html_content, internal_date, message_id, + * preview, mime_type * */ public enum Searchfield { @@ -92,9 +92,9 @@ public interface SearchSpecification extends Parcelable { /** * This class represents 1 value for a certain search field. One * value consists of three things: - * an attribute: equals, starts with, contains,... - * a searchfield: date, flags, sender, subject,... - * a value: "apple", "jesse",.. + * an attribute: equals, starts with, contains,... + * a searchfield: date, flags, sender, subject,... + * a value: "apple", "jesse",.. * * @author dzan */ diff --git a/src/com/fsck/k9/search/SqlQueryBuilder.java b/src/com/fsck/k9/search/SqlQueryBuilder.java index da6e0850f..1b6ecf6a2 100644 --- a/src/com/fsck/k9/search/SqlQueryBuilder.java +++ b/src/com/fsck/k9/search/SqlQueryBuilder.java @@ -5,8 +5,8 @@ import java.util.List; import com.fsck.k9.Account; import com.fsck.k9.mail.MessagingException; import com.fsck.k9.mail.Folder; -import com.fsck.k9.mail.store.LocalStore; -import com.fsck.k9.mail.store.LocalStore.LocalFolder; +import com.fsck.k9.mail.store.local.LocalFolder; +import com.fsck.k9.mail.store.local.LocalStore; import com.fsck.k9.search.SearchSpecification.Attribute; import com.fsck.k9.search.SearchSpecification.SearchCondition; import com.fsck.k9.search.SearchSpecification.Searchfield; diff --git a/src/com/fsck/k9/service/DatabaseUpgradeService.java b/src/com/fsck/k9/service/DatabaseUpgradeService.java index 249af1e66..ee5b9b26b 100644 --- a/src/com/fsck/k9/service/DatabaseUpgradeService.java +++ b/src/com/fsck/k9/service/DatabaseUpgradeService.java @@ -1,5 +1,6 @@ package com.fsck.k9.service; +import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; import android.app.Service; @@ -186,8 +187,8 @@ public class DatabaseUpgradeService extends Service { private void upgradeDatabases() { Preferences preferences = Preferences.getPreferences(this); - Account[] accounts = preferences.getAccounts(); - mProgressEnd = accounts.length; + List accounts = preferences.getAccounts(); + mProgressEnd = accounts.size(); mProgress = 0; for (Account account : accounts) { diff --git a/src/com/fsck/k9/service/NotificationActionService.java b/src/com/fsck/k9/service/NotificationActionService.java index 791db4257..107bd0f4c 100644 --- a/src/com/fsck/k9/service/NotificationActionService.java +++ b/src/com/fsck/k9/service/NotificationActionService.java @@ -1,6 +1,8 @@ package com.fsck.k9.service; +import java.io.Serializable; import java.util.ArrayList; +import java.util.List; import com.fsck.k9.Account; import com.fsck.k9.K9; @@ -8,6 +10,7 @@ import com.fsck.k9.Preferences; import com.fsck.k9.activity.MessageCompose; import com.fsck.k9.activity.MessageReference; import com.fsck.k9.controller.MessagingController; +import com.fsck.k9.helper.Utility; import com.fsck.k9.mail.Flag; import com.fsck.k9.mail.Message; @@ -35,13 +38,12 @@ public class NotificationActionService extends CoreService { return PendingIntent.getService(context, account.getAccountNumber(), i, PendingIntent.FLAG_UPDATE_CURRENT); } - public static PendingIntent getReadAllMessagesIntent(Context context, final Account account, - final ArrayList refs) { + public static PendingIntent getReadAllMessagesIntent(Context context, final Account account, final Serializable refs) { Intent i = new Intent(context, NotificationActionService.class); i.putExtra(EXTRA_ACCOUNT, account.getUuid()); i.putExtra(EXTRA_MESSAGE_LIST, refs); i.setAction(READ_ALL_ACTION); - + return PendingIntent.getService(context, account.getAccountNumber(), i, PendingIntent.FLAG_UPDATE_CURRENT); } @@ -53,8 +55,7 @@ public class NotificationActionService extends CoreService { return PendingIntent.getService(context, account.getAccountNumber(), i, PendingIntent.FLAG_UPDATE_CURRENT); } - public static Intent getDeleteAllMessagesIntent(Context context, final Account account, - final ArrayList refs) { + public static Intent getDeleteAllMessagesIntent(Context context, final Account account, final Serializable refs) { Intent i = new Intent(context, NotificationActionService.class); i.putExtra(EXTRA_ACCOUNT, account.getUuid()); i.putExtra(EXTRA_MESSAGE_LIST, refs); @@ -77,7 +78,7 @@ public class NotificationActionService extends CoreService { if (K9.DEBUG) Log.i(K9.LOG_TAG, "NotificationActionService marking messages as read"); - ArrayList refs = + List refs = intent.getParcelableArrayListExtra(EXTRA_MESSAGE_LIST); for (MessageReference ref : refs) { controller.setFlag(account, ref.folderName, ref.uid, Flag.SEEN, true); @@ -86,9 +87,9 @@ public class NotificationActionService extends CoreService { if (K9.DEBUG) Log.i(K9.LOG_TAG, "NotificationActionService deleting messages"); - ArrayList refs = + List refs = intent.getParcelableArrayListExtra(EXTRA_MESSAGE_LIST); - ArrayList messages = new ArrayList(); + List messages = new ArrayList(); for (MessageReference ref : refs) { Message m = ref.restoreToLocalMessage(this); @@ -121,7 +122,7 @@ public class NotificationActionService extends CoreService { } else { Log.w(K9.LOG_TAG, "Could not find account for notification action."); } - + return START_NOT_STICKY; } } diff --git a/src/com/fsck/k9/service/PollService.java b/src/com/fsck/k9/service/PollService.java index f24f761de..0fbe8d4c0 100644 --- a/src/com/fsck/k9/service/PollService.java +++ b/src/com/fsck/k9/service/PollService.java @@ -12,6 +12,7 @@ import com.fsck.k9.helper.power.TracingPowerManager; import com.fsck.k9.helper.power.TracingPowerManager.TracingWakeLock; import java.util.HashMap; +import java.util.Map; public class PollService extends CoreService { private static String START_SERVICE = "com.fsck.k9.service.PollService.startService"; @@ -77,7 +78,7 @@ public class PollService extends CoreService { } class Listener extends MessagingListener { - HashMap accountsChecked = new HashMap(); + Map accountsChecked = new HashMap(); private TracingWakeLock wakeLock = null; private int startId = -1; diff --git a/src/com/fsck/k9/service/RemoteControlReceiver.java b/src/com/fsck/k9/service/RemoteControlReceiver.java index 7843a1ac5..ecdbf12d2 100644 --- a/src/com/fsck/k9/service/RemoteControlReceiver.java +++ b/src/com/fsck/k9/service/RemoteControlReceiver.java @@ -11,6 +11,8 @@ import com.fsck.k9.K9; import com.fsck.k9.remotecontrol.K9RemoteControl; import com.fsck.k9.Preferences; +import java.util.List; + import static com.fsck.k9.remotecontrol.K9RemoteControl.*; public class RemoteControlReceiver extends CoreReceiver { @@ -25,12 +27,12 @@ public class RemoteControlReceiver extends CoreReceiver { } else if (K9RemoteControl.K9_REQUEST_ACCOUNTS.equals(intent.getAction())) { try { Preferences preferences = Preferences.getPreferences(context); - Account[] accounts = preferences.getAccounts(); - String[] uuids = new String[accounts.length]; - String[] descriptions = new String[accounts.length]; - for (int i = 0; i < accounts.length; i++) { + List accounts = preferences.getAccounts(); + String[] uuids = new String[accounts.size()]; + String[] descriptions = new String[accounts.size()]; + for (int i = 0; i < accounts.size(); i++) { //warning: account may not be isAvailable() - Account account = accounts[i]; + Account account = accounts.get(i); uuids[i] = account.getUuid(); descriptions[i] = account.getDescription(); diff --git a/src/com/fsck/k9/service/RemoteControlService.java b/src/com/fsck/k9/service/RemoteControlService.java index 126c71e28..e942f4d23 100644 --- a/src/com/fsck/k9/service/RemoteControlService.java +++ b/src/com/fsck/k9/service/RemoteControlService.java @@ -17,6 +17,8 @@ import android.content.SharedPreferences.Editor; import android.util.Log; import android.widget.Toast; +import java.util.List; + public class RemoteControlService extends CoreService { private final static String RESCHEDULE_ACTION = "com.fsck.k9.service.RemoteControlService.RESCHEDULE_ACTION"; private final static String PUSH_RESTART_ACTION = "com.fsck.k9.service.RemoteControlService.PUSH_RESTART_ACTION"; @@ -65,7 +67,7 @@ public class RemoteControlService extends CoreService { Log.i(K9.LOG_TAG, "RemoteControlService changing settings for account with UUID " + uuid); } } - Account[] accounts = preferences.getAccounts(); + List accounts = preferences.getAccounts(); for (Account account : accounts) { //warning: account may not be isAvailable() if (allAccounts || account.getUuid().equals(uuid)) { diff --git a/src/com/fsck/k9/view/AttachmentView.java b/src/com/fsck/k9/view/AttachmentView.java index 9c5de50a0..cffb37c2a 100644 --- a/src/com/fsck/k9/view/AttachmentView.java +++ b/src/com/fsck/k9/view/AttachmentView.java @@ -40,7 +40,7 @@ import com.fsck.k9.mail.MessagingException; import com.fsck.k9.mail.Part; import com.fsck.k9.mail.internet.MimeHeader; import com.fsck.k9.mail.internet.MimeUtility; -import com.fsck.k9.mail.store.LocalStore.LocalAttachmentBodyPart; +import com.fsck.k9.mail.store.local.LocalAttachmentBodyPart; import com.fsck.k9.provider.AttachmentProvider; public class AttachmentView extends FrameLayout implements OnClickListener, OnLongClickListener { diff --git a/src/com/fsck/k9/view/MessageOpenPgpView.java b/src/com/fsck/k9/view/MessageOpenPgpView.java index dd2cbb40b..1b224112f 100644 --- a/src/com/fsck/k9/view/MessageOpenPgpView.java +++ b/src/com/fsck/k9/view/MessageOpenPgpView.java @@ -5,6 +5,7 @@ import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.InputStream; import java.io.UnsupportedEncodingException; +import java.nio.charset.Charset; import android.app.Activity; import android.app.Fragment; @@ -279,12 +280,7 @@ public class MessageOpenPgpView extends LinearLayout { String accName = OpenPgpApiHelper.buildAccountName(identity); intent.putExtra(OpenPgpApi.EXTRA_ACCOUNT_NAME, accName); - InputStream is = null; - try { - is = new ByteArrayInputStream(mData.getBytes("UTF-8")); - } catch (UnsupportedEncodingException e) { - Log.e(K9.LOG_TAG, "UnsupportedEncodingException.", e); - } + InputStream is = new ByteArrayInputStream(mData.getBytes(Charset.forName("UTF-8"))); final ByteArrayOutputStream os = new ByteArrayOutputStream(); DecryptVerifyCallback callback = new DecryptVerifyCallback(os, REQUEST_CODE_DECRYPT_VERIFY); diff --git a/src/com/fsck/k9/view/NonLockingScrollView.java b/src/com/fsck/k9/view/NonLockingScrollView.java index 38e479ef8..5897c3b22 100644 --- a/src/com/fsck/k9/view/NonLockingScrollView.java +++ b/src/com/fsck/k9/view/NonLockingScrollView.java @@ -28,6 +28,7 @@ import android.webkit.WebView; import android.widget.ScrollView; import java.util.ArrayList; +import java.util.List; /** * A {@link ScrollView} that will never lock scrolling in a particular direction. @@ -59,7 +60,7 @@ public class NonLockingScrollView extends ScrollView { /** * The list of children who should always receive touch events, and not have them intercepted. */ - private final ArrayList mChildrenNeedingAllTouches = new ArrayList(); + private final List mChildrenNeedingAllTouches = new ArrayList(); private boolean mSkipWebViewScroll = true; @@ -122,7 +123,7 @@ public class NonLockingScrollView extends ScrollView { } private final Rect sHitFrame = new Rect(); - private boolean isEventOverChild(MotionEvent ev, ArrayList children) { + private boolean isEventOverChild(MotionEvent ev, List children) { final int actionIndex = ev.getActionIndex(); final float x = ev.getX(actionIndex) + getScrollX(); final float y = ev.getY(actionIndex) + getScrollY(); diff --git a/src/com/fsck/k9/view/SingleMessageView.java b/src/com/fsck/k9/view/SingleMessageView.java index 58a5821cf..bca3eef0d 100644 --- a/src/com/fsck/k9/view/SingleMessageView.java +++ b/src/com/fsck/k9/view/SingleMessageView.java @@ -47,6 +47,7 @@ import com.fsck.k9.fragment.MessageViewFragment; import com.fsck.k9.helper.ClipboardManager; import com.fsck.k9.helper.Contacts; import com.fsck.k9.helper.HtmlConverter; +import com.fsck.k9.helper.UrlEncodingHelper; import com.fsck.k9.helper.Utility; import com.fsck.k9.mail.Address; import com.fsck.k9.mail.Flag; @@ -55,8 +56,8 @@ import com.fsck.k9.mail.MessagingException; import com.fsck.k9.mail.Multipart; import com.fsck.k9.mail.Part; import com.fsck.k9.mail.internet.MimeUtility; -import com.fsck.k9.mail.store.LocalStore; -import com.fsck.k9.mail.store.LocalStore.LocalMessage; +import com.fsck.k9.mail.store.local.LocalAttachmentBodyPart; +import com.fsck.k9.mail.store.local.LocalMessage; import com.fsck.k9.provider.AttachmentProvider.AttachmentProviderColumns; import org.apache.commons.io.IOUtils; @@ -626,7 +627,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); @@ -790,7 +791,7 @@ public class SingleMessageView extends LinearLayout implements OnClickListener, // Try to get the filename from the URL int start = path.lastIndexOf("/"); if (start != -1 && start + 1 < path.length()) { - filename = URLDecoder.decode(path.substring(start + 1), "UTF-8"); + filename = UrlEncodingHelper.decodeUtf8(path.substring(start + 1)); } else { // Use a dummy filename if necessary filename = "saved_image"; diff --git a/tests-on-jvm/gradlew b/tests-on-jvm/gradlew old mode 100644 new mode 100755 diff --git a/tests-on-jvm/src/com/fsck/k9/mail/internet/TextBodyBuilderTest.java b/tests-on-jvm/src/com/fsck/k9/mail/internet/TextBodyBuilderTest.java index de7701b6a..64882691d 100644 --- a/tests-on-jvm/src/com/fsck/k9/mail/internet/TextBodyBuilderTest.java +++ b/tests-on-jvm/src/com/fsck/k9/mail/internet/TextBodyBuilderTest.java @@ -44,7 +44,7 @@ class TestingTextBodyBuilder extends TextBodyBuilder { // HtmlConverter depends on Android. // So we use dummy method for tests. @Override - public String textToHtmlFragment(String text) { + protected String textToHtmlFragment(String text) { return "" + text + ""; } } diff --git a/tests/src/com/fsck/k9/mail/internet/MimeMessageParseTest.java b/tests/src/com/fsck/k9/mail/internet/MimeMessageParseTest.java new file mode 100644 index 000000000..4558d4264 --- /dev/null +++ b/tests/src/com/fsck/k9/mail/internet/MimeMessageParseTest.java @@ -0,0 +1,236 @@ +package com.fsck.k9.mail.internet; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.apache.commons.io.IOUtils; + +import com.fsck.k9.mail.Address; +import com.fsck.k9.mail.Body; +import com.fsck.k9.mail.BodyPart; +import com.fsck.k9.mail.Message.RecipientType; +import com.fsck.k9.mail.Multipart; +import com.fsck.k9.mail.internet.MimeMessage; + +import android.test.AndroidTestCase; + +public class MimeMessageParseTest extends AndroidTestCase { + + static { + BinaryTempFileBody.setTempDirectory(new File(System.getProperty("java.io.tmpdir"))); + } + + private static ByteArrayInputStream toStream(String rawMailData) throws Exception { + return new ByteArrayInputStream(rawMailData.getBytes("ISO-8859-1")); + } + + private static MimeMessage parseWithoutRecurse(InputStream data) throws Exception { + return new MimeMessage(data, false); + } + + private static MimeMessage parseWithRecurse(InputStream data) throws Exception { + return new MimeMessage(data, true); + } + + private static void checkAddresses(Address[] actual, String... expected) { + for (int i = 0; i < actual.length; i++) { + assertEquals(actual[i].toEncodedString(), expected[i]); + } + assertEquals(expected.length, actual.length); + } + + private static String streamToString(InputStream stream) throws Exception { + return IOUtils.toString(stream, "ISO-8859-1"); + } + + private static List getLeafParts(Body body) { + if (body instanceof Multipart) { + List ret = new ArrayList(); + for (BodyPart child : ((Multipart) body).getBodyParts()) { + ret.addAll(getLeafParts(child.getBody())); + } + return ret; + } else { + return Collections.singletonList(body); + } + } + + private static void checkLeafParts(MimeMessage msg, String... expectedParts) throws Exception { + List actual = new ArrayList(); + for (Body leaf : getLeafParts(msg.getBody())) { + actual.add(streamToString(leaf.getInputStream())); + } + assertEquals(Arrays.asList(expectedParts), actual); + } + + public static void testSinglePart7BitNoRecurse() throws Exception { + MimeMessage msg = parseWithoutRecurse(toStream( + "From: \r\n" + + "To: \r\n" + + "Subject: Testmail\r\n" + + "MIME-Version: 1.0\r\n" + + "Content-type: text/plain\r\n" + + "Content-Transfer-Encoding: 7bit\r\n" + + "\r\n" + + "this is some test text.")); + + checkAddresses(msg.getFrom(), "adam@example.org"); + checkAddresses(msg.getRecipients(RecipientType.TO), "eva@example.org"); + assertEquals("Testmail", msg.getSubject()); + assertEquals("text/plain", msg.getContentType()); + assertEquals("this is some test text.", streamToString(msg.getBody().getInputStream())); + } + + public static void testSinglePart8BitRecurse() throws Exception { + MimeMessage msg = parseWithRecurse(toStream( + "From: \r\n" + + "To: \r\n" + + "Subject: Testmail\r\n" + + "MIME-Version: 1.0\r\n" + + "Content-type: text/plain; encoding=ISO-8859-1\r\n" + + "Content-Transfer-Encoding: 8bit\r\n" + + "\r\n" + + "gefährliche Umlaute")); + + checkAddresses(msg.getFrom(), "adam@example.org"); + checkAddresses(msg.getRecipients(RecipientType.TO), "eva@example.org"); + assertEquals("Testmail", msg.getSubject()); + assertEquals("text/plain; encoding=ISO-8859-1", msg.getContentType()); + assertEquals("gefährliche Umlaute", streamToString(msg.getBody().getInputStream())); + } + + public static void testSinglePartBase64NoRecurse() throws Exception { + MimeMessage msg = parseWithoutRecurse(toStream( + "From: \r\n" + + "To: \r\n" + + "Subject: Testmail\r\n" + + "MIME-Version: 1.0\r\n" + + "Content-type: text/plain\r\n" + + "Content-Transfer-Encoding: base64\r\n" + + "\r\n" + + "dGhpcyBpcyBzb21lIG1vcmUgdGVzdCB0ZXh0Lg==\r\n")); + + checkAddresses(msg.getFrom(), "adam@example.org"); + checkAddresses(msg.getRecipients(RecipientType.TO), "eva@example.org"); + assertEquals("Testmail", msg.getSubject()); + assertEquals("text/plain", msg.getContentType()); + assertEquals("this is some more test text.", streamToString(msg.getBody().getInputStream())); + } + + public static void testMultipartSingleLayerNoRecurse() throws Exception { + MimeMessage msg = parseWithoutRecurse(toStream( + "From: \r\n" + + "To: \r\n" + + "Subject: Testmail 2\r\n" + + "MIME-Version: 1.0\n" + + "Content-Type: multipart/mixed; boundary=frontier\n" + + "\n" + + "This is a message with multiple parts in MIME format.\n" + + "--frontier\n" + + "Content-Type: text/plain\n" + + "\n" + + "This is the body of the message.\n" + + "--frontier\n" + + "Content-Type: application/octet-stream\n" + + "Content-Transfer-Encoding: base64\n" + + "\n" + + "PGh0bWw+CiAgPGhlYWQ+CiAgPC9oZWFkPgogIDxib2R5PgogICAgPHA+VGhpcyBpcyB0aGUg\n" + + "Ym9keSBvZiB0aGUgbWVzc2FnZS48L3A+CiAgPC9ib2R5Pgo8L2h0bWw+Cg=\n" + + "--frontier--")); + + checkAddresses(msg.getFrom(), "x@example.org"); + checkAddresses(msg.getRecipients(RecipientType.TO), "y@example.org"); + assertEquals("Testmail 2", msg.getSubject()); + assertEquals("multipart/mixed; boundary=frontier", msg.getContentType()); + checkLeafParts(msg, + "This is the body of the message.", + "\n" + + " \n" + + " \n" + + " \n" + + "

    This is the body of the message.

    \n" + + " \n" + + "\n" + + ""); + } + + public static void testMultipartSingleLayerRecurse() throws Exception { + MimeMessage msg = parseWithRecurse(toStream( + "From: \r\n" + + "To: \r\n" + + "Subject: Testmail 2\r\n" + + "MIME-Version: 1.0\n" + + "Content-Type: multipart/mixed; boundary=frontier\n" + + "\n" + + "This is a message with multiple parts in MIME format.\n" + + "--frontier\n" + + "Content-Type: text/plain\n" + + "\n" + + "This is the body of the message.\n" + + "--frontier\n" + + "Content-Type: application/octet-stream\n" + + "Content-Transfer-Encoding: base64\n" + + "\n" + + "PGh0bWw+CiAgPGhlYWQ+CiAgPC9oZWFkPgogIDxib2R5PgogICAgPHA+VGhpcyBpcyB0aGUg\n" + + "Ym9keSBvZiB0aGUgbWVzc2FnZS48L3A+CiAgPC9ib2R5Pgo8L2h0bWw+Cg=\n" + + "--frontier--")); + + checkAddresses(msg.getFrom(), "x@example.org"); + checkAddresses(msg.getRecipients(RecipientType.TO), "y@example.org"); + assertEquals("Testmail 2", msg.getSubject()); + assertEquals("multipart/mixed; boundary=frontier", msg.getContentType()); + checkLeafParts(msg, + "This is the body of the message.", + "\n" + + " \n" + + " \n" + + " \n" + + "

    This is the body of the message.

    \n" + + " \n" + + "\n" + + ""); + } + + public static void testMultipartTwoLayersRecurse() throws Exception { + MimeMessage msg = parseWithRecurse(toStream( + "From: \r\n" + + "To: \r\n" + + "Subject: Testmail 2\r\n" + + "MIME-Version: 1.0\n" + + "Content-Type: multipart/mixed; boundary=1\n" + + "\n" + + "This is a message with multiple parts in MIME format.\n" + + "--1\n" + + "Content-Type: text/plain\n" + + "\n" + + "some text in the first part\n" + + "--1\n" + + "Content-Type: multipart/alternative; boundary=2\n" + + "\n" + + "--2\n" + + "Content-Type: text/plain\n" + + "\n" + + "alternative 1\n" + + "--2\n" + + "Content-Type: text/plain\n" + + "\n" + + "alternative 2\n" + + "--2--\n" + + "--1--")); + + checkAddresses(msg.getFrom(), "x@example.org"); + checkAddresses(msg.getRecipients(RecipientType.TO), "y@example.org"); + assertEquals("Testmail 2", msg.getSubject()); + assertEquals("multipart/mixed; boundary=1", msg.getContentType()); + checkLeafParts(msg, + "some text in the first part", + "alternative 1", + "alternative 2"); + } + +} diff --git a/tests/src/com/fsck/k9/mail/internet/ViewablesTest.java b/tests/src/com/fsck/k9/mail/internet/ViewablesTest.java index cb62079f3..cceb4bff5 100644 --- a/tests/src/com/fsck/k9/mail/internet/ViewablesTest.java +++ b/tests/src/com/fsck/k9/mail/internet/ViewablesTest.java @@ -100,7 +100,7 @@ public class ViewablesTest extends AndroidTestCase { } public void testTextPlusRfc822Message() throws MessagingException { - K9ActivityCommon.setLanguage(getContext(), "en"); + K9ActivityCommon.setLanguage(getContext(), "en"); Locale.setDefault(Locale.US); TimeZone.setDefault(TimeZone.getTimeZone("GMT+01:00")); diff --git a/tests/src/com/fsck/k9/net/ssl/TrustManagerFactoryTest.java b/tests/src/com/fsck/k9/net/ssl/TrustManagerFactoryTest.java index ca33ba106..45efc0177 100644 --- a/tests/src/com/fsck/k9/net/ssl/TrustManagerFactoryTest.java +++ b/tests/src/com/fsck/k9/net/ssl/TrustManagerFactoryTest.java @@ -96,70 +96,82 @@ public class TrustManagerFactoryTest extends AndroidTestCase { + "Dcf5/G8bZe2DnavBQfML1wI5d7NUWE8CWb95SsIvFXI0qZE0oIR+axBVl9u97uaO\n" + "-----END CERTIFICATE-----\n"; - private static final String STARFIELD_CERT = + private static final String LINUX_COM_FIRST_PARENT_CERT = "-----BEGIN CERTIFICATE-----\n" + - "MIIFBzCCA++gAwIBAgICAgEwDQYJKoZIhvcNAQEFBQAwaDELMAkGA1UEBhMCVVMx\n" + - "JTAjBgNVBAoTHFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xMjAwBgNVBAsT\n" + - "KVN0YXJmaWVsZCBDbGFzcyAyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTA2\n" + - "MTExNjAxMTU0MFoXDTI2MTExNjAxMTU0MFowgdwxCzAJBgNVBAYTAlVTMRAwDgYD\n" + - "VQQIEwdBcml6b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFy\n" + - "ZmllbGQgVGVjaG5vbG9naWVzLCBJbmMuMTkwNwYDVQQLEzBodHRwOi8vY2VydGlm\n" + - "aWNhdGVzLnN0YXJmaWVsZHRlY2guY29tL3JlcG9zaXRvcnkxMTAvBgNVBAMTKFN0\n" + - "YXJmaWVsZCBTZWN1cmUgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxETAPBgNVBAUT\n" + - "CDEwNjg4NDM1MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4qddo+1m\n" + - "72ovKzYf3Y3TBQKgyg9eGa44cs8W2lRKy0gK9KFzEWWFQ8lbFwyaK74PmFF6YCkN\n" + - "bN7i6OUVTVb/kNGnpgQ/YAdKym+lEOez+FyxvCsq3AF59R019Xoog/KTc4KJrGBt\n" + - "y8JIwh3UBkQXPKwBR6s+cIQJC7ggCEAgh6FjGso+g9I3s5iNMj83v6G3W1/eXDOS\n" + - "zz4HzrlIS+LwVVAv+HBCidGTlopj2WYN5lhuuW2QvcrchGbyOY5bplhVc8tibBvX\n" + - "IBY7LFn1y8hWMkpQJ7pV06gBy3KpdIsMrTrlFbYq32X43or174Q7+edUZQuAvUdF\n" + - "pfBE2FM7voDxLwIDAQABo4IBRDCCAUAwHQYDVR0OBBYEFElLUifRG7zyoSFqYntR\n" + - "QnqK19VWMB8GA1UdIwQYMBaAFL9ft9HO3R+G9FtVrNzXEMIOqYjnMBIGA1UdEwEB\n" + - "/wQIMAYBAf8CAQAwOQYIKwYBBQUHAQEELTArMCkGCCsGAQUFBzABhh1odHRwOi8v\n" + - "b2NzcC5zdGFyZmllbGR0ZWNoLmNvbTBMBgNVHR8ERTBDMEGgP6A9hjtodHRwOi8v\n" + - "Y2VydGlmaWNhdGVzLnN0YXJmaWVsZHRlY2guY29tL3JlcG9zaXRvcnkvc2Zyb290\n" + - "LmNybDBRBgNVHSAESjBIMEYGBFUdIAAwPjA8BggrBgEFBQcCARYwaHR0cDovL2Nl\n" + - "cnRpZmljYXRlcy5zdGFyZmllbGR0ZWNoLmNvbS9yZXBvc2l0b3J5MA4GA1UdDwEB\n" + - "/wQEAwIBBjANBgkqhkiG9w0BAQUFAAOCAQEAhlK6sx+mXmuQpmQq/EWyrp8+s2Kv\n" + - "2x9nxL3KoS/HnA0hV9D4NiHOOiU+eHaz2d283vtshF8Mow0S6xE7cV+AHvEfbQ5f\n" + - "wezUpfdlux9MlQETsmqcC+sfnbHn7RkNvIV88xe9WWOupxoFzUfjLZZiUTIKCGhL\n" + - "Indf90XcYd70yysiKUQl0p8Ld3qhJnxK1w/C0Ty6DqeVmlsFChD5VV/Bl4t0zF4o\n" + - "aRN+0AqNnQ9gVHrEjBs1D3R6cLKCzx214orbKsayUWm/EheSYBeqPVsJ+IdlHaek\n" + - "KOUiAgOCRJo0Y577KM/ozS4OUiDtSss4fJ2ubnnXlSyokfOGASGRS7VApA==\n" + + "MIIGNDCCBBygAwIBAgIBGzANBgkqhkiG9w0BAQsFADB9MQswCQYDVQQGEwJJTDEW\n" + + "MBQGA1UEChMNU3RhcnRDb20gTHRkLjErMCkGA1UECxMiU2VjdXJlIERpZ2l0YWwg\n" + + "Q2VydGlmaWNhdGUgU2lnbmluZzEpMCcGA1UEAxMgU3RhcnRDb20gQ2VydGlmaWNh\n" + + "dGlvbiBBdXRob3JpdHkwHhcNMDcxMDI0MjA1NzA5WhcNMTcxMDI0MjA1NzA5WjCB\n" + + "jDELMAkGA1UEBhMCSUwxFjAUBgNVBAoTDVN0YXJ0Q29tIEx0ZC4xKzApBgNVBAsT\n" + + "IlNlY3VyZSBEaWdpdGFsIENlcnRpZmljYXRlIFNpZ25pbmcxODA2BgNVBAMTL1N0\n" + + "YXJ0Q29tIENsYXNzIDIgUHJpbWFyeSBJbnRlcm1lZGlhdGUgU2VydmVyIENBMIIB\n" + + "IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4k85L6GMmoWtCA4IPlfyiAEh\n" + + "G5SpbOK426oZGEY6UqH1D/RujOqWjJaHeRNAUS8i8gyLhw9l33F0NENVsTUJm9m8\n" + + "H/rrQtCXQHK3Q5Y9upadXVACHJuRjZzArNe7LxfXyz6CnXPrB0KSss1ks3RVG7RL\n" + + "hiEs93iHMuAW5Nq9TJXqpAp+tgoNLorPVavD5d1Bik7mb2VsskDPF125w2oLJxGE\n" + + "d2H2wnztwI14FBiZgZl1Y7foU9O6YekO+qIw80aiuckfbIBaQKwn7UhHM7BUxkYa\n" + + "8zVhwQIpkFR+ZE3EMFICgtffziFuGJHXuKuMJxe18KMBL47SLoc6PbQpZ4rEAwID\n" + + "AQABo4IBrTCCAakwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYD\n" + + "VR0OBBYEFBHbI0X9VMxqcW+EigPXvvcBLyaGMB8GA1UdIwQYMBaAFE4L7xqkQFul\n" + + "F2mHMMo0aEPQQa7yMGYGCCsGAQUFBwEBBFowWDAnBggrBgEFBQcwAYYbaHR0cDov\n" + + "L29jc3Auc3RhcnRzc2wuY29tL2NhMC0GCCsGAQUFBzAChiFodHRwOi8vd3d3LnN0\n" + + "YXJ0c3NsLmNvbS9zZnNjYS5jcnQwWwYDVR0fBFQwUjAnoCWgI4YhaHR0cDovL3d3\n" + + "dy5zdGFydHNzbC5jb20vc2ZzY2EuY3JsMCegJaAjhiFodHRwOi8vY3JsLnN0YXJ0\n" + + "c3NsLmNvbS9zZnNjYS5jcmwwgYAGA1UdIAR5MHcwdQYLKwYBBAGBtTcBAgEwZjAu\n" + + "BggrBgEFBQcCARYiaHR0cDovL3d3dy5zdGFydHNzbC5jb20vcG9saWN5LnBkZjA0\n" + + "BggrBgEFBQcCARYoaHR0cDovL3d3dy5zdGFydHNzbC5jb20vaW50ZXJtZWRpYXRl\n" + + "LnBkZjANBgkqhkiG9w0BAQsFAAOCAgEAbQjxXHkqUPtUY+u8NEFcuKMDITfjvGkl\n" + + "LgrTuBW63grW+2AuDAZRo/066eNHs6QV4i5e4ujwPSR2dgggY7mOIIBmiDm2QRjF\n" + + "5CROq6zDlIdqlsFZICkuONDNFpFjaPtZRTmuK1n6gywQgCNSIrbzjPcwR/jL/wow\n" + + "bfwC9yGme1EeZRqvWy/HzFWacs7UMmWlRk6DTmpfPOPMJo5AxyTZCiCYQQeksV7x\n" + + "UAeY0kWa+y/FV+eerOPUl6yy4jRHTk7tCySxrciZwYbd6YNLmeIQoUAdRC3CH3nT\n" + + "B2/JYxltcgyGHMiPU3TtafZgLs8fvncv+wIF1YAF/OGqg8qmzoJ3ghM4upGdTMIu\n" + + "8vADdmuLC/+dnbzknxX6QEGlWA8zojLUxVhGNfIFoizu/V/DyvSvYuxzzIkPECK5\n" + + "gDoMoBTTMI/wnxXwulNPtfgF7/5AtDhA4GNAfB2SddxiNQAF7XkUHtMZ9ff3W6Xk\n" + + "FldOG+NlLFqsDBG/KLckyFK36gq+FqNFCbmtmtXBGB5L1fDIeYzcMKG6hFQxhHS0\n" + + "oqpdHhp2nWBfLlOnTNqIZNJzOH37OJE6Olk45LNFJtSrqIAZyCCfM6bQgoQvZuIa\n" + + "xs9SIp+63ZMk9TxEaQj/KteaOyfaPXI9778U7JElMTz3Bls62mslV2I1C/A73Zyq\n" + + "JZWQZ8NU4ds=\n" + "-----END CERTIFICATE-----\n"; private static final String LINUX_COM_CERT = - "-----BEGIN CERTIFICATE-----\n" + - "MIIFfDCCBGSgAwIBAgIHJ7DOOMo+MDANBgkqhkiG9w0BAQUFADCB3DELMAkGA1UE\n" + - "BhMCVVMxEDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAj\n" + - "BgNVBAoTHFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xOTA3BgNVBAsTMGh0\n" + - "dHA6Ly9jZXJ0aWZpY2F0ZXMuc3RhcmZpZWxkdGVjaC5jb20vcmVwb3NpdG9yeTEx\n" + - "MC8GA1UEAxMoU3RhcmZpZWxkIFNlY3VyZSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0\n" + - "eTERMA8GA1UEBRMIMTA2ODg0MzUwHhcNMTExMDA1MDI1MTQyWhcNMTQxMDA1MDI1\n" + - "MTQyWjBPMRQwEgYDVQQKFAsqLmxpbnV4LmNvbTEhMB8GA1UECxMYRG9tYWluIENv\n" + - "bnRyb2wgVmFsaWRhdGVkMRQwEgYDVQQDFAsqLmxpbnV4LmNvbTCCASIwDQYJKoZI\n" + - "hvcNAQEBBQADggEPADCCAQoCggEBANoZR/TDp2/8LtA8k9Li55I665ssC7rHX+Wk\n" + - "oiGa6xBeCKTvNy9mgaUVzHwrOQlwJ2GbxFI+X0e3W2sWXUDTSxESZSEW2VZnjEn2\n" + - "600Qm8XMhZPvqztLRweHH8IuBNNYZHnW4Z2L4DS/Mi03EmjKZt2g3heGQbrv74m4\n" + - "v9/g6Jgr5ZOIwES6LUJchSWV2zcL8VYunpxnAtbi2hq1YfA9oYU82ngP40Ds7HEB\n" + - "9pUlzcWu9gcasWGzTvbVBZ4nA29pz5zWn1LHYfSYVSmXKU/ggfZb2nXd5/NkbWQX\n" + - "7B2SNH9/OVrHtFZldzD1+ddfCt1DQjXfGv7QqpAVsFTdKspPDLMCAwEAAaOCAc0w\n" + - "ggHJMA8GA1UdEwEB/wQFMAMBAQAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUF\n" + - "BwMCMA4GA1UdDwEB/wQEAwIFoDA5BgNVHR8EMjAwMC6gLKAqhihodHRwOi8vY3Js\n" + - "LnN0YXJmaWVsZHRlY2guY29tL3NmczEtMjAuY3JsMFkGA1UdIARSMFAwTgYLYIZI\n" + - "AYb9bgEHFwEwPzA9BggrBgEFBQcCARYxaHR0cDovL2NlcnRpZmljYXRlcy5zdGFy\n" + - "ZmllbGR0ZWNoLmNvbS9yZXBvc2l0b3J5LzCBjQYIKwYBBQUHAQEEgYAwfjAqBggr\n" + - "BgEFBQcwAYYeaHR0cDovL29jc3Auc3RhcmZpZWxkdGVjaC5jb20vMFAGCCsGAQUF\n" + - "BzAChkRodHRwOi8vY2VydGlmaWNhdGVzLnN0YXJmaWVsZHRlY2guY29tL3JlcG9z\n" + - "aXRvcnkvc2ZfaW50ZXJtZWRpYXRlLmNydDAfBgNVHSMEGDAWgBRJS1In0Ru88qEh\n" + - "amJ7UUJ6itfVVjAhBgNVHREEGjAYggsqLmxpbnV4LmNvbYIJbGludXguY29tMB0G\n" + - "A1UdDgQWBBQ44sIiZfPIl4PY51fh2TCZkqtToTANBgkqhkiG9w0BAQUFAAOCAQEA\n" + - "HFMuDtEZ+hIrIp4hnRJXUiTsc4Vaycxd5X/axDzUx+ooT3y2jBw0rcNnFhgD1T3u\n" + - "9zKiOLGXidvy2G/ppy/ymE+gcNqcEzfV1pKggNqStCwpEX1K8GBD46mX5qJ1RxI+\n" + - "QoHo/FZe7Vt+dQjHHdGWh27iVWadpBo/FJnHOsTaHewKL8+Aho0M84nxnUolYxzC\n" + - "9H3ViEz+mfMISLzvWicxVU71aJ4yI9JmaL1ddRppBovZHOeWshizcMVtFwcza1S0\n" + - "ZfajonXj48ZkXMXGWuomWxE2dGro6ZW6DdyIjTpZHCJuIvGC10J3mHIR5XaTj6mv\n" + - "zkVBz5DhpshQe97x6OGLOA==\n" + - "-----END CERTIFICATE-----\n"; + "-----BEGIN CERTIFICATE-----\n" + + "MIIGhjCCBW6gAwIBAgIDAmiWMA0GCSqGSIb3DQEBCwUAMIGMMQswCQYDVQQGEwJJ\n" + + "TDEWMBQGA1UEChMNU3RhcnRDb20gTHRkLjErMCkGA1UECxMiU2VjdXJlIERpZ2l0\n" + + "YWwgQ2VydGlmaWNhdGUgU2lnbmluZzE4MDYGA1UEAxMvU3RhcnRDb20gQ2xhc3Mg\n" + + "MiBQcmltYXJ5IEludGVybWVkaWF0ZSBTZXJ2ZXIgQ0EwHhcNMTQwODIxMjEwMDI4\n" + + "WhcNMTYwODIxMDY0NDE0WjCBlDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlm\n" + + "b3JuaWExFjAUBgNVBAcTDVNhbiBGcmFuY2lzY28xHTAbBgNVBAoTFFRoZSBMaW51\n" + + "eCBGb3VuZGF0aW9uMRQwEgYDVQQDFAsqLmxpbnV4LmNvbTEjMCEGCSqGSIb3DQEJ\n" + + "ARYUaG9zdG1hc3RlckBsaW51eC5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw\n" + + "ggEKAoIBAQCjvFjOigXyqkSiVv0vz1CSDg08iilLnj8gRFRoRMA6fFWhQTp4QGLV\n" + + "1li5VMEQdZ/vyqTWjmB+FFkuTsBjFDg6gG3yw6DQBGyyM06A1dT9YKUa7LqxOxQr\n" + + "KhNOacPS/pAupAZ5jOO7fcZwIcpKcKEjjhHn7GXEVvb+K996TMA0vDYcp1lgXtil\n" + + "7Ij+1GUSA29NrnCZXUun2c5nS7OulRYcgtRyZBa13zfyaVJtEIl14ClP9gsLa/5u\n" + + "eXzZD71Jj48ZNbiKRThiUZ5FkEnljjSQa25Hr5g9DXY2JvI1r8OJOCUR8jPiRyNs\n" + + "Kgc1ZG0fibm9VoHjokUZ2aQxyQJP/C1TAgMBAAGjggLlMIIC4TAJBgNVHRMEAjAA\n" + + "MAsGA1UdDwQEAwIDqDAdBgNVHSUEFjAUBggrBgEFBQcDAgYIKwYBBQUHAwEwHQYD\n" + + "VR0OBBYEFI0nMnIXZOz02MlXPh2g9aHesvPPMB8GA1UdIwQYMBaAFBHbI0X9VMxq\n" + + "cW+EigPXvvcBLyaGMCEGA1UdEQQaMBiCCyoubGludXguY29tgglsaW51eC5jb20w\n" + + "ggFWBgNVHSAEggFNMIIBSTAIBgZngQwBAgIwggE7BgsrBgEEAYG1NwECAzCCASow\n" + + "LgYIKwYBBQUHAgEWImh0dHA6Ly93d3cuc3RhcnRzc2wuY29tL3BvbGljeS5wZGYw\n" + + "gfcGCCsGAQUFBwICMIHqMCcWIFN0YXJ0Q29tIENlcnRpZmljYXRpb24gQXV0aG9y\n" + + "aXR5MAMCAQEagb5UaGlzIGNlcnRpZmljYXRlIHdhcyBpc3N1ZWQgYWNjb3JkaW5n\n" + + "IHRvIHRoZSBDbGFzcyAyIFZhbGlkYXRpb24gcmVxdWlyZW1lbnRzIG9mIHRoZSBT\n" + + "dGFydENvbSBDQSBwb2xpY3ksIHJlbGlhbmNlIG9ubHkgZm9yIHRoZSBpbnRlbmRl\n" + + "ZCBwdXJwb3NlIGluIGNvbXBsaWFuY2Ugb2YgdGhlIHJlbHlpbmcgcGFydHkgb2Js\n" + + "aWdhdGlvbnMuMDUGA1UdHwQuMCwwKqAooCaGJGh0dHA6Ly9jcmwuc3RhcnRzc2wu\n" + + "Y29tL2NydDItY3JsLmNybDCBjgYIKwYBBQUHAQEEgYEwfzA5BggrBgEFBQcwAYYt\n" + + "aHR0cDovL29jc3Auc3RhcnRzc2wuY29tL3N1Yi9jbGFzczIvc2VydmVyL2NhMEIG\n" + + "CCsGAQUFBzAChjZodHRwOi8vYWlhLnN0YXJ0c3NsLmNvbS9jZXJ0cy9zdWIuY2xh\n" + + "c3MyLnNlcnZlci5jYS5jcnQwIwYDVR0SBBwwGoYYaHR0cDovL3d3dy5zdGFydHNz\n" + + "bC5jb20vMA0GCSqGSIb3DQEBCwUAA4IBAQBVkvlwVLfnTNZh1c8j+PQ1t2n6x1dh\n" + + "tQtZiAYWKvZwi+XqLwU8q2zMxKrTDuqyEVyfCtWCiC1Vkpz72pcyXz2dKu7F7ZVL\n" + + "86uVHcc1jAGmL59UCXz8LFbfAMcoVQW1f2WtNwsa/WGnPUes3jFSec+shB+XCpvE\n" + + "WU6mfcZD5TyvbC79Kn5e3Iq+B4DaXhU/BXASRbORgYd8C+dqj++w0PAcMrmjn3D6\n" + + "EmL1ofqpQ8wCJd5C1b5Fr4RbbYpK8v8AASRcp2Qj9WJjyV882FvXOOFj5V2Jjcnh\n" + + "G0h67ElS/klu9rPaZ+vr3iIB56wvk08O2Wq1IND3sN+Ke3UsvuPqDxAv\n" + + "-----END CERTIFICATE-----\n"; private File mKeyStoreFile; private LocalKeyStore mKeyStore; @@ -167,7 +179,7 @@ public class TrustManagerFactoryTest extends AndroidTestCase { private X509Certificate mCert2; private X509Certificate mCaCert; private X509Certificate mCert3; - private X509Certificate mStarfieldCert; + private X509Certificate mLinuxComFirstParentCert; private X509Certificate mLinuxComCert; @@ -176,7 +188,7 @@ public class TrustManagerFactoryTest extends AndroidTestCase { mCert2 = loadCert(K9_EXAMPLE_COM_CERT2); mCaCert = loadCert(CA_CERT); mCert3 = loadCert(CERT3); - mStarfieldCert = loadCert(STARFIELD_CERT); + mLinuxComFirstParentCert = loadCert(LINUX_COM_FIRST_PARENT_CERT); mLinuxComCert = loadCert(LINUX_COM_CERT); } @@ -267,20 +279,20 @@ public class TrustManagerFactoryTest extends AndroidTestCase { public void testGloballyTrustedCertificateChain() throws Exception { X509TrustManager trustManager = TrustManagerFactory.get("www.linux.com", PORT1); - X509Certificate[] certificates = new X509Certificate[] { mLinuxComCert, mStarfieldCert }; + X509Certificate[] certificates = new X509Certificate[] { mLinuxComCert, mLinuxComFirstParentCert}; trustManager.checkServerTrusted(certificates, "authType"); } public void testGloballyTrustedCertificateNotMatchingHost() throws Exception { X509TrustManager trustManager = TrustManagerFactory.get(NOT_MATCHING_HOST, PORT1); - assertCertificateRejection(trustManager, new X509Certificate[] { mLinuxComCert, mStarfieldCert }); + assertCertificateRejection(trustManager, new X509Certificate[] { mLinuxComCert, mLinuxComFirstParentCert}); } public void testGloballyTrustedCertificateNotMatchingHostOverride() throws Exception { mKeyStore.addCertificate(MATCHING_HOST, PORT1, mLinuxComCert); X509TrustManager trustManager = TrustManagerFactory.get(MATCHING_HOST, PORT1); - X509Certificate[] certificates = new X509Certificate[] { mLinuxComCert, mStarfieldCert }; + X509Certificate[] certificates = new X509Certificate[] { mLinuxComCert, mLinuxComFirstParentCert}; trustManager.checkServerTrusted(certificates, "authType"); }